7be50e3313
Two genuine bugs, found by reproducing rather than trusting the branch's own state: 1. scenes/player.tscn collided with PC2's client-side player scene at the exact same path (server-authoritative PlayerNode vs. the local single-player character) -- whichever merged last would silently overwrite the other. The branch's own last commit had literally committed unresolved <<<<<<< conflict markers into the file instead of resolving them. Renamed the server's version to scenes/server_player.tscn, restored scenes/player.tscn to match main's real client scene exactly, repointed scripts/server.gd's preload. 2. scenes/server.tscn's root Node had no script attached at all -- it only ever worked before because project.godot had `Server` registered as a project-wide autoload (the exact wholesale-overwrite this task was already asked to fix). Once that autoload was correctly removed, nothing attached server.gd to the scene tree anymore and the dedicated server silently did nothing. Attached scripts/server.gd directly to scenes/server.tscn's root node instead. Verified: godot --headless --import/--quit both clean, full 10-suite client smoke test all passing, and the dedicated server actually listens on UDP port 7777 when launched explicitly (`godot --path . scenes/server.tscn`) -- confirmed with `ss -ulnp`, not just trusting the startup print (first checked with `ss -tlnp`, which only shows TCP and falsely suggested it wasn't listening at all; ENet is UDP). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M6rUEynmv2QmweocAQYARN
175 lines
4.9 KiB
GDScript
175 lines
4.9 KiB
GDScript
## ServerMain - Dedicated server root node.
|
|
## Accepts ENet connections, spawns Player nodes for each peer,
|
|
## and runs the simulation tick loop at 20 Hz.
|
|
extends Node
|
|
|
|
|
|
## Maximum number of concurrent players.
|
|
const MAX_PLAYERS := 16
|
|
|
|
## Server tick rate (Hz).
|
|
const TICK_RATE := 20
|
|
|
|
## Tick duration in seconds.
|
|
const TICK_DURATION := 1.0 / TICK_RATE
|
|
|
|
## ENet server port.
|
|
const SERVER_PORT := 7777
|
|
|
|
## Network peer used for ENet communication.
|
|
var network_peer: ENetMultiplayerPeer
|
|
|
|
## Map of peer_id -> Player node.
|
|
var players: Dictionary = {}
|
|
|
|
## Current tick counter (server-authoritative).
|
|
var tick: int = 0
|
|
|
|
## Whether the server is running.
|
|
var is_running: bool = false
|
|
|
|
## Accumulated real time for fixed timestep simulation.
|
|
var accumulator: float = 0.0
|
|
|
|
## Player scene resource (loaded once).
|
|
var _player_scene: PackedScene
|
|
|
|
|
|
func _ready() -> void:
|
|
# Preload the player scene once.
|
|
_player_scene = preload("res://scenes/server_player.tscn")
|
|
|
|
# Create and assign the ENet server peer.
|
|
network_peer = _create_server_peer()
|
|
if network_peer:
|
|
multiplayer.multiplayer_peer = network_peer
|
|
is_running = true
|
|
print("[Server] Started on port %d" % SERVER_PORT)
|
|
else:
|
|
printerr("[Server] Failed to create ENet peer.")
|
|
get_tree().quit(1)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Server creation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func _create_server_peer() -> ENetMultiplayerPeer:
|
|
var peer := ENetMultiplayerPeer.new()
|
|
var error := peer.create_server(SERVER_PORT, MAX_PLAYERS)
|
|
if error != OK:
|
|
printerr("[Server] create_server error: %d" % error)
|
|
return null
|
|
return peer
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixed-timestep simulation loop
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func _process(delta: float) -> void:
|
|
if not is_running:
|
|
return
|
|
|
|
# Fixed timestep accumulator.
|
|
accumulator += delta
|
|
while accumulator >= TICK_DURATION:
|
|
accumulator -= TICK_DURATION
|
|
_tick()
|
|
|
|
|
|
func _tick() -> void:
|
|
"""Main server simulation tick (called at TICK_RATE)."""
|
|
tick += 1
|
|
|
|
# Process player inputs and update state.
|
|
for peer_id in players:
|
|
var player: Node3D = players[peer_id]
|
|
if player and player.has_method("process_server_input"):
|
|
player.process_server_input(tick)
|
|
|
|
# Broadcast snapshot to all clients.
|
|
_broadcast_snapshot()
|
|
|
|
|
|
func _broadcast_snapshot() -> void:
|
|
"""Broadcast the current authoritative state to all connected clients."""
|
|
var snapshot_data := {
|
|
"tick": tick,
|
|
"players": {},
|
|
}
|
|
|
|
for peer_id in players:
|
|
var player: Node3D = players[peer_id]
|
|
if player and player.has_method("get_server_state"):
|
|
snapshot_data.players[peer_id] = player.get_server_state()
|
|
|
|
# Send snapshot to all peers except the server itself (peer_id == 1).
|
|
var peer_ids: Array = multiplayer.get_peers()
|
|
for pid in peer_ids:
|
|
if pid == 1: # 1 is the server itself
|
|
continue
|
|
rpc_id(pid, "_receive_snapshot", snapshot_data)
|
|
|
|
|
|
# Remote: clients receive snapshots.
|
|
@rpc("any_peer", "call_remote", "reliable")
|
|
func _receive_snapshot(snapshot_data: Dictionary) -> void:
|
|
pass # Handled by client scene.
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Connection lifecycle
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func _on_peer_connected(peer_id: int) -> void:
|
|
print("[Server] Peer %d connected." % peer_id)
|
|
|
|
if players.size() >= MAX_PLAYERS:
|
|
print("[Server] Max players reached. Disconnecting peer %d." % peer_id)
|
|
rpc_id(peer_id, "_kick", "Server is full.")
|
|
return
|
|
|
|
# Spawn a Player node for this peer.
|
|
var player: Node3D = _player_scene.instantiate()
|
|
player.name = "Player_%d" % peer_id
|
|
player.peer_id = peer_id
|
|
add_child(player)
|
|
players[peer_id] = player
|
|
|
|
print("[Server] Spawned player node for peer %d." % peer_id)
|
|
|
|
|
|
func _on_peer_disconnected(peer_id: int) -> void:
|
|
print("[Server] Peer %d disconnected." % peer_id)
|
|
if players.has(peer_id):
|
|
var player: Node3D = players[peer_id]
|
|
if player.is_inside_tree():
|
|
player.queue_free()
|
|
players.erase(peer_id)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# RPCs
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@rpc("any_peer", "call_remote", "reliable")
|
|
func _kick(reason: String) -> void:
|
|
print("[Server] Kicking caller: %s" % reason)
|
|
rpc_id(multiplayer.get_remote_sender_id(), "_disconnect", reason)
|
|
|
|
|
|
@rpc("call_remote", "reliable")
|
|
func _disconnect(reason: String) -> void:
|
|
print("[Server] Disconnecting: %s" % reason)
|
|
get_tree().quit(0)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Signal connections
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func _enter_tree() -> void:
|
|
multiplayer.peer_connected.connect(_on_peer_connected)
|
|
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
|