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,201 @@
|
||||
extends Node
|
||||
class_name _RollbackSimulationServer
|
||||
|
||||
## Runs gameplay simulation during rollback
|
||||
##
|
||||
## Rollback involves restoring the game state to an earlier point in time and
|
||||
## re-running game logic from there. This class tracks which nodes participate
|
||||
## in rollback, which of them need to be actually simulated, and runs the
|
||||
## simulation itself.
|
||||
## [br][br]
|
||||
## Node simulation honors scene tree order. Processing order and physics
|
||||
## processing order are [b]not[/b] considered.
|
||||
## [br][br]
|
||||
## To participate in rollback, call [method register] using the method to call
|
||||
## to simulate the node. Only one [Callable] can be actively registered per
|
||||
## node.
|
||||
|
||||
var _history_server: _NetworkHistoryServer
|
||||
|
||||
var _callbacks := {} # node to callback
|
||||
var _simulated_ticks := {} # node to array of ticks
|
||||
|
||||
var _input_graph := _Graph.new() # Links inputs to objects controlled by them
|
||||
|
||||
# Currently simulated object
|
||||
var _current_object: Object = null
|
||||
# Predicted nodes for next simulation
|
||||
var _predicted_nodes := _Set.new()
|
||||
|
||||
var _group := StringName("__nf_rollback_sim" + str(get_instance_id()))
|
||||
|
||||
static var _logger := NetfoxLogger._for_netfox("RollbackSimulationServer")
|
||||
|
||||
## Register a [param]callback[/param] to run as part of the rollback loop
|
||||
func register(callback: Callable) -> void:
|
||||
if not is_instance_valid(callback.get_object()):
|
||||
_logger.error("Trying to register callback that belongs to an invalid object!")
|
||||
return
|
||||
|
||||
assert(callback.get_object() is Node, "Only nodes supported for now!")
|
||||
|
||||
_callbacks[callback.get_object()] = callback
|
||||
|
||||
## Deregister a [param]callback[/param] from the rollback loop
|
||||
func deregister(callback: Callable) -> void:
|
||||
if not callback or not callback.is_valid(): return
|
||||
|
||||
var object := callback.get_object()
|
||||
|
||||
if not is_instance_valid(object): return
|
||||
if _callbacks[object] != callback: return
|
||||
|
||||
_callbacks.erase(object)
|
||||
_input_graph.erase(object)
|
||||
_simulated_ticks.erase(object)
|
||||
|
||||
## Deregister a [param]node[/param] from the rollback loop
|
||||
func deregister_node(node: Node) -> void:
|
||||
if _callbacks.has(node):
|
||||
deregister(_callbacks[node])
|
||||
_input_graph.erase(node)
|
||||
|
||||
## Register [param]input[/param] as providing input for [param]node[/param]
|
||||
func register_rollback_input_for(node: Node, input: Node) -> void:
|
||||
_input_graph.link(input, node)
|
||||
|
||||
## Deregister [param]input[/param] from providing input for [param]node[/param]
|
||||
func deregister_rollback_input(node: Node, input: Node) -> void:
|
||||
_input_graph.unlink(input, node)
|
||||
|
||||
## Return true if the currently simulated node is being predicted
|
||||
func is_predicting_current() -> bool:
|
||||
if not _current_object or not is_instance_valid(_current_object):
|
||||
return false
|
||||
return _predicted_nodes.has(_current_object)
|
||||
|
||||
## Get the currently simulated object
|
||||
func get_simulated_object() -> Object:
|
||||
return _current_object
|
||||
|
||||
## Simulate a tick
|
||||
func simulate(delta: float, tick: int) -> void:
|
||||
_current_object = null
|
||||
|
||||
var input_snapshot := NetworkHistoryServer._get_rollback_input_snapshot(tick)
|
||||
var state_snapshot := NetworkHistoryServer._get_rollback_state_snapshot(tick)
|
||||
var nodes := _get_nodes_to_simulate(input_snapshot)
|
||||
_predicted_nodes.clear()
|
||||
|
||||
# Sort based on SceneTree order
|
||||
for node in nodes:
|
||||
node.add_to_group(_group)
|
||||
nodes = get_tree().get_nodes_in_group(_group)
|
||||
|
||||
# Determine predicted nodes
|
||||
for node in _callbacks.keys():
|
||||
if _is_predicting(input_snapshot, node):
|
||||
_predicted_nodes.add(node)
|
||||
|
||||
# Run callbacks and clear group
|
||||
for node in nodes:
|
||||
_current_object = node
|
||||
|
||||
var callback := _callbacks[node] as Callable
|
||||
var is_fresh := _is_tick_fresh_for(node, tick)
|
||||
callback.call(delta, tick, is_fresh)
|
||||
node.remove_from_group(_group)
|
||||
|
||||
_current_object = null
|
||||
_set_tick_simulated_for(node, tick)
|
||||
|
||||
# Metrics
|
||||
NetworkPerformance.push_rollback_nodes_simulated(nodes.size())
|
||||
|
||||
func _get_nodes_to_simulate(input_snapshot: _Snapshot) -> Array[Node]:
|
||||
var result: Array[Node] = []
|
||||
if not input_snapshot:
|
||||
return []
|
||||
|
||||
var tick := input_snapshot.tick
|
||||
for node in _callbacks.keys():
|
||||
var inputs := [] as Array[Node]
|
||||
inputs.assign(_input_graph.get_linked_to(node))
|
||||
|
||||
if inputs.is_empty():
|
||||
# Node has no input, simulate it
|
||||
result.append(node)
|
||||
continue
|
||||
|
||||
if NetworkRollback.is_mutated(node, tick):
|
||||
# Node is mutated, must simulate
|
||||
result.append(node)
|
||||
continue
|
||||
|
||||
if not input_snapshot.has_subjects(inputs, true):
|
||||
# We don't have input for node, don't simulate
|
||||
continue
|
||||
|
||||
result.append(node)
|
||||
|
||||
return result
|
||||
|
||||
func _is_predicting(input_snapshot: _Snapshot, node: Node) -> bool:
|
||||
var input_nodes := [] as Array[Node]
|
||||
input_nodes.assign(_input_graph.get_linked_to(node))
|
||||
|
||||
var is_owned := node.is_multiplayer_authority()
|
||||
var is_inputless := input_nodes.is_empty()
|
||||
var has_input := false
|
||||
|
||||
if not is_inputless and input_snapshot:
|
||||
has_input = input_snapshot.has_subjects(input_nodes, true)
|
||||
|
||||
if not is_owned and has_input:
|
||||
# We don't own the node, but we own input for it - not (input) predicting
|
||||
return false
|
||||
if not is_owned:
|
||||
# We don't own the node, so we can only guess - i.e. predict
|
||||
return true
|
||||
if is_owned and is_inputless:
|
||||
# We own the node, node doesn't depend on input, we're sure
|
||||
return false
|
||||
if is_owned and not has_input:
|
||||
# We own the node, node depends on input, we don't have data for input - predict
|
||||
return true
|
||||
# We own the node and we have data for node's input - we're sure
|
||||
return false
|
||||
|
||||
func _is_tick_fresh_for(node: Node, tick: int) -> bool:
|
||||
if not _simulated_ticks.has(node):
|
||||
return true
|
||||
var ticks := _simulated_ticks.get(node) as Array[int]
|
||||
return not ticks.has(tick)
|
||||
|
||||
func _set_tick_simulated_for(node: Node, tick: int) -> void:
|
||||
if not _simulated_ticks.has(node):
|
||||
_simulated_ticks[node] = [tick] as Array[int]
|
||||
else:
|
||||
_simulated_ticks[node].append(tick)
|
||||
|
||||
func _trim_ticks_simulated(beginning: int) -> void:
|
||||
for object in _simulated_ticks:
|
||||
_simulated_ticks[object] = _simulated_ticks[object]\
|
||||
.filter(func(tick): return tick >= beginning)
|
||||
|
||||
func _get_controlled_by(input: Node) -> Array[Node]:
|
||||
var result := [] as Array[Node]
|
||||
result.assign(_input_graph.get_linked_from(input))
|
||||
return result
|
||||
|
||||
func _get_inputs_of(node: Node) -> Array[Node]:
|
||||
var result := [] as Array[Node]
|
||||
result.assign(_input_graph.get_linked_to(node))
|
||||
return result
|
||||
|
||||
func _init(p_history_server: _NetworkHistoryServer = null):
|
||||
_history_server = p_history_server
|
||||
|
||||
func _ready():
|
||||
# Ensure dependencies
|
||||
if not _history_server: _history_server = NetworkHistoryServer
|
||||
Reference in New Issue
Block a user