From 035621e420fade50627ecf93cc7ecba048cfda6f Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 28 Sep 2021 14:57:17 -0600 Subject: [PATCH 01/76] Rupee bow forbids certain lobbies in HC in standard Allow a lobby in keydrop shuffle Key rule fix when bk isn't possible Added controller awareness to world traversal --- BaseClasses.py | 12 +++++++++--- DoorShuffle.py | 14 +++++++++----- Doors.py | 7 +++++-- KeyDoorShuffle.py | 2 +- Main.py | 2 +- RELEASENOTES.md | 3 +++ Rules.py | 6 +++++- 7 files changed, 33 insertions(+), 13 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index b7c13f45..58530558 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -559,7 +559,7 @@ class CollectionState(object): queue.append((new_entrance, new_crystal_state)) # else those connections that are not accessible yet if self.is_small_door(connection): - door = connection.door + door = connection.door if connection.door.smallKey else connection.door.controller dungeon_name = connection.parent_region.dungeon.name key_logic = self.world.key_logic[player][dungeon_name] if door.name not in self.reached_doors[player]: @@ -573,7 +573,7 @@ class CollectionState(object): checklist[connection.name] = (connection, crystal_state) elif door.name not in self.opened_doors[player]: opened_doors = self.opened_doors[player] - door = connection.door + door = connection.door if connection.door.smallKey else connection.door.controller if door.name not in opened_doors: self.door_counter[player][1][dungeon_name] += 1 opened_doors.add(door.name) @@ -956,7 +956,12 @@ class CollectionState(object): @staticmethod def is_small_door(connection): - return connection and connection.door and connection.door.smallKey + return connection and connection.door and (connection.door.smallKey or + CollectionState.is_controlled_by_small(connection)) + + @staticmethod + def is_controlled_by_small(connection): + return connection.door.controller and connection.door.controller.smallKey def is_door_open(self, door_name, player): return door_name in self.opened_doors[player] @@ -1641,6 +1646,7 @@ class Door(object): self.bk_shuffle_req = False self.standard_restricted = False # flag if portal is not allowed in HC in standard self.lw_restricted = False # flag if portal is not allowed in DW + self.rupee_bow_restricted = False # flag if portal is not allowed in HC in standard+rupee_bow # self.incognitoPos = -1 # self.sectorLink = False diff --git a/DoorShuffle.py b/DoorShuffle.py index c70cc040..917f78bd 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -368,7 +368,8 @@ def choose_portals(world, player): if world.doorShuffle[player] in ['basic', 'crossed']: cross_flag = world.doorShuffle[player] == 'crossed' - bk_shuffle = world.bigkeyshuffle[player] + # key drops allow the big key in the right place in Desert Tiles 2 + bk_shuffle = world.bigkeyshuffle[player] or world.keydropshuffle[player] std_flag = world.mode[player] == 'standard' # roast incognito doors world.get_room(0x60, player).delete(5) @@ -415,6 +416,7 @@ def choose_portals(world, player): for dungeon, info in shuffled_info: outstanding_portals = list(dungeon_portals[dungeon]) hc_flag = std_flag and dungeon == 'Hyrule Castle' + rupee_bow_flag = hc_flag and world.retro[player] # rupee bow if hc_flag: sanc = world.get_portal('Sanctuary', player) sanc.destination = True @@ -424,14 +426,14 @@ def choose_portals(world, player): info.required_passage = {x: y for x, y in info.required_passage.items() if len(y) > 0} for target_region, possible_portals in info.required_passage.items(): candidates = find_portal_candidates(master_door_list, dungeon, need_passage=True, crossed=cross_flag, - bk_shuffle=bk_shuffle) + bk_shuffle=bk_shuffle, rupee_bow=rupee_bow_flag) choice, portal = assign_portal(candidates, possible_portals, world, player) portal.destination = True clean_up_portal_assignment(portal_assignment, dungeon, portal, master_door_list, outstanding_portals) dead_end_choices = info.total - 1 - len(portal_assignment[dungeon]) for i in range(0, dead_end_choices): candidates = find_portal_candidates(master_door_list, dungeon, dead_end_allowed=True, - crossed=cross_flag, bk_shuffle=bk_shuffle) + crossed=cross_flag, bk_shuffle=bk_shuffle, rupee_bow=rupee_bow_flag) possible_portals = outstanding_portals if not info.sole_entrance else [x for x in outstanding_portals if x != info.sole_entrance] choice, portal = assign_portal(candidates, possible_portals, world, player) if choice.deadEnd: @@ -443,7 +445,7 @@ def choose_portals(world, player): the_rest = info.total - len(portal_assignment[dungeon]) for i in range(0, the_rest): candidates = find_portal_candidates(master_door_list, dungeon, crossed=cross_flag, - bk_shuffle=bk_shuffle, standard=hc_flag) + bk_shuffle=bk_shuffle, standard=hc_flag, rupee_bow=rupee_bow_flag) choice, portal = assign_portal(candidates, outstanding_portals, world, player) clean_up_portal_assignment(portal_assignment, dungeon, portal, master_door_list, outstanding_portals) @@ -564,7 +566,7 @@ def disconnect_portal(portal, world, player): def find_portal_candidates(door_list, dungeon, need_passage=False, dead_end_allowed=False, crossed=False, - bk_shuffle=False, standard=False): + bk_shuffle=False, standard=False, rupee_bow=False): ret = [x for x in door_list if bk_shuffle or not x.bk_shuffle_req] if crossed: ret = [x for x in ret if not x.dungeonLink or x.dungeonLink == dungeon or x.dungeonLink.startswith('link')] @@ -576,6 +578,8 @@ def find_portal_candidates(door_list, dungeon, need_passage=False, dead_end_allo ret = [x for x in ret if not x.deadEnd] if standard: ret = [x for x in ret if not x.standard_restricted] + if rupee_bow: + ret = [x for x in ret if not x.rupee_bow_restricted] return ret diff --git a/Doors.py b/Doors.py index b0979a5d..a23f46f0 100644 --- a/Doors.py +++ b/Doors.py @@ -1490,8 +1490,11 @@ def create_doors(world, player): world.get_door('GT Petting Zoo SE', player).dead_end() world.get_door('GT DMs Room SW', player).dead_end() world.get_door("GT Bob\'s Room SE", player).passage = False - world.get_door('Desert Tiles 2 SE', player).bk_shuffle_req = True # key-drop note (todo) - world.get_door('Swamp Lobby S', player).standard_restricted = True # key-drop note (todo) + world.get_door('Desert Tiles 2 SE', player).bk_shuffle_req = True # key-drop note: allows this to be a portal + world.get_door('Swamp Lobby S', player).standard_restricted = True + world.get_door('PoD Mimics 2 SW', player).rupee_bow_restricted = True # bow statue + # enemizer logic could get rid of the following restriction + world.get_door('PoD Pit Room S', player).rupee_bow_restricted = True # so mimics 1 shouldn't be required # can't unlink from boss right now world.get_door('Hera Lobby S', player).dungeonLink = 'Tower of Hera' diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index a84fe5e8..945ad601 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -171,7 +171,7 @@ class PlacementRule(object): if loc.item and loc.item.bigkey: bk_blocked = True break - else: + elif len(self.check_locations_w_bk) > self.needed_keys_w_bk: def loc_has_bk(l): return (big_key_loc is not None and big_key_loc == l) or (l.item and l.item.bigkey) diff --git a/Main.py b/Main.py index 0db8af04..cfd79062 100644 --- a/Main.py +++ b/Main.py @@ -29,7 +29,7 @@ from Fill import sell_potions, sell_keys, balance_multiworld_progression, balanc from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops from Utils import output_path, parse_player_names -__version__ = '0.5.1.2-u' +__version__ = '0.5.1.3-u' from source.classes.BabelFish import BabelFish diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 55a34c3f..dbfa2879 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -15,6 +15,9 @@ CLI: ```--bombbag``` # Bug Fixes and Notes. +* 0.5.1.3 + * Certain lobbies forbidden in standard when rupee bow is enabled + * Fixed issue with key logic * 0.5.1.2 * Allowed Blind's Cell to be shuffled anywhere if Blind is not the boss of Thieves Town * Remove unique annotation from a FastEnum that was causing problems diff --git a/Rules.py b/Rules.py index 42bfe2f2..9902347d 100644 --- a/Rules.py +++ b/Rules.py @@ -1942,7 +1942,11 @@ def add_key_logic_rules(world, player): key_logic = world.key_logic[player] for d_name, d_logic in key_logic.items(): for door_name, rule in d_logic.door_rules.items(): - add_rule(world.get_entrance(door_name, player), eval_small_key_door(door_name, d_name, player)) + door_entrance = world.get_entrance(door_name, player) + add_rule(door_entrance, eval_small_key_door(door_name, d_name, player)) + if door_entrance.door.dependents: + for dep in door_entrance.door.dependents: + add_rule(dep.entrance, eval_small_key_door(door_name, d_name, player)) for location in d_logic.bk_restricted: if not location.forced_item: forbid_item(location, d_logic.bk_name, player) From a23315f78398be136e4d4ea696c57e6b8b63721d Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 5 Oct 2021 14:13:24 -0600 Subject: [PATCH 02/76] Disarm EG glitch on mirror in most modes Added a check flag for the disarm on most transitions so if can be armed when wanted --- DoorShuffle.py | 2 +- RELEASENOTES.md | 1 + Rom.py | 4 +++- asm/drhooks.asm | 5 +++++ asm/normal.asm | 5 +++-- asm/overrides.asm | 6 ++++++ 6 files changed, 19 insertions(+), 4 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 917f78bd..61da6443 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -2044,7 +2044,7 @@ class DROptions(Flag): Town_Portal = 0x02 # If on, Players will start with mirror scroll Map_Info = 0x04 Debug = 0x08 - # Rails = 0x10 # Unused bit now + Fix_EG = 0x10 # used to be Rails = 0x10 # Unused bit now OriginalPalettes = 0x20 # Open_PoD_Wall = 0x40 # No longer pre-opening pod wall - unused # Open_Desert_Wall = 0x80 # No longer pre-opening desert wall - unused diff --git a/RELEASENOTES.md b/RELEASENOTES.md index dbfa2879..9b9857fc 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -17,6 +17,7 @@ CLI: ```--bombbag``` * 0.5.1.3 * Certain lobbies forbidden in standard when rupee bow is enabled + * PoD EG disarmed when mirroring (except in nologic) * Fixed issue with key logic * 0.5.1.2 * Allowed Blind's Cell to be shuffled anywhere if Blind is not the boss of Thieves Town diff --git a/Rom.py b/Rom.py index 22f017e3..ae29e127 100644 --- a/Rom.py +++ b/Rom.py @@ -32,7 +32,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '11f4f494e999a919aafd7d2624e67679' +RANDOMIZERBASEHASH = '1b937c39f026f9a687391488b7386542' class JsonRom(object): @@ -688,6 +688,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): dr_flags |= DROptions.OriginalPalettes if world.experimental[player]: dr_flags |= DROptions.DarkWorld_Spawns + if world.logic[player] != 'nologic': + dr_flags |= DROptions.Fix_EG # fix hc big key problems (map and compass too) diff --git a/asm/drhooks.asm b/asm/drhooks.asm index 86ae1ff1..1d3b485b 100644 --- a/asm/drhooks.asm +++ b/asm/drhooks.asm @@ -125,6 +125,11 @@ org $07a955 ; <- Bank07.asm : around 6564 (JP is a bit different) (STZ $05FC : S jsl BlockEraseFix nop #2 +org $02A0A8 +Mirror_SaveRoomData: +org $07A95B ; < bank_07.asm ; #_07A95B: JSL Mirror_SaveRoomData +jsl EGFixOnMirror + org $02b82a jsl FixShopCode diff --git a/asm/normal.asm b/asm/normal.asm index aabb24de..3bdf9622 100644 --- a/asm/normal.asm +++ b/asm/normal.asm @@ -73,8 +73,9 @@ TrapDoorFixer: rts Cleanup: - stz $047a - inc $11 + lda.l DRFlags : and #$10 : beq + + stz $047a + + inc $11 lda $ef rts diff --git a/asm/overrides.asm b/asm/overrides.asm index a041ae30..91029a8a 100644 --- a/asm/overrides.asm +++ b/asm/overrides.asm @@ -47,6 +47,12 @@ MirrorCheckOverride: rtl + lda.l DRScroll : rtl +EGFixOnMirror: + lda.l DRFlags : and #$10 : beq + + stz $047a + + jsl Mirror_SaveRoomData + rtl + BlockEraseFix: lda $7ef353 : and #$02 : beq + stz $05fc : stz $05fd From 1f92895222f3880539c8ce0b583158ac03c8a48c Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 6 Oct 2021 20:45:29 -0500 Subject: [PATCH 03/76] Added mirror edge to Pyramid Exit Ledge --- OverworldShuffle.py | 1 + Regions.py | 2 +- Rules.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 5ad3a08e..dce0174e 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -1071,6 +1071,7 @@ ow_connections = { ('HC Ledge Mirror Spot', 'Hyrule Castle Ledge'), ('HC Courtyard Mirror Spot', 'Hyrule Castle Courtyard'), ('HC Area Mirror Spot', 'Hyrule Castle Area'), + ('HC Courtyard Left Mirror Spot', 'Hyrule Castle Courtyard'), ('HC Area South Mirror Spot', 'Hyrule Castle Area'), ('HC East Entry Mirror Spot', 'Hyrule Castle East Entry'), ('Top of Pyramid', 'Pyramid Area'), diff --git a/Regions.py b/Regions.py index ab494e12..39ce6d3f 100644 --- a/Regions.py +++ b/Regions.py @@ -164,7 +164,7 @@ def create_regions(world, player): create_dw_region(player, 'Shield Shop Area', None, ['Shield Shop Fence (Outer) Ledge Drop', 'Forgotton Forest Mirror Spot', 'Shield Shop NW', 'Shield Shop NE']), create_dw_region(player, 'Shield Shop Fence', None, ['Shield Shop Fence (Inner) Ledge Drop', 'Red Shield Shop', 'Forgotton Forest Fence Mirror Spot']), create_dw_region(player, 'Pyramid Area', ['Pyramid'], ['Pyramid Fairy', 'Pyramid Hole', 'HC Ledge Mirror Spot', 'HC Courtyard Mirror Spot', 'HC Area Mirror Spot', 'HC East Entry Mirror Spot', 'Pyramid ES']), - create_dw_region(player, 'Pyramid Exit Ledge', None, ['Pyramid Exit Ledge Drop', 'Pyramid Entrance']), + create_dw_region(player, 'Pyramid Exit Ledge', None, ['Pyramid Exit Ledge Drop', 'HC Courtyard Left Mirror Spot', 'Pyramid Entrance']), create_dw_region(player, 'Pyramid Pass', None, ['Post Aga Inverted Teleporter', 'HC Area South Mirror Spot', 'Pyramid SW', 'Pyramid SE']), create_dw_region(player, 'Broken Bridge Area', None, ['Broken Bridge Hammer Rock (South)', 'Broken Bridge Water Drop', 'Wooden Bridge Mirror Spot', 'Broken Bridge SW']), create_dw_region(player, 'Broken Bridge Northeast', None, ['Broken Bridge Hammer Rock (North)', 'Broken Bridge Hookshot Gap', 'Broken Bridge Northeast Water Drop', 'Wooden Bridge Northeast Mirror Spot', 'Broken Bridge NE']), diff --git a/Rules.py b/Rules.py index 9ceac0d7..7ed4341b 100644 --- a/Rules.py +++ b/Rules.py @@ -1001,6 +1001,7 @@ def ow_rules(world, player): set_rule(world.get_entrance('HC Ledge Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('HC Courtyard Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('HC East Entry Mirror Spot', player), lambda state: state.has_Mirror(player)) + set_rule(world.get_entrance('HC Courtyard Left Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('HC Area South Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Top of Pyramid', player), lambda state: state.has('Beat Agahnim 1', player)) set_rule(world.get_entrance('Top of Pyramid (Inner)', player), lambda state: state.has('Beat Agahnim 1', player)) From 916d32350d8fd102425fdea61882535fce9dc828 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 7 Oct 2021 00:59:27 -0500 Subject: [PATCH 04/76] Fixed issue with Chaos/Limited OWR with Keep Similar off not using edge arrays --- OverworldShuffle.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index dce0174e..4dbcb72f 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -173,8 +173,11 @@ def link_overworld(world, player): if world.owCrossed[player] == 'limited': random.shuffle(crossed_candidates) for edge_set in crossed_candidates[:9]: - for edge in edge_set: - crossed_edges.append(edge) + if world.owKeepSimilar[player]: + for edge in edge_set: + crossed_edges.append(edge) + else: + crossed_edges.append(edge_set) for edge in copy.deepcopy(crossed_edges): if edge in parallel_links: crossed_edges.append(parallel_links[edge]) From f31b867029c110f8c56115e95985879519a4dac7 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 7 Oct 2021 01:05:08 -0500 Subject: [PATCH 05/76] Fixed issue with Chaos/Limited OWR with Keep Similar off not using edge arrays --- OverworldShuffle.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 4dbcb72f..64a43fe6 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -169,15 +169,12 @@ def link_overworld(world, player): if world.owCrossed[player] == 'chaos' and random.randint(0, 1): crossed_edges.append(edge) elif world.owCrossed[player] == 'limited': - crossed_candidates.append(edge) + crossed_candidates.append([edge]) if world.owCrossed[player] == 'limited': random.shuffle(crossed_candidates) for edge_set in crossed_candidates[:9]: - if world.owKeepSimilar[player]: - for edge in edge_set: - crossed_edges.append(edge) - else: - crossed_edges.append(edge_set) + for edge in edge_set: + crossed_edges.append(edge) for edge in copy.deepcopy(crossed_edges): if edge in parallel_links: crossed_edges.append(parallel_links[edge]) From 3c3ec43b7736945da32b6bd469a256bfdca13734 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 7 Oct 2021 01:22:04 -0500 Subject: [PATCH 06/76] Added new spot_types for OW logical edges --- OWEdges.py | 457 +++++++++++++++++++++++++++++++++++++++++++- OverworldShuffle.py | 15 +- 2 files changed, 470 insertions(+), 2 deletions(-) diff --git a/OWEdges.py b/OWEdges.py index ebf1b15b..adad2143 100644 --- a/OWEdges.py +++ b/OWEdges.py @@ -1353,4 +1353,459 @@ parallel_links = bidict({'Lost Woods SW': 'Skull Woods SW', 'Octoballoon NE': 'Bomber Corner NE', 'Octoballoon WC': 'Bomber Corner WC', 'Octoballoon WS': 'Bomber Corner WS' - }) \ No newline at end of file + }) + +OWExitTypes = { + 'Ledge': ['West Death Mountain Drop', + 'Spectacle Rock Drop', + 'East Death Mountain Spiral Ledge Drop', + 'East Death Mountain Fairy Ledge Drop', + 'East Death Mountain Mimic Ledge Drop', + 'Spiral Ledge Drop', + 'Mimic Ledge Drop', + 'Fairy Ascension Ledge Drop', + 'Fairy Ascension Plateau Ledge Drop', + 'TR Pegs Ledge Drop', + 'Mountain Entry Entrance Ledge Drop', + 'Mountain Entry Ledge Drop', + 'Zora Waterfall Water Drop', + 'Bonk Rock Ledge Drop', + 'Graveyard Ledge Drop', + 'River Bend Water Drop', + 'River Bend East Water Drop', + 'Potion Shop Water Drop', + 'Potion Shop Northeast Water Drop', + 'Zora Approach Bottom Ledge Drop', + 'Zora Approach Water Drop', + 'Zora Approach Ledge Drop', + 'Hyrule Castle Ledge Drop', + 'Hyrule Castle Ledge Courtyard Drop', + 'Wooden Bridge Water Drop', + 'Wooden Bridge Northeast Water Drop', + 'Sand Dunes Ledge Drop', + 'Stone Bridge East Ledge Drop', + 'Tree Line Ledge Drop', + 'Eastern Palace Ledge Drop', + 'Maze Race Ledge Drop', + 'Central Bonk Rocks Cliff Ledge Drop', + 'Links House Cliff Ledge Drop', + 'Stone Bridge Cliff Ledge Drop', + 'Lake Hylia Area Cliff Ledge Drop', + 'Lake Hylia Island FAWT Ledge Drop', + 'Stone Bridge EC Cliff Water Drop', + 'Tree Line WC Cliff Water Drop', + 'C Whirlpool Outer Cliff Ledge Drop', + 'C Whirlpool Cliff Ledge Drop', + 'South Teleporter Cliff Ledge Drop', + 'Statues Cliff Ledge Drop', + 'Desert Ledge Drop', + 'Checkerboard Ledge Drop', + 'Desert Mouth Drop', + 'Desert Teleporter Drop', + 'Desert Boss Cliff Ledge Drop', + 'Checkerboard Cliff Ledge Drop', + 'Suburb Cliff Ledge Drop', + 'Cave 45 Cliff Ledge Drop', + 'Desert C Whirlpool Cliff Ledge Drop', + 'Desert Pass Cliff Ledge Drop', + 'Desert Pass Southeast Cliff Ledge Drop', + 'Dam Cliff Ledge Drop', + 'Bombos Tablet Drop', + 'Cave 45 Ledge Drop', + 'Lake Hylia Water Drop', + 'Lake Hylia South Water Drop', + 'Lake Hylia Northeast Water Drop', + 'Lake Hylia Central Water Drop', + 'Lake Hylia Island Water Drop', + 'Desert Pass Ledge Drop', + 'Octoballoon Water Drop', + 'Octoballoon Waterfall Water Drop', + 'Dark Death Mountain Drop (West)', + 'Dark Death Mountain Drop (East)', + 'Floating Island Drop', + 'Turtle Rock Tail Ledge Drop', + 'Turtle Rock Ledge Drop', + 'Bumper Cave Ledge Drop', + 'Bumper Cave Entrance Drop', + 'Qirn Jump Water Drop', + 'Qirn Jump East Water Drop', + 'Dark Witch Water Drop', + 'Dark Witch Northeast Water Drop', + 'Catfish Approach Bottom Ledge Drop', + 'Catfish Approach Water Drop', + 'Catfish Approach Ledge Drop', + 'Shield Shop Fence (Outer) Ledge Drop', + 'Shield Shop Fence (Inner) Ledge Drop', + 'Pyramid Exit Ledge Drop', + 'Broken Bridge Water Drop', + 'Broken Bridge Northeast Water Drop', + 'Broken Bridge West Water Drop', + 'Dark Dunes Ledge Drop', + 'Hammer Bridge North Ledge Drop', + 'Dark Tree Line Ledge Drop', + 'Palace of Darkness Ledge Drop', + 'Dig Game To Ledge Drop', + 'Dig Game Ledge Drop', + 'Frog Ledge Drop', + 'Hammer Bridge Water Drop', + 'Dark Bonk Rocks Cliff Ledge Drop', + 'Bomb Shop Cliff Ledge Drop', + 'Hammer Bridge South Cliff Ledge Drop', + 'Ice Lake Area Cliff Ledge Drop', + 'Ice Palace Island FAWT Ledge Drop', + 'Hammer Bridge EC Cliff Water Drop', + 'Dark Tree Line WC Cliff Water Drop', + 'Dark C Whirlpool Outer Cliff Ledge Drop', + 'Dark C Whirlpool Cliff Ledge Drop', + 'Hype Cliff Ledge Drop', + 'Dark South Teleporter Cliff Ledge Drop', + 'Misery Mire Teleporter Ledge Drop', + 'Mire Cliff Ledge Drop', + 'Archery Game Cliff Ledge Drop', + 'Stumpy Approach Cliff Ledge Drop', + 'Mire C Whirlpool Cliff Ledge Drop', + 'Swamp Nook Cliff Ledge Drop', + 'Swamp Cliff Ledge Drop', + 'Ice Lake Water Drop', + 'Ice Lake Northeast Water Drop', + 'Ice Lake Southwest Water Drop', + 'Ice Lake Southeast Water Drop', + 'Bomber Corner Water Drop', + 'Bomber Corner Waterfall Water Drop' + ], + 'OWTerrain': ['Lost Woods Bush (West)', + 'Lost Woods Bush (East)', + 'Spectacle Rock Approach', + 'Spectacle Rock Leave', + 'DM Hammer Bridge (West)', + 'DM Hammer Bridge (East)', + 'Floating Island Bridge (East)', + 'Fairy Ascension Rocks (North)', + 'DM Broken Bridge (West)', + 'DM Broken Bridge (East)', + 'Fairy Ascension Rocks (South)', + 'Floating Island Bridge (West)', + 'TR Pegs Ledge Entry', + 'TR Pegs Ledge Leave', + 'Mountain Entry Entrance Rock (West)', + 'Mountain Entry Entrance Rock (East)', + 'Zora Waterfall Water Entry', + 'Waterfall of Wishing Cave Entry', + 'Zora Waterfall Landing', + 'Kings Grave Outer Rocks', + 'Graveyard Ladder (Bottom)', + 'Graveyard Ladder (Top)', + 'Kings Grave Inner Rocks', + 'River Bend West Pier', + 'River Bend East Pier', + 'Potion Shop Rock (South)', + 'Potion Shop Rock (North)', + 'Zora Approach Rocks (West)', + 'Zora Approach Rocks (East)', + 'Kakariko Southwest Bush (North)', + 'Kakariko Yard Bush (South)', + 'Kakariko Southwest Bush (South)', + 'Kakariko Yard Bush (North)', + 'Hyrule Castle Main Gate (South)', + 'Hyrule Castle Inner East Rock', + 'Hyrule Castle Southwest Bush (North)', + 'Hyrule Castle Southwest Bush (South)', + 'Hyrule Castle Courtyard Bush (South)', + 'Hyrule Castle Main Gate (North)', + 'Hyrule Castle Courtyard Bush (North)', + 'Hyrule Castle Outer East Rock', + 'Wooden Bridge Bush (South)', + 'Wooden Bridge Bush (North)', + 'Bat Cave Ledge Peg', + 'Maze Race Game', + 'Desert Palace Statue Move', + 'Checkerboard Ledge Approach', + 'Desert Ledge Outer Rocks', + 'Desert Ledge Inner Rocks', + 'Checkerboard Ledge Leave', + 'Flute Boy Bush (South)', + 'Cave 45 Inverted Approach', + 'Flute Boy Bush (North)', + 'Cave 45 Inverted Leave', + 'C Whirlpool Rock (Bottom)', + 'C Whirlpool Water Entry', + 'C Whirlpool Landing', + 'C Whirlpool Rock (Top)', + 'Statues Water Entry', + 'Statues Landing', + 'Lake Hylia Central Island Pier', + 'Lake Hylia Island Pier', + 'Lake Hylia West Pier', + 'Lake Hylia East Pier', + 'Desert Pass Ladder (South)', + 'Desert Pass Rocks (North)', + 'Desert Pass Rocks (South)', + 'Desert Pass Ladder (North)', + 'Octoballoon Pier', + 'Skull Woods Bush Rock (East)', + 'Skull Woods Bush Rock (West)', + 'Skull Woods Forgotten Bush (West)', + 'Skull Woods Forgotten Bush (East)', + 'GT Entry Approach', + 'Dark Death Mountain Ladder (North)', + 'GT Entry Leave', + 'Dark Death Mountain Ladder (South)', + 'Bumper Cave Entrance Rock', + 'Skull Woods Pass Bush Row (West)', + 'Skull Woods Pass Bush Row (East)', + 'Skull Woods Pass Rock (Top)', + 'Skull Woods Pass Rock (Bottom)', + 'Dark Graveyard Bush (South)', + 'Dark Graveyard Bush (North)', + 'Qirn Jump Pier', + 'Dark Witch Rock (South)', + 'Dark Witch Rock (North)', + 'Catfish Approach Rocks (West)', + 'Catfish Approach Rocks (East)', + 'Village of Outcasts Pegs', + 'Grassy Lawn Pegs', + 'Broken Bridge Hammer Rock (South)', + 'Broken Bridge Hammer Rock (North)', + 'Broken Bridge Hookshot Gap', + 'Peg Area Rocks (West)', + 'Peg Area Rocks (East)', + 'Frog Rock (Outer)', + 'Archery Game Rock (North)', + 'Frog Rock (Inner)', + 'Archery Game Rock (South)', + 'Hammer Bridge Pegs (North)', + 'Hammer Bridge Pegs (South)', + 'Hammer Bridge Pier', + 'Stumpy Approach Bush (South)', + 'Stumpy Approach Bush (North)', + 'Dark C Whirlpool Rock (Bottom)', + 'Dark C Whirlpool Water Entry', + 'Dark C Whirlpool Landing', + 'Dark C Whirlpool Rock (Top)', + 'Hype Cave Water Entry', + 'Hype Cave Landing', + 'Ice Lake Northeast Pier', + 'Ice Lake Moat Water Entry', + 'Ice Lake Northeast Pier Bomb Jump', + 'Ice Palace Approach', + 'Ice Palace Leave', + 'Bomber Corner Pier' + ], + 'Portal': ['West Death Mountain Teleporter', + 'East Death Mountain Teleporter', + 'TR Pegs Teleporter', + 'Kakariko Teleporter (Hammer)', + 'Kakariko Teleporter (Rock)', + 'Top of Pyramid', + 'Top of Pyramid (Inner)', + 'East Hyrule Teleporter', + 'Desert Teleporter', + 'South Hyrule Teleporter', + 'Lake Hylia Teleporter', + 'Dark Death Mountain Teleporter (West)', + 'Dark Death Mountain Teleporter (East)', + 'Turtle Rock Teleporter', + 'West Dark World Teleporter (Hammer)', + 'West Dark World Teleporter (Rock)', + 'Post Aga Inverted Teleporter', + 'East Dark World Teleporter', + 'Misery Mire Teleporter', + 'South Dark World Teleporter', + 'Ice Palace Teleporter' + ], + 'Whirlpool': ['Zora Whirlpool', + 'Kakariko Pond Whirlpool', + 'River Bend Whirlpool', + 'C Whirlpool', + 'Lake Hylia Whirlpool', + 'Octoballoon Whirlpool', + 'Qirn Jump Whirlpool', + 'Bomber Corner Whirlpool' + ], + 'Mirror': ['Skull Woods Back Mirror Spot', + 'Skull Woods Forgotten (West) Mirror Spot', + 'Skull Woods Forgotten (East) Mirror Spot', + 'Skull Woods Portal Entry Mirror Spot', + 'Skull Woods Forgotten (Middle) Mirror Spot', + 'Skull Woods Front Mirror Spot', + 'Dark Lumberjack Mirror Spot', + 'West Dark Death Mountain (Top) Mirror Spot', + 'Bubble Boy Mirror Spot', + 'West Dark Death Mountain (Bottom) Mirror Spot', + 'East Dark Death Mountain (Top West) Mirror Spot', + 'East Dark Death Mountain (Top East) Mirror Spot', + 'TR Ledge (West) Mirror Spot', + 'TR Ledge (East) Mirror Spot', + 'TR Isolated Mirror Spot', + 'East Dark Death Mountain (Bottom Plateau) Mirror Spot', + 'East Dark Death Mountain (Bottom Left) Mirror Spot', + 'East Dark Death Mountain (Bottom) Mirror Spot', + 'Dark Floating Island Mirror Spot', + 'Turtle Rock Mirror Spot', + 'Turtle Rock Ledge Mirror Spot', + 'Bumper Cave Area Mirror Spot', + 'Bumper Cave Entry Mirror Spot', + 'Bumper Cave Ledge Mirror Spot', + 'Catfish Mirror Spot', + 'Skull Woods Pass West Mirror Spot', + 'Skull Woods Pass East Top Mirror Spot', + 'Skull Woods Pass East Bottom Mirror Spot', + 'Outcast Fortune Mirror Spot', + 'Outcast Pond Mirror Spot', + 'Dark Chapel Mirror Spot', + 'Dark Chapel Ledge Mirror Spot', + 'Dark Graveyard Mirror Spot', + 'Dark Graveyard Ledge Mirror Spot', + 'Dark Graveyard Grave Mirror Spot', + 'Qirn Jump Mirror Spot', + 'Qirn Jump East Mirror Spot', + 'Dark Witch Mirror Spot', + 'Dark Witch Northeast Mirror Spot', + 'Catfish Approach Mirror Spot', + 'Catfish Approach Ledge Mirror Spot', + 'Village of Outcasts Mirror Spot', + 'Village of Outcasts Southwest Mirror Spot', + 'Hammer House Mirror Spot', + 'Shield Shop Mirror Spot', + 'Pyramid Mirror Spot', + 'Pyramid Pass Mirror Spot', + 'Pyramid Courtyard Mirror Spot', + 'Pyramid Uncle Mirror Spot', + 'Pyramid From Ledge Mirror Spot', + 'Pyramid Entry Mirror Spot', + 'Broken Bridge West Mirror Spot', + 'Broken Bridge East Mirror Spot', + 'Broken Bridge Northeast Mirror Spot', + 'Palace of Darkness Mirror Spot', + 'Hammer Pegs Mirror Spot', + 'Hammer Pegs Entry Mirror Spot', + 'Dark Dunes Mirror Spot', + 'Dig Game Mirror Spot', + 'Dig Game Ledge Mirror Spot', + 'Frog Mirror Spot', + 'Frog Prison Mirror Spot', + 'Archery Game Mirror Spot', + 'Stumpy Mirror Spot', + 'Stumpy Pass Mirror Spot', + 'Dark Bonk Rocks Mirror Spot', + 'Big Bomb Shop Mirror Spot', + 'Hammer Bridge North Mirror Spot', + 'Hammer Bridge South Mirror Spot', + 'Dark Hobo Mirror Spot', + 'Dark Tree Line Mirror Spot', + 'Darkness Nook Mirror Spot', + 'Misery Mire Mirror Spot', + 'Misery Mire Ledge Mirror Spot', + 'Misery Mire Blocked Mirror Spot', + 'Misery Mire Main Mirror Spot', + 'Stumpy Approach Mirror Spot', + 'Stumpy Bush Entry Mirror Spot', + 'Dark C Whirlpool Mirror Spot', + 'Dark C Whirlpool Outer Mirror Spot', + 'Hype Cave Mirror Spot', + 'Ice Lake Mirror Spot', + 'Ice Lake Southwest Mirror Spot', + 'Ice Lake Southeast Mirror Spot', + 'Ice Lake Northeast Mirror Spot', + 'Ice Palace Mirror Spot', + 'Shopping Mall Mirror Spot', + 'Swamp Nook Mirror Spot', + 'Swamp Nook Southeast Mirror Spot', + 'Swamp Nook Pegs Mirror Spot', + 'Swamp Mirror Spot', + 'Dark South Pass Mirror Spot', + 'Bomber Corner Mirror Spot', + 'Lost Woods East Mirror Spot', + 'Lost Woods Entry Mirror Spot', + 'Lost Woods Pedestal Mirror Spot', + 'Lost Woods Southwest Mirror Spot', + 'Lost Woods East (Forgotten) Mirror Spot', + 'Lost Woods West (Forgotten) Mirror Spot', + 'Lumberjack Mirror Spot', + 'West Death Mountain (Top) Mirror Spot', + 'Spectacle Rock Mirror Spot', + 'East Death Mountain (Top West) Mirror Spot', + 'East Death Mountain (Top East) Mirror Spot', + 'Fairy Ascension Mirror Spot', + 'Death Mountain Bridge Mirror Spot', + 'Spiral Cave Mirror Spot', + 'Mimic Cave Mirror Spot', + 'Isolated Ledge Mirror Spot', + 'Floating Island Mirror Spot', + 'TR Pegs Area Mirror Spot', + 'Mountain Entry Mirror Spot', + 'Mountain Entry Entrance Mirror Spot', + 'Mountain Entry Ledge Mirror Spot', + 'Zora Waterfall Mirror Spot', + 'Lost Woods Pass West Mirror Spot', + 'Lost Woods Pass East Top Mirror Spot', + 'Lost Woods Pass East Bottom Mirror Spot', + 'Kakariko Fortune Mirror Spot', + 'Kakariko Pond Mirror Spot', + 'Sanctuary Mirror Spot', + 'Bonk Rock Ledge Mirror Spot', + 'Graveyard Ledge Mirror Spot', + 'Kings Grave Mirror Spot', + 'River Bend Mirror Spot', + 'River Bend East Mirror Spot', + 'Potion Shop Mirror Spot', + 'Potion Shop Northeast Mirror Spot', + 'Zora Approach Mirror Spot', + 'Zora Approach Ledge Mirror Spot', + 'Kakariko Mirror Spot', + 'Kakariko Grass Mirror Spot', + 'Forgotton Forest Mirror Spot', + 'Forgotton Forest Fence Mirror Spot', + 'HC Ledge Mirror Spot', + 'HC Courtyard Mirror Spot', + 'HC Area Mirror Spot', + 'HC East Entry Mirror Spot', + 'HC Courtyard Left Mirror Spot', + 'HC Area South Mirror Spot', + 'Wooden Bridge Mirror Spot', + 'Wooden Bridge Northeast Mirror Spot', + 'Wooden Bridge West Mirror Spot', + 'Eastern Palace Mirror Spot', + 'Blacksmith Entry Mirror Spot', + 'Blacksmith Mirror Spot', + 'Bat Cave Ledge Mirror Spot', + 'Sand Dunes Mirror Spot', + 'Maze Race Mirror Spot', + 'Maze Race Ledge Mirror Spot', + 'Kakariko Suburb Mirror Spot', + 'Kakariko Suburb South Mirror Spot', + 'Flute Boy Mirror Spot', + 'Flute Boy Pass Mirror Spot', + 'Central Bonk Rocks Mirror Spot', + 'Links House Mirror Spot', + 'Stone Bridge Mirror Spot', + 'Stone Bridge South Mirror Spot', + 'Hobo Mirror Spot', + 'Tree Line Mirror Spot', + 'Eastern Nook Mirror Spot', + 'Desert Mirror Spot', + 'Desert Ledge Mirror Spot', + 'Checkerboard Mirror Spot', + 'DP Stairs Mirror Spot', + 'DP Entrance (North) Mirror Spot', + 'Bombos Tablet Ledge Mirror Spot', + 'Cave 45 Mirror Spot', + 'Flute Boy Entry Mirror Spot', + 'C Whirlpool Mirror Spot', + 'C Whirlpool Outer Mirror Spot', + 'Statues Mirror Spot', + 'Lake Hylia Mirror Spot', + 'Lake Hylia Northeast Mirror Spot', + 'South Shore Mirror Spot', + 'South Shore East Mirror Spot', + 'Lake Hylia Island Mirror Spot', + 'Lake Hylia Water Mirror Spot', + 'Lake Hylia Central Island Mirror Spot', + 'Ice Cave Mirror Spot', + 'Desert Pass Ledge Mirror Spot', + 'Desert Pass Mirror Spot', + 'Dam Mirror Spot', + 'South Pass Mirror Spot', + 'Octoballoon Mirror Spot' + ] +} \ No newline at end of file diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 64a43fe6..30048e24 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -1,6 +1,7 @@ import RaceRandom as random, logging, copy from BaseClasses import OWEdge, WorldType, RegionType, Direction, Terrain, PolSlot, Entrance -from OWEdges import OWTileRegions, OWTileGroups, OWEdgeGroups, OpenStd, parallel_links, IsParallel +from Regions import mark_dark_world_regions, mark_light_world_regions +from OWEdges import OWTileRegions, OWTileGroups, OWEdgeGroups, OWExitTypes, OpenStd, parallel_links, IsParallel __version__ = '0.1.9.4-u' @@ -146,6 +147,8 @@ def link_overworld(world, player): for (exitname, regionname) in ow_connections[owid][1]: connect_simple(world, exitname, regionname, player) + categorize_world_regions(world, player) + # crossed shuffle logging.getLogger('').debug('Crossing overworld edges') if world.owCrossed[player] in ['grouped', 'limited', 'chaos']: @@ -646,11 +649,20 @@ def create_flute_exits(world, player): and (region.name not in world.owswaps[player][1] or region.name in world.owswaps[player][2])): exitname = 'Flute From ' + region.name exit = Entrance(region.player, exitname, region) + exit.spot_type = 'Flute' exit.access_rule = lambda state: state.can_flute(player) exit.connect(world.get_region('Flute Sky', player)) region.exits.append(exit) world.initialize_regions() +def categorize_world_regions(world, player): + for type in OWExitTypes: + for exitname in OWExitTypes[type]: + world.get_entrance(exitname, player).spot_type = type + + mark_light_world_regions(world, player) + mark_dark_world_regions(world, player) + def update_world_regions(world, player): if world.owMixed[player]: for name in world.owswaps[player][1]: @@ -1443,6 +1455,7 @@ flute_data = { 0x32: (['Flute Boy Approach Area', 'Stumpy Approach Area'], 0x32, 0x03a0, 0x0c6c, 0x0500, 0x0cd0, 0x05a8, 0x0cdb, 0x0585, 0x0002, 0x0000, 0x0cd6, 0x05a8), 0x33: (['C Whirlpool Outer Area', 'Dark C Whirlpool Outer Area'], 0x33, 0x0180, 0x0c20, 0x0600, 0x0c80, 0x0628, 0x0c8f, 0x067d, 0x0000, 0x0000, 0x0c80, 0x0628), 0x34: (['Statues Area', 'Hype Cave Area'], 0x34, 0x088e, 0x0d00, 0x0866, 0x0d60, 0x08d8, 0x0d6f, 0x08e3, 0x0000, 0x000a, 0x0d60, 0x08d8), + #0x35: (['Lake Hylia Area', 'Ice Lake Area'], 0x35, 0x0d00, 0x0da6, 0x0a06, 0x0e08, 0x0a80, 0x0e13, 0x0a8b, 0xfffa, 0xfffa, 0x0d88, 0x0a88), 0x3e: (['Lake Hylia South Shore', 'Ice Lake Ledge (East)'], 0x35, 0x1860, 0x0f1e, 0x0d00, 0x0f98, 0x0da8, 0x0f8b, 0x0d85, 0x0000, 0x0000, 0x0f90, 0x0da4), 0x37: (['Ice Cave Area', 'Shopping Mall Area'], 0x37, 0x0786, 0x0cf6, 0x0e2e, 0x0d58, 0x0ea0, 0x0d63, 0x0eab, 0x000a, 0x0002, 0x0d48, 0x0ed0), 0x3a: (['Desert Pass Area', 'Swamp Nook Area'], 0x3a, 0x001a, 0x0e08, 0x04c6, 0x0e70, 0x0540, 0x0e7d, 0x054b, 0x0006, 0x000a, 0x0e70, 0x0540), From 2e5a5f351e20db0dcfa074ed61b4f57f34fa8a9a Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 14 Oct 2021 13:13:24 -0500 Subject: [PATCH 07/76] Allows starting flute to give access without reaching Kakariko Village --- BaseClasses.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/BaseClasses.py b/BaseClasses.py index 7ec38c89..4b9f9d00 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1296,6 +1296,8 @@ class CollectionState(object): return self.has('Fire Rod', player) or self.has('Lamp', player) def can_flute(self, player): + if any(map(lambda i: i.name == 'Ocarina', self.world.precollected_items)): + return True lw = self.world.get_region('Kakariko Area', player) return self.has('Ocarina', player) and lw.can_reach(self) and self.is_not_bunny(lw, player) From 3e6d447857b921e0cc50dec5876a0ed48edc486f Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 14 Oct 2021 13:25:47 -0500 Subject: [PATCH 08/76] Added Bat Cave Ledge Peg exit --- OverworldShuffle.py | 1 + Regions.py | 2 +- Rules.py | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 30048e24..e4d90be8 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -756,6 +756,7 @@ mandatory_connections = [# Whirlpool Connections ('Wooden Bridge Water Drop', 'Wooden Bridge Water'), #flippers ('Wooden Bridge Northeast Water Drop', 'Wooden Bridge Water'), #flippers ('Bat Cave Ledge Peg', 'Bat Cave Ledge'), #hammer + ('Bat Cave Ledge Peg (East)', 'Blacksmith Area'), #hammer ('Maze Race Game', 'Maze Race Prize'), #pearl ('Maze Race Ledge Drop', 'Maze Race Area'), ('Desert Palace Statue Move', 'Desert Palace Stairs'), #book diff --git a/Regions.py b/Regions.py index 39ce6d3f..556971f8 100644 --- a/Regions.py +++ b/Regions.py @@ -70,7 +70,7 @@ def create_regions(world, player): create_lw_region(player, 'Eastern Palace Area', None, ['Sahasrahlas Hut', 'Eastern Palace', 'Palace of Darkness Mirror Spot', 'Eastern Palace SW', 'Eastern Palace SE']), create_lw_region(player, 'Eastern Cliff', None, ['Sand Dunes Ledge Drop', 'Stone Bridge East Ledge Drop', 'Tree Line Ledge Drop', 'Eastern Palace Ledge Drop']), create_lw_region(player, 'Blacksmith Area', None, ['Blacksmiths Hut', 'Bat Cave Cave', 'Bat Cave Ledge Peg', 'Hammer Pegs Mirror Spot', 'Hammer Pegs Entry Mirror Spot', 'Blacksmith WS']), - create_lw_region(player, 'Bat Cave Ledge', None, ['Bat Cave Drop']), + create_lw_region(player, 'Bat Cave Ledge', None, ['Bat Cave Ledge Peg (East)', 'Bat Cave Drop']), create_lw_region(player, 'Sand Dunes Area', None, ['Dark Dunes Mirror Spot', 'Sand Dunes NW', 'Sand Dunes WN', 'Sand Dunes SC']), create_lw_region(player, 'Maze Race Area', None, ['Dig Game Mirror Spot', 'Maze Race ES']), create_lw_region(player, 'Maze Race Ledge', None, ['Two Brothers House (West)', 'Maze Race Game', 'Dig Game Ledge Mirror Spot']), diff --git a/Rules.py b/Rules.py index 7ed4341b..2dc4df99 100644 --- a/Rules.py +++ b/Rules.py @@ -759,6 +759,7 @@ def default_rules(world, player): set_rule(world.get_entrance('Hyrule Castle Inner East Rock', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Hyrule Castle Outer East Rock', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Bat Cave Ledge Peg', player), lambda state: state.has('Hammer', player)) + set_rule(world.get_entrance('Bat Cave Ledge Peg (East)', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('Desert Palace Statue Move', player), lambda state: state.has('Book of Mudora', player)) set_rule(world.get_entrance('Desert Ledge Outer Rocks', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Desert Ledge Inner Rocks', player), lambda state: state.can_lift_rocks(player)) @@ -1247,6 +1248,7 @@ def ow_bunny_rules(world, player): add_bunny_rule(world.get_entrance('Wooden Bridge Bush (North)', player), player) add_bunny_rule(world.get_entrance('Wooden Bridge Bush (South)', player), player) add_bunny_rule(world.get_entrance('Bat Cave Ledge Peg', player), player) + add_bunny_rule(world.get_entrance('Bat Cave Ledge Peg (East)', player), player) add_bunny_rule(world.get_entrance('Desert Ledge Outer Rocks', player), player) add_bunny_rule(world.get_entrance('Desert Ledge Inner Rocks', player), player) add_bunny_rule(world.get_entrance('Flute Boy Bush (North)', player), player) From f9b7b2f92655dd5a0cf61228c0f8a76a48e6b25c Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 14 Oct 2021 13:29:39 -0500 Subject: [PATCH 09/76] Reordered functions, no change --- EntranceShuffle.py | 118 +++++++++++++++++++++++---------------------- 1 file changed, 61 insertions(+), 57 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index a44edf0e..8109b80b 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -881,6 +881,7 @@ def link_entrances(world, player): if world.get_entrance('Ganons Tower' if not invFlag else 'Agahnims Tower', player).connected_region.name != 'Ganons Tower Portal' if not invFlag else 'GT Lobby': world.ganonstower_vanilla[player] = False + def connect_custom(world, player): if hasattr(world, 'custom_entrances') and world.custom_entrances[player]: for exit_name, region_name in world.custom_entrances[player]: @@ -953,6 +954,7 @@ def connect_entrance(world, entrancename, exitname, player): if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull']: world.spoiler.set_entrance(entrance.name, exit.name if exit is not None else region.name, 'entrance', player) + def connect_exit(world, exitname, entrancename, player): if not (ignore_pool or exitname == 'Chris Houlihan Room Exit'): if entrancename not in entrance_pool: @@ -1008,62 +1010,6 @@ def connect_two_way(world, entrancename, exitname, player): world.spoiler.set_entrance(entrance.name, exit.name, 'both', player) -def scramble_holes(world, player): - hole_entrances = [('Kakariko Well Cave', 'Kakariko Well Drop'), - ('Bat Cave Cave', 'Bat Cave Drop'), - ('North Fairy Cave', 'North Fairy Cave Drop'), - ('Lost Woods Hideout Stump', 'Lost Woods Hideout Drop'), - ('Lumberjack Tree Cave', 'Lumberjack Tree Tree'), - ('Sanctuary', 'Sanctuary Grave')] - - hole_targets = [('Kakariko Well Exit', 'Kakariko Well (top)'), - ('Bat Cave Exit', 'Bat Cave (right)'), - ('North Fairy Cave Exit', 'North Fairy Cave'), - ('Lost Woods Hideout Exit', 'Lost Woods Hideout (top)'), - ('Lumberjack Tree Exit', 'Lumberjack Tree (top)')] - - if world.mode[player] == 'standard': - # cannot move uncle cave - connect_two_way(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player) - connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player) - else: - hole_entrances.append(('Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Drop')) - hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance')) - - # do not shuffle sanctuary into pyramid hole unless shuffle is crossed - if world.shuffle[player] == 'crossed': - hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) - - # determine pyramid hole - if not world.shuffle_ganon: - if (world.mode[player] == 'inverted') == (0x03 in world.owswaps[player][0] and world.owMixed[player]): - connect_two_way(world, 'Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Pyramid Hole', 'Pyramid', player) - else: - connect_two_way(world, 'Inverted Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Inverted Pyramid Hole', 'Pyramid', player) - else: - hole_targets.append(('Pyramid Exit', 'Pyramid')) - - random.shuffle(hole_targets) - exit, target = hole_targets.pop() - if (world.mode[player] == 'inverted') == (0x03 in world.owswaps[player][0] and world.owMixed[player]): - connect_two_way(world, 'Pyramid Entrance', exit, player) - connect_entrance(world, 'Pyramid Hole', target, player) - else: - connect_two_way(world, 'Inverted Pyramid Entrance', exit, player) - connect_entrance(world, 'Inverted Pyramid Hole', target, player) - - if world.shuffle[player] != 'crossed': - hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) - - # shuffle the rest - random.shuffle(hole_targets) - for entrance, drop in hole_entrances: - exit, target = hole_targets.pop() - connect_two_way(world, entrance, exit, player) - connect_entrance(world, drop, target, player) - def connect_random(world, exitlist, targetlist, player, two_way=False): targetlist = list(targetlist) random.shuffle(targetlist) @@ -1076,7 +1022,6 @@ def connect_random(world, exitlist, targetlist, player, two_way=False): def connect_mandatory_exits(world, entrances, caves, must_be_exits, player): - # Keeps track of entrances that cannot be used to access each exit / cave if world.mode[player] == 'inverted': invalid_connections = Inverted_Must_Exit_Invalid_Connections.copy() @@ -1203,6 +1148,63 @@ def connect_doors(world, doors, targets, player): targets[:] = targets[placing:] +def scramble_holes(world, player): + hole_entrances = [('Kakariko Well Cave', 'Kakariko Well Drop'), + ('Bat Cave Cave', 'Bat Cave Drop'), + ('North Fairy Cave', 'North Fairy Cave Drop'), + ('Lost Woods Hideout Stump', 'Lost Woods Hideout Drop'), + ('Lumberjack Tree Cave', 'Lumberjack Tree Tree'), + ('Sanctuary', 'Sanctuary Grave')] + + hole_targets = [('Kakariko Well Exit', 'Kakariko Well (top)'), + ('Bat Cave Exit', 'Bat Cave (right)'), + ('North Fairy Cave Exit', 'North Fairy Cave'), + ('Lost Woods Hideout Exit', 'Lost Woods Hideout (top)'), + ('Lumberjack Tree Exit', 'Lumberjack Tree (top)')] + + if world.mode[player] == 'standard': + # cannot move uncle cave + connect_two_way(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player) + connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player) + else: + hole_entrances.append(('Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Drop')) + hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance')) + + # do not shuffle sanctuary into pyramid hole unless shuffle is crossed + if world.shuffle[player] == 'crossed': + hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) + + # determine pyramid hole + if not world.shuffle_ganon: + if (world.mode[player] == 'inverted') == (0x03 in world.owswaps[player][0] and world.owMixed[player]): + connect_two_way(world, 'Pyramid Entrance', 'Pyramid Exit', player) + connect_entrance(world, 'Pyramid Hole', 'Pyramid', player) + else: + connect_two_way(world, 'Inverted Pyramid Entrance', 'Pyramid Exit', player) + connect_entrance(world, 'Inverted Pyramid Hole', 'Pyramid', player) + else: + hole_targets.append(('Pyramid Exit', 'Pyramid')) + + random.shuffle(hole_targets) + exit, target = hole_targets.pop() + if (world.mode[player] == 'inverted') == (0x03 in world.owswaps[player][0] and world.owMixed[player]): + connect_two_way(world, 'Pyramid Entrance', exit, player) + connect_entrance(world, 'Pyramid Hole', target, player) + else: + connect_two_way(world, 'Inverted Pyramid Entrance', exit, player) + connect_entrance(world, 'Inverted Pyramid Hole', target, player) + + if world.shuffle[player] != 'crossed': + hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) + + # shuffle the rest + random.shuffle(hole_targets) + for entrance, drop in hole_entrances: + exit, target = hole_targets.pop() + connect_two_way(world, entrance, exit, player) + connect_entrance(world, drop, target, player) + + def skull_woods_shuffle(world, player): connect_random(world, ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'], ['Skull Left Drop', 'Skull Pinball', 'Skull Pot Circle', 'Skull Back Drop'], player) @@ -1351,6 +1353,7 @@ def simple_shuffle_dungeons(world, player): connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Turtle Rock Ledge Exit (West)', player) connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Turtle Rock Ledge Exit (East)', player) + def full_shuffle_dungeons(world, Dungeon_Exits, player): invFlag = world.mode[player] == 'inverted' @@ -1433,6 +1436,7 @@ def full_shuffle_dungeons(world, Dungeon_Exits, player): connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player) + def unbias_some_entrances(Dungeon_Exits, Cave_Exits, Old_Man_House, Cave_Three_Exits): def shuffle_lists_in_list(ls): for i, item in enumerate(ls): From 743e840087b588bc73e70d3dedae9282fc1a4950 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 14 Oct 2021 13:38:29 -0500 Subject: [PATCH 10/76] Fixed some incorrect names --- EntranceShuffle.py | 4 ++-- OverworldGlitchRules.py | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 8109b80b..b015c35a 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -2405,7 +2405,7 @@ Exit_Pool_Base = {'Links House Exit', 'Chest Game', 'Dark World Hammer Peg Cave', 'Red Shield Shop', - 'Dark Sanctuary Hint Exit', + 'Dark Sanctuary Hint', 'Fortune Teller (Dark)', 'Archery Game', 'Mire Shed', @@ -2488,7 +2488,7 @@ default_connections = [('Lumberjack House', 'Lumberjack House'), ('Dark Lake Hylia Ledge Spike Cave', 'Dark Lake Hylia Ledge Spike Cave'), ('Dark Lake Hylia Ledge Hint', 'Dark Lake Hylia Ledge Hint'), ('Bonk Fairy (Dark)', 'Bonk Fairy (Dark)'), - ('Dark Sanctuary Hint', 'Dark Sanctuary Hint Exit'), + ('Dark Sanctuary Hint', 'Dark Sanctuary Hint'), ('Fortune Teller (Dark)', 'Fortune Teller (Dark)'), ('Archery Game', 'Archery Game'), ('Dark Desert Hint', 'Dark Desert Hint'), diff --git a/OverworldGlitchRules.py b/OverworldGlitchRules.py index fcb9a18d..40c1efb9 100644 --- a/OverworldGlitchRules.py +++ b/OverworldGlitchRules.py @@ -152,8 +152,8 @@ def get_boots_clip_exits_lw(inverted = False): yield ('TR Pegs Ledge Clip', 'Death Mountain TR Pegs', 'Death Mountain TR Pegs Ledge') yield ('TR Pegs To EDM Clip', 'Death Mountain TR Pegs', 'East Death Mountain (Top East)') yield ('Zora DMD Clip', 'Death Mountain TR Pegs', 'Zora Waterfall Area') - yield ('Mountain Entry To Ledge Clip', 'Mountain Entry Area', 'Death Mountain Return Ledge') - yield ('Mountain Ledge Drop Clip', 'Death Mountain Return Ledge', 'Death Mountain Entrance') + yield ('Mountain Entry To Ledge Clip', 'Mountain Entry Area', 'Mountain Entry Ledge') + yield ('Mountain Ledge Drop Clip', 'Mountain Entry Ledge', 'Mountain Entry Entrance') yield ('Mountain Entry To Pond Clip', 'Mountain Entry Area', 'Kakariko Pond Area') yield ('Zora Waterfall Ledge Clip', 'Zora Waterfall Area', 'Zora Approach Area') @@ -225,12 +225,12 @@ def get_boots_clip_exits_dw(inverted): yield ('DDM Glitched Bridge Clip', 'West Dark Death Mountain (Bottom)', 'East Dark Death Mountain (Top)') yield ('Chapel DMD Clip', 'West Dark Death Mountain (Bottom)', 'Dark Chapel Area') yield ('Dark Graveyard DMD Clip', 'West Dark Death Mountain (Bottom)', 'Dark Graveyard Area') - yield ('EDDM West Dropdown Clip', 'East Dark Death Mountain (Top)', 'East Dark Death Mountain (West Lip)') + yield ('EDDM West Dropdown Clip', 'East Dark Death Mountain (Top)', 'East Dark Death Mountain (Bottom Left)') yield ('EDDM To WDDM Clip', 'East Dark Death Mountain (Top)', 'West Dark Death Mountain (Top)') yield ('TR Bridge Clip', 'East Dark Death Mountain (Top)', 'Dark Death Mountain Ledge') yield ('Dark Witch DMD FAWT Clip', 'East Dark Death Mountain (Bottom)', 'Dark Witch Area') - yield ('Qirn Jump DMD Clip', 'East Dark Death Mountain (West Lip)', 'Qirn Jump Area') - yield ('WDDM To EDDM Clip', 'East Dark Death Mountain (West Lip)', 'East Dark Death Mountain (Bottom)') + yield ('Qirn Jump DMD Clip', 'East Dark Death Mountain (Bottom Left)', 'Qirn Jump Area') + yield ('WDDM To EDDM Clip', 'East Dark Death Mountain (Bottom Left)', 'East Dark Death Mountain (Bottom)') #yield ('DW Floating Island Clip', 'East Dark Death Mountain (Bottom)', 'Dark Death Mountain Floating Island') #cannot guarantee camera correction yield ('TR To EDDM Clip', 'Turtle Rock Area', 'East Dark Death Mountain (Top)') yield ('Catfish DMD Clip', 'Turtle Rock Area', 'Catfish Area') @@ -312,8 +312,8 @@ def get_mirror_clip_spots_dw(): """ Out of bounds transitions using the mirror """ - yield ('Qirn Jump Bunny DMD Clip', 'East Dark Death Mountain (West Lip)', 'Qirn Jump Area') - yield ('EDDM Mirror Clip', 'East Dark Death Mountain (West Lip)', 'East Dark Death Mountain (Bottom)') + yield ('Qirn Jump Bunny DMD Clip', 'East Dark Death Mountain (Bottom Left)', 'Qirn Jump Area') + yield ('EDDM Mirror Clip', 'East Dark Death Mountain (Bottom Left)', 'East Dark Death Mountain (Bottom)') yield ('Desert East Mirror Clip', 'Misery Mire Area', 'Desert Palace Mouth') From 876b9adb940b5b3eab4a1fab44ac10bd5d4db946 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 14 Oct 2021 13:45:53 -0500 Subject: [PATCH 11/76] Fixed some mode specific things that get enabled in OWR --- OverworldShuffle.py | 4 ++-- Rom.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index e4d90be8..2ccab7aa 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -426,7 +426,7 @@ def reorganize_tile_groups(world, player): groups = {} for (name, groupType) in OWTileGroups.keys(): if world.mode[player] != 'standard' or name not in ['Castle', 'Links', 'Central Bonk Rocks']: - if world.shuffle[player] in ['vanilla', 'simple', 'dungeonssimple']: + if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'simple', 'restricted']: groups[(name,)] = ([], [], []) else: groups[(name, groupType)] = ([], [], []) @@ -434,7 +434,7 @@ def reorganize_tile_groups(world, player): for (name, groupType) in OWTileGroups.keys(): if world.mode[player] != 'standard' or name not in ['Castle', 'Links', 'Central Bonk Rocks']: (lw_owids, dw_owids) = OWTileGroups[(name, groupType,)] - if world.shuffle[player] in ['vanilla', 'simple', 'dungeonssimple']: + if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'simple', 'restricted']: (exist_owids, exist_lw_regions, exist_dw_regions) = groups[(name,)] exist_owids.extend(lw_owids) exist_owids.extend(dw_owids) diff --git a/Rom.py b/Rom.py index ad27a3b5..e494d8c4 100644 --- a/Rom.py +++ b/Rom.py @@ -2478,7 +2478,8 @@ def set_inverted_mode(world, player, rom, inverted_buffer): write_int16(rom, 0x15AEE + 2*0x25, 0x000C) if (world.mode[player] == 'inverted') != (0x03 in world.owswaps[player][0] and world.owMixed[player]): - if world.shuffle[player] in ['vanilla', 'dungeonsfull', 'dungeonssimple']: + if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull'] \ + or (world.shuffle[player] == 'simple' and (world.mode[player] == 'inverted' != (0x05 in world.owswaps[player][0] and world.owMixed[player]))): rom.write_bytes(snes_to_pc(0x308350), [0x00, 0x00, 0x01]) # mountain cave starts on OW write_int16(rom, snes_to_pc(0x02D8DE), 0x00F1) # change mountain cave spawn point to just outside old man cave From 0a187ba14cfa69eb30a6091f60c2bc7a265dcbf6 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 14 Oct 2021 14:01:38 -0500 Subject: [PATCH 12/76] Added Bat Cave Ledge Peg exit --- OWEdges.py | 1 + 1 file changed, 1 insertion(+) diff --git a/OWEdges.py b/OWEdges.py index adad2143..cfaa9c2c 100644 --- a/OWEdges.py +++ b/OWEdges.py @@ -1517,6 +1517,7 @@ OWExitTypes = { 'Wooden Bridge Bush (South)', 'Wooden Bridge Bush (North)', 'Bat Cave Ledge Peg', + 'Bat Cave Ledge Peg (East)', 'Maze Race Game', 'Desert Palace Statue Move', 'Checkerboard Ledge Approach', From 7ed4ad230496a93d47af63bf3d6a53452b7e77ba Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 14 Oct 2021 14:12:01 -0500 Subject: [PATCH 13/76] Some general error prevention and corrections --- DoorShuffle.py | 4 ++-- OverworldShuffle.py | 2 +- Regions.py | 2 +- Rom.py | 2 +- Rules.py | 23 ++++++++++++----------- 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index ca60be21..91ac070b 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1833,8 +1833,8 @@ def find_inaccessible_regions(world, player): queue.append(connect) world.inaccessible_regions[player].extend([r.name for r in all_regions.difference(visited_regions) if valid_inaccessible_region(r)]) if (world.mode[player] == 'inverted') != (0x1b in world.owswaps[player][0] and world.owMixed[player]): - ledge = world.get_region('Hyrule Castle Ledge', 1) - if any(x for x in ledge.exits if x.connected_region.name == 'Agahnims Tower Portal'): + ledge = world.get_region('Hyrule Castle Ledge', player) + if any(x for x in ledge.exits if x.connected_region and x.connected_region.name == 'Agahnims Tower Portal'): world.inaccessible_regions[player].append('Hyrule Castle Ledge') logger = logging.getLogger('') logger.debug('Inaccessible Regions:') diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 2ccab7aa..75679891 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -279,7 +279,7 @@ def link_overworld(world, player): region = world.get_region(regionname, player) for exit in region.exits: if exit.connected_region is not None and exit.connected_region.type in [RegionType.LightWorld, RegionType.DarkWorld] and exit.connected_region.name not in new_ignored: - if OWTileRegions[exit.connected_region.name] in [base_owid, owid] or OWTileRegions[regionname] == base_owid: + if exit.connected_region.name in OWTileRegions and (OWTileRegions[exit.connected_region.name] in [base_owid, owid] or OWTileRegions[regionname] == base_owid): new_ignored.add(exit.connected_region.name) getIgnored(exit.connected_region.name, base_owid, OWTileRegions[exit.connected_region.name]) diff --git a/Regions.py b/Regions.py index 556971f8..26b3aeb7 100644 --- a/Regions.py +++ b/Regions.py @@ -82,7 +82,7 @@ def create_regions(world, player): create_lw_region(player, 'Links House Area', None, ['Links House', 'Big Bomb Shop Mirror Spot', 'Links House NE', 'Links House WN', 'Links House WC', 'Links House WS', 'Links House SC', 'Links House ES']), create_lw_region(player, 'Stone Bridge Area', None, ['Hammer Bridge North Mirror Spot', 'Hammer Bridge South Mirror Spot', 'Stone Bridge NC', 'Stone Bridge EN', 'Stone Bridge WS', 'Stone Bridge SC']), create_lw_region(player, 'Stone Bridge Water', None, ['Dark Hobo Mirror Spot', 'Stone Bridge WC', 'Stone Bridge EC'], Terrain.Water), - create_lw_region(player, 'Hobo Bridge', ['Hobo'], ['Hobo EC']), + create_lw_region(player, 'Hobo Bridge', ['Hobo'], ['Hobo EC'], Terrain.Water), create_lw_region(player, 'Central Cliffs', None, ['Central Bonk Rocks Cliff Ledge Drop', 'Links House Cliff Ledge Drop', 'Stone Bridge Cliff Ledge Drop', 'Lake Hylia Area Cliff Ledge Drop', 'Lake Hylia Island FAWT Ledge Drop', 'Stone Bridge EC Cliff Water Drop', 'Tree Line WC Cliff Water Drop', 'C Whirlpool Outer Cliff Ledge Drop', 'C Whirlpool Cliff Ledge Drop', 'South Teleporter Cliff Ledge Drop', 'Statues Cliff Ledge Drop']), create_lw_region(player, 'Tree Line Area', None, ['Lake Hylia Fairy', 'Dark Tree Line Mirror Spot', 'Tree Line WN', 'Tree Line NW', 'Tree Line SE']), create_lw_region(player, 'Tree Line Water', None, ['Tree Line WC', 'Tree Line SC'], Terrain.Water), diff --git a/Rom.py b/Rom.py index e494d8c4..074ec8fc 100644 --- a/Rom.py +++ b/Rom.py @@ -2500,7 +2500,7 @@ def set_inverted_mode(world, player, rom, inverted_buffer): write_int16(rom, snes_to_pc(0x02D9B0), 0x0007) rom.write_byte(snes_to_pc(0x02D9B8), 0x12) - rom.write_bytes(0x180247, [0x00, 0x5A, 0x00, 0x00, 0x00, 0x00, 0x00]) #indicates the overworld door being used for the single entrance spawn point + rom.write_bytes(0x180247, [0x00, 0x5A, 0x00, 0x00, 0x00, 0x00, 0x00]) # indicates the overworld door being used for the single entrance spawn point if (world.mode[player] == 'inverted') != (0x05 in world.owswaps[player][0] and world.owMixed[player]): rom.write_bytes(snes_to_pc(0x1BC655), [0x4A, 0x1D, 0x82]) # add warp under rock if (world.mode[player] == 'inverted') != (0x07 in world.owswaps[player][0] and world.owMixed[player]): diff --git a/Rules.py b/Rules.py index 2dc4df99..a99fc196 100644 --- a/Rules.py +++ b/Rules.py @@ -106,14 +106,15 @@ def mirrorless_path_to_castle_courtyard(world, player): queue = collections.deque([(start.connected_region, [])]) while queue: (current, path) = queue.popleft() - for entrance in current.exits: - if entrance.connected_region not in seen: - new_path = path + [entrance.access_rule] - if entrance.connected_region == target: - return new_path - else: - queue.append((entrance.connected_region, new_path)) - seen.add(entrance.connected_region) + if current: + for entrance in current.exits: + if entrance.connected_region not in seen: + new_path = path + [entrance.access_rule] + if entrance.connected_region == target: + return new_path + else: + queue.append((entrance.connected_region, new_path)) + seen.add(entrance.connected_region) def set_rule(spot, rule): spot.access_rule = rule @@ -1863,7 +1864,7 @@ def set_big_bomb_rules(world, player): add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Flippers', player) or state.can_flute(player))) #TODO: Fix red bomb rules, artifically adding a bunch of rules to help reduce unbeatable seeds in OW shuffle - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: False) + set_rule(world.get_entrance('Pyramid Fairy', player), lambda state: False) #add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.can_reach('Pyramid Area', 'Region', player)) #add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.can_lift_heavy_rocks(player) and state.has('Flippers', player) and state.can_flute(player) and state.has('Hammer', player) and state.has('Hookshot', player) and state.has_Pearl(player) and state.has_Mirror(player))) @@ -2061,7 +2062,7 @@ def set_inverted_big_bomb_rules(world, player): raise Exception('No logic found for routing from %s to the pyramid.' % bombshop_entrance.name) if world.owShuffle[player] != 'vanilla' or world.owMixed[player] or world.owCrossed[player] != 'none': - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: False) #temp disable progression until routing to Pyramid get be guaranteed + set_rule(world.get_entrance('Pyramid Fairy', player), lambda state: False) #temp disable progression until routing to Pyramid get be guaranteed def set_bunny_rules(world, player, inverted): @@ -2200,7 +2201,7 @@ def set_bunny_rules(world, player, inverted): for ent_name in bunny_impassible_doors: bunny_exit = world.get_entrance(ent_name, player) - if is_bunny(bunny_exit.parent_region): + if bunny_exit.connected_region and is_bunny(bunny_exit.parent_region): add_rule(bunny_exit, get_rule_to_add(bunny_exit.parent_region)) doors_to_check = [x for x in world.doors if x.player == player and x not in bunny_impassible_doors] From 1327408a017c3248ca5ae2570c1974e02e472c88 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 14 Oct 2021 14:12:26 -0500 Subject: [PATCH 14/76] Fixed some mode specific things that get enabled in OWR --- Rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rules.py b/Rules.py index a99fc196..ef688f37 100644 --- a/Rules.py +++ b/Rules.py @@ -2061,7 +2061,7 @@ def set_inverted_big_bomb_rules(world, player): else: raise Exception('No logic found for routing from %s to the pyramid.' % bombshop_entrance.name) - if world.owShuffle[player] != 'vanilla' or world.owMixed[player] or world.owCrossed[player] != 'none': + if world.owShuffle[player] != 'vanilla' or world.owMixed[player] or world.owCrossed[player] not in ['none', 'polar']: set_rule(world.get_entrance('Pyramid Fairy', player), lambda state: False) #temp disable progression until routing to Pyramid get be guaranteed From 6aeabc5e667ab4023c8916a1d4b2f0fc2a0cf5d3 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 14 Oct 2021 14:15:51 -0500 Subject: [PATCH 15/76] Added rule changes that only apply to reachability checks, prior to actual game simluation --- BaseClasses.py | 2 +- Rules.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 4b9f9d00..ab033715 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1234,7 +1234,7 @@ class CollectionState(object): # In the future, this can be used to check if the player starts without bombs def can_use_bombs(self, player): - return (not self.world.bombbag[player] or self.has('Bomb Upgrade (+10)', player)) and self.can_farm_bombs(player) + return (not self.world.bombbag[player] or self.has('Bomb Upgrade (+10)', player)) and ((hasattr(self.world,"override_bomb_check") and self.world.override_bomb_check) or self.can_farm_bombs(player)) def can_hit_crystal(self, player): return (self.can_use_bombs(player) diff --git a/Rules.py b/Rules.py index ef688f37..c4429317 100644 --- a/Rules.py +++ b/Rules.py @@ -24,7 +24,8 @@ def set_rules(world, player): ow_bunny_rules(world, player) if world.mode[player] == 'standard': - standard_rules(world, player) + if world.get_region('Big Bomb Shop', player).entrances: # just some location that is placed late in the ER algorithm, prevent standard rules from applying when trying to search reachability in the overworld + standard_rules(world, player) elif world.mode[player] == 'open' or world.mode[player] == 'inverted': open_rules(world, player) else: From 9d7663c64847f03b7c4dcfe5355ce6fb08db900d Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 14 Oct 2021 22:01:56 -0500 Subject: [PATCH 16/76] Adding new shared/utility functions --- EntranceShuffle.py | 340 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 340 insertions(+) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index b015c35a..678b9959 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -1437,6 +1437,215 @@ def full_shuffle_dungeons(world, Dungeon_Exits, player): connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player) +def place_links_house(world, sectors, player): + invFlag = world.mode[player] == 'inverted' + if world.mode[player] == 'standard' or not world.shufflelinks[player]: + links_house = 'Links House' if not invFlag else 'Big Bomb Shop' + else: + def get_link_candidates(): + # find largest walkable sector + sector = None + invalid_sectors = list() + while (sector is None): + sector = max(sectors, key=lambda x: len(x) - (0 if x not in invalid_sectors else 1000)) + if not ((world.owCrossed[player] == 'polar' and world.owMixed[player]) or world.owCrossed[player] not in ['none', 'polar']) \ + and world.get_region(next(iter(next(iter(sector)))), player).type != (RegionType.LightWorld if not invFlag else RegionType.DarkWorld): + invalid_sectors.append(sector) + sector = None + regions = max(sector, key=lambda x: len(x)) + + # get entrances from list of regions + entrances = list() + for region_name in [r for r in regions if r ]: + if world.shuffle[player] == 'simple' and region_name in OWTileRegions and OWTileRegions[region_name] in [0x03, 0x05, 0x07]: + continue + region = world.get_region(region_name, player) + if region.type == RegionType.LightWorld if not invFlag else RegionType.DarkWorld: + for exit in region.exits: + if not exit.connected_region and exit.spot_type == 'Entrance': + entrances.append(exit.name) + return entrances + + links_house_doors = [i for i in get_link_candidates() if i in entrance_pool] + links_house = random.choice(links_house_doors) + connect_two_way(world, links_house, 'Links House Exit', player) + return links_house + + +def place_dark_sanc(world, links_house, sectors, player): + # get walkable sector in which links house was placed + links_region = world.get_entrance(links_house, player).parent_region.name + regions = next(s for s in sectors if any(links_region in w for w in s)) + regions = next(w for w in regions if links_region in w) + + # eliminate regions surrounding links until less than half of the candidate regions remain + explored_regions = list({links_region}) + was_progress = True + while was_progress and len(explored_regions) < len(regions) / 2: + was_progress = False + new_regions = list() + for region_name in explored_regions: + region = world.get_region(region_name, player) + for exit in region.exits: + if exit.connected_region and region.type == exit.connected_region.type and exit.connected_region.name in regions and exit.connected_region.name not in explored_regions + new_regions: + new_regions.append(exit.connected_region.name) + was_progress = True + explored_regions.extend(new_regions) + + # get entrances from remaining regions + sanc_doors = list() + for region_name in [r for r in regions if r not in explored_regions]: + if OWTileRegions[region_name] in [0x03, 0x05, 0x07]: + continue + region = world.get_region(region_name, player) + for exit in region.exits: + if not exit.connected_region and exit.spot_type == 'Entrance': + sanc_doors.append(exit.name) + + sanc_doors = [i for i in sanc_doors if i in entrance_pool] + sanc_door = random.choice(sanc_doors) + connect_entrance(world, sanc_door, 'Dark Sanctuary Hint', player) + world.get_entrance('Dark Sanctuary Hint Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) + return sanc_door + + +def place_blacksmith(world, links_house, player): + invFlag = world.mode[player] == 'inverted' + + assumed_inventory = list() + region = world.get_region('Frog Prison', player) + if world.logic[player] in ['noglitches', 'minorglitches'] and region.type == (RegionType.DarkWorld if not invFlag else RegionType.LightWorld): + assumed_inventory.append('Titans Mitts') + + links_region = world.get_entrance(links_house, player).parent_region.name + blacksmith_doors = list(build_accessible_entrance_list(world, links_region, player, assumed_inventory, False, True, True)) + + if invFlag: + dark_sanc = world.get_entrance('Dark Sanctuary Hint Exit', player).connected_region.name + blacksmith_doors = list(set(blacksmith_doors + list(build_accessible_entrance_list(world, dark_sanc, player, assumed_inventory, False, True, True)))) + elif world.doorShuffle[player] == 'vanilla' or world.intensity[player] < 3: + sanc_region = world.get_entrance('Sanctuary Exit', player).connected_region.name + blacksmith_doors = list(set(blacksmith_doors + list(build_accessible_entrance_list(world, sanc_region, player, assumed_inventory, False, True, True)))) + + random.shuffle(blacksmith_doors) + blacksmith_hut = blacksmith_doors.pop() + connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) + return blacksmith_hut + + +def place_old_man(world, pool, player): + # exit has to come from specific set of doors, the entrance is free to move about + if (world.mode[player] == 'inverted') == (0x03 in world.owswaps[player][0] and world.owMixed[player]): + region_name = 'West Death Mountain (Top)' + else: + region_name = 'West Dark Death Mountain (Top)' + old_man_entrances = list(build_accessible_entrance_list(world, region_name, player, [], False, True, True)) + random.shuffle(old_man_entrances) + old_man_exit = None + while not old_man_exit: + old_man_exit = old_man_entrances.pop() + if 'West Death Mountain (Bottom)' not in build_accessible_region_list(world, world.get_entrance(old_man_exit, player).parent_region.name, player, True): + old_man_exit = None + + old_man_entrances = [e for e in pool if e in entrance_pool and e not in entrance_exits] + random.shuffle(old_man_entrances) + old_man_entrance = old_man_entrances.pop() + if world.shuffle[player] != 'insanity': + connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) + connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) + else: + # skip assigning connections to West Entrance/Exit + connect_exit(world, 'Old Man Cave Exit (East)', old_man_exit, player, False) + connect_entrance(world, old_man_entrance, 'Old Man Cave Exit (East)', player, False) + + +def junk_fill_inaccessible(world, player): + from DoorShuffle import find_inaccessible_regions + find_inaccessible_regions(world, player) + + # remove regions that have a dungeon entrance + accessible_regions = list() + for region_name in world.inaccessible_regions[player]: + region = world.get_region(region_name, player) + for exit in region.exits: + if exit.connected_region and exit.connected_region.type == RegionType.Dungeon: + accessible_regions.append(region_name) + break + for region_name in accessible_regions.copy(): + accessible_regions = list(set(accessible_regions + list(build_accessible_region_list(world, region_name, player, True, False, False)))) + world.inaccessible_regions[player] = [r for r in world.inaccessible_regions[player] if r not in accessible_regions] + + # get inaccessible entrances + inaccessible_entrances = list() + for region_name in world.inaccessible_regions[player]: + region = world.get_region(region_name, player) + if region.type in [RegionType.LightWorld, RegionType.DarkWorld]: + for exit in region.exits: + if not exit.connected_region and exit.name in entrance_pool: + inaccessible_entrances.append(exit.name) + #TODO: assign non-item locations to the entrances that exist in the unreachable regions + + +def connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, player): + invFlag = world.mode[player] == 'inverted' + + random.shuffle(lw_entrances) + random.shuffle(dw_entrances) + + from DoorShuffle import find_inaccessible_regions + find_inaccessible_regions(world, player) + + # remove regions that have a dungeon entrance + accessible_regions = list() + for region_name in world.inaccessible_regions[player]: + region = world.get_region(region_name, player) + for exit in region.exits: + if exit.connected_region and exit.connected_region.type == RegionType.Dungeon: + accessible_regions.append(region_name) + break + for region_name in accessible_regions.copy(): + accessible_regions = list(set(accessible_regions + list(build_accessible_region_list(world, region_name, player, True, False, False)))) + world.inaccessible_regions[player] = [r for r in world.inaccessible_regions[player] if r not in accessible_regions] + + # split inaccessible into 2 lists for each world + inaccessible_regions = list(world.inaccessible_regions[player]) + must_exit_regions = list() + otherworld_must_exit_regions = list() + for region_name in inaccessible_regions.copy(): + region = world.get_region(region_name, player) + if region.type not in [RegionType.LightWorld, RegionType.DarkWorld] or not any((not exit.connected_region and exit.spot_type == 'Entrance') for exit in region.exits) \ + or (region_name == 'Pyramid Exit Ledge' and invFlag != (0x1b in world.owswaps[player][0] and world.owMixed[player])): + inaccessible_regions.remove(region_name) + elif region.type == (RegionType.LightWorld if not invFlag else RegionType.DarkWorld): + must_exit_regions.append(region_name) + elif region.type == (RegionType.DarkWorld if not invFlag else RegionType.LightWorld): + otherworld_must_exit_regions.append(region_name) + + def connect_one(region_name, pool): + inaccessible_entrances = list() + region = world.get_region(region_name, player) + for exit in region.exits: + if not exit.connected_region and exit.name in entrance_pool: + inaccessible_entrances.append(exit.name) + random.shuffle(inaccessible_entrances) + connect_mandatory_exits(world, pool, caves, [inaccessible_entrances.pop()], player) + connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, player) + + # connect one connector at a time to ensure multiple connectors aren't assigned to the same inaccessible set of regions + if world.shuffle[player] in ['crossed', 'insanity']: + combined_must_exit_regions = list(must_exit_regions + otherworld_must_exit_regions) + if len(combined_must_exit_regions) > 0: + random.shuffle(combined_must_exit_regions) + connect_one(combined_must_exit_regions[0], [e for e in lw_entrances if e in entrance_pool]) + else: + if len(otherworld_must_exit_regions) > 0: + random.shuffle(otherworld_must_exit_regions) + connect_one(otherworld_must_exit_regions[0], [e for e in (dw_entrances if not invFlag else lw_entrances) if e in entrance_pool]) + elif len(must_exit_regions) > 0: + random.shuffle(must_exit_regions) + connect_one(must_exit_regions[0], [e for e in (lw_entrances if not invFlag else dw_entrances) if e in entrance_pool]) + + def unbias_some_entrances(Dungeon_Exits, Cave_Exits, Old_Man_House, Cave_Three_Exits): def shuffle_lists_in_list(ls): for i, item in enumerate(ls): @@ -1477,6 +1686,137 @@ def unbias_some_entrances(Dungeon_Exits, Cave_Exits, Old_Man_House, Cave_Three_E tuplize_lists_in_list(Cave_Three_Exits) +def build_sectors(world, player): + from Main import copy_world + from OWEdges import OWTileRegions + + # perform accessibility check on duplicate world + for player in range(1, world.players + 1): + world.key_logic[player] = {} + base_world = copy_world(world) + world.key_logic = {} + + # build lists of contiguous regions accessible with full inventory (excl portals/mirror/flute/entrances) + regions = list(OWTileRegions.copy().keys()) + sectors = list() + while(len(regions) > 0): + explored_regions = build_accessible_region_list(base_world, regions[0], player, False, False, False) + regions = [r for r in regions if r not in explored_regions] + unique_regions = [_ for i in range(len(sectors)) for _ in sectors[i]] + if (any(r in unique_regions for r in explored_regions)): + for s in range(len(sectors)): + if (any(r in sectors[s] for r in explored_regions)): + sectors[s] = set(list(sectors[s]) + list(explored_regions)) + break + else: + sectors.append(explored_regions) + + # remove water regions if Flippers not in starting inventory + if not any(map(lambda i: i.name == 'Flippers', world.precollected_items)): + for s in range(len(sectors)): + terrains = list() + for regionname in sectors[s]: + region = world.get_region(regionname, player) + if region.terrain == Terrain.Land: + terrains.append(regionname) + sectors[s] = terrains + + # within each group, split into contiguous regions accessible only with starting inventory + for s in range(len(sectors)): + regions = list(sectors[s]).copy() + sectors2 = list() + while(len(regions) > 0): + explored_regions = build_accessible_region_list(base_world, regions[0], player, False, True, False) + regions = [r for r in regions if r not in explored_regions] + unique_regions = [_ for i in range(len(sectors2)) for _ in sectors2[i]] + if (any(r in unique_regions for r in explored_regions)): + for s2 in range(len(sectors2)): + if (any(r in sectors2[s2] for r in explored_regions)): + sectors2[s2] = set(list(sectors2[s2]) + list(explored_regions)) + break + else: + sectors2.append(explored_regions) + sectors[s] = sectors2 + + return sectors + + +def build_accessible_region_list(world, start_region, player, cross_world=False, region_rules=True, ignore_ledges = False): + def explore_region(region_name): + explored_regions.add(region_name) + region = world.get_region(region_name, player) + for exit in region.exits: + if exit.connected_region is not None \ + and ((any(map(lambda i: i.name == 'Ocarina', world.precollected_items)) and exit.spot_type == 'Flute') \ + or (exit.connected_region.type == region.type or (cross_world and exit.connected_region.type not in [RegionType.Cave, RegionType.Dungeon]))) \ + and (not region_rules or exit.access_rule(blank_state)) and (not ignore_ledges or exit.spot_type != 'Ledge') \ + and exit.connected_region.name not in explored_regions: + if exit.spot_type == 'Flute': + fluteregion = exit.connected_region + for flutespot in fluteregion.exits: + if flutespot.connected_region and flutespot.connected_region.name not in explored_regions: + explore_region(flutespot.connected_region.name) + else: + explore_region(exit.connected_region.name) + + connect_simple(world, 'Links House S&Q', start_region, player) + blank_state = CollectionState(world) + explored_regions = set() + explore_region(start_region) + + return explored_regions + + +def build_accessible_entrance_list(world, start_region, player, assumed_inventory=[], cross_world=False, region_rules=True, exit_rules=True): + from Main import copy_world + from Items import ItemFactory + + for player in range(1, world.players + 1): + world.key_logic[player] = {} + base_world = copy_world(world) + base_world.override_bomb_check = True + world.key_logic = {} + + connect_simple(base_world, 'Links House S&Q', start_region, player) + blank_state = CollectionState(base_world) + if base_world.mode[player] == 'standard': + blank_state.collect(ItemFactory('Zelda Delivered', player), True) + for item in assumed_inventory: + blank_state.collect(ItemFactory(item, player), True) + + explored_regions = build_accessible_region_list(base_world, start_region, player, cross_world, region_rules, False) + + entrances = set() + for region_name in explored_regions: + region = base_world.get_region(region_name, player) + for exit in region.exits: + if exit.name in entrance_pool and (not exit_rules or exit.access_rule(blank_state)): + entrances.add(exit.name) + + return entrances + + +def can_reach(world, entrance_name, region_name, player): + from Main import copy_world + from Items import ItemFactory + from DoorShuffle import find_inaccessible_regions + + for player in range(1, world.players + 1): + world.key_logic[player] = {} + base_world = copy_world(world) + base_world.override_bomb_check = True + world.key_logic = {} + + entrance = world.get_entrance(entrance_name, player) + connect_simple(base_world, 'Links House S&Q', entrance.parent_region.name, player) + blank_state = CollectionState(base_world) + if base_world.mode[player] == 'standard': + blank_state.collect(ItemFactory('Zelda Delivered', player), True) + + find_inaccessible_regions(world, player) + return region_name not in world.inaccessible_regions[player] + + LW_Dungeon_Entrances = ['Desert Palace Entrance (South)', 'Desert Palace Entrance (West)', 'Desert Palace Entrance (North)', From 600ea250030ccca771877906fa3b61847009a64b Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 14 Oct 2021 22:13:47 -0500 Subject: [PATCH 17/76] Initial check-in for ER rewrite --- EntranceShuffle.py | 2227 +++++++++++++------------------------------ Main.py | 2 + OverworldShuffle.py | 2 + 3 files changed, 678 insertions(+), 1553 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 678b9959..dad10a17 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -1,28 +1,32 @@ -import RaceRandom as random - -# ToDo: With shuffle_ganon option, prevent gtower from linking to an exit only location through a 2 entrance cave. +import logging from collections import defaultdict +import RaceRandom as random +from BaseClasses import CollectionState, RegionType, Terrain +from OWEdges import OWTileRegions entrance_pool = list() exit_pool = list() +entrance_exits = list() ignore_pool = False +suppress_spoiler = True def link_entrances(world, player): invFlag = world.mode[player] == 'inverted' - global entrance_pool, exit_pool, ignore_pool + global entrance_pool, exit_pool, ignore_pool, suppress_spoiler entrance_pool = Entrance_Pool_Base.copy() exit_pool = Exit_Pool_Base.copy() drop_connections = default_drop_connections.copy() dropexit_connections = default_dropexit_connections.copy() - isolated_entrances = Isolated_LH_Doors.copy() - # modifications to lists Dungeon_Exits = Dungeon_Exits_Base.copy() Cave_Exits = Cave_Exits_Base.copy() Old_Man_House = Old_Man_House_Base.copy() Cave_Three_Exits = Cave_Three_Exits_Base.copy() + sectors = build_sectors(world, player) + + # modifications to lists if invFlag == (0x1b in world.owswaps[player][0] and world.owMixed[player]): drop_connections.append(tuple(('Pyramid Hole', 'Pyramid'))) dropexit_connections.append(tuple(('Pyramid Entrance', 'Pyramid Exit'))) @@ -42,6 +46,7 @@ def link_entrances(world, player): connect_simple(world, 'Old Man S&Q', 'West Dark Death Mountain (Bottom)', player) unbias_some_entrances(Dungeon_Exits, Cave_Exits, Old_Man_House, Cave_Three_Exits) + Cave_Exits.extend(Cave_Exits_Directional) # setup mandatory connections for exitname, regionname in mandatory_connections: @@ -58,9 +63,6 @@ def link_entrances(world, player): connect_custom(world, player) - if invFlag == (0x05 in world.owswaps[player][0] and world.owMixed[player]): - isolated_entrances.append('Mimic Cave') - # if we do not shuffle, set default connections if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']: for entrancename, exitname in default_connections + drop_connections + default_item_connections + default_shop_connections: @@ -107,24 +109,48 @@ def link_entrances(world, player): for entrancename, exitname in inverted_default_dungeon_connections: connect_logical(world, entrancename, exitname, player, True) elif world.shuffle[player] == 'dungeonssimple': + suppress_spoiler = False simple_shuffle_dungeons(world, player) elif world.shuffle[player] == 'dungeonsfull': + suppress_spoiler = False full_shuffle_dungeons(world, Dungeon_Exits, player) elif world.shuffle[player] == 'simple': + suppress_spoiler = False simple_shuffle_dungeons(world, player) - old_man_entrances = list(Old_Man_Entrances) if not invFlag else list(Inverted_Old_Man_Entrances) + # shuffle dropdowns + scramble_holes(world, player) + + # list modification + lw_wdm_entrances = ['Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)', + 'Death Mountain Return Cave (East)', 'Spectacle Rock Cave', + 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)'] + lw_edm_entrances = ['Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'Paradox Cave (Top)', 'Spiral Cave', + 'Fairy Ascension Cave (Bottom)', 'Fairy Ascension Cave (Top)', 'Spiral Cave (Bottom)'] + ddm_entrances = ['Dark Death Mountain Fairy', 'Spike Cave'] + caves = list(Cave_Exits) three_exit_caves = list(Cave_Three_Exits) - single_doors = list(Single_Cave_Doors) - bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors) if not invFlag else list(Inverted_Bomb_Shop_Single_Cave_Doors) - blacksmith_doors = list(Blacksmith_Single_Cave_Doors) + ['Links House'] if not invFlag else [] - door_targets = list(Single_Cave_Targets) if not invFlag else list(Inverted_Single_Cave_Targets) - - # we shuffle all 2 entrance caves as pairs as a start + Two_Door_Caves_Directional = list() + Two_Door_Caves = [('Elder House (East)', 'Elder House (West)'), + ('Superbunny Cave (Bottom)', 'Superbunny Cave (Top)')] + if invFlag == (0x0a in world.owswaps[player][0] and world.owMixed[player]): + Two_Door_Caves_Directional.append(tuple({'Bumper Cave (Bottom)', 'Bumper Cave (Top)'})) + else: + Two_Door_Caves_Directional.append(tuple({'Old Man Cave (West)', 'Death Mountain Return Cave (West)'})) + if invFlag == (0x05 in world.owswaps[player][0] and world.owMixed[player]): + Two_Door_Caves_Directional.append(tuple({'Hookshot Cave', 'Hookshot Cave Back Entrance'})) + else: + Two_Door_Caves.append(tuple({'Hookshot Cave', 'Hookshot Cave Back Entrance'})) + if invFlag == (0x28 in world.owswaps[player][0] and world.owMixed[player]): + Two_Door_Caves.append(tuple({'Two Brothers House (East)', 'Two Brothers House (West)'})) + else: + Two_Door_Caves_Directional.append(tuple({'Two Brothers House (East)', 'Two Brothers House (West)'})) + + # shuffle all 2 entrance caves as pairs as a start # start with the ones that need to be directed - two_door_caves = list(Two_Door_Caves_Directional) if not invFlag else list(Inverted_Two_Door_Caves_Directional) + two_door_caves = list(Two_Door_Caves_Directional) random.shuffle(two_door_caves) random.shuffle(caves) while two_door_caves: @@ -133,8 +159,8 @@ def link_entrances(world, player): connect_two_way(world, entrance1, exit1, player) connect_two_way(world, entrance2, exit2, player) - # now the remaining pairs - two_door_caves = list(Two_Door_Caves) if not invFlag else list(Inverted_Two_Door_Caves) + # shuffle remaining 2 entrance cave pairs + two_door_caves = list(Two_Door_Caves) random.shuffle(two_door_caves) while two_door_caves: entrance1, entrance2 = two_door_caves.pop() @@ -142,567 +168,263 @@ def link_entrances(world, player): connect_two_way(world, entrance1, exit1, player) connect_two_way(world, entrance2, exit2, player) - # place links house - if world.mode[player] == 'standard' or not world.shufflelinks[player]: - links_house = 'Links House' if not invFlag else 'Big Bomb Shop' - else: - links_house_doors = [i for i in (LW_Single_Cave_Doors if not invFlag else DW_Single_Cave_Doors) if i not in isolated_entrances + ([] if not invFlag else Inverted_Dark_Sanctuary_Doors)] - links_house = random.choice(links_house_doors) - connect_two_way(world, links_house, 'Links House Exit', player) - - if links_house in bomb_shop_doors: - bomb_shop_doors.remove(links_house) - if links_house in blacksmith_doors: - blacksmith_doors.remove(links_house) - if links_house in old_man_entrances: - old_man_entrances.remove(links_house) - - if invFlag: - # place dark sanc - sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in bomb_shop_doors] - sanc_door = random.choice(sanc_doors) - bomb_shop_doors.remove(sanc_door) - connect_entrance(world, sanc_door, 'Dark Sanctuary Hint', player) - world.get_entrance('Dark Sanctuary Hint Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) - - lw_dm_entrances = ['Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'Paradox Cave (Top)', 'Old Man House (Bottom)', - 'Fairy Ascension Cave (Bottom)', 'Fairy Ascension Cave (Top)', 'Spiral Cave (Bottom)', 'Old Man Cave (East)', - 'Death Mountain Return Cave (East)', 'Spiral Cave', 'Old Man House (Top)', 'Spectacle Rock Cave', - 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)'] - - # place old man, bumper cave bottom to DDM entrances not in east bottom - else: - # at this point only Light World death mountain entrances remain - # place old man, has limited options - lw_dm_entrances = ['Old Man Cave (West)', 'Old Man House (Bottom)', 'Death Mountain Return Cave (West)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'Paradox Cave (Top)', - 'Fairy Ascension Cave (Bottom)', 'Fairy Ascension Cave (Top)', 'Spiral Cave', 'Spiral Cave (Bottom)'] - - random.shuffle(old_man_entrances) - old_man_exit = old_man_entrances.pop() - if not invFlag: - lw_dm_entrances.extend(old_man_entrances) - random.shuffle(lw_dm_entrances) - old_man_entrance = lw_dm_entrances.pop() - connect_two_way(world, old_man_entrance if invFlag == (0x0a in world.owswaps[player][0] and world.owMixed[player]) else 'Bumper Cave (Bottom)', 'Old Man Cave Exit (West)', player) - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) - - if invFlag and old_man_exit == 'Spike Cave': - bomb_shop_doors.remove('Spike Cave') - bomb_shop_doors.extend(old_man_entrances) - - # add old man house to ensure it is always somewhere on light death mountain + # shuffle LW DM entrances caves.extend(list(Old_Man_House)) caves.extend(list(three_exit_caves)) - # connect rest - connect_caves(world, lw_dm_entrances, [], caves, player) + if invFlag == (0x18 in world.owswaps[player][0] and world.owMixed[player]) or invFlag == (0x03 in world.owswaps[player][0] and world.owMixed[player]): # ability to activate flute in LW + candidates = [e for e in lw_wdm_entrances if e != 'Old Man House (Bottom)'] + random.shuffle(candidates) + old_man_exit = candidates.pop() + lw_wdm_entrances.remove(old_man_exit) + connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) - # scramble holes - scramble_holes(world, player) + if invFlag == (0x0a in world.owswaps[player][0] and world.owMixed[player]): + lw_wdm_entrances.extend(['Old Man Cave (West)', 'Death Mountain Return Cave (West)']) + else: + lw_wdm_entrances.extend(['Bumper Cave (Bottom)', 'Bumper Cave (Top)']) + + if 0x03 in world.owswaps[player][0] == 0x05 in world.owswaps[player][0]: # if WDM and EDM are in same world + candidates = lw_wdm_entrances + lw_edm_entrances + random.shuffle(candidates) + old_man_entrance = candidates.pop() + lw_wdm_entrances.remove(old_man_entrance) + if old_man_entrance in lw_wdm_entrances: + lw_wdm_entrances.remove(old_man_entrance) + elif old_man_entrance in lw_edm_entrances: + lw_edm_entrances.remove(old_man_entrance) + else: + random.shuffle(lw_wdm_entrances) + old_man_entrance = lw_wdm_entrances.pop() + connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) + else: + # force connection to DM + random.shuffle(ddm_entrances) + old_man_exit = ddm_entrances.pop() + connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) + + # place old man, bumper cave bottom to DDM entrances not in east bottom + if invFlag == (0x0a in world.owswaps[player][0] and world.owMixed[player]): + connect_two_way(world, 'Old Man Cave (West)', 'Old Man Cave Exit (West)', player) + else: + connect_two_way(world, 'Bumper Cave (Bottom)', 'Old Man Cave Exit (West)', player) + # connect remaining LW DM entrances + if 0x03 in world.owswaps[player][0] == 0x05 in world.owswaps[player][0]: # if WDM and EDM are in same world + connect_caves(world, lw_wdm_entrances + lw_edm_entrances, [], caves, player) + else: + # place Old Man House in WDM if not swapped + if invFlag == (0x03 in world.owswaps[player][0] and world.owMixed[player]): + connect_caves(world, lw_wdm_entrances, [], list(Old_Man_House), player) + else: + connect_caves(world, lw_edm_entrances, [], list(Old_Man_House), player) + caves.remove(Old_Man_House[0]) + + i = 0 + c = 0 + while i != len(lw_wdm_entrances): + random.shuffle(caves) + i = 0 + c = 0 + while i < len(lw_wdm_entrances): + i += len(caves[c]) + c += 1 + + connect_caves(world, lw_wdm_entrances, [], caves[0:c], player) + connect_caves(world, lw_edm_entrances, [], caves[c:], player) + + # place links house + links_house = place_links_house(world, sectors, player) + + if invFlag: + # place dark sanc + place_dark_sanc(world, links_house, sectors, player) + # place blacksmith, has limited options - if invFlag: - blacksmith_doors = [door for door in blacksmith_doors[:]] - random.shuffle(blacksmith_doors) - blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) - bomb_shop_doors.extend(blacksmith_doors) + place_blacksmith(world, links_house, player) + # junk fill inaccessible regions + # TODO: Should be obsolete someday when OWR rebalances the shuffle to prevent unreachable regions + junk_fill_inaccessible(world, player) + # place bomb shop, has limited options - if invFlag: - bomb_shop_doors = [door for door in bomb_shop_doors[:]] + bomb_shop_doors = list(entrance_pool) + if world.logic[player] in ['noglitches', 'minorglitches'] or (invFlag != (0x1b in world.owswaps[player][0] and world.owMixed[player])): + bomb_shop_doors = [e for e in entrance_pool if e not in ['Pyramid Fairy']] random.shuffle(bomb_shop_doors) bomb_shop = bomb_shop_doors.pop() connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - single_doors.extend(bomb_shop_doors) - + # place remaining doors - connect_doors(world, single_doors, door_targets, player) + connect_doors(world, list(entrance_pool), list(exit_pool), player) elif world.shuffle[player] == 'restricted': + suppress_spoiler = False simple_shuffle_dungeons(world, player) - if not invFlag: - lw_entrances = list(LW_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances) - dw_entrances = list(DW_Entrances + DW_Single_Cave_Doors) - dw_must_exits = list(DW_Entrances_Must_Exit) - old_man_entrances = list(Old_Man_Entrances) - caves = list(Cave_Exits + Cave_Three_Exits) - single_doors = list(Single_Cave_Doors) - bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors + Bomb_Shop_Multi_Cave_Doors) - blacksmith_doors = list(Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors) - door_targets = list(Single_Cave_Targets) - else: - lw_entrances = list(Inverted_LW_Entrances + LW_Single_Cave_Doors) - dw_entrances = list(Inverted_DW_Entrances + Inverted_DW_Single_Cave_Doors + Inverted_Old_Man_Entrances) - lw_must_exits = list(Inverted_LW_Entrances_Must_Exit) - old_man_entrances = list(Inverted_Old_Man_Entrances) - caves = list(Cave_Exits + Cave_Three_Exits + Old_Man_House) - single_doors = list(Single_Cave_Doors) - bomb_shop_doors = list(Inverted_Bomb_Shop_Single_Cave_Doors + Inverted_Bomb_Shop_Multi_Cave_Doors) - blacksmith_doors = list(Inverted_Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors) - door_targets = list(Inverted_Single_Cave_Targets) + # shuffle holes + scramble_holes(world, player) # place links house - if world.mode[player] == 'standard' or not world.shufflelinks[player]: - links_house = 'Links House' if not invFlag else 'Big Bomb Shop' - else: - links_house_doors = [i for i in (lw_entrances if not invFlag else dw_entrances) if i not in isolated_entrances + ([] if not invFlag else Inverted_Dark_Sanctuary_Doors)] - links_house = random.choice(links_house_doors) - connect_two_way(world, links_house, 'Links House Exit', player) - if not invFlag: - if links_house in lw_entrances: - lw_entrances.remove(links_house) - else: - if links_house in dw_entrances: - dw_entrances.remove(links_house) - + links_house = place_links_house(world, sectors, player) + # place dark sanc if invFlag: - sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in dw_entrances] - sanc_door = random.choice(sanc_doors) - dw_entrances.remove(sanc_door) - connect_entrance(world, sanc_door, 'Dark Sanctuary Hint', player) - world.get_entrance('Dark Sanctuary Hint Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) - - # in restricted, the only mandatory exits are in dark world (lw in inverted) - if not invFlag: - connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player) - else: - connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player) - - # place old man, has limited options - # exit has to come from specific set of doors, the entrance is free to move about - old_man_entrances = [door for door in old_man_entrances if door in (lw_entrances if not invFlag else dw_entrances)] - random.shuffle(old_man_entrances) - old_man_exit = old_man_entrances.pop() - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) - if not invFlag: - lw_entrances.remove(old_man_exit) - else: - dw_entrances.remove(old_man_exit) - + place_dark_sanc(world, links_house, sectors, player) + # place blacksmith, has limited options - all_entrances = lw_entrances + dw_entrances - # cannot place it anywhere already taken (or that are otherwise not eligible for placement) - blacksmith_doors = [door for door in blacksmith_doors if door in all_entrances] - random.shuffle(blacksmith_doors) - blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) - if blacksmith_hut in lw_entrances: - lw_entrances.remove(blacksmith_hut) - if blacksmith_hut in dw_entrances: - dw_entrances.remove(blacksmith_hut) - bomb_shop_doors.extend(blacksmith_doors) + place_blacksmith(world, links_house, player) + # determine pools + lw_entrances = list() + dw_entrances = list() + caves = list(Cave_Exits + Cave_Three_Exits + Old_Man_House) + for e in entrance_pool: + region = world.get_entrance(e, player).parent_region + if region.type == RegionType.LightWorld: + lw_entrances.append(e) + else: + dw_entrances.append(e) + + # place connectors in inaccessible regions + connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, player) + + # place old man, has limited options + place_old_man(world, lw_entrances if not invFlag else dw_entrances, player) + # place bomb shop, has limited options - all_entrances = lw_entrances + dw_entrances - # cannot place it anywhere already taken (or that are otherwise not eligible for placement) - bomb_shop_doors = [door for door in bomb_shop_doors if door in all_entrances] + bomb_shop_doors = list(entrance_pool) + if world.logic[player] in ['noglitches', 'minorglitches'] or (invFlag != (0x1b in world.owswaps[player][0] and world.owMixed[player])): + bomb_shop_doors = [e for e in entrance_pool if e not in ['Pyramid Fairy']] random.shuffle(bomb_shop_doors) bomb_shop = bomb_shop_doors.pop() connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - if bomb_shop in lw_entrances: - lw_entrances.remove(bomb_shop) - if bomb_shop in dw_entrances: - dw_entrances.remove(bomb_shop) - - # place the old man cave's entrance somewhere in the light world (dw for inverted) - if not invFlag: - random.shuffle(lw_entrances) - old_man_entrance = lw_entrances.pop() - else: - random.shuffle(dw_entrances) - old_man_entrance = dw_entrances.pop() - connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) - - if not invFlag: - # place Old Man House in Light World - connect_caves(world, lw_entrances, [], list(Old_Man_House), player) #for multiple seeds - - # now scramble the rest + + # shuffle connectors + lw_entrances = [e for e in lw_entrances if e in entrance_pool] + dw_entrances = [e for e in dw_entrances if e in entrance_pool] connect_caves(world, lw_entrances, dw_entrances, caves, player) - # scramble holes - scramble_holes(world, player) - # place remaining doors - doors = lw_entrances + dw_entrances - connect_doors(world, doors, door_targets, player) + connect_doors(world, list(entrance_pool), list(exit_pool), player) elif world.shuffle[player] == 'full': + suppress_spoiler = False skull_woods_shuffle(world, player) - if not invFlag: - lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances) - dw_entrances = list(DW_Entrances + DW_Dungeon_Entrances + DW_Single_Cave_Doors) - dw_must_exits = list(DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit) - lw_must_exits = list(LW_Dungeon_Entrances_Must_Exit) - old_man_entrances = list(Old_Man_Entrances + ['Tower of Hera']) - bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors + Bomb_Shop_Multi_Cave_Doors) - blacksmith_doors = list(Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors) - door_targets = list(Single_Cave_Targets) - else: - lw_entrances = list(Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + LW_Single_Cave_Doors) - dw_entrances = list(Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_DW_Single_Cave_Doors + Inverted_Old_Man_Entrances) - lw_must_exits = list(Inverted_LW_Dungeon_Entrances_Must_Exit + Inverted_LW_Entrances_Must_Exit) - old_man_entrances = list(Inverted_Old_Man_Entrances + Old_Man_Entrances + ['Ganons Tower', 'Tower of Hera']) - bomb_shop_doors = list(Inverted_Bomb_Shop_Single_Cave_Doors + Inverted_Bomb_Shop_Multi_Cave_Doors) - blacksmith_doors = list(Inverted_Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors) - door_targets = list(Inverted_Single_Cave_Targets) - - # randomize which desert ledge door is a must-exit - if random.randint(0, 1) == 0: - lw_must_exits.append('Desert Palace Entrance (North)') - lw_entrances.append('Desert Palace Entrance (West)') - else: - lw_must_exits.append('Desert Palace Entrance (West)') - lw_entrances.append('Desert Palace Entrance (North)') - caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits) # don't need to consider three exit caves, have one exit caves to avoid parity issues - old_man_house = list(Old_Man_House) - + caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits + Old_Man_House) + if world.mode[player] == 'standard': - # must connect front of hyrule castle to do escape connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) + caves.append(tuple(random.sample(['Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'], 2))) else: - lw_entrances.append('Hyrule Castle Entrance (South)') - if invFlag or world.doorShuffle[player] == 'vanilla': - caves.append(tuple(random.sample(['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'],3))) + caves.append(tuple(random.sample(['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'], 3))) if not world.shuffle_ganon: connect_two_way(world, 'Ganons Tower' if not invFlag else 'Agahnims Tower', 'Ganons Tower Exit', player) - hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)'] else: - if not invFlag: - dw_entrances.append('Ganons Tower') - else: - lw_entrances.append('Agahnims Tower') caves.append('Ganons Tower Exit') - hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)', 'Agahnims Tower'] - if invFlag: - # shuffle aga door first. if it's on hc ledge, then one other hc ledge door has to be must_exit - all_entrances_aga = lw_entrances + dw_entrances - aga_doors = [i for i in all_entrances_aga] - random.shuffle(aga_doors) - aga_door = aga_doors.pop() - - if aga_door in hc_ledge_entrances: - lw_entrances.remove(aga_door) - hc_ledge_entrances.remove(aga_door) - - random.shuffle(hc_ledge_entrances) - hc_ledge_must_exit = hc_ledge_entrances.pop() - lw_entrances.remove(hc_ledge_must_exit) - lw_must_exits.append(hc_ledge_must_exit) + # shuffle holes + scramble_holes(world, player) - if aga_door in lw_entrances: - lw_entrances.remove(aga_door) - elif aga_door in dw_entrances: - dw_entrances.remove(aga_door) - - connect_two_way(world, aga_door, 'Agahnims Tower Exit', player) - caves.remove('Agahnims Tower Exit') - # place links house - if world.mode[player] == 'standard' or not world.shufflelinks[player]: - links_house = 'Links House' if not invFlag else 'Big Bomb Shop' - else: - links_house_doors = [i for i in (lw_entrances + lw_must_exits if not invFlag else dw_entrances) if i not in isolated_entrances + ([] if not invFlag else Inverted_Dark_Sanctuary_Doors)] - links_house = random.choice(links_house_doors) - connect_two_way(world, links_house, 'Links House Exit', player) - if not invFlag: - if links_house in lw_entrances: - lw_entrances.remove(links_house) - if links_house in lw_must_exits: - lw_must_exits.remove(links_house) - else: - if links_house in dw_entrances: - dw_entrances.remove(links_house) - - # place dark sanc - sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in dw_entrances] - sanc_door = random.choice(sanc_doors) - dw_entrances.remove(sanc_door) - connect_entrance(world, sanc_door, 'Dark Sanctuary Hint', player) - world.get_entrance('Dark Sanctuary Hint Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) - - # we randomize which world requirements we fulfill first so we get better dungeon distribution - # we also places the Old Man House at this time to make sure he can be connected to the desert one way - # no dw must exits in inverted, but we randomize whether cave is in light or dark world - if random.randint(0, 1) == 0: - caves += old_man_house - connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player) - try: - caves.remove(old_man_house[0]) - except ValueError: - pass - else: #if the cave wasn't placed we get here - connect_caves(world, lw_entrances, [], old_man_house, player) - if not invFlag: - connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player) - else: - if not invFlag: - connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player) - caves += old_man_house - connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player) - try: - caves.remove(old_man_house[0]) - except ValueError: - pass - else: # if the cave wasn't placed we get here - connect_caves(world, lw_entrances, [], old_man_house, player) - else: - connect_caves(world, dw_entrances, [], old_man_house, player) - connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player) - - if world.mode[player] == 'standard': - # rest of hyrule castle must be in light world - connect_caves(world, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player) - # in full, Sanc must be in light world, so must all of HC if door shuffle is on - elif not invFlag and world.doorShuffle[player] != 'vanilla': - connect_caves(world, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)', 'Hyrule Castle Exit (South)')], player) - - # place old man, has limited options - # exit has to come from specific set of doors, the entrance is free to move about - if not invFlag: - old_man_entrances = [door for door in old_man_entrances if door in lw_entrances] - else: - old_man_entrances = [door for door in old_man_entrances if door in dw_entrances + lw_entrances] - random.shuffle(old_man_entrances) - old_man_exit = old_man_entrances.pop() - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) - if old_man_exit in dw_entrances: - dw_entrances.remove(old_man_exit) - old_man_world = 'dark' - elif invFlag or old_man_exit in lw_entrances: - lw_entrances.remove(old_man_exit) - old_man_world = 'light' - + links_house = place_links_house(world, sectors, player) + + # place dark sanc + if invFlag: + place_dark_sanc(world, links_house, sectors, player) + # place blacksmith, has limited options - all_entrances = lw_entrances + dw_entrances - # cannot place it anywhere already taken (or that are otherwise not eligible for placement) - blacksmith_doors = [door for door in blacksmith_doors if door in all_entrances] - random.shuffle(blacksmith_doors) - blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) - if blacksmith_hut in lw_entrances: - lw_entrances.remove(blacksmith_hut) - if blacksmith_hut in dw_entrances: - dw_entrances.remove(blacksmith_hut) - bomb_shop_doors.extend(blacksmith_doors) + place_blacksmith(world, links_house, player) + # determine pools + lw_entrances = list() + dw_entrances = list() + for e in entrance_pool: + region = world.get_entrance(e, player).parent_region + if region.type == RegionType.LightWorld: + lw_entrances.append(e) + else: + dw_entrances.append(e) + + # place connectors in inaccessible regions + connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, player) + + # place old man, has limited options + place_old_man(world, lw_entrances if not invFlag else dw_entrances, player) + # place bomb shop, has limited options - all_entrances = lw_entrances + dw_entrances - # cannot place it anywhere already taken (or that are otherwise not eligible for placement) - bomb_shop_doors = [door for door in bomb_shop_doors if door in all_entrances] + bomb_shop_doors = list(entrance_pool) + if world.logic[player] in ['noglitches', 'minorglitches'] or (invFlag != (0x1b in world.owswaps[player][0] and world.owMixed[player])): + bomb_shop_doors = [e for e in entrance_pool if e not in ['Pyramid Fairy']] random.shuffle(bomb_shop_doors) bomb_shop = bomb_shop_doors.pop() connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - if bomb_shop in lw_entrances: - lw_entrances.remove(bomb_shop) - if bomb_shop in dw_entrances: - dw_entrances.remove(bomb_shop) - - # place the old man cave's entrance somewhere in the same world he'll exit from - if old_man_world == 'light': - random.shuffle(lw_entrances) - old_man_entrance = lw_entrances.pop() - elif old_man_world == 'dark': - random.shuffle(dw_entrances) - old_man_entrance = dw_entrances.pop() - connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) - - # now scramble the rest + + # shuffle connectors + lw_entrances = [e for e in lw_entrances if e in entrance_pool] + dw_entrances = [e for e in dw_entrances if e in entrance_pool] connect_caves(world, lw_entrances, dw_entrances, caves, player) - # scramble holes - scramble_holes(world, player) - # place remaining doors - doors = lw_entrances + dw_entrances - connect_doors(world, doors, door_targets, player) + connect_doors(world, list(entrance_pool), list(exit_pool), player) elif world.shuffle[player] == 'crossed': + suppress_spoiler = False skull_woods_shuffle(world, player) - if not invFlag: - entrances = list(LW_Entrances + LW_Dungeon_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances + DW_Entrances + DW_Dungeon_Entrances + DW_Single_Cave_Doors) - must_exits = list(DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit + LW_Dungeon_Entrances_Must_Exit) - old_man_entrances = list(Old_Man_Entrances + ['Tower of Hera']) - bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors + Bomb_Shop_Multi_Cave_Doors) - blacksmith_doors = list(Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors) - door_targets = list(Single_Cave_Targets) - else: - entrances = list(Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + LW_Single_Cave_Doors + Inverted_Old_Man_Entrances + Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_DW_Single_Cave_Doors) - must_exits = list(Inverted_LW_Entrances_Must_Exit + Inverted_LW_Dungeon_Entrances_Must_Exit) - old_man_entrances = list(Inverted_Old_Man_Entrances + Old_Man_Entrances + ['Ganons Tower', 'Tower of Hera']) - bomb_shop_doors = list(Inverted_Bomb_Shop_Single_Cave_Doors + Inverted_Bomb_Shop_Multi_Cave_Doors) - blacksmith_doors = list(Inverted_Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors) - door_targets = list(Inverted_Single_Cave_Targets) - caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits + Old_Man_House) # don't need to consider three exit caves, have one exit caves to avoid parity issues - - if invFlag: - # randomize which desert ledge door is a must-exit - if random.randint(0, 1) == 0: - must_exits.append('Desert Palace Entrance (North)') - entrances.append('Desert Palace Entrance (West)') - else: - must_exits.append('Desert Palace Entrance (West)') - entrances.append('Desert Palace Entrance (North)') - + caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits + Old_Man_House) + if world.mode[player] == 'standard': - # must connect front of hyrule castle to do escape connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) + caves.append(tuple(random.sample(['Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'], 2))) else: - caves.append(tuple(random.sample(['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'],3))) - entrances.append('Hyrule Castle Entrance (South)') - + caves.append(tuple(random.sample(['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'], 3))) + if not world.shuffle_ganon: connect_two_way(world, 'Ganons Tower' if not invFlag else 'Agahnims Tower', 'Ganons Tower Exit', player) - hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)'] else: - entrances.append('Ganons Tower' if not invFlag else 'Agahnims Tower') caves.append('Ganons Tower Exit') - hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)', 'Agahnims Tower'] - if invFlag: - # shuffle aga door. if it's on hc ledge, then one other hc ledge door has to be must_exit - aga_choices = [x for x in entrances] - aga_door = random.choice(aga_choices) - - if aga_door in hc_ledge_entrances: - hc_ledge_entrances.remove(aga_door) - - random.shuffle(hc_ledge_entrances) - hc_ledge_must_exit = hc_ledge_entrances.pop() - entrances.remove(hc_ledge_must_exit) - must_exits.append(hc_ledge_must_exit) + # shuffle holes + scramble_holes(world, player) - entrances.remove(aga_door) - connect_two_way(world, aga_door, 'Agahnims Tower Exit', player) - caves.remove('Agahnims Tower Exit') - # place links house - if world.mode[player] == 'standard' or not world.shufflelinks[player]: - links_house = 'Links House' if not invFlag else 'Big Bomb Shop' - else: - links_house_doors = [i for i in entrances + must_exits if i not in isolated_entrances + ([] if not invFlag else Inverted_Dark_Sanctuary_Doors)] - if not invFlag and world.doorShuffle[player] == 'crossed' and world.intensity[player] >= 3: - exclusions = DW_Entrances + DW_Dungeon_Entrances + DW_Single_Cave_Doors\ - + DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit + ['Ganons Tower'] - links_house_doors = [i for i in links_house_doors if i not in exclusions] - links_house = random.choice(list(links_house_doors)) - connect_two_way(world, links_house, 'Links House Exit', player) - if links_house in entrances: - entrances.remove(links_house) - elif links_house in must_exits: - must_exits.remove(links_house) - + links_house = place_links_house(world, sectors, player) + + # place dark sanc if invFlag: - # place dark sanc - sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in entrances] - sanc_door = random.choice(sanc_doors) - entrances.remove(sanc_door) - connect_entrance(world, sanc_door, 'Dark Sanctuary Hint', player) - world.get_entrance('Dark Sanctuary Hint Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) - - #place must-exit caves - connect_mandatory_exits(world, entrances, caves, must_exits, player) - - if world.mode[player] == 'standard': - # rest of hyrule castle must be dealt with - connect_caves(world, entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player) - - # place old man, has limited options - # exit has to come from specific set of doors, the entrance is free to move about - old_man_entrances = [door for door in old_man_entrances if door in entrances] - random.shuffle(old_man_entrances) - old_man_exit = old_man_entrances.pop() - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) - entrances.remove(old_man_exit) - + place_dark_sanc(world, links_house, sectors, player) + # place blacksmith, has limited options - # cannot place it anywhere already taken (or that are otherwise not eligible for placement) - blacksmith_doors = [door for door in blacksmith_doors if door in entrances] - random.shuffle(blacksmith_doors) - blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) - entrances.remove(blacksmith_hut) - if not invFlag: - bomb_shop_doors.extend(blacksmith_doors) - + place_blacksmith(world, links_house, player) + + # place connectors in inaccessible regions + connect_inaccessible_regions(world, list(entrance_pool), [], caves, player) + + # place old man, has limited options + place_old_man(world, list(entrance_pool), player) + # place bomb shop, has limited options - # cannot place it anywhere already taken (or that are otherwise not eligible for placement) - bomb_shop_doors = [door for door in bomb_shop_doors if door in entrances] + bomb_shop_doors = list(entrance_pool) + if world.logic[player] in ['noglitches', 'minorglitches'] or (invFlag != (0x1b in world.owswaps[player][0] and world.owMixed[player])): + bomb_shop_doors = [e for e in entrance_pool if e not in ['Pyramid Fairy']] random.shuffle(bomb_shop_doors) bomb_shop = bomb_shop_doors.pop() connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - entrances.remove(bomb_shop) - - # place the old man cave's entrance somewhere - random.shuffle(entrances) - old_man_entrance = entrances.pop() - connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) - - # now scramble the rest - connect_caves(world, entrances, [], caves, player) - - # scramble holes - scramble_holes(world, player) + + # shuffle connectors + connect_caves(world, list(entrance_pool), [], caves, player) # place remaining doors - connect_doors(world, entrances, door_targets, player) + connect_doors(world, list(entrance_pool), list(exit_pool), player) elif world.shuffle[player] == 'insanity': # beware ye who enter here - ignore_pool = True + suppress_spoiler = False - if not invFlag: - entrances_must_exits = DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit + LW_Dungeon_Entrances_Must_Exit + ['Skull Woods Second Section Door (West)'] + # list preparation + caves = Cave_Exits + Dungeon_Exits + Cave_Three_Exits + Old_Man_House + \ + ['Skull Woods First Section Exit', 'Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)', + 'Kakariko Well Exit', 'Bat Cave Exit', 'North Fairy Cave Exit', 'Lost Woods Hideout Exit', 'Lumberjack Tree Exit', 'Sanctuary Exit'] - doors = LW_Entrances + LW_Dungeon_Entrances + LW_Dungeon_Entrances_Must_Exit + ['Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave'] + Old_Man_Entrances +\ - DW_Entrances + DW_Dungeon_Entrances + DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit + ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)'] +\ - LW_Single_Cave_Doors + DW_Single_Cave_Doors - else: - entrances_must_exits = Inverted_LW_Entrances_Must_Exit + Inverted_LW_Dungeon_Entrances_Must_Exit - - doors = Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_LW_Entrances_Must_Exit + Inverted_LW_Dungeon_Entrances_Must_Exit + ['Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave', 'Hyrule Castle Secret Entrance Stairs'] + Inverted_Old_Man_Entrances +\ - Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)'] +\ - LW_Single_Cave_Doors + Inverted_DW_Single_Cave_Doors + ['Desert Palace Entrance (West)', 'Desert Palace Entrance (North)'] - - exit_pool = list(doors) - - if invFlag: - # randomize which desert ledge door is a must-exit - if random.randint(0, 1) == 0: - entrances_must_exits.append('Desert Palace Entrance (North)') - exit_pool.append('Desert Palace Entrance (West)') - else: - entrances_must_exits.append('Desert Palace Entrance (West)') - exit_pool.append('Desert Palace Entrance (North)') - - # TODO: there are other possible entrances we could support here by way of exiting from a connector, - # and rentering to find bomb shop. However appended list here is all those that we currently have - # bomb shop logic for. - # Specifically we could potentially add: 'Dark Death Mountain Ledge (East)' and doors associated with pits - if not invFlag: - bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors + Bomb_Shop_Multi_Cave_Doors+['Desert Palace Entrance (East)', 'Turtle Rock Isolated Ledge Entrance', 'Bumper Cave (Top)', 'Hookshot Cave Back Entrance']) - blacksmith_doors = list(Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors) - door_targets = list(Single_Cave_Targets) - else: - bomb_shop_doors = list(Inverted_Bomb_Shop_Single_Cave_Doors + Inverted_Bomb_Shop_Multi_Cave_Doors + ['Turtle Rock Isolated Ledge Entrance', 'Hookshot Cave Back Entrance']) - blacksmith_doors = list(Inverted_Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors) - door_targets = list(Inverted_Single_Cave_Targets) - - random.shuffle(doors) - - if not invFlag: - old_man_entrances = list(Old_Man_Entrances) + ['Tower of Hera'] - else: - old_man_entrances = list(Inverted_Old_Man_Entrances + Old_Man_Entrances) + ['Tower of Hera', 'Ganons Tower'] - - caves = Cave_Exits + Dungeon_Exits + Cave_Three_Exits + ['Old Man House Exit (Bottom)', 'Old Man House Exit (Top)', 'Skull Woods First Section Exit', 'Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)', - 'Kakariko Well Exit', 'Bat Cave Exit', 'North Fairy Cave Exit', 'Lost Woods Hideout Exit', 'Lumberjack Tree Exit', 'Sanctuary Exit'] - - - # shuffle up holes hole_entrances = ['Kakariko Well Drop', 'Bat Cave Drop', 'North Fairy Cave Drop', 'Lost Woods Hideout Drop', 'Lumberjack Tree Tree', 'Sanctuary Grave', 'Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'] @@ -710,17 +432,15 @@ def link_entrances(world, player): 'Skull Left Drop', 'Skull Pinball', 'Skull Pot Circle'] if world.mode[player] == 'standard': - # cannot move uncle cave + connect_two_way(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player) connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player) - connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player) - connect_entrance(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player) + connect_two_way(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player) + caves.append(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) else: hole_entrances.append('Hyrule Castle Secret Entrance Drop') hole_targets.append('Hyrule Castle Secret Entrance') caves.append('Hyrule Castle Secret Entrance Exit') - if not invFlag: - doors.append('Hyrule Castle Secret Entrance Stairs') - exit_pool.append('Hyrule Castle Secret Entrance Stairs') + caves.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) if not world.shuffle_ganon: connect_two_way(world, 'Ganons Tower' if not invFlag else 'Agahnims Tower', 'Ganons Tower Exit', player) @@ -728,133 +448,62 @@ def link_entrances(world, player): connect_entrance(world, 'Pyramid Hole' if invFlag == (0x1b in world.owswaps[player][0] and world.owMixed[player]) else 'Inverted Pyramid Hole', 'Pyramid', player) else: caves.extend(['Ganons Tower Exit', 'Pyramid Exit']) + hole_entrances.append('Pyramid Hole' if invFlag == (0x1b in world.owswaps[player][0] and world.owMixed[player]) else 'Inverted Pyramid Hole') hole_targets.append('Pyramid') - exit_pool.extend(['Ganons Tower' if not invFlag else 'Agahnims Tower']) - doors.extend(['Ganons Tower' if not invFlag else 'Agahnims Tower']) - - if invFlag == (0x1b in world.owswaps[player][0] and world.owMixed[player]): - exit_pool.extend(['Pyramid Entrance']) - hole_entrances.append('Pyramid Hole') - entrances_must_exits.append('Pyramid Entrance') - doors.extend(['Pyramid Entrance']) - else: - exit_pool.extend(['Inverted Pyramid Entrance']) - hole_entrances.append('Inverted Pyramid Hole') - doors.extend(['Inverted Pyramid Entrance']) + # shuffle holes random.shuffle(hole_entrances) random.shuffle(hole_targets) - random.shuffle(exit_pool) - - # fill up holes for hole in hole_entrances: connect_entrance(world, hole, hole_targets.pop(), player) - # hyrule castle handling - if world.mode[player] == 'standard': - # must connect front of hyrule castle to do escape - connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player) - caves.append(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) - else: - doors.append('Hyrule Castle Entrance (South)') - caves.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) - if not invFlag: - exit_pool.append('Hyrule Castle Entrance (South)') - # place links house - if world.mode[player] == 'standard' or not world.shufflelinks[player]: - links_house = 'Links House' if not invFlag else 'Big Bomb Shop' - else: - links_house_doors = [i for i in doors if i not in isolated_entrances + ([] if not invFlag else Inverted_Dark_Sanctuary_Doors)] - if not invFlag and world.doorShuffle[player] == 'crossed' and world.intensity[player] >= 3: - exclusions = DW_Entrances + DW_Dungeon_Entrances + DW_Single_Cave_Doors \ - + DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit + ['Ganons Tower'] - links_house_doors = [i for i in links_house_doors if i not in exclusions] - links_house = random.choice(links_house_doors) - connect_two_way(world, links_house, 'Links House Exit', player) - exit_pool.remove(links_house) - doors.remove(links_house) + links_house = place_links_house(world, sectors, player) + # place dark sanc if invFlag: - # place dark sanc - sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in exit_pool] - sanc_door = random.choice(sanc_doors) - exit_pool.remove(sanc_door) - doors.remove(sanc_door) - connect_entrance(world, sanc_door, 'Dark Sanctuary Hint', player) - world.get_entrance('Dark Sanctuary Hint Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) + place_dark_sanc(world, links_house, sectors, player) - # now let's deal with mandatory reachable stuff - def extract_reachable_exit(cavelist): - random.shuffle(cavelist) - candidate = None - for cave in cavelist: - if isinstance(cave, tuple) and len(cave) > 1: - # special handling: TRock has two entries that we should consider entrance only - # ToDo this should be handled in a more sensible manner - if cave[0] in ['Turtle Rock Exit (Front)', 'Spectacle Rock Cave Exit (Peak)'] and len(cave) == 2: - continue - candidate = cave - break - if candidate is None: - raise RuntimeError('No suitable cave.') - cavelist.remove(candidate) - return candidate - - def connect_reachable_exit(entrance, caves, doors, exit_pool): - cave = extract_reachable_exit(caves) - - exit = cave[-1] - cave = cave[:-1] - connect_exit(world, exit, entrance, player) - connect_entrance(world, doors.pop(), exit, player) - # rest of cave now is forced to be in this world - exit_pool.remove(entrance) - caves.append(cave) - - # connect mandatory exits - for entrance in entrances_must_exits: - connect_reachable_exit(entrance, caves, doors, exit_pool) + # place blacksmith, place sanc exit first for additional blacksmith candidates + doors = list(entrance_pool) + random.shuffle(doors) + door = doors.pop() + connect_entrance(world, door, 'Sanctuary Exit', player, False) + doors = [e for e in doors if e not in entrance_exits] + door = doors.pop() + connect_exit(world, 'Sanctuary Exit', door, player, False) + caves.remove('Sanctuary Exit') + place_blacksmith(world, links_house, player) + # place connectors in inaccessible regions + connect_inaccessible_regions(world, list(entrance_pool), [], caves, player) + # place old man, has limited options - # exit has to come from specific set of doors, the entrance is free to move about - old_man_entrances = [entrance for entrance in old_man_entrances if entrance in exit_pool] - random.shuffle(old_man_entrances) - old_man_exit = old_man_entrances.pop() - exit_pool.remove(old_man_exit) - - connect_exit(world, 'Old Man Cave Exit (East)', old_man_exit, player) - connect_entrance(world, doors.pop(), 'Old Man Cave Exit (East)', player) + place_old_man(world, list(entrance_pool), player) caves.append('Old Man Cave Exit (West)') - # place blacksmith, has limited options - blacksmith_doors = [door for door in blacksmith_doors if door in doors] - random.shuffle(blacksmith_doors) - blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) - exit_pool.remove(blacksmith_hut) - doors.remove(blacksmith_hut) - - # place dam and pyramid fairy, have limited options - bomb_shop_doors = [door for door in bomb_shop_doors if door in doors] + # place bomb shop, has limited options + bomb_shop_doors = list(entrance_pool) + if world.logic[player] in ['noglitches', 'minorglitches'] or (invFlag != (0x1b in world.owswaps[player][0] and world.owMixed[player])): + bomb_shop_doors = [e for e in entrance_pool if e not in ['Pyramid Fairy']] random.shuffle(bomb_shop_doors) bomb_shop = bomb_shop_doors.pop() connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - exit_pool.remove(bomb_shop) - doors.remove(bomb_shop) - - # handle remaining caves + + # shuffle connectors + doors = list(entrance_pool) + exit_doors = [e for e in entrance_pool if e not in entrance_exits] + random.shuffle(doors) + random.shuffle(exit_doors) for cave in caves: if isinstance(cave, str): cave = (cave,) - for exit in cave: - connect_exit(world, exit, exit_pool.pop(), player) - connect_entrance(world, doors.pop(), exit, player) + connect_exit(world, exit, exit_doors.pop(), player, False) + connect_entrance(world, doors.pop(), exit, player, False) # place remaining doors - connect_doors(world, doors, door_targets, player) + connect_doors(world, list(entrance_pool), list(exit_pool), player) else: raise NotImplementedError('Shuffling not supported yet') @@ -866,7 +515,7 @@ def link_entrances(world, player): connect_exit(world, 'Chris Houlihan Room Exit', links_house.name, player) # check for swamp palace fix - if world.get_entrance('Dam', player).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Portal': + if not (world.get_entrance('Dam', player).connected_region.name in ['Dam', 'Swamp Portal'] and world.get_entrance('Swamp Palace', player).connected_region.name == ['Dam', 'Swamp Portal']): world.swamp_patch_required[player] = True # check for potion shop location @@ -896,10 +545,7 @@ def connect_simple(world, exitname, regionname, player): def connect_logical(world, entrancename, exitname, player, isTwoWay = False): if not ignore_pool: - if entrancename not in entrance_pool: - x = 9 - if exitname not in exit_pool: - x = 9 + logging.getLogger('').debug('Connecting %s -> %s', entrancename, exitname) assert entrancename in entrance_pool, 'Entrance not in pool: ' + entrancename assert exitname in exit_pool, 'Exit not in pool: ' + exitname @@ -920,14 +566,12 @@ def connect_logical(world, entrancename, exitname, player, isTwoWay = False): exit_pool.remove(exitname) -def connect_entrance(world, entrancename, exitname, player): +def connect_entrance(world, entrancename, exitname, player, mark_two_way=True): if not ignore_pool: - if entrancename not in entrance_pool: - x = 9 - if exitname not in exit_pool: - x = 9 + logging.getLogger('').debug('Connecting %s -> %s', entrancename, exitname) assert entrancename in entrance_pool, 'Entrance not in pool: ' + entrancename - assert exitname in exit_pool, 'Exit not in pool: ' + exitname + if mark_two_way: + assert exitname in exit_pool, 'Exit not in pool: ' + exitname entrance = world.get_entrance(entrancename, player) # check if we got an entrance or a region to connect to @@ -949,19 +593,18 @@ def connect_entrance(world, entrancename, exitname, player): if not ignore_pool: entrance_pool.remove(entrancename) - exit_pool.remove(exitname) + if mark_two_way: + exit_pool.remove(exitname) - if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull']: + if not suppress_spoiler: world.spoiler.set_entrance(entrance.name, exit.name if exit is not None else region.name, 'entrance', player) -def connect_exit(world, exitname, entrancename, player): +def connect_exit(world, exitname, entrancename, player, mark_two_way=True): if not (ignore_pool or exitname == 'Chris Houlihan Room Exit'): - if entrancename not in entrance_pool: - x = 9 - if exitname not in exit_pool: - x = 9 - assert entrancename in entrance_pool, 'Entrance not in pool: ' + entrancename + logging.getLogger('').debug('Connecting %s -> %s', exitname, entrancename) + if mark_two_way: + assert entrancename in entrance_pool, 'Entrance not in pool: ' + entrancename assert exitname in exit_pool, 'Exit not in pool: ' + exitname entrance = world.get_entrance(entrancename, player) @@ -974,19 +617,19 @@ def connect_exit(world, exitname, entrancename, player): exit.connect(entrance.parent_region, door_addresses[entrance.name][1], exit_ids[exit.name][1]) if not (ignore_pool or exitname == 'Chris Houlihan Room Exit'): - entrance_pool.remove(entrancename) + if mark_two_way: + entrance_pool.remove(entrancename) + else: + entrance_exits.append(entrancename) exit_pool.remove(exitname) - if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull']: + if not suppress_spoiler: world.spoiler.set_entrance(entrance.name, exit.name, 'exit', player) def connect_two_way(world, entrancename, exitname, player): if not ignore_pool: - if entrancename not in entrance_pool: - x = 9 - if exitname not in exit_pool: - x = 9 + logging.getLogger('').debug('Connecting %s <-> %s', entrancename, exitname) assert entrancename in entrance_pool, 'Entrance not in pool: ' + entrancename assert exitname in exit_pool, 'Exit not in pool: ' + exitname @@ -1005,8 +648,9 @@ def connect_two_way(world, entrancename, exitname, player): if not ignore_pool: entrance_pool.remove(entrancename) exit_pool.remove(exitname) + entrance_exits.append(entrancename) - if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull']: + if not suppress_spoiler: world.spoiler.set_entrance(entrance.name, exit.name, 'both', player) @@ -1021,74 +665,146 @@ def connect_random(world, exitlist, targetlist, player, two_way=False): connect_entrance(world, exit, target, player) -def connect_mandatory_exits(world, entrances, caves, must_be_exits, player): +def connect_mandatory_exits(world, entrances, caves, must_be_exits, player, must_deplete_mustexits=True): # Keeps track of entrances that cannot be used to access each exit / cave - if world.mode[player] == 'inverted': - invalid_connections = Inverted_Must_Exit_Invalid_Connections.copy() - else: - invalid_connections = Must_Exit_Invalid_Connections.copy() invalid_cave_connections = defaultdict(set) - if world.logic[player] in ['owglitches', 'nologic']: - import OverworldGlitchRules - for entrance in OverworldGlitchRules.get_non_mandatory_exits(world.mode[player] == 'inverted'): - invalid_connections[entrance] = set() - if entrance in must_be_exits: - must_be_exits.remove(entrance) - entrances.append(entrance) + # if world.logic[player] in ['owglitches', 'nologic']: + # import OverworldGlitchRules + # for entrance in OverworldGlitchRules.get_non_mandatory_exits(world.mode[player] == 'inverted'): + # if entrance in must_be_exits: + # must_be_exits.remove(entrance) + # entrances.append(entrance) + + # for insanity use only + def extract_reachable_exit(cavelist): + candidate = None + for cave in cavelist: + if isinstance(cave, tuple) and len(cave) > 1: + # special handling: TRock has two entries that we should consider entrance only + # ToDo this should be handled in a more sensible manner + if cave[0] in ['Turtle Rock Exit (Front)', 'Spectacle Rock Cave Exit (Peak)'] and len(cave) == 2: + continue + candidate = cave + break + if candidate is None: + raise RuntimeError('No suitable cave.') + return candidate """This works inplace""" random.shuffle(entrances) random.shuffle(caves) - # Handle inverted Aga Tower - if it depends on connections, then so does Hyrule Castle Ledge - if world.mode[player] == 'inverted': - for entrance in invalid_connections: - if world.get_entrance(entrance, player).connected_region == world.get_region('Agahnims Tower Portal', player): - for exit in invalid_connections[entrance]: - invalid_connections[exit] = invalid_connections[exit].union({'Agahnims Tower', 'Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)'}) - break - + from DoorShuffle import find_inaccessible_regions + used_caves = [] required_entrances = 0 # Number of entrances reserved for used_caves - while must_be_exits: + skip_remaining = False + while must_be_exits and not skip_remaining: exit = must_be_exits.pop() + # find multi exit cave + # * this is a mess, but it ensures a loose assignment possibility when the cave/entrance pool is plentiful, + # * but can also find and prepare for solutions when the cave/entrance pool is limiting + # * however, this probably could be better implemented cave = None - for candidate in caves: - if not isinstance(candidate, str) and (candidate in used_caves or len(candidate) < len(entrances) - required_entrances - 1): - cave = candidate - break + if world.shuffle[player] == 'insanity': + cave = extract_reachable_exit(caves) + else: + if must_deplete_mustexits: + cave_surplus = sum(0 if isinstance(x, str) else len(x) - 1 for x in caves) - (len(must_be_exits) + 1) + if cave_surplus < 0: + raise RuntimeError('Not enough multi-entrance caves left to connect unreachable regions!') + if len(entrances) < len(must_be_exits) + 1: + raise RuntimeError('Not enough entrances left to connect unreachable regions!') + if len(must_be_exits) == 0: # if assigning last must exit + for candidate in caves: + if not isinstance(candidate, str) and (candidate in used_caves or len(candidate) <= len(entrances) - required_entrances - 1): + cave = candidate + break + if cave is None and cave_surplus <= 1: # if options are limited + # attempt to find use caves already used + for candidate in caves: + if not isinstance(candidate, str) and candidate in used_caves: + cave = candidate + break + if cave is None: + # attempt to find caves with exact number of exits + for candidate in caves: + if not isinstance(candidate, str) and (len(entrances) - required_entrances - 1) - len(candidate) == 0: + cave = candidate + break + if cave is None: + # attempt to find caves with one left over exit + for candidate in caves: + if not isinstance(candidate, str) and (len(entrances) - required_entrances - 1) - len(candidate) == 1: + cave = candidate + break + + if cave is None: + for candidate in caves: + if not isinstance(candidate, str) and (candidate in used_caves or len(candidate) < len(entrances) - required_entrances - 1): + cave = candidate + break + if cave is None and must_deplete_mustexits: + for candidate in caves: + if not isinstance(candidate, str) and (candidate in used_caves or len(candidate) <= len(entrances) - required_entrances - 1): + cave = candidate + break + + inaccessible_entrances = list() + for region_name in world.inaccessible_regions[player]: + region = world.get_region(region_name, player) + if region.type in [RegionType.LightWorld, RegionType.DarkWorld]: + for x in region.exits: + if not x.connected_region and x.name in entrance_pool: + inaccessible_entrances.append(x.name) if cave is None: - raise RuntimeError('No more caves left. Should not happen!') + if must_deplete_mustexits: + raise RuntimeError('No more caves left. Should not happen!') + else: + must_be_exits.append(exit) + skip_remaining = True + continue # all caves are sorted so that the last exit is always reachable - connect_two_way(world, exit, cave[-1], player) - if len(cave) == 2: - entrance = next(e for e in entrances[::-1] if e not in invalid_connections[exit] and e not in invalid_cave_connections[tuple(cave)]) + if world.shuffle[player] == 'insanity': + connect_exit(world, cave[-1], exit, player, False) + entrance = next(e for e in entrances[::-1] if e not in entrance_exits + inaccessible_entrances + list(invalid_cave_connections[tuple(cave)])) entrances.remove(entrance) - connect_two_way(world, entrance, cave[0], player) + connect_entrance(world, entrance, cave[-1], player, False) + else: + connect_two_way(world, exit, cave[-1], player) + + if len(cave) == 2: + entrance = next(e for e in entrances[::-1] if e not in inaccessible_entrances and e not in invalid_cave_connections[tuple(cave)]) + entrances.remove(entrance) + if world.shuffle[player] == 'insanity': + connect_entrance(world, entrance, cave[0], player, False) + entrance = next(e for e in entrances[::-1] if e not in entrance_exits + inaccessible_entrances + list(invalid_cave_connections[tuple(cave)])) + entrances.remove(entrance) + connect_exit(world, cave[0], entrance, player, False) + else: + connect_two_way(world, entrance, cave[0], player) if cave in used_caves: required_entrances -= 2 used_caves.remove(cave) - if entrance in invalid_connections: - for exit2 in invalid_connections[entrance]: - invalid_connections[exit2] = invalid_connections[exit2].union(invalid_connections[exit]).union(invalid_cave_connections[tuple(cave)]) - elif cave[-1] == 'Spectacle Rock Cave Exit': #Spectacle rock only has one exit + elif cave[-1] == 'Spectacle Rock Cave Exit': # Spectacle rock only has one exit cave_entrances = [] for cave_exit in cave[:-1]: - entrance = next(e for e in entrances[::-1] if e not in invalid_connections[exit]) + entrance = next(e for e in entrances[::-1] if e not in inaccessible_entrances) cave_entrances.append(entrance) entrances.remove(entrance) - connect_two_way(world,entrance,cave_exit, player) - if entrance not in invalid_connections: - invalid_connections[exit] = set() - if all(entrance in invalid_connections for entrance in cave_entrances): - new_invalid_connections = invalid_connections[cave_entrances[0]].intersection(invalid_connections[cave_entrances[1]]) - for exit2 in new_invalid_connections: - invalid_connections[exit2] = invalid_connections[exit2].union(invalid_connections[exit]) - else:#save for later so we can connect to multiple exits + if world.shuffle[player] == 'insanity': + connect_entrance(world, entrance, cave_exit, player, False) + entrance = next(e for e in entrances[::-1] if e not in entrance_exits + inaccessible_entrances) + cave_entrances.append(entrance) + entrances.remove(entrance) + connect_exit(world, cave_exit, entrance, player, False) + else: + connect_two_way(world, entrance, cave_exit, player) + else: # save for later so we can connect to multiple exits if cave in used_caves: required_entrances -= 1 used_caves.remove(cave) @@ -1097,15 +813,24 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player): caves.append(cave[0:-1]) random.shuffle(caves) used_caves.append(cave[0:-1]) - invalid_cave_connections[tuple(cave[0:-1])] = invalid_cave_connections[tuple(cave)].union(invalid_connections[exit]) + invalid_cave_connections[tuple(cave[0:-1])] = invalid_cave_connections[tuple(cave)].union(inaccessible_entrances).union(entrance_exits) caves.remove(cave) + + find_inaccessible_regions(world, player) + for cave in used_caves: - if cave in caves: #check if we placed multiple entrances from this 3 or 4 exit + if cave in caves: # check if we placed multiple entrances from this 3 or 4 exit for cave_exit in cave: - entrance = next(e for e in entrances[::-1] if e not in invalid_cave_connections[tuple(cave)]) + entrance = next(e for e in entrances[::-1] if e not in inaccessible_entrances and e not in invalid_cave_connections[tuple(cave)]) invalid_cave_connections[tuple(cave)] = set() entrances.remove(entrance) - connect_two_way(world, entrance, cave_exit, player) + if world.shuffle[player] == 'insanity': + connect_entrance(world, entrance, cave_exit, player, False) + entrance = next(e for e in entrances[::-1] if e not in entrance_exits + inaccessible_entrances + list(invalid_cave_connections[tuple(cave)])) + entrances.remove(entrance) + connect_exit(world, cave_exit, entrance, player, False) + else: + connect_two_way(world, entrance, cave_exit, player) caves.remove(cave) @@ -1149,6 +874,8 @@ def connect_doors(world, doors, targets, player): def scramble_holes(world, player): + invFlag = world.mode[player] == 'inverted' + hole_entrances = [('Kakariko Well Cave', 'Kakariko Well Drop'), ('Bat Cave Cave', 'Bat Cave Drop'), ('North Fairy Cave', 'North Fairy Cave Drop'), @@ -1162,42 +889,65 @@ def scramble_holes(world, player): ('Lost Woods Hideout Exit', 'Lost Woods Hideout (top)'), ('Lumberjack Tree Exit', 'Lumberjack Tree (top)')] + # force uncle cave if world.mode[player] == 'standard': - # cannot move uncle cave connect_two_way(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player) connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player) else: hole_entrances.append(('Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Drop')) hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance')) - - # do not shuffle sanctuary into pyramid hole unless shuffle is crossed - if world.shuffle[player] == 'crossed': - hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) - - # determine pyramid hole - if not world.shuffle_ganon: - if (world.mode[player] == 'inverted') == (0x03 in world.owswaps[player][0] and world.owMixed[player]): - connect_two_way(world, 'Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Pyramid Hole', 'Pyramid', player) - else: - connect_two_way(world, 'Inverted Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Inverted Pyramid Hole', 'Pyramid', player) - else: + + if world.shuffle_ganon: + hole_entrances.append(('Pyramid Entrance', 'Pyramid Hole') if invFlag == (0x1b in world.owswaps[player][0] and world.owMixed[player]) else ('Inverted Pyramid Entrance', 'Inverted Pyramid Hole')) hole_targets.append(('Pyramid Exit', 'Pyramid')) - random.shuffle(hole_targets) - exit, target = hole_targets.pop() - if (world.mode[player] == 'inverted') == (0x03 in world.owswaps[player][0] and world.owMixed[player]): + # shuffle sanctuary hole in same world as other HC entrances + if world.shuffle[player] != 'crossed': + drop_owid_map = { # owid, is_light_world + 'Lost Woods Hideout Stump': (0x00, True), + 'Lumberjack Tree Cave': (0x02, True), + 'Sanctuary': (0x13, True), + 'North Fairy Cave': (0x15, True), + 'Kakariko Well Cave': (0x18, True), + 'Hyrule Castle Secret Entrance Stairs': (0x1b, True), + 'Bat Cave Cave': (0x22, True), + 'Inverted Pyramid Entrance': (0x1b, True), + 'Pyramid Entrance': (0x5b, False) + } + + region = world.get_entrance('Hyrule Castle Exit (South)', player).parent_region + if len(region.entrances) > 0: + hc_in_lw = region.entrances[0].parent_region.type == (RegionType.LightWorld if not invFlag else RegionType.DarkWorld) + else: + # checks if drop candidates exist in LW + drop_owids = [ 0x00, 0x02, 0x13, 0x15, 0x18, 0x1b, 0x22 ] + hc_in_lw = any([invFlag == (owid in world.owswaps[player][0] and world.owMixed[player]) for owid in drop_owids]) + + candidate_drops = list() + for door, drop in hole_entrances: + if hc_in_lw == (drop_owid_map[door][1] == (invFlag == (drop_owid_map[door][0] in world.owswaps[player][0] and world.owMixed[player]))): + candidate_drops.append(tuple((door, drop))) + + random.shuffle(candidate_drops) + door, drop = candidate_drops.pop() + hole_entrances.remove((door, drop)) + connect_two_way(world, door, 'Sanctuary Exit', player) + connect_entrance(world, drop, 'Sewer Drop', player) + else: + hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) + + # place pyramid hole + if not world.shuffle_ganon: + exit, target = ('Pyramid Exit', 'Pyramid') + if invFlag == (0x1b in world.owswaps[player][0] and world.owMixed[player]): connect_two_way(world, 'Pyramid Entrance', exit, player) connect_entrance(world, 'Pyramid Hole', target, player) else: connect_two_way(world, 'Inverted Pyramid Entrance', exit, player) connect_entrance(world, 'Inverted Pyramid Hole', target, player) - if world.shuffle[player] != 'crossed': - hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) - # shuffle the rest + random.shuffle(hole_entrances) random.shuffle(hole_targets) for entrance, drop in hole_entrances: exit, target = hole_targets.pop() @@ -1213,145 +963,122 @@ def skull_woods_shuffle(world, player): def simple_shuffle_dungeons(world, player): + invFlag = world.mode[player] == 'inverted' + skull_woods_shuffle(world, player) dungeon_entrances = ['Eastern Palace', 'Tower of Hera', 'Thieves Town', 'Skull Woods Final Section', 'Palace of Darkness', 'Ice Palace', 'Misery Mire', 'Swamp Palace'] - dungeon_exits = ['Eastern Palace Exit', 'Tower of Hera Exit', 'Thieves Town Exit', 'Skull Woods Final Section Exit', 'Palace of Darkness Exit', 'Ice Palace Exit', 'Misery Mire Exit', 'Swamp Palace Exit'] + dungeon_exits = ['Eastern Palace Exit', 'Tower of Hera Exit', 'Agahnims Tower Exit', 'Thieves Town Exit', 'Skull Woods Final Section Exit', 'Palace of Darkness Exit', 'Ice Palace Exit', 'Misery Mire Exit', 'Swamp Palace Exit'] - if world.mode[player] != 'inverted': + if not invFlag: if not world.shuffle_ganon: connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player) else: dungeon_entrances.append('Ganons Tower') dungeon_exits.append('Ganons Tower Exit') + random.shuffle(dungeon_exits) + at_door = dungeon_exits.pop() else: - # TODO: Should we be ignoring world.shuffle_ganon?? dungeon_entrances.append('Ganons Tower') - dungeon_exits.append('Agahnims Tower Exit') + if not world.shuffle_ganon: + at_door = 'Ganons Tower Exit' + else: + dungeon_exits.append('Ganons Tower Exit') + random.shuffle(dungeon_exits) + at_door = dungeon_exits.pop() - # shuffle up single entrance dungeons + # shuffle single-entrance dungeons connect_random(world, dungeon_entrances, dungeon_exits, player, True) - # mix up 4 door dungeons - multi_dungeons = ['Desert', 'Turtle Rock'] - if world.mode[player] == 'open' or (world.mode[player] == 'inverted' and world.shuffle_ganon): - multi_dungeons.append('Hyrule Castle') - random.shuffle(multi_dungeons) - - dp_target = multi_dungeons[0] - tr_target = multi_dungeons[1] - if world.mode[player] not in ['open', 'inverted'] or (world.mode[player] == 'inverted' and world.shuffle_ganon is False): - # place hyrule castle as intended + # shuffle multi-entrance dungeons + multi_dungeons = ['Desert Palace', 'Turtle Rock'] + if world.mode[player] == 'standard' or (world.mode[player] == 'inverted' and not world.shuffle_ganon): hc_target = 'Hyrule Castle' else: - hc_target = multi_dungeons[2] - - # door shuffle should restrict hyrule castle to the light world due to sanc being limited to the LW - if world.doorShuffle[player] != 'vanilla' and hc_target == 'Turtle Rock': - swap_w_dp = random.choice([True, False]) - if swap_w_dp: - hc_target, dp_target = dp_target, hc_target + multi_dungeons.append('Hyrule Castle') + + dungeon_owid_map = { # owid, is_lw_dungeon + 'Hyrule Castle': (0x1b, True), + 'Desert Palace': (0x30, True), + 'Turtle Rock': (0x47, False) + } + + # checks if drop candidates exist in LW + drop_owids = [ 0x00, 0x02, 0x13, 0x15, 0x18, 0x1b, 0x22 ] + drops_in_light_world = any([invFlag == (owid in world.owswaps[player][0] and world.owMixed[player]) for owid in drop_owids]) + + # placing HC in guaranteed same-world as available dropdowns + if not drops_in_light_world or not invFlag: + candidate_dungeons = list() + for d in multi_dungeons: + if not drops_in_light_world and dungeon_owid_map[d][1] == (invFlag != (dungeon_owid_map[d][0] in world.owswaps[player][0] and world.owMixed[player])): + # only adding DW candidates + candidate_dungeons.append(d) + elif not invFlag and dungeon_owid_map[d][1] == (invFlag == (dungeon_owid_map[d][0] in world.owswaps[player][0] and world.owMixed[player])): + # only adding LW candidates + candidate_dungeons.append(d) + random.shuffle(candidate_dungeons) + hc_target = candidate_dungeons.pop() + multi_dungeons.remove(hc_target) else: - hc_target, tr_target = tr_target, hc_target + random.shuffle(multi_dungeons) + hc_target = multi_dungeons.pop() - # ToDo improve this? - - if world.mode[player] != 'inverted': - if hc_target == 'Hyrule Castle': - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Exit (East)', player) - connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Hyrule Castle Exit (West)', player) - connect_two_way(world, 'Agahnims Tower', 'Agahnims Tower Exit', player) - elif hc_target == 'Desert': - connect_two_way(world, 'Desert Palace Entrance (South)', 'Hyrule Castle Exit (South)', player) - connect_two_way(world, 'Desert Palace Entrance (East)', 'Hyrule Castle Exit (East)', player) - connect_two_way(world, 'Desert Palace Entrance (West)', 'Hyrule Castle Exit (West)', player) - connect_two_way(world, 'Desert Palace Entrance (North)', 'Agahnims Tower Exit', player) - elif hc_target == 'Turtle Rock': - connect_two_way(world, 'Turtle Rock', 'Hyrule Castle Exit (South)', player) + dp_target = multi_dungeons.pop() + tr_target = multi_dungeons.pop() + assert 1==2 + + if hc_target == 'Hyrule Castle': + connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) + connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Exit (East)', player) + connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Hyrule Castle Exit (West)', player) + connect_two_way(world, 'Agahnims Tower', at_door, player) + elif hc_target == 'Desert Palace': + connect_two_way(world, 'Desert Palace Entrance (South)', 'Hyrule Castle Exit (South)', player) + connect_two_way(world, 'Desert Palace Entrance (East)', 'Hyrule Castle Exit (East)', player) + connect_two_way(world, 'Desert Palace Entrance (West)', 'Hyrule Castle Exit (West)', player) + connect_two_way(world, 'Desert Palace Entrance (North)', at_door, player) + elif hc_target == 'Turtle Rock': + connect_two_way(world, 'Turtle Rock', 'Hyrule Castle Exit (South)', player) + connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Hyrule Castle Exit (West)', player) + if invFlag == (0x45 in world.owswaps[player][0] and world.owMixed[player]): connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Hyrule Castle Exit (East)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Hyrule Castle Exit (West)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Agahnims Tower Exit', player) - - if dp_target == 'Hyrule Castle': - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Desert Palace Exit (South)', player) - connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Desert Palace Exit (East)', player) - connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Desert Palace Exit (West)', player) - connect_two_way(world, 'Agahnims Tower', 'Desert Palace Exit (North)', player) - elif dp_target == 'Desert': - connect_two_way(world, 'Desert Palace Entrance (South)', 'Desert Palace Exit (South)', player) - connect_two_way(world, 'Desert Palace Entrance (East)', 'Desert Palace Exit (East)', player) - connect_two_way(world, 'Desert Palace Entrance (West)', 'Desert Palace Exit (West)', player) - connect_two_way(world, 'Desert Palace Entrance (North)', 'Desert Palace Exit (North)', player) - elif dp_target == 'Turtle Rock': - connect_two_way(world, 'Turtle Rock', 'Desert Palace Exit (South)', player) - connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Desert Palace Exit (East)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Desert Palace Exit (West)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Desert Palace Exit (North)', player) - - if tr_target == 'Hyrule Castle': - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Turtle Rock Exit (Front)', player) - connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Turtle Rock Ledge Exit (East)', player) - connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Turtle Rock Ledge Exit (West)', player) - connect_two_way(world, 'Agahnims Tower', 'Turtle Rock Isolated Ledge Exit', player) - elif tr_target == 'Desert': - connect_two_way(world, 'Desert Palace Entrance (South)', 'Turtle Rock Exit (Front)', player) - connect_two_way(world, 'Desert Palace Entrance (North)', 'Turtle Rock Ledge Exit (East)', player) - connect_two_way(world, 'Desert Palace Entrance (West)', 'Turtle Rock Ledge Exit (West)', player) - connect_two_way(world, 'Desert Palace Entrance (East)', 'Turtle Rock Isolated Ledge Exit', player) - elif tr_target == 'Turtle Rock': - connect_two_way(world, 'Turtle Rock', 'Turtle Rock Exit (Front)', player) - connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Turtle Rock Isolated Ledge Exit', player) - connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Turtle Rock Ledge Exit (West)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Turtle Rock Ledge Exit (East)', player) - else: - if hc_target == 'Hyrule Castle': - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Exit (East)', player) - connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Hyrule Castle Exit (West)', player) - connect_two_way(world, 'Agahnims Tower', 'Ganons Tower Exit', player) - elif hc_target == 'Desert': - connect_two_way(world, 'Desert Palace Entrance (South)', 'Hyrule Castle Exit (South)', player) - connect_two_way(world, 'Desert Palace Entrance (East)', 'Hyrule Castle Exit (East)', player) - connect_two_way(world, 'Desert Palace Entrance (West)', 'Hyrule Castle Exit (West)', player) - connect_two_way(world, 'Desert Palace Entrance (North)', 'Ganons Tower Exit', player) - elif hc_target == 'Turtle Rock': - connect_two_way(world, 'Turtle Rock', 'Hyrule Castle Exit (South)', player) - connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Ganons Tower Exit', player) - connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Hyrule Castle Exit (West)', player) + connect_two_way(world, 'Dark Death Mountain Ledge (East)', at_door, player) + else: + connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', at_door, player) connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Hyrule Castle Exit (East)', player) - - if dp_target == 'Hyrule Castle': - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Desert Palace Exit (South)', player) - connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Desert Palace Exit (East)', player) - connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Desert Palace Exit (West)', player) - connect_two_way(world, 'Agahnims Tower', 'Desert Palace Exit (North)', player) - elif dp_target == 'Desert': - connect_two_way(world, 'Desert Palace Entrance (South)', 'Desert Palace Exit (South)', player) - connect_two_way(world, 'Desert Palace Entrance (East)', 'Desert Palace Exit (East)', player) - connect_two_way(world, 'Desert Palace Entrance (West)', 'Desert Palace Exit (West)', player) - connect_two_way(world, 'Desert Palace Entrance (North)', 'Desert Palace Exit (North)', player) - elif dp_target == 'Turtle Rock': - connect_two_way(world, 'Turtle Rock', 'Desert Palace Exit (South)', player) - connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Desert Palace Exit (East)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Desert Palace Exit (West)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Desert Palace Exit (North)', player) - - if tr_target == 'Hyrule Castle': - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Turtle Rock Exit (Front)', player) - connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Turtle Rock Ledge Exit (East)', player) - connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Turtle Rock Ledge Exit (West)', player) - connect_two_way(world, 'Agahnims Tower', 'Turtle Rock Isolated Ledge Exit', player) - elif tr_target == 'Desert': - connect_two_way(world, 'Desert Palace Entrance (South)', 'Turtle Rock Exit (Front)', player) - connect_two_way(world, 'Desert Palace Entrance (North)', 'Turtle Rock Ledge Exit (East)', player) - connect_two_way(world, 'Desert Palace Entrance (West)', 'Turtle Rock Ledge Exit (West)', player) - connect_two_way(world, 'Desert Palace Entrance (East)', 'Turtle Rock Isolated Ledge Exit', player) - elif tr_target == 'Turtle Rock': - connect_two_way(world, 'Turtle Rock', 'Turtle Rock Exit (Front)', player) - connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Turtle Rock Isolated Ledge Exit', player) - connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Turtle Rock Ledge Exit (West)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Turtle Rock Ledge Exit (East)', player) + + if dp_target == 'Hyrule Castle': + connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Desert Palace Exit (South)', player) + connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Desert Palace Exit (East)', player) + connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Desert Palace Exit (West)', player) + connect_two_way(world, 'Agahnims Tower', 'Desert Palace Exit (North)', player) + elif dp_target == 'Desert Palace': + connect_two_way(world, 'Desert Palace Entrance (South)', 'Desert Palace Exit (South)', player) + connect_two_way(world, 'Desert Palace Entrance (East)', 'Desert Palace Exit (East)', player) + connect_two_way(world, 'Desert Palace Entrance (West)', 'Desert Palace Exit (West)', player) + connect_two_way(world, 'Desert Palace Entrance (North)', 'Desert Palace Exit (North)', player) + elif dp_target == 'Turtle Rock': + connect_two_way(world, 'Turtle Rock', 'Desert Palace Exit (South)', player) + connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Desert Palace Exit (East)', player) + connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Desert Palace Exit (West)', player) + connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Desert Palace Exit (North)', player) + + if tr_target == 'Hyrule Castle': + connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Turtle Rock Exit (Front)', player) + connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Turtle Rock Ledge Exit (East)', player) + connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Turtle Rock Ledge Exit (West)', player) + connect_two_way(world, 'Agahnims Tower', 'Turtle Rock Isolated Ledge Exit', player) + elif tr_target == 'Desert Palace': + connect_two_way(world, 'Desert Palace Entrance (South)', 'Turtle Rock Exit (Front)', player) + connect_two_way(world, 'Desert Palace Entrance (North)', 'Turtle Rock Ledge Exit (East)', player) + connect_two_way(world, 'Desert Palace Entrance (West)', 'Turtle Rock Ledge Exit (West)', player) + connect_two_way(world, 'Desert Palace Entrance (East)', 'Turtle Rock Isolated Ledge Exit', player) + elif tr_target == 'Turtle Rock': + connect_two_way(world, 'Turtle Rock', 'Turtle Rock Exit (Front)', player) + connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Turtle Rock Isolated Ledge Exit', player) + connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Turtle Rock Ledge Exit (West)', player) + connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Turtle Rock Ledge Exit (East)', player) def full_shuffle_dungeons(world, Dungeon_Exits, player): @@ -1360,80 +1087,113 @@ def full_shuffle_dungeons(world, Dungeon_Exits, player): skull_woods_shuffle(world, player) dungeon_exits = list(Dungeon_Exits) - lw_entrances = list(LW_Dungeon_Entrances) if not invFlag else list(Inverted_LW_Dungeon_Entrances_Must_Exit) - dw_entrances = list(DW_Dungeon_Entrances) if not invFlag else list(Inverted_DW_Dungeon_Entrances) - if world.mode[player] != 'standard': - lw_entrances.append('Hyrule Castle Entrance (South)') + if world.mode[player] == 'standard': + # must connect front of hyrule castle to do escape + connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - if not invFlag: - if world.mode[player] == 'standard': - # must connect front of hyrule castle to do escape - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - elif world.doorShuffle[player] == 'vanilla': - dungeon_exits.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) - else: - lw_dungeon_entrances_must_exit = list(Inverted_LW_Dungeon_Entrances_Must_Exit) - # randomize which desert ledge door is a must-exit - if random.randint(0, 1) == 0: - lw_dungeon_entrances_must_exit.append('Desert Palace Entrance (North)') - lw_entrances.append('Desert Palace Entrance (West)') - else: - lw_dungeon_entrances_must_exit.append('Desert Palace Entrance (West)') - lw_entrances.append('Desert Palace Entrance (North)') - dungeon_exits.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) - if not world.shuffle_ganon: connect_two_way(world, 'Ganons Tower' if not invFlag else 'Agahnims Tower', 'Ganons Tower Exit', player) - hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)'] else: - if not invFlag: - dw_entrances.append('Ganons Tower') - else: - lw_entrances.append('Agahnims Tower') dungeon_exits.append('Ganons Tower Exit') - hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)', 'Agahnims Tower'] - if not invFlag: - if world.mode[player] == 'standard': - # rest of hyrule castle must be in light world, so it has to be the one connected to east exit of desert - hyrule_castle_exits = [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')] - connect_mandatory_exits(world, lw_entrances, hyrule_castle_exits, list(LW_Dungeon_Entrances_Must_Exit), player) - connect_caves(world, lw_entrances, [], hyrule_castle_exits, player) - elif world.doorShuffle[player] != 'vanilla': - # sanc is in light world, so must all of HC if door shuffle is on - connect_mandatory_exits(world, lw_entrances, - [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)', 'Hyrule Castle Exit (South)')], - list(LW_Dungeon_Entrances_Must_Exit), player) + # determine LW and DW entrances + # owid: (entrances, is_light_world) + dungeon_owid_map = {0x03: ({'Tower of Hera'}, True), + 0x1e: ({'Eastern Palace'}, True), + 0x1b: ({'Hyrule Castle Entrance (South)', + 'Hyrule Castle Entrance (West)', + 'Hyrule Castle Entrance (East)', + 'Agahnims Tower'}, True), + 0x30: ({'Desert Palace Entrance (South)', + 'Desert Palace Entrance (West)', + 'Desert Palace Entrance (East)', + 'Desert Palace Entrance (North)'}, True), + 0x40: ({'Skull Woods Final Section'}, False), + 0x43: ({'Ganons Tower'}, False), + 0x45: ({'Dark Death Mountain Ledge (West)', + 'Dark Death Mountain Ledge (East)', + 'Turtle Rock Isolated Ledge Entrance'}, False), + 0x47: ({'Turtle Rock'}, False), + 0x58: ({'Thieves Town'}, False), + 0x5e: ({'Palace of Darkness'}, False), + 0x70: ({'Misery Mire'}, False), + 0x75: ({'Ice Palace'}, False), + 0x7b: ({'Swamp Palace'}, False) + } + + lw_entrances = list() + dw_entrances = list() + for owid in dungeon_owid_map.keys(): + if dungeon_owid_map[owid][1] == (invFlag == (owid in world.owswaps[player][0] and world.owMixed[player])): + lw_entrances.extend([e for e in dungeon_owid_map[owid][0] if e in entrance_pool]) else: - connect_mandatory_exits(world, lw_entrances, dungeon_exits, list(LW_Dungeon_Entrances_Must_Exit), player) - connect_mandatory_exits(world, dw_entrances, dungeon_exits, list(DW_Dungeon_Entrances_Must_Exit), player) - else: - # shuffle aga door first. If it's on HC ledge, remaining HC ledge door must be must-exit - all_entrances_aga = lw_entrances + dw_entrances - aga_doors = [i for i in all_entrances_aga] - random.shuffle(aga_doors) - aga_door = aga_doors.pop() - - if aga_door in hc_ledge_entrances: - lw_entrances.remove(aga_door) - hc_ledge_entrances.remove(aga_door) - - random.shuffle(hc_ledge_entrances) - hc_ledge_must_exit = hc_ledge_entrances.pop() - lw_entrances.remove(hc_ledge_must_exit) - lw_dungeon_entrances_must_exit.append(hc_ledge_must_exit) - - if aga_door in lw_entrances: - lw_entrances.remove(aga_door) - elif aga_door in dw_entrances: - dw_entrances.remove(aga_door) - - connect_two_way(world, aga_door, 'Agahnims Tower Exit', player) - dungeon_exits.remove('Agahnims Tower Exit') - - connect_mandatory_exits(world, lw_entrances, dungeon_exits, lw_dungeon_entrances_must_exit, player) + dw_entrances.extend([e for e in dungeon_owid_map[owid][0] if e in entrance_pool]) + # determine must-exit entrances + from DoorShuffle import find_inaccessible_regions + find_inaccessible_regions(world, player) + + lw_must_exit = list() + dw_must_exit = list() + lw_related = list() + dw_related = list() + if invFlag == (0x45 in world.owswaps[player][0] and world.owMixed[player]): + dw_entrances.remove('Turtle Rock Isolated Ledge Entrance') + dw_must_exit.append('Turtle Rock Isolated Ledge Entrance') + if 'Dark Death Mountain Ledge' in world.inaccessible_regions[player]: + ledge = ['Dark Death Mountain Ledge (West)', 'Dark Death Mountain Ledge (East)'] + dw_entrances = [e for e in dw_entrances if e not in ledge] + random.shuffle(ledge) + dw_must_exit.append(ledge.pop()) + dw_related.extend(ledge) + if invFlag == (0x30 in world.owswaps[player][0] and world.owMixed[player]): + if 'Desert Palace Mouth' in world.inaccessible_regions[player]: + lw_entrances.remove('Desert Palace Entrance (East)') + lw_must_exit.append('Desert Palace Entrance (East)') + else: + dw_entrances.remove('Desert Palace Entrance (East)') + dw_must_exit.append('Desert Palace Entrance (East)') + if 'Desert Ledge' in world.inaccessible_regions[player]: + ledge = ['Desert Palace Entrance (West)', 'Desert Palace Entrance (North)'] + dw_entrances = [e for e in dw_entrances if e not in ledge] + random.shuffle(ledge) + dw_must_exit.append(ledge.pop()) + dw_related.extend(ledge) + if invFlag == (0x1b in world.owswaps[player][0] and world.owMixed[player]): + if 'Hyrule Castle Ledge' in world.inaccessible_regions[player]: + ledge = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)', 'Agahnims Tower'] + lw_entrances = [e for e in lw_entrances if e not in ledge] + random.shuffle(ledge) + lw_must_exit.append(ledge.pop()) + lw_related.extend(ledge) + random.shuffle(lw_must_exit) + random.shuffle(dw_must_exit) + + # place HC first, needs to be same world as Sanc drop + hyrule_castle_exits = ('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)', 'Hyrule Castle Exit (South)') + hyrule_castle_exits = list([tuple(e for e in hyrule_castle_exits if e in exit_pool)]) + hyrule_castle_exits.extend([e for e in dungeon_exits if isinstance(e, str)]) + dungeon_exits = [e for e in dungeon_exits if not isinstance(e, str)] + if invFlag == (0x13 in world.owswaps[player][0] and world.owMixed[player]): + connect_mandatory_exits(world, lw_entrances, hyrule_castle_exits, lw_must_exit, player, False) + dungeon_exits.extend([e for e in hyrule_castle_exits if isinstance(e, str)]) + hyrule_castle_exits = [e for e in hyrule_castle_exits if not isinstance(e, str)] + connect_caves(world, lw_entrances, [], hyrule_castle_exits, player) + else: + connect_mandatory_exits(world, dw_entrances, hyrule_castle_exits, dw_must_exit, player, False) + dungeon_exits.extend([e for e in hyrule_castle_exits if isinstance(e, str)]) + hyrule_castle_exits = [e for e in hyrule_castle_exits if not isinstance(e, str)] + connect_caves(world, [], dw_entrances, hyrule_castle_exits, player) + + # connect any remaining must-exit entrances + dungeon_exits.extend(hyrule_castle_exits) + connect_mandatory_exits(world, lw_entrances, dungeon_exits, lw_must_exit, player) + connect_mandatory_exits(world, dw_entrances, dungeon_exits, dw_must_exit, player) + + # shuffle the remaining entrances + lw_entrances = lw_entrances + lw_related + dw_entrances = dw_entrances + dw_related connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player) @@ -1817,673 +1577,34 @@ def can_reach(world, entrance_name, region_name, player): return region_name not in world.inaccessible_regions[player] -LW_Dungeon_Entrances = ['Desert Palace Entrance (South)', - 'Desert Palace Entrance (West)', - 'Desert Palace Entrance (North)', - 'Eastern Palace', - 'Tower of Hera', - 'Hyrule Castle Entrance (West)', - 'Hyrule Castle Entrance (East)', - 'Agahnims Tower'] +Dungeon_Exits_Base = [('Desert Palace Exit (South)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)'), + 'Desert Palace Exit (North)', + 'Eastern Palace Exit', + 'Tower of Hera Exit', + 'Thieves Town Exit', + 'Skull Woods Final Section Exit', + 'Ice Palace Exit', + 'Misery Mire Exit', + 'Palace of Darkness Exit', + 'Swamp Palace Exit', + 'Agahnims Tower Exit', + ('Turtle Rock Ledge Exit (East)', 'Turtle Rock Exit (Front)', 'Turtle Rock Ledge Exit (West)', 'Turtle Rock Isolated Ledge Exit')] -LW_Dungeon_Entrances_Must_Exit = ['Desert Palace Entrance (East)'] +Cave_Exits_Base = [('Elder House Exit (East)', 'Elder House Exit (West)'), + ('Two Brothers House Exit (East)', 'Two Brothers House Exit (West)'), + ('Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)'), + ('Fairy Ascension Cave Exit (Bottom)', 'Fairy Ascension Cave Exit (Top)'), + ('Bumper Cave Exit (Top)', 'Bumper Cave Exit (Bottom)'), + ('Hookshot Cave Back Exit', 'Hookshot Cave Front Exit')] -DW_Dungeon_Entrances = ['Thieves Town', - 'Skull Woods Final Section', - 'Ice Palace', - 'Misery Mire', - 'Palace of Darkness', - 'Swamp Palace', - 'Turtle Rock', - 'Dark Death Mountain Ledge (West)'] +Cave_Exits_Directional = [('Superbunny Cave Exit (Bottom)', 'Superbunny Cave Exit (Top)'), + ('Spiral Cave Exit (Top)', 'Spiral Cave Exit')] -DW_Dungeon_Entrances_Must_Exit = ['Dark Death Mountain Ledge (East)', - 'Turtle Rock Isolated Ledge Entrance'] +Cave_Three_Exits_Base = [('Spectacle Rock Cave Exit (Peak)', 'Spectacle Rock Cave Exit (Top)', 'Spectacle Rock Cave Exit'), + ('Paradox Cave Exit (Top)', 'Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Bottom)')] -Dungeon_Exits_Base = [['Desert Palace Exit (South)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)'], - 'Desert Palace Exit (North)', - 'Eastern Palace Exit', - 'Tower of Hera Exit', - 'Thieves Town Exit', - 'Skull Woods Final Section Exit', - 'Ice Palace Exit', - 'Misery Mire Exit', - 'Palace of Darkness Exit', - 'Swamp Palace Exit', - 'Agahnims Tower Exit', - ['Turtle Rock Ledge Exit (East)', - 'Turtle Rock Exit (Front)', 'Turtle Rock Ledge Exit (West)', 'Turtle Rock Isolated Ledge Exit']] +Old_Man_House_Base = [('Old Man House Exit (Bottom)', 'Old Man House Exit (Top)')] -DW_Entrances_Must_Exit = ['Bumper Cave (Top)', 'Hookshot Cave Back Entrance'] - -Two_Door_Caves_Directional = [('Bumper Cave (Bottom)', 'Bumper Cave (Top)'), - ('Hookshot Cave', 'Hookshot Cave Back Entrance')] - -Two_Door_Caves = [('Elder House (East)', 'Elder House (West)'), - ('Two Brothers House (East)', 'Two Brothers House (West)'), - ('Superbunny Cave (Bottom)', 'Superbunny Cave (Top)')] - -Old_Man_Entrances = ['Old Man Cave (East)', - 'Old Man House (Top)', - 'Death Mountain Return Cave (East)', - 'Spectacle Rock Cave', - 'Spectacle Rock Cave Peak', - 'Spectacle Rock Cave (Bottom)'] - -Old_Man_House_Base = [['Old Man House Exit (Bottom)', 'Old Man House Exit (Top)']] - -Cave_Exits_Base = [['Elder House Exit (East)', 'Elder House Exit (West)'], - ['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)'], - ['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)'], - ['Fairy Ascension Cave Exit (Bottom)', 'Fairy Ascension Cave Exit (Top)'], - ['Bumper Cave Exit (Top)', 'Bumper Cave Exit (Bottom)'], - ['Hookshot Cave Back Exit', 'Hookshot Cave Front Exit'], - ['Superbunny Cave Exit (Bottom)', 'Superbunny Cave Exit (Top)'], - ['Spiral Cave Exit (Top)', 'Spiral Cave Exit']] - - -Cave_Three_Exits_Base = [['Spectacle Rock Cave Exit (Peak)', 'Spectacle Rock Cave Exit (Top)', - 'Spectacle Rock Cave Exit'], - ['Paradox Cave Exit (Top)', 'Paradox Cave Exit (Middle)','Paradox Cave Exit (Bottom)']] - - -LW_Entrances = ['Elder House (East)', - 'Elder House (West)', - 'Two Brothers House (East)', - 'Two Brothers House (West)', - 'Old Man Cave (West)', - 'Old Man House (Bottom)', - 'Death Mountain Return Cave (West)', - 'Paradox Cave (Bottom)', - 'Paradox Cave (Middle)', - 'Paradox Cave (Top)', - 'Fairy Ascension Cave (Bottom)', - 'Fairy Ascension Cave (Top)', - 'Spiral Cave', - 'Spiral Cave (Bottom)'] - -DW_Entrances = ['Bumper Cave (Bottom)', - 'Superbunny Cave (Top)', - 'Superbunny Cave (Bottom)', - 'Hookshot Cave'] - -Bomb_Shop_Multi_Cave_Doors = ['Hyrule Castle Entrance (South)', - 'Misery Mire', - 'Thieves Town', - 'Bumper Cave (Bottom)', - 'Swamp Palace', - 'Hyrule Castle Secret Entrance Stairs', - 'Skull Woods First Section Door', - 'Skull Woods Second Section Door (East)', - 'Skull Woods Second Section Door (West)', - 'Skull Woods Final Section', - 'Ice Palace', - 'Turtle Rock', - 'Dark Death Mountain Ledge (West)', - 'Dark Death Mountain Ledge (East)', - 'Superbunny Cave (Top)', - 'Superbunny Cave (Bottom)', - 'Hookshot Cave', - 'Ganons Tower', - 'Desert Palace Entrance (South)', - 'Tower of Hera', - 'Two Brothers House (West)', - 'Old Man Cave (East)', - 'Old Man House (Bottom)', - 'Old Man House (Top)', - 'Death Mountain Return Cave (East)', - 'Death Mountain Return Cave (West)', - 'Spectacle Rock Cave Peak', - 'Spectacle Rock Cave', - 'Spectacle Rock Cave (Bottom)', - 'Paradox Cave (Bottom)', - 'Paradox Cave (Middle)', - 'Paradox Cave (Top)', - 'Fairy Ascension Cave (Bottom)', - 'Fairy Ascension Cave (Top)', - 'Spiral Cave', - 'Spiral Cave (Bottom)', - 'Palace of Darkness', - 'Hyrule Castle Entrance (West)', - 'Hyrule Castle Entrance (East)', - 'Agahnims Tower', - 'Desert Palace Entrance (West)', - 'Desert Palace Entrance (North)' - # all entrances below this line would be possible for blacksmith_hut - # if it were not for dwarf checking multi-entrance caves - ] - -Blacksmith_Multi_Cave_Doors = ['Eastern Palace', - 'Elder House (East)', - 'Elder House (West)', - 'Two Brothers House (East)', - 'Old Man Cave (West)', - 'Sanctuary', - 'Lumberjack Tree Cave', - 'Lost Woods Hideout Stump', - 'North Fairy Cave', - 'Bat Cave Cave', - 'Kakariko Well Cave'] - -LW_Single_Cave_Doors = ['Blinds Hideout', - 'Lake Hylia Fairy', - 'Light Hype Fairy', - 'Desert Fairy', - 'Chicken House', - 'Aginahs Cave', - 'Sahasrahlas Hut', - 'Cave Shop (Lake Hylia)', - 'Blacksmiths Hut', - 'Sick Kids House', - 'Lost Woods Gamble', - 'Fortune Teller (Light)', - 'Snitch Lady (East)', - 'Snitch Lady (West)', - 'Bush Covered House', - 'Tavern (Front)', - 'Light World Bomb Hut', - 'Kakariko Shop', - 'Mini Moldorm Cave', - 'Long Fairy Cave', - 'Good Bee Cave', - '20 Rupee Cave', - '50 Rupee Cave', - 'Ice Rod Cave', - 'Library', - 'Potion Shop', - 'Dam', - 'Lumberjack House', - 'Lake Hylia Fortune Teller', - 'Kakariko Gamble Game', - 'Waterfall of Wishing', - 'Capacity Upgrade', - 'Bonk Rock Cave', - 'Graveyard Cave', - 'Checkerboard Cave', - 'Cave 45', - 'Kings Grave', - 'Bonk Fairy (Light)', - 'Hookshot Fairy', - 'Mimic Cave', - 'Links House'] - -Isolated_LH_Doors = ['Kings Grave', - 'Waterfall of Wishing', - 'Desert Palace Entrance (South)', - 'Desert Palace Entrance (North)', - 'Capacity Upgrade', - 'Ice Palace', - 'Skull Woods Final Section', - 'Dark World Hammer Peg Cave', - 'Turtle Rock Isolated Ledge Entrance'] - -DW_Single_Cave_Doors = ['Bonk Fairy (Dark)', - 'Dark Sanctuary Hint', - 'Dark Lake Hylia Fairy', - 'C-Shaped House', - 'Big Bomb Shop', - 'Dark Death Mountain Fairy', - 'Dark Lake Hylia Shop', - 'Dark World Shop', - 'Red Shield Shop', - 'Mire Shed', - 'East Dark World Hint', - 'Dark Desert Hint', - 'Spike Cave', - 'Palace of Darkness Hint', - 'Dark Lake Hylia Ledge Spike Cave', - 'Cave Shop (Dark Death Mountain)', - 'Dark World Potion Shop', - 'Pyramid Fairy', - 'Archery Game', - 'Dark World Lumberjack Shop', - 'Hype Cave', - 'Brewery', - 'Dark Lake Hylia Ledge Hint', - 'Chest Game', - 'Dark Desert Fairy', - 'Dark Lake Hylia Ledge Fairy', - 'Fortune Teller (Dark)', - 'Dark World Hammer Peg Cave'] - -Blacksmith_Single_Cave_Doors = ['Blinds Hideout', - 'Lake Hylia Fairy', - 'Light Hype Fairy', - 'Desert Fairy', - 'Chicken House', - 'Aginahs Cave', - 'Sahasrahlas Hut', - 'Cave Shop (Lake Hylia)', - 'Blacksmiths Hut', - 'Sick Kids House', - 'Lost Woods Gamble', - 'Fortune Teller (Light)', - 'Snitch Lady (East)', - 'Snitch Lady (West)', - 'Bush Covered House', - 'Tavern (Front)', - 'Light World Bomb Hut', - 'Kakariko Shop', - 'Mini Moldorm Cave', - 'Long Fairy Cave', - 'Good Bee Cave', - '20 Rupee Cave', - '50 Rupee Cave', - 'Ice Rod Cave', - 'Library', - 'Potion Shop', - 'Dam', - 'Lumberjack House', - 'Lake Hylia Fortune Teller', - 'Kakariko Gamble Game'] - -Bomb_Shop_Single_Cave_Doors = ['Waterfall of Wishing', - 'Capacity Upgrade', - 'Bonk Rock Cave', - 'Graveyard Cave', - 'Checkerboard Cave', - 'Cave 45', - 'Kings Grave', - 'Bonk Fairy (Light)', - 'Hookshot Fairy', - 'East Dark World Hint', - 'Palace of Darkness Hint', - 'Dark Lake Hylia Fairy', - 'Dark Lake Hylia Ledge Fairy', - 'Dark Lake Hylia Ledge Spike Cave', - 'Dark Lake Hylia Ledge Hint', - 'Hype Cave', - 'Bonk Fairy (Dark)', - 'Brewery', - 'C-Shaped House', - 'Chest Game', - 'Dark World Hammer Peg Cave', - 'Red Shield Shop', - 'Dark Sanctuary Hint', - 'Fortune Teller (Dark)', - 'Dark World Shop', - 'Dark World Lumberjack Shop', - 'Dark World Potion Shop', - 'Archery Game', - 'Mire Shed', - 'Dark Desert Hint', - 'Dark Desert Fairy', - 'Spike Cave', - 'Cave Shop (Dark Death Mountain)', - 'Dark Death Mountain Fairy', - 'Mimic Cave', - 'Big Bomb Shop', - 'Dark Lake Hylia Shop'] - -Single_Cave_Doors = ['Pyramid Fairy'] - -Single_Cave_Targets = ['Blinds Hideout', - 'Bonk Fairy (Light)', - 'Lake Hylia Healer Fairy', - 'Swamp Healer Fairy', - 'Desert Healer Fairy', - 'Kings Grave', - 'Chicken House', - 'Aginahs Cave', - 'Sahasrahlas Hut', - 'Cave Shop (Lake Hylia)', - 'Sick Kids House', - 'Lost Woods Gamble', - 'Fortune Teller (Light)', - 'Snitch Lady (East)', - 'Snitch Lady (West)', - 'Bush Covered House', - 'Tavern (Front)', - 'Light World Bomb Hut', - 'Kakariko Shop', - 'Cave 45', - 'Graveyard Cave', - 'Checkerboard Cave', - 'Mini Moldorm Cave', - 'Long Fairy Cave', - 'Good Bee Cave', - '20 Rupee Cave', - '50 Rupee Cave', - 'Ice Rod Cave', - 'Bonk Rock Cave', - 'Library', - 'Potion Shop', - 'Hookshot Fairy', - 'Waterfall of Wishing', - 'Capacity Upgrade', - 'Pyramid Fairy', - 'East Dark World Hint', - 'Palace of Darkness Hint', - 'Dark Lake Hylia Healer Fairy', - 'Dark Lake Hylia Ledge Healer Fairy', - 'Dark Lake Hylia Ledge Spike Cave', - 'Dark Lake Hylia Ledge Hint', - 'Hype Cave', - 'Bonk Fairy (Dark)', - 'Brewery', - 'C-Shaped House', - 'Chest Game', - 'Dark World Hammer Peg Cave', - 'Red Shield Shop', - 'Dark Sanctuary Hint', - 'Fortune Teller (Dark)', - 'Village of Outcasts Shop', - 'Dark Lake Hylia Shop', - 'Dark World Lumberjack Shop', - 'Archery Game', - 'Mire Shed', - 'Dark Desert Hint', - 'Dark Desert Healer Fairy', - 'Spike Cave', - 'Cave Shop (Dark Death Mountain)', - 'Dark Death Mountain Healer Fairy', - 'Mimic Cave', - 'Dark World Potion Shop', - 'Lumberjack House', - 'Lake Hylia Fortune Teller', - 'Kakariko Gamble Game', - 'Dam'] - -Inverted_LW_Dungeon_Entrances = ['Desert Palace Entrance (South)', - 'Eastern Palace', - 'Tower of Hera', - 'Hyrule Castle Entrance (West)', - 'Hyrule Castle Entrance (East)'] - -Inverted_DW_Dungeon_Entrances = ['Thieves Town', - 'Skull Woods Final Section', - 'Ice Palace', - 'Misery Mire', - 'Palace of Darkness', - 'Swamp Palace', - 'Turtle Rock', - 'Dark Death Mountain Ledge (West)', - 'Dark Death Mountain Ledge (East)', - 'Turtle Rock Isolated Ledge Entrance', - 'Ganons Tower'] - -Inverted_LW_Dungeon_Entrances_Must_Exit = ['Desert Palace Entrance (East)'] - -Inverted_LW_Entrances_Must_Exit = ['Death Mountain Return Cave (West)', - 'Two Brothers House (West)'] - -Inverted_Two_Door_Caves_Directional = [('Old Man Cave (West)', 'Death Mountain Return Cave (West)'), - ('Two Brothers House (East)', 'Two Brothers House (West)')] - - -Inverted_Two_Door_Caves = [('Elder House (East)', 'Elder House (West)'), - ('Superbunny Cave (Bottom)', 'Superbunny Cave (Top)'), - ('Hookshot Cave', 'Hookshot Cave Back Entrance')] - - - -Inverted_Old_Man_Entrances = ['Dark Death Mountain Fairy', - 'Spike Cave'] - -Inverted_LW_Entrances = ['Elder House (East)', - 'Elder House (West)', - 'Two Brothers House (East)', - 'Old Man Cave (East)', - 'Old Man Cave (West)', - 'Old Man House (Bottom)', - 'Old Man House (Top)', - 'Death Mountain Return Cave (East)', - 'Paradox Cave (Bottom)', - 'Paradox Cave (Middle)', - 'Paradox Cave (Top)', - 'Spectacle Rock Cave', - 'Spectacle Rock Cave Peak', - 'Spectacle Rock Cave (Bottom)', - 'Fairy Ascension Cave (Bottom)', - 'Fairy Ascension Cave (Top)', - 'Spiral Cave', - 'Spiral Cave (Bottom)'] - - -Inverted_DW_Entrances = ['Bumper Cave (Bottom)', - 'Superbunny Cave (Top)', - 'Superbunny Cave (Bottom)', - 'Hookshot Cave', - 'Hookshot Cave Back Entrance'] - -Inverted_Bomb_Shop_Multi_Cave_Doors = ['Hyrule Castle Entrance (South)', - 'Misery Mire', - 'Thieves Town', - 'Bumper Cave (Bottom)', - 'Swamp Palace', - 'Hyrule Castle Secret Entrance Stairs', - 'Skull Woods First Section Door', - 'Skull Woods Second Section Door (East)', - 'Skull Woods Second Section Door (West)', - 'Skull Woods Final Section', - 'Ice Palace', - 'Turtle Rock', - 'Dark Death Mountain Ledge (West)', - 'Dark Death Mountain Ledge (East)', - 'Superbunny Cave (Top)', - 'Superbunny Cave (Bottom)', - 'Hookshot Cave', - 'Ganons Tower', - 'Desert Palace Entrance (South)', - 'Tower of Hera', - 'Two Brothers House (West)', - 'Old Man Cave (East)', - 'Old Man House (Bottom)', - 'Old Man House (Top)', - 'Death Mountain Return Cave (East)', - 'Death Mountain Return Cave (West)', - 'Spectacle Rock Cave Peak', - 'Paradox Cave (Bottom)', - 'Paradox Cave (Middle)', - 'Paradox Cave (Top)', - 'Fairy Ascension Cave (Bottom)', - 'Fairy Ascension Cave (Top)', - 'Spiral Cave', - 'Spiral Cave (Bottom)', - 'Palace of Darkness', - 'Hyrule Castle Entrance (West)', - 'Hyrule Castle Entrance (East)', - 'Agahnims Tower', - 'Desert Palace Entrance (West)', - 'Desert Palace Entrance (North)'] - -Inverted_DW_Single_Cave_Doors = ['Bonk Fairy (Dark)', - 'Dark Sanctuary Hint', - 'Big Bomb Shop', - 'Dark Lake Hylia Fairy', - 'C-Shaped House', - 'Bumper Cave (Top)', - 'Dark Lake Hylia Shop', - 'Dark World Shop', - 'Red Shield Shop', - 'Mire Shed', - 'East Dark World Hint', - 'Dark Desert Hint', - 'Palace of Darkness Hint', - 'Dark Lake Hylia Ledge Spike Cave', - 'Cave Shop (Dark Death Mountain)', - 'Dark World Potion Shop', - 'Pyramid Fairy', - 'Archery Game', - 'Dark World Lumberjack Shop', - 'Hype Cave', - 'Brewery', - 'Dark Lake Hylia Ledge Hint', - 'Chest Game', - 'Dark Desert Fairy', - 'Dark Lake Hylia Ledge Fairy', - 'Fortune Teller (Dark)', - 'Dark World Hammer Peg Cave'] - - -Inverted_Bomb_Shop_Single_Cave_Doors = ['Waterfall of Wishing', - 'Capacity Upgrade', - 'Bonk Rock Cave', - 'Graveyard Cave', - 'Checkerboard Cave', - 'Cave 45', - 'Kings Grave', - 'Bonk Fairy (Light)', - 'Hookshot Fairy', - 'East Dark World Hint', - 'Palace of Darkness Hint', - 'Dark Lake Hylia Fairy', - 'Dark Lake Hylia Ledge Fairy', - 'Dark Lake Hylia Ledge Spike Cave', - 'Dark Lake Hylia Ledge Hint', - 'Hype Cave', - 'Bonk Fairy (Dark)', - 'Brewery', - 'C-Shaped House', - 'Chest Game', - 'Dark World Hammer Peg Cave', - 'Red Shield Shop', - 'Dark Sanctuary Hint', - 'Fortune Teller (Dark)', - 'Dark World Shop', - 'Dark World Lumberjack Shop', - 'Dark World Potion Shop', - 'Archery Game', - 'Mire Shed', - 'Dark Desert Hint', - 'Dark Desert Fairy', - 'Spike Cave', - 'Cave Shop (Dark Death Mountain)', - 'Bumper Cave (Top)', - 'Mimic Cave', - 'Dark Lake Hylia Shop', - 'Links House', - 'Big Bomb Shop'] - -Inverted_Blacksmith_Single_Cave_Doors = ['Blinds Hideout', - 'Lake Hylia Fairy', - 'Light Hype Fairy', - 'Desert Fairy', - 'Chicken House', - 'Aginahs Cave', - 'Sahasrahlas Hut', - 'Cave Shop (Lake Hylia)', - 'Blacksmiths Hut', - 'Sick Kids House', - 'Lost Woods Gamble', - 'Fortune Teller (Light)', - 'Snitch Lady (East)', - 'Snitch Lady (West)', - 'Bush Covered House', - 'Tavern (Front)', - 'Light World Bomb Hut', - 'Kakariko Shop', - 'Mini Moldorm Cave', - 'Long Fairy Cave', - 'Good Bee Cave', - '20 Rupee Cave', - '50 Rupee Cave', - 'Ice Rod Cave', - 'Library', - 'Potion Shop', - 'Dam', - 'Lumberjack House', - 'Lake Hylia Fortune Teller', - 'Kakariko Gamble Game', - 'Links House'] - -Inverted_Single_Cave_Targets = ['Blinds Hideout', - 'Bonk Fairy (Light)', - 'Lake Hylia Healer Fairy', - 'Swamp Healer Fairy', - 'Desert Healer Fairy', - 'Kings Grave', - 'Chicken House', - 'Aginahs Cave', - 'Sahasrahlas Hut', - 'Cave Shop (Lake Hylia)', - 'Sick Kids House', - 'Lost Woods Gamble', - 'Fortune Teller (Light)', - 'Snitch Lady (East)', - 'Snitch Lady (West)', - 'Bush Covered House', - 'Tavern (Front)', - 'Light World Bomb Hut', - 'Kakariko Shop', - 'Cave 45', - 'Graveyard Cave', - 'Checkerboard Cave', - 'Mini Moldorm Cave', - 'Long Fairy Cave', - 'Good Bee Cave', - '20 Rupee Cave', - '50 Rupee Cave', - 'Ice Rod Cave', - 'Bonk Rock Cave', - 'Library', - 'Potion Shop', - 'Hookshot Fairy', - 'Waterfall of Wishing', - 'Capacity Upgrade', - 'Pyramid Fairy', - 'East Dark World Hint', - 'Palace of Darkness Hint', - 'Dark Lake Hylia Healer Fairy', - 'Dark Lake Hylia Ledge Healer Fairy', - 'Dark Lake Hylia Ledge Spike Cave', - 'Dark Lake Hylia Ledge Hint', - 'Hype Cave', - 'Bonk Fairy (Dark)', - 'Brewery', - 'C-Shaped House', - 'Chest Game', - 'Dark World Hammer Peg Cave', - 'Red Shield Shop', - 'Fortune Teller (Dark)', - 'Village of Outcasts Shop', - 'Dark Lake Hylia Shop', - 'Dark World Lumberjack Shop', - 'Archery Game', - 'Mire Shed', - 'Dark Desert Hint', - 'Dark Desert Healer Fairy', - 'Spike Cave', - 'Cave Shop (Dark Death Mountain)', - 'Dark Death Mountain Healer Fairy', - 'Mimic Cave', - 'Dark World Potion Shop', - 'Lumberjack House', - 'Lake Hylia Fortune Teller', - 'Kakariko Gamble Game', - 'Dam'] - -# in inverted we put dark sanctuary in west dark world for now -Inverted_Dark_Sanctuary_Doors = ['Dark Sanctuary Hint', - 'Fortune Teller (Dark)', - 'Brewery', - 'C-Shaped House', - 'Chest Game', - 'Dark World Lumberjack Shop', - 'Red Shield Shop', - 'Bumper Cave (Bottom)', - 'Bumper Cave (Top)', - 'Thieves Town'] - -# Entrances that cannot be used to access a must_exit entrance - symmetrical to allow reverse lookups -Must_Exit_Invalid_Connections = defaultdict(set, { - 'Dark Death Mountain Ledge (East)': {'Dark Death Mountain Ledge (West)', 'Mimic Cave'}, - 'Dark Death Mountain Ledge (West)': {'Dark Death Mountain Ledge (East)', 'Mimic Cave'}, - 'Mimic Cave': {'Dark Death Mountain Ledge (West)', 'Dark Death Mountain Ledge (East)'}, - 'Bumper Cave (Top)': {'Death Mountain Return Cave (West)'}, - 'Death Mountain Return Cave (West)': {'Bumper Cave (Top)'}, - 'Skull Woods Second Section Door (West)': {'Skull Woods Final Section'}, - 'Skull Woods Final Section': {'Skull Woods Second Section Door (West)'}, -}) -Inverted_Must_Exit_Invalid_Connections = defaultdict(set, { - 'Bumper Cave (Top)': {'Death Mountain Return Cave (West)'}, - 'Death Mountain Return Cave (West)': {'Bumper Cave (Top)'}, - 'Desert Palace Entrance (North)': {'Desert Palace Entrance (West)'}, - 'Desert Palace Entrance (West)': {'Desert Palace Entrance (North)'}, - 'Agahnims Tower': {'Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)'}, - 'Hyrule Castle Entrance (West)': {'Hyrule Castle Entrance (East)', 'Agahnims Tower'}, - 'Hyrule Castle Entrance (East)': {'Hyrule Castle Entrance (West)', 'Agahnims Tower'}, -}) Entrance_Pool_Base = {'Links House', 'Desert Palace Entrance (South)', diff --git a/Main.py b/Main.py index 056c010e..90e7fe86 100644 --- a/Main.py +++ b/Main.py @@ -513,7 +513,9 @@ def copy_world(world): connect_portal(portal, ret, player) ret.sanc_portal = world.sanc_portal + from OverworldShuffle import categorize_world_regions for player in range(1, world.players + 1): + categorize_world_regions(ret, player) set_rules(ret, player) return ret diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 75679891..a8081daa 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -256,6 +256,8 @@ def link_overworld(world, player): logging.getLogger('').warning("Edge '%s' could not find a valid connection" % back_set[0]) assert len(connected_edges) == len(default_connections) * 2, connected_edges + # TODO: Reshuffle some areas if impossible to reach, exception if non-dungeon ER enabled or if area is LW with no portal and flute shuffle is enabled + # flute shuffle def connect_flutes(flute_destinations): for o in range(0, len(flute_destinations)): From c7c20372b4f0247ee1f4789d692891d44668b34f Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 14 Oct 2021 23:29:08 -0500 Subject: [PATCH 18/76] Oops --- EntranceShuffle.py | 1 - 1 file changed, 1 deletion(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index dad10a17..b4928da2 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -1026,7 +1026,6 @@ def simple_shuffle_dungeons(world, player): dp_target = multi_dungeons.pop() tr_target = multi_dungeons.pop() - assert 1==2 if hc_target == 'Hyrule Castle': connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) From 579f6c609d26b542abf7d572a7f4a52bb0455e82 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 15 Oct 2021 00:42:42 -0500 Subject: [PATCH 19/76] Added Mystery options for OWG and Shuffle Ganon --- CLI.py | 2 +- Mystery.py | 4 +++- mystery_example.yml | 7 +++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CLI.py b/CLI.py index 1d0f9a43..41b812f8 100644 --- a/CLI.py +++ b/CLI.py @@ -97,7 +97,7 @@ def parse_cli(argv, no_defaults=False): 'ow_shuffle', 'ow_crossed', 'ow_keepsimilar', 'ow_mixed', 'ow_fluteshuffle', 'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', - 'bombbag', + 'bombbag', 'shuffleganon', 'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max', 'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'pseudoboots', 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters', diff --git a/Mystery.py b/Mystery.py index f18a4205..933a4fc1 100644 --- a/Mystery.py +++ b/Mystery.py @@ -120,7 +120,7 @@ def roll_settings(weights): if glitches_required not in ['none', 'no_logic']: print("Only NMG and No Logic supported") glitches_required = 'none' - ret.logic = {'none': 'noglitches', 'no_logic': 'nologic'}[glitches_required] + ret.logic = {'none': 'noglitches', 'owg': 'owglitches', 'no_logic': 'nologic'}[glitches_required] item_placement = get_choice('item_placement') # not supported in ER @@ -167,6 +167,8 @@ def roll_settings(weights): }[goal] ret.openpyramid = goal == 'fast_ganon' if ret.shuffle in ['vanilla', 'dungeonsfull', 'dungeonssimple'] else False + ret.shuffleganon = get_choice('shuffleganon') == 'on' + ret.crystals_gt = get_choice('tower_open') ret.crystals_ganon = get_choice('ganon_open') diff --git a/mystery_example.yml b/mystery_example.yml index 9d1d1501..f9e276a6 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -48,6 +48,12 @@ full: 2 crossed: 3 insanity: 1 + shufflelinks: + on: 1 + off: 1 + shuffleganon: + on: 1 + off: 1 world_state: standard: 1 open: 1 @@ -81,6 +87,7 @@ off: 1 glitches_required: none: 1 + owg: 0 no_logic: 0 accessibility: items: 1 From 62e925d589b175a0c625e0331512d34bfed232e2 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 15 Oct 2021 00:43:21 -0500 Subject: [PATCH 20/76] Some additional fixes --- EntranceShuffle.py | 3 ++- Rules.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index b4928da2..7d9713e6 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -432,7 +432,7 @@ def link_entrances(world, player): 'Skull Left Drop', 'Skull Pinball', 'Skull Pot Circle'] if world.mode[player] == 'standard': - connect_two_way(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player) + connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player) connect_two_way(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player) caves.append(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) @@ -513,6 +513,7 @@ def link_entrances(world, player): if links_house.connected_region and links_house.connected_region.name == 'Links House': break connect_exit(world, 'Chris Houlihan Room Exit', links_house.name, player) + ignore_pool = True # check for swamp palace fix if not (world.get_entrance('Dam', player).connected_region.name in ['Dam', 'Swamp Portal'] and world.get_entrance('Swamp Palace', player).connected_region.name == ['Dam', 'Swamp Portal']): diff --git a/Rules.py b/Rules.py index c4429317..3f606755 100644 --- a/Rules.py +++ b/Rules.py @@ -1344,7 +1344,8 @@ def no_glitches_rules(world, player): # add_rule(world.get_location(location, player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: False) # no glitches does not require block override forbid_bomb_jump_requirements(world, player) - add_conditional_lamps(world, player) + if world.get_region('Big Bomb Shop', player).entrances: # just some location that is placed late in the ER algorithm, prevent underworld rules from applying when trying to search reachability in the overworld + add_conditional_lamps(world, player) def fake_flipper_rules(world, player): From f958e2a3ea3c55555a24fba5bcd9b9b6cf0db6e1 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 15 Oct 2021 20:02:42 -0500 Subject: [PATCH 21/76] Fixed some errors, properly junk filling entrances in Simple ER --- EntranceShuffle.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 7d9713e6..de9dea4c 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -620,7 +620,7 @@ def connect_exit(world, exitname, entrancename, player, mark_two_way=True): if not (ignore_pool or exitname == 'Chris Houlihan Room Exit'): if mark_two_way: entrance_pool.remove(entrancename) - else: + elif world.shuffle[player] == 'insanity': entrance_exits.append(entrancename) exit_pool.remove(exitname) @@ -649,7 +649,8 @@ def connect_two_way(world, entrancename, exitname, player): if not ignore_pool: entrance_pool.remove(entrancename) exit_pool.remove(exitname) - entrance_exits.append(entrancename) + if world.shuffle[player] == 'insanity': + entrance_exits.append(entrancename) if not suppress_spoiler: world.spoiler.set_entrance(entrance.name, exit.name, 'both', player) @@ -1343,7 +1344,11 @@ def junk_fill_inaccessible(world, player): for exit in region.exits: if not exit.connected_region and exit.name in entrance_pool: inaccessible_entrances.append(exit.name) - #TODO: assign non-item locations to the entrances that exist in the unreachable regions + + junk_locations = [e for e in list(zip(*default_connections))[1] if e in exit_pool] + random.shuffle(junk_locations) + for entrance in inaccessible_entrances: + connect_entrance(world, entrance, junk_locations.pop(), player) def connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, player): From 395090d58fb86733c74f305c3b8d61c5a4efce05 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sat, 16 Oct 2021 13:37:08 -0500 Subject: [PATCH 22/76] Fixed Houlihan error in ER on subsequent generations --- EntranceShuffle.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index de9dea4c..b92f35fc 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -14,6 +14,7 @@ def link_entrances(world, player): invFlag = world.mode[player] == 'inverted' global entrance_pool, exit_pool, ignore_pool, suppress_spoiler + links_house = False entrance_pool = Entrance_Pool_Base.copy() exit_pool = Exit_Pool_Base.copy() drop_connections = default_drop_connections.copy() @@ -509,10 +510,12 @@ def link_entrances(world, player): # ensure Houlihan exits where Links House does # TODO: Plando should overrule this - for links_house in world.get_entrance('Links House Exit', player).connected_region.exits: - if links_house.connected_region and links_house.connected_region.name == 'Links House': - break - connect_exit(world, 'Chris Houlihan Room Exit', links_house.name, player) + if not links_house: + for links_house in world.get_entrance('Links House Exit', player).connected_region.exits: + if links_house.connected_region and links_house.connected_region.name == 'Links House': + links_house = links_house.name + break + connect_exit(world, 'Chris Houlihan Room Exit', links_house, player) ignore_pool = True # check for swamp palace fix From e994be11f64de279b42b838d3cd5801859472b2b Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sat, 16 Oct 2021 13:47:55 -0500 Subject: [PATCH 23/76] Added new Lite (fka. Beginner) ER mode --- BaseClasses.py | 4 +- EntranceShuffle.py | 51 ++++++++++++++++++- ItemList.py | 2 +- Rom.py | 2 +- Rules.py | 2 +- mystery_example.yml | 1 + resources/app/cli/args.json | 1 + resources/app/cli/lang/en.json | 8 ++- resources/app/gui/lang/en.json | 1 + .../app/gui/randomize/entrando/widgets.json | 1 + 10 files changed, 65 insertions(+), 8 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index ab033715..efe3adf6 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -120,7 +120,7 @@ class World(object): set_player_attr('can_access_trock_front', None) set_player_attr('can_access_trock_big_chest', None) set_player_attr('can_access_trock_middle', None) - set_player_attr('fix_fake_world', logic[player] not in ['owglitches', 'nologic'] or shuffle[player] in ['crossed', 'insanity', 'madness_legacy']) + set_player_attr('fix_fake_world', logic[player] not in ['owglitches', 'nologic'] or shuffle[player] in ['lite', 'crossed', 'insanity', 'madness_legacy']) set_player_attr('mapshuffle', False) set_player_attr('compassshuffle', False) set_player_attr('keyshuffle', False) @@ -2950,7 +2950,7 @@ class Pot(object): # byte 0: DDOO OEEE (DR, OR, ER) dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0} or_mode = {"vanilla": 0, "parallel": 1, "full": 1} -er_mode = {"vanilla": 0, "simple": 1, "restricted": 3, "full": 3, "crossed": 4, "insanity": 5, "dungeonsfull": 7, "dungeonssimple": 7} +er_mode = {"vanilla": 0, "simple": 1, "restricted": 2, "full": 3, "lite": 4, "crossed": 5, "insanity": 6, "dungeonsfull": 7, "dungeonssimple": 8} # byte 1: LLLW WSSR (logic, mode, sword, retro) logic_mode = {"noglitches": 0, "minorglitches": 1, "nologic": 2, "owglitches": 3, "majorglitches": 4} diff --git a/EntranceShuffle.py b/EntranceShuffle.py index b92f35fc..36e4b394 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -366,6 +366,44 @@ def link_entrances(world, player): dw_entrances = [e for e in dw_entrances if e in entrance_pool] connect_caves(world, lw_entrances, dw_entrances, caves, player) + # place remaining doors + connect_doors(world, list(entrance_pool), list(exit_pool), player) + elif world.shuffle[player] == 'lite': + for entrancename, exitname in default_connections + ([] if world.shopsanity[player] else default_shop_connections): + connect_logical(world, entrancename, exitname, player, False) + + # place bomb shop + bomb_shop = 'Big Bomb Shop' if invFlag == (0x2c in world.owswaps[player][0] and world.owMixed[player]) else 'Links House' + connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) + + suppress_spoiler = False + + # place links house + links_house = place_links_house(world, sectors, player) + + # shuffle dungeons + full_shuffle_dungeons(world, Dungeon_Exits, player) + + # shuffle dropdowns + scramble_holes(world, player) + + # place old man, has limited options + connector_entrances = [e for e in list(zip(*default_connector_connections))[0] if e in entrance_pool] + place_old_man(world, list(connector_entrances), player) + + caves = list(Cave_Exits + Cave_Three_Exits + Old_Man_House) + + # place connectors in inaccessible regions + connector_entrances = [e for e in connector_entrances if e in entrance_pool] + connect_inaccessible_regions(world, connector_entrances, [], caves, player) + + # shuffle remaining connectors + connector_entrances = [e for e in connector_entrances if e in entrance_pool] + connect_caves(world, connector_entrances, [], caves, player) + + # place blacksmith, has limited options + place_blacksmith(world, links_house, player) + # place remaining doors connect_doors(world, list(entrance_pool), list(exit_pool), player) elif world.shuffle[player] == 'crossed': @@ -1171,6 +1209,13 @@ def full_shuffle_dungeons(world, Dungeon_Exits, player): random.shuffle(ledge) lw_must_exit.append(ledge.pop()) lw_related.extend(ledge) + if world.shuffle[player] == 'lite': + lw_entrances.extend(dw_entrances) + lw_must_exit.extend(dw_must_exit) + lw_related.extend(dw_related) + dw_entrances = list() + dw_must_exit = list() + dw_related = list() random.shuffle(lw_must_exit) random.shuffle(dw_must_exit) @@ -1231,6 +1276,8 @@ def place_links_house(world, sectors, player): return entrances links_house_doors = [i for i in get_link_candidates() if i in entrance_pool] + if world.shuffle[player] == 'lite': + links_house_doors = [e for e in links_house_doors if e in list(zip(*(default_item_connections + (default_shop_connections if world.shopsanity[player] else []))))[0]] links_house = random.choice(links_house_doors) connect_two_way(world, links_house, 'Links House Exit', player) return links_house @@ -1304,6 +1351,8 @@ def place_old_man(world, pool, player): else: region_name = 'West Dark Death Mountain (Top)' old_man_entrances = list(build_accessible_entrance_list(world, region_name, player, [], False, True, True)) + if world.shuffle[player] == 'lite': + old_man_entrances = [e for e in old_man_entrances if e in pool] random.shuffle(old_man_entrances) old_man_exit = None while not old_man_exit: @@ -1400,7 +1449,7 @@ def connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, playe connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, player) # connect one connector at a time to ensure multiple connectors aren't assigned to the same inaccessible set of regions - if world.shuffle[player] in ['crossed', 'insanity']: + if world.shuffle[player] in ['lite', 'crossed', 'insanity']: combined_must_exit_regions = list(must_exit_regions + otherworld_must_exit_regions) if len(combined_must_exit_regions) > 0: random.shuffle(combined_must_exit_regions) diff --git a/ItemList.py b/ItemList.py index ff6c2188..5d135ff9 100644 --- a/ItemList.py +++ b/ItemList.py @@ -709,7 +709,7 @@ def balance_prices(world, player): def check_hints(world, player): - if world.shuffle[player] in ['simple', 'restricted', 'full', 'crossed', 'insanity']: + if world.shuffle[player] in ['simple', 'restricted', 'full', 'lite', 'crossed', 'insanity']: for shop, location_list in shop_to_location_table.items(): if shop in ['Capacity Upgrade', 'Light World Death Mountain Shop', 'Potion Shop']: continue # near the queen, near potions, and near 7 chests are fine diff --git a/Rom.py b/Rom.py index 074ec8fc..c9e4ffa5 100644 --- a/Rom.py +++ b/Rom.py @@ -1601,7 +1601,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_bytes(0x02F539, [0xEA, 0xEA, 0xEA, 0xEA, 0xEA] if world.powder_patch_required[player] else [0xAD, 0xBF, 0x0A, 0xF0, 0x4F]) # allow smith into multi-entrance caves in appropriate shuffles - if world.shuffle[player] in ['restricted', 'full', 'crossed', 'insanity'] or (world.shuffle[player] == 'simple' and world.mode[player] == 'inverted'): + if world.shuffle[player] in ['restricted', 'full', 'lite', 'crossed', 'insanity'] or (world.shuffle[player] == 'simple' and world.mode[player] == 'inverted'): rom.write_byte(0x18004C, 0x01) # set correct flag for hera basement item diff --git a/Rules.py b/Rules.py index 3f606755..382194cb 100644 --- a/Rules.py +++ b/Rules.py @@ -844,7 +844,7 @@ def ow_rules(world, player): if world.mode[player] != 'inverted': set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has_beam_sword(player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle set_rule(world.get_entrance('GT Entry Approach', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player)) - set_rule(world.get_entrance('GT Entry Leave', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player) or state.world.shuffle[player] in ('restricted', 'full', 'crossed', 'insanity')) + set_rule(world.get_entrance('GT Entry Leave', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player) or state.world.shuffle[player] in ('restricted', 'full', 'lite', 'crossed', 'insanity')) else: set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player)) diff --git a/mystery_example.yml b/mystery_example.yml index f9e276a6..ce1e56ca 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -46,6 +46,7 @@ simple: 2 restricted: 2 full: 2 + lite: 2 crossed: 3 insanity: 1 shufflelinks: diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index edbb11f0..bdb8eb83 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -146,6 +146,7 @@ "simple", "restricted", "full", + "lite", "crossed", "insanity", "dungeonsfull", diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index c4ff9781..a8ca7e1a 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -173,14 +173,18 @@ ], "shuffle": [ "Select Entrance Shuffling Algorithm. (default: %(default)s)", - "Full: Mix cave and dungeon entrances freely while limiting", - " multi-entrance caves to one world.", "Simple: Shuffle Dungeon Entrances/Exits between each other", " and keep all 4-entrance dungeons confined to one", " location. All caves outside of death mountain are", " shuffled in pairs and matched by original type.", "Restricted: Use Dungeons shuffling from Simple but freely", " connect remaining entrances.", + "Full: Mix cave and dungeon entrances freely while limiting", + " multi-entrance caves to one world.", + "Lite: Entrances are put into separate pools based on their", + " vanilla location. Dungeons, dropdowns, connector caves,", + " and locations that have an item are shuffled from", + " individual pools. Non-item locations remain vanilla.", "Crossed: Mix cave and dungeon entrances freely while allowing", " caves to cross between worlds.", "Insanity: Decouple entrances and exits from each other and", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 8544efff..63ddc4ab 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -140,6 +140,7 @@ "randomizer.entrance.entranceshuffle.simple": "Simple", "randomizer.entrance.entranceshuffle.restricted": "Restricted", "randomizer.entrance.entranceshuffle.full": "Full", + "randomizer.entrance.entranceshuffle.lite": "Lite", "randomizer.entrance.entranceshuffle.crossed": "Crossed", "randomizer.entrance.entranceshuffle.insanity": "Insanity", "randomizer.entrance.entranceshuffle.restricted_legacy": "Restricted (Legacy)", diff --git a/resources/app/gui/randomize/entrando/widgets.json b/resources/app/gui/randomize/entrando/widgets.json index ffeeb976..0a392da3 100644 --- a/resources/app/gui/randomize/entrando/widgets.json +++ b/resources/app/gui/randomize/entrando/widgets.json @@ -10,6 +10,7 @@ "simple", "restricted", "full", + "lite", "crossed", "insanity", "dungeonsfull", From 2be7de56dfddfb3caca5ea0aa922308d87f6ed73 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sat, 16 Oct 2021 13:51:51 -0500 Subject: [PATCH 24/76] Fixed issue with Flute Shuffle menu cancel now working when no OWR is enabled --- Rom.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Rom.py b/Rom.py index c9e4ffa5..e1acf138 100644 --- a/Rom.py +++ b/Rom.py @@ -646,8 +646,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): # patch overworld edges inverted_buffer = [0] * 0x82 + owMode = 0 if world.owShuffle[player] != 'vanilla' or world.owCrossed[player] not in ['none', 'polar'] or world.owMixed[player]: - owMode = 0 if world.owShuffle[player] == 'parallel': owMode = 1 elif world.owShuffle[player] == 'full': @@ -660,10 +660,6 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): world.fix_fake_world[player] = True if world.owMixed[player]: owMode |= 0x400 - - write_int16(rom, 0x150002, owMode) - - write_int16(rom, 0x150004, owFlags) rom.write_byte(0x18004C, 0x01) # patch for allowing Frogsmith to enter multi-entrance caves @@ -692,6 +688,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): write_int16(rom, edge.getAddress() + 0x0a, edge.vramLoc) write_int16(rom, edge.getAddress() + 0x0e, edge.getTarget()) + write_int16(rom, 0x150002, owMode) + write_int16(rom, 0x150004, owFlags) # patch entrance/exits/holes for region in world.regions: From 46c1105e4df43e2c82e8adcdb1369af507dfa9ab Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sat, 16 Oct 2021 13:56:35 -0500 Subject: [PATCH 25/76] Added new Lite (fka. Beginner) ER mode --- EntranceShuffle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 36e4b394..80a5f32a 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -1224,7 +1224,7 @@ def full_shuffle_dungeons(world, Dungeon_Exits, player): hyrule_castle_exits = list([tuple(e for e in hyrule_castle_exits if e in exit_pool)]) hyrule_castle_exits.extend([e for e in dungeon_exits if isinstance(e, str)]) dungeon_exits = [e for e in dungeon_exits if not isinstance(e, str)] - if invFlag == (0x13 in world.owswaps[player][0] and world.owMixed[player]): + if world.shuffle[player] == 'lite' or invFlag == (0x13 in world.owswaps[player][0] and world.owMixed[player]): connect_mandatory_exits(world, lw_entrances, hyrule_castle_exits, lw_must_exit, player, False) dungeon_exits.extend([e for e in hyrule_castle_exits if isinstance(e, str)]) hyrule_castle_exits = [e for e in hyrule_castle_exits if not isinstance(e, str)] From 6b7146867ac5381fdf340814a0f122e52d18cd0e Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sat, 16 Oct 2021 13:59:52 -0500 Subject: [PATCH 26/76] Added new Lite (fka. Beginner) ER mode --- EntranceShuffle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 80a5f32a..b1e0f0a6 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -945,7 +945,7 @@ def scramble_holes(world, player): hole_targets.append(('Pyramid Exit', 'Pyramid')) # shuffle sanctuary hole in same world as other HC entrances - if world.shuffle[player] != 'crossed': + if world.shuffle[player] not in ['lite', 'crossed']: drop_owid_map = { # owid, is_light_world 'Lost Woods Hideout Stump': (0x00, True), 'Lumberjack Tree Cave': (0x02, True), From 9d2b578556585d3a4dcb985122577f6e795f2494 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sat, 16 Oct 2021 14:35:15 -0500 Subject: [PATCH 27/76] Removed HC bias when crossworld ER is used --- EntranceShuffle.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index b1e0f0a6..dd879401 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -1224,16 +1224,17 @@ def full_shuffle_dungeons(world, Dungeon_Exits, player): hyrule_castle_exits = list([tuple(e for e in hyrule_castle_exits if e in exit_pool)]) hyrule_castle_exits.extend([e for e in dungeon_exits if isinstance(e, str)]) dungeon_exits = [e for e in dungeon_exits if not isinstance(e, str)] - if world.shuffle[player] == 'lite' or invFlag == (0x13 in world.owswaps[player][0] and world.owMixed[player]): - connect_mandatory_exits(world, lw_entrances, hyrule_castle_exits, lw_must_exit, player, False) - dungeon_exits.extend([e for e in hyrule_castle_exits if isinstance(e, str)]) - hyrule_castle_exits = [e for e in hyrule_castle_exits if not isinstance(e, str)] - connect_caves(world, lw_entrances, [], hyrule_castle_exits, player) - else: - connect_mandatory_exits(world, dw_entrances, hyrule_castle_exits, dw_must_exit, player, False) - dungeon_exits.extend([e for e in hyrule_castle_exits if isinstance(e, str)]) - hyrule_castle_exits = [e for e in hyrule_castle_exits if not isinstance(e, str)] - connect_caves(world, [], dw_entrances, hyrule_castle_exits, player) + if world.shuffle[player] != 'lite': + if invFlag == (0x13 in world.owswaps[player][0] and world.owMixed[player]): + connect_mandatory_exits(world, lw_entrances, hyrule_castle_exits, lw_must_exit, player, False) + dungeon_exits.extend([e for e in hyrule_castle_exits if isinstance(e, str)]) + hyrule_castle_exits = [e for e in hyrule_castle_exits if not isinstance(e, str)] + connect_caves(world, lw_entrances, [], hyrule_castle_exits, player) + else: + connect_mandatory_exits(world, dw_entrances, hyrule_castle_exits, dw_must_exit, player, False) + dungeon_exits.extend([e for e in hyrule_castle_exits if isinstance(e, str)]) + hyrule_castle_exits = [e for e in hyrule_castle_exits if not isinstance(e, str)] + connect_caves(world, [], dw_entrances, hyrule_castle_exits, player) # connect any remaining must-exit entrances dungeon_exits.extend(hyrule_castle_exits) From 0a0bf48809ecc4b52a9282d2218238d1aee58a32 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sat, 16 Oct 2021 15:26:17 -0500 Subject: [PATCH 28/76] Including Big Bomb Shop in shuffler in Lite ER --- EntranceShuffle.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index dd879401..e92073a4 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -372,10 +372,6 @@ def link_entrances(world, player): for entrancename, exitname in default_connections + ([] if world.shopsanity[player] else default_shop_connections): connect_logical(world, entrancename, exitname, player, False) - # place bomb shop - bomb_shop = 'Big Bomb Shop' if invFlag == (0x2c in world.owswaps[player][0] and world.owMixed[player]) else 'Links House' - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - suppress_spoiler = False # place links house @@ -404,6 +400,14 @@ def link_entrances(world, player): # place blacksmith, has limited options place_blacksmith(world, links_house, player) + # place bomb shop, has limited options + bomb_shop_doors = list(entrance_pool) + if world.logic[player] in ['noglitches', 'minorglitches'] or (invFlag != (0x1b in world.owswaps[player][0] and world.owMixed[player])): + bomb_shop_doors = [e for e in entrance_pool if e not in ['Pyramid Fairy']] + random.shuffle(bomb_shop_doors) + bomb_shop = bomb_shop_doors.pop() + connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) + # place remaining doors connect_doors(world, list(entrance_pool), list(exit_pool), player) elif world.shuffle[player] == 'crossed': From d638ee8595e7b80dce878b254629f09c2e6bcd93 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sat, 16 Oct 2021 17:23:28 -0500 Subject: [PATCH 29/76] Fix Old Man exiting from Old Man House in ER --- EntranceShuffle.py | 1 + 1 file changed, 1 insertion(+) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index e92073a4..960f80dc 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -1356,6 +1356,7 @@ def place_old_man(world, pool, player): else: region_name = 'West Dark Death Mountain (Top)' old_man_entrances = list(build_accessible_entrance_list(world, region_name, player, [], False, True, True)) + old_man_entrances = [e for e in old_man_entrances if e != 'Old Man House (Bottom)'] if world.shuffle[player] == 'lite': old_man_entrances = [e for e in old_man_entrances if e in pool] random.shuffle(old_man_entrances) From f01b75595d0cbfff5699219de25b0fd153dfb133 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sun, 17 Oct 2021 12:20:31 -0500 Subject: [PATCH 30/76] Fix issue with subsequent seed generations placing invalid entrances --- EntranceShuffle.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 960f80dc..43e87cbc 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -13,7 +13,10 @@ suppress_spoiler = True def link_entrances(world, player): invFlag = world.mode[player] == 'inverted' - global entrance_pool, exit_pool, ignore_pool, suppress_spoiler + global entrance_pool, exit_pool, ignore_pool, suppress_spoiler, entrance_exits + entrance_exits = list() + ignore_pool = False + suppress_spoiler = True links_house = False entrance_pool = Entrance_Pool_Base.copy() exit_pool = Exit_Pool_Base.copy() From f04dc94a8c834c8240d127fa3845ffb4c0359c0f Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sun, 17 Oct 2021 15:01:59 -0500 Subject: [PATCH 31/76] Fixed issue in Inverted Lite ER with Dark Chapel placement --- EntranceShuffle.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 43e87cbc..a7bdaa72 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -374,6 +374,8 @@ def link_entrances(world, player): elif world.shuffle[player] == 'lite': for entrancename, exitname in default_connections + ([] if world.shopsanity[player] else default_shop_connections): connect_logical(world, entrancename, exitname, player, False) + if invFlag: + world.get_entrance('Dark Sanctuary Hint Exit', player).connect(world.get_entrance('Dark Sanctuary Hint', player).parent_region) suppress_spoiler = False From 0126e7ab484afa921a5416a3fe4c0dd8b4c385d6 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sun, 17 Oct 2021 19:42:13 -0500 Subject: [PATCH 32/76] Improving the multi-entrance connection fill routine --- EntranceShuffle.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index a7bdaa72..04716d03 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -769,6 +769,11 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player, must raise RuntimeError('Not enough multi-entrance caves left to connect unreachable regions!') if len(entrances) < len(must_be_exits) + 1: raise RuntimeError('Not enough entrances left to connect unreachable regions!') + if cave_surplus > len(must_be_exits): + for candidate in caves: + if not isinstance(candidate, str) and (candidate in used_caves or len(candidate) < len(entrances) - required_entrances - 1): + cave = candidate + break if len(must_be_exits) == 0: # if assigning last must exit for candidate in caves: if not isinstance(candidate, str) and (candidate in used_caves or len(candidate) <= len(entrances) - required_entrances - 1): From 42b185115ad501e32d4f6c8478255f0af409e677 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sun, 17 Oct 2021 20:30:00 -0500 Subject: [PATCH 33/76] In Lite ER place Old Man after connecting inaccessible regions --- EntranceShuffle.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 04716d03..1e9180e7 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -388,16 +388,16 @@ def link_entrances(world, player): # shuffle dropdowns scramble_holes(world, player) - # place old man, has limited options - connector_entrances = [e for e in list(zip(*default_connector_connections))[0] if e in entrance_pool] - place_old_man(world, list(connector_entrances), player) - caves = list(Cave_Exits + Cave_Three_Exits + Old_Man_House) # place connectors in inaccessible regions - connector_entrances = [e for e in connector_entrances if e in entrance_pool] + connector_entrances = [e for e in list(zip(*default_connector_connections))[0] if e in entrance_pool] connect_inaccessible_regions(world, connector_entrances, [], caves, player) + # place old man, has limited options + connector_entrances = [e for e in connector_entrances if e in entrance_pool] + place_old_man(world, list(connector_entrances), player) + # shuffle remaining connectors connector_entrances = [e for e in connector_entrances if e in entrance_pool] connect_caves(world, connector_entrances, [], caves, player) From 302633dc733022d91873fcc3a60e15548e3be4a3 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sun, 17 Oct 2021 20:32:17 -0500 Subject: [PATCH 34/76] Added new experimental Lite+ ER mode --- BaseClasses.py | 4 +- EntranceShuffle.py | 53 +++++++++++++++++-- ItemList.py | 2 +- Rom.py | 2 +- Rules.py | 2 +- resources/app/cli/args.json | 1 + resources/app/gui/lang/en.json | 1 + .../app/gui/randomize/entrando/widgets.json | 1 + 8 files changed, 57 insertions(+), 9 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index efe3adf6..ca89ba13 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -120,7 +120,7 @@ class World(object): set_player_attr('can_access_trock_front', None) set_player_attr('can_access_trock_big_chest', None) set_player_attr('can_access_trock_middle', None) - set_player_attr('fix_fake_world', logic[player] not in ['owglitches', 'nologic'] or shuffle[player] in ['lite', 'crossed', 'insanity', 'madness_legacy']) + set_player_attr('fix_fake_world', logic[player] not in ['owglitches', 'nologic'] or shuffle[player] in ['lite', 'liteplus', 'crossed', 'insanity', 'madness_legacy']) set_player_attr('mapshuffle', False) set_player_attr('compassshuffle', False) set_player_attr('keyshuffle', False) @@ -2950,7 +2950,7 @@ class Pot(object): # byte 0: DDOO OEEE (DR, OR, ER) dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0} or_mode = {"vanilla": 0, "parallel": 1, "full": 1} -er_mode = {"vanilla": 0, "simple": 1, "restricted": 2, "full": 3, "lite": 4, "crossed": 5, "insanity": 6, "dungeonsfull": 7, "dungeonssimple": 8} +er_mode = {"vanilla": 0, "simple": 1, "restricted": 2, "full": 3, "lite": 4, "liteplus": 5, "crossed": 6, "insanity": 7, "dungeonsfull": 8, "dungeonssimple": 9} # byte 1: LLLW WSSR (logic, mode, sword, retro) logic_mode = {"noglitches": 0, "minorglitches": 1, "nologic": 2, "owglitches": 3, "majorglitches": 4} diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 1e9180e7..f65c55fc 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -413,6 +413,51 @@ def link_entrances(world, player): bomb_shop = bomb_shop_doors.pop() connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) + # place remaining doors + connect_doors(world, list(entrance_pool), list(exit_pool), player) + elif world.shuffle[player] == 'liteplus': + for entrancename, exitname in default_connections + ([] if world.shopsanity[player] else default_shop_connections): + connect_logical(world, entrancename, exitname, player, False) + if invFlag: + world.get_entrance('Dark Sanctuary Hint Exit', player).connect(world.get_entrance('Dark Sanctuary Hint', player).parent_region) + + suppress_spoiler = False + + # place links house + links_house = place_links_house(world, sectors, player) + + # shuffle dungeons + #full_shuffle_dungeons(world, Dungeon_Exits, player) + skull_woods_shuffle(world, player) + + # shuffle dropdowns + scramble_holes(world, player) + + caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits + Old_Man_House) + + # place connectors in inaccessible regions + connector_entrances = [e for e in list(zip(*default_connector_connections))[0] + list(zip(*default_dungeon_connections))[0] if e in entrance_pool] + connect_inaccessible_regions(world, connector_entrances, [], caves, player) + + # place old man, has limited options + connector_entrances = [e for e in connector_entrances if e in entrance_pool] + place_old_man(world, list(connector_entrances), player) + + # shuffle remaining connectors + connector_entrances = [e for e in connector_entrances if e in entrance_pool] + connect_caves(world, connector_entrances, [], caves, player) + + # place blacksmith, has limited options + place_blacksmith(world, links_house, player) + + # place bomb shop, has limited options + bomb_shop_doors = list(entrance_pool) + if world.logic[player] in ['noglitches', 'minorglitches'] or (invFlag != (0x1b in world.owswaps[player][0] and world.owMixed[player])): + bomb_shop_doors = [e for e in entrance_pool if e not in ['Pyramid Fairy']] + random.shuffle(bomb_shop_doors) + bomb_shop = bomb_shop_doors.pop() + connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) + # place remaining doors connect_doors(world, list(entrance_pool), list(exit_pool), player) elif world.shuffle[player] == 'crossed': @@ -959,7 +1004,7 @@ def scramble_holes(world, player): hole_targets.append(('Pyramid Exit', 'Pyramid')) # shuffle sanctuary hole in same world as other HC entrances - if world.shuffle[player] not in ['lite', 'crossed']: + if world.shuffle[player] not in ['lite', 'liteplus', 'crossed']: drop_owid_map = { # owid, is_light_world 'Lost Woods Hideout Stump': (0x00, True), 'Lumberjack Tree Cave': (0x02, True), @@ -1291,7 +1336,7 @@ def place_links_house(world, sectors, player): return entrances links_house_doors = [i for i in get_link_candidates() if i in entrance_pool] - if world.shuffle[player] == 'lite': + if world.shuffle[player] in ['lite', 'liteplus']: links_house_doors = [e for e in links_house_doors if e in list(zip(*(default_item_connections + (default_shop_connections if world.shopsanity[player] else []))))[0]] links_house = random.choice(links_house_doors) connect_two_way(world, links_house, 'Links House Exit', player) @@ -1367,7 +1412,7 @@ def place_old_man(world, pool, player): region_name = 'West Dark Death Mountain (Top)' old_man_entrances = list(build_accessible_entrance_list(world, region_name, player, [], False, True, True)) old_man_entrances = [e for e in old_man_entrances if e != 'Old Man House (Bottom)'] - if world.shuffle[player] == 'lite': + if world.shuffle[player] in ['lite', 'liteplus']: old_man_entrances = [e for e in old_man_entrances if e in pool] random.shuffle(old_man_entrances) old_man_exit = None @@ -1465,7 +1510,7 @@ def connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, playe connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, player) # connect one connector at a time to ensure multiple connectors aren't assigned to the same inaccessible set of regions - if world.shuffle[player] in ['lite', 'crossed', 'insanity']: + if world.shuffle[player] in ['lite', 'liteplus', 'crossed', 'insanity']: combined_must_exit_regions = list(must_exit_regions + otherworld_must_exit_regions) if len(combined_must_exit_regions) > 0: random.shuffle(combined_must_exit_regions) diff --git a/ItemList.py b/ItemList.py index 5d135ff9..3bea81da 100644 --- a/ItemList.py +++ b/ItemList.py @@ -709,7 +709,7 @@ def balance_prices(world, player): def check_hints(world, player): - if world.shuffle[player] in ['simple', 'restricted', 'full', 'lite', 'crossed', 'insanity']: + if world.shuffle[player] in ['simple', 'restricted', 'full', 'lite', 'liteplus', 'crossed', 'insanity']: for shop, location_list in shop_to_location_table.items(): if shop in ['Capacity Upgrade', 'Light World Death Mountain Shop', 'Potion Shop']: continue # near the queen, near potions, and near 7 chests are fine diff --git a/Rom.py b/Rom.py index e1acf138..98a1913a 100644 --- a/Rom.py +++ b/Rom.py @@ -1599,7 +1599,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_bytes(0x02F539, [0xEA, 0xEA, 0xEA, 0xEA, 0xEA] if world.powder_patch_required[player] else [0xAD, 0xBF, 0x0A, 0xF0, 0x4F]) # allow smith into multi-entrance caves in appropriate shuffles - if world.shuffle[player] in ['restricted', 'full', 'lite', 'crossed', 'insanity'] or (world.shuffle[player] == 'simple' and world.mode[player] == 'inverted'): + if world.shuffle[player] in ['restricted', 'full', 'lite', 'liteplus', 'crossed', 'insanity'] or (world.shuffle[player] == 'simple' and world.mode[player] == 'inverted'): rom.write_byte(0x18004C, 0x01) # set correct flag for hera basement item diff --git a/Rules.py b/Rules.py index 382194cb..f9765866 100644 --- a/Rules.py +++ b/Rules.py @@ -844,7 +844,7 @@ def ow_rules(world, player): if world.mode[player] != 'inverted': set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has_beam_sword(player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle set_rule(world.get_entrance('GT Entry Approach', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player)) - set_rule(world.get_entrance('GT Entry Leave', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player) or state.world.shuffle[player] in ('restricted', 'full', 'lite', 'crossed', 'insanity')) + set_rule(world.get_entrance('GT Entry Leave', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player) or state.world.shuffle[player] in ('restricted', 'full', 'lite', 'liteplus', 'crossed', 'insanity')) else: set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player)) diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index bdb8eb83..d9a9f294 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -147,6 +147,7 @@ "restricted", "full", "lite", + "liteplus", "crossed", "insanity", "dungeonsfull", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 63ddc4ab..c76700b3 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -141,6 +141,7 @@ "randomizer.entrance.entranceshuffle.restricted": "Restricted", "randomizer.entrance.entranceshuffle.full": "Full", "randomizer.entrance.entranceshuffle.lite": "Lite", + "randomizer.entrance.entranceshuffle.liteplus": "Lite+", "randomizer.entrance.entranceshuffle.crossed": "Crossed", "randomizer.entrance.entranceshuffle.insanity": "Insanity", "randomizer.entrance.entranceshuffle.restricted_legacy": "Restricted (Legacy)", diff --git a/resources/app/gui/randomize/entrando/widgets.json b/resources/app/gui/randomize/entrando/widgets.json index 0a392da3..ff189cb4 100644 --- a/resources/app/gui/randomize/entrando/widgets.json +++ b/resources/app/gui/randomize/entrando/widgets.json @@ -11,6 +11,7 @@ "restricted", "full", "lite", + "liteplus", "crossed", "insanity", "dungeonsfull", From f8ce8ae1e27869815c4e38707b00b8e4581270de Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sun, 17 Oct 2021 21:36:46 -0500 Subject: [PATCH 35/76] Fixed incorrect cross-pooling for Lite/Lite+ ER --- EntranceShuffle.py | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index f65c55fc..f725eb79 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -423,9 +423,6 @@ def link_entrances(world, player): suppress_spoiler = False - # place links house - links_house = place_links_house(world, sectors, player) - # shuffle dungeons #full_shuffle_dungeons(world, Dungeon_Exits, player) skull_woods_shuffle(world, player) @@ -433,10 +430,27 @@ def link_entrances(world, player): # shuffle dropdowns scramble_holes(world, player) + if world.mode[player] == 'standard': + connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) + Dungeon_Exits.append(tuple(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'))) + else: + Dungeon_Exits.append(tuple(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)', 'Hyrule Castle Exit (South)'))) + + if not world.shuffle_ganon: + connect_two_way(world, 'Ganons Tower' if not invFlag else 'Agahnims Tower', 'Ganons Tower Exit', player) + else: + Dungeon_Exits.append('Ganons Tower Exit') + caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits + Old_Man_House) + # place links house + links_house = place_links_house(world, sectors, player) + + # place blacksmith, has limited options + place_blacksmith(world, links_house, player) + # place connectors in inaccessible regions - connector_entrances = [e for e in list(zip(*default_connector_connections))[0] + list(zip(*default_dungeon_connections))[0] if e in entrance_pool] + connector_entrances = [e for e in list(zip(*default_connector_connections + default_dungeon_connections + open_default_dungeon_connections))[0] if e in entrance_pool] connect_inaccessible_regions(world, connector_entrances, [], caves, player) # place old man, has limited options @@ -447,9 +461,6 @@ def link_entrances(world, player): connector_entrances = [e for e in connector_entrances if e in entrance_pool] connect_caves(world, connector_entrances, [], caves, player) - # place blacksmith, has limited options - place_blacksmith(world, links_house, player) - # place bomb shop, has limited options bomb_shop_doors = list(entrance_pool) if world.logic[player] in ['noglitches', 'minorglitches'] or (invFlag != (0x1b in world.owswaps[player][0] and world.owMixed[player])): @@ -1374,6 +1385,8 @@ def place_dark_sanc(world, links_house, sectors, player): sanc_doors.append(exit.name) sanc_doors = [i for i in sanc_doors if i in entrance_pool] + if world.shuffle[player] in ['lite', 'liteplus']: + sanc_doors = [e for e in sanc_doors if e in list(zip(*(default_item_connections + (default_shop_connections if world.shopsanity[player] else []))))[0]] sanc_door = random.choice(sanc_doors) connect_entrance(world, sanc_door, 'Dark Sanctuary Hint', player) world.get_entrance('Dark Sanctuary Hint Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) @@ -1421,7 +1434,7 @@ def place_old_man(world, pool, player): if 'West Death Mountain (Bottom)' not in build_accessible_region_list(world, world.get_entrance(old_man_exit, player).parent_region.name, player, True): old_man_exit = None - old_man_entrances = [e for e in pool if e in entrance_pool and e not in entrance_exits] + old_man_entrances = [e for e in pool if e in entrance_pool and e not in entrance_exits + [old_man_exit]] random.shuffle(old_man_entrances) old_man_entrance = old_man_entrances.pop() if world.shuffle[player] != 'insanity': @@ -1503,11 +1516,12 @@ def connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, playe inaccessible_entrances = list() region = world.get_region(region_name, player) for exit in region.exits: - if not exit.connected_region and exit.name in entrance_pool: + if not exit.connected_region and exit.name in entrance_pool and (world.shuffle[player] not in ['lite', 'liteplus'] or exit.name in pool): inaccessible_entrances.append(exit.name) - random.shuffle(inaccessible_entrances) - connect_mandatory_exits(world, pool, caves, [inaccessible_entrances.pop()], player) - connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, player) + if len(inaccessible_entrances): + random.shuffle(inaccessible_entrances) + connect_mandatory_exits(world, pool, caves, [inaccessible_entrances.pop()], player) + connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, player) # connect one connector at a time to ensure multiple connectors aren't assigned to the same inaccessible set of regions if world.shuffle[player] in ['lite', 'liteplus', 'crossed', 'insanity']: From f1d511b26e7cfb12ade666b6aeac49bbdb8ff2ae Mon Sep 17 00:00:00 2001 From: codemann8 Date: Mon, 18 Oct 2021 20:40:31 -0500 Subject: [PATCH 36/76] Fixed issue with Blacksmith using wrong pool in Lite ER --- EntranceShuffle.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index f725eb79..4aae27b6 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -1410,6 +1410,8 @@ def place_blacksmith(world, links_house, player): elif world.doorShuffle[player] == 'vanilla' or world.intensity[player] < 3: sanc_region = world.get_entrance('Sanctuary Exit', player).connected_region.name blacksmith_doors = list(set(blacksmith_doors + list(build_accessible_entrance_list(world, sanc_region, player, assumed_inventory, False, True, True)))) + if world.shuffle[player] in ['lite', 'liteplus']: + blacksmith_doors = [e for e in blacksmith_doors if e in list(zip(*(default_item_connections + (default_shop_connections if world.shopsanity[player] else []))))[0]] random.shuffle(blacksmith_doors) blacksmith_hut = blacksmith_doors.pop() From 87f2e84b03c3fa60c2766da0a52c0c08eb43793b Mon Sep 17 00:00:00 2001 From: codemann8 Date: Mon, 18 Oct 2021 20:42:57 -0500 Subject: [PATCH 37/76] Placing Dark Chapel before Links in ER --- EntranceShuffle.py | 170 +++++++++++++++++++++++++-------------------- 1 file changed, 94 insertions(+), 76 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 4aae27b6..4ac75bf8 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -237,12 +237,12 @@ def link_entrances(world, player): connect_caves(world, lw_wdm_entrances, [], caves[0:c], player) connect_caves(world, lw_edm_entrances, [], caves[c:], player) - # place links house - links_house = place_links_house(world, sectors, player) - if invFlag: # place dark sanc - place_dark_sanc(world, links_house, sectors, player) + place_dark_sanc(world, sectors, player) + + # place links house + links_house = place_links_house(world, sectors, player) # place blacksmith, has limited options place_blacksmith(world, links_house, player) @@ -268,12 +268,12 @@ def link_entrances(world, player): # shuffle holes scramble_holes(world, player) - # place links house - links_house = place_links_house(world, sectors, player) - # place dark sanc if invFlag: - place_dark_sanc(world, links_house, sectors, player) + place_dark_sanc(world, sectors, player) + + # place links house + links_house = place_links_house(world, sectors, player) # place blacksmith, has limited options place_blacksmith(world, links_house, player) @@ -330,12 +330,12 @@ def link_entrances(world, player): # shuffle holes scramble_holes(world, player) - # place links house - links_house = place_links_house(world, sectors, player) - # place dark sanc if invFlag: - place_dark_sanc(world, links_house, sectors, player) + place_dark_sanc(world, sectors, player) + + # place links house + links_house = place_links_house(world, sectors, player) # place blacksmith, has limited options place_blacksmith(world, links_house, player) @@ -491,12 +491,12 @@ def link_entrances(world, player): # shuffle holes scramble_holes(world, player) - # place links house - links_house = place_links_house(world, sectors, player) - # place dark sanc if invFlag: - place_dark_sanc(world, links_house, sectors, player) + place_dark_sanc(world, sectors, player) + + # place links house + links_house = place_links_house(world, sectors, player) # place blacksmith, has limited options place_blacksmith(world, links_house, player) @@ -561,12 +561,12 @@ def link_entrances(world, player): for hole in hole_entrances: connect_entrance(world, hole, hole_targets.pop(), player) - # place links house - links_house = place_links_house(world, sectors, player) - # place dark sanc if invFlag: - place_dark_sanc(world, links_house, sectors, player) + place_dark_sanc(world, sectors, player) + + # place links house + links_house = place_links_house(world, sectors, player) # place blacksmith, place sanc exit first for additional blacksmith candidates doors = list(entrance_pool) @@ -1322,31 +1322,15 @@ def place_links_house(world, sectors, player): if world.mode[player] == 'standard' or not world.shufflelinks[player]: links_house = 'Links House' if not invFlag else 'Big Bomb Shop' else: - def get_link_candidates(): - # find largest walkable sector - sector = None - invalid_sectors = list() - while (sector is None): - sector = max(sectors, key=lambda x: len(x) - (0 if x not in invalid_sectors else 1000)) - if not ((world.owCrossed[player] == 'polar' and world.owMixed[player]) or world.owCrossed[player] not in ['none', 'polar']) \ - and world.get_region(next(iter(next(iter(sector)))), player).type != (RegionType.LightWorld if not invFlag else RegionType.DarkWorld): - invalid_sectors.append(sector) - sector = None - regions = max(sector, key=lambda x: len(x)) - - # get entrances from list of regions - entrances = list() - for region_name in [r for r in regions if r ]: - if world.shuffle[player] == 'simple' and region_name in OWTileRegions and OWTileRegions[region_name] in [0x03, 0x05, 0x07]: - continue - region = world.get_region(region_name, player) - if region.type == RegionType.LightWorld if not invFlag else RegionType.DarkWorld: - for exit in region.exits: - if not exit.connected_region and exit.spot_type == 'Entrance': - entrances.append(exit.name) - return entrances - - links_house_doors = [i for i in get_link_candidates() if i in entrance_pool] + if invFlag: + for dark_sanc in world.get_entrance('Dark Sanctuary Hint Exit', player).connected_region.exits: + if dark_sanc.connected_region and dark_sanc.connected_region.name == 'Dark Sanctuary Hint': + dark_sanc = dark_sanc.name + break + if invFlag and isinstance(dark_sanc, str): + links_house_doors = [i for i in get_distant_entrances(world, dark_sanc, sectors, player) if i in entrance_pool] + else: + links_house_doors = [i for i in get_starting_entrances(world, sectors, player) if i in entrance_pool] if world.shuffle[player] in ['lite', 'liteplus']: links_house_doors = [e for e in links_house_doors if e in list(zip(*(default_item_connections + (default_shop_connections if world.shopsanity[player] else []))))[0]] links_house = random.choice(links_house_doors) @@ -1354,37 +1338,11 @@ def place_links_house(world, sectors, player): return links_house -def place_dark_sanc(world, links_house, sectors, player): - # get walkable sector in which links house was placed - links_region = world.get_entrance(links_house, player).parent_region.name - regions = next(s for s in sectors if any(links_region in w for w in s)) - regions = next(w for w in regions if links_region in w) - - # eliminate regions surrounding links until less than half of the candidate regions remain - explored_regions = list({links_region}) - was_progress = True - while was_progress and len(explored_regions) < len(regions) / 2: - was_progress = False - new_regions = list() - for region_name in explored_regions: - region = world.get_region(region_name, player) - for exit in region.exits: - if exit.connected_region and region.type == exit.connected_region.type and exit.connected_region.name in regions and exit.connected_region.name not in explored_regions + new_regions: - new_regions.append(exit.connected_region.name) - was_progress = True - explored_regions.extend(new_regions) - - # get entrances from remaining regions - sanc_doors = list() - for region_name in [r for r in regions if r not in explored_regions]: - if OWTileRegions[region_name] in [0x03, 0x05, 0x07]: - continue - region = world.get_region(region_name, player) - for exit in region.exits: - if not exit.connected_region and exit.spot_type == 'Entrance': - sanc_doors.append(exit.name) - - sanc_doors = [i for i in sanc_doors if i in entrance_pool] +def place_dark_sanc(world, sectors, player): + if not world.shufflelinks[player]: + sanc_doors = [i for i in get_distant_entrances(world, 'Big Bomb Shop', sectors, player) if i in entrance_pool] + else: + sanc_doors = [i for i in get_starting_entrances(world, sectors, player) if i in entrance_pool] if world.shuffle[player] in ['lite', 'liteplus']: sanc_doors = [e for e in sanc_doors if e in list(zip(*(default_item_connections + (default_shop_connections if world.shopsanity[player] else []))))[0]] sanc_door = random.choice(sanc_doors) @@ -1690,6 +1648,66 @@ def build_accessible_entrance_list(world, start_region, player, assumed_inventor return entrances +def get_starting_entrances(world, sectors, player): + invFlag = world.mode[player] == 'inverted' + + # find largest walkable sector + sector = None + invalid_sectors = list() + while (sector is None): + sector = max(sectors, key=lambda x: len(x) - (0 if x not in invalid_sectors else 1000)) + if not ((world.owCrossed[player] == 'polar' and world.owMixed[player]) or world.owCrossed[player] not in ['none', 'polar']) \ + and world.get_region(next(iter(next(iter(sector)))), player).type != (RegionType.LightWorld if not invFlag else RegionType.DarkWorld): + invalid_sectors.append(sector) + sector = None + regions = max(sector, key=lambda x: len(x)) + + # get entrances from list of regions + entrances = list() + for region_name in [r for r in regions if r ]: + if world.shuffle[player] == 'simple' and region_name in OWTileRegions and OWTileRegions[region_name] in [0x03, 0x05, 0x07]: + continue + region = world.get_region(region_name, player) + if region.type == RegionType.LightWorld if not invFlag else RegionType.DarkWorld: + for exit in region.exits: + if not exit.connected_region and exit.spot_type == 'Entrance': + entrances.append(exit.name) + return entrances + + +def get_distant_entrances(world, start_entrance, sectors, player): + # get walkable sector in which initial entrance was placed + start_region = world.get_entrance(start_entrance, player).parent_region.name + regions = next(s for s in sectors if any(start_region in w for w in s)) + regions = next(w for w in regions if start_region in w) + + # eliminate regions surrounding the initial entrance until less than half of the candidate regions remain + explored_regions = list({start_region}) + was_progress = True + while was_progress and len(explored_regions) < len(regions) / 2: + was_progress = False + new_regions = list() + for region_name in explored_regions: + region = world.get_region(region_name, player) + for exit in region.exits: + if exit.connected_region and region.type == exit.connected_region.type and exit.connected_region.name in regions and exit.connected_region.name not in explored_regions + new_regions: + new_regions.append(exit.connected_region.name) + was_progress = True + explored_regions.extend(new_regions) + + # get entrances from remaining regions + candidates = list() + for region_name in [r for r in regions if r not in explored_regions]: + if OWTileRegions[region_name] in [0x03, 0x05, 0x07]: + continue + region = world.get_region(region_name, player) + for exit in region.exits: + if not exit.connected_region and exit.spot_type == 'Entrance': + candidates.append(exit.name) + + return candidates + + def can_reach(world, entrance_name, region_name, player): from Main import copy_world from Items import ItemFactory From c266abb456e932efb5c2bd919cfadb4347107c9b Mon Sep 17 00:00:00 2001 From: codemann8 Date: Mon, 18 Oct 2021 21:47:28 -0500 Subject: [PATCH 38/76] Rearranged spoiler log output --- BaseClasses.py | 78 ++++++++++++++++++++++++++++---------------------- 1 file changed, 44 insertions(+), 34 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index ca89ba13..d5316624 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -2689,6 +2689,8 @@ class Spoiler(object): 'ow_mixed': self.world.owMixed, 'ow_fluteshuffle': self.world.owFluteShuffle, 'shuffle': self.world.shuffle, + 'shuffleganon': self.world.shuffle_ganon, + 'shufflelinks': self.world.shufflelinks, 'door_shuffle': self.world.doorShuffle, 'intensity': self.world.intensity, 'item_pool': self.world.difficulty, @@ -2764,8 +2766,13 @@ class Spoiler(object): if self.metadata['goal'][player] == 'triforcehunt': outfile.write('Triforce Pieces Required:'.ljust(line_width) + '%s\n' % self.metadata['triforcegoal'][player]) outfile.write('Triforce Pieces Total:'.ljust(line_width) + '%s\n' % self.metadata['triforcepool'][player]) + outfile.write('Crystals Required for GT:'.ljust(line_width) + '%s\n' % str(self.world.crystals_gt_orig[player])) + outfile.write('Crystals Required for Ganon:'.ljust(line_width) + '%s\n' % str(self.world.crystals_ganon_orig[player])) + outfile.write('Accessibility:'.ljust(line_width) + '%s\n' % self.metadata['accessibility'][player]) outfile.write('Difficulty:'.ljust(line_width) + '%s\n' % self.metadata['item_pool'][player]) outfile.write('Item Functionality:'.ljust(line_width) + '%s\n' % self.metadata['item_functionality'][player]) + outfile.write('Shopsanity:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['shopsanity'][player] else 'No')) + outfile.write('Bombbag:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['bombbag'][player] else 'No')) outfile.write('Overworld Layout Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_shuffle'][player]) if self.metadata['ow_shuffle'][player] != 'vanilla': outfile.write('Keep Similar OW Edges Together:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['ow_keepsimilar'][player] else 'No')) @@ -2773,28 +2780,45 @@ class Spoiler(object): outfile.write('Swapped OW (Mixed):'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['ow_mixed'][player] else 'No')) outfile.write('Flute Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_fluteshuffle'][player]) outfile.write('Entrance Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['shuffle'][player]) + outfile.write('Shuffle GT/Ganon:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['shuffleganon'][player] else 'No')) + outfile.write('Shuffle Links:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['shufflelinks'][player] else 'No')) + outfile.write('Pyramid Hole Pre-opened:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['open_pyramid'][player] else 'No')) outfile.write('Door Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['door_shuffle'][player]) outfile.write('Intensity:'.ljust(line_width) + '%s\n' % self.metadata['intensity'][player]) - addition = ' (Random)' if self.world.crystals_gt_orig[player] == 'random' else '' - outfile.write('Crystals required for GT:'.ljust(line_width) + '%s\n' % (str(self.metadata['gt_crystals'][player]) + addition)) - addition = ' (Random)' if self.world.crystals_ganon_orig[player] == 'random' else '' - outfile.write('Crystals required for Ganon:'.ljust(line_width) + '%s\n' % (str(self.metadata['ganon_crystals'][player]) + addition)) - outfile.write('Pyramid hole pre-opened:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['open_pyramid'][player] else 'No')) - outfile.write('Accessibility:'.ljust(line_width) + '%s\n' % self.metadata['accessibility'][player]) - outfile.write('Map shuffle:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['mapshuffle'][player] else 'No')) - outfile.write('Compass shuffle:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['compassshuffle'][player] else 'No')) - outfile.write('Small Key shuffle:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['keyshuffle'][player] else 'No')) - outfile.write('Big Key shuffle:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['bigkeyshuffle'][player] else 'No')) - outfile.write('Boss shuffle:'.ljust(line_width) + '%s\n' % self.metadata['boss_shuffle'][player]) - outfile.write('Enemy shuffle:'.ljust(line_width) + '%s\n' % self.metadata['enemy_shuffle'][player]) - outfile.write('Enemy health:'.ljust(line_width) + '%s\n' % self.metadata['enemy_health'][player]) - outfile.write('Enemy damage:'.ljust(line_width) + '%s\n' % self.metadata['enemy_damage'][player]) - outfile.write('Pot shuffle:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['potshuffle'][player] else 'No')) - outfile.write('Hints:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['hints'][player] else 'No')) outfile.write('Experimental:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['experimental'][player] else 'No')) - outfile.write('Key Drops shuffled:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['keydropshuffle'][player] else 'No')) - outfile.write('Shopsanity:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['shopsanity'][player] else 'No')) - outfile.write('Bombbag:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['bombbag'][player] else 'No')) + outfile.write('Pot Shuffle:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['potshuffle'][player] else 'No')) + outfile.write('Key Drop Shuffle:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['keydropshuffle'][player] else 'No')) + outfile.write('Map Shuffle:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['mapshuffle'][player] else 'No')) + outfile.write('Compass Shuffle:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['compassshuffle'][player] else 'No')) + outfile.write('Small Key Shuffle:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['keyshuffle'][player] else 'No')) + outfile.write('Big Key Shuffle:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['bigkeyshuffle'][player] else 'No')) + outfile.write('Boss Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['boss_shuffle'][player]) + outfile.write('Enemy Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['enemy_shuffle'][player]) + outfile.write('Enemy Health:'.ljust(line_width) + '%s\n' % self.metadata['enemy_health'][player]) + outfile.write('Enemy Damage:'.ljust(line_width) + '%s\n' % self.metadata['enemy_damage'][player]) + outfile.write('Hints:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['hints'][player] else 'No')) + + if self.startinventory: + outfile.write('Starting Inventory:'.ljust(line_width)) + outfile.write('\n'.ljust(line_width+1).join(self.startinventory)) + outfile.write('\n\nRequirements:\n\n') + for dungeon, medallion in self.medallions.items(): + outfile.write(f'{dungeon}:'.ljust(line_width) + '%s Medallion\n' % medallion) + if self.world.crystals_gt_orig[player] == 'random': + outfile.write('Crystals Required for GT:'.ljust(line_width) + '%s\n' % (str(self.metadata['gt_crystals'][player]))) + if self.world.crystals_ganon_orig[player] == 'random': + outfile.write('Crystals Required for Ganon:'.ljust(line_width) + '%s\n' % (str(self.metadata['ganon_crystals'][player]))) + + if self.overworlds: + # overworlds: overworld transitions; + outfile.write('\n\nOverworld:\n\n') + outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: ' if self.world.players > 1 else '', self.world.fish.translate("meta","overworlds",entry['entrance']), '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', self.world.fish.translate("meta","overworlds",entry['exit'])) for entry in self.overworlds.values()])) + + if self.entrances: + # entrances: To/From overworld; Checking w/ & w/out "Exit" and translating accordingly + outfile.write('\n\nEntrances:\n\n') + outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: ' if self.world.players > 1 else '', self.world.fish.translate("meta","entrances",entry['entrance']), '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', self.world.fish.translate("meta","entrances",entry['exit'])) for entry in self.entrances.values()])) + if self.doors: outfile.write('\n\nDoors:\n\n') outfile.write('\n'.join( @@ -2815,21 +2839,7 @@ class Spoiler(object): # doorTypes: Small Key, Bombable, Bonkable outfile.write('\n\nDoor Types:\n\n') outfile.write('\n'.join(['%s%s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '', self.world.fish.translate("meta","doors",entry['doorNames']), self.world.fish.translate("meta","doorTypes",entry['type'])) for entry in self.doorTypes.values()])) - if self.entrances: - # entrances: To/From overworld; Checking w/ & w/out "Exit" and translating accordingly - outfile.write('\n\nEntrances:\n\n') - outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: ' if self.world.players > 1 else '', self.world.fish.translate("meta","entrances",entry['entrance']), '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', self.world.fish.translate("meta","entrances",entry['exit'])) for entry in self.entrances.values()])) - if self.overworlds: - # overworlds: overworld transitions; - outfile.write('\n\nOverworld:\n\n') - outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: ' if self.world.players > 1 else '', self.world.fish.translate("meta","overworlds",entry['entrance']), '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', self.world.fish.translate("meta","overworlds",entry['exit'])) for entry in self.overworlds.values()])) - outfile.write('\n\nMedallions:\n') - for dungeon, medallion in self.medallions.items(): - outfile.write(f'\n{dungeon}: {medallion} Medallion') - if self.startinventory: - outfile.write('\n\nStarting Inventory:\n\n') - outfile.write('\n'.join(self.startinventory)) - + # locations: Change up location names; in the instance of a location with multiple sections, it'll try to translate the room name # items: Item names outfile.write('\n\nLocations:\n\n') From d6906576a010f0eadd0ead3b3c4c6489d558bb85 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 19 Oct 2021 16:14:20 -0500 Subject: [PATCH 39/76] Fixed issue in Inverted ER where Links or Chapel end up too close to each other --- EntranceShuffle.py | 51 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 4ac75bf8..8eb59af5 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -424,7 +424,6 @@ def link_entrances(world, player): suppress_spoiler = False # shuffle dungeons - #full_shuffle_dungeons(world, Dungeon_Exits, player) skull_woods_shuffle(world, player) # shuffle dropdowns @@ -1680,6 +1679,51 @@ def get_distant_entrances(world, start_entrance, sectors, player): start_region = world.get_entrance(start_entrance, player).parent_region.name regions = next(s for s in sectors if any(start_region in w for w in s)) regions = next(w for w in regions if start_region in w) + + one_way_ledges = { + 'West Death Mountain (Bottom)': {'West Death Mountain (Top)', + 'Spectacle Rock Ledge'}, + 'East Death Mountain (Bottom)': {'West Death Mountain (Top East)', + 'Spiral Cave Ledge'}, + 'Fairy Ascension Plateau': {'Fairy Ascension Ledge'}, + 'Mountain Entry Area': {'Mountain Entry Ledge'}, + 'Sanctuary Area': {'Bonk Rock Ledge'}, + 'Graveyard Area': {'Graveyard Ledge'}, + 'Potion Shop Water': {'Potion Shop Area', + 'Potion Shop Northeast'}, + 'Zora Approach Water': {'Zora Approach Area'}, + 'Hyrule Castle Area': {'Hyrule Castle Ledge'}, + 'Wooden Bridge Water': {'Wooden Bridge Area', + 'Wooden Bridge Northeast'}, + 'Maze Race Area': {'Maze Race Ledge', + 'Maze Race Prize'}, + 'Flute Boy Approach Area': {'Cave 45 Ledge'}, + 'Desert Area': {'Desert Ledge' + 'Desert Checkerboard Ledge', + 'Desert Palace Mouth', + 'Bombos Tablet Ledge', + 'Desert Palace Teleporter Ledge'}, + 'Desert Pass Area': {'Desert Pass Ledge'}, + 'Lake Hylia Water': {'Lake Hylia South Shore', + 'Lake Hylia Island'}, + 'West Dark Death Mountain (Bottom)': {'West Dark Death Mountain (Top)'}, + 'West Dark Death Mountain (Top)': {'Dark Death Mountain Floating Island'}, + 'East Dark Death Mountain (Bottom)': {'East Dark Death Mountain (Top)'}, + 'Turtle Rock Area': {'Turtle Rock Ledge'}, + 'Bumper Cave Area': {'Bumper Cave Ledge'}, + 'Qirn Jump Water': {'Qirn Jump Area'}, + 'Dark Witch Water': {'Dark Witch Area', + 'Dark Witch Northeast'}, + 'Catfish Approach Water': {'Catfish Approach Area'}, + 'Pyramid Area': {'Pyramid Exit Ledge'}, + 'Broken Bridge Water': {'Broken Bridge West', + 'Broken Bridge Area', + 'Broken Bridge Northeast'}, + 'Misery Mire Area': {'Misery Mire Teleporter Ledge'}, + 'Ice Lake Water': {'Ice Lake Area', + 'Ice Lake Ledge (West)', + 'Ice Lake Ledge (East)'} + } # eliminate regions surrounding the initial entrance until less than half of the candidate regions remain explored_regions = list({start_region}) @@ -1688,6 +1732,11 @@ def get_distant_entrances(world, start_entrance, sectors, player): was_progress = False new_regions = list() for region_name in explored_regions: + if region_name in one_way_ledges: + for ledge in one_way_ledges[region_name]: + if ledge not in explored_regions + new_regions: + new_regions.append(ledge) + was_progress = True region = world.get_region(region_name, player) for exit in region.exits: if exit.connected_region and region.type == exit.connected_region.type and exit.connected_region.name in regions and exit.connected_region.name not in explored_regions + new_regions: From 10b8b1d6669140a445155208037db63a29ca32e7 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 19 Oct 2021 17:02:08 -0500 Subject: [PATCH 40/76] Allowing for more areas where Old Man can exit in ER --- EntranceShuffle.py | 107 +++++++++++++++++++++++++-------------------- 1 file changed, 59 insertions(+), 48 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 8eb59af5..105e7722 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -1382,7 +1382,7 @@ def place_old_man(world, pool, player): region_name = 'West Death Mountain (Top)' else: region_name = 'West Dark Death Mountain (Top)' - old_man_entrances = list(build_accessible_entrance_list(world, region_name, player, [], False, True, True)) + old_man_entrances = list(build_accessible_entrance_list(world, region_name, player, [], False, True, True, True)) old_man_entrances = [e for e in old_man_entrances if e != 'Old Man House (Bottom)'] if world.shuffle[player] in ['lite', 'liteplus']: old_man_entrances = [e for e in old_man_entrances if e in pool] @@ -1618,7 +1618,7 @@ def build_accessible_region_list(world, start_region, player, cross_world=False, return explored_regions -def build_accessible_entrance_list(world, start_region, player, assumed_inventory=[], cross_world=False, region_rules=True, exit_rules=True): +def build_accessible_entrance_list(world, start_region, player, assumed_inventory=[], cross_world=False, region_rules=True, exit_rules=True, include_one_ways=False): from Main import copy_world from Items import ItemFactory @@ -1635,8 +1635,17 @@ def build_accessible_entrance_list(world, start_region, player, assumed_inventor for item in assumed_inventory: blank_state.collect(ItemFactory(item, player), True) - explored_regions = build_accessible_region_list(base_world, start_region, player, cross_world, region_rules, False) + explored_regions = list(build_accessible_region_list(base_world, start_region, player, cross_world, region_rules, False)) + if include_one_ways: + new_regions = list() + for region_name in explored_regions: + if region_name in one_way_ledges: + for ledge in one_way_ledges[region_name]: + if ledge not in explored_regions + new_regions: + new_regions.append(ledge) + explored_regions.extend(new_regions) + entrances = set() for region_name in explored_regions: region = base_world.get_region(region_name, player) @@ -1679,51 +1688,6 @@ def get_distant_entrances(world, start_entrance, sectors, player): start_region = world.get_entrance(start_entrance, player).parent_region.name regions = next(s for s in sectors if any(start_region in w for w in s)) regions = next(w for w in regions if start_region in w) - - one_way_ledges = { - 'West Death Mountain (Bottom)': {'West Death Mountain (Top)', - 'Spectacle Rock Ledge'}, - 'East Death Mountain (Bottom)': {'West Death Mountain (Top East)', - 'Spiral Cave Ledge'}, - 'Fairy Ascension Plateau': {'Fairy Ascension Ledge'}, - 'Mountain Entry Area': {'Mountain Entry Ledge'}, - 'Sanctuary Area': {'Bonk Rock Ledge'}, - 'Graveyard Area': {'Graveyard Ledge'}, - 'Potion Shop Water': {'Potion Shop Area', - 'Potion Shop Northeast'}, - 'Zora Approach Water': {'Zora Approach Area'}, - 'Hyrule Castle Area': {'Hyrule Castle Ledge'}, - 'Wooden Bridge Water': {'Wooden Bridge Area', - 'Wooden Bridge Northeast'}, - 'Maze Race Area': {'Maze Race Ledge', - 'Maze Race Prize'}, - 'Flute Boy Approach Area': {'Cave 45 Ledge'}, - 'Desert Area': {'Desert Ledge' - 'Desert Checkerboard Ledge', - 'Desert Palace Mouth', - 'Bombos Tablet Ledge', - 'Desert Palace Teleporter Ledge'}, - 'Desert Pass Area': {'Desert Pass Ledge'}, - 'Lake Hylia Water': {'Lake Hylia South Shore', - 'Lake Hylia Island'}, - 'West Dark Death Mountain (Bottom)': {'West Dark Death Mountain (Top)'}, - 'West Dark Death Mountain (Top)': {'Dark Death Mountain Floating Island'}, - 'East Dark Death Mountain (Bottom)': {'East Dark Death Mountain (Top)'}, - 'Turtle Rock Area': {'Turtle Rock Ledge'}, - 'Bumper Cave Area': {'Bumper Cave Ledge'}, - 'Qirn Jump Water': {'Qirn Jump Area'}, - 'Dark Witch Water': {'Dark Witch Area', - 'Dark Witch Northeast'}, - 'Catfish Approach Water': {'Catfish Approach Area'}, - 'Pyramid Area': {'Pyramid Exit Ledge'}, - 'Broken Bridge Water': {'Broken Bridge West', - 'Broken Bridge Area', - 'Broken Bridge Northeast'}, - 'Misery Mire Area': {'Misery Mire Teleporter Ledge'}, - 'Ice Lake Water': {'Ice Lake Area', - 'Ice Lake Ledge (West)', - 'Ice Lake Ledge (East)'} - } # eliminate regions surrounding the initial entrance until less than half of the candidate regions remain explored_regions = list({start_region}) @@ -2310,6 +2274,53 @@ inverted_default_dungeon_connections = [('Ganons Tower', 'Agahnims Tower Exit'), ('Agahnims Tower', 'Ganons Tower Exit') ] +one_way_ledges = { + 'West Death Mountain (Bottom)': {'West Death Mountain (Top)', + 'Spectacle Rock Ledge'}, + 'East Death Mountain (Bottom)': {'East Death Mountain (Top East)', + 'Spiral Cave Ledge'}, + 'Fairy Ascension Plateau': {'Fairy Ascension Ledge'}, + 'Mountain Entry Area': {'Mountain Entry Ledge'}, + 'Sanctuary Area': {'Bonk Rock Ledge'}, + 'Graveyard Area': {'Graveyard Ledge'}, + 'Potion Shop Water': {'Potion Shop Area', + 'Potion Shop Northeast'}, + 'Zora Approach Water': {'Zora Approach Area'}, + 'Hyrule Castle Area': {'Hyrule Castle Ledge'}, + 'Wooden Bridge Water': {'Wooden Bridge Area', + 'Wooden Bridge Northeast'}, + 'Maze Race Area': {'Maze Race Ledge', + 'Maze Race Prize'}, + 'Flute Boy Approach Area': {'Cave 45 Ledge'}, + 'Desert Area': {'Desert Ledge', + 'Desert Palace Entrance (North) Spot', + 'Desert Checkerboard Ledge', + 'Desert Palace Mouth', + 'Desert Palace Stairs', + 'Bombos Tablet Ledge', + 'Desert Palace Teleporter Ledge'}, + 'Desert Pass Area': {'Desert Pass Ledge'}, + 'Lake Hylia Water': {'Lake Hylia South Shore', + 'Lake Hylia Island'}, + 'West Dark Death Mountain (Bottom)': {'West Dark Death Mountain (Top)'}, + 'West Dark Death Mountain (Top)': {'Dark Death Mountain Floating Island'}, + 'East Dark Death Mountain (Bottom)': {'East Dark Death Mountain (Top)'}, + 'Turtle Rock Area': {'Turtle Rock Ledge'}, + 'Bumper Cave Area': {'Bumper Cave Ledge'}, + 'Qirn Jump Water': {'Qirn Jump Area'}, + 'Dark Witch Water': {'Dark Witch Area', + 'Dark Witch Northeast'}, + 'Catfish Approach Water': {'Catfish Approach Area'}, + 'Pyramid Area': {'Pyramid Exit Ledge'}, + 'Broken Bridge Water': {'Broken Bridge West', + 'Broken Bridge Area', + 'Broken Bridge Northeast'}, + 'Misery Mire Area': {'Misery Mire Teleporter Ledge'}, + 'Ice Lake Water': {'Ice Lake Area', + 'Ice Lake Ledge (West)', + 'Ice Lake Ledge (East)'} +} + indirect_connections = { 'Turtle Rock Ledge': 'Turtle Rock', 'Big Bomb Shop': 'Pyramid Fairy', From 3d9aeca81f62adfde6b812e1728f29dc394ea030 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 19 Oct 2021 21:52:03 -0500 Subject: [PATCH 41/76] Fixed issue with Old Man exit not deliverable with starting equipment --- EntranceShuffle.py | 53 +++++++++++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 105e7722..c551b760 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -1390,7 +1390,7 @@ def place_old_man(world, pool, player): old_man_exit = None while not old_man_exit: old_man_exit = old_man_entrances.pop() - if 'West Death Mountain (Bottom)' not in build_accessible_region_list(world, world.get_entrance(old_man_exit, player).parent_region.name, player, True): + if 'West Death Mountain (Bottom)' not in build_accessible_region_list(world, world.get_entrance(old_man_exit, player).parent_region.name, player, True, True): old_man_exit = None old_man_entrances = [e for e in pool if e in entrance_pool and e not in entrance_exits + [old_man_exit]] @@ -1406,8 +1406,15 @@ def place_old_man(world, pool, player): def junk_fill_inaccessible(world, player): + from Main import copy_world from DoorShuffle import find_inaccessible_regions find_inaccessible_regions(world, player) + + for player in range(1, world.players + 1): + world.key_logic[player] = {} + base_world = copy_world(world) + base_world.override_bomb_check = True + world.key_logic = {} # remove regions that have a dungeon entrance accessible_regions = list() @@ -1418,7 +1425,7 @@ def junk_fill_inaccessible(world, player): accessible_regions.append(region_name) break for region_name in accessible_regions.copy(): - accessible_regions = list(set(accessible_regions + list(build_accessible_region_list(world, region_name, player, True, False, False)))) + accessible_regions = list(set(accessible_regions + list(build_accessible_region_list(base_world, region_name, player, False, True, False, False)))) world.inaccessible_regions[player] = [r for r in world.inaccessible_regions[player] if r not in accessible_regions] # get inaccessible entrances @@ -1454,7 +1461,7 @@ def connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, playe accessible_regions.append(region_name) break for region_name in accessible_regions.copy(): - accessible_regions = list(set(accessible_regions + list(build_accessible_region_list(world, region_name, player, True, False, False)))) + accessible_regions = list(set(accessible_regions + list(build_accessible_region_list(world, region_name, player, True, True, False, False)))) world.inaccessible_regions[player] = [r for r in world.inaccessible_regions[player] if r not in accessible_regions] # split inaccessible into 2 lists for each world @@ -1551,7 +1558,7 @@ def build_sectors(world, player): regions = list(OWTileRegions.copy().keys()) sectors = list() while(len(regions) > 0): - explored_regions = build_accessible_region_list(base_world, regions[0], player, False, False, False) + explored_regions = build_accessible_region_list(base_world, regions[0], player, False, False, False, False) regions = [r for r in regions if r not in explored_regions] unique_regions = [_ for i in range(len(sectors)) for _ in sectors[i]] if (any(r in unique_regions for r in explored_regions)): @@ -1577,7 +1584,7 @@ def build_sectors(world, player): regions = list(sectors[s]).copy() sectors2 = list() while(len(regions) > 0): - explored_regions = build_accessible_region_list(base_world, regions[0], player, False, True, False) + explored_regions = build_accessible_region_list(base_world, regions[0], player, False, False, True, False) regions = [r for r in regions if r not in explored_regions] unique_regions = [_ for i in range(len(sectors2)) for _ in sectors2[i]] if (any(r in unique_regions for r in explored_regions)): @@ -1592,26 +1599,38 @@ def build_sectors(world, player): return sectors -def build_accessible_region_list(world, start_region, player, cross_world=False, region_rules=True, ignore_ledges = False): +def build_accessible_region_list(world, start_region, player, build_copy_world=False, cross_world=False, region_rules=True, ignore_ledges = False): + from Main import copy_world + from Items import ItemFactory + def explore_region(region_name): explored_regions.add(region_name) - region = world.get_region(region_name, player) + region = base_world.get_region(region_name, player) for exit in region.exits: - if exit.connected_region is not None \ - and ((any(map(lambda i: i.name == 'Ocarina', world.precollected_items)) and exit.spot_type == 'Flute') \ - or (exit.connected_region.type == region.type or (cross_world and exit.connected_region.type not in [RegionType.Cave, RegionType.Dungeon]))) \ - and (not region_rules or exit.access_rule(blank_state)) and (not ignore_ledges or exit.spot_type != 'Ledge') \ - and exit.connected_region.name not in explored_regions: - if exit.spot_type == 'Flute': + if exit.connected_region is not None: + if any(map(lambda i: i.name == 'Ocarina', base_world.precollected_items)) and exit.spot_type == 'Flute': fluteregion = exit.connected_region for flutespot in fluteregion.exits: if flutespot.connected_region and flutespot.connected_region.name not in explored_regions: explore_region(flutespot.connected_region.name) - else: + elif exit.connected_region.name not in explored_regions \ + and (exit.connected_region.type == region.type or (cross_world and exit.connected_region.type in [RegionType.LightWorld, RegionType.DarkWorld])) \ + and (not region_rules or exit.access_rule(blank_state)) and (not ignore_ledges or exit.spot_type != 'Ledge'): explore_region(exit.connected_region.name) - connect_simple(world, 'Links House S&Q', start_region, player) - blank_state = CollectionState(world) + if build_copy_world: + for player in range(1, world.players + 1): + world.key_logic[player] = {} + base_world = copy_world(world) + base_world.override_bomb_check = True + world.key_logic = {} + else: + base_world = world + + connect_simple(base_world, 'Links House S&Q', start_region, player) + blank_state = CollectionState(base_world) + if base_world.mode[player] == 'standard': + blank_state.collect(ItemFactory('Zelda Delivered', player), True) explored_regions = set() explore_region(start_region) @@ -1635,7 +1654,7 @@ def build_accessible_entrance_list(world, start_region, player, assumed_inventor for item in assumed_inventory: blank_state.collect(ItemFactory(item, player), True) - explored_regions = list(build_accessible_region_list(base_world, start_region, player, cross_world, region_rules, False)) + explored_regions = list(build_accessible_region_list(base_world, start_region, player, False, cross_world, region_rules, False)) if include_one_ways: new_regions = list() From 3cded722ffa85363a3c8fb98be4ec7f916f45540 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 19 Oct 2021 22:14:31 -0500 Subject: [PATCH 42/76] Allow Sanc/Grave to Tile Swap when Standard + Crossworld ER --- OverworldShuffle.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index a8081daa..c9970ea9 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -427,14 +427,16 @@ def shuffle_tiles(world, groups, result_list, player): def reorganize_tile_groups(world, player): groups = {} for (name, groupType) in OWTileGroups.keys(): - if world.mode[player] != 'standard' or name not in ['Castle', 'Links', 'Central Bonk Rocks']: + if world.mode[player] != 'standard' or name not in ['Castle', 'Links', 'Central Bonk Rocks'] \ + or (world.mode[player] == 'standard' and world.shuffle[player] in ['lite', 'liteplus', 'crossed', 'insanity'] and name == 'Castle' and groupType == 'Entrance'): if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'simple', 'restricted']: groups[(name,)] = ([], [], []) else: groups[(name, groupType)] = ([], [], []) for (name, groupType) in OWTileGroups.keys(): - if world.mode[player] != 'standard' or name not in ['Castle', 'Links', 'Central Bonk Rocks']: + if world.mode[player] != 'standard' or name not in ['Castle', 'Links', 'Central Bonk Rocks'] \ + or (world.mode[player] == 'standard' and world.shuffle[player] in ['lite', 'liteplus', 'crossed', 'insanity'] and name == 'Castle' and groupType == 'Entrance'): (lw_owids, dw_owids) = OWTileGroups[(name, groupType,)] if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'simple', 'restricted']: (exist_owids, exist_lw_regions, exist_dw_regions) = groups[(name,)] From a4848c5baf39eb3d7333c5f502878587ce078065 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 21 Oct 2021 14:17:26 -0600 Subject: [PATCH 43/76] Baserom update --- RELEASENOTES.md | 1 + Rom.py | 2 +- data/base2current.bps | Bin 135928 -> 136122 bytes 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9b9857fc..c2fdf20a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -19,6 +19,7 @@ CLI: ```--bombbag``` * Certain lobbies forbidden in standard when rupee bow is enabled * PoD EG disarmed when mirroring (except in nologic) * Fixed issue with key logic + * Updated baserom * 0.5.1.2 * Allowed Blind's Cell to be shuffled anywhere if Blind is not the boss of Thieves Town * Remove unique annotation from a FastEnum that was causing problems diff --git a/Rom.py b/Rom.py index ae29e127..d8514556 100644 --- a/Rom.py +++ b/Rom.py @@ -32,7 +32,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '1b937c39f026f9a687391488b7386542' +RANDOMIZERBASEHASH = '1c59cec98ba4555db8eed1d2dea76497' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index 46dee4bec02691a6036774e7670b7a070e81727c..fca7a6ecdc7d2b97a0a85bb86dd89a16dfb4749c 100644 GIT binary patch delta 19407 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 z z(G@aHSH7ZFUyJ7dx{1pXr=A~IK!<4?3p@0_=IJrj3@ZPUYs z2Wi=yLdxrW44{OMrSc%HNzfQBr6xSfm7ZT#Xe~!HPNk`d&!p$a)AAr_KVz&ARVFsh zE2O5N%9*GSoI7KVWI>&2RO84JYA&jrCX=J0UE^#SHShn58P?)_>yQK1R@=sT2dD+8 zxW3q0jBMOeOfCGs;!$gH5i0I3vbO%zh!soGOWP;XOgS}BCE0bQd2w;W_Tth5a?>ZXGu&kZwDvYY_R;L$yqah2JMI%ZyO6Sn z@jx3VL(?`C%1<_mF~)B7f~Qp@qX4`!Cq`1s=ks~?=pUO&9mjaKJRaZv-_HY|`u|~s zj&f#3+J&$4r~YtxN>=Yb(b>H=;9?@^Ql<7Ho%mh0E1r!<+7FLV6BZakjJBz=7fK#MG7gKAeWo6%!3=|IssX-F;?A(IRk1H8%|$E zLsy&mXj3@T8VldF*6iIps+jUYV|?U~>aiywt<14)2 zx-MoKOq(C!8#2j6iQ&&OPvasuX$Vl^3LLQmqC+th-6-b!;i>TG{F%H!NBH;rrEadn z1}d)(wdB&Ek;g=?nW6+VD$NM zfZ{2n)cH0kbxkY1cpQxYHu3#ptMMu_#q83s9DcK4n1i!_KV@z!VdJ1+;TQN8n6z+c z1dWSfqFzH_7QO+)8q*KU#J%K&8*&>@Qk6qoTnhq|(S2dhd#c<26i`H=(I*B{{KMWk)y zaf{1^4^X`**k{u5sv)_xYI}==AuK{M9?-VlOaPGm56PZy7Y$DtBEGp9bna~9YWqAo zOHej+Q0F0(Z5-5j0%c8uIuotRrlj~LcAHO!=~V^lRy*+YzKI2asZGNwkD%EPGP0FQ!$mn`Jj3gMvw#fxAQ0DMGv61Sc_02E zb1W+LX4o@dD&dW#=@XVrsi7RT!*~C47I04q!+-*fN*jCbDZ%WEJ}i({-`(FWkIn3yhk8e6T6 zUGt0Pk}!*S0Na;E;!W`9Wux%XaB$|-tkT2f{5Wm9e%&4Jt9VQ+^#ETA-4a)MqHu24P zRuwtpxLRm&?w~s0>I(G}q4Ir)3jUfIE}1*`mBB1oF<~i>b-JHcn&3;jn zw(lA{Z7t-KknOWB&_^52X~% z?4S8HHI*f^+T~McU_54B4y{f>Z(j0sV;PmxD3Z>nyrqXm*aNMy!Br}uQj9hf)rZj4 zzn79Sd)HiV+_UC-1D{d|C_2NwhFJVWv7x+ftftFivj>dX!>$Z|J^@4%<1P}lUF3@b zTcr)L;v(UB@juwJrI%-R)BhO>EDn01$+^{rSku{q{xReHpKFW}d-}6_GoVQN!Q^Uj zH4U>kLbny8hAX?Y{-LxsI*w*vjy9_|qeLN2m(*h%3@n4w{$>4k>5ohOmuxRPUiP>QFJ?_ZWM`8dj_-s~*+T}oIF(h5_n-#@ zgrVlF*2^a@pN5OGNBCmdFW?ZMhyYY`!=^`d`U=r%oUX{(0|UpwhU|EW$3?}SS|+ZR z@^~g~Us%D;R3nkXu9WI?#21i~w_L}GnMtc(!$s>}%Lw6B#Q_=d*A*r@0kXT$EMCn-JJYYdgm0FyLz;L%+;1((ot;( zHaGsceJwUu{v^GFH}|rerkyjO`N>>Bf+o-FGRiNXVE#e@}*P0{5I6=g{ zy~a#I3JGr1P&~$?00z+%&dBfZ#Hsocla0leT+nSb+D+?53u#!~P})kR!mw+{am6S^ zHlx!e;z|PC?(uB2q^Z703(L_H6i#$czulz&&3D6STh$NfbnaCBVtcA53QFPp7XYnP+bnC>1QFITJbW7%B6f;xP3pEBiv9(_7=mF%2jbaUF zTqv=`fPvix9PB{5LYPMxc;wXUT&l4M&Bo7hz{NRYgAG_>;DZGQ0Z2F4f;592NHy4l z$p#0IVsHdW2B+>Tbh6kD}x!XH<$ zc@m$<0UIsKn>w)|V2tR8(Nax4hIQ6l=M**ct5<&=(mV;oly@Wgy9aHD;&pK&Qg=Sq z2G4#>i7VC@-AXl*=t9RQ&t2z?!T&+M*I7@epXw&0ORP;Mym1k{yN-;E?4vD%;N~z; z_gI0<*65}pV0N#(d3M5M7m?VAY8(h_eZwZ`zdjr}({61fS9^i=_pM?4z`xQL7dCahcc%T<6cD&1Pv9U1^ zX>@i29j*qxoX10E5E~mZuir(?I+54qJk0EhbY$->!+1{5;Iu8I_qzLraWR@J1LCt? z1Cax7HC+Q|FF{v32(~P>^ZnbZ>W+s`whUQL?gx|rZuJ53&VBFl*}F2^9EIWnouh@b zeSv{|^qM34syO0ZWplV6sF<>ZNc4?=d;xTpna3P zPQ$raZ}o~5Fbpo*8ZJSaKDsU~`3g`S%oAqrmw*OtYy=KY*>#|v3z|8x0juqDVZAW` zrvYj*GjAJ!+0#Jdk|`h%OrHuG7aO#zDb>Jt=yb3@s`nt7!nri$HwS{$senF`3g0T6 zPN;FkoODSJ8|2wXr828FzgFjvSCQC(5qs_*e{4N2kwS}|fv1?vgzo|J$;gy)$bL#A z!koqH+XIwXn*xJ#gGTSz)29&|T;;=YF|%%G-36s?Z*c#%WROd$_7Ch z_DFs|jYT}bChY;t%O!?Be89aNyhbLM{Pche21T0Yh&^J95QD?%<0hoqBR(oHIa_cM zn*cB64jnWWxpc?e4lri1JM+Nv%Rl>Pehmk5y(Ejfk><4TTQ!mrU)2@T5vJ?BxET{Zu4jf1gqBoI3ogF zTZp`qohy6a=3Sz*d)+|cp~SI|6?2dwxa7PGTtdOY12;i7<#WHwi+ZT8me7h|kp9mwq z@+a2*s(va2Lyha0jfYjnp&<51JqFM1s8A6ka{a*+xb~}L{1$BZYNF@;N8BxRB$C)b zzmKZX2K&@zW(EBFt9rZ@9^CHZG$IKHvXL||805i=+eJ7Bd$;@8&02(mnbLXipY0@m z1_tbyk9u$0F%-4d?x+je@dw=;0hew9S7$%K4!-gexAMEHl>)=S0I;dbJO?x2%AFC; z?0oX040&1EJg>iD#m+@Rix*QJ=+@3_b;vO|b9m;Er-Ga~kRhQc7p=5v?ubLsVOI*i zAI{h{3eSc4yJkod5`hX`oXo;Q^I~2*03OylgBbBjY(j^40s<^ay%|qq$!|+_mtyzy zxAF}*2D4vQ3=cFjgEl{wVP+AWRCULCNyRZS>bRA4xD;!J4H)A*^C@j{RFwRRWYOh6 z@(mI6HnE;G_1GzerU?61Cp}?0Gu#$7YO>G`H_MpD6rJK{W_0*I*@+=dz;eM?F%GcP z;{Iw$ZR$*s#v)n|e+|FNTO2toidHK&Ud-=Y)jn*5{P>oO`DlXpx2Q(3i9J%PQY&&% z<2<+-zRR0}ZpCAFTUWbzcYwEKLZv#hgU_XWgh6` z46X`10yFUcOI@rfyJRQi?-_@mhLiTB_!ndb#a<%Jwx=*+ z!mnaIM&$n{?o7FyPd0z%9SR&$3)9>Y>)GoEH}ACFP*?KExvQ+ejmemj<&53lWulE{q@uja$0wTx@M%;^-ST|VJS3z{DN@%22tt@v<^ zt+u9yek!a7#$*RI-SvCZA>%=~eb%4#I%D>%KT$=^A^Iu7=FRw%_Bd5h&^%mBsk8K0 zBi7lrZVN?Uie-^uz~c{AS2`LngREf9%-O}vjxAKnt5-e;gzvJ9qmY*6aL9G~3BdVp7u(XQ=$w zEpyuetTRlnQ`7|REG(B9uzF_c#zH-^_P#=Q%y;!dX2nLRk}MYddRs@XhQCVYy54%h z0nbZ>abKHjq3kD^21fgxaNJ*i-p{6B;QZN@;+&l9mAPxh8?{@O=Vmu_z(c^B#|Ld^ zzztlYIV@)jPRhTA!mnAXkd6!|!C(1>eHo?*F8CgW5hS?38SQm(m}lAO>`u28a)f_=&w)Nn@m9ZKITJydK#tZFt&*@8{*+E|dMDnf-y6^GKJsy39KQ(Z@C zuj(mE`&9p+)VadHf-M+UF|LAzi)F^762)d(p9C@hTQIvKqoO@a+L=cEjN#e=+t7$8 zk|4u$w?Cf0emQB1kc|lcCakaYYpcZ~A5b+2d^e!CX+l=c? zTX$?+zH&V;POT^?FE1&e<;9iF%-RCDsxXSbRjbuPWuXsWZ3S8ihe%MaUAcVg*0otX zHi)w~qKS*EOO z;1lquvWr$JE)|ukX;o#BZD>qTfR+r4jEJhhHFKr#^nv~1)2g+0I{5;$3>e=bD=$_z zhFWWm@7c$TTeWuen%wLyyS6e}FhlmG=OC_bBhS02Kv5toh6iaSuV&F1_*j-pRIRNw zY{=gDr8al%hHRdH)j@-zSjlLsN)^RCU&c!Zzbx^08at^+{#HokA9h>-H6;ZB4WZLs zYMx+RN@t2Y8@pLo7mq~<-wN^1kL!oU1DH^H%2g=5tJyDYOZ$vDu}-1z-%^VA#dZkF z!tlMYvCN<6Is;xSoA14M;@uAHb}TmVm;EN;CgE1$2-m z4;edQSdaQ6=4$B;Y4nNfVS<#@&L)p@VNUi<|;CD89f~K~9mvOOAt26~Ph1 z;_p%~v}055%CNnK#;o|eS{x|+4t(!_pfRXKHXhr=?n>-tk|x0zW%SHR8%&Jz22PDa%xs3W57_c0kmCj*7^ zUO=sB6KmxAu}%@D>Odt&?fg{IC^^-<;&V9o!0tG zW!OM|gLv0<)c}Tnep^ojmUr6cTxa$dv?tN3&zN*yfAh)}w;BFEq`!^f34CFPYBrt+ zohugE2HO9{jPr&$75?O?k#|)?Y~MM%*31^kn0e86<;%%Oq;Y?dBBh>n$~nYtgpCzb z{3L;;Y;;&@+VvPw#U@&W+^j& z%v~g!bKp?dANe$j)5Hy|WKGJgELyN%?;yVUpwUL}K9mT&u>5zN1#-8AEd zf^J+&sWL*Ag)DEd3%Q}`!X|;))b6T{+s@qx8wdMgu>IbIyYwa@GXsWJMh;yz>n`oX z*N$6wS2gnDcR^#=5##RG`91lF`SN}Fap$9x?#dsLKZdYzJ78XAB%TiID|fogpY$jF z4Ab)F*ENc=V5m9(J!xJa&xvTImrryFyhhW@@~i5}HPa?V{D+*HBZx!$BLm^Ru4&Z+hmI1Mumq(O1@WgmRc4ZZb+NtdmE z4EEIx#lPAm3ODd2Ow~k%r0lLQ-&A{lqh~(0P81>v6CnzTK0Ed7)VJ7-(M-&u9&PMG z$Y|nuHeT?KW`x%szt?n*l5shtQsl1F{9ZF@xq=b!pr|GwWIGQBRX&LaWftt?4UeDa zfQ23h)OlEHY-%i%wWhdv`*AoEX4F(A#m?_hD=kWk#iAXI*ztg7;Nn2T^g!L=Kon?{ zw#Aa8&=ADe!?%GqA3$;(4qfd#OUb6_;Pl2jD5^?X5q7Pw@UeDk7XhCTf$|m*yS3vjtyITjk1AmN}6w@ z9TEuDY?RZ++#WY->vSZ^w!j-~sJGj;KUEWxCMKyk=p8_LmSM!9!VR`L3Tn|6ghDsK+ zwc4a@J9$=rW(Kn5nlKD$fZJgvP|cVT8Hlkl(RXNr9V~O8diAHw0z*nNurZ`3(`{#) z&d}{=o6pc^8r$h}XB*E%x?vWFCjGe?*Ug{)tC)di2*8>;3r_@xo2vyXr0zs;|C&4_AK8-!u&a6KJ;?`v;98z&ZCkCa@YO4$;Mve{ zc=?b{lDMfvv+bn5N3;Q}%Gjup39vVeWb)OEYfkB}ABjX>D4PQ>P7w)hsqC=Pci?7< z%4yz6!R0ecnul!vmkMs$>ex=NtGIDcMNT^%EM{&~BN*NU2LNYnDA8bM8mk|$J6Apy z96|Tpv-;D`;)6qRGFPj+BqYmfbwt&&)0sBEV3&kM)#*z_y{nj%X?5g>T3yYeiN^c` z>s*Q+btPX+zLb19`9N|}vMl)udafp4OYTk%0NK+H|6oqOll*P+-Q?=zs$@;ay<|=D z{p9bGzYhi*b4%a^j^jDSz-L?@?hiBTf&@Xf0NO%n-JW5g_GMI|Hp0HF^k&1Y(u2*l zRCMzcDK!+PN~r@4QmO>`085xehcfDbcAP`o(E3E&0go(~ZGM*z6X2GH;k^6>P}}e! zh)IrocZ;X@6`PQA!ZguhZ%RkExw&S0c3#Z8s`^rI#LS*v4wDWi;)7w{;Tc{X-8ytF57(yH z%Ab|gZlPl7wEoP8Z4V9y!QcY7s>Z9H6hO6%lNR%0uY)>>>kVqHxfm|sUBngS~(n+2OExT zz!~Ub7=Z-Xc*FbA>xY!VtRZE{8hu{P&q2W?IgHEJQoRZJ#{>qv9^J}!ODI)Oy2NY` zeKrGn5P*qreNzPP4V6uw@nula941&Z4>WkdbImh+)2`~6HiO09xDeT~csb&nLuqj= zoRcqS$HBm(K@O5dW#vA}Kidges1-N705Jj5RdG9tC}ERdW$JRo+9Db;wALj9w&DjqDb( zh#aw^k{vaxOeLiK#HnvLdsDP=WwX#0WwY5pTM9b<4d-o=%Xk)BX3BU444*MxA;T@< z%m`F)?4o=d(>8;eZG+0`tp(bNd^wCse!Q8=?1W=m5|*B;&>>Hw<$ZpOBaRH9?kKu~ zPr9OSLjgkM#xflAGCKm=-|X` zfWwXjOk91Pi|7|K$#ZK<2YwZk)!jPM*urJT73fIv7)M4O41dEOhHdN#*vWofdIJU~KQN_@txTI&iTIJ(wth2n>R@mw(`BlR`6cS@AK}7w(+Iy6LV76Fn?1*#b5FdHQX=kHD)$o=KvH8@|UcJd&Ndjj84Mp^){HrseT6< zb-AP(P-I%W^kPW%7U~xrSTgZk)!H zYo^q2Iqt?(1kI}9Hn|(8BWPg__t4!q13}Btx4Ro>B4{movAb~=g7%|NayNc48-X;M z(ZiUIAUT@Q!#Ec~YBZq-vI&=KYSDxq#`(~lI?MebG-je@mBXdS{UoJb@_%c%kBZ-9 zU_wH}$U-^7;wZ%dTEMb}RH%YQHm5_Bx1yaItUX6LG`yyb8QMCPKg;F0D@x!Q<~!~8 zRH*iW%%MJpRnf*}+Vwy_hUGps&IVMn_5!M(VyzczlPUQuGzu=1Lfb-Wp>`O6-&4ya z{|#Nswit}5{9Hr|MJEoYtH|?H$H)1(Iz}_4i@%3BoJeRj$o)m8*sh@{va~C2;u6GHw10*l}W% zK(h9+b{PC{q8tx~g>8TGvK-;QlXHb}LMi+L#YZm^vc3r9!AB=QMvvO+2sFqrCpH!X zt5s6I{%pe*e|h;JW*)>(C*qr6^63}8`%}v4!9mTU;}oVa z8)D_&L52}>{v;Ibb_T-N5(_`+F960+jDK ztRCw<-#J(s+aDQ-IXXIK968%vuN9f{&zkDBJ}A3UuU&$&TlLxuQ~n+X53bPnJQ<=i zp@5-@^~}U+b^q)xh61;85>6JEz&YX)O_tm%2qBIc>Q+vfv_Wp=aL{r3plxFS>_58- zkAch2t($xh8AR(-_=f^U{akl}Z%A+@k6!XMh9xUx^+|%u8r9bVgOabUWD@7q$?OVsJdNZTHt#aD02)xGq-#zPxSL)k!vZJXH0N`1rPy zJ=r|`2=uxUo;LOTVSY(Xh3v4Pq^5)&w#iab!4|0&b>}}NtUgj|6lAEt@*d;FFi>)i zEuvO6N=t_V6xCMt{3&5vPg!lt>{+AeeLg`!*^SxZgD1#q$xG*TsC6q$q$yIHHBC-G zQ5ttRma`ij%c*r*B+!Sz&o@TLj=G?;c$F|468A^EZ4Yq;BmNVPn3bFebyV z9L_AOqk;@;<-tCZ1kocE=yTL%fB` zZ-P^NL@#UkmO*-xXoJJ$2FhO1!p7yiY5XGRO+|Q~YBM$Ku&MLe*E(U8$ztZea0gRN zzncZb+lyed=??@vh3~$J<5ylpKGI>gr+Pl>ye@oyXzKQJJAgS|a{apSBkFPB_796~ z#wsovzLxVQ87|9hCK+^A{98zVWu)dzEVINQZ+@%w3D9n1J;JIUZQ`Nqr7W^Wr zX~=$~@Nmbf#-^TkC{P5rV{b$oY_KT-<()^Xp+`0JAORp;5Oz?6EArEAbE$2kw6YqYYe0U0VpJ~ zRIz$6@}HYsj4|5Gp>WN;5hyCH>fTt%f`_bYl=A9K)LnrRtC}NLY<-vC3l;bIM3CS~$V(SYG~fF=qCRK%php4n_<_phz3>wLJijXxzEH zTp<+zF)CYKy2#CD4(kfBxOZ{NfUaY$KGl(KW;px4=jh=X<(J#^C`83J6$*6EKErNYUaks6 zFA8VJz{mG}d0VEzPxqtj#cLI`VoO699R6KM%pEr)x>i^mj0x5Sqa-Ko$E&H#mwx5U zjL>ps`Lx%yF2+P_TOuQZ`@S2x=*O#!`*3B|@R=aDtYq%f8!Ksq35C119 z@rWn$W6;gN6E5$~L_+jjZoyGRL@UIfkgF+jFf-c1}J5uey-%XB=$eP|_Q$`3jX&Rx z4+#Ry{7|4hp+_3gw}`$LcjarheX4unhqXF)njep3^YRqxx11klybA}PQzw`Pu-5U+ zW_awWFLDoGe>yxWai)@rU_2H9IC4)pwX|DG<)CLno3z4hQyC0RQ>pf69DTrpy$vZa z$3pKQy|)4%i+XFKzs|t-$z8glX-9h#v*1{NX;pW zvz|O`Af5t_M`ThBOG0X>FvfkqJD>vD1t@zY+ThF$pe7X9Gzn>9MV5RNgM(7-aT;y) zST7~qF{Fmc_o=CfrxYauss3|hJZ2HTadf1ygE{<`GC1ph4)#Vq2)D`rxafaTl6#aM zLmHkVF<+6>I*sPQq#QkWDEymjU`z85{{ysG?=~~1`0nIJ%~hRWx@cqBaf93mfn%8OYZ;TawPhRpE*&zH8W;F}i`ccAvg5W$^- zrp{-uzOw9McL>Jm6Plyl1oEhL?eME5>~sLy;4q;$^hQ zlfpev;*P96L{<8YL6Xns8?0FsimQSPaLIbjJOIXz6ur%+1uklxhn1A zpYvY0B3$v}pJQ}0@gDAfHNtDB)Bgqo(|%kjFbL7lf8t+n!m@}d@XD(wyQ{P^l36wm z{`o4>b6s+^)lsV1XYE*wGb4dE3XXamKHV=0QG*k@ak?SX65c4Ko@%9aqGI&&;l)aK zrUHJo@0ks--T%y_=bQ4dR)Il+0asI=bsh=aLQ!@ND`VV&HUl=imf&B(;Jz3)D1#}r zaIHuqBL)tjSfG`CaSrylFZh7A|8J=6lez_#qD>}fJG482r6^7~_DwKPU<((%Vex(N z_cvceey^lS!+YR_2xE|nCa(n<{-TM7ECGs7gz*({;aVP$rPYPjK^?{_U)RA8YjjW>hA#*b!wuj?8R|08=_OK zIsT$mtWDy30A`LGa$LLjHpcS?fvfzc9Dw(C4jTt}VwWZSLOj5@`#D`81BsFL2TG zwY*|A5urqAyq`d}HyKOgd9O>MgT>(dXYSSJ74V2nuf)0%@M!4iylDb{AQskDO)s?gOO>Dx!9%~3_#Alg z_i&dI677^T50?N6eEwbZ92t#4L~ci|VRKxN$l?tBc7fR(V?6YQ9`wdprOme{1E8Q_ycs z2F#4db^(x^cov+QGgAJ+@F7HQ>m?U@$$h-!Azt#yUh)}`eJ}FcpKzbDG$zH}r*~Gt zeU#n~LpsRLDua83F$3OyPY}OVhKnSQ>hL3-S(b3kWUrncG#APn!pB(Z>=6@l`w;3u z#97!`1v0nJeO0NOj>>I`eQ@fBiGxn6SvO&g3e7lyffE7c%s4{!Lp1&h_IwDyPr#QS zrVop{rt4*PbR2g=XRH;eky@1WZv_+nzlzN^rl~WE^sAij_Fg`vn7rr?v=@7T>6ci16)g{u2bFfeK&Qi ztx{)qD?OrIbH8Ma+j%3crkAUA8{>J`^kHQ|#e=p%mU3&uc`7N`HK@m7HI}LD$KN?&{!VbNI1N1s>faH!f&W2LkwG2y zzxro5?$yib9$nHzn>~Few#V|3LaCBY=}6w}Pbj#sG~f`hN*XcGH?e;iQK=xAR^>K% z&V^ARc5hEFqag)qsD~v{g=grQrJ)1YG++|eNlS!MX<6x={in`B6l|n4l1s|seoJfi zZWjNdfmk|e-+h82;Bu%U(fG~R;aB*0hV<*;E*%oJm2{-2Me{pIQyjzq6P5U7PXjq0 z4-N=RP4ux6p*W=6?HGhv%<-CGe|(L*g!bz1N%z6 zHyi380sov0MpU$pgyw))-w-UfKqY!Vk8H8PudvjuUtwkks1qm(q0h>6XvR}}-jKwL zr}9~t{ZwK&-;MxL#BAyyJ61zKVpf%n;7F{sK|Pb{yyuO??KW7%>`M{7?k`1J^dgYDmVHo8cuy6W$3+0K)NY7H%^6&CjutG;;7(5-q8E zF?30wYUP5@9OaxA%7vh?5mV$;E9{b?!-w&7JEVh>m`+26Oc`yy-=6QAO7q%CQy2UQ z3|_9!W${`JadhG4cHD^JCek0l$u1~jgz|Dqz^n_R=@`40xLuGIhUyLY$rbn;Rb`QT zSK%SU;A&eoi#z)uiMAceAwzv21F5?-e>}4lkNBa7p#@63Q19)BLORR%CmHGoBhXoU z9+Ig6=#Earbukw9QBVj5eZJaSan6u2g97CPp&ZLoa@mWoDPd5UC=dGh4bymV7>1S4 zvIQ|QL9hzA^E)_6ulT5gXdgnCB-*`fzLmWm6sSI{bu1F z{SPn&Va7$H7stm1gMTgSeA=TVdq%+n>fU0_Uu{0&R<9~&#*we30A=sn6E^2QRg5}6TXVdoqafH-0d WMR^R&#RoM=R0CX3I}ktI`sP2W$4vA9 delta 19318 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 Date: Thu, 21 Oct 2021 14:20:22 -0600 Subject: [PATCH 44/76] Bug fix for inaccessible region function (always used player 1's HC Ledge) --- DoorShuffle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 61da6443..3d3a6eda 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1837,7 +1837,7 @@ def find_inaccessible_regions(world, player): queue.append(connect) world.inaccessible_regions[player].extend([r.name for r in all_regions.difference(visited_regions) if valid_inaccessible_region(r)]) if world.mode[player] == 'inverted': - ledge = world.get_region('Hyrule Castle Ledge', 1) + ledge = world.get_region('Hyrule Castle Ledge', player) if any(x for x in ledge.exits if x.connected_region.name == 'Agahnims Tower Portal'): world.inaccessible_regions[player].append('Hyrule Castle Ledge') logger = logging.getLogger('') From 12846df681731f8e51173f5b2425226a9677e75a Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 21 Oct 2021 14:34:21 -0600 Subject: [PATCH 45/76] Accounted for bomb usage in Mire 2 (non-enemizer, of course) --- Rules.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Rules.py b/Rules.py index 9902347d..0d657844 100644 --- a/Rules.py +++ b/Rules.py @@ -304,7 +304,11 @@ def global_rules(world, player): set_rule(world.get_entrance('Mire Lobby Gap', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player)) set_rule(world.get_entrance('Mire Post-Gap Gap', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player)) set_rule(world.get_entrance('Mire Falling Bridge WN', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player)) # this is due to the fact the the door opposite is blocked - set_rule(world.get_entrance('Mire 2 NE', player), lambda state: state.has_sword(player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Hammer', player) or state.has('Cane of Somaria', player) or state.can_shoot_arrows(player)) # need to defeat wizzrobes, bombs don't work ... + set_rule(world.get_entrance('Mire 2 NE', player), lambda state: state.has_sword(player) or + (state.has('Fire Rod', player) and (state.can_use_bombs(player) or state.can_extend_magic(player, 9))) or # 9 fr shots or 8 with some bombs + (state.has('Ice Rod', player) and state.can_use_bombs(player)) or # freeze popo and throw, bomb to finish + state.has('Hammer', player) or state.has('Cane of Somaria', player) or state.can_shoot_arrows(player)) # need to defeat wizzrobes, bombs don't work ... + # byrna could work with sufficient magic set_rule(world.get_location('Misery Mire - Spike Chest', player), lambda state: (state.world.can_take_damage and state.has_hearts(player, 4)) or state.has('Cane of Byrna', player) or state.has('Cape', player)) set_rule(world.get_entrance('Mire Left Bridge Hook Path', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Mire Tile Room NW', player), lambda state: state.has_fire_source(player)) From 25be090f757461ded4c15c29125657cb7345c482 Mon Sep 17 00:00:00 2001 From: cassidy Date: Thu, 21 Oct 2021 17:02:56 -0400 Subject: [PATCH 46/76] Remove diggable light world portals in inverted --- Rom.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Rom.py b/Rom.py index d8514556..b32408d3 100644 --- a/Rom.py +++ b/Rom.py @@ -2306,6 +2306,12 @@ def set_inverted_mode(world, player, rom): write_int16(rom, snes_to_pc(0x02E8D5), 0x07C8) write_int16(rom, snes_to_pc(0x02E8F7), 0x01F8) rom.write_byte(snes_to_pc(0x08D40C), 0xD0) # morph proof + rom.write_byte(snes_to_pc(0x1BC428), 0x00) # remove diggable light world portals + rom.write_byte(snes_to_pc(0x1BC42A), 0x00) + rom.write_byte(snes_to_pc(0x1BC590), 0x00) + rom.write_byte(snes_to_pc(0x1BC5A1), 0x00) + rom.write_byte(snes_to_pc(0x1BC5B1), 0x00) + rom.write_byte(snes_to_pc(0x1BC5C7), 0x00) # the following bytes should only be written in vanilla # or they'll overwrite the randomizer's shuffles if world.shuffle[player] == 'vanilla': From 593355f8398db3e45a9aa679838c803860a93c8e Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 21 Oct 2021 16:34:27 -0600 Subject: [PATCH 47/76] Quadrant glitch mistake reversion Fix for inverted diggable portals --- Main.py | 2 +- RELEASENOTES.md | 3 +++ Rom.py | 2 +- data/base2current.bps | Bin 136122 -> 136114 bytes 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Main.py b/Main.py index cfd79062..75fe9242 100644 --- a/Main.py +++ b/Main.py @@ -29,7 +29,7 @@ from Fill import sell_potions, sell_keys, balance_multiworld_progression, balanc from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops from Utils import output_path, parse_player_names -__version__ = '0.5.1.3-u' +__version__ = '0.5.1.4-u' from source.classes.BabelFish import BabelFish diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c2fdf20a..7098107e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -15,6 +15,9 @@ CLI: ```--bombbag``` # Bug Fixes and Notes. +* 0.5.1.4 + * Revert quadrant glitch fix for baserom + * Fix for inverted * 0.5.1.3 * Certain lobbies forbidden in standard when rupee bow is enabled * PoD EG disarmed when mirroring (except in nologic) diff --git a/Rom.py b/Rom.py index b32408d3..5680714e 100644 --- a/Rom.py +++ b/Rom.py @@ -32,7 +32,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '1c59cec98ba4555db8eed1d2dea76497' +RANDOMIZERBASEHASH = '11daec4f3e1afc96cd044585dfba9df8' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index fca7a6ecdc7d2b97a0a85bb86dd89a16dfb4749c..a0d85faa0dc758ea3e0d3f04179760dc1e401269 100644 GIT binary patch delta 4440 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 YI9DR{9ZpXiAk36@)K1wIWY7ES|LBAu_y7O^ From 73402a2e67c0a9a42eb35377251a6d3af15ba93c Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 21 Oct 2021 21:47:02 -0500 Subject: [PATCH 48/76] Merged in DR v0.5.1.4 --- data/base2current.bps | Bin 140861 -> 141044 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/data/base2current.bps b/data/base2current.bps index 981a4ebbe1e3044fb33966c5dc23894017bf9a8f..7cdb4c14bc0cda340d51f2ea7fd398303c90baed 100644 GIT binary patch delta 20292 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

#t!CG|8)qQsDO#MnF$F=r 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μ}crpIWM8=nKXC~Pi8<&6sZxeUC2=;w( zx_MlXGhMody>@bfc>oOqo-ek(m=io<{td(MiKX#3j2*RNrJ+qusfJ5CYCF^st|;;G z3ynr=o#uZa**fiHw|;n9ozC~lm5%43bZMUuH{7Do>=PqL!o1%OpCs6_KQt-3D{J#p zkD7UnOWZV);XP!vZBo@+=#aaym?0QEX#@M*$SepFheoEr&m#YF(d@(h$Nz;3zd7Tv z*wKBO2Bb}N_Nq*PnI_YkX2#WMU`~8lSD`mHn~%tFocO`Z$O#3!jq-G6+E$er=sQhb zURP#$|D|OT_(2d%0j+IySKa@10Ka|P4V)P)DqnZ;vGMnw=yJp>uXo8;ZnPTSvTYDE zMx&J3f2ua*+|Z^&1y=howFPeALRG=xgxi?d4YM|_o;`^j4jhkOnnpOzG*?VQBdbpr7Y`u|BW1|I?FH?4gp;!$6f*a)oO=1 zo=buot;JhiH2c6=*1O;f{nqE8olFypag8t43=g=5e;WuLe*CH_H~NXGIB6@6#W4Vf zxmmJd?2r2Z;)8>W54F*#;psryI4X$_m)nAN69uiQ=SsKZKm@^TVtf-$q(ps`QGnMX zdOt$&J_NDw4(UX22hf2q{ILwQo-?A4Sh9r4)0H@D7d|V4syLnQg+Z{Ds@gjjrP1-d zsc=;`EORc|*WN&(V%>n>HDvP;tZkg}(8Z`sAv65rXepKVt8#+u3 z1K$+o)OrXhvpcBB5BBoq+12c89^VUtK(|)+<)<@0O4}}AHXK6O$UT?H^l)gLpjZ)o z`6B%hz#{UG7`VuKmcK_VbJcWm`DBrXcnAQ#eAZV@en8GAzz3izo#8NQnA-e8qfx8w z+gyehB|*N=)D>sG06;7N%W`r~u2A9-1^GM)SQ)2IBu))Ep8|j5Akq;3Qk1QE5Kk}9s`<9SF zXCkVdD_~td%w|<3wRlfHb#~f9{7F7UBA=(^NAW(e5gfKWDDZ^mCa;0vU!!evfa#z}d3jD≶Kv3Q+sSPw z#3|7`bMd6F;St0VI1d9HhgWB%33#vPb0rwx$Bx;u==_EXO2M)2Z`z)mJE?}9TZDx>8=p2)nQ#xI@qo4r3 zw`Ikp3s`<18rb;tpO6#xVFXmlkndWChNLLh3gN&%n>NoPr+%gnnA*8~;(tvJ>1(G~ z`d|m8$w;_p2sDTtB2`0>qg5z(4jQaDYXZ5WK;O$D0Dq=JacIsK{7{A7Q{WuUuQ7Bt>Q86d{tu{i+-U#+ delta 20194 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 From b46452f7481b1c3936e3354aaae9194432817730 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 21 Oct 2021 22:18:59 -0500 Subject: [PATCH 49/76] Fixed portal to match grass in Inverted --- Rom.py | 2 +- data/base2current.bps | Bin 141044 -> 141044 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index 1617538b..e43d7c4f 100644 --- a/Rom.py +++ b/Rom.py @@ -33,7 +33,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'cfafbec1231207dc4a7f1992ea7500c6' +RANDOMIZERBASEHASH = 'beae526152a17a203c6a17b227826679' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index 7cdb4c14bc0cda340d51f2ea7fd398303c90baed..5ad54acecdadf4e1401b15980bac564dc13f4866 100644 GIT binary patch delta 40 ycmV+@0N4NY%?R|(2(U>31kV;Q>9b7%n#Tft1B231REFR=(9}$n#Te(0E6ksx9P|M16~OK@^ItJ;<2TL<`i@Q From 13ae8accc484f9d0debeda4dad9497fa618ed397 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 22 Oct 2021 00:25:10 -0500 Subject: [PATCH 50/76] Smith deletion on S+Q only if cannot reach from start --- EntranceShuffle.py | 2 ++ OverworldShuffle.py | 46 ++++++++++++++++++++++++++++++++++++++++++ Rom.py | 5 +---- data/base2current.bps | Bin 141044 -> 141069 bytes 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index c551b760..27e6df66 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -73,6 +73,8 @@ def link_entrances(world, player): connect_logical(world, entrancename, exitname, player, False) for entrancename, exitname in default_connector_connections + dropexit_connections: connect_logical(world, entrancename, exitname, player, True) + if invFlag: + world.get_entrance('Dark Sanctuary Hint Exit', player).connect(world.get_entrance('Dark Sanctuary Hint', player).parent_region) if not invFlag: for entrancename, exitname in open_default_connections: diff --git a/OverworldShuffle.py b/OverworldShuffle.py index c9970ea9..25ff9d8c 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -674,6 +674,52 @@ def update_world_regions(world, player): for name in world.owswaps[player][2]: world.get_region(name, player).type = RegionType.LightWorld +def can_reach_smith(world, player): + from Items import ItemFactory + from BaseClasses import CollectionState + + invFlag = world.mode[player] == 'inverted' + + def explore_region(region_name, region=None): + nonlocal found + explored_regions.add(region_name) + if not found: + if not region: + region = world.get_region(region_name, player) + for exit in region.exits: + if not found and exit.connected_region is not None: + if any(map(lambda i: i.name == 'Ocarina', world.precollected_items)) and exit.spot_type == 'Flute': + fluteregion = exit.connected_region + for flutespot in fluteregion.exits: + if flutespot.connected_region and flutespot.connected_region.name not in explored_regions: + explore_region(flutespot.connected_region.name, flutespot.connected_region) + elif exit.connected_region.name not in explored_regions \ + and exit.connected_region.type in [RegionType.LightWorld, RegionType.DarkWorld] \ + and exit.access_rule(blank_state): + explore_region(exit.connected_region.name, exit.connected_region) + elif exit.name == 'Sanctuary S': + sanc_region = exit.connected_region + if len(sanc_region.exits) and sanc_region.exits[0].name == 'Sanctuary Exit': + explore_region(sanc_region.exits[0].connected_region.name, sanc_region.exits[0].connected_region) + elif exit.connected_region.name == 'Blacksmiths Hut' and exit.access_rule(blank_state): + found = True + + blank_state = CollectionState(world) + if world.mode[player] == 'standard': + blank_state.collect(ItemFactory('Zelda Delivered', player), True) + if world.logic[player] in ['noglitches', 'minorglitches'] and world.get_region('Frog Prison', player).type == (RegionType.DarkWorld if not invFlag else RegionType.LightWorld): + blank_state.collect(ItemFactory('Titans Mitts', player), True) + + found = False + explored_regions = set() + explore_region('Links House') + if not found: + if not invFlag: + explore_region('Sanctuary') + else: + explore_region('Dark Sanctuary Hint') + return found + test_connections = [ #('Links House ES', 'Octoballoon WS'), #('Links House NE', 'Lost Woods Pass SW') diff --git a/Rom.py b/Rom.py index e43d7c4f..c7937e24 100644 --- a/Rom.py +++ b/Rom.py @@ -33,7 +33,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'beae526152a17a203c6a17b227826679' +RANDOMIZERBASEHASH = 'c6c2a2d5d89a3c84871f58806bbb3acf' class JsonRom(object): @@ -661,8 +661,6 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): if world.owMixed[player]: owMode |= 0x400 - rom.write_byte(0x18004C, 0x01) # patch for allowing Frogsmith to enter multi-entrance caves - # patches map data specific for OW Shuffle #inverted_buffer[0x03] = inverted_buffer[0x03] | 0x2 # convenient portal on WDM inverted_buffer[0x1A] = inverted_buffer[0x1A] | 0x2 # rocks added to prevent OWG hardlock @@ -2514,7 +2512,6 @@ def set_inverted_mode(world, player, rom, inverted_buffer): if (world.mode[player] == 'inverted') != (0x10 in world.owswaps[player][0] and world.owMixed[player]): rom.write_bytes(snes_to_pc(0x1BC67A), [0x2E, 0x0B, 0x82]) # add warp under rock rom.write_byte(snes_to_pc(0x1BC43A), 0x00) # remove secret portal - if (world.mode[player] == 'inverted') != (0x1B in world.owswaps[player][0] and world.owMixed[player]): write_int16(rom, 0x15AEE + 2 * 0x06, 0x0020) # post aga hyrule castle spawn rom.write_byte(0x15B8C + 0x06, 0x1B) diff --git a/data/base2current.bps b/data/base2current.bps index 5ad54acecdadf4e1401b15980bac564dc13f4866..daf77e69ccf89d2044d3fcf39dae204306a0edd0 100644 GIT binary patch delta 9783 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 Date: Fri, 22 Oct 2021 18:32:00 -0500 Subject: [PATCH 51/76] Smith deletion on S+Q only if cannot reach from start --- Rom.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Rom.py b/Rom.py index c7937e24..629e3518 100644 --- a/Rom.py +++ b/Rom.py @@ -688,6 +688,10 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): write_int16(rom, 0x150002, owMode) write_int16(rom, 0x150004, owFlags) + + from OverworldShuffle import can_reach_smith + if not can_reach_smith(world, player): + rom.write_byte(0x18005d, 0x01) # patch for deleting smith on S+Q # patch entrance/exits/holes for region in world.regions: From 1ad99fb9bfc8a8e93df3ff86cb94eb955b81e9b6 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sat, 23 Oct 2021 19:26:20 -0500 Subject: [PATCH 52/76] Fixed issue where Pyramid Exit leads to Pyramid instead of HC Ledge --- Rom.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Rom.py b/Rom.py index 629e3518..4abf7f38 100644 --- a/Rom.py +++ b/Rom.py @@ -2604,6 +2604,23 @@ def set_inverted_mode(world, player, rom, inverted_buffer): write_int16(rom, 0xDB96F + 2 * 0x35, 0x001B) # move pyramid exit door write_int16(rom, 0xDBA71 + 2 * 0x35, 0x011C) + + if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']: + rom.write_byte(0xDBB73 + 0x35, 0x36) # move pyramid exit door + + write_int16(rom, 0x15AEE + 2 * 0x37, 0x0010) # pyramid exit to new hc area + rom.write_byte(0x15B8C + 0x37, 0x1B) + write_int16(rom, 0x15BDB + 2 * 0x37, 0x000E) + write_int16(rom, 0x15C79 + 2 * 0x37, 0x0600) + write_int16(rom, 0x15D17 + 2 * 0x37, 0x0676) + write_int16(rom, 0x15DB5 + 2 * 0x37, 0x0604) + write_int16(rom, 0x15E53 + 2 * 0x37, 0x06E8) + write_int16(rom, 0x15EF1 + 2 * 0x37, 0x066D) + write_int16(rom, 0x15F8F + 2 * 0x37, 0x06F3) + rom.write_byte(0x1602D + 0x37, 0x00) + rom.write_byte(0x1607C + 0x37, 0x0A) + write_int16(rom, 0x160CB + 2 * 0x37, 0x0000) + write_int16(rom, 0x16169 + 2 * 0x37, 0x811C) if (world.mode[player] == 'inverted') != (0x29 in world.owswaps[player][0] and world.owMixed[player]): rom.write_bytes(snes_to_pc(0x06B2AB), [0xF0, 0xE1, 0x05]) # frog pickup on contact if (world.mode[player] == 'inverted') != (0x2C in world.owswaps[player][0] and world.owMixed[player]): From e7c1cbda6354678121385f4eaaf4af945fa8e7c4 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sat, 23 Oct 2021 19:27:08 -0500 Subject: [PATCH 53/76] Remove outdated hammer rule for Pyramid Exit Ledge Drop --- OverworldShuffle.py | 2 +- Rules.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 25ff9d8c..dd5a161c 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -876,7 +876,7 @@ mandatory_connections = [# Whirlpool Connections ('Grassy Lawn Pegs', 'Village of Outcasts Area'), #hammer ('Shield Shop Fence (Outer) Ledge Drop', 'Shield Shop Fence'), ('Shield Shop Fence (Inner) Ledge Drop', 'Shield Shop Area'), - ('Pyramid Exit Ledge Drop', 'Pyramid Area'), #hammer(inverted) + ('Pyramid Exit Ledge Drop', 'Pyramid Area'), ('Broken Bridge Hammer Rock (South)', 'Broken Bridge Northeast'), #hammer/glove ('Broken Bridge Hammer Rock (North)', 'Broken Bridge Area'), #hammer/glove ('Broken Bridge Hookshot Gap', 'Broken Bridge West'), #hookshot diff --git a/Rules.py b/Rules.py index c0cb5505..a92ed288 100644 --- a/Rules.py +++ b/Rules.py @@ -1017,7 +1017,6 @@ def ow_rules(world, player): add_rule(world.get_entrance('Pyramid Hole', player), lambda state: False) set_rule(world.get_entrance('Pyramid Entrance', player), lambda state: False) - set_rule(world.get_entrance('Pyramid Exit Ledge Drop', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('Pyramid Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Pyramid Pass Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Pyramid Courtyard Mirror Spot', player), lambda state: state.has_Mirror(player)) From 0b6c629b863c12433201b8c747dcf31e3e7ed2cc Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sun, 24 Oct 2021 20:32:33 -0500 Subject: [PATCH 54/76] Repurposed Lite ER to a beginner friendly ER mode --- EntranceShuffle.py | 149 +++++++++++++++++++++++++++++++-------------- 1 file changed, 104 insertions(+), 45 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 27e6df66..6b917f9f 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -23,7 +23,7 @@ def link_entrances(world, player): drop_connections = default_drop_connections.copy() dropexit_connections = default_dropexit_connections.copy() - Dungeon_Exits = Dungeon_Exits_Base.copy() + Dungeon_Exits = LW_Dungeon_Exits + DW_Mid_Dungeon_Exits + DW_Late_Dungeon_Exits Cave_Exits = Cave_Exits_Base.copy() Old_Man_House = Old_Man_House_Base.copy() Cave_Three_Exits = Cave_Three_Exits_Base.copy() @@ -381,32 +381,71 @@ def link_entrances(world, player): suppress_spoiler = False - # place links house - links_house = place_links_house(world, sectors, player) - # shuffle dungeons - full_shuffle_dungeons(world, Dungeon_Exits, player) + skull_woods_shuffle(world, player) + # build dungeon lists + lw_dungeons = LW_Dungeon_Exits.copy() + dw_dungeons = DW_Late_Dungeon_Exits.copy() + + if world.mode[player] == 'standard': + connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) + lw_dungeons.append(tuple(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'))) + else: + lw_dungeons.append(tuple(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)', 'Hyrule Castle Exit (South)'))) + + if not world.shuffle_ganon: + connect_two_way(world, 'Ganons Tower' if not invFlag else 'Agahnims Tower', 'Ganons Tower Exit', player) + else: + dw_dungeons.append('Ganons Tower Exit') + + unbias_dungeons(lw_dungeons) + unbias_dungeons(dw_dungeons) + # shuffle dropdowns scramble_holes(world, player) - caves = list(Cave_Exits + Cave_Three_Exits + Old_Man_House) + # place links house + links_house = place_links_house(world, sectors, player) - # place connectors in inaccessible regions - connector_entrances = [e for e in list(zip(*default_connector_connections))[0] if e in entrance_pool] - connect_inaccessible_regions(world, connector_entrances, [], caves, player) - - # place old man, has limited options - connector_entrances = [e for e in connector_entrances if e in entrance_pool] - place_old_man(world, list(connector_entrances), player) - - # shuffle remaining connectors - connector_entrances = [e for e in connector_entrances if e in entrance_pool] - connect_caves(world, connector_entrances, [], caves, player) - # place blacksmith, has limited options place_blacksmith(world, links_house, player) + # determine pools + Cave_Base = list(Cave_Exits + Cave_Three_Exits) + lw_entrances = list() + dw_entrances = list() + for e in entrance_pool: + region = world.get_entrance(e, player).parent_region + if region.type == RegionType.LightWorld: + lw_entrances.append(e) + else: + dw_entrances.append(e) + + # place connectors in inaccessible regions + caves = Cave_Base + lw_dungeons + Cave_Base + connector_entrances = [e for e in list(zip(*default_connector_connections + default_dungeon_connections + open_default_dungeon_connections))[0] if e in lw_entrances] + connect_inaccessible_regions(world, connector_entrances, [], caves, player) + lw_dungeons = list(set(lw_dungeons) & set(caves)) + Old_Man_House + + caves = list(set(Cave_Base) & set(caves)) + dw_dungeons + connector_entrances = [e for e in list(zip(*default_connector_connections + default_dungeon_connections + open_default_dungeon_connections))[0] if e in dw_entrances] + connect_inaccessible_regions(world, [], connector_entrances, caves, player) + dw_dungeons = list(set(dw_dungeons) & set(caves)) + caves = list(set(Cave_Base) & set(caves)) + DW_Mid_Dungeon_Exits + + # place old man, has limited options + lw_entrances = [e for e in lw_entrances if e in list(zip(*default_connector_connections + default_dungeon_connections + open_default_dungeon_connections))[0] and e in entrance_pool] + dw_entrances = [e for e in dw_entrances if e in list(zip(*default_connector_connections + default_dungeon_connections + open_default_dungeon_connections))[0] and e in entrance_pool] + place_old_man(world, lw_entrances if not invFlag else dw_entrances, player) + + # shuffle remaining connectors + lw_entrances = [e for e in lw_entrances if e in list(zip(*default_connector_connections + default_dungeon_connections + open_default_dungeon_connections))[0] and e in entrance_pool] + dw_entrances = [e for e in dw_entrances if e in list(zip(*default_connector_connections + default_dungeon_connections + open_default_dungeon_connections))[0] and e in entrance_pool] + connect_caves(world, lw_entrances, [], lw_dungeons, player) + connect_caves(world, [], dw_entrances, dw_dungeons, player) + connect_caves(world, lw_entrances, dw_entrances, caves, player) + # place bomb shop, has limited options bomb_shop_doors = list(entrance_pool) if world.logic[player] in ['noglitches', 'minorglitches'] or (invFlag != (0x1b in world.owswaps[player][0] and world.owMixed[player])): @@ -1016,7 +1055,7 @@ def scramble_holes(world, player): hole_targets.append(('Pyramid Exit', 'Pyramid')) # shuffle sanctuary hole in same world as other HC entrances - if world.shuffle[player] not in ['lite', 'liteplus', 'crossed']: + if world.shuffle[player] not in ['liteplus', 'crossed']: drop_owid_map = { # owid, is_light_world 'Lost Woods Hideout Stump': (0x00, True), 'Lumberjack Tree Cave': (0x02, True), @@ -1032,6 +1071,8 @@ def scramble_holes(world, player): region = world.get_entrance('Hyrule Castle Exit (South)', player).parent_region if len(region.entrances) > 0: hc_in_lw = region.entrances[0].parent_region.type == (RegionType.LightWorld if not invFlag else RegionType.DarkWorld) + elif world.shuffle[player] == 'lite': + hc_in_lw = True else: # checks if drop candidates exist in LW drop_owids = [ 0x00, 0x02, 0x13, 0x15, 0x18, 0x1b, 0x22 ] @@ -1280,13 +1321,6 @@ def full_shuffle_dungeons(world, Dungeon_Exits, player): random.shuffle(ledge) lw_must_exit.append(ledge.pop()) lw_related.extend(ledge) - if world.shuffle[player] == 'lite': - lw_entrances.extend(dw_entrances) - lw_must_exit.extend(dw_must_exit) - lw_related.extend(dw_related) - dw_entrances = list() - dw_must_exit = list() - dw_related = list() random.shuffle(lw_must_exit) random.shuffle(dw_must_exit) @@ -1295,17 +1329,16 @@ def full_shuffle_dungeons(world, Dungeon_Exits, player): hyrule_castle_exits = list([tuple(e for e in hyrule_castle_exits if e in exit_pool)]) hyrule_castle_exits.extend([e for e in dungeon_exits if isinstance(e, str)]) dungeon_exits = [e for e in dungeon_exits if not isinstance(e, str)] - if world.shuffle[player] != 'lite': - if invFlag == (0x13 in world.owswaps[player][0] and world.owMixed[player]): - connect_mandatory_exits(world, lw_entrances, hyrule_castle_exits, lw_must_exit, player, False) - dungeon_exits.extend([e for e in hyrule_castle_exits if isinstance(e, str)]) - hyrule_castle_exits = [e for e in hyrule_castle_exits if not isinstance(e, str)] - connect_caves(world, lw_entrances, [], hyrule_castle_exits, player) - else: - connect_mandatory_exits(world, dw_entrances, hyrule_castle_exits, dw_must_exit, player, False) - dungeon_exits.extend([e for e in hyrule_castle_exits if isinstance(e, str)]) - hyrule_castle_exits = [e for e in hyrule_castle_exits if not isinstance(e, str)] - connect_caves(world, [], dw_entrances, hyrule_castle_exits, player) + if invFlag == (0x13 in world.owswaps[player][0] and world.owMixed[player]): + connect_mandatory_exits(world, lw_entrances, hyrule_castle_exits, lw_must_exit, player, False) + dungeon_exits.extend([e for e in hyrule_castle_exits if isinstance(e, str)]) + hyrule_castle_exits = [e for e in hyrule_castle_exits if not isinstance(e, str)] + connect_caves(world, lw_entrances, [], hyrule_castle_exits, player) + else: + connect_mandatory_exits(world, dw_entrances, hyrule_castle_exits, dw_must_exit, player, False) + dungeon_exits.extend([e for e in hyrule_castle_exits if isinstance(e, str)]) + hyrule_castle_exits = [e for e in hyrule_castle_exits if not isinstance(e, str)] + connect_caves(world, [], dw_entrances, hyrule_castle_exits, player) # connect any remaining must-exit entrances dungeon_exits.extend(hyrule_castle_exits) @@ -1492,7 +1525,7 @@ def connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, playe connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, player) # connect one connector at a time to ensure multiple connectors aren't assigned to the same inaccessible set of regions - if world.shuffle[player] in ['lite', 'liteplus', 'crossed', 'insanity']: + if world.shuffle[player] in ['liteplus', 'crossed', 'insanity']: combined_must_exit_regions = list(must_exit_regions + otherworld_must_exit_regions) if len(combined_must_exit_regions) > 0: random.shuffle(combined_must_exit_regions) @@ -1546,6 +1579,30 @@ def unbias_some_entrances(Dungeon_Exits, Cave_Exits, Old_Man_House, Cave_Three_E tuplize_lists_in_list(Cave_Three_Exits) +def unbias_dungeons(Dungeon_Exits): + def shuffle_lists_in_list(ls): + for i, item in enumerate(ls): + if isinstance(item, list): + ls[i] = random.sample(item, len(item)) + + def tuplize_lists_in_list(ls): + for i, item in enumerate(ls): + if isinstance(item, list): + ls[i] = tuple(item) + + shuffle_lists_in_list(Dungeon_Exits) + + # TR fixup + for i, item in enumerate(Dungeon_Exits[-1]): + if 'Turtle Rock Ledge Exit (East)' == item: + if 0 != i: + Dungeon_Exits[-1][i] = Dungeon_Exits[-1][0] + Dungeon_Exits[-1][0] = 'Turtle Rock Ledge Exit (East)' + break + + tuplize_lists_in_list(Dungeon_Exits) + + def build_sectors(world, player): from Main import copy_world from OWEdges import OWTileRegions @@ -1763,19 +1820,21 @@ def can_reach(world, entrance_name, region_name, player): return region_name not in world.inaccessible_regions[player] -Dungeon_Exits_Base = [('Desert Palace Exit (South)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)'), +LW_Dungeon_Exits = [('Desert Palace Exit (South)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)'), 'Desert Palace Exit (North)', 'Eastern Palace Exit', 'Tower of Hera Exit', - 'Thieves Town Exit', - 'Skull Woods Final Section Exit', - 'Ice Palace Exit', + 'Agahnims Tower Exit'] + +DW_Late_Dungeon_Exits = ['Ice Palace Exit', 'Misery Mire Exit', - 'Palace of Darkness Exit', - 'Swamp Palace Exit', - 'Agahnims Tower Exit', ('Turtle Rock Ledge Exit (East)', 'Turtle Rock Exit (Front)', 'Turtle Rock Ledge Exit (West)', 'Turtle Rock Isolated Ledge Exit')] +DW_Mid_Dungeon_Exits = ['Thieves Town Exit', + 'Skull Woods Final Section Exit', + 'Palace of Darkness Exit', + 'Swamp Palace Exit'] + Cave_Exits_Base = [('Elder House Exit (East)', 'Elder House Exit (West)'), ('Two Brothers House Exit (East)', 'Two Brothers House Exit (West)'), ('Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)'), From a34b2b3674476454b552b97f4ff872e6faa1dc7d Mon Sep 17 00:00:00 2001 From: codemann8 Date: Mon, 25 Oct 2021 13:05:21 -0500 Subject: [PATCH 55/76] Fixed issue with inaccessible regions not getting connected --- EntranceShuffle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 6b917f9f..794f1bf8 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -1522,7 +1522,7 @@ def connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, playe if len(inaccessible_entrances): random.shuffle(inaccessible_entrances) connect_mandatory_exits(world, pool, caves, [inaccessible_entrances.pop()], player) - connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, player) + connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, player) # connect one connector at a time to ensure multiple connectors aren't assigned to the same inaccessible set of regions if world.shuffle[player] in ['liteplus', 'crossed', 'insanity']: From 0d46f40241a9340b53ee252f29f9b368d09bf37d Mon Sep 17 00:00:00 2001 From: codemann8 Date: Mon, 25 Oct 2021 13:22:42 -0500 Subject: [PATCH 56/76] Renamed Lite+ ER to Lean ER --- BaseClasses.py | 4 ++-- EntranceShuffle.py | 16 ++++++++-------- ItemList.py | 2 +- OverworldShuffle.py | 4 ++-- Rom.py | 2 +- Rules.py | 2 +- resources/app/cli/args.json | 2 +- resources/app/gui/lang/en.json | 2 +- .../app/gui/randomize/entrando/widgets.json | 2 +- 9 files changed, 18 insertions(+), 18 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 19166478..9f75f702 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -120,7 +120,7 @@ class World(object): set_player_attr('can_access_trock_front', None) set_player_attr('can_access_trock_big_chest', None) set_player_attr('can_access_trock_middle', None) - set_player_attr('fix_fake_world', logic[player] not in ['owglitches', 'nologic'] or shuffle[player] in ['lite', 'liteplus', 'crossed', 'insanity', 'madness_legacy']) + set_player_attr('fix_fake_world', logic[player] not in ['owglitches', 'nologic'] or shuffle[player] in ['lite', 'lean', 'crossed', 'insanity', 'madness_legacy']) set_player_attr('mapshuffle', False) set_player_attr('compassshuffle', False) set_player_attr('keyshuffle', False) @@ -2966,7 +2966,7 @@ class Pot(object): # byte 0: DDOO OEEE (DR, OR, ER) dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0} or_mode = {"vanilla": 0, "parallel": 1, "full": 1} -er_mode = {"vanilla": 0, "simple": 1, "restricted": 2, "full": 3, "lite": 4, "liteplus": 5, "crossed": 6, "insanity": 7, "dungeonsfull": 8, "dungeonssimple": 9} +er_mode = {"vanilla": 0, "simple": 1, "restricted": 2, "full": 3, "lite": 4, "lean": 5, "crossed": 6, "insanity": 7, "dungeonsfull": 8, "dungeonssimple": 9} # byte 1: LLLW WSSR (logic, mode, sword, retro) logic_mode = {"noglitches": 0, "minorglitches": 1, "nologic": 2, "owglitches": 3, "majorglitches": 4} diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 794f1bf8..f852a3f1 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -456,7 +456,7 @@ def link_entrances(world, player): # place remaining doors connect_doors(world, list(entrance_pool), list(exit_pool), player) - elif world.shuffle[player] == 'liteplus': + elif world.shuffle[player] == 'lean': for entrancename, exitname in default_connections + ([] if world.shopsanity[player] else default_shop_connections): connect_logical(world, entrancename, exitname, player, False) if invFlag: @@ -1055,7 +1055,7 @@ def scramble_holes(world, player): hole_targets.append(('Pyramid Exit', 'Pyramid')) # shuffle sanctuary hole in same world as other HC entrances - if world.shuffle[player] not in ['liteplus', 'crossed']: + if world.shuffle[player] not in ['lean', 'crossed']: drop_owid_map = { # owid, is_light_world 'Lost Woods Hideout Stump': (0x00, True), 'Lumberjack Tree Cave': (0x02, True), @@ -1365,7 +1365,7 @@ def place_links_house(world, sectors, player): links_house_doors = [i for i in get_distant_entrances(world, dark_sanc, sectors, player) if i in entrance_pool] else: links_house_doors = [i for i in get_starting_entrances(world, sectors, player) if i in entrance_pool] - if world.shuffle[player] in ['lite', 'liteplus']: + if world.shuffle[player] in ['lite', 'lean']: links_house_doors = [e for e in links_house_doors if e in list(zip(*(default_item_connections + (default_shop_connections if world.shopsanity[player] else []))))[0]] links_house = random.choice(links_house_doors) connect_two_way(world, links_house, 'Links House Exit', player) @@ -1377,7 +1377,7 @@ def place_dark_sanc(world, sectors, player): sanc_doors = [i for i in get_distant_entrances(world, 'Big Bomb Shop', sectors, player) if i in entrance_pool] else: sanc_doors = [i for i in get_starting_entrances(world, sectors, player) if i in entrance_pool] - if world.shuffle[player] in ['lite', 'liteplus']: + if world.shuffle[player] in ['lite', 'lean']: sanc_doors = [e for e in sanc_doors if e in list(zip(*(default_item_connections + (default_shop_connections if world.shopsanity[player] else []))))[0]] sanc_door = random.choice(sanc_doors) connect_entrance(world, sanc_door, 'Dark Sanctuary Hint', player) @@ -1402,7 +1402,7 @@ def place_blacksmith(world, links_house, player): elif world.doorShuffle[player] == 'vanilla' or world.intensity[player] < 3: sanc_region = world.get_entrance('Sanctuary Exit', player).connected_region.name blacksmith_doors = list(set(blacksmith_doors + list(build_accessible_entrance_list(world, sanc_region, player, assumed_inventory, False, True, True)))) - if world.shuffle[player] in ['lite', 'liteplus']: + if world.shuffle[player] in ['lite', 'lean']: blacksmith_doors = [e for e in blacksmith_doors if e in list(zip(*(default_item_connections + (default_shop_connections if world.shopsanity[player] else []))))[0]] random.shuffle(blacksmith_doors) @@ -1419,7 +1419,7 @@ def place_old_man(world, pool, player): region_name = 'West Dark Death Mountain (Top)' old_man_entrances = list(build_accessible_entrance_list(world, region_name, player, [], False, True, True, True)) old_man_entrances = [e for e in old_man_entrances if e != 'Old Man House (Bottom)'] - if world.shuffle[player] in ['lite', 'liteplus']: + if world.shuffle[player] in ['lite', 'lean']: old_man_entrances = [e for e in old_man_entrances if e in pool] random.shuffle(old_man_entrances) old_man_exit = None @@ -1517,7 +1517,7 @@ def connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, playe inaccessible_entrances = list() region = world.get_region(region_name, player) for exit in region.exits: - if not exit.connected_region and exit.name in entrance_pool and (world.shuffle[player] not in ['lite', 'liteplus'] or exit.name in pool): + if not exit.connected_region and exit.name in entrance_pool and (world.shuffle[player] not in ['lite', 'lean'] or exit.name in pool): inaccessible_entrances.append(exit.name) if len(inaccessible_entrances): random.shuffle(inaccessible_entrances) @@ -1525,7 +1525,7 @@ def connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, playe connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, player) # connect one connector at a time to ensure multiple connectors aren't assigned to the same inaccessible set of regions - if world.shuffle[player] in ['liteplus', 'crossed', 'insanity']: + if world.shuffle[player] in ['lean', 'crossed', 'insanity']: combined_must_exit_regions = list(must_exit_regions + otherworld_must_exit_regions) if len(combined_must_exit_regions) > 0: random.shuffle(combined_must_exit_regions) diff --git a/ItemList.py b/ItemList.py index 3bea81da..7eb5a11e 100644 --- a/ItemList.py +++ b/ItemList.py @@ -709,7 +709,7 @@ def balance_prices(world, player): def check_hints(world, player): - if world.shuffle[player] in ['simple', 'restricted', 'full', 'lite', 'liteplus', 'crossed', 'insanity']: + if world.shuffle[player] in ['simple', 'restricted', 'full', 'lite', 'lean', 'crossed', 'insanity']: for shop, location_list in shop_to_location_table.items(): if shop in ['Capacity Upgrade', 'Light World Death Mountain Shop', 'Potion Shop']: continue # near the queen, near potions, and near 7 chests are fine diff --git a/OverworldShuffle.py b/OverworldShuffle.py index dd5a161c..be133079 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -428,7 +428,7 @@ def reorganize_tile_groups(world, player): groups = {} for (name, groupType) in OWTileGroups.keys(): if world.mode[player] != 'standard' or name not in ['Castle', 'Links', 'Central Bonk Rocks'] \ - or (world.mode[player] == 'standard' and world.shuffle[player] in ['lite', 'liteplus', 'crossed', 'insanity'] and name == 'Castle' and groupType == 'Entrance'): + or (world.mode[player] == 'standard' and world.shuffle[player] in ['lite', 'lean', 'crossed', 'insanity'] and name == 'Castle' and groupType == 'Entrance'): if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'simple', 'restricted']: groups[(name,)] = ([], [], []) else: @@ -436,7 +436,7 @@ def reorganize_tile_groups(world, player): for (name, groupType) in OWTileGroups.keys(): if world.mode[player] != 'standard' or name not in ['Castle', 'Links', 'Central Bonk Rocks'] \ - or (world.mode[player] == 'standard' and world.shuffle[player] in ['lite', 'liteplus', 'crossed', 'insanity'] and name == 'Castle' and groupType == 'Entrance'): + or (world.mode[player] == 'standard' and world.shuffle[player] in ['lite', 'lean', 'crossed', 'insanity'] and name == 'Castle' and groupType == 'Entrance'): (lw_owids, dw_owids) = OWTileGroups[(name, groupType,)] if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'simple', 'restricted']: (exist_owids, exist_lw_regions, exist_dw_regions) = groups[(name,)] diff --git a/Rom.py b/Rom.py index 4abf7f38..58cd90bc 100644 --- a/Rom.py +++ b/Rom.py @@ -1603,7 +1603,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_bytes(0x02F539, [0xEA, 0xEA, 0xEA, 0xEA, 0xEA] if world.powder_patch_required[player] else [0xAD, 0xBF, 0x0A, 0xF0, 0x4F]) # allow smith into multi-entrance caves in appropriate shuffles - if world.shuffle[player] in ['restricted', 'full', 'lite', 'liteplus', 'crossed', 'insanity'] or (world.shuffle[player] == 'simple' and world.mode[player] == 'inverted'): + if world.shuffle[player] in ['restricted', 'full', 'lite', 'lean', 'crossed', 'insanity'] or (world.shuffle[player] == 'simple' and world.mode[player] == 'inverted'): rom.write_byte(0x18004C, 0x01) # set correct flag for hera basement item diff --git a/Rules.py b/Rules.py index a92ed288..ef61752b 100644 --- a/Rules.py +++ b/Rules.py @@ -848,7 +848,7 @@ def ow_rules(world, player): if world.mode[player] != 'inverted': set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has_beam_sword(player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle set_rule(world.get_entrance('GT Entry Approach', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player)) - set_rule(world.get_entrance('GT Entry Leave', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player) or state.world.shuffle[player] in ('restricted', 'full', 'lite', 'liteplus', 'crossed', 'insanity')) + set_rule(world.get_entrance('GT Entry Leave', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player) or state.world.shuffle[player] in ('restricted', 'full', 'lite', 'lean', 'crossed', 'insanity')) else: set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player)) diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index d9a9f294..97fb2612 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -147,7 +147,7 @@ "restricted", "full", "lite", - "liteplus", + "lean", "crossed", "insanity", "dungeonsfull", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index c76700b3..7a8f3191 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -141,7 +141,7 @@ "randomizer.entrance.entranceshuffle.restricted": "Restricted", "randomizer.entrance.entranceshuffle.full": "Full", "randomizer.entrance.entranceshuffle.lite": "Lite", - "randomizer.entrance.entranceshuffle.liteplus": "Lite+", + "randomizer.entrance.entranceshuffle.lean": "Lean", "randomizer.entrance.entranceshuffle.crossed": "Crossed", "randomizer.entrance.entranceshuffle.insanity": "Insanity", "randomizer.entrance.entranceshuffle.restricted_legacy": "Restricted (Legacy)", diff --git a/resources/app/gui/randomize/entrando/widgets.json b/resources/app/gui/randomize/entrando/widgets.json index ff189cb4..ff30506b 100644 --- a/resources/app/gui/randomize/entrando/widgets.json +++ b/resources/app/gui/randomize/entrando/widgets.json @@ -11,7 +11,7 @@ "restricted", "full", "lite", - "liteplus", + "lean", "crossed", "insanity", "dungeonsfull", From 84793d784aecd6ae5958b45a0b5b41751f3c63ab Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 26 Oct 2021 13:51:15 -0500 Subject: [PATCH 57/76] Moved flute icon location for clarity --- OverworldShuffle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index be133079..d5f82cb9 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -1503,7 +1503,7 @@ flute_data = { 0x2e: (['Tree Line Area', 'Dark Tree Line Area'], 0x2e, 0x0100, 0x0a1a, 0x0c00, 0x0a78, 0x0c30, 0x0a87, 0x0c7d, 0x0006, 0x0000, 0x0a78, 0x0c58), 0x2f: (['Eastern Nook Area', 'Palace of Darkness Nook Area'], 0x2f, 0x0798, 0x0afa, 0x0eb2, 0x0b58, 0x0f30, 0x0b67, 0x0f37, 0xfff6, 0x000e, 0x0b50, 0x0f30), 0x38: (['Desert Palace Teleporter Ledge', 'Misery Mire Teleporter Ledge'], 0x30, 0x1880, 0x0f1e, 0x0000, 0x0fa8, 0x0078, 0x0f8d, 0x008d, 0x0000, 0x0000, 0x0fb0, 0x0070), - 0x32: (['Flute Boy Approach Area', 'Stumpy Approach Area'], 0x32, 0x03a0, 0x0c6c, 0x0500, 0x0cd0, 0x05a8, 0x0cdb, 0x0585, 0x0002, 0x0000, 0x0cd6, 0x05a8), + 0x32: (['Flute Boy Approach Area', 'Stumpy Approach Area'], 0x32, 0x03a0, 0x0c6c, 0x0500, 0x0cd0, 0x05a8, 0x0cdb, 0x0585, 0x0002, 0x0000, 0x0cd6, 0x0568), 0x33: (['C Whirlpool Outer Area', 'Dark C Whirlpool Outer Area'], 0x33, 0x0180, 0x0c20, 0x0600, 0x0c80, 0x0628, 0x0c8f, 0x067d, 0x0000, 0x0000, 0x0c80, 0x0628), 0x34: (['Statues Area', 'Hype Cave Area'], 0x34, 0x088e, 0x0d00, 0x0866, 0x0d60, 0x08d8, 0x0d6f, 0x08e3, 0x0000, 0x000a, 0x0d60, 0x08d8), #0x35: (['Lake Hylia Area', 'Ice Lake Area'], 0x35, 0x0d00, 0x0da6, 0x0a06, 0x0e08, 0x0a80, 0x0e13, 0x0a8b, 0xfffa, 0xfffa, 0x0d88, 0x0a88), From 92e272e617307021c23b2a349ca18fd5dd54e46a Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 26 Oct 2021 13:52:32 -0500 Subject: [PATCH 58/76] Efficiency improvement of ER --- EntranceShuffle.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index f852a3f1..949e21b8 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -1662,20 +1662,21 @@ def build_accessible_region_list(world, start_region, player, build_copy_world=F from Main import copy_world from Items import ItemFactory - def explore_region(region_name): + def explore_region(region_name, region=None): explored_regions.add(region_name) - region = base_world.get_region(region_name, player) + if not region: + region = base_world.get_region(region_name, player) for exit in region.exits: if exit.connected_region is not None: if any(map(lambda i: i.name == 'Ocarina', base_world.precollected_items)) and exit.spot_type == 'Flute': fluteregion = exit.connected_region for flutespot in fluteregion.exits: if flutespot.connected_region and flutespot.connected_region.name not in explored_regions: - explore_region(flutespot.connected_region.name) + explore_region(flutespot.connected_region.name, flutespot.connected_region) elif exit.connected_region.name not in explored_regions \ and (exit.connected_region.type == region.type or (cross_world and exit.connected_region.type in [RegionType.LightWorld, RegionType.DarkWorld])) \ and (not region_rules or exit.access_rule(blank_state)) and (not ignore_ledges or exit.spot_type != 'Ledge'): - explore_region(exit.connected_region.name) + explore_region(exit.connected_region.name, exit.connected_region) if build_copy_world: for player in range(1, world.players + 1): From a6be79c4a163a6b547959e704a7f40e677bd424e Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 26 Oct 2021 14:29:03 -0500 Subject: [PATCH 59/76] Version bump 0.2.0.0 --- CHANGELOG.md | 8 ++++++++ OverworldShuffle.py | 2 +- README.md | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43071f28..f0bb9599 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +### 0.2.0.0 +- Massive overhaul of ER algorithm +- Added 2 new ER modes (Lite and Lean) +- Added new mystery options (Logic/Shuffle Ganon) +- Smith deletion on S+Q only occurs if Blacksmith not reachable from starting locations +- Various minor fixes and improvements +- ~~Merged DR v0.5.1.4 - ROM bug fixes/keylogic improvements~~ + ### 0.1.9.4 - Hotfix for bad 0.1.9.3 version diff --git a/OverworldShuffle.py b/OverworldShuffle.py index d5f82cb9..89ada285 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -3,7 +3,7 @@ from BaseClasses import OWEdge, WorldType, RegionType, Direction, Terrain, PolSl from Regions import mark_dark_world_regions, mark_light_world_regions from OWEdges import OWTileRegions, OWTileGroups, OWEdgeGroups, OWExitTypes, OpenStd, parallel_links, IsParallel -__version__ = '0.1.9.4-u' +__version__ = '0.2.0.0-u' def link_overworld(world, player): # setup mandatory connections diff --git a/README.md b/README.md index d9e5e15f..edd0fd79 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,42 @@ New flute spots are chosen at random, with restrictions that limit the promixity New flute spots are chosen at random with minimum bias. +## New Entrance Shuffle Options (--shuffle) + +### Lite + +This mode is intended to be a beginner-friendly introduction to playing ER. It focuses on reducing low% world traversal in late-game dungeons while reducing the number of entrances needing to be checked. + +This mode groups entrances into types and shuffles them freely within those groups. +- Dungeons and Connectors (Multi-Entrance Caves) +- Item Locations (Single-Entrance Caves with an item, includes Potion Shop and Red Bomb Shop, includes Shops only if Shopsanity is enabled) +- Dropdowns and their associated exits (Skull Woods dropdowns are handled the same as in Crossed) +- Non-item locations (junk locations) all remain vanilla + +Lite mode shuffles all connectors same-world, to limit bunny traversal. And to prevent Low% enemy and boss combat, some dungeons are confined to specific worlds. + +The following dungeons are guaranteed to be in the Light World: +- Hyrule Castle +- Eastern Palace +- Desert Palace +- Tower of Hera +- Agahnim's Tower + +The following are guaranteed to be in the Dark World: +- Ice Palace +- Misery Mire +- Turtle Rock +- Ganon's Tower + +### Lean + +This mode is intended to be a more refined and more competitive format to Crossed ER. It focuses on reducing the number of entrances needing to be checked, while giving the player unique routing options based on the entrance pools defined below, as opposed to mindlessly checking all the remaining entrances. The Dungeons/Connectors can connect cross-world. + +This mode groups entrances into types and shuffles them freely within those groups. +- Dungeons and Connectors (Multi-Entrance Caves) +- Item Locations (Single-Entrance Caves with an item, includes Potion Shop and Red Bomb Shop, includes Shops only if Shopsanity is enabled) +- Dropdowns and their associated exits (Skull Woods dropdowns are handled the same as in Crossed) +- Non-item locations (junk locations) all remain vanilla # Command Line Options From 32d6a55838be2a46b6e446cbf4171edf1b9e0506 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 26 Oct 2021 16:04:41 -0500 Subject: [PATCH 60/76] Changelog update --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0bb9599..f56cf7a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Added 2 new ER modes (Lite and Lean) - Added new mystery options (Logic/Shuffle Ganon) - Smith deletion on S+Q only occurs if Blacksmith not reachable from starting locations +- Spoiler log improvements to prevent spoiling in the beginning 'meta' section - Various minor fixes and improvements - ~~Merged DR v0.5.1.4 - ROM bug fixes/keylogic improvements~~ From 08ab32537bd6fae1ae0b2c44c928b8acd850a6e2 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 27 Oct 2021 01:19:01 -0500 Subject: [PATCH 61/76] Implemented Whirlpool Shuffle --- BaseClasses.py | 6 + CLI.py | 3 +- Main.py | 2 + Mystery.py | 1 + OWEdges.py | 104 +++++++---- OverworldShuffle.py | 166 +++++++++++++----- Rom.py | 7 +- data/base2current.bps | Bin 141069 -> 141107 bytes mystery_example.yml | 3 + resources/app/cli/args.json | 4 + resources/app/cli/lang/en.json | 3 + resources/app/gui/lang/en.json | 5 + .../app/gui/randomize/overworld/widgets.json | 4 + source/classes/constants.py | 1 + source/gui/randomize/overworld.py | 2 +- 15 files changed, 225 insertions(+), 86 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 9f75f702..b6484e83 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -26,6 +26,7 @@ class World(object): self.owCrossed = owCrossed.copy() self.owKeepSimilar = {} self.owMixed = owMixed.copy() + self.owWhirlpoolShuffle = {} self.owFluteShuffle = {} self.shuffle = shuffle.copy() self.doorShuffle = doorShuffle.copy() @@ -76,6 +77,7 @@ class World(object): self.spoiler = Spoiler(self) self.lamps_needed_for_dark_rooms = 1 self.owswaps = {} + self.owwhirlpools = {} self.owedges = [] self._owedge_cache = {} self.owflutespots = {} @@ -105,6 +107,7 @@ class World(object): set_player_attr('_region_cache', {}) set_player_attr('player_names', []) set_player_attr('owswaps', [[],[],[]]) + set_player_attr('owwhirlpools', []) set_player_attr('remote_items', False) set_player_attr('required_medallions', ['Ether', 'Quake']) set_player_attr('swamp_patch_required', False) @@ -2693,6 +2696,7 @@ class Spoiler(object): 'ow_crossed': self.world.owCrossed, 'ow_keepsimilar': self.world.owKeepSimilar, 'ow_mixed': self.world.owMixed, + 'ow_whirlpool': self.world.owWhirlpoolShuffle, 'ow_fluteshuffle': self.world.owFluteShuffle, 'shuffle': self.world.shuffle, 'shuffleganon': self.world.shuffle_ganon, @@ -2784,6 +2788,7 @@ class Spoiler(object): outfile.write('Keep Similar OW Edges Together:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['ow_keepsimilar'][player] else 'No')) outfile.write('Crossed OW:'.ljust(line_width) + '%s\n' % self.metadata['ow_crossed'][player]) outfile.write('Swapped OW (Mixed):'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['ow_mixed'][player] else 'No')) + outfile.write('Whirlpool Shuffle:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['ow_whirlpool'][player] else 'No')) outfile.write('Flute Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_fluteshuffle'][player]) outfile.write('Entrance Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['shuffle'][player]) outfile.write('Shuffle GT/Ganon:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['shuffleganon'][player] else 'No')) @@ -2807,6 +2812,7 @@ class Spoiler(object): if self.startinventory: outfile.write('Starting Inventory:'.ljust(line_width)) outfile.write('\n'.ljust(line_width+1).join(self.startinventory)) + outfile.write('\n\nRequirements:\n\n') for dungeon, medallion in self.medallions.items(): outfile.write(f'{dungeon}:'.ljust(line_width) + '%s Medallion\n' % medallion) diff --git a/CLI.py b/CLI.py index 41b812f8..198325b7 100644 --- a/CLI.py +++ b/CLI.py @@ -94,7 +94,7 @@ def parse_cli(argv, no_defaults=False): playerargs = parse_cli(shlex.split(getattr(ret, f"p{player}")), True) for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', - 'ow_shuffle', 'ow_crossed', 'ow_keepsimilar', 'ow_mixed', 'ow_fluteshuffle', + 'ow_shuffle', 'ow_crossed', 'ow_keepsimilar', 'ow_mixed', 'ow_whirlpool', 'ow_fluteshuffle', 'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', 'bombbag', 'shuffleganon', @@ -149,6 +149,7 @@ def parse_settings(): "ow_crossed": "none", "ow_keepsimilar": False, "ow_mixed": False, + "ow_whirlpool": False, "ow_fluteshuffle": "vanilla", "shuffle": "vanilla", "shufflelinks": False, diff --git a/Main.py b/Main.py index 34bcc656..9827efb1 100644 --- a/Main.py +++ b/Main.py @@ -87,6 +87,7 @@ def main(args, seed=None, fish=None): world.crystals_ganon_orig = args.crystals_ganon.copy() world.crystals_gt_orig = args.crystals_gt.copy() world.owKeepSimilar = args.ow_keepsimilar.copy() + world.owWhirlpoolShuffle = args.ow_whirlpool.copy() world.owFluteShuffle = args.ow_fluteshuffle.copy() world.open_pyramid = args.openpyramid.copy() world.boss_shuffle = args.shufflebosses.copy() @@ -406,6 +407,7 @@ def copy_world(world): ret.crystals_ganon_orig = world.crystals_ganon_orig.copy() ret.crystals_gt_orig = world.crystals_gt_orig.copy() ret.owKeepSimilar = world.owKeepSimilar.copy() + ret.owWhirlpoolShuffle = world.owWhirlpoolShuffle.copy() ret.owFluteShuffle = world.owFluteShuffle.copy() ret.open_pyramid = world.open_pyramid.copy() ret.boss_shuffle = world.boss_shuffle.copy() diff --git a/Mystery.py b/Mystery.py index 933a4fc1..8f1b5158 100644 --- a/Mystery.py +++ b/Mystery.py @@ -138,6 +138,7 @@ def roll_settings(weights): ret.ow_crossed = get_choice('overworld_crossed') ret.ow_keepsimilar = get_choice('overworld_keepsimilar') == 'on' ret.ow_mixed = get_choice('overworld_swap') == 'on' + ret.ow_whirlpool = get_choice('whirlpool_shuffle') == 'on' overworld_flute = get_choice('flute_shuffle') ret.ow_fluteshuffle = overworld_flute if overworld_flute != 'none' else 'vanilla' entrance_shuffle = get_choice('entrance_shuffle') diff --git a/OWEdges.py b/OWEdges.py index cfaa9c2c..b9064281 100644 --- a/OWEdges.py +++ b/OWEdges.py @@ -969,7 +969,7 @@ OWTileRegions = bidict({ }) OWTileGroups = { - ("Woods", "Regular"): ( + ("Woods", "Regular", "None"): ( [ 0x00, 0x2d, 0x80 ], @@ -977,7 +977,7 @@ OWTileGroups = { 0x40, 0x6d ] ), - ("Lumberjack", "Regular"): ( + ("Lumberjack", "Regular", "None"): ( [ 0x02 ], @@ -985,7 +985,7 @@ OWTileGroups = { 0x42 ] ), - ("West Mountain", "Regular"): ( + ("West Mountain", "Regular", "None"): ( [ 0x03 ], @@ -993,7 +993,7 @@ OWTileGroups = { 0x43 ] ), - ("East Mountain", "Regular"): ( + ("East Mountain", "Regular", "None"): ( [ 0x05 ], @@ -1001,23 +1001,31 @@ OWTileGroups = { 0x45 ] ), - ("East Mountain", "Entrance"): ( + ("East Mountain", "Entrance", "None"): ( [ - 0x07, + 0x07 ], [ 0x47 ] ), - ("Lake", "Regular"): ( + ("Lake", "Regular", "Zora"): ( [ - 0x0f, 0x35, 0x81 + 0x0f, 0x81 ], [ - 0x4f, 0x75 + 0x4f ] ), - ("Mountain Entry", "Regular"): ( + ("Lake", "Regular", "Lake"): ( + [ + 0x35 + ], + [ + 0x75 + ] + ), + ("Mountain Entry", "Regular", "None"): ( [ 0x0a ], @@ -1025,7 +1033,7 @@ OWTileGroups = { 0x4a ] ), - ("Woods Pass", "Regular"): ( + ("Woods Pass", "Regular", "None"): ( [ 0x10 ], @@ -1033,7 +1041,7 @@ OWTileGroups = { 0x50 ] ), - ("Fortune", "Regular"): ( + ("Fortune", "Regular", "None"): ( [ 0x11 ], @@ -1041,15 +1049,39 @@ OWTileGroups = { 0x51 ] ), - ("Whirlpools", "Regular"): ( + ("Whirlpools", "Regular", "Pond"): ( [ - 0x12, 0x15, 0x33, 0x3f + 0x12 ], [ - 0x52, 0x55, 0x73, 0x7f + 0x52 ] ), - ("Castle", "Entrance"): ( + ("Whirlpools", "Regular", "Witch"): ( + [ + 0x15 + ], + [ + 0x55 + ] + ), + ("Whirlpools", "Regular", "CWhirlpool"): ( + [ + 0x33 + ], + [ + 0x73 + ] + ), + ("Whirlpools", "Regular", "Southeast"): ( + [ + 0x3f + ], + [ + 0x7f + ] + ), + ("Castle", "Entrance", "None"): ( [ 0x13, 0x14 ], @@ -1057,7 +1089,7 @@ OWTileGroups = { 0x53, 0x54 ] ), - ("Castle", "Regular"): ( + ("Castle", "Regular", "None"): ( [ 0x1a, 0x1b ], @@ -1065,7 +1097,7 @@ OWTileGroups = { 0x5a, 0x5b ] ), - ("Witch", "Regular"): ( + ("Witch", "Regular", "None"): ( [ 0x16 ], @@ -1073,7 +1105,7 @@ OWTileGroups = { 0x56 ] ), - ("Water Approach", "Regular"): ( + ("Water Approach", "Regular", "None"): ( [ 0x17 ], @@ -1081,7 +1113,7 @@ OWTileGroups = { 0x57 ] ), - ("Village", "Regular"): ( + ("Village", "Regular", "None"): ( [ 0x18 ], @@ -1089,7 +1121,7 @@ OWTileGroups = { 0x58 ] ), - ("Wooden Bridge", "Regular"): ( + ("Wooden Bridge", "Regular", "None"): ( [ 0x1d ], @@ -1097,7 +1129,7 @@ OWTileGroups = { 0x5d ] ), - ("Eastern", "Regular"): ( + ("Eastern", "Regular", "None"): ( [ 0x1e ], @@ -1105,7 +1137,7 @@ OWTileGroups = { 0x5e ] ), - ("Blacksmith", "Regular"): ( + ("Blacksmith", "Regular", "None"): ( [ 0x22 ], @@ -1113,7 +1145,7 @@ OWTileGroups = { 0x62 ] ), - ("Dunes", "Regular"): ( + ("Dunes", "Regular", "None"): ( [ 0x25 ], @@ -1121,7 +1153,7 @@ OWTileGroups = { 0x65 ] ), - ("Game", "Regular"): ( + ("Game", "Regular", "None"): ( [ 0x28, 0x29 ], @@ -1129,7 +1161,7 @@ OWTileGroups = { 0x68, 0x69 ] ), - ("Grove", "Regular"): ( + ("Grove", "Regular", "None"): ( [ 0x2a ], @@ -1137,7 +1169,7 @@ OWTileGroups = { 0x6a ] ), - ("Central Bonk Rocks", "Regular"): ( + ("Central Bonk Rocks", "Regular", "None"): ( [ 0x2b ], @@ -1145,7 +1177,7 @@ OWTileGroups = { 0x6b ] ), - # ("Links", "Regular"): ( + # ("Links", "Regular", "None"): ( # [ # 0x2c # ], @@ -1153,7 +1185,7 @@ OWTileGroups = { # 0x6c # ] # ), - ("Tree Line", "Regular"): ( + ("Tree Line", "Regular", "None"): ( [ 0x2e ], @@ -1161,7 +1193,7 @@ OWTileGroups = { 0x6e ] ), - ("Nook", "Regular"): ( + ("Nook", "Regular", "None"): ( [ 0x2f ], @@ -1169,7 +1201,7 @@ OWTileGroups = { 0x6f ] ), - ("Desert", "Regular"): ( + ("Desert", "Regular", "None"): ( [ 0x30, 0x3a ], @@ -1177,7 +1209,7 @@ OWTileGroups = { 0x70, 0x7a ] ), - ("Grove Approach", "Regular"): ( + ("Grove Approach", "Regular", "None"): ( [ 0x32 ], @@ -1185,7 +1217,7 @@ OWTileGroups = { 0x72 ] ), - ("Hype", "Regular"): ( + ("Hype", "Regular", "None"): ( [ 0x34 ], @@ -1193,7 +1225,7 @@ OWTileGroups = { 0x74 ] ), - ("Shopping Mall", "Regular"): ( + ("Shopping Mall", "Regular", "None"): ( [ 0x37 ], @@ -1201,7 +1233,7 @@ OWTileGroups = { 0x77 ] ), - ("Swamp", "Regular"): ( + ("Swamp", "Regular", "None"): ( [ 0x3b ], @@ -1209,7 +1241,7 @@ OWTileGroups = { 0x7b ] ), - ("South Pass", "Regular"): ( + ("South Pass", "Regular", "None"): ( [ 0x3c ], diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 89ada285..9ed24210 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -187,6 +187,44 @@ def link_overworld(world, player): trimmed_groups = performSwap(trimmed_groups, crossed_edges) assert len(crossed_edges) == 0, 'Not all edges were crossed successfully: ' + ', '.join(crossed_edges) + # whirlpool shuffle + logging.getLogger('').debug('Shuffling whirlpools') + + if not world.owWhirlpoolShuffle[player]: + for (_, from_whirlpool, from_region), (_, to_whirlpool, to_region) in default_whirlpool_connections: + connect_simple(world, from_whirlpool, to_region, player) + connect_simple(world, to_whirlpool, from_region, player) + else: + whirlpool_candidates = [[],[]] + for (from_owid, from_whirlpool, from_region), (to_owid, to_whirlpool, to_region) in default_whirlpool_connections: + if world.owCrossed[player] != 'none': + whirlpool_candidates[0].append(tuple((from_owid, from_whirlpool, from_region))) + whirlpool_candidates[0].append(tuple((to_owid, to_whirlpool, to_region))) + else: + if world.get_region(from_region, player).type == RegionType.LightWorld: + whirlpool_candidates[0].append(tuple((from_owid, from_whirlpool, from_region))) + else: + whirlpool_candidates[1].append(tuple((from_owid, from_whirlpool, from_region))) + + if world.get_region(to_region, player).type == RegionType.LightWorld: + whirlpool_candidates[0].append(tuple((to_owid, to_whirlpool, to_region))) + else: + whirlpool_candidates[1].append(tuple((to_owid, to_whirlpool, to_region))) + + # shuffle happens here + world.owwhirlpools[player] = [None] * 8 + whirlpool_map = [ 0x35, 0x0f, 0x15, 0x33, 0x12, 0x3f, 0x55, 0x7f ] + for whirlpools in whirlpool_candidates: + random.shuffle(whirlpools) + while len(whirlpools): + from_owid, from_whirlpool, from_region = whirlpools.pop() + to_owid, to_whirlpool, to_region = whirlpools.pop() + connect_simple(world, from_whirlpool, to_region, player) + connect_simple(world, to_whirlpool, from_region, player) + world.owwhirlpools[player][next(i for i, v in enumerate(whirlpool_map) if v == to_owid)] = from_owid + world.owwhirlpools[player][next(i for i, v in enumerate(whirlpool_map) if v == from_owid)] = to_owid + world.spoiler.set_overworld(from_whirlpool, to_whirlpool, 'both', player) + # layout shuffle logging.getLogger('').debug('Shuffling overworld layout') connected_edges = [] @@ -387,22 +425,33 @@ def connect_two_way(world, edgename1, edgename2, player, connected_edges=None): def shuffle_tiles(world, groups, result_list, player): swapped_edges = list() + valid_whirlpool_parity = False - # tile shuffle happens here - removed = list() - for group in groups.keys(): - if random.randint(0, 1): - removed.append(group) - - # save shuffled tiles to list - for group in groups.keys(): - if group not in removed: - (owids, lw_regions, dw_regions) = groups[group] - (exist_owids, exist_lw_regions, exist_dw_regions) = result_list - exist_owids.extend(owids) - exist_lw_regions.extend(lw_regions) - exist_dw_regions.extend(dw_regions) - result_list = [exist_owids, exist_lw_regions, exist_dw_regions] + while not valid_whirlpool_parity: + # tile shuffle happens here + removed = list() + for group in groups.keys(): + # if group[0] in ['Links', 'Central Bonk Rocks', 'Castle']: # TODO: Standard + Inverted + if random.randint(0, 1): + removed.append(group) + + # save shuffled tiles to list + new_results = [[],[],[]] + for group in groups.keys(): + if group not in removed: + (owids, lw_regions, dw_regions) = groups[group] + (exist_owids, exist_lw_regions, exist_dw_regions) = new_results + exist_owids.extend(owids) + exist_lw_regions.extend(lw_regions) + exist_dw_regions.extend(dw_regions) + + # check whirlpool parity + valid_whirlpool_parity = world.owCrossed[player] != 'none' or len(set(new_results[0]) & set({0x0f, 0x12, 0x15, 0x33, 0x35, 0x3f, 0x55, 0x7f})) % 2 == 0 + + (exist_owids, exist_lw_regions, exist_dw_regions) = result_list + exist_owids.extend(new_results[0]) + exist_lw_regions.extend(new_results[1]) + exist_dw_regions.extend(new_results[2]) # replace LW edges with DW ignore_list = list() #TODO: Remove ignore_list when special OW areas are included in pool @@ -426,36 +475,62 @@ def shuffle_tiles(world, groups, result_list, player): def reorganize_tile_groups(world, player): groups = {} - for (name, groupType) in OWTileGroups.keys(): + for (name, groupType, whirlpoolGroup) in OWTileGroups.keys(): if world.mode[player] != 'standard' or name not in ['Castle', 'Links', 'Central Bonk Rocks'] \ or (world.mode[player] == 'standard' and world.shuffle[player] in ['lite', 'lean', 'crossed', 'insanity'] and name == 'Castle' and groupType == 'Entrance'): if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'simple', 'restricted']: - groups[(name,)] = ([], [], []) + if world.owWhirlpoolShuffle[player] or world.owCrossed[player] != 'none': + groups[(name, whirlpoolGroup)] = ([], [], []) + else: + groups[(name,)] = ([], [], []) else: - groups[(name, groupType)] = ([], [], []) + if world.owWhirlpoolShuffle[player] or world.owCrossed[player] != 'none': + groups[(name, groupType, whirlpoolGroup)] = ([], [], []) + else: + groups[(name, groupType)] = ([], [], []) - for (name, groupType) in OWTileGroups.keys(): + for (name, groupType, whirlpoolGroup) in OWTileGroups.keys(): if world.mode[player] != 'standard' or name not in ['Castle', 'Links', 'Central Bonk Rocks'] \ or (world.mode[player] == 'standard' and world.shuffle[player] in ['lite', 'lean', 'crossed', 'insanity'] and name == 'Castle' and groupType == 'Entrance'): - (lw_owids, dw_owids) = OWTileGroups[(name, groupType,)] + (lw_owids, dw_owids) = OWTileGroups[(name, groupType, whirlpoolGroup)] if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'simple', 'restricted']: - (exist_owids, exist_lw_regions, exist_dw_regions) = groups[(name,)] - exist_owids.extend(lw_owids) - exist_owids.extend(dw_owids) - for owid in lw_owids: - exist_lw_regions.extend(OWTileRegions.inverse[owid]) - for owid in dw_owids: - exist_dw_regions.extend(OWTileRegions.inverse[owid]) - groups[(name,)] = (exist_owids, exist_lw_regions, exist_dw_regions) + if world.owWhirlpoolShuffle[player] or world.owCrossed[player] != 'none': + (exist_owids, exist_lw_regions, exist_dw_regions) = groups[(name, whirlpoolGroup)] + exist_owids.extend(lw_owids) + exist_owids.extend(dw_owids) + for owid in lw_owids: + exist_lw_regions.extend(OWTileRegions.inverse[owid]) + for owid in dw_owids: + exist_dw_regions.extend(OWTileRegions.inverse[owid]) + groups[(name, whirlpoolGroup)] = (exist_owids, exist_lw_regions, exist_dw_regions) + else: + (exist_owids, exist_lw_regions, exist_dw_regions) = groups[(name,)] + exist_owids.extend(lw_owids) + exist_owids.extend(dw_owids) + for owid in lw_owids: + exist_lw_regions.extend(OWTileRegions.inverse[owid]) + for owid in dw_owids: + exist_dw_regions.extend(OWTileRegions.inverse[owid]) + groups[(name,)] = (exist_owids, exist_lw_regions, exist_dw_regions) else: - (exist_owids, exist_lw_regions, exist_dw_regions) = groups[(name, groupType)] - exist_owids.extend(lw_owids) - exist_owids.extend(dw_owids) - for owid in lw_owids: - exist_lw_regions.extend(OWTileRegions.inverse[owid]) - for owid in dw_owids: - exist_dw_regions.extend(OWTileRegions.inverse[owid]) - groups[(name, groupType)] = (exist_owids, exist_lw_regions, exist_dw_regions) + if world.owWhirlpoolShuffle[player] or world.owCrossed[player] != 'none': + (exist_owids, exist_lw_regions, exist_dw_regions) = groups[(name, groupType, whirlpoolGroup)] + exist_owids.extend(lw_owids) + exist_owids.extend(dw_owids) + for owid in lw_owids: + exist_lw_regions.extend(OWTileRegions.inverse[owid]) + for owid in dw_owids: + exist_dw_regions.extend(OWTileRegions.inverse[owid]) + groups[(name, groupType, whirlpoolGroup)] = (exist_owids, exist_lw_regions, exist_dw_regions) + else: + (exist_owids, exist_lw_regions, exist_dw_regions) = groups[(name, groupType)] + exist_owids.extend(lw_owids) + exist_owids.extend(dw_owids) + for owid in lw_owids: + exist_lw_regions.extend(OWTileRegions.inverse[owid]) + for owid in dw_owids: + exist_dw_regions.extend(OWTileRegions.inverse[owid]) + groups[(name, groupType)] = (exist_owids, exist_lw_regions, exist_dw_regions) return groups def remove_reserved(world, groupedlist, connected_edges, player): @@ -733,17 +808,7 @@ temporary_mandatory_connections = [ ] # these are connections that cannot be shuffled and always exist. They link together separate parts of the world we need to divide into regions -mandatory_connections = [# Whirlpool Connections - ('C Whirlpool', 'River Bend Water'), - ('River Bend Whirlpool', 'C Whirlpool Water'), - ('Lake Hylia Whirlpool', 'Zora Waterfall Water'), - ('Zora Whirlpool', 'Lake Hylia Water'), - ('Kakariko Pond Whirlpool', 'Octoballoon Water'), - ('Octoballoon Whirlpool', 'Kakariko Pond Area'), - ('Qirn Jump Whirlpool', 'Bomber Corner Water'), - ('Bomber Corner Whirlpool', 'Qirn Jump Water'), - - # Intra-tile OW Connections +mandatory_connections = [# Intra-tile OW Connections ('Lost Woods Bush (West)', 'Lost Woods East Area'), #pearl ('Lost Woods Bush (East)', 'Lost Woods West Area'), #pearl ('West Death Mountain Drop', 'West Death Mountain (Bottom)'), @@ -967,6 +1032,13 @@ mandatory_connections = [# Whirlpool Connections ('Dark Tree Line WC Cliff Water Drop', 'Dark Tree Line Water') #fake flipper ] +default_whirlpool_connections = [ + ((0x33, 'C Whirlpool', 'C Whirlpool Water'), (0x15, 'River Bend Whirlpool', 'River Bend Water')), + ((0x35, 'Lake Hylia Whirlpool', 'Lake Hylia Water'), (0x0f, 'Zora Whirlpool', 'Zora Waterfall Water')), + ((0x12, 'Kakariko Pond Whirlpool', 'Kakariko Pond Area'), (0x3f, 'Octoballoon Whirlpool', 'Octoballoon Water')), + ((0x55, 'Qirn Jump Whirlpool', 'Qirn Jump Water'), (0x7f, 'Bomber Corner Whirlpool', 'Bomber Corner Water')) +] + default_flute_connections = [ 0x0b, 0x16, 0x18, 0x2c, 0x2f, 0x38, 0x3b, 0x3f ] diff --git a/Rom.py b/Rom.py index 58cd90bc..5be364ec 100644 --- a/Rom.py +++ b/Rom.py @@ -33,7 +33,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'c6c2a2d5d89a3c84871f58806bbb3acf' +RANDOMIZERBASEHASH = 'e9dea70e0a0b15bfa0ff7ecd63228a0c' class JsonRom(object): @@ -644,6 +644,11 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_byte(snes_to_pc(0x0AB793 + o), data[11] & 0xff) # Y low byte rom.write_byte(snes_to_pc(0x0AB79B + o), data[11] // 0x100) # Y high byte + # patch whirlpools + if world.owWhirlpoolShuffle[player]: + owFlags |= 0x01 + write_int16s(rom, snes_to_pc(0x02EA5C), world.owwhirlpools[player]) + # patch overworld edges inverted_buffer = [0] * 0x82 owMode = 0 diff --git a/data/base2current.bps b/data/base2current.bps index daf77e69ccf89d2044d3fcf39dae204306a0edd0..aa914956d578657a299b8406d23a1e7e69766b86 100644 GIT binary patch delta 293 zcmV+=0owkJ&Iq&42(U>31oI6H^0Q3=kpc)Pf{8_lsu+&5-~;gk0h_bZ2m1s8cazQx z7XeqZ<_xO!6-|HuYa(pw0E$$w1HssR5I! zuW3c5i5P1*SGkwn0s%q-1Xs5q0|A@@0eP3c1py!p1*QcchLfcQg^I#3V3*+q0VO9M z5T>n3K*>PR6Qy0r1n>l_UwEdSfDNTx$rsQ9p9_GaB}(*j-$b`H1_8VR2RIL44=4ak zm-q+)ISZ`>0B{eG4v delta 273 zcmV+s0q*{@&Ipap2(U>31V9b7%kpcoxk+bFl@dE*gv)Bjw1Q}(Bs_H<9og$u? zpdwbsq?R2=n(Gx-l8HsL>bj=oB{z+m(~RVAOlsV1(yQ`0VOXA5T>n3K*>PR6Qy0r1n>l_ zOL(T8fDNTx$rsQ9n+t%Mr3HnG!Z2W?BTDoG;5@fa1_8VR2XGIN4 Date: Wed, 27 Oct 2021 01:19:55 -0500 Subject: [PATCH 62/76] Unnecessary tautology check --- BaseClasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index b6484e83..dd4f4133 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -115,7 +115,7 @@ class World(object): set_player_attr('ganon_at_pyramid', True) set_player_attr('ganonstower_vanilla', True) set_player_attr('sewer_light_cone', self.mode[player] == 'standard') - set_player_attr('fix_trock_doors', self.shuffle[player] != 'vanilla' or ((self.mode[player] == 'inverted') != (0x05 in self.owswaps[player][0] and self.owMixed[player]))) + set_player_attr('fix_trock_doors', self.shuffle[player] != 'vanilla' or ((self.mode[player] == 'inverted') != 0x05 in self.owswaps[player][0])) set_player_attr('fix_skullwoods_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'] or self.doorShuffle[player] not in ['vanilla']) set_player_attr('fix_palaceofdarkness_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple']) set_player_attr('fix_trock_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple']) From c8e04ff8b69ef8d8128e038f3a0da9ecea058dad Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 27 Oct 2021 01:20:56 -0500 Subject: [PATCH 63/76] Fixing some Lite ER to not be treated as cross-world --- BaseClasses.py | 2 +- OverworldShuffle.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index dd4f4133..49310325 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -123,7 +123,7 @@ class World(object): set_player_attr('can_access_trock_front', None) set_player_attr('can_access_trock_big_chest', None) set_player_attr('can_access_trock_middle', None) - set_player_attr('fix_fake_world', logic[player] not in ['owglitches', 'nologic'] or shuffle[player] in ['lite', 'lean', 'crossed', 'insanity', 'madness_legacy']) + set_player_attr('fix_fake_world', logic[player] not in ['owglitches', 'nologic'] or shuffle[player] in ['lean', 'crossed', 'insanity', 'madness_legacy']) set_player_attr('mapshuffle', False) set_player_attr('compassshuffle', False) set_player_attr('keyshuffle', False) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 9ed24210..6f9cf404 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -477,7 +477,7 @@ def reorganize_tile_groups(world, player): groups = {} for (name, groupType, whirlpoolGroup) in OWTileGroups.keys(): if world.mode[player] != 'standard' or name not in ['Castle', 'Links', 'Central Bonk Rocks'] \ - or (world.mode[player] == 'standard' and world.shuffle[player] in ['lite', 'lean', 'crossed', 'insanity'] and name == 'Castle' and groupType == 'Entrance'): + or (world.mode[player] == 'standard' and world.shuffle[player] in ['lean', 'crossed', 'insanity'] and name == 'Castle' and groupType == 'Entrance'): if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'simple', 'restricted']: if world.owWhirlpoolShuffle[player] or world.owCrossed[player] != 'none': groups[(name, whirlpoolGroup)] = ([], [], []) @@ -491,7 +491,7 @@ def reorganize_tile_groups(world, player): for (name, groupType, whirlpoolGroup) in OWTileGroups.keys(): if world.mode[player] != 'standard' or name not in ['Castle', 'Links', 'Central Bonk Rocks'] \ - or (world.mode[player] == 'standard' and world.shuffle[player] in ['lite', 'lean', 'crossed', 'insanity'] and name == 'Castle' and groupType == 'Entrance'): + or (world.mode[player] == 'standard' and world.shuffle[player] in ['lean', 'crossed', 'insanity'] and name == 'Castle' and groupType == 'Entrance'): (lw_owids, dw_owids) = OWTileGroups[(name, groupType, whirlpoolGroup)] if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'simple', 'restricted']: if world.owWhirlpoolShuffle[player] or world.owCrossed[player] != 'none': From 34e115a2639822bbbb041c7a2bc9cd5be84e1ff4 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 27 Oct 2021 01:22:03 -0500 Subject: [PATCH 64/76] Implemented Whirlpool Shuffle --- asm/owrando.asm | 63 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/asm/owrando.asm b/asm/owrando.asm index 8082d38f..d4a5ebd1 100644 --- a/asm/owrando.asm +++ b/asm/owrando.asm @@ -14,6 +14,10 @@ jsl OWEdgeTransition : nop #4 ;LDA $02A4E3,X : ORA $7EF3CA ;org $02e238 ;LDX #$9E : - DEX : DEX : CMP $DAEE,X : BNE - ;jsl OWSpecialTransition : nop #5 +; whirlpool shuffle cross world change +org $02b3bd +jsl OWWhirlpoolUpdate ;JSL $02EA6C + ; flute menu cancel org $0ab7af ;LDA $F2 : ORA $F0 : AND #$C0 jml OWFluteCancel2 : nop @@ -123,6 +127,14 @@ OWWorldCheck16: plx : and.w #$00ff : rtl } +OWWhirlpoolUpdate: +{ + jsl $02ea6c ; what we wrote over + lda.l OWFlags : and #$01 : beq + + ldx $8a : jsr OWWorldUpdate + + rtl +} + OWFluteCancel: { lda.l OWFlags+1 : and #$01 : bne + @@ -341,32 +353,39 @@ OWNewDestination: sep #$30 : lda OWOppSlotOffset,y : !add $04 : asl : and #$7f : sta $700 ; crossed OW shuffle - LDA.l OWMode+1 : AND.b #!FLAG_OW_CROSSED : beq .return - ldx $05 : lda.l OWTileWorldAssoc,x : cmp.l $7ef3ca : beq .return - sta.l $7ef3ca ; change world - lda #$38 : sta $012f ; play sfx - #$3b is an alternative - - ; toggle bunny mode - + lda $7ef357 : bne .nobunny - lda.l InvertedMode : bne .inverted - lda $7ef3ca : and.b #$40 : bra + - .inverted lda $7ef3ca : and.b #$40 : eor #$40 - + cmp #$40 : bne .nobunny - ; turn into bunny - lda $5d : cmp #$04 : beq + ; if swimming, continue - lda #$17 : sta $5d - + lda #$01 : sta $02e0 : sta $56 - bra .return - - .nobunny - lda $5d : cmp #$17 : bne + ; retain current state unless bunny - stz $5d - + stz $02e0 : stz $56 + lda.l OWMode+1 : and.b #!FLAG_OW_CROSSED : beq .return + ldx $05 : jsr OWWorldUpdate .return lda $05 : sta $8a rep #$30 : rts } +OWWorldUpdate: ; x = owid of destination screen +{ + lda.l OWTileWorldAssoc,x : cmp.l $7ef3ca : beq .return + sta.l $7ef3ca ; change world + lda #$38 : sta $012f ; play sfx - #$3b is an alternative + + ; toggle bunny mode + + lda $7ef357 : bne .nobunny + lda.l InvertedMode : bne .inverted + lda $7ef3ca : and.b #$40 : bra + + .inverted lda $7ef3ca : and.b #$40 : eor #$40 + + cmp #$40 : bne .nobunny + ; turn into bunny + lda $5d : cmp #$04 : beq + ; if swimming, continue + lda #$17 : sta $5d + + lda #$01 : sta $02e0 : sta $56 + bra .return + + .nobunny + lda $5d : cmp #$17 : bne + ; retain current state unless bunny + stz $5d + + stz $02e0 : stz $56 + + .return + rts +} OWSpecialTransition: { LDX #$9E @@ -531,7 +550,7 @@ OWNorthEdges: ; Min Max Width Mid OW Slot/OWID VRAM *FREE* Dest Index dw $00a0, $00a0, $0000, $00a0, $0000, $0000, $0000, $0040 ;Lost Woods dw $0458, $0540, $00e8, $04cc, $0a0a, $0000, $0000, $0000 -dw $0f70, $0f90, $0020, $0f80, $0f0f, $0000, $0000, $0041 +dw $0f38, $0f60, $0028, $0f4c, $0f0f, $0000, $0000, $0041 dw $0058, $0058, $0000, $0058, $1010, $0000, $0000, $0001 dw $0178, $0178, $0000, $0178, $1010, $0000, $0000, $0002 dw $0388, $0388, $0000, $0388, $1111, $0000, $0000, $0003 From 152adde3cb071634d5d935947e77ec46f9a402cc Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 27 Oct 2021 01:42:32 -0500 Subject: [PATCH 65/76] Version bump 0.2.1.0 --- CHANGELOG.md | 3 +++ OverworldShuffle.py | 2 +- README.md | 6 ++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f56cf7a6..f76fb7c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +### 0.2.1.0 +- Implemented Whirlpool Shuffle + ### 0.2.0.0 - Massive overhaul of ER algorithm - Added 2 new ER modes (Lite and Lean) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 6f9cf404..6e3f52f2 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -3,7 +3,7 @@ from BaseClasses import OWEdge, WorldType, RegionType, Direction, Terrain, PolSl from Regions import mark_dark_world_regions, mark_light_world_regions from OWEdges import OWTileRegions, OWTileGroups, OWEdgeGroups, OWExitTypes, OpenStd, parallel_links, IsParallel -__version__ = '0.2.0.0-u' +__version__ = '0.2.1.0-u' def link_overworld(world, player): # setup mandatory connections diff --git a/README.md b/README.md index edd0fd79..da4f5dad 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,8 @@ Every transition independently is a candidate to be chosen as a cross-world conn Note: Only parallel connections (a connection that also exists in the opposite world) are considered for cross-world connections, which means that the same connection in the opposite world will also connect cross-world. +Note: If Whirlpool Shuffle is enabled, those connections can be cross-world but do not count towards the 9 transitions that are crossed. + Motive: Why 9 connections? To imitate the effect of the 9 standard portals that exist. ### Chaos @@ -111,6 +113,10 @@ OW tiles are randomly chosen to become a part of the opposite world. When on the Note: Tiles are put into groups that must be shuffled together when certain settings are enabled. For instance, if ER is disabled, then any tiles that have a connector cave that leads to another tile, those tiles must swap together; (an exception to this is the Old Man Rescue cave which has been modified similar to how Inverted modifies it, Old Man Rescue is ALWAYS accessible from the Light World) +## Whirlpool Shuffle (--ow_whirlpool) + +When enabled, the whirlpool connections are shuffled. If Crossed OW is enabled, the whirlpools can also be cross-world as well. For Limited Crossed OW, this doesn't count towards the limited number of crossed edge transitions. + ## Flute Shuffle (--ow_fluteshuffle) When enabled, new flute spots are generated and gives the player the option to cancel out of the flute menu by pressing X. From 7f3a373b68b3c7987233e0c91e7f67d875bc3d67 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 27 Oct 2021 21:59:34 -0500 Subject: [PATCH 66/76] Fixed output path in Mystery to use the saved settings if not specified on CLI --- Mystery.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Mystery.py b/Mystery.py index 8f1b5158..e1f91164 100644 --- a/Mystery.py +++ b/Mystery.py @@ -63,7 +63,8 @@ def main(): erargs.create_spoiler = args.create_spoiler erargs.race = True erargs.outputname = seedname - erargs.outputpath = args.outputpath + if args.outputpath: + erargs.outputpath = args.outputpath erargs.loglevel = args.loglevel if args.rom: From d22d421527fae605907ebe3fbc2e19dd758f7782 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 27 Oct 2021 22:01:02 -0500 Subject: [PATCH 67/76] Fixed issue with Links House candidate list in ER is blank --- EntranceShuffle.py | 47 ++++++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 949e21b8..8f5f7a08 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -1364,9 +1364,10 @@ def place_links_house(world, sectors, player): if invFlag and isinstance(dark_sanc, str): links_house_doors = [i for i in get_distant_entrances(world, dark_sanc, sectors, player) if i in entrance_pool] else: - links_house_doors = [i for i in get_starting_entrances(world, sectors, player) if i in entrance_pool] + links_house_doors = [i for i in get_starting_entrances(world, sectors, player, world.shuffle[player] == 'insanity') if i in entrance_pool] if world.shuffle[player] in ['lite', 'lean']: links_house_doors = [e for e in links_house_doors if e in list(zip(*(default_item_connections + (default_shop_connections if world.shopsanity[player] else []))))[0]] + links_house = random.choice(links_house_doors) connect_two_way(world, links_house, 'Links House Exit', player) return links_house @@ -1376,7 +1377,7 @@ def place_dark_sanc(world, sectors, player): if not world.shufflelinks[player]: sanc_doors = [i for i in get_distant_entrances(world, 'Big Bomb Shop', sectors, player) if i in entrance_pool] else: - sanc_doors = [i for i in get_starting_entrances(world, sectors, player) if i in entrance_pool] + sanc_doors = [i for i in get_starting_entrances(world, sectors, player, world.shuffle[player] == 'insanity') if i in entrance_pool] if world.shuffle[player] in ['lite', 'lean']: sanc_doors = [e for e in sanc_doors if e in list(zip(*(default_item_connections + (default_shop_connections if world.shopsanity[player] else []))))[0]] sanc_door = random.choice(sanc_doors) @@ -1735,30 +1736,36 @@ def build_accessible_entrance_list(world, start_region, player, assumed_inventor return entrances -def get_starting_entrances(world, sectors, player): +def get_starting_entrances(world, sectors, player, force_starting_world=True): invFlag = world.mode[player] == 'inverted' # find largest walkable sector sector = None invalid_sectors = list() - while (sector is None): - sector = max(sectors, key=lambda x: len(x) - (0 if x not in invalid_sectors else 1000)) - if not ((world.owCrossed[player] == 'polar' and world.owMixed[player]) or world.owCrossed[player] not in ['none', 'polar']) \ - and world.get_region(next(iter(next(iter(sector)))), player).type != (RegionType.LightWorld if not invFlag else RegionType.DarkWorld): - invalid_sectors.append(sector) - sector = None - regions = max(sector, key=lambda x: len(x)) - - # get entrances from list of regions entrances = list() - for region_name in [r for r in regions if r ]: - if world.shuffle[player] == 'simple' and region_name in OWTileRegions and OWTileRegions[region_name] in [0x03, 0x05, 0x07]: - continue - region = world.get_region(region_name, player) - if region.type == RegionType.LightWorld if not invFlag else RegionType.DarkWorld: - for exit in region.exits: - if not exit.connected_region and exit.spot_type == 'Entrance': - entrances.append(exit.name) + while not len(entrances): + while (sector is None): + sector = max(sectors, key=lambda x: len(x) - (0 if x not in invalid_sectors else 1000)) + if not ((world.owCrossed[player] == 'polar' and world.owMixed[player]) or world.owCrossed[player] not in ['none', 'polar']) \ + and world.get_region(next(iter(next(iter(sector)))), player).type != (RegionType.LightWorld if not invFlag else RegionType.DarkWorld): + invalid_sectors.append(sector) + sector = None + regions = max(sector, key=lambda x: len(x)) + + # get entrances from list of regions + entrances = list() + for region_name in regions: + if world.shuffle[player] == 'simple' and region_name in OWTileRegions and OWTileRegions[region_name] in [0x03, 0x05, 0x07]: + continue + region = world.get_region(region_name, player) + if not force_starting_world or region.type == (RegionType.LightWorld if not invFlag else RegionType.DarkWorld): + for exit in region.exits: + if not exit.connected_region and exit.spot_type == 'Entrance': + entrances.append(exit.name) + + invalid_sectors.append(sector) + sector = None + return entrances From deb30151057898279f68b445e89b21640199de9e Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 27 Oct 2021 22:02:21 -0500 Subject: [PATCH 68/76] Fixed issue with Links House candidate list in ER is blank --- EntranceShuffle.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 8f5f7a08..3fd0c278 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -1361,10 +1361,11 @@ def place_links_house(world, sectors, player): if dark_sanc.connected_region and dark_sanc.connected_region.name == 'Dark Sanctuary Hint': dark_sanc = dark_sanc.name break + if invFlag and isinstance(dark_sanc, str): links_house_doors = [i for i in get_distant_entrances(world, dark_sanc, sectors, player) if i in entrance_pool] else: - links_house_doors = [i for i in get_starting_entrances(world, sectors, player, world.shuffle[player] == 'insanity') if i in entrance_pool] + links_house_doors = [i for i in get_starting_entrances(world, sectors, player, world.shuffle[player] != 'insanity') if i in entrance_pool] if world.shuffle[player] in ['lite', 'lean']: links_house_doors = [e for e in links_house_doors if e in list(zip(*(default_item_connections + (default_shop_connections if world.shopsanity[player] else []))))[0]] @@ -1377,7 +1378,7 @@ def place_dark_sanc(world, sectors, player): if not world.shufflelinks[player]: sanc_doors = [i for i in get_distant_entrances(world, 'Big Bomb Shop', sectors, player) if i in entrance_pool] else: - sanc_doors = [i for i in get_starting_entrances(world, sectors, player, world.shuffle[player] == 'insanity') if i in entrance_pool] + sanc_doors = [i for i in get_starting_entrances(world, sectors, player, world.shuffle[player] != 'insanity') if i in entrance_pool] if world.shuffle[player] in ['lite', 'lean']: sanc_doors = [e for e in sanc_doors if e in list(zip(*(default_item_connections + (default_shop_connections if world.shopsanity[player] else []))))[0]] sanc_door = random.choice(sanc_doors) From a5ceb2f61c19f720d43f0976774a65c3c10837f0 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 27 Oct 2021 22:33:15 -0500 Subject: [PATCH 69/76] Moving shuffling dropdowns to later in modes where HC isn't placed early This fixes unwanted cross-world scenarios --- EntranceShuffle.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 3fd0c278..83a7faea 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -329,9 +329,6 @@ def link_entrances(world, player): else: caves.append('Ganons Tower Exit') - # shuffle holes - scramble_holes(world, player) - # place dark sanc if invFlag: place_dark_sanc(world, sectors, player) @@ -371,6 +368,9 @@ def link_entrances(world, player): dw_entrances = [e for e in dw_entrances if e in entrance_pool] connect_caves(world, lw_entrances, dw_entrances, caves, player) + # shuffle holes + scramble_holes(world, player) + # place remaining doors connect_doors(world, list(entrance_pool), list(exit_pool), player) elif world.shuffle[player] == 'lite': @@ -467,9 +467,6 @@ def link_entrances(world, player): # shuffle dungeons skull_woods_shuffle(world, player) - # shuffle dropdowns - scramble_holes(world, player) - if world.mode[player] == 'standard': connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) Dungeon_Exits.append(tuple(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'))) @@ -509,6 +506,9 @@ def link_entrances(world, player): bomb_shop = bomb_shop_doors.pop() connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) + # shuffle dropdowns + scramble_holes(world, player) + # place remaining doors connect_doors(world, list(entrance_pool), list(exit_pool), player) elif world.shuffle[player] == 'crossed': From a965e854a7f806c782d4ce125cd3c115daa1a7ae Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 28 Oct 2021 00:21:46 -0500 Subject: [PATCH 70/76] Changed spoiler output to occur in stages rather than once at the end --- BaseClasses.py | 134 +++++++++++++++++++++++++++---------------------- Main.py | 20 +++++--- 2 files changed, 88 insertions(+), 66 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 49310325..a2bf19b5 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -2600,6 +2600,56 @@ class Spoiler(object): else: self.doorTypes[(doorNames, player)] = OrderedDict([('player', player), ('doorNames', doorNames), ('type', type)]) + def parse_meta(self): + from Main import __version__ as ERVersion + from OverworldShuffle import __version__ as ORVersion + + self.startinventory = list(map(str, self.world.precollected_items)) + self.metadata = {'version': ERVersion, + 'versions': {'Door':ERVersion, 'Overworld':ORVersion}, + 'logic': self.world.logic, + 'mode': self.world.mode, + 'retro': self.world.retro, + 'bombbag': self.world.bombbag, + 'weapons': self.world.swords, + 'goal': self.world.goal, + 'ow_shuffle': self.world.owShuffle, + 'ow_crossed': self.world.owCrossed, + 'ow_keepsimilar': self.world.owKeepSimilar, + 'ow_mixed': self.world.owMixed, + 'ow_whirlpool': self.world.owWhirlpoolShuffle, + 'ow_fluteshuffle': self.world.owFluteShuffle, + 'shuffle': self.world.shuffle, + 'shuffleganon': self.world.shuffle_ganon, + 'shufflelinks': self.world.shufflelinks, + 'door_shuffle': self.world.doorShuffle, + 'intensity': self.world.intensity, + 'item_pool': self.world.difficulty, + 'item_functionality': self.world.difficulty_adjustments, + 'gt_crystals': self.world.crystals_needed_for_gt, + 'ganon_crystals': self.world.crystals_needed_for_ganon, + 'open_pyramid': self.world.open_pyramid, + 'accessibility': self.world.accessibility, + 'hints': self.world.hints, + 'mapshuffle': self.world.mapshuffle, + 'compassshuffle': self.world.compassshuffle, + 'keyshuffle': self.world.keyshuffle, + 'bigkeyshuffle': self.world.bigkeyshuffle, + 'boss_shuffle': self.world.boss_shuffle, + 'enemy_shuffle': self.world.enemy_shuffle, + 'enemy_health': self.world.enemy_health, + 'enemy_damage': self.world.enemy_damage, + 'potshuffle': self.world.potshuffle, + 'players': self.world.players, + 'teams': self.world.teams, + 'experimental': self.world.experimental, + 'keydropshuffle': self.world.keydropshuffle, + 'shopsanity': self.world.shopsanity, + 'triforcegoal': self.world.treasure_hunt_count, + 'triforcepool': self.world.treasure_hunt_total, + 'code': {p: Settings.make_code(self.world, p) for p in range(1, self.world.players + 1)} + } + def parse_data(self): self.medallions = OrderedDict() if self.world.players == 1: @@ -2610,8 +2660,6 @@ class Spoiler(object): self.medallions[f'Misery Mire ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][0] self.medallions[f'Turtle Rock ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][1] - self.startinventory = list(map(str, self.world.precollected_items)) - self.locations = OrderedDict() listed_locations = set() @@ -2682,54 +2730,8 @@ class Spoiler(object): for portal in self.world.dungeon_portals[player]: self.set_lobby(portal.name, portal.door.name, player) - from Main import __version__ as ERVersion - from OverworldShuffle import __version__ as ORVersion - self.metadata = {'version': ERVersion, - 'versions': {'Door':ERVersion, 'Overworld':ORVersion}, - 'logic': self.world.logic, - 'mode': self.world.mode, - 'retro': self.world.retro, - 'bombbag': self.world.bombbag, - 'weapons': self.world.swords, - 'goal': self.world.goal, - 'ow_shuffle': self.world.owShuffle, - 'ow_crossed': self.world.owCrossed, - 'ow_keepsimilar': self.world.owKeepSimilar, - 'ow_mixed': self.world.owMixed, - 'ow_whirlpool': self.world.owWhirlpoolShuffle, - 'ow_fluteshuffle': self.world.owFluteShuffle, - 'shuffle': self.world.shuffle, - 'shuffleganon': self.world.shuffle_ganon, - 'shufflelinks': self.world.shufflelinks, - 'door_shuffle': self.world.doorShuffle, - 'intensity': self.world.intensity, - 'item_pool': self.world.difficulty, - 'item_functionality': self.world.difficulty_adjustments, - 'gt_crystals': self.world.crystals_needed_for_gt, - 'ganon_crystals': self.world.crystals_needed_for_ganon, - 'open_pyramid': self.world.open_pyramid, - 'accessibility': self.world.accessibility, - 'hints': self.world.hints, - 'mapshuffle': self.world.mapshuffle, - 'compassshuffle': self.world.compassshuffle, - 'keyshuffle': self.world.keyshuffle, - 'bigkeyshuffle': self.world.bigkeyshuffle, - 'boss_shuffle': self.world.boss_shuffle, - 'enemy_shuffle': self.world.enemy_shuffle, - 'enemy_health': self.world.enemy_health, - 'enemy_damage': self.world.enemy_damage, - 'potshuffle': self.world.potshuffle, - 'players': self.world.players, - 'teams': self.world.teams, - 'experimental': self.world.experimental, - 'keydropshuffle': self.world.keydropshuffle, - 'shopsanity': self.world.shopsanity, - 'triforcegoal': self.world.treasure_hunt_count, - 'triforcepool': self.world.treasure_hunt_total, - 'code': {p: Settings.make_code(self.world, p) for p in range(1, self.world.players + 1)} - } - def to_json(self): + self.parse_meta() self.parse_data() out = OrderedDict() out['Overworld'] = list(self.overworlds.values()) @@ -2751,8 +2753,8 @@ class Spoiler(object): return json.dumps(out) - def to_file(self, filename): - self.parse_data() + def meta_to_file(self, filename): + self.parse_meta() with open(filename, 'w') as outfile: line_width = 35 outfile.write('ALttP Entrance Randomizer - Seed: %s\n\n' % (self.world.seed)) @@ -2764,9 +2766,6 @@ class Spoiler(object): for player in range(1, self.world.players + 1): if self.world.players > 1: outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_names(player))) - if len(self.hashes) > 0: - for team in range(self.world.teams): - outfile.write('%s%s\n' % (f"Hash - {self.world.player_names[player][team]} (Team {team+1}): " if self.world.teams > 1 else 'Hash: ', self.hashes[player, team])) outfile.write('Settings Code:'.ljust(line_width) + '%s\n' % self.metadata["code"][player]) outfile.write('Logic:'.ljust(line_width) + '%s\n' % self.metadata['logic'][player]) outfile.write('Mode:'.ljust(line_width) + '%s\n' % self.metadata['mode'][player]) @@ -2812,14 +2811,28 @@ class Spoiler(object): if self.startinventory: outfile.write('Starting Inventory:'.ljust(line_width)) outfile.write('\n'.ljust(line_width+1).join(self.startinventory)) - + + def to_file(self, filename): + self.parse_data() + with open(filename, 'a') as outfile: + line_width = 35 + if self.world.players > 1: + outfile.write('\nHashes:') + for player in range(1, self.world.players + 1): + if self.world.players > 1: + outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_names(player))) + if len(self.hashes) > 0: + for team in range(self.world.teams): + outfile.write('%s%s\n' % (f"Hash - {self.world.player_names[player][team]} (Team {team+1}): " if self.world.teams > 1 else 'Hash: ', self.hashes[player, team])) outfile.write('\n\nRequirements:\n\n') for dungeon, medallion in self.medallions.items(): outfile.write(f'{dungeon}:'.ljust(line_width) + '%s Medallion\n' % medallion) - if self.world.crystals_gt_orig[player] == 'random': - outfile.write('Crystals Required for GT:'.ljust(line_width) + '%s\n' % (str(self.metadata['gt_crystals'][player]))) - if self.world.crystals_ganon_orig[player] == 'random': - outfile.write('Crystals Required for Ganon:'.ljust(line_width) + '%s\n' % (str(self.metadata['ganon_crystals'][player]))) + for player in range(1, self.world.players + 1): + player_name = '' if self.world.players == 1 else str(' (Player ' + str(player) + ')') + if self.world.crystals_gt_orig[player] == 'random': + outfile.write(str('Crystals Required for GT' + player_name + ':').ljust(line_width) + '%s\n' % (str(self.metadata['gt_crystals'][player]))) + if self.world.crystals_ganon_orig[player] == 'random': + outfile.write(str('Crystals Required for Ganon' + player_name + ':').ljust(line_width) + '%s\n' % (str(self.metadata['ganon_crystals'][player]))) if self.overworlds: # overworlds: overworld transitions; @@ -2868,6 +2881,8 @@ class Spoiler(object): outfile.write(f'\n\nBosses ({self.world.get_player_names(player)}):\n\n') outfile.write('\n'.join([f'{x}: {y}' for x, y in bossmap.items() if y not in ['Agahnim', 'Agahnim 2', 'Ganon']])) + def playthru_to_file(self, filename): + with open(filename, 'a') as outfile: # locations: Change up location names; in the instance of a location with multiple sections, it'll try to translate the room name # items: Item names outfile.write('\n\nPlaythrough:\n\n') @@ -2896,7 +2911,6 @@ class Spoiler(object): outfile.write('\n'.join(path_listings)) - flooded_keys = { 'Trench 1 Switch': 'Swamp Palace - Trench 1 Pot Key', 'Trench 2 Switch': 'Swamp Palace - Trench 2 Pot Key' diff --git a/Main.py b/Main.py index 9827efb1..b5740bf4 100644 --- a/Main.py +++ b/Main.py @@ -131,6 +131,15 @@ def main(args, seed=None, fish=None): world.player_names[player].append(name) logger.info('') + if world.owShuffle[1] != 'vanilla' or world.owCrossed[1] not in ['none', 'polar'] or world.owMixed[1] or str(world.seed).startswith('M'): + outfilebase = f'OR_{args.outputname if args.outputname else world.seed}' + else: + outfilebase = f'DR_{args.outputname if args.outputname else world.seed}' + + if args.create_spoiler and not args.jsonout: + logger.info(world.fish.translate("cli","cli","patching.spoiler")) + world.spoiler.meta_to_file(output_path('%s_Spoiler.txt' % outfilebase)) + for player in range(1, world.players + 1): world.difficulty_requirements[player] = difficulties[world.difficulty[player]] @@ -266,11 +275,6 @@ def main(args, seed=None, fish=None): customize_shops(world, player) balance_money_progression(world) - if world.owShuffle[1] != 'vanilla' or world.owCrossed[1] not in ['none', 'polar'] or world.owMixed[1] or str(world.seed).startswith('M'): - outfilebase = f'OR_{args.outputname if args.outputname else world.seed}' - else: - outfilebase = f'DR_{args.outputname if args.outputname else world.seed}' - rom_names = [] jsonout = {} enemized = False @@ -338,6 +342,10 @@ def main(args, seed=None, fish=None): with open(output_path('%s_multidata' % outfilebase), 'wb') as f: f.write(multidata) + if args.create_spoiler and not args.jsonout: + logger.info(world.fish.translate("cli","cli","patching.spoiler")) + world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase)) + if not args.skip_playthrough: logger.info(world.fish.translate("cli","cli","calc.playthrough")) create_playthrough(world) @@ -350,7 +358,7 @@ def main(args, seed=None, fish=None): with open(output_path('%s_Spoiler.json' % outfilebase), 'w') as outfile: outfile.write(world.spoiler.to_json()) else: - world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase)) + world.spoiler.playthru_to_file(output_path('%s_Spoiler.txt' % outfilebase)) YES = world.fish.translate("cli","cli","yes") NO = world.fish.translate("cli","cli","no") From 3cd8f4a741e8035824bbf99912643acad4ac3733 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 28 Oct 2021 01:01:53 -0500 Subject: [PATCH 71/76] Adding no_race CLI option for Mystery --- Mystery.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Mystery.py b/Mystery.py index e1f91164..711c88db 100644 --- a/Mystery.py +++ b/Mystery.py @@ -28,6 +28,7 @@ def main(): parser.add_argument('--names', default='') parser.add_argument('--teams', default=1, type=lambda value: max(int(value), 1)) parser.add_argument('--create_spoiler', action='store_true') + parser.add_argument('--no_race', action='store_true') parser.add_argument('--rom') parser.add_argument('--enemizercli') parser.add_argument('--outputpath') @@ -61,7 +62,7 @@ def main(): erargs.seed = seed erargs.names = args.names erargs.create_spoiler = args.create_spoiler - erargs.race = True + erargs.race = not args.no_race erargs.outputname = seedname if args.outputpath: erargs.outputpath = args.outputpath From 4b0d8fe762f6cb0cc97cd7a9b16cf33b2f81be51 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 28 Oct 2021 12:55:12 -0500 Subject: [PATCH 72/76] Fixed infinite loop issue and better error handling in ER --- EntranceShuffle.py | 55 ++++++++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 83a7faea..129903eb 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -335,9 +335,6 @@ def link_entrances(world, player): # place links house links_house = place_links_house(world, sectors, player) - - # place blacksmith, has limited options - place_blacksmith(world, links_house, player) # determine pools lw_entrances = list() @@ -370,6 +367,9 @@ def link_entrances(world, player): # shuffle holes scramble_holes(world, player) + + # place blacksmith, has limited options + place_blacksmith(world, links_house, player) # place remaining doors connect_doors(world, list(entrance_pool), list(exit_pool), player) @@ -384,6 +384,9 @@ def link_entrances(world, player): # shuffle dungeons skull_woods_shuffle(world, player) + # shuffle dropdowns + scramble_holes(world, player) + # build dungeon lists lw_dungeons = LW_Dungeon_Exits.copy() dw_dungeons = DW_Late_Dungeon_Exits.copy() @@ -402,9 +405,6 @@ def link_entrances(world, player): unbias_dungeons(lw_dungeons) unbias_dungeons(dw_dungeons) - # shuffle dropdowns - scramble_holes(world, player) - # place links house links_house = place_links_house(world, sectors, player) @@ -423,15 +423,16 @@ def link_entrances(world, player): dw_entrances.append(e) # place connectors in inaccessible regions - caves = Cave_Base + lw_dungeons + Cave_Base - connector_entrances = [e for e in list(zip(*default_connector_connections + default_dungeon_connections + open_default_dungeon_connections))[0] if e in lw_entrances] - connect_inaccessible_regions(world, connector_entrances, [], caves, player) - lw_dungeons = list(set(lw_dungeons) & set(caves)) + Old_Man_House - - caves = list(set(Cave_Base) & set(caves)) + dw_dungeons - connector_entrances = [e for e in list(zip(*default_connector_connections + default_dungeon_connections + open_default_dungeon_connections))[0] if e in dw_entrances] + caves = Cave_Base + (dw_dungeons if not invFlag else lw_dungeons) + connector_entrances = [e for e in list(zip(*default_connector_connections + default_dungeon_connections + open_default_dungeon_connections))[0] if e in (dw_entrances if not invFlag else lw_entrances)] connect_inaccessible_regions(world, [], connector_entrances, caves, player) - dw_dungeons = list(set(dw_dungeons) & set(caves)) + + caves = list(set(Cave_Base) & set(caves)) + (lw_dungeons if not invFlag else dw_dungeons) + connector_entrances = [e for e in list(zip(*default_connector_connections + default_dungeon_connections + open_default_dungeon_connections))[0] if e in (lw_entrances if not invFlag else dw_entrances)] + connect_inaccessible_regions(world, connector_entrances, [], caves, player) + + lw_dungeons = list(set(lw_dungeons) & set(caves)) + (Old_Man_House if not invFlag else []) + dw_dungeons = list(set(dw_dungeons) & set(caves)) + ([] if not invFlag else Old_Man_House) caves = list(set(Cave_Base) & set(caves)) + DW_Mid_Dungeon_Exits # place old man, has limited options @@ -480,6 +481,9 @@ def link_entrances(world, player): caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits + Old_Man_House) + # shuffle dropdowns + scramble_holes(world, player) + # place links house links_house = place_links_house(world, sectors, player) @@ -506,9 +510,6 @@ def link_entrances(world, player): bomb_shop = bomb_shop_doors.pop() connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - # shuffle dropdowns - scramble_holes(world, player) - # place remaining doors connect_doors(world, list(entrance_pool), list(exit_pool), player) elif world.shuffle[player] == 'crossed': @@ -1072,7 +1073,7 @@ def scramble_holes(world, player): if len(region.entrances) > 0: hc_in_lw = region.entrances[0].parent_region.type == (RegionType.LightWorld if not invFlag else RegionType.DarkWorld) elif world.shuffle[player] == 'lite': - hc_in_lw = True + hc_in_lw = not invFlag else: # checks if drop candidates exist in LW drop_owids = [ 0x00, 0x02, 0x13, 0x15, 0x18, 0x1b, 0x22 ] @@ -1369,6 +1370,7 @@ def place_links_house(world, sectors, player): if world.shuffle[player] in ['lite', 'lean']: links_house_doors = [e for e in links_house_doors if e in list(zip(*(default_item_connections + (default_shop_connections if world.shopsanity[player] else []))))[0]] + assert len(links_house_doors), 'No valid candidates to place Links House' links_house = random.choice(links_house_doors) connect_two_way(world, links_house, 'Links House Exit', player) return links_house @@ -1381,6 +1383,8 @@ def place_dark_sanc(world, sectors, player): sanc_doors = [i for i in get_starting_entrances(world, sectors, player, world.shuffle[player] != 'insanity') if i in entrance_pool] if world.shuffle[player] in ['lite', 'lean']: sanc_doors = [e for e in sanc_doors if e in list(zip(*(default_item_connections + (default_shop_connections if world.shopsanity[player] else []))))[0]] + + assert len(sanc_doors), 'No valid candidates to place Dark Chapel' sanc_door = random.choice(sanc_doors) connect_entrance(world, sanc_door, 'Dark Sanctuary Hint', player) world.get_entrance('Dark Sanctuary Hint Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) @@ -1407,8 +1411,8 @@ def place_blacksmith(world, links_house, player): if world.shuffle[player] in ['lite', 'lean']: blacksmith_doors = [e for e in blacksmith_doors if e in list(zip(*(default_item_connections + (default_shop_connections if world.shopsanity[player] else []))))[0]] - random.shuffle(blacksmith_doors) - blacksmith_hut = blacksmith_doors.pop() + assert len(blacksmith_doors), 'No valid candidates to place Blacksmiths Hut' + blacksmith_hut = random.choice(blacksmith_doors) connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) return blacksmith_hut @@ -1533,12 +1537,15 @@ def connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, playe random.shuffle(combined_must_exit_regions) connect_one(combined_must_exit_regions[0], [e for e in lw_entrances if e in entrance_pool]) else: - if len(otherworld_must_exit_regions) > 0: + pool = [e for e in dw_entrances if e in entrance_pool] + if len(otherworld_must_exit_regions) > 0 and len(pool): random.shuffle(otherworld_must_exit_regions) - connect_one(otherworld_must_exit_regions[0], [e for e in (dw_entrances if not invFlag else lw_entrances) if e in entrance_pool]) + connect_one(otherworld_must_exit_regions[0], pool) elif len(must_exit_regions) > 0: - random.shuffle(must_exit_regions) - connect_one(must_exit_regions[0], [e for e in (lw_entrances if not invFlag else dw_entrances) if e in entrance_pool]) + pool = [e for e in lw_entrances if e in entrance_pool] + if len(pool): + random.shuffle(must_exit_regions) + connect_one(must_exit_regions[0], pool) def unbias_some_entrances(Dungeon_Exits, Cave_Exits, Old_Man_House, Cave_Three_Exits): From ebd301e4e53b08dd23ccc31835905acce4b62704 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 28 Oct 2021 13:55:48 -0500 Subject: [PATCH 73/76] Fixed various issue involving dropdown entrances getting chosen for non-dropdowns --- EntranceShuffle.py | 68 ++++++++++++++++++++++------------------------ 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 129903eb..e845587f 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -257,8 +257,7 @@ def link_entrances(world, player): bomb_shop_doors = list(entrance_pool) if world.logic[player] in ['noglitches', 'minorglitches'] or (invFlag != (0x1b in world.owswaps[player][0] and world.owMixed[player])): bomb_shop_doors = [e for e in entrance_pool if e not in ['Pyramid Fairy']] - random.shuffle(bomb_shop_doors) - bomb_shop = bomb_shop_doors.pop() + bomb_shop = random.choice(bomb_shop_doors) connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) # place remaining doors @@ -301,8 +300,7 @@ def link_entrances(world, player): bomb_shop_doors = list(entrance_pool) if world.logic[player] in ['noglitches', 'minorglitches'] or (invFlag != (0x1b in world.owswaps[player][0] and world.owMixed[player])): bomb_shop_doors = [e for e in entrance_pool if e not in ['Pyramid Fairy']] - random.shuffle(bomb_shop_doors) - bomb_shop = bomb_shop_doors.pop() + bomb_shop = random.choice(bomb_shop_doors) connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) # shuffle connectors @@ -331,33 +329,33 @@ def link_entrances(world, player): # place dark sanc if invFlag: - place_dark_sanc(world, sectors, player) + place_dark_sanc(world, sectors, player, list(zip(*drop_connections + dropexit_connections))[0]) # place links house - links_house = place_links_house(world, sectors, player) + links_house = place_links_house(world, sectors, player, list(zip(*drop_connections + dropexit_connections))[0]) # determine pools lw_entrances = list() dw_entrances = list() for e in entrance_pool: - region = world.get_entrance(e, player).parent_region - if region.type == RegionType.LightWorld: - lw_entrances.append(e) - else: - dw_entrances.append(e) + if e not in list(zip(*drop_connections + dropexit_connections))[0]: + region = world.get_entrance(e, player).parent_region + if region.type == RegionType.LightWorld: + lw_entrances.append(e) + else: + dw_entrances.append(e) # place connectors in inaccessible regions - connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, player) + connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, player, list(zip(*drop_connections + dropexit_connections))[0]) # place old man, has limited options - place_old_man(world, lw_entrances if not invFlag else dw_entrances, player) + place_old_man(world, lw_entrances if not invFlag else dw_entrances, player, list(zip(*drop_connections + dropexit_connections))[0]) # place bomb shop, has limited options - bomb_shop_doors = list(entrance_pool) + bomb_shop_doors = [e for e in entrance_pool if e not in list(zip(*drop_connections + dropexit_connections))[0]] if world.logic[player] in ['noglitches', 'minorglitches'] or (invFlag != (0x1b in world.owswaps[player][0] and world.owMixed[player])): - bomb_shop_doors = [e for e in entrance_pool if e not in ['Pyramid Fairy']] - random.shuffle(bomb_shop_doors) - bomb_shop = bomb_shop_doors.pop() + bomb_shop_doors = [e for e in bomb_shop_doors if e not in ['Pyramid Fairy']] + bomb_shop = random.choice(bomb_shop_doors) connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) # shuffle connectors @@ -384,9 +382,6 @@ def link_entrances(world, player): # shuffle dungeons skull_woods_shuffle(world, player) - # shuffle dropdowns - scramble_holes(world, player) - # build dungeon lists lw_dungeons = LW_Dungeon_Exits.copy() dw_dungeons = DW_Late_Dungeon_Exits.copy() @@ -405,6 +400,9 @@ def link_entrances(world, player): unbias_dungeons(lw_dungeons) unbias_dungeons(dw_dungeons) + # shuffle dropdowns + scramble_holes(world, player) + # place links house links_house = place_links_house(world, sectors, player) @@ -451,8 +449,7 @@ def link_entrances(world, player): bomb_shop_doors = list(entrance_pool) if world.logic[player] in ['noglitches', 'minorglitches'] or (invFlag != (0x1b in world.owswaps[player][0] and world.owMixed[player])): bomb_shop_doors = [e for e in entrance_pool if e not in ['Pyramid Fairy']] - random.shuffle(bomb_shop_doors) - bomb_shop = bomb_shop_doors.pop() + bomb_shop = random.choice(bomb_shop_doors) connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) # place remaining doors @@ -506,8 +503,7 @@ def link_entrances(world, player): bomb_shop_doors = list(entrance_pool) if world.logic[player] in ['noglitches', 'minorglitches'] or (invFlag != (0x1b in world.owswaps[player][0] and world.owMixed[player])): bomb_shop_doors = [e for e in entrance_pool if e not in ['Pyramid Fairy']] - random.shuffle(bomb_shop_doors) - bomb_shop = bomb_shop_doors.pop() + bomb_shop = random.choice(bomb_shop_doors) connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) # place remaining doors @@ -552,8 +548,7 @@ def link_entrances(world, player): bomb_shop_doors = list(entrance_pool) if world.logic[player] in ['noglitches', 'minorglitches'] or (invFlag != (0x1b in world.owswaps[player][0] and world.owMixed[player])): bomb_shop_doors = [e for e in entrance_pool if e not in ['Pyramid Fairy']] - random.shuffle(bomb_shop_doors) - bomb_shop = bomb_shop_doors.pop() + bomb_shop = random.choice(bomb_shop_doors) connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) # shuffle connectors @@ -1352,7 +1347,7 @@ def full_shuffle_dungeons(world, Dungeon_Exits, player): connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player) -def place_links_house(world, sectors, player): +def place_links_house(world, sectors, player, ignore_list=[]): invFlag = world.mode[player] == 'inverted' if world.mode[player] == 'standard' or not world.shufflelinks[player]: links_house = 'Links House' if not invFlag else 'Big Bomb Shop' @@ -1370,13 +1365,15 @@ def place_links_house(world, sectors, player): if world.shuffle[player] in ['lite', 'lean']: links_house_doors = [e for e in links_house_doors if e in list(zip(*(default_item_connections + (default_shop_connections if world.shopsanity[player] else []))))[0]] + #TODO: Need to improve Links House placement to choose a better sector or eliminate entrances that are after ledge drops + links_house_doors = [e for e in links_house_doors if e not in ignore_list] assert len(links_house_doors), 'No valid candidates to place Links House' links_house = random.choice(links_house_doors) connect_two_way(world, links_house, 'Links House Exit', player) return links_house -def place_dark_sanc(world, sectors, player): +def place_dark_sanc(world, sectors, player, ignore_list=[]): if not world.shufflelinks[player]: sanc_doors = [i for i in get_distant_entrances(world, 'Big Bomb Shop', sectors, player) if i in entrance_pool] else: @@ -1384,6 +1381,7 @@ def place_dark_sanc(world, sectors, player): if world.shuffle[player] in ['lite', 'lean']: sanc_doors = [e for e in sanc_doors if e in list(zip(*(default_item_connections + (default_shop_connections if world.shopsanity[player] else []))))[0]] + sanc_doors = [e for e in sanc_doors if e not in ignore_list] assert len(sanc_doors), 'No valid candidates to place Dark Chapel' sanc_door = random.choice(sanc_doors) connect_entrance(world, sanc_door, 'Dark Sanctuary Hint', player) @@ -1417,14 +1415,14 @@ def place_blacksmith(world, links_house, player): return blacksmith_hut -def place_old_man(world, pool, player): +def place_old_man(world, pool, player, ignore_list=[]): # exit has to come from specific set of doors, the entrance is free to move about if (world.mode[player] == 'inverted') == (0x03 in world.owswaps[player][0] and world.owMixed[player]): region_name = 'West Death Mountain (Top)' else: region_name = 'West Dark Death Mountain (Top)' old_man_entrances = list(build_accessible_entrance_list(world, region_name, player, [], False, True, True, True)) - old_man_entrances = [e for e in old_man_entrances if e != 'Old Man House (Bottom)'] + old_man_entrances = [e for e in old_man_entrances if e != 'Old Man House (Bottom)' and e not in ignore_list] if world.shuffle[player] in ['lite', 'lean']: old_man_entrances = [e for e in old_man_entrances if e in pool] random.shuffle(old_man_entrances) @@ -1434,7 +1432,7 @@ def place_old_man(world, pool, player): if 'West Death Mountain (Bottom)' not in build_accessible_region_list(world, world.get_entrance(old_man_exit, player).parent_region.name, player, True, True): old_man_exit = None - old_man_entrances = [e for e in pool if e in entrance_pool and e not in entrance_exits + [old_man_exit]] + old_man_entrances = [e for e in pool if e in entrance_pool and e not in ignore_list and e not in entrance_exits + [old_man_exit]] random.shuffle(old_man_entrances) old_man_entrance = old_man_entrances.pop() if world.shuffle[player] != 'insanity': @@ -1484,7 +1482,7 @@ def junk_fill_inaccessible(world, player): connect_entrance(world, entrance, junk_locations.pop(), player) -def connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, player): +def connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, player, ignore_list=[]): invFlag = world.mode[player] == 'inverted' random.shuffle(lw_entrances) @@ -1512,7 +1510,7 @@ def connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, playe for region_name in inaccessible_regions.copy(): region = world.get_region(region_name, player) if region.type not in [RegionType.LightWorld, RegionType.DarkWorld] or not any((not exit.connected_region and exit.spot_type == 'Entrance') for exit in region.exits) \ - or (region_name == 'Pyramid Exit Ledge' and invFlag != (0x1b in world.owswaps[player][0] and world.owMixed[player])): + or (region_name == 'Pyramid Exit Ledge' and world.shuffle[player] != 'insanity' or invFlag != (0x1b in world.owswaps[player][0] and world.owMixed[player])): inaccessible_regions.remove(region_name) elif region.type == (RegionType.LightWorld if not invFlag else RegionType.DarkWorld): must_exit_regions.append(region_name) @@ -1523,12 +1521,12 @@ def connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, playe inaccessible_entrances = list() region = world.get_region(region_name, player) for exit in region.exits: - if not exit.connected_region and exit.name in entrance_pool and (world.shuffle[player] not in ['lite', 'lean'] or exit.name in pool): + if not exit.connected_region and exit.name in [e for e in entrance_pool if e not in ignore_list] and (world.shuffle[player] not in ['lite', 'lean'] or exit.name in pool): inaccessible_entrances.append(exit.name) if len(inaccessible_entrances): random.shuffle(inaccessible_entrances) connect_mandatory_exits(world, pool, caves, [inaccessible_entrances.pop()], player) - connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, player) + connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, player, ignore_list) # connect one connector at a time to ensure multiple connectors aren't assigned to the same inaccessible set of regions if world.shuffle[player] in ['lean', 'crossed', 'insanity']: From d336c1814436bf9bd220ebf94d6305804af33ddc Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 28 Oct 2021 14:01:28 -0500 Subject: [PATCH 74/76] Version bump 0.2.1.1 --- CHANGELOG.md | 6 ++++++ OverworldShuffle.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f76fb7c7..7bc7474a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +### 0.2.1.1 +- Many fixes to ER: infinite loops, preventing cross-world scenarios in non-cross-world modes +- Spoiler log improvements, outputs in stages so a Spoiler is available if an error occurs +- Added no_race option for Mystery +- Fixed output_path in Mystery to use the saved setting if none is specified on CLI + ### 0.2.1.0 - Implemented Whirlpool Shuffle diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 6e3f52f2..449af86d 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -3,7 +3,7 @@ from BaseClasses import OWEdge, WorldType, RegionType, Direction, Terrain, PolSl from Regions import mark_dark_world_regions, mark_light_world_regions from OWEdges import OWTileRegions, OWTileGroups, OWEdgeGroups, OWExitTypes, OpenStd, parallel_links, IsParallel -__version__ = '0.2.1.0-u' +__version__ = '0.2.1.1-u' def link_overworld(world, player): # setup mandatory connections From 3bdbc4e26821cfcb6ee0a4eacfc59a54a22f3435 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 28 Oct 2021 21:41:22 -0500 Subject: [PATCH 75/76] Fixed issue with whirlpools not changing world in Crossed OW --- Rom.py | 2 +- data/base2current.bps | Bin 141107 -> 141111 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index 5be364ec..bdb02639 100644 --- a/Rom.py +++ b/Rom.py @@ -33,7 +33,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'e9dea70e0a0b15bfa0ff7ecd63228a0c' +RANDOMIZERBASEHASH = '77b7b9a93f622b0cd08c49e9ee4eac4a' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index aa914956d578657a299b8406d23a1e7e69766b86..a552143f7db2b8a7e3c3c6c125d579da90ad6709 100644 GIT binary patch delta 184 zcmV;p07w6`&Iq^82(U>31pEpE^|MU@iUt9rv)Bjw1Q~ybs_H<9og$u?pdwbsq?R2= zn(Gx-l8Ill>31oI6H^0Q3=iUt9jv)Bjw1Q~aTs_H<9og$u?pdwbsq?R2= zn(Gx-l8INd>wWA#QUCw| From 8efed3a8292e197b8b266ebf3c6a1a39da48aed4 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 28 Oct 2021 21:42:43 -0500 Subject: [PATCH 76/76] Version bump 0.2.1.2 --- CHANGELOG.md | 3 +++ OverworldShuffle.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bc7474a..f5d855fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +### 0.2.1.2 +- Fixed issue with whirlpools not changing world when in Crossed OW + ### 0.2.1.1 - Many fixes to ER: infinite loops, preventing cross-world scenarios in non-cross-world modes - Spoiler log improvements, outputs in stages so a Spoiler is available if an error occurs diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 449af86d..8207d66b 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -3,7 +3,7 @@ from BaseClasses import OWEdge, WorldType, RegionType, Direction, Terrain, PolSl from Regions import mark_dark_world_regions, mark_light_world_regions from OWEdges import OWTileRegions, OWTileGroups, OWEdgeGroups, OWExitTypes, OpenStd, parallel_links, IsParallel -__version__ = '0.2.1.1-u' +__version__ = '0.2.1.2-u' def link_overworld(world, player): # setup mandatory connections