diff --git a/worlds/oot/EntranceShuffle.py b/worlds/oot/EntranceShuffle.py index ff88395a..1a8b4f2d 100644 --- a/worlds/oot/EntranceShuffle.py +++ b/worlds/oot/EntranceShuffle.py @@ -413,14 +413,7 @@ def shuffle_random_entrances(ootworld): target_entrance_pools[pool_type] = assume_entrance_pool(entrance_pool, ootworld) # Build all_state and none_state - all_state = world.get_all_state(use_cache=False) - all_state.child_reachable_regions[player] = set() - all_state.adult_reachable_regions[player] = set() - all_state.child_blocked_connections[player] = set() - all_state.adult_blocked_connections[player] = set() - all_state.day_reachable_regions[player] = set() - all_state.dampe_reachable_regions[player] = set() - all_state.stale[player] = True + 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: @@ -529,7 +522,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, entrance_pool, target_entrances, locations_to_ensure_reachable, all_state, none_state, check_all=False, retry_count=100): +def shuffle_entrance_pool(ootworld, entrance_pool, target_entrances, locations_to_ensure_reachable, all_state, none_state, check_all=False, retry_count=20): restrictive_entrances, soft_entrances = split_entrances_by_requirements(ootworld, entrance_pool, target_entrances) @@ -618,6 +611,9 @@ 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() + if ootworld.shuffle_interior_entrances or ootworld.shuffle_overworld_entrances or ootworld.spawn_positions: time_travel_state = none_state.copy() time_travel_state.collect(ootworld.create_item('Time Travel'), event=True) diff --git a/worlds/oot/Options.py b/worlds/oot/Options.py index f550586c..ee441410 100644 --- a/worlds/oot/Options.py +++ b/worlds/oot/Options.py @@ -163,10 +163,10 @@ class MQDungeons(Range): world_options: typing.Dict[str, type(Option)] = { "starting_age": StartingAge, - # "shuffle_interior_entrances": InteriorEntrances, + "shuffle_interior_entrances": InteriorEntrances, "shuffle_grotto_entrances": GrottoEntrances, "shuffle_dungeon_entrances": DungeonEntrances, - # "shuffle_overworld_entrances": OverworldEntrances, + "shuffle_overworld_entrances": OverworldEntrances, "owl_drops": OwlDrops, "warp_songs": WarpSongs, "spawn_positions": SpawnPositions, diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index f03e5532..9a3e4da0 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -7,7 +7,7 @@ logger = logging.getLogger("Ocarina of Time") from .Location import OOTLocation, LocationFactory, location_name_to_id from .Entrance import OOTEntrance -from .EntranceShuffle import shuffle_random_entrances, entrance_shuffle_table +from .EntranceShuffle import shuffle_random_entrances, entrance_shuffle_table, EntranceShuffleError from .Items import OOTItem, item_table, oot_data_to_ap_id from .ItemPool import generate_itempool, add_dungeon_items, get_junk_item, get_junk_pool from .Regions import OOTRegion, TimeOfDay @@ -186,8 +186,6 @@ class OOTWorld(World): self.mq_dungeons_random = False # this will be a deprecated option later self.ocarina_songs = False # just need to pull in the OcarinaSongs module self.big_poe_count = 1 # disabled due to client-side issues for now - self.shuffle_interior_entrances = 'off' - self.shuffle_overworld_entrances = False # disabled due to stability issues # Set internal names used by the OoT generator self.keysanity = self.shuffle_smallkeys in ['keysanity', 'remove', 'any_dungeon', 'overworld'] @@ -487,7 +485,35 @@ class OOTWorld(World): def set_rules(self): # This has to run AFTER creating items but BEFORE set_entrances_based_rules if self.entrance_shuffle: - shuffle_random_entrances(self) + # 10 attempts at shuffling entrances + tries = 10 + while tries: + try: + shuffle_random_entrances(self) + except EntranceShuffleError as e: + tries -= 1 + logging.getLogger('').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(): + entrance.connect(self.world.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 + + # Write entrances to spoiler log all_entrances = self.get_shuffled_entrances() all_entrances.sort(key=lambda x: x.name) all_entrances.sort(key=lambda x: x.type) @@ -663,7 +689,6 @@ class OOTWorld(World): fill_restrictive(self.world, self.world.get_all_state(False), song_locations[:], songs[:], True, True) logger.debug(f"Successfully placed songs for player {self.player} after {6 - tries} attempt(s)") - tries = 0 except FillError as e: tries -= 1 if tries == 0: @@ -677,6 +702,8 @@ class OOTWorld(World): location.item = None location.locked = False location.event = 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 @@ -912,3 +939,26 @@ class OOTWorld(World): 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 = self.world.get_all_state(use_cache=False) + # Remove event progression items + for item, player in all_state.prog_items: + if (item not in item_table or item_table[item][2] is None) and player == self.player: + all_state.prog_items[(item, player)] = 0 + # Remove all events and checked locations + all_state.locations_checked = {loc for loc in all_state.locations_checked if loc.player != self.player} + all_state.events = {loc for loc in all_state.events if loc.player != self.player} + + # Invalidate caches + all_state.child_reachable_regions[self.player] = set() + all_state.adult_reachable_regions[self.player] = set() + all_state.child_blocked_connections[self.player] = set() + all_state.adult_blocked_connections[self.player] = set() + all_state.day_reachable_regions[self.player] = set() + all_state.dampe_reachable_regions[self.player] = set() + all_state.stale[self.player] = True + + return all_state