From ac3f0d73a6232ad128bab25eb93c531e5a08503e Mon Sep 17 00:00:00 2001 From: rowan Date: Fri, 6 Jun 2025 19:32:51 -0400 Subject: [PATCH] fuck it im bored --- godot/addons/godot_object_serializer/LICENSE | 21 + .../binary_serializer.gd | 63 +++ .../dictionary_serializer.gd | 101 +++++ godot/addons/godot_object_serializer/example | 131 +++++++ .../object_serializer.gd | 185 +++++++++ .../addons/godot_object_serializer/plugin.cfg | 7 + godot/addons/serde/binary.gd | 0 godot/addons/serde/encoding.gd | 62 --- godot/addons/serde/json.gd | 364 ------------------ godot/addons/serde/serde.gd | 228 ----------- godot/project.godot | 6 + godot/src/persistence.gd | 31 +- 12 files changed, 526 insertions(+), 673 deletions(-) create mode 100644 godot/addons/godot_object_serializer/LICENSE create mode 100644 godot/addons/godot_object_serializer/binary_serializer.gd create mode 100644 godot/addons/godot_object_serializer/dictionary_serializer.gd create mode 100644 godot/addons/godot_object_serializer/example create mode 100644 godot/addons/godot_object_serializer/object_serializer.gd create mode 100644 godot/addons/godot_object_serializer/plugin.cfg delete mode 100644 godot/addons/serde/binary.gd delete mode 100644 godot/addons/serde/encoding.gd delete mode 100644 godot/addons/serde/json.gd delete mode 100644 godot/addons/serde/serde.gd diff --git a/godot/addons/godot_object_serializer/LICENSE b/godot/addons/godot_object_serializer/LICENSE new file mode 100644 index 0000000..eff7d83 --- /dev/null +++ b/godot/addons/godot_object_serializer/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Charles Crete + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/godot/addons/godot_object_serializer/binary_serializer.gd b/godot/addons/godot_object_serializer/binary_serializer.gd new file mode 100644 index 0000000..d8fbcbf --- /dev/null +++ b/godot/addons/godot_object_serializer/binary_serializer.gd @@ -0,0 +1,63 @@ +## Serializer to be used with Godot's built-in binary serialization ([method @GlobalScope.var_to_bytes] and [method @GlobalScope.bytes_to_var]). +## This serializes objects but leaves built-in Godot types as-is. +class_name BinarySerializer + + +## Serialize [param data] to value which can be passed to [method @GlobalScope.var_to_bytes]. +static func serialize_var(value: Variant) -> Variant: + match typeof(value): + TYPE_OBJECT: + var name: StringName = value.get_script().get_global_name() + var object_entry := ObjectSerializer._get_entry(name, value.get_script()) + if !object_entry: + assert( + false, + ( + "Could not find type (%s) in registry\n%s" + % [name if name else "no name", value.get_script().source_code] + ) + ) + + return object_entry.serialize(value, serialize_var) + TYPE_ARRAY: + return value.map(serialize_var) + TYPE_DICTIONARY: + var result := {} + for i: Variant in value: + result[i] = serialize_var(value[i]) + return result + + return value + + +## Serialize [param data] into bytes with [method BinaryObjectSerializer.serialize_var] and [method @GlobalScope.var_to_bytes]. +static func serialize_bytes(value: Variant) -> PackedByteArray: + return var_to_bytes(serialize_var(value)) + + +## Deserialize [param data] from [method @GlobalScope.bytes_to_var] to value. +static func deserialize_var(value: Variant) -> Variant: + match typeof(value): + TYPE_DICTIONARY: + if value.has(ObjectSerializer.type_field): + var type: String = value.get(ObjectSerializer.type_field) + if type.begins_with(ObjectSerializer.object_type_prefix): + var entry := ObjectSerializer._get_entry(type) + if !entry: + assert(false, "Could not find type (%s) in registry" % type) + + return entry.deserialize(value, deserialize_var) + + var result := {} + for i: Variant in value: + result[i] = deserialize_var(value[i]) + return result + TYPE_ARRAY: + return value.map(deserialize_var) + + return value + + +## Deserialize bytes [param data] to value with [method @GlobalScope.bytes_to_var] and [method BinaryObjectSerializer.deserialize_var]. +static func deserialize_bytes(value: PackedByteArray) -> Variant: + return deserialize_var(bytes_to_var(value)) diff --git a/godot/addons/godot_object_serializer/dictionary_serializer.gd b/godot/addons/godot_object_serializer/dictionary_serializer.gd new file mode 100644 index 0000000..6acbaf4 --- /dev/null +++ b/godot/addons/godot_object_serializer/dictionary_serializer.gd @@ -0,0 +1,101 @@ +## Serializer to be used with JSON serialization ([method JSON.stringify] and [method JSON.parse_string]). +## This serializes objects and built-in Godot types. +class_name DictionarySerializer + +# Types that can natively be represented in JSON +const _JSON_SERIALIZABLE_TYPES = [ + TYPE_NIL, TYPE_BOOL, TYPE_INT, TYPE_FLOAT, TYPE_STRING, TYPE_STRING_NAME +] + +## Controls if [PackedByteArray] should be serialized as base64 (instead of array of bytes as uint8). +## It's highly recommended to leave this enabled as it will result to smaller serialized payloads and should be faster. +## This can be changed, but must be configured before any serialization or deserialization. +static var bytes_as_base64 := true +## The type of the object for [PackedByteArray] when [member bytes_as_base64] is enabled. +## This should be set to something unlikely to clash with built-in type names or [member ObjectSerializer.object_type_prefix]. +## This can be changed, but must be configured before any serialization or deserialization. +static var bytes_to_base64_type := "PackedByteArray_Base64" + + +## Serialize [param data] into value which can be passed to [method JSON.stringify]. +static func serialize_var(value: Variant) -> Variant: + match typeof(value): + TYPE_OBJECT: + var name: StringName = value.get_script().get_global_name() + var object_entry := ObjectSerializer._get_entry(name, value.get_script()) + if !object_entry: + assert( + false, + ( + "Could not find type (%s) in registry\n%s" + % [name if name else "no name", value.get_script().source_code] + ) + ) + + return object_entry.serialize(value, serialize_var) + + TYPE_ARRAY: + return value.map(serialize_var) + + TYPE_DICTIONARY: + var result := {} + for i: Variant in value: + result[i] = serialize_var(value[i]) + return result + + TYPE_PACKED_BYTE_ARRAY: + if bytes_as_base64: + return { + ObjectSerializer.type_field: bytes_to_base64_type, + ObjectSerializer.args_field: + Marshalls.raw_to_base64(value) if !value.is_empty() else "" + } + + if _JSON_SERIALIZABLE_TYPES.has(typeof(value)): + return value + + return { + ObjectSerializer.type_field: type_string(typeof(value)), + ObjectSerializer.args_field: JSON.from_native(value)["args"] + } + + +## Serialize [param data] into JSON string with [method DictionaryObjectSerializer.serialize_var] and [method JSON.stringify]. Supports same arguments as [method JSON.stringify] +static func serialize_json( + value: Variant, indent := "", sort_keys := true, full_precision := false +) -> String: + return JSON.stringify(serialize_var(value), indent, sort_keys, full_precision) + + +## Deserialize [data] from [JSON.parse_string] into value. +static func deserialize_var(value: Variant) -> Variant: + match typeof(value): + TYPE_DICTIONARY: + if value.has(ObjectSerializer.type_field): + var type: String = value.get(ObjectSerializer.type_field) + if bytes_as_base64 and type == bytes_to_base64_type: + return Marshalls.base64_to_raw(value[ObjectSerializer.args_field]) + + if type.begins_with(ObjectSerializer.object_type_prefix): + var entry := ObjectSerializer._get_entry(type) + if !entry: + assert(false, "Could not find type (%s) in registry" % type) + + return entry.deserialize(value, deserialize_var, true) + + return JSON.to_native({"type": type, "args": value[ObjectSerializer.args_field]}) + + var result := {} + for i: Variant in value: + result[i] = deserialize_var(value[i]) + return result + + TYPE_ARRAY: + return value.map(deserialize_var) + + return value + + +## Deserialize JSON string [param data] to value with [method JSON.parse_string] and [method DictionaryObjectSerializer.deserialize_var]. +static func deserialize_json(value: String) -> Variant: + return deserialize_var(JSON.parse_string(value)) diff --git a/godot/addons/godot_object_serializer/example b/godot/addons/godot_object_serializer/example new file mode 100644 index 0000000..8990a19 --- /dev/null +++ b/godot/addons/godot_object_serializer/example @@ -0,0 +1,131 @@ +extends SceneTree + + +# Example data class. Can extend any type, include Resource +class Data: + # Supports all primitive types (String, int, float, bool, null), including @export variables + @export var string: String + # Supports all extended built-in types (Vector2/3/4/i, Rect2/i, Transform2D/3D, Color, Packed*Array, etc) + var vector: Vector3 + # Supports enum + var enum_state: State + # Supports arrays, including Array[Variant] + var array: Array[int] + # Supports dictionaries, including Dictionary[Variant, Variant] + var dictionary: Dictionary[String, Vector2] + # Supports efficient byte array serialization to base64 + var packed_byte_array: PackedByteArray + # Supports nested data, either as a field or in array/dictionary + var nested: DataResource + + +class DataResource: + extends Resource + var name: String + + +enum State { OPENED, CLOSED } + + +# _static_init is used to register scripts without having to instanciate the script. +# It's recommended to either place all registrations in a single script, or have each script register itself. +static func _static_init() -> void: + # Required: Register possible object scripts + ObjectSerializer.register_script("Data", Data) + ObjectSerializer.register_script("DataResource", DataResource) + + +# Setup testing data +var data := Data.new() + + +func _init() -> void: + data.string = "Lorem ipsum" + data.vector = Vector3(1, 2, 3) + data.enum_state = State.CLOSED + data.array = [1, 2] + data.dictionary = {"position": Vector2(1, 2)} + data.packed_byte_array = PackedByteArray([1, 2, 3, 4, 5, 6, 7, 8]) + var data_resource := DataResource.new() + data_resource.name = "dolor sit amet" + data.nested = data_resource + + json_serialization() + binary_serialization() + + +func json_serialization() -> void: + # Serialize to JSON + # Alternative: DictionarySerializer.serialize_json(data) + var serialized: Variant = DictionarySerializer.serialize_var(data) + var json := JSON.stringify(serialized, "\t") + print(json) + """ Output: + { + "._type": "Object_Data", + "string": "Lorem ipsum", + "vector": { + "._type": "Vector3", + "._": [ + 1.0, + 2.0, + 3.0 + ] + }, + "enum_state": 1, + "array": [ + 1, + 2 + ], + "dictionary": { + "position": { + "._type": "Vector2", + "._": [ + 1.0, + 2.0 + ] + } + }, + "packed_byte_array": { + "._type": "PackedByteArray_Base64", + "._": "AQIDBAUGBwg=" + }, + "nested": { + "._type": "Object_DataResource", + "name": "dolor sit amet" + } + } + """ + + # Verify after JSON deserialization + # Alternative: DictionarySerializer.deserialize_json(json) + var parsed_json = JSON.parse_string(json) + var deserialized: Data = DictionarySerializer.deserialize_var(parsed_json) + _assert_data(deserialized) + + +func binary_serialization() -> void: + # Serialize to bytes + # Alternative: BinarySerializer.serialize_bytes(data) + var serialized: Variant = BinarySerializer.serialize_var(data) + var bytes := var_to_bytes(serialized) + print(bytes) + # Output: List of bytes + + # Verify after bytes deserialization. + # Alternative: BinarySerializer.deserialize_bytes(bytes) + var parsed_bytes = bytes_to_var(bytes) + var deserialized: Data = BinarySerializer.deserialize_var(parsed_bytes) + _assert_data(deserialized) + + +func _assert_data(deserialized: Data) -> void: + assert(data.string == deserialized.string, "string is different") + assert(data.vector == deserialized.vector, "vector is different") + assert(data.enum_state == deserialized.enum_state, "enum_state is different") + assert(data.array == deserialized.array, "array is different") + assert(data.dictionary == deserialized.dictionary, "dictionary is different") + assert( + data.packed_byte_array == deserialized.packed_byte_array, "packed_byte_array is different" + ) + assert(data.nested.name == deserialized.nested.name, "nested.name is different") diff --git a/godot/addons/godot_object_serializer/object_serializer.gd b/godot/addons/godot_object_serializer/object_serializer.gd new file mode 100644 index 0000000..7267f6b --- /dev/null +++ b/godot/addons/godot_object_serializer/object_serializer.gd @@ -0,0 +1,185 @@ +## 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 diff --git a/godot/addons/godot_object_serializer/plugin.cfg b/godot/addons/godot_object_serializer/plugin.cfg new file mode 100644 index 0000000..77c1f97 --- /dev/null +++ b/godot/addons/godot_object_serializer/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="Godot Object Serializer" +description="Safely serialize/deserialize objects and built-in types in Godot" +author="Cretezy" +version="0.3.0" +script="" diff --git a/godot/addons/serde/binary.gd b/godot/addons/serde/binary.gd deleted file mode 100644 index e69de29..0000000 diff --git a/godot/addons/serde/encoding.gd b/godot/addons/serde/encoding.gd deleted file mode 100644 index f58a8eb..0000000 --- a/godot/addons/serde/encoding.gd +++ /dev/null @@ -1,62 +0,0 @@ -class_name Encoding extends RefCounted - -enum Type { - Ascii, - Utf8, - Utf16, - Utf32, - Wchar -} - -static var Ascii = from(Type.Ascii) -static var Utf8 = from(Type.Utf8) -static var Utf16 = from(Type.Utf16) -static var Utf32 = from(Type.Utf32) -static var Wchar = from(Type.Wchar) - -var type: Type - -func _init(_type: Type) -> void: - type = _type - -static func from(type: Type) -> Encoding: - return Encoding.new(type) - -static func byte_size_of(encoding: Type) -> int: - match encoding: - Type.Ascii: return 1 - Type.Utf8: return 1 - Type.Utf16: return 2 - Type.Utf32: return 4 - Type.Wchar: - match OS.get_name(): - "Windows": return 2 - _: return 4 - return -1 - -func byte_size() -> int: - return byte_size_of(type) - -static func encode_str_as(value: String, encoding: Type) -> PackedByteArray: - match encoding: - Type.Ascii: return value.to_ascii_buffer() - Type.Utf8: return value.to_utf8_buffer() - Type.Utf16: return value.to_utf16_buffer() - Type.Utf32: return value.to_utf32_buffer() - Type.Wchar: return value.to_wchar_buffer() - return PackedByteArray() - -func encode_str(value: String) -> PackedByteArray: - return encode_str_as(value, type) - -static func decode_str_as(value: PackedByteArray, encoding: Type) -> String: - match encoding: - Type.Ascii: return value.get_string_from_ascii() - Type.Utf8: return value.get_string_from_utf8() - Type.Utf16: return value.get_string_from_utf16() - Type.Utf32: return value.get_string_from_utf32() - Type.Wchar: return value.get_string_from_wchar() - return "" - -func decode_str(value: PackedByteArray) -> String: - return decode_str_as(value, type) diff --git a/godot/addons/serde/json.gd b/godot/addons/serde/json.gd deleted file mode 100644 index f60cb73..0000000 --- a/godot/addons/serde/json.gd +++ /dev/null @@ -1,364 +0,0 @@ -class_name Json - -static func stringify(value: Variant, allow_eval: bool = false) -> String: - var ser = Json.Serializer.new(allow_eval) - Serde.serialize(ser, value, allow_eval) - return ser.output - -static func parse_string(value: String, into: Variant = null, allow_eval: bool = false) -> Variant: - var de = Json.Deserializer.from_string(value) - if into != null and into.has_method(Serde.DeserializeMethod): - return into.call(Serde.DeserializeMethod, de) - else: - return de.deserialize_any(Serde.GenericVisitor.new()) - -class SerializeSeq extends Serde.SerializeSeq: - var ser: Json.Serializer - var first: bool = true - - func _init(ser: Json.Serializer) -> void: - self.ser = ser - ser.write('[') - - func serialize_element(value: Variant) -> Result: - if not first: ser.write(',') - first = false - Serde.serialize(ser, value, ser.allow_eval) - return Result.Unit - - func end() -> Result: - ser.write(']') - return Result.Unit - -class SerializeMap extends Serde.SerializeMap: - var ser: Json.Serializer - var first: bool = true - - func _init(ser: Json.Serializer) -> void: - self.ser = ser - ser.write('{') - - func serialize_key(value: Variant) -> Result: - if not first: ser.write(',') - first = false - Serde.serialize(ser, value, ser.allow_eval) - return Result.Unit - - func serialize_value(value: Variant) -> Result: - ser.write(':') - Serde.serialize(ser, value, ser.allow_eval) - return Result.Unit - - func end() -> Result: - ser.write('}') - return Result.Unit - -class Serializer extends Serde.Serializer: - var output: String - var allow_eval: bool - - func _init(allow_eval: bool = false) -> void: - self.allow_eval = allow_eval - - func write(value: String) -> Result: - output += value - return Result.Unit - - func write_string(value: Variant) -> Result: - return write(str(value)) - - func serialize_nil() -> Result: - return write("null") - - func serialize_bool(value: bool) -> Result: - return write_string(value) - - func serialize_int(value: int): - return write_string(value) - - func serialize_float(value: float) -> Result: - return write_string(value) - - func serialize_string(value: String): - return write('"%s"' % value) - - func serialize_seq(_len: int): - return Json.SerializeSeq.new(self) - - func serialize_dict(_len: int): - return Json.SerializeMap.new(self) - - func serialize_object(_name: StringName, len: int): - return serialize_dict(len) - -class Parser extends RefCounted: - var de: Json.Deserializer - var iter: PeekableIter - - func _init(de: Json.Deserializer) -> void: - self.de = de - iter = de._iter - - static func eq(a: Variant, b: Variant) -> bool: - return a == b - - static func _join(value: Array) -> String: - return "".join(value) - - static func _collect(results: Array[Result]) -> Result: - return Result.collect_ok(results).map(_join) - - func any() -> Result: - match iter.peek(): - var x when x.is_some(): - return Result.ok(iter.next().unwrap()) - _: return Result.err(Error.new("eof")) - - func char(ch: String) -> Result: - match iter.peek(): - var x when x.filter(eq.bind(ch)).is_some(): return Result.ok(iter.next().unwrap()) - var x: return Result.err(Error.new('expecting "%s", got "%s"' % [ch, x.unwrap()])) - - func str(value: String) -> Result: - for i in range(0, value.length()): - var ch = self.char(value[i]) - if ch.is_err(): return ch - return Result.ok(value) - - func any_of(parsers: Array[Callable]) -> Result: - for parser in parsers: - var result = parser.call() - if result.is_ok(): return result - return Result.err(Error.new('none of the provided parsers matched input')) - - func all_of(parsers: Array) -> Result: - var results: Array[String] = [] - for parser in parsers: - match parser.call(): - var x when x.is_ok(): results.append(x.unwrap()) - var x: return x - return Result.ok(results) - - func many(parser: Callable, limit: int = 1000) -> Result: - var results: Array[String] = [] - var value = parser.call() - - while value.is_ok() and limit > 0: - results.append(value.unwrap()) - value = parser.call() - limit -= 1 - - if limit <= 0: return Result.err(Error.new("executed too many times (%s)" % limit)) - elif len(results) == 0: return Result.err(Error.new("many matched nothing")) - else: return Result.ok(results) - - func until(parser: Callable) -> Result: - return many( - func (): - match parser.call(): - var x when x.is_err(): return any() - _: return Result.err(null) - ) - - func negate(parser: Callable) -> Result: - match parser.call(): - var x when x.is_ok(): return Result.err('negated parser passed with value "%s"' % x.unwrap()) - _: return Result.Unit - - func skip(parser: Callable) -> Result: - return parser.call().chain(func(_v: Variant) -> Result: return Result.ok('')) - - func any_char(chars: String) -> Result: - for ch in chars: - if self.char(ch).is_ok(): return Result.ok(ch) - return Result.err(Error.new('expected any of "%s"' % chars)) - - func optional(parser: Callable, default_value: Variant = '') -> Result.Ok: - match parser.call(): - var x when x.is_ok(): return x - _: return Result.ok(default_value) - - func ws() -> Result: - return any_char(' \t') - - func skip_ws() -> Result: - return skip(many.bind(ws)) - - func parse_sign() -> Result: - return any_char('+-') - - func parse_digit() -> Result: - return any_char('1234567890') - - func parse_digits() -> Result: - return many(parse_digit).map("".join) - - func parse_integer() -> Result: - if self.char('0').is_ok(): return Result.ok(0) - var sign = optional(parse_sign) - var digits = parse_digits() - return _collect([sign, digits]) - - func parse_exponent() -> Result: - var e = any_char('eE') - var sign = optional(parse_sign) - var digits = parse_digits() - return _collect([e, sign, digits]) - - func parse_fractional() -> Result: - var point = self.char('.') - var digits = parse_digits() - var exp = optional(parse_exponent) - return _collect([point, digits, exp]) - - func parse_float() -> Result: - var integer = parse_integer() - var fractional = optional(parse_fractional) - return _collect([integer, fractional]) - - func parse_string() -> Result: - var quote = char.bind('"') - var slash = self.char.bind('\\') - var unescaped_quote = all_of.bind([negate.bind(slash), quote]) - - var open = skip(quote) - var str_value = until(unescaped_quote).map(_join) - return _collect([open, str_value]) - -class SeqAccess extends Serde.SeqAccess: - var de: Json.Deserializer - var first: bool = true - var parser: Parser: - get: return de._parser - - func _init(de: Json.Deserializer) -> void: - self.de = de - - func next_element() -> Result: - parser.skip_ws() - - if parser.char(']').is_ok(): - return Result.ok(Option.Unit) - - if not first and parser.char(',').is_err(): - return Result.err(Error.new('expected comma')) - - first = false - - parser.skip_ws() - return de.deserialize_any(Serde.GenericVisitor.new()).map(Option.some) - -class MapAccess extends Serde.MapAccess: - var de: Json.Deserializer - var first: bool = true - - var iter: PeekableIter: - get: return de._iter - - var parser: Parser: - get: return de._parser - - func _init(de: Json.Deserializer) -> void: - self.de = de - - func next_key() -> Result: - parser.skip_ws() - - if iter.peek().filter(Parser.eq.bind('}')).is_some(): - return Result.ok(Option.Unit) - - if not first: - var comma = parser.char(',') - if comma.is_err(): return comma - - first = false - parser.skip_ws() - var key = de.deserialize_any(Serde.GenericVisitor.new()).map(Option.some) - return key - - func next_value() -> Result: - parser.skip_ws() - - var colon = parser.char(':') - if colon.is_err(): return colon - - parser.skip_ws() - return de.deserialize_any(Serde.GenericVisitor.new()).map(Option.some) - -class Deserializer extends Serde.Deserializer: - var _input: String - var _iter: PeekableIter - var _parser: Parser - - func _init(value: String) -> void: - _input = value - _iter = ListIterator.new(_input, RangeIterator.new(0, len(_input))).into_peekable() - _parser = Parser.new(self) - - static func from_string(value: String) -> Json.Deserializer: - return Json.Deserializer.new(value) - - func deserialize_any(visitor: Visitor): - var next = _iter.peek() - if next.is_none(): return Result.err(Error.new("eof")) - match next.unwrap(): - 'n': return deserialize_nil(visitor) - 't', 'f': return deserialize_bool(visitor) - '"': return deserialize_string(visitor) - '+', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': - return deserialize_float(visitor) - '[': return deserialize_seq(visitor) - '{': return deserialize_dict(visitor) - var x: return Result.err(Error.new('unexpected character "%s" at position %s' % [x, 0])) - - func deserialize_nil(visitor: Visitor): - _parser.skip_ws() - match _parser.str('null'): - var x when x.is_ok(): return Result.ok(visitor.visit_nil()) - var x: return x - - func deserialize_bool(visitor: Visitor): - _parser.skip_ws() - return _parser.any_of([_parser.str.bind('true'), _parser.str.bind('false')]).map(visitor.visit_bool) - - func deserialize_int(visitor: Visitor): - _parser.skip_ws() - return _parser.parse_integer().chain(Convert.FromString.to_int).map(visitor.visit_int) - - func deserialize_float(visitor: Visitor): - _parser.skip_ws() - return _parser.parse_float().chain(Convert.FromString.to_float).map(visitor.visit_float) - - func deserialize_string(visitor: Visitor): - _parser.skip_ws() - return _parser.parse_string().map(visitor.visit_string) - - func _deserialize_complex(open: Callable, close: Callable, on_success: Callable): - _parser.skip_ws() - var open_result = open.call() - if open_result.is_err(): return open_result - - var value = on_success.call() - - var close_result = close.call() - if close_result.is_err(): return close_result - return Result.ok(value) - - func deserialize_seq(visitor: Visitor): - var open = _parser.char.bind('[') - var close = _parser.char.bind(']') - var visit = func() -> Variant: - return visitor.visit_seq(Json.SeqAccess.new(self)) - - return _deserialize_complex(open, close, visit) - - func deserialize_dict(visitor: Visitor): - var open = _parser.char.bind('{') - var close = _parser.char.bind('}') - var visit = func() -> Variant: - return visitor.visit_dict(Json.MapAccess.new(self)) - - return _deserialize_complex(open, close, visit) - - func deserialize_object(_name: StringName, _fields: Array[StringName], visitor: Visitor): - return deserialize_dict(visitor) diff --git a/godot/addons/serde/serde.gd b/godot/addons/serde/serde.gd deleted file mode 100644 index 590a0d2..0000000 --- a/godot/addons/serde/serde.gd +++ /dev/null @@ -1,228 +0,0 @@ -class_name Serde - -const SerializeMethod = &"serialize" -const DeserializeMethod = &"deserialize" - -enum SerdeType { - Nil, - Bool, - Int, - Float, - String, - Seq, - Dict, - Object -} - -static func to_serde_type(godot_type: int) -> Result: - match godot_type: - TYPE_NIL: return Result.ok(Result.ok(SerdeType.Nil)) - TYPE_BOOL: return Result.ok(Result.ok(SerdeType.Bool)) - TYPE_INT: return Result.ok(Result.ok(SerdeType.Int)) - TYPE_FLOAT: return Result.ok(SerdeType.Float) - TYPE_STRING: return Result.ok(SerdeType.String) - TYPE_STRING_NAME: return Result.ok(SerdeType.String) - TYPE_NODE_PATH: return Result.ok(SerdeType.String) - - TYPE_VECTOR2: return Result.ok(SerdeType.Seq) - TYPE_VECTOR2I: return Result.ok(SerdeType.Seq) - TYPE_VECTOR3: return Result.ok(SerdeType.Seq) - TYPE_VECTOR3I: return Result.ok(SerdeType.Seq) - TYPE_VECTOR4I: return Result.ok(SerdeType.Seq) - TYPE_VECTOR4I: return Result.ok(SerdeType.Seq) - - TYPE_RECT2: return Result.ok(SerdeType.Object) - TYPE_RECT2I: return Result.ok(SerdeType.Object) - TYPE_AABB: return Result.ok(SerdeType.Object) - - TYPE_TRANSFORM2D: return Result.ok(SerdeType.Object) - TYPE_BASIS: return Result.ok(SerdeType.Object) - TYPE_TRANSFORM3D: return Result.ok(SerdeType.Object) - TYPE_PLANE: return Result.ok(SerdeType.Object) - TYPE_PROJECTION: return Result.ok(SerdeType.Object) - TYPE_QUATERNION: return Result.ok(SerdeType.Seq) - - TYPE_COLOR: return Result.ok(SerdeType.Seq) - TYPE_RID: return Result.ok(SerdeType.Int) - TYPE_OBJECT: return Result.ok(SerdeType.Object) - TYPE_DICTIONARY: return Result.ok(SerdeType.Dict) - - TYPE_ARRAY: return Result.ok(SerdeType.Seq) - TYPE_PACKED_BYTE_ARRAY: return Result.ok(SerdeType.Seq) - TYPE_PACKED_COLOR_ARRAY: return Result.ok(SerdeType.Seq) - TYPE_PACKED_INT32_ARRAY: return Result.ok(SerdeType.Seq) - TYPE_PACKED_INT64_ARRAY: return Result.ok(SerdeType.Seq) - TYPE_PACKED_STRING_ARRAY: return Result.ok(SerdeType.Seq) - TYPE_PACKED_FLOAT32_ARRAY: return Result.ok(SerdeType.Seq) - TYPE_PACKED_FLOAT64_ARRAY: return Result.ok(SerdeType.Seq) - TYPE_PACKED_VECTOR2_ARRAY: return Result.ok(SerdeType.Seq) - TYPE_PACKED_VECTOR3_ARRAY: return Result.ok(SerdeType.Seq) - TYPE_PACKED_VECTOR4_ARRAY: return Result.ok(SerdeType.Seq) - - TYPE_SIGNAL: return Result.err("Refusing to (de)serialize Signal") - TYPE_CALLABLE: return Result.err("Refusing to (de)serialize Callable") - - _: return Result.err(Error.InvalidArgument.new(godot_type)) - -static func _serialize_seq(serializer: Serializer, value: Variant) -> Variant: - var ser = serializer.serialize_seq(len(value)) - for element in value: - ser.serialize_element(element) - return ser.end() - -static func _serialize_dict(serializer: Serializer, value: Dictionary) -> Variant: - var ser: SerializeMap = serializer.serialize_dict(value.size()) - for key in value.keys(): - ser.serialize_entry(key, value[key]) - return ser.end() - -static func get_object_name(value: Variant) -> Option: - var name: Option = Option.none - - if value.has_method("get_script"): - var script_name = value.get_script().get_global_name() - if script_name != null: name = Option.some(script_name) - - if name == null and value.has_method("get_class"): - var native_name = value.get_class() - if native_name != null: name = Option.some(native_name) - - return name - -static func _serialize_object(serializer: Serializer, value: Variant) -> Variant: - var object_name = get_object_name(value).unwrap() - var ser = serializer.serialize_object(object_name, len(value.get_property_list())) - ser.serialize(value) - return ser.end() - -static func default_serialize(serializer: Serializer, value: Variant, allow_eval: bool = false) -> Variant: - match to_serde_type(typeof(value)).unwrap(): - SerdeType.Nil: return serializer.serialize_nil() - SerdeType.Bool: return serializer.serialize_bool(value) - SerdeType.Int: return serializer.serialize_int(value) - SerdeType.Float: return serializer.serialize_float(value) - SerdeType.String: return serializer.serialize_string(value) - SerdeType.Seq: return _serialize_seq(serializer, value) - SerdeType.Dict: return _serialize_dict(serializer, value) - SerdeType.Object: return _serialize_object(serializer, value) - return Result.err("could not deserialize type %s" % typeof(value)) - - -static func serialize(serializer: Serializer, value: Variant, allow_eval: bool = false) -> Variant: - if typeof(value) == TYPE_OBJECT and value.has_method(SerializeMethod): - return value.call(serializer, value, allow_eval) - else: - return default_serialize(serializer, value, allow_eval) - -class SerializeSeq extends RefCounted: - func serialize_element(element: Variant): Error.NotImplemented.raise('serialize_element') - func end(): Error.NotImplemented.raise('end') - -class SerializeMap extends RefCounted: - func serialize_key(key: Variant): Error.NotImplemented.raise('serialize_key') - func serialize_value(value: Variant): Error.NotImplemented.raise('serialize_value') - func end(): Error.NotImplemented.raise('end') - func serialize_entry(key: Variant, value: Variant): - return Result.collect_ok([serialize_key(key), serialize_value(value)]) - -class SerializeObject extends RefCounted: - func serialize_field(name: StringName, value: Variant): Error.NotImplemented.raise('serialize_field') - func serialize(object: Variant): Error.NotImplemented.raise('serialize') - func end(): Error.NotImplemented.raise('end') - -class Serializer extends RefCounted: - func serialize_nil(): Error.NotImplemented.raise('serialize_nil') - func serialize_bool(_value: bool): Error.NotImplemented.raise('serialize_bool') - func serialize_int(_value: int): Error.NotImplemented.raise('serialize_int') - func serialize_float(_value: float): Error.NotImplemented.raise('serialize_float') - func serialize_string(_value: String): Error.NotImplemented.raise('serialize_string') - func serialize_seq(_len: int): Error.NotImplemented.raise('serialize_string') - func serialize_dict(_len: int): Error.NotImplemented.raise('serialize_string') - func serialize_object(_name: StringName, _len: int): Error.NotImplemented.raise('serialize_string') - -class Visitor extends RefCounted: - func visit_nil(): Error.NotImplemented.raise('visit_nil') - func visit_bool(_value: bool): Error.NotImplemented.raise('visit_bool') - func visit_int(_value: int): Error.NotImplemented.raise('visit_int') - func visit_float(_value: float): Error.NotImplemented.raise('visit_float') - func visit_string(_value: String): Error.NotImplemented.raise('visit_string') - func visit_seq(_access: SeqAccess): Error.NotImplemented.raise('visit_seq') - func visit_dict(_access: MapAccess): Error.NotImplemented.raise('visit_dict') - -class GenericVisitor extends Visitor: - func visit_nil(): return null - func visit_bool(value: bool): - print('bool', value) - return value - func visit_int(value: int): - print('int', value) - return value - func visit_float(value: float): - print('float', value) - return value - func visit_string(value: String): - print('str', value) - return value - func visit_seq(access: SeqAccess): - print('seq', access) - return access.collect() - func visit_dict(access: MapAccess): - var a = access.collect() - print('dict', a) - return a - -class AccessIterable extends Iterator: - var _next: Callable - - func _init(next: Callable) -> void: - _next = next - - func next() -> Option: - return _next.call().to_option().flatten() - -class SeqAccess extends RefCounted: - func next_element(): Error.NotImplemented.raise('next_element') - - func iterate() -> AccessIterable: - return AccessIterable.new(next_element) - - func collect() -> Array: - var list = [] - - for item in iterate(): - list.append(item) - - return list - -class MapAccess extends RefCounted: - func next_key(): Error.NotImplemented.raise('next_key') - func next_value(): Error.NotImplemented.raise('next_value') - - func next_entry() -> Result: - var key = next_key() - var val = next_value() - return Result.collect_ok([key, val]) - - func iterate() -> AccessIterable: - return AccessIterable.new(next_entry) - - func collect() -> Dictionary: - var dict: Dictionary = {} - - print('collecting map') - for entry in iterate(): - print('entry', entry) - dict.set(entry[0], entry[1]) - - return dict - -class Deserializer extends RefCounted: - func deserialize_any(_visitor: Visitor): Error.NotImplemented.raise('deserialize_any') - func deserialize_nil(_visitor: Visitor): Error.NotImplemented.raise('deserialize_nil') - func deserialize_bool(_visitor: Visitor): Error.NotImplemented.raise('deserialize_bool') - func deserialize_int(_visitor: Visitor): Error.NotImplemented.raise('deserialize_int') - func deserialize_float(_visitor: Visitor): Error.NotImplemented.raise('deserialize_float') - func deserialize_string(_visitor: Visitor): Error.NotImplemented.raise('deserialize_string') - func deserialize_seq(_visitor: Visitor): Error.NotImplemented.raise('deserialize_seq') - func deserialize_dict(_visitor: Visitor): Error.NotImplemented.raise('deserialize_dict') - func deserialize_object(_name: StringName, _fields: Array[StringName], _visitor: Visitor): Error.NotImplemented.raise('deserialize_object') diff --git a/godot/project.godot b/godot/project.godot index 79b2a29..f49c41e 100644 --- a/godot/project.godot +++ b/godot/project.godot @@ -24,31 +24,37 @@ persist="" move_up={ "deadzone": 0.2, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":1,"axis_value":-1.0,"script":null) ] } move_down={ "deadzone": 0.2, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":1,"axis_value":1.0,"script":null) ] } move_left={ "deadzone": 0.2, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":0,"axis_value":-1.0,"script":null) ] } move_right={ "deadzone": 0.2, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":0,"axis_value":1.0,"script":null) ] } run={ "deadzone": 0.2, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194325,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":0,"pressure":0.0,"pressed":true,"script":null) ] } interact={ "deadzone": 0.2, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":1,"pressure":0.0,"pressed":true,"script":null) ] } diff --git a/godot/src/persistence.gd b/godot/src/persistence.gd index af316bb..bf9693e 100644 --- a/godot/src/persistence.gd +++ b/godot/src/persistence.gd @@ -3,18 +3,15 @@ class_name Persistence extends Node @export var path = "user://saves/data.sav" @export var group_name = "persist" -const PersistenceOptionsMethod = "_get_persistence_options" const SaveMethod = "on_save" const OnBeforeLoadMethod = "on_before_load" const LoadMethod = "on_load" -# TODO: binary (de)serialization - static func get_instance_data(node: Node): return { path = node.get_path(), scene = node.scene_file_path, - data = JSON.from_native(node.call(SaveMethod)) + data = node.call(SaveMethod) } func save() -> Result: @@ -32,7 +29,11 @@ func save() -> Result: continue var data = get_instance_data(node) - file.store_line(Json.stringify(data)) + var serial = BinarySerializer.serialize_var(data) + var bytes = var_to_bytes(serial) + var length = bytes.size() + file.store_32(length) + file.store_buffer(var_to_bytes(serial)) return Result.Unit @@ -52,7 +53,7 @@ func _get_or_add(instance_data: Dictionary) -> Variant: return node -func load(fail_on_err: bool = false) -> Result: +func load() -> Result: if not FileAccess.file_exists(path): return Result.err('ENOENT: "%s" does not exist' % path) @@ -62,17 +63,10 @@ func load(fail_on_err: bool = false) -> Result: var length = file.get_length() while file.get_position() < length: - var line = file.get_line() - var result = Json.parse_string(line) - - if result.is_err(): - if fail_on_err: return result - else: - push_error('could not read save entry. error: "%s"' % result.unwrap_err()) - continue - - var save_data = result.unwrap() - + var byte_len = file.get_32() + var bytes = file.get_buffer(byte_len) + var result = bytes_to_var(bytes) + var save_data = BinarySerializer.deserialize_var(result) var instance = await _get_or_add(save_data) if not instance.has_method(LoadMethod): @@ -80,7 +74,6 @@ func load(fail_on_err: bool = false) -> Result: instance.queue_free() continue - var instance_data = JSON.to_native(save_data.data) - instance.call(LoadMethod, instance_data) + instance.call(LoadMethod, save_data.data) return Result.Unit