From d64a4e63a2e58c55bb593577eacde0de0925a62c Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 8 Jan 2021 16:31:33 -0700 Subject: [PATCH] A lot of generation improvements and bug squashing --- BaseClasses.py | 12 ++-- DoorShuffle.py | 79 ++++++++++++++++----- Doors.py | 1 + DungeonGenerator.py | 46 ++++++++++--- EntranceShuffle.py | 6 +- KeyDoorShuffle.py | 15 ++-- Main.py | 6 +- RELEASENOTES.md | 163 ++------------------------------------------ Rom.py | 2 +- 9 files changed, 130 insertions(+), 200 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index c0248d11..1ee0afb9 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -153,9 +153,10 @@ class World(object): self._door_cache[(door.name, door.player)] = door def remove_door(self, door, player): - if (door, player) in self._door_cache.keys(): - del self._door_cache[(door, player)] - self.doors.remove(door) + if (door.name, player) in self._door_cache.keys(): + del self._door_cache[(door.name, player)] + if door in self.doors: + self.doors.remove(door) def get_regions(self, player=None): return self.regions if player is None else self._region_cache[player].values() @@ -1219,6 +1220,7 @@ class Door(object): # self.connected = False # combine with Dest? 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.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 @@ -1230,7 +1232,7 @@ class Door(object): self.dead = False self.entrance = entrance - if entrance is not None: + if entrance is not None and not entrance.door: entrance.door = self def getAddress(self): @@ -1316,7 +1318,7 @@ class Door(object): return self def no_exit(self): - self.blocked = True + self.blocked = self.blocked_orig = True return self def no_entrance(self): diff --git a/DoorShuffle.py b/DoorShuffle.py index 922142f4..a3e12987 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -14,11 +14,33 @@ from Items import ItemFactory from RoomData import DoorKind, PairedDoor from DungeonGenerator import ExplorationState, convert_regions, generate_dungeon, pre_validate, determine_required_paths, drop_entrances from DungeonGenerator import create_dungeon_builders, split_dungeon_builder, simple_dungeon_builder, default_dungeon_entrances -from DungeonGenerator import dungeon_portals, dungeon_drops +from DungeonGenerator import dungeon_portals, dungeon_drops, GenerationException from KeyDoorShuffle import analyze_dungeon, validate_vanilla_key_logic, build_key_layout, validate_key_layout def link_doors(world, player): + attempt, valid = 1, False + while not valid: + try: + link_doors_main(world, player) + valid = True + except GenerationException as e: + logging.getLogger('').debug(f'Irreconcilable generation. {str(e)} Starting a new attempt.') + attempt += 1 + if attempt > 10: + raise Exception('Could not create world in 10 attempts. Generation algorithms need more work', e) + for door in world.doors: + if door.player == player: + door.dest = None + ent = door.entrance + if door.type != DoorType.Logical and ent.connected_region is not None: + ent.connected_region.entrances = [x for x in ent.connected_region.entrances if x != ent] + ent.connected_region = None + for portal in world.dungeon_portals[player]: + disconnect_portal(portal, world, player) + + +def link_doors_main(world, player): # Drop-down connections & push blocks for exitName, regionName in logical_connections: @@ -45,7 +67,8 @@ def link_doors(world, player): mirror_route = world.get_entrance('Sanctuary Mirror Route', player) mr_door = mirror_route.door sanctuary = mirror_route.parent_region - sanctuary.exits.remove(mirror_route) + if mirror_route in sanctuary.exits: + sanctuary.exits.remove(mirror_route) world.remove_entrance(mirror_route, player) world.remove_door(mr_door, player) @@ -388,7 +411,10 @@ def choose_portals(world, player): possible_portals = outstanding_portals if not info.sole_entrance else [x for x in outstanding_portals if x != info.sole_entrance] choice, portal = assign_portal(candidates, possible_portals, world, player) if choice.deadEnd: - portal.deadEnd = True + if choice.passage: + portal.destination = True + else: + portal.deadEnd = True clean_up_portal_assignment(portal_assignment, dungeon, portal, master_door_list, outstanding_portals) the_rest = info.total - len(portal_assignment[dungeon]) for i in range(0, the_rest): @@ -477,7 +503,6 @@ def connect_portal(portal, world, player): ent, ext, entrance_name = portal_map[portal.name] if world.mode[player] == 'inverted' and portal.name in ['Ganons Tower', 'Agahnims Tower']: ext = 'Inverted ' + ext - # ent = 'Inverted ' + ent portal_entrance = world.get_entrance(portal.door.entrance.name, player) # ensures I get the right one for copying target_exit = world.get_entrance(ext, player) portal_entrance.connected_region = target_exit.parent_region @@ -491,22 +516,17 @@ def connect_portal(portal, world, player): portal_entrance.parent_region.entrances.append(edit_entrance) -# todo: remove this? -def connect_portal_copy(portal, world, player): +def disconnect_portal(portal, world, player): ent, ext, entrance_name = portal_map[portal.name] - if world.mode[player] == 'inverted' and portal.name in ['Ganons Tower', 'Agahnims Tower']: - ext = 'Inverted ' + ext - portal_entrance = world.get_entrance(portal.door.entrance.name, player) # ensures I get the right one for copying - target_exit = world.get_entrance(ext, player) - portal_entrance.connected_region = target_exit.parent_region - portal_region = world.get_region(portal.name + ' Portal', player) - portal_region.entrances.append(portal_entrance) + portal_entrance = world.get_entrance(portal.door.entrance.name, player) + # portal_region = world.get_region(portal.name + ' Portal', player) edit_entrance = world.get_entrance(entrance_name, player) - edit_entrance.connected_region = portal_entrance.parent_region chosen_door = world.get_door(portal_entrance.name, player) - chosen_door.blocked = False - connect_door_only(world, chosen_door, portal_region, player) - portal_entrance.parent_region.entrances.append(edit_entrance) + + # reverse work + if edit_entrance in portal_entrance.parent_region.entrances: + portal_entrance.parent_region.entrances.remove(edit_entrance) + chosen_door.blocked = chosen_door.blocked_orig def find_portal_candidates(door_list, dungeon, need_passage=False, dead_end_allowed=False, crossed=False, bk_shuffle=False): @@ -710,6 +730,7 @@ def main_dungeon_generation(dungeon_builders, recombinant_builders, connections_ continue origin_list = list(builder.entrance_list) find_enabled_origins(builder.sectors, enabled_entrances, origin_list, entrances_map, name) + split_dungeon = treat_split_as_whole_dungeon(split_dungeon, name, origin_list, world, player) if len(origin_list) <= 0 or not pre_validate(builder, origin_list, split_dungeon, world, player): if last_key == builder.name or loops > 1000: origin_name = world.get_region(origin_list[0], player).entrances[0].parent_region.name if len(origin_list) > 0 else 'no origin' @@ -857,6 +878,22 @@ def aga_tower_enabled(enabled): return False +def treat_split_as_whole_dungeon(split_dungeon, name, origin_list, world, player): + # what about ER dungeons? - find an example? (bad key doors 0 keys not valid) + if split_dungeon and name in multiple_portal_map: + possible_entrances = [] + for portal_name in multiple_portal_map[name]: + portal = world.get_portal(portal_name, player) + portal_entrance = world.get_entrance(portal_map[portal_name][0], player) + if not portal.destination and portal_entrance.parent_region.name not in world.inaccessible_regions[player]: + possible_entrances.append(portal) + if len(possible_entrances) == 1: + single_portal = possible_entrances[0] + if single_portal.door.entrance.parent_region.name in origin_list and len(origin_list) == 1: + return False + return split_dungeon + + # goals: # 1. have enough chests to be interesting (2 more than dungeon items) # 2. have a balanced amount of regions added (check) @@ -2828,6 +2865,14 @@ portal_map = { 'Ganons Tower': ('Ganons Tower', 'Ganons Tower Exit', 'Enter Ganons Tower'), } + +multiple_portal_map = { + 'Hyrule Castle': ['Sanctuary', 'Hyrule Castle West', 'Hyrule Castle South', 'Hyrule Castle East'], + 'Desert Palace': ['Desert West', 'Desert South', 'Desert East', 'Desert Back'], + 'Skull Woods': ['Skull 1', 'Skull 2 West', 'Skull 2 East', 'Skull 3'], + 'Turtle Rock': ['Turtle Rock Lazy Eyes', 'Turtle Rock Eye Bridge', 'Turtle Rock Chest', 'Turtle Rock Main'], +} + split_portals = { 'Desert Palace': ['Back', 'Main'], 'Skull Woods': ['1', '2', '3'] diff --git a/Doors.py b/Doors.py index 460bf0d9..343dd6d1 100644 --- a/Doors.py +++ b/Doors.py @@ -1301,6 +1301,7 @@ def create_doors(world, player): world.dungeon_portals[player] += dungeon_portals world.get_door('Sanctuary S', player).dead_end(allowPassage=True) + world.get_door('Eastern Hint Tile Blocked Path SE', player).passage = False world.get_door('TR Big Chest Entrance SE', player).passage = False world.get_door('Sewers Secret Room Key Door S', player).dungeonLink = 'Hyrule Castle' world.get_door('Desert Cannonball S', player).dead_end() diff --git a/DungeonGenerator.py b/DungeonGenerator.py index d14715b2..4fd59d80 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -1225,15 +1225,15 @@ def simple_dungeon_builder(name, sector_list): def create_dungeon_builders(all_sectors, connections_tuple, world, player, dungeon_entrances=None, split_dungeon_entrances=None): logger = logging.getLogger('') + logger.info('Shuffling Dungeon Sectors') if dungeon_entrances is None: dungeon_entrances = default_dungeon_entrances if split_dungeon_entrances is None: split_dungeon_entrances = split_region_starts define_sector_features(all_sectors) - finished, dungeon_map = False, {} + finished, dungeon_map, attempts = False, {}, 0 while not finished: - logger.info('Shuffling Dungeon Sectors') candidate_sectors = dict.fromkeys(all_sectors) global_pole = GlobalPolarity(candidate_sectors) @@ -1248,6 +1248,7 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, for r_name in ['Hyrule Dungeon Cellblock', 'Sanctuary']: # need to deliver zelda assign_sector(find_sector(r_name, candidate_sectors), current_dungeon, candidate_sectors, global_pole) + standard_stair_check(dungeon_map, current_dungeon, candidate_sectors, global_pole) entrances_map, potentials, connections = connections_tuple accessible_sectors, reverse_d_map = set(), {} for key in dungeon_entrances.keys(): @@ -1324,11 +1325,27 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, assign_the_rest(dungeon_map, neutral_sectors, global_pole, builder_info) dungeon_map.update(complete_dungeons) finished = True - except NeutralizingException: - pass + except (NeutralizingException, GenerationException) as e: + attempts += 1 + logger.debug(f'Attempt {attempts} failed with {str(e)}') + if attempts >= 10: + raise Exception('Could not find a valid seed quickly, something is likely horribly wrong.', e) return dungeon_map +def standard_stair_check(dungeon_map, dungeon, candidate_sectors, global_pole): + # this is because there must be at least one non-dead stairway in hc to get out + # this check may not be necessary + filtered_sectors = [x for x in candidate_sectors if any(y for y in x.outstanding_doors if not y.dead and y.type == DoorType.SpiralStairs)] + valid = False + while not valid: + chosen_sector = random.choice(filtered_sectors) + filtered_sectors.remove(chosen_sector) + valid = global_pole.is_valid_choice(dungeon_map, dungeon, [chosen_sector]) + if valid: + assign_sector(chosen_sector, dungeon, candidate_sectors, global_pole) + + def identify_destination_sectors(accessible_sectors, reverse_d_map, dungeon_map, connections, dungeon_entrances, split_dungeon_entrances): accessible_overworld, found_connections, explored = set(), set(), False @@ -1578,6 +1595,8 @@ def assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barrier if len(crystal_switches) == 0: raise GenerationException('No crystal switches to assign') sector_list = list(crystal_switches) + if len(population) > len(sector_list): + raise GenerationException('Not enough crystal switch sectors for those needed') choices = random.sample(sector_list, k=len(population)) for i, choice in enumerate(choices): builder = dungeon_map[population[i]] @@ -1588,7 +1607,7 @@ def assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barrier def ensure_crystal_switches_reachable(dungeon_map, crystal_switches, polarized_sectors, crystal_barriers, global_pole): invalid_builders = [] for name, builder in dungeon_map.items(): - if builder.c_switch_present and not builder.c_locked: + if builder.c_switch_present and builder.c_switch_required and not builder.c_locked: invalid_builders.append(builder) while len(invalid_builders) > 0: valid_builders = [] @@ -1597,7 +1616,7 @@ def ensure_crystal_switches_reachable(dungeon_map, crystal_switches, polarized_s reachable_crystals = defaultdict() for sector in builder.sectors: if sector.equations is None: - sector.equations = calc_sector_equations(sector, builder) + sector.equations = calc_sector_equations(sector) if sector.is_entrance_sector() and not sector.destination_entrance: need_switch = True for region in sector.get_start_regions(): @@ -1631,7 +1650,7 @@ def ensure_crystal_switches_reachable(dungeon_map, crystal_switches, polarized_s valid, sector, which_list = False, None, None while not valid: if len(candidates) <= 0: - raise GenerationException(f'need to provide more sophisticatedted crystal connection for {entrance_sector}') + raise GenerationException(f'need to provide more sophisticated crystal connection for {entrance_sector}') sector, which_list = random.choice(list(candidates.items())) del candidates[sector] valid = global_pole.is_valid_choice(dungeon_map, builder, [sector]) @@ -1690,7 +1709,7 @@ def find_pol_cand_for_c_switch(access, reachable_crystals, polarized_candidates) def pol_cand_matches_access_reach(sector, access, reachable_crystals): if sector.equations is None: - sector.equations = calc_sector_equations(sector, None) + sector.equations = calc_sector_equations(sector) for eq in sector.equations: key, cost_door = eq.cost if key in access.keys() and access[key]: @@ -1712,7 +1731,7 @@ def find_crystal_cand(access, crystal_switches): def crystal_cand_matches_access(sector, access): if sector.equations is None: - sector.equations = calc_sector_equations(sector, None) + sector.equations = calc_sector_equations(sector) for eq in sector.equations: key, cost_door = eq.cost if key in access.keys() and access[key] and eq.c_switch and len(sector.outstanding_doors) > 1: @@ -1984,6 +2003,9 @@ def polarity_step_3(dungeon_map, polarized_sectors, global_pole): sample_target = 100 if combos > 10 else combos * 2 while best_choices is None or samples < sample_target: samples += 1 + if len(odd_candidates) < len(odd_builders): + raise GenerationException(f'Unable to fix dungeon parity - not enough candidates.' + f' Ref: {next(iter(odd_builders)).name}') choices = random.sample(odd_candidates, k=len(odd_builders)) valid = global_pole.is_valid_multi_choice(dungeon_map, odd_builders, choices) charge = calc_total_charge(dungeon_map, odd_builders, choices) @@ -3649,14 +3671,14 @@ def copy_door_equations(builder, sector_list): for sector in builder.sectors + sector_list: if sector.equations is None: # todo: sort equations? - sector.equations = calc_sector_equations(sector, builder) + sector.equations = calc_sector_equations(sector) curr_list = equations[sector] = [] for equation in sector.equations: curr_list.append(equation.copy()) return equations -def calc_sector_equations(sector, builder): +def calc_sector_equations(sector): equations = [] is_entrance = sector.is_entrance_sector() and not sector.destination_entrance if is_entrance: @@ -3686,6 +3708,8 @@ def calc_door_equation(door, sector, look_for_entrance): eq.benefit[hook_from_door(door)].append(door) eq.required = True eq.c_switch = door.crystal == CrystalBarrier.Either + # exceptions for long entrances ??? + # if door.name in ['PoD Dark Alley']: eq.entrance_flag = True return eq, flag eq = DoorEquation(door) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 710767f6..282bc5db 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -3181,7 +3181,7 @@ default_connections = [('Waterfall of Wishing', 'Waterfall of Wishing'), ('Sanctuary Grave', 'Sewer Drop'), ('Sanctuary Exit', 'Light World'), - ('Old Man Cave (West)', 'Old Man Cave'), + ('Old Man Cave (West)', 'Old Man Cave Ledge'), ('Old Man Cave (East)', 'Old Man Cave'), ('Old Man Cave Exit (West)', 'Light World'), ('Old Man Cave Exit (East)', 'Death Mountain'), @@ -3327,7 +3327,7 @@ inverted_default_connections = [('Waterfall of Wishing', 'Waterfall of Wishing' ('Two Brothers House (West)', 'Two Brothers House'), ('Two Brothers House Exit (East)', 'Light World'), ('Two Brothers House Exit (West)', 'Maze Race Ledge'), - ('Sanctuary', 'Sanctuary'), + ('Sanctuary', 'Sanctuary Portal'), ('Sanctuary Grave', 'Sewer Drop'), ('Sanctuary Exit', 'Light World'), ('Old Man House (Bottom)', 'Old Man House'), @@ -3398,7 +3398,7 @@ inverted_default_connections = [('Waterfall of Wishing', 'Waterfall of Wishing' ('Old Man Cave Exit (West)', 'West Dark World'), ('Old Man Cave Exit (East)', 'Dark Death Mountain'), ('Dark Death Mountain Fairy', 'Old Man Cave'), - ('Bumper Cave (Bottom)', 'Old Man Cave'), + ('Bumper Cave (Bottom)', 'Old Man Cave Ledge'), ('Bumper Cave (Top)', 'Dark Death Mountain Healer Fairy'), ('Bumper Cave Exit (Top)', 'Death Mountain Return Ledge'), ('Bumper Cave Exit (Bottom)', 'Light World'), diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 5b690c22..a903f8ab 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -335,8 +335,9 @@ def adjust_locations_rules(key_logic, rule, accessible_loc, key_layout, key_coun test_set = None needed = rule.needed_keys_w_bk if needed > 0: - accessible_loc.update(key_counter.other_locations) - blocked_loc = key_layout.all_locations-accessible_loc + all_accessible = set(accessible_loc) + all_accessible.update(key_counter.other_locations) + blocked_loc = key_layout.all_locations-all_accessible for location in blocked_loc: if location not in key_logic.location_rules.keys(): loc_rule = LocationRule() @@ -375,8 +376,12 @@ def refine_placement_rules(key_layout, max_ctr): rules_to_remove.append(rule) if rule.bk_relevant and len(rule.check_locations_w_bk) == rule.needed_keys_w_bk + 1: new_restricted = set(max_ctr.free_locations) - rule.check_locations_w_bk - if len(new_restricted - key_logic.bk_restricted) > 0: - key_logic.bk_restricted.update(new_restricted) # bk must be in one of the check_locations + if len(new_restricted | key_logic.bk_restricted) < len(key_layout.all_chest_locations): + if len(new_restricted - key_logic.bk_restricted) > 0: + key_logic.bk_restricted.update(new_restricted) # bk must be in one of the check_locations + changed = True + else: + rules_to_remove.append(rule) changed = True if rule.needed_keys_w_bk > key_layout.max_chests or len(rule.check_locations_w_bk) < rule.needed_keys_w_bk: logging.getLogger('').warning('Invalid rule - what went wrong here??') @@ -501,6 +506,8 @@ def find_bk_locked_sections(key_layout, world, player): key_layout.all_chest_locations.update(counter.free_locations) key_layout.item_locations.update(counter.free_locations) key_layout.item_locations.update(counter.key_only_locations) + key_layout.all_locations.update(key_layout.item_locations) + key_layout.all_locations.update(counter.other_locations) if counter.big_key_opened and counter.important_location: big_chest_allowed_big_key = False if not counter.big_key_opened: diff --git a/Main.py b/Main.py index 03af476f..e675156c 100644 --- a/Main.py +++ b/Main.py @@ -17,7 +17,7 @@ from InvertedRegions import create_inverted_regions, mark_dark_world_regions from EntranceShuffle import link_entrances, link_inverted_entrances from Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, JsonRom, get_hash_string from Doors import create_doors -from DoorShuffle import link_doors, connect_portal_copy +from DoorShuffle import link_doors, connect_portal from RoomData import create_rooms from Rules import set_rules from Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive @@ -25,7 +25,7 @@ from Fill import distribute_items_cutoff, distribute_items_staleness, distribute from ItemList import generate_itempool, difficulties, fill_prizes, fill_specific_items from Utils import output_path, parse_player_names -__version__ = '0.2.0-dev' +__version__ = '0.3.0.0-u' class EnemizerError(RuntimeError): pass @@ -477,7 +477,7 @@ def copy_world(world): ret.dungeon_portals = world.dungeon_portals for player, portals in world.dungeon_portals.items(): for portal in portals: - connect_portal_copy(portal, ret, player) + connect_portal(portal, ret, player) ret.sanc_portal = world.sanc_portal for player in range(1, world.players + 1): diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 26fdf9fe..179da74b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,165 +1,16 @@ # New Features -## Lobby shuffle added as Intensity level 3 +None Yet -* Standard notes: - * The sanctuary is vanilla, and will be missing the exit door until Zelda is rescued - * In entrance shuffle the hyrule castle left and right exit door will be missing until Zelda is rescued. This - replaces the rails that used to block those lobby exits - * In non-entrance shuffle, Agahnims tower can be in logic if you have cape and/or Master sword, but you are never - required to beat Agahnim 1 until Zelda is rescued. -* Open notes: - * The Sanctuary is limited to be in a LW dungeon unless you have ER Crossed or higher enabled - * Mirroring from the Sanctuary to the new "Sanctuary" lobby is now in logic, as is exiting there. - * In ER crossed or higher, if the Sanctuary is in the Dark World, Link starts as Bunny there until the Moon Pearl - is found. Nothing inside that dungeon is in logic until the Moon Pearl is found. (Unless it is a multi-entrance - dungeon that you can access from some LW entrance) -* Lobby list is found in the spoiler -* Exits for Multi-entrance dungeons after beating bosses now makes more sense. Generally you'll exit from a entrance - from which the boss can logically be reached. If there are multiple, ones that do not lead to regions only accessible - by connector are preferred. The exit is randomly chosen if there's no obvious preference. However, In certain poor - cases like Skull Woods in ER, sometimes an exit is chosen not because you can reach the boss from there, but to - prevent a potential forced S&Q. -* Palette changes: - * Certain doors/transition no longer have an effect on the palette choice (dead ends mostly or just bridges) - * Sanctuary palette used on the adjacent rooms to Sanctuary (Sanctuary stays the dungeon color for now) - * Sewer palette comes back for part of Hyrule Castle for areas "near" the sewer dropdown - * There is a setting to keep original palettes (--standardize_palettes original) -* Known issues: - * Palettes are not perfect - * Some ugly colors - * Invisible floors can be see in many palettes - -## Key Drop Shuffle - ---keydropshuffle added. This add 33 new locations to the game where keys are found under pots -and where enemies drop keys. This includes 32 small key location and the ball and chain guard who normally drop the HC -Big Key. - -* Overall location count updated -* Setting mentioned in spoiler -* Minor change: if a key is Universal or for that dungeon, then if will use the old mechanics of picking up the key without -an entire pose and should be obtainable with the hookshot or boomerang as before - -## --mixed_travel setting -* Due to Hammerjump, Hovering in PoD Arena, and the Mire Big Key Chest bomb jump two sections of a supertile that are -otherwise unconnected logically can be reach using these glitches. To prevent the player from unintentionally - * prevent: Rails are added to the 3 spots to prevent these tricks. This setting is recommend for those learning - crossed dungeon mode to learn what is dangerous and what is not. No logic seeds ignore this setting. - * allow: The rooms are left alone and it is up to the discretion of the player whether to use these tricks or not. - * force: The two disjointed sections are forced to be in the same dungeon but performing the glitches is never - logically required to complete that game. - -## Keysanity menu redesign - -Redesign of Keysanity Menu complete for crossed dungeon and moved out of experimental. -* First screen about Big Keys and Small Keys - * 1st Column: The map is required for information about the Big Key - * If you don't have the map, it'll be blank until you obtain the Big Key - * If have the map: - * 0 indicates there is no Big Key for that dungeon - * A red symbol indicates the Ball N Chain guard has the big key for that dungeon (does not apply in - --keydropshuffle) - * Blank if there a big key but you haven't found it yet - * 2nd Column displays the current number of keys for that dungeon. Suppressed in retro (always blank) - * 3rd Column only displays if you have the map. It shows the number of keys left to collect for that dungeon. If - --keydropshuffle is off, this does not count key drops. If on, it does. - * (Note: the key columns can display up to 35 using the letters A-Z after 9) -* Second screen about Maps / Compass - * 1st Column: indicates if you have found the map or not for that dungeon - * 2nd and 3rd Column: You must have the compass to see these columns. A two-digit display that shows you how - many chests are left in the dungeon. If --keydropshuffle is off, this does not count key drops. If on, it does. - -## Potshuffle by compiling - -Same flag as before but uses python logic written by compiling instead of the enemizer logic-less version. - -## Other features - -### Spoiler log improvements - -* In crossed mode, the new dungeon is listed along with the location designated by a '@' sign -* Random gt crystals and ganon crystal are noted in the settings for better reproduction of seeds - -### Experimental features - -* Only the item counter is currently experimental - * Item counter is suppressed in Triforce Hunt - -#### Temporary debug features - -* Removed the red square in the upper right corner of the hud if the castle gate is closed +## ??? # Bug Fixes -* 2.0.20u - * Problem with Desert Wall not being pre-opened in intensity 3 fixed -* 2.0.19u - * Generation improvement - * Possible fix for shop vram corruption - * The Cane of Byrna does not count as a chest key anymore -* 2.0.18u - * Generation improvements - * Bombs/Dash doors more consistent with the amount in vanilla. -* 2.0.17u - * Generation improvements -* 2.0.16u - * Prevent HUD from showing key counter when in the overworld. (Aga 2 doesn't always clear the dungeon indicator) - * Fixed key logic regarding certain isolated "important" locations - * Fixed a problem with keydropshuffle thinking certain progression items are keys - * A couple of inverted rules fixed - * A more accurate count of which locations are blocked by teh big key in Ganon's Tower - * Updated base rom to 31.0.7 (includes potential hera basement cage fix) -* 2.0.15u - * Allow Aga Tower lobby door as a a paired keydoor (typo) - * Fix portal check for multi-entrance dungeons -* 2.0.14u - * Removal of key doors no longer messes up certain lobbies - * Fixed ER entrances when Desert Back is a connector -* 2.0.13u - * Minor portal re-work for certain logic and spoiler information - * Repaired certain exits wrongly affected by Sanctuary placement (ER crossed + intensity 3) - * Fix for inverted ER + intensity 3 - * Fix for current small keys missing on keysanity menu - * Logic added for cases where you can flood Swamp Trench 1 before finding flippers and lock yourself out of getting - something behind the trench that leads to the flippers -* 2.0.12u - * Another fix for animated tiles (fairy fountains) - * GT Big Key stat fixed on credits - * Any denomination of rupee 20 or below can be removed to make room for Crossed Dungeon's extra dungeon items. This - helps retro generate more often. - * Fix for TR Lobbies in intensity 3 and ER shuffles that was causing a hardlock - * Standard ER logic revised for lobby shuffle and rain state considerations. -* 2.0.11u - * Fix output path setting in settings.json - * Fix trock entrances when intensity <= 2 -* 2.0.10u - * Fix POD, TR, GT and SKULL 3 entrances if sanc ends up in that dungeon in crossed ER+ - * TR Lobbies that need a bomb and can be entered before bombing should be pre-opened - * Animated tiles are loaded correctly in lobbies - * If a wallmaster grabs you and the lobby is dark, the lamp turns on now - * Certain key rules no longer override item requirements (e.g. Somaria behind TR Hub) - * Old Man Cave is correctly one way in the graph - * Some key logic fixes -* 2.0.9-u - * /missing command in MultiClient fixed -* 2.0.8-u - * Player sprite disappears after picking up a key drop in keydropshuffle - * Sewers and Hyrule Castle compass problems - * Double count of the Hera Basement Cage item (both overall and compass) - * Unnecessary/inconsistent rug cutoff - * TR Crystal Maze thought you get through backwards without Somaria - * Ensure Thieves Attic Window area can always be reached - * Fixed where HC big key was not counted -* Prior fixes - * Fixed a situation where logic did not account properly for Big Key doors in standard Hyrule Castle - * Fixed a problem ER shuffle generation that did not account for lobbies moving around - * Fixed a problem with camera unlock (GT Mimics and Mire Minibridge) - * Fixed a problem with bad-pseudo layer at PoD map Balcony (unable to hit switch with Bomb) - * Fixed a problem with the Ganon hint when hints are turned off +* 0.3.0.0-u + * Generation improvements. Basic >95% success. Crossed >80% success. + * Possible increased generation times as certain generation problem tries a partial re-roll # Known Issues -* Potenial keylocks in multi-entrance dungeons -* Incorrect vanilla keylogic for Mire -* ER - Potential for Skull Woods West to be completely inaccessible in non-beatable logic \ No newline at end of file +* Potential keylocks in multi-entrance dungeons +* Incorrect vanilla key logic for Mire \ No newline at end of file diff --git a/Rom.py b/Rom.py index 7cef05f5..63cdcd46 100644 --- a/Rom.py +++ b/Rom.py @@ -699,7 +699,7 @@ def patch_rom(world, rom, player, team, enemized): for name, pair in boss_indicator.items(): dungeon_id, boss_door = pair opposite_door = world.get_door(boss_door, player).dest - if opposite_door and opposite_door.roomIndex > -1: + if opposite_door and isinstance(opposite_door, Door) and opposite_door.roomIndex > -1: dungeon_name = opposite_door.entrance.parent_region.dungeon.name dungeon_id = boss_indicator[dungeon_name][0] rom.write_byte(0x13f000+dungeon_id, opposite_door.roomIndex)