extends Node # Headless dedicated server entry point (task 1.6). Parses CLI args, hosts # via NetworkManager, logs structured lines, and watches for physics-tick # overrun (§9 gotcha 9: Engine.max_physics_steps_per_frame defaults to 8; # a tick overrunning 16.7ms backs up the accumulator and the next frame # runs multiple ticks, spiking CPU further — worth logging, not just # silently absorbing). # # Run: godot --headless --path Game res://scenes/server_boot.tscn -- --port=7777 # # Deliberately does not spawn a match yet — that's Phase 2's networked_match # scene. This is just the process shell: listen, log, idle cheaply. const LOG_LEVELS := {"debug": 0, "info": 1, "warn": 2, "error": 3} var _boot_ms := 0 var _last_physics_frame := 0 var _log_level := 1 # info var _watchdog_armed := false # skip the first _process(): engine startup scheduling can batch several physics frames before the first idle frame runs, which isn't a real overrun func _ready() -> void: _boot_ms = Time.get_ticks_msec() Engine.max_fps = 60 # a server never renders; this just caps the idle-frame poll rate so it doesn't spin var port := NetworkManager.DEFAULT_PORT var max_clients := NetworkManager.MAX_CLIENTS for arg in OS.get_cmdline_user_args(): if arg.begins_with("--port="): port = int(arg.substr("--port=".length())) elif arg.begins_with("--max-clients="): max_clients = int(arg.substr("--max-clients=".length())) elif arg.begins_with("--log-level="): var level_name := arg.substr("--log-level=".length()) if LOG_LEVELS.has(level_name): _log_level = LOG_LEVELS[level_name] else: _log("error", "bad_log_level", {"given": level_name, "valid": LOG_LEVELS.keys()}) get_tree().quit(1) return NetworkManager.client_connected.connect(_on_client_connected) NetworkManager.client_disconnected.connect(_on_client_disconnected) MatchNet.player_joined.connect(_on_player_joined) MatchNet.player_left.connect(_on_player_left) var err := NetworkManager.host(port, max_clients) if err != OK: _log("error", "server_boot_failed", {"port": port, "error": error_string(err)}) get_tree().quit(1) return _log("info", "server_started", {"port": port, "max_clients": max_clients}) _last_physics_frame = Engine.get_physics_frames() func _process(_delta: float) -> void: NetworkManager.poll() var current := Engine.get_physics_frames() var steps := current - _last_physics_frame _last_physics_frame = current # §9 gotcha 6: with physics_jitter_fix = 0.0, frames legitimately # alternate between 0 and 2 ticks even on an idle, healthy server — # that's expected quantisation, not backlog. A real overrun is the # accumulator failing to drain back down, i.e. 3+ ticks in one frame. if steps > 2 and _watchdog_armed: _log("warn", "physics_overrun", {"steps": steps}) _watchdog_armed = true func _physics_process(_delta: float) -> void: NetworkManager.poll() func _on_client_connected(peer_id: int) -> void: _log("debug", "peer_connected", {"peer_id": peer_id}) func _on_client_disconnected(peer_id: int) -> void: _log("debug", "peer_disconnected", {"peer_id": peer_id}) func _on_player_joined(peer_id: int, player_name: String) -> void: _log("info", "player_joined", {"peer_id": peer_id, "name": player_name}) func _on_player_left(peer_id: int) -> void: _log("info", "player_left", {"peer_id": peer_id}) func _log(level: String, event: String, fields: Dictionary) -> void: if LOG_LEVELS.get(level, 1) < _log_level: return var parts := PackedStringArray() for key in fields: parts.append("%s=%s" % [key, str(fields[key])]) var elapsed_sec := (Time.get_ticks_msec() - _boot_ms) / 1000.0 print("[%.3f] %s %s %s" % [elapsed_sec, level.to_upper(), event, " ".join(parts)])