from enum import IntEnum, auto from .generated import data_pb2 as data_pb2 from .items import SYMBOL_ITEMS from typing import TYPE_CHECKING, NamedTuple from .options import ShuffleLetters, CyanDoorBehavior if TYPE_CHECKING: from . import Lingo2World def calculate_letter_histogram(solution: str) -> dict[str, int]: histogram = dict() for l in solution: if l.isalpha(): real_l = l.upper() histogram[real_l] = min(histogram.get(real_l, 0) + 1, 2) return histogram class AccessRequirements: items: set[str] progressives: dict[str, int] rooms: set[str] letters: dict[str, int] cyans: bool # This is an AND of ORs. or_logic: list[list["AccessRequirements"]] # When complete_at is set, at least that many of the requirements in possibilities must be accessible. This should # only be used for doors with complete_at > 1, as or_logic is more efficient for complete_at == 1. complete_at: int | None possibilities: list["AccessRequirements"] def __init__(self): self.items = set() self.progressives = dict() self.rooms = set() self.letters = dict() self.cyans = False self.or_logic = list() self.complete_at = None self.possibilities = list() def copy(self) -> "AccessRequirements": reqs = AccessRequirements() reqs.items = self.items.copy() reqs.progressives = self.progressives.copy() reqs.rooms = self.rooms.copy() reqs.letters = self.letters.copy() reqs.cyans = self.cyans reqs.or_logic = [[other_req.copy() for other_req in disjunction] for disjunction in self.or_logic] reqs.complete_at = self.complete_at reqs.possibilities = self.possibilities.copy() return reqs def merge(self, other: "AccessRequirements"): for item in other.items: self.items.add(item) for item, amount in other.progressives.items(): self.progressives[item] = max(amount, self.progressives.get(item, 0)) for room in other.rooms: self.rooms.add(room) for letter, level in other.letters.items(): self.letters[letter] = max(self.letters.get(letter, 0), level) self.cyans = self.cyans or other.cyans for disjunction in other.or_logic: self.or_logic.append([sub_req.copy() for sub_req in disjunction]) if other.complete_at is not None: # Merging multiple requirements that use complete_at sucks, and is part of why we want to minimize use of # it. If both requirements use complete_at, we will cheat by using the or_logic field, which supports # conjunctions of requirements. if self.complete_at is not None: print("Merging requirements with complete_at > 1. This is messy and should be avoided!") left_req = AccessRequirements() left_req.complete_at = self.complete_at left_req.possibilities = [sub_req.copy() for sub_req in self.possibilities] self.or_logic.append([left_req]) self.complete_at = None self.possibilities = list() right_req = AccessRequirements() right_req.complete_at = other.complete_at right_req.possibilities = [sub_req.copy() for sub_req in other.possibilities] self.or_logic.append([right_req]) else: self.complete_at = other.complete_at self.possibilities = [sub_req.copy() for sub_req in other.possibilities] def is_empty(self) -> bool: return (len(self.items) == 0 and len(self.progressives) == 0 and len(self.rooms) == 0 and len(self.letters) == 0 and not self.cyans and len(self.or_logic) == 0 and self.complete_at is None) def __eq__(self, other: "AccessRequirements"): return (self.items == other.items and self.progressives == other.progressives and self.rooms == other.rooms and self.letters == other.letters and self.cyans == other.cyans and self.or_logic == other.or_logic and self.complete_at == other.complete_at and self.possibilities == other.possibilities) def simplify(self): resimplify = False if len(self.or_logic) > 0: old_or_logic = self.or_logic def remove_redundant(sub_reqs: "AccessRequirements"): new_reqs = sub_reqs.copy() new_reqs.letters = {l: v for l, v in new_reqs.letters.items() if self.letters.get(l, 0) < v} if new_reqs != sub_reqs: return new_reqs else: return sub_reqs self.or_logic = [] for disjunction in old_or_logic: new_disjunction = [] for ssr in disjunction: new_ssr = remove_redundant(ssr) if not new_ssr.is_empty(): new_disjunction.append(new_ssr) else: new_disjunction.clear() break if len(new_disjunction) == 1: self.merge(new_disjunction[0]) resimplify = True elif len(new_disjunction) > 1: if all(cjr == new_disjunction[0] for cjr in new_disjunction): self.merge(new_disjunction[0]) resimplify = True else: self.or_logic.append(new_disjunction) if resimplify: self.simplify() def get_referenced_rooms(self): result = set(self.rooms) for disjunction in self.or_logic: for sub_req in disjunction: result = result.union(sub_req.get_referenced_rooms()) for sub_req in self.possibilities: result = result.union(sub_req.get_referenced_rooms()) return result def remove_room(self, room: str): if room in self.rooms: self.rooms.remove(room) for disjunction in self.or_logic: for sub_req in disjunction: sub_req.remove_room(room) for sub_req in self.possibilities: sub_req.remove_room(room) def __repr__(self): parts = [] if len(self.items) > 0: parts.append(f"items={self.items}") if len(self.progressives) > 0: parts.append(f"progressives={self.progressives}") if len(self.rooms) > 0: parts.append(f"rooms={self.rooms}") if len(self.letters) > 0: parts.append(f"letters={self.letters}") if self.cyans: parts.append(f"cyans=True") if len(self.or_logic) > 0: parts.append(f"or_logic={self.or_logic}") if self.complete_at is not None: parts.append(f"complete_at={self.complete_at}") if len(self.possibilities) > 0: parts.append(f"possibilities={self.possibilities}") return "AccessRequirements(" + ", ".join(parts) + ")" class PlayerLocation(NamedTuple): code: int | None reqs: AccessRequirements class LetterBehavior(IntEnum): VANILLA = auto() ITEM = auto() UNLOCKED = auto() class Lingo2PlayerLogic: world: "Lingo2World" shuffled_maps: set[int] locations_by_room: dict[int, list[PlayerLocation]] event_loc_item_by_room: dict[int, dict[str, str]] item_by_door: dict[int, tuple[str, int]] panel_reqs: dict[int, AccessRequirements] proxy_reqs: dict[int, dict[str, AccessRequirements]] door_reqs: dict[int, AccessRequirements] real_items: list[str] double_letter_amount: dict[str, int] goal_room_id: int def __init__(self, world: "Lingo2World"): self.world = world self.locations_by_room = {} self.event_loc_item_by_room = {} self.item_by_door = {} self.panel_reqs = dict() self.proxy_reqs = dict() self.door_reqs = dict() self.real_items = list() self.double_letter_amount = dict() def should_shuffle_map(game_map) -> bool: if game_map.type == data_pb2.MapType.NORMAL_MAP: return True elif game_map.type == data_pb2.MapType.ICARUS: return bool(world.options.enable_icarus) return False self.shuffled_maps = set(game_map.id for game_map in world.static_logic.objects.maps if should_shuffle_map(game_map)) if self.world.options.shuffle_doors: for progressive in world.static_logic.objects.progressives: for i in range(0, len(progressive.doors)): door = world.static_logic.objects.doors[progressive.doors[i]] if door.map_id not in self.shuffled_maps: continue self.item_by_door[progressive.doors[i]] = (progressive.name, i + 1) self.real_items.append(progressive.name) for door_group in world.static_logic.objects.door_groups: if door_group.type == data_pb2.DoorGroupType.CONNECTOR: if not self.world.options.shuffle_doors or self.world.options.shuffle_worldports: continue elif door_group.type == data_pb2.DoorGroupType.COLOR_CONNECTOR: if not self.world.options.shuffle_control_center_colors or self.world.options.shuffle_worldports: continue elif door_group.type == data_pb2.D
extends Node2D


func _ready():
	# Some helpful logging.
	if Steam.isSubscribed():
		global._print("Provisioning successful! Build ID: %d" % Steam.getAppBuildId())
	else:
		global._print("Provisioning failed.")

	# Undo the load screen removing our cursor
	get_tree().get_root().set_disable_input(false)
	Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)

	# Increase the WebSocket input buffer size so that we can download large
	# data packages.
	ProjectSettings.set_setting("network/limits/websocket_client/max_in_buffer_kb", 8192)

	# Create the global AP manager, if it doesn't already exist.
	if not global.has_node("Archipelago"):
		var ap_script = ResourceLoader.load("user://maps/Archipelago/manager.gd")
		var ap_instance = ap_script.new()
		ap_instance.name = "Archipelago"

		#apclient_instance.SCRIPT_doorControl = load("user://maps/Archipelago/doorControl.gd")
		#apclient_instance.SCRIPT_effects = load("user://maps/Archipelago/effects.gd")
		#apclient_instance.SCRIPT_location = load("user://maps/Archipelago/location.gd")
		#apclient_instance.SCRIPT_mypainting = load("user://maps/Archipelago/mypainting.gd")
		#apclient_instance.SCRIPT_panel = load("user://maps/Archipelago/panel.gd")
		#apclient_instance.SCRIPT_textclient = load("user://maps/Archipelago/textclient.gd")

		ap_instance.SCRIPT_client = load("user://maps/Archipelago/client.gd")
		ap_instance.SCRIPT_locationListener = load("user://maps/Archipelago/locationListener.gd")
		ap_instance.SCRIPT_uuid = load("user://maps/Archipelago/vendor/uuid.gd")

		global.add_child(ap_instance)

		# Let's also inject any scripts we need to inject now.
		installScriptExtension(ResourceLoader.load("user://maps/Archipelago/animationListener.gd"))
		installScriptExtension(ResourceLoader.load("user://maps/Archipelago/door.gd"))
		installScriptExtension(ResourceLoader.load("user://maps/Archipelago/painting.gd"))
		installScriptExtension(ResourceLoader.load("user://maps/Archipelago/player.gd"))
		installScriptExtension(ResourceLoader.load("user://maps/Archipelago/saver.gd"))
		installScriptExtension(ResourceLoader.load("user://maps/Archipelago/teleportListener.gd"))

		var proto_script = load("user://maps/Archipelago/generated/proto.gd")
		var gamedata_script = load("user://maps/Archipelago/gamedata.gd")
		var gamedata_instance = gamedata_script.new(proto_script)
		gamedata_instance.load(
			FileAccess.get_file_as_bytes("user://maps/Archipelago/generated/data.binpb")
		)
		gamedata_instance.name = "Gamedata"
		global.add_child(gamedata_instance)

		var messages_script = load("user://maps/Archipelago/messages.gd")
		var messages_instance = messages_script.new()
		messages_instance.name = "Messages"
		global.add_child(messages_instance)

	var ap = global.get_node("Archipelago")
	ap.connect("ap_connected", connectionSuccessful)
	ap.connect("could_not_connect", connectionUnsuccessful)
	ap.connect("connect_status", connectionStatus)

	# Populate textboxes with AP settings.
	$Panel/server_box.text = ap.ap_server
	$Panel/player_box.text = ap.ap_user
	$Panel/password_box.text = ap.ap_pass

	var history_box = $Panel/connection_history
	if ap.connection_history.is_empty():
		history_box.disabled = true
	else:
		history_box.disabled = false

		var i = 0
		for details in ap.connection_history:
			history_box.get_popup().add_item("%s (%s)" % [details[1], details[0]], i)
			i += 1

	history_box.get_popup().connect("id_pressed", historySelected)

	# Show client version.
	$Panel/title.text = "ARCHIPELAGO (%s)" % ap.my_version

	# Increase font size in text boxes.
	$Panel/server_box.add_theme_font_size_override("font_size", 36)
	$Panel/player_box.add_theme_font_size_override("font_size", 36)
	$Panel/password_box.add_theme_font_size_override("font_size", 36)


# Adapted from https://gitlab.com/Delta-V-Modding/Mods/-/blob/main/game/ModLoader.gd
func installScriptExtension(childScript: Resource):
	# Force Godot to compile the script now.
	# We need to do this here to ensure that the inheritance chain is
	# properly set up, and multiple mods can chain-extend the same
	# class multiple times.
	# This is also needed to make Godot instantiate the extended class
	# when creating singletons.
	# The actual instance is thrown away.
	childScript.new()

	var parentScript = childScript.get_base_script()
	var parentScriptPath = parentScript.resource_path
	global._print("ModLoader: Installing script extension over %s" % parentScriptPath)
	childScript.take_over_path(parentScriptPath)


func connectionStatus(message):
	var popup = self.get_node("Panel/AcceptDialog")
	popup.title = "Connecting to Archipelago"
	popup.dialog_text = message
	popup.exclusive = true
	popup.get_ok_button().visible = false
	popup.popup_centered()


func connectionSuccessful():
	var ap = global.get_node("Archipelago")

	# Save connection details
	var connection_details = [ap.ap_server, ap.ap_user, ap.ap_pass]
	if ap.connection_history.has(connection_details):
		ap.connection_history.erase(connection_details)
	ap.connection_history.push_front(connection_details)
	if ap.connection_history.size() > 10:
		ap.connection_history.resize(10)
	ap.saveSettings()

	# Switch to the_entry
	Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
	global.user = ap.getSaveFileName()
	global.universe = "lingo"
	global.map = "the_entry"

	unlocks.resetKeys()
	unlocks.resetCollectables()
	unlocks.resetData()
	unlocks.loadKeys()
	unlocks.loadCollectables()
	unlocks.loadData()
	unlocks.unlockKey("capslock", 1)

	clearResourceCache("res://objects/meshes/gridDoor.tscn")
	clearResourceCache("res://objects/nodes/door.tscn")
	clearResourceCache("res://objects/nodes/listeners/animationListener.tscn")
	clearResourceCache("res://objects/nodes/listeners/teleportListener.tscn")
	clearResourceCache("res://objects/nodes/player.tscn")
	clearResourceCache("res://objects/nodes/saver.tscn")

	var paintings_dir = DirAccess.open("res://objects/meshes/paintings")
	if paintings_dir:
		paintings_dir.list_dir_begin()
		var file_name = paintings_dir.get_next()
		while file_name != "":
			if not paintings_dir.current_is_dir() and file_name.ends_with(".tscn"):
				clearResourceCache("res://objects/meshes/paintings/" + file_name)
			file_name = paintings_dir.get_next()

	switcher.switch_map("res://objects/scenes/the_entry.tscn")


func connectionUnsuccessful(error_message):
	$Panel/connect_button.disabled = false

	var popup = $Panel/AcceptDialog
	popup.title = "Could not connect to Archipelago"
	popup.dialog_text = error_message
	popup.exclusive = true
	popup.get_ok_button().visible = true
	popup.popup_centered()


func historySelected(index):
	var ap = global.get_node("Archipelago")
	var details = ap.connection_history[index]

	$Panel/server_box.text = details[0]
	$Panel/player_box.text = details[1]
	$Panel/password_box.text = details[2]


func clearResourceCache(path):
	ResourceLoader.load_threaded_request(path, "", false, ResourceLoader.CACHE_MODE_REPLACE)
	ResourceLoader.load_threaded_get(path)