diff --git a/addons/netfox.extras/plugin.cfg b/addons/netfox.extras/plugin.cfg index 2d68d889..273f9e3e 100644 --- a/addons/netfox.extras/plugin.cfg +++ b/addons/netfox.extras/plugin.cfg @@ -3,5 +3,5 @@ name="netfox.extras" description="Game-specific utilities for Netfox" author="Tamas Galffy and contributors" -version="1.35.3" +version="1.36.0" script="netfox-extras.gd" diff --git a/addons/netfox.internals/plugin.cfg b/addons/netfox.internals/plugin.cfg index ad1ba0bc..662d083f 100644 --- a/addons/netfox.internals/plugin.cfg +++ b/addons/netfox.internals/plugin.cfg @@ -3,5 +3,5 @@ name="netfox.internals" description="Shared internals for netfox addons" author="Tamas Galffy and contributors" -version="1.35.3" +version="1.36.0" script="plugin.gd" diff --git a/addons/netfox.noray/plugin.cfg b/addons/netfox.noray/plugin.cfg index a5bfc19c..6c79abde 100644 --- a/addons/netfox.noray/plugin.cfg +++ b/addons/netfox.noray/plugin.cfg @@ -3,5 +3,5 @@ name="netfox.noray" description="Bulletproof your connectivity with noray integration for netfox" author="Tamas Galffy and contributors" -version="1.35.3" +version="1.36.0" script="netfox-noray.gd" diff --git a/addons/netfox/encoder/diff-history-encoder.gd b/addons/netfox/encoder/diff-history-encoder.gd index 221e76a6..384b66ab 100644 --- a/addons/netfox/encoder/diff-history-encoder.gd +++ b/addons/netfox/encoder/diff-history-encoder.gd @@ -3,6 +3,7 @@ class_name _DiffHistoryEncoder var _history: _PropertyHistoryBuffer var _property_cache: PropertyCache +var _schema_handler: _NetworkSchema var _full_snapshot := {} var _encoded_snapshot := {} @@ -14,9 +15,10 @@ var _has_received := false static var _logger := NetfoxLogger._for_netfox("DiffHistoryEncoder") -func _init(p_history: _PropertyHistoryBuffer, p_property_cache: PropertyCache): +func _init(p_history: _PropertyHistoryBuffer, p_property_cache: PropertyCache, p_schema_handler: _NetworkSchema) -> void: _history = p_history _property_cache = p_property_cache + _schema_handler = p_schema_handler func add_properties(properties: Array[PropertyEntry]) -> void: var has_new_properties := false @@ -30,7 +32,7 @@ func add_properties(properties: Array[PropertyEntry]) -> void: _version = (_version + 1) % 256 func encode(tick: int, reference_tick: int, properties: Array[PropertyEntry]) -> PackedByteArray: - assert(properties.size() <= 255, "Property indices may not fit into bytes!") + assert(properties.size() <= 255, "Property indices may not fit into bytes; too many properties!") var snapshot := _history.get_snapshot(tick) var property_strings := properties.map(func(it): return it.to_string()) @@ -47,12 +49,12 @@ func encode(tick: int, reference_tick: int, properties: Array[PropertyEntry]) -> var buffer := StreamPeerBuffer.new() buffer.put_u8(_version) - for property in diff_snapshot.properties(): - var property_idx := _property_indexes.get_by_value(property) as int - var property_value = diff_snapshot.get_value(property) - + for property_path in diff_snapshot.properties(): + var property_idx := _property_indexes.get_by_value(property_path) as int buffer.put_u8(property_idx) - buffer.put_var(property_value) + + var value := diff_snapshot.get_value(property_path) + _schema_handler.encode(property_path, value, buffer) return buffer.data_array @@ -66,6 +68,7 @@ func decode(data: PackedByteArray, properties: Array[PropertyEntry]) -> _Propert buffer.data_array = data var packet_version := buffer.get_u8() + # TODO: Extract schema versioning into shared code to avoid duplication if packet_version != _version: if not _has_received: # This is the first time we receive data @@ -80,13 +83,15 @@ func decode(data: PackedByteArray, properties: Array[PropertyEntry]) -> _Propert while buffer.get_available_bytes() > 0: var property_idx := buffer.get_u8() - var property_value := buffer.get_var() + if not _property_indexes.has_key(property_idx): _logger.warning("Received unknown property index %d, ignoring!", [property_idx]) - continue + break + + var property_path := _property_indexes.get_by_key(property_idx) + var value := _schema_handler.decode(property_path, buffer) - var property_entry := _property_indexes.get_by_key(property_idx) - result.set_value(property_entry, property_value) + result.set_value(property_path, value) return result diff --git a/addons/netfox/encoder/redundant-history-encoder.gd b/addons/netfox/encoder/redundant-history-encoder.gd index 8ba510ec..5da2e0a0 100644 --- a/addons/netfox/encoder/redundant-history-encoder.gd +++ b/addons/netfox/encoder/redundant-history-encoder.gd @@ -8,20 +8,24 @@ var redundancy: int = 4: var _history: _PropertyHistoryBuffer var _properties: Array[PropertyEntry] var _property_cache: PropertyCache +var _schema_handler: _NetworkSchema var _version := 0 var _has_received := false var _logger := NetfoxLogger._for_netfox("RedundantHistoryEncoder") +func _init(p_history: _PropertyHistoryBuffer, p_property_cache: PropertyCache, p_schema_handler: _NetworkSchema): + _history = p_history + _property_cache = p_property_cache + _schema_handler = p_schema_handler + func get_redundancy() -> int: return redundancy func set_redundancy(p_redundancy: int): if p_redundancy <= 0: - _logger.warning( - "Attempting to set redundancy to %d, which would send no data!", [p_redundancy] - ) + _logger.warning("Attempting to set redundancy to %d, which would send no data!", [p_redundancy]) return redundancy = p_redundancy @@ -31,10 +35,12 @@ func set_properties(properties: Array[PropertyEntry]) -> void: _version = (_version + 1) % 256 _properties = properties.duplicate() -func encode(tick: int, properties: Array[PropertyEntry]) -> Array: +func encode(tick: int, properties: Array[PropertyEntry]) -> PackedByteArray: if _history.is_empty(): - return [] - var data := [] + return PackedByteArray() + + var buffer := StreamPeerBuffer.new() + buffer.put_u8(_version) for i in range(mini(redundancy, _history.size())): var offset_tick := tick - i @@ -43,16 +49,21 @@ func encode(tick: int, properties: Array[PropertyEntry]) -> Array: var snapshot := _history.get_snapshot(offset_tick) for property in properties: - data.append(snapshot.get_value(property.to_string())) + var path := property.to_string() + var value := snapshot.get_value(path) + _schema_handler.encode(path, value, buffer) - data.append(_version) - return data + return buffer.data_array -func decode(data: Array, properties: Array[PropertyEntry]) -> Array[_PropertySnapshot]: - if data.is_empty() or properties.is_empty(): - return [] - - var packet_version := data.pop_back() as int +func decode(data: PackedByteArray, properties: Array[PropertyEntry]) -> Array[_PropertySnapshot]: + var result: Array[_PropertySnapshot] = [] + + if data.is_empty(): + return result + + var buffer := StreamPeerBuffer.new() + buffer.data_array = data + var packet_version := buffer.get_u8() if packet_version != _version: if not _has_received: @@ -61,21 +72,24 @@ func decode(data: Array, properties: Array[PropertyEntry]) -> Array[_PropertySna else: # Version mismatch, can't parse _logger.warning("Version mismatch! own: %d, received: %s", [_version, packet_version]) - return [] - - var result: Array[_PropertySnapshot] = [] - var redundancy := data.size() / properties.size() - result.assign(range(redundancy) - .map(func(__): return _PropertySnapshot.new()) - ) + return result + + _has_received = true - for i in range(data.size()): - var offset_idx := i / properties.size() - var prop_idx := i % properties.size() + while buffer.get_available_bytes() > 0: + var snapshot = _PropertySnapshot.new() - result[offset_idx].set_value(properties[prop_idx].to_string(), data[i]) + for property in properties: + # Stop if we run out of data mid-snapshot + if buffer.get_available_bytes() == 0: + break - _has_received = true + var path := property.to_string() + var value := _schema_handler.decode(path, buffer) + + snapshot.set_value(path, value) + + result.append(snapshot) return result @@ -109,8 +123,3 @@ func apply(tick: int, snapshots: Array[_PropertySnapshot], sender: int = 0) -> i earliest_new_tick = offset_tick return earliest_new_tick - - -func _init(p_history: _PropertyHistoryBuffer, p_property_cache: PropertyCache): - _history = p_history - _property_cache = p_property_cache diff --git a/addons/netfox/encoder/snapshot-history-encoder.gd b/addons/netfox/encoder/snapshot-history-encoder.gd index 24e6ae18..db796758 100644 --- a/addons/netfox/encoder/snapshot-history-encoder.gd +++ b/addons/netfox/encoder/snapshot-history-encoder.gd @@ -4,52 +4,63 @@ class_name _SnapshotHistoryEncoder var _history: _PropertyHistoryBuffer var _property_cache: PropertyCache var _properties: Array[PropertyEntry] +var _schema_handler: _NetworkSchema var _version := -1 var _has_received := false static var _logger := NetfoxLogger._for_netfox("_SnapshotHistoryEncoder") -func _init(p_history: _PropertyHistoryBuffer, p_property_cache: PropertyCache): +func _init(p_history: _PropertyHistoryBuffer, p_property_cache: PropertyCache, p_schema_handler: _NetworkSchema) -> void: _history = p_history _property_cache = p_property_cache + _schema_handler = p_schema_handler func set_properties(properties: Array[PropertyEntry]) -> void: if _properties != properties: _version = (_version + 1) % 256 _properties = properties.duplicate() -func encode(tick: int, properties: Array[PropertyEntry]) -> Array: +func encode(tick: int, properties: Array[PropertyEntry]) -> PackedByteArray: var snapshot := _history.get_snapshot(tick) - var data := [] - data.resize(properties.size()) + var buffer := StreamPeerBuffer.new() + + buffer.put_u8(_version) - for i in range(properties.size()): - data[i] = snapshot.get_value(properties[i].to_string()) - data.append(_version) + for property in properties: + var path: String = property.to_string() + var value = snapshot.get_value(path) + _schema_handler.encode(path, value, buffer) - return data + return buffer.data_array -func decode(data: Array, properties: Array[PropertyEntry]) -> _PropertySnapshot: +func decode(data: PackedByteArray, properties: Array[PropertyEntry]) -> _PropertySnapshot: var result := _PropertySnapshot.new() - var packet_version = data.pop_back() + + if data.is_empty(): + return result + + var buffer := StreamPeerBuffer.new() + buffer.data_array = data + var packet_version: int = buffer.get_u8() if packet_version != _version: if not _has_received: - # First packet, assume version is OK _version = packet_version else: - # Version mismatch, can't parse _logger.warning("Version mismatch! own: %d, received: %s", [_version, packet_version]) return result - if properties.size() != data.size(): - _logger.warning("Received snapshot with %d entries, with %d known - parsing as much as possible", [data.size(), properties.size()]) + _has_received = true - for i in range(0, mini(data.size(), properties.size())): - result.set_value(properties[i].to_string(), data[i]) + for property in properties: + if buffer.get_available_bytes() == 0: + _logger.warning("Received snapshot with %d properties, expected %d!", [result.size(), properties.size()]) + break - _has_received = true + var path := property.to_string() + var value := _schema_handler.decode(path, buffer) + result.set_value(path, value) return result diff --git a/addons/netfox/plugin.cfg b/addons/netfox/plugin.cfg index 8d27a9ff..3baed0ed 100644 --- a/addons/netfox/plugin.cfg +++ b/addons/netfox/plugin.cfg @@ -3,5 +3,5 @@ name="netfox" description="Shared internals for netfox addons" author="Tamas Galffy and contributors" -version="1.35.3" +version="1.36.0" script="netfox.gd" diff --git a/addons/netfox/rollback/composite/rollback-history-transmitter.gd b/addons/netfox/rollback/composite/rollback-history-transmitter.gd index c341cb92..5d1a1fca 100644 --- a/addons/netfox/rollback/composite/rollback-history-transmitter.gd +++ b/addons/netfox/rollback/composite/rollback-history-transmitter.gd @@ -17,6 +17,8 @@ var _input_property_config: _PropertyConfig var _property_cache: PropertyCache var _skipset: _Set +var _schema_handler: _NetworkSchema + # Collaborators var _input_encoder: _RedundantHistoryEncoder var _full_state_encoder: _SnapshotHistoryEncoder @@ -58,7 +60,8 @@ func configure( p_state_property_config: _PropertyConfig, p_input_property_config: _PropertyConfig, p_visibility_filter: PeerVisibilityFilter, p_property_cache: PropertyCache, - p_skipset: _Set + p_skipset: _Set, + p_schema_handler: _NetworkSchema, ) -> void: _state_history = p_state_history _input_history = p_input_history @@ -67,13 +70,13 @@ func configure( _visibility_filter = p_visibility_filter _property_cache = p_property_cache _skipset = p_skipset + _schema_handler = p_schema_handler - _input_encoder = _RedundantHistoryEncoder.new(_input_history, _property_cache) - _full_state_encoder = _SnapshotHistoryEncoder.new(_state_history, _property_cache) - _diff_state_encoder = _DiffHistoryEncoder.new(_state_history, _property_cache) + _input_encoder = _RedundantHistoryEncoder.new(_input_history, _property_cache, _schema_handler) + _full_state_encoder = _SnapshotHistoryEncoder.new(_state_history, _property_cache, _schema_handler) + _diff_state_encoder = _DiffHistoryEncoder.new(_state_history, _property_cache, _schema_handler) _is_initialized = true - reset() func reset() -> void: @@ -203,7 +206,7 @@ func _notification(what): NetworkRollback.free_input_submission_data_for(root) @rpc("any_peer", "unreliable", "call_remote") -func _submit_input(tick: int, data: Array) -> void: +func _submit_input(tick: int, data: PackedByteArray) -> void: if not _is_initialized: # Settings not processed yet return @@ -217,7 +220,7 @@ func _submit_input(tick: int, data: Array) -> void: # `serialized_state` is a serialized _PropertySnapshot @rpc("any_peer", "unreliable_ordered", "call_remote") -func _submit_full_state(data: Array, tick: int) -> void: +func _submit_full_state(data: PackedByteArray, tick: int) -> void: if not _is_initialized: # Settings not processed yet return diff --git a/addons/netfox/rollback/netfox_schemas.gd.gd.uid b/addons/netfox/rollback/netfox_schemas.gd.gd.uid new file mode 100644 index 00000000..edc7e281 --- /dev/null +++ b/addons/netfox/rollback/netfox_schemas.gd.gd.uid @@ -0,0 +1 @@ +uid://bc61m7m28c1k7 diff --git a/addons/netfox/rollback/rollback-synchronizer.gd b/addons/netfox/rollback/rollback-synchronizer.gd index 06bb435a..b5aa5eed 100644 --- a/addons/netfox/rollback/rollback-synchronizer.gd +++ b/addons/netfox/rollback/rollback-synchronizer.gd @@ -77,6 +77,8 @@ var _skipset: _Set = _Set.new() var _properties_dirty: bool = false +var _schema := _NetworkSchema.new({}) + var _property_cache := PropertyCache.new(root) var _freshness_store := RollbackFreshnessStore.new() @@ -95,7 +97,7 @@ var _history_transmitter: _RollbackHistoryTransmitter var _history_recorder: _RollbackHistoryRecorder ## Process settings. -## +## [br][br] ## Call this after any change to configuration. Updates based on authority too ## ( calls process_authority ). func process_settings() -> void: @@ -116,11 +118,11 @@ func process_settings() -> void: _nodes.erase(self) _history_transmitter.sync_settings(root, enable_input_broadcast, full_state_interval, diff_ack_interval) - _history_transmitter.configure(_states, _inputs, _state_property_config, _input_property_config, visibility_filter, _property_cache, _skipset) + _history_transmitter.configure(_states, _inputs, _state_property_config, _input_property_config, visibility_filter, _property_cache, _skipset, _schema) _history_recorder.configure(_states, _inputs, _state_property_config, _input_property_config, _property_cache, _skipset) ## Process settings based on authority. -## +## [br][br] ## Call this whenever the authority of any of the nodes managed by ## RollbackSynchronizer changes. Make sure to do this at the same time on all ## peers. @@ -159,8 +161,32 @@ func add_input(node: Variant, property: String) -> void: _properties_dirty = true _reprocess_settings.call_deferred() +## Set the schema for transmitting properties over the network. +## [br][br] +## The [param schema] must be a dictionary, with the keys being property path +## strings, and the values are the associated [NetworkSchemaSerializer] objects. +## Properties are interpreted relative to the [member root] node. The schema can +## contain both state and input properties. Properties not specified in the +## schema will use a generic fallback serializer. By using the right serializer +## for the right property, bandwidth usage can be lowered. +## [br][br] +## See [NetworkSchemas] for many common serializers. +## [br][br] +## Example: +## [codeblock] +## rollback_synchronizer.set_schema({ +## ":transform": NetworkSchemas.transform3f32(), +## ":velocity": NetworkSchemas.vec3f32(), +## "Input:movement": NetworkSchemas.vec3f32() +## }) +## [/codeblock] +func set_schema(schema: Dictionary) -> void: + _schema = _NetworkSchema.new(schema) + _properties_dirty = true + _reprocess_settings.call_deferred() + ## Check if input is available for the current tick. -## +## [br][br] ## This input is not always current, it may be from multiple ticks ago. ## [br][br] ## Returns true if input is available. @@ -168,7 +194,7 @@ func has_input() -> bool: return _has_input ## Get the age of currently available input in ticks. -## +## [br][br] ## The available input may be from the current tick, or from multiple ticks ago. ## This number of tick is the input's age. ## [br][br] @@ -181,7 +207,7 @@ func get_input_age() -> int: return -1 ## Check if the current tick is predicted. -## +## [br][br] ## A tick becomes predicted if there's no up-to-date input available. It will be ## simulated and recorded, but will not be broadcast, nor considered ## authoritative. @@ -189,7 +215,7 @@ func is_predicting() -> bool: return _is_predicted_tick ## Ignore a node's prediction for the current rollback tick. -## +## [br][br] ## Call this when the input is too old to base predictions on. This call is ## ignored if [member enable_prediction] is false. func ignore_prediction(node: Node) -> void: diff --git a/addons/netfox/schemas/network-schema-serializer.gd b/addons/netfox/schemas/network-schema-serializer.gd new file mode 100644 index 00000000..a8737891 --- /dev/null +++ b/addons/netfox/schemas/network-schema-serializer.gd @@ -0,0 +1,21 @@ +extends RefCounted +class_name NetworkSchemaSerializer + +## Base class for serializers, to use with [NetworkSchemas] +## +## Each serializer must be able to encode and decode values passed to it. +## Data is stored in [StreamPeerBuffer] objects. +## [br][br] +## To implement a custom serializer, extend this class and pass an instance of +## it in place of a NetworkSchemaSerializer, for example to [method +## RollbackSynchronizer.set_schema]. +## +## @tutorial(Network schemas): https://foxssake.github.io/netfox/latest/netfox/guides/network-schemas/ + +## Encode [param value] into [param buffer] +func encode(value: Variant, buffer: StreamPeerBuffer) -> void: + pass + +## Decode a value from [param buffer] and return it +func decode(buffer: StreamPeerBuffer) -> Variant: + return null diff --git a/addons/netfox/schemas/network-schema-serializer.gd.uid b/addons/netfox/schemas/network-schema-serializer.gd.uid new file mode 100644 index 00000000..decbb997 --- /dev/null +++ b/addons/netfox/schemas/network-schema-serializer.gd.uid @@ -0,0 +1 @@ +uid://btmpightc21cy diff --git a/addons/netfox/schemas/network-schema.gd b/addons/netfox/schemas/network-schema.gd new file mode 100644 index 00000000..f5fa24a5 --- /dev/null +++ b/addons/netfox/schemas/network-schema.gd @@ -0,0 +1,15 @@ +extends RefCounted +class_name _NetworkSchema + +var _serializers: Dictionary +var _fallback: NetworkSchemaSerializer + +func _init(serializers: Dictionary, fallback: NetworkSchemaSerializer = NetworkSchemas.variant()) -> void: + _serializers = serializers + _fallback = fallback + +func encode(path: String, value: Variant, buffer: StreamPeerBuffer) -> void: + (_serializers.get(path, _fallback) as NetworkSchemaSerializer).encode(value, buffer) + +func decode(path: String, buffer: StreamPeerBuffer) -> Variant: + return (_serializers.get(path, _fallback) as NetworkSchemaSerializer).decode(buffer) diff --git a/addons/netfox/schemas/network-schema.gd.uid b/addons/netfox/schemas/network-schema.gd.uid new file mode 100644 index 00000000..09fc7e01 --- /dev/null +++ b/addons/netfox/schemas/network-schema.gd.uid @@ -0,0 +1 @@ +uid://cmqrteshb5sop diff --git a/addons/netfox/schemas/network-schemas.gd b/addons/netfox/schemas/network-schemas.gd new file mode 100644 index 00000000..a3857cad --- /dev/null +++ b/addons/netfox/schemas/network-schemas.gd @@ -0,0 +1,769 @@ +extends Object +class_name NetworkSchemas + +## Provides various schema serializers +## +## While some method names are abbreviated, they use a few naming schemes. For +## example: [br][br] +## [method uint16] - unsigned integer, 16 bits[br] +## [method vec2t] - [Vector2], component of specified [i]type[/i][br] +## [method vec3f32] - [Vector3], each component as a [method float32][br] +## [br] +## To handle collections, see [method array_of] and [method dictionary]. +## +## @tutorial(Network schemas): https://foxssake.github.io/netfox/latest/netfox/guides/network-schemas/ + +## Serialize any data type supported by [method @GlobalScope.var_to_bytes]. +## [br][br] +## Final size depends on the value. +static func variant() -> NetworkSchemaSerializer: + return _VariantSerializer.new() + +## Serialize strings in UTF-8 encoding. +## [br][br] +## Final size depends on the string, the string itself is zero-terminated. +static func string() -> NetworkSchemaSerializer: + return _StringSerializer.new() + +## Serialize booleans as 8 bits. +## [br][br] +## Final size is 1 byte. +static func bool8() -> NetworkSchemaSerializer: + return _BoolSerializer.new() + +## Serialize unsigned integers as 8 bits. +## [br][br] +## Final size is 1 byte. +static func uint8() -> NetworkSchemaSerializer: + return _Uint8Serializer.new() + +## Serialize unsigned integers as 16 bits. +## [br][br] +## Final size is 2 bytes. +static func uint16() -> NetworkSchemaSerializer: + return _Uint16Serializer.new() + +## Serialize unsigned integers as 32 bits. +## [br][br] +## Final size is 4 bytes. +static func uint32() -> NetworkSchemaSerializer: + return _Uint32Serializer.new() + +## Serialize unsigned integers as 64 bits. +## [br][br] +## Final size is 8 bytes. +static func uint64() -> NetworkSchemaSerializer: + return _Uint64Serializer.new() + +## Serialize signed integers as 8 bits. +## [br][br] +## Final size is 1 byte. +static func int8() -> NetworkSchemaSerializer: + return _Int8Serializer.new() + +## Serialize signed integers as 16 bits. +## [br][br] +## Final size is 2 bytes. +static func int16() -> NetworkSchemaSerializer: + return _Int16Serializer.new() + +## Serialize signed integers as 32 bits. +## [br][br] +## Final size is 4 bytes. +static func int32() -> NetworkSchemaSerializer: + return _Int32Serializer.new() + +## Serialize signed integers as 64 bits. +## [br][br] +## Final size is 8 bytes. +static func int64() -> NetworkSchemaSerializer: + return _Int64Serializer.new() + +## Serialize floats in half-precision, as 16 bits. +## [br][br] +## This is only supported in Godot 4.4 and up, earlier versions fall back to +## [method float32]. +## [br][br] +## Final size is 2 bytes, 4 if using fallback. +static func float16() -> NetworkSchemaSerializer: + return _Float16Serializer.new() + +## Serialize floats in single-precision, as 32 bits. +## [br][br] +## Final size is 4 bytes. +static func float32() -> NetworkSchemaSerializer: + return _Float32Serializer.new() + +## Serialize floats in double-precision, as 64 bits. +## [br][br] +## Final size is 8 bytes. +static func float64() -> NetworkSchemaSerializer: + return _Float64Serializer.new() + +## Serialize signed fractions in the [code][-1.0, +1.0][/code] range as 8 bits. +## [br][br] +## Final size is 1 byte. +static func sfrac8() -> NetworkSchemaSerializer: + return _QuantizingSerializer.new(uint8(), -1., 1., 0, 0xFF) + +## Serialize signed fractions in the [code][-1.0, +1.0][/code] range as 16 bits. +## [br][br] +## Final size is 2 bytes. +static func sfrac16() -> NetworkSchemaSerializer: + return _QuantizingSerializer.new(uint16(), -1., 1., 0, 0xFFFF) + +## Serialize signed fractions in the [code][-1.0, +1.0][/code] range as 32 bits. +## [br][br] +## Final size is 4 bytes. +static func sfrac32() -> NetworkSchemaSerializer: + return _QuantizingSerializer.new(uint32(), -1., 1., 0, 0xFFFFFFFF) + +## Serialize signed fractions in the [code][0.0, 1.0][/code] range as 8 bits. +## [br][br] +## Final size is 1 byte. +static func ufrac8() -> NetworkSchemaSerializer: + return _QuantizingSerializer.new(uint8(), 0., 1., 0, 0xFF) + +## Serialize signed fractions in the [code][0.0, 1.0][/code] range as 16 bits. +## [br][br] +## Final size is 2 bytes. +static func ufrac16() -> NetworkSchemaSerializer: + return _QuantizingSerializer.new(uint16(), 0., 1., 0, 0xFFFF) + +## Serialize signed fractions in the [code][0.0, 1.0][/code] range as 32 bits. +## [br][br] +## Final size is 4 bytes. +static func ufrac32() -> NetworkSchemaSerializer: + return _QuantizingSerializer.new(uint32(), 0., 1., 0, 0xFFFFFFFF) + +## Serialize degrees as 8 bits. The value will always decode to the +## [code][0.0, 360.0)[/code] range. +## [br][br] +## Final size is 1 byte. +static func degrees8() -> NetworkSchemaSerializer: + return _ModuloSerializer.new(uint8(), 360., 0xFF) + +## Serialize degrees as 16 bits. The value will always decode to the +## [code][0.0, 360.0)[/code] range. +## [br][br] +## Final size is 2 bytes. +static func degrees16() -> NetworkSchemaSerializer: + return _ModuloSerializer.new(uint16(), 360., 0xFFFF) + +## Serialize degrees as 32 bits. The value will always decode to the +## [code][0.0, 360.0)[/code] range. +## [br][br] +## Final size is 4 bytes. +static func degrees32() -> NetworkSchemaSerializer: + return _ModuloSerializer.new(uint32(), 360., 0xFFFFFFFF) + +## Serialize radians as 8 bits. The value will always decode to the +## [code][0.0, TAU)[/code] range. +## [br][br] +## Final size is 1 byte. +static func radians8() -> NetworkSchemaSerializer: + return _ModuloSerializer.new(uint8(), TAU, 0xFF) + +## Serialize radians as 16 bits. The value will always decode to the +## [code][0.0, TAU)[/code] range. +## [br][br] +## Final size is 2 bytes. +static func radians16() -> NetworkSchemaSerializer: + return _ModuloSerializer.new(uint16(), TAU, 0xFFFF) + +## Serialize radians as 32 bits. The value will always decode to the +## [code][0.0, TAU)[/code] range. +## [br][br] +## Final size is 4 bytes. +static func radians32() -> NetworkSchemaSerializer: + return _ModuloSerializer.new(uint32(), TAU, 0xFFFFFFFF) + +## Serialize [Vector2] objects, using [param component_serializer] to +## serialize each component of the vector. +## [br][br] +## Serializes 2 components, size depends on the [param component_serializer]. +static func vec2t(component_serializer: NetworkSchemaSerializer) -> NetworkSchemaSerializer: + return _GenericVec2Serializer.new(component_serializer) + +## Serialize [Vector2] objects, with each component being a half-precision +## float. +## [br][br] +## This is only supported in Godot 4.4 and up. Earlier versions fall back to +## [method vec2f32]. +## [br][br] +## Final size is 4 bytes, 8 if using fallback. +static func vec2f16() -> NetworkSchemaSerializer: + return vec2t(float16()) + +## Serialize [Vector2] objects, with each component being a single-precision +## float. +## [br][br] +## Final size is 8 bytes. +static func vec2f32() -> NetworkSchemaSerializer: + return vec2t(float32()) + +## Serialize [Vector2] objects, with each component being a double-precision +## float. +## [br][br] +## Final size is 16 bytes. +static func vec2f64() -> NetworkSchemaSerializer: + return vec2t(float64()) + +## Serialize [Vector3] objects, using [param component_serializer] to +## serialize each component of the vector. +## [br][br] +## Serializes 3 components, size depends on the [param component_serializer]. +static func vec3t(component_serializer: NetworkSchemaSerializer) -> NetworkSchemaSerializer: + return _GenericVec3Serializer.new(component_serializer) + +## Serialize [Vector3] objects, with each component being a half-precision +## float. +## [br][br] +## This is only supported in Godot 4.4 and up. Earlier versions fall back to +## [method vec3f32]. +## [br][br] +## Final size is 6 bytes, 12 if using fallback. +static func vec3f16() -> NetworkSchemaSerializer: + return vec3t(float16()) + +## Serialize [Vector3] objects, with each component being a double-precision +## float. +## [br][br] +## Final size is 12 bytes. +static func vec3f32() -> NetworkSchemaSerializer: + return vec3t(float32()) + +## Serialize [Vector3] objects, with each component being a double-precision +## float. +## [br][br] +## Final size is 24 bytes. +static func vec3f64() -> NetworkSchemaSerializer: + return vec3t(float64()) + +## Serialize [Vector4] objects, using [param component_serializer] to +## serialize each component of the vector. +## [br][br] +## Serializes 4 components, size depends on the [param component_serializer]. +static func vec4t(component_serializer: NetworkSchemaSerializer) -> NetworkSchemaSerializer: + return _GenericVec4Serializer.new(component_serializer) + +## Serialize [Vector4] objects, with each component being a half-precision +## float. +## [br][br] +## This is only supported in Godot 4.4 and up. Earlier versions fall back to +## [method vec4f32]. +## [br][br] +## Final size is 8 bytes, 16 if using fallback. +static func vec4f16() -> NetworkSchemaSerializer: + return vec4t(float16()) + +## Serialize [Vector4] objects, with each component being a double-precision +## float. +## [br][br] +## Final size is 16 bytes. +static func vec4f32() -> NetworkSchemaSerializer: + return vec4t(float32()) + +## Serialize [Vector4] objects, with each component being a double-precision +## float. +## [br][br] +## Final size is 32 bytes. +static func vec4f64() -> NetworkSchemaSerializer: + return vec4t(float64()) + +# Normals +## Serialize normalized [Vector2] objects, using [param component_serializer] to +## serialize each component of the vector. +## [br][br] +## Serializes 1 component, size depends on the [param component_serializer]. +static func normal2t(component_serializer: NetworkSchemaSerializer) -> NetworkSchemaSerializer: + return _Normal2Serializer.new(component_serializer) + +## Serialize normalized [Vector2] objects, with each component being a +## half-precision float. +## [br][br] +## This is only supported in Godot 4.4 and up. Earlier versions fall back to +## [method normal2f32]. +## [br][br] +## Final size is 2 bytes, 4 if using fallback. +static func normal2f16() -> NetworkSchemaSerializer: + return normal2t(float16()) + +## Serialize normalized [Vector2] objects, with each component being a +## single-precision float. +## [br][br] +## Final size is 4 bytes. +static func normal2f32() -> NetworkSchemaSerializer: + return normal2t(float32()) + +## Serialize normalized [Vector2] objects, with each component being a +## double-precision float. +## [br][br] +## Final size is 8 bytes. +static func normal2f64() -> NetworkSchemaSerializer: + return normal2t(float64()) + +## Serialize normalized [Vector3] objects, using [param component_serializer] to +## serialize each component of the vector. +## [br][br] +## Serializes 2 components, size depends on the [param component_serializer]. +static func normal3t(component_serializer: NetworkSchemaSerializer) -> NetworkSchemaSerializer: + return _Normal3Serializer.new(component_serializer) + +## Serialize normalized [Vector3] objects, with each component being a +## half-precision float. +## [br][br] +## This is only supported in Godot 4.4 and up. Earlier versions fall back to +## [method normal3f32]. +## [br][br] +## Final size is 4 bytes, 8 if using fallback. +static func normal3f16() -> NetworkSchemaSerializer: + return normal3t(float16()) + +## Serialize normalized [Vector3] objects, with each component being a +## single-precision float. +## [br][br] +## Final size is 8 bytes. +static func normal3f32() -> NetworkSchemaSerializer: + return normal3t(float32()) + +## Serialize normalized [Vector3] objects, with each component being a +## double-precision float. +## [br][br] +## Final size is 16 bytes. +static func normal3f64() -> NetworkSchemaSerializer: + return normal3t(float64()) + +# Quaternion +## Serialize [Quaternion] objects, using [param component_serializer] to +## serialize each component of the quaternion. +## [br][br] +## Serializes 4 components, size depends on the [param component_serializer]. +static func quatt(component_serializer: NetworkSchemaSerializer) -> NetworkSchemaSerializer: + return _GenericQuaternionSerializer.new(component_serializer) + +## Serialize [Quaternion] objects, with each component being a half-precision +## float. +## [br][br] +## This is only supported in Godot 4.4 and up. Earlier versions fall back to +## [method quat32f]. +## [br][br] +## Final size is 8 bytes, 16 if using fallback. +static func quatf16() -> NetworkSchemaSerializer: + return quatt(float16()) + +## Serialize [Quaternion] objects, with each component being a single-precision +## float. +## [br][br] +## Final size is 16 bytes. +static func quatf32() -> NetworkSchemaSerializer: + return quatt(float32()) + +## Serialize [Quaternion] objects, with each component being a double-precision +## float. +## [br][br] +## Final size is 32 bytes. +static func quatf64() -> NetworkSchemaSerializer: + return quatt(float64()) + +# Transforms +## Serialize [Transform2D] objects, using [param component_serializer] to +## serialize each component of the transform. +## [br][br] +## Serializes a 2x3 matrix in 6 components, final size depends on [param +## component_serializer]. +static func transform2t(component_serializer: NetworkSchemaSerializer) -> NetworkSchemaSerializer: + return _GenericTransform2DSerializer.new(component_serializer) + +## Serialize [Transform2D] objects, with each component being a half-precision +## float. +## [br][br] +## This is only supported in Godot 4.4 and up. Earlier versions fall back to +## [method transform2f32]. +## [br][br] +## Final size is 12 bytes, 24 if using fallback. +static func transform2f16() -> NetworkSchemaSerializer: + return transform2t(float16()) + +## Serialize [Transform2D] objects, with each component being a single-precision +## float. +## [br][br] +## Final size is 24 bytes. +static func transform2f32() -> NetworkSchemaSerializer: + return transform2t(float32()) + +## Serialize [Transform2D] objects, with each component being a double-precision +## float. +## [br][br] +## Final size is 48 bytes. +static func transform2f64() -> NetworkSchemaSerializer: + return transform2t(float64()) + +## Serialize [Transform3D] objects, using [param component_serializer] to +## serialize each component of the transform. +## [br][br] +## Serializes a 3x4 matrix in 12 components, final size depends on [param +## component_serializer]. +static func transform3t(component_serializer: NetworkSchemaSerializer) -> NetworkSchemaSerializer: + return _GenericTransform3DSerializer.new(component_serializer) + +## Serialize [Transform3D] objects, with each component being a half-precision +## float. +## [br][br] +## This is only supported in Godot 4.4 and up. Earlier versions fall back to +## [method transform3f32]. +## [br][br] +## Final size is 24 bytes, 48 if using fallback. +static func transform3f16() -> NetworkSchemaSerializer: + return transform3t(float16()) + +## Serialize [Transform3D] objects, with each component being a single-precision +## float. +## [br][br] +## Final size is 48 bytes. +static func transform3f32() -> NetworkSchemaSerializer: + return transform3t(float32()) + +## Serialize [Transform2D] objects, with each component being a double-precision +## float. +## [br][br] +## Final size is 96 bytes. +static func transform3f64() -> NetworkSchemaSerializer: + return transform3t(float64()) + +# Collections + +## Serialize homogenoeous arrays, using [param item_serializer] to +## serialize each item, and [param size_serializer] to serialize the array's +## size. +## [br][br] +## To serialize heterogenoeous arrays, use [method variant] as the item +## serializer. +## [br][br] +## Final size is [code]sizeof(size_serializer) + array.size() * sizeof(item_serializer)[/code] +static func array_of(item_serializer: NetworkSchemaSerializer = variant(), size_serializer: NetworkSchemaSerializer = uint16()) -> NetworkSchemaSerializer: + return _ArraySerializer.new(item_serializer, size_serializer) + +## Serialize homogenoeous dictionaries, using [param key_serialize] and +## [param value_serializer] to serialize key-value pairs, and +## [param size_serializer] to serialize the number of entries. +## [br][br] +## If either the keys or values are not homogenoeous, use [method variant]. +## [br][br] +## Final size is [code]sizeof(size_serializer) + dictionary.size() * (sizeof(key_serializer) + sizeof(value_serializer))[/code] +static func dictionary(key_serializer: NetworkSchemaSerializer = variant(), + value_serializer: NetworkSchemaSerializer = variant(), + size_serializer: NetworkSchemaSerializer = uint16()) -> NetworkSchemaSerializer: + return _DictionarySerializer.new(key_serializer, value_serializer, size_serializer) + +# Serializer classes + +class _VariantSerializer extends NetworkSchemaSerializer: + func encode(v: Variant, b: StreamPeerBuffer) -> void: + b.put_var(v, false) + + func decode(b: StreamPeerBuffer) -> Variant: + return b.get_var(false) + +class _StringSerializer extends NetworkSchemaSerializer: + func encode(v: Variant, b: StreamPeerBuffer) -> void: + b.put_utf8_string(str(v)) + + func decode(b: StreamPeerBuffer) -> Variant: + return b.get_utf8_string() + +class _BoolSerializer extends NetworkSchemaSerializer: + func encode(v: Variant, b: StreamPeerBuffer) -> void: + b.put_u8(1 if v else 0) + + func decode(b: StreamPeerBuffer) -> Variant: + return b.get_u8() > 0 + +class _Uint8Serializer extends NetworkSchemaSerializer: + func encode(v: Variant, b: StreamPeerBuffer) -> void: b.put_u8(v) + func decode(b: StreamPeerBuffer) -> Variant: return b.get_u8() + +class _Uint16Serializer extends NetworkSchemaSerializer: + func encode(v: Variant, b: StreamPeerBuffer) -> void: b.put_u16(v) + func decode(b: StreamPeerBuffer) -> Variant: return b.get_u16() + +class _Uint32Serializer extends NetworkSchemaSerializer: + func encode(v: Variant, b: StreamPeerBuffer) -> void: b.put_u32(v) + func decode(b: StreamPeerBuffer) -> Variant: return b.get_u32() + +class _Uint64Serializer extends NetworkSchemaSerializer: + func encode(v: Variant, b: StreamPeerBuffer) -> void: b.put_u64(v) + func decode(b: StreamPeerBuffer) -> Variant: return b.get_u64() + +class _Int8Serializer extends NetworkSchemaSerializer: + func encode(v: Variant, b: StreamPeerBuffer) -> void: b.put_8(v) + func decode(b: StreamPeerBuffer) -> Variant: return b.get_8() + +class _Int16Serializer extends NetworkSchemaSerializer: + func encode(v: Variant, b: StreamPeerBuffer) -> void: b.put_16(v) + func decode(b: StreamPeerBuffer) -> Variant: return b.get_16() + +class _Int32Serializer extends NetworkSchemaSerializer: + func encode(v: Variant, b: StreamPeerBuffer) -> void: b.put_32(v) + func decode(b: StreamPeerBuffer) -> Variant: return b.get_32() + +class _Int64Serializer extends NetworkSchemaSerializer: + func encode(v: Variant, b: StreamPeerBuffer) -> void: b.put_64(v) + func decode(b: StreamPeerBuffer) -> Variant: return b.get_64() + +class _Float16Serializer extends NetworkSchemaSerializer: + func encode(v: Variant, b: StreamPeerBuffer) -> void: + if Engine.get_version_info().hex >= 0x040400: + b.put_half(v) + else: + b.put_float(v) + + func decode(b: StreamPeerBuffer) -> Variant: + if Engine.get_version_info().hex >= 0x040400: + return b.get_half() + else: + return b.get_float() + +class _Float32Serializer extends NetworkSchemaSerializer: + func encode(v: Variant, b: StreamPeerBuffer) -> void: b.put_float(v) + func decode(b: StreamPeerBuffer) -> Variant: return b.get_float() + +class _Float64Serializer extends NetworkSchemaSerializer: + func encode(v: Variant, b: StreamPeerBuffer) -> void: b.put_double(v) + func decode(b: StreamPeerBuffer) -> Variant: return b.get_double() + +class _GenericVec2Serializer extends NetworkSchemaSerializer: + var component: NetworkSchemaSerializer + + func _init(p_component: NetworkSchemaSerializer): + component = p_component + + func encode(v: Variant, b: StreamPeerBuffer) -> void: + component.encode(v.x, b) + component.encode(v.y, b) + + func decode(b: StreamPeerBuffer) -> Variant: + return Vector2(component.decode(b), component.decode(b)) + +class _GenericVec3Serializer extends NetworkSchemaSerializer: + var component: NetworkSchemaSerializer + + func _init(p_component: NetworkSchemaSerializer): + component = p_component + + func encode(v: Variant, b: StreamPeerBuffer) -> void: + component.encode(v.x, b) + component.encode(v.y, b) + component.encode(v.z, b) + + func decode(b: StreamPeerBuffer) -> Variant: + return Vector3( + component.decode(b), component.decode(b), component.decode(b) + ) + +class _Normal2Serializer extends NetworkSchemaSerializer: + var component: NetworkSchemaSerializer + + func _init(p_component: NetworkSchemaSerializer): + component = p_component + + func encode(v: Variant, b: StreamPeerBuffer) -> void: + component.encode((v as Vector2).angle(), b) + + func decode(b: StreamPeerBuffer) -> Variant: + return Vector2.RIGHT.rotated(component.decode(b)) + +class _Normal3Serializer extends NetworkSchemaSerializer: + var component: NetworkSchemaSerializer + + func _init(p_component: NetworkSchemaSerializer): + component = p_component + + func encode(v: Variant, b: StreamPeerBuffer) -> void: + var uv := (v as Vector3).octahedron_encode() + component.encode(uv.x, b) + component.encode(uv.y, b) + + func decode(b: StreamPeerBuffer) -> Variant: + return Vector3.octahedron_decode( + Vector2(component.decode(b), component.decode(b)) + ) + +class _GenericVec4Serializer extends NetworkSchemaSerializer: + var component: NetworkSchemaSerializer + + func _init(p_component: NetworkSchemaSerializer): + component = p_component + + func encode(v: Variant, b: StreamPeerBuffer) -> void: + component.encode(v.x, b) + component.encode(v.y, b) + component.encode(v.z, b) + component.encode(v.w, b) + + func decode(b: StreamPeerBuffer) -> Variant: + return Vector4( + component.decode(b), component.decode(b), component.decode(b), component.decode(b) + ) + +class _GenericQuaternionSerializer extends NetworkSchemaSerializer: + var component: NetworkSchemaSerializer + + func _init(p_component: NetworkSchemaSerializer): + component = p_component + + func encode(v: Variant, b: StreamPeerBuffer) -> void: + component.encode(v.x, b) + component.encode(v.y, b) + component.encode(v.z, b) + component.encode(v.w, b) + + func decode(b: StreamPeerBuffer) -> Variant: + return Quaternion( + component.decode(b), component.decode(b), component.decode(b), component.decode(b) + ) + +class _GenericTransform2DSerializer extends NetworkSchemaSerializer: + var component: NetworkSchemaSerializer + + func _init(p_component: NetworkSchemaSerializer): + component = p_component + + func encode(v: Variant, b: StreamPeerBuffer) -> void: + var t := v as Transform2D + + component.encode(t.x.x, b); component.encode(t.x.y, b) + component.encode(t.y.x, b); component.encode(t.y.y, b) + component.encode(t.origin.x, b); component.encode(t.origin.y, b) + + func decode(b: StreamPeerBuffer) -> Variant: + return Transform2D( + Vector2(component.decode(b), component.decode(b)), + Vector2(component.decode(b), component.decode(b)), + Vector2(component.decode(b), component.decode(b)), + ) + +class _GenericTransform3DSerializer extends NetworkSchemaSerializer: + var component: NetworkSchemaSerializer + + func _init(p_component: NetworkSchemaSerializer): + component = p_component + + func encode(v: Variant, b: StreamPeerBuffer) -> void: + var t := v as Transform3D + + component.encode(t.basis.x.x, b); component.encode(t.basis.x.y, b); component.encode(t.basis.x.z, b) + component.encode(t.basis.y.x, b); component.encode(t.basis.y.y, b); component.encode(t.basis.y.z, b) + component.encode(t.basis.z.x, b); component.encode(t.basis.z.y, b); component.encode(t.basis.z.z, b) + component.encode(t.origin.x, b); component.encode(t.origin.y, b); component.encode(t.origin.z, b) + + func decode(b: StreamPeerBuffer) -> Variant: + return Transform3D( + Basis( + Vector3(component.decode(b), component.decode(b), component.decode(b)), + Vector3(component.decode(b), component.decode(b), component.decode(b)), + Vector3(component.decode(b), component.decode(b), component.decode(b)), + ), + Vector3(component.decode(b), component.decode(b), component.decode(b)) + ) + +class _QuantizingSerializer extends NetworkSchemaSerializer: + var component: NetworkSchemaSerializer + var from_min: Variant + var from_max: Variant + var to_min: Variant + var to_max: Variant + + func _init( + p_component: NetworkSchemaSerializer, p_from_min: Variant, + p_from_max: Variant, p_to_min: Variant, p_to_max: Variant + ): + component = p_component + from_min = p_from_min + from_max = p_from_max + to_min = p_to_min + to_max = p_to_max + + func encode(v: Variant, b: StreamPeerBuffer) -> void: + var f := inverse_lerp(from_min, from_max, v) + var s := lerp(to_min, to_max, f) + component.encode(s, b) + + func decode(b: StreamPeerBuffer) -> Variant: + var s := component.decode(b) + var f := inverse_lerp(to_min, to_max, s) + return lerp(from_min, from_max, f) + +class _ModuloSerializer extends NetworkSchemaSerializer: + var component: NetworkSchemaSerializer + var value_max: Variant + var component_max: Variant + + func _init(p_component: NetworkSchemaSerializer, p_value_max: Variant, p_component_max: Variant): + component = p_component + value_max = p_value_max + component_max = p_component_max + + func encode(v: Variant, b: StreamPeerBuffer) -> void: + var f = fposmod(float(v), value_max) / value_max + var s = f * component_max + component.encode(s, b) + + func decode(b: StreamPeerBuffer) -> Variant: + var s = float(component.decode(b)) + return (s / component_max) * value_max + +class _ArraySerializer extends NetworkSchemaSerializer: + var component: NetworkSchemaSerializer + var size: NetworkSchemaSerializer + + func _init(p_component: NetworkSchemaSerializer, p_size: NetworkSchemaSerializer): + component = p_component + size = p_size + + func encode(v: Variant, b: StreamPeerBuffer) -> void: + var array := v as Array + + size.encode(array.size(), b) + for item in array: + component.encode(item, b) + + func decode(b: StreamPeerBuffer) -> Variant: + var array := [] + + var item_count = size.decode(b) + array.resize(item_count) + for i in item_count: + array[i] = component.decode(b) + + return array + +class _DictionarySerializer extends NetworkSchemaSerializer: + var key_serializer: NetworkSchemaSerializer + var value_serializer: NetworkSchemaSerializer + var size_serializer: NetworkSchemaSerializer + + func _init(p_key_serializer: NetworkSchemaSerializer, p_value_serializer: NetworkSchemaSerializer, p_size_serializer: NetworkSchemaSerializer): + key_serializer = p_key_serializer + value_serializer = p_value_serializer + size_serializer = p_size_serializer + + func encode(v: Variant, b: StreamPeerBuffer) -> void: + var dictionary := v as Dictionary + + size_serializer.encode(dictionary.size(), b) + for key in dictionary: + var value = dictionary[key] + key_serializer.encode(key, b) + value_serializer.encode(value, b) + + func decode(b: StreamPeerBuffer) -> Variant: + var dictionary := {} + + var size = size_serializer.decode(b) + for i in size: + var key = key_serializer.decode(b) + var value = value_serializer.decode(b) + dictionary[key] = value + + return dictionary diff --git a/addons/netfox/schemas/network-schemas.gd.uid b/addons/netfox/schemas/network-schemas.gd.uid new file mode 100644 index 00000000..1a217c5a --- /dev/null +++ b/addons/netfox/schemas/network-schemas.gd.uid @@ -0,0 +1 @@ +uid://byaxeh31f2i6m diff --git a/addons/netfox/serializers/netfox_serializer.gd.uid b/addons/netfox/serializers/netfox_serializer.gd.uid new file mode 100644 index 00000000..b3ec724b --- /dev/null +++ b/addons/netfox/serializers/netfox_serializer.gd.uid @@ -0,0 +1 @@ +uid://bf67e0hnmp70h diff --git a/addons/netfox/state-synchronizer.gd b/addons/netfox/state-synchronizer.gd index b66d3345..a750a07e 100644 --- a/addons/netfox/state-synchronizer.gd +++ b/addons/netfox/state-synchronizer.gd @@ -49,6 +49,8 @@ var _property_cache: PropertyCache var _property_config: _PropertyConfig = _PropertyConfig.new() var _properties_dirty: bool = false +var _schema: _NetworkSchema + var _state_history := _PropertyHistoryBuffer.new() # Collaborators @@ -71,8 +73,8 @@ func process_settings() -> void: _property_cache = PropertyCache.new(root) _property_config.set_properties_from_paths(properties, _property_cache) - _full_state_encoder = _SnapshotHistoryEncoder.new(_state_history, _property_cache) - _diff_state_encoder = _DiffHistoryEncoder.new(_state_history, _property_cache) + _full_state_encoder = _SnapshotHistoryEncoder.new(_state_history, _property_cache, _schema) + _diff_state_encoder = _DiffHistoryEncoder.new(_state_history, _property_cache, _schema) _diff_state_encoder.add_properties(_property_config.get_properties()) @@ -103,6 +105,28 @@ func add_state(node: Variant, property: String) -> void: _properties_dirty = true _reprocess_settings.call_deferred() +## Set the schema for transmitting properties over the network. +## [br][br] +## The [param schema] must be a dictionary, with the keys being property path +## strings, and the values are the associated [NetworkSchemaSerializer] objects. +## Properties are interpreted relative to the [member root] node. Properties not +## specified in the schema will use a generic fallback serializer. By using the +## right serializer for the right property, bandwidth usage can be lowered. +## [br][br] +## See [NetworkSchemas] for many common serializers. +## [br][br] +## Example: +## [codeblock] +## state_synchronizer.set_schema({ +## ":transform": NetworkSchemas.transform3f32(), +## ":velocity": NetworkSchemas.vec3f32() +## }) +## [/codeblock] +func set_schema(schema: Dictionary) -> void: + _schema = _NetworkSchema.new(schema) + _properties_dirty = true + _reprocess_settings.call_deferred() + func _notification(what) -> void: if what == NOTIFICATION_EDITOR_PRE_SAVE: update_configuration_warnings() @@ -218,7 +242,7 @@ func _send_full_state(tick: int, peer: int = 0) -> void: # `serialized_state` is a serialized _PropertySnapshot @rpc("any_peer", "unreliable_ordered", "call_remote") -func _submit_full_state(data: Array, tick: int) -> void: +func _submit_full_state(data: PackedByteArray, tick: int) -> void: if not _is_initialized: return var sender := multiplayer.get_remote_sender_id() diff --git a/docs/netfox/guides/network-schemas.md b/docs/netfox/guides/network-schemas.md new file mode 100644 index 00000000..3ee7bde3 --- /dev/null +++ b/docs/netfox/guides/network-schemas.md @@ -0,0 +1,169 @@ +# Network Schemas + +By default, *netfox* uses Godot's [Binary serialization API] to serialize data +before transmitting it over the network. This is designed to work under various +circumstances, with various data types, without knowing anything about them in +advance. + +However, during development, developers often have knowledge about the +individual properties, such as their type and possible range of values. In +addition, some values may be less important as others, and thus can accept some +loss of precision. + +Schemas enable developers to specify how each property should be serialized, +allowing them to use this knowledge to reduce packet sizes, and thus bandwidth +usage. + +## Lossless vs. lossy + +Most serializers are either lossless or lossy. This section gives a short +theoretical introduction on what each means and when are they useful. + +### Lossless compression + +When the same amount of information can be represented with less data ( bytes +), it is *lossless compression*. + +For example, to represent a 2D normal vector, we do not need to serialize both +of its component ( x, y ). Since we know the vector's length to be 1 by +definition, we can store the vector's angle compared to predetermined reference +vector. From that, we can completely reconstruct the original vector on +deserialization. + +Another example is when the range of values the vector can take on is much +smaller than its underlying datatype supports. For example, an inventory where +items can't stack beyond 99. Instead of defaulting to a 64 bit integer, it is +sufficient to serialize this data as a 8 bit integer. That is 1/8th of the +original data, while still perfectly representing the range of values needed. + +Lossless compression is an excellent tool, since the same information is kept, +but with less data usage. Unfortunately, lossless compression is not feasible +for every property. + +### Lossy compression + +If some information is lost when using less data ( bytes ) to represent a +value, it is *lossy compression*. This can be useful in cases where the benefit +of reduced packet size outweighs the drawbacks of lost information. + +For example, movement vectors for NPCs may be serialized as half precision +floats, instead of the default single precision. Since players don't directly +control NPC's, they won't notice any difference between their original input +and what was serialized. + +While lossy compression can be a useful tool, it is important to judge whether +the loss of information or precision does not detract too much from the game +experience. + +## Registering a schema + +Both [RollbackSynchronizer] and [StateSynchronizer] expose a `set_schema()` +method, that can be used to register the schema used for transmitting +properties over the network. This method takes a dictionary, with the keys +being property path strings, and the values being serializers: + +```gdscript + rollback_synchronizer.set_schema({ + ":transform": NetworkSchemas.transform3f32(), + ":velocity": NetworkSchemas.vec3f32(), + ":speed": NetworkSchemas.float32(), + ":mass": NetworkSchemas.float32(), + + "Input:movement": NetworkSchemas.vec3f32(), + "Input:aim": NetworkSchemas.vec3f32() + }) +``` + +## Built-in serializers + +`NetworkSchemas` provides many built-in serializers in the form of static +methods. Each supported type has multiple serializers for different sizes. + +While many serializers are usable as-is, there are some generic ones that take +other serializers as arguments. For example, `vec3t()` serializes a Vector3, +and using the serializer passed to it to save each component of the vector. +This way, `vec3t(float16())` will save 3 half-precision floats, ending up with +6 bytes of data, while `vec3t(float32())` will save 3 single-precision floats, +ending up with 12 bytes. + +!!!note + Many built-in serializers use half-precision floats. These are only + supported in Godot 4.4 and up. Earlier versions fall back to + single-precision floats. + + For example, `float16()` may fall back to `float32()`, `vec2f16()` to + `vec2f32()`, etc. + +### Algebraic types + +| Type | Methods | Size | +|-----------------------|---------------------------------------------------------|--------------------------------------------------------------------| +| Booleans | `bool8()` | 1 byte | +| Signed integers | `int8()`, `int16()`, `int32()`, `int64()` | 1, 2, 4, or 8 bytes | +| Unsigned integers | `uint8()`, `uint16()`, `uint32()`, `uint64()` | 1, 2, 4, or 8 bytes | +| Floats | `float16()`, `float32()`, `float64()` | 2, 4, or 8 bytes | +| Vector2 | `vec2f16()`, `vec2f32()`, `vec2f64()` | 4, 8, or 16 bytes | +| Vector3 | `vec3f16()`, `vec3f32()`, `vec3f64()` | 6, 8, or 24 bytes | +| Vector4 | `vec4f16()`, `vec4f32()`, `vec4f64()` | 8, 16, or 32 bytes | +| Quaternion | `quatf16()`, `quatf32()`, `quatf64()` | 8, 16, or 32 bytes | +| Transform2D | `transform2f16()`, `transform2f32()`, `transform2f64()` | 12, 24, or 48 bytes | +| Transform3D | `transform3f16()`, `transform3f32()`, `transform3f64()` | 24, 48, or 96 bytes | + +### Compressed types + +| Type | Methods | Size | +|-----------------------|---------------------------------------------------------|--------------------------------------------------------------------| +| Numbers in `[0, 1]` | `ufrac8()`, `ufrac16()`, `ufrac32()` | 1, 2, or 4 bytes | +| Numbers in `[-1, +1]` | `sfrac8()`, `sfrac16()`, `sfrac32()` | 1, 2, or 4 bytes | +| Degrees | `degrees8()`, `degrees16()`, `degrees32()` | 1, 2, or 4 bytes | +| Radians | `radians8()`, `radians16()`, `radians32()` | 1, 2, or 4 bytes | +| Normalized 2D vectors | `normal2f16()`, `normal2f32()`, `normal2f64()` | 2, 4, or 8 bytes | +| Normalized 3D vectors | `normal3f16()`, `normal3f32()`, `normal3f64()` | 4, 8, or 16 bytes | + +### Generic types + +| Type | Methods | Size | +|-----------------------|---------------------------------------------------------|--------------------------------------------------------------------| +| Vector2 | `vec2t()` | `2 * sizeof(component)` | +| Vector3 | `vec3t()` | `3 * sizeof(component)` | +| Vector4 | `vec4t()` | `4 * sizeof(component)` | +| Quaternion | `quatt()` | `4 * sizeof(component)` | +| Transform2D | `transform2t()` | `6 * sizeof(component)` | +| Transform3D | `transform3t()` | `12 * sizeof(component)` | +| Normalized Vector2 | `normal2t()` | `sizeof(component)` | +| Normalized Vector3 | `normal3t()` | `2 * sizeof(component)` | + +### Collections and others + +| Type | Methods | Size | +|-----------------------|---------------------------------------------------------|--------------------------------------------------------------------| +| Arrays | `array_of()` | `sizeof(size) + array.size() * sizeof(item)` | +| Dictionaries | `dictionary()` | `sizeof(size) + dictionary.size() * (sizeof(key) + sizeof(value))` | +| Strings | `string()` | Size in UTF-8 + null-terminator at the end | +| Variant | `variant()` | Same as [var_to_bytes()] | + +## Implementing a custom serializer + +Custom serializers are also supported. To implement one, extend the +`NetworkSchemaSerializer` class, and implement the `encode()` and `decode()` +methods. + +For example, consider a `Node` serializer that encodes the node's path: + +```gdscript +--8<-- "examples/snippets/network-schemas/example-node-serializer.gd" +``` + +This custom serializer can now be used in schemas: + +```gdscript +rollback_synchronizer.set_schema({ + "Input:target": ExampleNodeSerializer.new() +}) +``` + + +[Binary serialization API]: https://docs.godotengine.org/en/stable/tutorials/io/binary_serialization_api.html +[RollbackSynchronizer]: ../nodes/rollback-synchronizer.md +[StateSynchronizer]: ../nodes/state-synchronizer.md +[var_to_bytes()]: https://docs.godotengine.org/en/stable/classes/class_%40globalscope.html#class-globalscope-method-var-to-bytes diff --git a/examples/forest-brawl/scripts/brawler-controller.gd b/examples/forest-brawl/scripts/brawler-controller.gd index 3a4d227f..dbffaf9e 100644 --- a/examples/forest-brawl/scripts/brawler-controller.gd +++ b/examples/forest-brawl/scripts/brawler-controller.gd @@ -64,6 +64,16 @@ func _ready(): material.albedo_color = color mesh.set_surface_override_material(0, material) + rollback_synchronizer.set_schema({ + ":transform": NetworkSchemas.transform3f32(), + ":velocity": NetworkSchemas.vec3f32(), + ":speed": NetworkSchemas.float32(), + ":mass": NetworkSchemas.float32(), + + "Input:movement": NetworkSchemas.vec3f32(), + "Input:aim": NetworkSchemas.vec3f32() + }) + func _process(delta): # Update animation # Running diff --git a/examples/snippets/network-schemas/example-node-serializer.gd b/examples/snippets/network-schemas/example-node-serializer.gd new file mode 100644 index 00000000..179e9b16 --- /dev/null +++ b/examples/snippets/network-schemas/example-node-serializer.gd @@ -0,0 +1,13 @@ +extends NetworkSchemaSerializer +class_name ExampleNodeSerializer + +# Needs to be set from the outside +static var scene_tree: SceneTree + +func encode(value: Variant, buffer: StreamPeerBuffer) -> void: + var node := value as Node + buffer.put_utf8_string(node.get_path()) + +func decode(buffer: StreamPeerBuffer) -> Variant: + var path := buffer.get_utf8_string() + return scene_tree.root.get_node(path) diff --git a/examples/snippets/network-schemas/example-node-serializer.gd.uid b/examples/snippets/network-schemas/example-node-serializer.gd.uid new file mode 100644 index 00000000..e0d21bb5 --- /dev/null +++ b/examples/snippets/network-schemas/example-node-serializer.gd.uid @@ -0,0 +1 @@ +uid://2pi50io58wll diff --git a/mkdocs.yml b/mkdocs.yml index 7ebf18e6..326df72c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -54,6 +54,7 @@ nav: - 'netfox/guides/property-paths.md' - 'netfox/guides/network-rollback.md' - 'netfox/guides/network-performance.md' + - 'netfox/guides/network-schemas.md' - 'netfox/guides/visibility-management.md' - 'netfox/guides/netfox-sharp.md' - Nodes: diff --git a/test/netfox/encoder/diff-history-encoder.test.gd b/test/netfox/encoder/diff-history-encoder.test.gd index 3e53d2f1..8bf6e47c 100644 --- a/test/netfox/encoder/diff-history-encoder.test.gd +++ b/test/netfox/encoder/diff-history-encoder.test.gd @@ -26,8 +26,9 @@ func before_case(__): target_history = _PropertyHistoryBuffer.new() property_cache = PropertyCache.new(root_node) - source_encoder = _DiffHistoryEncoder.new(source_history, property_cache) - target_encoder = _DiffHistoryEncoder.new(target_history, property_cache) + var schema := _NetworkSchema.new({}) + source_encoder = _DiffHistoryEncoder.new(source_history, property_cache, schema) + target_encoder = _DiffHistoryEncoder.new(target_history, property_cache, schema) source_encoder.add_properties(property_entries) target_encoder.add_properties(property_entries) diff --git a/test/netfox/encoder/redundant-history-encoder.test.gd b/test/netfox/encoder/redundant-history-encoder.test.gd index 3ac43f54..2d81dd9b 100644 --- a/test/netfox/encoder/redundant-history-encoder.test.gd +++ b/test/netfox/encoder/redundant-history-encoder.test.gd @@ -25,8 +25,9 @@ func before_case(__): target_history = _PropertyHistoryBuffer.new() property_cache = PropertyCache.new(root_node) - source_encoder = _RedundantHistoryEncoder.new(source_history, property_cache) - target_encoder = _RedundantHistoryEncoder.new(target_history, property_cache) + var schema := _NetworkSchema.new({}) + source_encoder = _RedundantHistoryEncoder.new(source_history, property_cache, schema) + target_encoder = _RedundantHistoryEncoder.new(target_history, property_cache, schema) # By setting different redundancies, we also test for the encoders # recognizing redundancy in incoming data diff --git a/test/netfox/encoder/snapshot-history-encoder.test.gd b/test/netfox/encoder/snapshot-history-encoder.test.gd index 1437d7dd..3c6e074c 100644 --- a/test/netfox/encoder/snapshot-history-encoder.test.gd +++ b/test/netfox/encoder/snapshot-history-encoder.test.gd @@ -20,8 +20,9 @@ func before_case(__): target_history = _PropertyHistoryBuffer.new() property_cache = PropertyCache.new(root_node) - source_encoder = _SnapshotHistoryEncoder.new(source_history, property_cache) - target_encoder = _SnapshotHistoryEncoder.new(target_history, property_cache) + var schema := _NetworkSchema.new({}) + source_encoder = _SnapshotHistoryEncoder.new(source_history, property_cache, schema) + target_encoder = _SnapshotHistoryEncoder.new(target_history, property_cache, schema) source_encoder.set_properties(property_entries) target_encoder.set_properties(property_entries) diff --git a/test/netfox/schemas/network-schemas.test.gd b/test/netfox/schemas/network-schemas.test.gd new file mode 100644 index 00000000..bf0fa972 --- /dev/null +++ b/test/netfox/schemas/network-schemas.test.gd @@ -0,0 +1,121 @@ +extends VestTest + +func get_suite_name() -> String: + return "NetworkSchemas" + +func suite() -> void: + var has_half := (Engine.get_version_info().hex >= 0x040400) as bool + + var cases := [ + ["variant", NetworkSchemas.variant(), 32, 12], + ["string", NetworkSchemas.string(), "hi!!", 8], + + ["bool8", NetworkSchemas.bool8(), true, 1], + + ["int8", NetworkSchemas.int8(), 107, 1], + ["int16", NetworkSchemas.int16(), 107, 2], + ["int32", NetworkSchemas.int32(), 107, 4], + ["int64", NetworkSchemas.int64(), 107, 8], + + ["uint8", NetworkSchemas.uint8(), 107, 1], + ["uint16", NetworkSchemas.uint16(), 107, 2], + ["uint32", NetworkSchemas.uint32(), 107, 4], + ["uint64", NetworkSchemas.uint64(), 107, 8], + + ["sfrac8", NetworkSchemas.sfrac8(), -63. / 255., 1], + ["sfrac16", NetworkSchemas.sfrac16(), -63. / 255., 2], + ["sfrac32", NetworkSchemas.sfrac32(), -63. / 255., 4], + + ["ufrac8", NetworkSchemas.ufrac8(), 63. / 255., 1], + ["ufrac16", NetworkSchemas.ufrac16(), 63. / 255., 2], + ["ufrac32", NetworkSchemas.ufrac32(), 63. / 255., 4], + + ["degrees8", NetworkSchemas.degrees8(), 127. / 255. * 360., 1], + ["degrees16", NetworkSchemas.degrees16(), 127. / 255. * 360., 2], + ["degrees32", NetworkSchemas.degrees32(), 127. / 255. * 360., 4], + + ["radians8", NetworkSchemas.radians8(), 127. / 255. * TAU, 1], + ["radians16", NetworkSchemas.radians16(), 127. / 255. * TAU, 2], + ["radians32", NetworkSchemas.radians32(), 127. / 255. * TAU, 4], + + ["float16", NetworkSchemas.float16(), 2.0, 2 if has_half else 4], + ["float32", NetworkSchemas.float32(), 2.0, 4], + ["float64", NetworkSchemas.float64(), 2.0, 8], + + ["vec2f16", NetworkSchemas.vec2f32(), Vector2(+1, -1), 4 if has_half else 8], + ["vec2f32", NetworkSchemas.vec2f32(), Vector2(+1, -1), 8], + ["vec2f64", NetworkSchemas.vec2f64(), Vector2(+1, -1), 16], + ["vec3f32", NetworkSchemas.vec3f32(), Vector3(+1, -1, .5), 12], + ["vec3f64", NetworkSchemas.vec3f64(), Vector3(+1, -1, .5), 24], + ["vec4f32", NetworkSchemas.vec4f32(), Vector4(+1, -1, .5, -5), 16], + ["vec4f64", NetworkSchemas.vec4f64(), Vector4(+1, -1, .5, -5), 32], + + ["normal2f16", NetworkSchemas.normal2f16(), Vector2.RIGHT.rotated(PI / 6.), 2 if has_half else 4], + ["normal2f32", NetworkSchemas.normal2f32(), Vector2.RIGHT.rotated(PI / 6.), 4], + ["normal2f64", NetworkSchemas.normal2f64(), Vector2.RIGHT.rotated(PI / 6.), 8], + ["normal3f16", NetworkSchemas.normal3f16(), Vector3.UP, 4 if has_half else 8], + ["normal3f32", NetworkSchemas.normal3f32(), Vector3.UP, 8], + ["normal3f64", NetworkSchemas.normal3f64(), Vector3.UP, 16], + + ["quat16f", NetworkSchemas.quatf16(), Quaternion.from_euler(Vector3.ONE), 8 if has_half else 16], + ["quat32f", NetworkSchemas.quatf32(), Quaternion.from_euler(Vector3.ONE), 16], + ["quat64f", NetworkSchemas.quatf64(), Quaternion.from_euler(Vector3.ONE), 32], + + ["transform2f16", NetworkSchemas.transform2f16(), Transform2D.IDENTITY.rotated(37.), 12 if has_half else 24], + ["transform2f32", NetworkSchemas.transform2f32(), Transform2D.IDENTITY.rotated(37.), 24], + ["transform2f64", NetworkSchemas.transform2f64(), Transform2D.IDENTITY.rotated(37.), 48], + ["transform3f16", NetworkSchemas.transform3f16(), Transform3D.IDENTITY.rotated(Vector3.ONE, 37.), 24 if has_half else 48], + ["transform3f32", NetworkSchemas.transform3f32(), Transform3D.IDENTITY.rotated(Vector3.ONE, 37.), 48], + ["transform3f64", NetworkSchemas.transform3f64(), Transform3D.IDENTITY.rotated(Vector3.ONE, 37.), 96], + + ["array", NetworkSchemas.array_of(NetworkSchemas.uint16()), [1, 2, 3], 8], + ["dictionary", NetworkSchemas.dictionary(NetworkSchemas.uint16(), NetworkSchemas.uint16()), { 1: 32, 2: 48 }, 10] + ] + + for case in cases: + var name := case[0] as String + var serializer := case[1] as NetworkSchemaSerializer + var value = case[2] + var expected_size := case[3] as int + + test(name, func(): + var buffer := StreamPeerBuffer.new() + serializer.encode(value, buffer) + buffer.seek(0) + var decoded = serializer.decode(buffer) + + expect_equal(decoded, value) + expect_equal(buffer.data_array.size(), expected_size) + ) + + test("should handle negative degrees", func(): + var input_value := -60. + var expected_value := 300. + var threshold := 1. + + var buffer := StreamPeerBuffer.new() + var serializer := NetworkSchemas.degrees16() + serializer.encode(input_value, buffer) + + buffer.seek(0) + var decoded := serializer.decode(buffer) as float + + expect(abs(decoded - expected_value) < threshold, "Decoded %s while expected %s!" % [decoded, expected_value]) + Vest.message("Difference: %s" % [abs(decoded - expected_value)]) + ) + + test("should handle negative radians", func(): + var input_value := -TAU / 6. + var expected_value := 5. / 6. * TAU + var threshold := 1. / TAU + + var buffer := StreamPeerBuffer.new() + var serializer := NetworkSchemas.radians16() + serializer.encode(input_value, buffer) + + buffer.seek(0) + var decoded := serializer.decode(buffer) as float + + expect(abs(decoded - expected_value) < threshold, "Decoded %s while expected %s!" % [decoded, expected_value]) + Vest.message("Difference: %s" % [abs(decoded - expected_value)]) + ) diff --git a/test/netfox/schemas/network-schemas.test.gd.uid b/test/netfox/schemas/network-schemas.test.gd.uid new file mode 100644 index 00000000..07a2c648 --- /dev/null +++ b/test/netfox/schemas/network-schemas.test.gd.uid @@ -0,0 +1 @@ +uid://bk7cc3vjakruu