| 
									
										
										
										
											2023-11-26 11:17:59 -05:00
										 |  |  | import Utils | 
					
						
							|  |  |  | import settings | 
					
						
							|  |  |  | import base64 | 
					
						
							|  |  |  | import threading | 
					
						
							|  |  |  | import requests | 
					
						
							|  |  |  | 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 | 
					
						
							| 
									
										
										
										
											2024-07-24 07:46:14 -04:00
										 |  |  | from .Options import FFMQOptions | 
					
						
							| 
									
										
										
										
											2023-11-26 11:17:59 -05:00
										 |  |  | 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): | 
					
						
							| 
									
										
										
										
											2024-07-31 11:40:45 -04:00
										 |  |  |     setup_en = Tutorial( | 
					
						
							| 
									
										
										
										
											2023-11-26 11:17:59 -05:00
										 |  |  |         "Multiworld Setup Guide", | 
					
						
							|  |  |  |         "A guide to playing Final Fantasy Mystic Quest with Archipelago.", | 
					
						
							|  |  |  |         "English", | 
					
						
							|  |  |  |         "setup_en.md", | 
					
						
							|  |  |  |         "setup/en", | 
					
						
							|  |  |  |         ["Alchav"] | 
					
						
							| 
									
										
										
										
											2024-07-31 11:40:45 -04:00
										 |  |  |         ) | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     setup_fr = Tutorial( | 
					
						
							|  |  |  |         setup_en.tutorial_name, | 
					
						
							|  |  |  |         setup_en.description, | 
					
						
							|  |  |  |         "Français", | 
					
						
							|  |  |  |         "setup_fr.md", | 
					
						
							|  |  |  |         "setup/fr", | 
					
						
							|  |  |  |         ["Artea"] | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     tutorials = [setup_en, setup_fr] | 
					
						
							| 
									
										
										
										
											2025-04-04 17:11:45 -04:00
										 |  |  |     game_info_languages = ["en", "fr"] | 
					
						
							| 
									
										
										
										
											2023-11-26 11:17:59 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 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 | 
					
						
							| 
									
										
										
										
											2024-07-24 07:46:14 -04:00
										 |  |  |     options_dataclass = FFMQOptions | 
					
						
							|  |  |  |     options: FFMQOptions | 
					
						
							| 
									
										
										
										
											2023-11-26 11:17:59 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |     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): | 
					
						
							| 
									
										
										
										
											2024-07-24 07:46:14 -04:00
										 |  |  |         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 | 
					
						
							| 
									
										
										
										
											2023-11-26 11:17:59 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |     @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"): | 
					
						
							| 
									
										
										
										
											2024-07-24 07:46:14 -04:00
										 |  |  |             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)) | 
					
						
							| 
									
										
										
										
											2023-11-26 11:17:59 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |                 seed = hex(multiworld.random.randint(0, 0xFFFFFFFF)).split("0x")[1].upper() | 
					
						
							| 
									
										
										
										
											2024-07-24 07:46:14 -04:00
										 |  |  |                 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 | 
					
						
							| 
									
										
										
										
											2023-11-26 11:17:59 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-06 12:24:59 -05:00
										 |  |  |                 query = f"s={seed}&m={map_shuffle}&c={crest_shuffle}&b={battlefield_shuffle}&cs={companion_shuffle}&km={kaeli_mom}" | 
					
						
							| 
									
										
										
										
											2023-11-26 11:17:59 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |                 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: | 
					
						
							| 
									
										
										
										
											2025-04-01 18:19:07 +02:00
										 |  |  |                             world.rooms = rooms_data[query] = Utils.parse_yaml(response.text) | 
					
						
							| 
									
										
										
										
											2023-11-26 11:17:59 -05:00
										 |  |  |                             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): | 
					
						
							| 
									
										
										
										
											2025-01-19 23:06:09 -05:00
										 |  |  |         if not item.advancement: | 
					
						
							|  |  |  |             return None | 
					
						
							| 
									
										
										
										
											2023-11-26 11:17:59 -05:00
										 |  |  |         if "Progressive" in item.name: | 
					
						
							|  |  |  |             i = item.code - 256 | 
					
						
							| 
									
										
										
										
											2025-01-19 23:06:09 -05:00
										 |  |  |             if remove: | 
					
						
							|  |  |  |                 if state.has(self.item_id_to_name[i+1], self.player): | 
					
						
							|  |  |  |                     if state.has(self.item_id_to_name[i+2], 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] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-26 11:17:59 -05:00
										 |  |  |             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] | 
					
						
							| 
									
										
										
										
											2025-01-19 23:06:09 -05:00
										 |  |  |         return item.name | 
					
						
							| 
									
										
										
										
											2023-11-26 11:17:59 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |     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] = {} | 
					
						
							| 
									
										
										
										
											2024-07-24 07:46:14 -04:00
										 |  |  |         if self.options.map_shuffle: | 
					
						
							| 
									
										
										
										
											2023-11-26 11:17:59 -05:00
										 |  |  |             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: | 
					
						
							| 
									
										
										
										
											2024-07-24 07:46:14 -04:00
										 |  |  |                     if location.address and self.options.map_shuffle != "dungeons": | 
					
						
							| 
									
										
										
										
											2023-11-26 11:17:59 -05:00
										 |  |  |                         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 = [] | 
					
						
							| 
									
										
										
										
											2024-07-24 07:46:14 -04:00
										 |  |  |                                     if self.options.map_shuffle != "dungeons": | 
					
						
							| 
									
										
										
										
											2023-11-26 11:17:59 -05:00
										 |  |  |                                         hint.append((subregion.split("Subregion ")[-1] + (" Region" if subregion not | 
					
						
							|  |  |  |                                                     in single_location_regions else ""))) | 
					
						
							| 
									
										
										
										
											2024-07-24 07:46:14 -04:00
										 |  |  |                                     if self.options.map_shuffle != "overworld": | 
					
						
							| 
									
										
										
										
											2023-11-26 11:17:59 -05:00
										 |  |  |                                         hint.append(overworld_spot.name.split("Overworld - ")[-1].replace("Pazuzu", | 
					
						
							|  |  |  |                                             "Pazuzu's")) | 
					
						
							| 
									
										
										
										
											2024-07-24 07:46:14 -04:00
										 |  |  |                                     hint = " - ".join(hint).replace(" - Mac Ship", "") | 
					
						
							| 
									
										
										
										
											2023-11-26 11:17:59 -05:00
										 |  |  |                                     if location.address in hint_data[self.player]: | 
					
						
							|  |  |  |                                         hint_data[self.player][location.address] += f"/{hint}" | 
					
						
							|  |  |  |                                     else: | 
					
						
							|  |  |  |                                         hint_data[self.player][location.address] = hint |