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:
@@ -0,0 +1,273 @@
|
||||
extends Node
|
||||
class_name _NetworkHistoryServer
|
||||
|
||||
## Tracks the history of objects' properties
|
||||
##
|
||||
## Specifically, history is stored for rollback state properties, rollback input
|
||||
## properties, and synchronized state properties.
|
||||
## [br][br]
|
||||
## Keeping history lets rollback restore earlier game states for resimulation,
|
||||
## and enables [_NetworkSynchronizationServer] to send diff states by comparing
|
||||
## against historical data.
|
||||
|
||||
var _rb_input_properties := _PropertyPool.new()
|
||||
var _rb_state_properties := _PropertyPool.new()
|
||||
var _sync_state_properties := _PropertyPool.new()
|
||||
|
||||
var _rb_history_size := NetworkRollback.history_limit
|
||||
var _sync_history_size := ProjectSettings.get_setting("netfox/state_synchronizer/history_limit", 64) as int
|
||||
|
||||
# Source of truth for history
|
||||
var _rb_input_history := _PerObjectHistory.new(_rb_history_size)
|
||||
var _rb_state_history := _PerObjectHistory.new(_rb_history_size)
|
||||
var _sync_history := _PerObjectHistory.new(_sync_history_size)
|
||||
|
||||
# Cached snapshots for syncing
|
||||
var _rb_input_snapshots := _HistoryBuffer.new(_rb_history_size)
|
||||
var _rb_state_snapshots := _HistoryBuffer.new(_rb_history_size)
|
||||
var _sync_state_snapshots := _HistoryBuffer.new(_sync_history_size)
|
||||
|
||||
static var _logger := NetfoxLogger._for_netfox("NetworkHistoryServer")
|
||||
|
||||
## Register a rollback state property
|
||||
func register_rollback_state(node: Node, property: NodePath) -> void:
|
||||
_rb_state_properties.add(node, property)
|
||||
|
||||
## Deregister a rollback state property
|
||||
func deregister_rollback_state(node: Node, property: NodePath) -> void:
|
||||
_rb_state_properties.erase(node, property)
|
||||
|
||||
## Register a rollback input property
|
||||
func register_rollback_input(node: Node, property: NodePath) -> void:
|
||||
_rb_input_properties.add(node, property)
|
||||
|
||||
## Deregister a rollback input property
|
||||
func deregister_rollback_input(node: Node, property: NodePath) -> void:
|
||||
_rb_input_properties.erase(node, property)
|
||||
|
||||
## Register a synchronized state property
|
||||
func register_sync_state(node: Node, property: NodePath) -> void:
|
||||
_sync_state_properties.add(node, property)
|
||||
|
||||
## Deregister a synchronized state property
|
||||
func deregister_sync_state(node: Node, property: NodePath) -> void:
|
||||
_sync_state_properties.erase(node, property)
|
||||
|
||||
## Deregister a node, no longer tracking any property it had registered using
|
||||
## any of the [code]register_*()[/code] methods
|
||||
func deregister(node: Node) -> void:
|
||||
_rb_state_properties.erase_subject(node)
|
||||
_rb_input_properties.erase_subject(node)
|
||||
_sync_state_properties.erase_subject(node)
|
||||
|
||||
_rb_state_history.erase_subject(node)
|
||||
_rb_input_history.erase_subject(node)
|
||||
_sync_history.erase_subject(node)
|
||||
|
||||
## Return the latest tick where any of the [param]subjects[/param] had rollback
|
||||
## state data available
|
||||
func get_latest_state_tick_for(subjects: Array, tick: int) -> int:
|
||||
return _get_latest_for(subjects, tick, _rb_state_history)
|
||||
|
||||
## Return how old is the latest rollback state data for any of the [param]
|
||||
## subjects[/param], in ticks
|
||||
func get_state_age_for(subjects: Array, tick: int) -> int:
|
||||
var latest_state := get_latest_state_tick_for(subjects, tick)
|
||||
if latest_state < 0:
|
||||
return -1
|
||||
else:
|
||||
return tick - latest_state
|
||||
|
||||
## Return the latest tick where any of the [param]subjects[/param] had rollback
|
||||
## input data available
|
||||
func get_latest_input_for(subjects: Array, tick: int) -> int:
|
||||
return _get_latest_for(subjects, tick, _rb_input_history)
|
||||
|
||||
## Return how old is the latest rollback input data for any of the [param]
|
||||
## subjects[/param], in ticks
|
||||
func get_input_age_for(subjects: Array, tick: int) -> int:
|
||||
var latest_input := get_latest_input_for(subjects, tick)
|
||||
if latest_input < 0:
|
||||
return -1
|
||||
else:
|
||||
return tick - latest_input
|
||||
|
||||
func _record_rollback_input(tick: int) -> void:
|
||||
_record(tick, _rb_input_history, _rb_input_snapshots, _rb_input_properties, true, func(subject: Node):
|
||||
return subject.is_multiplayer_authority()
|
||||
)
|
||||
|
||||
func _record_rollback_state(tick: int) -> void:
|
||||
var input_snapshot := _get_rollback_input_snapshot(tick - 1)
|
||||
|
||||
_record(tick, _rb_state_history, _rb_state_snapshots, _rb_state_properties, false, func(subject: Node):
|
||||
if not subject.is_multiplayer_authority():
|
||||
return false
|
||||
if RollbackSimulationServer._is_predicting(input_snapshot, subject):
|
||||
return false
|
||||
return true
|
||||
)
|
||||
|
||||
func _record_sync_state(tick: int) -> void:
|
||||
_record(tick, _sync_history, _sync_state_snapshots, _sync_state_properties, true, func(subject: Node):
|
||||
return subject.is_multiplayer_authority()
|
||||
)
|
||||
|
||||
func _restore_rollback_input(tick: int) -> bool:
|
||||
return _restore_latest(tick, _rb_input_history)
|
||||
|
||||
func _restore_rollback_state(tick: int) -> bool:
|
||||
return _restore_latest(tick, _rb_state_history)
|
||||
|
||||
func _restore_synchronizer_state(tick: int) -> bool:
|
||||
return _restore_latest(tick, _sync_history)
|
||||
|
||||
func _get_rollback_input_snapshot(tick: int) -> _Snapshot:
|
||||
return _rb_input_snapshots.get_at(tick)
|
||||
|
||||
func _get_rollback_state_snapshot(tick: int) -> _Snapshot:
|
||||
return _rb_state_snapshots.get_at(tick)
|
||||
|
||||
func _get_synchronizer_state_snapshot(tick: int) -> _Snapshot:
|
||||
return _sync_state_snapshots.get_at(tick)
|
||||
|
||||
func _merge_rollback_input(snapshot: _Snapshot) -> bool:
|
||||
_merge_snapshot(snapshot, _rb_input_snapshots, true)
|
||||
return _merge_history(snapshot, _rb_input_history, true)
|
||||
|
||||
func _merge_rollback_state(snapshot: _Snapshot) -> bool:
|
||||
_merge_snapshot(snapshot, _rb_state_snapshots, true)
|
||||
return _merge_history(snapshot, _rb_state_history)
|
||||
|
||||
func _merge_synchronizer_state(snapshot: _Snapshot) -> bool:
|
||||
_merge_snapshot(snapshot, _sync_state_snapshots, true)
|
||||
return _merge_history(snapshot, _sync_history)
|
||||
|
||||
func _record(tick: int, history: _PerObjectHistory, snapshots: _HistoryBuffer, property_pool: _PropertyPool, only_auth: bool, auth_filter: Callable) -> void:
|
||||
var snapshot := snapshots.get_at(tick, _Snapshot.new(tick)) as _Snapshot
|
||||
if not snapshots.has_at(tick):
|
||||
snapshots.set_at(tick, snapshot)
|
||||
|
||||
for subject in property_pool.get_subjects():
|
||||
assert(subject is Node, "Only nodes supported for now!")
|
||||
|
||||
var is_auth := auth_filter.call(subject)
|
||||
|
||||
if only_auth and not is_auth:
|
||||
continue
|
||||
if not is_auth and history.is_auth(tick, subject):
|
||||
continue
|
||||
|
||||
var subject_snapshot := history.ensure_snapshot(tick, subject, false) #!!
|
||||
assert(not property_pool.get_properties_of(subject).is_empty(), "Subject present in property pool without properties! Please report a bug!")
|
||||
for property in property_pool.get_properties_of(subject):
|
||||
subject_snapshot.record_property(property)
|
||||
snapshot.record_property(subject, property)
|
||||
snapshot.set_auth(subject, is_auth)
|
||||
subject_snapshot.set_auth(is_auth)
|
||||
|
||||
match history:
|
||||
_rb_input_history:
|
||||
_logger.trace("Recorded input @%d: %s", [tick, snapshot])
|
||||
_rb_state_history:
|
||||
_logger.trace("Recorded state @%d: %s", [tick, snapshot])
|
||||
|
||||
func _restore_latest(tick: int, history: _PerObjectHistory) -> bool:
|
||||
var any_applied := false
|
||||
|
||||
for subject in history.subjects():
|
||||
# Grab latest snapshot up to tick
|
||||
var snapshot := history.get_latest_snapshot(tick, subject)
|
||||
|
||||
# Apply if any
|
||||
if snapshot:
|
||||
snapshot.apply()
|
||||
any_applied = true
|
||||
|
||||
match history:
|
||||
_rb_input_history: _logger.trace("Restored input @%d: %s", [tick, snapshot])
|
||||
_rb_state_history: _logger.trace("Restored state @%d: %s", [tick, snapshot])
|
||||
|
||||
return any_applied
|
||||
|
||||
func _merge_snapshot(snapshot: _Snapshot, snapshots: _HistoryBuffer, reverse: bool = false) -> bool:
|
||||
var tick := snapshot.tick
|
||||
|
||||
if not snapshots.has_at(snapshot.tick):
|
||||
snapshots.set_at(tick, snapshot)
|
||||
return true
|
||||
|
||||
var original_snapshot := snapshots.get_at(tick) as _Snapshot
|
||||
if reverse:
|
||||
var original_subjects := original_snapshot.get_auth_subjects()
|
||||
var incoming_subjects := snapshot.get_auth_subjects()
|
||||
|
||||
# Merge the original snapshot on top of the incoming
|
||||
# This prevents players from changing history, e.g. rewrite their past
|
||||
# inputs
|
||||
snapshots.set_at(tick, snapshot)
|
||||
snapshot.merge(original_snapshot)
|
||||
|
||||
# Only return true if we've received inputs for a new node
|
||||
return incoming_subjects.any(func(it): return not original_subjects.has(it))
|
||||
else:
|
||||
return original_snapshot.merge(snapshot)
|
||||
|
||||
func _merge_history(snapshot: _Snapshot, history: _PerObjectHistory, reverse: bool = false) -> bool:
|
||||
var tick := snapshot.tick
|
||||
var has_updated := false
|
||||
|
||||
if tick < NetworkRollback.history_start:
|
||||
_logger.warning("Snapshot being merged is too old! (@%d)", [tick])
|
||||
return false
|
||||
|
||||
for subject in snapshot.get_subjects():
|
||||
var object_snapshot := history.ensure_snapshot(tick, subject, not reverse)
|
||||
|
||||
# Never overwrite auth data
|
||||
if object_snapshot.is_auth() and not snapshot.is_auth(subject):
|
||||
continue
|
||||
|
||||
for property in snapshot.get_subject_properties(subject):
|
||||
# If merging in reverse, don't update anything that we already have
|
||||
# a value for - only accept previously unknown property values
|
||||
if reverse and object_snapshot.has_value(property):
|
||||
continue
|
||||
|
||||
var original_value := object_snapshot.get_value(property)
|
||||
var new_value := snapshot.get_property(subject, property)
|
||||
|
||||
object_snapshot.set_value(property, new_value)
|
||||
if not has_updated and original_value != new_value:
|
||||
has_updated = true
|
||||
object_snapshot.set_auth(snapshot.is_auth(subject))
|
||||
|
||||
return has_updated
|
||||
|
||||
func _get_latest_for(subjects: Array, tick: int, history: _PerObjectHistory) -> int:
|
||||
var latest := -1
|
||||
|
||||
for subject in subjects:
|
||||
var subject_latest := history.get_latest_tick(tick, subject)
|
||||
if subject_latest < 0:
|
||||
continue
|
||||
|
||||
# Should we track `mini()` instead?
|
||||
latest = maxi(latest, subject_latest)
|
||||
|
||||
return latest
|
||||
|
||||
func _get_earliest_for(subjects: Array, tick: int, history: _PerObjectHistory) -> int:
|
||||
var earliest := -1
|
||||
|
||||
for subject in subjects:
|
||||
var subject_latest := history.get_latest_tick(tick, subject)
|
||||
if subject_latest < 0:
|
||||
return -1
|
||||
|
||||
if earliest < 0:
|
||||
earliest = earliest
|
||||
else:
|
||||
earliest = mini(earliest, subject_latest)
|
||||
|
||||
return earliest
|
||||
Reference in New Issue
Block a user