Skip to content

Commit 2fb8c76

Browse files
feat: Track and expose latest confirmed tick (#518)
Allows NetworkRollback to track and expose the latest tick for which each node with a RollbackSynchronizer has received input submissions. Useful for scenarios where you need to ensure all input ticks are submitted before proceeding with actions that are difficult to reverse, such as character death, visual effects, or audio triggers. Closes #470 --------- Co-authored-by: Tamás Gálffy <[email protected]>
1 parent 7be2a3c commit 2fb8c76

File tree

8 files changed

+115
-36
lines changed

8 files changed

+115
-36
lines changed

addons/netfox.extras/plugin.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
name="netfox.extras"
44
description="Game-specific utilities for Netfox"
55
author="Tamas Galffy and contributors"
6-
version="1.34.1"
6+
version="1.35.0"
77
script="netfox-extras.gd"

addons/netfox.internals/plugin.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
name="netfox.internals"
44
description="Shared internals for netfox addons"
55
author="Tamas Galffy and contributors"
6-
version="1.34.1"
6+
version="1.35.0"
77
script="plugin.gd"

addons/netfox.noray/plugin.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
name="netfox.noray"
44
description="Bulletproof your connectivity with noray integration for netfox"
55
author="Tamas Galffy and contributors"
6-
version="1.34.1"
6+
version="1.35.0"
77
script="netfox-noray.gd"

addons/netfox/plugin.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
name="netfox"
44
description="Shared internals for netfox addons"
55
author="Tamas Galffy and contributors"
6-
version="1.34.1"
6+
version="1.35.0"
77
script="netfox.gd"

addons/netfox/rollback/composite/rollback-history-transmitter.gd

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ func transmit_input(tick: int) -> void:
103103
var input_tick: int = tick + NetworkRollback.input_delay
104104
var input_data := _input_encoder.encode(input_tick, _get_owned_input_props())
105105
var state_owning_peer := root.get_multiplayer_authority()
106+
NetworkRollback.register_input_submission(root, tick)
106107

107108
if enable_input_broadcast:
108109
for peer in _visibility_filter.get_rpc_target_peers():
@@ -197,6 +198,10 @@ func _send_full_state(tick: int, peer: int = 0) -> void:
197198
NetworkPerformance.push_full_state(full_state_snapshot)
198199
NetworkPerformance.push_sent_state(full_state_snapshot)
199200

201+
func _notification(what):
202+
if what == NOTIFICATION_PREDELETE:
203+
NetworkRollback.free_input_submission_data_for(root)
204+
200205
@rpc("any_peer", "unreliable", "call_remote")
201206
func _submit_input(tick: int, data: Array) -> void:
202207
if not _is_initialized:
@@ -208,6 +213,7 @@ func _submit_input(tick: int, data: Array) -> void:
208213
var earliest_received_input = _input_encoder.apply(tick, snapshots, sender)
209214
if earliest_received_input >= 0:
210215
_earliest_input_tick = mini(_earliest_input_tick, earliest_received_input)
216+
NetworkRollback.register_input_submission(root, tick)
211217

212218
# `serialized_state` is a serialized _PropertySnapshot
213219
@rpc("any_peer", "unreliable_ordered", "call_remote")

addons/netfox/rollback/network-rollback.gd

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ var _rollback_stage: String = ""
168168
var _is_rollback: bool = false
169169
var _simulated_nodes: _Set = _Set.new()
170170
var _mutated_nodes: Dictionary = {}
171+
var _input_submissions: Dictionary = {}
171172

172173
const _STAGE_BEFORE := "B"
173174
const _STAGE_PREPARE := "P"
@@ -266,6 +267,31 @@ func is_just_mutated(target: Object, p_tick: int = tick) -> bool:
266267
else:
267268
return false
268269

270+
## Register that a node has submitted its input for a specific tick
271+
func register_input_submission(root_node: Node, tick: int) -> void:
272+
if not _input_submissions.has(root_node):
273+
_input_submissions[root_node] = tick
274+
else:
275+
_input_submissions[root_node] = maxi(_input_submissions[root_node], tick)
276+
277+
## Get the latest input tick submitted by a specific root node
278+
## [br][br]
279+
## Returns [code]-1[/code] if no input was submitted for the node, ever.
280+
func get_latest_input_tick(root_node: Node) -> int:
281+
if _input_submissions.has(root_node):
282+
return _input_submissions[root_node]
283+
return -1
284+
285+
## Check if a node has submitted input for a specific tick (or later)
286+
func has_input_for_tick(root_node: Node, tick: int) -> bool:
287+
return _input_submissions.has(root_node) and _input_submissions[root_node] >= tick
288+
289+
## Free all input submission data for a node
290+
## [br][br]
291+
## Use this once the node is freed.
292+
func free_input_submission_data_for(root_node: Node) -> void:
293+
_input_submissions.erase(root_node)
294+
269295
func _ready():
270296
NetfoxLogger.register_tag(_get_rollback_tag)
271297
NetworkTime.after_tick_loop.connect(_rollback)

docs/netfox/guides/network-rollback.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,22 @@ To actually run a rollback tick on them, call
129129

130130
These methods are called by [RollbackSynchronizer] under the hood.
131131

132+
## Input Submission Status
133+
134+
In certain scenarios you may wish to delay committing to something hard to
135+
reverse like death, VFX or audio until its known for sure the outcome won't
136+
change. One way of doing this is to check which nodes have submitted input and
137+
are past a point of rollback.
138+
139+
You can query the status of Nodes with
140+
`NetworkRollback.get_latest_input_tick(root_node)` or
141+
`NetworkRollback.has_input_for_tick(root_node, tick)`. `root_node` being what
142+
the relevant [RollbackSynchronizer] has configured.
143+
144+
All tracked nodes can be retrieved from
145+
`NetworkRollback.get_input_submissions()` which will return the entire
146+
`<root_node, latest_tick>` dictionary.
147+
132148
## Settings
133149

134150
![Network rollback settings](../assets/network-rollback-settings.png)

test/netfox/rollback/network-rollback.test.gd

Lines changed: 63 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -18,35 +18,66 @@ func after_case(__):
1818
network_rollback.queue_free()
1919
mutated_node.queue_free()
2020

21-
#region Mutate
22-
func test_should_be_mutated_after():
23-
# Given
24-
network_rollback.mutate(mutated_node, 8)
25-
26-
# When + Then
27-
expect(network_rollback.is_mutated(mutated_node, 10))
28-
expect_not(network_rollback.is_just_mutated(mutated_node, 10))
29-
30-
func test_should_just_be_mutated():
31-
# Given
32-
network_rollback.mutate(mutated_node, 8)
33-
34-
# When + Then
35-
expect(network_rollback.is_mutated(mutated_node, 8))
36-
expect(network_rollback.is_just_mutated(mutated_node, 8))
37-
38-
func test_should_not_be_mutated_after():
39-
# Given
40-
network_rollback.mutate(mutated_node, 8)
41-
42-
# When + Then
43-
expect_not(network_rollback.is_mutated(mutated_node, 4))
44-
expect_not(network_rollback.is_just_mutated(mutated_node, 4))
45-
46-
func test_unknown_should_not_be_mutated():
47-
# Given nothing
48-
49-
# Then
50-
expect_not(network_rollback.is_mutated(mutated_node, 8))
51-
expect_not(network_rollback.is_just_mutated(mutated_node, 8))
52-
#endregion
21+
func suite() -> void:
22+
define("mutate()", func():
23+
test("should be mutated after", func():
24+
# Given
25+
network_rollback.mutate(mutated_node, 8)
26+
27+
# When + Then
28+
expect(network_rollback.is_mutated(mutated_node, 10))
29+
expect_not(network_rollback.is_just_mutated(mutated_node, 10))
30+
)
31+
32+
test("should just be mutated", func():
33+
# Given
34+
network_rollback.mutate(mutated_node, 8)
35+
36+
# When + Then
37+
expect(network_rollback.is_mutated(mutated_node, 8))
38+
expect(network_rollback.is_just_mutated(mutated_node, 8))
39+
)
40+
41+
test("should not be mutated after", func():
42+
# Given
43+
network_rollback.mutate(mutated_node, 8)
44+
45+
# When + Then
46+
expect_not(network_rollback.is_mutated(mutated_node, 4))
47+
expect_not(network_rollback.is_just_mutated(mutated_node, 4))
48+
)
49+
50+
test("unknown should not be mutated", func():
51+
# Given nothing
52+
53+
# Then
54+
expect_not(network_rollback.is_mutated(mutated_node, 8))
55+
expect_not(network_rollback.is_just_mutated(mutated_node, 8))
56+
)
57+
)
58+
59+
define("input submission", func():
60+
test("should have input after submit", func():
61+
# Given
62+
network_rollback.register_input_submission(mutated_node, 2)
63+
64+
# Then
65+
expect(network_rollback.has_input_for_tick(mutated_node, 2), "Node should have input!")
66+
expect(network_rollback.has_input_for_tick(mutated_node, 1), "Node should have future input!")
67+
expect_not(network_rollback.has_input_for_tick(mutated_node, 3), "Node shouldn't yet have input!")
68+
)
69+
70+
test("should return latest input tick", func():
71+
# Given
72+
network_rollback.register_input_submission(mutated_node, 2)
73+
74+
# Then
75+
expect_equal(network_rollback.get_latest_input_tick(mutated_node), 2)
76+
)
77+
78+
test("should return no input tick", func():
79+
# Given nothing
80+
# Then
81+
expect_equal(network_rollback.get_latest_input_tick(mutated_node), -1)
82+
)
83+
)

0 commit comments

Comments
 (0)