From 3f53502a5907ed1982d28a392c54331f0c1c2c42 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Thu, 25 Sep 2025 12:09:50 -0400 Subject: Move the client into the apworld Only works on source right now, not as an apworld. --- apworld/client/manager.gd | 571 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 571 insertions(+) create mode 100644 apworld/client/manager.gd (limited to 'apworld/client/manager.gd') diff --git a/apworld/client/manager.gd b/apworld/client/manager.gd new file mode 100644 index 0000000..b170c77 --- /dev/null +++ b/apworld/client/manager.gd @@ -0,0 +1,571 @@ +extends Node + +const MOD_VERSION = 7 + +var SCRIPT_client +var SCRIPT_keyboard +var SCRIPT_locationListener +var SCRIPT_minimap +var SCRIPT_uuid +var SCRIPT_victoryListener + +var ap_server = "" +var ap_user = "" +var ap_pass = "" +var connection_history = [] +var show_compass = false + +var client +var keyboard + +var _localdata_file = "" +var _last_new_item = -1 +var _batch_locations = false +var _held_locations = [] +var _held_location_scouts = [] +var _location_scouts = {} +var _item_locks = {} +var _inverse_item_locks = {} +var _held_letters = {} +var _letters_setup = false + +const kSHUFFLE_LETTERS_VANILLA = 0 +const kSHUFFLE_LETTERS_UNLOCKED = 1 +const kSHUFFLE_LETTERS_PROGRESSIVE = 2 +const kSHUFFLE_LETTERS_VANILLA_CYAN = 3 +const kSHUFFLE_LETTERS_ITEM_CYAN = 4 + +const kLETTER_BEHAVIOR_VANILLA = 0 +const kLETTER_BEHAVIOR_ITEM = 1 +const kLETTER_BEHAVIOR_UNLOCKED = 2 + +const kCYAN_DOOR_BEHAVIOR_H2 = 0 +const kCYAN_DOOR_BEHAVIOR_DOUBLE_LETTER = 1 +const kCYAN_DOOR_BEHAVIOR_ITEM = 2 + +var apworld_version = [0, 0] +var cyan_door_behavior = kCYAN_DOOR_BEHAVIOR_H2 +var daedalus_roof_access = false +var keyholder_sanity = false +var port_pairings = {} +var shuffle_control_center_colors = false +var shuffle_doors = false +var shuffle_gallery_paintings = false +var shuffle_letters = kSHUFFLE_LETTERS_VANILLA +var shuffle_symbols = false +var shuffle_worldports = false +var strict_cyan_ending = false +var strict_purple_ending = false +var victory_condition = -1 + +signal could_not_connect +signal connect_status +signal ap_connected + + +func _init(): + # Read AP settings from file, if there are any + if FileAccess.file_exists("user://ap_settings"): + var file = FileAccess.open("user://ap_settings", FileAccess.READ) + var data = file.get_var(true) + file.close() + + if typeof(data) != TYPE_ARRAY: + global._print("AP settings file is corrupted") + data = [] + + if data.size() > 0: + ap_server = data[0] + + if data.size() > 1: + ap_user = data[1] + + if data.size() > 2: + ap_pass = data[2] + + if data.size() > 3: + connection_history = data[3] + + if data.size() > 4: + show_compass = data[4] + + +func _ready(): + client = SCRIPT_client.new() + client.SCRIPT_uuid = SCRIPT_uuid + + 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) + + add_child(client) + + keyboard = SCRIPT_keyboard.new() + add_child(keyboard) + + +func saveSettings(): + # Save the AP settings to disk. + var path = "user://ap_settings" + var file = FileAccess.open(path, FileAccess.WRITE) + + var data = [ + ap_server, + ap_user, + ap_pass, + connection_history, + show_compass, + ] + file.store_var(data, true) + file.close() + + +func saveLocaldata(): + # Save the MW/slot specific settings to disk. + var dir = DirAccess.open("user://") + var folder = "archipelago_data" + if not dir.dir_exists(folder): + dir.make_dir(folder) + + var file = FileAccess.open(_localdata_file, FileAccess.WRITE) + + var data = [ + _last_new_item, + ] + file.store_var(data, true) + file.close() + + +func connectToServer(): + _last_new_item = -1 + _batch_locations = false + _held_locations = [] + _held_location_scouts = [] + _location_scouts = {} + _letters_setup = false + _held_letters = {} + + client.connectToServer(ap_server, ap_user, ap_pass) + + +func getSaveFileName(): + return "zzAP_%s_%d" % [client._seed, client._slot] + + +func disconnect_from_ap(): + client.disconnect_from_ap() + + +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] + + var gamedata = global.get_node("Gamedata") + + var prog_id = null + if _inverse_item_locks.has(item): + for lock in _inverse_item_locks.get(item): + if lock[1] != amount: + continue + + if gamedata.progressive_id_by_ap_id.has(item): + prog_id = lock[0] + + if gamedata.get_door_map_name(lock[0]) != global.map: + continue + + var receivers = gamedata.get_door_receivers(lock[0]) + var scene = get_tree().get_root().get_node_or_null("scene") + if scene != null: + for receiver in receivers: + var rnode = scene.get_node_or_null(receiver) + if rnode != null: + rnode.handleTriggered() + + var letter_id = gamedata.letter_id_by_ap_id.get(item, 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): + var player = get_tree().get_root().get_node_or_null("scene/player") + if player != null: + player.emit_signal("evaluate_solvability") + + # Show a message about the item if it's new. + if index != null and index > _last_new_item: + _last_new_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 + if prog_id != null: + var door = gamedata.objects.get_doors()[prog_id] + full_item_name = "%s (%s)" % [item_name, door.get_name()] + + var message + if from == client._slot: + message = "Found %s" % wrapInItemColorTags(full_item_name, flags) + else: + message = ( + "Received %s from %s" % [wrapInItemColorTags(full_item_name, flags), player_name] + ) + + if gamedata.anti_trap_ids.has(item): + keyboard.block_letter(gamedata.anti_trap_ids[item]) + + global._print(message) + + 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): + var parts = [] + for message_part in message["data"]: + if !message_part.has("type") and message_part.has("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] + ) + 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"]) + + 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} + + if player == client._slot 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] + var room = gamedata.objects.get_rooms()[letter.get_room_id()] + if room.get_map_id() == map_id: + var collectable = get_tree().get_root().get_node("scene").get_node_or_null( + letter.get_path() + ) + if collectable != null: + collectable.setScoutedText(item_name) + + +func _client_could_not_connect(message): + emit_signal("could_not_connect", message) + + +func _client_connect_status(message): + emit_signal("connect_status", message) + + +func _client_connected(slot_data): + var gamedata = global.get_node("Gamedata") + + _localdata_file = "user://archipelago_data/%s_%d" % [client._seed, client._slot] + _last_new_item = -1 + + if FileAccess.file_exists(_localdata_file): + var ap_file = FileAccess.open(_localdata_file, FileAccess.READ) + var localdata = [] + if ap_file != null: + localdata = ap_file.get_var(true) + ap_file.close() + + if typeof(localdata) != TYPE_ARRAY: + print("AP localdata file is corrupted") + localdata = [] + + if localdata.size() > 0: + _last_new_item = localdata[0] + + # Read slot data. + cyan_door_behavior = int(slot_data.get("cyan_door_behavior", 0)) + daedalus_roof_access = bool(slot_data.get("daedalus_roof_access", false)) + keyholder_sanity = bool(slot_data.get("keyholder_sanity", false)) + shuffle_control_center_colors = bool(slot_data.get("shuffle_control_center_colors", false)) + shuffle_doors = bool(slot_data.get("shuffle_doors", false)) + shuffle_gallery_paintings = bool(slot_data.get("shuffle_gallery_paintings", false)) + shuffle_letters = int(slot_data.get("shuffle_letters", 0)) + shuffle_symbols = bool(slot_data.get("shuffle_symbols", false)) + shuffle_worldports = bool(slot_data.get("shuffle_worldports", false)) + strict_cyan_ending = bool(slot_data.get("strict_cyan_ending", false)) + strict_purple_ending = bool(slot_data.get("strict_purple_ending", false)) + victory_condition = int(slot_data.get("victory_condition", 0)) + + if slot_data.has("version"): + apworld_version = [int(slot_data["version"][0]), int(slot_data["version"][1])] + + port_pairings.clear() + if slot_data.has("port_pairings"): + var raw_pp = slot_data.get("port_pairings") + + for p1 in raw_pp.keys(): + port_pairings[int(p1)] = int(raw_pp[p1]) + + # Set up item locks. + _item_locks = {} + + if shuffle_doors: + for door in gamedata.objects.get_doors(): + if ( + door.get_type() == gamedata.SCRIPT_proto.DoorType.STANDARD + or door.get_type() == gamedata.SCRIPT_proto.DoorType.ITEM_ONLY + ): + _item_locks[door.get_id()] = [door.get_ap_id(), 1] + + for progressive in gamedata.objects.get_progressives(): + for i in range(0, progressive.get_doors().size()): + var door = gamedata.objects.get_doors()[progressive.get_doors()[i]] + _item_locks[door.get_id()] = [progressive.get_ap_id(), i + 1] + + for door_group in gamedata.objects.get_door_groups(): + if door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.CONNECTOR: + if shuffle_worldports: + continue + elif door_group.get_type() != gamedata.SCRIPT_proto.DoorGroupType.SHUFFLE_GROUP: + continue + + for door in door_group.get_doors(): + _item_locks[door] = [door_group.get_ap_id(), 1] + + if shuffle_control_center_colors: + for door in gamedata.objects.get_doors(): + if door.get_type() == gamedata.SCRIPT_proto.DoorType.CONTROL_CENTER_COLOR: + _item_locks[door.get_id()] = [door.get_ap_id(), 1] + + for door_group in gamedata.objects.get_door_groups(): + if ( + door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.COLOR_CONNECTOR + and not shuffle_worldports + ): + for door in door_group.get_doors(): + _item_locks[door] = [door_group.get_ap_id(), 1] + + if shuffle_gallery_paintings: + for door in gamedata.objects.get_doors(): + if door.get_type() == gamedata.SCRIPT_proto.DoorType.GALLERY_PAINTING: + _item_locks[door.get_id()] = [door.get_ap_id(), 1] + + if cyan_door_behavior == kCYAN_DOOR_BEHAVIOR_ITEM: + for door_group in gamedata.objects.get_door_groups(): + if door_group.get_type() == gamedata.SCRIPT_proto.DoorGroupType.CYAN_DOORS: + for door in door_group.get_doors(): + if not _item_locks.has(door): + _item_locks[door] = [door_group.get_ap_id(), 1] + + # Create a reverse item locks map for processing items. + _inverse_item_locks = {} + + for door_id in _item_locks.keys(): + var lock = _item_locks.get(door_id) + + if not _inverse_item_locks.has(lock[0]): + _inverse_item_locks[lock[0]] = [] + + _inverse_item_locks[lock[0]].append([door_id, lock[1]]) + + emit_signal("ap_connected") + + +func start_batching_locations(): + _batch_locations = true + + +func send_location(loc_id): + if _batch_locations: + _held_locations.append(loc_id) + else: + client.sendLocation(loc_id) + + +func scout_location(loc_id): + if _location_scouts.has(loc_id): + return _location_scouts.get(loc_id) + + if _batch_locations: + _held_location_scouts.append(loc_id) + else: + client.scoutLocation(loc_id) + + return null + + +func stop_batching_locations(): + _batch_locations = false + + if not _held_locations.is_empty(): + client.sendLocations(_held_locations) + _held_locations.clear() + + if not _held_location_scouts.is_empty(): + client.scoutLocations(_held_location_scouts) + _held_location_scouts.clear() + + +func colorForItemType(flags): + var int_flags = int(flags) + if int_flags & 1: # progression + if int_flags & 2: # proguseful + return "#f0d200" + else: + return "#bc51e0" + elif int_flags & 2: # useful + return "#2b67ff" + elif int_flags & 4: # trap + return "#d63a22" + else: # filler + return "#14de9e" + + +func wrapInItemColorTags(text, flags): + var int_flags = int(flags) + if int_flags & 1 and int_flags & 2: # proguseful + return "[rainbow]%s[/rainbow]" % text + else: + return "[color=%s]%s[/color]" % [colorForItemType(flags), text] + + +func get_letter_behavior(key, level2): + if shuffle_letters == kSHUFFLE_LETTERS_UNLOCKED: + return kLETTER_BEHAVIOR_UNLOCKED + + if [kSHUFFLE_LETTERS_VANILLA_CYAN, kSHUFFLE_LETTERS_ITEM_CYAN].has(shuffle_letters): + if level2: + if shuffle_letters == kSHUFFLE_LETTERS_VANILLA_CYAN: + return kLETTER_BEHAVIOR_VANILLA + else: + return kLETTER_BEHAVIOR_ITEM + else: + return kLETTER_BEHAVIOR_UNLOCKED + + if not level2 and ["h", "i", "n", "t"].has(key): + # This differs from the equivalent function in the apworld. Logically it is + # the same as UNLOCKED since they are in the starting room, but VANILLA + # means the player still has to actually pick up the letters. + return kLETTER_BEHAVIOR_VANILLA + + if shuffle_letters == kSHUFFLE_LETTERS_PROGRESSIVE: + return kLETTER_BEHAVIOR_ITEM + + return kLETTER_BEHAVIOR_VANILLA + + +func setup_keys(): + keyboard.load_seed() + + _letters_setup = true + + for k in _held_letters.keys(): + _process_key_item(k, _held_letters[k]) + + _held_letters.clear() + + +func _process_key_item(key, level): + if not _letters_setup: + _held_letters[key] = max(_held_letters.get(key, 0), level) + return + + if shuffle_letters == kSHUFFLE_LETTERS_ITEM_CYAN: + level += 1 + + keyboard.collect_remote_letter(key, level) -- cgit 1.4.1