Invalid access to property or key 'room_name' on a base object of type 'Nil'

Godot Version

4.6.2

Question

Hi, I’m working on a text-based adventure game and I’m running into an issue when I try to save and then load the current room the player is in. Currently I have a main panel where most of the information is (on the left of the following image) and a side panel that always has the information of the current room the player is in and updates automatically when the room changes or when something changes in their current room (on the right of the image). Specifically this side_panel is where the problems start happening when saving.

After saving and loading, if I move to another room I get the following error in the side_panel.gd script (Invalid access to property or key ‘room_name’ on a base object of type ‘Nil’.). I noticed when loading the game as well it never updates the side_panel to the correct information.

Any help on the matter would be greatly appreciated. Thank you!

The following is my side_panel.gd script. The error is happening on line 12 of the script. Specifically the line that says room_name.text = new_room.room_name.

extends PanelContainer

class_name side_panel

@onready var room_name = $MarginContainer/Rows/Title/RoomName
@onready var room_description = $MarginContainer/Rows/RoomDescription
@onready var exit = $MarginContainer/Rows/List/Exits
@onready var npcs = $MarginContainer/Rows/List/NPCs
@onready var items = $MarginContainer/Rows/List/Items

func handle_room_changed(new_room):
	room_name.text = new_room.room_name #This is the error line.
	room_description.text = new_room.room_description
	
	exit.text = new_room.exit_desc()
	
	var npc_s = new_room.npc_desc()
	if npc_s == "":
		npcs.hide()
	else:
		npcs.show()
		npcs.bbcode_text = new_room.npc_desc()
		
	var items_s = new_room.item_desc()
	if items_s == "":
		items.hide()
	else:
		items.show()
		items.bbcode_text = new_room.item_desc()
		
func handle_room_updated(cur_room):
	handle_room_changed(cur_room)
	
	

This is my script for saving/loading.

extends Node

## Called when the node enters the scene tree for the first time.
func _ready() -> void:
	_load()

const FILE_PATH: String = "user://SaveFile.json"

var save_data: Dictionary = {
	"player_inventory": [],
	"current_room_name": "",
	"current_room_desc": "",
	"current_room_exits": {},
	"current_room_npc_array": [],
	"current_room_items": [],
	
	"side_panel_room_name": "",
	"side_panel_room_desc": "",
	"side_panel_room_exits": {},
	"side_panel_room_npc_array": [],
	"side_panel_room_items": []
}

#"current_room": Room.new()
func _save() -> void:
	var file: FileAccess = FileAccess.open(FILE_PATH, FileAccess.WRITE)
	file.store_var(save_data, true)
	file.close()

func _load() -> void:
	if FileAccess.file_exists(FILE_PATH):
		var file: FileAccess = FileAccess. open(FILE_PATH, FileAccess.READ)
		var data: Dictionary = file.get_var(true)
		for i in data:
			if save_data.has(i):
				save_data[i] = data[i]
		file.close()

And this is a portion of my command processor. The match statement is what commands the user has. The save and load commands is at the end of the match statement.

I included all of the functions that emit the signals.

extends Node

signal change_room(new_room)
signal update_room(cur_room)

var cur_room = null
var vertus = null
var side_panel_instance = side_panel.new()

func initialize(start_room, vertus) -> String:
	self.vertus = vertus
	return changing_rooms(start_room)

func process_a_command(input: String) -> String:
	# Splits the string the user inputs at each space.
	var player_words = input.split(" ", false)
	if player_words.size() == 0:
		push_error("Player has no words to parse.") #"Error: No words have been parsed."
	var word1 = player_words[0].to_lower()
	var word2 = ""
	var word3 = ""
	var word4 = ""
	if player_words.size() > 1:
		word2 = player_words[1].to_lower()
	if player_words.size() > 2:
		word3 = player_words[2].to_lower()
	if player_words.size() > 3:
		word4 = player_words[3].to_lower()

	match word1:
		"move":
			var array = move(word2, word3, word4)
			var s = ""
			for i in array:
				s = s + str(i)
			return s
		"help":
			return player_help()

		"grab":
			return grab(word2, word3)

		"inventory":
			return show_inventory()

		"drop":
			return drop_item(word2, word3)

		"use":
			return use_item(word2, word3)

		"speak":
			return speak(word2)

		"give":
			return give(word2, word3)

		"save":
			# Handles saving all the information back into the player_inventory save_data.
			SaveSystem.save_data.player_inventory = vertus.vertus_inventory
			
			# Handles saving all the information back into the current room save_data.
			SaveSystem.save_data.current_room_name = cur_room.room_name
			SaveSystem.save_data.current_room_desc = cur_room.room_description
			SaveSystem.save_data.current_room_exits = cur_room.room_exits
			SaveSystem.save_data.current_room_npc_array = cur_room.npc_array
			SaveSystem.save_data.current_room_items = cur_room.items
			
			# Handles saving all the information back into the side_panel save_data.
			SaveSystem.save_data.side_panel_room_name = side_panel_instance.room_name
			SaveSystem.save_data.side_panel_room_desc = side_panel_instance.room_description
			SaveSystem.save_data.side_panel_room_exits = side_panel_instance.exit
			SaveSystem.save_data.side_panel_room_npc_array = side_panel_instance.npcs
			SaveSystem.save_data.side_panel_room_items = side_panel_instance.items
			
			SaveSystem._save()
			return IType.color("Game Saved", "SYSTEM")

		"load":
			SaveSystem._load()
			
			# Handles loading all the information back into the player_inventory.
			vertus.vertus_inventory = SaveSystem.save_data.player_inventory
			
			# Handles loading all the information back into the current room.
			cur_room.room_name = SaveSystem.save_data.current_room_name
			cur_room.room_description = SaveSystem.save_data.current_room_desc
			cur_room.room_exits = SaveSystem.save_data.current_room_exits
			cur_room.npc_array = SaveSystem.save_data.current_room_npc_array
			cur_room.items = SaveSystem.save_data.current_room_items
			
			# Handles loading all the information back into the side_panel.
			side_panel_instance.room_name = SaveSystem.save_data.side_panel_room_name
			side_panel_instance.room_description = SaveSystem.save_data.side_panel_room_desc
			side_panel_instance.exit = SaveSystem.save_data.side_panel_room_exits
			side_panel_instance.npcs = SaveSystem.save_data.side_panel_room_npc_array
			side_panel_instance.items = SaveSystem.save_data.side_panel_room_items
			
			return IType.color("Game Loaded", "SYSTEM")
		_:
			return IType.color("Think carefully...what does Vertus do?", "SYSTEM")

func grab(word2: String, word3: String) -> String:
	if word2 == "":
		print( "in the if")
		return IType.color("There is nothing for Vertus to take.", "SYSTEM")
		
	elif word2 != "" && word3 == "":
		for items in cur_room.items:
			if word2.to_lower() == items.item_name.to_lower():
				cur_room.remove_items(items)
				vertus.add_to_inventory(items)
				emit_signal("update_room", cur_room)
				return "Vertus discovers a " + IType.color(items.item_name, "ITEM") + "."
				
	elif word2 != "" && word3 != "":
		for items in cur_room.items:
			var str = word2 + " " + word3
			print(str)
			if str == items.item_name.to_lower():
				cur_room.remove_items(items)
				vertus.add_to_inventory(items)
				emit_signal("update_room", cur_room)
				return "Vertus discovers a " + IType.color(items.item_name, "ITEM") + "."
	return IType.color("Vertus searched in vain.", "SYSTEM")

func drop_item(word2: String, word3: String = "") -> String:
	if word2 == "":
		return IType.color("There is nothing for Vertus to drop.", "SYSTEM")
		
	elif word2 != "" && word3 == "":
		for items in vertus.vertus_inventory:
			if word2.to_lower() == items.item_name.to_lower():
				vertus.remove_from_inventory(items)
				cur_room.add_items(items)
				emit_signal("update_room", cur_room)
				return "Vertus drops the " + IType.color(items.item_name, "ITEM") + "."
				
	elif word2 != "" && word3 != "":
		var str = word2 + " " + word3
		print(str)
		for items in vertus.vertus_inventory:
			if str == items.item_name.to_lower():
				vertus.remove_from_inventory(items)
				cur_room.add_items(items)
				emit_signal("update_room", cur_room)
				return "Vertus drops the " + IType.color(items.item_name, "ITEM") + "."
			
	return IType.color("This item is not in Vertus's posession.", "SYSTEM")

func changing_rooms(new_room: Room) -> String:
	cur_room = new_room
	emit_signal("change_room", new_room)
	return new_room.description()

This is the game script where (handle_room_changed) and (handle_room_updated) from the side_panel.gd are used.

extends Control

@onready var command_p = $CommandParser
@onready var room_manager = $RoomManager
@onready var vertus = $Vertus
@onready var info = $GameBackground/MarginContainer/GameColumns/GameRows/GameInfo
@onready var input = $GameBackground/MarginContainer/GameColumns/GameRows/GameInput/HBoxContainer/Input

func _ready() -> void:
	var side_panel = $GameBackground/MarginContainer/GameColumns/SidePanel
	
	command_p.connect("change_room", Callable(side_panel, "handle_room_changed"))
	command_p.connect("update_room", Callable(side_panel, "handle_room_updated"))
	
	# The start up text
	info.handle_response(IType.color("Welcome to Vertus's journey. Type help if ya want to see your commands.", "SYSTEM"))
	var initial_room_response = command_p.initialize(room_manager.get_child(0), vertus)
	info.handle_response(initial_room_response)

# Handles what happens when text is submitted.
func _on_input_text_submitted(new_text: String) -> void:
	# If text is empty do nothing.
	if new_text.is_empty():
		return
	
	# Otherwise process the response in the command parser.
	# new_text is what the user enetered.
	var response = command_p.process_a_command(new_text)
	info.handle_response_w_input(response, new_text)

Look at the call stack to determine where the call to handle_room_changed() came from and why did it pass null as an argument. In the code you posted nothing calls handle_room_updated() nor handle_room_changed()

Ah my bad I meant to upload that too. I’ll update my post to include that.

This is the game script where they are both called.

extends Control

@onready var command_p = $CommandParser
@onready var room_manager = $RoomManager
@onready var vertus = $Vertus
@onready var info = $GameBackground/MarginContainer/GameColumns/GameRows/GameInfo
@onready var input = $GameBackground/MarginContainer/GameColumns/GameRows/GameInput/HBoxContainer/Input

func _ready() -> void:
	var side_panel = $GameBackground/MarginContainer/GameColumns/SidePanel
	
	command_p.connect("change_room", Callable(side_panel, "handle_room_changed"))
	command_p.connect("update_room", Callable(side_panel, "handle_room_updated"))
	
	# The start up text
	info.handle_response(IType.color("Welcome to Vertus's journey. Type help if ya want to see your commands.", "SYSTEM"))
	var initial_room_response = command_p.initialize(room_manager.get_child(0), vertus)
	info.handle_response(initial_room_response)

# Handles what happens when text is submitted.
func _on_input_text_submitted(new_text: String) -> void:
	# If text is empty do nothing.
	if new_text.is_empty():
		return
	
	# Otherwise process the response in the command parser.
	# new_text is what the user enetered.
	var response = command_p.process_a_command(new_text)
	info.handle_response_w_input(response, new_text)

At which lines are they called?

Don’t you need to bind the curr_room to this callable.

I’m using them on line 12 and 13

Those are signal connections, not function calls. Who and when emits those signals? There’s no emit() or emit_signal() in any of the code you posted.

If a signal is declared to require an argument, you need to supply this argument when calling emit() on the signal. Otherwise that argument will be null, which is exactly what appears to be causing your error.

I’m sorry do you mean having cur room in that callable instead of the side_panel?

Like this

command_p.connect("update_room", Callable(cur_room, "handle_room_updated"))

In my command processor I have a signal called update room that is emitted whenever we’re not changing what the current room is but something inside of the current room itself changes. The intention was that new information to show up on the side panel

My bad I should have been more clear on where everything was being emitted lol.

Both signals are being emmitted in the other function in the command processor. For example,

func drop_item(word2: String, word3: String = "") -> String:
	if word2 == "":
		return IType.color("There is nothing for Vertus to drop.", "SYSTEM")
		
	elif word2 != "" && word3 == "":
		for items in vertus.vertus_inventory:
			if word2.to_lower() == items.item_name.to_lower():
				vertus.remove_from_inventory(items)
				cur_room.add_items(items)
				emit_signal("update_room", cur_room)
				return "Vertus drops the " + IType.color(items.item_name, "ITEM") + "."
				
	elif word2 != "" && word3 != "":
		var str = word2 + " " + word3
		print(str)
		for items in vertus.vertus_inventory:
			if str == items.item_name.to_lower():
				vertus.remove_from_inventory(items)
				cur_room.add_items(items)
				emit_signal("update_room", cur_room)
				return "Vertus drops the " + IType.color(items.item_name, "ITEM") + "."
			
	return IType.color("This item is not in Vertus's posession.", "SYSTEM")

And

func changing_rooms(new_room: Room) -> String:
	cur_room = new_room
	emit_signal("change_room", new_room)
	return new_room.description()

I’ll update the post again to include more of the file.

My guess is part of the problem has something to do when I’m emitting the signal for the changing_rooms func. As it only crashes when the player changes what room their in.

Do appreciate you taking the time to look at this.

Print the value of the room argument before emitting the signal.

Before the load occurs the value is this. It’s correctly getting the room I had set up

image

After the load I get this. It ends up being null which I’m not sure why.

image

I printed the value of the current room after loading and it seems to hold the same room information as before the load occurs. Made sure to check if all of the room exits were the same as well. Everything matches so I’m looking into why it suddenly turns null whenever we need to change rooms.

If you’re (de)serializing a node using store_var() and get_var(), you need to be aware that there are some gotchas there - most notably the node won’t be automatically added to the scene tree.

Ah ok I wasn’t aware of that gotcha. How would you recommend adding the node to the scene tree. Or is there any alternatives you’d recommend besides the store_var()and get_var()?

It really depends on what exactly are you’re trying to store there. If node references or parent/child relations are involved, serialization will be a bit more complicated.

But first determine exactly why and where that reference becomes null. Print its value every step of the way from loading it to receiving it in that signal handler.

Thank ya I’ll do that!

To follow @normalized’s advice, just press Ctrl + Shift + F and search for handle_room_changed. One of those lines that shows up is passing in a null value. This error most often occurs as part of a race condition. I suspect that you are calling handle_room_changed() in a Node before the ready signal is complete. Typically this means you’re calling it in the _ready(), _process() or _physics_process() fucntion. (It’s important to understand that _process() and _physics_process() can start running before _ready() is complete.)

I would log the status of cur_room before each emit_signal. That may give you a hint on the cause.