218 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			218 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
|   | 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 option_definitions | ||
|  | 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): | ||
|  |     tutorials = [Tutorial( | ||
|  |         "Multiworld Setup Guide", | ||
|  |         "A guide to playing Final Fantasy Mystic Quest with Archipelago.", | ||
|  |         "English", | ||
|  |         "setup_en.md", | ||
|  |         "setup/en", | ||
|  |         ["Alchav"] | ||
|  |     )] | ||
|  | 
 | ||
|  | 
 | ||
|  | 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 | ||
|  |     option_definitions = option_definitions | ||
|  | 
 | ||
|  |     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 | ||
|  | 
 | ||
|  |     data_version = 1 | ||
|  |      | ||
|  |     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.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin": | ||
|  |             self.multiworld.brown_boxes[self.player].value = 1 | ||
|  |         if self.multiworld.enemies_scaling_lower[self.player].value > \ | ||
|  |                 self.multiworld.enemies_scaling_upper[self.player].value: | ||
|  |             (self.multiworld.enemies_scaling_lower[self.player].value, | ||
|  |              self.multiworld.enemies_scaling_upper[self.player].value) =\ | ||
|  |                 (self.multiworld.enemies_scaling_upper[self.player].value, | ||
|  |                  self.multiworld.enemies_scaling_lower[self.player].value) | ||
|  |         if self.multiworld.bosses_scaling_lower[self.player].value > \ | ||
|  |                 self.multiworld.bosses_scaling_upper[self.player].value: | ||
|  |             (self.multiworld.bosses_scaling_lower[self.player].value, | ||
|  |              self.multiworld.bosses_scaling_upper[self.player].value) =\ | ||
|  |                 (self.multiworld.bosses_scaling_upper[self.player].value, | ||
|  |                  self.multiworld.bosses_scaling_lower[self.player].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.multiworld.map_shuffle[world.player] or world.multiworld.crest_shuffle[world.player] or | ||
|  |                     world.multiworld.crest_shuffle[world.player]): | ||
|  |                 if world.multiworld.map_shuffle_seed[world.player].value.isdigit(): | ||
|  |                     multiworld.random.seed(int(world.multiworld.map_shuffle_seed[world.player].value)) | ||
|  |                 elif world.multiworld.map_shuffle_seed[world.player].value != "random": | ||
|  |                     multiworld.random.seed(int(hash(world.multiworld.map_shuffle_seed[world.player].value)) | ||
|  |                                                 + int(world.multiworld.seed)) | ||
|  | 
 | ||
|  |                 seed = hex(multiworld.random.randint(0, 0xFFFFFFFF)).split("0x")[1].upper() | ||
|  |                 map_shuffle = multiworld.map_shuffle[world.player].value | ||
|  |                 crest_shuffle = multiworld.crest_shuffle[world.player].current_key | ||
|  |                 battlefield_shuffle = multiworld.shuffle_battlefield_rewards[world.player].current_key | ||
|  | 
 | ||
|  |                 query = f"s={seed}&m={map_shuffle}&c={crest_shuffle}&b={battlefield_shuffle}" | ||
|  | 
 | ||
|  |                 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.multiworld.map_shuffle[self.player]: | ||
|  |             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.multiworld.map_shuffle[self.player] != "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.multiworld.map_shuffle[self.player] != "dungeons": | ||
|  |                                         hint.append((subregion.split("Subregion ")[-1] + (" Region" if subregion not | ||
|  |                                                     in single_location_regions else ""))) | ||
|  |                                     if self.multiworld.map_shuffle[self.player] != "overworld" and subregion not in \ | ||
|  |                                             ("Subregion Mac's Ship", "Subregion Doom Castle"): | ||
|  |                                         hint.append(overworld_spot.name.split("Overworld - ")[-1].replace("Pazuzu", | ||
|  |                                             "Pazuzu's")) | ||
|  |                                     hint = " - ".join(hint) | ||
|  |                                     if location.address in hint_data[self.player]: | ||
|  |                                         hint_data[self.player][location.address] += f"/{hint}" | ||
|  |                                     else: | ||
|  |                                         hint_data[self.player][location.address] = hint | ||
|  | 
 |