├── RefSerializer.gd └── RefSerializer.gd.uid /RefSerializer.gd: -------------------------------------------------------------------------------- 1 | class_name RefSerializer 2 | ## Utility class for registering and serializing light-weight RefCounted-based structs. 3 | ## 4 | ## RefSerializer allows you to register custom types based on [RefCounted], serialize them and store in files. The advantage of using RefCounted objects is that they are lighter than Resources and custom serialization allows for more compact storing. The types are not bound to any scripts, so there is no problems with compatibility. 5 | 6 | ## Notification received after deserializing object, if [member send_deserialized_notification] is enabled. 7 | const NOTIFICATION_DESERIALIZED = 2137 8 | 9 | ## Dictionary key used to store object's type. 10 | const TYPE_KEY = &"$type" 11 | 12 | ## Metadata name assigned to created RefCounted objects. 13 | const TYPE_META = &"_type" 14 | 15 | static var _types: Dictionary[StringName, Callable] 16 | static var _default_cache: Dictionary[StringName, RefCounted] 17 | 18 | ## If [code]false[/code], properties with values equal to their defaults will not be serialized. This has a slight performance impact, but decreases storage size. 19 | static var serialize_defaults: bool = false 20 | 21 | ## If [code]true[/code], properties that begin with underscore will not be serialized. This has a slight performance impact, but can be useful for redundant or temporary properties. 22 | static var skip_underscore_properties: bool = false 23 | 24 | ## If [code]true[/code], deserialized object will receive [constant NOTIFICATION_DESERIALIZED], which can be used to initialize some values (e.g. properties skipped because of underscore). 25 | static var send_deserialized_notification: bool = true 26 | 27 | ## Registers a custom type. You need to call this before creating or loading any instance of that type. [param constructor] can be any method that returns a [RefCounted] object, but it's most convenient to use [code]new[/code] method of a class. 28 | ## [codeblock] 29 | ## class Item: 30 | ## var value: int 31 | ## 32 | ## RefSerializer.register_type(&"Item", Item.new) 33 | static func register_type(type: StringName, constructor: Callable): 34 | _types[type] = constructor 35 | 36 | if not serialize_defaults: 37 | _default_cache[type] = constructor.call() 38 | 39 | ## Creates a new instance of a registered [param type]. Only objects created using this method can be serialized. 40 | ## [codeblock] 41 | ## var item: Item = RefSerializer.create_object(&"Item") 42 | static func create_object(type: StringName) -> RefCounted: 43 | var constructor = _types.get(type) 44 | if constructor is Callable: 45 | var object: RefCounted = constructor.call() 46 | object.set_meta(TYPE_META, type) 47 | return object 48 | 49 | assert(false, "Type not registered: %s" % type) 50 | return null 51 | 52 | ## Creates a new instance of [param object]'s type and copies all properties to the new object. The original object needs to have been created with [method create_object] or this method. If [param deep] is [code]true[/code], all [Array] and [Dictionary] properties will be recursively duplicated. 53 | static func duplicate_object(object: RefCounted, deep := false) -> RefCounted: 54 | var type: StringName = object.get_meta(TYPE_META, &"") 55 | if type.is_empty(): 56 | push_error("Object %s has no type info" % object) 57 | return null 58 | 59 | var duplicate := create_object(object.get_meta(TYPE_META)) 60 | 61 | for property in object.get_property_list(): 62 | if not property["usage"] & PROPERTY_USAGE_SCRIPT_VARIABLE: 63 | continue 64 | 65 | var property_name: StringName = property["name"] 66 | if skip_underscore_properties and property_name.begins_with("_"): 67 | continue 68 | 69 | var value: Variant = object.get(property_name) 70 | if deep: 71 | value = _duplicate_value(value) 72 | 73 | duplicate.set(property_name, value) 74 | 75 | return duplicate 76 | 77 | static func _duplicate_value(value: Variant) -> Variant: 78 | if value is RefCounted: 79 | return duplicate_object(value) 80 | elif value is Object: 81 | assert(false, "Objects can't be serialized. Only registered RefCounteds are supported.") 82 | return null 83 | elif value is Array: 84 | var old_array := value as Array 85 | var new_array := Array([], old_array.get_typed_builtin(), old_array.get_typed_class_name(), old_array.get_typed_script()) 86 | new_array.resize(old_array.size()) 87 | 88 | for i in old_array.size(): 89 | new_array[i] = _duplicate_value(old_array[i]) 90 | return new_array 91 | elif value is Dictionary: 92 | var old_dictionary := value as Dictionary 93 | var new_dictionary := Dictionary({}, 94 | old_dictionary.get_typed_key_builtin(), old_dictionary.get_typed_key_class_name(), old_dictionary.get_typed_key_script(), 95 | old_dictionary.get_typed_value_builtin(), old_dictionary.get_typed_value_class_name(), old_dictionary.get_typed_value_script()) 96 | 97 | for key in old_dictionary: 98 | new_dictionary[key] = _duplicate_value(old_dictionary[key]) 99 | return new_dictionary 100 | 101 | return value 102 | 103 | ## Serializes a registered object (created via [method create_object]) into a Dictionary, storing values of its properties. If a property value is equal to its default, it will not be stored unless [member serialize_defaults] is enabled. You can use [method deserialize_object] to re-create the object. 104 | ## [br][br]This method only supports [RefCounted] objects created with [method create_object]. The objects are serialized recursively if they are stored in any of the properties. If a property value is [Resource] or [Node], it will be serialized as [code]null[/code]. 105 | static func serialize_object(object: RefCounted) -> Dictionary[StringName, Variant]: 106 | var data: Dictionary[StringName, Variant] 107 | 108 | var type: StringName = object.get_meta(TYPE_META, &"") 109 | if type.is_empty(): 110 | push_error("Object %s has no type info" % object) 111 | return data 112 | 113 | var default: RefCounted 114 | if not serialize_defaults: 115 | default = _default_cache.get(type) 116 | 117 | data[TYPE_KEY] = type 118 | for property in object.get_property_list(): 119 | if not property["usage"] & PROPERTY_USAGE_SCRIPT_VARIABLE: 120 | continue 121 | 122 | var property_name: StringName = property["name"] 123 | if skip_underscore_properties and property_name.begins_with("_"): 124 | continue 125 | 126 | var value: Variant = object.get(property_name) 127 | if default and value == default.get(property_name): 128 | continue 129 | 130 | data[property_name] = _serialize_value(value) 131 | 132 | return data 133 | 134 | static func _serialize_value(value: Variant) -> Variant: 135 | if value is RefCounted: 136 | return serialize_object(value) 137 | elif value is Object: 138 | assert(false, "Objects can't be serialized. Only registered RefCounteds are supported.") 139 | return null 140 | elif value is Array: 141 | return value.map(func(element: Variant) -> Variant: return _serialize_value(element)) 142 | elif value is Dictionary: 143 | var new_value: Dictionary 144 | for key in value: 145 | new_value[key] = _serialize_value(value[key]) 146 | return new_value 147 | 148 | return value 149 | 150 | ## Deserializes a Dictionary created using [method serialize_object], returning an instance of its class. The Dictionary can be created manually, it just needs a [code]$type[/code] key with class name, other fields will be used to assign properties. 151 | static func deserialize_object(data: Dictionary[StringName, Variant]) -> RefCounted: 152 | var type: StringName = data.get(TYPE_KEY, &"") 153 | if type.is_empty(): 154 | push_error("Object data has no type info.") 155 | return null 156 | 157 | var object := create_object(type) 158 | for property in data: 159 | if property == TYPE_KEY: 160 | continue 161 | 162 | var value = _deserialize_value(data[property]) 163 | if value is Array or value is Dictionary: 164 | object.get(property).assign(value) 165 | else: 166 | object.set(property, value) 167 | 168 | if send_deserialized_notification: 169 | object.notification(NOTIFICATION_DESERIALIZED) 170 | 171 | return object 172 | 173 | static func _deserialize_value(value: Variant) -> Variant: 174 | if value is Dictionary: 175 | var type: StringName = value.get(TYPE_KEY, &"") 176 | if not type.is_empty(): 177 | return deserialize_object(value) 178 | else: 179 | var new_value: Dictionary 180 | for key in value: 181 | new_value[key] = _deserialize_value(value[key]) 182 | return new_value 183 | elif value is Array: 184 | return value.map(func(element: Variant) -> Variant: return _deserialize_value(element)) 185 | 186 | return value 187 | 188 | ## Saves the registered object under the given path. The extension is irrelevant. The object is serialized before saving, using [method serialize_object], and stored in a text format with [method @GlobalScope.var_to_str]. 189 | static func save_as_text(object: RefCounted, path: String): 190 | var data := serialize_object(object) 191 | var file := FileAccess.open(path, FileAccess.WRITE) 192 | file.store_string(var_to_str(data)) 193 | 194 | ## Saves the registered object under the given path. The extension is irrelevant. The object is serialized before saving, using [method serialize_object], and stored as a JSON string using [method JSON.from_native]. [param indent] specifies how the resulting JSON should be indented. You can pass empty [String] to disable indentation and save space. 195 | static func save_as_json(object: RefCounted, path: String, indent := "\t"): 196 | var data := serialize_object(object) 197 | var file := FileAccess.open(path, FileAccess.WRITE) 198 | file.store_string(JSON.stringify(JSON.from_native(data), indent)) 199 | 200 | ## Saves the registered object under the given path. The extension is irrelevant. The object is serialized before saving, using [method serialize_object], and stored in a binary format with [method FileAccess.store_var]. 201 | static func save_as_binary(object: RefCounted, path: String): 202 | var data := serialize_object(object) 203 | var file := FileAccess.open(path, FileAccess.WRITE) 204 | file.store_var(data) 205 | 206 | ## Loads and deserializes an object from a file saved in a text format. Only supports the format saved with [method save_as_text]. 207 | ## [br][br][b]Note:[/b] As of now, the method [method @GlobalScope.str_to_var] used internally, allows for deserializing Objects and potentially arbitrary code execution, making it not suitable for save files. If you want to safely store the data as text, use [method save_as_json] and [method load_from_json] instead. 208 | static func load_from_text(path: String) -> RefCounted: 209 | var data: Dictionary[StringName, Variant] = str_to_var(FileAccess.get_file_as_string(path)) 210 | return deserialize_object(data) 211 | 212 | ## Loads and deserializes an object from a file saved as a JSON string. Only supports the format saved with [method save_as_json]. 213 | static func load_from_json(path: String) -> RefCounted: 214 | var data: Dictionary[StringName, Variant] = JSON.to_native(JSON.parse_string(FileAccess.get_file_as_string(path))) 215 | return deserialize_object(data) 216 | 217 | ## Loads and deserializes an object from a file saved in a binary format. Only supports the format saved with [method save_as_binary]. 218 | static func load_from_binary(path: String) -> RefCounted: 219 | var file := FileAccess.open(path, FileAccess.READ) 220 | var data: Dictionary[StringName, Variant] = file.get_var() 221 | return deserialize_object(data) 222 | -------------------------------------------------------------------------------- /RefSerializer.gd.uid: -------------------------------------------------------------------------------- 1 | uid://d0mutscvvn8mm 2 | --------------------------------------------------------------------------------