1456 lines
		
	
	
		
			72 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			1456 lines
		
	
	
		
			72 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| import logging
 | |
| import threading
 | |
| import copy
 | |
| import functools
 | |
| import settings
 | |
| import typing
 | |
| from typing import Optional, List, AbstractSet, Union  # remove when 3.8 support is dropped
 | |
| from collections import Counter, deque
 | |
| from string import printable
 | |
| 
 | |
| logger = logging.getLogger("Ocarina of Time")
 | |
| 
 | |
| from .Location import OOTLocation, LocationFactory, location_name_to_id, build_location_name_groups
 | |
| from .Entrance import OOTEntrance
 | |
| from .EntranceShuffle import shuffle_random_entrances, entrance_shuffle_table, EntranceShuffleError
 | |
| from .HintList import getRequiredHints
 | |
| from .Hints import HintArea, HintAreaNotFound, hint_dist_keys, get_hint_area, buildWorldGossipHints
 | |
| from .Items import OOTItem, item_table, oot_data_to_ap_id, oot_is_item_of_type
 | |
| from .ItemPool import generate_itempool, get_junk_item, get_junk_pool
 | |
| from .Regions import OOTRegion, TimeOfDay
 | |
| from .Rules import set_rules, set_shop_rules, set_entrances_based_rules
 | |
| from .RuleParser import Rule_AST_Transformer
 | |
| from .Options import OoTOptions, oot_option_groups
 | |
| from .Utils import data_path, read_json
 | |
| from .LocationList import business_scrubs, set_drop_location_names, dungeon_song_locations
 | |
| from .DungeonList import dungeon_table, create_dungeons
 | |
| from .LogicTricks import normalized_name_tricks
 | |
| from .Rom import Rom
 | |
| from .Patches import OoTContainer, patch_rom
 | |
| from .N64Patch import create_patch_file
 | |
| from .Cosmetics import patch_cosmetics
 | |
| 
 | |
| from BaseClasses import MultiWorld, CollectionState, Tutorial, LocationProgressType
 | |
| from Options import Range, Toggle, VerifyKeys, Accessibility, PlandoConnections, PlandoItems
 | |
| from Fill import fill_restrictive, fast_fill, FillError
 | |
| from worlds.generic.Rules import exclusion_rules, add_item_rule
 | |
| from worlds.AutoWorld import World, AutoLogicRegister, WebWorld
 | |
| 
 | |
| # OoT's generate_output doesn't benefit from more than 2 threads, instead it uses a lot of memory.
 | |
| i_o_limiter = threading.Semaphore(2)
 | |
| 
 | |
| 
 | |
| class OOTCollectionState(metaclass=AutoLogicRegister):
 | |
|     def init_mixin(self, parent: MultiWorld):
 | |
|         oot_ids = parent.get_game_players(OOTWorld.game) + parent.get_game_groups(OOTWorld.game)
 | |
|         self.child_reachable_regions = {player: set() for player in oot_ids}
 | |
|         self.adult_reachable_regions = {player: set() for player in oot_ids}
 | |
|         self.child_blocked_connections = {player: set() for player in oot_ids}
 | |
|         self.adult_blocked_connections = {player: set() for player in oot_ids}
 | |
|         self.day_reachable_regions = {player: set() for player in oot_ids}
 | |
|         self.dampe_reachable_regions = {player: set() for player in oot_ids}
 | |
|         self.age = {player: None for player in oot_ids}
 | |
| 
 | |
|     def copy_mixin(self, ret) -> CollectionState:
 | |
|         ret.child_reachable_regions = {player: copy.copy(self.child_reachable_regions[player]) for player in
 | |
|                                        self.child_reachable_regions}
 | |
|         ret.adult_reachable_regions = {player: copy.copy(self.adult_reachable_regions[player]) for player in
 | |
|                                        self.adult_reachable_regions}
 | |
|         ret.child_blocked_connections = {player: copy.copy(self.child_blocked_connections[player]) for player in
 | |
|                                          self.child_blocked_connections}
 | |
|         ret.adult_blocked_connections = {player: copy.copy(self.adult_blocked_connections[player]) for player in
 | |
|                                          self.adult_blocked_connections}
 | |
|         ret.day_reachable_regions = {player: copy.copy(self.adult_reachable_regions[player]) for player in
 | |
|                                      self.day_reachable_regions}
 | |
|         ret.dampe_reachable_regions = {player: copy.copy(self.adult_reachable_regions[player]) for player in
 | |
|                                        self.dampe_reachable_regions}
 | |
|         return ret
 | |
| 
 | |
| 
 | |
| class OOTSettings(settings.Group):
 | |
|     class RomFile(settings.UserFilePath):
 | |
|         """File name of the OoT v1.0 ROM"""
 | |
|         description = "Ocarina of Time ROM File"
 | |
|         copy_to = "The Legend of Zelda - Ocarina of Time.z64"
 | |
|         md5s = [
 | |
|             "5bd1fe107bf8106b2ab6650abecd54d6",  # normal
 | |
|             "6697768a7a7df2dd27a692a2638ea90b",  # byte-swapped
 | |
|             "05f0f3ebacbc8df9243b6148ffe4792f",  # decompressed
 | |
|         ]
 | |
| 
 | |
|     class RomStart(str):
 | |
|         """
 | |
|         Set this to false to never autostart a rom (such as after patching),
 | |
|                     true  for operating system default program
 | |
|         Alternatively, a path to a program to open the .z64 file with
 | |
|         """
 | |
| 
 | |
|     rom_file: RomFile = RomFile(RomFile.copy_to)
 | |
|     rom_start: typing.Union[RomStart, bool] = True
 | |
| 
 | |
| 
 | |
| class OOTWeb(WebWorld):
 | |
|     setup = Tutorial(
 | |
|         "Multiworld Setup Guide",
 | |
|         "A guide to setting up the Archipelago Ocarina of Time software on your computer.",
 | |
|         "English",
 | |
|         "setup_en.md",
 | |
|         "setup/en",
 | |
|         ["Edos"]
 | |
|     )
 | |
| 
 | |
|     # Very out of date, requires updating to match current
 | |
|     # setup_es = Tutorial(
 | |
|     #     setup.tutorial_name,
 | |
|     #     setup.description,
 | |
|     #     "Español",
 | |
|     #     "setup_es.md",
 | |
|     #     "setup/es",
 | |
|     #     setup.authors
 | |
|     # )
 | |
| 
 | |
|     setup_fr = Tutorial(
 | |
|         setup.tutorial_name,
 | |
|         setup.description,
 | |
|         "Français",
 | |
|         "setup_fr.md",
 | |
|         "setup/fr",
 | |
|         ["TheLynk"]
 | |
|     )
 | |
| 
 | |
|     setup_de = Tutorial(
 | |
|         setup.tutorial_name,
 | |
|         setup.description,
 | |
|         "Deutsch",
 | |
|         "setup_de.md",
 | |
|         "setup/de",
 | |
|         ["Held_der_Zeit"]
 | |
|     )
 | |
| 
 | |
|     tutorials = [setup, setup_fr, setup_de]
 | |
|     option_groups = oot_option_groups
 | |
|     game_info_languages = ["en", "de"]
 | |
| 
 | |
| 
 | |
| class OOTWorld(World):
 | |
|     """
 | |
|     The Legend of Zelda: Ocarina of Time is a 3D action/adventure game. Travel through Hyrule in two time periods,
 | |
|     learn magical ocarina songs, and explore twelve dungeons on your quest. Use Link's many items and abilities
 | |
|     to rescue the Seven Sages, and then confront Ganondorf to save Hyrule!
 | |
|     """
 | |
|     game: str = "Ocarina of Time"
 | |
|     options_dataclass = OoTOptions
 | |
|     options: OoTOptions
 | |
|     settings: typing.ClassVar[OOTSettings]
 | |
|     topology_present: bool = True
 | |
|     item_name_to_id = {item_name: oot_data_to_ap_id(data, False) for item_name, data in item_table.items() if
 | |
|                        data[2] is not None and item_name not in {
 | |
|                         'Keaton Mask', 'Skull Mask', 'Spooky Mask', 'Bunny Hood',
 | |
|                         'Mask of Truth', 'Goron Mask', 'Zora Mask', 'Gerudo Mask',
 | |
|                         'Buy Magic Bean', 'Milk',
 | |
|                         'Small Key', 'Map', 'Compass', 'Boss Key',
 | |
|                        }}  # These are items which aren't used, but have get-item values
 | |
|     location_name_to_id = location_name_to_id
 | |
|     web = OOTWeb()
 | |
| 
 | |
|     required_client_version = (0, 4, 0)
 | |
| 
 | |
|     item_name_groups = {
 | |
|         # internal groups
 | |
|         "medallions": {"Light Medallion", "Forest Medallion", "Fire Medallion",
 | |
|             "Water Medallion", "Shadow Medallion", "Spirit Medallion"},
 | |
|         "stones": {"Kokiri Emerald", "Goron Ruby", "Zora Sapphire"},
 | |
|         "rewards": {"Light Medallion", "Forest Medallion", "Fire Medallion",
 | |
|             "Water Medallion", "Shadow Medallion", "Spirit Medallion",
 | |
|             "Kokiri Emerald", "Goron Ruby", "Zora Sapphire"},
 | |
|         "logic_bottles": {"Bottle", "Bottle with Milk", "Deliver Letter",
 | |
|             "Sell Big Poe", "Bottle with Red Potion", "Bottle with Green Potion",
 | |
|             "Bottle with Blue Potion", "Bottle with Fairy", "Bottle with Fish",
 | |
|             "Bottle with Blue Fire", "Bottle with Bugs", "Bottle with Poe"},
 | |
| 
 | |
|         # hint groups
 | |
|         "Bottles": {"Bottle", "Bottle with Milk", "Rutos Letter",
 | |
|             "Bottle with Big Poe", "Bottle with Red Potion", "Bottle with Green Potion",
 | |
|             "Bottle with Blue Potion", "Bottle with Fairy", "Bottle with Fish",
 | |
|             "Bottle with Blue Fire", "Bottle with Bugs", "Bottle with Poe"},
 | |
|         "Adult Trade Item": {"Pocket Egg", "Pocket Cucco", "Cojiro", "Odd Mushroom",
 | |
|             "Odd Potion", "Poachers Saw", "Broken Sword", "Prescription",
 | |
|             "Eyeball Frog", "Eyedrops", "Claim Check"},
 | |
|         "Keys": {"Small Key (Bottom of the Well)", "Small Key (Fire Temple)", "Small Key (Forest Temple)",
 | |
|                  "Small Key (Ganons Castle)", "Small Key (Gerudo Training Ground)", "Small Key (Shadow Temple)",
 | |
|                  "Small Key (Spirit Temple)", "Small Key (Thieves Hideout)", "Small Key (Water Temple)",
 | |
|                  "Small Key Ring (Bottom of the Well)", "Small Key Ring (Fire Temple)",
 | |
|                  "Small Key Ring (Forest Temple)", "Small Key Ring (Ganons Castle)",
 | |
|                  "Small Key Ring (Gerudo Training Ground)", "Small Key Ring (Shadow Temple)",
 | |
|                  "Small Key Ring (Spirit Temple)", "Small Key Ring (Thieves Hideout)", "Small Key Ring (Water Temple)",
 | |
|                  "Boss Key (Fire Temple)", "Boss Key (Forest Temple)", "Boss Key (Ganons Castle)",
 | |
|                  "Boss Key (Shadow Temple)", "Boss Key (Spirit Temple)", "Boss Key (Water Temple)"},
 | |
| 
 | |
|         # aliases
 | |
|         "Longshot": {"Progressive Hookshot"},  # fuzzy hinting thought Longshot was Slingshot
 | |
|         "Hookshot": {"Progressive Hookshot"},  # for consistency, mostly
 | |
|     }
 | |
| 
 | |
|     location_name_groups = build_location_name_groups()
 | |
| 
 | |
| 
 | |
|     def __init__(self, world, player):
 | |
|         self.hint_data_available = threading.Event()
 | |
|         self.collectible_flags_available = threading.Event()
 | |
|         super(OOTWorld, self).__init__(world, player)
 | |
| 
 | |
| 
 | |
|     @classmethod
 | |
|     def stage_assert_generate(cls, multiworld: MultiWorld):
 | |
|         oot_settings = OOTWorld.settings
 | |
|         rom = Rom(file=oot_settings.rom_file)
 | |
| 
 | |
| 
 | |
|     # Option parsing, handling incompatible options, building useful-item table
 | |
|     def generate_early(self):
 | |
|         self.parser = Rule_AST_Transformer(self, self.player)
 | |
| 
 | |
|         for option_name in self.options_dataclass.type_hints:
 | |
|             result = getattr(self.options, option_name)
 | |
|             if isinstance(result, Range):
 | |
|                 option_value = int(result)
 | |
|             elif isinstance(result, Toggle):
 | |
|                 option_value = bool(result)
 | |
|             elif isinstance(result, VerifyKeys):
 | |
|                 option_value = result.value
 | |
|             elif isinstance(result, PlandoConnections):
 | |
|                 option_value = result.value
 | |
|             elif isinstance(result, PlandoItems):
 | |
|                 option_value = result.value
 | |
|             else:
 | |
|                 option_value = result.current_key
 | |
|             setattr(self, option_name, option_value)
 | |
| 
 | |
|         self.regions = []  # internal caches of regions for this world, used later
 | |
|         self._regions_cache = {}
 | |
| 
 | |
|         self.shop_prices = {}
 | |
|         self.remove_from_start_inventory = []  # some items will be precollected but not in the inventory
 | |
|         self.starting_items = Counter()
 | |
|         self.songs_as_items = False
 | |
|         self.file_hash = [self.random.randint(0, 31) for i in range(5)]
 | |
|         self.connect_name = ''.join(self.random.choices(printable, k=16))
 | |
|         self.collectible_flag_addresses = {}
 | |
| 
 | |
|         # Incompatible option handling
 | |
|         # ER and glitched logic are not compatible; glitched takes priority
 | |
|         if self.logic_rules == 'glitched':
 | |
|             self.shuffle_interior_entrances = 'off'
 | |
|             self.shuffle_dungeon_entrances = 'off'
 | |
|             self.spawn_positions = 'off'
 | |
|             self.shuffle_bosses = 'off'
 | |
|             self.shuffle_grotto_entrances = False
 | |
|             self.shuffle_overworld_entrances = False
 | |
|             self.owl_drops = False
 | |
|             self.warp_songs = False
 | |
| 
 | |
|         # Fix spawn positions option
 | |
|         new_sp = []
 | |
|         if self.spawn_positions in {'child', 'both'}:
 | |
|             new_sp.append('child')
 | |
|         if self.spawn_positions in {'adult', 'both'}:
 | |
|             new_sp.append('adult')
 | |
|         self.spawn_positions = new_sp
 | |
| 
 | |
|         # Closed forest and adult start are not compatible; closed forest takes priority
 | |
|         if self.open_forest == 'closed':
 | |
|             self.starting_age = 'child'
 | |
|             # These ER options force closed forest to become closed deku
 | |
|             if (self.shuffle_interior_entrances == 'all' or self.shuffle_overworld_entrances or self.warp_songs or self.spawn_positions):
 | |
|                 self.open_forest = 'closed_deku'
 | |
| 
 | |
|         # Ganon boss key should not be in itempool in triforce hunt
 | |
|         if self.triforce_hunt:
 | |
|             self.shuffle_ganon_bosskey = 'triforce'
 | |
| 
 | |
|         # Force itempool to higher settings if it doesn't have enough hearts
 | |
|         max_required_hearts = 3
 | |
|         if self.bridge == 'hearts':
 | |
|             max_required_hearts = max(max_required_hearts, self.bridge_hearts)
 | |
|         if self.shuffle_ganon_bosskey == 'hearts':
 | |
|             max_required_hearts = max(max_required_hearts, self.ganon_bosskey_hearts)
 | |
|         if max_required_hearts > 3 and self.item_pool_value == 'minimal':
 | |
|             self.item_pool_value = 'scarce'
 | |
|         if max_required_hearts > 12 and self.item_pool_value == 'scarce':
 | |
|             self.item_pool_value = 'balanced'
 | |
| 
 | |
|         # If songs/keys locked to own world by settings, add them to local_items
 | |
|         local_types = []
 | |
|         if self.shuffle_song_items != 'any':
 | |
|             local_types.append('Song')
 | |
|         if self.shuffle_mapcompass != 'keysanity':
 | |
|             local_types += ['Map', 'Compass']
 | |
|         if self.shuffle_smallkeys != 'keysanity':
 | |
|             local_types.append('SmallKey')
 | |
|         if self.shuffle_hideoutkeys != 'keysanity':
 | |
|             local_types.append('HideoutSmallKey')
 | |
|         if self.shuffle_bosskeys != 'keysanity':
 | |
|             local_types.append('BossKey')
 | |
|         if self.shuffle_ganon_bosskey != 'keysanity':
 | |
|             local_types.append('GanonBossKey')
 | |
|         self.options.local_items.value |= set(name for name, data in item_table.items() if data[0] in local_types)
 | |
| 
 | |
|         # If any songs are itemlinked, set songs_as_items
 | |
|         for group in self.multiworld.groups.values():
 | |
|             if self.songs_as_items or group['game'] != self.game or self.player not in group['players']:
 | |
|                 continue
 | |
|             for item_name in group['item_pool']:
 | |
|                 if oot_is_item_of_type(item_name, 'Song'):
 | |
|                     self.songs_as_items = True
 | |
|                     break
 | |
| 
 | |
|         # Determine skipped trials in GT
 | |
|         # This needs to be done before the logic rules in GT are parsed
 | |
|         trial_list = ['Forest', 'Fire', 'Water', 'Spirit', 'Shadow', 'Light']
 | |
|         chosen_trials = self.random.sample(trial_list, self.trials)  # chooses a list of trials to NOT skip
 | |
|         self.skipped_trials = {trial: (trial not in chosen_trials) for trial in trial_list}
 | |
| 
 | |
|         # Determine tricks in logic
 | |
|         if self.logic_rules == 'glitchless':
 | |
|             for trick in self.logic_tricks:
 | |
|                 normalized_name = trick.casefold()
 | |
|                 if normalized_name in normalized_name_tricks:
 | |
|                     setattr(self, normalized_name_tricks[normalized_name]['name'], True)
 | |
|                 else:
 | |
|                     raise Exception(f'Unknown OOT logic trick for player {self.player}: {trick}')
 | |
| 
 | |
|         # No Logic forces all tricks on, prog balancing off and beatable-only
 | |
|         elif self.logic_rules == 'no_logic':
 | |
|             self.options.progression_balancing.value = False
 | |
|             self.options.accessibility.value = Accessibility.option_minimal
 | |
|             for trick in normalized_name_tricks.values():
 | |
|                 setattr(self, trick['name'], True)
 | |
| 
 | |
|         # Not implemented for now, but needed to placate the generator. Remove as they are implemented
 | |
|         self.ocarina_songs = False  # just need to pull in the OcarinaSongs module
 | |
|         self.mix_entrance_pools = False
 | |
|         self.decouple_entrances = False
 | |
|         self.available_tokens = 100
 | |
|         # Deprecated LACS options
 | |
|         self.lacs_condition = 'vanilla'
 | |
|         self.lacs_stones = 3
 | |
|         self.lacs_medallions = 6
 | |
|         self.lacs_rewards = 9
 | |
|         self.lacs_tokens = 100
 | |
|         self.lacs_hearts = 20
 | |
|         # RuleParser hack
 | |
|         self.triforce_goal_per_world = self.triforce_goal
 | |
| 
 | |
|         # Set internal names used by the OoT generator
 | |
|         self.keysanity = self.shuffle_smallkeys in ['keysanity', 'remove', 'any_dungeon', 'overworld']
 | |
|         self.trials_random = self.options.trials.randomized
 | |
|         self.mq_dungeons_random = self.options.mq_dungeons_count.randomized
 | |
|         self.easier_fire_arrow_entry = self.fae_torch_count < 24
 | |
| 
 | |
|         if self.misc_hints:
 | |
|             self.misc_hints = ['ganondorf', 'altar', 'warp_songs', 'dampe_diary',
 | |
|                 '10_skulltulas', '20_skulltulas', '30_skulltulas', '40_skulltulas', '50_skulltulas']
 | |
|         else:
 | |
|             self.misc_hints = []
 | |
| 
 | |
|         # Hint stuff
 | |
|         self.clearer_hints = True  # this is being enforced since non-oot items do not have non-clear hint text
 | |
|         self.gossip_hints = {}
 | |
|         self.required_locations = []
 | |
|         self.empty_areas = {}
 | |
|         self.major_item_locations = []
 | |
|         self.hinted_dungeon_reward_locations = {}
 | |
| 
 | |
|         # ER names
 | |
|         self.shuffle_special_dungeon_entrances = self.shuffle_dungeon_entrances == 'all'
 | |
|         self.shuffle_dungeon_entrances = self.shuffle_dungeon_entrances != 'off'
 | |
|         self.ensure_tod_access = (self.shuffle_interior_entrances != 'off') or self.shuffle_overworld_entrances or self.spawn_positions
 | |
|         self.entrance_shuffle = (
 | |
|             self.shuffle_interior_entrances != 'off'
 | |
|             or self.shuffle_bosses != 'off'
 | |
|             or self.shuffle_dungeon_entrances
 | |
|             or self.shuffle_special_dungeon_entrances
 | |
|             or self.spawn_positions
 | |
|             or self.shuffle_grotto_entrances
 | |
|             or self.shuffle_overworld_entrances
 | |
|             or self.owl_drops
 | |
|             or self.warp_songs
 | |
|         )
 | |
|         self.disable_trade_revert = (self.shuffle_interior_entrances != 'off') or self.shuffle_overworld_entrances
 | |
|         self.shuffle_special_interior_entrances = self.shuffle_interior_entrances == 'all'
 | |
| 
 | |
|         # Convert the double option used by shopsanity into a single option
 | |
|         if self.shopsanity == 'random_number':
 | |
|             self.shopsanity = 'random'
 | |
|         elif self.shopsanity == 'fixed_number':
 | |
|             self.shopsanity = str(self.shop_slots)
 | |
| 
 | |
|         # Rename options
 | |
|         self.dungeon_shortcuts_choice = self.dungeon_shortcuts
 | |
|         if self.dungeon_shortcuts_choice == 'random_dungeons':
 | |
|             self.dungeon_shortcuts_choice = 'random'
 | |
|         self.key_rings_list          = {s.replace("'", "") for s in self.key_rings_list}
 | |
|         self.dungeon_shortcuts       = {s.replace("'", "") for s in self.dungeon_shortcuts_list}
 | |
|         self.mq_dungeons_specific    = {s.replace("'", "") for s in self.mq_dungeons_list}
 | |
|         # self.empty_dungeons_specific = {s.replace("'", "") for s in self.empty_dungeons_list}
 | |
| 
 | |
|         # Determine which dungeons have key rings.
 | |
|         keyring_dungeons = [d['name'] for d in dungeon_table if d['small_key']] + ['Thieves Hideout']
 | |
|         if self.key_rings == 'off':
 | |
|             self.key_rings = []
 | |
|         elif self.key_rings == 'all':
 | |
|             self.key_rings = keyring_dungeons
 | |
|         elif self.key_rings == 'choose':
 | |
|             self.key_rings = self.key_rings_list
 | |
|         elif self.key_rings == 'random_dungeons':
 | |
|             self.key_rings = self.random.sample(keyring_dungeons,
 | |
|                 self.random.randint(0, len(keyring_dungeons)))
 | |
| 
 | |
|         # Determine which dungeons are MQ. Not compatible with glitched logic.
 | |
|         mq_dungeons = set()
 | |
|         all_dungeons = [d['name'] for d in dungeon_table]
 | |
|         if self.logic_rules != 'glitched':
 | |
|             if self.mq_dungeons_mode == 'mq':
 | |
|                 mq_dungeons = all_dungeons
 | |
|             elif self.mq_dungeons_mode == 'specific':
 | |
|                 mq_dungeons = self.mq_dungeons_specific
 | |
|             elif self.mq_dungeons_mode == 'count':
 | |
|                 mq_dungeons = self.random.sample(all_dungeons, self.mq_dungeons_count)
 | |
|         else:
 | |
|             self.mq_dungeons_mode = 'count'
 | |
|             self.mq_dungeons_count = 0
 | |
|         self.dungeon_mq = {item['name']: (item['name'] in mq_dungeons) for item in dungeon_table}
 | |
|         self.dungeon_mq['Thieves Hideout'] = False  # fix for bug in SaveContext:287
 | |
| 
 | |
|         # Empty dungeon placeholder for the moment
 | |
|         self.empty_dungeons = {name: False for name in self.dungeon_mq}
 | |
| 
 | |
|         # Determine which dungeons have shortcuts. Not compatible with glitched logic.
 | |
|         shortcut_dungeons = ['Deku Tree', 'Dodongos Cavern', \
 | |
|             'Jabu Jabus Belly', 'Forest Temple', 'Fire Temple', \
 | |
|             'Water Temple', 'Shadow Temple', 'Spirit Temple']
 | |
|         if self.logic_rules != 'glitched':
 | |
|             if self.dungeon_shortcuts_choice == 'off':
 | |
|                 self.dungeon_shortcuts = set()
 | |
|             elif self.dungeon_shortcuts_choice == 'all':
 | |
|                 self.dungeon_shortcuts = set(shortcut_dungeons)
 | |
|             elif self.dungeon_shortcuts_choice == 'random':
 | |
|                 self.dungeon_shortcuts = self.random.sample(shortcut_dungeons,
 | |
|                     self.random.randint(0, len(shortcut_dungeons)))
 | |
|             # == 'choice', leave as previous
 | |
|         else:
 | |
|             self.dungeon_shortcuts = set()
 | |
| 
 | |
|         # fixing some options
 | |
|         # Fixes starting time spelling: "witching_hour" -> "witching-hour"
 | |
|         self.starting_tod = self.starting_tod.replace('_', '-')
 | |
|         self.shuffle_scrubs = self.shuffle_scrubs.replace('_prices', '')
 | |
| 
 | |
|         # Convert adult trade option to expected Set
 | |
|         self.adult_trade_start = {self.adult_trade_start.title().replace('_', ' ')}
 | |
| 
 | |
|         # Get hint distribution
 | |
|         self.hint_dist_user = read_json(data_path('Hints', f'{self.hint_dist}.json'))
 | |
| 
 | |
|         self.added_hint_types = {}
 | |
|         self.item_added_hint_types = {}
 | |
|         self.hint_exclusions = set()
 | |
|         if self.shuffle_child_trade == 'skip_child_zelda':
 | |
|             self.hint_exclusions.add('Song from Impa')
 | |
|         self.hint_type_overrides = {}
 | |
|         self.item_hint_type_overrides = {}
 | |
| 
 | |
|         # unused hint stuff
 | |
|         self.named_item_pool = {}
 | |
|         self.hint_text_overrides = {}
 | |
| 
 | |
|         for dist in hint_dist_keys:
 | |
|             self.added_hint_types[dist] = []
 | |
|             for loc in self.hint_dist_user['add_locations']:
 | |
|                 if 'types' in loc:
 | |
|                     if dist in loc['types']:
 | |
|                         self.added_hint_types[dist].append(loc['location'])
 | |
|             self.item_added_hint_types[dist] = []
 | |
|             for i in self.hint_dist_user['add_items']:
 | |
|                 if dist in i['types']:
 | |
|                     self.item_added_hint_types[dist].append(i['item'])
 | |
|             self.hint_type_overrides[dist] = []
 | |
|             for loc in self.hint_dist_user['remove_locations']:
 | |
|                 if dist in loc['types']:
 | |
|                     self.hint_type_overrides[dist].append(loc['location'])
 | |
|             self.item_hint_type_overrides[dist] = []
 | |
|             for i in self.hint_dist_user['remove_items']:
 | |
|                 if dist in i['types']:
 | |
|                     self.item_hint_type_overrides[dist].append(i['item'])
 | |
| 
 | |
|         self.always_hints = [hint.name for hint in getRequiredHints(self)]
 | |
| 
 | |
|         # Determine items which are not considered advancement based on settings. They will never be excluded.
 | |
|         self.nonadvancement_items = {'Double Defense', 'Deku Stick Capacity', 'Deku Nut Capacity'}
 | |
|         if (self.damage_multiplier != 'ohko' and self.damage_multiplier != 'quadruple' and
 | |
|                 self.shuffle_scrubs == 'off' and not self.shuffle_grotto_entrances):
 | |
|             # nayru's love may be required to prevent forced damage
 | |
|             self.nonadvancement_items.add('Nayrus Love')
 | |
|         if getattr(self, 'logic_grottos_without_agony', False) and self.hints != 'agony':
 | |
|             # Stone of Agony skippable if not used for hints or grottos
 | |
|             self.nonadvancement_items.add('Stone of Agony')
 | |
|         if (not self.shuffle_special_interior_entrances and not self.shuffle_overworld_entrances and
 | |
|                 not self.warp_songs and not self.spawn_positions):
 | |
|             # Serenade and Prelude are never required unless one of those settings is enabled
 | |
|             self.nonadvancement_items.add('Serenade of Water')
 | |
|             self.nonadvancement_items.add('Prelude of Light')
 | |
|         if not self.blue_fire_arrows:
 | |
|             # Ice Arrows serve no purpose if they're not hacked to have one
 | |
|             self.nonadvancement_items.add('Ice Arrows')
 | |
|         if not self.bombchus_in_logic:
 | |
|             # Nonrenewable bombchus are not a default logical explosive
 | |
|             self.nonadvancement_items.update({
 | |
|                 'Bombchus (5)',
 | |
|                 'Bombchus (10)',
 | |
|                 'Bombchus (20)',
 | |
|             })
 | |
|         if not (self.bridge == 'hearts' or self.shuffle_ganon_bosskey == 'hearts'):
 | |
|             self.nonadvancement_items.update({
 | |
|                 'Heart Container',
 | |
|                 'Piece of Heart',
 | |
|                 'Piece of Heart (Treasure Chest Game)'
 | |
|             })
 | |
|         if self.logic_rules == 'glitchless':
 | |
|             # Both two-handed swords can be required in glitch logic, so only consider them nonprogression in glitchless
 | |
|             self.nonadvancement_items.add('Biggoron Sword')
 | |
|             self.nonadvancement_items.add('Giants Knife')
 | |
|             if not getattr(self, 'logic_water_central_gs_fw', False):
 | |
|                 # Farore's Wind skippable if not used for this logic trick in Water Temple
 | |
|                 self.nonadvancement_items.add('Farores Wind')
 | |
| 
 | |
| 
 | |
|     # Reads a group of regions from the given JSON file.
 | |
|     def load_regions_from_json(self, file_path):
 | |
|         region_json = read_json(file_path)
 | |
| 
 | |
|         for region in region_json:
 | |
|             new_region = OOTRegion(region['region_name'], self.player, self.multiworld)
 | |
|             if 'pretty_name' in region:
 | |
|                 new_region.pretty_name = region['pretty_name']
 | |
|             if 'font_color' in region:
 | |
|                 new_region.font_color = region['font_color']
 | |
|             if 'scene' in region:
 | |
|                 new_region.scene = region['scene']
 | |
|             if 'dungeon' in region:
 | |
|                 new_region.dungeon = region['dungeon']
 | |
|                 new_region.set_hint_data(region['dungeon'])
 | |
|             if 'is_boss_room' in region:
 | |
|                 new_region.is_boss_room = region['is_boss_room']
 | |
|             if 'hint' in region:
 | |
|                 new_region.set_hint_data(region['hint'])
 | |
|             if 'alt_hint' in region:
 | |
|                 new_region.alt_hint = HintArea[region['alt_hint']]
 | |
|             if 'time_passes' in region:
 | |
|                 new_region.time_passes = region['time_passes']
 | |
|                 new_region.provides_time = TimeOfDay.ALL
 | |
|             if new_region.name == 'Ganons Castle Grounds':
 | |
|                 new_region.provides_time = TimeOfDay.DAMPE
 | |
|             if 'locations' in region:
 | |
|                 for location, rule in region['locations'].items():
 | |
|                     new_location = LocationFactory(location, self.player)
 | |
|                     if new_location.type in ['HintStone', 'Hint']:
 | |
|                         continue
 | |
|                     new_location.parent_region = new_region
 | |
|                     new_location.rule_string = rule
 | |
|                     self.parser.parse_spot_rule(new_location)
 | |
|                     if new_location.never:
 | |
|                         # We still need to fill the location even if ALR is off.
 | |
|                         logger.debug('Unreachable location: %s', new_location.name)
 | |
|                     new_location.player = self.player
 | |
|                     # Change some attributes of Drop locations
 | |
|                     if new_location.type == 'Drop':
 | |
|                         new_location.name = new_region.name + ' ' + new_location.name
 | |
|                         new_location.show_in_spoiler = False
 | |
|                     new_region.locations.append(new_location)
 | |
|             if 'events' in region:
 | |
|                 for event, rule in region['events'].items():
 | |
|                     # Allow duplicate placement of events
 | |
|                     lname = '%s from %s' % (event, new_region.name)
 | |
|                     new_location = OOTLocation(self.player, lname, type='Event', parent=new_region)
 | |
|                     new_location.rule_string = rule
 | |
|                     self.parser.parse_spot_rule(new_location)
 | |
|                     if new_location.never:
 | |
|                         logger.debug('Dropping unreachable event: %s', new_location.name)
 | |
|                     else:
 | |
|                         new_location.player = self.player
 | |
|                         new_region.locations.append(new_location)
 | |
|                         self.make_event_item(event, new_location)
 | |
|                         new_location.show_in_spoiler = False
 | |
|             if 'exits' in region:
 | |
|                 for exit, rule in region['exits'].items():
 | |
|                     new_exit = OOTEntrance(self.player, self.multiworld, '%s -> %s' % (new_region.name, exit), new_region)
 | |
|                     new_exit.vanilla_connected_region = exit
 | |
|                     new_exit.rule_string = rule
 | |
|                     self.parser.parse_spot_rule(new_exit)
 | |
|                     if new_exit.never:
 | |
|                         logger.debug('Dropping unreachable exit: %s', new_exit.name)
 | |
|                     else:
 | |
|                         new_region.exits.append(new_exit)
 | |
| 
 | |
|             self.multiworld.regions.append(new_region)
 | |
|             self.regions.append(new_region)
 | |
|             self._regions_cache[new_region.name] = new_region
 | |
| 
 | |
| 
 | |
|     # Sets deku scrub prices
 | |
|     def set_scrub_prices(self):
 | |
|         # Get Deku Scrub Locations
 | |
|         scrub_locations = [location for location in self.get_locations() if location.type in {'Scrub', 'GrottoScrub'}]
 | |
|         scrub_dictionary = {}
 | |
|         self.scrub_prices = {}
 | |
|         for location in scrub_locations:
 | |
|             if location.default not in scrub_dictionary:
 | |
|                 scrub_dictionary[location.default] = []
 | |
|             scrub_dictionary[location.default].append(location)
 | |
| 
 | |
|         # Loop through each type of scrub.
 | |
|         for (scrub_item, default_price, text_id, text_replacement) in business_scrubs:
 | |
|             price = default_price
 | |
|             if self.shuffle_scrubs == 'low':
 | |
|                 price = 10
 | |
|             elif self.shuffle_scrubs == 'random':
 | |
|                 # this is a random value between 0-99
 | |
|                 # average value is ~33 rupees
 | |
|                 price = int(self.random.betavariate(1, 2) * 99)
 | |
| 
 | |
|             # Set price in the dictionary as well as the location.
 | |
|             self.scrub_prices[scrub_item] = price
 | |
|             if scrub_item in scrub_dictionary:
 | |
|                 for location in scrub_dictionary[scrub_item]:
 | |
|                     location.price = price
 | |
|                     if location.item is not None:
 | |
|                         location.item.price = price
 | |
| 
 | |
| 
 | |
|     # Sets prices for shuffled shop locations
 | |
|     def random_shop_prices(self):
 | |
|         shop_item_indexes = ['7', '5', '8', '6']
 | |
|         self.shop_prices = {}
 | |
|         for region in self.regions:
 | |
|             if self.shopsanity == 'random':
 | |
|                 shop_item_count = self.random.randint(0, 4)
 | |
|             else:
 | |
|                 shop_item_count = int(self.shopsanity)
 | |
| 
 | |
|             for location in region.locations:
 | |
|                 if location.type == 'Shop':
 | |
|                     if location.name[-1:] in shop_item_indexes[:shop_item_count]:
 | |
|                         if self.shopsanity_prices == 'normal':
 | |
|                             self.shop_prices[location.name] = int(self.random.betavariate(1.5, 2) * 60) * 5
 | |
|                         elif self.shopsanity_prices == 'affordable':
 | |
|                             self.shop_prices[location.name] = 10
 | |
|                         elif self.shopsanity_prices == 'starting_wallet':
 | |
|                             self.shop_prices[location.name] = self.random.randrange(0,100,5)
 | |
|                         elif self.shopsanity_prices == 'adults_wallet':
 | |
|                             self.shop_prices[location.name] = self.random.randrange(0,201,5)
 | |
|                         elif self.shopsanity_prices == 'giants_wallet':
 | |
|                             self.shop_prices[location.name] = self.random.randrange(0,501,5)
 | |
|                         elif self.shopsanity_prices == 'tycoons_wallet':
 | |
|                             self.shop_prices[location.name] = self.random.randrange(0,1000,5)
 | |
| 
 | |
| 
 | |
|     # Fill boss prizes
 | |
|     def fill_bosses(self, bossCount=9):
 | |
|         boss_location_names = (
 | |
|             'Queen Gohma',
 | |
|             'King Dodongo',
 | |
|             'Barinade',
 | |
|             'Phantom Ganon',
 | |
|             'Volvagia',
 | |
|             'Morpha',
 | |
|             'Bongo Bongo',
 | |
|             'Twinrova',
 | |
|             'Links Pocket'
 | |
|         )
 | |
|         boss_rewards = sorted(map(self.create_item, self.item_name_groups['rewards']))
 | |
|         boss_locations = [self.multiworld.get_location(loc, self.player) for loc in boss_location_names]
 | |
| 
 | |
|         placed_prizes = [loc.item.name for loc in boss_locations if loc.item is not None]
 | |
|         prizepool = [item for item in boss_rewards if item.name not in placed_prizes]
 | |
|         prize_locs = [loc for loc in boss_locations if loc.item is None]
 | |
| 
 | |
|         while bossCount:
 | |
|             bossCount -= 1
 | |
|             self.random.shuffle(prizepool)
 | |
|             self.random.shuffle(prize_locs)
 | |
|             item = prizepool.pop()
 | |
|             loc = prize_locs.pop()
 | |
|             loc.place_locked_item(item)
 | |
|             self.hinted_dungeon_reward_locations[item.name] = loc
 | |
| 
 | |
| 
 | |
|     # Separate the result from generate_itempool into main and prefill pools
 | |
|     def divide_itempools(self):
 | |
|         prefill_item_types = set()
 | |
|         if self.shopsanity != 'off':
 | |
|             prefill_item_types.add('Shop')
 | |
|         if self.shuffle_song_items != 'any':
 | |
|             prefill_item_types.add('Song')
 | |
|         if self.shuffle_smallkeys != 'keysanity':
 | |
|             prefill_item_types.add('SmallKey')
 | |
|         if self.shuffle_bosskeys != 'keysanity':
 | |
|             prefill_item_types.add('BossKey')
 | |
|         if self.shuffle_hideoutkeys != 'keysanity':
 | |
|             prefill_item_types.add('HideoutSmallKey')
 | |
|         if self.shuffle_ganon_bosskey != 'keysanity':
 | |
|             prefill_item_types.add('GanonBossKey')
 | |
|         if self.shuffle_mapcompass != 'keysanity':
 | |
|             prefill_item_types.update({'Map', 'Compass'})
 | |
| 
 | |
|         main_items = []
 | |
|         prefill_items = []
 | |
|         for item in self.itempool:
 | |
|             if item.type in prefill_item_types:
 | |
|                 prefill_items.append(item)
 | |
|             else:
 | |
|                 main_items.append(item)
 | |
|         return main_items, prefill_items
 | |
| 
 | |
| 
 | |
|     # only returns proper result after create_items and divide_itempools are run
 | |
|     def get_pre_fill_items(self):
 | |
|         return self.pre_fill_items
 | |
| 
 | |
| 
 | |
|     # Note on allow_arbitrary_name:
 | |
|     # OoT defines many helper items and event names that are treated indistinguishably from regular items,
 | |
|     #   but are only defined in the logic files. This means we need to create items for any name.
 | |
|     # Allowing any item name to be created is dangerous in case of plando, so this is a middle ground.
 | |
|     def create_item(self, name: str, allow_arbitrary_name: bool = False):
 | |
|         if name in item_table:
 | |
|             return OOTItem(name, self.player, item_table[name], False,
 | |
|                            (name in self.nonadvancement_items if getattr(self, 'nonadvancement_items',
 | |
|                                                                          None) else False))
 | |
|         if allow_arbitrary_name:
 | |
|             return OOTItem(name, self.player, ('Event', True, None, None), True, False)
 | |
|         raise Exception(f"Invalid item name: {name}")
 | |
| 
 | |
|     def make_event_item(self, name, location, item=None):
 | |
|         if item is None:
 | |
|             item = self.create_item(name, allow_arbitrary_name=True)
 | |
|         self.multiworld.push_item(location, item, collect=False)
 | |
|         location.locked = True
 | |
|         if name not in item_table:
 | |
|             location.internal = True
 | |
|         return item
 | |
| 
 | |
| 
 | |
|     # Create regions, locations, and entrances
 | |
|     def create_regions(self):
 | |
|         if self.logic_rules == 'glitchless' or self.logic_rules == 'no_logic':  # enables ER + NL
 | |
|             world_type = 'World'
 | |
|         else:
 | |
|             world_type = 'Glitched World'
 | |
|         overworld_data_path = data_path(world_type, 'Overworld.json')
 | |
|         bosses_data_path = data_path(world_type, 'Bosses.json')
 | |
|         menu = OOTRegion('Menu', self.player, self.multiworld)
 | |
|         start = OOTEntrance(self.player, self.multiworld, 'New Game', menu)
 | |
|         menu.exits.append(start)
 | |
|         self.multiworld.regions.append(menu)
 | |
|         self.load_regions_from_json(overworld_data_path)
 | |
|         self.load_regions_from_json(bosses_data_path)
 | |
|         start.connect(self.get_region('Root'))
 | |
|         create_dungeons(self)
 | |
|         self.parser.create_delayed_rules()
 | |
| 
 | |
|         if self.shopsanity != 'off':
 | |
|             self.random_shop_prices()
 | |
|         self.set_scrub_prices()
 | |
| 
 | |
|         # Bind entrances to vanilla
 | |
|         for region in self.regions:
 | |
|             for exit in region.exits:
 | |
|                 exit.connect(self.get_region(exit.vanilla_connected_region))
 | |
| 
 | |
| 
 | |
|     # Create items, starting item handling, boss prize fill (before entrance randomizer)
 | |
|     def create_items(self):
 | |
|         # Generate itempool
 | |
|         generate_itempool(self)
 | |
| 
 | |
|         junk_pool = get_junk_pool(self)
 | |
|         removed_items = []
 | |
|         # Determine starting items
 | |
|         for item in self.multiworld.precollected_items[self.player]:
 | |
|             if item.name in self.remove_from_start_inventory:
 | |
|                 self.remove_from_start_inventory.remove(item.name)
 | |
|                 removed_items.append(item.name)
 | |
|             else:
 | |
|                 self.starting_items[item.name] += 1
 | |
|                 if item.type == 'Song':
 | |
|                     self.songs_as_items = True
 | |
|                 # Call the junk fill and get a replacement
 | |
|                 if item in self.itempool:
 | |
|                     self.itempool.remove(item)
 | |
|                     self.itempool.append(self.create_item(*get_junk_item(self.random, pool=junk_pool)))
 | |
|         if self.start_with_consumables:
 | |
|             self.starting_items['Deku Sticks'] = 30
 | |
|             self.starting_items['Deku Nuts'] = 40
 | |
|         if self.start_with_rupees:
 | |
|             self.starting_items['Rupees'] = 999
 | |
| 
 | |
|         # Divide itempool into prefill and main pools
 | |
|         self.itempool, self.pre_fill_items = self.divide_itempools()
 | |
| 
 | |
|         self.multiworld.itempool += self.itempool
 | |
|         self.remove_from_start_inventory.extend(removed_items)
 | |
| 
 | |
|         # Fill boss prizes. needs to happen before entrance shuffle
 | |
|         self.fill_bosses()
 | |
| 
 | |
| 
 | |
|     def set_rules(self):
 | |
|         # This has to run AFTER creating items but BEFORE set_entrances_based_rules
 | |
|         if self.entrance_shuffle:
 | |
|             # 10 attempts at shuffling entrances
 | |
|             tries = 10
 | |
|             while tries:
 | |
|                 try:
 | |
|                     shuffle_random_entrances(self)
 | |
|                 except EntranceShuffleError as e:
 | |
|                     tries -= 1
 | |
|                     logger.debug(
 | |
|                         f"Failed shuffling entrances for world {self.player}, retrying {tries} more times")
 | |
|                     if tries == 0:
 | |
|                         raise e
 | |
|                     # Restore original state and delete assumed entrances
 | |
|                     for entrance in self.get_shuffled_entrances():
 | |
|                         if entrance.connected_region is not None:
 | |
|                             entrance.disconnect()
 | |
|                         entrance.connect(self.multiworld.get_region(entrance.vanilla_connected_region, self.player))
 | |
|                         if entrance.assumed:
 | |
|                             assumed_entrance = entrance.assumed
 | |
|                             if assumed_entrance.connected_region is not None:
 | |
|                                 assumed_entrance.disconnect()
 | |
|                             del assumed_entrance
 | |
|                         entrance.reverse = None
 | |
|                         entrance.replaces = None
 | |
|                         entrance.assumed = None
 | |
|                         entrance.shuffled = False
 | |
|                     # Clean up root entrances
 | |
|                     root = self.get_region("Root Exits")
 | |
|                     root.exits = root.exits[:8]
 | |
|                 else:
 | |
|                     break
 | |
| 
 | |
|         set_rules(self)
 | |
|         set_entrances_based_rules(self)
 | |
| 
 | |
| 
 | |
|     def generate_basic(self):  # mostly killing locations that shouldn't exist by settings
 | |
| 
 | |
|         # Gather items for ice trap appearances
 | |
|         self.fake_items = []
 | |
|         if self.ice_trap_appearance in ['major_only', 'anything']:
 | |
|             self.fake_items.extend(item for item in self.itempool if item.index and self.is_major_item(item))
 | |
|         if self.ice_trap_appearance in ['junk_only', 'anything']:
 | |
|             self.fake_items.extend(item for item in self.itempool if
 | |
|                                    item.index and not item.type == 'Shop' and not self.is_major_item(item) and item.name != 'Ice Trap')
 | |
| 
 | |
|         # Kill unreachable events that can't be gotten even with all items
 | |
|         # Make sure to only kill actual internal events, not in-game "events"
 | |
|         all_state = self.get_state_with_complete_itempool()
 | |
|         all_locations = self.get_locations()
 | |
|         all_state.sweep_for_advancements(locations=all_locations)
 | |
|         reachable = self.multiworld.get_reachable_locations(all_state, self.player)
 | |
|         unreachable = [loc for loc in all_locations if
 | |
|                        (loc.internal or loc.type == 'Drop') and loc.address is None and loc.locked and loc not in reachable]
 | |
|         for loc in unreachable:
 | |
|             loc.parent_region.locations.remove(loc)
 | |
|         # Exception: Sell Big Poe is an event which is only reachable if Bottle with Big Poe is in the item pool.
 | |
|         # We allow it to be removed only if Bottle with Big Poe is not in the itempool.
 | |
|         bigpoe = self.multiworld.get_location('Sell Big Poe from Market Guard House', self.player)
 | |
|         if not all_state.has('Bottle with Big Poe', self.player) and bigpoe not in reachable:
 | |
|             bigpoe.parent_region.locations.remove(bigpoe)
 | |
| 
 | |
|         # If fast scarecrow then we need to kill the Pierre location as it will be unreachable
 | |
|         if self.free_scarecrow:
 | |
|             loc = self.multiworld.get_location("Pierre", self.player)
 | |
|             loc.parent_region.locations.remove(loc)
 | |
|         # If open zora's domain then we need to kill Deliver Rutos Letter
 | |
|         if self.zora_fountain == 'open':
 | |
|             loc = self.multiworld.get_location("Deliver Rutos Letter", self.player)
 | |
|             loc.parent_region.locations.remove(loc)
 | |
| 
 | |
| 
 | |
|     def pre_fill(self):
 | |
| 
 | |
|         def prefill_state(base_state):
 | |
|             state = base_state.copy()
 | |
|             for item in self.get_pre_fill_items():
 | |
|                 self.collect(state, item)
 | |
|             state.sweep_for_advancements(locations=self.get_locations())
 | |
|             return state
 | |
| 
 | |
|         # Prefill shops, songs, and dungeon items
 | |
|         items = self.get_pre_fill_items()
 | |
|         locations = list(self.multiworld.get_unfilled_locations(self.player))
 | |
|         self.random.shuffle(locations)
 | |
| 
 | |
|         # Set up initial state
 | |
|         state = CollectionState(self.multiworld)
 | |
|         for item in self.itempool:
 | |
|             self.collect(state, item)
 | |
|         state.sweep_for_advancements(locations=self.get_locations())
 | |
| 
 | |
|         # Place dungeon items
 | |
|         special_fill_types = ['GanonBossKey', 'BossKey', 'SmallKey', 'HideoutSmallKey', 'Map', 'Compass']
 | |
|         type_to_setting = {
 | |
|             'Map': 'shuffle_mapcompass',
 | |
|             'Compass': 'shuffle_mapcompass',
 | |
|             'SmallKey': 'shuffle_smallkeys',
 | |
|             'BossKey': 'shuffle_bosskeys',
 | |
|             'HideoutSmallKey': 'shuffle_hideoutkeys',
 | |
|             'GanonBossKey': 'shuffle_ganon_bosskey',
 | |
|         }
 | |
|         special_fill_types.sort(key=lambda x: 0 if getattr(self, type_to_setting[x]) == 'dungeon' else 1)
 | |
| 
 | |
|         for fill_stage in special_fill_types:
 | |
|             stage_items = list(filter(lambda item: oot_is_item_of_type(item, fill_stage), self.pre_fill_items))
 | |
|             if not stage_items:
 | |
|                 continue
 | |
|             if fill_stage in ['GanonBossKey', 'HideoutSmallKey']:
 | |
|                 locations = gather_locations(self.multiworld, fill_stage, self.player)
 | |
|                 if isinstance(locations, list):
 | |
|                     for item in stage_items:
 | |
|                         self.pre_fill_items.remove(item)
 | |
|                     self.random.shuffle(locations)
 | |
|                     fill_restrictive(self.multiworld, prefill_state(state), locations, stage_items,
 | |
|                         single_player_placement=True, lock=True, allow_excluded=True)
 | |
|             else:
 | |
|                 for dungeon_info in dungeon_table:
 | |
|                     dungeon_name = dungeon_info['name']
 | |
|                     dungeon_items = list(filter(lambda item: dungeon_name in item.name, stage_items))
 | |
|                     if not dungeon_items:
 | |
|                         continue
 | |
|                     locations = gather_locations(self.multiworld, fill_stage, self.player, dungeon=dungeon_name)
 | |
|                     if isinstance(locations, list):
 | |
|                         for item in dungeon_items:
 | |
|                             self.pre_fill_items.remove(item)
 | |
|                         self.random.shuffle(locations)
 | |
|                         fill_restrictive(self.multiworld, prefill_state(state), locations, dungeon_items,
 | |
|                             single_player_placement=True, lock=True, allow_excluded=True)
 | |
| 
 | |
|         # Place songs
 | |
|         # 5 built-in retries because this section can fail sometimes
 | |
|         if self.shuffle_song_items != 'any':
 | |
|             tries = 10
 | |
|             if self.shuffle_song_items == 'song':
 | |
|                 song_locations = list(filter(lambda location: location.type == 'Song',
 | |
|                                              self.multiworld.get_unfilled_locations(player=self.player)))
 | |
|             elif self.shuffle_song_items == 'dungeon':
 | |
|                 song_locations = list(filter(lambda location: location.name in dungeon_song_locations,
 | |
|                                              self.multiworld.get_unfilled_locations(player=self.player)))
 | |
|             else:
 | |
|                 raise Exception(f"Unknown song shuffle type: {self.shuffle_song_items}")
 | |
| 
 | |
|             songs = list(filter(lambda item: item.type == 'Song', self.pre_fill_items))
 | |
|             for song in songs:
 | |
|                 self.pre_fill_items.remove(song)
 | |
| 
 | |
|             important_warps = (self.shuffle_special_interior_entrances or self.shuffle_overworld_entrances or
 | |
|                                self.warp_songs or self.spawn_positions)
 | |
|             song_order = {
 | |
|                 'Zeldas Lullaby': 1,
 | |
|                 'Eponas Song': 1,
 | |
|                 'Sarias Song': 3 if important_warps else 0,
 | |
|                 'Suns Song': 0,
 | |
|                 'Song of Time': 0,
 | |
|                 'Song of Storms': 3,
 | |
|                 'Minuet of Forest': 2 if important_warps else 0,
 | |
|                 'Bolero of Fire': 2 if important_warps else 0,
 | |
|                 'Serenade of Water': 2 if important_warps else 0,
 | |
|                 'Requiem of Spirit': 2,
 | |
|                 'Nocturne of Shadow': 2,
 | |
|                 'Prelude of Light': 2 if important_warps else 0,
 | |
|             }
 | |
|             songs.sort(key=lambda song: song_order.get(song.name, 0))
 | |
| 
 | |
|             while tries:
 | |
|                 try:
 | |
|                     self.random.shuffle(song_locations)
 | |
|                     fill_restrictive(self.multiworld, prefill_state(state), song_locations[:], songs[:],
 | |
|                                      single_player_placement=True, lock=True, allow_excluded=True)
 | |
|                     logger.debug(f"Successfully placed songs for player {self.player} after {6 - tries} attempt(s)")
 | |
|                 except FillError as e:
 | |
|                     tries -= 1
 | |
|                     if tries == 0:
 | |
|                         raise Exception(f"Failed placing songs for player {self.player}. Error cause: {e}")
 | |
|                     logger.debug(f"Failed placing songs for player {self.player}. Retries left: {tries}")
 | |
|                     # undo what was done
 | |
|                     for song in songs:
 | |
|                         song.location = None
 | |
|                         song.world = None
 | |
|                     for location in song_locations:
 | |
|                         location.item = None
 | |
|                         location.locked = False
 | |
|                 else:
 | |
|                     break
 | |
| 
 | |
|         # Place shop items
 | |
|         # fast fill will fail because there is some logic on the shop items. we'll gather them up and place the shop items
 | |
|         if self.shopsanity != 'off':
 | |
|             shop_prog = list(filter(lambda item: item.type == 'Shop' and item.advancement, self.pre_fill_items))
 | |
|             shop_junk = list(filter(lambda item: item.type == 'Shop' and not item.advancement, self.pre_fill_items))
 | |
|             shop_locations = list(
 | |
|                 filter(lambda location: location.type == 'Shop' and location.name not in self.shop_prices,
 | |
|                        self.multiworld.get_unfilled_locations(player=self.player)))
 | |
|             shop_prog.sort(key=lambda item: {
 | |
|                 'Buy Deku Shield': 2 * int(self.open_forest == 'closed'),
 | |
|                 'Buy Goron Tunic': 1,
 | |
|                 'Buy Zora Tunic': 1,
 | |
|             }.get(item.name, 0))  # place Deku Shields if needed, then tunics, then other advancement
 | |
|             self.random.shuffle(shop_locations)
 | |
|             self.pre_fill_items = []  # all prefill should be done
 | |
|             fill_restrictive(self.multiworld, prefill_state(state), shop_locations, shop_prog,
 | |
|                 single_player_placement=True, lock=True, allow_excluded=True)
 | |
|             fast_fill(self.multiworld, shop_junk, shop_locations)
 | |
|             for loc in shop_locations:
 | |
|                 loc.locked = True
 | |
|         set_shop_rules(self)  # sets wallet requirements on shop items, must be done after they are filled
 | |
| 
 | |
|         # Exclude locations in Ganon's Castle proportional to the number of items required to make the bridge
 | |
|         # Check for dungeon ER later
 | |
|         if self.logic_rules == 'glitchless':
 | |
|             if self.bridge == 'medallions':
 | |
|                 ganon_junk_fill = self.bridge_medallions / 9
 | |
|             elif self.bridge == 'stones':
 | |
|                 ganon_junk_fill = self.bridge_stones / 9
 | |
|             elif self.bridge == 'dungeons':
 | |
|                 ganon_junk_fill = self.bridge_rewards / 9
 | |
|             elif self.bridge == 'vanilla':
 | |
|                 ganon_junk_fill = 2 / 9
 | |
|             elif self.bridge == 'tokens':
 | |
|                 ganon_junk_fill = self.bridge_tokens / 100
 | |
|             elif self.bridge == 'hearts':
 | |
|                 ganon_junk_fill = self.bridge_hearts / 20
 | |
|             elif self.bridge == 'open':
 | |
|                 ganon_junk_fill = 0
 | |
|             else:
 | |
|                 raise Exception("Unexpected bridge setting")
 | |
| 
 | |
|             ganon_junk_fill = min(1, ganon_junk_fill)
 | |
|             gc = next(filter(lambda dungeon: dungeon.name == 'Ganons Castle', self.dungeons))
 | |
|             locations = [loc.name for region in gc.regions for loc in region.locations if loc.item is None]
 | |
|             junk_fill_locations = self.random.sample(locations, round(len(locations) * ganon_junk_fill))
 | |
|             exclusion_rules(self.multiworld, self.player, junk_fill_locations)
 | |
| 
 | |
|         # Locations which are not sendable must be converted to events
 | |
|         # This includes all locations for which show_in_spoiler is false, and shuffled shop items.
 | |
|         for loc in self.get_locations():
 | |
|             if loc.address is not None and (
 | |
|                     not loc.show_in_spoiler or oot_is_item_of_type(loc.item, 'Shop')
 | |
|                     or (self.shuffle_child_trade == 'skip_child_zelda' and loc.name in ['HC Zeldas Letter', 'Song from Impa'])):
 | |
|                 loc.address = None
 | |
| 
 | |
| 
 | |
|     def generate_output(self, output_directory: str):
 | |
| 
 | |
|         # Write entrances to spoiler log
 | |
|         all_entrances = self.get_shuffled_entrances()
 | |
|         all_entrances.sort(reverse=True, key=lambda x: (x.type, x.name))
 | |
|         if not self.decouple_entrances:
 | |
|             while all_entrances:
 | |
|                 loadzone = all_entrances.pop()
 | |
|                 if loadzone.type != 'Overworld':
 | |
|                     if loadzone.primary:
 | |
|                         entrance = loadzone
 | |
|                     else:
 | |
|                         entrance = loadzone.reverse
 | |
|                     if entrance.reverse is not None:
 | |
|                         self.multiworld.spoiler.set_entrance(entrance, entrance.replaces.reverse, 'both', self.player)
 | |
|                     else:
 | |
|                         self.multiworld.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player)
 | |
|                 else:
 | |
|                     reverse = loadzone.replaces.reverse
 | |
|                     if reverse in all_entrances:
 | |
|                         all_entrances.remove(reverse)
 | |
|                     self.multiworld.spoiler.set_entrance(loadzone, reverse, 'both', self.player)
 | |
|         else:
 | |
|             for entrance in all_entrances:
 | |
|                 self.multiworld.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player)
 | |
| 
 | |
|         if self.hints != 'none':
 | |
|             self.hint_data_available.wait()
 | |
| 
 | |
|         with i_o_limiter:
 | |
|             # Make traps appear as other random items
 | |
|             trap_location_ids = [loc.address for loc in self.get_locations() if loc.item.trap]
 | |
|             self.trap_appearances = {}
 | |
|             for loc_id in trap_location_ids:
 | |
|                 self.trap_appearances[loc_id] = self.create_item(self.random.choice(self.fake_items).name)
 | |
| 
 | |
|             # Seed hint RNG, used for ganon text lines also
 | |
|             self.hint_rng = self.random
 | |
| 
 | |
|             outfile_name = self.multiworld.get_out_file_name_base(self.player)
 | |
|             oot_settings = OOTWorld.settings
 | |
|             rom = Rom(file=oot_settings.rom_file)
 | |
|             try:
 | |
|                 if self.hints != 'none':
 | |
|                     buildWorldGossipHints(self)
 | |
|                 patch_rom(self, rom)
 | |
|                 patch_cosmetics(self, rom)
 | |
|             except Exception as e:
 | |
|                 logger.error(e)
 | |
|                 raise e
 | |
|             finally:
 | |
|                 self.collectible_flags_available.set()
 | |
|             rom.update_header()
 | |
|             patch_data = create_patch_file(rom, self.random)
 | |
|             rom.restore()
 | |
| 
 | |
|             apz5 = OoTContainer(patch_data, outfile_name, output_directory,
 | |
|                 player=self.player,
 | |
|                 player_name=self.multiworld.get_player_name(self.player))
 | |
|             apz5.write()
 | |
| 
 | |
| 
 | |
|     # Gathers hint data for OoT. Loops over all world locations for woth, barren, and major item locations.
 | |
|     @classmethod
 | |
|     def stage_generate_output(cls, multiworld: MultiWorld, output_directory: str):
 | |
|         def hint_type_players(hint_type: str) -> set:
 | |
|             return {autoworld.player for autoworld in multiworld.get_game_worlds("Ocarina of Time")
 | |
|                     if autoworld.hints != 'none' 
 | |
|                     and autoworld.hint_dist_user['distribution'][hint_type]['copies'] > 0
 | |
|                     and (autoworld.hint_dist_user['distribution'][hint_type]['fixed'] > 0 
 | |
|                       or autoworld.hint_dist_user['distribution'][hint_type]['weight'] > 0)}
 | |
| 
 | |
|         try:
 | |
|             item_hint_players = hint_type_players('item')
 | |
|             barren_hint_players = hint_type_players('barren')
 | |
|             woth_hint_players = hint_type_players('woth')
 | |
| 
 | |
|             items_by_region = {}
 | |
|             for player in barren_hint_players:
 | |
|                 items_by_region[player] = {}
 | |
|                 for r in multiworld.worlds[player].regions:
 | |
|                     items_by_region[player][r._hint_text] = {'dungeon': False, 'weight': 0, 'is_barren': True}
 | |
|                 for d in multiworld.worlds[player].dungeons:
 | |
|                     items_by_region[player][d.hint_text] = {'dungeon': True, 'weight': 0, 'is_barren': True}
 | |
|                 del (items_by_region[player]["Link's pocket"])
 | |
|                 del (items_by_region[player][None])
 | |
| 
 | |
|             if item_hint_players:  # loop once over all locations to gather major items. Check oot locations for barren/woth if needed
 | |
|                 for loc in multiworld.get_locations():
 | |
|                     player = loc.item.player
 | |
|                     autoworld = multiworld.worlds[player]
 | |
|                     if ((player in item_hint_players and (autoworld.is_major_item(loc.item) or loc.item.name in autoworld.item_added_hint_types['item']))
 | |
|                                 or (loc.player in item_hint_players and loc.name in multiworld.worlds[loc.player].added_hint_types['item'])):
 | |
|                         autoworld.major_item_locations.append(loc)
 | |
| 
 | |
|                     if loc.game == "Ocarina of Time" and loc.item.code and (not loc.locked or
 | |
|                         (oot_is_item_of_type(loc.item, 'Song') or
 | |
|                             (oot_is_item_of_type(loc.item, 'SmallKey')         and multiworld.worlds[loc.player].shuffle_smallkeys     in ('overworld', 'any_dungeon', 'regional')) or
 | |
|                             (oot_is_item_of_type(loc.item, 'HideoutSmallKey')  and multiworld.worlds[loc.player].shuffle_hideoutkeys   in ('overworld', 'any_dungeon', 'regional')) or
 | |
|                             (oot_is_item_of_type(loc.item, 'BossKey')          and multiworld.worlds[loc.player].shuffle_bosskeys      in ('overworld', 'any_dungeon', 'regional')) or
 | |
|                             (oot_is_item_of_type(loc.item, 'GanonBossKey')     and multiworld.worlds[loc.player].shuffle_ganon_bosskey in ('overworld', 'any_dungeon', 'regional')))):
 | |
|                         if loc.player in barren_hint_players:
 | |
|                             hint_area = get_hint_area(loc)
 | |
|                             items_by_region[loc.player][hint_area]['weight'] += 1
 | |
|                             if loc.item.advancement or loc.item.useful:
 | |
|                                 items_by_region[loc.player][hint_area]['is_barren'] = False
 | |
|                         if loc.player in woth_hint_players and loc.item.advancement:
 | |
|                             # Skip item at location and see if game is still beatable
 | |
|                             state = CollectionState(multiworld)
 | |
|                             state.locations_checked.add(loc)
 | |
|                             if not multiworld.can_beat_game(state):
 | |
|                                 multiworld.worlds[loc.player].required_locations.append(loc)
 | |
|             elif barren_hint_players or woth_hint_players:  # Check only relevant oot locations for barren/woth
 | |
|                 for player in (barren_hint_players | woth_hint_players):
 | |
|                     for loc in multiworld.worlds[player].get_locations():
 | |
|                         if loc.item.code and (not loc.locked or
 | |
|                             (oot_is_item_of_type(loc.item, 'Song') or
 | |
|                                 (oot_is_item_of_type(loc.item, 'SmallKey')         and multiworld.worlds[loc.player].shuffle_smallkeys     in ('overworld', 'any_dungeon', 'regional')) or
 | |
|                                 (oot_is_item_of_type(loc.item, 'HideoutSmallKey')  and multiworld.worlds[loc.player].shuffle_hideoutkeys   in ('overworld', 'any_dungeon', 'regional')) or
 | |
|                                 (oot_is_item_of_type(loc.item, 'BossKey')          and multiworld.worlds[loc.player].shuffle_bosskeys      in ('overworld', 'any_dungeon', 'regional')) or
 | |
|                                 (oot_is_item_of_type(loc.item, 'GanonBossKey')     and multiworld.worlds[loc.player].shuffle_ganon_bosskey in ('overworld', 'any_dungeon', 'regional')))):
 | |
|                             if player in barren_hint_players:
 | |
|                                 hint_area = get_hint_area(loc)
 | |
|                                 items_by_region[player][hint_area]['weight'] += 1
 | |
|                                 if loc.item.advancement or loc.item.useful:
 | |
|                                     items_by_region[player][hint_area]['is_barren'] = False
 | |
|                             if player in woth_hint_players and loc.item.advancement:
 | |
|                                 state = CollectionState(multiworld)
 | |
|                                 state.locations_checked.add(loc)
 | |
|                                 if not multiworld.can_beat_game(state):
 | |
|                                     multiworld.worlds[player].required_locations.append(loc)
 | |
|             for player in barren_hint_players:
 | |
|                 multiworld.worlds[player].empty_areas = {region: info for (region, info) in items_by_region[player].items()
 | |
|                                                     if info['is_barren']}
 | |
|         except Exception as e:
 | |
|             raise e
 | |
|         finally:
 | |
|             for autoworld in multiworld.get_game_worlds("Ocarina of Time"):
 | |
|                 autoworld.hint_data_available.set()
 | |
| 
 | |
| 
 | |
|     def fill_slot_data(self):
 | |
|         self.collectible_flags_available.wait()
 | |
| 
 | |
|         slot_data = {
 | |
|             'collectible_override_flags': self.collectible_override_flags,
 | |
|             'collectible_flag_offsets': self.collectible_flag_offsets
 | |
|         }
 | |
|         slot_data.update(self.options.as_dict(
 | |
|             "open_forest", "open_kakariko", "open_door_of_time", "zora_fountain", "gerudo_fortress",
 | |
|             "bridge", "bridge_stones", "bridge_medallions", "bridge_rewards", "bridge_tokens", "bridge_hearts",
 | |
|             "shuffle_ganon_bosskey", "ganon_bosskey_medallions", "ganon_bosskey_stones", "ganon_bosskey_rewards",
 | |
|             "ganon_bosskey_tokens", "ganon_bosskey_hearts", "trials",
 | |
|             "triforce_hunt", "triforce_goal", "extra_triforce_percentage",
 | |
|             "shopsanity", "shop_slots", "shopsanity_prices", "tokensanity",
 | |
|             "dungeon_shortcuts", "dungeon_shortcuts_list",
 | |
|             "mq_dungeons_mode", "mq_dungeons_list", "mq_dungeons_count",
 | |
|             "shuffle_interior_entrances", "shuffle_grotto_entrances", "shuffle_dungeon_entrances",
 | |
|             "shuffle_overworld_entrances", "shuffle_bosses", "key_rings", "key_rings_list", "enhance_map_compass",
 | |
|             "shuffle_mapcompass", "shuffle_smallkeys", "shuffle_hideoutkeys", "shuffle_bosskeys",
 | |
|             "logic_rules", "logic_no_night_tokens_without_suns_song", "logic_tricks",
 | |
|             "warp_songs", "shuffle_song_items","shuffle_medigoron_carpet_salesman", "shuffle_frog_song_rupees",
 | |
|             "shuffle_scrubs", "shuffle_child_trade", "shuffle_freestanding_items", "shuffle_pots", "shuffle_crates",
 | |
|             "shuffle_cows", "shuffle_beehives", "shuffle_kokiri_sword", "shuffle_ocarinas", "shuffle_gerudo_card",
 | |
|             "shuffle_beans", "starting_age", "bombchus_in_logic", "spawn_positions", "owl_drops",
 | |
|             "no_epona_race", "skip_some_minigame_phases", "complete_mask_quest", "free_scarecrow", "plant_beans",
 | |
|             "chicken_count", "big_poe_count", "fae_torch_count", "blue_fire_arrows",
 | |
|             "damage_multiplier", "deadly_bonks", "starting_tod", "junk_ice_traps",
 | |
|             "start_with_consumables", "adult_trade_start", "plando_connections"
 | |
|             )
 | |
|         )
 | |
|         return slot_data
 | |
| 
 | |
| 
 | |
|     def modify_multidata(self, multidata: dict):
 | |
| 
 | |
|         # Replace connect name
 | |
|         multidata['connect_names'][self.connect_name] = multidata['connect_names'][self.multiworld.player_name[self.player]]
 | |
| 
 | |
|         # Remove undesired items from start_inventory
 | |
|         # This is because we don't want them to show up in the autotracker,
 | |
|         # they just don't exist in-game.
 | |
|         for item_name in self.remove_from_start_inventory:
 | |
|             item_id = self.item_name_to_id.get(item_name, None)
 | |
|             if item_id is None:
 | |
|                 continue
 | |
|             multidata["precollected_items"][self.player].remove(item_id)
 | |
| 
 | |
|         # If skip child zelda, push item onto autotracker
 | |
|         if self.shuffle_child_trade == 'skip_child_zelda':
 | |
|             impa_item_id = self.item_name_to_id.get(self.get_location('Song from Impa').item.name, None)
 | |
|             zelda_item_id = self.item_name_to_id.get(self.get_location('HC Zeldas Letter').item.name, None)
 | |
|             if impa_item_id:
 | |
|                 multidata["precollected_items"][self.player].append(impa_item_id)
 | |
|             if zelda_item_id:
 | |
|                 multidata["precollected_items"][self.player].append(zelda_item_id)
 | |
| 
 | |
| 
 | |
|     def extend_hint_information(self, er_hint_data: dict):
 | |
| 
 | |
|         er_hint_data[self.player] = {}
 | |
| 
 | |
|         hint_entrances = set()
 | |
|         for entrance in entrance_shuffle_table:
 | |
|             if entrance[0] in {'Dungeon', 'DungeonSpecial', 'Interior', 'SpecialInterior', 'Grotto', 'Grave'}:
 | |
|                 hint_entrances.add(entrance[1][0])
 | |
| 
 | |
|         # Get main hint entrance to region.
 | |
|         # If the region is directly adjacent to a hint-entrance, we return that one.
 | |
|         # If it's in a dungeon, scan all the entrances for all the regions in the dungeon.
 | |
|         #   This should terminate on the first region anyway, but we scan everything to be safe.
 | |
|         # If it's one of the special cases, go one level deeper.
 | |
|         # If it's a boss room, go one level deeper to the boss door region, which is in a dungeon.
 | |
|         # Otherwise return None.
 | |
|         def get_entrance_to_region(region):
 | |
|             special_case_regions = {
 | |
|                 "Beyond Door of Time",
 | |
|                 "Kak Impas House Near Cow",
 | |
|             }
 | |
| 
 | |
|             for entrance in region.entrances:
 | |
|                 if entrance.name in hint_entrances:
 | |
|                     return entrance
 | |
|             if region.dungeon is not None:
 | |
|                 for r in region.dungeon.regions:
 | |
|                     for e in r.entrances:
 | |
|                         if e.name in hint_entrances:
 | |
|                             return e
 | |
|             if region.is_boss_room or region.name in special_case_regions:
 | |
|                 return get_entrance_to_region(region.entrances[0].parent_region)
 | |
|             return None
 | |
| 
 | |
|         if (self.shuffle_interior_entrances != 'off' or self.shuffle_dungeon_entrances
 | |
|             or self.shuffle_grotto_entrances or self.shuffle_bosses != 'off'):
 | |
|             for region in self.regions:
 | |
|                 if not any(bool(loc.address) for loc in region.locations): # check if region has any non-event locations
 | |
|                     continue
 | |
|                 main_entrance = get_entrance_to_region(region)
 | |
|                 if main_entrance is not None and (main_entrance.shuffled or (region.is_boss_room and self.shuffle_bosses != 'off')):
 | |
|                     for location in region.locations:
 | |
|                         if type(location.address) == int:
 | |
|                             er_hint_data[self.player][location.address] = main_entrance.name
 | |
|                             logger.debug(f"Set {location.name} hint data to {main_entrance.name}")
 | |
| 
 | |
| 
 | |
|     def write_spoiler(self, spoiler_handle: typing.TextIO) -> None:
 | |
|         required_trials_str = ", ".join(t for t in self.skipped_trials if not self.skipped_trials[t])
 | |
|         if required_trials_str == "":
 | |
|             required_trials_str = "None"
 | |
|         spoiler_handle.write(f"\n\nTrials ({self.multiworld.get_player_name(self.player)}): {required_trials_str}\n")
 | |
| 
 | |
|         if self.shopsanity != 'off':
 | |
|             spoiler_handle.write(f"\nShop Prices ({self.multiworld.get_player_name(self.player)}):\n")
 | |
|             for k, v in self.shop_prices.items():
 | |
|                 spoiler_handle.write(f"{k}: {v} Rupees\n")
 | |
| 
 | |
| 
 | |
|     # Key ring handling:
 | |
|     # Key rings are multiple items glued together into one, so we need to give
 | |
|     # the appropriate number of keys in the collection state when they are
 | |
|     # picked up.
 | |
|     def collect(self, state: CollectionState, item: OOTItem) -> bool:
 | |
|         state._oot_stale[self.player] = True
 | |
|         if item.advancement and item.special and item.special.get('alias', False):
 | |
|             alt_item_name, count = item.special.get('alias')
 | |
|             state.prog_items[self.player][alt_item_name] += count
 | |
|             return True
 | |
|         return super().collect(state, item)
 | |
| 
 | |
|     def remove(self, state: CollectionState, item: OOTItem) -> bool:
 | |
|         if item.advancement and item.special and item.special.get('alias', False):
 | |
|             alt_item_name, count = item.special.get('alias')
 | |
|             state.prog_items[self.player][alt_item_name] -= count
 | |
|             if state.prog_items[self.player][alt_item_name] < 1:
 | |
|                 del (state.prog_items[self.player][alt_item_name])
 | |
|             state._oot_stale[self.player] = True
 | |
|             return True
 | |
|         changed = super().remove(state, item)
 | |
|         if changed:
 | |
|             state._oot_stale[self.player] = True
 | |
|         return changed
 | |
| 
 | |
| 
 | |
|     # Helper functions
 | |
|     def region_has_shortcuts(self, regionname):
 | |
|         region = self.get_region(regionname)
 | |
|         try:
 | |
|             dungeon_name = HintArea.at(region).dungeon_name
 | |
|             return dungeon_name in self.dungeon_shortcuts
 | |
|         except HintAreaNotFound:
 | |
|             return False
 | |
| 
 | |
|     def get_shufflable_entrances(self, type=None, only_primary=False):
 | |
|         return [entrance for entrance in self.get_entrances() if ((type == None or entrance.type == type)
 | |
|             and (not only_primary or entrance.primary))]
 | |
| 
 | |
|     def get_shuffled_entrances(self, type=None, only_primary=False):
 | |
|         return [entrance for entrance in self.get_shufflable_entrances(type=type, only_primary=only_primary) if
 | |
|                 entrance.shuffled]
 | |
| 
 | |
|     def get_locations(self):
 | |
|         return self.multiworld.get_locations(self.player)
 | |
| 
 | |
|     def get_entrances(self):
 | |
|         return self.multiworld.get_entrances(self.player)
 | |
| 
 | |
|     def is_major_item(self, item: OOTItem):
 | |
|         if item.type == 'Token':
 | |
|             return self.bridge == 'tokens' or self.lacs_condition == 'tokens'
 | |
| 
 | |
|         if item.name in self.nonadvancement_items:
 | |
|             return True
 | |
| 
 | |
|         if item.type in ('Drop', 'Event', 'Shop', 'DungeonReward') or not item.advancement:
 | |
|             return False
 | |
| 
 | |
|         if item.name.startswith('Bombchus') and not self.bombchus_in_logic:
 | |
|             return False
 | |
| 
 | |
|         if item.type in ['Map', 'Compass']:
 | |
|             return False
 | |
|         if item.type == 'SmallKey' and self.shuffle_smallkeys in ['dungeon', 'vanilla']:
 | |
|             return False
 | |
|         if item.type == 'HideoutSmallKey' and self.shuffle_hideoutkeys == 'vanilla':
 | |
|             return False
 | |
|         if item.type == 'BossKey' and self.shuffle_bosskeys in ['dungeon', 'vanilla']:
 | |
|             return False
 | |
|         if item.type == 'GanonBossKey' and self.shuffle_ganon_bosskey in ['dungeon', 'vanilla']:
 | |
|             return False
 | |
| 
 | |
|         return True
 | |
| 
 | |
|     # Specifically ensures that only real items are gotten, not any events.
 | |
|     # In particular, ensures that Time Travel needs to be found.
 | |
|     def get_state_with_complete_itempool(self):
 | |
|         all_state = CollectionState(self.multiworld)
 | |
|         for item in self.itempool + self.pre_fill_items:
 | |
|             self.multiworld.worlds[item.player].collect(all_state, item)
 | |
|         # If free_scarecrow give Scarecrow Song
 | |
|         if self.free_scarecrow:
 | |
|             all_state.collect(self.create_item("Scarecrow Song"), prevent_sweep=True)
 | |
|         all_state._oot_stale[self.player] = True
 | |
| 
 | |
|         return all_state
 | |
| 
 | |
|     def get_filler_item_name(self) -> str:
 | |
|         return get_junk_item(self.random, count=1, pool=get_junk_pool(self))[0]
 | |
| 
 | |
| 
 | |
| def valid_dungeon_item_location(world: OOTWorld, option: str, dungeon: str, loc: OOTLocation) -> bool:
 | |
|     if option == 'dungeon':
 | |
|         return (getattr(loc.parent_region.dungeon, 'name', None) == dungeon
 | |
|             and (world.shuffle_song_items != 'dungeon' or loc.name not in dungeon_song_locations))
 | |
|     elif option == 'any_dungeon':
 | |
|         return (loc.parent_region.dungeon is not None
 | |
|             and (world.shuffle_song_items != 'dungeon' or loc.name not in dungeon_song_locations))
 | |
|     elif option == 'overworld':
 | |
|         return (loc.parent_region.dungeon is None
 | |
|             and (loc.type != 'Shop' or loc.name in world.shop_prices)
 | |
|             and (world.shuffle_song_items != 'song' or loc.type != 'Song')
 | |
|             and (world.shuffle_song_items != 'dungeon' or loc.name not in dungeon_song_locations))
 | |
|     elif option == 'regional':
 | |
|         color = HintArea.for_dungeon(dungeon).color
 | |
|         return (HintArea.at(loc).color == color
 | |
|             and (loc.type != 'Shop' or loc.name in world.shop_prices)
 | |
|             and (world.shuffle_song_items != 'song' or loc.type != 'Song')
 | |
|             and (world.shuffle_song_items != 'dungeon' or loc.name not in dungeon_song_locations))
 | |
|     return False
 | |
|     # raise ValueError(f'Unexpected argument to valid_dungeon_item_location: {option}')
 | |
| 
 | |
| 
 | |
| def gather_locations(multiworld: MultiWorld,
 | |
|     item_type: str,
 | |
|     players: Union[int, AbstractSet[int]],
 | |
|     dungeon: str = ''
 | |
| ) -> Optional[List[OOTLocation]]:
 | |
|     type_to_setting = {
 | |
|         'Map': 'shuffle_mapcompass',
 | |
|         'Compass': 'shuffle_mapcompass',
 | |
|         'SmallKey': 'shuffle_smallkeys',
 | |
|         'BossKey': 'shuffle_bosskeys',
 | |
|         'HideoutSmallKey': 'shuffle_hideoutkeys',
 | |
|         'GanonBossKey': 'shuffle_ganon_bosskey',
 | |
|     }
 | |
| 
 | |
|     # Special handling for atypical item types
 | |
|     if item_type == 'HideoutSmallKey':
 | |
|         dungeon = 'Thieves Hideout'
 | |
|     elif item_type == 'GanonBossKey':
 | |
|         dungeon = 'Ganons Castle'
 | |
| 
 | |
|     if isinstance(players, int):
 | |
|         players = {players}
 | |
|     fill_opts = {p: getattr(multiworld.worlds[p], type_to_setting[item_type]) for p in players}
 | |
|     locations = []
 | |
|     if any(map(lambda v: v == 'keysanity', fill_opts.values())):
 | |
|         return None
 | |
|     for player, option in fill_opts.items():
 | |
|         condition = functools.partial(valid_dungeon_item_location,
 | |
|             multiworld.worlds[player], option, dungeon)
 | |
|         locations += filter(condition, multiworld.get_unfilled_locations(player=player))
 | |
| 
 | |
|     return locations
 | |
| 
 | 
