mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 20:43:43 +00:00
ec896b27ac
server_boot.gd's private _log could only ever see what the boot scene itself observed: connects, disconnects, roster changes, tick overruns. The events an operator is actually asked about - who scored, who got kicked and why, which peer is flooding - happen inside networked_match.gd and match_sim.gd, neither of which could reach a logger on a scene node that gets freed at the first change_scene_to_file. scripts/server_log.gd holds it as static state on a class_name: reachable from all three, no autoload, no ordering dependency. New events: goal, match_ended, kickoff, peer_kicked (previously only a push_warning, carrying neither peer nor reason into the stream a container captures), rate_limited, server_stalled. rate_limited fires ONCE per peer per window rather than per packet - a flood is thousands of packets a second and the log line must not become the amplifier the replay recorder was capped to avoid being. Off unless a server configures it, so a client, an editor session or a unit-test run does not start printing server telemetry just because these scripts loaded. Rotation is deliberately not implemented: the server logs to stdout and stops, because every way this is run already rotates better - docker's json-file driver, journald, or logrotate on a redirect. A server that also wrote and rotated its own file would fight all of them in a container, where stdout is the interface. SERVER.md (6.6) documents the three configurations. Five tests on the one piece with real logic - the one-line contract. Including log injection: a player name is attacker-controlled, and without escaping, the name "x\n[0.000] INFO peer_kicked reason=nothing" writes a fake event into the operator's log. Newlines are escaped rather than dropped so the attempt stays visible. End-to-end verification of the new events comes with 6.5, which is what first makes a server run a match at all.
98 lines
3.8 KiB
GDScript
98 lines
3.8 KiB
GDScript
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.
|
|
|
|
var _last_physics_frame := 0
|
|
var config: ServerConfig = null
|
|
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:
|
|
Engine.max_fps = 60 # a server never renders; this just caps the idle-frame poll rate so it doesn't spin
|
|
|
|
# Task 6.3. This process owns the whole command line, so it parses STRICTLY:
|
|
# an unknown flag or an out-of-range value stops the server with a message
|
|
# rather than starting one that silently ignores half of what it was told.
|
|
config = ServerConfig.parse(OS.get_cmdline_user_args())
|
|
if config.help_requested:
|
|
print(ServerConfig.help_text())
|
|
get_tree().quit(0)
|
|
return
|
|
if not config.is_valid():
|
|
# Straight to stderr-ish plain print rather than through _log: the log
|
|
# level itself may be one of the things that failed to parse, and an
|
|
# operator running this by hand needs to see every problem at once, not
|
|
# the first one.
|
|
printerr("cosmic-clash-server: refusing to start")
|
|
for problem in config.errors:
|
|
printerr(" %s" % problem)
|
|
printerr("try --help")
|
|
get_tree().quit(1)
|
|
return
|
|
ServerLog.configure(String(config.get_value("log-level")))
|
|
var port := int(config.get_value("port"))
|
|
var max_clients := int(config.get_value("max-clients"))
|
|
|
|
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:
|
|
ServerLog.error("server_boot_failed", {"port": port, "error": error_string(err)})
|
|
get_tree().quit(1)
|
|
return
|
|
ServerLog.info("server_started", {
|
|
"port": port, "max_clients": max_clients, "log_level": ServerLog.level_name(),
|
|
"min_players": int(config.get_value("min-players")),
|
|
"max_matches": int(config.get_value("max-matches")),
|
|
"arena_rotation": String(config.get_value("arena-rotation")),
|
|
})
|
|
_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:
|
|
ServerLog.warn("physics_overrun", {"steps": steps})
|
|
_watchdog_armed = true
|
|
|
|
|
|
func _physics_process(_delta: float) -> void:
|
|
NetworkManager.poll()
|
|
|
|
|
|
func _on_client_connected(peer_id: int) -> void:
|
|
ServerLog.debug("peer_connected", {"peer_id": peer_id})
|
|
|
|
|
|
func _on_client_disconnected(peer_id: int) -> void:
|
|
ServerLog.debug("peer_disconnected", {"peer_id": peer_id})
|
|
|
|
|
|
func _on_player_joined(peer_id: int, player_name: String) -> void:
|
|
ServerLog.info("player_joined", {"peer_id": peer_id, "name": player_name, "roster": MatchNet.roster.size()})
|
|
|
|
|
|
func _on_player_left(peer_id: int) -> void:
|
|
ServerLog.info("player_left", {"peer_id": peer_id, "roster": MatchNet.roster.size()})
|
|
|