From abe463a7e4348527abe5051d71917f134c4e32c3 Mon Sep 17 00:00:00 2001 From: Andrey Romantsev Date: Mon, 22 Jun 2026 21:48:54 +0200 Subject: [PATCH 1/5] fix: stale auth state poisoning --- .../netfox/servers/data/per-object-history.gd | 17 ++++++++++ .../netfox/servers/network-history-server.gd | 8 +++++ .../servers/network-history-server.test.gd | 34 +++++++++++++++++-- 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/addons/netfox/servers/data/per-object-history.gd b/addons/netfox/servers/data/per-object-history.gd index 450a4ae96..036dd6171 100644 --- a/addons/netfox/servers/data/per-object-history.gd +++ b/addons/netfox/servers/data/per-object-history.gd @@ -96,3 +96,20 @@ func get_property(tick: int, subject: Object, property: NodePath, default: Varia var snapshot := history.get_at(tick) as _ObjectSnapshot return snapshot.get_value(property, default) + +func truncate_after(tick: int, subject: Object) -> void: + if not _data.has(subject): + return + + var history: _HistoryBuffer = _data[subject] + if history.is_empty(): + return + + var truncated_history := _HistoryBuffer.new(_history_size) + var latest_tick: int = mini(tick, history.get_latest_index()) + for kept_tick in range(history.get_earliest_index(), latest_tick + 1): + if not history.has_at(kept_tick): + continue + truncated_history.set_at(kept_tick, history.get_at(kept_tick)) + + _data[subject] = truncated_history diff --git a/addons/netfox/servers/network-history-server.gd b/addons/netfox/servers/network-history-server.gd index 6d511f022..6d0f8c4cd 100644 --- a/addons/netfox/servers/network-history-server.gd +++ b/addons/netfox/servers/network-history-server.gd @@ -289,6 +289,14 @@ func _merge_history(snapshot: _Snapshot, history: _PerObjectHistory, reverse: bo return false for subject in snapshot.get_subjects(): + # When receiving auth snapshots from the past, clear all newer history. + # This prevents old (potentially incorrect) auth states from poisoning hisotory + # and preventing local resimulation from recording new correct states. + if not reverse and history == _rb_state_history and snapshot.is_auth(subject): + var latest_tick: int = history.get_latest_tick(NetworkTime.tick, subject) + if latest_tick > tick: + history.truncate_after(tick, subject) + var object_snapshot := history.ensure_snapshot(tick, subject, not reverse) if object_snapshot == null: _logger.warning("Snapshot being merged is out of bounds! (@%d)", [tick]) diff --git a/test/netfox/servers/network-history-server.test.gd b/test/netfox/servers/network-history-server.test.gd index eb421ee22..7dae4d171 100644 --- a/test/netfox/servers/network-history-server.test.gd +++ b/test/netfox/servers/network-history-server.test.gd @@ -5,24 +5,30 @@ func get_suite_name() -> String: var servers: TestingServers var subject: StateNode +var nodes: Array[Node3D] = [] func before_case(__) -> void: # Makes sure local peer is 1, otherwise identifiers get random local IDs Vest.get_tree().root.multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new() - NetworkMocks.set_tick(0, 0) + NetworkMocks.set_tick(10, 0) servers = await TestingServers.create() subject = await create_subject() func after_case(__) -> void: servers.queue_free() + NetworkHistoryServer.deregister(subject) RollbackLivenessServer.deregister(subject) subject.queue_free() + for node in nodes: + node.queue_free() + nodes.clear() + func suite() -> void: define("_merge_rollback_state()", func(): test("should overwrite auth state for same tick", func(): - var node := await get_node() + var node := await get_node("OverwriteAuth") var history_server := servers.history_server() var baseline := _Snapshot.of(0, [ @@ -47,6 +53,26 @@ func suite() -> void: [node, "position", Vector3.ONE] ], [node])) ) + test("should truncate newer auth history for merged subject only", func(): + var rewound_node := await get_node("Rewound") + var history_server := servers.history_server() + + history_server.register_rollback_state(rewound_node, "position") + + rewound_node.position = Vector3(1, 0, 0) + history_server._record_rollback_state(5) + + rewound_node.position = Vector3(2, 0, 0) + history_server._record_rollback_state(6) + + var authoritative_snapshot := _Snapshot.of(5, [ + [rewound_node, "position", Vector3(100, 0, 0)] + ], [rewound_node]) + + expect_equal(history_server.get_latest_state_tick_for([rewound_node], NetworkTime.tick), 6) + history_server._merge_rollback_state(authoritative_snapshot) + expect_equal(history_server.get_latest_state_tick_for([rewound_node], NetworkTime.tick), 5) + ) ) define("push_rollback_state()", func(): test("should create a snapshot at spawn tick", func(): @@ -145,8 +171,10 @@ class StateNode extends Node: func _rollback_destroy() -> void: destroyed = true -func get_node() -> Node3D: +func get_node(name: String) -> Node3D: var node := Node3D.new() + node.name = name + nodes.append(node) Vest.get_tree().root.add_child.call_deferred(node) await node.ready From e7cbd1e423f4eab64da45ec8313587a2dd908b7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20G=C3=A1lffy?= Date: Wed, 8 Jul 2026 21:06:20 +0200 Subject: [PATCH 2/5] adj --- addons/netfox.internals/history-buffer.gd | 14 ++++++++ .../netfox/servers/data/per-object-history.gd | 16 ++------- .../netfox/servers/network-history-server.gd | 6 ++-- .../servers/data/per-object-history.test.gd | 34 +++++++++++++++++++ 4 files changed, 53 insertions(+), 17 deletions(-) create mode 100644 test/netfox/servers/data/per-object-history.test.gd diff --git a/addons/netfox.internals/history-buffer.gd b/addons/netfox.internals/history-buffer.gd index 4aa793e99..b17930394 100644 --- a/addons/netfox.internals/history-buffer.gd +++ b/addons/netfox.internals/history-buffer.gd @@ -133,6 +133,20 @@ func get_latest_at(at: int) -> Variant: func clear(): _tail = _head + _previous.fill(_head) + +# TODO: Test +func truncate_after(at: int) -> void: + if _head <= at: + # Nothing to do + return + + if at < _tail: + # `at` is earlier than tail, need to get rid of all data + clear() + return + + _head = at + 1 func is_empty() -> bool: return size() == 0 diff --git a/addons/netfox/servers/data/per-object-history.gd b/addons/netfox/servers/data/per-object-history.gd index 036dd6171..51b57ad1b 100644 --- a/addons/netfox/servers/data/per-object-history.gd +++ b/addons/netfox/servers/data/per-object-history.gd @@ -98,18 +98,8 @@ func get_property(tick: int, subject: Object, property: NodePath, default: Varia return snapshot.get_value(property, default) func truncate_after(tick: int, subject: Object) -> void: - if not _data.has(subject): - return - - var history: _HistoryBuffer = _data[subject] - if history.is_empty(): + var history: _HistoryBuffer = _data.get(subject, null) + if history == null or history.is_empty(): return - var truncated_history := _HistoryBuffer.new(_history_size) - var latest_tick: int = mini(tick, history.get_latest_index()) - for kept_tick in range(history.get_earliest_index(), latest_tick + 1): - if not history.has_at(kept_tick): - continue - truncated_history.set_at(kept_tick, history.get_at(kept_tick)) - - _data[subject] = truncated_history + history.truncate_after(tick) diff --git a/addons/netfox/servers/network-history-server.gd b/addons/netfox/servers/network-history-server.gd index 6d0f8c4cd..d7c95185f 100644 --- a/addons/netfox/servers/network-history-server.gd +++ b/addons/netfox/servers/network-history-server.gd @@ -290,12 +290,10 @@ func _merge_history(snapshot: _Snapshot, history: _PerObjectHistory, reverse: bo for subject in snapshot.get_subjects(): # When receiving auth snapshots from the past, clear all newer history. - # This prevents old (potentially incorrect) auth states from poisoning hisotory + # This prevents old (potentially incorrect) auth states from poisoning history # and preventing local resimulation from recording new correct states. if not reverse and history == _rb_state_history and snapshot.is_auth(subject): - var latest_tick: int = history.get_latest_tick(NetworkTime.tick, subject) - if latest_tick > tick: - history.truncate_after(tick, subject) + history.truncate_after(tick, subject) var object_snapshot := history.ensure_snapshot(tick, subject, not reverse) if object_snapshot == null: diff --git a/test/netfox/servers/data/per-object-history.test.gd b/test/netfox/servers/data/per-object-history.test.gd new file mode 100644 index 000000000..3d59c89e2 --- /dev/null +++ b/test/netfox/servers/data/per-object-history.test.gd @@ -0,0 +1,34 @@ +extends VestTest + +func get_suite_name() -> String: + return "_PerObjectHistory" + +func suite() -> void: + define("truncate_after()", func(): + test("should ignore unknown subject", func(): + var history := _PerObjectHistory.new(16) + var subject := Node.new() + + history.truncate_after(2, subject) + expect_does_not_contain(history.subjects(), subject) + + subject.queue_free() + ) + + test("should truncate", func(): + var history := _PerObjectHistory.new(16) + var subject := Node.new() + + history.set_property(0, subject, "hp", 12.) + history.set_property(1, subject, "hp", 16.) + history.set_property(2, subject, "hp", 20.) + history.set_property(3, subject, "hp", 24.) + + history.truncate_after(2, subject) + + expect_contains(history.subjects(), subject) + expect_equal(history.get_latest_tick(4, subject), 2) + + subject.queue_free() + ) + ) From 0ee27dc3263fb95fe2881345ea1b704e9921a5eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20G=C3=A1lffy?= Date: Wed, 8 Jul 2026 21:06:27 +0200 Subject: [PATCH 3/5] lint --- .../servers/data/per-object-history.test.gd | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/netfox/servers/data/per-object-history.test.gd b/test/netfox/servers/data/per-object-history.test.gd index 3d59c89e2..da539ace8 100644 --- a/test/netfox/servers/data/per-object-history.test.gd +++ b/test/netfox/servers/data/per-object-history.test.gd @@ -8,27 +8,27 @@ func suite() -> void: test("should ignore unknown subject", func(): var history := _PerObjectHistory.new(16) var subject := Node.new() - + history.truncate_after(2, subject) expect_does_not_contain(history.subjects(), subject) - + subject.queue_free() ) - - test("should truncate", func(): + + test("should truncate", func(): var history := _PerObjectHistory.new(16) var subject := Node.new() - + history.set_property(0, subject, "hp", 12.) history.set_property(1, subject, "hp", 16.) history.set_property(2, subject, "hp", 20.) history.set_property(3, subject, "hp", 24.) - + history.truncate_after(2, subject) - + expect_contains(history.subjects(), subject) expect_equal(history.get_latest_tick(4, subject), 2) - + subject.queue_free() ) ) From d835f27328cee9fab0d5fb6c2d272700007670cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20G=C3=A1lffy?= Date: Wed, 8 Jul 2026 21:06:33 +0200 Subject: [PATCH 4/5] bv --- addons/netfox.extras/plugin.cfg | 2 +- addons/netfox.internals/plugin.cfg | 2 +- addons/netfox.noray/plugin.cfg | 2 +- addons/netfox/plugin.cfg | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/addons/netfox.extras/plugin.cfg b/addons/netfox.extras/plugin.cfg index a4e98a53d..438497b58 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.47.0" +version="1.47.1" script="netfox-extras.gd" diff --git a/addons/netfox.internals/plugin.cfg b/addons/netfox.internals/plugin.cfg index 2d48b3f50..1d8aeeb6e 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.47.0" +version="1.47.1" script="plugin.gd" diff --git a/addons/netfox.noray/plugin.cfg b/addons/netfox.noray/plugin.cfg index 4bb769af4..3ee7e7b13 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.47.0" +version="1.47.1" script="netfox-noray.gd" diff --git a/addons/netfox/plugin.cfg b/addons/netfox/plugin.cfg index 8574f6331..02f596d10 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.47.0" +version="1.47.1" script="netfox.gd" From ef932ae4b32d2b2d96fb799b8922290a327b0b97 Mon Sep 17 00:00:00 2001 From: Andrey Romantsev Date: Thu, 9 Jul 2026 00:54:47 +0200 Subject: [PATCH 5/5] refactor: per-peer state sending --- addons/netfox.internals/history-buffer.gd | 14 --- .../netfox/servers/data/per-object-history.gd | 7 -- addons/netfox/servers/data/snapshot.gd | 8 +- .../netfox/servers/network-command-server.gd | 2 +- .../netfox/servers/network-history-server.gd | 6 - .../servers/network-synchronization-server.gd | 88 ++++++++++---- .../servers/data/per-object-history.test.gd | 34 ------ .../servers/network-history-server.test.gd | 34 +----- .../network-synchronization-server.test.gd | 114 ++++++++++++++++++ 9 files changed, 184 insertions(+), 123 deletions(-) delete mode 100644 test/netfox/servers/data/per-object-history.test.gd diff --git a/addons/netfox.internals/history-buffer.gd b/addons/netfox.internals/history-buffer.gd index b17930394..4aa793e99 100644 --- a/addons/netfox.internals/history-buffer.gd +++ b/addons/netfox.internals/history-buffer.gd @@ -133,20 +133,6 @@ func get_latest_at(at: int) -> Variant: func clear(): _tail = _head - _previous.fill(_head) - -# TODO: Test -func truncate_after(at: int) -> void: - if _head <= at: - # Nothing to do - return - - if at < _tail: - # `at` is earlier than tail, need to get rid of all data - clear() - return - - _head = at + 1 func is_empty() -> bool: return size() == 0 diff --git a/addons/netfox/servers/data/per-object-history.gd b/addons/netfox/servers/data/per-object-history.gd index 51b57ad1b..450a4ae96 100644 --- a/addons/netfox/servers/data/per-object-history.gd +++ b/addons/netfox/servers/data/per-object-history.gd @@ -96,10 +96,3 @@ func get_property(tick: int, subject: Object, property: NodePath, default: Varia var snapshot := history.get_at(tick) as _ObjectSnapshot return snapshot.get_value(property, default) - -func truncate_after(tick: int, subject: Object) -> void: - var history: _HistoryBuffer = _data.get(subject, null) - if history == null or history.is_empty(): - return - - history.truncate_after(tick) diff --git a/addons/netfox/servers/data/snapshot.gd b/addons/netfox/servers/data/snapshot.gd index ef99781f5..52aac8463 100644 --- a/addons/netfox/servers/data/snapshot.gd +++ b/addons/netfox/servers/data/snapshot.gd @@ -10,19 +10,17 @@ var _auth_subjects := _Set.new() static func make_patch(from: _Snapshot, to: _Snapshot, tick: int = to.tick) -> _Snapshot: var patch := _Snapshot.new(tick) - for subject in from.get_subjects(): + for subject in to.get_subjects(): assert(is_instance_valid(subject)) - # Target has no knowledge of subject, don't patch - if not to.has_subject(subject): - continue # Only patch to auth subjects if not to.is_auth(subject): continue for property in to._data[subject]: # Target snapshot has different value, patch it - if from.get_property(subject, property) != to.get_property(subject, property): + if not from.has_property(subject, property) or \ + from.get_property(subject, property) != to.get_property(subject, property): patch.set_property(subject, property, to.get_property(subject, property)) patch.set_auth(subject, to.is_auth(subject)) diff --git a/addons/netfox/servers/network-command-server.gd b/addons/netfox/servers/network-command-server.gd index b32ea1de4..9dbd2e52a 100644 --- a/addons/netfox/servers/network-command-server.gd +++ b/addons/netfox/servers/network-command-server.gd @@ -41,7 +41,7 @@ func _ready(): func register_command(handler: Callable, mode: MultiplayerPeer.TransferMode = MultiplayerPeer.TRANSFER_MODE_RELIABLE, channel: int = 0) -> Command: var idx := _next_idx _next_idx += 1 - return register_command_at(idx, handler) + return register_command_at(idx, handler, mode, channel) ## Register a command at a specific index ## [br][br] diff --git a/addons/netfox/servers/network-history-server.gd b/addons/netfox/servers/network-history-server.gd index d7c95185f..6d511f022 100644 --- a/addons/netfox/servers/network-history-server.gd +++ b/addons/netfox/servers/network-history-server.gd @@ -289,12 +289,6 @@ func _merge_history(snapshot: _Snapshot, history: _PerObjectHistory, reverse: bo return false for subject in snapshot.get_subjects(): - # When receiving auth snapshots from the past, clear all newer history. - # This prevents old (potentially incorrect) auth states from poisoning history - # and preventing local resimulation from recording new correct states. - if not reverse and history == _rb_state_history and snapshot.is_auth(subject): - history.truncate_after(tick, subject) - var object_snapshot := history.ensure_snapshot(tick, subject, not reverse) if object_snapshot == null: _logger.warning("Snapshot being merged is out of bounds! (@%d)", [tick]) diff --git a/addons/netfox/servers/network-synchronization-server.gd b/addons/netfox/servers/network-synchronization-server.gd index 430b2ac95..0acf7b952 100644 --- a/addons/netfox/servers/network-synchronization-server.gd +++ b/addons/netfox/servers/network-synchronization-server.gd @@ -39,6 +39,8 @@ var _rb_full_scheduler := _IntervalScheduler.new(_rb_full_interval) var _input_redundancy := NetworkRollback.input_redundancy +var _rb_sent_state_history := {} # peer to _HistoryBuffer of _Snapshot + var _last_sync_state_sent := _Snapshot.new(0) var _sync_enable_diffs := ProjectSettings.get_setting("netfox/state_synchronizer/enable_diff_states", true) as bool var _sync_full_interval := ProjectSettings.get_setting("netfox/state_synchronizer/full_state_interval", 24) as int @@ -66,6 +68,46 @@ static var _logger := NetfoxLogger._for_netfox("NetworkSynchronizationServer") signal _on_input(snapshot: _Snapshot) signal _on_state(snapshot: _Snapshot) +func _get_peer_rb_sent_history(peer: int) -> _HistoryBuffer: + if not _rb_sent_state_history.has(peer): + _rb_sent_state_history[peer] = _HistoryBuffer.new(NetworkRollback.history_limit) + return _rb_sent_state_history[peer] + +func _get_last_sent_rollback_state(peer: int, tick: int) -> _Snapshot: + var history: _HistoryBuffer = _rb_sent_state_history.get(peer, null) + if history == null or not history.has_latest_at(tick): + return null + return history.get_latest_at(tick) + +func _remember_sent_rollback_state(peer: int, snapshot: _Snapshot) -> void: + var reference := _get_last_sent_rollback_state(peer, snapshot.tick) + var remembered := reference.duplicate() if reference else _Snapshot.new(snapshot.tick) + for subject in snapshot.get_subjects(): + for property in snapshot.get_subject_properties(subject): + remembered.set_property(subject, property, snapshot.get_property(subject, property)) + remembered.set_auth(subject, snapshot.is_auth(subject)) + remembered.tick = snapshot.tick + _get_peer_rb_sent_history(peer).set_at(snapshot.tick, remembered) + +func _make_peer_snapshot(snapshot: _Snapshot, peer: int, properties: _PropertyPool) -> _Snapshot: + var result := _Snapshot.new(snapshot.tick) + for subject in properties.get_subjects(): + if not _is_node_visible_to(peer, subject): + continue + if not snapshot.is_auth(subject): + continue + + var has_property := false + for property in properties.get_properties_of(subject): + if not snapshot.has_property(subject, property): + continue + result.set_property(subject, property, snapshot.get_property(subject, property)) + has_property = true + + if has_property: + result.set_auth(subject, true) + return result + ## Register a [param property] of [param node] to be synchronized ## as rollback state func register_rollback_state(node: Node, property: NodePath) -> void: @@ -209,37 +251,33 @@ func _synchronize_state(tick: int) -> void: if not _rb_enable_diffs: is_full = true - # Check if we have history to diff to - var reference_snapshot := NetworkHistoryServer._get_rollback_state_snapshot(tick - 1) - if not reference_snapshot: - is_full = true + for peer in multiplayer.get_peers(): + var peer_snapshot := _make_peer_snapshot(snapshot, peer, _rb_owned_state_properties) + if peer_snapshot.is_empty(): + continue - if is_full: - # Send full states - for peer in multiplayer.get_peers(): - var filter := func(subject): return _is_node_visible_to(peer, subject) + var peer_is_full := is_full + var reference_snapshot := _get_last_sent_rollback_state(peer, tick) + if reference_snapshot == null: + peer_is_full = true - var packets := _dense_serializer.write_for(peer, snapshot, _rb_owned_state_properties, filter) + if peer_is_full: + var packets := _dense_serializer.write_for(peer, peer_snapshot, _rb_owned_state_properties) for packet in packets: _cmd_full_state.send(packet, peer) - - NetworkPerformance.push_full_state_props(snapshot.size()) - NetworkPerformance.push_sent_state_props(snapshot.size()) - else: - var diff := _Snapshot.make_patch(reference_snapshot, snapshot) - if diff.is_empty(): - # Nothing changed, don't send anything - return - - # Send diff states - for peer in multiplayer.get_peers(): - var filter := func(subject): return _is_node_visible_to(peer, subject) - - var packets := _sparse_serializer.write_for(peer, diff, _rb_owned_state_properties, filter) + _remember_sent_rollback_state(peer, peer_snapshot) + NetworkPerformance.push_full_state_props(peer_snapshot.size()) + NetworkPerformance.push_sent_state_props(peer_snapshot.size()) + else: + var diff := _Snapshot.make_patch(reference_snapshot, peer_snapshot) + if diff.is_empty(): + continue + + var packets := _sparse_serializer.write_for(peer, diff, _rb_owned_state_properties) for packet in packets: _cmd_diff_state.send(packet, peer) - - NetworkPerformance.push_full_state_props(snapshot.size()) + _remember_sent_rollback_state(peer, diff) + NetworkPerformance.push_full_state_props(peer_snapshot.size()) NetworkPerformance.push_sent_state_props(diff.size()) func _synchronize_sync_state(tick: int) -> void: diff --git a/test/netfox/servers/data/per-object-history.test.gd b/test/netfox/servers/data/per-object-history.test.gd deleted file mode 100644 index da539ace8..000000000 --- a/test/netfox/servers/data/per-object-history.test.gd +++ /dev/null @@ -1,34 +0,0 @@ -extends VestTest - -func get_suite_name() -> String: - return "_PerObjectHistory" - -func suite() -> void: - define("truncate_after()", func(): - test("should ignore unknown subject", func(): - var history := _PerObjectHistory.new(16) - var subject := Node.new() - - history.truncate_after(2, subject) - expect_does_not_contain(history.subjects(), subject) - - subject.queue_free() - ) - - test("should truncate", func(): - var history := _PerObjectHistory.new(16) - var subject := Node.new() - - history.set_property(0, subject, "hp", 12.) - history.set_property(1, subject, "hp", 16.) - history.set_property(2, subject, "hp", 20.) - history.set_property(3, subject, "hp", 24.) - - history.truncate_after(2, subject) - - expect_contains(history.subjects(), subject) - expect_equal(history.get_latest_tick(4, subject), 2) - - subject.queue_free() - ) - ) diff --git a/test/netfox/servers/network-history-server.test.gd b/test/netfox/servers/network-history-server.test.gd index 7dae4d171..eb421ee22 100644 --- a/test/netfox/servers/network-history-server.test.gd +++ b/test/netfox/servers/network-history-server.test.gd @@ -5,30 +5,24 @@ func get_suite_name() -> String: var servers: TestingServers var subject: StateNode -var nodes: Array[Node3D] = [] func before_case(__) -> void: # Makes sure local peer is 1, otherwise identifiers get random local IDs Vest.get_tree().root.multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new() - NetworkMocks.set_tick(10, 0) + NetworkMocks.set_tick(0, 0) servers = await TestingServers.create() subject = await create_subject() func after_case(__) -> void: servers.queue_free() - NetworkHistoryServer.deregister(subject) RollbackLivenessServer.deregister(subject) subject.queue_free() - for node in nodes: - node.queue_free() - nodes.clear() - func suite() -> void: define("_merge_rollback_state()", func(): test("should overwrite auth state for same tick", func(): - var node := await get_node("OverwriteAuth") + var node := await get_node() var history_server := servers.history_server() var baseline := _Snapshot.of(0, [ @@ -53,26 +47,6 @@ func suite() -> void: [node, "position", Vector3.ONE] ], [node])) ) - test("should truncate newer auth history for merged subject only", func(): - var rewound_node := await get_node("Rewound") - var history_server := servers.history_server() - - history_server.register_rollback_state(rewound_node, "position") - - rewound_node.position = Vector3(1, 0, 0) - history_server._record_rollback_state(5) - - rewound_node.position = Vector3(2, 0, 0) - history_server._record_rollback_state(6) - - var authoritative_snapshot := _Snapshot.of(5, [ - [rewound_node, "position", Vector3(100, 0, 0)] - ], [rewound_node]) - - expect_equal(history_server.get_latest_state_tick_for([rewound_node], NetworkTime.tick), 6) - history_server._merge_rollback_state(authoritative_snapshot) - expect_equal(history_server.get_latest_state_tick_for([rewound_node], NetworkTime.tick), 5) - ) ) define("push_rollback_state()", func(): test("should create a snapshot at spawn tick", func(): @@ -171,10 +145,8 @@ class StateNode extends Node: func _rollback_destroy() -> void: destroyed = true -func get_node(name: String) -> Node3D: +func get_node() -> Node3D: var node := Node3D.new() - node.name = name - nodes.append(node) Vest.get_tree().root.add_child.call_deferred(node) await node.ready diff --git a/test/netfox/servers/network-synchronization-server.test.gd b/test/netfox/servers/network-synchronization-server.test.gd index f2da6c67e..13d05c51c 100644 --- a/test/netfox/servers/network-synchronization-server.test.gd +++ b/test/netfox/servers/network-synchronization-server.test.gd @@ -40,6 +40,120 @@ func suite() -> void: test("should submit owned", func(): skip() ) + + test("Includes newly authoritative subjects missing from the baseline", func() -> void: + var host: RefCounted = RefCounted.new() + var remote_player: RefCounted = RefCounted.new() + var position: NodePath = NodePath("position") + var reference: _Snapshot = _Snapshot.new(100) + reference.set_property(host, position, Vector3.ZERO) + reference.set_auth(host, true) + + var resimulated: _Snapshot = _Snapshot.new(100) + resimulated.set_property(host, position, Vector3(1, 0, 0)) + resimulated.set_auth(host, true) + resimulated.set_property(remote_player, position, Vector3(2, 0, 0)) + resimulated.set_auth(remote_player, true) + + var diff: _Snapshot = _Snapshot.make_patch(reference, resimulated) + + expect_true(diff.has_property(remote_player, position)) + expect_equal(diff.get_property(remote_player, position), Vector3(2, 0, 0)) + ) + ) + + define("_make_peer_snapshot()", func(): + test("should include only visible owned auth state for target peer", func(): + var visible_node := await get_node("visible") + var hidden_node := await get_node("hidden") + var remote_node := await get_node("remote") + remote_node.set_multiplayer_authority(2) + + visible_node.position = Vector3.ONE + hidden_node.position = Vector3.UP + remote_node.position = Vector3.RIGHT + + servers.synchronization_server().register_rollback_state(visible_node, ^"position") + servers.synchronization_server().register_rollback_state(hidden_node, ^"position") + servers.synchronization_server().register_rollback_state(remote_node, ^"position") + + var filter := PeerVisibilityFilter.new() + filter.default_visibility = false + filter.set_visibility_for(2, true) + filter.update_visibility(PackedInt32Array([2, 3])) + servers.synchronization_server().register_visibility_filter(visible_node, filter) + + var hidden_filter := PeerVisibilityFilter.new() + hidden_filter.default_visibility = false + hidden_filter.update_visibility(PackedInt32Array([2, 3])) + servers.synchronization_server().register_visibility_filter(hidden_node, hidden_filter) + + var snapshot := _Snapshot.of(5, [ + [visible_node, ^"position", visible_node.position], + [hidden_node, ^"position", hidden_node.position], + [remote_node, ^"position", remote_node.position] + ], [visible_node, hidden_node, remote_node]) + + var peer_snapshot := servers.synchronization_server()._make_peer_snapshot( + snapshot, + 2, + servers.synchronization_server()._rb_owned_state_properties + ) + + expect(peer_snapshot.has_property(visible_node, ^"position")) + expect_equal(peer_snapshot.get_property(visible_node, ^"position"), Vector3.ONE) + expect(peer_snapshot.is_auth(visible_node)) + expect_not(peer_snapshot.has_subject(hidden_node)) + expect_not(peer_snapshot.has_subject(remote_node)) + + visible_node.queue_free() + hidden_node.queue_free() + remote_node.queue_free() + ) + ) + + define("rollback sent state history", func(): + test("should remember rollback state separately per peer", func(): + var node := await get_node("subject") + var snapshot_tick_5 := _Snapshot.of(5, [ + [node, ^"position", Vector3.ONE] + ], [node]) + var snapshot_tick_8 := _Snapshot.of(8, [ + [node, ^"position", Vector3.UP] + ], [node]) + + servers.synchronization_server()._remember_sent_rollback_state(2, snapshot_tick_5) + servers.synchronization_server()._remember_sent_rollback_state(3, snapshot_tick_8) + + var peer_2_latest := servers.synchronization_server()._get_last_sent_rollback_state(2, 8) + var peer_3_latest := servers.synchronization_server()._get_last_sent_rollback_state(3, 8) + + expect_not_null(peer_2_latest) + expect_not_null(peer_3_latest) + expect_equal(peer_2_latest.tick, 5) + expect_equal(peer_2_latest.get_property(node, ^"position"), Vector3.ONE) + expect_equal(peer_3_latest.tick, 8) + expect_equal(peer_3_latest.get_property(node, ^"position"), Vector3.UP) + expect_null(servers.synchronization_server()._get_last_sent_rollback_state(4, 8)) + + node.queue_free() + ) + + test("should duplicate remembered rollback state", func(): + var node := await get_node("subject") + var snapshot := _Snapshot.of(5, [ + [node, ^"position", Vector3.ONE] + ], [node]) + + servers.synchronization_server()._remember_sent_rollback_state(2, snapshot) + snapshot.set_property(node, ^"position", Vector3.ZERO) + + var remembered := servers.synchronization_server()._get_last_sent_rollback_state(2, 5) + expect_not_null(remembered) + expect_equal(remembered.get_property(node, ^"position"), Vector3.ONE) + + node.queue_free() + ) ) define("synchronize_sync_state()", func():