Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion addons/netfox.extras/plugin.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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"
2 changes: 1 addition & 1 deletion addons/netfox.internals/plugin.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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"
2 changes: 1 addition & 1 deletion addons/netfox.noray/plugin.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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"
2 changes: 1 addition & 1 deletion addons/netfox/plugin.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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"
8 changes: 3 additions & 5 deletions addons/netfox/servers/data/snapshot.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
2 changes: 1 addition & 1 deletion addons/netfox/servers/network-command-server.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
88 changes: 63 additions & 25 deletions addons/netfox/servers/network-synchronization-server.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
114 changes: 114 additions & 0 deletions test/netfox/servers/network-synchronization-server.test.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down