From 05827d25733698a26cc0f305966e6a8a03be4684 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Thu, 25 Sep 2025 18:26:53 -0400 Subject: Game talks through CommonClient now --- apworld/client/client.gd | 381 +++++++------------------------ apworld/client/main.gd | 16 +- apworld/client/manager.gd | 201 +++++++--------- apworld/client/panel.gd | 4 +- apworld/client/player.gd | 20 +- apworld/client/textclient.gd | 2 +- apworld/client/vendor/LICENSE | 28 +-- apworld/client/vendor/WebSocketServer.gd | 173 ++++++++++++++ apworld/client/vendor/uuid.gd | 195 ---------------- 9 files changed, 369 insertions(+), 651 deletions(-) create mode 100644 apworld/client/vendor/WebSocketServer.gd delete mode 100644 apworld/client/vendor/uuid.gd (limited to 'apworld/client') diff --git a/apworld/client/client.gd b/apworld/client/client.gd index 843647d..67edf29 100644 --- a/apworld/client/client.gd +++ b/apworld/client/client.gd @@ -2,20 +2,10 @@ extends Node const ap_version = {"major": 0, "minor": 6, "build": 3, "class": "Version"} -var SCRIPT_uuid +var SCRIPT_websocketserver -var _ws = WebSocketPeer.new() +var _server var _should_process = false -var _initiated_disconnect = false -var _try_wss = false -var _has_connected = false - -var _datapackages = {} -var _pending_packages = [] -var _item_id_to_name = {} # All games -var _location_id_to_name = {} # All games -var _item_name_to_id = {} # Lingo 2 only -var _location_name_to_id = {} # Lingo 2 only var _remote_version = {"major": 0, "minor": 0, "build": 0} var _gen_version = {"major": 0, "minor": 0, "build": 0} @@ -24,13 +14,9 @@ var ap_server = "" var ap_user = "" var ap_pass = "" -var _authenticated = false var _seed = "" var _team = 0 var _slot = 0 -var _players = [] -var _player_name_by_slot = {} -var _game_by_player = {} var _checked_locations = [] var _received_indexes = [] var _received_items = {} @@ -39,317 +25,135 @@ var _slot_data = {} signal could_not_connect signal connect_status signal client_connected(slot_data) -signal item_received(item_id, index, player, flags, amount) -signal message_received(message) -signal location_scout_received(item_id, location_id, player, flags) +signal item_received(item, amount) +signal location_scout_received(location_id, item_name, player_name, flags, for_self) +signal text_message_received(message) +signal item_sent_notification(message) +signal hint_received(message) func _init(): set_process_mode(Node.PROCESS_MODE_ALWAYS) - _ws.inbound_buffer_size = 8388608 - global._print("Instantiated APClient") - # Read AP datapackages from file, if there are any - if FileAccess.file_exists("user://ap_datapackages"): - var file = FileAccess.open("user://ap_datapackages", FileAccess.READ) - var data = file.get_var(true) - file.close() - - if typeof(data) != TYPE_DICTIONARY: - global._print("AP datapackages file is corrupted") - data = {} - - _datapackages = data - - processDatapackages() - func _ready(): - pass - #_ws.connect("connection_closed", _closed) - #_ws.connect("connection_failed", _closed) - #_ws.connect("server_disconnected", _closed) - #_ws.connect("connection_error", _errored) - #_ws.connect("connection_established", _connected) + _server = SCRIPT_websocketserver.new() + _server.client_connected.connect(_on_web_socket_server_client_connected) + _server.client_disconnected.connect(_on_web_socket_server_client_disconnected) + _server.message_received.connect(_on_web_socket_server_message_received) + add_child(_server) + _server.listen(43182) func _reset_state(): _should_process = false - _authenticated = false - _try_wss = false - _has_connected = false _received_items = {} _received_indexes = [] -func _errored(): - if _try_wss: - global._print("Could not connect to AP with ws://, now trying wss://") - connectToServer(ap_server, ap_user, ap_pass) - else: - global._print("AP connection failed") - _reset_state() +func disconnect_from_ap(): + sendMessage([{"cmd": "Disconnect"}]) - emit_signal( - "could_not_connect", - "Could not connect to Archipelago. Check that your server and port are correct. See the error log for more information." - ) +func _on_web_socket_server_client_connected(peer_id: int) -> void: + var peer: WebSocketPeer = _server.peers[peer_id] + print("Remote client connected: %d. Protocol: %s" % [peer_id, peer.get_selected_protocol()]) + _server.send(-peer_id, "[%d] connected" % peer_id) -func _closed(_was_clean = true): - global._print("Connection closed") - _reset_state() - if not _initiated_disconnect: - emit_signal("could_not_connect", "Disconnected from Archipelago") +func _on_web_socket_server_client_disconnected(peer_id: int) -> void: + var peer: WebSocketPeer = _server.peers[peer_id] + print( + ( + "Remote client disconnected: %d. Code: %d, Reason: %s" + % [peer_id, peer.get_close_code(), peer.get_close_reason()] + ) + ) + _server.send(-peer_id, "[%d] disconnected" % peer_id) - _initiated_disconnect = false +func _on_web_socket_server_message_received(_peer_id: int, packet: String) -> void: + global._print("Got data from server: " + packet) + var json = JSON.new() + var jserror = json.parse(packet) + if jserror != OK: + global._print("Error parsing packet from AP: " + jserror.error_string) + return -func _connected(_proto = ""): - global._print("Connected!") - _try_wss = false + for message in json.data: + var cmd = message["cmd"] + global._print("Received command: " + cmd) + if cmd == "Connected": + _seed = message["seed_name"] + _remote_version = message["version"] + _gen_version = message["generator_version"] + _team = message["team"] + _slot = message["slot"] + _checked_locations = message["checked_locations"] + _slot_data = message["slot_data"] -func disconnect_from_ap(): - _initiated_disconnect = true - _ws.close() - - -func _process(_delta): - if _should_process: - _ws.poll() - - var state = _ws.get_ready_state() - if state == WebSocketPeer.STATE_OPEN: - if not _has_connected: - _has_connected = true - - _connected() - - while _ws.get_available_packet_count(): - var packet = _ws.get_packet() - global._print("Got data from server: " + packet.get_string_from_utf8()) - var json = JSON.new() - var jserror = json.parse(packet.get_string_from_utf8()) - if jserror != OK: - global._print("Error parsing packet from AP: " + jserror.error_string) - return - - for message in json.data: - var cmd = message["cmd"] - global._print("Received command: " + cmd) - - if cmd == "RoomInfo": - _seed = message["seed_name"] - _remote_version = message["version"] - _gen_version = message["generator_version"] - - var needed_games = [] - for game in message["datapackage_checksums"].keys(): - if ( - !_datapackages.has(game) - or ( - _datapackages[game]["checksum"] - != message["datapackage_checksums"][game] - ) - ): - needed_games.append(game) - - if !needed_games.is_empty(): - _pending_packages = needed_games - var cur_needed = _pending_packages.pop_front() - requestDatapackages([cur_needed]) - else: - connectToRoom() - - elif cmd == "DataPackage": - for game in message["data"]["games"].keys(): - _datapackages[game] = message["data"]["games"][game] - saveDatapackages() - - if !_pending_packages.is_empty(): - var cur_needed = _pending_packages.pop_front() - requestDatapackages([cur_needed]) - else: - processDatapackages() - connectToRoom() - - elif cmd == "Connected": - _authenticated = true - _team = message["team"] - _slot = message["slot"] - _players = message["players"] - _checked_locations = message["checked_locations"] - _slot_data = message["slot_data"] - - for player in _players: - _player_name_by_slot[player["slot"]] = player["alias"] - _game_by_player[player["slot"]] = message["slot_info"][str( - player["slot"] - )]["game"] - - emit_signal("client_connected", _slot_data) - - elif cmd == "ConnectionRefused": - var error_message = "" - for error in message["errors"]: - var submsg = "" - if error == "InvalidSlot": - submsg = "Invalid player name." - elif error == "InvalidGame": - submsg = "The specified player is not playing Lingo." - elif error == "IncompatibleVersion": - submsg = ( - "The Archipelago server is not the correct version for this client. Expected v%d.%d.%d. Found v%d.%d.%d." - % [ - ap_version["major"], - ap_version["minor"], - ap_version["build"], - _remote_version["major"], - _remote_version["minor"], - _remote_version["build"] - ] - ) - elif error == "InvalidPassword": - submsg = "Incorrect password." - elif error == "InvalidItemsHandling": - submsg = "Invalid item handling flag. This is a bug with the client." - - if submsg != "": - if error_message != "": - error_message += " " - error_message += submsg - - if error_message == "": - error_message = "Unknown error." - - _initiated_disconnect = true - _ws.close() - - emit_signal("could_not_connect", error_message) - global._print("Connection to AP refused") - global._print(message) - - elif cmd == "ReceivedItems": - var i = 0 - for item in message["items"]: - var index = int(message["index"] + i) - i += 1 - - if _received_indexes.has(index): - # Do not re-process items. - continue - - _received_indexes.append(index) - - var item_id = int(item["item"]) - _received_items[item_id] = _received_items.get(item_id, 0) + 1 - - emit_signal( - "item_received", - item_id, - index, - int(item["player"]), - int(item["flags"]), - _received_items[item_id] - ) - - elif cmd == "PrintJSON": - emit_signal("message_received", message) - - elif cmd == "LocationInfo": - for loc in message["locations"]: - emit_signal( - "location_scout_received", - int(loc["item"]), - int(loc["location"]), - int(loc["player"]), - int(loc["flags"]) - ) - - elif state == WebSocketPeer.STATE_CLOSED: - if _has_connected: - _closed() - else: - _errored() - - -func saveDatapackages(): - # Save the AP datapackages to disk. - var file = FileAccess.open("user://ap_datapackages", FileAccess.WRITE) - file.store_var(_datapackages, true) - file.close() + client_connected.emit(_slot_data) + elif cmd == "ConnectionRefused": + could_not_connect.emit(message["text"]) + global._print("Connection to AP refused") -func connectToServer(server, un, pw): - ap_server = server - ap_user = un - ap_pass = pw + elif cmd == "ItemReceived": + for item in message["items"]: + var index = int(item["index"]) + if _received_indexes.has(index): + # Do not re-process items. + continue - _initiated_disconnect = false - - var url = "" - if ap_server.begins_with("ws://") or ap_server.begins_with("wss://"): - url = ap_server - _try_wss = false - elif _try_wss: - url = "wss://" + ap_server - _try_wss = false - else: - url = "ws://" + ap_server - _try_wss = true - - var err = _ws.connect_to_url(url) - if err != OK: - emit_signal( - "could_not_connect", - ( - "Could not connect to Archipelago. Check that your server and port are correct. See the error log for more information. Error code: %d." - % err - ) - ) - global._print("Could not connect to AP: %d" % err) - return - _should_process = true + _received_indexes.append(index) - emit_signal("connect_status", "Connecting...") + var item_id = int(item["id"]) + _received_items[item_id] = _received_items.get(item_id, 0) + 1 + item_received.emit(item, _received_items[item_id]) -func sendMessage(msg): - var payload = JSON.stringify(msg) - _ws.send_text(payload) + elif cmd == "TextMessage": + text_message_received.emit(message["data"]) + + elif cmd == "ItemSentNotif": + item_sent_notification.emit(message) + elif cmd == "HintReceived": + hint_received.emit(message) -func requestDatapackages(games): - emit_signal("connect_status", "Downloading %s data package..." % games[0]) + elif cmd == "LocationInfo": + for loc in message["locations"]: + location_scout_received.emit( + int(loc["id"]), + loc["item"], + loc["player"], + int(loc["flags"]), + int(loc["for_self"]) + ) - sendMessage([{"cmd": "GetDataPackage", "games": games}]) +func connectToServer(server, un, pw): + sendMessage([{"cmd": "Connect", "server": server, "player": un, "password": pw}]) + + ap_server = server + ap_user = un + ap_pass = pw -func processDatapackages(): - _item_id_to_name = {} - _location_id_to_name = {} - for game in _datapackages.keys(): - var package = _datapackages[game] + _should_process = true - _item_id_to_name[game] = {} - for item_name in package["item_name_to_id"].keys(): - _item_id_to_name[game][int(package["item_name_to_id"][item_name])] = item_name + connect_status.emit("Connecting...") - _location_id_to_name[game] = {} - for location_name in package["location_name_to_id"].keys(): - _location_id_to_name[game][int(package["location_name_to_id"][location_name])] = location_name - if _datapackages.has("Lingo 2"): - _item_name_to_id = _datapackages["Lingo 2"]["item_name_to_id"] - _location_name_to_id = _datapackages["Lingo 2"]["location_name_to_id"] +func sendMessage(msg): + var payload = JSON.stringify(msg) + _server.send(0, payload) func connectToRoom(): - emit_signal("connect_status", "Authenticating...") + connect_status.emit("Authenticating...") sendMessage( [ @@ -358,20 +162,11 @@ func connectToRoom(): "password": ap_pass, "game": "Lingo 2", "name": ap_user, - "uuid": SCRIPT_uuid.v4(), - "version": ap_version, - "items_handling": 0b111, # always receive our items - "tags": [], - "slot_data": true } ] ) -func sendConnectUpdate(tags): - sendMessage([{"cmd": "ConnectUpdate", "tags": tags}]) - - func requestSync(): sendMessage([{"cmd": "Sync"}]) diff --git a/apworld/client/main.gd b/apworld/client/main.gd index cff92bc..9d66358 100644 --- a/apworld/client/main.gd +++ b/apworld/client/main.gd @@ -30,8 +30,8 @@ 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_uuid = runtime.load_script("vendor/uuid.gd") ap_instance.SCRIPT_victoryListener = runtime.load_script("victoryListener.gd") + ap_instance.SCRIPT_websocketserver = runtime.load_script("vendor/WebSocketServer.gd") global.add_child(ap_instance) @@ -79,9 +79,9 @@ func _ready(): var ap = global.get_node("Archipelago") var gamedata = global.get_node("Gamedata") - ap.connect("ap_connected", connectionSuccessful) - ap.connect("could_not_connect", connectionUnsuccessful) - ap.connect("connect_status", connectionStatus) + ap.ap_connected.connect(connectionSuccessful) + ap.could_not_connect.connect(connectionUnsuccessful) + ap.connect_status.connect(connectionStatus) # Populate textboxes with AP settings. get_node("../Panel/server_box").text = ap.ap_server @@ -99,7 +99,7 @@ func _ready(): history_box.get_popup().add_item("%s (%s)" % [details[1], details[0]], i) i += 1 - history_box.get_popup().connect("id_pressed", historySelected) + history_box.get_popup().id_pressed.connect(historySelected) # Show client version. get_node("../Panel/title").text = ( @@ -112,14 +112,14 @@ func _ready(): get_node("../Panel/password_box").add_theme_font_size_override("font_size", 36) # Set up version mismatch dialog. - get_node("../Panel/VersionMismatch").connect("confirmed", startGame) + get_node("../Panel/VersionMismatch").confirmed.connect(startGame) get_node("../Panel/VersionMismatch").get_cancel_button().pressed.connect( versionMismatchDeclined ) # Set up buttons. - get_node("../Panel/connect_button").connect("pressed", _connect_pressed) - get_node("../Panel/quit_button").connect("pressed", _back_pressed) + get_node("../Panel/connect_button").pressed.connect(_connect_pressed) + get_node("../Panel/quit_button").pressed.connect(_back_pressed) func _connect_pressed(): diff --git a/apworld/client/manager.gd b/apworld/client/manager.gd index b170c77..46c5456 100644 --- a/apworld/client/manager.gd +++ b/apworld/client/manager.gd @@ -6,8 +6,8 @@ var SCRIPT_client var SCRIPT_keyboard var SCRIPT_locationListener var SCRIPT_minimap -var SCRIPT_uuid var SCRIPT_victoryListener +var SCRIPT_websocketserver var ap_server = "" var ap_user = "" @@ -92,14 +92,17 @@ func _init(): func _ready(): client = SCRIPT_client.new() - client.SCRIPT_uuid = SCRIPT_uuid + client.SCRIPT_websocketserver = SCRIPT_websocketserver - client.connect("item_received", _process_item) - client.connect("message_received", _process_message) - client.connect("location_scout_received", _process_location_scout) - client.connect("could_not_connect", _client_could_not_connect) - client.connect("connect_status", _client_connect_status) - client.connect("client_connected", _client_connected) + client.item_received.connect(_process_item) + client.location_scout_received.connect(_process_location_scout) + client.text_message_received.connect(_process_text_message) + client.item_sent_notification.connect(_process_item_sent_notification) + client.hint_received.connect(_process_hint_received) + + client.could_not_connect.connect(_client_could_not_connect) + client.connect_status.connect(_client_connect_status) + client.client_connected.connect(_client_connected) add_child(client) @@ -163,20 +166,17 @@ func get_item_id_for_door(door_id): return _item_locks.get(door_id, null) -func _process_item(item, index, from, flags, amount): - var item_name = "Unknown" - if client._item_id_to_name["Lingo 2"].has(item): - item_name = client._item_id_to_name["Lingo 2"][item] - +func _process_item(item, amount): var gamedata = global.get_node("Gamedata") + var item_id = int(item["id"]) var prog_id = null - if _inverse_item_locks.has(item): - for lock in _inverse_item_locks.get(item): + if _inverse_item_locks.has(item_id): + for lock in _inverse_item_locks.get(item_id): if lock[1] != amount: continue - if gamedata.progressive_id_by_ap_id.has(item): + if gamedata.progressive_id_by_ap_id.has(item_id): prog_id = lock[0] if gamedata.get_door_map_name(lock[0]) != global.map: @@ -190,38 +190,35 @@ func _process_item(item, index, from, flags, amount): if rnode != null: rnode.handleTriggered() - var letter_id = gamedata.letter_id_by_ap_id.get(item, null) + var letter_id = gamedata.letter_id_by_ap_id.get(item_id, null) if letter_id != null: var letter = gamedata.objects.get_letters()[letter_id] if not letter.has_level2() or not letter.get_level2(): _process_key_item(letter.get_key(), amount) - if gamedata.symbol_item_ids.has(item): + if gamedata.symbol_item_ids.has(item_id): var player = get_tree().get_root().get_node_or_null("scene/player") if player != null: - player.emit_signal("evaluate_solvability") + player.evaluate_solvability.emit() # Show a message about the item if it's new. - if index != null and index > _last_new_item: - _last_new_item = index + if int(item["index"]) > _last_new_item: + _last_new_item = int(item["index"]) saveLocaldata() - var player_name = "Unknown" - if client._player_name_by_slot.has(float(from)): - player_name = client._player_name_by_slot[float(from)] - - var full_item_name = item_name + var full_item_name = item["text"] if prog_id != null: var door = gamedata.objects.get_doors()[prog_id] - full_item_name = "%s (%s)" % [item_name, door.get_name()] + full_item_name = "%s (%s)" % [full_item_name, door.get_name()] var message - if from == client._slot: - message = "Found %s" % wrapInItemColorTags(full_item_name, flags) - else: + if "sender" in item: message = ( - "Received %s from %s" % [wrapInItemColorTags(full_item_name, flags), player_name] + "Received %s from %s" + % [wrapInItemColorTags(full_item_name, item["flags"]), item["sender"]] ) + else: + message = "Found %s" % wrapInItemColorTags(full_item_name, item["flags"]) if gamedata.anti_trap_ids.has(item): keyboard.block_letter(gamedata.anti_trap_ids[item]) @@ -231,112 +228,68 @@ func _process_item(item, index, from, flags, amount): global.get_node("Messages").showMessage(message) -func _process_message(message): - parse_printjson_for_textclient(message) - - if ( - !message.has("receiving") - or !message.has("item") - or message["item"]["player"] != client._slot - ): - return - - var item_name = "Unknown" - var item_player_game = client._game_by_player[message["receiving"]] - if client._item_id_to_name[item_player_game].has(int(message["item"]["item"])): - item_name = client._item_id_to_name[item_player_game][int(message["item"]["item"])] - - var location_name = "Unknown" - var location_player_game = client._game_by_player[message["item"]["player"]] - if client._location_id_to_name[location_player_game].has(int(message["item"]["location"])): - location_name = (client._location_id_to_name[location_player_game][int( - message["item"]["location"] - )]) - - var player_name = "Unknown" - if client._player_name_by_slot.has(message["receiving"]): - player_name = client._player_name_by_slot[message["receiving"]] - - var item_color = colorForItemType(message["item"]["flags"]) - - if message["type"] == "Hint": - var is_for = "" - if message["receiving"] != client._slot: - is_for = " for %s" % player_name - if !message.has("found") || !message["found"]: - global.get_node("Messages").showMessage( - ( - "Hint: %s%s is on %s" - % [ - wrapInItemColorTags(item_name, message["item"]["flags"]), - is_for, - location_name - ] - ) - ) - else: - if message["receiving"] != client._slot: - var sentMsg = ( - "Sent %s to %s" - % [wrapInItemColorTags(item_name, message["item"]["flags"]), player_name] - ) - #if _hinted_locations.has(message["item"]["location"]): - # sentMsg += " ([color=#fafad2]Hinted![/color])" - global.get_node("Messages").showMessage(sentMsg) - - -func parse_printjson_for_textclient(message): +func _process_item_sent_notification(message): + var sentMsg = ( + "Sent %s to %s" + % [ + wrapInItemColorTags(message["item_name"], message["item_flags"]), + message["receiver_name"] + ] + ) + #if _hinted_locations.has(message["item"]["location"]): + # sentMsg += " ([color=#fafad2]Hinted![/color])" + global.get_node("Messages").showMessage(sentMsg) + + +func _process_hint_received(message): + var is_for = "" + if message["self"] == 0: + is_for = " for %s" % message["receiver_name"] + + global.get_node("Messages").showMessage( + ( + "Hint: %s%s is on %s" + % [ + wrapInItemColorTags(message["item_name"], message["item_flags"]), + is_for, + message["location_name"] + ] + ) + ) + + +func _process_text_message(message): var parts = [] - for message_part in message["data"]: - if !message_part.has("type") and message_part.has("text"): + for message_part in message: + if message_part["type"] == "text": parts.append(message_part["text"]) - elif message_part["type"] == "player_id": - if int(message_part["text"]) == client._slot: - parts.append( - "[color=#ee00ee]%s[/color]" % client._player_name_by_slot[client._slot] - ) + elif message_part["type"] == "player": + if message_part["self"] == 1: + parts.append("[color=#ee00ee]%s[/color]" % message_part["text"]) else: - var from = float(message_part["text"]) - parts.append("[color=#fafad2]%s[/color]" % client._player_name_by_slot[from]) - elif message_part["type"] == "item_id": - var item_name = "Unknown" - var item_player_game = client._game_by_player[message_part["player"]] - if client._item_id_to_name[item_player_game].has(int(message_part["text"])): - item_name = client._item_id_to_name[item_player_game][int(message_part["text"])] - - parts.append(wrapInItemColorTags(item_name, message_part["flags"])) - elif message_part["type"] == "location_id": - var location_name = "Unknown" - var location_player_game = client._game_by_player[message_part["player"]] - if client._location_id_to_name[location_player_game].has(int(message_part["text"])): - location_name = client._location_id_to_name[location_player_game][int( - message_part["text"] - )] - - parts.append("[color=#00ff7f]%s[/color]" % location_name) - elif message_part.has("text"): - parts.append(message_part["text"]) + parts.append("[color=#fafad2]%s[/color]" % message_part["text"]) + elif message_part["type"] == "item": + parts.append(wrapInItemColorTags(message_part["text"], message_part["flags"])) + elif message_part["type"] == "location": + parts.append("[color=#00ff7f]%s[/color]" % message_part["text"]) var textclient_node = global.get_node("Textclient") if textclient_node != null: textclient_node.parse_printjson("".join(parts)) -func _process_location_scout(item_id, location_id, player, flags): - _location_scouts[location_id] = {"item": item_id, "player": player, "flags": flags} +func _process_location_scout(location_id, item_name, player_name, flags, for_self): + _location_scouts[location_id] = { + "item": item_name, "player": player_name, "flags": flags, "for_self": for_self + } - if player == client._slot and flags & 4 != 0: + if for_self and flags & 4 != 0: # This is a trap for us, so let's not display it. return var gamedata = global.get_node("Gamedata") var map_id = gamedata.map_id_by_name.get(global.map) - var item_name = "Unknown" - var item_player_game = client._game_by_player[float(player)] - if client._item_id_to_name[item_player_game].has(item_id): - item_name = client._item_id_to_name[item_player_game][item_id] - var letter_id = gamedata.letter_id_by_ap_id.get(location_id, null) if letter_id != null: var letter = gamedata.objects.get_letters()[letter_id] @@ -350,11 +303,11 @@ func _process_location_scout(item_id, location_id, player, flags): func _client_could_not_connect(message): - emit_signal("could_not_connect", message) + could_not_connect.emit(message) func _client_connect_status(message): - emit_signal("connect_status", message) + connect_status.emit(message) func _client_connected(slot_data): @@ -463,7 +416,7 @@ func _client_connected(slot_data): _inverse_item_locks[lock[0]].append([door_id, lock[1]]) - emit_signal("ap_connected") + ap_connected.emit() func start_batching_locations(): diff --git a/apworld/client/panel.gd b/apworld/client/panel.gd index fdaaf0e..2cef28e 100644 --- a/apworld/client/panel.gd +++ b/apworld/client/panel.gd @@ -29,8 +29,8 @@ func _ready(): checkSymbolSolvable() if not symbol_solvable: - get_tree().get_root().get_node("scene/player").connect( - "evaluate_solvability", evaluateSolvability + get_tree().get_root().get_node("scene/player").evaluate_solvability.connect( + evaluateSolvability ) diff --git a/apworld/client/player.gd b/apworld/client/player.gd index e58f1bc..fb88880 100644 --- a/apworld/client/player.gd +++ b/apworld/client/player.gd @@ -105,20 +105,12 @@ func _ready(): != ap.kLETTER_BEHAVIOR_VANILLA ): var scout = ap.scout_location(letter.get_ap_id()) - if ( - scout != null - and not (scout["player"] == ap.client._slot and scout["flags"] & 4 != 0) - ): - var item_name = "Unknown" - var item_player_game = ap.client._game_by_player[float(scout["player"])] - if ap.client._item_id_to_name[item_player_game].has(scout["item"]): - item_name = ap.client._item_id_to_name[item_player_game][scout["item"]] - - var collectable = get_tree().get_root().get_node("scene").get_node_or_null( - letter.get_path() - ) - if collectable != null: - collectable.setScoutedText.call_deferred(item_name) + if scout != null and not (scout["for_self"] and scout["flags"] & 4 != 0): + var collectable = get_tree().get_root().get_node("scene").get_node_or_null( + letter.get_path() + ) + if collectable != null: + collectable.setScoutedText.call_deferred(scout["item"]) # Set up mastery locations. for mastery in gamedata.objects.get_masteries(): diff --git a/apworld/client/textclient.gd b/apworld/client/textclient.gd index 26831b4..9841063 100644 --- a/apworld/client/textclient.gd +++ b/apworld/client/textclient.gd @@ -47,7 +47,7 @@ func _ready(): entry.add_theme_color_override("cursor_color", Color(0, 0, 0, 1)) entry.add_theme_stylebox_override("focus", entry_style) panel.add_child(entry) - entry.connect("text_submitted", text_entered) + entry.text_submitted.connect(text_entered) func _input(event): diff --git a/apworld/client/vendor/LICENSE b/apworld/client/vendor/LICENSE index 115ba15..12763b1 100644 --- a/apworld/client/vendor/LICENSE +++ b/apworld/client/vendor/LICENSE @@ -1,21 +1,21 @@ -MIT License +WebSocketServer.gd: -Copyright (c) 2023 Xavier Sellier +Copyright (c) 2014-present Godot Engine contributors. Copyright (c) 2007-2014 +Juan Linietsky, Ariel Manzur. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/apworld/client/vendor/WebSocketServer.gd b/apworld/client/vendor/WebSocketServer.gd new file mode 100644 index 0000000..2cee494 --- /dev/null +++ b/apworld/client/vendor/WebSocketServer.gd @@ -0,0 +1,173 @@ +class_name WebSocketServer +extends Node + +signal message_received(peer_id: int, message: String) +signal client_connected(peer_id: int) +signal client_disconnected(peer_id: int) + +@export var handshake_headers := PackedStringArray() +@export var supported_protocols := PackedStringArray() +@export var handshake_timout := 3000 +@export var use_tls := false +@export var tls_cert: X509Certificate +@export var tls_key: CryptoKey +@export var refuse_new_connections := false: + set(refuse): + if refuse: + pending_peers.clear() + + +class PendingPeer: + var connect_time: int + var tcp: StreamPeerTCP + var connection: StreamPeer + var ws: WebSocketPeer + + func _init(p_tcp: StreamPeerTCP) -> void: + tcp = p_tcp + connection = p_tcp + connect_time = Time.get_ticks_msec() + + +var tcp_server := TCPServer.new() +var pending_peers: Array[PendingPeer] = [] +var peers: Dictionary + + +func listen(port: int) -> int: + assert(not tcp_server.is_listening()) + return tcp_server.listen(port) + + +func stop() -> void: + tcp_server.stop() + pending_peers.clear() + peers.clear() + + +func send(peer_id: int, message: String) -> int: + var type := typeof(message) + if peer_id <= 0: + # Send to multiple peers, (zero = broadcast, negative = exclude one). + for id: int in peers: + if id == -peer_id: + continue + if type == TYPE_STRING: + peers[id].send_text(message) + else: + peers[id].put_packet(message) + return OK + + assert(peers.has(peer_id)) + var socket: WebSocketPeer = peers[peer_id] + if type == TYPE_STRING: + return socket.send_text(message) + return socket.send(var_to_bytes(message)) + + +func get_message(peer_id: int) -> Variant: + assert(peers.has(peer_id)) + var socket: WebSocketPeer = peers[peer_id] + if socket.get_available_packet_count() < 1: + return null + var pkt: PackedByteArray = socket.get_packet() + if socket.was_string_packet(): + return pkt.get_string_from_utf8() + return bytes_to_var(pkt) + + +func has_message(peer_id: int) -> bool: + assert(peers.has(peer_id)) + return peers[peer_id].get_available_packet_count() > 0 + + +func _create_peer() -> WebSocketPeer: + var ws := WebSocketPeer.new() + ws.supported_protocols = supported_protocols + ws.handshake_headers = handshake_headers + return ws + + +func poll() -> void: + if not tcp_server.is_listening(): + return + + while not refuse_new_connections and tcp_server.is_connection_available(): + var conn: StreamPeerTCP = tcp_server.take_connection() + assert(conn != null) + pending_peers.append(PendingPeer.new(conn)) + + var to_remove := [] + + for p in pending_peers: + if not _connect_pending(p): + if p.connect_time + handshake_timout < Time.get_ticks_msec(): + # Timeout. + to_remove.append(p) + continue # Still pending. + + to_remove.append(p) + + for r: RefCounted in to_remove: + pending_peers.erase(r) + + to_remove.clear() + + for id: int in peers: + var p: WebSocketPeer = peers[id] + p.poll() + + if p.get_ready_state() != WebSocketPeer.STATE_OPEN: + client_disconnected.emit(id) + to_remove.append(id) + continue + + while p.get_available_packet_count(): + message_received.emit(id, get_message(id)) + + for r: int in to_remove: + peers.erase(r) + to_remove.clear() + + +func _connect_pending(p: PendingPeer) -> bool: + if p.ws != null: + # Poll websocket client if doing handshake. + p.ws.poll() + var state := p.ws.get_ready_state() + if state == WebSocketPeer.STATE_OPEN: + var id := randi_range(2, 1 << 30) + peers[id] = p.ws + client_connected.emit(id) + return true # Success. + elif state != WebSocketPeer.STATE_CONNECTING: + return true # Failure. + return false # Still connecting. + elif p.tcp.get_status() != StreamPeerTCP.STATUS_CONNECTED: + return true # TCP disconnected. + elif not use_tls: + # TCP is ready, create WS peer. + p.ws = _create_peer() + p.ws.accept_stream(p.tcp) + return false # WebSocketPeer connection is pending. + + else: + if p.connection == p.tcp: + assert(tls_key != null and tls_cert != null) + var tls := StreamPeerTLS.new() + tls.accept_stream(p.tcp, TLSOptions.server(tls_key, tls_cert)) + p.connection = tls + p.connection.poll() + var status: StreamPeerTLS.Status = p.connection.get_status() + if status == StreamPeerTLS.STATUS_CONNECTED: + p.ws = _create_peer() + p.ws.accept_stream(p.connection) + return false # WebSocketPeer connection is pending. + if status != StreamPeerTLS.STATUS_HANDSHAKING: + return true # Failure. + + return false + + +func _process(_delta: float) -> void: + poll() diff --git a/apworld/client/vendor/uuid.gd b/apworld/client/vendor/uuid.gd deleted file mode 100644 index b63fa04..0000000 --- a/apworld/client/vendor/uuid.gd +++ /dev/null @@ -1,195 +0,0 @@ -# Note: The code might not be as pretty it could be, since it's written -# in a way that maximizes performance. Methods are inlined and loops are avoided. -extends Node - -const BYTE_MASK: int = 0b11111111 - - -static func uuidbin(): - randomize() - # 16 random bytes with the bytes on index 6 and 8 modified - return [ - randi() & BYTE_MASK, - randi() & BYTE_MASK, - randi() & BYTE_MASK, - randi() & BYTE_MASK, - randi() & BYTE_MASK, - randi() & BYTE_MASK, - ((randi() & BYTE_MASK) & 0x0f) | 0x40, - randi() & BYTE_MASK, - ((randi() & BYTE_MASK) & 0x3f) | 0x80, - randi() & BYTE_MASK, - randi() & BYTE_MASK, - randi() & BYTE_MASK, - randi() & BYTE_MASK, - randi() & BYTE_MASK, - randi() & BYTE_MASK, - randi() & BYTE_MASK, - ] - - -static func uuidbinrng(rng: RandomNumberGenerator): - rng.randomize() - return [ - rng.randi() & BYTE_MASK, - rng.randi() & BYTE_MASK, - rng.randi() & BYTE_MASK, - rng.randi() & BYTE_MASK, - rng.randi() & BYTE_MASK, - rng.randi() & BYTE_MASK, - ((rng.randi() & BYTE_MASK) & 0x0f) | 0x40, - rng.randi() & BYTE_MASK, - ((rng.randi() & BYTE_MASK) & 0x3f) | 0x80, - rng.randi() & BYTE_MASK, - rng.randi() & BYTE_MASK, - rng.randi() & BYTE_MASK, - rng.randi() & BYTE_MASK, - rng.randi() & BYTE_MASK, - rng.randi() & BYTE_MASK, - rng.randi() & BYTE_MASK, - ] - - -static func v4(): - # 16 random bytes with the bytes on index 6 and 8 modified - var b = uuidbin() - - return ( - "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x" - % [ - # low - b[0], - b[1], - b[2], - b[3], - # mid - b[4], - b[5], - # hi - b[6], - b[7], - # clock - b[8], - b[9], - # clock - b[10], - b[11], - b[12], - b[13], - b[14], - b[15] - ] - ) - - -static func v4_rng(rng: RandomNumberGenerator): - # 16 random bytes with the bytes on index 6 and 8 modified - var b = uuidbinrng(rng) - - return ( - "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x" - % [ - # low - b[0], - b[1], - b[2], - b[3], - # mid - b[4], - b[5], - # hi - b[6], - b[7], - # clock - b[8], - b[9], - # clock - b[10], - b[11], - b[12], - b[13], - b[14], - b[15] - ] - ) - - -var _uuid: Array - - -func _init(rng := RandomNumberGenerator.new()) -> void: - _uuid = uuidbinrng(rng) - - -func as_array() -> Array: - return _uuid.duplicate() - - -func as_dict(big_endian := true) -> Dictionary: - if big_endian: - return { - "low": (_uuid[0] << 24) + (_uuid[1] << 16) + (_uuid[2] << 8) + _uuid[3], - "mid": (_uuid[4] << 8) + _uuid[5], - "hi": (_uuid[6] << 8) + _uuid[7], - "clock": (_uuid[8] << 8) + _uuid[9], - "node": - ( - (_uuid[10] << 40) - + (_uuid[11] << 32) - + (_uuid[12] << 24) - + (_uuid[13] << 16) - + (_uuid[14] << 8) - + _uuid[15] - ) - } - else: - return { - "low": _uuid[0] + (_uuid[1] << 8) + (_uuid[2] << 16) + (_uuid[3] << 24), - "mid": _uuid[4] + (_uuid[5] << 8), - "hi": _uuid[6] + (_uuid[7] << 8), - "clock": _uuid[8] + (_uuid[9] << 8), - "node": - ( - _uuid[10] - + (_uuid[11] << 8) - + (_uuid[12] << 16) - + (_uuid[13] << 24) - + (_uuid[14] << 32) - + (_uuid[15] << 40) - ) - } - - -func as_string() -> String: - return ( - "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x" - % [ - # low - _uuid[0], - _uuid[1], - _uuid[2], - _uuid[3], - # mid - _uuid[4], - _uuid[5], - # hi - _uuid[6], - _uuid[7], - # clock - _uuid[8], - _uuid[9], - # node - _uuid[10], - _uuid[11], - _uuid[12], - _uuid[13], - _uuid[14], - _uuid[15] - ] - ) - - -func is_equal(other) -> bool: - # Godot Engine compares Array recursively - # There's no need for custom comparison here. - return _uuid == other._uuid -- cgit 1.4.1