492 lines
16 KiB
GDScript
492 lines
16 KiB
GDScript
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 {}
|