diff --git a/addons/netfox.extras/plugin.cfg b/addons/netfox.extras/plugin.cfg index 4f4e74ad..a1a852ed 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.42.5" +version="1.43.0" script="netfox-extras.gd" diff --git a/addons/netfox.internals/plugin.cfg b/addons/netfox.internals/plugin.cfg index c7d35c84..6b5a7e5f 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.42.5" +version="1.43.0" script="plugin.gd" diff --git a/addons/netfox.noray/plugin.cfg b/addons/netfox.noray/plugin.cfg index 8686a08c..6f4e0817 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.42.5" +version="1.43.0" script="netfox-noray.gd" diff --git a/addons/netfox/plugin.cfg b/addons/netfox/plugin.cfg index 3146e8c0..a74f0659 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.42.5" +version="1.43.0" script="netfox.gd" diff --git a/addons/netfox/rollback/predictive-synchronizer.gd b/addons/netfox/rollback/predictive-synchronizer.gd index bdd6a3c8..afb227e7 100644 --- a/addons/netfox/rollback/predictive-synchronizer.gd +++ b/addons/netfox/rollback/predictive-synchronizer.gd @@ -29,6 +29,8 @@ var _liveness_nodes: Array[Node] = [] var _properties_dirty: bool = false +static var _managed_roots := {} # root node to PredictiveSynchronizer + ## Process settings. ## [br][br] ## Call this after any change to configuration. @@ -42,7 +44,7 @@ func process_settings() -> void: _sim_nodes.clear() # Gather all prediction-aware nodes to call during prediction ticks - var managed_nodes := [root] + root.find_children("*") + var managed_nodes := [root] + _collect_managed_nodes(root) for node in managed_nodes: if NetworkRollback.is_rollback_aware(node): _sim_nodes.append(node) @@ -108,11 +110,16 @@ func _enter_tree() -> void: if Engine.is_editor_hint(): return + _managed_roots[root] = self + if not NetworkTime.is_initial_sync_done(): # Wait for time sync to complete await NetworkTime.after_sync process_settings.call_deferred() +func _exit_tree() -> void: + _managed_roots.erase(root) + func _reprocess_settings() -> void: if not _properties_dirty or Engine.is_editor_hint(): return @@ -160,3 +167,27 @@ func _get_configuration_warnings() -> PackedStringArray: )) return result + +# Find managed nodes recursively from given root, ignoring branches managed by +# a different RollbackSynchronizer +func _collect_managed_nodes(root: Node) -> Array[Node]: + var result: Array[Node] = [] + for child in root.get_children(): + if _is_foreign_rollback_root(child): + continue + result.append(child) + result.append_array(_collect_managed_nodes(child)) + return result + +# Returns true if the node is the root of a different RollbackSynchronizer +func _is_foreign_rollback_root(node: Node) -> bool: + if not _managed_roots.has(node): + # No RBS treats node as root + return false + + if _managed_roots[node] == self: + # Node is our own root + return false + + # Node is foreign root + return true diff --git a/addons/netfox/rollback/rollback-synchronizer.gd b/addons/netfox/rollback/rollback-synchronizer.gd index 48d9dae7..7e843b02 100644 --- a/addons/netfox/rollback/rollback-synchronizer.gd +++ b/addons/netfox/rollback/rollback-synchronizer.gd @@ -65,7 +65,9 @@ var _schema_nodes := _Set.new() var _properties_dirty: bool = false -static var _logger: NetfoxLogger = NetfoxLogger._for_netfox("RollbackSynchronizer") +static var _managed_roots := {} # root node to RollbackSynchronizer + +@onready var _logger: NetfoxLogger = NetfoxLogger._for_netfox("RollbackSynchronizer:" + root.name) ## Process settings. ## [br][br] @@ -80,7 +82,8 @@ func process_settings() -> void: process_authority() # Register nodes for simulation and liveness - var managed_nodes := [root] + root.find_children("*") + var managed_nodes := [root] + _collect_managed_nodes(root) + _logger.debug("Filtering managed nodes: %s", [managed_nodes]) for node in managed_nodes: if NetworkRollback.is_rollback_aware(node): RollbackSimulationServer.register(NetworkRollback._get_rollback_method(node)) @@ -354,6 +357,8 @@ func _enter_tree() -> void: if Engine.is_editor_hint(): return + _managed_roots[root] = self + if not visibility_filter: visibility_filter = PeerVisibilityFilter.new() @@ -365,9 +370,36 @@ func _enter_tree() -> void: await NetworkTime.after_sync process_settings.call_deferred() +func _exit_tree() -> void: + _managed_roots.erase(root) + func _reprocess_settings() -> void: if not _properties_dirty or Engine.is_editor_hint(): return _properties_dirty = false process_settings() + +# Find managed nodes recursively from given root, ignoring branches managed by +# a different RollbackSynchronizer +func _collect_managed_nodes(root: Node) -> Array[Node]: + var result: Array[Node] = [] + for child in root.get_children(): + if _is_foreign_rollback_root(child): + continue + result.append(child) + result.append_array(_collect_managed_nodes(child)) + return result + +# Returns true if the node is the root of a different RollbackSynchronizer +func _is_foreign_rollback_root(node: Node) -> bool: + if not _managed_roots.has(node): + # No RBS treats node as root + return false + + if _managed_roots[node] == self: + # Node is our own root + return false + + # Node is foreign root + return true diff --git a/addons/netfox/servers/rollback-simulation-server.gd b/addons/netfox/servers/rollback-simulation-server.gd index 8e9f9dc3..fd34d6dd 100644 --- a/addons/netfox/servers/rollback-simulation-server.gd +++ b/addons/netfox/servers/rollback-simulation-server.gd @@ -41,6 +41,7 @@ func register(callback: Callable) -> void: return assert(callback.get_object() is Node, "Only nodes supported for now!") + assert(not _callbacks.has(callback.get_object()), "Double register() of node %s!" % [callback.get_object()]) _callbacks[callback.get_object()] = callback diff --git a/test/netfox/rollback-synchronizer.test.gd b/test/netfox/rollback-synchronizer.test.gd index b0e8c02e..40ddb0ec 100644 --- a/test/netfox/rollback-synchronizer.test.gd +++ b/test/netfox/rollback-synchronizer.test.gd @@ -4,6 +4,49 @@ func get_suite_name() -> String: return "RollbackSynchronizer" func suite(): + test("Nested RollbackSynchronizer support", func(): + # Setup node layout: + # primary_root/ + # ├── primary_rbs + # └── secondary_root/ + # └── secondary_rbs + + var primary_root := RollbackAware.new(); primary_root.name = "Primary Root" + var primary_rbs := RollbackSynchronizer.new(); primary_rbs.name = "Primary RBS" + var secondary_root := RollbackAware.new(); secondary_root.name = "Secondary Root" + var secondary_rbs := RollbackSynchronizer.new(); secondary_rbs.name = "Secondary RBS" + + primary_rbs.owner = primary_root + secondary_root.owner = primary_root + secondary_rbs.owner = primary_root + + primary_root.add_child(primary_rbs) + primary_root.add_child(secondary_root) + secondary_root.add_child(secondary_rbs) + + primary_rbs.root = primary_root + secondary_rbs.root = secondary_root + + Vest.get_tree().root.add_child.call_deferred(primary_root) + await primary_root.ready + + # Pick up managed nodes + primary_rbs.process_settings() + secondary_rbs.process_settings() + + # Check managed nodes + expect_equal(primary_rbs._sim_nodes, [primary_root]) + expect_equal(secondary_rbs._sim_nodes, [secondary_root]) + + print("Node tree:") + primary_root.print_tree_pretty() + print("Primary managed: %s" % [primary_rbs._sim_nodes]) + print("Secondary managed: %s" % [secondary_rbs._sim_nodes]) + + # Cleanup + primary_root.queue_free() + ) + # Messy to set up, keeping cases for later define("Input age and predicting", func(): test("should return -1 on no input", func(): todo()) @@ -20,3 +63,13 @@ func suite(): test("should return -1 for no state", func(): todo()) test("should return latest", func(): todo()) ) + +class RollbackAware extends Node: + func _rollback_tick(_dt: float, _t: int, _if: int) -> void: + pass + + func _to_vest(): + return _to_string() + + func _to_string() -> String: + return "RollbackAware:" + name