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:
2026-07-02 20:55:20 -04:00
parent ce39b237c3
commit b0c83af092
4416 changed files with 57418 additions and 902676 deletions
+59 -169
View File
@@ -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])