From f9e1db41e9d461cd476e6a2d1d28a7edb22f984d Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Sun, 14 Nov 2021 07:30:40 -0600 Subject: [PATCH] OoT: implement decoupled entrance pools --- worlds/oot/EntranceShuffle.py | 43 ++++++++++++++++++++++------------- worlds/oot/__init__.py | 32 ++++++++++++++------------ 2 files changed, 45 insertions(+), 30 deletions(-) diff --git a/worlds/oot/EntranceShuffle.py b/worlds/oot/EntranceShuffle.py index f0313141..a53354c8 100644 --- a/worlds/oot/EntranceShuffle.py +++ b/worlds/oot/EntranceShuffle.py @@ -29,7 +29,7 @@ def assume_entrance_pool(entrance_pool, ootworld): assumed_pool = [] for entrance in entrance_pool: assumed_forward = entrance.assume_reachable() - if entrance.reverse != None: + if entrance.reverse != None and not ootworld.decouple_entrances: assumed_return = entrance.reverse.assume_reachable() 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 \ @@ -309,6 +309,8 @@ entrance_shuffle_table = [ ('Overworld', ('ZD Behind King Zora -> Zoras Fountain', { 'index': 0x0225 }), ('Zoras Fountain -> ZD Behind King Zora', { 'index': 0x01A1 })), + ('Overworld', ('GV Lower Stream -> Lake Hylia', { 'index': 0x0219 })), + ('OwlDrop', ('LH Owl Flight -> Hyrule Field', { 'index': 0x027E, 'addresses': [0xAC9F26] })), ('OwlDrop', ('DMT Owl Flight -> Kak Impas Rooftop', { 'index': 0x0554, 'addresses': [0xAC9EF2] })), @@ -377,16 +379,24 @@ def shuffle_random_entrances(ootworld): entrance_pools['Dungeon'] = ootworld.get_shufflable_entrances(type='Dungeon', only_primary=True) if ootworld.open_forest == 'closed': entrance_pools['Dungeon'].remove(world.get_entrance('KF Outside Deku Tree -> Deku Tree Lobby', player)) + if ootworld.decouple_entrances: + entrance_pools['DungeonReverse'] = [entrance.reverse for entrance in entrance_pools['Dungeon']] if ootworld.shuffle_interior_entrances != 'off': entrance_pools['Interior'] = ootworld.get_shufflable_entrances(type='Interior', only_primary=True) if ootworld.shuffle_special_interior_entrances: entrance_pools['Interior'] += ootworld.get_shufflable_entrances(type='SpecialInterior', only_primary=True) + if ootworld.decouple_entrances: + entrance_pools['InteriorReverse'] = [entrance.reverse for entrance in entrance_pools['Interior']] if ootworld.shuffle_grotto_entrances: entrance_pools['GrottoGrave'] = ootworld.get_shufflable_entrances(type='Grotto', only_primary=True) entrance_pools['GrottoGrave'] += ootworld.get_shufflable_entrances(type='Grave', only_primary=True) + if ootworld.decouple_entrances: + entrance_pools['GrottoGraveReverse'] = [entrance.reverse for entrance in entrance_pools['GrottoGrave']] if ootworld.shuffle_overworld_entrances: exclude_overworld_reverse = ootworld.mix_entrance_pools == 'all' and not ootworld.decouple_entrances entrance_pools['Overworld'] = ootworld.get_shufflable_entrances(type='Overworld', only_primary=exclude_overworld_reverse) + if not ootworld.decouple_entrances: + entrance_pools['Overworld'].remove(world.get_entrance('GV Lower Stream -> Lake Hylia', player)) # Mark shuffled entrances for entrance in chain(chain.from_iterable(one_way_entrance_pools.values()), chain.from_iterable(entrance_pools.values())): @@ -631,24 +641,25 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all time_travel_state.collect(ootworld.create_item('Time Travel'), event=True) time_travel_state._oot_update_age_reachable_regions(player) - # For various reasons, we don't want the player to end up through certain entrances as the wrong age + # Unless entrances are decoupled, we don't want the player to end up through certain entrances as the wrong age # This means we need to hard check that none of the relevant entrances are ever reachable as that age # This is mostly relevant when shuffling special interiors (such as windmill or kak potion shop) # Warp Songs and Overworld Spawns can also end up inside certain indoors so those need to be handled as well CHILD_FORBIDDEN = ['OGC Great Fairy Fountain -> Castle Grounds', 'GV Carpenter Tent -> GV Fortress Side'] ADULT_FORBIDDEN = ['HC Great Fairy Fountain -> Castle Grounds', 'HC Storms Grotto -> Castle Grounds'] - for entrance in ootworld.get_shufflable_entrances(): - if entrance.shuffled and entrance.replaces: - if entrance.replaces.name in CHILD_FORBIDDEN and not entrance_unreachable_as(entrance, 'child', already_checked=[entrance.replaces.reverse]): - raise EntranceShuffleError(f'{entrance.replaces.name} replaced by an entrance with potential child access') - if entrance.replaces.name in ADULT_FORBIDDEN and not entrance_unreachable_as(entrance, 'adult', already_checked=[entrance.replaces.reverse]): - raise EntranceShuffleError(f'{entrance.replaces.name} replaced by an entrance with potential adult access') - else: - if entrance.name in CHILD_FORBIDDEN and not entrance_unreachable_as(entrance, 'child', already_checked=[entrance.reverse]): - raise EntranceShuffleError(f'{entrance.name} potentially accessible as child') - if entrance.name in ADULT_FORBIDDEN and not entrance_unreachable_as(entrance, 'adult', already_checked=[entrance.reverse]): - raise EntranceShuffleError(f'{entrance.name} potentially accessible as adult') + if not ootworld.decouple_entrances: + for entrance in ootworld.get_shufflable_entrances(): + if entrance.shuffled and entrance.replaces: + if entrance.replaces.name in CHILD_FORBIDDEN and not entrance_unreachable_as(entrance, 'child', already_checked=[entrance.replaces.reverse]): + raise EntranceShuffleError(f'{entrance.replaces.name} replaced by an entrance with potential child access') + if entrance.replaces.name in ADULT_FORBIDDEN and not entrance_unreachable_as(entrance, 'adult', already_checked=[entrance.replaces.reverse]): + raise EntranceShuffleError(f'{entrance.replaces.name} replaced by an entrance with potential adult access') + else: + if entrance.name in CHILD_FORBIDDEN and not entrance_unreachable_as(entrance, 'child', already_checked=[entrance.reverse]): + raise EntranceShuffleError(f'{entrance.name} potentially accessible as child') + if entrance.name in ADULT_FORBIDDEN and not entrance_unreachable_as(entrance, 'adult', already_checked=[entrance.reverse]): + raise EntranceShuffleError(f'{entrance.name} potentially accessible as adult') # Check if all locations are reachable if not beatable-only or game is not yet complete if locations_to_ensure_reachable: @@ -745,14 +756,14 @@ def get_entrance_replacing(region, entrance_name, player): def change_connections(entrance, target): entrance.connect(target.disconnect()) entrance.replaces = target.replaces - if entrance.reverse: + if entrance.reverse and not entrance.world.worlds[entrance.player].decouple_entrances: target.replaces.reverse.connect(entrance.reverse.assumed.disconnect()) target.replaces.reverse.replaces = entrance.reverse def restore_connections(entrance, target): target.connect(entrance.disconnect()) entrance.replaces = None - if entrance.reverse: + if entrance.reverse and not entrance.world.worlds[entrance.player].decouple_entrances: entrance.reverse.assumed.connect(target.replaces.reverse.disconnect()) target.replaces.reverse.replaces = None @@ -769,7 +780,7 @@ def check_entrances_compatibility(entrance, target, rollbacks): def confirm_replacement(entrance, target): delete_target_entrance(target) logging.getLogger('').debug(f'Connected {entrance} to {entrance.connected_region}') - if entrance.reverse: + if entrance.reverse and not entrance.world.worlds[entrance.player].decouple_entrances: replaced_reverse = target.replaces.reverse delete_target_entrance(entrance.reverse.assumed) logging.getLogger('').debug(f'Connected {replaced_reverse} to {replaced_reverse.connected_region}') diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index a97a6a0b..be894682 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -513,20 +513,6 @@ class OOTWorld(World): 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) - for loadzone in all_entrances: - if loadzone.primary: - entrance = loadzone - else: - entrance = loadzone.reverse - if entrance.reverse is not None: - self.world.spoiler.set_entrance(entrance, entrance.replaces, 'both', self.player) - else: - self.world.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player) - set_rules(self) set_entrances_based_rules(self) @@ -790,6 +776,24 @@ class OOTWorld(World): create_patch_file(rom, output_path(output_directory, outfile_name + '.apz5')) rom.restore() + # 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) + if not self.decouple_entrances: + for loadzone in all_entrances: + if loadzone.primary: + entrance = loadzone + else: + entrance = loadzone.reverse + if entrance.reverse is not None: + self.world.spoiler.set_entrance(entrance, entrance.replaces, 'both', self.player) + else: + self.world.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player) + else: + for entrance in all_entrances: + self.world.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player) + # Gathers hint data for OoT. Loops over all world locations for woth, barren, and major item locations. @classmethod def stage_generate_output(cls, world: MultiWorld, output_directory: str):