diff --git a/BaseClasses.py b/BaseClasses.py index f03d47a2..b8554ed8 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1703,16 +1703,16 @@ class Entrance(object): self.temp_path = [] def can_reach(self, state): - # Destination Pickup OW Only No Ledges Can S&Q - multi_step_locations = { 'Pyramid Crack': ('Big Bomb', True, True, False), - 'Missing Smith': ('Frog', True, False, True), - 'Middle Aged Man': ('Dark Blacksmith Ruins', True, False, True) } + # Destination Pickup OW Only No Ledges Can S&Q Allow Mirror + multi_step_locations = { 'Pyramid Crack': ('Big Bomb', True, True, False, True), + 'Missing Smith': ('Frog', True, False, True, True), + 'Middle Aged Man': ('Dark Blacksmith Ruins', True, False, True, True) } if self.name in multi_step_locations: if self not in state.path: - world = self.parent_region.world if self.parent_region else None + world = self.parent_region.world step_location = world.get_location(multi_step_locations[self.name][0], self.player) - if step_location.can_reach(state) and self.can_reach_thru(state, step_location.parent_region, multi_step_locations[self.name][1], multi_step_locations[self.name][2], multi_step_locations[self.name][3]) and self.access_rule(state): + if step_location.can_reach(state) and self.can_reach_thru(state, step_location, multi_step_locations[self.name][1], multi_step_locations[self.name][2], multi_step_locations[self.name][3], multi_step_locations[self.name][4]) and self.access_rule(state): if not self in state.path: path = state.path.get(step_location.parent_region, (step_location.parent_region.name, None)) item_name = step_location.item.name if step_location.item else 'Pick Up Item' @@ -1734,35 +1734,141 @@ class Entrance(object): return False - def can_reach_thru(self, state, start_region, ignore_underworld=False, ignore_ledges=False, allow_save_quit=False): - def explore_region(region, path = []): + def can_reach_thru(self, state, step_location, ignore_underworld=False, ignore_ledges=False, allow_save_quit=False, allow_mirror_reentry=False): + def explore_region(region, destination, path = []): nonlocal found - if region not in explored_regions or len(explored_regions[region]) > len(path): + if region not in explored_regions: explored_regions[region] = path for exit in region.exits: if exit.connected_region and (not ignore_ledges or exit.spot_type != 'Ledge') \ and exit.connected_region.name not in ['Dig Game Area'] \ and exit.access_rule(state): - if exit.connected_region == self.parent_region: + if exit.connected_region == destination: found = True - explored_regions[self.parent_region] = path + [exit] + explored_regions[destination] = path + [exit] elif not ignore_underworld or region.type == exit.connected_region.type or exit.connected_region.type not in [RegionType.Cave, RegionType.Dungeon]: - explore_region(exit.connected_region, path + [exit]) + exits_to_traverse.append(tuple((exit, path))) - found = False - explored_regions = {} - explore_region(start_region.entrances[0].parent_region) - if found: - self.temp_path = explored_regions[self.parent_region] - elif allow_save_quit: - world = self.parent_region.world if self.parent_region else None - exit = world.get_entrance('Links House S&Q', self.player) - explore_region(exit.connected_region, [exit]) + def traverse_paths(region, destination, start_path=[]): + nonlocal explored_regions, exits_to_traverse + explored_regions = {} + exits_to_traverse = list() + explore_region(region, destination, start_path) + while not found and len(exits_to_traverse): + exit, path = exits_to_traverse.pop(0) + explore_region(exit.connected_region, destination, path + [exit]) if found: - self.temp_path = explored_regions[self.parent_region] + self.temp_path = explored_regions[destination] - #TODO: Implement residual mirror portal placing for the previous leg, to be used for the final destination + start_region = step_location.parent_region + explored_regions = {} + exits_to_traverse = list() + found = False + + if not found and allow_mirror_reentry and state.has('Magic Mirror', self.player): + # check for path using mirror portal re-entry at location of the follower pickup + # this is checked first as this often the shortest path + follower_region = start_region + if follower_region.type not in [RegionType.LightWorld, RegionType.DarkWorld]: + follower_region = start_region.entrances[0].parent_region + if (follower_region.world.mode[self.player] != 'inverted') == (follower_region.type == RegionType.LightWorld): + from OWEdges import OWTileRegions + from OverworldShuffle import ow_connections + owid = OWTileRegions[follower_region.name] + (mirror_map, other_world) = ow_connections[owid % 0x40] + mirror_map.extend(other_world) + mirror_exit = None + while len(mirror_map): + if mirror_map[0][1] == follower_region.name: + mirror_exit = mirror_map[0][0] + break + mirror_map.pop(0) + if mirror_exit: + mirror_region = follower_region.world.get_entrance(mirror_exit, self.player).parent_region + if mirror_region.can_reach(state): + traverse_paths(mirror_region, self.parent_region) + if found: + path = state.path.get(mirror_region, (mirror_region.name, None)) + path = (follower_region.name, (mirror_exit, path)) + item_name = step_location.item.name if step_location.item else 'Pick Up Item' + if start_region.name != follower_region.name: + path = (start_region.name, (start_region.entrances[0].name, path)) + path = (f'{step_location.parent_region.name} Exit', ('Leave Item Area', (item_name, path))) + else: + path = (item_name, path) + path = ('Use Mirror Portal', (follower_region.name, path)) + while len(self.temp_path): + exit = self.temp_path.pop(0) + path = (exit.name, (exit.parent_region.name, path)) + item_name = self.connected_region.locations[0].item.name if self.connected_region.locations[0].item else 'Deliver Item' + path = (self.parent_region.name, path) + state.path[self] = (self.name, path) + + if not found: + # check normal paths + traverse_paths(start_region.entrances[0].parent_region, self.parent_region) + if not found and allow_save_quit: + # check paths involving save and quit + exit = self.parent_region.world.get_entrance('Links House S&Q', self.player) + traverse_paths(exit.connected_region, self.parent_region, [exit]) + + if not found and allow_mirror_reentry and state.has('Magic Mirror', self.player): + # check for paths using mirror portal re-entry at location of final destination + # this is checked last as this is the most complicated/exhaustive check + follower_region = start_region + if follower_region.type not in [RegionType.LightWorld, RegionType.DarkWorld]: + follower_region = start_region.entrances[0].parent_region + if (follower_region.world.mode[self.player] != 'inverted') == (follower_region.type == RegionType.LightWorld): + dest_region = self.parent_region + if dest_region.type not in [RegionType.LightWorld, RegionType.DarkWorld]: + dest_region = start_region.entrances[0].parent_region + if (dest_region.world.mode[self.player] != 'inverted') != (dest_region.type == RegionType.LightWorld): + from OWEdges import OWTileRegions + from OverworldShuffle import ow_connections + owid = OWTileRegions[dest_region.name] + (mirror_map, other_world) = ow_connections[owid % 0x40] + mirror_map.extend(other_world) + mirror_map = [(x, d) for (x, d) in mirror_map if x in [e.name for e in dest_region.exits]] + # loop thru potential places to leave a mirror portal + while len(mirror_map) and not found: + mirror_exit = dest_region.world.get_entrance(mirror_map[0][0], self.player) + if mirror_exit.connected_region.type != dest_region.type: + # find path from placed mirror portal to the follower pickup + from Items import ItemFactory + mirror_item = ItemFactory('Magic Mirror', self.player) + while state.prog_items['Magic Mirror', self.player]: + state.remove(mirror_item) + temp_ignore_ledges = ignore_ledges + ignore_ledges = False + traverse_paths(mirror_exit.connected_region, start_region) + ignore_ledges = temp_ignore_ledges + state.collect(mirror_item, True) + if found: + path_to_pickup = self.temp_path + # find path from follower pickup to placed mirror portal + found = False + state.remove(mirror_item) + traverse_paths(follower_region, mirror_exit.connected_region) + state.collect(mirror_item, True) + mirror_map.pop(0) + if found: + path = state.path.get(self.parent_region, (self.parent_region.name, None)) + path = (mirror_exit.name, path) + + while len(path_to_pickup): + exit = path_to_pickup.pop(0) + path = (exit.name, (exit.parent_region.name, path)) + item_name = step_location.item.name if step_location.item else 'Pick Up Item' + path = (f'{step_location.parent_region.name} Exit', (item_name, path)) + + while len(self.temp_path): + exit = self.temp_path.pop(0) + path = (exit.name, (exit.parent_region.name, path)) + path = ('Use Mirror Portal', (mirror_exit.connected_region.name, path)) + path = (self.parent_region.name, path) + state.path[self] = (self.name, path) + return found def connect(self, region, addresses=None, target=None, vanilla=None): @@ -2732,6 +2838,7 @@ class Spoiler(object): self.world = world self.hashes = {} self.overworlds = {} + self.maps = {} self.entrances = {} self.doors = {} self.doorTypes = {} @@ -2747,12 +2854,22 @@ class Spoiler(object): self.shops = [] self.bosses = OrderedDict() + self.suppress_spoiler_locations = ['Big Bomb', 'Dark Blacksmith Ruins', 'Frog', 'Middle Aged Man'] + def set_overworld(self, entrance, exit, direction, player): if self.world.players == 1: self.overworlds[(entrance, direction, player)] = OrderedDict([('entrance', entrance), ('exit', exit), ('direction', direction)]) else: self.overworlds[(entrance, direction, player)] = OrderedDict([('player', player), ('entrance', entrance), ('exit', exit), ('direction', direction)]) + def set_map(self, type, text, data, player): + if type not in self.maps: + self.maps[type] = {} + if self.world.players == 1: + self.maps[type][player] = OrderedDict([('text', text), ('data', data)]) + else: + self.maps[type][player] = OrderedDict([('player', player), ('text', text), ('data', data)]) + def set_entrance(self, entrance, exit, direction, player): if self.world.players == 1: self.entrances[(entrance, direction, player)] = OrderedDict([('entrance', entrance), ('exit', exit), ('direction', direction)]) @@ -2922,6 +3039,7 @@ class Spoiler(object): self.parse_data() out = OrderedDict() out['Overworld'] = list(self.overworlds.values()) + out['Maps'] = list(self.maps.values()) out['Entrances'] = list(self.entrances.values()) out['Doors'] = list(self.doors.values()) out['Lobbies'] = list(self.lobbies.values()) @@ -2935,7 +3053,7 @@ class Spoiler(object): if self.shops: out['Shops'] = self.shops out['playthrough'] = self.playthrough - out['paths'] = self.paths + out['paths'] = {l:p for (l, p) in self.paths.items() if l not in self.suppress_spoiler_locations} out['Bosses'] = self.bosses out['meta'] = self.metadata @@ -3031,8 +3149,16 @@ class Spoiler(object): outfile.write(f'{fairy}: {bottle}\n') if self.overworlds: - # overworlds: overworld transitions; outfile.write('\n\nOverworld:\n\n') + # overworld tile swaps + if 'swaps' in self.maps: + outfile.write('OW Tile Swaps:\n') + for player in self.maps['swaps']: + if self.world.players > 1: + outfile.write(str('(Player ' + str(player) + ')\n')) # player name + outfile.write(self.maps['swaps'][player]['text'] + '\n\n') + + # overworld transitions outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: ' if self.world.players > 1 else '', self.world.fish.translate("meta","overworlds",entry['entrance']), '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', self.world.fish.translate("meta","overworlds",entry['exit'])) for entry in self.overworlds.values()])) if self.entrances: @@ -3096,14 +3222,21 @@ class Spoiler(object): # locations: Change up location names; in the instance of a location with multiple sections, it'll try to translate the room name outfile.write('\n\nPaths:\n\n') path_listings = [] + displayed_regions = [] for location, path in sorted(self.paths.items()): - path_lines = [] - for region, exit in path: - if exit is not None: - path_lines.append("{} -> {}".format(self.world.fish.translate("meta","rooms",region), self.world.fish.translate("meta","entrances",exit))) - else: - path_lines.append(self.world.fish.translate("meta","rooms",region)) - path_listings.append("{}\n {}".format(self.world.fish.translate("meta","locations",location), "\n => ".join(path_lines))) + if self.world.players == 1: + region = self.world.get_location(location.split(' @', 1)[0], 1).parent_region + if region.name in displayed_regions: + continue + displayed_regions.append(region.name) + if location not in self.suppress_spoiler_locations: + path_lines = [] + for region, exit in path: + if exit is not None: + path_lines.append("{} -> {}".format(self.world.fish.translate("meta","rooms",region), self.world.fish.translate("meta","entrances",exit))) + else: + path_lines.append(self.world.fish.translate("meta","rooms",region)) + path_listings.append("{}\n {}".format(self.world.fish.translate("meta","locations",location), "\n => ".join(path_lines))) outfile.write('\n'.join(path_listings)) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62d13649..93f9b2f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +### 0.2.3.0/1/2 +- Fixed issue in Crossed OW where mirror portal sprites would disappear when changing worlds +- Added Big Red Bomb logic to support using residual mirror portals for later re-entry +- Suppressed irrelevant paths in spoiler playthru +- Suppressed identical paths in spoiler playthru if multiple locations within the same region contain progression + +### 0.2.2.3 +- Fixed GT entrance not being opened when Inverted and WDM is Tile Swapped +- The Goal sign is moved to the area where the hole will be (always in opposite world as starting) + +### 0.2.2.2 +- Fixed Whirlpool Shuffle with Grouped Crossed OW +- Made filename not spoil OWR in Mystery +- Fixed Triforce Hunt goal + +### 0.2.2.1 +- Allow normal Link speed with Old Man following if not in his cave or WDM +- Fixed issue with Flute exits not getting placed on the correct tiles +- Hints in Lite/Lean ER no longer refer to entrances that are guaranteed vanilla +- Added Links House entrance to hint candidate list in ER when it is shuffled +- Added Tile Swaps ASCII map to Spoiler Log when Tile Swap is enabled +- Fixed issue with Whirlpool Shuffle not abiding by Polar rules + ### 0.2.2.0 - Delivering Big Red Bomb is now in logic - Smith/Purple Chest have proper dynamic pathing to fix logical issues diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 34733fc2..5b3b821d 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -428,13 +428,21 @@ def link_entrances(world, player): caves = Cave_Base + (dw_dungeons if not invFlag else lw_dungeons) connector_entrances = [e for e in list(zip(*default_connector_connections + default_dungeon_connections + open_default_dungeon_connections))[0] if e in (dw_entrances if not invFlag else lw_entrances)] connect_inaccessible_regions(world, [], connector_entrances, caves, player) + if invFlag: + lw_dungeons = list(set(lw_dungeons) & set(caves)) + else: + dw_dungeons = list(set(dw_dungeons) & set(caves)) caves = list(set(Cave_Base) & set(caves)) + (lw_dungeons if not invFlag else dw_dungeons) connector_entrances = [e for e in list(zip(*default_connector_connections + default_dungeon_connections + open_default_dungeon_connections))[0] if e in (lw_entrances if not invFlag else dw_entrances)] connect_inaccessible_regions(world, connector_entrances, [], caves, player) + if not invFlag: + lw_dungeons = list(set(lw_dungeons) & set(caves)) + else: + dw_dungeons = list(set(dw_dungeons) & set(caves)) - lw_dungeons = list(set(lw_dungeons) & set(caves)) + (Old_Man_House if not invFlag else []) - dw_dungeons = list(set(dw_dungeons) & set(caves)) + ([] if not invFlag else Old_Man_House) + lw_dungeons = lw_dungeons + (Old_Man_House if not invFlag else []) + dw_dungeons = dw_dungeons + ([] if not invFlag else Old_Man_House) caves = list(set(Cave_Base) & set(caves)) + DW_Mid_Dungeon_Exits # place old man, has limited options diff --git a/Main.py b/Main.py index 88b97105..237e9b9b 100644 --- a/Main.py +++ b/Main.py @@ -157,8 +157,8 @@ def main(args, seed=None, fish=None): for player, name in enumerate(team, 1): world.player_names[player].append(name) logger.info('') - - if world.owShuffle[1] != 'vanilla' or world.owCrossed[1] not in ['none', 'polar'] or world.owMixed[1] or str(world.seed).startswith('M'): + + if world.owShuffle[1] != 'vanilla' or world.owCrossed[1] not in ['none', 'polar'] or world.owMixed[1] or str(args.outputname).startswith('M'): outfilebase = f'OR_{args.outputname if args.outputname else world.seed}' else: outfilebase = f'DR_{args.outputname if args.outputname else world.seed}' @@ -596,7 +596,7 @@ def create_playthrough(world): # get locations containing progress items prog_locations = [location for location in world.get_filled_locations() if location.item.advancement] - optional_locations = ['Trench 1 Switch', 'Trench 2 Switch', 'Ice Block Drop', 'Big Bomb'] + optional_locations = ['Trench 1 Switch', 'Trench 2 Switch', 'Ice Block Drop'] state_cache = [None] collection_spheres = [] state = CollectionState(world) @@ -703,9 +703,6 @@ def create_playthrough(world): old_world.spoiler.paths = dict() for player in range(1, world.players + 1): old_world.spoiler.paths.update({location.gen_name(): get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere if location.player == player}) - for path in dict(old_world.spoiler.paths).values(): - if any(exit == 'Pyramid Fairy' for (_, exit) in path): - old_world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = get_path(state, world.get_region('Big Bomb Shop', player)) # we can finally output our playthrough old_world.spoiler.playthrough = {"0": [str(item) for item in world.precollected_items if item.advancement]} diff --git a/OWEdges.py b/OWEdges.py index 14849137..f13e3b32 100644 --- a/OWEdges.py +++ b/OWEdges.py @@ -1478,7 +1478,6 @@ OWExitTypes = { 'Dark Bonk Rocks Cliff Ledge Drop', 'Bomb Shop Cliff Ledge Drop', 'Hammer Bridge South Cliff Ledge Drop', - 'Ice Lake Northeast Pier Hop', 'Ice Lake Moat Bomb Jump', 'Ice Lake Area Cliff Ledge Drop', 'Ice Palace Island FAWT Ledge Drop', @@ -1623,6 +1622,7 @@ OWExitTypes = { 'Hype Cave Landing', 'Ice Lake Northeast Water Drop', 'Ice Lake Northeast Pier', + 'Ice Lake Northeast Pier Hop', 'Ice Lake Moat Water Entry', 'Ice Palace Approach', 'Ice Palace Leave', diff --git a/OverworldShuffle.py b/OverworldShuffle.py index cad95825..f5729ab3 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -3,7 +3,7 @@ from BaseClasses import OWEdge, WorldType, RegionType, Direction, Terrain, PolSl from Regions import mark_dark_world_regions, mark_light_world_regions from OWEdges import OWTileRegions, OWTileGroups, OWEdgeGroups, OWExitTypes, OpenStd, parallel_links, IsParallel -__version__ = '0.2.2.0-u' +__version__ = '0.2.3.2-u' def link_overworld(world, player): # setup mandatory connections @@ -137,6 +137,24 @@ def link_overworld(world, player): assert len(swapped_edges) == 0, 'Not all edges were swapped successfully: ' + ', '.join(swapped_edges ) update_world_regions(world, player) + + # update spoiler + s = list(map(lambda x: ' ' if x not in world.owswaps[player][0] else 'S', [i for i in range(0x40)])) + text_output = tile_swap_spoiler_table.replace('s', '%s') % ( s[0x02], s[0x07], + s[0x00], s[0x03], s[0x05], + s[0x00], s[0x02],s[0x03], s[0x05], s[0x07], s[0x0a], s[0x0f], + s[0x0a], s[0x0f], + s[0x10],s[0x11],s[0x12],s[0x13],s[0x14],s[0x15],s[0x16],s[0x17], s[0x10],s[0x11],s[0x12],s[0x13],s[0x14],s[0x15],s[0x16],s[0x17], + s[0x18], s[0x1a],s[0x1b], s[0x1d],s[0x1e], + s[0x22], s[0x25], s[0x1a], s[0x1d], + s[0x28],s[0x29],s[0x2a],s[0x2b],s[0x2c],s[0x2d],s[0x2e],s[0x2f], s[0x18], s[0x1b], s[0x1e], + s[0x30], s[0x32],s[0x33],s[0x34],s[0x35], s[0x37], s[0x22], s[0x25], + s[0x3a],s[0x3b],s[0x3c], s[0x3f], + s[0x28],s[0x29],s[0x2a],s[0x2b],s[0x2c],s[0x2d],s[0x2e],s[0x2f], + s[0x32],s[0x33],s[0x34], s[0x37], + s[0x30], s[0x35], + s[0x3a],s[0x3b],s[0x3c], s[0x3f]) + world.spoiler.set_map('swaps', text_output, world.owswaps[player][0], player) # apply tile logical connections for owid in ow_connections.keys(): @@ -153,7 +171,8 @@ def link_overworld(world, player): logging.getLogger('').debug('Crossing overworld edges') if world.owCrossed[player] in ['grouped', 'limited', 'chaos']: if world.owCrossed[player] == 'grouped': - crossed_edges = shuffle_tiles(world, tile_groups, [[],[],[]], player) + ow_crossed_tiles = [[],[],[]] + crossed_edges = shuffle_tiles(world, tile_groups, ow_crossed_tiles, player) elif world.owCrossed[player] in ['limited', 'chaos']: crossed_edges = list() crossed_candidates = list() @@ -196,23 +215,31 @@ def link_overworld(world, player): connect_simple(world, to_whirlpool, from_region, player) else: whirlpool_candidates = [[],[]] + world.owwhirlpools[player] = [None] * 8 for (from_owid, from_whirlpool, from_region), (to_owid, to_whirlpool, to_region) in default_whirlpool_connections: - if world.owCrossed[player] != 'none': - whirlpool_candidates[0].append(tuple((from_owid, from_whirlpool, from_region))) - whirlpool_candidates[0].append(tuple((to_owid, to_whirlpool, to_region))) + if world.owCrossed[player] == 'polar' and world.owMixed[player] and from_owid == 0x55: + # connect the 2 DW whirlpools in Polar Mixed + connect_simple(world, from_whirlpool, to_region, player) + connect_simple(world, to_whirlpool, from_region, player) + world.owwhirlpools[player][7] = from_owid + world.owwhirlpools[player][6] = to_owid + world.spoiler.set_overworld(from_whirlpool, to_whirlpool, 'both', player) else: - if world.get_region(from_region, player).type == RegionType.LightWorld: + if ((world.owCrossed[player] == 'none' or (world.owCrossed[player] == 'polar' and not world.owMixed[player])) and (world.get_region(from_region, player).type == RegionType.LightWorld)) \ + or world.owCrossed[player] not in ['none', 'polar', 'grouped'] \ + or (world.owCrossed[player] == 'grouped' and ((from_owid < 0x40) == (from_owid not in ow_crossed_tiles[0]))): whirlpool_candidates[0].append(tuple((from_owid, from_whirlpool, from_region))) else: whirlpool_candidates[1].append(tuple((from_owid, from_whirlpool, from_region))) - if world.get_region(to_region, player).type == RegionType.LightWorld: + if ((world.owCrossed[player] == 'none' or (world.owCrossed[player] == 'polar' and not world.owMixed[player])) and (world.get_region(to_region, player).type == RegionType.LightWorld)) \ + or world.owCrossed[player] not in ['none', 'polar', 'grouped'] \ + or (world.owCrossed[player] == 'grouped' and ((to_owid < 0x40) == (to_owid not in ow_crossed_tiles[0]))): whirlpool_candidates[0].append(tuple((to_owid, to_whirlpool, to_region))) else: whirlpool_candidates[1].append(tuple((to_owid, to_whirlpool, to_region))) # shuffle happens here - world.owwhirlpools[player] = [None] * 8 whirlpool_map = [ 0x35, 0x0f, 0x15, 0x33, 0x12, 0x3f, 0x55, 0x7f ] for whirlpools in whirlpool_candidates: random.shuffle(whirlpools) @@ -446,7 +473,7 @@ def shuffle_tiles(world, groups, result_list, player): exist_dw_regions.extend(dw_regions) # check whirlpool parity - valid_whirlpool_parity = world.owCrossed[player] != 'none' or len(set(new_results[0]) & set({0x0f, 0x12, 0x15, 0x33, 0x35, 0x3f, 0x55, 0x7f})) % 2 == 0 + valid_whirlpool_parity = world.owCrossed[player] not in ['none', 'grouped'] or len(set(new_results[0]) & set({0x0f, 0x12, 0x15, 0x33, 0x35, 0x3f, 0x55, 0x7f})) % 2 == 0 (exist_owids, exist_lw_regions, exist_dw_regions) = result_list exist_owids.extend(new_results[0]) @@ -723,9 +750,7 @@ def reorganize_groups(world, groups, player): def create_flute_exits(world, player): for region in (r for r in world.regions if r.player == player and r.terrain == Terrain.Land and r.name not in ['Zoras Domain', 'Master Sword Meadow', 'Hobo Bridge']): - if (not world.owMixed[player] and region.type == RegionType.LightWorld) \ - or (world.owMixed[player] and region.type in [RegionType.LightWorld, RegionType.DarkWorld] \ - and (region.name not in world.owswaps[player][1] or region.name in world.owswaps[player][2])): + if region.type == (RegionType.LightWorld if world.mode != 'inverted' else RegionType.DarkWorld): exitname = 'Flute From ' + region.name exit = Entrance(region.player, exitname, region) exit.spot_type = 'Flute' @@ -1589,3 +1614,23 @@ flute_data = { 0x3c: (['South Pass Area', 'Dark South Pass Area'], 0x3c, 0x0584, 0x0ed0, 0x081e, 0x0f38, 0x0898, 0x0f45, 0x08a3, 0xfffe, 0x0002, 0x0f38, 0x0898), 0x3f: (['Octoballoon Area', 'Bomber Corner Area'], 0x3f, 0x0810, 0x0f05, 0x0e75, 0x0f67, 0x0ef3, 0x0f72, 0x0efa, 0xfffb, 0x000b, 0x0f80, 0x0ef0) } + +tile_swap_spoiler_table = \ +""" 0 1 2 3 4 5 6 7 + +---+-+---+---+-+ + 01234567 A(00)| |s| | |s| + +--------+ | s +-+ s | s +-+ +A(00)|s ss s s| B(08)| |s| | |s| +B(08)| s s| +-+-+-+-+-+-+-+-+ +C(10)|ssssssss| C(10)|s|s|s|s|s|s|s|s| +D(18)|s ss ss | +-+-+-+-+-+-+-+-+ +E(20)| s s | D(18)| |s| |s| | +F(28)|ssssssss| | s +-+ s +-+ s | +G(30)|s ssss s| E(20)| |s| |s| | +H(38)| sss s| +-+-+-+-+-+-+-+-+ + +--------+ F(28)|s|s|s|s|s|s|s|s| + +-+-+-+-+-+-+-+-+ + G(30)| |s|s|s| |s| + | s +-+-+-+ s +-+ + H(38)| |s|s|s| |s| + +---+-+-+-+---+-+""" diff --git a/Rom.py b/Rom.py index 574d920b..657b1176 100644 --- a/Rom.py +++ b/Rom.py @@ -33,7 +33,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'ad69dddbfd546f665e730c2ee9ed3674' +RANDOMIZERBASEHASH = '7c1873254dcd5fb8b18934d806cd1949' class JsonRom(object): @@ -683,20 +683,20 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): # patches map data specific for OW Shuffle #inverted_buffer[0x03] = inverted_buffer[0x03] | 0x2 # convenient portal on WDM - inverted_buffer[0x1A] = inverted_buffer[0x1A] | 0x2 # rocks added to prevent OWG hardlock - inverted_buffer[0x1B] = inverted_buffer[0x1B] | 0x2 # rocks added to prevent OWG hardlock - inverted_buffer[0x22] = inverted_buffer[0x22] | 0x2 # rocks added to prevent OWG hardlock - inverted_buffer[0x3F] = inverted_buffer[0x3F] | 0x2 # added C to terrain + inverted_buffer[0x1A] |= 0x2 # rocks added to prevent OWG hardlock + inverted_buffer[0x1B] |= 0x2 # rocks added to prevent OWG hardlock + inverted_buffer[0x22] |= 0x2 # rocks added to prevent OWG hardlock + inverted_buffer[0x3F] |= 0x2 # added C to terrain #inverted_buffer[0x43] = inverted_buffer[0x43] | 0x2 # convenient portal on WDDM - inverted_buffer[0x5A] = inverted_buffer[0x5A] | 0x2 # rocks added to prevent OWG hardlock - inverted_buffer[0x5B] = inverted_buffer[0x5B] | 0x2 # rocks added to prevent OWG hardlock - inverted_buffer[0x62] = inverted_buffer[0x62] | 0x2 # rocks added to prevent OWG hardlock - inverted_buffer[0x7F] = inverted_buffer[0x7F] | 0x2 # added C to terrain + inverted_buffer[0x5A] |= 0x2 # rocks added to prevent OWG hardlock + inverted_buffer[0x5B] |= 0x2 # rocks added to prevent OWG hardlock + inverted_buffer[0x62] |= 0x2 # rocks added to prevent OWG hardlock + inverted_buffer[0x7F] |= 0x2 # added C to terrain if world.owMixed[player]: for b in world.owswaps[player][0]: # load inverted maps - inverted_buffer[b] = (inverted_buffer[b] & 0xFE) | ((inverted_buffer[b] + 1) % 2) + inverted_buffer[b] ^= 0x1 # set world flag rom.write_byte(0x153A00 + b, 0x00 if b >= 0x40 else 0x40) @@ -1522,10 +1522,9 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_byte(0x18004A, 0x00 if world.mode[player] != 'inverted' else 0x01) # Inverted mode rom.write_byte(0x18005D, 0x00) # Hammer always breaks barrier - rom.write_byte(0x2AF79, 0xD0 if world.mode[player] != 'inverted' else 0xF0) # vortexes: Normal (D0=light to dark, F0=dark to light, 42 = both) - rom.write_byte(0x3A943, 0xD0 if world.mode[player] != 'inverted' else 0xF0) # Mirror: Normal (D0=Dark to Light, F0=light to dark, 42 = both) - rom.write_byte(0x3A96D, 0xF0 if world.mode[player] != 'inverted' else 0xD0) # Residual Portal: Normal (F0= Light Side, D0=Dark Side, 42 = both (Darth Vader)) - rom.write_byte(0x3A9A7, 0xD0) # Residual Portal: Normal (D0= Light Side, F0=Dark Side, 42 = both (Darth Vader)) + rom.write_byte(0x03A943, 0xD0 if world.mode[player] != 'inverted' else 0xF0) # Mirror: Normal (D0=Dark to Light, F0=light to dark, 42 = both) + rom.write_byte(0x03A96D, 0xF0 if world.mode[player] != 'inverted' else 0xD0) # Residual Portal: Normal (F0= Light Side, D0=Dark Side, 42 = both (Darth Vader)) + rom.write_byte(0x03A9A7, 0xD0) # Residual Portal: Normal (D0= Light Side, F0=Dark Side, 42 = both (Darth Vader)) rom.write_bytes(0x180080, [50, 50, 70, 70]) # values to fill for Capacity Upgrades (Bomb5, Bomb10, Arrow5, Arrow10) @@ -2260,14 +2259,14 @@ def write_strings(rom, world, player, team): else: hint_count = 4 for entrance in all_entrances: - if entrance.name in entrances_to_hint: - if hint_count > 0: + if hint_count > 0: + if entrance.name in entrances_to_hint: this_hint = entrances_to_hint[entrance.name] + ' leads to ' + hint_text(entrance.connected_region) + '.' tt[hint_locations.pop(0)] = this_hint entrances_to_hint.pop(entrance.name) hint_count -= 1 - else: - break + else: + break #Next we handle hints for randomly selected other entrances, curating the selection intelligently based on shuffle. if world.shuffle[player] not in ['simple', 'restricted', 'restricted_legacy']: @@ -2279,9 +2278,22 @@ def write_strings(rom, world, player, team): entrances_to_hint.update({'Agahnims Tower': 'The sealed castle door'}) elif world.shuffle[player] == 'restricted': entrances_to_hint.update(ConnectorEntrances) - entrances_to_hint.update(OtherEntrances) + entrances_to_hint.update(ItemEntrances) + if world.shuffle[player] not in ['lite', 'lean']: + entrances_to_hint.update(ShopEntrances) + entrances_to_hint.update(OtherEntrances) + elif world.shopsanity[player]: + entrances_to_hint.update(ShopEntrances) + if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull']: + if world.mode[player] == 'inverted' != (0x2c in world.owswaps[player][0] and world.owMixed[player]): + entrances_to_hint.update({'Links House': 'The hero\'s old residence'}) + if world.shufflelinks[player]: + entrances_to_hint.update({'Big Bomb Shop': 'The old bomb shop'}) + else: + entrances_to_hint.update({'Big Bomb Shop': 'The old bomb shop'}) + if world.shufflelinks[player]: + entrances_to_hint.update({'Links House': 'The hero\'s old residence'}) entrances_to_hint.update({'Dark Sanctuary Hint': 'The dark sanctuary cave'}) - entrances_to_hint.update({'Big Bomb Shop': 'The old bomb shop'}) if world.shuffle[player] in ['insanity', 'madness_legacy', 'insanity_legacy']: entrances_to_hint.update(InsanityEntrances) if world.shuffle_ganon: @@ -2353,17 +2365,15 @@ def write_strings(rom, world, player, team): else: this_hint = location + ' contains ' + hint_text(world.get_location(location, player).item) + '.' tt[hint_locations.pop(0)] = this_hint - - # Adding a guaranteed hint for the Flute in overworld shuffle. + + # Lastly we write hints to show where certain interesting items are. It is done the way it is to re-use the silver code and also to give one hint per each type of item regardless of how many exist. This supports many settings well. + items_to_hint = RelevantItems.copy() if world.owShuffle[player] != 'vanilla' or world.owMixed[player]: + # Adding a guaranteed hint for the Flute in overworld shuffle. this_location = world.find_items_not_key_only('Ocarina', player) if this_location: this_hint = this_location[0].item.hint_text + ' can be found ' + hint_text(this_location[0]) + '.' tt[hint_locations.pop(0)] = this_hint - - # Lastly we write hints to show where certain interesting items are. It is done the way it is to re-use the silver code and also to give one hint per each type of item regardless of how many exist. This supports many settings well. - items_to_hint = RelevantItems.copy() - if world.owShuffle[player] != 'vanilla' or world.owMixed[player]: items_to_hint.remove('Ocarina') if world.keyshuffle[player]: items_to_hint.extend(SmallKeys) @@ -2372,7 +2382,7 @@ def write_strings(rom, world, player, team): random.shuffle(items_to_hint) hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull'] else 8 hint_count += 2 if world.doorShuffle[player] == 'crossed' else 0 - hint_count += 1 if world.owShuffle[player] != 'vanilla' or world.owMixed[player] else 0 + hint_count += 1 if world.owShuffle[player] != 'vanilla' or world.owCrossed[player] != 'none' or world.owMixed[player] else 0 while hint_count > 0: this_item = items_to_hint.pop(0) this_location = world.find_items_not_key_only(this_item, player) @@ -2583,7 +2593,7 @@ def set_inverted_mode(world, player, rom, inverted_buffer): if world.mode[player] == 'inverted': # load inverted maps for b in range(0x00, len(inverted_buffer)): - inverted_buffer[b] = (inverted_buffer[b] & 0xFE) | ((inverted_buffer[b] + 1) % 2) + inverted_buffer[b] ^= 0x1 rom.write_byte(snes_to_pc(0x0283E0), 0xF0) # residual portals rom.write_byte(snes_to_pc(0x02B34D), 0xF0) @@ -2870,16 +2880,45 @@ DungeonEntrances = {'Eastern Palace': 'Eastern Palace', 'Desert Palace Entrance (North)': 'The northmost cave in the desert' } -OtherEntrances = {'Blinds Hideout': 'Blind\'s old house', - 'Lake Hylia Fairy': 'A cave NE of Lake Hylia', +ItemEntrances = {'Blinds Hideout': 'Blind\'s old house', + 'Chicken House': 'The chicken lady\'s house', + 'Aginahs Cave': 'The open desert cave', + 'Sahasrahlas Hut': 'The house near armos', + 'Blacksmiths Hut': 'The old smithery', + 'Sick Kids House': 'The central house in Kakariko', + 'Mini Moldorm Cave': 'The cave south of Lake Hylia', + 'Ice Rod Cave': 'The sealed cave SE Lake Hylia', + 'Library': 'The old library', + 'Potion Shop': 'The witch\'s building', + 'Dam': 'The old dam', + 'Waterfall of Wishing': 'Going behind the waterfall', + 'Bonk Rock Cave': 'The rock pile near Sanctuary', + 'Graveyard Cave': 'The graveyard ledge', + 'Checkerboard Cave': 'The NE desert ledge', + 'Cave 45': 'The ledge south of haunted grove', + 'Kings Grave': 'The northeastmost grave', + 'C-Shaped House': 'The NE house in Village of Outcasts', + 'Mire Shed': 'The western hut in the mire', + 'Spike Cave': 'The ledge cave on west dark DM', + 'Hype Cave': 'The cave south of the old bomb shop', + 'Brewery': 'The Village of Outcasts building with no door', + 'Chest Game': 'The westmost building in the Village of Outcasts' + } + +ShopEntrances = {'Cave Shop (Lake Hylia)': 'The cave NW Lake Hylia', + 'Kakariko Shop': 'The old Kakariko shop', + 'Capacity Upgrade': 'The cave on the island', + 'Dark Lake Hylia Shop': 'The building NW dark Lake Hylia', + 'Dark World Shop': 'The hammer sealed building', + 'Red Shield Shop': 'The fenced in building', + 'Cave Shop (Dark Death Mountain)': 'The base of east dark DM', + 'Dark World Potion Shop': 'The building near the catfish', + 'Dark World Lumberjack Shop': 'The northmost Dark World building' + } + +OtherEntrances = {'Lake Hylia Fairy': 'A cave NE of Lake Hylia', 'Light Hype Fairy': 'The cave south of your house', 'Desert Fairy': 'The cave near the desert', - 'Chicken House': 'The chicken lady\'s house', - 'Aginahs Cave': 'The open desert cave', - 'Sahasrahlas Hut': 'The house near armos', - 'Cave Shop (Lake Hylia)': 'The cave NW Lake Hylia', - 'Blacksmiths Hut': 'The old smithery', - 'Sick Kids House': 'The central house in Kakariko', 'Lost Woods Gamble': 'A tree trunk door', 'Fortune Teller (Light)': 'A building NE of Kakariko', 'Snitch Lady (East)': 'A house guarded by a snitch', @@ -2887,49 +2926,24 @@ OtherEntrances = {'Blinds Hideout': 'Blind\'s old house', 'Bush Covered House': 'A house with an uncut lawn', 'Tavern (Front)': 'A building with a backdoor', 'Light World Bomb Hut': 'A Kakariko building with no door', - 'Kakariko Shop': 'The old Kakariko shop', - 'Mini Moldorm Cave': 'The cave south of Lake Hylia', 'Long Fairy Cave': 'The eastmost portal cave', 'Good Bee Cave': 'The open cave SE Lake Hylia', '20 Rupee Cave': 'The rock SE Lake Hylia', '50 Rupee Cave': 'The rock near the desert', - 'Ice Rod Cave': 'The sealed cave SE Lake Hylia', - 'Library': 'The old library', - 'Potion Shop': 'The witch\'s building', - 'Dam': 'The old dam', 'Lumberjack House': 'The lumberjack house', 'Lake Hylia Fortune Teller': 'The building NW Lake Hylia', 'Kakariko Gamble Game': 'The old Kakariko gambling den', - 'Waterfall of Wishing': 'Going behind the waterfall', - 'Capacity Upgrade': 'The cave on the island', - 'Bonk Rock Cave': 'The rock pile near Sanctuary', - 'Graveyard Cave': 'The graveyard ledge', - 'Checkerboard Cave': 'The NE desert ledge', - 'Cave 45': 'The ledge south of haunted grove', - 'Kings Grave': 'The northeastmost grave', 'Bonk Fairy (Light)': 'The rock pile near your home', 'Hookshot Fairy': 'The left paired cave on east DM', 'Bonk Fairy (Dark)': 'The rock pile near the old bomb shop', 'Dark Lake Hylia Fairy': 'The cave NE dark Lake Hylia', - 'C-Shaped House': 'The NE house in Village of Outcasts', 'Dark Death Mountain Fairy': 'The SW cave on dark DM', - 'Dark Lake Hylia Shop': 'The building NW dark Lake Hylia', - 'Dark World Shop': 'The hammer sealed building', - 'Red Shield Shop': 'The fenced in building', - 'Mire Shed': 'The western hut in the mire', 'East Dark World Hint': 'The dark cave near the eastmost portal', 'Dark Desert Hint': 'The cave east of the mire', - 'Spike Cave': 'The ledge cave on west dark DM', 'Palace of Darkness Hint': 'The building south of Kiki', 'Dark Lake Hylia Ledge Spike Cave': 'The rock SE dark Lake Hylia', - 'Cave Shop (Dark Death Mountain)': 'The base of east dark DM', - 'Dark World Potion Shop': 'The building near the catfish', 'Archery Game': 'The old archery game', - 'Dark World Lumberjack Shop': 'The northmost Dark World building', - 'Hype Cave': 'The cave south of the old bomb shop', - 'Brewery': 'The Village of Outcasts building with no door', 'Dark Lake Hylia Ledge Hint': 'The open cave SE dark Lake Hylia', - 'Chest Game': 'The westmost building in the Village of Outcasts', 'Dark Desert Fairy': 'The eastern hut in the mire', 'Dark Lake Hylia Ledge Fairy': 'The sealed cave SE dark Lake Hylia', 'Fortune Teller (Dark)': 'The building NE the Village of Outcasts' diff --git a/Rules.py b/Rules.py index 6423e909..135f8230 100644 --- a/Rules.py +++ b/Rules.py @@ -56,7 +56,8 @@ def set_rules(world, player): # require aga2 to beat ganon add_rule(world.get_location('Ganon', player), lambda state: state.has('Beat Agahnim 2', player)) elif world.goal[player] == 'triforcehunt': - add_rule(world.get_location('Murahdahla', player), lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= int(state.world.treasure_hunt_count[player])) + if ('Murahdahla', player) in world._location_cache: + add_rule(world.get_location('Murahdahla', player), lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= int(state.world.treasure_hunt_count[player])) # if swamp and dam have not been moved we require mirror for swamp palace if not world.swamp_patch_required[player]: @@ -729,7 +730,7 @@ def default_rules(world, player): # Underworld Logic set_rule(world.get_entrance('Old Man Cave Exit (West)', player), lambda state: False) # drop cannot be climbed up - set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: state.has('Mirror', player)) # can erase block + set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: state.has('Magic Mirror', player)) # can erase block set_rule(world.get_entrance('Bumper Cave Exit (Top)', player), lambda state: state.has('Cape', player)) set_rule(world.get_entrance('Bumper Cave Exit (Bottom)', player), lambda state: state.has('Cape', player) or state.has('Hookshot', player)) set_rule(world.get_entrance('Superbunny Cave Exit (Bottom)', player), lambda state: False) # Cannot get to bottom exit from top. Just exists for shuffling diff --git a/asm/owrando.asm b/asm/owrando.asm index 283c358f..0fc9716a 100644 --- a/asm/owrando.asm +++ b/asm/owrando.asm @@ -14,6 +14,9 @@ jsl OWEdgeTransition : nop #4 ;LDA $02A4E3,X : ORA $7EF3CA ;org $02e238 ;LDX #$9E : - DEX : DEX : CMP $DAEE,X : BNE - ;jsl OWSpecialTransition : nop #5 +org $05af75 +jsl OWPreserveMirrorSprite : nop #2 ; LDA $7EF3CA : BNE $05AFDF + ; whirlpool shuffle cross world change org $02b3bd jsl OWWhirlpoolUpdate ;JSL $02EA6C @@ -37,6 +40,10 @@ db #$b0 ; BCS to replace BEQ org $06907f ; < 3107f - sprite_prep.asm:2170 (LDA $7EF3CA) lda $8a : and.b #$40 +; override Link speed with Old Man following +org $09a32e ; < bank_09.asm:7457 (LDA.b #$0C : STA.b $5E) +jsl OWOldManSpeed + ; Dark Bonk Rocks Rain Sequence Guards (allowing Tile Swap on Dark Bonk Rocks) ;org $09c957 ; <- 4c957 ;dw #$cb5f ; matches value on Central Bonk Rocks screen @@ -137,6 +144,19 @@ OWWhirlpoolUpdate: rtl } +OWPreserveMirrorSprite: +{ + lda.l OWMode+1 : and.b #!FLAG_OW_CROSSED : beq .vanilla + rtl ; if OW Crossed, skip world check and continue + .vanilla + lda $7ef3ca : bne .deleteMirror + rtl + + .deleteMirror + pla : lda #$de : pha ; in vanilla, if in dark world, jump to $05afdf + rtl +} + OWFluteCancel: { lda.l OWFlags+1 : and #$01 : bne + @@ -162,6 +182,22 @@ OWSmithAccept: clc : rtl + sec : rtl } +OWOldManSpeed: +{ + lda $1b : beq .outdoors + lda $a0 : and #$fe : cmp #$f0 : beq .vanilla ; if in cave where you find Old Man + bra .normalspeed + .outdoors + lda $8a : cmp #$03 : beq .vanilla ; if on WDM screen + + .normalspeed + lda $5e : cmp #$0c : rtl + stz $5e : rtl + + .vanilla + lda #$0c : sta $5e ; what we wrote over + rtl +} org $aa9000 OWEdgeTransition: @@ -374,7 +410,7 @@ OWWorldUpdate: ; x = owid of destination screen lda #$38 : sta $012f ; play sfx - #$3b is an alternative ; toggle bunny mode - + lda $7ef357 : bne .nobunny + lda $7ef357 : bne .nobunny lda.l InvertedMode : bne .inverted lda $7ef3ca : and.b #$40 : bra + .inverted lda $7ef3ca : and.b #$40 : eor #$40 diff --git a/data/base2current.bps b/data/base2current.bps index ce5c9e1e..e136579b 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ