From 6fd6d493cd16b41bf88742ff6f4b7635ec3fa67c Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Thu, 28 Aug 2025 14:25:50 -0400 Subject: Client is starting to work! --- client/Archipelago/client.gd | 378 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 378 insertions(+) create mode 100644 client/Archipelago/client.gd (limited to 'client/Archipelago/client.gd') diff --git a/client/Archipelago/client.gd b/client/Archipelago/client.gd new file mode 100644 index 0000000..5c4bc51 --- /dev/null +++ b/client/Archipelago/client.gd @@ -0,0 +1,378 @@ +extends Node + +const ap_version = {"major": 0, "minor": 6, "build": 3, "class": "Version"} + +var SCRIPT_uuid + +var _ws = WebSocketPeer.new() +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} + +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_items = [] +var _slot_data = {} + +signal could_not_connect +signal connect_status +signal client_connected +signal item_received(item_id, index, player, flags) +signal message_received(message) + + +func _init(): + global._print("Instantiated APClient") + + # Read AP settings 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) + + +func _reset_state(): + _should_process = false + _authenticated = false + _try_wss = false + _has_connected = false + + +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() + + 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 _closed(_was_clean = true): + global._print("Connection closed") + _reset_state() + + if not _initiated_disconnect: + emit_signal("could_not_connect", "Disconnected from Archipelago") + + _initiated_disconnect = false + + +func _connected(_proto = ""): + global._print("Connected!") + _try_wss = false + + +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") + + 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.disconnect_from_host() + + 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"]: + if not _received_items.has(int(item["item"])): + _received_items.append(int(item["item"])) + + emit_signal( + "item_received", + int(item["item"]), + int(message["index"]) + i, + int(item["player"]), + int(item["flags"]) + ) + i += 1 + + elif cmd == "PrintJSON": + emit_signal("message_received", message) + + 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() + + +func connectToServer(server, un, pw): + ap_server = server + ap_user = un + ap_pass = pw + + _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: " + err) + return + _should_process = true + + emit_signal("connect_status", "Connecting...") + + +func sendMessage(msg): + var payload = JSON.stringify(msg) + _ws.send_text(payload) + + +func requestDatapackages(games): + emit_signal("connect_status", "Downloading %s data package..." % games[0]) + + sendMessage([{"cmd": "GetDataPackage", "games": games}]) + + +func processDatapackages(): + _item_id_to_name = {} + _location_id_to_name = {} + for game in _datapackages.keys(): + var package = _datapackages[game] + + _item_id_to_name[game] = {} + for item_name in package["item_name_to_id"].keys(): + _item_id_to_name[game][package["item_name_to_id"][item_name]] = item_name + + _location_id_to_name[game] = {} + for location_name in package["location_name_to_id"].keys(): + _location_id_to_name[game][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 connectToRoom(): + emit_signal("connect_status", "Authenticating...") + + sendMessage( + [ + { + "cmd": "Connect", + "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"}]) + + +func sendLocation(loc_id): + sendMessage([{"cmd": "LocationChecks", "locations": [loc_id]}]) + + +func setValue(key, value, operation = "replace"): + sendMessage( + [ + { + "cmd": "Set", + "key": "Lingo2_%d_%s" % [_slot, key], + "want_reply": false, + "operations": [{"operation": operation, "value": value}] + } + ] + ) + + +func say(textdata): + sendMessage([{"cmd": "Say", "text": textdata}]) + + +func completedGoal(): + sendMessage([{"cmd": "StatusUpdate", "status": 30}]) # CLIENT_GOAL + + +func hasItem(item_id): + return _received_items.has(item_id) -- cgit 1.4.1