Files
Factory47/addons/savekit/binary_deserializer.gd
2026-06-04 16:53:41 -05:00

189 lines
8.1 KiB
GDScript

extends "deserializer.gd"
## Deserializes save data from binary format.
const BinarySerializer := preload("binary_serializer.gd")
var _node_deserialization_stack: Array[NodePath] = []
var _saved_nodes: Dictionary[NodePath, Dictionary]
var _saved_resources_by_id: Dictionary[int, Dictionary]
var _loaded_resources_by_id: Dictionary[int, SaveKitResource]
func prepare_load_from_memory(data: PackedByteArray) -> bool:
if data.size() < BinarySerializer._FILE_HEADER_SIZE:
push_error("Save data is too small to contain required header information")
return false
var version := data.decode_u32(BinarySerializer._SERIALIZATION_VERSION_U32_OFFSET)
if version != BinarySerializer._SERIALIZATION_VERSION:
push_error("Unsupported save data version: ", version)
return false
var saved_nodes_length := data.decode_var_size(BinarySerializer._FILE_HEADER_SIZE)
_saved_nodes.assign(data.decode_var(BinarySerializer._FILE_HEADER_SIZE) as Dictionary)
_saved_resources_by_id.assign(data.decode_var(BinarySerializer._FILE_HEADER_SIZE + saved_nodes_length) as Dictionary)
_node_deserialization_stack.assign(_saved_nodes.keys())
sort_node_paths_in_load_order(_node_deserialization_stack)
return true
func decode_var(value: Variant, expected_type: Variant.Type, expected_class_name: StringName = &"") -> Variant:
match expected_type:
TYPE_CALLABLE:
push_error("Cannot deserialize callable value ", value)
return null
TYPE_OBJECT:
var buffer := value as PackedByteArray
if not buffer:
push_warning("Expected a PackedByteArray when deserializing an object, got: ", value)
return null
var type_tag := buffer.get(0)
match type_tag:
BinarySerializer._ENCODED_RESOURCE_REFERENCE_TAG:
return _decode_resource_reference(buffer, expected_class_name)
BinarySerializer._ENCODED_NODE_REFERENCE_TAG:
var path_length := buffer.decode_u32(BinarySerializer._ENCODED_NODE_REFERENCE_PATH_LENGTH_U32_OFFSET)
if path_length <= 0:
push_warning("Invalid path length ", path_length, " found when deserializing a node reference")
return null
var path_buffer := buffer.slice(BinarySerializer._ENCODED_NODE_REFERENCE_DATA_OFFSET, BinarySerializer._ENCODED_NODE_REFERENCE_DATA_OFFSET + path_length)
var node_path := NodePath(path_buffer.get_string_from_utf8())
return _decode_node_reference(node_path)
BinarySerializer._SAVED_RESOURCE_REFERENCE_TAG:
return _load_resource(buffer)
_:
push_warning("Unknown type tag ", type_tag, " found when deserializing an object")
return null
TYPE_ARRAY:
var array := value as Array
if not array:
push_warning("Expected an array when deserializing an array, got: ", value)
return null
return array.map(_decode_var_with_type_info)
TYPE_DICTIONARY:
var dictionary := value as Dictionary
if not dictionary:
push_warning("Expected a dictionary when deserializing a dictionary, got: ", value)
return null
var decoded_dictionary: Dictionary
for key: Variant in dictionary:
var decoded_key: Variant = _decode_var_with_type_info(key)
var decoded_value: Variant = _decode_var_with_type_info(dictionary[key])
decoded_dictionary[decoded_key] = decoded_value
return decoded_dictionary
_:
return value
func _decode_var_with_type_info(value: Variant) -> Variant:
var buffer := value as PackedByteArray
if not buffer:
push_warning("Expected a PackedByteArray when decoding a typed value, got: ", value)
return null
var type := buffer.decode_u8(BinarySerializer._ENCODED_TYPED_VALUE_TYPE_U8_OFFSET) as Variant.Type
var classname_length := buffer.decode_u16(BinarySerializer._ENCODED_TYPED_VALUE_CLASS_NAME_LENGTH_U16_OFFSET)
var classname: StringName = ""
if classname_length:
var classname_buffer := buffer.slice(BinarySerializer._ENCODED_TYPED_VALUE_DATA_OFFSET, BinarySerializer._ENCODED_TYPED_VALUE_DATA_OFFSET + classname_length)
classname = StringName(classname_buffer.get_string_from_utf8())
var encoded_value := buffer.slice(BinarySerializer._ENCODED_TYPED_VALUE_DATA_OFFSET + classname_length)
return decode_var(bytes_to_var(encoded_value), type, classname)
func _decode_node_reference(node_path: NodePath) -> Node:
# To ensure we can convert this node path into a valid node reference, we need to effectively "preload" the target node and all of its ancestors.
# This process is similar to load_node(), but circumventing the normal order and without actually loading data into the nodes yet.
if node_path.get_name_count() > 1:
var parent_node := _decode_node_reference(node_path.slice(0, -1))
if not parent_node:
return null
var save_dict: Dictionary = _saved_nodes.get(node_path, {})
var scene_file_path: String = save_dict.get(BinarySerializer._NODE_SCENE_FILE_PATH_KEY, "")
return find_or_instantiate_node(node_path, scene_file_path, false)
## Decodes a reference to a resource, loading it by UID or path as appropriate.
##
## Note: [param expected_class_name] should refer to a class that exists within [ClassDB] (i.e., built-in or GDExtension classes). It should [i]not[/i] contain the name of a script-defined [code]class_name[/code].
func _decode_resource_reference(buffer: PackedByteArray, expected_class_name: StringName) -> Resource:
var path_length := buffer.decode_u32(BinarySerializer._ENCODED_RESOURCE_REFERENCE_PATH_LENGTH_U32_OFFSET)
var uid_length := buffer.decode_u32(BinarySerializer._ENCODED_RESOURCE_REFERENCE_UID_LENGTH_U32_OFFSET)
var path_buffer := buffer.slice(BinarySerializer._ENCODED_RESOURCE_REFERENCE_DATA_OFFSET, BinarySerializer._ENCODED_RESOURCE_REFERENCE_DATA_OFFSET + path_length)
var uid_buffer: PackedByteArray
if uid_length:
uid_buffer = buffer.slice(BinarySerializer._ENCODED_RESOURCE_REFERENCE_DATA_OFFSET + path_length, BinarySerializer._ENCODED_RESOURCE_REFERENCE_DATA_OFFSET + path_length + uid_length)
var resource_path := path_buffer.get_string_from_utf8()
if uid_buffer:
var id := ResourceUID.text_to_id(uid_buffer.get_string_from_utf8())
if ResourceUID.has_id(id):
resource_path = ResourceUID.get_id_path(id)
var allowed_extensions := ResourceLoader.get_recognized_extensions_for_type(expected_class_name if expected_class_name else &"Resource")
return ResourceUtils.safe_load_resource(resource_path, allowed_extensions)
## Returns how many nodes remain to be loaded from the save data. This can be used to determine loading progress.
func get_remaining_node_count() -> int:
return _node_deserialization_stack.size()
func is_finished() -> bool:
return not _node_deserialization_stack
func load_node() -> Node:
var node_path: NodePath = _node_deserialization_stack.pop_back()
if not node_path:
return null
var save_dict: Dictionary = _saved_nodes[node_path]
_saved_nodes.erase(node_path)
var scene_file_path: String = save_dict.get(BinarySerializer._NODE_SCENE_FILE_PATH_KEY, "")
save_dict.erase(BinarySerializer._NODE_SCENE_FILE_PATH_KEY)
var node := find_or_instantiate_node(node_path, scene_file_path, true)
if node:
load_node_from_dict(node, save_dict)
return node
## Loads a [SaveKitResource] from the save data. If the resource has already been loaded, the existing instance will be returned.
func _load_resource(buffer: PackedByteArray) -> SaveKitResource:
var resource_id := buffer.decode_u64(BinarySerializer._SAVED_RESOURCE_REFERENCE_ID_U64_OFFSET)
var resource: SaveKitResource = _loaded_resources_by_id.get(resource_id)
if not resource:
var save_dict: Dictionary = _saved_resources_by_id.get(resource_id, {})
if not save_dict:
push_error("No saved resource found with ID ", resource_id)
return null
_saved_resources_by_id.erase(resource_id)
var script: Script = _decode_resource_reference(save_dict.get(BinarySerializer._SAVED_RESOURCE_SCRIPT_KEY) as PackedByteArray, "Script")
if not script:
push_error("Failed to decode script for resource with ID ", resource_id, ", cannot load resource")
return null
save_dict.erase(BinarySerializer._SAVED_RESOURCE_SCRIPT_KEY)
@warning_ignore("unsafe_method_access")
resource = script.new()
resource.load_from_dict(self , save_dict)
_loaded_resources_by_id[resource_id] = resource
return resource