signalis-eb/godot/addons/godot_object_serializer/object_serializer.gd
2025-06-06 19:32:51 -04:00

185 lines
6.4 KiB
GDScript

## Main godot-object-serializer class. Stores the script registry.
## [url]https://github.com/Cretezy/godot-object-serializer[/url]
class_name ObjectSerializer
## The field containing the type in serialized object values. Not recommended to change.
##
## This should be set to something unlikely to clash with keys in objects/dictionaries.
##
## This can be changed, but must be configured before any serialization or deserialization.
static var type_field := "._type"
## The field containing the constructor arguments in serialized object values. Not recommended to change.
##
## This should be set to something unlikely to clash with keys in objects.
##
## This can be changed, but must be configured before any serialization or deserialization.
static var args_field := "._"
## The prefix for object types stored in [member type_field]. Not recommended to change.
##
## This should be set to something unlikely to clash with built-in type names.
##
## This can be changed, but must be configured before any serialization or deserialization.
static var object_type_prefix := "Object_"
## By default, variables with [@GlobalScope.PROPERTY_USAGE_SCRIPT_VARIABLE] are serialized (all variables have this by default).
## When [member require_export_storage] is true, variables will also require [@GlobalScope.PROPERTY_USAGE_STORAGE] to be serialized.
## This can be set on variables using [annotation @GDScript.@export_storage]. Example: [code]@export_storage var name: String[/code]
static var require_export_storage := false
## Registry of object types
static var _script_registry: Dictionary[String, _ScriptRegistryEntry]
## Registers a script (an object type) to be serialized/deserialized. All custom types (including nested types) must be registered [b]before[/b] using this library.
## [param name] can be empty if script uses [code]class_name[/code] (e.g [code]ObjectSerializer.register_script("", Data)[/code]), but it's generally better to set the name.
static func register_script(name: StringName, script: Script) -> void:
var script_name := _get_script_name(script, name)
assert(script_name, "Script must have name\n" + script.source_code)
var entry := _ScriptRegistryEntry.new()
entry.script_type = script
entry.type = object_type_prefix + script_name
_script_registry[entry.type] = entry
## Registers multiple scripts (object types) to be serialized/deserialized from a dictionary.
## See [method ObjectSerializer.register_script]
static func register_scripts(scripts: Dictionary[String, Script]) -> void:
for name in scripts:
register_script(name, scripts[name])
static func _get_script_name(script: Script, name: StringName = "") -> StringName:
if name:
return name
if script.resource_name:
return script.resource_name
if script.get_global_name():
return script.get_global_name()
return ""
static func _get_entry(name: StringName = "", script: Script = null) -> _ScriptRegistryEntry:
if name:
var entry: _ScriptRegistryEntry = _script_registry.get(name)
if entry:
return entry
if script:
for i: String in _script_registry:
var entry: _ScriptRegistryEntry = _script_registry.get(i)
if entry:
if script == entry.script_type:
return entry
return null
class _ScriptRegistryEntry:
var type: String
var script_type: Script
func serialize(value: Variant, next: Callable) -> Variant:
if value.has_method("_serialize"):
var result: Dictionary = value._serialize(next)
result[ObjectSerializer.type_field] = type
return result
var result := {ObjectSerializer.type_field: type}
var excluded_properties: Array[String] = []
if value.has_method("_get_excluded_properties"):
excluded_properties = value._get_excluded_properties()
var partial: Dictionary = {}
if value.has_method("_serialize_partial"):
partial = value._serialize_partial(next)
for property: Dictionary in value.get_property_list():
if (
property.usage & PROPERTY_USAGE_SCRIPT_VARIABLE
and (
!ObjectSerializer.require_export_storage
or property.usage & PROPERTY_USAGE_STORAGE
)
and !excluded_properties.has(property.name)
and !partial.has(property.name)
):
result[property.name] = next.call(value.get(property.name))
for key in partial:
result[key] = partial[key]
if value.has_method("_get_constructor_args"):
var args: Array = value._get_constructor_args()
result[ObjectSerializer.args_field] = args
return result
## When [param json_keys] is enabled, attempt to convert int/float/bool string keys into values
func deserialize(value: Variant, next: Callable, json_keys := false) -> Variant:
if script_type.has_method("_deserialize"):
return script_type._deserialize(value, next)
var instance: Variant
if value.has(ObjectSerializer.args_field):
instance = script_type.new.callv(value[ObjectSerializer.args_field])
else:
instance = script_type.new()
var excluded_properties: Array[String] = []
if instance.has_method("_get_excluded_properties"):
excluded_properties = instance._get_excluded_properties()
var partial: Dictionary = {}
if instance.has_method("_deserialize_partial"):
partial = instance._deserialize_partial(value, next)
for key: String in value:
if (
key == ObjectSerializer.type_field
or key == ObjectSerializer.args_field
or excluded_properties.has(key)
or partial.has(key)
):
continue
var key_value: Variant = next.call(value[key])
match typeof(key_value):
TYPE_DICTIONARY:
if json_keys and instance[key].is_typed_key():
match instance[key].get_typed_key_builtin():
TYPE_STRING:
instance[key].assign(key_value)
TYPE_BOOL:
var dict: Dictionary[bool, Variant] = {}
for i in key_value:
dict[i == "true"] = key_value[i]
instance[key].assign(dict)
TYPE_INT:
var dict: Dictionary[int, Variant] = {}
for i in key_value:
dict[int(i)] = key_value[i]
instance[key].assign(dict)
TYPE_FLOAT:
var dict: Dictionary[float, Variant] = {}
for i in key_value:
dict[float(i)] = key_value[i]
instance[key].assign(dict)
_:
assert(
false,
"Trying to deserialize from JSON to a dictionary with non-primitive (String/int/float/bool) keys"
)
else:
instance[key].assign(key_value)
TYPE_ARRAY:
instance[key].assign(key_value)
_:
instance[key] = key_value
for key in partial:
instance[key] = partial[key]
return instance