From 510ca12f40a8db44e6ca962089bc0c6363ba1e19 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Tue, 10 Feb 2026 15:39:32 -0500 Subject: Initial multiplayer stuff --- apworld/client/main.gd | 1 + apworld/client/manager.gd | 1 + apworld/client/multiplayerManager.gd | 264 +++++++++++++++++++++++++++++++++++ apworld/client/player.gd | 4 + 4 files changed, 270 insertions(+) create mode 100644 apworld/client/multiplayerManager.gd (limited to 'apworld') diff --git a/apworld/client/main.gd b/apworld/client/main.gd index 8cac24c..4c27c46 100644 --- a/apworld/client/main.gd +++ b/apworld/client/main.gd @@ -30,6 +30,7 @@ func _ready(): ap_instance.SCRIPT_keyboard = runtime.load_script("keyboard.gd") ap_instance.SCRIPT_locationListener = runtime.load_script("locationListener.gd") ap_instance.SCRIPT_minimap = runtime.load_script("minimap.gd") + ap_instance.SCRIPT_multiplayerManager = runtime.load_script("multiplayerManager.gd") ap_instance.SCRIPT_victoryListener = runtime.load_script("victoryListener.gd") ap_instance.SCRIPT_websocketserver = runtime.load_script("vendor/WebSocketServer.gd") diff --git a/apworld/client/manager.gd b/apworld/client/manager.gd index f10a0b7..5c6e942 100644 --- a/apworld/client/manager.gd +++ b/apworld/client/manager.gd @@ -4,6 +4,7 @@ var SCRIPT_client var SCRIPT_keyboard var SCRIPT_locationListener var SCRIPT_minimap +var SCRIPT_multiplayerManager var SCRIPT_victoryListener var SCRIPT_websocketserver diff --git a/apworld/client/multiplayerManager.gd b/apworld/client/multiplayerManager.gd new file mode 100644 index 0000000..dd4499f --- /dev/null +++ b/apworld/client/multiplayerManager.gd @@ -0,0 +1,264 @@ +extends Node + +var player_steam_id +var player_node +var active_lobby_id = 0 +var active_lobby_members = {} +var is_remote_signal = false +var next_message_id = 0 +var messages_needing_ack = {} + +const MAX_PLAYERS = 250 +const PROTOCOL_VERSION = 4000 +const RECIPIENT_BROADCAST_ALL = -1 + + +func _ready(): + # P2P solve messages should still be received while paused. + set_process_mode(Node.PROCESS_MODE_ALWAYS) + + player_steam_id = Steam.getSteamID() + player_node = get_node("/root/scene/player") + + Steam.lobby_match_list.connect(_on_lobby_match_list) + Steam.lobby_created.connect(_on_lobby_created) + Steam.lobby_joined.connect(_on_lobby_joined) + Steam.lobby_chat_update.connect(_on_lobby_chat_update) + Steam.lobby_data_update.connect(_on_lobby_data_update) + Steam.persona_state_change.connect(_on_persona_state_change) + Steam.p2p_session_request.connect(_on_p2p_session_request) + + _setup_recurring_task(0.1, _broadcast_player_location) + _setup_recurring_task(5.0, _resend_messages_needing_ack) + _setup_recurring_task(10.0, _request_lobby_list) + + _request_lobby_list() + + +func _process(_delta: float) -> void: + _read_p2p_packet() + + +func _exit_tree(): + if active_lobby_id != 0: + Steam.leaveLobby(active_lobby_id) + + +func _setup_recurring_task(wait_time, function): + var timer = Timer.new() + timer.set_wait_time(wait_time) + timer.set_one_shot(false) + timer.timeout.connect(function) + add_child(timer) + timer.start() + + +func _broadcast_player_location(): + if active_lobby_id == 0: + return + _send_p2p_packet( + { + "global_transform": player_node.global_transform, + }, + RECIPIENT_BROADCAST_ALL, + Steam.P2P_SEND_UNRELIABLE_NO_DELAY, + false + ) + + +func _resend_messages_needing_ack(): + for message_id in messages_needing_ack: + var message = messages_needing_ack[message_id] + if message["recipient_id"] in active_lobby_members: + _send_p2p_packet(message["data"], message["recipient_id"], message["mode"], true) + else: + messages_needing_ack.erase(message_id) + + +func _request_lobby_list(): + var ap = global.get_node("Archipelago") + if !ap._already_connected: + return + + Steam.addRequestLobbyListDistanceFilter(Steam.LOBBY_DISTANCE_FILTER_WORLDWIDE) + Steam.addRequestLobbyListNumericalFilter( + "protocol_version", PROTOCOL_VERSION, Steam.LOBBY_COMPARISON_EQUAL + ) + Steam.addRequestLobbyListStringFilter("map", global.map, Steam.LOBBY_COMPARISON_EQUAL) + Steam.addRequestLobbyListStringFilter("seed", ap.client._seed, Steam.LOBBY_COMPARISON_EQUAL) + Steam.requestLobbyList() + + +func _on_lobby_match_list(lobbies: Array) -> void: + if active_lobby_id != 0 && not active_lobby_id in lobbies: + # Not sure why this happens, but it seems to sometimes. + lobbies.append(active_lobby_id) + var best_lobby_id = 0 + var best_lobby_size = -1 + for lobby_id in lobbies: + var lobby_size = Steam.getNumLobbyMembers(lobby_id) + if ( + lobby_size > best_lobby_size + || (lobby_size == best_lobby_size && lobby_id < best_lobby_id) + ): + best_lobby_id = lobby_id + best_lobby_size = lobby_size + if best_lobby_id == 0: + Steam.createLobby(Steam.LOBBY_TYPE_INVISIBLE, MAX_PLAYERS) + elif best_lobby_id != active_lobby_id: + Steam.joinLobby(best_lobby_id) + elif best_lobby_size <= 1: + _request_lobby_list() + + +func _on_lobby_created(result: int, lobby_id: int) -> void: + if result != Steam.RESULT_OK: + return + var ap = global.get_node("Archipelago") + if !ap._already_connected: + return + var _ignore = Steam.setLobbyData(lobby_id, "map", global.map) + _ignore = Steam.setLobbyData(lobby_id, "protocol_version", str(PROTOCOL_VERSION)) + _ignore = Steam.setLobbyData(lobby_id, "seed", ap.client._seed) + _request_lobby_list() + + +func _on_lobby_joined(lobby_id: int, _permissions: int, _locked: bool, result: int) -> void: + if result != Steam.RESULT_OK: + return + if active_lobby_id != 0 && active_lobby_id != lobby_id: + Steam.leaveLobby(active_lobby_id) + active_lobby_id = lobby_id + _update_lobby_members() + # Broadcast out all our solves in case we have existing save data. + _send_hi(RECIPIENT_BROADCAST_ALL) + + +func _on_lobby_chat_update( + _lobby_id: int, member_id: int, _making_change_id: int, chat_state: int +) -> void: + _update_lobby_members() + # New user joined, so send them all our already solved panels. + if chat_state == Steam.CHAT_MEMBER_STATE_CHANGE_ENTERED: + _send_hi(member_id) + + +func _send_hi(recipient_id): + pass + #_send_p2p_packet( + # { + # "hi": true, + # "solves": solves, + # }, + # recipient_id, + # Steam.P2P_SEND_RELIABLE, + # true + #) + + +func _on_lobby_data_update(_lobby_id: int, _member_id: int, _key: int) -> void: + _update_lobby_members() + + +func _on_persona_state_change(_persona_id: int, _flag: int) -> void: + _update_lobby_members() + + +func _update_lobby_members(): + if active_lobby_id == 0: + return + var lobby_size: int = Steam.getNumLobbyMembers(active_lobby_id) + var prior_lobby_members = active_lobby_members + active_lobby_members = {} + for i in range(0, lobby_size): + var member_id: int = Steam.getLobbyMemberByIndex(active_lobby_id, i) + if member_id != player_steam_id: + var steam_name = Steam.getFriendPersonaName(member_id) + print(steam_name) + if member_id in prior_lobby_members: + active_lobby_members[member_id] = prior_lobby_members[member_id] + else: + active_lobby_members[member_id] = ( + load("res://objects/nodes/multiplayer/avatar.tscn").instantiate() + ) + active_lobby_members[member_id].set_steam_name(steam_name) + get_node("/root/scene").add_child.call_deferred(active_lobby_members[member_id]) + active_lobby_members[member_id].steam_id = member_id + active_lobby_members[member_id].steam_name = steam_name + active_lobby_members[member_id].material = "transparentWhite" + for member in prior_lobby_members.values(): + if not member.steam_id in active_lobby_members: + member.queue_free() + + +func _on_p2p_session_request(remote_id: int) -> void: + if remote_id in active_lobby_members: + var _ignore = Steam.acceptP2PSessionWithUser(remote_id) + + +func _read_p2p_packet() -> void: + var packet_size: int = Steam.getAvailableP2PPacketSize(0) + if packet_size > 0: + var packet: Dictionary = Steam.readP2PPacket(packet_size, 0) + var remote_id = packet["remote_steam_id"] + if remote_id in active_lobby_members: + var serialized: PackedByteArray = packet["data"] + var data: Dictionary = bytes_to_var(serialized) + if "hi" in data: + _receive_hi(remote_id) + if "global_transform" in data: + _receive_member_location(remote_id, data["global_transform"]) + #if "solves" in data: + # for solve in data["solves"]: + # _receive_solve(solve) + #if "unsolves" in data: + # for unsolve in data["unsolves"]: + # _receive_unsolve(unsolve) + if "message_id" in data: + _send_p2p_packet( + { + "ack": data["message_id"], + }, + remote_id, + Steam.P2P_SEND_RELIABLE_WITH_BUFFERING, + false + ) + if "ack" in data: + messages_needing_ack.erase(data["ack"]) + + +func _receive_hi(member_id: int) -> void: + var member = active_lobby_members[member_id] + member.said_hi = true + + +func _receive_member_location(member_id: int, global_transform) -> void: + var member = active_lobby_members[member_id] + member.global_move_to(global_transform) + if !member.visible: + member.show() + + +func _send_p2p_packet(data: Dictionary, recipient_id: int, mode: int, needs_ack: bool) -> void: + if recipient_id == RECIPIENT_BROADCAST_ALL: + for member_id in active_lobby_members.keys(): + _send_p2p_packet(data.duplicate(), member_id, mode, needs_ack) + return + + if needs_ack: + var message_id + if "message_id" in data: + message_id = data["message_id"] + else: + message_id = next_message_id + next_message_id += 1 + data["message_id"] = message_id + if not message_id in messages_needing_ack: + messages_needing_ack[message_id] = { + "data": data, + "recipient_id": recipient_id, + "mode": mode, + } + var serialized: PackedByteArray = [] + serialized.append_array(var_to_bytes(data)) + var _ignore = Steam.sendP2PPacket(recipient_id, serialized, mode) diff --git a/apworld/client/player.gd b/apworld/client/player.gd index dabc15d..b5aff18 100644 --- a/apworld/client/player.gd +++ b/apworld/client/player.gd @@ -196,6 +196,10 @@ func _ready(): minimap.visible = ap.show_minimap get_parent().add_child.call_deferred(minimap) + var multiplayer = ap.SCRIPT_multiplayer.new() + multiplayer.name = "Multiplayer" + get_parent().add_child.call_deferred(multiplayer) + if ap.music_mapping.has(global.map): var song_setter = get_node_or_null("/root/scene/songSetter") if song_setter: -- cgit 1.4.1