## NetworkManager — ENet Transport + Player Replication Singleton ## ## Autoload that wraps Godot 4's ENetMultiplayerPeer and provides ## player spawn/despawn broadcasting to all connected clients. ## Server creates RPC broadcasts; clients receive and emit signals. ## ## Architecture: ## server mode → start_server(port) → ENetMultiplayerPeer server ## client mode → join_server(host,port)→ ENetMultiplayerPeer client ## replication → _broadcast_spawn_player / _broadcast_despawn_player ## (server→all clients RPC) ## ## Channels (3-lane layout per Phase 0 research): ## 0 unreliable-ordered → 128Hz input / transform deltas ## 1 reliable-ordered → game events, spawn, damage, chat ## 2 unreliable → telemetry / VOIP metadata extends Node # --------------------------------------------------------------------------- # Signals # --------------------------------------------------------------------------- signal server_started(port: int) signal server_stopped() signal player_connected(peer_id: int) signal player_disconnected(peer_id: int) signal connection_succeeded() signal connection_failed(error_message: String) # --- Player replication signals (emitted on all peers after RPC broadcast) --- signal remote_player_spawned(peer_id: int, pos: Vector3) signal remote_player_despawned(peer_id: int) # --------------------------------------------------------------------------- # Constants # --------------------------------------------------------------------------- const DEFAULT_PORT: int = 34197 const CHANNELS: int = 3 # 0=input, 1=events, 2=telemetry # Max clients is read from ServerConfig at start_server() time. # This default is used before ServerConfig is available. var max_clients: int = 16 # Channel indices enum Chan { INPUT = 0, EVENTS = 1, TELEMETRY = 2, } # --------------------------------------------------------------------------- # State # --------------------------------------------------------------------------- var is_server: bool = false : get = _is_server var is_client: bool = false : get = _is_client var peer: ENetMultiplayerPeer = null func _is_server() -> bool: return is_server func _is_client() -> bool: return is_client # --------------------------------------------------------------------------- # Server API # --------------------------------------------------------------------------- ## Start a dedicated server on [port]. ## Uses max_clients (which should be set from ServerConfig before calling). ## Returns OK or ERR_* on failure. func start_server(port: int = DEFAULT_PORT) -> Error: if peer and peer.get_connection_status() != MultiplayerPeer.CONNECTION_DISCONNECTED: stop() # If ServerConfig is available, use it for max_clients if ServerConfig and ServerConfig.has_method(&"get_config_path"): max_clients = ServerConfig.max_players peer = ENetMultiplayerPeer.new() peer.set_bind_ip("*") var err: Error = peer.create_server(port, max_clients, CHANNELS, 0, 0) if err != OK: peer = null return err multiplayer.multiplayer_peer = peer multiplayer.multiplayer_peer.peer_connected.connect(_on_peer_connected) multiplayer.multiplayer_peer.peer_disconnected.connect(_on_peer_disconnected) is_server = true server_started.emit(port) print("[NetworkManager] Server started on port %d" % port) return OK ## Stop the server / disconnect. func stop() -> void: if not peer: return if is_server: multiplayer.multiplayer_peer.peer_connected.disconnect(_on_peer_connected) multiplayer.multiplayer_peer.peer_disconnected.disconnect(_on_peer_disconnected) peer.close() multiplayer.multiplayer_peer = null peer = null is_server = false is_client = false server_stopped.emit() print("[NetworkManager] Stopped") # --------------------------------------------------------------------------- # Client API # --------------------------------------------------------------------------- ## Connect to a remote server. func join_server(host: String, port: int = DEFAULT_PORT) -> Error: if peer and peer.get_connection_status() != MultiplayerPeer.CONNECTION_DISCONNECTED: stop() peer = ENetMultiplayerPeer.new() var err: Error = peer.create_client(host, port, CHANNELS, 0, 0) if err != OK: peer = null return err multiplayer.multiplayer_peer = peer connection_succeeded.emit() print("[NetworkManager] Connecting to %s:%d ..." % [host, port]) is_client = true return OK # --------------------------------------------------------------------------- # Player Replication RPCs (broadcast server → all clients) # --------------------------------------------------------------------------- ## Server calls this when a new player joins. ## Broadcasts to all clients so they can create a visual player node. @rpc("authority", "call_local", "reliable") func broadcast_spawn_player(peer_id: int, pos: Vector3, is_team_a: bool) -> void: if not multiplayer.is_server(): print("[NetworkManager] Client received spawn: peer=%d at (%.1f, %.1f)" % [peer_id, pos.x, pos.z]) remote_player_spawned.emit(peer_id, pos) ## Server calls this when a player leaves. @rpc("authority", "call_local", "reliable") func broadcast_despawn_player(peer_id: int) -> void: if not multiplayer.is_server(): print("[NetworkManager] Client received despawn: peer=%d" % peer_id) remote_player_despawned.emit(peer_id) # --------------------------------------------------------------------------- # Event handlers # --------------------------------------------------------------------------- func _on_peer_connected(id: int) -> void: print("[NetworkManager] Peer connected: %d" % id) player_connected.emit(id) func _on_peer_disconnected(id: int) -> void: print("[NetworkManager] Peer disconnected: %d" % id) player_disconnected.emit(id) func _process(_delta: float) -> void: # Godot does internal ENet polling via MultiplayerAPI; # explicit polling is reserved for future custom packet handling. pass func _exit_tree() -> void: stop()