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
+213
View File
@@ -0,0 +1,213 @@
extends Node
# =============================================================================
# Server Browser — Heartbeat Sender
# =============================================================================
# Periodically POSTs server state to the configured master server so the
# server appears in the public server browser.
#
# Relies on cvars defined by the config system:
# sv_master_server — Master server URL (e.g. "http://master.example.com:28961")
# sv_server_id — Unique server identifier (auto-generated if empty)
# sv_heartbeat_interval — Seconds between heartbeats (default 60, range [10,300])
# sv_server_tags — Comma-separated tags for filtering
# sv_server_password — Whether the server requires a password
#
# Signals:
# heartbeat_sent(success: bool, server_id: String, http_code: int)
# master_connection_failed(error_message: String)
# =============================================================================
signal heartbeat_sent(success: bool, server_id: String, http_code: int)
signal master_connection_failed(error_message: String)
const DEFAULT_HEARTBEAT_INTERVAL: float = 60.0
const MIN_INTERVAL: float = 10.0
const MAX_INTERVAL: float = 300.0
const DEFAULT_MASTER_SERVER: String = "http://127.0.0.1:28961"
# HTTP request node for async requests
var _http: HTTPRequest
var _timer: Timer
var _server_id: String = ""
var _enabled: bool = false
func _ready() -> void:
# Create HTTP request node
_http = HTTPRequest.new()
_http.name = "MasterServerHTTP"
_http.timeout = 10
_http.use_threads = true
add_child(_http)
_http.request_completed.connect(_on_heartbeat_completed)
# Create timer for periodic heartbeats
_timer = Timer.new()
_timer.name = "HeartbeatTimer"
_timer.one_shot = false
add_child(_timer)
_timer.timeout.connect(_send_heartbeat)
func start() -> void:
"""Begin the heartbeat loop. Call after cvars are loaded and server is ready."""
var master_url = _get_cvar_string("sv_master_server", "")
if master_url.is_empty():
push_warning("[ServerBrowser] sv_master_server is empty — heartbeat disabled")
return
# Strip trailing slash
master_url = master_url.trim_suffix("/")
if not master_url.begins_with("http"):
master_url = "http://" + master_url
if not master_url.begins_with("http://") and not master_url.begins_with("https://"):
push_warning("[ServerBrowser] Invalid master server URL: ", master_url)
return
_server_id = _get_cvar_string("sv_server_id", "")
_enabled = true
var interval = _get_cvar_float("sv_heartbeat_interval", DEFAULT_HEARTBEAT_INTERVAL)
interval = clamp(interval, MIN_INTERVAL, MAX_INTERVAL)
_timer.wait_time = interval
_timer.start()
print("[ServerBrowser] Heartbeat enabled — master: ", master_url, " interval: ", interval, "s")
# Send initial heartbeat immediately
_send_heartbeat()
func stop() -> void:
"""Stop sending heartbeats."""
_enabled = false
_timer.stop()
print("[ServerBrowser] Heartbeat disabled")
func set_server_id(new_id: String) -> void:
"""Update the server id (e.g., after first heartbeat response)."""
_server_id = new_id
func _get_master_url() -> String:
var raw = _get_cvar_string("sv_master_server", DEFAULT_MASTER_SERVER).trim_suffix("/")
if raw.is_empty():
return ""
if not raw.begins_with("http"):
raw = "http://" + raw
return raw
func _send_heartbeat() -> void:
if not _enabled:
return
var master_url = _get_master_url()
if master_url.is_empty():
return
# Build heartbeat payload
var payload = {
"server_id": _server_id,
"name": _get_cvar_string("sv_server_name", "Tactical Shooter Server"),
"map": _get_cvar_string("sv_map", "unknown"),
"players": _get_player_count(),
"max_players": _get_cvar_int("sv_max_players", 16),
"game_mode": _get_cvar_string("sv_game_mode", "deathmatch"),
"version": _get_cvar_string("sv_version", "1.0.0"),
"tags": _get_cvar_string("sv_server_tags", ""),
"password": _get_cvar_int("sv_server_password", 0) != 0,
"host": _get_external_ip(),
"port": _get_cvar_int("sv_port", 0),
"steam_id": _get_cvar_string("sv_steam_id", ""),
}
var json_string = JSON.stringify(payload)
var headers = ["Content-Type: application/json"]
var endpoint = master_url + "/api/v1/heartbeat"
var err = _http.request(endpoint, headers, HTTPClient.METHOD_POST, json_string)
if err != OK:
push_error("[ServerBrowser] Failed to send heartbeat: HTTP request error ", err)
master_connection_failed.emit("HTTP request failed (code " + str(err) + ")")
func _on_heartbeat_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
if result != HTTPRequest.RESULT_SUCCESS:
var msg = "Heartbeat failed — network error " + str(result)
push_warning("[ServerBrowser] ", msg)
master_connection_failed.emit(msg)
heartbeat_sent.emit(false, _server_id, response_code)
return
if response_code != 200:
var msg = "Heartbeat failed — HTTP " + str(response_code)
push_warning("[ServerBrowser] ", msg)
master_connection_failed.emit(msg)
heartbeat_sent.emit(false, _server_id, response_code)
return
# Parse response for server_id (first heartbeat may get one assigned)
var body_text = body.get_string_from_utf8()
var json = JSON.new()
var parse_err = json.parse(body_text)
if parse_err == OK and json.data is Dictionary:
if json.data.has("server_id") and not json.data["server_id"].is_empty():
_server_id = json.data["server_id"]
heartbeat_sent.emit(true, _server_id, response_code)
# -------------------------------------------------------------------------
# Helper: read cvars from the config system (if available)
# -------------------------------------------------------------------------
func _get_cvar_string(name: String, default_value: String) -> String:
"""Try the CvarRegistry autoload first, fall back to ProjectSettings."""
if Engine.has_singleton("CvarRegistry"):
var reg = Engine.get_singleton("CvarRegistry")
if reg.has_method("get_string"):
return reg.get_string(name, default_value)
# Fallback to ProjectSettings if no cvar registry exists
if ProjectSettings.has_setting(name):
return ProjectSettings.get_setting(name)
return default_value
func _get_cvar_int(name: String, default_value: int) -> int:
if Engine.has_singleton("CvarRegistry"):
var reg = Engine.get_singleton("CvarRegistry")
if reg.has_method("get_int"):
return reg.get_int(name, default_value)
if ProjectSettings.has_setting(name):
return ProjectSettings.get_setting(name)
return default_value
func _get_cvar_float(name: String, default_value: float) -> float:
if Engine.has_singleton("CvarRegistry"):
var reg = Engine.get_singleton("CvarRegistry")
if reg.has_method("get_float"):
return reg.get_float(name, default_value)
if ProjectSettings.has_setting(name):
return ProjectSettings.get_setting(name)
return default_value
func _get_player_count() -> int:
"""Get current connected player count."""
# Try to read from game's player manager singleton
if Engine.has_singleton("PlayerManager"):
var pm = Engine.get_singleton("PlayerManager")
if pm.has_method("get_player_count"):
return pm.get_player_count()
return 0
func _get_external_ip() -> String:
"""Return our public/external IP if known, or empty for auto-detect."""
return _get_cvar_string("sv_external_ip", "")