diff --git a/DoorShuffle.py b/DoorShuffle.py index 9d22893c..c70cc040 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -14,7 +14,7 @@ from RoomData import DoorKind, PairedDoor, reset_rooms 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, GenerationException -from KeyDoorShuffle import analyze_dungeon, build_key_layout, validate_key_layout +from KeyDoorShuffle import analyze_dungeon, build_key_layout, validate_key_layout, determine_prize_lock from Utils import ncr, kth_combination @@ -1358,10 +1358,8 @@ def combine_layouts(recombinant_builders, dungeon_builders, entrances_map): if recombine.master_sector is None: recombine.master_sector = builder.master_sector recombine.master_sector.name = recombine.name - recombine.pre_open_stonewalls = builder.pre_open_stonewalls else: recombine.master_sector.regions.extend(builder.master_sector.regions) - recombine.pre_open_stonewalls.update(builder.pre_open_stonewalls) recombine.layout_starts = list(entrances_map[recombine.name]) dungeon_builders[recombine.name] = recombine @@ -1465,6 +1463,7 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True start_regions = [x for x in start_regions if x not in excluded.keys()] key_layout = build_key_layout(builder, start_regions, proposal, world, player) + determine_prize_lock(key_layout, world, player) while not validate_key_layout(key_layout, world, player): itr += 1 stop_early = False @@ -2043,8 +2042,8 @@ class DROptions(Flag): Debug = 0x08 # Rails = 0x10 # Unused bit now OriginalPalettes = 0x20 - Open_PoD_Wall = 0x40 # If on, pre opens the PoD wall, no bow required - Open_Desert_Wall = 0x80 # If on, pre opens the desert wall, no fire required + # Open_PoD_Wall = 0x40 # No longer pre-opening pod wall - unused + # Open_Desert_Wall = 0x80 # No longer pre-opening desert wall - unused Hide_Total = 0x100 DarkWorld_Spawns = 0x200 diff --git a/DungeonGenerator.py b/DungeonGenerator.py index cb834877..64fd3513 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -56,24 +56,6 @@ def pre_validate(builder, entrance_region_names, split_dungeon, world, player): def generate_dungeon(builder, entrance_region_names, split_dungeon, world, player): - stonewalls = check_for_stonewalls(builder) - sector = generate_dungeon_main(builder, entrance_region_names, split_dungeon, world, player) - for stonewall in stonewalls: - if not stonewall_valid(stonewall): - builder.pre_open_stonewalls.add(stonewall) - return sector - - -def check_for_stonewalls(builder): - stonewalls = set() - for sector in builder.sectors: - for door in sector.outstanding_doors: - if door.stonewall: - stonewalls.add(door) - return stonewalls - - -def generate_dungeon_main(builder, entrance_region_names, split_dungeon, world, player): if builder.valid_proposal: # we made this earlier in gen, just use it proposed_map = builder.valid_proposal else: @@ -112,6 +94,15 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon if (access_region.name in world.inaccessible_regions[player] and region.name not in world.enabled_entrances[player]): excluded[region] = None + elif len(region.entrances) == 1: # for holes + access_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') + if access_region.name == 'Sewer Drop': + access_region = next(x.parent_region for x in access_region.entrances) + if (access_region.name in world.inaccessible_regions[player] and + region.name not in world.enabled_entrances[player]): + excluded[region] = None entrance_regions = [x for x in entrance_regions if x not in excluded.keys()] doors_to_connect = {} all_regions = set() @@ -586,7 +577,8 @@ def determine_paths_for_dungeon(world, player, all_regions, name): paths.append(boss) if 'Thieves Boss' in all_r_names: paths.append('Thieves Boss') - paths.append(('Thieves Blind\'s Cell', 'Thieves Boss')) + if world.get_dungeon("Thieves Town", player).boss.enemizer_name == 'Blind': + paths.append(('Thieves Blind\'s Cell', 'Thieves Boss')) for drop_check in drop_path_checks: if drop_check in all_r_names: paths.append((drop_check, non_hole_portals)) @@ -612,35 +604,6 @@ def winnow_hangers(hangers, hooks): hangers[hanger].remove(door) -def stonewall_valid(stonewall): - bad_door = stonewall.dest - if bad_door.blocked: - return True # great we're done with this one - loop_region = stonewall.entrance.parent_region - start_regions = [bad_door.entrance.parent_region] - if bad_door.dependents: - for dep in bad_door.dependents: - start_regions.append(dep.entrance.parent_region) - queue = deque(start_regions) - visited = set(start_regions) - while len(queue) > 0: - region = queue.popleft() - if region == loop_region: - return False # guaranteed loop - possible_entrances = list(region.entrances) - for entrance in possible_entrances: - parent = entrance.parent_region - if parent.type != RegionType.Dungeon: - return False # you can get stuck from an entrance - else: - door = entrance.door - if (door is None or (door != stonewall and not door.blocked)) and parent not in visited: - visited.add(parent) - queue.append(parent) - # we didn't find anything bad - return True - - def create_graph_piece_from_state(door, o_state, b_state, proposed_map, exception, world, player): # todo: info about dungeon events - not sure about that graph_piece = GraphPiece() @@ -1220,8 +1183,6 @@ class DungeonBuilder(object): self.path_entrances = None # used for pathing/key doors, I think self.split_flag = False - self.pre_open_stonewalls = set() # used by stonewall system - self.candidates = None self.total_keys = None self.key_doors_num = None @@ -1298,6 +1259,9 @@ 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) + if key == 'Thieves Town' and world.get_dungeon("Thieves Town", player).boss.enemizer_name == 'Blind': + assign_sector(find_sector("Thieves Blind's Cell", candidate_sectors), current_dungeon, + candidate_sectors, global_pole) entrances_map, potentials, connections = connections_tuple accessible_sectors, reverse_d_map = set(), {} for key in dungeon_entrances.keys(): @@ -3921,7 +3885,7 @@ dungeon_boss_sectors = { 'Palace of Darkness': ['PoD Boss'], 'Swamp Palace': ['Swamp Boss'], 'Skull Woods': ['Skull Boss'], - 'Thieves Town': ['Thieves Blind\'s Cell', 'Thieves Boss'], + 'Thieves Town': ['Thieves Boss'], 'Ice Palace': ['Ice Boss'], 'Misery Mire': ['Mire Boss'], 'Turtle Rock': ['TR Boss'], diff --git a/ItemList.py b/ItemList.py index eda7291f..c0f2f86c 100644 --- a/ItemList.py +++ b/ItemList.py @@ -4,7 +4,6 @@ import math import RaceRandom as random from BaseClasses import Region, RegionType, Shop, ShopType, Location, CollectionState -from Bosses import place_bosses from Dungeons import get_dungeon_item_pool from EntranceShuffle import connect_entrance from Regions import shop_to_location_table, retro_shops, shop_table_by_location @@ -371,7 +370,6 @@ def generate_itempool(world, player): tr_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)] world.required_medallions[player] = (mm_medallion, tr_medallion) - place_bosses(world, player) set_up_shops(world, player) if world.retro[player]: diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index f8982653..d35a3ce7 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -28,6 +28,7 @@ class KeyLayout(object): self.found_doors = set() self.prize_relevant = None + self.prize_can_lock = None # if true, then you may need to beat the bo # bk special? # bk required? True if big chests or big doors exists @@ -1438,7 +1439,10 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa found_forced_bk = state.found_forced_bk() smalls_done = not smalls_avail or not enough_small_locations(state, available_small_locations) bk_done = state.big_key_opened or num_bigs == 0 or (available_big_locations == 0 and not found_forced_bk) - prize_done = not key_layout.prize_relevant or state.prize_doors_opened + # prize door should not be opened if the boss is reachable - but not reached yet + allow_for_prize_lock = (key_layout.prize_can_lock and + not any(x for x in state.found_locations if '- Prize' in x.name)) + prize_done = not key_layout.prize_relevant or state.prize_doors_opened or allow_for_prize_lock if smalls_done and bk_done and prize_done: return False else: @@ -1522,6 +1526,39 @@ def enough_small_locations(state, avail_small_loc): return avail_small_loc >= len(unique_d_set) +def determine_prize_lock(key_layout, world, player): + if ((world.retro[player] and (world.mode[player] != 'standard' or key_layout.sector.name != 'Hyrule Castle')) + or world.logic[player] == 'nologic'): + return # done, doesn't matter what + flat_proposal = key_layout.flat_prop + state = ExplorationState(dungeon=key_layout.sector.name) + state.key_locations = key_layout.max_chests + state.big_key_special = check_bk_special(key_layout.sector.regions, world, player) + prize_lock_possible = False + for region in key_layout.start_regions: + dungeon_entrance, portal_door = find_outside_connection(region) + prize_relevant_flag = prize_relevance(key_layout, dungeon_entrance) + if prize_relevant_flag: + state.append_door_to_list(portal_door, state.prize_doors) + state.prize_door_set[portal_door] = dungeon_entrance + key_layout.prize_relevant = prize_relevant_flag + prize_lock_possible = True + else: + state.visit_region(region, key_checks=True) + state.add_all_doors_check_keys(region, flat_proposal, world, player) + if not prize_lock_possible: + return # done, no prize entrances to worry about + expand_key_state(state, flat_proposal, world, player) + while len(state.small_doors) > 0 or len(state.big_doors) > 0: + if len(state.big_doors) > 0: + open_a_door(state.big_doors[0].door, state, flat_proposal, world, player) + elif len(state.small_doors) > 0: + open_a_door(state.small_doors[0].door, state, flat_proposal, world, player) + expand_key_state(state, flat_proposal, world, player) + if any(x for x in state.found_locations if '- Prize' in x.name): + key_layout.prize_can_lock = True + + def cnt_avail_small_locations(free_locations, key_only, state, world, player): if not world.keyshuffle[player] and not world.retro[player]: bk_adj = 1 if state.big_key_opened and not state.big_key_special else 0 diff --git a/Main.py b/Main.py index 045cb9c2..aa22fc7b 100644 --- a/Main.py +++ b/Main.py @@ -10,6 +10,7 @@ import time import zlib from BaseClasses import World, CollectionState, Item, Region, Location, Shop, Entrance, Settings +from Bosses import place_bosses from Items import ItemFactory from KeyDoorShuffle import validate_key_placement from OverworldGlitchRules import create_owg_connections @@ -30,7 +31,7 @@ from Utils import output_path, parse_player_names from source.item.FillUtil import create_item_pool_config -__version__ = '0.5.1.1-u' +__version__ = '1.0.0.1-u' from source.classes.BabelFish import BabelFish @@ -149,6 +150,7 @@ def main(args, seed=None, fish=None): create_rooms(world, player) create_dungeons(world, player) adjust_locations(world, player) + place_bosses(world, player) create_item_pool_config(world) if any(world.potshuffle.values()): diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4d212275..55a34c3f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -15,6 +15,14 @@ CLI: ```--bombbag``` # Bug Fixes and Notes. +* 0.5.1.2 + * Allowed Blind's Cell to be shuffled anywhere if Blind is not the boss of Thieves Town + * Remove unique annotation from a FastEnum that was causing problems + * Updated prevent mixed_travel setting to prevent more mixed travel + * Prevent key door loops on the same supertile where you could have spent 2 keys on one logical door + * Promoted dynamic soft-lock prevention on "stonewalls" from experimental to be the primary prevention (Stonewalls are now never pre-opened) + * Fix to money balancing algorithm with small item_pool, thanks Catobat + * Many fixes and refinements to key logic and generation * 0.5.1.1 * Shop hints in ER are now more generic instead of using "near X" because they aren't near that anymore * Added memory location for mutliworld scripts to read what item was just obtain (longer than one frame) diff --git a/Rom.py b/Rom.py index d6248488..22f017e3 100644 --- a/Rom.py +++ b/Rom.py @@ -32,7 +32,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'ef6e3e1aa59838c01dbd5b1b2387e70c' +RANDOMIZERBASEHASH = '11f4f494e999a919aafd7d2624e67679' class JsonRom(object): @@ -754,13 +754,6 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_bytes(paired_door.address_a(world, player), paired_door.rom_data_a(world, player)) rom.write_bytes(paired_door.address_b(world, player), paired_door.rom_data_b(world, player)) if world.doorShuffle[player] != 'vanilla': - if not world.experimental[player]: - for builder in world.dungeon_layouts[player].values(): - for stonewall in builder.pre_open_stonewalls: - if stonewall.name == 'Desert Wall Slide NW': - dr_flags |= DROptions.Open_Desert_Wall - elif stonewall.name == 'PoD Bow Statue Down Ladder': - dr_flags |= DROptions.Open_PoD_Wall for name, pair in boss_indicator.items(): dungeon_id, boss_door = pair opposite_door = world.get_door(boss_door, player).dest diff --git a/Rules.py b/Rules.py index c0676fd7..8130809f 100644 --- a/Rules.py +++ b/Rules.py @@ -1973,7 +1973,7 @@ def add_key_logic_rules(world, player): big_chest = world.get_location(chest.name, player) add_rule(big_chest, create_rule(d_logic.bk_name, player)) if len(d_logic.bk_doors) == 0 and len(d_logic.bk_chests) <= 1: - set_always_allow(big_chest, lambda state, item: item.name == d_logic.bk_name and item.player == player) + set_always_allow(big_chest, allow_big_key_in_big_chest(d_logic.bk_name, player)) if world.retro[player]: for d_name, layout in world.key_layout[player].items(): for door in layout.flat_prop: @@ -2011,6 +2011,10 @@ def eval_small_key_door(door_name, dungeon, player): return lambda state: eval_small_key_door_main(state, door_name, dungeon, player) +def allow_big_key_in_big_chest(bk_name, player): + return lambda state, item: item.name == bk_name and item.player == player + + def retro_in_hc(spot): return spot.parent_region.dungeon.name == 'Hyrule Castle' if spot.parent_region.dungeon else False diff --git a/asm/overrides.asm b/asm/overrides.asm index f9842866..a041ae30 100644 --- a/asm/overrides.asm +++ b/asm/overrides.asm @@ -35,11 +35,7 @@ rtl OnFileLoadOverride: jsl OnFileLoad ; what I wrote over - lda.l DRFlags : and #$80 : beq + ;flag is off - lda $7ef086 : ora #$80 : sta $7ef086 - + lda.l DRFlags : and #$40 : beq + ;flag is off - lda $7ef036 : ora #$80 : sta $7ef036 - + lda.l DRFlags : and #$02 : beq + + + lda.l DRFlags : and #$02 : beq + ; Mirror Scroll lda $7ef353 : bne + lda #$01 : sta $7ef353 + rtl diff --git a/data/base2current.bps b/data/base2current.bps index 17b3e497..46dee4be 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ