## SPDX-License-Identifier: GPL-3.0-or-later ## Copyright (c) 2024 interstellardevelopment.org extends Node signal playerlist_changed() ## Connection Code # Connect the signals. func _ready() -> void: if multiplayer.connected_to_server.connect(_on_connected_ok): pass if multiplayer.connection_failed.connect(close_network): pass if multiplayer.server_disconnected.connect(close_network): pass if multiplayer.peer_disconnected.connect(_on_peer_disconnected): pass if playerlist_changed.connect(_on_playerlist_changed): pass func _on_playerlist_changed() -> void: pass # Start the network listener. func start_server() -> Error: var peer: ENetMultiplayerPeer = ENetMultiplayerPeer.new() if peer.create_server(Game.port, Game.max_clients) != OK: Log.warning("Couldn't create the server at port %d!" % Game.port) return FAILED multiplayer.multiplayer_peer = peer Log.info("Created the server at port %d." % Game.port) return OK # Connect to a server. func join_server() -> Error: var peer: ENetMultiplayerPeer = ENetMultiplayerPeer.new() if peer.create_client(Game.ip, Game.port) != OK: Log.warning("Couldn't connect to the server at %s:%d!" % [Game.ip, Game.port]) return FAILED multiplayer.multiplayer_peer = peer Log.info("Connected to the server at %s:%d." % [Game.ip, Game.port]) return OK # Close all network connections. func close_network() -> void: multiplayer.multiplayer_peer = null Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) Log.info("Closed all network connections.") ## Matchmaking Code var mutex: Mutex = Mutex.new() var rooms: Dictionary = {} var server_peers: Dictionary = {} var peers: Dictionary = {} var isManager: bool = false var managerID: int func join_room() -> void: if join_server() != OK: Log.warning("Failed to connect to sever.") func _on_connected_ok() -> void: if rpc_id(1, "send_roomname", Game.roomname) != OK: Log.warning("Failed to send roomname!") func _on_peer_disconnected(id: int) -> void: mutex.lock() if multiplayer.get_unique_id() == 1: if rooms.has(id): var roomname: String = rooms[id] if !rooms.erase(roomname): Log.warning("The room %s associated with %d doesn't exist in rooms, even though it should!" % [roomname, id]) mutex.unlock() return if !rooms.erase(id): Log.warning("The peer %d doesn't exist in rooms, even though it should!" % id) mutex.unlock() return for peer: int in server_peers[roomname]: if peer == id: continue if rpc_id(peer, "host_disconnected_message"): Log.warning("Peer %d couldn't be disconnected!" % peer) mutex.unlock() return if !server_peers.erase(roomname): Log.warning("Room %s couldn't be found, even though it should exist!" % roomname) mutex.unlock() return Log.info("Room %s terminated." % roomname) else: for room: String in server_peers: for peer: int in server_peers[room]: if peer == id: var peer_array: Array = server_peers[room] peer_array.erase(id) server_peers[room] = peer_array for found_peer: int in server_peers[room]: if rpc_id(found_peer, "peer_disconnected_message", id) != OK: Log.warning("Failed to send disconnect message to %d!" % found_peer) mutex.unlock() return mutex.unlock() # The following function sends the roomname selected by the client. @rpc("any_peer", "call_remote", "reliable") func send_roomname(roomname: String) -> void: mutex.lock() if multiplayer.get_unique_id() == 1: if !rooms.has(roomname) and !rooms.has(multiplayer.get_remote_sender_id()): rooms[roomname] = multiplayer.get_remote_sender_id() rooms[multiplayer.get_remote_sender_id()] = roomname server_peers[roomname] = [multiplayer.get_remote_sender_id()] Log.info("Room %s registered." % roomname) if rpc_id(multiplayer.get_remote_sender_id(), "server_response", 0, rooms[roomname]) != OK: Log.warning("Failed to send elevation reply to %d!") elif server_peers[roomname] is Array: # For some odd reason, Godot doesn't allow casting server_peers[roomname] to an Array, but this works? var peer_array: Array = server_peers[roomname] peer_array.append(multiplayer.get_remote_sender_id()) server_peers[roomname] = peer_array if rpc_id(multiplayer.get_remote_sender_id(), "server_response", 1, rooms[roomname]) != OK: Log.warning("Failed to send join reply to %d!") mutex.unlock() return Log.debug("Room %s joined by peer %d." % [roomname, multiplayer.get_remote_sender_id()]) else: Log.warning("Peer %d tried to send this client a request meant for the authority!" % multiplayer.get_remote_sender_id()) if rpc_id(multiplayer.get_remote_sender_id(), "server_response", 2, 0) != OK: Log.warning("Failed to send fail reply to %d!") mutex.unlock() # This is the message sent out if the host of the room has left. @rpc("authority", "call_remote", "reliable") func host_disconnected_message() -> void: Log.info("Host disconnected from the session.") managerID = 1 close_network() Game._ready() # This is the message sent out if a peer in the room has left. @rpc("authority", "call_remote", "reliable") func peer_disconnected_message(id: int) -> void: for peer: String in peers: if peers[peer] == id: if !peers.erase(peer): Log.warning("Peer %d never existed, but was tasked to remove it!" % id) return if emit_signal("playerlist_changed") != OK: Log.warning("Couldn't emit playerlist_changed signal.") return Log.info("Peer %d successfully removed." % id) # This is the initial server response, which tells the client what its role is. @rpc("authority", "call_remote", "reliable") func server_response(status: int, manager: int) -> void: match status: 0: # create new room host isManager = true if get_tree().change_scene_to_file("res://scenes/ui/lobby.tscn") != OK: Log.error("Couldn't change to the lobby scene! Closing application.", "Couldn't change to the lobby scene!") peers[Game.username] = manager managerID = manager 1: # create new room member managerID = manager if rpc_id(managerID, "request_playerlist", Game.username) != OK: Log.warning("Failed to send username to room host %d." % multiplayer.get_remote_sender_id()) 2: Log.warning("Sent a join request to a regular peer, not the server!") # This informs the server of the recently joined player. @rpc("any_peer", "call_remote", "reliable") func inform_server(roomname: String) -> void: mutex.lock() if multiplayer.get_unique_id() == 1: if multiplayer.get_remote_sender_id() == rooms[roomname]: var peer_array: Array = server_peers[roomname] peer_array.append(multiplayer.get_remote_sender_id()) server_peers[roomname] = peer_array else: Log.warning("Peer %d attempted to trick the server into adding it to the room!" % multiplayer.get_remote_sender_id()) else: Log.warning("Peer %d tried to send this client a request meant for the authority!" % multiplayer.get_remote_sender_id()) mutex.unlock() # Get the playerlist from the room host. @rpc("any_peer", "call_remote", "reliable") func request_playerlist(peername: String) -> void: if isManager: if !peers.has(peername) and !peers.has(multiplayer.get_remote_sender_id()): peers[peername] = multiplayer.get_remote_sender_id() if rpc_id(multiplayer.get_remote_sender_id(), "send_playerlist", 0, peers) != OK: Log.warning("Failed to send join status to %d." % multiplayer.get_remote_sender_id()) if !peers.erase(peername): Log.warning("Couldn't reverse adding %d to the playerlist." % peername) return if rpc_id(1, "inform_server", Game.roomname) != OK: Log.warning("Failed to send playerinfo to server.") return for peer: String in peers: var remote_id: int = peers[peer] if remote_id != multiplayer.get_unique_id(): if rpc_id(remote_id, "send_playerlist", 1, peers) != OK: Log.warning("Failed to send playerlist to %d." % multiplayer.get_remote_sender_id()) if emit_signal("playerlist_changed") != OK: Log.warning("Couldn't emit playerlist_changed signal.") else: if rpc_id(multiplayer.get_remote_sender_id(), "send_playerlist", 2, {}) != OK: Log.warning("Couldn't send failure response to peer %d!" % multiplayer.get_remote_sender_id()) Log.warning("Peer %d tried to register with name %s, which already exists!" % [multiplayer.get_remote_sender_id(), peername]) else: if rpc_id(multiplayer.get_remote_sender_id(), "send_playerlist", 3, {}) != OK: Log.warning("Couldn't send failure response to peer %d!" % multiplayer.get_remote_sender_id()) Log.warning("Peer %d tried to send this client a request meant for the room host!" % multiplayer.get_remote_sender_id()) # Receive the playerlist on the client. @rpc("any_peer", "call_remote", "reliable") func send_playerlist(status: int, newPeers: Dictionary) -> void: if multiplayer.get_remote_sender_id() == managerID: match status: 0: peers = newPeers Log.info("Room %s successfully joined." % Game.roomname) if get_tree().change_scene_to_file("res://scenes/ui/lobby.tscn") != OK: Log.error("Couldn't change to the lobby scene! Closing application.", "Couldn't change to the lobby scene!") 1: peers = newPeers Log.debug("Playerlist received.") if emit_signal("playerlist_changed") != OK: Log.warning("Couldn't emit playerlist_changed signal.") 2: Log.warning("Name %s is already taken in the room!" % Game.username) 3: Log.warning("Sent a playerlist request to a regular peer, not the host!") else: Log.warning("Non-manager peer tried to send a manager only request: send_playerlist") ## General Game RPCs # Does RPCs to all clients. func get_ids() -> Array: var ret: Array = [] for peer: String in peers: ret.append(peers[peer]) return ret func verify_id(id: int) -> bool: if id in get_ids(): return true return false func start_game_call() -> void: for peer: int in Networking.get_ids(): if rpc_id(peer, "start_game") != OK: Log.warning("Couldn't send RPC to %d!" % peer) # Starts the game. @rpc("any_peer", "call_local", "reliable") func start_game() -> void: if multiplayer.get_remote_sender_id() == managerID: if get_tree().change_scene_to_file("res://scenes/maps/testmap.tscn") != OK: Log.warning("Couldn't load the map!") else: Log.warning("Non-manager peer tried to send a manager only request: start_game") func player_sync_call(position: Vector3, rotation: Vector3) -> void: for peer: int in Networking.get_ids(): if peer != multiplayer.get_unique_id(): if rpc_id(peer, "player_sync", position, rotation) != OK: Log.warning("Couldn't send RPC to %d!" % peer) # Synchronizes the player state. @rpc("any_peer", "call_remote", "unreliable") func player_sync(position: Vector3, rotation: Vector3) -> void: if verify_id(multiplayer.get_remote_sender_id()): for peer: String in peers: if peers[peer] == multiplayer.get_remote_sender_id() and has_node("/root/"+Game.mapname+"/"+peer): var player: Player = get_node("/root/"+Game.mapname+"/"+peer) player.position = position player.rotation = rotation else: Log.warning("A non affiliated peer tried to send: player_sync") func npc_sync_call(position: Vector3, rotation: Vector3, node_name: String) -> void: for peer: int in Networking.get_ids(): if peer != multiplayer.get_unique_id(): if rpc_id(peer, "npc_sync", position, rotation, node_name) != OK: Log.warning("Couldn't send RPC to %d!" % peer) # Synchronizes the npc state. @rpc("any_peer", "call_remote", "unreliable") func npc_sync(position: Vector3, rotation: Vector3, node_name: String) -> void: if managerID == multiplayer.get_remote_sender_id(): var npc: NPC = get_node("/root/"+Game.mapname+"/"+node_name) npc.position = position npc.rotation = rotation else: Log.warning("Non-manager peer tried to send a manager only request: npc_sync") func object_sync_call(position: Vector3, rotation: Vector3, node_name: String) -> void: for peer: int in Networking.get_ids(): if peer != multiplayer.get_unique_id(): if rpc_id(peer, "object_sync", position, rotation, node_name) != OK: Log.warning("Couldn't send RPC to %d!" % peer) # Synchronizes the npc state. @rpc("any_peer", "call_remote", "unreliable") func object_sync(position: Vector3, rotation: Vector3, node_name: String) -> void: if managerID == multiplayer.get_remote_sender_id(): var carryable: Carryable = get_node("/root/"+Game.mapname+"/NPCCarryables/"+node_name) carryable.position = position carryable.rotation = rotation else: Log.warning("Non-manager peer tried to send a manager only request: npc_sync") func pick_up_sync_call(npc: NPC, carryable: Carryable) -> void: var npc_path: NodePath = npc.get_path() var carryable_path: NodePath = carryable.get_path() for peer: int in Networking.get_ids(): if rpc_id(peer, "pick_up_sync", npc_path, carryable_path) != OK: Log.warning("Couldn't send RPC to %d!" % peer) # Synchronizes the text above the npc. @rpc("any_peer", "call_local", "reliable") func npc_text_sync(text: String, npc: NodePath) -> void: if managerID == multiplayer.get_remote_sender_id(): (get_node(npc) as NPC).set_text(text) else: Log.warning("Non-manager peer tried to send a manager only request: npc_text_sync") func npc_text_sync_call(text: String, npc: NPC) -> void: var npc_path: NodePath = npc.get_path() for peer: int in Networking.get_ids(): if rpc_id(peer, "npc_text_sync", text, npc_path) != OK: Log.warning("Couldn't send RPC to %d!" % peer) # Synchronizes the object that is picked up by the npc. @rpc("any_peer", "call_local", "reliable") func pick_up_sync(npc: NodePath, carryable: NodePath) -> void: if managerID == multiplayer.get_remote_sender_id(): (get_node(npc) as NPC).pick_up(get_node(carryable) as Carryable) else: Log.warning("Non-manager peer tried to send a manager only request: pick_up_sync") func set_down_sync_call(npc: NPC, carryable: Carryable) -> void: var npc_path: NodePath = npc.get_path() var carryable_path: NodePath = carryable.get_path() for peer: int in Networking.get_ids(): if rpc_id(peer, "set_down_sync", npc_path, carryable_path) != OK: Log.warning("Couldn't send RPC to %d!" % peer) # Synchronizes the object that is picked up by the npc. @rpc("any_peer", "call_local", "reliable") func set_down_sync(npc: NodePath, carryable: NodePath) -> void: if managerID == multiplayer.get_remote_sender_id(): (get_node(npc) as NPC).set_down(get_node(carryable) as Carryable) else: Log.warning("Non-manager peer tried to send a manager only request: set_down_sync")