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,28 @@
extends RefCounted
class_name PropertyCache
var root: Node
var _cache: Dictionary = {}
static var _logger: NetfoxLogger = NetfoxLogger._for_netfox("PropertyCache")
func _init(p_root: Node):
root = p_root
func get_entry(path: String) -> PropertyEntry:
if not _cache.has(path):
var parsed = PropertyEntry.parse(root, path)
if not parsed.is_valid():
_logger.warning("Invalid property path: %s", [path])
_cache[path] = parsed
return _cache[path]
func properties() -> Array:
var result: Array[PropertyEntry]
# Can be slow, but no other way to do this with type-safety
# See: https://github.com/godotengine/godot/issues/72627
result.assign(_cache.values())
return result
func clear():
_cache.clear()
@@ -0,0 +1 @@
uid://cjb41satp02xy
@@ -0,0 +1,36 @@
extends RefCounted
class_name _PropertyConfig
var _properties: Array[PropertyEntry] = []
var _auth_properties: Dictionary = {} # Peer (int) to owned properties (Array[PropertyEntry])
var local_peer_id: int
func clear() -> void:
_properties.clear()
_auth_properties.clear()
func set_properties(p_properties: Array[PropertyEntry]) -> void:
clear()
_properties.assign(p_properties)
func set_properties_from_paths(property_paths: Array[String], property_cache: PropertyCache) -> void:
clear()
for path in property_paths:
_properties.append(property_cache.get_entry(path))
func get_properties() -> Array[PropertyEntry]:
return _properties
func get_owned_properties() -> Array[PropertyEntry]:
return get_properties_owned_by(local_peer_id)
func get_properties_owned_by(peer: int) -> Array[PropertyEntry]:
if not _auth_properties.has(peer):
var owned_properties: Array[PropertyEntry] = []
for property_entry in _properties:
if property_entry.node.get_multiplayer_authority() == peer:
owned_properties.append(property_entry)
_auth_properties[peer] = owned_properties
return _auth_properties[peer]
@@ -0,0 +1 @@
uid://b58ojksbbkrvh
@@ -0,0 +1,51 @@
extends RefCounted
class_name PropertyEntry
var _path: String
var node: Node
var property: String
static var _logger := NetfoxLogger._for_netfox("PropertyEntry")
func get_value() -> Variant:
return node.get_indexed(property)
func set_value(value):
node.set_indexed(property, value)
func is_valid() -> bool:
if not node or not is_instance_valid(node):
# Node is invalid
return false
# Return true if node has given property
return node.get_property_list()\
.any(func(it): return it["name"] == property)
func _to_string() -> String:
return _path
static func parse(root: Node, path: String) -> PropertyEntry:
var result = PropertyEntry.new()
result.node = root.get_node(NodePath(path))
result.property = path.erase(0, path.find(":") + 1)
result._path = path
return result
static func make_path(root: Node, node: Variant, property: String) -> String:
var node_path := ""
if node is String:
node_path = node
elif node is NodePath:
node_path = str(node)
elif node is Node:
node_path = str(root.get_path_to(node))
else:
_logger.error("Can't stringify node reference: %s", [node])
return ""
if node_path == ".":
node_path = ""
return "%s:%s" % [node_path, property]
@@ -0,0 +1 @@
uid://cixo40ot0fqqv
@@ -0,0 +1,28 @@
extends _HistoryBuffer
class_name _PropertyHistoryBuffer
func get_snapshot(tick: int) -> _PropertySnapshot:
if _buffer.has(tick):
return _buffer[tick]
else:
return _PropertySnapshot.new()
func set_snapshot(tick: int, data) -> void:
if data is Dictionary:
var snapshot := _PropertySnapshot.from_dictionary(data)
super(tick, snapshot)
elif data is _PropertySnapshot:
super(tick, data)
else:
push_error("Data not a PropertSnapshot! %s" % [data])
func get_history(tick: int) -> _PropertySnapshot:
var snapshot = super(tick)
return snapshot if snapshot else _PropertySnapshot.new()
func trim(earliest_tick_to_keep: int = NetworkRollback.history_start) -> void:
super(earliest_tick_to_keep)
func merge(data: _PropertySnapshot, tick:int) -> void:
set_snapshot(tick, get_snapshot(tick).merge(data))
@@ -0,0 +1 @@
uid://dlkog3qntq03x
@@ -0,0 +1,86 @@
extends RefCounted
class_name _PropertySnapshot
# Maps property paths to their values
# Dictionary[String, Variant]
var _snapshot: Dictionary = {}
static var _logger := NetfoxLogger._for_netfox("PropertySnapshot")
func as_dictionary() -> Dictionary:
return _snapshot.duplicate()
static func from_dictionary(data: Dictionary) -> _PropertySnapshot:
return _PropertySnapshot.new(data)
func set_value(property_path: String, data: Variant) -> void:
_snapshot[property_path] = data
func get_value(property_path: String) -> Variant:
return _snapshot.get(property_path)
func properties() -> Array:
return _snapshot.keys()
func has(property_path: String) -> bool:
return _snapshot.has(property_path)
func size() -> int:
return _snapshot.size()
func equals(other: _PropertySnapshot):
return _snapshot == other._snapshot
func is_empty() -> bool:
return _snapshot.is_empty()
func apply(cache: PropertyCache) -> void:
for property_path in _snapshot:
var property_entry := cache.get_entry(property_path)
var value = _snapshot[property_path]
property_entry.set_value(value)
func merge(data: _PropertySnapshot) -> _PropertySnapshot:
var result := _snapshot.duplicate()
for key in data.as_dictionary():
result[key] = data._snapshot[key]
return _PropertySnapshot.from_dictionary(result)
func make_patch(data: _PropertySnapshot) -> _PropertySnapshot:
var result := {}
for property_path in data.properties():
var old_property = get_value(property_path)
var new_property = data.get_value(property_path)
if old_property != new_property:
result[property_path] = new_property
return _PropertySnapshot.from_dictionary(result)
func sanitize(sender: int, property_cache: PropertyCache) -> void:
var sanitized := {}
for property in _snapshot.keys():
var property_entry := property_cache.get_entry(property)
var authority := property_entry.node.get_multiplayer_authority()
if authority == sender:
sanitized[property] = _snapshot[property]
else:
_logger.warning(
"Received data for property %s, owned by %s, from sender %s",
[ property, authority, sender ]
)
_snapshot = sanitized
static func extract(properties: Array[PropertyEntry]) -> _PropertySnapshot:
var result = {}
for property in properties:
result[property.to_string()] = property.get_value()
return _PropertySnapshot.from_dictionary(result)
func _init(p_snapshot: Dictionary = {}) -> void:
_snapshot = p_snapshot
@@ -0,0 +1 @@
uid://d2dgbafx6338f