import Utils import settings import base64 import threading import requests import yaml from worlds.AutoWorld import World, WebWorld from BaseClasses import Tutorial from .Regions import create_regions, location_table, set_rules, stage_set_rules, rooms, non_dead_end_crest_rooms,\ non_dead_end_crest_warps from .Items import item_table, item_groups, create_items, FFMQItem, fillers from .Output import generate_output from .Options import FFMQOptions from .Client import FFMQClient # removed until lists are supported # class FFMQSettings(settings.Group): # class APIUrls(list): # """A list of API URLs to get map shuffle, crest shuffle, and battlefield reward shuffle data from.""" # api_urls: APIUrls = [ # "https://api.ffmqrando.net/", # "http://ffmqr.jalchavware.com:5271/" # ] class FFMQWebWorld(WebWorld): setup_en = Tutorial( "Multiworld Setup Guide", "A guide to playing Final Fantasy Mystic Quest with Archipelago.", "English", "setup_en.md", "setup/en", ["Alchav"] ) setup_fr = Tutorial( setup_en.tutorial_name, setup_en.description, "Français", "setup_fr.md", "setup/fr", ["Artea"] ) tutorials = [setup_en, setup_fr] class FFMQWorld(World): """Final Fantasy: Mystic Quest is a simple, humorous RPG for the Super Nintendo. You travel across four continents, linked in the middle of the world by the Focus Tower, which has been locked by four magical coins. Make your way to the bottom of the Focus Tower, then straight up through the top!""" # -Giga Otomia game = "Final Fantasy Mystic Quest" item_name_to_id = {name: data.id for name, data in item_table.items() if data.id is not None} location_name_to_id = location_table options_dataclass = FFMQOptions options: FFMQOptions topology_present = True item_name_groups = item_groups generate_output = generate_output create_items = create_items create_regions = create_regions set_rules = set_rules stage_set_rules = stage_set_rules web = FFMQWebWorld() # settings: FFMQSettings def __init__(self, world, player: int): self.rom_name_available_event = threading.Event() self.rom_name = None self.rooms = None super().__init__(world, player) def generate_early(self): if self.options.sky_coin_mode == "shattered_sky_coin": self.options.brown_boxes.value = 1 if self.options.enemies_scaling_lower.value > self.options.enemies_scaling_upper.value: self.options.enemies_scaling_lower.value, self.options.enemies_scaling_upper.value = \ self.options.enemies_scaling_upper.value, self.options.enemies_scaling_lower.value if self.options.bosses_scaling_lower.value > self.options.bosses_scaling_upper.value: self.options.bosses_scaling_lower.value, self.options.bosses_scaling_upper.value = \ self.options.bosses_scaling_upper.value, self.options.bosses_scaling_lower.value @classmethod def stage_generate_early(cls, multiworld): # api_urls = Utils.get_options()["ffmq_options"].get("api_urls", None) api_urls = [ "https://api.ffmqrando.net/", "http://ffmqr.jalchavware.com:5271/" ] rooms_data = {} for world in multiworld.get_game_worlds("Final Fantasy Mystic Quest"): if (world.options.map_shuffle or world.options.crest_shuffle or world.options.shuffle_battlefield_rewards or world.options.companions_locations): if world.options.map_shuffle_seed.value.isdigit(): multiworld.random.seed(int(world.options.map_shuffle_seed.value)) elif world.options.map_shuffle_seed.value != "random": multiworld.random.seed(int(hash(world.options.map_shuffle_seed.value)) + int(world.multiworld.seed)) seed = hex(multiworld.random.randint(0, 0xFFFFFFFF)).split("0x")[1].upper() map_shuffle = world.options.map_shuffle.value crest_shuffle = world.options.crest_shuffle.current_key battlefield_shuffle = world.options.shuffle_battlefield_rewards.current_key companion_shuffle = world.options.companions_locations.value kaeli_mom = world.options.kaelis_mom_fight_minotaur.current_key query = f"s={seed}&m={map_shuffle}&c={crest_shuffle}&b={battlefield_shuffle}&cs={companion_shuffle}&km={kaeli_mom}" if query in rooms_data: world.rooms = rooms_data[query] continue if not api_urls: raise Exception("No FFMQR API URLs specified in host.yaml") errors = [] for api_url in api_urls.copy(): try: response = requests.get(f"{api_url}GenerateRooms?{query}") except (ConnectionError, requests.exceptions.HTTPError, requests.exceptions.ConnectionError, requests.exceptions.RequestException) as err: api_urls.remove(api_url) errors.append([api_url, err]) else: if response.ok: world.rooms = rooms_data[query] = yaml.load(response.text, yaml.Loader) break else: api_urls.remove(api_url) errors.append([api_url, response]) else: error_text = f"Failed to fetch map shuffle data for FFMQ player {world.player}" for error in errors: error_text += f"\n{error[0]} - got error {error[1].status_code} {error[1].reason} {error[1].text}" raise Exception(error_text) api_urls.append(api_urls.pop(0)) else: world.rooms = rooms def create_item(self, name: str): return FFMQItem(name, self.player) def collect_item(self, state, item, remove=False): if "Progressive" in item.name: i = item.code - 256 if state.has(self.item_id_to_name[i], self.player): if state.has(self.item_id_to_name[i+1], self.player): return self.item_id_to_name[i+2] return self.item_id_to_name[i+1] return self.item_id_to_name[i] return item.name if item.advancement else None def modify_multidata(self, multidata): # wait for self.rom_name to be available. self.rom_name_available_event.wait() rom_name = getattr(self, "rom_name", None) # we skip in case of error, so that the original error in the output thread is the one that gets raised if rom_name: new_name = base64.b64encode(bytes(self.rom_name)).decode() payload = multidata["connect_names"][self.multiworld.player_name[self.player]] multidata["connect_names"][new_name] = payload def get_filler_item_name(self): r = self.multiworld.random.randint(0, 201) for item, count in fillers.items(): r -= count r -= fillers[item] if r <= 0: return item def extend_hint_information(self, hint_data): hint_data[self.player] = {} if self.options.map_shuffle: single_location_regions = ["Subregion Volcano Battlefield", "Subregion Mac's Ship", "Subregion Doom Castle"] for subregion in ["Subregion Foresta", "Subregion Aquaria", "Subregion Frozen Fields", "Subregion Fireburg", "Subregion Volcano Battlefield", "Subregion Windia", "Subregion Mac's Ship", "Subregion Doom Castle"]: region = self.multiworld.get_region(subregion, self.player) for location in region.locations: if location.address and self.options.map_shuffle != "dungeons": hint_data[self.player][location.address] = (subregion.split("Subregion ")[-1] + (" Region" if subregion not in single_location_regions else "")) for overworld_spot in region.exits: if ("Subregion" in overworld_spot.connected_region.name or overworld_spot.name == "Overworld - Mac Ship Doom" or "Focus Tower" in overworld_spot.name or "Doom Castle" in overworld_spot.name or overworld_spot.name == "Overworld - Giant Tree"): continue exits = list(overworld_spot.connected_region.exits) + [overworld_spot] checked_regions = set() while exits: exit_check = exits.pop() if (exit_check.connected_region not in checked_regions and "Subregion" not in exit_check.connected_region.name): checked_regions.add(exit_check.connected_region) exits.extend(exit_check.connected_region.exits) for location in exit_check.connected_region.locations: if location.address: hint = [] if self.options.map_shuffle != "dungeons": hint.append((subregion.split("Subregion ")[-1] + (" Region" if subregion not in single_location_regions else ""))) if self.options.map_shuffle != "overworld": hint.append(overworld_spot.name.split("Overworld - ")[-1].replace("Pazuzu", "Pazuzu's")) hint = " - ".join(hint).replace(" - Mac Ship", "") if location.address in hint_data[self.player]: hint_data[self.player][location.address] += f"/{hint}" else: hint_data[self.player][location.address] = hint