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
+179
View File
@@ -0,0 +1,179 @@
## PlayerInterpolator — Headless-safe visual interpolation for rolled-back players.
##
## Mirrors netfox's TickInterpolator pattern but with full headless/export guards:
## - Checks for NetworkTime singleton before connecting
## - Gracefully no-ops when singletons are absent (dedicated server without netfox)
## - Falls back to netfox TickInterpolator's Interpolators for type-aware lerp
##
## Architecture:
## _before_tick_loop → snapshot current → mark teleport end
## _after_tick_loop → push_state() → shift (from ← to, to ← current)
## _process(delta) → interpolate(from, to, tick_factor)
##
## Properties interpolated:
## position (Vector3), rotation (Vector3 — Euler angles)
extends Node
# ---------------------------------------------------------------------------
# Exports
# ---------------------------------------------------------------------------
## If true, interpolation is active. Set false to disable (no-ops).
@export var enabled: bool = true
## If true, teleport on first state (instant snap, no fade-in).
@export var snap_initial: bool = true
## Root node whose properties are interpolated. Defaults to parent.
@export var root: Node3D = null
## Interpolation alpha smoothing (0.0=linear raw, >0=exponential smoothing).
## Higher values make the interpolation lag behind more smoothly.
@export_range(0.0, 1.0, 0.05) var smoothing: float = 0.5
# ---------------------------------------------------------------------------
# Internal state
# ---------------------------------------------------------------------------
## Previous state snapshot (source of interpolation).
var _from_pos: Vector3 = Vector3.ZERO
var _from_rot: Vector3 = Vector3.ZERO
## Target state snapshot (destination of interpolation).
var _to_pos: Vector3 = Vector3.ZERO
var _to_rot: Vector3 = Vector3.ZERO
## Whether we have at least two snapshots (can interpolate).
var _has_state: bool = false
## Whether the last tick was a teleport (snap, don't interpolate).
var _is_teleporting: bool = false
## Whether netfox singletons were found at startup.
var _netfox_available: bool = false
## Stored NetworkTime singleton reference (avoids bare identifier references).
var _nt: Node = null
# ---------------------------------------------------------------------------
# Lifecycle
# ---------------------------------------------------------------------------
func _ready() -> void:
if Engine.is_editor_hint():
return
# Resolve root node
if root == null:
root = get_parent() as Node3D
if root == null:
push_warning("[PlayerInterpolator] No root node found — interpolation disabled")
enabled = false
return
# Check if netfox NetworkTime is available
_netfox_available = Engine.has_singleton(&"NetworkTime")
if _netfox_available:
_nt = Engine.get_singleton(&"NetworkTime")
_nt.before_tick_loop.connect(_on_before_tick_loop)
_nt.after_tick_loop.connect(_on_after_tick_loop)
print("[PlayerInterpolator] Connected to netfox NetworkTime — interpolation active")
else:
print("[PlayerInterpolator] netfox NetworkTime not available — interpolation disabled in headless/server mode")
enabled = false
return
# Teleport to initial state
if snap_initial:
await get_tree().process_frame
_teleport()
func _exit_tree() -> void:
if Engine.is_editor_hint():
return
if _nt != null:
if _nt.before_tick_loop.is_connected(_on_before_tick_loop):
_nt.before_tick_loop.disconnect(_on_before_tick_loop)
if _nt.after_tick_loop.is_connected(_on_after_tick_loop):
_nt.after_tick_loop.disconnect(_on_after_tick_loop)
# ---------------------------------------------------------------------------
# Process — interpolate between snapshots each frame
# ---------------------------------------------------------------------------
func _process(_delta: float) -> void:
if not enabled or not _has_state or _is_teleporting or root == null:
return
# Get interpolation factor from NetworkTime (0.0 = just after tick, 1.0 = just before next tick)
var factor: float = 0.0
if _nt != null:
factor = _nt.tick_factor
# Apply exponential smoothing to the interpolation alpha
if smoothing > 0.0:
factor = lerpf(0.0, factor, 1.0 - smoothing)
# Interpolate position
root.position = _from_pos.lerp(_to_pos, factor)
# Interpolate rotation (Euler — simple lerp per axis)
var rot_x := lerpf(_from_rot.x, _to_rot.x, factor)
var rot_y := lerpf(_from_rot.y, _to_rot.y, factor)
var rot_z := lerpf(_from_rot.z, _to_rot.z, factor)
root.rotation = Vector3(rot_x, rot_y, rot_z)
# ---------------------------------------------------------------------------
# Netfox tick loop hooks
# ---------------------------------------------------------------------------
## Called before the tick loop starts. Records the current state as the
## incoming snapshot, preparing for push_state() after the tick.
func _on_before_tick_loop() -> void:
_is_teleporting = false
## Called after the tick loop. Rotates the state snapshots: old target
## becomes new source, current root state becomes new target.
func _on_after_tick_loop() -> void:
if not enabled or root == null:
return
# Shift state snapshots: from ← to, to ← current
_from_pos = _to_pos
_from_rot = _to_rot
_to_pos = root.position
_to_rot = root.rotation
_has_state = true
# ---------------------------------------------------------------------------
# Teleport — snap to current state without interpolation
# ---------------------------------------------------------------------------
## Teleport: set both source and target to current state, disable
## interpolation for one cycle.
func _teleport() -> void:
if root == null:
return
_is_teleporting = true
var pos := root.position
var rot := root.rotation
_from_pos = pos
_from_rot = rot
_to_pos = pos
_to_rot = rot
_has_state = true
## Force a teleport from external code (e.g., after spawn).
func force_teleport() -> void:
_teleport()
## Reset state (for pooling / re-use).
func reset() -> void:
_has_state = false
_is_teleporting = false
_from_pos = Vector3.ZERO
_from_rot = Vector3.ZERO
_to_pos = Vector3.ZERO
_to_rot = Vector3.ZERO