@tool extends Node class_name StateSynchronizer ## Synchronizes state from authority. ## ## Similar to Godot's [MultiplayerSynchronizer], but is tied to the network tick loop. Works well ## with [TickInterpolator]. ## [br][br] ## @tutorial(StateSynchronizer Guide): https://foxssake.github.io/netfox/latest/netfox/nodes/state-synchronizer/ ## The root node for resolving node paths in properties. @export var root: Node ## Properties to record and broadcast. @export var properties: Array[String] ## Ticks to wait between sending full states. ## [br][br] ## If set to 0, full states will never be sent. If set to 1, only full states ## will be sent. If set higher, full states will be sent regularly, but not ## for every tick. ## [br][br] ## Only considered if [member _NetworkRollback.enable_diff_states] is true. ## @deprecated: This can now be configured in the project settings. @export_range(0, 128, 1, "or_greater") var full_state_interval: int = 24 ## @deprecated: This is no longer used. @export_range(0, 128, 1, "or_greater") var diff_ack_interval: int = 0 ## Decides which peers will receive updates var visibility_filter := PeerVisibilityFilter.new() var _properties_dirty: bool = false var _properties := _PropertyPool.new() var _schema_nodes := _Set.new() var _is_initialized: bool = false static var _logger := NetfoxLogger._for_netfox("StateSynchronizer") ## Process settings. ## [br][br] ## Call this after any change to configuration. func process_settings() -> void: # Remove old configuration for node in _properties.get_subjects(): for property in _properties.get_properties_of(node): NetworkHistoryServer.deregister_sync_state(node, property) NetworkSynchronizationServer.deregister_sync_state(node, property) # Register new configuration _properties.set_from_paths(root, properties) for node in _properties.get_subjects(): NetworkSynchronizationServer.register_visibility_filter(node, visibility_filter) NetworkIdentityServer.register_node(node) for property in _properties.get_properties_of(node): NetworkHistoryServer.register_sync_state(node, property) NetworkSynchronizationServer.register_sync_state(node, property) _is_initialized = true ## Add a state property. ## [br][br] ## Settings will be automatically updated. The [param node] may be a string or ## [NodePath] pointing to a node, or an actual [Node] instance. If the given ## property is already tracked, this method does nothing. func add_state(node: Variant, property: String) -> void: var property_path := PropertyEntry.make_path(root, node, property) if not property_path or properties.has(property_path): return properties.push_back(property_path) _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: # Remove previous schema for node in _schema_nodes: NetworkSynchronizationServer.deregister_schema_for(node) _schema_nodes.clear() # Register new schema for prop in schema: var prop_entry := PropertyEntry.parse(root, prop) var serializer := schema[prop] as NetworkSchemaSerializer NetworkSynchronizationServer.register_schema(prop_entry.node, prop_entry.property, serializer) _schema_nodes.add(prop_entry.node) func _notification(what) -> void: if what == NOTIFICATION_EDITOR_PRE_SAVE: update_configuration_warnings() elif what == NOTIFICATION_PREDELETE: for node in _properties.get_subjects(): NetworkSynchronizationServer.deregister(node) NetworkHistoryServer.deregister(node) NetworkIdentityServer.deregister(node) func _get_configuration_warnings() -> PackedStringArray: if not root: root = get_parent() # Explore state properties if not root: return ["No valid root node found!"] return _NetfoxEditorUtils.gather_properties(root, "_get_synchronized_state_properties", func(node, prop): add_state(node, prop) ) func _enter_tree() -> void: if Engine.is_editor_hint(): return if not visibility_filter: visibility_filter = PeerVisibilityFilter.new() if not visibility_filter.get_parent(): add_child(visibility_filter) process_settings.call_deferred() func _ready(): # Reprocess authority # Important if nodes are pre-placed in the scene - node starts as owned by # us ( offline peer is 1 ), but once we connect, we no longer own the node multiplayer.connected_to_server.connect(process_settings) func _reprocess_settings() -> void: if not _properties_dirty: return _properties_dirty = false process_settings()