From a992767b94d55137fbda0516c26580c4b147cbfa Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Tue, 24 Oct 2023 19:24:26 -0400 Subject: [PATCH 01/14] OoT: remove no-cache from generate_basic's all_state call It shouldn't need this? --- worlds/oot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index f794171661b8..5e5327427f9f 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -769,7 +769,7 @@ def generate_basic(self): # mostly killing locations that shouldn't exist by se # 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.multiworld.get_all_state(use_cache=True) + all_state = self.multiworld.get_all_state(True) all_locations = self.get_locations() reachable = self.multiworld.get_reachable_locations(all_state, self.player) unreachable = [loc for loc in all_locations if From 7a48cdd4327a71da62fae35cb1cf4c9c5e34cfa5 Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Tue, 24 Oct 2023 23:18:14 -0400 Subject: [PATCH 02/14] OoT: avoid recaches during create_regions --- worlds/oot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 5e5327427f9f..f794171661b8 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -769,7 +769,7 @@ def generate_basic(self): # mostly killing locations that shouldn't exist by se # 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.multiworld.get_all_state(True) + all_state = self.multiworld.get_all_state(use_cache=True) all_locations = self.get_locations() reachable = self.multiworld.get_reachable_locations(all_state, self.player) unreachable = [loc for loc in all_locations if From cb704b8eb9a3ef362bc9c1bbd03baf536d2b01fb Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Sat, 28 Oct 2023 22:00:43 -0400 Subject: [PATCH 03/14] OoT: prefill optimizations Split itempool into main and prefill pools Modify only prefill pool Faster state creation --- worlds/oot/Rules.py | 18 ++-- worlds/oot/__init__.py | 212 ++++++++++++++++++++++------------------- 2 files changed, 124 insertions(+), 106 deletions(-) diff --git a/worlds/oot/Rules.py b/worlds/oot/Rules.py index fa198e0ce10e..3da3728c5942 100644 --- a/worlds/oot/Rules.py +++ b/worlds/oot/Rules.py @@ -1,8 +1,12 @@ from collections import deque import logging +import typing from .Regions import TimeOfDay +from .DungeonList import dungeon_table +from .Hints import HintArea from .Items import oot_is_item_of_type +from .LocationList import dungeon_song_locations from BaseClasses import CollectionState from worlds.generic.Rules import set_rule, add_rule, add_item_rule, forbid_item @@ -150,11 +154,16 @@ def set_rules(ootworld): location = world.get_location('Forest Temple MQ First Room Chest', player) forbid_item(location, 'Boss Key (Forest Temple)', ootworld.player) - if ootworld.shuffle_song_items == 'song' and not ootworld.songs_as_items: + if ootworld.shuffle_song_items in {'song', 'dungeon'} and not ootworld.songs_as_items: # Sheik in Ice Cavern is the only song location in a dungeon; need to ensure that it cannot be anything else. # This is required if map/compass included, or any_dungeon shuffle. location = world.get_location('Sheik in Ice Cavern', player) - add_item_rule(location, lambda item: item.player == player and oot_is_item_of_type(item, 'Song')) + add_item_rule(location, lambda item: oot_is_item_of_type(item, 'Song')) + + if ootworld.shuffle_child_trade == 'skip_child_zelda': + # Song from Impa must be local + location = world.get_location('Song from Impa', player) + add_item_rule(location, lambda item: item.player == player) for name in ootworld.always_hints: add_rule(world.get_location(name, player), guarantee_hint) @@ -176,11 +185,6 @@ def required_wallets(price): return parser.parse_rule('(Progressive_Wallet, %d)' % required_wallets(location.price)) -def limit_to_itemset(location, itemset): - old_rule = location.item_rule - location.item_rule = lambda item: item.name in itemset and old_rule(item) - - # This function should be run once after the shop items are placed in the world. # It should be run before other items are placed in the world so that logic has # the correct checks for them. This is safe to do since every shop is still diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index f794171661b8..67db8e43df2f 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -170,15 +170,19 @@ class OOTWorld(World): 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): rom = Rom(file=get_options()['oot_options']['rom_file']) + + # Option parsing, handling incompatible options, building useful-item table def generate_early(self): self.parser = Rule_AST_Transformer(self, self.player) @@ -490,6 +494,8 @@ def generate_early(self): # 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) @@ -561,8 +567,9 @@ def load_regions_from_json(self, file_path): self.multiworld.regions.append(new_region) self.regions.append(new_region) self._regions_cache[new_region.name] = new_region - # self.multiworld._recache() + + # 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'}] @@ -591,6 +598,8 @@ def set_scrub_prices(self): 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 = {} @@ -616,6 +625,8 @@ def random_shop_prices(self): elif self.shopsanity_prices == 'tycoons_wallet': self.shop_prices[location.name] = self.multiworld.random.randrange(0,1000,5) + + # Fill boss prizes def fill_bosses(self, bossCount=9): boss_location_names = ( 'Queen Gohma', @@ -644,6 +655,44 @@ def fill_bosses(self, bossCount=9): 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, @@ -663,7 +712,9 @@ def make_event_item(self, name, location, item=None): location.internal = True return item - def create_regions(self): # create and link regions + + # 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: @@ -689,6 +740,8 @@ def create_regions(self): # create and link 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) @@ -714,12 +767,16 @@ def create_items(self): 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: @@ -757,6 +814,7 @@ def set_rules(self): 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 @@ -791,35 +849,63 @@ def generate_basic(self): # mostly killing locations that shouldn't exist by se 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_events() + return state + + # Prefill shops, songs, and dungeon items + items = self.get_pre_fill_items() + locations = list(self.multiworld.get_unfilled_locations(self.player)) + self.multiworld.random.shuffle(locations) + + # Set up initial state + state = CollectionState(self.multiworld) + for item in self.itempool: + self.collect(state, item) + state.sweep_for_events() + # Place dungeon items special_fill_types = ['GanonBossKey', 'BossKey', 'SmallKey', 'HideoutSmallKey', 'Map', 'Compass'] - world_items = [item for item in self.multiworld.itempool if item.player == self.player] + 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: 1 if getattr(self, type_to_setting[x]) == 'dungeon' else 0) + for fill_stage in special_fill_types: - stage_items = list(filter(lambda item: oot_is_item_of_type(item, fill_stage), world_items)) + 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.multiworld.itempool.remove(item) + self.pre_fill_items.remove(item) self.multiworld.random.shuffle(locations) - fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), locations, stage_items, + 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): - dungeon_items = list(filter(lambda item: dungeon_name in item.name, stage_items)) - if not dungeon_items: - continue for item in dungeon_items: - self.multiworld.itempool.remove(item) + self.pre_fill_items.remove(item) self.multiworld.random.shuffle(locations) - fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), locations, dungeon_items, + fill_restrictive(self.multiworld, prefill_state(state), locations, dungeon_items, single_player_placement=True, lock=True, allow_excluded=True) # Place songs @@ -835,9 +921,9 @@ def pre_fill(self): else: raise Exception(f"Unknown song shuffle type: {self.shuffle_song_items}") - songs = list(filter(lambda item: item.player == self.player and item.type == 'Song', self.multiworld.itempool)) + songs = list(filter(lambda item: item.type == 'Song', self.pre_fill_items)) for song in songs: - self.multiworld.itempool.remove(song) + 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) @@ -860,7 +946,7 @@ def pre_fill(self): while tries: try: self.multiworld.random.shuffle(song_locations) - fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), song_locations[:], songs[:], + 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: @@ -882,10 +968,8 @@ def pre_fill(self): # 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.player == self.player and item.type == 'Shop' - and item.advancement, self.multiworld.itempool)) - shop_junk = list(filter(lambda item: item.player == self.player and item.type == 'Shop' - and not item.advancement, self.multiworld.itempool)) + 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))) @@ -895,30 +979,14 @@ def pre_fill(self): 'Buy Zora Tunic': 1, }.get(item.name, 0)) # place Deku Shields if needed, then tunics, then other advancement self.multiworld.random.shuffle(shop_locations) - for item in shop_prog + shop_junk: - self.multiworld.itempool.remove(item) - fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), shop_locations, shop_prog, + 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 - # If skip child zelda is active and Song from Impa is unfilled, put a local giveable item into it. - impa = self.multiworld.get_location("Song from Impa", self.player) - if self.shuffle_child_trade == 'skip_child_zelda': - if impa.item is None: - candidate_items = list(item for item in self.multiworld.itempool if item.player == self.player) - if candidate_items: - item_to_place = self.multiworld.random.choice(candidate_items) - self.multiworld.itempool.remove(item_to_place) - else: - item_to_place = self.create_item("Recovery Heart") - impa.place_locked_item(item_to_place) - # Give items to startinventory - self.multiworld.push_precollected(impa.item) - self.multiworld.push_precollected(self.create_item("Zeldas Letter")) - # 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': @@ -953,49 +1021,6 @@ def pre_fill(self): or (self.shuffle_child_trade == 'skip_child_zelda' and loc.name in ['HC Zeldas Letter', 'Song from Impa'])): loc.address = None - # Handle item-linked dungeon items and songs - @classmethod - def stage_pre_fill(cls, multiworld: MultiWorld): - special_fill_types = ['Song', 'GanonBossKey', 'BossKey', 'SmallKey', 'HideoutSmallKey', 'Map', 'Compass'] - for group_id, group in multiworld.groups.items(): - if group['game'] != cls.game: - continue - group_items = [item for item in multiworld.itempool if item.player == group_id] - for fill_stage in special_fill_types: - group_stage_items = list(filter(lambda item: oot_is_item_of_type(item, fill_stage), group_items)) - if not group_stage_items: - continue - if fill_stage in ['Song', 'GanonBossKey', 'HideoutSmallKey']: - # No need to subdivide by dungeon name - locations = gather_locations(multiworld, fill_stage, group['players']) - if isinstance(locations, list): - for item in group_stage_items: - multiworld.itempool.remove(item) - multiworld.random.shuffle(locations) - fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_stage_items, - single_player_placement=False, lock=True, allow_excluded=True) - if fill_stage == 'Song': - # We don't want song locations to contain progression unless it's a song - # or it was marked as priority. - # We do this manually because we'd otherwise have to either - # iterate twice or do many function calls. - for loc in locations: - if loc.progress_type == LocationProgressType.DEFAULT: - loc.progress_type = LocationProgressType.EXCLUDED - add_item_rule(loc, lambda i: not (i.advancement or i.useful)) - else: - # Perform the fill task once per dungeon - for dungeon_info in dungeon_table: - dungeon_name = dungeon_info['name'] - locations = gather_locations(multiworld, fill_stage, group['players'], dungeon=dungeon_name) - if isinstance(locations, list): - group_dungeon_items = list(filter(lambda item: dungeon_name in item.name, group_stage_items)) - for item in group_dungeon_items: - multiworld.itempool.remove(item) - multiworld.random.shuffle(locations) - fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_dungeon_items, - single_player_placement=False, lock=True, allow_excluded=True) - def generate_output(self, output_directory: str): if self.hints != 'none': @@ -1304,9 +1329,8 @@ def is_major_item(self, item: OOTItem): # 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.multiworld.itempool: - if item.player == self.player: - self.multiworld.worlds[item.player].collect(all_state, item) + 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"), event=True) @@ -1346,7 +1370,6 @@ def gather_locations(multiworld: MultiWorld, dungeon: str = '' ) -> Optional[List[OOTLocation]]: type_to_setting = { - 'Song': 'shuffle_song_items', 'Map': 'shuffle_mapcompass', 'Compass': 'shuffle_mapcompass', 'SmallKey': 'shuffle_smallkeys', @@ -1365,21 +1388,12 @@ def gather_locations(multiworld: MultiWorld, players = {players} fill_opts = {p: getattr(multiworld.worlds[p], type_to_setting[item_type]) for p in players} locations = [] - if item_type == 'Song': - if any(map(lambda v: v == 'any', fill_opts.values())): - return None - for player, option in fill_opts.items(): - if option == 'song': - condition = lambda location: location.type == 'Song' - elif option == 'dungeon': - condition = lambda location: location.name in dungeon_song_locations - locations += filter(condition, multiworld.get_unfilled_locations(player=player)) - else: - 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)) + 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 + From dec1584ba215c616c7c4b16e0cb9a2ac516d10a6 Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Sat, 28 Oct 2023 22:21:24 -0400 Subject: [PATCH 04/14] OoT: optimize entrance randomizer??? I hope??? --- worlds/oot/EntranceShuffle.py | 8 +++----- worlds/oot/__init__.py | 3 ++- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/worlds/oot/EntranceShuffle.py b/worlds/oot/EntranceShuffle.py index 3c1b2d78c6c9..adabefce68ae 100644 --- a/worlds/oot/EntranceShuffle.py +++ b/worlds/oot/EntranceShuffle.py @@ -2,6 +2,7 @@ import logging from worlds.generic.Rules import set_rule, add_rule +from BaseClasses import CollectionState from .Hints import get_hint_area, HintAreaNotFound from .Regions import TimeOfDay @@ -542,10 +543,7 @@ def shuffle_random_entrances(ootworld): # Build all_state and none_state all_state = ootworld.get_state_with_complete_itempool() - none_state = all_state.copy() - for item_tuple in none_state.prog_items: - if item_tuple[1] == player: - none_state.prog_items[item_tuple] = 0 + none_state = CollectionState(ootworld.multiworld) # Plando entrances if world.plando_connections[player]: @@ -628,7 +626,7 @@ def shuffle_random_entrances(ootworld): logging.getLogger('').error(f'Root Exit: {exit} -> {exit.connected_region}') logging.getLogger('').error(f'Root has too many entrances left after shuffling entrances') # Game is beatable - new_all_state = world.get_all_state(use_cache=False) + new_all_state = ootworld.get_state_with_complete_itempool() if not world.has_beaten_game(new_all_state, player): raise EntranceShuffleError('Cannot beat game') # Validate world diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 67db8e43df2f..b547be8a8ce5 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -827,7 +827,8 @@ def generate_basic(self): # mostly killing locations that shouldn't exist by se # 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.multiworld.get_all_state(use_cache=True) + all_state = self.get_state_with_complete_itempool() + all_state.sweep_for_events() all_locations = self.get_locations() reachable = self.multiworld.get_reachable_locations(all_state, self.player) unreachable = [loc for loc in all_locations if From df7413305e592db9476af6275e5885b5bbeee4f0 Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Sun, 29 Oct 2023 12:50:17 -0400 Subject: [PATCH 05/14] OoT: Entrance randomizer time no longer grows significantly with multiworld size --- worlds/oot/EntranceShuffle.py | 26 +++++++++++++------------- worlds/oot/__init__.py | 20 ++++++++++++++++---- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/worlds/oot/EntranceShuffle.py b/worlds/oot/EntranceShuffle.py index adabefce68ae..8d721f7de033 100644 --- a/worlds/oot/EntranceShuffle.py +++ b/worlds/oot/EntranceShuffle.py @@ -424,14 +424,14 @@ def _add_boss_entrances(): } interior_entrance_bias = { - 'Kakariko Village -> Kak Potion Shop Front': 4, - 'Kak Backyard -> Kak Potion Shop Back': 4, - 'Kakariko Village -> Kak Impas House': 3, - 'Kak Impas Ledge -> Kak Impas House Back': 3, - 'Goron City -> GC Shop': 2, - 'Zoras Domain -> ZD Shop': 2, + 'ToT Entrance -> Temple of Time': 4, + 'Kakariko Village -> Kak Potion Shop Front': 3, + 'Kak Backyard -> Kak Potion Shop Back': 3, + 'Kakariko Village -> Kak Impas House': 2, + 'Kak Impas Ledge -> Kak Impas House Back': 2, 'Market Entrance -> Market Guard House': 2, - 'ToT Entrance -> Temple of Time': 1, + 'Goron City -> GC Shop': 1, + 'Zoras Domain -> ZD Shop': 1, } @@ -444,7 +444,8 @@ def shuffle_random_entrances(ootworld): player = ootworld.player # Gather locations to keep reachable for validation - all_state = world.get_all_state(use_cache=True) + all_state = ootworld.get_state_with_complete_itempool() + all_state.sweep_for_events(locations=ootworld.get_events()) locations_to_ensure_reachable = {loc for loc in world.get_reachable_locations(all_state, player) if not (loc.type == 'Drop' or (loc.type == 'Event' and 'Subrule' in loc.name))} # Set entrance data for all entrances @@ -698,7 +699,7 @@ def place_one_way_priority_entrance(ootworld, priority_name, allowed_regions, al raise EntranceShuffleError(f'Unable to place priority one-way entrance for {priority_name} in world {ootworld.player}') -def shuffle_entrance_pool(ootworld, pool_type, entrance_pool, target_entrances, locations_to_ensure_reachable, all_state, none_state, check_all=False, retry_count=20): +def shuffle_entrance_pool(ootworld, pool_type, entrance_pool, target_entrances, locations_to_ensure_reachable, all_state, none_state, check_all=False, retry_count=10): restrictive_entrances, soft_entrances = split_entrances_by_requirements(ootworld, entrance_pool, target_entrances) @@ -743,7 +744,6 @@ def shuffle_entrances(ootworld, pool_type, entrances, target_entrances, rollback def split_entrances_by_requirements(ootworld, entrances_to_split, assumed_entrances): - world = ootworld.multiworld player = ootworld.player # Disconnect all root assumed entrances and save original connections @@ -753,7 +753,7 @@ def split_entrances_by_requirements(ootworld, entrances_to_split, assumed_entran if entrance.connected_region: original_connected_regions[entrance] = entrance.disconnect() - all_state = world.get_all_state(use_cache=False) + all_state = ootworld.get_state_with_complete_itempool() restrictive_entrances = [] soft_entrances = [] @@ -791,8 +791,8 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all all_state = all_state_orig.copy() none_state = none_state_orig.copy() - all_state.sweep_for_events() - none_state.sweep_for_events() + all_state.sweep_for_events(locations=ootworld.get_events()) + none_state.sweep_for_events(locations=ootworld.get_events()) if ootworld.shuffle_interior_entrances or ootworld.shuffle_overworld_entrances or ootworld.spawn_positions: time_travel_state = none_state.copy() diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index b547be8a8ce5..861b17151fd7 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -198,9 +198,10 @@ def generate_early(self): option_value = result.current_key setattr(self, option_name, option_value) - self.shop_prices = {} 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 @@ -1274,17 +1275,20 @@ def region_has_shortcuts(self, regionname): return False def get_shufflable_entrances(self, type=None, only_primary=False): - return [entrance for entrance in self.multiworld.get_entrances(self.player) if ( - (type == None or entrance.type == type) and (not only_primary or entrance.primary))] + 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] + @functools.cache def get_locations(self): + locations = [] for region in self.regions: for loc in region.locations: - yield loc + locations.append(loc) + return locations def get_location(self, location): return self.multiworld.get_location(location, self.player) @@ -1297,9 +1301,17 @@ def get_region(self, region_name): self._regions_cache[region_name] = ret return ret + @functools.cache + def get_entrances(self): + return [entrance for entrance in self.multiworld.get_entrances() if entrance.player == self.player] + def get_entrance(self, entrance): return self.multiworld.get_entrance(entrance, self.player) + @functools.cache + def get_events(self): + return [loc for loc in self.get_locations() if loc.event] + def is_major_item(self, item: OOTItem): if item.type == 'Token': return self.bridge == 'tokens' or self.lacs_condition == 'tokens' From 8e4680acdb02d4f0610f39aaca94c2fe745acd86 Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Sun, 29 Oct 2023 12:57:01 -0400 Subject: [PATCH 06/14] OoT: use event cache during prefill sweeps --- worlds/oot/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 861b17151fd7..022df7ed9414 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -858,7 +858,7 @@ def prefill_state(base_state): state = base_state.copy() for item in self.get_pre_fill_items(): self.collect(state, item) - state.sweep_for_events() + state.sweep_for_events(self.get_events()) return state # Prefill shops, songs, and dungeon items @@ -870,7 +870,7 @@ def prefill_state(base_state): state = CollectionState(self.multiworld) for item in self.itempool: self.collect(state, item) - state.sweep_for_events() + state.sweep_for_events(self.get_events()) # Place dungeon items special_fill_types = ['GanonBossKey', 'BossKey', 'SmallKey', 'HideoutSmallKey', 'Map', 'Compass'] From a5d2a58993e79cc25ca65462199715279fc22019 Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Sun, 29 Oct 2023 13:09:08 -0400 Subject: [PATCH 07/14] OoT: recache locations only once after generate_basic Eventually we want to remove the full recache and just delete them from the cache directly, but I will save that for after #2366 --- worlds/oot/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 022df7ed9414..c2f682171895 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -851,6 +851,14 @@ def generate_basic(self): # mostly killing locations that shouldn't exist by se loc = self.multiworld.get_location("Deliver Rutos Letter", self.player) loc.parent_region.locations.remove(loc) + @classmethod + def stage_generate_basic(cls, multiworld: MultiWorld): + # This is cleanup from all OoTWorld.generate_basic because we deleted locations. + # We only actually have to clean the cache once. + # TODO: when #2366 is merged, change how we remove these locations from the cache, + # hopefully we can avoid a full recache + multiworld.clear_location_cache() + def pre_fill(self): From 4ef44a63b7aad3f1d65665d7b5d9fdd6cb000901 Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Sun, 29 Oct 2023 13:17:38 -0400 Subject: [PATCH 08/14] 0 is less than 1 and not the other way around. --- worlds/oot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index c2f682171895..0eb2a775bc0e 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -890,7 +890,7 @@ def prefill_state(base_state): 'HideoutSmallKey': 'shuffle_hideoutkeys', 'GanonBossKey': 'shuffle_ganon_bosskey', } - special_fill_types.sort(key=lambda x: 1 if getattr(self, type_to_setting[x]) == 'dungeon' else 0) + 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)) From b9b2ce6ded8d91a1978b8467d83e2680ee5b65d3 Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Sun, 29 Oct 2023 15:20:56 -0400 Subject: [PATCH 09/14] OoT: no longer needed to clear cache manually --- worlds/oot/Entrance.py | 2 -- worlds/oot/__init__.py | 8 -------- 2 files changed, 10 deletions(-) diff --git a/worlds/oot/Entrance.py b/worlds/oot/Entrance.py index e480c957a672..e33765e72121 100644 --- a/worlds/oot/Entrance.py +++ b/worlds/oot/Entrance.py @@ -1,6 +1,4 @@ - from BaseClasses import Entrance -from .Regions import TimeOfDay class OOTEntrance(Entrance): game: str = 'Ocarina of Time' diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 0eb2a775bc0e..0f7f965f227b 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -851,14 +851,6 @@ def generate_basic(self): # mostly killing locations that shouldn't exist by se loc = self.multiworld.get_location("Deliver Rutos Letter", self.player) loc.parent_region.locations.remove(loc) - @classmethod - def stage_generate_basic(cls, multiworld: MultiWorld): - # This is cleanup from all OoTWorld.generate_basic because we deleted locations. - # We only actually have to clean the cache once. - # TODO: when #2366 is merged, change how we remove these locations from the cache, - # hopefully we can avoid a full recache - multiworld.clear_location_cache() - def pre_fill(self): From 5969024a0feee64aaa718a2db83ebc66e9fb1c97 Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Sun, 29 Oct 2023 16:11:56 -0400 Subject: [PATCH 10/14] OoT: all entrances used in ER gain unique name Prevents new entrance cache from breaking --- worlds/oot/Entrance.py | 8 ++++---- worlds/oot/EntranceShuffle.py | 19 ++++++++++--------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/worlds/oot/Entrance.py b/worlds/oot/Entrance.py index e33765e72121..6c4b6428f53e 100644 --- a/worlds/oot/Entrance.py +++ b/worlds/oot/Entrance.py @@ -27,16 +27,16 @@ def disconnect(self): self.connected_region = None return previously_connected - def get_new_target(self): + def get_new_target(self, pool_type): root = self.multiworld.get_region('Root Exits', self.player) - target_entrance = OOTEntrance(self.player, self.multiworld, 'Root -> ' + self.connected_region.name, root) + target_entrance = OOTEntrance(self.player, self.multiworld, f'Root -> ({self.name}) ({pool_type})', root) target_entrance.connect(self.connected_region) target_entrance.replaces = self root.exits.append(target_entrance) return target_entrance - def assume_reachable(self): + def assume_reachable(self, pool_type): if self.assumed == None: - self.assumed = self.get_new_target() + self.assumed = self.get_new_target(pool_type) self.disconnect() return self.assumed diff --git a/worlds/oot/EntranceShuffle.py b/worlds/oot/EntranceShuffle.py index 8d721f7de033..5caff186d84c 100644 --- a/worlds/oot/EntranceShuffle.py +++ b/worlds/oot/EntranceShuffle.py @@ -26,12 +26,12 @@ def set_all_entrances_data(world, player): return_entrance.data['index'] = 0x7FFF -def assume_entrance_pool(entrance_pool, ootworld): +def assume_entrance_pool(entrance_pool, ootworld, pool_type): assumed_pool = [] for entrance in entrance_pool: - assumed_forward = entrance.assume_reachable() + assumed_forward = entrance.assume_reachable(pool_type) if entrance.reverse != None and not ootworld.decouple_entrances: - assumed_return = entrance.reverse.assume_reachable() + assumed_return = entrance.reverse.assume_reachable(pool_type) if not (ootworld.mix_entrance_pools != 'off' and (ootworld.shuffle_overworld_entrances or ootworld.shuffle_special_interior_entrances)): if (entrance.type in ('Dungeon', 'Grotto', 'Grave') and entrance.reverse.name != 'Spirit Temple Lobby -> Desert Colossus From Spirit Lobby') or \ (entrance.type == 'Interior' and ootworld.shuffle_special_interior_entrances): @@ -42,15 +42,15 @@ def assume_entrance_pool(entrance_pool, ootworld): return assumed_pool -def build_one_way_targets(world, types_to_include, exclude=(), target_region_names=()): +def build_one_way_targets(world, pool, types_to_include, exclude=(), target_region_names=()): one_way_entrances = [] for pool_type in types_to_include: one_way_entrances += world.get_shufflable_entrances(type=pool_type) valid_one_way_entrances = list(filter(lambda entrance: entrance.name not in exclude, one_way_entrances)) if target_region_names: - return [entrance.get_new_target() for entrance in valid_one_way_entrances + return [entrance.get_new_target(pool) for entrance in valid_one_way_entrances if entrance.connected_region.name in target_region_names] - return [entrance.get_new_target() for entrance in valid_one_way_entrances] + return [entrance.get_new_target(pool) for entrance in valid_one_way_entrances] # Abbreviations @@ -525,12 +525,12 @@ def shuffle_random_entrances(ootworld): for pool_type, entrance_pool in one_way_entrance_pools.items(): if pool_type == 'OwlDrop': valid_target_types = ('WarpSong', 'OwlDrop', 'Overworld', 'Extra') - one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, valid_target_types, exclude=['Prelude of Light Warp -> Temple of Time']) + one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, pool_type, valid_target_types, exclude=['Prelude of Light Warp -> Temple of Time']) for target in one_way_target_entrance_pools[pool_type]: set_rule(target, lambda state: state._oot_reach_as_age(target.parent_region, 'child', player)) elif pool_type in {'Spawn', 'WarpSong'}: valid_target_types = ('Spawn', 'WarpSong', 'OwlDrop', 'Overworld', 'Interior', 'SpecialInterior', 'Extra') - one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, valid_target_types) + one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, pool_type, valid_target_types) # Ensure that the last entrance doesn't assume the rest of the targets are reachable for target in one_way_target_entrance_pools[pool_type]: add_rule(target, (lambda entrances=entrance_pool: (lambda state: any(entrance.connected_region == None for entrance in entrances)))()) @@ -540,7 +540,7 @@ def shuffle_random_entrances(ootworld): target_entrance_pools = {} for pool_type, entrance_pool in entrance_pools.items(): - target_entrance_pools[pool_type] = assume_entrance_pool(entrance_pool, ootworld) + target_entrance_pools[pool_type] = assume_entrance_pool(entrance_pool, ootworld, pool_type) # Build all_state and none_state all_state = ootworld.get_state_with_complete_itempool() @@ -958,6 +958,7 @@ def confirm_replacement(entrance, target): def delete_target_entrance(target): + print(f"Deleting entrance: {target.name}") if target.connected_region != None: target.disconnect() if target.parent_region != None: From bea20af2eeeba38645487ab76e46f914e13d51c2 Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Sun, 29 Oct 2023 16:15:31 -0400 Subject: [PATCH 11/14] remove debug print oops --- worlds/oot/EntranceShuffle.py | 1 - 1 file changed, 1 deletion(-) diff --git a/worlds/oot/EntranceShuffle.py b/worlds/oot/EntranceShuffle.py index 5caff186d84c..660693624f27 100644 --- a/worlds/oot/EntranceShuffle.py +++ b/worlds/oot/EntranceShuffle.py @@ -958,7 +958,6 @@ def confirm_replacement(entrance, target): def delete_target_entrance(target): - print(f"Deleting entrance: {target.name}") if target.connected_region != None: target.disconnect() if target.parent_region != None: From 7ee2cca2ac48065a043b73a0ee69662a4d247fcb Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Sun, 29 Oct 2023 18:41:06 -0400 Subject: [PATCH 12/14] OoT: use AP caches instead goodbye event cache --- worlds/oot/__init__.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 0f7f965f227b..fc8e11bf5382 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -858,7 +858,7 @@ def prefill_state(base_state): state = base_state.copy() for item in self.get_pre_fill_items(): self.collect(state, item) - state.sweep_for_events(self.get_events()) + state.sweep_for_events(self.get_locations()) return state # Prefill shops, songs, and dungeon items @@ -870,7 +870,7 @@ def prefill_state(base_state): state = CollectionState(self.multiworld) for item in self.itempool: self.collect(state, item) - state.sweep_for_events(self.get_events()) + state.sweep_for_events(self.get_locations()) # Place dungeon items special_fill_types = ['GanonBossKey', 'BossKey', 'SmallKey', 'HideoutSmallKey', 'Map', 'Compass'] @@ -1282,13 +1282,8 @@ 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] - @functools.cache def get_locations(self): - locations = [] - for region in self.regions: - for loc in region.locations: - locations.append(loc) - return locations + return self.multiworld.get_locations(self.player) def get_location(self, location): return self.multiworld.get_location(location, self.player) @@ -1301,17 +1296,12 @@ def get_region(self, region_name): self._regions_cache[region_name] = ret return ret - @functools.cache def get_entrances(self): - return [entrance for entrance in self.multiworld.get_entrances() if entrance.player == self.player] + return self.multiworld.get_entrances(self.player) def get_entrance(self, entrance): return self.multiworld.get_entrance(entrance, self.player) - @functools.cache - def get_events(self): - return [loc for loc in self.get_locations() if loc.event] - def is_major_item(self, item: OOTItem): if item.type == 'Token': return self.bridge == 'tokens' or self.lacs_condition == 'tokens' From fc84b6a027b15de60f1d45e41df85c652e9e3980 Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Sun, 29 Oct 2023 18:47:30 -0400 Subject: [PATCH 13/14] OoT: also remove get_events from EntranceShuffle --- worlds/oot/EntranceShuffle.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/worlds/oot/EntranceShuffle.py b/worlds/oot/EntranceShuffle.py index 660693624f27..bbdc30490c18 100644 --- a/worlds/oot/EntranceShuffle.py +++ b/worlds/oot/EntranceShuffle.py @@ -445,7 +445,7 @@ def shuffle_random_entrances(ootworld): # Gather locations to keep reachable for validation all_state = ootworld.get_state_with_complete_itempool() - all_state.sweep_for_events(locations=ootworld.get_events()) + all_state.sweep_for_events(locations=ootworld.get_locations()) locations_to_ensure_reachable = {loc for loc in world.get_reachable_locations(all_state, player) if not (loc.type == 'Drop' or (loc.type == 'Event' and 'Subrule' in loc.name))} # Set entrance data for all entrances @@ -791,8 +791,8 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all all_state = all_state_orig.copy() none_state = none_state_orig.copy() - all_state.sweep_for_events(locations=ootworld.get_events()) - none_state.sweep_for_events(locations=ootworld.get_events()) + all_state.sweep_for_events(locations=ootworld.get_locations()) + none_state.sweep_for_events(locations=ootworld.get_locations()) if ootworld.shuffle_interior_entrances or ootworld.shuffle_overworld_entrances or ootworld.spawn_positions: time_travel_state = none_state.copy() From ce02e78d284054582130eb16bb4162748860c2aa Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Sun, 29 Oct 2023 20:57:52 -0400 Subject: [PATCH 14/14] OoT: skip-child-zelda Impa item shows up on tracker again --- worlds/oot/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index fc8e11bf5382..865ad125452e 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -1161,6 +1161,15 @@ def modify_multidata(self, multidata: dict): 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):