Files
tactical-shooter/addons/netfox/servers/rollback-simulation-server.gd
shawn b0c83af092 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>
2026-07-02 20:55:20 -04:00

202 lines
6.3 KiB
GDScript

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