From 0c640bf9dd252fa98a7723d8359a9066208c81ce Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 10 Jul 2023 10:35:18 -0600 Subject: [PATCH 01/30] Fix for pyrmaid hole logic --- Main.py | 2 +- RELEASENOTES.md | 3 +++ Rules.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Main.py b/Main.py index 107b0694..e3f3df67 100644 --- a/Main.py +++ b/Main.py @@ -34,7 +34,7 @@ from source.overworld.EntranceShuffle2 import link_entrances_new from source.tools.BPS import create_bps_from_data from source.classes.CustomSettings import CustomSettings -version_number = '1.2.0.17' +version_number = '1.2.0.18' version_branch = '-u' __version__ = f'{version_number}{version_branch}' diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e3d09589..a1013f6c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -109,6 +109,9 @@ These are now independent of retro mode and have three options: None, Random, an # Bug Fixes and Notes +* 1.2.0.18u + * Fixed an issue with pyramid hole being in logic when it is not opened. + * * 1.2.0.17u * Fixed logic bug that allowed Pearl to be behind Graveyard Cave or King's Tomb entrances with only Mirror and West Dark World access (cross world shuffles only) * Removed backup locations for Dungeon Only and Major Only algorithms. If item cannot be placed in the appropriate location, the seed will fail to generate instead diff --git a/Rules.py b/Rules.py index 80392396..26e70f5f 100644 --- a/Rules.py +++ b/Rules.py @@ -887,7 +887,7 @@ def ow_inverted_rules(world, player): set_rule(world.get_entrance('Hyrule Castle Main Gate', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Hyrule Castle Main Gate (North)', player), lambda state: state.has_Mirror(player)) set_rule(world.get_location('Frog', player), lambda state: state.can_lift_heavy_rocks(player) and state.has_Pearl(player)) - set_rule(world.get_entrance('Pyramid Hole', player), lambda state: world.open_pyramid[player] or world.goal[player] == 'trinity' or state.has('Beat Agahnim 2', player)) + set_rule(world.get_entrance('Pyramid Hole', player), lambda state: world.is_pyramid_open(player) or state.has('Beat Agahnim 2', player)) else: set_rule(world.get_entrance('East Dark Death Mountain Teleporter (Top)', player), lambda state: state.can_lift_heavy_rocks(player) and state.has('Hammer', player) and state.has_Pearl(player)) # bunny cannot use hammer set_rule(world.get_entrance('East Dark Death Mountain Teleporter (Bottom)', player), lambda state: state.can_lift_heavy_rocks(player)) From 5d2ceaf75c22522a9cb15b45760e907d4df582cf Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 10 Jul 2023 13:56:05 -0600 Subject: [PATCH 02/30] Updated baserom for crystal custscene and hera music fix Updated a couple of error messages and when they are displayed Updated Ganonhunt goal text to be more consistent across randomizers --- ItemList.py | 3 ++- Main.py | 4 +++- README.md | 2 +- RELEASENOTES.md | 7 +++++-- Rom.py | 2 +- data/base2current.bps | Bin 94044 -> 94169 bytes resources/app/gui/lang/en.json | 2 +- 7 files changed, 13 insertions(+), 7 deletions(-) diff --git a/ItemList.py b/ItemList.py index b9e3f4c8..a44a3f6e 100644 --- a/ItemList.py +++ b/ItemList.py @@ -1287,7 +1287,8 @@ def make_customizer_pool(world, player): bow_found = next((i for i in pool if i in {'Bow', 'Progressive Bow'}), None) if not bow_found: missing_items.append('Progressive Bow') - logging.getLogger('').warning(f'The following items are not in the custom item pool {", ".join(missing_items)}') + if missing_items: + logging.getLogger('').warning(f'The following items are not in the custom item pool {", ".join(missing_items)}') g, t = set_default_triforce(world.goal[player], world.treasure_hunt_count[player], world.treasure_hunt_total[player]) diff --git a/Main.py b/Main.py index e3f3df67..2edf2c03 100644 --- a/Main.py +++ b/Main.py @@ -628,7 +628,9 @@ def create_playthrough(world): logging.getLogger('').debug(world.fish.translate("cli", "cli", "building.calculating.spheres"), len(collection_spheres), len(sphere), len(prog_locations)) if not sphere: - logging.getLogger('').error(world.fish.translate("cli", "cli", "cannot.reach.items"), [world.fish.translate("cli","cli","cannot.reach.item") % (location.item.name, location.item.player, location.name, location.player) for location in sphere_candidates]) + if world.accessibility[location.item.player] != 'none': + logging.getLogger('').error(world.fish.translate("cli", "cli", "cannot.reach.items"), + [world.fish.translate("cli","cli","cannot.reach.item") % (location.item.name, location.item.player, location.name, location.player) for location in sphere_candidates]) if any([location.name not in optional_locations and world.accessibility[location.item.player] != 'none' for location in sphere_candidates]): raise RuntimeError(world.fish.translate("cli", "cli", "cannot.reach.progression")) else: diff --git a/README.md b/README.md index fbe9eed5..21a444cc 100644 --- a/README.md +++ b/README.md @@ -404,7 +404,7 @@ CLI: `--logic owglitches` New supported goals: * Trinity: Find one of 3 triforces to win. One is at pedestal. One is with Ganon. One is with Murahdahla who wants you to find 8 of 10 triforce pieces to complete. -* Triforce Hunt + Ganon: Collect the requisite triforce pieces, then defeat Ganon. (Aga2 not required). Use `ganonhunt` on CLI +* Ganonhunt: Collect the requisite triforce pieces, then defeat Ganon. (Aga2 not required). Use `ganonhunt` on CLI * Completionist: All dungeons not enough for you? You have to obtain every item in the game too. This option turns on the collection rate counter and forces accessibility to be 100% locations. Finish by defeating Ganon. diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a1013f6c..5dc0ee0a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -57,7 +57,7 @@ Please see [Customizer documentation](docs/Customizer.md) on how to create custo ## New Goals -### Triforce Hunt + Ganon +### Ganonhunt Collect the requisite triforce pieces, then defeat Ganon. (Aga2 not required). Use `ganonhunt` on CLI ### Completionist @@ -111,7 +111,10 @@ These are now independent of retro mode and have three options: None, Random, an * 1.2.0.18u * Fixed an issue with pyramid hole being in logic when it is not opened. - * + * Crystal cutscene at GT use new symmetrical layouts (thanks Codemann) + * Fix for Hera Boss music (thanks Codemann) + * Fixed accessibility: none using a spoiling message + * Fixed warning message about custom item pool when it is fine * 1.2.0.17u * Fixed logic bug that allowed Pearl to be behind Graveyard Cave or King's Tomb entrances with only Mirror and West Dark World access (cross world shuffles only) * Removed backup locations for Dungeon Only and Major Only algorithms. If item cannot be placed in the appropriate location, the seed will fail to generate instead diff --git a/Rom.py b/Rom.py index ea0b9c68..482080b2 100644 --- a/Rom.py +++ b/Rom.py @@ -37,7 +37,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '9903cdfc3fc69112919ec49fb63e09ab' +RANDOMIZERBASEHASH = '467681d6160233f7af2761c631e26985' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index e58c28ab1b469f73a96a04d2e5d5d50a2ca92b26..9b3d673a51005eba1d3d336d10206d582cb1852b 100644 GIT binary patch delta 8191 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 delta 8271 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°`X4)5(K{T}vV8Q!2ldJX#&(odD~x(*$f+FJ%jrb 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 WZ`*gx12YFmOGf(@j^A=;)c*lzQAw!) diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index eb53f640..9d56137c 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -241,7 +241,7 @@ "randomizer.item.goal.triforcehunt": "Triforce Hunt", "randomizer.item.goal.trinity": "Trinity", "randomizer.item.goal.crystals": "Crystals", - "randomizer.item.goal.ganonhunt": "Triforce Hunt + Ganon", + "randomizer.item.goal.ganonhunt": "Ganonhunt", "randomizer.item.goal.completionist": "Completionist", "randomizer.item.crystals_gt": "Crystals to open GT", From d135405ed3bb76bd68d16e20ac1235eb65294129 Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 12 Jul 2023 09:16:00 -0600 Subject: [PATCH 03/30] Customizer: Exception raised for placements of items that are not in pool --- ItemList.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ItemList.py b/ItemList.py index a44a3f6e..5ffb5152 100644 --- a/ItemList.py +++ b/ItemList.py @@ -1397,6 +1397,8 @@ def fill_specific_items(world): track_dungeon_items(item_to_place, loc, world) loc.event = (event_flag or item_to_place.advancement or item_to_place.bigkey or item_to_place.smallkey) + else: + raise Exception(f'Did not find "{item}" in item pool to place at "{location}"') advanced_placements = world.customizer.get_advanced_placements() if advanced_placements: for player, placement_list in advanced_placements.items(): @@ -1406,7 +1408,7 @@ def fill_specific_items(world): item_to_place, event_flag = get_item_and_event_flag(item, world, player, dungeon_pool, prize_set, prize_pool) if not item_to_place: - continue + raise Exception(f'Did not find "{item}" in item pool to place for a LocationGroup"') locations = placement['locations'] handled = False while not handled: From 1b81151941eca3fb963e010b1bb023ead6ed262d Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 12 Jul 2023 09:50:53 -0600 Subject: [PATCH 04/30] Customizer: Fixed issue with Assured sword and start inventory --- ItemList.py | 31 +++++++++++++++++-------------- RELEASENOTES.md | 4 +++- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/ItemList.py b/ItemList.py index 5ffb5152..168d841d 100644 --- a/ItemList.py +++ b/ItemList.py @@ -1297,20 +1297,23 @@ def make_customizer_pool(world, player): if pieces < t: pool.extend(['Triforce Piece'] * (t - pieces)) - if not world.customizer.get_start_inventory(): - if world.logic[player] in ['owglitches', 'nologic']: - precollected_items.append('Pegasus Boots') - if 'Pegasus Boots' in pool: - pool.remove('Pegasus Boots') - pool.append('Rupees (20)') - if world.swords[player] == 'assured': - precollected_items.append('Progressive Sword') - if 'Progressive Sword' in pool: - pool.remove('Progressive Sword') - pool.append('Rupees (50)') - elif 'Fighter Sword' in pool: - pool.remove('Fighter Sword') - pool.append('Rupees (50)') + sphere_0 = world.customizer.get_start_inventory() + no_start_inventory = not sphere_0 or not sphere_0[player] + init_equip = [] if no_start_inventory else sphere_0[player] + if (world.logic[player] in ['owglitches', 'nologic'] + and (no_start_inventory or all(x != 'Pegasus Boots' for x in init_equip))): + precollected_items.append('Pegasus Boots') + if 'Pegasus Boots' in pool: + pool.remove('Pegasus Boots') + pool.append('Rupees (20)') + if world.swords[player] == 'assured' and (no_start_inventory or all(' Sword' not in x for x in init_equip)): + precollected_items.append('Progressive Sword') + if 'Progressive Sword' in pool: + pool.remove('Progressive Sword') + pool.append('Rupees (50)') + elif 'Fighter Sword' in pool: + pool.remove('Fighter Sword') + pool.append('Rupees (50)') return pool, placed_items, precollected_items, clock_mode, 1 diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5dc0ee0a..5f11c963 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -113,7 +113,9 @@ These are now independent of retro mode and have three options: None, Random, an * Fixed an issue with pyramid hole being in logic when it is not opened. * Crystal cutscene at GT use new symmetrical layouts (thanks Codemann) * Fix for Hera Boss music (thanks Codemann) - * Fixed accessibility: none using a spoiling message + * Customizer: fixed an issue with assured sword and start_inventory + * Customizer: warns when trying to specifically place an item that's not in the item pool + * Fixed "accessibility: none" displaying a spoiling message * Fixed warning message about custom item pool when it is fine * 1.2.0.17u * Fixed logic bug that allowed Pearl to be behind Graveyard Cave or King's Tomb entrances with only Mirror and West Dark World access (cross world shuffles only) From a987e58fc9079ef2d0358c2438dda1ac59df81c7 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Mon, 17 Jul 2023 01:37:55 -0500 Subject: [PATCH 05/30] Adding Flute Activation only if an activated flute cannot be found elsewhere --- ItemList.py | 3 ++- Rules.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ItemList.py b/ItemList.py index bb23ecec..7fe75e3b 100644 --- a/ItemList.py +++ b/ItemList.py @@ -212,7 +212,8 @@ def generate_itempool(world, player): loc.locked = True loc.forced_item = loc.item - if not world.is_tile_swapped(0x18, player): + if (world.flute_mode[player] != 'active' and not world.is_tile_swapped(0x18, player) + and 'Ocarina (Activated)' not in list(map(str, [i for i in world.precollected_items if i.player == player]))): region = world.get_region('Kakariko Village',player) loc = Location(player, "Flute Activation", parent=region) diff --git a/Rules.py b/Rules.py index f1383ee6..024c1436 100644 --- a/Rules.py +++ b/Rules.py @@ -69,7 +69,8 @@ def set_rules(world, player): elif world.goal[player] == 'completionist': add_rule(world.get_location('Ganon', player), lambda state: state.everything(player)) - if not world.is_tile_swapped(0x18, player): + if (world.flute_mode[player] != 'active' and not world.is_tile_swapped(0x18, player) + and 'Ocarina (Activated)' not in list(map(str, [i for i in world.precollected_items if i.player == player]))): if not world.is_copied_world: # Commented out below, this would be needed for rando implementations where Inverted requires flute activation in bunny territory # kak_region = self.world.get_region('Kakariko Village', player) From 213d3d3aa0798722225e9c3707c2945b96c1d3b9 Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 19 Jul 2023 12:31:50 -0600 Subject: [PATCH 06/30] Fixed an issue where certain vanilla door types would not allow other types to be placed. Customizer: fixed an issue where last ditch placements would move customized items. Those are now locked and the generation will fail instead if no alternatives are found. --- DoorShuffle.py | 28 +++++++++++++++------------- ItemList.py | 2 ++ RELEASENOTES.md | 2 ++ 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index a0add44b..35cc7624 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1962,7 +1962,7 @@ def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, all_cu if flex_map[dungeon] > 0: queue.append(dungeon) # time to re-assign - reassign_big_key_doors(bk_map, world, player) + reassign_big_key_doors(bk_map, used_doors, world, player) for name, big_list in bk_map.items(): used_doors.update(flatten_pair_list(big_list)) return used_doors @@ -2047,7 +2047,7 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, all_ else: builder.key_doors_num -= 1 # time to re-assign - reassign_key_doors(small_map, world, player) + reassign_key_doors(small_map, used_doors, world, player) for dungeon_name in pool: if world.keyshuffle[player] != 'universal': builder = world.dungeon_layouts[player][dungeon_name] @@ -2129,7 +2129,7 @@ def shuffle_bomb_dash_doors(door_type_pools, used_doors, start_regions_map, all_ suggestion_map[dungeon] = pair queue.append(dungeon) # time to re-assign - reassign_bd_doors(bd_map, world, player) + reassign_bd_doors(bd_map, used_doors, world, player) for name, pair in bd_map.items(): used_doors.update(flatten_pair_list(pair[0])) used_doors.update(flatten_pair_list(pair[1])) @@ -2539,7 +2539,7 @@ def find_current_bk_doors(builder): return current_doors -def reassign_big_key_doors(bk_map, world, player): +def reassign_big_key_doors(bk_map, used_doors, world, player): logger = logging.getLogger('') for name, big_doors in bk_map.items(): flat_proposal = flatten_pair_list(big_doors) @@ -2547,11 +2547,12 @@ def reassign_big_key_doors(bk_map, world, player): queue = deque(find_current_bk_doors(builder)) while len(queue) > 0: d = queue.pop() - if d.type is DoorType.Interior and d not in flat_proposal and d.dest not in flat_proposal: + if (d.type is DoorType.Interior and d not in flat_proposal and d.dest not in flat_proposal + and d not in used_doors and d.dest not in used_doors): if not d.entranceFlag: world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) d.bigKey = False - elif d.type is DoorType.Normal and d not in flat_proposal: + elif d.type is DoorType.Normal and d not in flat_proposal and d not in used_doors: if not d.entranceFlag: world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) d.bigKey = False @@ -2795,7 +2796,7 @@ def find_valid_bd_combination(builder, suggested, world, player): return bomb_proposal, dash_proposal, ttl_needed -def reassign_bd_doors(bd_map, world, player): +def reassign_bd_doors(bd_map, used_doors, world, player): for name, pair in bd_map.items(): flat_bomb_proposal = flatten_pair_list(pair[0]) flat_dash_proposal = flatten_pair_list(pair[1]) @@ -2808,10 +2809,10 @@ def reassign_bd_doors(bd_map, world, player): queue = deque(find_current_bd_doors(builder, world)) while len(queue) > 0: d = queue.pop() - if d.type is DoorType.Interior and not_in_proposal(d): + if d.type is DoorType.Interior and not_in_proposal(d) and d not in used_doors and d.dest not in used_doors: if not d.entranceFlag: world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) - elif d.type is DoorType.Normal and not_in_proposal(d): + elif d.type is DoorType.Normal and not_in_proposal(d) and d not in used_doors: if not d.entranceFlag: world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) do_bombable_dashable(pair[0], DoorKind.Bombable, world, player) @@ -3003,7 +3004,7 @@ def valid_key_door_pair(door1, door2): return len(door1.entrance.parent_region.exits) <= 1 or len(door2.entrance.parent_region.exits) <= 1 -def reassign_key_doors(small_map, world, player): +def reassign_key_doors(small_map, used_doors, world, player): logger = logging.getLogger('') for name, small_doors in small_map.items(): logger.debug(f'Key doors for {name}') @@ -3013,7 +3014,7 @@ def reassign_key_doors(small_map, world, player): queue = deque(find_current_key_doors(builder)) while len(queue) > 0: d = queue.pop() - if d.type is DoorType.SpiralStairs and d not in proposal: + if d.type is DoorType.SpiralStairs and d not in proposal and d not in used_doors: room = world.get_room(d.roomIndex, player) if room.doorList[d.doorListPos][1] == DoorKind.StairKeyLow: room.delete(d.doorListPos) @@ -3023,13 +3024,14 @@ def reassign_key_doors(small_map, world, player): else: room.delete(d.doorListPos) d.smallKey = False - elif d.type is DoorType.Interior and d not in flat_proposal and d.dest not in flat_proposal: + elif (d.type is DoorType.Interior and d not in flat_proposal and d.dest not in flat_proposal + and d not in used_doors and d.dest not in used_doors): if not d.entranceFlag: world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) d.smallKey = False d.dest.smallKey = False queue.remove(d.dest) - elif d.type is DoorType.Normal and d not in flat_proposal: + elif d.type is DoorType.Normal and d not in flat_proposal and d not in used_doors: if not d.entranceFlag: world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) d.smallKey = False diff --git a/ItemList.py b/ItemList.py index 168d841d..9ca15555 100644 --- a/ItemList.py +++ b/ItemList.py @@ -1396,6 +1396,7 @@ def fill_specific_items(world): dungeon_pool, prize_set, prize_pool) if item_to_place: world.push_item(loc, item_to_place, False) + loc.locked = True track_outside_keys(item_to_place, loc, world) track_dungeon_items(item_to_place, loc, world) loc.event = (event_flag or item_to_place.advancement @@ -1431,6 +1432,7 @@ def fill_specific_items(world): if loc.item: continue world.push_item(loc, item_to_place, False) + loc.locked = True track_outside_keys(item_to_place, loc, world) track_dungeon_items(item_to_place, loc, world) loc.event = (event_flag or item_to_place.advancement diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5f11c963..7010ecb6 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -113,6 +113,8 @@ These are now independent of retro mode and have three options: None, Random, an * Fixed an issue with pyramid hole being in logic when it is not opened. * Crystal cutscene at GT use new symmetrical layouts (thanks Codemann) * Fix for Hera Boss music (thanks Codemann) + * Fixed an issue where certain vanilla door types would not allow other types to be placed. + * Customizer: fixed an issue where last ditch placements would move customized items. Those are now locked and the generation will fail instead is no alternative are found. * Customizer: fixed an issue with assured sword and start_inventory * Customizer: warns when trying to specifically place an item that's not in the item pool * Fixed "accessibility: none" displaying a spoiling message From b6275d0688f7ab92cb5811f7aac8243c956ddabd Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 19 Jul 2023 12:57:07 -0600 Subject: [PATCH 07/30] Typo --- RELEASENOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 7010ecb6..45577d04 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -114,7 +114,7 @@ These are now independent of retro mode and have three options: None, Random, an * Crystal cutscene at GT use new symmetrical layouts (thanks Codemann) * Fix for Hera Boss music (thanks Codemann) * Fixed an issue where certain vanilla door types would not allow other types to be placed. - * Customizer: fixed an issue where last ditch placements would move customized items. Those are now locked and the generation will fail instead is no alternative are found. + * Customizer: fixed an issue where last ditch placements would move customized items. Those are now locked and the generation will fail instead if no alternatives are found. * Customizer: fixed an issue with assured sword and start_inventory * Customizer: warns when trying to specifically place an item that's not in the item pool * Fixed "accessibility: none" displaying a spoiling message From 2c6af4e44cc438b5eaeb9dc26c6a9e3835e13d27 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Mon, 17 Jul 2023 04:28:12 -0500 Subject: [PATCH 08/30] More Swapped ER fixes --- source/overworld/EntranceShuffle2.py | 43 +++++++++++++++++++--------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/source/overworld/EntranceShuffle2.py b/source/overworld/EntranceShuffle2.py index f11d0081..16ce2dfc 100644 --- a/source/overworld/EntranceShuffle2.py +++ b/source/overworld/EntranceShuffle2.py @@ -289,6 +289,7 @@ def do_main_shuffle(entrances, exits, avail, mode_def): bomb_shop_options = [x for x in bomb_shop_options if x not in ['Spectacle Rock Cave', 'Spectacle Rock Cave (Bottom)']] if avail.swapped and len(bomb_shop_options) > 1: bomb_shop_options = [x for x in bomb_shop_options if x != 'Big Bomb Shop'] + bomb_shop_choice = random.choice(bomb_shop_options) connect_entrance(bomb_shop_choice, bomb_shop, avail) rem_entrances.remove(bomb_shop_choice) @@ -433,6 +434,8 @@ def do_blacksmith(entrances, exits, avail): if avail.swapped: blacksmith_options = [e for e in blacksmith_options if e not in Forbidden_Swap_Entrances] blacksmith_options = [x for x in blacksmith_options if x in entrances] + + assert len(blacksmith_options), 'No available entrances left to place Blacksmith' blacksmith_choice = random.choice(blacksmith_options) connect_entrance(blacksmith_choice, 'Blacksmiths Hut', avail) entrances.remove(blacksmith_choice) @@ -563,6 +566,7 @@ def do_dark_sanc(entrances, exits, avail): choices = [e for e in avail.world.districts[avail.player]['Northwest Dark World'].entrances if e not in forbidden and e in entrances] else: choices = [e for e in get_starting_entrances(avail) if e not in forbidden and e in entrances] + choice = random.choice(choices) entrances.remove(choice) exits.remove('Dark Sanctuary Hint') @@ -596,6 +600,10 @@ def do_links_house(entrances, exits, avail, cross_world): if avail.inverted: dark_sanc_region = avail.world.get_entrance('Dark Sanctuary Hint Exit', avail.player).connected_region.name forbidden.extend(get_nearby_entrances(avail, dark_sanc_region)) + else: + if (avail.world.doorShuffle[avail.player] != 'vanilla' and avail.world.intensity[avail.player] > 2 + and not avail.world.is_tile_swapped(0x1b, avail.player)): + forbidden.append('Hyrule Castle Entrance (South)') if avail.swapped: forbidden.append(links_house_vanilla) forbidden.extend(Forbidden_Swap_Entrances) @@ -1381,13 +1389,17 @@ def do_mandatory_connections(avail, entrances, cave_options, must_exit): elif cave[-1] == 'Spectacle Rock Cave Exit': # Spectacle rock only has one exit cave_entrances = [] for cave_exit in rnd_cave[:-1]: - entrance = next(e for e in entrances[::-1] if e not in invalid_connections[exit] and e not in must_exit) - cave_entrances.append(entrance) - entrances.remove(entrance) - connect_two_way(entrance, cave_exit, avail) - if avail.swapped and combine_map[entrance] != cave_exit: - swap_ent, _ = connect_cave_swap(entrance, cave_exit, cave) - entrances.remove(swap_ent) + if avail.swapped and cave_exit not in avail.exits: + entrance = avail.world.get_entrance(cave_exit, avail.player).parent_region.entrances[0].name + cave_entrances.append(entrance) + else: + entrance = next(e for e in entrances[::-1] if e not in invalid_connections[exit] and e not in must_exit) + cave_entrances.append(entrance) + entrances.remove(entrance) + connect_two_way(entrance, cave_exit, avail) + if avail.swapped and combine_map[entrance] != cave_exit: + swap_ent, _ = connect_cave_swap(entrance, cave_exit, cave) + entrances.remove(swap_ent) if entrance not in invalid_connections: invalid_connections[exit] = set() if all(entrance in invalid_connections for entrance in cave_entrances): @@ -1408,13 +1420,16 @@ def do_mandatory_connections(avail, entrances, cave_options, must_exit): for cave in used_caves: if cave in cave_options: # 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)]) - invalid_cave_connections[tuple(cave)] = set() - entrances.remove(entrance) - connect_two_way(entrance, cave_exit, avail) - if avail.swapped and combine_map[entrance] != cave_exit: - swap_ent, _ = connect_cave_swap(entrance, cave_exit, cave) - entrances.remove(swap_ent) + if avail.swapped and cave_exit not in avail.exits: + continue + else: + entrance = next(e for e in entrances[::-1] if e not in invalid_cave_connections[tuple(cave)]) + invalid_cave_connections[tuple(cave)] = set() + entrances.remove(entrance) + connect_two_way(entrance, cave_exit, avail) + if avail.swapped and combine_map[entrance] != cave_exit: + swap_ent, _ = connect_cave_swap(entrance, cave_exit, cave) + entrances.remove(swap_ent) cave_options.remove(cave) if avail.swapped: entrances.extend(swap_forbidden) From 6d03c5c5325bf7cf9579b8d52b003c61c6865b6e Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 19 Jul 2023 14:14:01 -0500 Subject: [PATCH 09/30] Merge branch 'OverworldShuffleDev' of https://github.com/codemann8/ALttPDoorRandomizer into OverworldShuffleDev Adding console output when generation fails to reach required items --- Main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Main.py b/Main.py index c426cef7..cb2d4983 100644 --- a/Main.py +++ b/Main.py @@ -877,6 +877,7 @@ def create_playthrough(world): if world.has_beaten_game(state): required_locations.clear() else: + logging.getLogger('').error(world.fish.translate("cli", "cli", "cannot.reach.items"), [world.fish.translate("cli","cli","cannot.reach.item") % (loc.item.name, loc.item.player, loc.name, loc.player) for loc in required_locations]) raise RuntimeError(world.fish.translate("cli","cli","cannot.reach.required")) # store the required locations for statistical analysis From ea70d7cb5add7a6c952103ace2fdb3ade4405794 Mon Sep 17 00:00:00 2001 From: Catobat <69204835+Catobat@users.noreply.github.com> Date: Wed, 26 Jul 2023 02:40:40 +0200 Subject: [PATCH 10/30] Move Triforce Pieces Min and Max handling to Main --- CLI.py | 2 +- Main.py | 22 ++++++++++++++++++++-- source/tools/MysteryUtils.py | 16 +++++++--------- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/CLI.py b/CLI.py index 312c1d80..85283053 100644 --- a/CLI.py +++ b/CLI.py @@ -229,7 +229,7 @@ def parse_settings(): "triforce_pool_max": 0, "triforce_goal_min": 0, "triforce_goal_max": 0, - "triforce_min_difference": 10, + "triforce_min_difference": 0, "code": "", "multi": 1, diff --git a/Main.py b/Main.py index 2edf2c03..049f5932 100644 --- a/Main.py +++ b/Main.py @@ -124,8 +124,6 @@ def main(args, seed=None, fish=None): world.potshuffle = args.shufflepots.copy() world.mixed_travel = args.mixed_travel.copy() world.standardize_palettes = args.standardize_palettes.copy() - world.treasure_hunt_count = {k: int(v) for k, v in args.triforce_goal.items()} - world.treasure_hunt_total = {k: int(v) for k, v in args.triforce_pool.items()} world.shufflelinks = args.shufflelinks.copy() world.shuffletavern = args.shuffletavern.copy() world.pseudoboots = args.pseudoboots.copy() @@ -135,6 +133,26 @@ def main(args, seed=None, fish=None): world.collection_rate = args.collection_rate.copy() world.colorizepots = args.colorizepots.copy() + world.treasure_hunt_count = {} + world.treasure_hunt_total = {} + for p in args.triforce_goal: + if int(args.triforce_goal[p]) != 0 or int(args.triforce_pool[p]) != 0 or int(args.triforce_goal_min[p]) != 0 or int(args.triforce_goal_max[p]) != 0 or int(args.triforce_pool_min[p]) != 0 or int(args.triforce_pool_max[p]) != 0: + if int(args.triforce_goal[p]) != 0: + world.treasure_hunt_count[p] = int(args.triforce_goal[p]) + elif int(args.triforce_goal_min[p]) != 0 and int(args.triforce_goal_max[p]) != 0: + world.treasure_hunt_count[p] = random.randint(int(args.triforce_goal_min[p]), int(args.triforce_goal_max[p])) + else: + world.treasure_hunt_count[p] = 8 if world.goal[p] == 'trinity' else 20 + if int(args.triforce_pool[p]) != 0: + world.treasure_hunt_total[p] = int(args.triforce_pool[p]) + elif int(args.triforce_pool_min[p]) != 0 and int(args.triforce_pool_max[p]) != 0: + world.treasure_hunt_total[p] = random.randint(max(int(args.triforce_pool_min[p]), world.treasure_hunt_count[p] + int(args.triforce_min_difference[p])), int(args.triforce_pool_max[p])) + else: + world.treasure_hunt_total[p] = 10 if world.goal[p] == 'trinity' else 30 + else: + # this will be handled in ItemList.py and custom item pool is used to determine the numbers + world.treasure_hunt_count[p], world.treasure_hunt_total[p] = 0, 0 + world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)} world.finish_init() diff --git a/source/tools/MysteryUtils.py b/source/tools/MysteryUtils.py index 8f371b6c..32ee9d5f 100644 --- a/source/tools/MysteryUtils.py +++ b/source/tools/MysteryUtils.py @@ -125,15 +125,13 @@ def roll_settings(weights): ret.crystals_ganon = get_choice('ganon_open') - from ItemList import set_default_triforce - default_tf_goal, default_tf_pool = set_default_triforce(ret.goal, 0, 0) - goal_min = get_choice_default('triforce_goal_min', default=default_tf_goal) - goal_max = get_choice_default('triforce_goal_max', default=default_tf_goal) - pool_min = get_choice_default('triforce_pool_min', default=default_tf_pool) - pool_max = get_choice_default('triforce_pool_max', default=default_tf_pool) - ret.triforce_goal = random.randint(int(goal_min), int(goal_max)) - min_diff = get_choice_default('triforce_min_difference', default=default_tf_pool-default_tf_goal) - ret.triforce_pool = random.randint(max(int(pool_min), ret.triforce_goal + int(min_diff)), int(pool_max)) + ret.triforce_pool = get_choice_default('triforce_pool', default=0) + ret.triforce_goal = get_choice_default('triforce_goal', default=0) + ret.triforce_pool_min = get_choice_default('triforce_pool_min', default=0) + ret.triforce_pool_max = get_choice_default('triforce_pool_max', default=0) + ret.triforce_goal_min = get_choice_default('triforce_goal_min', default=0) + ret.triforce_goal_max = get_choice_default('triforce_goal_max', default=0) + ret.triforce_min_difference = get_choice_default('triforce_min_difference', default=0) ret.mode = get_choice('world_state') if ret.mode == 'retro': From 66bd8960a1738d3b91b63f920ce05a9c9a91e078 Mon Sep 17 00:00:00 2001 From: Catobat <69204835+Catobat@users.noreply.github.com> Date: Wed, 26 Jul 2023 15:03:45 +0200 Subject: [PATCH 11/30] Support Triforce Piece settings in Customizer YAML --- source/classes/CustomSettings.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index deb9f4d2..ca0f2062 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -144,6 +144,11 @@ class CustomSettings(object): args.pseudoboots[p] = get_setting(settings['pseudoboots'], args.pseudoboots[p]) args.triforce_goal[p] = get_setting(settings['triforce_goal'], args.triforce_goal[p]) args.triforce_pool[p] = get_setting(settings['triforce_pool'], args.triforce_pool[p]) + args.triforce_goal_min[p] = get_setting(settings['triforce_goal_min'], args.triforce_goal_min[p]) + args.triforce_goal_max[p] = get_setting(settings['triforce_goal_max'], args.triforce_goal_max[p]) + args.triforce_pool_min[p] = get_setting(settings['triforce_pool_min'], args.triforce_pool_min[p]) + args.triforce_pool_max[p] = get_setting(settings['triforce_pool_max'], args.triforce_pool_max[p]) + args.triforce_min_difference[p] = get_setting(settings['triforce_min_difference'], args.triforce_min_difference[p]) args.beemizer[p] = get_setting(settings['beemizer'], args.beemizer[p]) # mystery usage From bf9ad536fe3aa34f7130815c8ec41fa6671c622f Mon Sep 17 00:00:00 2001 From: Catobat <69204835+Catobat@users.noreply.github.com> Date: Wed, 26 Jul 2023 15:27:47 +0200 Subject: [PATCH 12/30] Implement triforce_max_difference --- CLI.py | 3 ++- Main.py | 2 +- README.md | 3 ++- mystery_example.yml | 1 + mystery_testsuite.yml | 1 + resources/app/cli/args.json | 1 + source/classes/CustomSettings.py | 1 + source/tools/MysteryUtils.py | 1 + 8 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CLI.py b/CLI.py index 85283053..2c925c6c 100644 --- a/CLI.py +++ b/CLI.py @@ -132,7 +132,7 @@ def parse_cli(argv, no_defaults=False): 'flute_mode', 'bow_mode', 'take_any', 'boots_hint', 'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', - 'usestartinventory', 'bombbag', 'overworld_map', 'restrict_boss_items', + 'usestartinventory', 'bombbag', 'overworld_map', 'restrict_boss_items', 'triforce_max_difference', 'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max', 'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'shuffletavern', 'pseudoboots', 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters', @@ -230,6 +230,7 @@ def parse_settings(): "triforce_goal_min": 0, "triforce_goal_max": 0, "triforce_min_difference": 0, + "triforce_max_difference": 10000, "code": "", "multi": 1, diff --git a/Main.py b/Main.py index 049f5932..17444ce7 100644 --- a/Main.py +++ b/Main.py @@ -146,7 +146,7 @@ def main(args, seed=None, fish=None): if int(args.triforce_pool[p]) != 0: world.treasure_hunt_total[p] = int(args.triforce_pool[p]) elif int(args.triforce_pool_min[p]) != 0 and int(args.triforce_pool_max[p]) != 0: - world.treasure_hunt_total[p] = random.randint(max(int(args.triforce_pool_min[p]), world.treasure_hunt_count[p] + int(args.triforce_min_difference[p])), int(args.triforce_pool_max[p])) + world.treasure_hunt_total[p] = random.randint(max(int(args.triforce_pool_min[p]), world.treasure_hunt_count[p] + int(args.triforce_min_difference[p])), min(int(args.triforce_pool_max[p]), world.treasure_hunt_count[p] + int(args.triforce_max_difference[p]))) else: world.treasure_hunt_total[p] = 10 if world.goal[p] == 'trinity' else 30 else: diff --git a/README.md b/README.md index 21a444cc..df21ad17 100644 --- a/README.md +++ b/README.md @@ -573,13 +573,14 @@ Create bps patch(es) instead of generating rom(s) for distribution. `--bps` ### Triforce Hunt Settings -A collection of settings to control the triforce piece pool for the CLI/Mystery +A collection of settings to control the triforce piece pool if not specified through --triforce_goal and --triforce_pool * --triforce_goal_min: Minimum number of pieces to collect to win * --triforce_goal_max: Maximum number of pieces to collect to win * --triforce_pool_min: Minimum number of pieces in item pool * --triforce_pool_max: Maximum number of pieces in item pool * --triforce_min_difference: Minimum difference between pool and goal to win +* --triforce_max_difference: Maximum difference between pool and goal to win ### Seed diff --git a/mystery_example.yml b/mystery_example.yml index 344875f4..92abd726 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -107,6 +107,7 @@ triforce_pool_min: 20 triforce_pool_max: 40 triforce_min_difference: 10 + triforce_max_difference: 15 dungeon_items: standard: 10 mc: 3 diff --git a/mystery_testsuite.yml b/mystery_testsuite.yml index f919b7cc..d6e46832 100644 --- a/mystery_testsuite.yml +++ b/mystery_testsuite.yml @@ -83,6 +83,7 @@ triforce_goal_max: 30 triforce_pool_min: 30 triforce_pool_max: 40 triforce_min_difference: 10 +triforce_max_difference: 12 map_shuffle: on: 1 off: 1 diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 5254a1c4..2eaf847e 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -339,6 +339,7 @@ "triforce_goal_min": {}, "triforce_goal_max": {}, "triforce_min_difference": {}, + "triforce_max_difference": {}, "custom": { "type": "bool", "help": "suppress" diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index ca0f2062..a0cea069 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -149,6 +149,7 @@ class CustomSettings(object): args.triforce_pool_min[p] = get_setting(settings['triforce_pool_min'], args.triforce_pool_min[p]) args.triforce_pool_max[p] = get_setting(settings['triforce_pool_max'], args.triforce_pool_max[p]) args.triforce_min_difference[p] = get_setting(settings['triforce_min_difference'], args.triforce_min_difference[p]) + args.triforce_max_difference[p] = get_setting(settings['triforce_max_difference'], args.triforce_max_difference[p]) args.beemizer[p] = get_setting(settings['beemizer'], args.beemizer[p]) # mystery usage diff --git a/source/tools/MysteryUtils.py b/source/tools/MysteryUtils.py index 32ee9d5f..0405efff 100644 --- a/source/tools/MysteryUtils.py +++ b/source/tools/MysteryUtils.py @@ -132,6 +132,7 @@ def roll_settings(weights): ret.triforce_goal_min = get_choice_default('triforce_goal_min', default=0) ret.triforce_goal_max = get_choice_default('triforce_goal_max', default=0) ret.triforce_min_difference = get_choice_default('triforce_min_difference', default=0) + ret.triforce_max_difference = get_choice_default('triforce_max_difference', default=10000) ret.mode = get_choice('world_state') if ret.mode == 'retro': From 8d17d9564075fefaa275a40044bb123367c1cce1 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 27 Jul 2023 13:45:44 -0600 Subject: [PATCH 13/30] Fixed a generation bug Add ganonhunt to pyramid open --- BaseClasses.py | 2 +- DoorShuffle.py | 20 +++++++++----------- RELEASENOTES.md | 3 +++ 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index af1436eb..c9d4be13 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -283,7 +283,7 @@ class World(object): else: if self.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull']: return False - elif self.goal[player] in ['crystals', 'trinity']: + elif self.goal[player] in ['crystals', 'trinity', 'ganonhunt']: return True else: return False diff --git a/DoorShuffle.py b/DoorShuffle.py index 35cc7624..462eb406 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -2547,13 +2547,12 @@ def reassign_big_key_doors(bk_map, used_doors, world, player): queue = deque(find_current_bk_doors(builder)) while len(queue) > 0: d = queue.pop() - if (d.type is DoorType.Interior and d not in flat_proposal and d.dest not in flat_proposal - and d not in used_doors and d.dest not in used_doors): - if not d.entranceFlag: + if d.type is DoorType.Interior and d not in flat_proposal and d.dest not in flat_proposal: + if not d.entranceFlag and d not in used_doors and d.dest not in used_doors: world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) d.bigKey = False - elif d.type is DoorType.Normal and d not in flat_proposal and d not in used_doors: - if not d.entranceFlag: + elif d.type is DoorType.Normal and d not in flat_proposal : + if not d.entranceFlag and d not in used_doors: world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) d.bigKey = False for obj in big_doors: @@ -3014,7 +3013,7 @@ def reassign_key_doors(small_map, used_doors, world, player): queue = deque(find_current_key_doors(builder)) while len(queue) > 0: d = queue.pop() - if d.type is DoorType.SpiralStairs and d not in proposal and d not in used_doors: + if d.type is DoorType.SpiralStairs and d not in proposal: room = world.get_room(d.roomIndex, player) if room.doorList[d.doorListPos][1] == DoorKind.StairKeyLow: room.delete(d.doorListPos) @@ -3024,15 +3023,14 @@ def reassign_key_doors(small_map, used_doors, world, player): else: room.delete(d.doorListPos) d.smallKey = False - elif (d.type is DoorType.Interior and d not in flat_proposal and d.dest not in flat_proposal - and d not in used_doors and d.dest not in used_doors): - if not d.entranceFlag: + elif d.type is DoorType.Interior and d not in flat_proposal and d.dest not in flat_proposal: + if not d.entranceFlag and d not in used_doors and d.dest not in used_doors: world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) d.smallKey = False d.dest.smallKey = False queue.remove(d.dest) - elif d.type is DoorType.Normal and d not in flat_proposal and d not in used_doors: - if not d.entranceFlag: + elif d.type is DoorType.Normal and d not in flat_proposal: + if not d.entranceFlag and d not in used_doors: world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) d.smallKey = False for dp in world.paired_doors[player]: diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 45577d04..0b8d2151 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -109,6 +109,9 @@ These are now independent of retro mode and have three options: None, Random, an # Bug Fixes and Notes +* 1.2.0.19u + * Fixed a bug with dungeon generation + * Changed the "Ganonhunt" goal to use open pyramid on the Auto setting * 1.2.0.18u * Fixed an issue with pyramid hole being in logic when it is not opened. * Crystal cutscene at GT use new symmetrical layouts (thanks Codemann) From 75fc1f7cc3b06fb451564ef161275aff54883039 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 28 Jul 2023 12:29:47 -0600 Subject: [PATCH 14/30] Fixed customizer example Fixed /missing command in multiworld for non-pottery lottery settings --- Main.py | 2 +- MultiClient.py | 15 ++++++++++++--- RELEASENOTES.md | 5 ++++- docs/customizer_example.yaml | 5 ++++- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/Main.py b/Main.py index 17444ce7..dd5ebc3e 100644 --- a/Main.py +++ b/Main.py @@ -34,7 +34,7 @@ from source.overworld.EntranceShuffle2 import link_entrances_new from source.tools.BPS import create_bps_from_data from source.classes.CustomSettings import CustomSettings -version_number = '1.2.0.18' +version_number = '1.2.0.19' version_branch = '-u' __version__ = f'{version_number}{version_branch}' diff --git a/MultiClient.py b/MultiClient.py index fbde673d..646c60cf 100644 --- a/MultiClient.py +++ b/MultiClient.py @@ -66,6 +66,8 @@ class Context: self.lookup_name_to_id = {} self.lookup_id_to_name = {} + self.pottery_locations_enabled = None + def color_code(*args): codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37 , 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43, @@ -96,6 +98,8 @@ SHOP_SRAM_START = WRAM_START + 0x0164B8 # 2 bytes? ITEM_SRAM_SIZE = 0x250 SHOP_SRAM_LEN = 0x29 # 41 tracked items +POT_LOCATION_TABLE = 0x142A60 + RECV_PROGRESS_ADDR = SAVEDATA_START + 0x4D0 # 2 bytes RECV_ITEM_ADDR = SAVEDATA_START + 0x4D2 # 1 byte RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3 # 1 byte @@ -826,12 +830,14 @@ def get_location_name_from_address(ctx, address): def filter_location(ctx, location): + if location in location_table_pot_items: + tile_idx, mask = location_table_pot_items[location] + tracking_data = ctx.pottery_locations_enabled + tile_pots = tracking_data[tile_idx] | (tracking_data[tile_idx+1] << 8) + return (mask & tile_pots) == 0 if (not ctx.key_drop_mode and location in PotShuffle.key_drop_data and PotShuffle.key_drop_data[location][0] == 'Drop'): return True - if (not ctx.pottery_mode and location in PotShuffle.key_drop_data - and PotShuffle.key_drop_data[location][0] == 'Pot'): - return True if not ctx.shop_mode and location in Regions.flat_normal_shops: return True if not ctx.retro_mode and location in Regions.flat_retro_shops: @@ -996,6 +1002,9 @@ async def game_watcher(ctx : Context): logging.warning("ROM change detected, please reconnect to the multiworld server") await disconnect(ctx) + if ctx.pottery_locations_enabled is None: + ctx.pottery_locations_enabled = await snes_read(ctx, POT_LOCATION_TABLE, 0x250) + gamemode = await snes_read(ctx, WRAM_START + 0x10, 1) if gamemode is None or gamemode[0] not in INGAME_MODES: continue diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0b8d2151..7416496f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -110,8 +110,11 @@ These are now independent of retro mode and have three options: None, Random, an # Bug Fixes and Notes * 1.2.0.19u - * Fixed a bug with dungeon generation + * Added min/max for triforce pool, goal, and differnce for CLI and Customizer. (Thanks Catobat) + * Fixed a bug with dungeon generation + * Multiworld: Fixed /missing command to not list all the pots * Changed the "Ganonhunt" goal to use open pyramid on the Auto setting + * Customizer: Fixed the example yaml for shopsanity * 1.2.0.18u * Fixed an issue with pyramid hole being in logic when it is not opened. * Crystal cutscene at GT use new symmetrical layouts (thanks Codemann) diff --git a/docs/customizer_example.yaml b/docs/customizer_example.yaml index 3d0c7624..76514990 100644 --- a/docs/customizer_example.yaml +++ b/docs/customizer_example.yaml @@ -1,7 +1,7 @@ meta: algorithm: balanced players: 1 - seed: 42 + seed: 41 # note to self: seed 42 had an interesting Swamp Palace problem names: Lonk settings: 1: @@ -56,6 +56,9 @@ item_pool: Sanctuary Heart Container: 3 Shovel: 3 Single Arrow: 1 + Green Potion: 1 + Blue Potion: 1 + Red Potion: 1 placements: 1: Palace of Darkness - Big Chest: Hammer From 1592fac79293265aba0e564d9c11e4db602467d7 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 28 Jul 2023 12:32:28 -0600 Subject: [PATCH 15/30] Typo --- RELEASENOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 7416496f..3231157f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -110,7 +110,7 @@ These are now independent of retro mode and have three options: None, Random, an # Bug Fixes and Notes * 1.2.0.19u - * Added min/max for triforce pool, goal, and differnce for CLI and Customizer. (Thanks Catobat) + * Added min/max for triforce pool, goal, and difference for CLI and Customizer. (Thanks Catobat) * Fixed a bug with dungeon generation * Multiworld: Fixed /missing command to not list all the pots * Changed the "Ganonhunt" goal to use open pyramid on the Auto setting From 982032e156bcaee7a3b643089d47f714b2dd15c5 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sat, 29 Jul 2023 05:04:46 -0500 Subject: [PATCH 16/30] Supporting old keyshuffle keywords --- BaseClasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index 263bd223..d6ac2c28 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -3450,7 +3450,7 @@ flutespot_mode = {"vanilla": 0, "balanced": 1, "random": 2} # byte 13: FBBB TTSS (flute_mode, bow_mode, take_any, small_key_mode) flute_mode = {'normal': 0, 'active': 1} -keyshuffle_mode = {'none': 0, 'wild': 1, 'universal': 2} # reserved 8 modes? +keyshuffle_mode = {'none': 0, 'off': 0, 'wild': 1, 'on': 1, 'universal': 2} # reserved 8 modes? take_any_mode = {'none': 0, 'random': 1, 'fixed': 2} bow_mode = {'progressive': 0, 'silvers': 1, 'retro': 2, 'retro_silvers': 3} From 35f3abc1c1041d69aee2ebe8e9bf23430923f422 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sat, 29 Jul 2023 05:10:47 -0500 Subject: [PATCH 17/30] Creating user notes field to be supplied by the seed roller --- BaseClasses.py | 3 +++ CLI.py | 3 ++- Main.py | 2 +- docs/customizer_example.yaml | 1 + resources/app/cli/args.json | 1 + source/classes/CustomSettings.py | 6 ++++-- 6 files changed, 12 insertions(+), 4 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index d6ac2c28..c79eb691 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -2898,6 +2898,7 @@ class Spoiler(object): 'triforcegoal': self.world.treasure_hunt_count, 'triforcepool': self.world.treasure_hunt_total, 'race': self.world.settings.world_rep['meta']['race'], + 'user_notes': self.world.settings.world_rep['meta']['user_notes'], 'code': {p: Settings.make_code(self.world, p) for p in range(1, self.world.players + 1)}, 'seed': self.world.seed } @@ -3047,6 +3048,8 @@ class Spoiler(object): outfile.write('ALttP Overworld Randomizer - Seed: %s\n\n' % (self.world.seed)) for k,v in self.metadata["versions"].items(): outfile.write((k + ' Version:').ljust(line_width) + '%s\n' % v) + if self.metadata['user_notes']: + outfile.write('User Notes:'.ljust(line_width) + '%s\n' % self.metadata['user_notes']) outfile.write('Filling Algorithm:'.ljust(line_width) + '%s\n' % self.world.algorithm) outfile.write('Players:'.ljust(line_width) + '%d\n' % self.world.players) outfile.write('Teams:'.ljust(line_width) + '%d\n' % self.world.teams) diff --git a/CLI.py b/CLI.py index b554d20f..1249f89b 100644 --- a/CLI.py +++ b/CLI.py @@ -355,7 +355,8 @@ def parse_settings(): "outputpath": os.path.join("."), "saveonexit": "ask", "outputname": "", - "startinventoryarray": {} + "startinventoryarray": {}, + "notes": "" } if sys.platform.lower().find("windows"): diff --git a/Main.py b/Main.py index 3d8d46e3..449d7977 100644 --- a/Main.py +++ b/Main.py @@ -183,7 +183,7 @@ def main(args, seed=None, fish=None): world.player_names[player].append(name) logger.info('') world.settings = CustomSettings() - world.settings.create_from_world(world, args.race) + world.settings.create_from_world(world, args) outfilebase = f'OR_{args.outputname if args.outputname else world.seed}' diff --git a/docs/customizer_example.yaml b/docs/customizer_example.yaml index 82505821..4c31cb7e 100644 --- a/docs/customizer_example.yaml +++ b/docs/customizer_example.yaml @@ -3,6 +3,7 @@ meta: players: 1 seed: 41 # note to self: seed 42 had an interesting Swamp Palace problem names: Lonk + notes: "Some notes specified by the user" settings: 1: door_shuffle: basic diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 08ccec98..75456d19 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -576,5 +576,6 @@ ] }, "outputname": {}, + "notes": {}, "code": {} } \ No newline at end of file diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index 5d02ca14..99972d7f 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -63,6 +63,7 @@ class CustomSettings(object): args.suppress_rom = get_setting(meta['suppress_rom'], args.suppress_rom) args.names = get_setting(meta['names'], args.names) args.race = get_setting(meta['race'], args.race) + args.notes = get_setting(meta['user_notes'], args.notes) self.player_range = range(1, args.multi + 1) if 'settings' in self.file_source: for p in self.player_range: @@ -227,14 +228,15 @@ class CustomSettings(object): return self.file_source['drops'] return None - def create_from_world(self, world, race): + def create_from_world(self, world, settings): self.player_range = range(1, world.players + 1) settings_dict, meta_dict = {}, {} self.world_rep['meta'] = meta_dict meta_dict['players'] = world.players meta_dict['algorithm'] = world.algorithm meta_dict['seed'] = world.seed - meta_dict['race'] = race + meta_dict['race'] = settings.race + meta_dict['user_notes'] = settings.notes self.world_rep['settings'] = settings_dict for p in self.player_range: settings_dict[p] = {} From 0ee88618a72cf720862d8db0f77b563db0438366 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 1 Aug 2023 11:31:59 -0600 Subject: [PATCH 18/30] Paired dungeon shuffle --- BaseClasses.py | 3 +- DoorShuffle.py | 57 +++++++++++++--------------------- resources/app/cli/args.json | 1 + resources/app/cli/lang/en.json | 1 + resources/app/gui/lang/en.json | 1 + 5 files changed, 26 insertions(+), 37 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index c9d4be13..3663a962 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -80,6 +80,7 @@ class World(object): self.rooms = [] self._room_cache = {} self.dungeon_layouts = {} + self.dungeon_pool = {} self.inaccessible_regions = {} self.enabled_entrances = {} self.key_logic = {} @@ -2922,7 +2923,7 @@ class Pot(object): # byte 0: DDDE EEEE (DR, ER) -dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0, "partitioned": 3} +dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0, "partitioned": 3, 'paired': 4} er_mode = {"vanilla": 0, "simple": 1, "restricted": 2, "full": 3, "crossed": 4, "insanity": 5, 'lite': 8, 'lean': 9, "dungeonsfull": 7, "dungeonssimple": 6} diff --git a/DoorShuffle.py b/DoorShuffle.py index 462eb406..abf1def8 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -88,7 +88,9 @@ def link_doors_prep(world, player): find_inaccessible_regions(world, player) - if world.intensity[player] >= 3 and world.doorShuffle[player] != 'vanilla': + if world.doorShuffle[player] != 'vanilla': + create_dungeon_pool(world, player) + if world.intensity[player] >= 3 and world.doorShuffle[player] != 'vanilla': choose_portals(world, player) else: if world.shuffle[player] == 'vanilla': @@ -132,6 +134,20 @@ def create_dungeon_pool(world, player): pool = None if world.doorShuffle[player] == 'basic': pool = [([name], regions) for name, regions in dungeon_regions.items()] + elif world.doorShuffle[player] == 'paired': + dungeon_pool = list(dungeon_regions.keys()) + groups = [] + while dungeon_pool: + if len(dungeon_pool) == 3: + groups.append(list(dungeon_pool)) + dungeon_pool.clear() + else: + choice_a = random.choice(dungeon_pool) + dungeon_pool.remove(choice_a) + choice_b = random.choice(dungeon_pool) + dungeon_pool.remove(choice_b) + groups.append([choice_a, choice_b]) + pool = [(group, list(chain.from_iterable([dungeon_regions[d] for d in group]))) for group in groups] elif world.doorShuffle[player] == 'partitioned': groups = [['Hyrule Castle', 'Eastern Palace', 'Desert Palace', 'Tower of Hera', 'Agahnims Tower'], ['Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town'], @@ -142,38 +158,17 @@ def create_dungeon_pool(world, player): elif world.doorShuffle[player] != 'vanilla': logging.getLogger('').error('Invalid door shuffle setting: %s' % world.doorShuffle[player]) raise Exception('Invalid door shuffle setting: %s' % world.doorShuffle[player]) - return pool + world.dungeon_pool[player] = pool def link_doors_main(world, player): - pool = create_dungeon_pool(world, player) + pool = world.dungeon_pool[player] if pool: main_dungeon_pool(pool, world, player) if world.doorShuffle[player] != 'vanilla': create_door_spoiler(world, player) -# todo: I think this function is not necessary -def mark_regions(world, player): - # traverse dungeons and make sure dungeon property is assigned - player_dungeons = [dungeon for dungeon in world.dungeons if dungeon.player == player] - for dungeon in player_dungeons: - queue = deque(dungeon.regions) - while len(queue) > 0: - region = world.get_region(queue.popleft(), player) - if region.name not in dungeon.regions: - dungeon.regions.append(region.name) - region.dungeon = dungeon - for ext in region.exits: - d = world.check_for_door(ext.name, player) - connected = ext.connected_region - if d is not None and connected is not None: - if d.dest is not None and connected.name not in dungeon.regions and connected.type == RegionType.Dungeon and connected.name not in queue: - queue.append(connected) # needs to be added - elif connected is not None and connected.name not in dungeon.regions and connected.type == RegionType.Dungeon and connected.name not in queue: - queue.append(connected) # needs to be added - - def create_door_spoiler(world, player): logger = logging.getLogger('') shuffled_door_types = [DoorType.Normal, DoorType.SpiralStairs] @@ -437,17 +432,7 @@ def pair_existing_key_doors(world, player, door_a, door_b): def choose_portals(world, player): if world.doorShuffle[player] != ['vanilla']: shuffle_flag = world.doorShuffle[player] != 'basic' - allowed = {} - if world.doorShuffle[player] == 'basic': - allowed = {name: {name} for name in dungeon_regions} - elif world.doorShuffle[player] == 'partitioned': - groups = [['Hyrule Castle', 'Eastern Palace', 'Desert Palace', 'Tower of Hera', 'Agahnims Tower'], - ['Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town'], - ['Ice Palace', 'Misery Mire', 'Turtle Rock', 'Ganons Tower']] - allowed = {name: set(group) for group in groups for name in group} - elif world.doorShuffle[player] == 'crossed': - all_dungeons = set(dungeon_regions.keys()) - allowed = {name: all_dungeons for name in dungeon_regions} + allowed = {name: set(group[0]) for group in world.dungeon_pool[player] for name in group[0]} # key drops allow the big key in the right place in Desert Tiles 2 bk_shuffle = world.bigkeyshuffle[player] or world.pottery[player] not in ['none', 'cave'] @@ -592,7 +577,7 @@ def customizer_portals(master_door_list, world, player): assigned_doors.add(door) # restricts connected doors to the customized portals if assigned_doors: - pool = create_dungeon_pool(world, player) + pool = world.dungeon_pool[player] if pool: pool_map = {} for pool, region_list in pool: diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 2eaf847e..9aff422a 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -175,6 +175,7 @@ "door_shuffle": { "choices": [ "basic", + "paired", "partitioned", "crossed", "vanilla" diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 4d5fb151..49bd891a 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -220,6 +220,7 @@ "door_shuffle": [ "Select Door Shuffling Algorithm. (default: %(default)s)", "Basic: Doors are mixed within a single dungeon.", + "Paired Dungeon are paired (with one trio) and only mixed in those groups", "Partitioned Doors are mixed in 3 partitions: L1-3+HC+AT, D1-4, D5-8", "Crossed: Doors are mixed between all dungeons.", "Vanilla: All doors are connected the same way they were in the", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 9d56137c..fb81c391 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -62,6 +62,7 @@ "randomizer.dungeon.dungeondoorshuffle": "Dungeon Door Shuffle", "randomizer.dungeon.dungeondoorshuffle.vanilla": "Vanilla", "randomizer.dungeon.dungeondoorshuffle.basic": "Basic", + "randomizer.dungeon.dungeondoorshuffle.paired": "Paired", "randomizer.dungeon.dungeondoorshuffle.partitioned": "Partitioned", "randomizer.dungeon.dungeondoorshuffle.crossed": "Crossed", From 1817cf38242e6ba14a05bab682b5461fa3970edb Mon Sep 17 00:00:00 2001 From: Catobat <69204835+Catobat@users.noreply.github.com> Date: Tue, 1 Aug 2023 22:00:58 +0200 Subject: [PATCH 19/30] Fix byte 12 in settings code --- BaseClasses.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index c9d4be13..122d6de4 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -3028,8 +3028,8 @@ class Settings(object): (flute_mode[w.flute_mode[p]] << 7 | bow_mode[w.bow_mode[p]] << 4 | take_any_mode[w.take_any[p]] << 2 | keyshuffle_mode[w.keyshuffle[p]]), - ((0x80 if w.pseudoboots[p] else 0) | overworld_map_mode[w.overworld_map[p]] << 6 - | trap_door_mode[w.trap_door_mode[p]] << 4 | key_logic_algo[w.key_logic_algorithm[p]]), + ((0x80 if w.pseudoboots[p] else 0) | overworld_map_mode[w.overworld_map[p]] << 5 + | trap_door_mode[w.trap_door_mode[p]] << 3 | key_logic_algo[w.key_logic_algorithm[p]]), ]) return base64.b64encode(code, "+-".encode()).decode() @@ -3099,8 +3099,8 @@ class Settings(object): args.keyshuffle[p] = r(keyshuffle_mode)[settings[11] & 0x3] if len(settings) > 12: args.pseudoboots[p] = True if settings[12] & 0x80 else False - args.overworld_map[p] = r(overworld_map_mode)[(settings[12] & 0x60) >> 6] - args.trap_door_mode[p] = r(trap_door_mode)[(settings[12] & 0x14) >> 4] + args.overworld_map[p] = r(overworld_map_mode)[(settings[12] & 0x60) >> 5] + args.trap_door_mode[p] = r(trap_door_mode)[(settings[12] & 0x18) >> 3] args.key_logic_algorithm[p] = r(key_logic_algo)[settings[12] & 0x07] From 7197a23b4520a67c9005d60b2859de060c585c51 Mon Sep 17 00:00:00 2001 From: Catobat <69204835+Catobat@users.noreply.github.com> Date: Wed, 2 Aug 2023 02:23:19 +0200 Subject: [PATCH 20/30] Add setting for self-looping doors --- BaseClasses.py | 15 ++++++++++----- CLI.py | 3 ++- Main.py | 2 ++ README.md | 8 +++++++- mystery_example.yml | 3 +++ mystery_testsuite.yml | 3 +++ resources/app/cli/args.json | 4 ++++ resources/app/cli/lang/en.json | 1 + resources/app/gui/lang/en.json | 1 + resources/app/gui/randomize/dungeon/widgets.json | 6 ++++++ source/classes/CustomSettings.py | 2 ++ source/classes/constants.py | 1 + source/dungeon/DungeonStitcher.py | 14 +++++++++++--- source/tools/MysteryUtils.py | 1 + 14 files changed, 54 insertions(+), 10 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 122d6de4..89b39a43 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -145,6 +145,7 @@ class World(object): set_player_attr('colorizepots', True) set_player_attr('pot_pool', {}) set_player_attr('decoupledoors', False) + set_player_attr('door_self_loops', False) set_player_attr('door_type_mode', 'original') set_player_attr('trap_door_mode', 'optional') set_player_attr('key_logic_algorithm', 'default') @@ -2472,6 +2473,7 @@ class Spoiler(object): 'trap_door_mode': self.world.trap_door_mode, 'key_logic': self.world.key_logic_algorithm, 'decoupledoors': self.world.decoupledoors, + 'door_self_loops': self.world.door_self_loops, 'dungeon_counters': self.world.dungeon_counters, 'item_pool': self.world.difficulty, 'item_functionality': self.world.difficulty_adjustments, @@ -2682,6 +2684,7 @@ class Spoiler(object): outfile.write(f"Trap Door Mode: {self.metadata['trap_door_mode'][player]}\n") outfile.write(f"Key Logic Algorithm: {self.metadata['key_logic'][player]}\n") outfile.write(f"Decouple Doors: {yn(self.metadata['decoupledoors'][player])}\n") + outfile.write(f"Spiral Stairs can self-loop: {yn(self.metadata['door_self_loops'][player])}\n") outfile.write(f"Experimental: {yn(self.metadata['experimental'][player])}\n") outfile.write(f"Dungeon Counters: {self.metadata['dungeon_counters'][player]}\n") outfile.write(f"Drop Shuffle: {yn(self.metadata['dropshuffle'][player])}\n") @@ -2947,10 +2950,10 @@ mixed_travel_mode = {"prevent": 0, "allow": 1, "force": 2} pottery_mode = {'none': 0, 'keys': 2, 'lottery': 3, 'dungeon': 4, 'cave': 5, 'cavekeys': 6, 'reduced': 7, 'clustered': 8, 'nonempty': 9} -# byte 5: CCCC CTTX (crystals gt, ctr2, experimental) +# byte 5: SCCC CTTX (self-loop doors, crystals gt, ctr2, experimental) counter_mode = {"default": 0, "off": 1, "on": 2, "pickup": 3} -# byte 6: CCCC CPAA (crystals ganon, pyramid, access +# byte 6: ?CCC CPAA (crystals ganon, pyramid, access access_mode = {"items": 0, "locations": 1, "none": 2} # byte 7: B?MC DDEE (big, ?, maps, compass, door_type, enemies) @@ -3008,7 +3011,8 @@ class Settings(object): (0x80 if w.shuffletavern[p] else 0) | (0x10 if w.dropshuffle[p] else 0) | (pottery_mode[w.pottery[p]]), - ((8 if w.crystals_gt_orig[p] == "random" else int(w.crystals_gt_orig[p])) << 3) + (0x80 if w.door_self_loops[p] else 0) + | ((8 if w.crystals_gt_orig[p] == "random" else int(w.crystals_gt_orig[p])) << 3) | (counter_mode[w.dungeon_counters[p]] << 1) | (1 if w.experimental[p] else 0), ((8 if w.crystals_ganon_orig[p] == "random" else int(w.crystals_ganon_orig[p])) << 3) @@ -3067,12 +3071,13 @@ class Settings(object): args.dropshuffle[p] = True if settings[4] & 0x10 else False args.pottery[p] = r(pottery_mode)[settings[4] & 0x0F] + args.door_self_loops[p] = True if settings[5] & 0x80 else False args.dungeon_counters[p] = r(counter_mode)[(settings[5] & 0x6) >> 1] - cgt = (settings[5] & 0xf8) >> 3 + cgt = (settings[5] & 0x78) >> 3 args.crystals_gt[p] = "random" if cgt == 8 else cgt args.experimental[p] = True if settings[5] & 0x1 else False - cgan = (settings[6] & 0xf8) >> 3 + cgan = (settings[6] & 0x78) >> 3 args.crystals_ganon[p] = "random" if cgan == 8 else cgan args.openpyramid[p] = True if settings[6] & 0x4 else False diff --git a/CLI.py b/CLI.py index 2c925c6c..ec7ce8c9 100644 --- a/CLI.py +++ b/CLI.py @@ -141,7 +141,7 @@ def parse_cli(argv, no_defaults=False): 'heartbeep', 'remote_items', 'shopsanity', 'dropshuffle', 'pottery', 'keydropshuffle', 'mixed_travel', 'standardize_palettes', 'code', 'reduce_flashing', 'shuffle_sfx', 'msu_resume', 'collection_rate', 'colorizepots', 'decoupledoors', 'door_type_mode', - 'trap_door_mode', 'key_logic_algorithm']: + 'trap_door_mode', 'key_logic_algorithm', 'door_self_loops']: value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) if player == 1: setattr(ret, name, {1: value}) @@ -218,6 +218,7 @@ def parse_settings(): 'trap_door_mode': 'optional', 'key_logic_algorithm': 'default', 'decoupledoors': False, + 'door_self_loops': False, 'experimental': False, 'dungeon_counters': 'default', 'mixed_travel': 'prevent', diff --git a/Main.py b/Main.py index dd5ebc3e..d1dca6af 100644 --- a/Main.py +++ b/Main.py @@ -115,6 +115,7 @@ def main(args, seed=None, fish=None): world.trap_door_mode = args.trap_door_mode.copy() world.key_logic_algorithm = args.key_logic_algorithm.copy() world.decoupledoors = args.decoupledoors.copy() + world.door_self_loops = args.door_self_loops.copy() world.experimental = args.experimental.copy() world.dungeon_counters = args.dungeon_counters.copy() world.fish = fish @@ -487,6 +488,7 @@ def copy_world(world): ret.beemizer = world.beemizer.copy() ret.intensity = world.intensity.copy() ret.decoupledoors = world.decoupledoors.copy() + ret.door_self_loops = world.door_self_loops.copy() ret.experimental = world.experimental.copy() ret.shopsanity = world.shopsanity.copy() ret.dropshuffle = world.dropshuffle.copy() diff --git a/README.md b/README.md index df21ad17..24e79807 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,13 @@ CLI: `--key_logic [default|partial|strict]` This is similar to insanity mode in ER where door entrances and exits are not paired anymore. Tends to remove more logic from dungeons as many rooms will not be required to traverse to explore. Hope you like transitions. -CLI `--decoupledoors` +CLI: `--decoupledoors` + +### Self-Looping Spiral Stairs + +If enabled, spiral stairs are allowed to lead to themselves. + +CLI: `--door_self_loops` ### Pottery diff --git a/mystery_example.yml b/mystery_example.yml index 92abd726..bd8cbbe2 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -31,6 +31,9 @@ partial: 0 strict: 0 decoupledoors: off + door_self_loops: + on: 1 + off: 1 dropshuffle: on: 1 off: 1 diff --git a/mystery_testsuite.yml b/mystery_testsuite.yml index d6e46832..c7250d2b 100644 --- a/mystery_testsuite.yml +++ b/mystery_testsuite.yml @@ -31,6 +31,9 @@ key_logic_algorithm: decoupledoors: off: 9 # more strict on: 1 +door_self_loops: + on: 1 + off: 1 dropshuffle: on: 1 off: 1 diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 2eaf847e..626a3b72 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -212,6 +212,10 @@ "action": "store_true", "type": "bool" }, + "door_self_loops": { + "action": "store_true", + "type": "bool" + }, "experimental": { "action": "store_true", "type": "bool" diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 4d5fb151..031a1061 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -253,6 +253,7 @@ "strict: Ensure small keys are available" ], "decoupledoors" : [ "Door entrances and exits are decoupled" ], + "door_self_loops" : [ "Spiral stairs are allowed to self-loop" ], "experimental": [ "Enable experimental features. (default: %(default)s)" ], "dungeon_counters": [ "Enable dungeon chest counters. (default: %(default)s)" ], "crystals_ganon": [ diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 9d56137c..87bf4e03 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -58,6 +58,7 @@ "randomizer.dungeon.smallkeyshuffle.universal": "Universal", "randomizer.dungeon.bigkeyshuffle": "Big Keys", "randomizer.dungeon.decoupledoors": "Decouple Doors", + "randomizer.dungeon.door_self_loops": "Allow Self-Looping Spiral Stairs", "randomizer.dungeon.dungeondoorshuffle": "Dungeon Door Shuffle", "randomizer.dungeon.dungeondoorshuffle.vanilla": "Vanilla", diff --git a/resources/app/gui/randomize/dungeon/widgets.json b/resources/app/gui/randomize/dungeon/widgets.json index bcf7232a..4be6a385 100644 --- a/resources/app/gui/randomize/dungeon/widgets.json +++ b/resources/app/gui/randomize/dungeon/widgets.json @@ -66,6 +66,12 @@ } }, "decoupledoors": { + "type": "checkbox", + "config": { + "padx": [20,0] + } + }, + "door_self_loops": { "type": "checkbox", "config": { "padx": [20,0], diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index a0cea069..2ddc1747 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -113,6 +113,7 @@ class CustomSettings(object): args.trap_door_mode[p] = get_setting(settings['trap_door_mode'], args.trap_door_mode[p]) args.key_logic_algorithm[p] = get_setting(settings['key_logic_algorithm'], args.key_logic_algorithm[p]) args.decoupledoors[p] = get_setting(settings['decoupledoors'], args.decoupledoors[p]) + args.door_self_loops[p] = get_setting(settings['door_self_loops'], args.door_self_loops[p]) args.dungeon_counters[p] = get_setting(settings['dungeon_counters'], args.dungeon_counters[p]) args.crystals_gt[p] = get_setting(settings['crystals_gt'], args.crystals_gt[p]) args.crystals_ganon[p] = get_setting(settings['crystals_ganon'], args.crystals_ganon[p]) @@ -232,6 +233,7 @@ class CustomSettings(object): settings_dict[p]['trap_door_mode'] = world.trap_door_mode[p] settings_dict[p]['key_logic_algorithm'] = world.key_logic_algorithm[p] settings_dict[p]['decoupledoors'] = world.decoupledoors[p] + settings_dict[p]['door_self_loops'] = world.door_self_loops[p] settings_dict[p]['logic'] = world.logic[p] settings_dict[p]['mode'] = world.mode[p] settings_dict[p]['swords'] = world.swords[p] diff --git a/source/classes/constants.py b/source/classes/constants.py index 812042b8..1ca3c08e 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -106,6 +106,7 @@ SETTINGSTOPROCESS = { "door_type_mode": "door_type_mode", "trap_door_mode": "trap_door_mode", "decoupledoors": "decoupledoors", + "door_self_loops": "door_self_loops", "experimental": "experimental", "dungeon_counters": "dungeon_counters", "mixed_travel": "mixed_travel", diff --git a/source/dungeon/DungeonStitcher.py b/source/dungeon/DungeonStitcher.py index 1eac3fb1..ec61b829 100644 --- a/source/dungeon/DungeonStitcher.py +++ b/source/dungeon/DungeonStitcher.py @@ -22,7 +22,7 @@ def generate_dungeon(builder, entrance_region_names, split_dungeon, world, playe queue = collections.deque(proposed_map.items()) while len(queue) > 0: a, b = queue.popleft() - if world.decoupledoors[player]: + if a == b or world.decoupledoors[player]: connect_doors_one_way(a, b) else: connect_doors(a, b) @@ -128,14 +128,14 @@ def create_random_proposal(doors_to_connect, world, player): next_hook = random.choice(hooks_left) primary_door = random.choice(primary_bucket[next_hook]) opp_hook, secondary_door = type_map[next_hook], None - while (secondary_door is None or secondary_door == primary_door + while (secondary_door is None or (secondary_door == primary_door and not world.door_self_loops[player]) or decouple_check(primary_bucket[next_hook], secondary_bucket[opp_hook], primary_door, secondary_door, world, player)): secondary_door = random.choice(secondary_bucket[opp_hook]) proposal[primary_door] = secondary_door primary_bucket[next_hook].remove(primary_door) secondary_bucket[opp_hook].remove(secondary_door) - if not world.decoupledoors[player]: + if primary_door != secondary_door and not world.decoupledoors[player]: proposal[secondary_door] = primary_door primary_bucket[opp_hook].remove(secondary_door) secondary_bucket[next_hook].remove(primary_door) @@ -205,6 +205,14 @@ def modify_proposal(proposed_map, explored_state, doors_to_connect, hash_code_se old_attempt = proposed_map[new_door] else: old_attempt = next(x for x in proposed_map if proposed_map[x] == new_door) + # ensure nothing gets messed up when something loops with itself + if attempt == old_target and old_attempt == new_door: + old_attempt = new_door + old_target = attempt + elif attempt == old_target: + old_target = old_attempt + elif old_attempt == new_door: + old_attempt = old_target proposed_map[old_attempt] = old_target if not world.decoupledoors[player]: proposed_map[old_target] = old_attempt diff --git a/source/tools/MysteryUtils.py b/source/tools/MysteryUtils.py index 0405efff..5000f670 100644 --- a/source/tools/MysteryUtils.py +++ b/source/tools/MysteryUtils.py @@ -87,6 +87,7 @@ def roll_settings(weights): ret.trap_door_mode = get_choice('trap_door_mode') ret.key_logic_algorithm = get_choice('key_logic_algorithm') ret.decoupledoors = get_choice('decoupledoors') == 'on' + ret.door_self_loops = get_choice('door_self_loops') == 'on' ret.experimental = get_choice('experimental') == 'on' ret.collection_rate = get_choice('collection_rate') == 'on' From 9497ef36ad17867ab6a3d435719c49ef26e1698b Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 2 Aug 2023 08:05:54 -0600 Subject: [PATCH 21/30] Paired needs a bunch of generation work apparently. Disabling it. Minor generation improvements. --- DungeonGenerator.py | 32 ++++++++++++++++++++++---------- resources/app/cli/args.json | 1 - 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index f97f8ecf..d96b654a 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -1677,10 +1677,24 @@ def assign_location_sectors_minimal(dungeon_map, free_location_sectors, global_p random.shuffle(sector_list) orig_location_set = build_orig_location_set(dungeon_map) num_dungeon_items = requested_dungeon_items(world, player) + locations_to_distribute = sum(sector.chest_locations for sector in free_location_sectors.keys()) + reserved_per_dungeon = {d_name: count_reserved_locations(world, player, orig_location_set[d_name]) + for d_name in dungeon_map.keys()} + base_free, found_enough = 2, False + while not found_enough: + needed = sum(max(0, max(base_free, reserved_per_dungeon[d]) + num_dungeon_items - len(orig_location_set[d])) + for d in dungeon_map.keys()) + if needed > locations_to_distribute: + if base_free == 0: + raise Exception('Unable to meet minimum requirements, check for customizer problems') + base_free -= 1 + else: + found_enough = True d_idx = {builder.name: i for i, builder in enumerate(dungeon_map.values())} next_sector = sector_list.pop() while not valid: - choice, totals, location_set = weighted_random_location(dungeon_map, choices, orig_location_set, world, player) + choice, totals, location_set = weighted_random_location(dungeon_map, choices, orig_location_set, + base_free, world, player) if not choice: break choices[choice].append(next_sector) @@ -1691,7 +1705,7 @@ def assign_location_sectors_minimal(dungeon_map, free_location_sectors, global_p valid = True for d_name, idx in d_idx.items(): free_items = count_reserved_locations(world, player, location_set[d_name]) - target = max(free_items, 2) + num_dungeon_items + target = max(free_items, base_free) + num_dungeon_items if totals[idx] < target: valid = False break @@ -1699,8 +1713,7 @@ def assign_location_sectors_minimal(dungeon_map, free_location_sectors, global_p if len(sector_list) == 0: choices = defaultdict(list) sector_list = list(free_location_sectors) - else: - next_sector = sector_list.pop() + next_sector = sector_list.pop() else: choices[choice].remove(next_sector) for builder, choice_list in choices.items(): @@ -1709,7 +1722,7 @@ def assign_location_sectors_minimal(dungeon_map, free_location_sectors, global_p return free_location_sectors -def weighted_random_location(dungeon_map, choices, orig_location_set, world, player): +def weighted_random_location(dungeon_map, choices, orig_location_set, base_free, world, player): population = [] totals = [] location_set = {x: set(y) for x, y in orig_location_set.items()} @@ -1720,7 +1733,7 @@ def weighted_random_location(dungeon_map, choices, orig_location_set, world, pla builder_set = location_set[dungeon_builder.name] builder_set.update(set().union(*(s.chest_location_set for s in choices[dungeon_builder]))) free_items = count_reserved_locations(world, player, builder_set) - target = max(free_items, 2) + num_dungeon_items + target = max(free_items, base_free) + num_dungeon_items if ttl < target: population.append(dungeon_builder) choice = random.choice(population) if len(population) > 0 else None @@ -1775,7 +1788,7 @@ def count_reserved_locations(world, player, proposed_set): return 2 -def assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, global_pole, assign_one=False): +def assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, global_pole): population = [] some_c_switches_present = False for name, builder in dungeon_map.items(): @@ -1784,7 +1797,7 @@ def assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barrier if builder.c_switch_present and not builder.c_locked: some_c_switches_present = True if len(population) == 0: # nothing needs a switch - if assign_one and not some_c_switches_present: # something should have one + if len(crystal_barriers) > 0 and not some_c_switches_present: # something should have one if len(crystal_switches) == 0: raise GenerationException('No crystal switches to assign. Ref %s' % next(iter(dungeon_map.keys()))) valid, builder_choice, switch_choice = False, None, None @@ -3139,8 +3152,7 @@ def balance_split(candidate_sectors, dungeon_map, global_pole, builder_info): check_for_forced_assignments(dungeon_map, candidate_sectors, global_pole) check_for_forced_crystal(dungeon_map, candidate_sectors, global_pole) crystal_switches, crystal_barriers, neutral_sectors, polarized_sectors = categorize_sectors(candidate_sectors) - leftover = assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, - global_pole, len(crystal_barriers) > 0) + leftover = assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, global_pole) ensure_crystal_switches_reachable(dungeon_map, leftover, polarized_sectors, crystal_barriers, global_pole) for sector in leftover: if sector.polarity().is_neutral(): diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 9aff422a..2eaf847e 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -175,7 +175,6 @@ "door_shuffle": { "choices": [ "basic", - "paired", "partitioned", "crossed", "vanilla" From f2b8c840a25a5c433534d9fdde1345e7f1f6babb Mon Sep 17 00:00:00 2001 From: Catobat <69204835+Catobat@users.noreply.github.com> Date: Wed, 2 Aug 2023 17:02:15 +0200 Subject: [PATCH 22/30] Fix potential bug with re-linking decoupled doors --- source/dungeon/DungeonStitcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/dungeon/DungeonStitcher.py b/source/dungeon/DungeonStitcher.py index ec61b829..504fc03b 100644 --- a/source/dungeon/DungeonStitcher.py +++ b/source/dungeon/DungeonStitcher.py @@ -200,7 +200,6 @@ def modify_proposal(proposed_map, explored_state, doors_to_connect, hash_code_se unvisted_bucket[opp_hook].sort(key=lambda d: d.name) new_door = random.choice(unvisted_bucket[opp_hook]) old_target = proposed_map[attempt] - proposed_map[attempt] = new_door if not world.decoupledoors[player]: old_attempt = proposed_map[new_door] else: @@ -213,6 +212,7 @@ def modify_proposal(proposed_map, explored_state, doors_to_connect, hash_code_se old_target = old_attempt elif old_attempt == new_door: old_attempt = old_target + proposed_map[attempt] = new_door proposed_map[old_attempt] = old_target if not world.decoupledoors[player]: proposed_map[old_target] = old_attempt From c0c3204fd50953f6f580b998d5fdbb026af5d205 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sat, 29 Jul 2023 05:10:47 -0500 Subject: [PATCH 23/30] Notes field --- BaseClasses.py | 6 +++++- CLI.py | 3 ++- Main.py | 2 +- docs/customizer_example.yaml | 1 + resources/app/cli/args.json | 1 + source/classes/CustomSettings.py | 6 ++++-- 6 files changed, 14 insertions(+), 5 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 3663a962..3d97414b 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -2501,7 +2501,9 @@ class Spoiler(object): 'triforcegoal': self.world.treasure_hunt_count, 'triforcepool': self.world.treasure_hunt_total, 'race': self.world.settings.world_rep['meta']['race'], - 'code': {p: Settings.make_code(self.world, p) for p in range(1, self.world.players + 1)} + 'user_notes': self.world.settings.world_rep['meta']['user_notes'], + 'code': {p: Settings.make_code(self.world, p) for p in range(1, self.world.players + 1)}, + 'seed': self.world.seed } for p in range(1, self.world.players + 1): @@ -2642,6 +2644,8 @@ class Spoiler(object): self.parse_meta() with open(filename, 'w') as outfile: outfile.write('ALttP Dungeon Randomizer Version %s - Seed: %s\n\n' % (self.metadata['version'], self.world.seed)) + if self.metadata['user_notes']: + outfile.write('User Notes: %s\n' % self.metadata['user_notes']) outfile.write('Filling Algorithm: %s\n' % self.world.algorithm) outfile.write('Players: %d\n' % self.world.players) outfile.write('Teams: %d\n' % self.world.teams) diff --git a/CLI.py b/CLI.py index 2c925c6c..8f790ed8 100644 --- a/CLI.py +++ b/CLI.py @@ -346,7 +346,8 @@ def parse_settings(): "outputpath": os.path.join("."), "saveonexit": "ask", "outputname": "", - "startinventoryarray": {} + "startinventoryarray": {}, + "notes": "" } if sys.platform.lower().find("windows"): diff --git a/Main.py b/Main.py index dd5ebc3e..7c124808 100644 --- a/Main.py +++ b/Main.py @@ -172,7 +172,7 @@ def main(args, seed=None, fish=None): world.player_names[player].append(name) logger.info('') world.settings = CustomSettings() - world.settings.create_from_world(world, args.race) + world.settings.create_from_world(world, args) outfilebase = f'DR_{args.outputname if args.outputname else world.seed}' diff --git a/docs/customizer_example.yaml b/docs/customizer_example.yaml index 76514990..acdf5188 100644 --- a/docs/customizer_example.yaml +++ b/docs/customizer_example.yaml @@ -3,6 +3,7 @@ meta: players: 1 seed: 41 # note to self: seed 42 had an interesting Swamp Palace problem names: Lonk + notes: "Some notes specified by the user" settings: 1: door_shuffle: basic diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 2eaf847e..bdc6b4db 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -513,5 +513,6 @@ ] }, "outputname": {}, + "notes": {}, "code": {} } diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index a0cea069..c0e82eed 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -63,6 +63,7 @@ class CustomSettings(object): args.suppress_rom = get_setting(meta['suppress_rom'], args.suppress_rom) args.names = get_setting(meta['names'], args.names) args.race = get_setting(meta['race'], args.race) + args.notes = get_setting(meta['user_notes'], args.notes) self.player_range = range(1, args.multi + 1) if 'settings' in self.file_source: for p in self.player_range: @@ -214,14 +215,15 @@ class CustomSettings(object): return self.file_source['drops'] return None - def create_from_world(self, world, race): + def create_from_world(self, world, settings): self.player_range = range(1, world.players + 1) settings_dict, meta_dict = {}, {} self.world_rep['meta'] = meta_dict meta_dict['players'] = world.players meta_dict['algorithm'] = world.algorithm meta_dict['seed'] = world.seed - meta_dict['race'] = race + meta_dict['race'] = settings.race + meta_dict['user_notes'] = settings.notes self.world_rep['settings'] = settings_dict for p in self.player_range: settings_dict[p] = {} From 1566813f81a51743df987b762c4bcc6352d0ccb2 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 2 Aug 2023 15:01:37 -0500 Subject: [PATCH 24/30] Changing yaml output to show hex numbers as hex and not as decimal --- Utils.py | 7 +++++++ source/classes/CustomSettings.py | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Utils.py b/Utils.py index a208efe1..1a2a1c1c 100644 --- a/Utils.py +++ b/Utils.py @@ -737,6 +737,13 @@ class bidict(dict): super(bidict, self).__delitem__(key) +class HexInt(int): pass + +def hex_representer(dumper, data): + import yaml + return yaml.ScalarNode('tag:yaml.org,2002:int', f"{data:#0{4}x}") + + if __name__ == '__main__': # make_new_base2current() # read_entrance_data(old_rom=sys.argv[1]) diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index 99972d7f..7dc86366 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -4,6 +4,7 @@ import urllib.parse import yaml from typing import Any from yaml.representer import Representer +from Utils import HexInt, hex_representer from collections import defaultdict from pathlib import Path @@ -350,7 +351,7 @@ class CustomSettings(object): for p in self.player_range: if p in world.owswaps and len(world.owswaps[p][0]) > 0: flips[p] = {} - flips[p]['force_flip'] = list(f for f in world.owswaps[p][0] if f < 0x40 or f >= 0x80) + flips[p]['force_flip'] = list(HexInt(f) for f in world.owswaps[p][0] if f < 0x40 or f >= 0x80) flips[p]['force_flip'].sort() flips[p]['undefined_chance'] = 0 @@ -416,6 +417,7 @@ class CustomSettings(object): def write_to_file(self, destination): yaml.add_representer(defaultdict, Representer.represent_dict) + yaml.add_representer(HexInt, hex_representer) with open(destination, 'w') as file: yaml.dump(self.world_rep, file) From f442cff06119c12b18cbac68f9c18529ded2da09 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 3 Aug 2023 15:06:54 -0600 Subject: [PATCH 25/30] Logic added for openable trap doors --- BaseClasses.py | 3 +- DoorShuffle.py | 52 ++++++++++++--- Main.py | 2 +- RELEASENOTES.md | 3 + Rules.py | 101 +++++++++++++++++++++++++++- test/dungeons/trap_test.yaml | 125 +++++++++++++++++++++++++++++++++++ 6 files changed, 271 insertions(+), 15 deletions(-) create mode 100644 test/dungeons/trap_test.yaml diff --git a/BaseClasses.py b/BaseClasses.py index 3d97414b..a62ca101 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1760,6 +1760,7 @@ class Door(object): self.dest = None self.blocked = False # Indicates if the door is normally blocked off as an exit. (Sanc door or always closed) self.blocked_orig = False + self.trapped = False self.stonewall = False # Indicate that the door cannot be enter until exited (Desert Torches, PoD Eye Statue) self.smallKey = False # There's a small key door on this side self.bigKey = False # There's a big key door on this side @@ -1870,7 +1871,7 @@ class Door(object): return self def no_exit(self): - self.blocked = self.blocked_orig = True + self.blocked = self.blocked_orig = self.trapped = True return self def no_entrance(self): diff --git a/DoorShuffle.py b/DoorShuffle.py index abf1def8..7a78246b 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -88,8 +88,7 @@ def link_doors_prep(world, player): find_inaccessible_regions(world, player) - if world.doorShuffle[player] != 'vanilla': - create_dungeon_pool(world, player) + create_dungeon_pool(world, player) if world.intensity[player] >= 3 and world.doorShuffle[player] != 'vanilla': choose_portals(world, player) else: @@ -1844,12 +1843,12 @@ def shuffle_trap_doors(door_type_pools, paths, start_regions_map, all_custom, wo builder.candidates.trap = filter_key_door_pool(builder.candidates.trap, all_custom[dungeon]) remaining -= len(custom_trap_doors[dungeon]) ttl += len(builder.candidates.trap) - if ttl == 0: + if ttl == 0 and all(len(custom_trap_doors[dungeon]) == 0 for dungeon in pool): continue for dungeon in pool: builder = world.dungeon_layouts[player][dungeon] proportion = len(builder.candidates.trap) - calc = int(round(proportion * door_type_pool.traps/ttl)) + calc = 0 if ttl == 0 else int(round(proportion * door_type_pool.traps/ttl)) suggested = min(proportion, calc) remaining -= suggested suggestion_map[dungeon] = suggested @@ -1981,7 +1980,10 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, all_ remaining = max(0, remaining) for dungeon in pool: builder = world.dungeon_layouts[player][dungeon] - calculated = int(round(builder.key_doors_num*total_keys/ttl)) + if ttl == 0: + calculated = 0 + else: + calculated = int(round(builder.key_doors_num*total_keys/ttl)) max_keys = max(0, builder.location_cnt - calc_used_dungeon_items(builder, world, player)) cand_len = max(0, len(builder.candidates.small) - builder.key_drop_cnt) limit = min(max_keys, cand_len, max_computation) @@ -2211,9 +2213,10 @@ def find_valid_trap_combination(builder, suggested, start_regions, paths, world, sample_list = build_sample_list(combinations, 1000) proposal = kth_combination(sample_list[itr], trap_door_pool, trap_doors_needed) proposal.extend(custom_trap_doors) + filtered_proposal = [x for x in proposal if x.name not in trap_door_exceptions] start_regions, event_starts = filter_start_regions(builder, start_regions, world, player) - while not validate_trap_layout(proposal, builder, start_regions, paths, world, player): + while not validate_trap_layout(filtered_proposal, builder, start_regions, paths, world, player): itr += 1 if itr >= len(sample_list): if not drop: @@ -2248,6 +2251,12 @@ def filter_start_regions(builder, start_regions, world, player): portal_entrance_region = portal.door.entrance.parent_region.name if portal_entrance_region not in builder.path_entrances: excluded[region] = None + if not portal: + drop_region = next((x.parent_region for x in region.entrances + if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld] + or x.parent_region.name == 'Sewer Drop'), None) + if drop_region and drop_region.name in world.inaccessible_regions[player]: + excluded[region] = None if std_flag and (not portal or portal.find_portal_entrance().parent_region.name != 'Hyrule Castle Courtyard'): excluded[region] = None if portal is None: @@ -2343,10 +2352,12 @@ def reassign_trap_doors(trap_map, world, player): elif kind in [DoorKind.Trap2, DoorKind.TrapTriggerable]: room.change(d.doorListPos, DoorKind.Normal) d.blocked = False + d.trapped = False # connect_one_way(world, d.name, d.dest.name, player) elif d.type is DoorType.Normal and d not in traps: world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) d.blocked = False + d.trapped = False for d in traps: change_door_to_trap(d, world, player) world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', 'Trap Door', player) @@ -2384,24 +2395,45 @@ def change_door_to_trap(d, world, player): elif d.direction in [Direction.North, Direction.West]: new_kind = DoorKind.TrapTriggerable if new_kind: - d.blocked = True + d.blocked = is_trap_door_blocked(d) + d.trapped = True pos = 3 if d.type == DoorType.Normal else 4 verify_door_list_pos(d, room, world, player, pos) d.trapFlag = {0: 0x4, 1: 0x2, 2: 0x1, 3: 0x8}[d.doorListPos] room.change(d.doorListPos, new_kind) - if d.entrance.connected_region is not None: + if d.entrance.connected_region is not None and d.blocked: d.entrance.connected_region.entrances.remove(d.entrance) d.entrance.connected_region = None elif d.type is DoorType.Normal: - d.blocked = True + d.blocked = is_trap_door_blocked(d) + d.trapped = True verify_door_list_pos(d, room, world, player, pos=3) d.trapFlag = {0: 0x4, 1: 0x2, 2: 0x1}[d.doorListPos] room.change(d.doorListPos, DoorKind.Trap) - if d.entrance.connected_region is not None: + if d.entrance.connected_region is not None and d.blocked: d.entrance.connected_region.entrances.remove(d.entrance) d.entrance.connected_region = None +trap_door_exceptions = { + 'PoD Mimics 2 SW', 'TR Twin Pokeys NW', 'Thieves Blocked Entry SW', 'Hyrule Dungeon Armory Interior Key Door N', + 'Desert Compass Key Door WN', 'TR Tile Room SE', 'Mire Cross SW', 'Tower Circle of Pots ES', + 'Eastern Single Eyegore ES', 'Eastern Duo Eyegores SE', 'Swamp Push Statue S', + 'Skull 2 East Lobby WS', 'GT Hope Room WN', 'Eastern Courtyard Ledge S', 'Ice Lobby SE', 'GT Speed Torch WN', + 'Ice Switch Room ES', 'Ice Switch Room NE', 'Skull Torch Room WS', 'GT Speed Torch NE', 'GT Speed Torch WS', + 'GT Torch Cross WN', 'Mire Tile Room SW', 'Mire Tile Room ES', 'TR Torches WN', 'PoD Lobby N', 'PoD Middle Cage S', + 'Ice Bomb Jump NW', 'GT Hidden Spikes SE', 'Ice Tall Hint EN', 'GT Conveyor Cross EN', 'Eastern Pot Switch WN', + 'Thieves Conveyor Maze WN', 'Thieves Conveyor Maze SW', 'Eastern Dark Square Key Door WN', 'Eastern Lobby NW', + 'Eastern Lobby NE', 'Ice Cross Bottom SE', 'Desert Back Lobby S', 'Desert West S', + 'Desert West Lobby ES', 'Mire Hidden Shooters SE', 'Mire Hidden Shooters ES', 'Mire Hidden Shooters WS', + 'Tower Dark Pits EN', 'Tower Dark Maze ES', 'TR Tongue Pull WS', +} + + +def is_trap_door_blocked(door): + return door.name not in trap_door_exceptions + + def find_big_key_candidates(builder, start_regions, used, world, player): if world.door_type_mode[player] != 'original': # big, all, chaos # traverse dungeon and find candidates diff --git a/Main.py b/Main.py index 7c124808..be6266e2 100644 --- a/Main.py +++ b/Main.py @@ -34,7 +34,7 @@ from source.overworld.EntranceShuffle2 import link_entrances_new from source.tools.BPS import create_bps_from_data from source.classes.CustomSettings import CustomSettings -version_number = '1.2.0.19' +version_number = '1.2.0.20' version_branch = '-u' __version__ = f'{version_number}{version_branch}' diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3231157f..59f1e778 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -109,6 +109,9 @@ These are now independent of retro mode and have three options: None, Random, an # Bug Fixes and Notes +* 1.2.0.20u + * Added logic for trap doors that could be opened using existing room triggers + * Added a notes field for user added notes either via CLI or Customizer * 1.2.0.19u * Added min/max for triforce pool, goal, and difference for CLI and Customizer. (Thanks Catobat) * Fixed a bug with dungeon generation diff --git a/Rules.py b/Rules.py index 26e70f5f..0b390f3d 100644 --- a/Rules.py +++ b/Rules.py @@ -276,11 +276,18 @@ def global_rules(world, player): # Start of door rando rules # TODO: Do these need to flag off when door rando is off? - some of them, yes + def is_trapped(entrance): + return world.get_entrance(entrance, player).door.trapped + # Eastern Palace # Eyegore room needs a bow set_rule(world.get_entrance('Eastern Duo Eyegores NE', player), lambda state: state.can_shoot_arrows(player)) set_rule(world.get_entrance('Eastern Single Eyegore NE', player), lambda state: state.can_shoot_arrows(player)) set_rule(world.get_entrance('Eastern Map Balcony Hook Path', player), lambda state: state.has('Hookshot', player)) + if is_trapped('Eastern Single Eyegore ES'): + set_rule(world.get_entrance('Eastern Single Eyegore ES', player), lambda state: state.can_shoot_arrows(player)) + if is_trapped('Eastern Duo Eyegores SE'): + set_rule(world.get_entrance('Eastern Duo Eyegores SE', player), lambda state: state.can_shoot_arrows(player)) # Boss rules. Same as below but no BK or arrow requirement. set_defeat_dungeon_boss_rule(world.get_location('Eastern Palace - Prize', player)) @@ -305,13 +312,18 @@ def global_rules(world, player): set_rule(world.get_entrance('Tower Red Spears WN', player), lambda state: state.can_kill_most_things(player)) set_rule(world.get_entrance('Tower Red Guards EN', player), lambda state: state.can_kill_most_things(player)) set_rule(world.get_entrance('Tower Red Guards SW', player), lambda state: state.can_kill_most_things(player)) + set_rule(world.get_entrance('Tower Circle of Pots NW', player), lambda state: state.can_kill_most_things(player)) + if is_trapped('Tower Circle of Pots ES'): + set_rule(world.get_entrance('Tower Circle of Pots ES', player), + lambda state: state.can_kill_most_things(player)) set_rule(world.get_entrance('Tower Altar NW', player), lambda state: state.has_sword(player)) set_defeat_dungeon_boss_rule(world.get_location('Agahnim 1', player)) - set_rule(world.get_entrance('PoD Arena Landing Bonk Path', player), lambda state: state.has_Boots(player)) set_rule(world.get_entrance('PoD Mimics 1 NW', player), lambda state: state.can_shoot_arrows(player)) set_rule(world.get_entrance('PoD Mimics 2 NW', player), lambda state: state.can_shoot_arrows(player)) + if is_trapped('PoD Mimics 2 SW'): + set_rule(world.get_entrance('PoD Mimics 2 SW', player), lambda state: state.can_shoot_arrows(player)) set_rule(world.get_entrance('PoD Bow Statue Down Ladder', player), lambda state: state.can_shoot_arrows(player)) set_rule(world.get_entrance('PoD Map Balcony Drop Down', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('PoD Dark Pegs Landing to Right', player), lambda state: state.has('Hammer', player)) @@ -360,6 +372,8 @@ def global_rules(world, player): set_rule(world.get_entrance('Skull Big Chest Hookpath', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Skull Torch Room WN', player), lambda state: state.has('Fire Rod', player)) + if is_trapped('Skull Torch Room WS'): + set_rule(world.get_entrance('Skull Torch Room WS', player), lambda state: state.has('Fire Rod', player)) set_rule(world.get_entrance('Skull Vines NW', player), lambda state: state.has_sword(player)) hidden_pits_door = world.get_door('Skull Small Hall WS', player) @@ -397,6 +411,8 @@ def global_rules(world, player): set_rule(world.get_location('Thieves\' Town - Prize', player), lambda state: state.has('Maiden Unmasked', player) and world.get_location('Thieves\' Town - Prize', player).parent_region.dungeon.boss.can_defeat(state)) set_rule(world.get_entrance('Ice Lobby WS', player), lambda state: state.can_melt_things(player)) + if is_trapped('Ice Lobby SE'): + set_rule(world.get_entrance('Ice Lobby SE', player), lambda state: state.can_melt_things(player)) set_rule(world.get_entrance('Ice Hammer Block ES', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player)) set_rule(world.get_location('Ice Palace - Hammer Block Key Drop', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player)) set_rule(world.get_location('Ice Palace - Map Chest', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player)) @@ -411,6 +427,12 @@ def global_rules(world, player): set_rule(world.get_entrance('Ice Hookshot Balcony Path', player), lambda state: state.has('Hookshot', player)) if not world.get_door('Ice Switch Room SE', player).entranceFlag: set_rule(world.get_entrance('Ice Switch Room SE', player), lambda state: state.has('Cane of Somaria', player) or state.has('Convenient Block', player)) + if is_trapped('Ice Switch Room ES'): + set_rule(world.get_entrance('Ice Switch Room ES', player), + lambda state: state.has('Cane of Somaria', player) or state.has('Convenient Block', player)) + if is_trapped('Ice Switch Room NE'): + set_rule(world.get_entrance('Ice Switch Room NE', player), + lambda state: state.has('Cane of Somaria', player) or state.has('Convenient Block', player)) set_defeat_dungeon_boss_rule(world.get_location('Ice Palace - Boss', player)) set_defeat_dungeon_boss_rule(world.get_location('Ice Palace - Prize', player)) @@ -431,8 +453,15 @@ def global_rules(world, player): 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)) + if is_trapped('Mire Tile Room SW'): + set_rule(world.get_entrance('Mire Tile Room SW', player), lambda state: state.has_fire_source(player)) + if is_trapped('Mire Tile Room ES'): + set_rule(world.get_entrance('Mire Tile Room ES', player), lambda state: state.has_fire_source(player)) set_rule(world.get_entrance('Mire Attic Hint Hole', player), lambda state: state.has_fire_source(player)) set_rule(world.get_entrance('Mire Dark Shooters SW', player), lambda state: state.has('Cane of Somaria', player)) + if is_trapped('Mire Dark Shooters SE'): + set_rule(world.get_entrance('Mire Dark Shooters SE', player), + lambda state: state.has('Cane of Somaria', player)) set_defeat_dungeon_boss_rule(world.get_location('Misery Mire - Boss', player)) set_defeat_dungeon_boss_rule(world.get_location('Misery Mire - Prize', player)) @@ -448,6 +477,9 @@ def global_rules(world, player): set_rule(world.get_entrance('TR Hub Path', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('TR Hub Ledges Path', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('TR Torches NW', player), lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player)) + if is_trapped('TR Torches WN'): + set_rule(world.get_entrance('TR Torches WN', player), + lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player)) set_rule(world.get_entrance('TR Big Chest Entrance Gap', player), lambda state: state.has('Cane of Somaria', player) or state.has('Hookshot', player)) set_rule(world.get_entrance('TR Big Chest Gap', player), lambda state: state.has('Cane of Somaria', player) or state.has_Boots(player)) set_rule(world.get_entrance('TR Dark Ride Up Stairs', player), lambda state: state.has('Cane of Somaria', player)) @@ -467,10 +499,20 @@ def global_rules(world, player): set_rule(world.get_location('Ganons Tower - Bob\'s Torch', player), lambda state: state.has_Boots(player)) set_rule(world.get_entrance('GT Hope Room EN', player), lambda state: state.has('Cane of Somaria', player)) + if is_trapped('GT Hope Room WN'): + set_rule(world.get_entrance('GT Hope Room WN', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('GT Conveyor Cross Hammer Path', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('GT Conveyor Cross Hookshot Path', player), lambda state: state.has('Hookshot', player)) + if is_trapped('GT Conveyor Cross EN'): + set_rule(world.get_entrance('GT Conveyor Cross EN', player), lambda state: state.has('Hammer', player)) if not world.get_door('GT Speed Torch SE', player).entranceFlag: set_rule(world.get_entrance('GT Speed Torch SE', player), lambda state: state.has('Fire Rod', player)) + if is_trapped('GT Speed Torch NE'): + set_rule(world.get_entrance('GT Speed Torch NE', player), lambda state: state.has('Fire Rod', player)) + if is_trapped('GT Speed Torch WS'): + set_rule(world.get_entrance('GT Speed Torch WS', player), lambda state: state.has('Fire Rod', player)) + if is_trapped('GT Speed Torch WN'): + set_rule(world.get_entrance('GT Speed Torch WN', player), lambda state: state.has('Fire Rod', player)) set_rule(world.get_entrance('GT Hookshot South-Mid Path', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('GT Hookshot Mid-North Path', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('GT Hookshot East-Mid Path', player), lambda state: state.has('Hookshot', player) or state.has_Boots(player)) @@ -505,6 +547,8 @@ def global_rules(world, player): set_rule(world.get_entrance('GT Lanmolas 2 ES', player), lambda state: world.get_region('GT Lanmolas 2', player).dungeon.bosses['middle'].can_defeat(state)) set_rule(world.get_entrance('GT Lanmolas 2 NW', player), lambda state: world.get_region('GT Lanmolas 2', player).dungeon.bosses['middle'].can_defeat(state)) set_rule(world.get_entrance('GT Torch Cross ES', player), lambda state: state.has_fire_source(player)) + if is_trapped('GT Torch Cross WN'): + set_rule(world.get_entrance('GT Torch Cross WN', player), lambda state: state.has_fire_source(player)) set_rule(world.get_entrance('GT Falling Torches NE', player), lambda state: state.has_fire_source(player)) # todo: the following only applies to crystal state propagation from this supertile # you can also reset the supertile, but I'm not sure how to model that @@ -760,13 +804,29 @@ def bomb_rules(world, player): ('GT Petting Zoo SE', False), # Dont make anyone do this room with bombs and/or pots. ('GT DMs Room SW', False) # Four red stalfos ] + conditional_kill_traps = [ + ('Hyrule Dungeon Armory Interior Key Door N', True), + ('Desert Compass Key Door WN', True), + ('Thieves Blocked Entry SW', True), + ('TR Tongue Pull WS', True), + ('TR Twin Pokeys NW', False), + ] for killdoor,bombable in easy_kill_rooms: if bombable: add_rule(world.get_entrance(killdoor, player), lambda state: (state.can_use_bombs(player) or state.can_kill_most_things(player))) else: add_rule(world.get_entrance(killdoor, player), lambda state: state.can_kill_most_things(player)) + for kill_door, bombable in conditional_kill_traps: + if world.get_entrance(kill_door, player).door.trapped: + if bombable: + add_rule(world.get_entrance(kill_door, player), + lambda state: (state.can_use_bombs(player) or state.can_kill_most_things(player))) + else: + add_rule(world.get_entrance(kill_door, player), lambda state: state.can_kill_most_things(player)) add_rule(world.get_entrance('Ice Stalfos Hint SE', player), lambda state: state.can_use_bombs(player)) # Need bombs for big stalfos knights - add_rule(world.get_entrance('Mire Cross ES', player), lambda state: state.can_kill_most_things(player)) # 4 Sluggulas. Bombs don't work // or (state.can_use_bombs(player) and state.has('Magic Powder'), player) + add_rule(world.get_entrance('Mire Cross ES', player), lambda state: state.can_kill_most_things(player)) # 4 Sluggulas. Bombs don't work // or (state.can_use_bombs(player) and state.has('Magic Powder'), player) + if world.get_entrance('Mire Cross SW', player).door.trapped: + add_rule(world.get_entrance('Mire Cross SW', player), lambda state: state.can_kill_most_things(player)) enemy_kill_drops = [ # Location, bool-bombable ('Hyrule Castle - Map Guard Key Drop', True), @@ -1143,6 +1203,9 @@ def swordless_rules(world, player): set_rule(world.get_entrance('Tower Altar NW', player), lambda state: True) set_rule(world.get_entrance('Skull Vines NW', player), lambda state: True) set_rule(world.get_entrance('Ice Lobby WS', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player)) + if world.get_entrance('Ice Lobby SE', player).door.trapped: + set_rule(world.get_entrance('Ice Lobby SE', player), + lambda state: state.has('Fire Rod', player) or state.has('Bombos', player)) set_rule(world.get_location('Ice Palace - Freezor Chest', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player)) set_rule(world.get_location('Ether Tablet', player), lambda state: state.has('Book of Mudora', player) and state.has('Hammer', player)) @@ -1156,7 +1219,7 @@ def swordless_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('Hammer', player)) - +# todo: new traps std_kill_rooms = { 'Hyrule Dungeon Armory Main': ['Hyrule Dungeon Armory S', 'Hyrule Dungeon Armory ES'], # One green guard 'Hyrule Dungeon Armory Boomerang': ['Hyrule Dungeon Armory Boomerang WS'], # One blue guard @@ -1187,6 +1250,18 @@ std_kill_rooms = { 'GT Wizzrobes 2': ['GT Wizzrobes 2 SE', 'GT Wizzrobes 2 NE'] # Wizzrobes. Bombs don't work } # all trap rooms? +std_kill_doors_if_trapped = { + 'Hyrule Dungeon Armory Main': 'Hyrule Dungeon Armory Interior Key Door N', + # 'Eastern Single Eyegore ES', # arrow rule is sufficient + # 'Eastern Duo Eyegores S', # arrow rule is sufficient + 'TR Twin Pokeys': 'TR Twin Pokeys NW', + 'Thieves Basement Block': 'Thieves Blocked Entry SW', + 'Desert Compass Room': 'Desert Compass Key Door WN', + 'Mire Cross': 'Mire Cross SW', + 'Tower Circle of Pots': 'Tower Circle of Pots ES', + # 'Ice Lobby S' # can melt rule is sufficient +} + def add_connection(parent_name, target_name, entrance_name, world, player): parent = world.get_region(parent_name, player) target = world.get_region(target_name, player) @@ -1241,6 +1316,10 @@ def standard_rules(world, player): if region.name in std_kill_rooms: for ent in std_kill_rooms[region.name]: add_rule(world.get_entrance(ent, player), lambda state: standard_escape_rule(state)) + if region.name in std_kill_doors_if_trapped: + ent = world.get_entrance(std_kill_doors_if_trapped[region.name], player) + if ent.door.trapped: + add_rule(ent, lambda state: standard_escape_rule(state)) set_rule(world.get_location('Zelda Pickup', player), lambda state: state.has('Big Key (Escape)', player)) set_rule(world.get_entrance('Hyrule Castle Throne Room Tapestry', player), lambda state: state.has('Zelda Herself', player)) @@ -1866,6 +1945,11 @@ def set_bunny_rules(world, player, inverted): if is_bunny(bunny_exit.parent_region): add_rule(bunny_exit, get_rule_to_add(bunny_exit.parent_region)) + for ent_name in bunny_impassible_if_trapped: + bunny_exit = world.get_entrance(ent_name, player) + if bunny_exit.door.trapped 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] doors_to_check = [x for x in doors_to_check if x.type in [DoorType.Normal, DoorType.Interior] and not x.blocked] for door in doors_to_check: @@ -1997,6 +2081,17 @@ bunny_impassible_doors = { 'GT Validation Block Path' } +bunny_impassible_if_trapped = { + 'Hyrule Dungeon Armory Interior Key Door N', 'Eastern Pot Switch WN', 'Eastern Lobby NW', + 'Eastern Lobby NE', 'Desert Compass Key Door WN', 'Tower Circle of Pots ES', 'PoD Mimics 2 SW', + 'PoD Middle Cage S', 'Swamp Push Statue S', 'Skull 2 East Lobby WS', 'Skull Torch Room WS', + 'Thieves Conveyor Maze WN', 'Thieves Conveyor Maze SW', 'Thieves Blocked Entry SW', 'Ice Bomb Jump NW', + 'Ice Tall Hint EN', 'Ice Switch Room ES', 'Ice Switch Room NE', 'Mire Cross SW', + 'Mire Tile Room SW', 'Mire Tile Room ES', 'TR Twin Pokeys NW', 'TR Torches WN', 'GT Hope Room WN', + 'GT Speed Torch NE', 'GT Speed Torch WS', 'GT Torch Cross WN', 'GT Hidden Spikes SE', 'GT Conveyor Cross EN', + 'GT Speed Torch WN', 'Ice Lobby SE' +} + def add_key_logic_rules(world, player): key_logic = world.key_logic[player] diff --git a/test/dungeons/trap_test.yaml b/test/dungeons/trap_test.yaml new file mode 100644 index 00000000..d82a8ade --- /dev/null +++ b/test/dungeons/trap_test.yaml @@ -0,0 +1,125 @@ +meta: + players: 1 +settings: + 1: + door_shuffle: basic + intensity: 3 + door_type_mode: all +doors: + 1: + doors: + PoD Mimics 2 SW: + type: Trap Door +# TR Twin Pokeys NW: # not possible due to trap flags +# type: Trap Door + Thieves Blocked Entry SW: + type: Trap Door + Hyrule Dungeon Armory Interior Key Door N: + type: Trap Door + Desert Compass Key Door WN: + type: Trap Door + TR Tile Room SE: + type: Trap Door +# Mire Cross SW: # not possible due to trap flags +# type: Trap Door + Tower Circle of Pots ES: + type: Trap Door + Eastern Single Eyegore ES: + type: Trap Door + Eastern Duo Eyegores SE: + type: Trap Door + Swamp Push Statue S: + type: Trap Door +# Skull 2 East Lobby WS: # currently not possible due to trap flags +# type: Trap Door + GT Hope Room WN : + type: Trap Door + +# Eastern Courtyard Ledge S: # currently not possible due to trap flags +# type: Trap Door + Ice Switch Room ES : + type: Trap Door + Ice Switch Room NE : + type: Trap Door + Skull Torch Room WS : + type: Trap Door + GT Speed Torch NE : + type: Trap Door + GT Speed Torch WS : + type: Trap Door + GT Torch Cross WN : + type: Trap Door + Mire Tile Room SW : + type: Trap Door + Mire Tile Room ES : + type: Trap Door + TR Torches WN : + type: Trap Door + PoD Lobby N: + type: Trap Door + PoD Middle Cage S: + type: Trap Door + Ice Bomb Jump NW: + type: Trap Door + GT Hidden Spikes SE: + type: Trap Door + Ice Tall Hint EN: + type: Trap Door + GT Conveyor Cross EN: + type: Trap Door + Eastern Pot Switch WN: + type: Trap Door + Thieves Conveyor Maze WN: + type: Trap Door +# Thieves Conveyor Maze SW: #not possible due to 4 door limit +# type: Trap Door + Eastern Dark Square Key Door WN: + type: Trap Door + Eastern Lobby NW: + type: Trap Door + Eastern Lobby NE: + type: Trap Door +# Ice Cross Bottom SE: # not possible due to trap flags +# type: Trap Door + Desert Back Lobby S: + type: Trap Door +# Desert West S: need enough lobbies for basic, should otherwise work +# type: Trap Door + Desert West Lobby ES: + type: Trap Door +# Mire Hidden Shooters SE: # not possible due to trap flags +# type: Trap Door +# Mire Hidden Shooters ES: # not possible due to trap flags +# type: Trap Door + Mire Hidden Shooters WS: + type: Trap Door + Tower Dark Pits EN: + type: Trap Door + Tower Dark Maze ES: + type: Trap Door + TR Tongue Pull WS: + type: Trap Door + +# Lower layer: not valid + # Sewers Pull Switch N: + # type: Trap Door + # PoD Sexy Statue W: # not possible due to trap flags and low layer too, so likely not an exception + # type: Trap Door + +# Not valid due to disappearing somaria block +# Mire Dark Shooters SE: +# type: Trap Door + + # These triggers don't open doors +# Ice Compass Room NE: +# type: Trap Door +# Hera Torches NE: +# type: Trap Door +# Mire Spikes WS: +# type: Trap Door +# Mire Spikes SW: +# type: Trap Door +# Mire Spikes NW: +# type: Trap Door +# Tower Room 03 WN: +# type: Trap Door From e6597a7ab9c43c8ab88da11a83f4f6838bf06d17 Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 7 Aug 2023 09:36:50 -0600 Subject: [PATCH 26/30] Fixed inverted problem with experimental (logically assumed link's house was in the light world) Fixed hint typo --- PotShuffle.py | 2 +- RELEASENOTES.md | 4 +++- source/overworld/EntranceShuffle2.py | 4 ++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/PotShuffle.py b/PotShuffle.py index 6a4df35e..b90d7863 100644 --- a/PotShuffle.py +++ b/PotShuffle.py @@ -1022,7 +1022,7 @@ key_drop_data = { 'Ice Palace - Jelly Key Drop': ['Drop', (0x09DA21, 0xE, 3), 'dropped in Ice Palace', 'Small Key (Ice Palace)'], 'Ice Palace - Conveyor Key Drop': ['Drop', (0x09DE08, 0x3E, 8), 'dropped in Ice Palace', 'Small Key (Ice Palace)'], 'Ice Palace - Hammer Block Key Drop': ['Pot', 0x3F, 'under a block in Ice Palace', 'Small Key (Ice Palace)'], - 'Ice Palace - Many Pots Pot Key': ['Pot', 0x9F, 'int a pot in Ice Palace', 'Small Key (Ice Palace)'], + 'Ice Palace - Many Pots Pot Key': ['Pot', 0x9F, 'in a pot in Ice Palace', 'Small Key (Ice Palace)'], 'Misery Mire - Spikes Pot Key': ['Pot', 0xB3, 'in a pot in Misery Mire', 'Small Key (Misery Mire)'], 'Misery Mire - Fishbone Pot Key': ['Pot', 0xA1, 'in a pot in forgotten Mire', 'Small Key (Misery Mire)'], 'Misery Mire - Conveyor Crystal Key Drop': ['Drop', (0x09E7FB, 0xC1, 9), 'dropped in Misery Mire', 'Small Key (Misery Mire)'], diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 59f1e778..9dfc8bbd 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -111,7 +111,9 @@ These are now independent of retro mode and have three options: None, Random, an * 1.2.0.20u * Added logic for trap doors that could be opened using existing room triggers - * Added a notes field for user added notes either via CLI or Customizer + * Fixed a problem with inverted generation and the experimental flag + * Added a notes field for user added notes either via CLI or Customizer (thanks Hiimcody and Codemann) + * Fixed a typo for a specific pot hint * 1.2.0.19u * Added min/max for triforce pool, goal, and difference for CLI and Customizer. (Thanks Catobat) * Fixed a bug with dungeon generation diff --git a/source/overworld/EntranceShuffle2.py b/source/overworld/EntranceShuffle2.py index 6afc7b9e..da71c661 100644 --- a/source/overworld/EntranceShuffle2.py +++ b/source/overworld/EntranceShuffle2.py @@ -66,6 +66,10 @@ def link_entrances_new(world, player): default_map['Old Man Cave (East)'] = 'Death Mountain Return Cave Exit (West)' one_way_map['Bumper Cave (Top)'] = 'Dark Death Mountain Healer Fairy' del default_map['Bumper Cave (Top)'] + del one_way_map['Big Bomb Shop'] + one_way_map['Links House'] = 'Big Bomb Shop' + del default_map['Links House'] + default_map['Big Bomb Shop'] = 'Links House Exit' avail_pool.default_map = default_map avail_pool.one_way_map = one_way_map From 9e26c9c42cb9c726f1208183306e063e6d8f1ec4 Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 7 Aug 2023 10:44:55 -0600 Subject: [PATCH 27/30] Fixed minor issue with dungeon counter interfering with timer --- Main.py | 2 +- RELEASENOTES.md | 2 ++ Rom.py | 2 +- data/base2current.bps | Bin 93219 -> 93217 bytes 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Main.py b/Main.py index 7d3ae0eb..4f49cc48 100644 --- a/Main.py +++ b/Main.py @@ -31,7 +31,7 @@ from Utils import output_path, parse_player_names from source.item.FillUtil import create_item_pool_config, massage_item_pool, district_item_pool_config from source.tools.BPS import create_bps_from_data -__version__ = '1.1.5-dev' +__version__ = '1.1.6-dev' from source.classes.BabelFish import BabelFish diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4c65204f..b3734893 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -181,6 +181,8 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o # Bug Fixes and Notes +* 1.1.6 + * Minor issue with dungeon counter hud interfering with timer * 1.1.5 * MultiServer can not disable forfeits if desired * 1.1.4 diff --git a/Rom.py b/Rom.py index b76cfef8..b09b7107 100644 --- a/Rom.py +++ b/Rom.py @@ -37,7 +37,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'fc4d4a01f8c4e00280ea5640297f8e9c' +RANDOMIZERBASEHASH = 'e30a2490da811232e0a438da8fa662ee' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index 25cc58b2c300e723aaa269298be9cd15e9323ea3..c616e5516f89d5176466cff4f5bd8336b754f234 100644 GIT binary patch delta 63 zcmV-F0KosF*ae~31+ZEH15<&lvt0qw Date: Mon, 7 Aug 2023 10:49:55 -0600 Subject: [PATCH 28/30] Fixed minor issue with dungeon counter interfering with timer (rom re-build) --- RELEASENOTES.md | 2 ++ Rom.py | 2 +- data/base2current.bps | Bin 94169 -> 94167 bytes 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 61ab6abb..35155c50 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -201,6 +201,8 @@ These are now independent of retro mode and have three options: None, Random, an * Fix for unintentional decoupled door in standard * Fix a problem with BK doors being one-sided * Change to how wilds keys are placed in standard, better randomization + * Removed a Triforce text + * Fix for Desert Tiles 1 key door * 1.2.0.7-u * Fix for some misery mire key logic * Minor standard generation fix diff --git a/Rom.py b/Rom.py index 482080b2..5b65c852 100644 --- a/Rom.py +++ b/Rom.py @@ -37,7 +37,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '467681d6160233f7af2761c631e26985' +RANDOMIZERBASEHASH = '168574b64461acded5f2e8394a05577e' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index 9b3d673a51005eba1d3d336d10206d582cb1852b..80dff6bf7a424495c72c7b12a3e5ab3083e2fc14 100644 GIT binary patch delta 79 zcmV-V0I>hr-v!s-1+ZQL1RRdzakF9pSLQ9vILCf~52=43k21%8k2J@Ak2c4CpAQm{ lxdbBjyQJbEuW$2y$^Y;opC}TLv*_pPhX{CIs4;^=YVPr8Cu0Br delta 81 zcmV-X0IvVn-v!y<1+ZQL1ni9ya Date: Mon, 7 Aug 2023 12:19:35 -0600 Subject: [PATCH 29/30] Fix for hera boss music (the last?) --- RELEASENOTES.md | 2 ++ Rom.py | 2 +- data/base2current.bps | Bin 94167 -> 94175 bytes 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 35155c50..336c604e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -110,10 +110,12 @@ These are now independent of retro mode and have three options: None, Random, an # Bug Fixes and Notes * 1.2.0.20u + * New generation feature that allows Spiral Stair to link to themselves (thank Catobat) * Added logic for trap doors that could be opened using existing room triggers * Fixed a problem with inverted generation and the experimental flag * Added a notes field for user added notes either via CLI or Customizer (thanks Hiimcody and Codemann) * Fixed a typo for a specific pot hint + * Fix for Hera Boss music (thanks Codemann) * 1.1.6 (from Stable) * Minor issue with dungeon counter hud interfering with timer * 1.2.0.19u diff --git a/Rom.py b/Rom.py index 5b65c852..5dc3652a 100644 --- a/Rom.py +++ b/Rom.py @@ -37,7 +37,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '168574b64461acded5f2e8394a05577e' +RANDOMIZERBASEHASH = '61662913cc0cb12fb870d794937d88d9' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index 80dff6bf7a424495c72c7b12a3e5ab3083e2fc14..23872f82d3be21e2fabd52ac809761a819691b9f 100644 GIT binary patch delta 5805 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 delta 5761 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 From fd3bdc6b712ca9c9b8413682d65dfc1359228254 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Mon, 7 Aug 2023 16:04:58 -0500 Subject: [PATCH 30/30] Version bump 0.3.2.1 --- CHANGELOG.md | 5 +++++ OverworldShuffle.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b156a0dd..1f235164 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.3.2.1 +- \~Merged in DR v1.2.0.20~ +- Some minor Swapped ER improvements +- Fixed generation error with Flute Activation + ## 0.3.2.0 - New Swapped ER mode option - Fixed issue with flipper rules not properly getting pearl requirement added diff --git a/OverworldShuffle.py b/OverworldShuffle.py index c34b6d62..76773b77 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -7,7 +7,7 @@ from OWEdges import OWTileRegions, OWEdgeGroups, OWEdgeGroupsTerrain, OWExitType from OverworldGlitchRules import create_owg_connections from Utils import bidict -version_number = '0.3.2.0' +version_number = '0.3.2.1' # branch indicator is intentionally different across branches version_branch = '-u'