Phase 7: netfox + godot-jolt stack upgrade

Stack installed:
- netfox v1.35.3 (core + extras + noray + internals)
- godot-jolt v0.16.0-stable

Architecture:
- Server: ENet transport (works headless, no netfox deps)
- Client/Editor: netfox rollback (RollbackSynchronizer, TickInterpolator)

New/modified:
- docs/migration-netfox-plan.md — migration architecture
- scripts/network/network_manager.gd — netfox-aware ENet fallback
- scripts/network/player.gd — clean base player
- client/characters/player_netfox.gd — rollback player w/ WeaponManager
- client/characters/input/player_net_input.gd — BaseNetInput subclass
- client/characters/character/fps_character_controller.gd — netfox input feed
- client/weapons/ — weapon data, registry, TacticalWeaponHitscan, WeaponManager
- client/scripts/round_replicator.gd — client-side round state bridge
- server/scripts/round_manager.gd — improved state machine
- server/scripts/plugin_api/plugin_manager.gd — refined plugin system
- config: enemy_tag, ally_tag for meatball targeting

Removed: old C++ SimulationServer GDExtension (replaced by netfox rollback)
This commit is contained in:
2026-07-02 17:38:50 -04:00
parent e2dc429caa
commit e7299b17e9
3237 changed files with 523530 additions and 18 deletions
@@ -0,0 +1,72 @@
extends PhysicsDriver
class_name PhysicsDriver2D
# Physics driver based on netfox ticks
# Requires a custom build of Godot with https://github.com/godotengine/godot/pull/76462
var scene_collision_objects: Array = []
var collision_objects_snapshots: Dictionary[int, Dictionary] = {}
func _init_physics_space() -> void:
physics_space = get_viewport().world_2d.space
PhysicsServer2D.space_set_active(physics_space, false)
get_tree().node_added.connect(node_added)
get_tree().node_removed.connect(node_removed)
scan_tree()
func _physics_step(delta) -> void:
PhysicsServer2D.space_flush_queries(physics_space)
PhysicsServer2D.space_step(physics_space, delta)
func _snapshot_space(tick: int) -> void:
var rid_states: Dictionary[RID, Array] = {}
for element in scene_collision_objects:
if element is CharacterBody2D:
element.force_update_transform() # force colliders to update
var rid = element.get_rid()
rid_states[rid] = get_body_states(rid)
snapshots[tick] = rid_states
func _rollback_space(tick) -> void:
if snapshots.has(tick):
var rid_states = snapshots[tick]
for rid in rid_states.keys():
set_body_states(rid, rid_states[rid])
for body in scene_collision_objects:
if body is CharacterBody2D or body is AnimatableBody2D:
body.force_update_transform() # force colliders to update
func get_body_states(rid: RID) -> Array:
var body_state: Array = [Vector3.ZERO, Quaternion.IDENTITY, Vector3.ZERO, Vector3.ZERO]
body_state[0] = PhysicsServer2D.body_get_state(rid, PhysicsServer2D.BODY_STATE_TRANSFORM)
body_state[1] = PhysicsServer2D.body_get_state(rid, PhysicsServer2D.BODY_STATE_LINEAR_VELOCITY)
body_state[2] = PhysicsServer2D.body_get_state(rid, PhysicsServer2D.BODY_STATE_ANGULAR_VELOCITY)
body_state[3] = PhysicsServer2D.body_get_state(rid, PhysicsServer2D.BODY_STATE_SLEEPING)
return body_state
func set_body_states(rid: RID, body_state: Array) -> void:
PhysicsServer2D.body_set_state(rid, PhysicsServer2D.BODY_STATE_TRANSFORM, body_state[0])
PhysicsServer2D.body_set_state(rid, PhysicsServer2D.BODY_STATE_LINEAR_VELOCITY, body_state[1])
PhysicsServer2D.body_set_state(rid, PhysicsServer2D.BODY_STATE_ANGULAR_VELOCITY, body_state[2])
PhysicsServer2D.body_set_state(rid, PhysicsServer2D.BODY_STATE_SLEEPING, body_state[3])
func scan_tree():
scene_collision_objects.clear()
scene_collision_objects = get_all_children(get_node('/root'))
func get_all_children(in_node: Node) -> Array:
var nodes = []
nodes = in_node.find_children("*", "PhysicsBody2D", true, false)
return nodes
func node_added(node: Node) -> void:
if node is PhysicsBody2D:
scene_collision_objects.append(node)
func node_removed(node: Node) -> void:
if node is PhysicsBody2D:
scene_collision_objects.erase(node)
@@ -0,0 +1 @@
uid://c8p8gymii2y5v
@@ -0,0 +1,71 @@
extends PhysicsDriver
class_name PhysicsDriver3D
# Physics driver based on netfox ticks
# Requires a custom build of Godot with https://github.com/godotengine/godot/pull/76462
# Maps ticks ( int ) to global snapshots ( Dictionary<RID, Array> )
var scene_collision_objects: Array = []
func _init_physics_space() -> void:
physics_space = get_viewport().world_3d.space
PhysicsServer3D.space_set_active(physics_space, false)
get_tree().node_added.connect(node_added)
get_tree().node_removed.connect(node_removed)
scan_tree()
func _physics_step(delta) -> void:
PhysicsServer3D.space_flush_queries(physics_space)
PhysicsServer3D.space_step(physics_space, delta)
func _snapshot_space(tick: int) -> void:
# Maps RIDs to physics state ( Array )
var rid_states := {}
for element in scene_collision_objects:
var rid = element.get_rid()
rid_states[rid] = get_body_states(rid)
snapshots[tick] = rid_states
func _rollback_space(tick) -> void:
if snapshots.has(tick):
var rid_states = snapshots[tick]
for rid in rid_states.keys():
set_body_states(rid, rid_states[rid])
for body in scene_collision_objects:
if body is CharacterBody3D or body is AnimatableBody3D:
body.force_update_transform()
func get_body_states(rid: RID) -> Array:
var body_state: Array = [Vector3.ZERO, Quaternion.IDENTITY, Vector3.ZERO, Vector3.ZERO]
body_state[0] = PhysicsServer3D.body_get_state(rid, PhysicsServer3D.BODY_STATE_TRANSFORM)
body_state[1] = PhysicsServer3D.body_get_state(rid, PhysicsServer3D.BODY_STATE_LINEAR_VELOCITY)
body_state[2] = PhysicsServer3D.body_get_state(rid, PhysicsServer3D.BODY_STATE_ANGULAR_VELOCITY)
body_state[3] = PhysicsServer3D.body_get_state(rid, PhysicsServer3D.BODY_STATE_SLEEPING)
return body_state
func set_body_states(rid: RID, body_state: Array) -> void:
PhysicsServer3D.body_set_state(rid, PhysicsServer3D.BODY_STATE_TRANSFORM, body_state[0])
PhysicsServer3D.body_set_state(rid, PhysicsServer3D.BODY_STATE_LINEAR_VELOCITY, body_state[1])
PhysicsServer3D.body_set_state(rid, PhysicsServer3D.BODY_STATE_ANGULAR_VELOCITY, body_state[2])
PhysicsServer3D.body_set_state(rid, PhysicsServer3D.BODY_STATE_SLEEPING, body_state[3])
func scan_tree():
scene_collision_objects.clear()
scene_collision_objects = get_all_children(get_node('/root'))
func get_all_children(in_node: Node) -> Array:
var nodes = []
nodes = in_node.find_children("*", "PhysicsBody3D", true, false)
return nodes
func node_added(node: Node) -> void:
if node is PhysicsBody3D:
scene_collision_objects.append(node)
func node_removed(node: Node) -> void:
if node is PhysicsBody3D:
scene_collision_objects.erase(node)
@@ -0,0 +1 @@
uid://wr5ur5dqrkni
@@ -0,0 +1,44 @@
@icon("res://addons/netfox.extras/icons/network-rigid-body-2d.svg")
extends RigidBody2D
class_name NetworkRigidBody2D
## A rollback / state synchronizer class for RigidBody2D.
## Set state property path to physics_state to synchronize the state of this body.
@onready var direct_state = PhysicsServer2D.body_get_direct_state(get_rid())
var physics_state: Array:
get: return get_state()
set(v): set_state(v)
enum {
ORIGIN,
ROT,
LIN_VEL,
ANG_VEL,
SLEEPING
}
func _notification(notification: int):
if notification == NOTIFICATION_READY:
add_to_group("network_rigid_body")
func get_state() -> Array:
var body_state: Array = [Vector3.ZERO, Quaternion.IDENTITY, Vector3.ZERO, Vector3.ZERO, false]
body_state[ORIGIN] = direct_state.transform.origin
body_state[ROT] = direct_state.transform.get_rotation()
body_state[LIN_VEL] = direct_state.linear_velocity
body_state[ANG_VEL] = direct_state.angular_velocity
body_state[SLEEPING] = direct_state.sleeping
return body_state
func set_state(remote_state: Array) -> void:
direct_state.transform = Transform2D(remote_state[ROT], remote_state[ORIGIN])
direct_state.linear_velocity = remote_state[LIN_VEL]
direct_state.angular_velocity = remote_state[ANG_VEL]
direct_state.sleeping = remote_state[SLEEPING]
## Override and apply any logic, forces or impulses to the rigid body as you would in physics_process
## The physics engine will run its simulation during rollback_tick with other nodes
func _physics_rollback_tick(_delta, _tick):
pass
@@ -0,0 +1 @@
uid://c8hw7ol53m55g
@@ -0,0 +1,46 @@
@icon("res://addons/netfox.extras/physics/network-rigid-body-3d.gd")
extends RigidBody3D
class_name NetworkRigidBody3D
## A rollback / state synchronizer class for RigidBody3D.
## Set state property path to physics_state to synchronize the state of this body.
@onready var direct_state = PhysicsServer3D.body_get_direct_state(get_rid())
var physics_state: Array:
get: return get_state()
set(v): set_state(v)
enum {
ORIGIN,
QUAT,
LIN_VEL,
ANG_VEL,
SLEEPING
}
func _notification(notification: int):
if notification == NOTIFICATION_READY:
add_to_group("network_rigid_body")
func get_state() -> Array:
var body_state: Array = [Vector3.ZERO, Quaternion.IDENTITY, Vector3.ZERO, Vector3.ZERO, false]
body_state[ORIGIN] = direct_state.transform.origin
body_state[QUAT] = direct_state.transform.basis.get_rotation_quaternion()
body_state[LIN_VEL] = direct_state.linear_velocity
body_state[ANG_VEL] = direct_state.angular_velocity
body_state[SLEEPING] = direct_state.sleeping
return body_state
func set_state(remote_state: Array) -> void:
direct_state.transform.origin = remote_state[ORIGIN]
direct_state.transform.basis = Basis(remote_state[QUAT])
direct_state.linear_velocity = remote_state[LIN_VEL]
direct_state.angular_velocity = remote_state[ANG_VEL]
direct_state.sleeping = remote_state[SLEEPING]
## Override and apply any logic, forces or impulses to the rigid body as you would in physics_process
## The physics engine will run its simulation during rollback_tick with other nodes
func _physics_rollback_tick(_delta, _tick):
pass
@@ -0,0 +1 @@
uid://bklxcdyxgbjg2
@@ -0,0 +1,88 @@
extends Object
class PhysicsDriverToggle:
const INACTIVE_SUFFIX := ".off"
func get_name() -> String:
return "???"
func get_files() -> Array[String]:
return []
func get_error_messages() -> Array[String]:
return []
func is_enabled() -> bool:
return get_files().any(func(it): return FileAccess.file_exists(it))
func toggle() -> Array[String]:
var errors := get_error_messages()
if not errors.is_empty():
return errors
var enable := not is_enabled()
var uid_files := get_files().map(func(it): return it + ".uid")
var renames = (get_files() + uid_files).map(func(it):
if enable: return [it + INACTIVE_SUFFIX, it]
else: return [it, it + INACTIVE_SUFFIX]
)
for rename in renames:
var result := DirAccess.rename_absolute(rename[0], rename[1])
if result != OK:
errors.append(
"Failed rename \"%s\" -> \"%s\"; reason: %s" %
[rename[0], rename[1], error_string(result)]
)
return errors
class Rapier2DPhysicsDriverToggle extends PhysicsDriverToggle:
func get_name() -> String:
return "Rapier2D"
func get_files() -> Array[String]:
return [
"res://addons/netfox.extras/physics/rapier_driver_2d.gd",
]
func get_error_messages() -> Array[String]:
if not ClassDB.class_exists("RapierPhysicsServer2D"):
return ["Rapier physics is not available! Is the extension installed?"]
return []
class Rapier3DPhysicsDriverToggle extends PhysicsDriverToggle:
func get_name() -> String:
return "Rapier3D"
func get_files() -> Array[String]:
return [
"res://addons/netfox.extras/physics/rapier_driver_3d.gd",
]
func get_error_messages() -> Array[String]:
if not ClassDB.class_exists("RapierPhysicsServer3D"):
return ["Rapier physics is not available! Is the extension installed?"]
return []
class GodotPhysicsDriverToggle extends PhysicsDriverToggle:
func get_name() -> String:
return "Godot"
func get_files() -> Array[String]:
return [
"res://addons/netfox.extras/physics/godot_driver_3d.gd",
"res://addons/netfox.extras/physics/godot_driver_2d.gd"
]
func get_error_messages() -> Array[String]:
if not PhysicsServer3D.has_method("space_step") or not PhysicsServer2D.has_method("space_step"):
return ["Physics stepping is not available! Is this the right Godot build?"]
return []
static func all() -> Array[PhysicsDriverToggle]:
return [
Rapier2DPhysicsDriverToggle.new(),
Rapier3DPhysicsDriverToggle.new(),
GodotPhysicsDriverToggle.new()
]
@@ -0,0 +1 @@
uid://bu4ppfj0ovkbr
@@ -0,0 +1,86 @@
extends Node
class_name PhysicsDriver
# Physics driver based on netfox ticks
# Step physics in time with netfox and participates in rollback
var physics_space: RID
var snapshots: Dictionary = {}
# Number of physics steps to take per network tick
@export var physics_factor: int = 2
# Snapshot and Rollback entire physics space.
@export var rollback_physics_space: bool = true
func _enter_tree():
#regular ticks
NetworkTime.before_tick.connect(before_tick)
NetworkTime.after_tick_loop.connect(after_tick_loop)
#rollback ticks
if rollback_physics_space:
NetworkRollback.on_prepare_tick.connect(on_prepare_tick)
NetworkRollback.on_process_tick.connect(on_process_tick)
func _exit_tree():
NetworkTime.before_tick.disconnect(before_tick)
NetworkTime.after_tick_loop.disconnect(after_tick_loop)
#rollback ticks
if NetworkRollback.on_prepare_tick.is_connected(on_prepare_tick):
NetworkRollback.on_prepare_tick.disconnect(on_prepare_tick)
NetworkRollback.on_process_tick.disconnect(on_process_tick)
func _ready() -> void:
_init_physics_space()
# Emitted before a tick is run.
func before_tick(_delta: float, tick: int) -> void:
_snapshot_space(tick)
step_physics(_delta)
func on_prepare_tick(tick: int) -> void:
if NetworkRollback._rollback_from == tick:
# First tick of rollback loop, rewind
_rollback_space(tick)
else:
# Subsequent ticks are re-writing history.
_snapshot_space(tick)
func on_process_tick(_tick: int) -> void:
step_physics(NetworkTime.ticktime)
func after_tick_loop() -> void:
# Remove old snapshots
for i in snapshots.keys():
if i < NetworkRollback.history_start:
snapshots.erase(i)
func step_physics(_delta: float) -> void:
# Break up physics into smaller steps if needed
var frac_delta = _delta / physics_factor
var rollback_participants = get_tree().get_nodes_in_group("network_rigid_body")
for i in range(physics_factor):
for net_rigid_body in rollback_participants:
net_rigid_body._physics_rollback_tick(frac_delta, NetworkTime.tick)
_physics_step(frac_delta)
## Override this method to initialize the physics space.
func _init_physics_space() -> void:
pass
## Override this method to take one step in the physics space.
## [br][br]
## It should also flush and update all Godot nodes
func _physics_step(_delta) -> void:
pass
## Override this method to record the current state of the physics space.
func _snapshot_space(_tick: int) -> void:
pass
## Override this method to restore the physics space to a previous state.
func _rollback_space(_tick) -> void:
pass
@@ -0,0 +1 @@
uid://dcws6qk4hxtun
@@ -0,0 +1,18 @@
extends PhysicsDriver
class_name RapierDriver2D
func _init_physics_space() -> void:
physics_space = get_viewport().world_2d.space
PhysicsServer2D.space_set_active(physics_space, false)
func _physics_step(delta) -> void:
RapierPhysicsServer2D.space_step(physics_space, delta)
RapierPhysicsServer2D.space_flush_queries(physics_space)
func _snapshot_space(tick: int) -> void:
snapshots[tick] = RapierPhysicsServer2D.export_binary(physics_space)
func _rollback_space(tick) -> void:
if snapshots.has(tick):
RapierPhysicsServer2D.import_binary(physics_space, snapshots[tick])
@@ -0,0 +1 @@
uid://bcc28dvk0pufe
@@ -0,0 +1,18 @@
extends PhysicsDriver
class_name RapierDriver3D
func _init_physics_space() -> void:
physics_space = get_viewport().world_3d.space
PhysicsServer3D.space_set_active(physics_space, false)
func _physics_step(delta) -> void:
RapierPhysicsServer3D.space_step(physics_space, delta)
RapierPhysicsServer3D.space_flush_queries(physics_space)
func _snapshot_space(tick: int) -> void:
snapshots[tick] = RapierPhysicsServer3D.export_binary(physics_space)
func _rollback_space(tick) -> void:
if snapshots.has(tick):
RapierPhysicsServer3D.import_binary(physics_space, snapshots[tick])
@@ -0,0 +1 @@
uid://bo4lplbeepibj