Max recursion reached with ResourceSaver.save

Godot Version

3.5.3

Question

Hi! Could you help me?
I am working on a simulation game, it has no 3d or 2d only UI. The game data is structured in a way that a single Node (Colony) holds all of the data that needs to be saved, as it’s children and grandchildren (all simple Nodes) and so on, also as variables. So to save the game, I .pack(Colony) then ResourceSaver.save(). It works rather well except in cases when the scene gets even deeper. Colony has People node, that has Alive node, that has person nodes one by one. Person has genome, genome has genes. But person also has Womb and Womb may have another Person as child. And that is when it stops working, I get error:

write: Max recursion reached
<C++ Source> core/variant_parser.cpp:1653 @ write()

I guess the hierarchy is too deep for the resource saver but I don’t want to flatten my structure as it has it’s purpose. I see no way to increase the max recursion. I can only imagine a custom save method that also uses recursion so I’m afraid I would get the same problem that way. Also it is my first project so I am not familiar with how it is usually done.
Do you have any advice?

There’s no one way to do things.

Start dissecting the problem and perhaps splitting your data into different resources which can be saved individually.

There is also the option to include the SQLite database and then you can do just about anything. There is a plugin for that, last I checked.

EDIT: I saw too late that you are using version 3.5.3, this code is for version 4 but hopefully you can adapt it.

I found the built in serialisation options to be lacking so I built my own, loosely based on how I have solved it before in C#, it’s a bit more work but you have full control and it works for my needs so it might work for you.

How it looks on a per-class basis, note that when writing and loading you should always write/read the version first and the order you read/write your variables in is critical, the serialize and deserialize functions must always match or you will end up reading part of a string and storing it as an int or something:

class_name LevelData extends Serializable

var grid:Array[GridData] = []

func serialize(writer:SerializationWriter, version:int) -> SerializationWriter:
	_settings.serialize(writer, version)

	writer.write_ushort(grid.size())
	for i:int in range(grid.size()):
		grid[i].serialize(writer, version)

	# When creating new save versions, earlier versions will exit out early while new versions store more data
	if (version < 2):
		return writer

	return writer

func deserialize(reader:SerializationReader) -> LevelData:
	_settings = LevelSettings.new().deserialize(reader)

	var grid_size:int = reader.read_ushort()
	grid.resize(grid_size)
	for i:int in grid_size:
		grid[i] = GridData.new().deserialize(reader)

	if (version < 2):
		return self

	return self

And here are the main classes, shortened to not include all the types as those are easy to add as you need them, but do note that I duplicate a lot, read_bool & read_byte for example is the same thing, but I think there is value in keeping it verbose

Reader:

class_name SerializationReader

var _buffer:PackedByteArray
var _position:int = 0

func _init(buffer:PackedByteArray, start_position:int = 0) -> void:
	_buffer = buffer
	_position = start_position

func read_bool() -> bool:
	var value:bool = _buffer.decode_u8(_position)
	_position += Serializable.BYTE_SIZE
	return value

func read_byte() -> int:
	var value:int = _buffer.decode_u8(_position)
	_position += Serializable.BYTE_SIZE
	return value

func read_sbyte() -> int:
	var value:int = _buffer.decode_s8(_position)
	_position += Serializable.BYTE_SIZE
	return value

func read_vector3i() -> Vector3i:
	var value:Vector3i = Vector3i(_buffer.decode_s32(_position), _buffer.decode_s32(_position + 1), _buffer.decode_s32(_position + 2))
	_position += Serializable.VECTOR3I_SIZE
	return value

func read_string() -> String:
	var length:int = read_int()
	var string_data:PackedByteArray = PackedByteArray(_buffer.slice(_position, _position + length))
	_position += string_data.size()
	return string_data.get_string_from_utf8()

Writer:

class_name SerializationWriter

var _buffer:PackedByteArray

func _init(buffer:PackedByteArray) -> void:
	_buffer = buffer

func write_bool(value:bool) -> void:
	var buffer_size:int = _buffer.size()
	_buffer.resize(buffer_size + Serializable.BYTE_SIZE)
	_buffer.encode_u8(buffer_size, value)

func write_byte(value:int) -> void:
	var buffer_size:int = _buffer.size()
	_buffer.resize(buffer_size + Serializable.BYTE_SIZE)
	_buffer.encode_u8(buffer_size, value)

func write_sbyte(value:int) -> void:
	var buffer_size:int = _buffer.size()
	_buffer.resize(buffer_size + Serializable.BYTE_SIZE)
	_buffer.encode_s8(buffer_size, value)

func write_vector3i(value:Vector3i) -> void:
	var buffer_size:int = _buffer.size()
	_buffer.resize(buffer_size + Serializable.VECTOR3I_SIZE)
	_buffer.encode_float(buffer_size, value.x)
	_buffer.encode_float(buffer_size + Serializable.INT32_SIZE, value.y)
	_buffer.encode_float(buffer_size + Serializable.INT32_SIZE * 2, value.z)

func write_string(value:String) -> void:
	var data:PackedByteArray = value.to_utf8_buffer()
	write_int(data.size())

	_buffer.append_array(data)

func get_buffer() -> PackedByteArray:
	return _buffer

Serializable class:

class_name Serializable

const NULL_SIZE:int = 1
const BYTE_SIZE:int = 1
const INT16_SIZE:int = 2
const INT32_SIZE:int = 4
const INT64_SIZE:int = 8
const FLOAT_SIZE:int = 4
const DOUBLE_SIZE:int = 8
const VECTOR2_SIZE:int = FLOAT_SIZE * 2
const VECTOR3_SIZE:int = FLOAT_SIZE * 3
const VECTOR4_SIZE:int = FLOAT_SIZE * 4
const VECTOR2I_SIZE:int = INT32_SIZE * 2
const VECTOR3I_SIZE:int = INT32_SIZE * 3
const VECTOR4I_SIZE:int = INT32_SIZE * 4
const STRING_SIZE:int = INT32_SIZE # This int holds the length of the utf8 encoded string (UTF8 characters are 1 to 4 bytes each)

func serialize(writer:SerializationWriter, _version:int) -> SerializationWriter:
	return writer

func deserialize(_reader:SerializationReader) -> Variant:
	return

func to_byte_array() -> PackedByteArray:
	return serialize(SerializationWriter.new(PackedByteArray()), Version.game_version).get_buffer()

func from_byte_array(data:PackedByteArray) -> Variant:
	deserialize(SerializationReader.new(data))
	return self

I have support for saving byte, sbyte, short, ushort etc even though GDScript just treats everything the same like some ignorant child, so make sure you know what you are doing and not storing a bigger number than a type can hold, but also, if you have a value that will never go outside of 0-255 you should store it as a byte if file size is a concern.

Disclaimer: I’m new to GDScript and it seems like every day I learn something new that makes me dislike it more and more, so this might actually turn out to be a terrible idea because of how GDScript works, but it does function for now and I should be able to update the system without actually redoing anything game related.