From 579c3bfd2311322def538a6986325a3189516d20 Mon Sep 17 00:00:00 2001 From: Catobat <69204835+Catobat@users.noreply.github.com> Date: Fri, 6 Aug 2021 14:17:24 +0200 Subject: [PATCH 01/15] Fix money balancing with reduced item pool --- BaseClasses.py | 2 ++ Fill.py | 21 +++++++++++---------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index b7a80af7..19cc8def 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -808,6 +808,8 @@ class CollectionState(object): def collect(self, item, event=False, location=None): if location: self.locations_checked.add(location) + if not item: + return changed = False if item.name.startswith('Progressive '): if 'Sword' in item.name: diff --git a/Fill.py b/Fill.py index 7572b350..6436a7ee 100644 --- a/Fill.py +++ b/Fill.py @@ -553,7 +553,7 @@ def balance_money_progression(world): base_value = sum(rupee_rooms.values()) available_money = {player: base_value for player in range(1, world.players+1)} for loc in world.get_locations(): - if loc.item.name in rupee_chart: + if loc.item and loc.item.name in rupee_chart: available_money[loc.item.player] += rupee_chart[loc.item.name] total_price = {player: 0 for player in range(1, world.players+1)} @@ -618,7 +618,7 @@ def balance_money_progression(world): slot = shop_to_location_table[location.parent_region.name].index(location.name) shop = location.parent_region.shop shop_item = shop.inventory[slot] - if interesting_item(location, location.item, world, location.item.player): + if location.item and interesting_item(location, location.item, world, location.item.player): if location.item.name.startswith('Rupee') and loc_player == location.item.player: if shop_item['price'] < rupee_chart[location.item.name]: wallet[loc_player] -= shop_item['price'] # will get picked up in the location_free block @@ -638,14 +638,15 @@ def balance_money_progression(world): if location_free: state.collect(location.item, True, location) unchecked_locations.remove(location) - if location.item.name.startswith('Rupee'): - wallet[location.item.player] += rupee_chart[location.item.name] - if location.item.name != 'Rupees (300)': + if location.item: + if location.item.name.startswith('Rupee'): + wallet[location.item.player] += rupee_chart[location.item.name] + if location.item.name != 'Rupees (300)': + balance_locations[location.item.player].add(location) + if interesting_item(location, location.item, world, location.item.player): + checked_locations.append(location) + elif location.item.name in acceptable_balancers: balance_locations[location.item.player].add(location) - if interesting_item(location, location.item, world, location.item.player): - checked_locations.append(location) - elif location.item.name in acceptable_balancers: - balance_locations[location.item.player].add(location) for room, income in rupee_rooms.items(): for player in range(1, world.players+1): if room not in rooms_visited[player] and world.get_region(room, player) in state.reachable_regions[player]: @@ -710,5 +711,5 @@ def balance_money_progression(world): else: state.collect(location.item, True, location) unchecked_locations.remove(location) - if location.item.name.startswith('Rupee'): + if location.item and location.item.name.startswith('Rupee'): wallet[location.item.player] += rupee_chart[location.item.name] From 5614dea2b51703f9bdd844930ed9638e6e494e25 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 26 Aug 2021 15:21:10 -0600 Subject: [PATCH 02/15] Fix can_beat_game error Add start_region awareness to door finder combinations Added dungeon table --- BaseClasses.py | 2 -- DoorShuffle.py | 8 ++++++++ Dungeons.py | 44 ++++++++++++++++++++++++++++++++------------ KeyDoorShuffle.py | 13 +++++++------ 4 files changed, 47 insertions(+), 20 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index b9b3dd1c..f92203d9 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -438,8 +438,6 @@ class World(object): return True state = starting_state.copy() else: - if self.has_beaten_game(self.state): - return True state = CollectionState(self) if self.has_beaten_game(state): diff --git a/DoorShuffle.py b/DoorShuffle.py index 3ed21895..ca377c65 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1456,6 +1456,14 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True random.shuffle(sample_list) proposal = kth_combination(sample_list[itr], builder.candidates, builder.key_doors_num) + # eliminate start region if portal marked as destination + excluded = {} + for region in start_regions: + portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None) + if portal and portal.destination: + excluded[region] = None + 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) while not validate_key_layout(key_layout, world, player): itr += 1 diff --git a/Dungeons.py b/Dungeons.py index 73f53794..6fe38cfb 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -375,6 +375,38 @@ flexible_starts = { 'Skull Woods': ['Skull Left Drop', 'Skull Pinball'] } + +class DungeonInfo: + + def __init__(self, free, keys, bk, map, compass, bk_drop, drops, prize=None): + # todo reduce static maps ideas: prize, bk_name, sm_name, cmp_name, map_name): + self.free_items = free + self.key_num = keys + self.bk_present = bk + self.map_present = map + self.compass_present = compass + self.bk_drops = bk_drop + self.key_drops = drops + self.prize = prize + + +dungeon_table = { + 'Hyrule Castle': DungeonInfo(6, 1, False, True, False, True, 3, None), + 'Eastern Palace': DungeonInfo(3, 0, True, True, True, False, 2, 'Eastern Palace - Prize'), + 'Desert Palace': DungeonInfo(2, 1, True, True, True, False, 3, 'Desert Palace - Prize'), + 'Tower of Hera': DungeonInfo(2, 1, True, True, True, False, 0, 'Tower of Hera - Prize'), + 'Agahnims Tower': DungeonInfo(0, 2, False, False, False, False, 2, None), + 'Palace of Darkness': DungeonInfo(5, 6, True, True, True, False, 0, 'Palace of Darkness - Prize'), + 'Swamp Palace': DungeonInfo(6, 1, True, True, True, False, 5, 'Swamp Palace - Prize'), + 'Skull Woods': DungeonInfo(2, 3, True, True, True, False, 2, 'Skull Woods - Prize'), + 'Thieves Town': DungeonInfo(4, 1, True, True, True, False, 2, "Thieves' Town - Prize"), + 'Ice Palace': DungeonInfo(3, 2, True, True, True, False, 4, 'Ice Palace - Prize'), + 'Misery Mire': DungeonInfo(2, 3, True, True, True, False, 3, 'Misery Mire - Prize'), + 'Turtle Rock': DungeonInfo(5, 4, True, True, True, False, 2, 'Turtle Rock - Prize'), + 'Ganons Tower': DungeonInfo(20, 4, True, True, True, False, 4, None), +} + + dungeon_keys = { 'Hyrule Castle': 'Small Key (Escape)', 'Eastern Palace': 'Small Key (Eastern Palace)', @@ -407,18 +439,6 @@ dungeon_bigs = { 'Ganons Tower': 'Big Key (Ganons Tower)' } -dungeon_prize = { - 'Eastern Palace': 'Eastern Palace - Prize', - 'Desert Palace': 'Desert Palace - Prize', - 'Tower of Hera': 'Tower of Hera - Prize', - 'Palace of Darkness': 'Palace of Darkness - Prize', - 'Swamp Palace': 'Swamp Palace - Prize', - 'Skull Woods': 'Skull Woods - Prize', - 'Thieves Town': 'Thieves Town - Prize', - 'Ice Palace': 'Ice Palace - Prize', - 'Misery Mire': 'Misery Mire - Prize', - 'Turtle Rock': 'Turtle Rock - Prize', -} dungeon_hints = { 'Hyrule Castle': 'in Hyrule Castle', diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index cf18f4f4..c5aab0fc 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -4,8 +4,9 @@ from collections import defaultdict, deque from BaseClasses import DoorType, dungeon_keys, KeyRuleType, RegionType from Regions import dungeon_events -from Dungeons import dungeon_keys, dungeon_bigs, dungeon_prize -from DungeonGenerator import ExplorationState, special_big_key_doors +from Dungeons import dungeon_keys, dungeon_bigs, dungeon_table +from DungeonGenerator import ExplorationState, special_big_key_doors, count_locations_exclude_big_chest, prize_or_event +from DungeonGenerator import reserved_location, blind_boss_unavail class KeyLayout(object): @@ -1387,7 +1388,7 @@ def validate_key_layout(key_layout, world, player): dungeon_entrance, portal_door = find_outside_connection(region) if (len(key_layout.start_regions) > 1 and dungeon_entrance and dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy'] - and key_layout.key_logic.dungeon in dungeon_prize): + and dungeon_table[key_layout.key_logic.dungeon].prize): state.append_door_to_list(portal_door, state.prize_doors) state.prize_door_set[portal_door] = dungeon_entrance key_layout.prize_relevant = True @@ -1550,7 +1551,7 @@ def create_key_counters(key_layout, world, player): dungeon_entrance, portal_door = find_outside_connection(region) if (len(key_layout.start_regions) > 1 and dungeon_entrance and dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy'] - and key_layout.key_logic.dungeon in dungeon_prize): + and dungeon_table[key_layout.key_logic.dungeon].prize): state.append_door_to_list(portal_door, state.prize_doors) state.prize_door_set[portal_door] = dungeon_entrance key_layout.prize_relevant = True @@ -1975,8 +1976,8 @@ def validate_key_placement(key_layout, world, player): len(counter.key_only_locations) + keys_outside if key_layout.prize_relevant: found_prize = any(x for x in counter.important_locations if '- Prize' in x.name) - if not found_prize and key_layout.sector.name in dungeon_prize: - prize_loc = world.get_location(dungeon_prize[key_layout.sector.name], player) + if not found_prize and dungeon_table[key_layout.sector.name].prize: + prize_loc = world.get_location(dungeon_table[key_layout.sector.name].prize, player) # todo: pyramid fairy only care about crystals 5 & 6 found_prize = 'Crystal' not in prize_loc.item.name else: From 1f884649d1dd9bcf1d735f74e639f91e202645f3 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 26 Aug 2021 15:32:35 -0600 Subject: [PATCH 03/15] Remove unnecessary imports --- KeyDoorShuffle.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index c5aab0fc..be4f03a2 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -5,8 +5,7 @@ from collections import defaultdict, deque from BaseClasses import DoorType, dungeon_keys, KeyRuleType, RegionType from Regions import dungeon_events from Dungeons import dungeon_keys, dungeon_bigs, dungeon_table -from DungeonGenerator import ExplorationState, special_big_key_doors, count_locations_exclude_big_chest, prize_or_event -from DungeonGenerator import reserved_location, blind_boss_unavail +from DungeonGenerator import ExplorationState, special_big_key_doors class KeyLayout(object): From 759e9979da18e5038a63ffe7d3f5e70013709aa2 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 26 Aug 2021 15:51:12 -0600 Subject: [PATCH 04/15] Rule fix for bigkey shuffle, use the always_allow for the big key, but require it for every other item --- Rules.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Rules.py b/Rules.py index 28d627da..3114bde0 100644 --- a/Rules.py +++ b/Rules.py @@ -1950,9 +1950,11 @@ def add_key_logic_rules(world, player): forbid_item(location, d_logic.small_key_name, player) for door in d_logic.bk_doors: add_rule(world.get_entrance(door.name, player), create_rule(d_logic.bk_name, player)) - if len(d_logic.bk_doors) > 0 or len(d_logic.bk_chests) > 1: - for chest in d_logic.bk_chests: - add_rule(world.get_location(chest.name, player), create_rule(d_logic.bk_name, player)) + for chest in d_logic.bk_chests: + 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) if world.retro[player]: for d_name, layout in world.key_layout[player].items(): for door in layout.flat_prop: From 9e7223795f7205edede0354e9dba754e3424aade Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 27 Aug 2021 15:03:27 -0600 Subject: [PATCH 05/15] Fix for path checking. should get pinball more often --- DoorShuffle.py | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index ca377c65..39d6283f 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1915,14 +1915,18 @@ def check_required_paths(paths, world, player): if dungeon_name in world.dungeon_layouts[player].keys(): builder = world.dungeon_layouts[player][dungeon_name] if len(paths[dungeon_name]) > 0: - states_to_explore = defaultdict(list) + states_to_explore = {} for path in paths[dungeon_name]: if type(path) is tuple: - states_to_explore[tuple([path[0]])] = path[1] + states_to_explore[tuple([path[0]])] = (path[1], 'any') else: - states_to_explore[tuple(builder.path_entrances)].append(path) + common_starts = tuple(builder.path_entrances) + if common_starts not in states_to_explore: + states_to_explore[common_starts] = ([], 'all') + states_to_explore[common_starts][0].append(path) cached_initial_state = None - for start_regs, dest_regs in states_to_explore.items(): + for start_regs, info in states_to_explore.items(): + dest_regs, path_type = info if type(dest_regs) is not list: dest_regs = [dest_regs] check_paths = convert_regions(dest_regs, world, player) @@ -1939,11 +1943,17 @@ def check_required_paths(paths, world, player): cached_initial_state = state else: state = cached_initial_state - valid, bad_region = check_if_regions_visited(state, check_paths) + if path_type == 'any': + valid, bad_region = check_if_any_regions_visited(state, check_paths) + else: + valid, bad_region = check_if_all_regions_visited(state, check_paths) if not valid: if check_for_pinball_fix(state, bad_region, world, player): explore_state(state, world, player) - valid, bad_region = check_if_regions_visited(state, check_paths) + if path_type == 'any': + valid, bad_region = check_if_any_regions_visited(state, check_paths) + else: + valid, bad_region = check_if_all_regions_visited(state, check_paths) if not valid: raise Exception('%s cannot reach %s' % (dungeon_name, bad_region.name)) @@ -1983,7 +1993,7 @@ def explore_state_not_inaccessible(state, world, player): state.add_all_doors_check_unattached(connect_region, world, player) -def check_if_regions_visited(state, check_paths): +def check_if_any_regions_visited(state, check_paths): valid = False breaking_region = None for region_target in check_paths: @@ -1995,6 +2005,13 @@ def check_if_regions_visited(state, check_paths): return valid, breaking_region +def check_if_all_regions_visited(state, check_paths): + for region_target in check_paths: + if not state.visited_at_all(region_target): + return False, region_target + return True, None + + def check_for_pinball_fix(state, bad_region, world, player): pinball_region = world.get_region('Skull Pinball', player) # todo: lobby shuffle From 91fbcd38dad5f700fc73740a03cfa49bd86f7e4e Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 27 Aug 2021 15:13:37 -0600 Subject: [PATCH 06/15] Fix shop hints in ER modes where shops are not located vanilla --- ItemList.py | 10 ++++++++++ Regions.py | 6 +++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/ItemList.py b/ItemList.py index 5f5b5e7e..eda7291f 100644 --- a/ItemList.py +++ b/ItemList.py @@ -604,6 +604,7 @@ def customize_shops(world, player): upgrade.location = loc change_shop_items_to_rupees(world, player, shops_to_customize) balance_prices(world, player) + check_hints(world, player) def randomize_price(price): @@ -707,6 +708,15 @@ def balance_prices(world, player): # loc.parent_region.shop.inventory[slot]['price'] = new_price +def check_hints(world, player): + if world.shuffle[player] in ['simple', 'restricted', 'full', 'crossed', 'insanity']: + for shop, location_list in shop_to_location_table.items(): + if shop in ['Capacity Upgrade', 'Light World Death Mountain Shop', 'Potion Shop']: + continue # near the queen, near potions, and near 7 chests are fine + for loc_name in location_list: # other shops are indistinguishable in ER + world.get_location(loc_name, player).hint_text = f'for sale' + + repeatable_shop_items = ['Single Arrow', 'Arrows (10)', 'Bombs (3)', 'Bombs (10)', 'Red Potion', 'Small Heart', 'Blue Shield', 'Red Shield', 'Bee', 'Small Key (Universal)', 'Blue Potion', 'Green Potion'] diff --git a/Regions.py b/Regions.py index 35a7eda3..546cd49b 100644 --- a/Regions.py +++ b/Regions.py @@ -1374,9 +1374,9 @@ location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'), 'Red Shield Shop - Left': (None, None, False, 'for sale as a curiosity'), 'Red Shield Shop - Middle': (None, None, False, 'for sale as a curiosity'), 'Red Shield Shop - Right': (None, None, False, 'for sale as a curiosity'), - 'Potion Shop - Left': (None, None, False, 'for sale near the witch'), - 'Potion Shop - Middle': (None, None, False, 'for sale near the witch'), - 'Potion Shop - Right': (None, None, False, 'for sale near the witch'), + 'Potion Shop - Left': (None, None, False, 'for sale near potions'), + 'Potion Shop - Middle': (None, None, False, 'for sale near potions'), + 'Potion Shop - Right': (None, None, False, 'for sale near potions'), } lookup_id_to_name = {data[0]: name for name, data in location_table.items() if type(data[0]) == int} From 67c4fee636b4fafb661aa612e5d8454e2b70db60 Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 30 Aug 2021 15:20:27 -0600 Subject: [PATCH 07/15] Boss shuffle fix - fixes some bias discovered by krebel --- Bosses.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Bosses.py b/Bosses.py index 2718431e..90acafcf 100644 --- a/Bosses.py +++ b/Bosses.py @@ -181,11 +181,11 @@ def place_bosses(world, player): logging.getLogger('').debug('Bosses chosen %s', bosses) - random.shuffle(bosses) for [loc, level] in boss_locations: loc_text = loc + (' ('+level+')' if level else '') - boss = next((b for b in bosses if can_place_boss(world, player, b, loc, level)), None) - if not boss: + try: + boss = random.choice([b for b in bosses if can_place_boss(world, player, b, loc, level)]) + except IndexError: raise FillError('Could not place boss for location %s' % loc_text) bosses.remove(boss) From 07287d85a765440e539f0e741ba937de6f4b8fe4 Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 30 Aug 2021 15:21:25 -0600 Subject: [PATCH 08/15] Improve exclusion calculation --- BaseClasses.py | 1 + DoorShuffle.py | 2 +- DungeonGenerator.py | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index f92203d9..820b2fed 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -77,6 +77,7 @@ class World(object): self._room_cache = {} self.dungeon_layouts = {} self.inaccessible_regions = {} + self.enabled_entrances = {} self.key_logic = {} self.pool_adjustment = {} self.key_layout = defaultdict(dict) diff --git a/DoorShuffle.py b/DoorShuffle.py index 39d6283f..e89a7bb4 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -753,7 +753,7 @@ def handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map, def main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, world, player): entrances_map, potentials, connections = connections_tuple - enabled_entrances = {} + enabled_entrances = world.enabled_entrances[player] = {} sector_queue = deque(dungeon_builders.values()) last_key, loops = None, 0 logging.getLogger('').info(world.fish.translate("cli", "cli", "generating.dungeon")) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 948115f3..f579f285 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -109,7 +109,8 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon p_region = portal.door.entrance.connected_region access_region = next(x.parent_region for x in p_region.entrances if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]) - if access_region.name in world.inaccessible_regions[player]: + 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 = {} From dad6016498ea22ae9f5591c24498a8caa8e5e39d Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 30 Aug 2021 15:57:45 -0600 Subject: [PATCH 09/15] Rom features: update cutofftable Add a multiworld ram slot for item that just got grabbed --- Rom.py | 2 +- asm/doortables.asm | 2 ++ data/base2current.bps | Bin 136271 -> 136284 bytes 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index d39675db..e2eeb5f1 100644 --- a/Rom.py +++ b/Rom.py @@ -32,7 +32,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '988f1546b14d8f2e6ee30b9de44882da' +RANDOMIZERBASEHASH = 'f0947723c296ce13c7f4c909d8318042' class JsonRom(object): diff --git a/asm/doortables.asm b/asm/doortables.asm index 8ee0dd60..eae16430 100644 --- a/asm/doortables.asm +++ b/asm/doortables.asm @@ -582,6 +582,8 @@ dw $00bc, $00a2, $00a3, $00c2, $001a, $0049, $0014, $008c dw $009f, $0066, $005d, $00a8, $00a9, $00aa, $00b9, $0052 ; HC West Hall, TR Dash Bridge, TR Hub, Pod Arena, GT Petting Zoo, Ice Spike Cross dw $0050, $00c5, $00c6, $0009, $0003, $002a, $007d, $005e +; Sewer Drop, Mire Cross, GT Crystal Circles +dw $0011, $00b2, $003d dw $ffff ; dungeon tables diff --git a/data/base2current.bps b/data/base2current.bps index 5f41171067dccc266f98a3c0b2daee5a07a03f06..ddbb10748c14ecc4470df7ce1e0920cd009f2c01 100644 GIT binary patch delta 668 zcmV;N0%QHps0iGs2(Ur{1PKWC_p?R;Dn0>ClW{*T1dp4*ev_X+H8!z_$xhG?uY2=; z@CB()n*<>CiC)lFlOuq!6t7wHe$SAAFpvtbk)H;DlS^&yk^j&LsY#nOlL2mjwY)lPpeIHaPx?{m>kfAAo@qqgnZZ z36Fx4Ncowm1Z&Cu&^((OfS)J+fenv>lP3O~CH|EIsY7eY0GDEuyG|Smb^EKiffbK} zmtK?MP8b|B`+*OSf|oG*pD+5Y1F0!%sVZx021G?h6M-g^6;CfA6_bG#qd5JWjFGgb zplfKCu7$yBbx;cfQ=bAfK|W3 zlR^xDkw1Vu$zZ=@3xJabvwKhh0}T%ufSCc00Z@N{2AdkQiBkp@2TK)!fq;_}vzBNL z1OYdbq--w%7PHiB5(*aySAo6o1jz!h323PdpaEPN;st0ofDE(!cGEWnaU+kHHj~w^ z@BvAa#;-2{9JBVX6etlXF@dQVZzc48k2g?%uO^>Xvm4k8B>_>BC;cuG0A2tQ0I~o* zFNP`;m3#I9Pl1zN{UQQiKC^-SgMtl#_nRJDmWB8Kn=g2lw-bN?G?WP6-~iF^d{Tlm Cr4)St delta 632 zcmWO3Jxmi}90qXT_IegdOQ00WmnD2D2v3 z4Fi_)*Hm&b#W2WO@i1pX90;J(#eqv)l<4AM;$#zu;lrMG{XNh0Jl~eITT9v%H=@>b z?p@J~>K*6|j5`@vbhflCKazT7gK&2zoYL?a5r4HkwJC z>#PogLBxKf))y7yc@#zdDNJ*f;^RKrbhOVz2nL Date: Tue, 31 Aug 2021 14:10:47 -0600 Subject: [PATCH 10/15] Minor rom fix --- Rom.py | 2 +- data/base2current.bps | Bin 136284 -> 136284 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index e2eeb5f1..53d6666e 100644 --- a/Rom.py +++ b/Rom.py @@ -32,7 +32,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'f0947723c296ce13c7f4c909d8318042' +RANDOMIZERBASEHASH = '091671c7f555331ff2f411b304141c29' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index ddbb10748c14ecc4470df7ce1e0920cd009f2c01..4571d623111492b36542f9572a826fd4ecbbef1d 100644 GIT binary patch delta 57 zcmV-90LK5^s0iGs2(U5D0acSS%_JAEJAg34An*#WX!CwQslBA|7K)b}I{Sld&9`mM P0p~^tEZfI>QeH Date: Tue, 31 Aug 2021 15:36:38 -0600 Subject: [PATCH 11/15] Couple of big key logic fixes - one with bk forced Another with bk assumed to be found early in dungeon Catobat map check fix from rom --- BaseClasses.py | 1 + KeyDoorShuffle.py | 15 +++++++++++++-- Main.py | 2 +- RELEASENOTES.md | 11 +++++++++++ 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 820b2fed..b5462eb4 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -142,6 +142,7 @@ class World(object): set_player_attr('force_fix', {'gt': False, 'sw': False, 'pod': False, 'tr': False}) set_player_attr('exp_cache', defaultdict(dict)) + set_player_attr('enabled_entrances', {}) def get_name_string_for_object(self, obj): return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})' diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index be4f03a2..23dd24be 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -282,6 +282,12 @@ def analyze_dungeon(key_layout, world, player): if not big_avail: if chest_keys == non_big_locs and chest_keys > 0 and available <= possible_smalls and not avail_bigs: key_logic.bk_restricted.update(filter_big_chest(key_counter.free_locations)) + # note to self: this is due to the enough_small_locations function in validate_key_layout_sub_loop + # I don't like this exception here or there + elif available <= possible_smalls and avail_bigs and non_big_locs > 0: + max_ctr = find_max_counter(key_layout) + bk_lockdown = [x for x in max_ctr.free_locations if x not in key_counter.free_locations] + key_logic.bk_restricted.update(filter_big_chest(bk_lockdown)) # try to relax the rules here? - smallest requirement that doesn't force a softlock child_queue = deque() for child in key_counter.child_doors.keys(): @@ -1619,8 +1625,13 @@ def can_open_door_by_counter(door, counter: KeyCounter, layout, world, player): # ttl_small_key_only = len(counter.key_only_locations) return cnt_avail_small_locations_by_ctr(ttl_locations, counter, layout, world, player) > 0 elif door.bigKey: - available_big_locations = cnt_avail_big_locations_by_ctr(ttl_locations, counter, layout, world, player) - return not counter.big_key_opened and available_big_locations > 0 and not layout.big_key_special + if counter.big_key_opened: + return False + if layout.big_key_special: + return any(x for x in counter.other_locations.keys() if x.forced_item and x.forced_item.bigkey) + else: + available_big_locations = cnt_avail_big_locations_by_ctr(ttl_locations, counter, layout, world, player) + return available_big_locations > 0 else: return True diff --git a/Main.py b/Main.py index ee9e83fb..be2cb2e6 100644 --- a/Main.py +++ b/Main.py @@ -28,7 +28,7 @@ from Fill import sell_potions, sell_keys, balance_multiworld_progression, balanc from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops from Utils import output_path, parse_player_names -__version__ = '0.5.1.0-u' +__version__ = '0.5.1.1-u' from source.classes.BabelFish import BabelFish diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 37877c89..4d212275 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -15,6 +15,17 @@ CLI: ```--bombbag``` # Bug Fixes and Notes. +* 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) + * Fix for bias in boss shuffle "full" + * Fix for certain lone big chests in keysanity (allowed you to get contents without big key) + * Fix for pinball checking + * Fix for multi-entrance dungeons + * 2 fixes for big key placement logic + * ensure big key is placed early if the validator assumes it) + * Open big key doors appropriately when generating rules and big key is forced somewhere + * Updated cutoff entrances for intensity 3 * 0.5.1.0 * Large logic refactor introducing a new method of key logic * Some performance optimization From 2760841836d793543cdbf097bd6611b3248548bc Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 2 Sep 2021 17:00:36 -0600 Subject: [PATCH 12/15] Prevent keys doors on door pairs that loop on themselves in the same supertile. (Excludes dead ends) Thus, there's not chance for a keys to be wasted. --- DoorShuffle.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index e89a7bb4..2574e4f5 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1582,7 +1582,7 @@ def find_key_door_candidates(region, checked, world, player): if d2.type == DoorType.Normal: room_b = world.get_room(d2.roomIndex, player) pos_b, kind_b = room_b.doorList[d2.doorListPos] - valid = kind in okay_normals and kind_b in okay_normals + valid = kind in okay_normals and kind_b in okay_normals and valid_key_door_pair(d, d2) else: valid = kind in okay_normals if valid and 0 <= d2.doorListPos < 4: @@ -1599,6 +1599,12 @@ def find_key_door_candidates(region, checked, world, player): return candidates, checked_doors +def valid_key_door_pair(door1, door2): + if door1.roomIndex != door2.roomIndex: + return True + return len(door1.entrance.parent_region.exits) <= 1 or len(door2.entrance.parent_region.exits) <= 1 + + def reassign_key_doors(builder, world, player): logger = logging.getLogger('') logger.debug('Key doors for %s', builder.name) From e48dbe3d27c4f0417293f2fb8f8df2b925dc15a5 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 2 Sep 2021 18:10:49 -0600 Subject: [PATCH 13/15] Prize relevance refinement Subtle change on bk restriction - only restrict if bk was determined to be the way forward --- KeyDoorShuffle.py | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 23dd24be..1ff840f6 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -26,7 +26,7 @@ class KeyLayout(object): self.item_locations = set() self.found_doors = set() - self.prize_relevant = False + self.prize_relevant = None # bk special? # bk required? True if big chests or big doors exists @@ -37,7 +37,7 @@ class KeyLayout(object): self.max_chests = calc_max_chests(builder, self, world, player) self.all_locations = set() self.item_locations = set() - self.prize_relevant = False + self.prize_relevant = None class KeyLogic(object): @@ -284,7 +284,7 @@ def analyze_dungeon(key_layout, world, player): key_logic.bk_restricted.update(filter_big_chest(key_counter.free_locations)) # note to self: this is due to the enough_small_locations function in validate_key_layout_sub_loop # I don't like this exception here or there - elif available <= possible_smalls and avail_bigs and non_big_locs > 0: + elif available < possible_smalls and avail_bigs and non_big_locs > 0: max_ctr = find_max_counter(key_layout) bk_lockdown = [x for x in max_ctr.free_locations if x not in key_counter.free_locations] key_logic.bk_restricted.update(filter_big_chest(bk_lockdown)) @@ -1380,6 +1380,15 @@ def forced_big_key_avail(locations): return None +def prize_relevance(key_layout, dungeon_entrance): + if len(key_layout.start_regions) > 1 and dungeon_entrance and dungeon_table[key_layout.key_logic.dungeon].prize: + if dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower']: + return 'GT' + elif dungeon_entrance.name == 'Pyramid Fairy': + return 'BigBomb' + return None + + # Soft lock stuff def validate_key_layout(key_layout, world, player): # retro is all good - except for hyrule castle in standard mode @@ -1391,12 +1400,11 @@ def validate_key_layout(key_layout, world, player): state.big_key_special = check_bk_special(key_layout.sector.regions, world, player) for region in key_layout.start_regions: dungeon_entrance, portal_door = find_outside_connection(region) - if (len(key_layout.start_regions) > 1 and dungeon_entrance and - dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy'] - and dungeon_table[key_layout.key_logic.dungeon].prize): + 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 = True + key_layout.prize_relevant = prize_relevant_flag else: state.visit_region(region, key_checks=True) state.add_all_doors_check_keys(region, flat_proposal, world, player) @@ -1554,12 +1562,11 @@ def create_key_counters(key_layout, world, player): state.big_key_special = True for region in key_layout.start_regions: dungeon_entrance, portal_door = find_outside_connection(region) - if (len(key_layout.start_regions) > 1 and dungeon_entrance and - dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy'] - and dungeon_table[key_layout.key_logic.dungeon].prize): + 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 = True + key_layout.prize_relevant = prize_relevant_flag else: state.visit_region(region, key_checks=True) state.add_all_doors_check_keys(region, flat_proposal, world, player) @@ -1988,8 +1995,10 @@ def validate_key_placement(key_layout, world, player): found_prize = any(x for x in counter.important_locations if '- Prize' in x.name) if not found_prize and dungeon_table[key_layout.sector.name].prize: prize_loc = world.get_location(dungeon_table[key_layout.sector.name].prize, player) - # todo: pyramid fairy only care about crystals 5 & 6 - found_prize = 'Crystal' not in prize_loc.item.name + if key_layout.prize_relevant == 'BigBomb': + found_prize = prize_loc.item.name not in ['Crystal 5', 'Crystal 6'] + elif key_layout.prize_relevant == 'GT': + found_prize = 'Crystal' not in prize_loc.item.name or world.crystals_needed_for_gt[player] < 7 else: found_prize = False can_progress = (not counter.big_key_opened and big_found and any(d.bigKey for d in counter.child_doors)) or \ From ec7c1489c788ef4cb59df59509cbfc89c2491d15 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 7 Sep 2021 16:25:42 -0600 Subject: [PATCH 14/15] Removed rails flag and just edited the object table for prevent mode in mixed travel Updated rails in PoD Arena to prevent hovering better --- DoorShuffle.py | 2 +- Rom.py | 16 ++++++++++++++-- data/base2current.bps | Bin 136284 -> 135945 bytes 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 2574e4f5..9d22893c 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -2041,7 +2041,7 @@ class DROptions(Flag): Town_Portal = 0x02 # If on, Players will start with mirror scroll Map_Info = 0x04 Debug = 0x08 - Rails = 0x10 # If on, draws rails + # 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 diff --git a/Rom.py b/Rom.py index 53d6666e..d6248488 100644 --- a/Rom.py +++ b/Rom.py @@ -32,7 +32,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '091671c7f555331ff2f411b304141c29' +RANDOMIZERBASEHASH = 'ef6e3e1aa59838c01dbd5b1b2387e70c' class JsonRom(object): @@ -671,7 +671,19 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): dr_flags |= DROptions.Debug if world.doorShuffle[player] == 'crossed' and world.logic[player] != 'nologic'\ and world.mixed_travel[player] == 'prevent': - dr_flags |= DROptions.Rails + # PoD Falling Bridge or Hammjump + # 1FA607: db $2D, $79, $69 ; 0x0069: Vertical Rail ↕ | { 0B, 1E } | Size: 05 + # 1FA60A: db $14, $99, $5D ; 0x005D: Large Horizontal Rail ↔ | { 05, 26 } | Size: 01 + rom.write_bytes(0xfa607, [0x2d, 0x79, 0x69, 0x14, 0x99, 0x5d]) + # PoD Arena + # 1FA573: db $D4, $B2, $22 ; 0x0022: Horizontal Rail ↔ | { 35, 2C } | Size: 02 + # 1FA576: db $D4, $CE, $22 ; 0x0022: Horizontal Rail ↔ | { 35, 33 } | Size: 01 + # 1FA579: db $D9, $AE, $69 ; 0x0069: Vertical Rail ↕ | { 36, 2B } | Size: 06 + rom.write_bytes(0xfa573, [0xd4, 0xb2, 0x22, 0xd4, 0xce, 0x22, 0xd9, 0xae, 0x69]) + # Mire BK Pond + # 1FB1FC: db $C8, $9D, $69 ; 0x0069: Vertical Rail ↕ | { 32, 27 } | Size: 01 + # 1FB1FF: db $B4, $AC, $5D ; 0x005D: Large Horizontal Rail ↔ | { 2D, 2B } | Size: 00 + rom.write_bytes(0xfb1fc, [0xc8, 0x9d, 0x69, 0xb4, 0xac, 0x5d]) if world.standardize_palettes[player] == 'original': dr_flags |= DROptions.OriginalPalettes if world.experimental[player]: diff --git a/data/base2current.bps b/data/base2current.bps index 4571d623111492b36542f9572a826fd4ecbbef1d..17b3e4976a8e05b8f7e0a167166c55236e3a9806 100644 GIT binary patch delta 7248 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 Date: Fri, 10 Sep 2021 09:13:20 -0600 Subject: [PATCH 15/15] More refinement for prize locked parts of a dungeon and key rule interaction --- Fill.py | 6 +++++- KeyDoorShuffle.py | 27 ++++++++++++++++++++++----- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/Fill.py b/Fill.py index 779e1ec4..2cf1f25e 100644 --- a/Fill.py +++ b/Fill.py @@ -245,7 +245,11 @@ def valid_key_placement(item, location, itempool, world): return True key_logic = world.key_logic[item.player][dungeon.name] unplaced_keys = len([x for x in itempool if x.name == key_logic.small_key_name and x.player == item.player]) - return key_logic.check_placement(unplaced_keys, location if item.bigkey else None) + prize_loc = None + if key_logic.prize_location: + prize_loc = world.get_location(key_logic.prize_location, location.player) + cr_count = world.crystals_needed_for_gt[location.player] + return key_logic.check_placement(unplaced_keys, location if item.bigkey else None, prize_loc, cr_count) else: inside_dungeon_item = ((item.smallkey and not world.keyshuffle[item.player]) or (item.bigkey and not world.bigkeyshuffle[item.player])) diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 1ff840f6..1b74ae80 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -58,10 +58,11 @@ class KeyLogic(object): self.outside_keys = 0 self.dungeon = dungeon_name self.sm_doors = {} + self.prize_location = None - def check_placement(self, unplaced_keys, big_key_loc=None): + def check_placement(self, unplaced_keys, big_key_loc=None, prize_loc=None, cr_count=7): for rule in self.placement_rules: - if not rule.is_satisfiable(self.outside_keys, unplaced_keys, big_key_loc): + if not rule.is_satisfiable(self.outside_keys, unplaced_keys, big_key_loc, prize_loc, cr_count): return False if big_key_loc: for rule_a, rule_b in itertools.combinations(self.placement_rules, 2): @@ -120,6 +121,7 @@ class PlacementRule(object): self.check_locations_wo_bk = None self.bk_relevant = True self.key_reduced = False + self.prize_relevance = None def contradicts(self, rule, unplaced_keys, big_key_loc): bk_blocked = big_key_loc in self.bk_conditional_set if self.bk_conditional_set else False @@ -154,7 +156,14 @@ class PlacementRule(object): left -= rule_needed return False - def is_satisfiable(self, outside_keys, unplaced_keys, big_key_loc): + def is_satisfiable(self, outside_keys, unplaced_keys, big_key_loc, prize_location, cr_count): + if self.prize_relevance and prize_location: + if self.prize_relevance == 'BigBomb': + if prize_location.item.name not in ['Crystal 5', 'Crystal 6']: + return True + elif self.prize_relevance == 'GT': + if 'Crystal' not in prize_location.item.name or cr_count < 7: + return True bk_blocked = False if self.bk_conditional_set: for loc in self.bk_conditional_set: @@ -258,6 +267,7 @@ def analyze_dungeon(key_layout, world, player): find_bk_locked_sections(key_layout, world, player) key_logic.bk_chests.update(find_big_chest_locations(key_layout.all_chest_locations)) key_logic.bk_chests.update(find_big_key_locked_locations(key_layout.all_chest_locations)) + key_logic.prize_location = dungeon_table[key_layout.sector.name].prize if world.retro[player] and world.mode[player] != 'standard': return @@ -361,6 +371,7 @@ def create_exhaustive_placement_rules(key_layout, world, player): rule.bk_conditional_set = blocked_loc rule.needed_keys_wo_bk = min_keys rule.check_locations_wo_bk = set(filter_big_chest(accessible_loc)) + rule.prize_relevance = key_layout.prize_relevant if rule_prize_relevant(key_counter) else None if valid_rule: key_logic.placement_rules.append(rule) adjust_locations_rules(key_logic, rule, accessible_loc, key_layout, key_counter, max_ctr) @@ -368,6 +379,10 @@ def create_exhaustive_placement_rules(key_layout, world, player): refine_location_rules(key_layout) +def rule_prize_relevant(key_counter): + return not key_counter.prize_doors_opened and not key_counter.prize_received + + def skip_key_counter_due_to_prize(key_layout, key_counter): return key_layout.prize_relevant and key_counter.prize_received and not key_counter.prize_doors_opened @@ -467,7 +482,7 @@ def refine_placement_rules(key_layout, max_ctr): if rule.needed_keys_wo_bk == 0: rules_to_remove.append(rule) if len(rule.check_locations_wo_bk) < rule.needed_keys_wo_bk or rule.needed_keys_wo_bk > key_layout.max_chests: - if len(rule.bk_conditional_set) > 0: + if not rule.prize_relevance and len(rule.bk_conditional_set) > 0: key_logic.bk_restricted.update(rule.bk_conditional_set) rules_to_remove.append(rule) changed = True # impossible for bk to be here, I think @@ -1432,7 +1447,8 @@ 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) - if smalls_done and bk_done: + prize_done = not key_layout.prize_relevant or state.prize_doors_opened + if smalls_done and bk_done and prize_done: return False else: # todo: pretty sure you should OR these paths together, maybe when there's one location and it can @@ -1468,6 +1484,7 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa if not valid: return False # todo: feel like you only open these if the boss is available??? + # todo: or if a crystal isn't valid placement on this boss if not state.prize_doors_opened and key_layout.prize_relevant: state_copy = state.copy() open_a_door(next(iter(state_copy.prize_door_set)), state_copy, flat_proposal, world, player)