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
+155
View File
@@ -0,0 +1,155 @@
## RoundReplicator — Client-side round state synced from RoundManager Self-RPCs.
##
## The server's round_manager signals arrive on clients via netfox Self-RPC
## (@rpc with call_local). This replicator listens to those signals and
## maintains a local round state snapshot for rollback-accessible use.
##
## This script is ONLY loaded from client scenes.
extends Node
# ---------------------------------------------------------------------------
# Signals (mirrors server RoundManager for client-side consumers)
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# Round state (mirrors server RoundManager)
# ---------------------------------------------------------------------------
enum RoundState {
INACTIVE = 0,
WAITING_FOR_PLAYERS = 1,
WARMUP = 2,
LIVE = 3,
POST_ROUND = 4,
MATCH_END = 5,
}
# Current round state (populated from server broadcasts)
var round_state: int = RoundState.INACTIVE
var round_number: int = 0
var team_a_score: int = 0
var team_b_score: int = 0
var time_remaining: float = 0.0
var round_time_seconds: float = 600.0
var win_limit: int = 3
# ---------------------------------------------------------------------------
# Rollback-friendly state (used by rollback_tick in player scripts)
# ---------------------------------------------------------------------------
## Whether the round is in freeze time (before round start).
var is_frozen: bool = false
## The network tick when freeze ends (for rollback checks).
var freeze_end_tick: int = -1
# ---------------------------------------------------------------------------
# Lifecycle
# ---------------------------------------------------------------------------
func _ready() -> void:
# Connect to RoundManager signals (which now arrive via Self-RPC)
if Engine.has_singleton(&"RoundManager"):
var rm = Engine.get_singleton(&"RoundManager")
if rm.round_started.is_connected(_on_round_start):
return # Already connected
rm.round_started.connect(_on_round_start)
rm.round_ended.connect(_on_round_end)
rm.match_ended.connect(_on_match_end)
rm.score_changed.connect(_on_score_change)
else:
push_warning("[RoundReplicator] RoundManager not available")
func _on_connected() -> void:
print("[RoundReplicator] Connected to server — awaiting round state")
# ---------------------------------------------------------------------------
# Signal handlers
# ---------------------------------------------------------------------------
func _on_round_start(round_num: int) -> void:
round_number = round_num
# Read round_time_seconds from RoundManager (replicated via _rpc_round_started)
if Engine.has_singleton(&"RoundManager"):
var rm = Engine.get_singleton(&"RoundManager")
round_time_seconds = rm.round_time_seconds if rm.round_time_seconds > 0 else 600
time_remaining = round_time_seconds
round_state = RoundState.LIVE
# Freeze time handling (10 seconds for first round, 5 for subsequent)
var freeze_time: float = 10.0 if round_num <= 1 else 5.0
is_frozen = true
var nt = Engine.get_singleton(&"NetworkTime")
if nt:
freeze_end_tick = nt.tick + int(freeze_time * 64.0) # 64 tick-rate assumption
else:
freeze_end_tick = -1
print("[RoundReplicator] Round %d started (%.0fs, frozen for %.0fs)" % [round_num, round_time_seconds, freeze_time])
func _on_round_end(winner_team: int, reason: String) -> void:
# Read scores from RoundManager (replicated by Self-RPC)
if Engine.has_singleton(&"RoundManager"):
var rm = Engine.get_singleton(&"RoundManager")
team_a_score = rm.team_a_score
team_b_score = rm.team_b_score
is_frozen = false
round_state = RoundState.POST_ROUND
print("[RoundReplicator] Round ended: %s wins (%s). Score: %d-%d" % [_team_name(winner_team), reason, team_a_score, team_b_score])
func _on_match_end(winner_team: int) -> void:
# Read scores from RoundManager
if Engine.has_singleton(&"RoundManager"):
var rm = Engine.get_singleton(&"RoundManager")
team_a_score = rm.team_a_score
team_b_score = rm.team_b_score
round_state = RoundState.MATCH_END
print("[RoundReplicator] MATCH OVER: %s wins %d-%d" % [_team_name(winner_team), team_a_score, team_b_score])
func _on_score_change(a_score: int, b_score: int) -> void:
team_a_score = a_score
team_b_score = b_score
print("[RoundReplicator] Score updated: %d-%d" % [a_score, b_score])
func _process(delta: float) -> void:
# Update time remaining (for HUD display)
if round_state == RoundState.LIVE:
time_remaining -= delta
if time_remaining < 0.0:
time_remaining = 0.0
# Update freeze state
if is_frozen and freeze_end_tick >= 0:
var nt = Engine.get_singleton(&"NetworkTime")
if nt:
if nt.tick >= freeze_end_tick:
is_frozen = false
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
## Get a snapshot of the current round state (for HUD/UI).
func get_snapshot() -> Dictionary:
return {
"state": round_state,
"round_number": round_number,
"team_a_score": team_a_score,
"team_b_score": team_b_score,
"time_remaining": time_remaining,
"is_frozen": is_frozen,
}
## Check if the player can move (not frozen, not dead, round is LIVE).
func can_player_move(player_is_alive: bool) -> bool:
return player_is_alive and round_state == RoundState.LIVE and not is_frozen
## Check if the round is currently active (LIVE or WARMUP).
func is_round_active() -> bool:
return round_state == RoundState.LIVE or round_state == RoundState.WARMUP
## Get the human-readable team name.
static func _team_name(team: int) -> String:
match team:
0: return "Draw"
1: return "Team A"
2: return "Team B"
_: return "Unknown"