diff options
| author | Star Rauchenberger <fefferburbia@gmail.com> | 2026-02-10 15:39:32 -0500 |
|---|---|---|
| committer | Star Rauchenberger <fefferburbia@gmail.com> | 2026-02-10 15:39:32 -0500 |
| commit | 510ca12f40a8db44e6ca962089bc0c6363ba1e19 (patch) | |
| tree | 6dfa36051a936f2bafc4947acc4c8349c8f11572 | |
| parent | 8f5184fdb2545b46d930af17e99be76f59516906 (diff) | |
| download | lingo2-archipelago-510ca12f40a8db44e6ca962089bc0c6363ba1e19.tar.gz lingo2-archipelago-510ca12f40a8db44e6ca962089bc0c6363ba1e19.tar.bz2 lingo2-archipelago-510ca12f40a8db44e6ca962089bc0c6363ba1e19.zip | |
Initial multiplayer stuff multiplayer
| -rw-r--r-- | apworld/client/main.gd | 1 | ||||
| -rw-r--r-- | apworld/client/manager.gd | 1 | ||||
| -rw-r--r-- | apworld/client/multiplayerManager.gd | 264 | ||||
| -rw-r--r-- | apworld/client/player.gd | 4 |
4 files changed, 270 insertions, 0 deletions
| 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(): | |||
| 30 | ap_instance.SCRIPT_keyboard = runtime.load_script("keyboard.gd") | 30 | ap_instance.SCRIPT_keyboard = runtime.load_script("keyboard.gd") |
| 31 | ap_instance.SCRIPT_locationListener = runtime.load_script("locationListener.gd") | 31 | ap_instance.SCRIPT_locationListener = runtime.load_script("locationListener.gd") |
| 32 | ap_instance.SCRIPT_minimap = runtime.load_script("minimap.gd") | 32 | ap_instance.SCRIPT_minimap = runtime.load_script("minimap.gd") |
| 33 | ap_instance.SCRIPT_multiplayerManager = runtime.load_script("multiplayerManager.gd") | ||
| 33 | ap_instance.SCRIPT_victoryListener = runtime.load_script("victoryListener.gd") | 34 | ap_instance.SCRIPT_victoryListener = runtime.load_script("victoryListener.gd") |
| 34 | ap_instance.SCRIPT_websocketserver = runtime.load_script("vendor/WebSocketServer.gd") | 35 | ap_instance.SCRIPT_websocketserver = runtime.load_script("vendor/WebSocketServer.gd") |
| 35 | 36 | ||
| 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 | |||
| 4 | var SCRIPT_keyboard | 4 | var SCRIPT_keyboard |
| 5 | var SCRIPT_locationListener | 5 | var SCRIPT_locationListener |
| 6 | var SCRIPT_minimap | 6 | var SCRIPT_minimap |
| 7 | var SCRIPT_multiplayerManager | ||
| 7 | var SCRIPT_victoryListener | 8 | var SCRIPT_victoryListener |
| 8 | var SCRIPT_websocketserver | 9 | var SCRIPT_websocketserver |
| 9 | 10 | ||
| 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 @@ | |||
| 1 | extends Node | ||
| 2 | |||
| 3 | var player_steam_id | ||
| 4 | var player_node | ||
| 5 | var active_lobby_id = 0 | ||
| 6 | var active_lobby_members = {} | ||
| 7 | var is_remote_signal = false | ||
| 8 | var next_message_id = 0 | ||
| 9 | var messages_needing_ack = {} | ||
| 10 | |||
| 11 | const MAX_PLAYERS = 250 | ||
| 12 | const PROTOCOL_VERSION = 4000 | ||
| 13 | const RECIPIENT_BROADCAST_ALL = -1 | ||
| 14 | |||
| 15 | |||
| 16 | func _ready(): | ||
| 17 | # P2P solve messages should still be received while paused. | ||
| 18 | set_process_mode(Node.PROCESS_MODE_ALWAYS) | ||
| 19 | |||
| 20 | player_steam_id = Steam.getSteamID() | ||
| 21 | player_node = get_node("/root/scene/player") | ||
| 22 | |||
| 23 | Steam.lobby_match_list.connect(_on_lobby_match_list) | ||
| 24 | Steam.lobby_created.connect(_on_lobby_created) | ||
| 25 | Steam.lobby_joined.connect(_on_lobby_joined) | ||
| 26 | Steam.lobby_chat_update.connect(_on_lobby_chat_update) | ||
| 27 | Steam.lobby_data_update.connect(_on_lobby_data_update) | ||
| 28 | Steam.persona_state_change.connect(_on_persona_state_change) | ||
| 29 | Steam.p2p_session_request.connect(_on_p2p_session_request) | ||
| 30 | |||
| 31 | _setup_recurring_task(0.1, _broadcast_player_location) | ||
| 32 | _setup_recurring_task(5.0, _resend_messages_needing_ack) | ||
| 33 | _setup_recurring_task(10.0, _request_lobby_list) | ||
| 34 | |||
| 35 | _request_lobby_list() | ||
| 36 | |||
| 37 | |||
| 38 | func _process(_delta: float) -> void: | ||
| 39 | _read_p2p_packet() | ||
| 40 | |||
| 41 | |||
| 42 | func _exit_tree(): | ||
| 43 | if active_lobby_id != 0: | ||
| 44 | Steam.leaveLobby(active_lobby_id) | ||
| 45 | |||
| 46 | |||
| 47 | func _setup_recurring_task(wait_time, function): | ||
| 48 | var timer = Timer.new() | ||
| 49 | timer.set_wait_time(wait_time) | ||
| 50 | timer.set_one_shot(false) | ||
| 51 | timer.timeout.connect(function) | ||
| 52 | add_child(timer) | ||
| 53 | timer.start() | ||
| 54 | |||
| 55 | |||
| 56 | func _broadcast_player_location(): | ||
| 57 | if active_lobby_id == 0: | ||
| 58 | return | ||
| 59 | _send_p2p_packet( | ||
| 60 | { | ||
| 61 | "global_transform": player_node.global_transform, | ||
| 62 | }, | ||
| 63 | RECIPIENT_BROADCAST_ALL, | ||
| 64 | Steam.P2P_SEND_UNRELIABLE_NO_DELAY, | ||
| 65 | false | ||
| 66 | ) | ||
| 67 | |||
| 68 | |||
| 69 | func _resend_messages_needing_ack(): | ||
| 70 | for message_id in messages_needing_ack: | ||
| 71 | var message = messages_needing_ack[message_id] | ||
| 72 | if message["recipient_id"] in active_lobby_members: | ||
| 73 | _send_p2p_packet(message["data"], message["recipient_id"], message["mode"], true) | ||
| 74 | else: | ||
| 75 | messages_needing_ack.erase(message_id) | ||
| 76 | |||
| 77 | |||
| 78 | func _request_lobby_list(): | ||
| 79 | var ap = global.get_node("Archipelago") | ||
| 80 | if !ap._already_connected: | ||
| 81 | return | ||
| 82 | |||
| 83 | Steam.addRequestLobbyListDistanceFilter(Steam.LOBBY_DISTANCE_FILTER_WORLDWIDE) | ||
| 84 | Steam.addRequestLobbyListNumericalFilter( | ||
| 85 | "protocol_version", PROTOCOL_VERSION, Steam.LOBBY_COMPARISON_EQUAL | ||
| 86 | ) | ||
| 87 | Steam.addRequestLobbyListStringFilter("map", global.map, Steam.LOBBY_COMPARISON_EQUAL) | ||
| 88 | Steam.addRequestLobbyListStringFilter("seed", ap.client._seed, Steam.LOBBY_COMPARISON_EQUAL) | ||
| 89 | Steam.requestLobbyList() | ||
| 90 | |||
| 91 | |||
| 92 | func _on_lobby_match_list(lobbies: Array) -> void: | ||
| 93 | if active_lobby_id != 0 && not active_lobby_id in lobbies: | ||
| 94 | # Not sure why this happens, but it seems to sometimes. | ||
| 95 | lobbies.append(active_lobby_id) | ||
| 96 | var best_lobby_id = 0 | ||
| 97 | var best_lobby_size = -1 | ||
| 98 | for lobby_id in lobbies: | ||
| 99 | var lobby_size = Steam.getNumLobbyMembers(lobby_id) | ||
| 100 | if ( | ||
| 101 | lobby_size > best_lobby_size | ||
| 102 | || (lobby_size == best_lobby_size && lobby_id < best_lobby_id) | ||
| 103 | ): | ||
| 104 | best_lobby_id = lobby_id | ||
| 105 | best_lobby_size = lobby_size | ||
| 106 | if best_lobby_id == 0: | ||
| 107 | Steam.createLobby(Steam.LOBBY_TYPE_INVISIBLE, MAX_PLAYERS) | ||
| 108 | elif best_lobby_id != active_lobby_id: | ||
| 109 | Steam.joinLobby(best_lobby_id) | ||
| 110 | elif best_lobby_size <= 1: | ||
| 111 | _request_lobby_list() | ||
| 112 | |||
| 113 | |||
| 114 | func _on_lobby_created(result: int, lobby_id: int) -> void: | ||
| 115 | if result != Steam.RESULT_OK: | ||
| 116 | return | ||
| 117 | var ap = global.get_node("Archipelago") | ||
| 118 | if !ap._already_connected: | ||
| 119 | return | ||
| 120 | var _ignore = Steam.setLobbyData(lobby_id, "map", global.map) | ||
| 121 | _ignore = Steam.setLobbyData(lobby_id, "protocol_version", str(PROTOCOL_VERSION)) | ||
| 122 | _ignore = Steam.setLobbyData(lobby_id, "seed", ap.client._seed) | ||
| 123 | _request_lobby_list() | ||
| 124 | |||
| 125 | |||
| 126 | func _on_lobby_joined(lobby_id: int, _permissions: int, _locked: bool, result: int) -> void: | ||
| 127 | if result != Steam.RESULT_OK: | ||
| 128 | return | ||
| 129 | if active_lobby_id != 0 && active_lobby_id != lobby_id: | ||
| 130 | Steam.leaveLobby(active_lobby_id) | ||
| 131 | active_lobby_id = lobby_id | ||
| 132 | _update_lobby_members() | ||
| 133 | # Broadcast out all our solves in case we have existing save data. | ||
| 134 | _send_hi(RECIPIENT_BROADCAST_ALL) | ||
| 135 | |||
| 136 | |||
| 137 | func _on_lobby_chat_update( | ||
| 138 | _lobby_id: int, member_id: int, _making_change_id: int, chat_state: int | ||
| 139 | ) -> void: | ||
| 140 | _update_lobby_members() | ||
| 141 | # New user joined, so send them all our already solved panels. | ||
| 142 | if chat_state == Steam.CHAT_MEMBER_STATE_CHANGE_ENTERED: | ||
| 143 | _send_hi(member_id) | ||
| 144 | |||
| 145 | |||
| 146 | func _send_hi(recipient_id): | ||
| 147 | pass | ||
| 148 | #_send_p2p_packet( | ||
| 149 | # { | ||
| 150 | # "hi": true, | ||
| 151 | # "solves": solves, | ||
| 152 | # }, | ||
| 153 | # recipient_id, | ||
| 154 | # Steam.P2P_SEND_RELIABLE, | ||
| 155 | # true | ||
| 156 | #) | ||
| 157 | |||
| 158 | |||
| 159 | func _on_lobby_data_update(_lobby_id: int, _member_id: int, _key: int) -> void: | ||
| 160 | _update_lobby_members() | ||
| 161 | |||
| 162 | |||
| 163 | func _on_persona_state_change(_persona_id: int, _flag: int) -> void: | ||
| 164 | _update_lobby_members() | ||
| 165 | |||
| 166 | |||
| 167 | func _update_lobby_members(): | ||
| 168 | if active_lobby_id == 0: | ||
| 169 | return | ||
| 170 | var lobby_size: int = Steam.getNumLobbyMembers(active_lobby_id) | ||
| 171 | var prior_lobby_members = active_lobby_members | ||
| 172 | active_lobby_members = {} | ||
| 173 | for i in range(0, lobby_size): | ||
| 174 | var member_id: int = Steam.getLobbyMemberByIndex(active_lobby_id, i) | ||
| 175 | if member_id != player_steam_id: | ||
| 176 | var steam_name = Steam.getFriendPersonaName(member_id) | ||
| 177 | print(steam_name) | ||
| 178 | if member_id in prior_lobby_members: | ||
| 179 | active_lobby_members[member_id] = prior_lobby_members[member_id] | ||
| 180 | else: | ||
| 181 | active_lobby_members[member_id] = ( | ||
| 182 | load("res://objects/nodes/multiplayer/avatar.tscn").instantiate() | ||
| 183 | ) | ||
| 184 | active_lobby_members[member_id].set_steam_name(steam_name) | ||
| 185 | get_node("/root/scene").add_child.call_deferred(active_lobby_members[member_id]) | ||
| 186 | active_lobby_members[member_id].steam_id = member_id | ||
| 187 | active_lobby_members[member_id].steam_name = steam_name | ||
| 188 | active_lobby_members[member_id].material = "transparentWhite" | ||
| 189 | for member in prior_lobby_members.values(): | ||
| 190 | if not member.steam_id in active_lobby_members: | ||
| 191 | member.queue_free() | ||
| 192 | |||
| 193 | |||
| 194 | func _on_p2p_session_request(remote_id: int) -> void: | ||
| 195 | if remote_id in active_lobby_members: | ||
| 196 | var _ignore = Steam.acceptP2PSessionWithUser(remote_id) | ||
| 197 | |||
| 198 | |||
| 199 | func _read_p2p_packet() -> void: | ||
| 200 | var packet_size: int = Steam.getAvailableP2PPacketSize(0) | ||
| 201 | if packet_size > 0: | ||
| 202 | var packet: Dictionary = Steam.readP2PPacket(packet_size, 0) | ||
| 203 | var remote_id = packet["remote_steam_id"] | ||
| 204 | if remote_id in active_lobby_members: | ||
| 205 | var serialized: PackedByteArray = packet["data"] | ||
| 206 | var data: Dictionary = bytes_to_var(serialized) | ||
| 207 | if "hi" in data: | ||
| 208 | _receive_hi(remote_id) | ||
| 209 | if "global_transform" in data: | ||
| 210 | _receive_member_location(remote_id, data["global_transform"]) | ||
| 211 | #if "solves" in data: | ||
| 212 | # for solve in data["solves"]: | ||
| 213 | # _receive_solve(solve) | ||
| 214 | #if "unsolves" in data: | ||
| 215 | # for unsolve in data["unsolves"]: | ||
| 216 | # _receive_unsolve(unsolve) | ||
| 217 | if "message_id" in data: | ||
| 218 | _send_p2p_packet( | ||
| 219 | { | ||
| 220 | "ack": data["message_id"], | ||
| 221 | }, | ||
| 222 | remote_id, | ||
| 223 | Steam.P2P_SEND_RELIABLE_WITH_BUFFERING, | ||
| 224 | false | ||
| 225 | ) | ||
| 226 | if "ack" in data: | ||
| 227 | messages_needing_ack.erase(data["ack"]) | ||
| 228 | |||
| 229 | |||
| 230 | func _receive_hi(member_id: int) -> void: | ||
| 231 | var member = active_lobby_members[member_id] | ||
| 232 | member.said_hi = true | ||
| 233 | |||
| 234 | |||
| 235 | func _receive_member_location(member_id: int, global_transform) -> void: | ||
| 236 | var member = active_lobby_members[member_id] | ||
| 237 | member.global_move_to(global_transform) | ||
| 238 | if !member.visible: | ||
| 239 | member.show() | ||
| 240 | |||
| 241 | |||
| 242 | func _send_p2p_packet(data: Dictionary, recipient_id: int, mode: int, needs_ack: bool) -> void: | ||
| 243 | if recipient_id == RECIPIENT_BROADCAST_ALL: | ||
| 244 | for member_id in active_lobby_members.keys(): | ||
| 245 | _send_p2p_packet(data.duplicate(), member_id, mode, needs_ack) | ||
| 246 | return | ||
| 247 | |||
| 248 | if needs_ack: | ||
| 249 | var message_id | ||
| 250 | if "message_id" in data: | ||
| 251 | message_id = data["message_id"] | ||
| 252 | else: | ||
| 253 | message_id = next_message_id | ||
| 254 | next_message_id += 1 | ||
| 255 | data["message_id"] = message_id | ||
| 256 | if not message_id in messages_needing_ack: | ||
| 257 | messages_needing_ack[message_id] = { | ||
| 258 | "data": data, | ||
| 259 | "recipient_id": recipient_id, | ||
| 260 | "mode": mode, | ||
| 261 | } | ||
| 262 | var serialized: PackedByteArray = [] | ||
| 263 | serialized.append_array(var_to_bytes(data)) | ||
| 264 | 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(): | |||
| 196 | minimap.visible = ap.show_minimap | 196 | minimap.visible = ap.show_minimap |
| 197 | get_parent().add_child.call_deferred(minimap) | 197 | get_parent().add_child.call_deferred(minimap) |
| 198 | 198 | ||
| 199 | var multiplayer = ap.SCRIPT_multiplayer.new() | ||
| 200 | multiplayer.name = "Multiplayer" | ||
| 201 | get_parent().add_child.call_deferred(multiplayer) | ||
| 202 | |||
| 199 | if ap.music_mapping.has(global.map): | 203 | if ap.music_mapping.has(global.map): |
| 200 | var song_setter = get_node_or_null("/root/scene/songSetter") | 204 | var song_setter = get_node_or_null("/root/scene/songSetter") |
| 201 | if song_setter: | 205 | if song_setter: |
