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,136 @@
extends RefCounted
class_name _DiffHistoryEncoder
var _history: _PropertyHistoryBuffer
var _property_cache: PropertyCache
var _full_snapshot := {}
var _encoded_snapshot := {}
var _property_indexes := _BiMap.new()
var _version := 0
var _has_received := false
static var _logger := NetfoxLogger._for_netfox("DiffHistoryEncoder")
func _init(p_history: _PropertyHistoryBuffer, p_property_cache: PropertyCache):
_history = p_history
_property_cache = p_property_cache
func add_properties(properties: Array[PropertyEntry]) -> void:
var has_new_properties := false
for property_entry in properties:
var is_new := _ensure_property_idx(property_entry.to_string())
has_new_properties = has_new_properties or is_new
# If we added any new properties, increment version
if has_new_properties:
_version = (_version + 1) % 256
func encode(tick: int, reference_tick: int, properties: Array[PropertyEntry]) -> PackedByteArray:
assert(properties.size() <= 255, "Property indices may not fit into bytes!")
var snapshot := _history.get_snapshot(tick)
var property_strings := properties.map(func(it): return it.to_string())
var reference_snapshot := _history.get_history(reference_tick)
var diff_snapshot := reference_snapshot.make_patch(snapshot)
_full_snapshot = snapshot.as_dictionary()
_encoded_snapshot = diff_snapshot.as_dictionary()
if diff_snapshot.is_empty():
return PackedByteArray()
var buffer := StreamPeerBuffer.new()
buffer.put_u8(_version)
for property in diff_snapshot.properties():
var property_idx := _property_indexes.get_by_value(property) as int
var property_value = diff_snapshot.get_value(property)
buffer.put_u8(property_idx)
buffer.put_var(property_value)
return buffer.data_array
func decode(data: PackedByteArray, properties: Array[PropertyEntry]) -> _PropertySnapshot:
var result := _PropertySnapshot.new()
if data.is_empty():
return result
var buffer := StreamPeerBuffer.new()
buffer.data_array = data
var packet_version := buffer.get_u8()
if packet_version != _version:
if not _has_received:
# This is the first time we receive data
# Assume the version is OK
_version = packet_version
else:
# Since we don't remove entries, only add, we can still parse what
# we can
_logger.warning("Property config version mismatch - own %d != received %d", [_version, packet_version])
_has_received = true
while buffer.get_available_bytes() > 0:
var property_idx := buffer.get_u8()
var property_value := buffer.get_var()
if not _property_indexes.has_key(property_idx):
_logger.warning("Received unknown property index %d, ignoring!", [property_idx])
continue
var property_entry := _property_indexes.get_by_key(property_idx)
result.set_value(property_entry, property_value)
return result
func apply(tick: int, snapshot: _PropertySnapshot, reference_tick: int, sender: int = -1) -> bool:
if tick < NetworkRollback.history_start:
# State too old!
_logger.error(
"Received diff snapshot for @%d, rejecting because older than %s frames",
[tick, NetworkRollback.history_limit]
)
return false
if snapshot.is_empty():
return true
if sender > 0:
snapshot.sanitize(sender, _property_cache)
if snapshot.is_empty():
_logger.warning("Received invalid diff from #%s for @%s", [sender, tick])
return false
if not _history.has(reference_tick):
# Reference tick missing, hope for the best
_logger.warning("Reference tick %d missing for #%s applying %d", [reference_tick, sender, tick])
var reference_snapshot := _history.get_snapshot(reference_tick)
_history.set_snapshot(tick, reference_snapshot.merge(snapshot))
return true
# TODO: Rework metrics so these are not needed
func get_encoded_snapshot() -> Dictionary:
return _encoded_snapshot
func get_full_snapshot() -> Dictionary:
return _full_snapshot
func _ensure_property_idx(property: String) -> bool:
if _property_indexes.has_value(property):
return false
assert(_property_indexes.size() < 256, "Property index map is full, can't add new property!")
var idx := hash(property) % 256
while _property_indexes.has_key(idx):
idx = hash(idx + 1) % 256
_property_indexes.put(idx, property)
return true
@@ -0,0 +1 @@
uid://dc73evbedbmvs
@@ -0,0 +1,116 @@
extends RefCounted
class_name _RedundantHistoryEncoder
var redundancy: int = 4:
get = get_redundancy,
set = set_redundancy
var _history: _PropertyHistoryBuffer
var _properties: Array[PropertyEntry]
var _property_cache: PropertyCache
var _version := 0
var _has_received := false
var _logger := NetfoxLogger._for_netfox("RedundantHistoryEncoder")
func get_redundancy() -> int:
return redundancy
func set_redundancy(p_redundancy: int):
if p_redundancy <= 0:
_logger.warning(
"Attempting to set redundancy to %d, which would send no data!", [p_redundancy]
)
return
redundancy = p_redundancy
func set_properties(properties: Array[PropertyEntry]) -> void:
if _properties != properties:
_version = (_version + 1) % 256
_properties = properties.duplicate()
func encode(tick: int, properties: Array[PropertyEntry]) -> Array:
if _history.is_empty():
return []
var data := []
for i in range(mini(redundancy, _history.size())):
var offset_tick := tick - i
if offset_tick < _history.get_earliest_tick():
break
var snapshot := _history.get_snapshot(offset_tick)
for property in properties:
data.append(snapshot.get_value(property.to_string()))
data.append(_version)
return data
func decode(data: Array, properties: Array[PropertyEntry]) -> Array[_PropertySnapshot]:
if data.is_empty() or properties.is_empty():
return []
var packet_version := data.pop_back() as int
if packet_version != _version:
if not _has_received:
# First packet, assume version is OK
_version = packet_version
else:
# Version mismatch, can't parse
_logger.warning("Version mismatch! own: %d, received: %s", [_version, packet_version])
return []
var result: Array[_PropertySnapshot] = []
var redundancy := data.size() / properties.size()
result.assign(range(redundancy)
.map(func(__): return _PropertySnapshot.new())
)
for i in range(data.size()):
var offset_idx := i / properties.size()
var prop_idx := i % properties.size()
result[offset_idx].set_value(properties[prop_idx].to_string(), data[i])
_has_received = true
return result
# Returns earliest new tick as int, or -1 if no new ticks applied
func apply(tick: int, snapshots: Array[_PropertySnapshot], sender: int = 0) -> int:
var earliest_new_tick = -1
for i in range(snapshots.size()):
var offset_tick := tick - i
var snapshot := snapshots[i]
if offset_tick < NetworkRollback.history_start:
# Data too old
_logger.warning(
"Received data for %s, rejecting because older than %s frames",
[offset_tick, NetworkRollback.history_limit]
)
continue
if sender > 0:
snapshot.sanitize(sender, _property_cache)
if snapshot.is_empty():
# No valid properties ( probably after sanitize )
_logger.warning("Received invalid data from %d for tick %d", [sender, tick])
continue
var known_snapshot := _history.get_snapshot(offset_tick)
if not known_snapshot.equals(snapshot):
# Received a new snapshot, store and emit signal
_history.set_snapshot(offset_tick, snapshot)
earliest_new_tick = offset_tick
return earliest_new_tick
func _init(p_history: _PropertyHistoryBuffer, p_property_cache: PropertyCache):
_history = p_history
_property_cache = p_property_cache
@@ -0,0 +1 @@
uid://ytn07qahpatv
@@ -0,0 +1,67 @@
extends RefCounted
class_name _SnapshotHistoryEncoder
var _history: _PropertyHistoryBuffer
var _property_cache: PropertyCache
var _properties: Array[PropertyEntry]
var _version := -1
var _has_received := false
static var _logger := NetfoxLogger._for_netfox("_SnapshotHistoryEncoder")
func _init(p_history: _PropertyHistoryBuffer, p_property_cache: PropertyCache):
_history = p_history
_property_cache = p_property_cache
func set_properties(properties: Array[PropertyEntry]) -> void:
if _properties != properties:
_version = (_version + 1) % 256
_properties = properties.duplicate()
func encode(tick: int, properties: Array[PropertyEntry]) -> Array:
var snapshot := _history.get_snapshot(tick)
var data := []
data.resize(properties.size())
for i in range(properties.size()):
data[i] = snapshot.get_value(properties[i].to_string())
data.append(_version)
return data
func decode(data: Array, properties: Array[PropertyEntry]) -> _PropertySnapshot:
var result := _PropertySnapshot.new()
var packet_version = data.pop_back()
if packet_version != _version:
if not _has_received:
# First packet, assume version is OK
_version = packet_version
else:
# Version mismatch, can't parse
_logger.warning("Version mismatch! own: %d, received: %s", [_version, packet_version])
return result
if properties.size() != data.size():
_logger.warning("Received snapshot with %d entries, with %d known - parsing as much as possible", [data.size(), properties.size()])
for i in range(0, mini(data.size(), properties.size())):
result.set_value(properties[i].to_string(), data[i])
_has_received = true
return result
func apply(tick: int, snapshot: _PropertySnapshot, sender: int = -1) -> bool:
if tick < NetworkRollback.history_start:
# State too old!
_logger.error("Received full snapshot for %s, rejecting because older than %s frames", [tick, NetworkRollback.history_limit])
return false
if sender > 0:
snapshot.sanitize(sender, _property_cache)
if snapshot.is_empty(): return false
_history.set_snapshot(tick, snapshot)
return true
@@ -0,0 +1 @@
uid://dwhlghjsgdy4o