fuck it im bored

This commit is contained in:
Rowan 2025-06-06 19:32:51 -04:00
parent aaf5b6bb72
commit ac3f0d73a6
12 changed files with 526 additions and 673 deletions

View file

@ -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.

View file

@ -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))

View file

@ -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))

View file

@ -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")

View file

@ -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

View file

@ -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=""

View file

@ -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)

View file

@ -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)

View file

@ -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')

View file

@ -24,31 +24,37 @@ persist=""
move_up={ move_up={
"deadzone": 0.2, "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) "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={ move_down={
"deadzone": 0.2, "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) "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={ move_left={
"deadzone": 0.2, "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) "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={ move_right={
"deadzone": 0.2, "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) "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={ run={
"deadzone": 0.2, "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) "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={ interact={
"deadzone": 0.2, "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) "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)
] ]
} }

View file

@ -3,18 +3,15 @@ class_name Persistence extends Node
@export var path = "user://saves/data.sav" @export var path = "user://saves/data.sav"
@export var group_name = "persist" @export var group_name = "persist"
const PersistenceOptionsMethod = "_get_persistence_options"
const SaveMethod = "on_save" const SaveMethod = "on_save"
const OnBeforeLoadMethod = "on_before_load" const OnBeforeLoadMethod = "on_before_load"
const LoadMethod = "on_load" const LoadMethod = "on_load"
# TODO: binary (de)serialization
static func get_instance_data(node: Node): static func get_instance_data(node: Node):
return { return {
path = node.get_path(), path = node.get_path(),
scene = node.scene_file_path, scene = node.scene_file_path,
data = JSON.from_native(node.call(SaveMethod)) data = node.call(SaveMethod)
} }
func save() -> Result: func save() -> Result:
@ -32,7 +29,11 @@ func save() -> Result:
continue continue
var data = get_instance_data(node) 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 return Result.Unit
@ -52,7 +53,7 @@ func _get_or_add(instance_data: Dictionary) -> Variant:
return node return node
func load(fail_on_err: bool = false) -> Result: func load() -> Result:
if not FileAccess.file_exists(path): if not FileAccess.file_exists(path):
return Result.err('ENOENT: "%s" does not exist' % 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() var length = file.get_length()
while file.get_position() < length: while file.get_position() < length:
var line = file.get_line() var byte_len = file.get_32()
var result = Json.parse_string(line) var bytes = file.get_buffer(byte_len)
var result = bytes_to_var(bytes)
if result.is_err(): var save_data = BinarySerializer.deserialize_var(result)
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 instance = await _get_or_add(save_data) var instance = await _get_or_add(save_data)
if not instance.has_method(LoadMethod): if not instance.has_method(LoadMethod):
@ -80,7 +74,6 @@ func load(fail_on_err: bool = false) -> Result:
instance.queue_free() instance.queue_free()
continue continue
var instance_data = JSON.to_native(save_data.data) instance.call(LoadMethod, save_data.data)
instance.call(LoadMethod, instance_data)
return Result.Unit return Result.Unit