extends Node # Network Manager - Handles multiplayer connections and player spawning # Supports both hosting and joining games # Auto-detects platform and uses appropriate networking: # - ENet for native builds (Windows, Linux, Mac) # - WebRTC for web builds (requires signaling server) signal player_connected(peer_id, player_info) signal player_disconnected(peer_id, player_info) signal connection_failed() signal connection_succeeded() const DEFAULT_PORT = 21212 const MAX_PLAYERS = 8 const MATCHBOX_SERVER = "wss://matchbox.thefirstboss.com" const STUN_SERVER = "stun:ruinborn.thefirstboss.com:3578" var players_info = {} # Dictionary of peer_id -> {local_player_count: int, player_names: []} var local_player_count = 1 # How many local players on this machine var is_hosting = false var show_room_labels = false # Show room labels when in debug mode var network_mode: int = 0 # 0 = ENet, 1 = WebRTC, 2 = WebSocket var room_id = "" # Room ID for Matchbox (WebRTC) or WebSocket server URL var matchbox_client: Node = null # Matchbox client instance const WEBSOCKET_SERVER_URL = "ws://ruinborn.thefirstboss.com:21212" # WebSocket server URL func _ready(): # Detect if running in browser - default to WebRTC on web, ENet on native if OS.get_name() == "Web": network_mode = 1 # WebRTC default for web print("NetworkManager: Detected Web platform, defaulting to WebRTC") print("NetworkManager: Matchbox server: ", MATCHBOX_SERVER) else: network_mode = 0 # ENet default for native print("NetworkManager: Using ENet by default for native platform") print("NetworkManager: You can switch network modes in the menu") # Connect multiplayer signals multiplayer.peer_connected.connect(_on_peer_connected) multiplayer.peer_disconnected.connect(_on_peer_disconnected) multiplayer.connected_to_server.connect(_on_connected_to_server) multiplayer.connection_failed.connect(_on_connection_failed) multiplayer.server_disconnected.connect(_on_server_disconnected) func set_network_mode(mode: int): # 0 = ENet, 1 = WebRTC, 2 = WebSocket # WebRTC is only available on web builds if mode == 1 and OS.get_name() != "Web": push_error("NetworkManager: WebRTC is not available on native platforms. WebRTC only works in web builds. Falling back to ENet.") network_mode = 0 print("NetworkManager: ENet mode enabled (WebRTC not available on native platforms)") return network_mode = mode match mode: 0: print("NetworkManager: ENet mode enabled") 1: print("NetworkManager: WebRTC mode enabled") print("NetworkManager: Matchbox server: ", MATCHBOX_SERVER) 2: print("NetworkManager: WebSocket mode enabled") print("NetworkManager: WebSocket server: ", WEBSOCKET_SERVER_URL) func force_webrtc_mode(enable: bool): # Legacy function for backwards compatibility if enable: set_network_mode(1) else: set_network_mode(0) func host_game(port: int = DEFAULT_PORT, matchbox_room: String = "") -> bool: var peer var error if network_mode == 1: # WebRTC # WebRTC for browser builds using Matchbox signaling # Generate room ID if not provided if matchbox_room.is_empty(): room_id = _generate_room_id() else: room_id = matchbox_room print("NetworkManager: Creating WebRTC host with room ID: ", room_id) print("NetworkManager: Share this room code with players!") print("NetworkManager: Matchbox URL: ", MATCHBOX_SERVER, "/", room_id) # Create Matchbox client var matchbox_script = load("res://scripts/matchbox_client.gd") matchbox_client = Node.new() matchbox_client.set_script(matchbox_script) add_child(matchbox_client) # Connect Matchbox signals matchbox_client.peer_joined.connect(_on_matchbox_peer_joined) matchbox_client.peer_left.connect(_on_matchbox_peer_left) matchbox_client.peer_connected.connect(_on_matchbox_peer_connected) matchbox_client.connection_succeeded.connect(_on_matchbox_connected) matchbox_client.connection_failed.connect(_on_matchbox_connection_failed) matchbox_client.webrtc_ready.connect(_on_matchbox_webrtc_ready) # Setup WebRTC peer (will be initialized when Matchbox connects) # For now, we'll set it up after Matchbox connection is_hosting = true # Connect to Matchbox room (pass is_hosting = true) if not matchbox_client.connect_to_room(room_id, true): push_error("Failed to connect to Matchbox room") return false # Register the host as a player (peer_id 1) players_info[1] = { "local_player_count": local_player_count, "player_names": _generate_player_names(local_player_count, 1) } return true elif network_mode == 2: # WebSocket # WebSocket for both native and web builds # Generate room ID if not provided if matchbox_room.is_empty(): room_id = _generate_room_id() else: room_id = matchbox_room print("NetworkManager: Creating WebSocket host with room ID: ", room_id) print("NetworkManager: Share this room code with players!") print("NetworkManager: WebSocket URL: ", WEBSOCKET_SERVER_URL, "/", room_id) peer = WebSocketMultiplayerPeer.new() var url = WEBSOCKET_SERVER_URL + "/" + room_id error = peer.create_server(port) if error != OK: push_error("Failed to create WebSocket server: " + str(error)) return false multiplayer.multiplayer_peer = peer is_hosting = true print("WebSocket server started on port ", port) print("Players can join at: ", url) # Register the host as a player var my_id = multiplayer.get_unique_id() players_info[my_id] = { "local_player_count": local_player_count, "player_names": _generate_player_names(local_player_count, my_id) } return true else: # ENet (mode 0) # ENet for native builds peer = ENetMultiplayerPeer.new() error = peer.create_server(port, MAX_PLAYERS) if error != OK: push_error("Failed to create ENet server: " + str(error)) return false multiplayer.multiplayer_peer = peer is_hosting = true print("ENet server started on port ", port) print("Players can join at: ", get_local_ip(), ":", port) # Register the host as a player var my_id = multiplayer.get_unique_id() players_info[my_id] = { "local_player_count": local_player_count, "player_names": _generate_player_names(local_player_count, my_id) } return true func join_game(address: String, port: int = DEFAULT_PORT) -> bool: var peer var error if network_mode == 1: # WebRTC # WebRTC for browser builds using Matchbox signaling # 'address' is the room ID for WebRTC room_id = address print("NetworkManager: Joining WebRTC game with room ID: ", room_id) print("NetworkManager: Matchbox URL: ", MATCHBOX_SERVER, "/", room_id) # Create Matchbox client var matchbox_script = load("res://scripts/matchbox_client.gd") matchbox_client = Node.new() matchbox_client.set_script(matchbox_script) add_child(matchbox_client) # Connect Matchbox signals matchbox_client.peer_joined.connect(_on_matchbox_peer_joined) matchbox_client.peer_left.connect(_on_matchbox_peer_left) matchbox_client.peer_connected.connect(_on_matchbox_peer_connected) matchbox_client.connection_succeeded.connect(_on_matchbox_connected) matchbox_client.connection_failed.connect(_on_matchbox_connection_failed) matchbox_client.webrtc_ready.connect(_on_matchbox_webrtc_ready) is_hosting = false # Connect to Matchbox room (pass is_hosting = false) if not matchbox_client.connect_to_room(room_id, false): push_error("Failed to connect to Matchbox room") return false return true elif network_mode == 2: # WebSocket # WebSocket for both native and web builds # 'address' is the room ID for WebSocket room_id = address print("NetworkManager: Joining WebSocket game with room ID: ", room_id) print("NetworkManager: WebSocket URL: ", WEBSOCKET_SERVER_URL, "/", room_id) peer = WebSocketMultiplayerPeer.new() var url = WEBSOCKET_SERVER_URL + "/" + room_id error = peer.create_client(url) if error != OK: push_error("Failed to create WebSocket client: " + str(error)) return false multiplayer.multiplayer_peer = peer is_hosting = false print("Attempting to connect to WebSocket server: ", url) return true else: # ENet (mode 0) # ENet for native builds peer = ENetMultiplayerPeer.new() error = peer.create_client(address, port) if error != OK: push_error("Failed to create ENet client: " + str(error)) return false multiplayer.multiplayer_peer = peer is_hosting = false print("Attempting to connect to ", address, ":", port) return true func disconnect_from_game(): if matchbox_client: matchbox_client.disconnect_from_room() matchbox_client.queue_free() matchbox_client = null if multiplayer.multiplayer_peer: multiplayer.multiplayer_peer.close() multiplayer.multiplayer_peer = null players_info.clear() is_hosting = false room_id = "" func set_local_player_count(count: int): local_player_count = max(1, min(count, 4)) # Limit to 1-4 local players func _generate_player_names(count: int, peer_id: int) -> Array: var names = [] for i in range(count): names.append("Player%d_%d" % [peer_id, i + 1]) return names # Called when a peer connects to the server func _on_peer_connected(id: int): print("Peer connected: ", id) # Called when a peer disconnects func _on_peer_disconnected(id: int): print("Peer disconnected: ", id) # Get player_info before erasing it var player_info = {} if players_info.has(id): player_info = players_info[id] players_info.erase(id) player_disconnected.emit(id, player_info) # Called on client when successfully connected to server func _on_connected_to_server(): print("Successfully connected to server") connection_succeeded.emit() # Send our player info to the server var my_id = multiplayer.get_unique_id() _register_player.rpc_id(1, my_id, local_player_count) # Called on client when connection fails func _on_connection_failed(): print("Connection failed") multiplayer.multiplayer_peer = null connection_failed.emit() # Called on client when disconnected from server func _on_server_disconnected(): print("Server disconnected") multiplayer.multiplayer_peer = null players_info.clear() # RPC to register a player with the server @rpc("any_peer", "reliable") func _register_player(peer_id: int, local_count: int): if not multiplayer.is_server(): return players_info[peer_id] = { "local_player_count": local_count, "player_names": _generate_player_names(local_count, peer_id) } print("NetworkManager: Registered player ", peer_id, " with ", local_count, " local players") print("NetworkManager: Total players_info: ", players_info) # Sync all player info to the new client (so they know about everyone) _sync_players.rpc_id(peer_id, players_info) # Notify all clients (including the new one) about the new player _notify_player_joined.rpc(peer_id, players_info[peer_id]) # Emit signal on server player_connected.emit(peer_id, players_info[peer_id]) # RPC to sync all player info to a newly connected client @rpc("authority", "reliable") func _sync_players(all_players_info: Dictionary): players_info = all_players_info print("NetworkManager: Client synced all player info: ", players_info) # Emit signals for all existing players (so game world can spawn them) for peer_id in players_info.keys(): # Emit for ALL players including ourselves (game world will handle it) print("NetworkManager: Emitting player_connected for peer ", peer_id) player_connected.emit(peer_id, players_info[peer_id]) # RPC to notify all clients when a player joins @rpc("authority", "reliable") func _notify_player_joined(peer_id: int, player_info: Dictionary): print("NetworkManager: Notified about player ", peer_id, " joining") if not players_info.has(peer_id): players_info[peer_id] = player_info player_connected.emit(peer_id, player_info) func get_all_player_ids() -> Array: return players_info.keys() func get_player_info(peer_id: int) -> Dictionary: return players_info.get(peer_id, {}) # Matchbox callback handlers func _on_matchbox_connected(): print("NetworkManager: Connected to Matchbox server") # WebRTC peer will be set up when Welcome message is received with our peer ID func _on_matchbox_webrtc_ready(): print("NetworkManager: WebRTC mesh is ready") # Register ourselves if we're not hosting (clients need to register) if not is_hosting and matchbox_client: var my_peer_id = matchbox_client.get_my_peer_id() if my_peer_id > 0 and not players_info.has(my_peer_id): players_info[my_peer_id] = { "local_player_count": local_player_count, "player_names": _generate_player_names(local_player_count, my_peer_id) } print("NetworkManager: Registered joining player with peer ID: ", my_peer_id) # Emit connection_succeeded signal so the game can start connection_succeeded.emit() func _on_matchbox_connection_failed(): print("NetworkManager: Failed to connect to Matchbox server") connection_failed.emit() func _on_matchbox_peer_joined(peer_id: int): print("NetworkManager: Matchbox peer joined: ", peer_id) # Peer connection will be created by Matchbox client # Once connected, we'll add it to WebRTC mesh func _on_matchbox_peer_left(peer_id: int): print("NetworkManager: Matchbox peer left: ", peer_id) # Player disconnect will be handled by multiplayer signals func _on_matchbox_peer_connected(peer_id: int): print("NetworkManager: Matchbox peer connected: ", peer_id) # Add peer to WebRTC mesh if matchbox_client: matchbox_client.add_peer_to_mesh(peer_id) # Register player info if not players_info.has(peer_id): players_info[peer_id] = { "local_player_count": 1, # Default, will be updated via RPC "player_names": _generate_player_names(1, peer_id) } # Emit player connected signal player_connected.emit(peer_id, players_info[peer_id]) func get_room_id() -> String: return room_id func get_local_ip() -> String: var addresses = IP.get_local_addresses() for addr in addresses: # Skip localhost and IPv6 if addr.begins_with("127.") or ":" in addr: continue return addr return "127.0.0.1" func _generate_room_id() -> String: # Generate a random 6-character room code var chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" # Avoid confusing chars like O/0, I/1 var code = "" for i in range(6): code += chars[randi() % chars.length()] return code func get_webrtc_peer() -> WebRTCMultiplayerPeer: if network_mode == 1 and multiplayer.multiplayer_peer is WebRTCMultiplayerPeer: return multiplayer.multiplayer_peer as WebRTCMultiplayerPeer return null # Create a WebRTC peer connection with STUN server configured func create_peer_connection() -> WebRTCPeerConnection: var peer_connection = WebRTCPeerConnection.new() # Configure STUN server for NAT traversal var config = { "iceServers": [ { "urls": [STUN_SERVER] } ] } var error = peer_connection.initialize(config) if error != OK: push_error("Failed to initialize WebRTC peer connection: " + str(error)) return null print("WebRTC peer connection initialized with STUN server: ", STUN_SERVER) return peer_connection # Add a peer connection for WebRTC mesh networking func add_webrtc_peer(peer_id: int) -> bool: var webrtc = get_webrtc_peer() if not webrtc: push_error("Not using WebRTC") return false var peer_connection = create_peer_connection() if not peer_connection: return false var error = webrtc.add_peer(peer_connection, peer_id) if error != OK: push_error("Failed to add WebRTC peer: " + str(error)) return false print("Added WebRTC peer: ", peer_id, " with STUN server configured") return true # Get the peer connection info for a specific peer func get_peer_connection(peer_id: int) -> Dictionary: var webrtc = get_webrtc_peer() if not webrtc: return {} return webrtc.get_peer(peer_id) # Get connection info to share with remote peer (for manual signaling) # This would typically be sent via a signaling server # Note: With matchbox, signaling is handled automatically func get_connection_offer(peer_id: int) -> Dictionary: var peer_conn = get_peer_connection(peer_id) if peer_conn.is_empty(): return {} # In a real implementation, you'd get the SDP offer from the peer connection # With matchbox, this is handled automatically via the signaling server print("To implement: Get SDP offer for peer ", peer_id) return {}