| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  | import copy | 
					
						
							|  |  |  | import collections | 
					
						
							| 
									
										
										
										
											2021-10-10 16:50:01 +02:00
										 |  |  | from itertools import zip_longest, chain | 
					
						
							| 
									
										
										
										
											2017-12-17 00:25:46 -05:00
										 |  |  | import logging | 
					
						
							| 
									
										
										
										
											2019-05-30 01:10:16 +02:00
										 |  |  | import os | 
					
						
							| 
									
										
										
										
											2017-12-17 00:25:46 -05:00
										 |  |  | import time | 
					
						
							| 
									
										
										
										
											2020-01-04 22:08:13 +01:00
										 |  |  | import zlib | 
					
						
							| 
									
										
										
										
											2020-08-21 18:35:48 +02:00
										 |  |  | import concurrent.futures | 
					
						
							| 
									
										
										
										
											2021-01-03 14:32:32 +01:00
										 |  |  | import pickle | 
					
						
							| 
									
										
										
										
											2021-07-21 18:08:15 +02:00
										 |  |  | import tempfile | 
					
						
							|  |  |  | import zipfile | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  | from typing import Dict, Tuple, Optional, Set | 
					
						
							| 
									
										
										
										
											2017-12-17 00:25:46 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-17 06:07:11 +01:00
										 |  |  | from BaseClasses import MultiWorld, CollectionState, Region, RegionType, LocationProgressType, Location | 
					
						
							| 
									
										
										
										
											2021-07-12 13:54:47 +02:00
										 |  |  | from worlds.alttp.Items import item_name_groups | 
					
						
							| 
									
										
										
										
											2021-07-22 15:51:50 +02:00
										 |  |  | from worlds.alttp.Regions import lookup_vanilla_location_to_entrance | 
					
						
							| 
									
										
										
										
											2021-01-04 15:14:20 +01:00
										 |  |  | from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned | 
					
						
							| 
									
										
										
										
											2021-08-30 01:16:04 +02:00
										 |  |  | from worlds.alttp.Shops import SHOP_ID_START, total_shop_slots, FillDisabledShopSlots | 
					
						
							| 
									
										
										
										
											2021-08-09 09:15:41 +02:00
										 |  |  | from Utils import output_path, get_options, __version__, version_tuple | 
					
						
							| 
									
										
										
										
											2021-07-14 08:24:34 -05:00
										 |  |  | from worlds.generic.Rules import locality_rules, exclusion_rules | 
					
						
							| 
									
										
										
										
											2021-07-12 14:32:39 +02:00
										 |  |  | from worlds import AutoWorld | 
					
						
							| 
									
										
										
										
											2017-05-15 20:28:04 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-11 00:46:18 +02:00
										 |  |  | ordered_areas = ( | 
					
						
							|  |  |  |     'Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace', | 
					
						
							|  |  |  |     'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace', | 
					
						
							|  |  |  |     'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total" | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = None): | 
					
						
							|  |  |  |     if not baked_server_options: | 
					
						
							|  |  |  |         baked_server_options = get_options()["server_options"] | 
					
						
							| 
									
										
										
										
											2019-12-28 17:12:27 +01:00
										 |  |  |     if args.outputpath: | 
					
						
							| 
									
										
										
										
											2020-01-10 07:25:16 +01:00
										 |  |  |         os.makedirs(args.outputpath, exist_ok=True) | 
					
						
							| 
									
										
										
										
											2019-12-28 17:12:27 +01:00
										 |  |  |         output_path.cached_path = args.outputpath | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-01-02 12:38:26 +11:00
										 |  |  |     start = time.perf_counter() | 
					
						
							| 
									
										
										
										
											2017-05-15 20:28:04 +02:00
										 |  |  |     # initialize the world | 
					
						
							| 
									
										
										
										
											2021-03-14 08:38:02 +01:00
										 |  |  |     world = MultiWorld(args.multi) | 
					
						
							| 
									
										
										
										
											2020-07-14 07:01:51 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-06 11:32:49 +02:00
										 |  |  |     logger = logging.getLogger() | 
					
						
							| 
									
										
										
										
											2021-10-09 02:30:46 +02:00
										 |  |  |     world.set_seed(seed, args.race, str(args.outputname if args.outputname else world.seed)) | 
					
						
							| 
									
										
										
										
											2017-05-20 14:07:40 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-03-14 08:38:02 +01:00
										 |  |  |     world.shuffle = args.shuffle.copy() | 
					
						
							|  |  |  |     world.logic = args.logic.copy() | 
					
						
							|  |  |  |     world.mode = args.mode.copy() | 
					
						
							|  |  |  |     world.difficulty = args.difficulty.copy() | 
					
						
							|  |  |  |     world.item_functionality = args.item_functionality.copy() | 
					
						
							|  |  |  |     world.timer = args.timer.copy() | 
					
						
							|  |  |  |     world.goal = args.goal.copy() | 
					
						
							| 
									
										
										
										
											2020-09-11 03:23:00 +02:00
										 |  |  |     world.open_pyramid = args.open_pyramid.copy() | 
					
						
							| 
									
										
										
										
											2019-12-17 15:55:53 +01:00
										 |  |  |     world.boss_shuffle = args.shufflebosses.copy() | 
					
						
							|  |  |  |     world.enemy_health = args.enemy_health.copy() | 
					
						
							|  |  |  |     world.enemy_damage = args.enemy_damage.copy() | 
					
						
							| 
									
										
										
										
											2021-11-03 05:34:11 +00:00
										 |  |  |     world.beemizer_total_chance = args.beemizer_total_chance.copy() | 
					
						
							|  |  |  |     world.beemizer_trap_chance = args.beemizer_trap_chance.copy() | 
					
						
							| 
									
										
										
										
											2020-02-02 20:10:56 -05:00
										 |  |  |     world.timer = args.timer.copy() | 
					
						
							| 
									
										
										
										
											2020-10-28 16:20:59 -07:00
										 |  |  |     world.countdown_start_time = args.countdown_start_time.copy() | 
					
						
							|  |  |  |     world.red_clock_time = args.red_clock_time.copy() | 
					
						
							|  |  |  |     world.blue_clock_time = args.blue_clock_time.copy() | 
					
						
							|  |  |  |     world.green_clock_time = args.green_clock_time.copy() | 
					
						
							| 
									
										
										
										
											2020-04-12 15:46:32 -07:00
										 |  |  |     world.dungeon_counters = args.dungeon_counters.copy() | 
					
						
							| 
									
										
										
										
											2020-06-17 01:02:54 -07:00
										 |  |  |     world.triforce_pieces_available = args.triforce_pieces_available.copy() | 
					
						
							| 
									
										
										
										
											2020-06-07 15:22:24 +02:00
										 |  |  |     world.triforce_pieces_required = args.triforce_pieces_required.copy() | 
					
						
							| 
									
										
										
										
											2020-08-23 15:03:06 +02:00
										 |  |  |     world.shop_shuffle = args.shop_shuffle.copy() | 
					
						
							| 
									
										
										
										
											2020-09-20 04:35:45 +02:00
										 |  |  |     world.shuffle_prizes = args.shuffle_prizes.copy() | 
					
						
							| 
									
										
										
										
											2020-10-06 13:22:03 -07:00
										 |  |  |     world.sprite_pool = args.sprite_pool.copy() | 
					
						
							| 
									
										
										
										
											2020-10-07 19:51:46 +02:00
										 |  |  |     world.dark_room_logic = args.dark_room_logic.copy() | 
					
						
							| 
									
										
										
										
											2021-01-02 12:49:43 +01:00
										 |  |  |     world.plando_items = args.plando_items.copy() | 
					
						
							| 
									
										
										
										
											2021-01-02 16:44:58 +01:00
										 |  |  |     world.plando_texts = args.plando_texts.copy() | 
					
						
							| 
									
										
										
										
											2021-01-02 22:41:03 +01:00
										 |  |  |     world.plando_connections = args.plando_connections.copy() | 
					
						
							| 
									
										
										
										
											2021-01-02 23:00:14 +01:00
										 |  |  |     world.required_medallions = args.required_medallions.copy() | 
					
						
							| 
									
										
										
										
											2021-02-21 20:17:24 +01:00
										 |  |  |     world.game = args.game.copy() | 
					
						
							| 
									
										
										
										
											2021-08-09 09:15:41 +02:00
										 |  |  |     world.player_name = args.name.copy() | 
					
						
							|  |  |  |     world.enemizer = args.enemizercli | 
					
						
							|  |  |  |     world.sprite = args.sprite.copy() | 
					
						
							| 
									
										
										
										
											2021-03-22 13:14:19 -07:00
										 |  |  |     world.glitch_triforce = args.glitch_triforce  # This is enabled/disabled globally, no per player option. | 
					
						
							| 
									
										
										
										
											2019-08-11 08:55:38 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-17 06:07:11 +01:00
										 |  |  |     world.set_options(args) | 
					
						
							| 
									
										
										
										
											2022-02-17 07:07:34 +01:00
										 |  |  |     world.set_item_links() | 
					
						
							| 
									
										
										
										
											2022-02-17 06:07:11 +01:00
										 |  |  |     world.state = CollectionState(world) | 
					
						
							| 
									
										
										
										
											2021-01-03 14:32:32 +01:00
										 |  |  |     logger.info('Archipelago Version %s  -  Seed: %s\n', __version__, world.seed) | 
					
						
							| 
									
										
										
										
											2020-01-14 10:42:27 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-11 14:22:44 +02:00
										 |  |  |     logger.info("Found World Types:") | 
					
						
							| 
									
										
										
										
											2021-07-12 15:11:48 +02:00
										 |  |  |     longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types) | 
					
						
							| 
									
										
										
										
											2021-08-16 18:40:26 +02:00
										 |  |  |     numlength = 8 | 
					
						
							| 
									
										
										
										
											2021-06-11 14:22:44 +02:00
										 |  |  |     for name, cls in AutoWorld.AutoWorldRegister.world_types.items(): | 
					
						
							| 
									
										
										
										
											2021-08-27 20:46:23 +02:00
										 |  |  |         if not cls.hidden: | 
					
						
							| 
									
										
										
										
											2021-08-27 14:52:33 +02:00
										 |  |  |             logger.info(f"  {name:{longest_name}}: {len(cls.item_names):3} Items | " | 
					
						
							|  |  |  |                         f"{len(cls.location_names):3} Locations") | 
					
						
							| 
									
										
										
										
											2021-10-17 20:53:06 +02:00
										 |  |  |             logger.info(f"   Item IDs: {min(cls.item_id_to_name):{numlength}} - " | 
					
						
							| 
									
										
										
										
											2021-08-27 14:52:33 +02:00
										 |  |  |                         f"{max(cls.item_id_to_name):{numlength}} | " | 
					
						
							|  |  |  |                         f"Location IDs: {min(cls.location_id_to_name):{numlength}} - " | 
					
						
							|  |  |  |                         f"{max(cls.location_id_to_name):{numlength}}") | 
					
						
							| 
									
										
										
										
											2017-05-20 14:07:40 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-29 20:37:28 -05:00
										 |  |  |     AutoWorld.call_stage(world, "assert_generate") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-28 00:26:02 +02:00
										 |  |  |     AutoWorld.call_all(world, "generate_early") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-01-14 10:42:27 +01:00
										 |  |  |     logger.info('') | 
					
						
							| 
									
										
										
										
											2021-05-22 06:27:22 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-09 21:22:21 +02:00
										 |  |  |     for player in world.player_ids: | 
					
						
							| 
									
										
										
										
											2021-09-23 03:53:16 +02:00
										 |  |  |         for item_name, count in world.start_inventory[player].value.items(): | 
					
						
							|  |  |  |             for _ in range(count): | 
					
						
							|  |  |  |                 world.push_precollected(world.create_item(item_name, player)) | 
					
						
							| 
									
										
										
										
											2017-05-20 14:07:40 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-21 20:17:24 +01:00
										 |  |  |     for player in world.player_ids: | 
					
						
							| 
									
										
										
										
											2021-07-21 18:08:15 +02:00
										 |  |  |         if player in world.get_game_players("A Link to the Past"): | 
					
						
							| 
									
										
										
										
											2021-07-12 15:11:48 +02:00
										 |  |  |             # enforce pre-defined local items. | 
					
						
							|  |  |  |             if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]: | 
					
						
							| 
									
										
										
										
											2021-09-17 00:17:54 +02:00
										 |  |  |                 world.local_items[player].value.add('Triforce Piece') | 
					
						
							| 
									
										
										
										
											2021-01-05 09:56:20 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-12 15:11:48 +02:00
										 |  |  |             # Not possible to place pendants/crystals out side of boss prizes yet. | 
					
						
							| 
									
										
										
										
											2021-09-17 00:17:54 +02:00
										 |  |  |             world.non_local_items[player].value -= item_name_groups['Pendants'] | 
					
						
							|  |  |  |             world.non_local_items[player].value -= item_name_groups['Crystals'] | 
					
						
							| 
									
										
										
										
											2020-12-19 15:36:29 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-12 15:11:48 +02:00
										 |  |  |         # items can't be both local and non-local, prefer local | 
					
						
							| 
									
										
										
										
											2021-09-17 00:17:54 +02:00
										 |  |  |         world.non_local_items[player].value -= world.local_items[player].value | 
					
						
							| 
									
										
										
										
											2020-01-06 19:13:42 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-22 15:51:50 +02:00
										 |  |  |     logger.info('Creating World.') | 
					
						
							| 
									
										
										
										
											2021-06-11 14:22:44 +02:00
										 |  |  |     AutoWorld.call_all(world, "create_regions") | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-22 15:51:50 +02:00
										 |  |  |     logger.info('Creating Items.') | 
					
						
							|  |  |  |     AutoWorld.call_all(world, "create_items") | 
					
						
							| 
									
										
										
										
											2019-04-18 16:11:11 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-05-20 14:07:40 +02:00
										 |  |  |     logger.info('Calculating Access Rules.') | 
					
						
							| 
									
										
										
										
											2021-02-24 00:36:37 +01:00
										 |  |  |     if world.players > 1: | 
					
						
							|  |  |  |         for player in world.player_ids: | 
					
						
							|  |  |  |             locality_rules(world, player) | 
					
						
							| 
									
										
										
										
											2021-09-15 01:02:06 +02:00
										 |  |  |     else: | 
					
						
							| 
									
										
										
										
											2021-09-17 00:17:54 +02:00
										 |  |  |         world.non_local_items[1].value = set() | 
					
						
							|  |  |  |         world.local_items[1].value = set() | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-11 18:02:48 +02:00
										 |  |  |     AutoWorld.call_all(world, "set_rules") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-14 08:24:34 -05:00
										 |  |  |     for player in world.player_ids: | 
					
						
							| 
									
										
										
										
											2021-09-17 00:17:54 +02:00
										 |  |  |         exclusion_rules(world, player, world.exclude_locations[player].value) | 
					
						
							| 
									
										
										
										
											2022-02-01 16:36:14 +01:00
										 |  |  |         world.priority_locations[player].value -= world.exclude_locations[player].value | 
					
						
							|  |  |  |         for location_name in world.priority_locations[player].value: | 
					
						
							|  |  |  |             world.get_location(location_name, player).progress_type = LocationProgressType.PRIORITY | 
					
						
							| 
									
										
										
										
											2021-07-14 08:24:34 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-11 14:22:44 +02:00
										 |  |  |     AutoWorld.call_all(world, "generate_basic") | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  |     # temporary home for item links, should be moved out of Main | 
					
						
							| 
									
										
										
										
											2022-02-17 06:07:11 +01:00
										 |  |  |     for group_id, group in world.groups.items(): | 
					
						
							|  |  |  |         def find_common_pool(players: Set[int], shared_pool: Set[str]): | 
					
						
							|  |  |  |             advancement = set() | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  |             counters = {player: {name: 0 for name in shared_pool} for player in players} | 
					
						
							|  |  |  |             for item in world.itempool: | 
					
						
							|  |  |  |                 if item.player in counters and item.name in shared_pool: | 
					
						
							|  |  |  |                     counters[item.player][item.name] += 1 | 
					
						
							| 
									
										
										
										
											2022-02-17 06:07:11 +01:00
										 |  |  |                     if item.advancement: | 
					
						
							|  |  |  |                         advancement.add(item.name) | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-22 16:35:41 -08:00
										 |  |  |             for player in players.copy(): | 
					
						
							|  |  |  |                 if all([counters[player][item] == 0 for item in shared_pool]): | 
					
						
							|  |  |  |                     players.remove(player) | 
					
						
							|  |  |  |                     del(counters[player]) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if not players: | 
					
						
							|  |  |  |                 return None, None | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  |             for item in shared_pool: | 
					
						
							|  |  |  |                 count = min(counters[player][item] for player in players) | 
					
						
							|  |  |  |                 if count: | 
					
						
							|  |  |  |                     for player in players: | 
					
						
							|  |  |  |                         counters[player][item] = count | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     for player in players: | 
					
						
							|  |  |  |                         del(counters[player][item]) | 
					
						
							| 
									
										
										
										
											2022-02-17 06:07:11 +01:00
										 |  |  |             return counters, advancement | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-17 06:07:11 +01:00
										 |  |  |         common_item_count, common_advancement_items = find_common_pool(group["players"], group["item_pool"]) | 
					
						
							| 
									
										
										
										
											2022-02-22 16:35:41 -08:00
										 |  |  |         if not common_item_count: | 
					
						
							|  |  |  |             continue | 
					
						
							| 
									
										
										
										
											2022-02-22 10:14:26 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  |         new_itempool = [] | 
					
						
							|  |  |  |         for item_name, item_count in next(iter(common_item_count.values())).items(): | 
					
						
							| 
									
										
										
										
											2022-02-17 06:07:11 +01:00
										 |  |  |             advancement = item_name in common_advancement_items | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  |             for _ in range(item_count): | 
					
						
							| 
									
										
										
										
											2022-02-17 06:07:11 +01:00
										 |  |  |                 new_item = group["world"].create_item(item_name) | 
					
						
							|  |  |  |                 new_item.advancement = advancement | 
					
						
							|  |  |  |                 new_itempool.append(new_item) | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-17 06:07:11 +01:00
										 |  |  |         region = Region("Menu", RegionType.Generic, "ItemLink", group_id, world) | 
					
						
							|  |  |  |         world.regions.append(region) | 
					
						
							|  |  |  |         locations = region.locations = [] | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  |         for item in world.itempool: | 
					
						
							| 
									
										
										
										
											2022-02-17 06:07:11 +01:00
										 |  |  |             count = common_item_count.get(item.player, {}).get(item.name, 0) | 
					
						
							|  |  |  |             if count: | 
					
						
							|  |  |  |                 loc = Location(group_id, f"Item Link: {item.name} -> {world.player_name[item.player]} {count}", | 
					
						
							|  |  |  |                                None, region) | 
					
						
							| 
									
										
										
										
											2022-02-22 10:14:26 +01:00
										 |  |  |                 loc.access_rule = lambda state, item_name = item.name, group_id_ = group_id, count_ = count: \ | 
					
						
							|  |  |  |                     state.has(item_name, group_id_, count_) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-17 06:07:11 +01:00
										 |  |  |                 locations.append(loc) | 
					
						
							|  |  |  |                 loc.place_locked_item(item) | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  |                 common_item_count[item.player][item.name] -= 1 | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 new_itempool.append(item) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         itemcount = len(world.itempool) | 
					
						
							|  |  |  |         world.itempool = new_itempool | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         while itemcount > len(world.itempool): | 
					
						
							| 
									
										
										
										
											2022-03-20 11:07:51 -04:00
										 |  |  |             items_to_add = [] | 
					
						
							| 
									
										
										
										
											2022-02-05 20:15:56 +01:00
										 |  |  |             for player in group["players"]: | 
					
						
							| 
									
										
										
										
											2022-02-17 06:07:11 +01:00
										 |  |  |                 if group["replacement_items"][player]: | 
					
						
							| 
									
										
										
										
											2022-03-20 11:07:51 -04:00
										 |  |  |                     items_to_add.append(AutoWorld.call_single(world, "create_item", player, | 
					
						
							| 
									
										
										
										
											2022-02-17 06:07:11 +01:00
										 |  |  |                                                                 group["replacement_items"][player])) | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  |                 else: | 
					
						
							| 
									
										
										
										
											2022-03-20 11:07:51 -04:00
										 |  |  |                     items_to_add.append(AutoWorld.call_single(world, "create_filler", player)) | 
					
						
							|  |  |  |             world.random.shuffle(items_to_add) | 
					
						
							|  |  |  |             world.itempool.extend(items_to_add[:itemcount - len(world.itempool)]) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-17 06:07:11 +01:00
										 |  |  |     if any(world.item_links.values()): | 
					
						
							|  |  |  |         world._recache() | 
					
						
							|  |  |  |         world._all_state = None | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-02 12:49:43 +01:00
										 |  |  |     logger.info("Running Item Plando") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-03-21 00:47:17 +01:00
										 |  |  |     for item in world.itempool: | 
					
						
							|  |  |  |         item.world = world | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-04 15:14:20 +01:00
										 |  |  |     distribute_planned(world) | 
					
						
							| 
									
										
										
										
											2021-01-02 12:49:43 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-09 06:50:11 +02:00
										 |  |  |     logger.info('Running Pre Main Fill.') | 
					
						
							| 
									
										
										
										
											2021-01-24 08:26:39 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-09 06:50:11 +02:00
										 |  |  |     AutoWorld.call_all(world, "pre_fill") | 
					
						
							| 
									
										
										
										
											2017-05-20 14:07:40 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-17 20:53:06 +02:00
										 |  |  |     logger.info(f'Filling the world with {len(world.itempool)} items.') | 
					
						
							| 
									
										
										
										
											2017-05-20 14:07:40 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-03-14 08:38:02 +01:00
										 |  |  |     if world.algorithm == 'flood': | 
					
						
							| 
									
										
										
										
											2017-05-20 14:07:40 +02:00
										 |  |  |         flood_items(world)  # different algo, biased towards early game progress items | 
					
						
							| 
									
										
										
										
											2021-03-14 08:38:02 +01:00
										 |  |  |     elif world.algorithm == 'balanced': | 
					
						
							| 
									
										
										
										
											2021-08-10 09:03:44 +02:00
										 |  |  |         distribute_items_restrictive(world) | 
					
						
							| 
									
										
										
										
											2017-06-03 21:28:02 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-30 01:16:04 +02:00
										 |  |  |     AutoWorld.call_all(world, 'post_fill') | 
					
						
							| 
									
										
										
										
											2020-12-23 15:30:21 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-05 08:07:12 +01:00
										 |  |  |     if world.players > 1: | 
					
						
							|  |  |  |         balance_multiworld_progression(world) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-03 17:30:10 +02:00
										 |  |  |     logger.info(f'Beginning output...') | 
					
						
							| 
									
										
										
										
											2021-05-16 00:21:00 +02:00
										 |  |  |     outfilebase = 'AP_' + world.seed_name | 
					
						
							| 
									
										
										
										
											2020-03-06 23:08:46 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-21 18:08:15 +02:00
										 |  |  |     output = tempfile.TemporaryDirectory() | 
					
						
							|  |  |  |     with output as temp_dir: | 
					
						
							| 
									
										
										
										
											2021-10-11 00:46:18 +02:00
										 |  |  |         with concurrent.futures.ThreadPoolExecutor(world.players + 2) as pool: | 
					
						
							| 
									
										
										
										
											2021-09-03 20:35:40 +02:00
										 |  |  |             check_accessibility_task = pool.submit(world.fulfills_accessibility) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-10 16:50:01 +02:00
										 |  |  |             output_file_futures = [pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir)] | 
					
						
							| 
									
										
										
										
											2021-09-03 20:35:40 +02:00
										 |  |  |             for player in world.player_ids: | 
					
						
							|  |  |  |                 # skip starting a thread for methods that say "pass". | 
					
						
							|  |  |  |                 if AutoWorld.World.generate_output.__code__ is not world.worlds[player].generate_output.__code__: | 
					
						
							| 
									
										
										
										
											2021-10-11 00:46:18 +02:00
										 |  |  |                     output_file_futures.append( | 
					
						
							|  |  |  |                         pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir)) | 
					
						
							| 
									
										
										
										
											2021-09-03 20:35:40 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |             def get_entrance_to_region(region: Region): | 
					
						
							|  |  |  |                 for entrance in region.entrances: | 
					
						
							|  |  |  |                     if entrance.parent_region.type in (RegionType.DarkWorld, RegionType.LightWorld, RegionType.Generic): | 
					
						
							|  |  |  |                         return entrance | 
					
						
							|  |  |  |                 for entrance in region.entrances:  # BFS might be better here, trying DFS for now. | 
					
						
							|  |  |  |                     return get_entrance_to_region(entrance.parent_region) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             # collect ER hint info | 
					
						
							|  |  |  |             er_hint_data = {player: {} for player in world.get_game_players("A Link to the Past") if | 
					
						
							|  |  |  |                             world.shuffle[player] != "vanilla" or world.retro[player]} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             for region in world.regions: | 
					
						
							|  |  |  |                 if region.player in er_hint_data and region.locations: | 
					
						
							|  |  |  |                     main_entrance = get_entrance_to_region(region) | 
					
						
							|  |  |  |                     for location in region.locations: | 
					
						
							|  |  |  |                         if type(location.address) == int:  # skips events and crystals | 
					
						
							|  |  |  |                             if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name: | 
					
						
							|  |  |  |                                 er_hint_data[region.player][location.address] = main_entrance.name | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             checks_in_area = {player: {area: list() for area in ordered_areas} | 
					
						
							|  |  |  |                               for player in range(1, world.players + 1)} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             for player in range(1, world.players + 1): | 
					
						
							|  |  |  |                 checks_in_area[player]["Total"] = 0 | 
					
						
							| 
									
										
										
										
											2021-08-09 09:15:41 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-21 18:08:15 +02:00
										 |  |  |             for location in world.get_filled_locations(): | 
					
						
							| 
									
										
										
										
											2021-09-03 20:35:40 +02:00
										 |  |  |                 if type(location.address) is int: | 
					
						
							|  |  |  |                     main_entrance = get_entrance_to_region(location.parent_region) | 
					
						
							|  |  |  |                     if location.game != "A Link to the Past": | 
					
						
							|  |  |  |                         checks_in_area[location.player]["Light World"].append(location.address) | 
					
						
							|  |  |  |                     elif location.parent_region.dungeon: | 
					
						
							|  |  |  |                         dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower', | 
					
						
							|  |  |  |                                        'Inverted Ganons Tower': 'Ganons Tower'} \ | 
					
						
							|  |  |  |                             .get(location.parent_region.dungeon.name, location.parent_region.dungeon.name) | 
					
						
							|  |  |  |                         checks_in_area[location.player][dungeonname].append(location.address) | 
					
						
							| 
									
										
										
										
											2021-11-23 22:47:41 +01:00
										 |  |  |                     elif location.parent_region.type == RegionType.LightWorld: | 
					
						
							|  |  |  |                         checks_in_area[location.player]["Light World"].append(location.address) | 
					
						
							|  |  |  |                     elif location.parent_region.type == RegionType.DarkWorld: | 
					
						
							|  |  |  |                         checks_in_area[location.player]["Dark World"].append(location.address) | 
					
						
							| 
									
										
										
										
											2021-09-03 20:35:40 +02:00
										 |  |  |                     elif main_entrance.parent_region.type == RegionType.LightWorld: | 
					
						
							|  |  |  |                         checks_in_area[location.player]["Light World"].append(location.address) | 
					
						
							|  |  |  |                     elif main_entrance.parent_region.type == RegionType.DarkWorld: | 
					
						
							|  |  |  |                         checks_in_area[location.player]["Dark World"].append(location.address) | 
					
						
							|  |  |  |                     checks_in_area[location.player]["Total"] += 1 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             oldmancaves = [] | 
					
						
							|  |  |  |             takeanyregions = ["Old Man Sword Cave", "Take-Any #1", "Take-Any #2", "Take-Any #3", "Take-Any #4"] | 
					
						
							|  |  |  |             for index, take_any in enumerate(takeanyregions): | 
					
						
							| 
									
										
										
										
											2021-09-13 03:38:18 +02:00
										 |  |  |                 for region in [world.get_region(take_any, player) for player in | 
					
						
							|  |  |  |                                world.get_game_players("A Link to the Past") if world.retro[player]]: | 
					
						
							| 
									
										
										
										
											2021-10-11 00:46:18 +02:00
										 |  |  |                     item = world.create_item( | 
					
						
							|  |  |  |                         region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'], | 
					
						
							|  |  |  |                         region.player) | 
					
						
							| 
									
										
										
										
											2021-09-03 20:35:40 +02:00
										 |  |  |                     player = region.player | 
					
						
							|  |  |  |                     location_id = SHOP_ID_START + total_shop_slots + index | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                     main_entrance = get_entrance_to_region(region) | 
					
						
							|  |  |  |                     if main_entrance.parent_region.type == RegionType.LightWorld: | 
					
						
							|  |  |  |                         checks_in_area[player]["Light World"].append(location_id) | 
					
						
							|  |  |  |                     else: | 
					
						
							|  |  |  |                         checks_in_area[player]["Dark World"].append(location_id) | 
					
						
							|  |  |  |                     checks_in_area[player]["Total"] += 1 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                     er_hint_data[player][location_id] = main_entrance.name | 
					
						
							|  |  |  |                     oldmancaves.append(((location_id, player), (item.code, player))) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             FillDisabledShopSlots(world) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             def write_multidata(): | 
					
						
							|  |  |  |                 import NetUtils | 
					
						
							|  |  |  |                 slot_data = {} | 
					
						
							|  |  |  |                 client_versions = {} | 
					
						
							|  |  |  |                 games = {} | 
					
						
							| 
									
										
										
										
											2022-04-08 11:16:36 +02:00
										 |  |  |                 minimum_versions = {"server": AutoWorld.World.required_server_version, "clients": client_versions} | 
					
						
							| 
									
										
										
										
											2022-01-30 13:57:12 +01:00
										 |  |  |                 slot_info = {} | 
					
						
							|  |  |  |                 names = [[name for player, name in sorted(world.player_name.items())]] | 
					
						
							| 
									
										
										
										
											2021-09-03 20:35:40 +02:00
										 |  |  |                 for slot in world.player_ids: | 
					
						
							| 
									
										
										
										
											2022-04-08 11:16:36 +02:00
										 |  |  |                     player_world: AutoWorld.World = world.worlds[slot] | 
					
						
							|  |  |  |                     minimum_versions["server"] = max(minimum_versions["server"], player_world.required_server_version) | 
					
						
							|  |  |  |                     client_versions[slot] = player_world.required_client_version | 
					
						
							| 
									
										
										
										
											2021-09-03 20:35:40 +02:00
										 |  |  |                     games[slot] = world.game[slot] | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  |                     slot_info[slot] = NetUtils.NetworkSlot(names[0][slot - 1], world.game[slot], | 
					
						
							|  |  |  |                                                            world.player_types[slot]) | 
					
						
							|  |  |  |                 for slot, group in world.groups.items(): | 
					
						
							|  |  |  |                     games[slot] = world.game[slot] | 
					
						
							|  |  |  |                     slot_info[slot] = NetUtils.NetworkSlot(group["name"], world.game[slot], world.player_types[slot], | 
					
						
							|  |  |  |                                                            group_members=sorted(group["players"])) | 
					
						
							| 
									
										
										
										
											2022-03-18 18:19:21 +01:00
										 |  |  |                 precollected_items = {player: [item.code for item in world_precollected if type(item.code) == int] | 
					
						
							| 
									
										
										
										
											2021-10-10 16:50:01 +02:00
										 |  |  |                                       for player, world_precollected in world.precollected_items.items()} | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  |                 precollected_hints = {player: set() for player in range(1, world.players + 1 + len(world.groups))} | 
					
						
							| 
									
										
										
										
											2022-01-14 19:27:44 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-03 20:35:40 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |                 for slot in world.player_ids: | 
					
						
							|  |  |  |                     slot_data[slot] = world.worlds[slot].fill_slot_data() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-22 12:49:43 -08:00
										 |  |  |                 def precollect_hint(location): | 
					
						
							| 
									
										
										
										
											2022-02-19 10:52:05 -08:00
										 |  |  |                     entrance = er_hint_data.get(location.player, {}).get(location.address, "") | 
					
						
							| 
									
										
										
										
											2021-10-03 14:40:25 +02:00
										 |  |  |                     hint = NetUtils.Hint(location.item.player, location.player, location.address, | 
					
						
							| 
									
										
										
										
											2022-02-19 10:52:05 -08:00
										 |  |  |                                          location.item.code, False, entrance, location.item.flags) | 
					
						
							| 
									
										
										
										
											2021-10-03 14:40:25 +02:00
										 |  |  |                     precollected_hints[location.player].add(hint) | 
					
						
							| 
									
										
										
										
											2022-02-22 12:49:43 -08:00
										 |  |  |                     if location.item.player not in world.groups: | 
					
						
							| 
									
										
										
										
											2022-02-21 15:33:39 -08:00
										 |  |  |                         precollected_hints[location.item.player].add(hint) | 
					
						
							|  |  |  |                     else: | 
					
						
							| 
									
										
										
										
											2022-02-22 12:49:43 -08:00
										 |  |  |                         for player in world.groups[location.item.player]["players"]: | 
					
						
							| 
									
										
										
										
											2022-02-21 15:33:39 -08:00
										 |  |  |                             precollected_hints[player].add(hint) | 
					
						
							| 
									
										
										
										
											2021-10-03 14:40:25 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-18 05:52:29 +01:00
										 |  |  |                 locations_data: Dict[int, Dict[int, Tuple[int, int, int]]] = {player: {} for player in world.player_ids} | 
					
						
							| 
									
										
										
										
											2021-09-03 20:35:40 +02:00
										 |  |  |                 for location in world.get_filled_locations(): | 
					
						
							|  |  |  |                     if type(location.address) == int: | 
					
						
							| 
									
										
										
										
											2022-03-26 01:12:54 +01:00
										 |  |  |                         assert location.item.code is not None, "item code None should be event, " \ | 
					
						
							|  |  |  |                                                                "location.address should then also be None" | 
					
						
							| 
									
										
										
										
											2022-01-18 05:52:29 +01:00
										 |  |  |                         locations_data[location.player][location.address] = \ | 
					
						
							| 
									
										
										
										
											2022-01-18 06:16:16 +01:00
										 |  |  |                             location.item.code, location.item.player, location.item.flags | 
					
						
							| 
									
										
										
										
											2022-03-24 17:15:52 +01:00
										 |  |  |                         if location.name in world.start_location_hints[location.player]: | 
					
						
							| 
									
										
										
										
											2021-10-03 14:40:25 +02:00
										 |  |  |                             precollect_hint(location) | 
					
						
							| 
									
										
										
										
											2021-09-30 19:49:36 +02:00
										 |  |  |                         elif location.item.name in world.start_hints[location.item.player]: | 
					
						
							| 
									
										
										
										
											2021-10-03 14:40:25 +02:00
										 |  |  |                             precollect_hint(location) | 
					
						
							| 
									
										
										
										
											2022-02-22 12:49:43 -08:00
										 |  |  |                         elif any([location.item.name in world.start_hints[player] | 
					
						
							|  |  |  |                                   for player in world.groups.get(location.item.player, {}).get("players", [])]): | 
					
						
							|  |  |  |                             precollect_hint(location) | 
					
						
							| 
									
										
										
										
											2021-09-03 20:35:40 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |                 multidata = { | 
					
						
							|  |  |  |                     "slot_data": slot_data, | 
					
						
							| 
									
										
										
										
											2022-01-30 13:57:12 +01:00
										 |  |  |                     "slot_info": slot_info, | 
					
						
							|  |  |  |                     "names": names,  # TODO: remove around 0.2.5 in favor of slot_info | 
					
						
							|  |  |  |                     "games": games,  # TODO: remove around 0.2.5 in favor of slot_info | 
					
						
							| 
									
										
										
										
											2021-09-03 20:35:40 +02:00
										 |  |  |                     "connect_names": {name: (0, player) for player, name in world.player_name.items()}, | 
					
						
							|  |  |  |                     "remote_items": {player for player in world.player_ids if | 
					
						
							|  |  |  |                                      world.worlds[player].remote_items}, | 
					
						
							| 
									
										
										
										
											2021-09-23 03:48:37 +02:00
										 |  |  |                     "remote_start_inventory": {player for player in world.player_ids if | 
					
						
							|  |  |  |                                                world.worlds[player].remote_start_inventory}, | 
					
						
							| 
									
										
										
										
											2021-09-03 20:35:40 +02:00
										 |  |  |                     "locations": locations_data, | 
					
						
							|  |  |  |                     "checks_in_area": checks_in_area, | 
					
						
							| 
									
										
										
										
											2021-10-11 00:46:18 +02:00
										 |  |  |                     "server_options": baked_server_options, | 
					
						
							| 
									
										
										
										
											2021-09-03 20:35:40 +02:00
										 |  |  |                     "er_hint_data": er_hint_data, | 
					
						
							|  |  |  |                     "precollected_items": precollected_items, | 
					
						
							|  |  |  |                     "precollected_hints": precollected_hints, | 
					
						
							|  |  |  |                     "version": tuple(version_tuple), | 
					
						
							|  |  |  |                     "tags": ["AP"], | 
					
						
							|  |  |  |                     "minimum_versions": minimum_versions, | 
					
						
							|  |  |  |                     "seed_name": world.seed_name | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |                 AutoWorld.call_all(world, "modify_multidata", multidata) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 multidata = zlib.compress(pickle.dumps(multidata), 9) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 with open(os.path.join(temp_dir, f'{outfilebase}.archipelago'), 'wb') as f: | 
					
						
							| 
									
										
										
										
											2022-02-02 16:29:29 +01:00
										 |  |  |                     f.write(bytes([3]))  # version of format | 
					
						
							| 
									
										
										
										
											2021-09-03 20:35:40 +02:00
										 |  |  |                     f.write(multidata) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             multidata_task = pool.submit(write_multidata) | 
					
						
							|  |  |  |             if not check_accessibility_task.result(): | 
					
						
							|  |  |  |                 if not world.can_beat_game(): | 
					
						
							|  |  |  |                     raise Exception("Game appears as unbeatable. Aborting.") | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     logger.warning("Location Accessibility requirements not fulfilled.") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  |             # retrieve exceptions via .result() if they occurred. | 
					
						
							| 
									
										
										
										
											2021-11-30 05:33:56 +01:00
										 |  |  |             multidata_task.result() | 
					
						
							| 
									
										
										
										
											2021-09-13 03:38:18 +02:00
										 |  |  |             for i, future in enumerate(concurrent.futures.as_completed(output_file_futures), start=1): | 
					
						
							|  |  |  |                 if i % 10 == 0 or i == len(output_file_futures): | 
					
						
							| 
									
										
										
										
											2021-09-03 20:35:40 +02:00
										 |  |  |                     logger.info(f'Generating output files ({i}/{len(output_file_futures)}).') | 
					
						
							|  |  |  |                 future.result() | 
					
						
							| 
									
										
										
										
											2021-08-27 14:52:33 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-13 01:32:32 +02:00
										 |  |  |         if args.spoiler > 1: | 
					
						
							| 
									
										
										
										
											2021-07-21 18:08:15 +02:00
										 |  |  |             logger.info('Calculating playthrough.') | 
					
						
							|  |  |  |             create_playthrough(world) | 
					
						
							| 
									
										
										
										
											2021-08-27 14:52:33 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-13 01:32:32 +02:00
										 |  |  |         if args.spoiler: | 
					
						
							| 
									
										
										
										
											2021-07-21 18:08:15 +02:00
										 |  |  |             world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase)) | 
					
						
							| 
									
										
										
										
											2021-08-27 14:52:33 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-25 16:15:51 +02:00
										 |  |  |         zipfilename = output_path(f"AP_{world.seed_name}.zip") | 
					
						
							|  |  |  |         logger.info(f'Creating final archive at {zipfilename}.') | 
					
						
							|  |  |  |         with zipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED, | 
					
						
							| 
									
										
										
										
											2021-07-21 18:08:15 +02:00
										 |  |  |                              compresslevel=9) as zf: | 
					
						
							|  |  |  |             for file in os.scandir(temp_dir): | 
					
						
							| 
									
										
										
										
											2021-08-27 14:52:33 +02:00
										 |  |  |                 zf.write(file.path, arcname=file.name) | 
					
						
							| 
									
										
										
										
											2019-04-18 11:23:24 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-21 18:35:48 +02:00
										 |  |  |     logger.info('Done. Enjoy. Total Time: %s', time.perf_counter() - start) | 
					
						
							| 
									
										
										
										
											2017-05-15 20:28:04 +02:00
										 |  |  |     return world | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-03-22 23:18:40 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-05-16 21:23:47 +02:00
										 |  |  | def create_playthrough(world): | 
					
						
							| 
									
										
										
										
											2021-04-08 19:53:24 +02:00
										 |  |  |     """Destructive to the world while it is run, damage gets repaired afterwards.""" | 
					
						
							| 
									
										
										
										
											2017-05-16 21:23:47 +02:00
										 |  |  |     # get locations containing progress items | 
					
						
							| 
									
										
										
										
											2021-02-27 17:11:54 +01:00
										 |  |  |     prog_locations = {location for location in world.get_filled_locations() if location.item.advancement} | 
					
						
							| 
									
										
										
										
											2018-01-01 15:55:13 -05:00
										 |  |  |     state_cache = [None] | 
					
						
							| 
									
										
										
										
											2017-05-16 21:23:47 +02:00
										 |  |  |     collection_spheres = [] | 
					
						
							|  |  |  |     state = CollectionState(world) | 
					
						
							| 
									
										
										
										
											2021-02-27 17:11:54 +01:00
										 |  |  |     sphere_candidates = set(prog_locations) | 
					
						
							| 
									
										
										
										
											2020-08-14 00:34:41 +02:00
										 |  |  |     logging.debug('Building up collection spheres.') | 
					
						
							| 
									
										
										
										
											2017-05-16 21:23:47 +02:00
										 |  |  |     while sphere_candidates: | 
					
						
							| 
									
										
										
										
											2017-06-24 11:11:56 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-21 18:08:15 +02:00
										 |  |  |         # build up spheres of collection radius. | 
					
						
							|  |  |  |         # Everything in each sphere is independent from each other in dependencies and only depends on lower spheres | 
					
						
							| 
									
										
										
										
											2021-02-27 17:11:54 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |         sphere = {location for location in sphere_candidates if state.can_reach(location)} | 
					
						
							| 
									
										
										
										
											2017-05-16 21:23:47 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         for location in sphere: | 
					
						
							| 
									
										
										
										
											2018-01-01 15:55:13 -05:00
										 |  |  |             state.collect(location.item, True, location) | 
					
						
							| 
									
										
										
										
											2017-06-17 14:40:37 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-27 17:11:54 +01:00
										 |  |  |         sphere_candidates -= sphere | 
					
						
							| 
									
										
										
										
											2017-05-16 21:23:47 +02:00
										 |  |  |         collection_spheres.append(sphere) | 
					
						
							| 
									
										
										
										
											2018-01-01 15:55:13 -05:00
										 |  |  |         state_cache.append(state.copy()) | 
					
						
							| 
									
										
										
										
											2017-05-26 09:55:24 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-14 00:34:41 +02:00
										 |  |  |         logging.debug('Calculated sphere %i, containing %i of %i progress items.', len(collection_spheres), len(sphere), | 
					
						
							|  |  |  |                       len(prog_locations)) | 
					
						
							| 
									
										
										
										
											2017-05-26 09:55:24 +02:00
										 |  |  |         if not sphere: | 
					
						
							| 
									
										
										
										
											2020-08-14 00:34:41 +02:00
										 |  |  |             logging.debug('The following items could not be reached: %s', ['%s (Player %d) at %s (Player %d)' % ( | 
					
						
							|  |  |  |                 location.item.name, location.item.player, location.name, location.player) for location in | 
					
						
							|  |  |  |                                                                            sphere_candidates]) | 
					
						
							| 
									
										
										
										
											2021-09-17 00:17:54 +02:00
										 |  |  |             if any([world.accessibility[location.item.player] != 'minimal' for location in sphere_candidates]): | 
					
						
							| 
									
										
										
										
											2020-08-14 00:34:41 +02:00
										 |  |  |                 raise RuntimeError(f'Not all progression items reachable ({sphere_candidates}). ' | 
					
						
							|  |  |  |                                    f'Something went terribly wrong here.') | 
					
						
							| 
									
										
										
										
											2017-06-23 22:15:29 +02:00
										 |  |  |             else: | 
					
						
							| 
									
										
										
										
											2021-03-17 10:53:40 +01:00
										 |  |  |                 world.spoiler.unreachables = sphere_candidates | 
					
						
							| 
									
										
										
										
											2017-06-23 22:15:29 +02:00
										 |  |  |                 break | 
					
						
							| 
									
										
										
										
											2021-03-17 11:46:44 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     # in the second phase, we cull each sphere such that the game is still beatable, | 
					
						
							|  |  |  |     # reducing each range of influence to the bare minimum required inside it | 
					
						
							| 
									
										
										
										
											2021-03-17 10:53:40 +01:00
										 |  |  |     restore_later = {} | 
					
						
							| 
									
										
										
										
											2021-02-03 07:14:53 +01:00
										 |  |  |     for num, sphere in reversed(tuple(enumerate(collection_spheres))): | 
					
						
							| 
									
										
										
										
											2021-02-03 06:55:08 +01:00
										 |  |  |         to_delete = set() | 
					
						
							| 
									
										
										
										
											2017-05-16 21:23:47 +02:00
										 |  |  |         for location in sphere: | 
					
						
							|  |  |  |             # we remove the item at location and check if game is still beatable | 
					
						
							| 
									
										
										
										
											2021-06-11 14:22:44 +02:00
										 |  |  |             logging.debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, | 
					
						
							|  |  |  |                           location.item.player) | 
					
						
							| 
									
										
										
										
											2017-05-16 21:23:47 +02:00
										 |  |  |             old_item = location.item | 
					
						
							|  |  |  |             location.item = None | 
					
						
							| 
									
										
										
										
											2019-07-11 00:12:09 -04:00
										 |  |  |             if world.can_beat_game(state_cache[num]): | 
					
						
							| 
									
										
										
										
											2021-02-03 06:55:08 +01:00
										 |  |  |                 to_delete.add(location) | 
					
						
							| 
									
										
										
										
											2021-03-17 10:53:40 +01:00
										 |  |  |                 restore_later[location] = old_item | 
					
						
							| 
									
										
										
										
											2017-05-16 21:23:47 +02:00
										 |  |  |             else: | 
					
						
							|  |  |  |                 # still required, got to keep it around | 
					
						
							|  |  |  |                 location.item = old_item | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # cull entries in spheres for spoiler walkthrough at end | 
					
						
							| 
									
										
										
										
											2021-02-03 06:55:08 +01:00
										 |  |  |         sphere -= to_delete | 
					
						
							| 
									
										
										
										
											2017-05-16 21:23:47 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-01-09 08:31:49 +01:00
										 |  |  |     # second phase, sphere 0 | 
					
						
							| 
									
										
										
										
											2021-03-17 11:46:44 +01:00
										 |  |  |     removed_precollected = [] | 
					
						
							| 
									
										
										
										
											2021-10-11 00:12:00 +02:00
										 |  |  |     for item in (i for i in chain.from_iterable(world.precollected_items.values()) if i.advancement): | 
					
						
							| 
									
										
										
										
											2021-02-27 17:11:54 +01:00
										 |  |  |         logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player) | 
					
						
							| 
									
										
										
										
											2021-10-11 01:39:25 +02:00
										 |  |  |         world.precollected_items[item.player].remove(item) | 
					
						
							| 
									
										
										
										
											2020-01-09 08:31:49 +01:00
										 |  |  |         world.state.remove(item) | 
					
						
							|  |  |  |         if not world.can_beat_game(): | 
					
						
							|  |  |  |             world.push_precollected(item) | 
					
						
							| 
									
										
										
										
											2021-03-17 11:46:44 +01:00
										 |  |  |         else: | 
					
						
							|  |  |  |             removed_precollected.append(item) | 
					
						
							| 
									
										
										
										
											2020-01-09 08:31:49 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-01-06 14:25:49 -05:00
										 |  |  |     # we are now down to just the required progress items in collection_spheres. Unfortunately | 
					
						
							|  |  |  |     # the previous pruning stage could potentially have made certain items dependant on others | 
					
						
							|  |  |  |     # in the same or later sphere (because the location had 2 ways to access but the item originally | 
					
						
							|  |  |  |     # used to access it was deemed not required.) So we need to do one final sphere collection pass | 
					
						
							|  |  |  |     # to build up the correct spheres | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-03 06:55:08 +01:00
										 |  |  |     required_locations = {item for sphere in collection_spheres for item in sphere} | 
					
						
							| 
									
										
										
										
											2018-01-06 14:25:49 -05:00
										 |  |  |     state = CollectionState(world) | 
					
						
							|  |  |  |     collection_spheres = [] | 
					
						
							|  |  |  |     while required_locations: | 
					
						
							| 
									
										
										
										
											2019-12-13 22:37:52 +01:00
										 |  |  |         state.sweep_for_events(key_only=True) | 
					
						
							| 
									
										
										
										
											2018-01-06 14:25:49 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-03 14:26:00 +01:00
										 |  |  |         sphere = set(filter(state.can_reach, required_locations)) | 
					
						
							| 
									
										
										
										
											2018-01-06 14:25:49 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |         for location in sphere: | 
					
						
							|  |  |  |             state.collect(location.item, True, location) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-27 17:11:54 +01:00
										 |  |  |         required_locations -= sphere | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-01-06 14:25:49 -05:00
										 |  |  |         collection_spheres.append(sphere) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-11 14:22:44 +02:00
										 |  |  |         logging.debug('Calculated final sphere %i, containing %i of %i progress items.', len(collection_spheres), | 
					
						
							|  |  |  |                       len(sphere), len(required_locations)) | 
					
						
							| 
									
										
										
										
											2018-01-06 14:25:49 -05:00
										 |  |  |         if not sphere: | 
					
						
							| 
									
										
										
										
											2021-02-25 02:07:28 +01:00
										 |  |  |             raise RuntimeError(f'Not all required items reachable. Unreachable locations: {required_locations}') | 
					
						
							| 
									
										
										
										
											2017-05-16 21:23:47 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-01-01 15:55:13 -05:00
										 |  |  |     def flist_to_iter(node): | 
					
						
							|  |  |  |         while node: | 
					
						
							|  |  |  |             value, node = node | 
					
						
							|  |  |  |             yield value | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-01-06 16:25:14 -05:00
										 |  |  |     def get_path(state, region): | 
					
						
							|  |  |  |         reversed_path_as_flist = state.path.get(region, (region, None)) | 
					
						
							|  |  |  |         string_path_flat = reversed(list(map(str, flist_to_iter(reversed_path_as_flist)))) | 
					
						
							|  |  |  |         # Now we combine the flat string list into (region, exit) pairs | 
					
						
							|  |  |  |         pathsiter = iter(string_path_flat) | 
					
						
							|  |  |  |         pathpairs = zip_longest(pathsiter, pathsiter) | 
					
						
							|  |  |  |         return list(pathpairs) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-09 17:44:24 +02:00
										 |  |  |     world.spoiler.paths = {} | 
					
						
							|  |  |  |     topology_worlds = (player for player in world.player_ids if world.worlds[player].topology_present) | 
					
						
							|  |  |  |     for player in topology_worlds: | 
					
						
							| 
									
										
										
										
											2021-06-11 14:22:44 +02:00
										 |  |  |         world.spoiler.paths.update( | 
					
						
							|  |  |  |             {str(location): get_path(state, location.parent_region) for sphere in collection_spheres for location in | 
					
						
							| 
									
										
										
										
											2021-07-09 17:44:24 +02:00
										 |  |  |              sphere if location.player == player}) | 
					
						
							| 
									
										
										
										
											2021-07-21 18:08:15 +02:00
										 |  |  |         if player in world.get_game_players("A Link to the Past"): | 
					
						
							| 
									
										
										
										
											2021-08-28 12:56:52 +02:00
										 |  |  |             # If Pyramid Fairy Entrance needs to be reached, also path to Big Bomb Shop | 
					
						
							|  |  |  |             # Maybe move the big bomb over to the Event system instead? | 
					
						
							|  |  |  |             if any(exit_path == 'Pyramid Fairy' for path in world.spoiler.paths.values() for (_, exit_path) in path): | 
					
						
							|  |  |  |                 if world.mode[player] != 'inverted': | 
					
						
							|  |  |  |                     world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = \ | 
					
						
							|  |  |  |                         get_path(state, world.get_region('Big Bomb Shop', player)) | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = \ | 
					
						
							|  |  |  |                         get_path(state, world.get_region('Inverted Big Bomb Shop', player)) | 
					
						
							| 
									
										
										
										
											2018-01-01 15:55:13 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-05-16 21:23:47 +02:00
										 |  |  |     # we can finally output our playthrough | 
					
						
							| 
									
										
										
										
											2021-10-11 00:12:00 +02:00
										 |  |  |     world.spoiler.playthrough = {"0": sorted([str(item) for item in | 
					
						
							|  |  |  |                                               chain.from_iterable(world.precollected_items.values()) | 
					
						
							| 
									
										
										
										
											2021-10-10 16:50:01 +02:00
										 |  |  |                                               if item.advancement])} | 
					
						
							| 
									
										
										
										
											2021-02-03 14:26:00 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-01-09 08:31:49 +01:00
										 |  |  |     for i, sphere in enumerate(collection_spheres): | 
					
						
							| 
									
										
										
										
											2021-03-17 10:53:40 +01:00
										 |  |  |         world.spoiler.playthrough[str(i + 1)] = {str(location): str(location.item) for location in sorted(sphere)} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # repair the world again | 
					
						
							|  |  |  |     for location, item in restore_later.items(): | 
					
						
							|  |  |  |         location.item = item | 
					
						
							| 
									
										
										
										
											2021-03-17 11:46:44 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     for item in removed_precollected: | 
					
						
							|  |  |  |         world.push_precollected(item) |