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_indexes = [] var _received_items = {} 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) func _init(): set_process_mode(Node.PROCESS_MODE_ALWAYS) 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) 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() 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", _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.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"]: 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 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][int(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][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 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 sendLocations(loc_ids): sendMessage([{"cmd": "LocationChecks", "locations": loc_ids}]) 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) func getItemAmount(item_id): return _received_items.get(item_id, 0)