diff --git a/addons/netfox.extras/plugin.cfg b/addons/netfox.extras/plugin.cfg index efc5ac64..aff8cb6f 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.34.1" +version="1.35.0" script="netfox-extras.gd" diff --git a/addons/netfox.internals/plugin.cfg b/addons/netfox.internals/plugin.cfg index d0c4163f..362d935a 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.34.1" +version="1.35.0" script="plugin.gd" diff --git a/addons/netfox.noray/plugin.cfg b/addons/netfox.noray/plugin.cfg index 269ac6e4..8540cd4d 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.34.1" +version="1.35.0" script="netfox-noray.gd" diff --git a/addons/netfox/plugin.cfg b/addons/netfox/plugin.cfg index e1e6d77e..51a72c5a 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.34.1" +version="1.35.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 b903adf7..c341cb92 100644 --- a/addons/netfox/rollback/composite/rollback-history-transmitter.gd +++ b/addons/netfox/rollback/composite/rollback-history-transmitter.gd @@ -103,6 +103,7 @@ func transmit_input(tick: int) -> void: var input_tick: int = tick + NetworkRollback.input_delay var input_data := _input_encoder.encode(input_tick, _get_owned_input_props()) var state_owning_peer := root.get_multiplayer_authority() + NetworkRollback.register_input_submission(root, tick) if enable_input_broadcast: for peer in _visibility_filter.get_rpc_target_peers(): @@ -197,6 +198,10 @@ func _send_full_state(tick: int, peer: int = 0) -> void: NetworkPerformance.push_full_state(full_state_snapshot) NetworkPerformance.push_sent_state(full_state_snapshot) +func _notification(what): + if what == NOTIFICATION_PREDELETE: + NetworkRollback.free_input_submission_data_for(root) + @rpc("any_peer", "unreliable", "call_remote") func _submit_input(tick: int, data: Array) -> void: if not _is_initialized: @@ -208,6 +213,7 @@ func _submit_input(tick: int, data: Array) -> void: var earliest_received_input = _input_encoder.apply(tick, snapshots, sender) if earliest_received_input >= 0: _earliest_input_tick = mini(_earliest_input_tick, earliest_received_input) + NetworkRollback.register_input_submission(root, tick) # `serialized_state` is a serialized _PropertySnapshot @rpc("any_peer", "unreliable_ordered", "call_remote") diff --git a/addons/netfox/rollback/network-rollback.gd b/addons/netfox/rollback/network-rollback.gd index 8488a3cf..7a216f27 100644 --- a/addons/netfox/rollback/network-rollback.gd +++ b/addons/netfox/rollback/network-rollback.gd @@ -168,6 +168,7 @@ var _rollback_stage: String = "" var _is_rollback: bool = false var _simulated_nodes: _Set = _Set.new() var _mutated_nodes: Dictionary = {} +var _input_submissions: Dictionary = {} const _STAGE_BEFORE := "B" const _STAGE_PREPARE := "P" @@ -266,6 +267,31 @@ func is_just_mutated(target: Object, p_tick: int = tick) -> bool: else: return false +## Register that a node has submitted its input for a specific tick +func register_input_submission(root_node: Node, tick: int) -> void: + if not _input_submissions.has(root_node): + _input_submissions[root_node] = tick + else: + _input_submissions[root_node] = maxi(_input_submissions[root_node], tick) + +## Get the latest input tick submitted by a specific root node +## [br][br] +## Returns [code]-1[/code] if no input was submitted for the node, ever. +func get_latest_input_tick(root_node: Node) -> int: + if _input_submissions.has(root_node): + return _input_submissions[root_node] + return -1 + +## Check if a node has submitted input for a specific tick (or later) +func has_input_for_tick(root_node: Node, tick: int) -> bool: + return _input_submissions.has(root_node) and _input_submissions[root_node] >= tick + +## Free all input submission data for a node +## [br][br] +## Use this once the node is freed. +func free_input_submission_data_for(root_node: Node) -> void: + _input_submissions.erase(root_node) + func _ready(): NetfoxLogger.register_tag(_get_rollback_tag) NetworkTime.after_tick_loop.connect(_rollback) diff --git a/docs/netfox/guides/network-rollback.md b/docs/netfox/guides/network-rollback.md index 6c2aad03..0f691a4f 100644 --- a/docs/netfox/guides/network-rollback.md +++ b/docs/netfox/guides/network-rollback.md @@ -129,6 +129,22 @@ To actually run a rollback tick on them, call These methods are called by [RollbackSynchronizer] under the hood. +## Input Submission Status + +In certain scenarios you may wish to delay committing to something hard to +reverse like death, VFX or audio until its known for sure the outcome won't +change. One way of doing this is to check which nodes have submitted input and +are past a point of rollback. + +You can query the status of Nodes with +`NetworkRollback.get_latest_input_tick(root_node)` or +`NetworkRollback.has_input_for_tick(root_node, tick)`. `root_node` being what +the relevant [RollbackSynchronizer] has configured. + +All tracked nodes can be retrieved from +`NetworkRollback.get_input_submissions()` which will return the entire +`` dictionary. + ## Settings ![Network rollback settings](../assets/network-rollback-settings.png) diff --git a/test/netfox/rollback/network-rollback.test.gd b/test/netfox/rollback/network-rollback.test.gd index 4ac6bb90..66be4ba6 100644 --- a/test/netfox/rollback/network-rollback.test.gd +++ b/test/netfox/rollback/network-rollback.test.gd @@ -18,35 +18,66 @@ func after_case(__): network_rollback.queue_free() mutated_node.queue_free() -#region Mutate -func test_should_be_mutated_after(): - # Given - network_rollback.mutate(mutated_node, 8) - - # When + Then - expect(network_rollback.is_mutated(mutated_node, 10)) - expect_not(network_rollback.is_just_mutated(mutated_node, 10)) - -func test_should_just_be_mutated(): - # Given - network_rollback.mutate(mutated_node, 8) - - # When + Then - expect(network_rollback.is_mutated(mutated_node, 8)) - expect(network_rollback.is_just_mutated(mutated_node, 8)) - -func test_should_not_be_mutated_after(): - # Given - network_rollback.mutate(mutated_node, 8) - - # When + Then - expect_not(network_rollback.is_mutated(mutated_node, 4)) - expect_not(network_rollback.is_just_mutated(mutated_node, 4)) - -func test_unknown_should_not_be_mutated(): - # Given nothing - - # Then - expect_not(network_rollback.is_mutated(mutated_node, 8)) - expect_not(network_rollback.is_just_mutated(mutated_node, 8)) -#endregion +func suite() -> void: + define("mutate()", func(): + test("should be mutated after", func(): + # Given + network_rollback.mutate(mutated_node, 8) + + # When + Then + expect(network_rollback.is_mutated(mutated_node, 10)) + expect_not(network_rollback.is_just_mutated(mutated_node, 10)) + ) + + test("should just be mutated", func(): + # Given + network_rollback.mutate(mutated_node, 8) + + # When + Then + expect(network_rollback.is_mutated(mutated_node, 8)) + expect(network_rollback.is_just_mutated(mutated_node, 8)) + ) + + test("should not be mutated after", func(): + # Given + network_rollback.mutate(mutated_node, 8) + + # When + Then + expect_not(network_rollback.is_mutated(mutated_node, 4)) + expect_not(network_rollback.is_just_mutated(mutated_node, 4)) + ) + + test("unknown should not be mutated", func(): + # Given nothing + + # Then + expect_not(network_rollback.is_mutated(mutated_node, 8)) + expect_not(network_rollback.is_just_mutated(mutated_node, 8)) + ) + ) + + define("input submission", func(): + test("should have input after submit", func(): + # Given + network_rollback.register_input_submission(mutated_node, 2) + + # Then + expect(network_rollback.has_input_for_tick(mutated_node, 2), "Node should have input!") + expect(network_rollback.has_input_for_tick(mutated_node, 1), "Node should have future input!") + expect_not(network_rollback.has_input_for_tick(mutated_node, 3), "Node shouldn't yet have input!") + ) + + test("should return latest input tick", func(): + # Given + network_rollback.register_input_submission(mutated_node, 2) + + # Then + expect_equal(network_rollback.get_latest_input_tick(mutated_node), 2) + ) + + test("should return no input tick", func(): + # Given nothing + # Then + expect_equal(network_rollback.get_latest_input_tick(mutated_node), -1) + ) + )