b0c83af092
Complete replacement of the tactical-shooter project with the netfox-cs-sample (MIT) — a CS 1.6 inspired multiplayer FPS built with Godot 4 and netfox. ## What's new - Full CS-style gameplay: teams (T/CT), rounds, economy, buy menu - 6 weapons: Knife, Glock, USP, AK-47, M4A1, AWP - Bomb plant/defuse with 2 bombsites - Flashbang & smoke grenades - Proper netfox rollback netcode at 64 tick - Network popup UI for host/join - HUD, crosshair, round timer, scoreboard - All netfox singletons registered as autoloads (works in exported builds) ## Architecture - Listen-server (host from client, no dedicated server binary) - Multiplayer-fps game lives at examples/multiplayer-fps/ - Netfox addons registered as autoloads for exported build compat - Godot 4.7 with Forward+ renderer ## Removed - Old headless-server architecture (client_main, server_main, player.gd, etc.) - Custom netfox bootstrap with ENet fallback - Old ChaffGames FPS template (2,420 lines, 844 KB) - SimulationServer GDExtension stub - Godot-jolt physics (netfox sample uses default Godot physics) - Duplicate weapon_data.gd, anti_cheat.gd, round_manager.gd, etc. - Server browser API Python venv (87 MB) - test_range map and modular assets ## Preserved - Git history - Server config at config/default_server_config.cfg - Windows export preset - Build directory (gitignored) Co-authored-by: naxIO <naxIO@users.noreply.github.com>
382 lines
14 KiB
GDScript
382 lines
14 KiB
GDScript
extends Node
|
|
class_name _NetworkSynchronizationServer
|
|
|
|
## Synchronizes properties over the network
|
|
##
|
|
## Handles synchronization of rollback and state properties (
|
|
## [RollbackSynchronizer] and [StateSynchronizer] ), while respecting visibility
|
|
## filters and schemas for serialization.
|
|
## [br][br]
|
|
## Packets are sent per tick, instead of per object. So for every simulated
|
|
## rollback tick, a packet is sent with states, and for every recorded input,
|
|
## a packet is sent with the inputs.
|
|
## [br][br]
|
|
## Optionally, diff states can be used, sending only the property values that
|
|
## have changed, saving on bandwidth.
|
|
|
|
# Dependencies
|
|
var _command_server: _NetworkCommandServer
|
|
var _history_server: _NetworkHistoryServer
|
|
var _identity_server: _NetworkIdentityServer
|
|
var _simulation_server: _RollbackSimulationServer
|
|
|
|
# Configuration
|
|
var _rb_input_properties := _PropertyPool.new()
|
|
var _rb_state_properties := _PropertyPool.new()
|
|
var _rb_owned_input_properties := _PropertyPool.new()
|
|
var _rb_owned_state_properties := _PropertyPool.new()
|
|
var _sync_state_properties := _PropertyPool.new()
|
|
var _sync_owned_state_properties := _PropertyPool.new()
|
|
|
|
var _visibility_filters := {} # Node to PeerVisibilityFilter
|
|
|
|
var _rb_enable_input_broadcast := ProjectSettings.get_setting("netfox/rollback/enable_input_broadcast", false) as bool
|
|
var _rb_enable_diffs := NetworkRollback.enable_diff_states
|
|
var _rb_full_interval := ProjectSettings.get_setting("netfox/rollback/full_state_interval", 24) as int
|
|
var _rb_full_scheduler := _IntervalScheduler.new(_rb_full_interval)
|
|
|
|
var _input_redundancy := NetworkRollback.input_redundancy
|
|
|
|
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
|
|
var _sync_full_scheduler := _IntervalScheduler.new(_sync_full_interval)
|
|
|
|
var _schemas := _NetworkSchema.new()
|
|
|
|
var _dense_serializer: _DenseSnapshotSerializer
|
|
var _sparse_serializer: _SparseSnapshotSerializer
|
|
var _redundant_serializer: _RedundantSnapshotSerializer
|
|
|
|
var _cmd_full_state: NetworkCommandServer.Command
|
|
var _cmd_diff_state: NetworkCommandServer.Command
|
|
var _cmd_input: NetworkCommandServer.Command
|
|
|
|
var _cmd_full_sync: NetworkCommandServer.Command
|
|
var _cmd_diff_sync: NetworkCommandServer.Command
|
|
|
|
static var _logger := NetfoxLogger._for_netfox("NetworkSynchronizationServer")
|
|
|
|
signal _on_input(snapshot: _Snapshot)
|
|
signal _on_state(snapshot: _Snapshot)
|
|
|
|
## Register a [param]property[/param] of [param]node[/param] to be synchronized
|
|
## as rollback state
|
|
func register_rollback_state(node: Node, property: NodePath) -> void:
|
|
_rb_state_properties.add(node, property)
|
|
if node.is_multiplayer_authority():
|
|
_rb_owned_state_properties.add(node, property)
|
|
|
|
## Deregister a [param]property[/param] of [param]node[/param] from being
|
|
## synchronized as rollback state
|
|
func deregister_rollback_state(node: Node, property: NodePath) -> void:
|
|
_rb_state_properties.erase(node, property)
|
|
_rb_owned_state_properties.erase(node, property)
|
|
|
|
## Register a [param]property[/param] of [param]node[/param] to be synchronized
|
|
## as rollback input
|
|
func register_rollback_input(node: Node, property: NodePath) -> void:
|
|
_rb_input_properties.add(node, property)
|
|
if node.is_multiplayer_authority():
|
|
_rb_owned_input_properties.add(node, property)
|
|
|
|
## Deregister a [param]property[/param] of [param]node[/param] from being
|
|
## synchronized as rollback input
|
|
func deregister_rollback_input(node: Node, property: NodePath) -> void:
|
|
_rb_input_properties.erase(node, property)
|
|
_rb_owned_input_properties.erase(node, property)
|
|
|
|
## Register a [param]property[/param] of [param]node[/param] to be synchronized
|
|
## as synchronized state
|
|
func register_sync_state(node: Node, property: NodePath) -> void:
|
|
_sync_state_properties.add(node, property)
|
|
if node.is_multiplayer_authority():
|
|
_sync_owned_state_properties.add(node, property)
|
|
|
|
## Deregister a [param]property[/param] of [param]node[/param] from being
|
|
## synchronized as synchronized state
|
|
func deregister_sync_state(node: Node, property: NodePath) -> void:
|
|
_sync_state_properties.erase(node, property)
|
|
_sync_owned_state_properties.erase(node, property)
|
|
|
|
## Register a [param]serializer[/param] to use when transmitting
|
|
## [param]property[/param] of [param]node[/param] over the network
|
|
func register_schema(node: Node, property: NodePath, serializer: NetworkSchemaSerializer) -> void:
|
|
_schemas.add(node, property, serializer)
|
|
|
|
## Deregister any serializers used for [param]property[/param] on
|
|
## [param]node[/param] when transmitting over the network
|
|
func deregister_schema(node: Node, property: NodePath) -> void:
|
|
_schemas.erase(node, property)
|
|
|
|
## Deregister all serializers registered for any properties of
|
|
## [param]node[/param]
|
|
func deregister_schema_for(node: Node) -> void:
|
|
_schemas.erase_subject(node)
|
|
|
|
## Register a visibility [param]filter[/param] for use with [param]node[/param]
|
|
func register_visibility_filter(node: Node, filter: PeerVisibilityFilter) -> void:
|
|
_visibility_filters[node] = filter
|
|
|
|
## Deregister the visibility filter used for [param]node[/param]
|
|
func deregister_visibility_filter(node: Node) -> void:
|
|
_visibility_filters.erase(node)
|
|
|
|
## Deregister any and all settings associated with [param]node[/param]
|
|
func deregister(node: Node) -> void:
|
|
_rb_state_properties.erase_subject(node)
|
|
_rb_input_properties.erase_subject(node)
|
|
_rb_owned_state_properties.erase_subject(node)
|
|
_rb_owned_input_properties.erase_subject(node)
|
|
_sync_state_properties.erase_subject(node)
|
|
_visibility_filters.erase(node)
|
|
_schemas.erase_subject(node)
|
|
|
|
func _is_node_visible_to(peer: int, node: Node) -> bool:
|
|
var filter := _visibility_filters.get(node) as PeerVisibilityFilter
|
|
if not filter:
|
|
return true
|
|
else:
|
|
return filter.get_visible_peers().has(peer)
|
|
|
|
func _synchronize_input(tick: int) -> void:
|
|
# We don't own inputs, nothing to synchronize
|
|
if _rb_owned_input_properties.is_empty():
|
|
return
|
|
|
|
var snapshots := [] as Array[_Snapshot]
|
|
var notified_peers := _Set.new()
|
|
|
|
if not _rb_enable_input_broadcast:
|
|
# If input broadcast is off, find which peers need to know our inputs
|
|
# That is all peers who own state controlled by our input
|
|
|
|
# Grab owned input objects
|
|
for input_subject in _rb_owned_input_properties.get_subjects():
|
|
# Grab state objects controlled by input
|
|
var controlled_nodes := RollbackSimulationServer._get_controlled_by(input_subject)
|
|
|
|
# Notify peers owning nodes about the input
|
|
for node in controlled_nodes:
|
|
notified_peers.add(node.get_multiplayer_authority())
|
|
else:
|
|
# If input broadcast is on, send inputs to everyone
|
|
for peer in multiplayer.get_peers():
|
|
notified_peers.add(peer)
|
|
|
|
# Make sure to not send input to ourselves
|
|
notified_peers.erase(multiplayer.get_unique_id())
|
|
|
|
# Prepare snapshot package
|
|
for offset in _input_redundancy:
|
|
# Grab snapshot from NetworkHistoryServer
|
|
var snapshot := NetworkHistoryServer._get_rollback_input_snapshot(tick - offset)
|
|
if not snapshot:
|
|
break
|
|
|
|
_logger.trace("Submitting input: %s", [snapshot])
|
|
snapshots.append(snapshot)
|
|
|
|
_logger.trace("Submitting input to peers: %s", [notified_peers])
|
|
for peer in notified_peers:
|
|
var data := _redundant_serializer.write_for(peer, snapshots, _rb_owned_input_properties)
|
|
_cmd_input.send(data, peer)
|
|
|
|
func _synchronize_state(tick: int) -> void:
|
|
# We don't own state, nothing to synchronize
|
|
if _rb_owned_state_properties.is_empty():
|
|
return
|
|
|
|
# Grab snapshot from NetworkHistoryServer
|
|
var snapshot := NetworkHistoryServer._get_rollback_state_snapshot(tick)
|
|
if not snapshot:
|
|
# No data for tick
|
|
return
|
|
|
|
if snapshot.is_empty():
|
|
# Nothing to send
|
|
return
|
|
|
|
# Figure out whether to send full- or diff state
|
|
var is_full := _rb_full_scheduler.is_now()
|
|
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
|
|
|
|
if is_full:
|
|
# Send full states
|
|
for peer in multiplayer.get_peers():
|
|
var filter := func(subject): return _is_node_visible_to(peer, subject)
|
|
|
|
var data := _dense_serializer.write_for(peer, snapshot, _rb_owned_state_properties, filter)
|
|
if data.is_empty():
|
|
# Peer can't see anything, send nothing
|
|
continue
|
|
|
|
_cmd_full_state.send(data, 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 data := _sparse_serializer.write_for(peer, diff, _rb_owned_state_properties, filter)
|
|
if data.is_empty():
|
|
# Peer can't see any changes, send nothing
|
|
continue
|
|
|
|
_cmd_diff_state.send(data, peer)
|
|
|
|
NetworkPerformance.push_full_state_props(snapshot.size())
|
|
NetworkPerformance.push_sent_state_props(diff.size())
|
|
|
|
func _synchronize_sync_state(tick: int) -> void:
|
|
# We don't own sync state, nothing to synchronize
|
|
if _sync_owned_state_properties.is_empty():
|
|
return
|
|
|
|
# Grab snapshot from NetworkHistoryServer
|
|
var snapshot := NetworkHistoryServer._get_synchronizer_state_snapshot(tick)
|
|
if not snapshot:
|
|
return
|
|
|
|
# Figure out whether to send full- or diff state
|
|
var is_full := _sync_full_scheduler.is_now()
|
|
if not _sync_enable_diffs:
|
|
is_full = true
|
|
|
|
if is_full:
|
|
# Send full states
|
|
for peer in multiplayer.get_peers():
|
|
var filter := func(subject): return _is_node_visible_to(peer, subject)
|
|
|
|
var data := _dense_serializer.write_for(peer, snapshot, _sync_owned_state_properties, filter)
|
|
if data.is_empty():
|
|
# Peer can't see anything, send nothing
|
|
continue
|
|
|
|
_cmd_full_sync.send(data, peer)
|
|
|
|
NetworkPerformance.push_full_state_props(snapshot.size())
|
|
NetworkPerformance.push_sent_state_props(snapshot.size())
|
|
else:
|
|
var diff := _Snapshot.make_patch(_last_sync_state_sent, snapshot)
|
|
|
|
# Send diffs
|
|
for peer in multiplayer.get_peers():
|
|
var filter := func(subject): return _is_node_visible_to(peer, subject)
|
|
|
|
var data := _sparse_serializer.write_for(peer, diff, _sync_owned_state_properties, filter)
|
|
if data.is_empty():
|
|
# Peer can't see anything, send nothing
|
|
continue
|
|
|
|
_cmd_diff_sync.send(data, peer)
|
|
|
|
NetworkPerformance.push_full_state_props(snapshot.size())
|
|
NetworkPerformance.push_sent_state_props(diff.size())
|
|
|
|
# Remember last sent state for diffing
|
|
# NOTE: This is a shared instance, theoretically shouldn't screw things up
|
|
_last_sync_state_sent = snapshot
|
|
|
|
func _init(
|
|
p_command_server: _NetworkCommandServer = null,
|
|
p_history_server: _NetworkHistoryServer = null,
|
|
p_identity_server: _NetworkIdentityServer = null,
|
|
p_simulation_server: _RollbackSimulationServer = null
|
|
):
|
|
_command_server = p_command_server
|
|
_history_server = p_history_server
|
|
_identity_server = p_identity_server
|
|
_simulation_server = p_simulation_server
|
|
|
|
func _ready():
|
|
# Ensure dependencies
|
|
if not _command_server: _command_server = NetworkCommandServer
|
|
if not _history_server: _history_server = NetworkHistoryServer
|
|
if not _identity_server: _identity_server = NetworkIdentityServer
|
|
if not _simulation_server: _simulation_server = RollbackSimulationServer
|
|
|
|
# Setup serializers
|
|
_dense_serializer = _DenseSnapshotSerializer.new(_schemas, _identity_server)
|
|
_sparse_serializer = _SparseSnapshotSerializer.new(_schemas, _identity_server)
|
|
_redundant_serializer = _RedundantSnapshotSerializer.new(_schemas, _identity_server)
|
|
|
|
# Setup commands
|
|
_cmd_full_state = _command_server.register_command(_handle_full_state, MultiplayerPeer.TRANSFER_MODE_UNRELIABLE)
|
|
_cmd_diff_state = _command_server.register_command(_handle_diff_state, MultiplayerPeer.TRANSFER_MODE_UNRELIABLE)
|
|
_cmd_input = _command_server.register_command(_handle_input, MultiplayerPeer.TRANSFER_MODE_UNRELIABLE)
|
|
|
|
_cmd_full_sync = _command_server.register_command(_handle_full_sync, MultiplayerPeer.TRANSFER_MODE_UNRELIABLE_ORDERED)
|
|
_cmd_diff_sync = _command_server.register_command(_handle_diff_sync, MultiplayerPeer.TRANSFER_MODE_UNRELIABLE_ORDERED)
|
|
|
|
func _handle_input(sender: int, data: PackedByteArray):
|
|
var buffer := StreamPeerBuffer.new()
|
|
buffer.data_array = data
|
|
|
|
var snapshots := _redundant_serializer.read_from(sender, _rb_input_properties, buffer, true)
|
|
|
|
for snapshot in snapshots:
|
|
snapshot.sanitize(sender)
|
|
|
|
_logger.trace("Ingesting input: %s", [snapshot])
|
|
if NetworkHistoryServer._merge_rollback_input(snapshot):
|
|
_on_input.emit(snapshot)
|
|
|
|
func _handle_full_state(sender: int, data: PackedByteArray):
|
|
var buffer := StreamPeerBuffer.new()
|
|
buffer.data_array = data
|
|
|
|
var snapshot := _dense_serializer.read_from(sender, _rb_state_properties, buffer, true)
|
|
|
|
_ingest_state(sender, snapshot)
|
|
|
|
func _handle_diff_state(sender: int, data: PackedByteArray):
|
|
var buffer := StreamPeerBuffer.new()
|
|
buffer.data_array = data
|
|
|
|
var diff := _sparse_serializer.read_from(sender, _rb_state_properties, buffer)
|
|
_logger.trace("Received diff state for @%d", [diff.tick])
|
|
|
|
_ingest_state(sender, diff)
|
|
|
|
func _handle_full_sync(sender: int, data: PackedByteArray):
|
|
var buffer := StreamPeerBuffer.new()
|
|
buffer.data_array = data
|
|
|
|
var snapshot := _dense_serializer.read_from(sender, _sync_state_properties, buffer, true)
|
|
snapshot.sanitize(sender)
|
|
|
|
NetworkHistoryServer._merge_synchronizer_state(snapshot)
|
|
_logger.trace("Ingested sync state: %s", [snapshot])
|
|
|
|
func _handle_diff_sync(sender: int, data: PackedByteArray):
|
|
var buffer := StreamPeerBuffer.new()
|
|
buffer.data_array = data
|
|
|
|
var snapshot := _sparse_serializer.read_from(sender, _sync_state_properties, buffer)
|
|
snapshot.sanitize(sender)
|
|
|
|
NetworkHistoryServer._merge_synchronizer_state(snapshot)
|
|
_logger.trace("Ingested sync diff: %s", [snapshot])
|
|
|
|
func _ingest_state(sender: int, snapshot: _Snapshot) -> void:
|
|
snapshot.sanitize(sender)
|
|
|
|
NetworkHistoryServer._merge_rollback_state(snapshot)
|
|
_logger.trace("Ingested state: %s", [snapshot])
|
|
|
|
_on_state.emit(snapshot)
|