Fresh start: replace with naxIO/netfox-cs-sample foundation
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>
This commit is contained in:
@@ -22,43 +22,20 @@ class_name StateSynchronizer
|
||||
## for every tick.
|
||||
## [br][br]
|
||||
## Only considered if [member _NetworkRollback.enable_diff_states] is true.
|
||||
## @deprecated: This can now be configured in the project settings.
|
||||
@export_range(0, 128, 1, "or_greater")
|
||||
var full_state_interval: int = 24 # TODO: Don't tie to a network rollback setting?
|
||||
var full_state_interval: int = 24
|
||||
|
||||
## Ticks to wait between unreliably acknowledging diff states.
|
||||
## [br][br]
|
||||
## This can reduce the amount of properties sent in diff states, due to clients
|
||||
## more often acknowledging received states. To avoid introducing hickups, these
|
||||
## are sent unreliably.
|
||||
## [br][br]
|
||||
## If set to 0, diff states will never be acknowledged. If set to 1, all diff
|
||||
## states will be acknowledged. If set higher, ack's will be sent regularly, but
|
||||
## not for every diff state.
|
||||
## [br][br]
|
||||
## If enabled, it's worth to tune this setting until network traffic is actually
|
||||
## reduced.
|
||||
## [br][br]
|
||||
## Only considered if [member _NetworkRollback.enable_diff_states] is true.
|
||||
## @deprecated: This is no longer used.
|
||||
@export_range(0, 128, 1, "or_greater")
|
||||
var diff_ack_interval: int = 0 # TODO: Don't tie to a network rollback setting?
|
||||
var diff_ack_interval: int = 0
|
||||
|
||||
## Decides which peers will receive updates
|
||||
var visibility_filter := PeerVisibilityFilter.new()
|
||||
|
||||
var _property_cache: PropertyCache
|
||||
var _property_config: _PropertyConfig = _PropertyConfig.new()
|
||||
var _properties_dirty: bool = false
|
||||
|
||||
var _state_history := _PropertyHistoryBuffer.new()
|
||||
|
||||
# Collaborators
|
||||
var _full_state_encoder: _SnapshotHistoryEncoder
|
||||
var _diff_state_encoder: _DiffHistoryEncoder
|
||||
|
||||
# State
|
||||
var _ackd_state: Dictionary = {}
|
||||
var _next_full_state_tick: int
|
||||
var _next_diff_ack_tick: int
|
||||
var _properties := _PropertyPool.new()
|
||||
var _schema_nodes := _Set.new()
|
||||
|
||||
var _is_initialized: bool = false
|
||||
|
||||
@@ -68,24 +45,21 @@ static var _logger := NetfoxLogger._for_netfox("StateSynchronizer")
|
||||
## [br][br]
|
||||
## Call this after any change to configuration.
|
||||
func process_settings() -> void:
|
||||
_property_cache = PropertyCache.new(root)
|
||||
_property_config.set_properties_from_paths(properties, _property_cache)
|
||||
# Remove old configuration
|
||||
for node in _properties.get_subjects():
|
||||
for property in _properties.get_properties_of(node):
|
||||
NetworkHistoryServer.deregister_sync_state(node, property)
|
||||
NetworkSynchronizationServer.deregister_sync_state(node, property)
|
||||
|
||||
_full_state_encoder = _SnapshotHistoryEncoder.new(_state_history, _property_cache)
|
||||
_diff_state_encoder = _DiffHistoryEncoder.new(_state_history, _property_cache)
|
||||
# Register new configuration
|
||||
_properties.set_from_paths(root, properties)
|
||||
for node in _properties.get_subjects():
|
||||
NetworkSynchronizationServer.register_visibility_filter(node, visibility_filter)
|
||||
NetworkIdentityServer.register_node(node)
|
||||
|
||||
_diff_state_encoder.add_properties(_property_config.get_properties())
|
||||
|
||||
_next_full_state_tick = NetworkTime.tick
|
||||
_next_diff_ack_tick = NetworkTime.tick
|
||||
|
||||
# Scatter full state sends, so not all nodes send at the same tick
|
||||
if is_inside_tree():
|
||||
_next_full_state_tick += hash(root.get_path()) % maxi(1, full_state_interval)
|
||||
_next_diff_ack_tick += hash(root.get_path()) % maxi(1, diff_ack_interval)
|
||||
else:
|
||||
_next_full_state_tick += hash(root.name) % maxi(1, full_state_interval)
|
||||
_next_diff_ack_tick += hash(root.name) % maxi(1, diff_ack_interval)
|
||||
for property in _properties.get_properties_of(node):
|
||||
NetworkHistoryServer.register_sync_state(node, property)
|
||||
NetworkSynchronizationServer.register_sync_state(node, property)
|
||||
|
||||
_is_initialized = true
|
||||
|
||||
@@ -103,9 +77,44 @@ func add_state(node: Variant, property: String) -> void:
|
||||
_properties_dirty = true
|
||||
_reprocess_settings.call_deferred()
|
||||
|
||||
## Set the schema for transmitting properties over the network.
|
||||
## [br][br]
|
||||
## The [param schema] must be a dictionary, with the keys being property path
|
||||
## strings, and the values are the associated [NetworkSchemaSerializer] objects.
|
||||
## Properties are interpreted relative to the [member root] node. Properties not
|
||||
## specified in the schema will use a generic fallback serializer. By using the
|
||||
## right serializer for the right property, bandwidth usage can be lowered.
|
||||
## [br][br]
|
||||
## See [NetworkSchemas] for many common serializers.
|
||||
## [br][br]
|
||||
## Example:
|
||||
## [codeblock]
|
||||
## state_synchronizer.set_schema({
|
||||
## ":transform": NetworkSchemas.transform3f32(),
|
||||
## ":velocity": NetworkSchemas.vec3f32()
|
||||
## })
|
||||
## [/codeblock]
|
||||
func set_schema(schema: Dictionary) -> void:
|
||||
# Remove previous schema
|
||||
for node in _schema_nodes:
|
||||
NetworkSynchronizationServer.deregister_schema_for(node)
|
||||
_schema_nodes.clear()
|
||||
|
||||
# Register new schema
|
||||
for prop in schema:
|
||||
var prop_entry := PropertyEntry.parse(root, prop)
|
||||
var serializer := schema[prop] as NetworkSchemaSerializer
|
||||
NetworkSynchronizationServer.register_schema(prop_entry.node, prop_entry.property, serializer)
|
||||
_schema_nodes.add(prop_entry.node)
|
||||
|
||||
func _notification(what) -> void:
|
||||
if what == NOTIFICATION_EDITOR_PRE_SAVE:
|
||||
update_configuration_warnings()
|
||||
elif what == NOTIFICATION_PREDELETE:
|
||||
for node in _properties.get_subjects():
|
||||
NetworkSynchronizationServer.deregister(node)
|
||||
NetworkHistoryServer.deregister(node)
|
||||
NetworkIdentityServer.deregister(node)
|
||||
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
if not root:
|
||||
@@ -120,14 +129,6 @@ func _get_configuration_warnings() -> PackedStringArray:
|
||||
add_state(node, prop)
|
||||
)
|
||||
|
||||
func _connect_signals() -> void:
|
||||
NetworkTime.after_tick.connect(_after_tick)
|
||||
NetworkTime.after_tick_loop.connect(_after_loop)
|
||||
|
||||
func _disconnect_signals() -> void:
|
||||
NetworkTime.after_tick.disconnect(_after_tick)
|
||||
NetworkTime.after_tick_loop.disconnect(_after_loop)
|
||||
|
||||
func _enter_tree() -> void:
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
@@ -137,27 +138,13 @@ func _enter_tree() -> void:
|
||||
if not visibility_filter.get_parent():
|
||||
add_child(visibility_filter)
|
||||
|
||||
_connect_signals.call_deferred()
|
||||
process_settings.call_deferred()
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
_disconnect_signals()
|
||||
|
||||
func _after_tick(_dt: float, tick: int) -> void:
|
||||
if is_multiplayer_authority():
|
||||
# Submit snapshot
|
||||
var state := _PropertySnapshot.extract(_property_config.get_properties())
|
||||
_state_history.set_snapshot(tick, state)
|
||||
_broadcast_state(tick, state)
|
||||
elif not _state_history.is_empty():
|
||||
var state := _state_history.get_history(tick)
|
||||
state.apply(_property_cache)
|
||||
|
||||
func _after_loop() -> void:
|
||||
_state_history.trim(NetworkTime.tick - NetworkRollback.history_limit) # TODO: Don't tie to rollback?
|
||||
func _ready():
|
||||
# Reprocess authority
|
||||
# Important if nodes are pre-placed in the scene - node starts as owned by
|
||||
# us ( offline peer is 1 ), but once we connect, we no longer own the node
|
||||
multiplayer.connected_to_server.connect(process_settings)
|
||||
|
||||
func _reprocess_settings() -> void:
|
||||
if not _properties_dirty:
|
||||
@@ -165,100 +152,3 @@ func _reprocess_settings() -> void:
|
||||
|
||||
_properties_dirty = false
|
||||
process_settings()
|
||||
|
||||
func _broadcast_state(tick: int, state: _PropertySnapshot) -> void:
|
||||
var is_sending_diffs := NetworkRollback.enable_diff_states # TODO: Don't tie to a rollback setting?
|
||||
var is_full_state_tick := not is_sending_diffs or (full_state_interval > 0 and tick > _next_full_state_tick)
|
||||
|
||||
if is_full_state_tick:
|
||||
# Broadcast new full state
|
||||
for peer in visibility_filter.get_rpc_target_peers():
|
||||
_send_full_state(tick, peer)
|
||||
|
||||
# Adjust next full state if sending diffs
|
||||
if is_sending_diffs:
|
||||
_next_full_state_tick = tick + full_state_interval
|
||||
else:
|
||||
# Send diffs to each peer
|
||||
for peer in visibility_filter.get_visible_peers():
|
||||
var reference_tick := _ackd_state.get(peer, -1) as int
|
||||
if reference_tick < 0 or not _state_history.has(reference_tick):
|
||||
# Peer hasn't ack'd any tick, or we don't have the ack'd tick
|
||||
# Send full state
|
||||
_logger.trace("Reference tick @%d not found for peer #%s, sending full tick", [reference_tick, peer])
|
||||
_send_full_state(tick, peer)
|
||||
continue
|
||||
|
||||
# Prepare diff
|
||||
var diff_state_data := _diff_state_encoder.encode(tick, reference_tick, _property_config.get_properties())
|
||||
|
||||
if _diff_state_encoder.get_full_snapshot().size() == _diff_state_encoder.get_encoded_snapshot().size():
|
||||
# State is completely different, send full state
|
||||
_send_full_state(tick, peer)
|
||||
else:
|
||||
# Send only diff
|
||||
_submit_diff_state.rpc_id(peer, diff_state_data, tick, reference_tick)
|
||||
|
||||
# Push metrics
|
||||
NetworkPerformance.push_full_state(_diff_state_encoder.get_full_snapshot())
|
||||
NetworkPerformance.push_sent_state(_diff_state_encoder.get_encoded_snapshot())
|
||||
|
||||
func _send_full_state(tick: int, peer: int = 0) -> void:
|
||||
var full_state_snapshot := _state_history.get_snapshot(tick).as_dictionary()
|
||||
var full_state_data := _full_state_encoder.encode(tick, _property_config.get_properties())
|
||||
|
||||
_submit_full_state.rpc_id(peer, full_state_data, tick)
|
||||
|
||||
if peer <= 0:
|
||||
NetworkPerformance.push_full_state_broadcast(full_state_snapshot)
|
||||
NetworkPerformance.push_sent_state_broadcast(full_state_snapshot)
|
||||
else:
|
||||
NetworkPerformance.push_full_state(full_state_snapshot)
|
||||
NetworkPerformance.push_sent_state(full_state_snapshot)
|
||||
|
||||
# `serialized_state` is a serialized _PropertySnapshot
|
||||
@rpc("any_peer", "unreliable_ordered", "call_remote")
|
||||
func _submit_full_state(data: Array, tick: int) -> void:
|
||||
if not _is_initialized: return
|
||||
|
||||
var sender := multiplayer.get_remote_sender_id()
|
||||
var snapshot := _full_state_encoder.decode(data, _property_config.get_properties_owned_by(sender))
|
||||
|
||||
if not _full_state_encoder.apply(tick, snapshot, sender):
|
||||
# Invalid data
|
||||
return
|
||||
|
||||
if NetworkRollback.enable_diff_states:
|
||||
_ack_full_state.rpc_id(sender, tick)
|
||||
|
||||
# State is a serialized _PropertySnapshot (Dictionary[String, Variant])
|
||||
@rpc("any_peer", "unreliable_ordered", "call_remote")
|
||||
func _submit_diff_state(data: PackedByteArray, tick: int, reference_tick: int) -> void:
|
||||
if not _is_initialized: return
|
||||
|
||||
var sender = multiplayer.get_remote_sender_id()
|
||||
var diff_snapshot := _diff_state_encoder.decode(data, _property_config.get_properties_owned_by(sender))
|
||||
if not _diff_state_encoder.apply(tick, diff_snapshot, reference_tick, sender):
|
||||
# Invalid data
|
||||
return
|
||||
|
||||
if NetworkRollback.enable_diff_states:
|
||||
if diff_ack_interval > 0 and tick > _next_diff_ack_tick:
|
||||
_ack_diff_state.rpc_id(sender, tick)
|
||||
_next_diff_ack_tick = tick + diff_ack_interval
|
||||
|
||||
@rpc("any_peer", "reliable", "call_remote")
|
||||
func _ack_full_state(tick: int) -> void:
|
||||
var sender_id := multiplayer.get_remote_sender_id()
|
||||
_ackd_state[sender_id] = tick
|
||||
|
||||
_logger.trace("Peer %d ack'd full state for tick %d", [sender_id, tick])
|
||||
|
||||
@rpc("any_peer", "unreliable_ordered", "call_remote")
|
||||
func _ack_diff_state(tick: int) -> void:
|
||||
if not _is_initialized: return
|
||||
|
||||
var sender_id := multiplayer.get_remote_sender_id()
|
||||
_ackd_state[sender_id] = tick
|
||||
|
||||
_logger.trace("Peer %d ack'd diff state for tick %d", [sender_id, tick])
|
||||
|
||||
Reference in New Issue
Block a user