## 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) # --- Client prediction signals --- ## Emitted on the server when a client sends input. payload: {peer_id, tick, input_dict} signal client_input_received(peer_id: int, tick: int, input_dict: Dictionary) ## Emitted on the client when the server sends authoritative state. ## entity_id: the simulation entity this state belongs to. signal server_state_received(entity_id: int, snapshot_dict: Dictionary) # --------------------------------------------------------------------------- # 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) # --------------------------------------------------------------------------- # Client Prediction RPCs (Phase 1 — client-side prediction) # --------------------------------------------------------------------------- ## Client → Server: send raw input for the given local tick. ## Called by ClientPrediction.on_after_tick() each physics tick. ## Uses ENet channel 0 (unreliable-ordered) for lowest-latency input delivery. @rpc("unreliable", "any_peer", "call_remote", Chan.INPUT) func send_client_input(tick: int, input_dict: Dictionary) -> void: if not multiplayer.is_server(): return var peer_id: int = multiplayer.get_remote_sender_id() client_input_received.emit(peer_id, tick, input_dict) ## Server → Client: send authoritative entity snapshot for reconciliation. ## Called by server-side code (e.g. GameServer after each tick). ## entity_id identifies which simulation entity this state belongs to. ## Uses ENet channel 1 (reliable-ordered) so state corrections are not dropped. @rpc("unreliable", "authority", "call_remote", Chan.EVENTS) func send_server_state(entity_id: int, snapshot_dict: Dictionary) -> void: if multiplayer.is_server(): return server_state_received.emit(entity_id, snapshot_dict) # --------------------------------------------------------------------------- # 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()