diff --git a/BaseClasses.py b/BaseClasses.py index ec35c0bf..abab6cb4 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -76,11 +76,12 @@ class World(object): self.dynamic_locations = [] self.spoiler = Spoiler(self) self.lamps_needed_for_dark_rooms = 1 - self.owswaps = {} - self.owwhirlpools = {} self.owedges = [] self._owedge_cache = {} + self.owswaps = {} + self.owwhirlpools = {} self.owflutespots = {} + self.owsectors = {} self.doors = [] self._door_cache = {} self.paired_doors = {} @@ -108,6 +109,7 @@ class World(object): set_player_attr('player_names', []) set_player_attr('owswaps', [[],[],[]]) set_player_attr('owwhirlpools', []) + set_player_attr('owsectors', None) set_player_attr('remote_items', False) set_player_attr('required_medallions', ['Ether', 'Quake']) set_player_attr('bottle_refills', ['Bottle (Green Potion)', 'Bottle (Green Potion)']) @@ -1742,6 +1744,11 @@ class Region(object): self.crystal_switch = False def can_reach(self, state): + from Utils import stack_size3a + from DungeonGenerator import GenerationException + if stack_size3a() > 500: + raise GenerationException(f'Infinite loop detected for "{self.name}" located at \'Region.can_reach\'') + if state.stale[self.player]: state.update_reachable_regions(self.player) return self in state.reachable_regions[self.player] @@ -1862,7 +1869,8 @@ class Entrance(object): from OWEdges import OWTileRegions from OverworldShuffle import ow_connections owid = OWTileRegions[follower_region.name] - (mirror_map, other_world) = ow_connections[owid % 0x40] + (mirror_map_orig, other_world) = ow_connections[owid % 0x40] + mirror_map = list(mirror_map_orig).copy() mirror_map.extend(other_world) mirror_exit = None while len(mirror_map): @@ -1914,7 +1922,8 @@ class Entrance(object): from OWEdges import OWTileRegions from OverworldShuffle import ow_connections owid = OWTileRegions[dest_region.name] - (mirror_map, other_world) = ow_connections[owid % 0x40] + (mirror_map_orig, other_world) = ow_connections.copy()[owid % 0x40] + mirror_map = list(mirror_map_orig).copy() 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 @@ -2950,12 +2959,10 @@ class Spoiler(object): 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)]) + self.maps[(type, player)] = OrderedDict([('type', type), ('text', text), ('data', data)]) else: - self.maps[type][player] = OrderedDict([('player', player), ('text', text), ('data', data)]) + self.maps[(type, player)] = OrderedDict([('player', player), ('type', type), ('text', text), ('data', data)]) def set_entrance(self, entrance, exit, direction, player): if self.world.players == 1: @@ -3238,12 +3245,15 @@ class Spoiler(object): if self.overworlds: 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']: + for player in range(1, self.world.players + 1): + if ('swaps', player) in self.maps: + outfile.write('OW Tile Swaps:\n') + break + for player in range(1, self.world.players + 1): + if ('swaps', player) in self.maps: if self.world.players > 1: outfile.write(str('(Player ' + str(player) + ')\n')) # player name - outfile.write(self.maps['swaps'][player]['text'] + '\n\n') + 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()])) diff --git a/Bosses.py b/Bosses.py index d266531f..1f691c26 100644 --- a/Bosses.py +++ b/Bosses.py @@ -33,7 +33,7 @@ def LanmolasDefeatRule(state, player): state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Cane of Somaria', player) or - state.has('Cane of Byrna', player) or + (state.has('Cane of Byrna', player) and state.can_kill_with_bombs(player)) or state.can_shoot_arrows(player) or state.has_special_weapon_level(player, 1))) @@ -60,7 +60,7 @@ def ArrghusDefeatRule(state, player): return True return ((state.has('Fire Rod', player) and (state.can_shoot_arrows(player) or state.can_extend_magic(player, 12))) or #assuming mostly gitting two puff with one shot - (state.has('Ice Rod', player) and (state.can_shoot_arrows(player) or state.can_extend_magic(player, 16)))) + (state.has('Ice Rod', player) and state.can_use_bombs(player) and (state.can_shoot_arrows(player) or state.can_extend_magic(player, 16)))) def MothulaDefeatRule(state, player): @@ -103,8 +103,9 @@ def KholdstareDefeatRule(state, player): def VitreousDefeatRule(state, player): return (state.special_weapon_check(player, 2) and - (state.can_shoot_arrows(player) or state.has_blunt_weapon(player) or - state.has_special_weapon_level(player, 2))) + ((state.can_shoot_arrows(player) and state.can_kill_with_bombs(player)) or + state.has_blunt_weapon(player) or + state.has_special_weapon_level(player, 2))) def TrinexxDefeatRule(state, player): if not (state.has('Fire Rod', player) and state.has('Ice Rod', player)): diff --git a/CHANGELOG.md b/CHANGELOG.md index 93f9b2f2..e2990d4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +### 0.2.3.5 +- Fixed issue with multiworld generation +- Added infinite loop detection +- Move mirror portal off screen during mirror bonk in Crossed OW + +### 0.2.3.4 +- Fixed major issue with subsequent seeds using same seed/settings resulting different +- Flute Shuffle now awards separated regions a prorated number of flute spots based on size +- Fixed spoiler log, was missing OW Tile Swap map +- Fixed spoiler log JSON output +- Fake flipper damage fix improved to skip the long delay after the scroll +- Fixed missing Blue Potion in Lake Shop in Inverted +- Added legacy OW Crossed option 'None (Allowed)' to support old behavior when invalid option was used in Mystery +- ~~Merged DR v0.5.1.6 - Money balancing fix/Boss logic fixes with Bombbag~~ + +### 0.2.3.3 +- Added OW Layout validation that reduces the cases where some screens are unreachable +- Fixed issue with mirror portals showing up in DW in Crossed OW +- Corrected Lost/Skull Woods Pass regions to be more accurate + ### 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 diff --git a/DoorShuffle.py b/DoorShuffle.py index 6e80d19d..7964755e 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1820,7 +1820,7 @@ def find_inaccessible_regions(world, player): else: start_regions = ['Links House', 'Dark Sanctuary Hint'] regs = convert_regions(start_regions, world, player) - all_regions = set([r for r in world.regions if r.player == player and r.type is not RegionType.Dungeon]) + all_regions = [r for r in world.regions if r.player == player and r.type is not RegionType.Dungeon] visited_regions = set() queue = deque(regs) while len(queue) > 0: @@ -1836,7 +1836,7 @@ def find_inaccessible_regions(world, player): if connect and connect not in queue and connect not in visited_regions: if connect.type is not RegionType.Dungeon or connect.name.endswith(' Portal'): queue.append(connect) - world.inaccessible_regions[player].extend([r.name for r in all_regions.difference(visited_regions) if valid_inaccessible_region(r)]) + world.inaccessible_regions[player].extend([r.name for r in all_regions if r not in visited_regions and valid_inaccessible_region(r)]) if (world.mode[player] == 'inverted') != (0x1b in world.owswaps[player][0] and world.owMixed[player]): ledge = world.get_region('Hyrule Castle Ledge', player) if any(x for x in ledge.exits if x.connected_region and x.connected_region.name == 'Agahnims Tower Portal'): diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 5b3b821d..337eb453 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -1,7 +1,8 @@ import logging -from collections import defaultdict +from collections import defaultdict, OrderedDict import RaceRandom as random -from BaseClasses import CollectionState, RegionType, Terrain +from BaseClasses import CollectionState, RegionType +from OverworldShuffle import build_accessible_region_list from OWEdges import OWTileRegions entrance_pool = list() @@ -28,7 +29,9 @@ def link_entrances(world, player): Old_Man_House = Old_Man_House_Base.copy() Cave_Three_Exits = Cave_Three_Exits_Base.copy() - sectors = build_sectors(world, player) + from OverworldShuffle import build_sectors + if not world.owsectors[player]: + world.owsectors[player] = build_sectors(world, player) # modifications to lists if invFlag == (0x1b in world.owswaps[player][0] and world.owMixed[player]): @@ -37,9 +40,9 @@ def link_entrances(world, player): connect_simple(world, 'Other World S&Q', 'Pyramid Area', player) else: entrance_pool.remove('Pyramid Hole') - entrance_pool.add('Inverted Pyramid Hole') + entrance_pool.append('Inverted Pyramid Hole') entrance_pool.remove('Pyramid Entrance') - entrance_pool.add('Inverted Pyramid Entrance') + entrance_pool.append('Inverted Pyramid Entrance') drop_connections.append(tuple(('Inverted Pyramid Hole', 'Pyramid'))) dropexit_connections.append(tuple(('Inverted Pyramid Entrance', 'Pyramid Exit'))) connect_simple(world, 'Other World S&Q', 'Hyrule Castle Ledge', player) @@ -241,10 +244,10 @@ def link_entrances(world, player): if invFlag: # place dark sanc - place_dark_sanc(world, sectors, player) + place_dark_sanc(world, player) # place links house - links_house = place_links_house(world, sectors, player) + links_house = place_links_house(world, player) # place blacksmith, has limited options place_blacksmith(world, links_house, player) @@ -271,10 +274,10 @@ def link_entrances(world, player): # place dark sanc if invFlag: - place_dark_sanc(world, sectors, player) + place_dark_sanc(world, player) # place links house - links_house = place_links_house(world, sectors, player) + links_house = place_links_house(world, player) # place blacksmith, has limited options place_blacksmith(world, links_house, player) @@ -331,10 +334,10 @@ def link_entrances(world, player): # place dark sanc if invFlag: - place_dark_sanc(world, sectors, player, list(zip(*drop_connections + dropexit_connections))[0]) + place_dark_sanc(world, player, list(zip(*drop_connections + dropexit_connections))[0]) # place links house - links_house = place_links_house(world, sectors, player, list(zip(*drop_connections + dropexit_connections))[0]) + links_house = place_links_house(world, player, list(zip(*drop_connections + dropexit_connections))[0]) # determine pools lw_entrances = list() @@ -408,7 +411,7 @@ def link_entrances(world, player): scramble_holes(world, player) # place links house - links_house = place_links_house(world, sectors, player) + links_house = place_links_house(world, player) # place blacksmith, has limited options place_blacksmith(world, links_house, player) @@ -429,21 +432,21 @@ def link_entrances(world, player): 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)) + lw_dungeons = list(OrderedDict.fromkeys(lw_dungeons + caves)) else: - dw_dungeons = list(set(dw_dungeons) & set(caves)) + dw_dungeons = list(OrderedDict.fromkeys(dw_dungeons + caves)) - caves = list(set(Cave_Base) & set(caves)) + (lw_dungeons if not invFlag else dw_dungeons) + caves = list(OrderedDict.fromkeys(Cave_Base + 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)) + lw_dungeons = list(OrderedDict.fromkeys(lw_dungeons + caves)) else: - dw_dungeons = list(set(dw_dungeons) & set(caves)) + dw_dungeons = list(OrderedDict.fromkeys(dw_dungeons + caves)) 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 + caves = list(OrderedDict.fromkeys(Cave_Base + caves)) + DW_Mid_Dungeon_Exits # place old man, has limited options lw_entrances = [e for e in lw_entrances if e in list(zip(*default_connector_connections + default_dungeon_connections + open_default_dungeon_connections))[0] and e in entrance_pool] @@ -494,7 +497,7 @@ def link_entrances(world, player): scramble_holes(world, player) # place links house - links_house = place_links_house(world, sectors, player) + links_house = place_links_house(world, player) # place blacksmith, has limited options place_blacksmith(world, links_house, player) @@ -542,10 +545,10 @@ def link_entrances(world, player): # place dark sanc if invFlag: - place_dark_sanc(world, sectors, player) + place_dark_sanc(world, player) # place links house - links_house = place_links_house(world, sectors, player) + links_house = place_links_house(world, player) # place blacksmith, has limited options place_blacksmith(world, links_house, player) @@ -616,10 +619,10 @@ def link_entrances(world, player): # place dark sanc if invFlag: - place_dark_sanc(world, sectors, player) + place_dark_sanc(world, player) # place links house - links_house = place_links_house(world, sectors, player) + links_house = place_links_house(world, player) # place blacksmith, place sanc exit first for additional blacksmith candidates doors = list(entrance_pool) @@ -1371,7 +1374,7 @@ def full_shuffle_dungeons(world, Dungeon_Exits, player): connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player) -def place_links_house(world, sectors, player, ignore_list=[]): +def place_links_house(world, player, ignore_list=[]): invFlag = world.mode[player] == 'inverted' if world.mode[player] == 'standard' or not world.shufflelinks[player]: links_house = 'Links House' if not invFlag else 'Big Bomb Shop' @@ -1383,9 +1386,9 @@ def place_links_house(world, sectors, player, ignore_list=[]): break if invFlag and isinstance(dark_sanc, str): - links_house_doors = [i for i in get_distant_entrances(world, dark_sanc, sectors, player) if i in entrance_pool] + links_house_doors = [i for i in get_distant_entrances(world, dark_sanc, player) if i in entrance_pool] else: - links_house_doors = [i for i in get_starting_entrances(world, sectors, player, world.shuffle[player] != 'insanity') if i in entrance_pool] + links_house_doors = [i for i in get_starting_entrances(world, player, world.shuffle[player] != 'insanity') if i in entrance_pool] if world.shuffle[player] in ['lite', 'lean']: links_house_doors = [e for e in links_house_doors if e in list(zip(*(default_item_connections + (default_shop_connections if world.shopsanity[player] else []))))[0]] @@ -1397,11 +1400,11 @@ def place_links_house(world, sectors, player, ignore_list=[]): return links_house -def place_dark_sanc(world, sectors, player, ignore_list=[]): +def place_dark_sanc(world, player, ignore_list=[]): if not world.shufflelinks[player]: - sanc_doors = [i for i in get_distant_entrances(world, 'Big Bomb Shop', sectors, player) if i in entrance_pool] + sanc_doors = [i for i in get_distant_entrances(world, 'Big Bomb Shop', player) if i in entrance_pool] else: - sanc_doors = [i for i in get_starting_entrances(world, sectors, player, world.shuffle[player] != 'insanity') if i in entrance_pool] + sanc_doors = [i for i in get_starting_entrances(world, player, world.shuffle[player] != 'insanity') if i in entrance_pool] if world.shuffle[player] in ['lite', 'lean']: sanc_doors = [e for e in sanc_doors if e in list(zip(*(default_item_connections + (default_shop_connections if world.shopsanity[player] else []))))[0]] @@ -1426,10 +1429,10 @@ def place_blacksmith(world, links_house, player): if invFlag: dark_sanc = world.get_entrance('Dark Sanctuary Hint Exit', player).connected_region.name - blacksmith_doors = list(set(blacksmith_doors + list(build_accessible_entrance_list(world, dark_sanc, player, assumed_inventory, False, True, True)))) + blacksmith_doors = list(OrderedDict.fromkeys(blacksmith_doors + list(build_accessible_entrance_list(world, dark_sanc, player, assumed_inventory, False, True, True)))) elif world.doorShuffle[player] == 'vanilla' or world.intensity[player] < 3: sanc_region = world.get_entrance('Sanctuary Exit', player).connected_region.name - blacksmith_doors = list(set(blacksmith_doors + list(build_accessible_entrance_list(world, sanc_region, player, assumed_inventory, False, True, True)))) + blacksmith_doors = list(OrderedDict.fromkeys(blacksmith_doors + list(build_accessible_entrance_list(world, sanc_region, player, assumed_inventory, False, True, True)))) if world.shuffle[player] in ['lite', 'lean']: blacksmith_doors = [e for e in blacksmith_doors if e in list(zip(*(default_item_connections + (default_shop_connections if world.shopsanity[player] else []))))[0]] @@ -1473,8 +1476,8 @@ def junk_fill_inaccessible(world, player): from DoorShuffle import find_inaccessible_regions find_inaccessible_regions(world, player) - for player in range(1, world.players + 1): - world.key_logic[player] = {} + for p in range(1, world.players + 1): + world.key_logic[p] = {} base_world = copy_world(world) base_world.override_bomb_check = True world.key_logic = {} @@ -1488,7 +1491,7 @@ def junk_fill_inaccessible(world, player): accessible_regions.append(region_name) break for region_name in accessible_regions.copy(): - accessible_regions = list(set(accessible_regions + list(build_accessible_region_list(base_world, region_name, player, False, True, False, False)))) + accessible_regions = list(OrderedDict.fromkeys(accessible_regions + list(build_accessible_region_list(base_world, region_name, player, False, True, False, False)))) world.inaccessible_regions[player] = [r for r in world.inaccessible_regions[player] if r not in accessible_regions] # get inaccessible entrances @@ -1524,7 +1527,7 @@ def connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, playe accessible_regions.append(region_name) break for region_name in accessible_regions.copy(): - accessible_regions = list(set(accessible_regions + list(build_accessible_region_list(world, region_name, player, True, True, False, False)))) + accessible_regions = list(OrderedDict.fromkeys(accessible_regions + list(build_accessible_region_list(world, region_name, player, True, True, False, False)))) world.inaccessible_regions[player] = [r for r in world.inaccessible_regions[player] if r not in accessible_regions] # split inaccessible into 2 lists for each world @@ -1634,106 +1637,12 @@ def unbias_dungeons(Dungeon_Exits): tuplize_lists_in_list(Dungeon_Exits) -def build_sectors(world, player): - from Main import copy_world - from OWEdges import OWTileRegions - - # perform accessibility check on duplicate world - for player in range(1, world.players + 1): - world.key_logic[player] = {} - base_world = copy_world(world) - world.key_logic = {} - - # build lists of contiguous regions accessible with full inventory (excl portals/mirror/flute/entrances) - regions = list(OWTileRegions.copy().keys()) - sectors = list() - while(len(regions) > 0): - explored_regions = build_accessible_region_list(base_world, regions[0], player, False, False, False, False) - regions = [r for r in regions if r not in explored_regions] - unique_regions = [_ for i in range(len(sectors)) for _ in sectors[i]] - if (any(r in unique_regions for r in explored_regions)): - for s in range(len(sectors)): - if (any(r in sectors[s] for r in explored_regions)): - sectors[s] = set(list(sectors[s]) + list(explored_regions)) - break - else: - sectors.append(explored_regions) - - # remove water regions if Flippers not in starting inventory - if not any(map(lambda i: i.name == 'Flippers', world.precollected_items)): - for s in range(len(sectors)): - terrains = list() - for regionname in sectors[s]: - region = world.get_region(regionname, player) - if region.terrain == Terrain.Land: - terrains.append(regionname) - sectors[s] = terrains - - # within each group, split into contiguous regions accessible only with starting inventory - for s in range(len(sectors)): - regions = list(sectors[s]).copy() - sectors2 = list() - while(len(regions) > 0): - explored_regions = build_accessible_region_list(base_world, regions[0], player, False, False, True, False) - regions = [r for r in regions if r not in explored_regions] - unique_regions = [_ for i in range(len(sectors2)) for _ in sectors2[i]] - if (any(r in unique_regions for r in explored_regions)): - for s2 in range(len(sectors2)): - if (any(r in sectors2[s2] for r in explored_regions)): - sectors2[s2] = set(list(sectors2[s2]) + list(explored_regions)) - break - else: - sectors2.append(explored_regions) - sectors[s] = sectors2 - - return sectors - - -def build_accessible_region_list(world, start_region, player, build_copy_world=False, cross_world=False, region_rules=True, ignore_ledges = False): - from Main import copy_world - from Items import ItemFactory - - def explore_region(region_name, region=None): - explored_regions.add(region_name) - if not region: - region = base_world.get_region(region_name, player) - for exit in region.exits: - if exit.connected_region is not None: - if any(map(lambda i: i.name == 'Ocarina', base_world.precollected_items)) and exit.spot_type == 'Flute': - fluteregion = exit.connected_region - for flutespot in fluteregion.exits: - if flutespot.connected_region and flutespot.connected_region.name not in explored_regions: - explore_region(flutespot.connected_region.name, flutespot.connected_region) - elif exit.connected_region.name not in explored_regions \ - and (exit.connected_region.type == region.type or (cross_world and exit.connected_region.type in [RegionType.LightWorld, RegionType.DarkWorld])) \ - and (not region_rules or exit.access_rule(blank_state)) and (not ignore_ledges or exit.spot_type != 'Ledge'): - explore_region(exit.connected_region.name, exit.connected_region) - - if build_copy_world: - for player in range(1, world.players + 1): - world.key_logic[player] = {} - base_world = copy_world(world) - base_world.override_bomb_check = True - world.key_logic = {} - else: - base_world = world - - connect_simple(base_world, 'Links House S&Q', start_region, player) - blank_state = CollectionState(base_world) - if base_world.mode[player] == 'standard': - blank_state.collect(ItemFactory('Zelda Delivered', player), True) - explored_regions = set() - explore_region(start_region) - - return explored_regions - - def build_accessible_entrance_list(world, start_region, player, assumed_inventory=[], cross_world=False, region_rules=True, exit_rules=True, include_one_ways=False): from Main import copy_world from Items import ItemFactory - for player in range(1, world.players + 1): - world.key_logic[player] = {} + for p in range(1, world.players + 1): + world.key_logic[p] = {} base_world = copy_world(world) base_world.override_bomb_check = True world.key_logic = {} @@ -1756,17 +1665,17 @@ def build_accessible_entrance_list(world, start_region, player, assumed_inventor new_regions.append(ledge) explored_regions.extend(new_regions) - entrances = set() + entrances = list() for region_name in explored_regions: region = base_world.get_region(region_name, player) for exit in region.exits: if exit.name in entrance_pool and (not exit_rules or exit.access_rule(blank_state)): - entrances.add(exit.name) + entrances.append(exit.name) return entrances -def get_starting_entrances(world, sectors, player, force_starting_world=True): +def get_starting_entrances(world, player, force_starting_world=True): invFlag = world.mode[player] == 'inverted' # find largest walkable sector @@ -1775,7 +1684,7 @@ def get_starting_entrances(world, sectors, player, force_starting_world=True): entrances = list() while not len(entrances): while (sector is None): - sector = max(sectors, key=lambda x: len(x) - (0 if x not in invalid_sectors else 1000)) + sector = max(world.owsectors[player], key=lambda x: len(x) - (0 if x not in invalid_sectors else 1000)) if not ((world.owCrossed[player] == 'polar' and world.owMixed[player]) or world.owCrossed[player] not in ['none', 'polar']) \ and world.get_region(next(iter(next(iter(sector)))), player).type != (RegionType.LightWorld if not invFlag else RegionType.DarkWorld): invalid_sectors.append(sector) @@ -1799,10 +1708,10 @@ def get_starting_entrances(world, sectors, player, force_starting_world=True): return entrances -def get_distant_entrances(world, start_entrance, sectors, player): +def get_distant_entrances(world, start_entrance, player): # get walkable sector in which initial entrance was placed start_region = world.get_entrance(start_entrance, player).parent_region.name - regions = next(s for s in sectors if any(start_region in w for w in s)) + regions = next(s for s in world.owsectors[player] if any(start_region in w for w in s)) regions = next(w for w in regions if start_region in w) # eliminate regions surrounding the initial entrance until less than half of the candidate regions remain @@ -1842,8 +1751,8 @@ def can_reach(world, entrance_name, region_name, player): from Items import ItemFactory from DoorShuffle import find_inaccessible_regions - for player in range(1, world.players + 1): - world.key_logic[player] = {} + for p in range(1, world.players + 1): + world.key_logic[p] = {} base_world = copy_world(world) base_world.override_bomb_check = True world.key_logic = {} @@ -1889,7 +1798,7 @@ Cave_Three_Exits_Base = [('Spectacle Rock Cave Exit (Peak)', 'Spectacle Rock Cav Old_Man_House_Base = [('Old Man House Exit (Bottom)', 'Old Man House Exit (Top)')] -Entrance_Pool_Base = {'Links House', +Entrance_Pool_Base = ['Links House', 'Desert Palace Entrance (South)', 'Desert Palace Entrance (West)', 'Desert Palace Entrance (East)', @@ -2027,9 +1936,9 @@ Entrance_Pool_Base = {'Links House', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (North)', - 'Pyramid Hole'} + 'Pyramid Hole'] -Exit_Pool_Base = {'Links House Exit', +Exit_Pool_Base = ['Links House Exit', 'Desert Palace Exit (South)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)', @@ -2167,7 +2076,7 @@ Exit_Pool_Base = {'Links House Exit', 'Skull Left Drop', 'Skull Pinball', 'Skull Pot Circle', - 'Pyramid'} + 'Pyramid'] # these are connections that cannot be shuffled and always exist. They link together separate parts of the world we need to divide into regions mandatory_connections = [('Links House S&Q', 'Links House'), diff --git a/Fill.py b/Fill.py index 891aae6d..a8ff9609 100644 --- a/Fill.py +++ b/Fill.py @@ -762,9 +762,12 @@ def balance_money_progression(world): if sphere_costs[player] > 0 and sphere_costs[player] > wallet[player]: insolvent.add(player) if len([p for p in solvent if len(locked_by_money[p]) > 0]) == 0: - target_player = min(insolvent, key=lambda p: sphere_costs[p]-wallet[p]) - difference = sphere_costs[target_player]-wallet[target_player] - logger.debug(f'Money balancing needed: Player {target_player} short {difference}') + if len(insolvent) > 0: + target_player = min(insolvent, key=lambda p: sphere_costs[p]-wallet[p]) + difference = sphere_costs[target_player]-wallet[target_player] + logger.debug(f'Money balancing needed: Player {target_player} short {difference}') + else: + difference = 0 while difference > 0: swap_targets = [x for x in unchecked_locations if x not in sphere_locations and x.item.name.startswith('Rupees') and x.item.player == target_player] if len(swap_targets) == 0: diff --git a/Main.py b/Main.py index 18843b8d..b9db1ee8 100644 --- a/Main.py +++ b/Main.py @@ -30,7 +30,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.5-u' +__version__ = '0.5.1.6-u' from source.classes.BabelFish import BabelFish @@ -202,6 +202,7 @@ def main(args, seed=None, fish=None): for player in range(1, world.players + 1): link_overworld(world, player) + create_shops(world, player) update_world_regions(world, player) create_flute_exits(world, player) diff --git a/OWEdges.py b/OWEdges.py index f13e3b32..fc307d8c 100644 --- a/OWEdges.py +++ b/OWEdges.py @@ -326,6 +326,8 @@ def create_owedges(world, player): world.initialize_owedges(edges) def create_owedge(player, name, owIndex, direction, terrain, edge_id, owSlotIndex=0xff): + if name not in OWExitTypes['OWEdge']: + OWExitTypes['OWEdge'].append(name) return OWEdge(player, name, owIndex, direction, terrain, edge_id, owSlotIndex) @@ -720,6 +722,7 @@ OWTileRegions = bidict({ 'Lost Woods Pass West Area': 0x10, 'Lost Woods Pass East Top Area': 0x10, + 'Lost Woods Pass Portal Area': 0x10, 'Lost Woods Pass East Bottom Area': 0x10, 'Kakariko Fortune Area': 0x11, @@ -862,6 +865,7 @@ OWTileRegions = bidict({ 'Skull Woods Pass West Area': 0x50, 'Skull Woods Pass East Top Area': 0x50, + 'Skull Woods Pass Portal Area': 0x50, 'Skull Woods Pass East Bottom Area': 0x50, 'Dark Fortune Area': 0x51, @@ -1390,6 +1394,7 @@ parallel_links = bidict({'Lost Woods SW': 'Skull Woods SW', }) OWExitTypes = { + 'OWEdge': [], 'Ledge': ['West Death Mountain Drop', 'Spectacle Rock Drop', 'East Death Mountain Spiral Ledge Drop', @@ -1518,6 +1523,10 @@ OWExitTypes = { 'Zora Waterfall Water Entry', 'Waterfall of Wishing Cave Entry', 'Zora Waterfall Landing', + 'Lost Woods Pass Hammer (North)', + 'Lost Woods Pass Hammer (South)', + 'Lost Woods Pass Rock (North)', + 'Lost Woods Pass Rock (South)', 'Kings Grave Outer Rocks', 'Graveyard Ladder (Bottom)', 'Graveyard Ladder (Top)', @@ -1587,8 +1596,10 @@ OWExitTypes = { 'Bumper Cave Entrance Rock', 'Skull Woods Pass Bush Row (West)', 'Skull Woods Pass Bush Row (East)', - 'Skull Woods Pass Rock (Top)', - 'Skull Woods Pass Rock (Bottom)', + 'Skull Woods Pass Bush (North)', + 'Skull Woods Pass Bush (South)', + 'Skull Woods Pass Rock (North)', + 'Skull Woods Pass Rock (South)', 'Dark Graveyard Bush (South)', 'Dark Graveyard Bush (North)', 'Qirn Jump East Water Drop', @@ -1632,8 +1643,7 @@ OWExitTypes = { 'Portal': ['West Death Mountain Teleporter', 'East Death Mountain Teleporter', 'TR Pegs Teleporter', - 'Kakariko Teleporter (Hammer)', - 'Kakariko Teleporter (Rock)', + 'Kakariko Teleporter', 'Top of Pyramid', 'Top of Pyramid (Inner)', 'East Hyrule Teleporter', @@ -1643,8 +1653,7 @@ OWExitTypes = { 'Dark Death Mountain Teleporter (West)', 'Dark Death Mountain Teleporter (East)', 'Turtle Rock Teleporter', - 'West Dark World Teleporter (Hammer)', - 'West Dark World Teleporter (Rock)', + 'West Dark World Teleporter', 'Post Aga Inverted Teleporter', 'East Dark World Teleporter', 'Misery Mire Teleporter', @@ -1687,6 +1696,7 @@ OWExitTypes = { 'Catfish Mirror Spot', 'Skull Woods Pass West Mirror Spot', 'Skull Woods Pass East Top Mirror Spot', + 'Skull Woods Pass Portal Mirror Spot', 'Skull Woods Pass East Bottom Mirror Spot', 'Outcast Fortune Mirror Spot', 'Outcast Pond Mirror Spot', @@ -1777,6 +1787,7 @@ OWExitTypes = { 'Zora Waterfall Mirror Spot', 'Lost Woods Pass West Mirror Spot', 'Lost Woods Pass East Top Mirror Spot', + 'Lost Woods Pass Portal Mirror Spot', 'Lost Woods Pass East Bottom Mirror Spot', 'Kakariko Fortune Mirror Spot', 'Kakariko Pond Mirror Spot', diff --git a/OverworldShuffle.py b/OverworldShuffle.py index f5729ab3..3f753b85 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -1,9 +1,10 @@ import RaceRandom as random, logging, copy +from collections import OrderedDict from BaseClasses import OWEdge, WorldType, RegionType, Direction, Terrain, PolSlot, Entrance 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.3.2-u' +__version__ = '0.2.3.5-u' def link_overworld(world, player): # setup mandatory connections @@ -140,8 +141,8 @@ def link_overworld(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], + 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], @@ -150,10 +151,10 @@ def link_overworld(world, player): 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]) + 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 @@ -265,6 +266,8 @@ def link_overworld(world, player): assert len(forward_set) == len(back_set) for (forward_edge, back_edge) in zip(forward_set, back_set): connect_two_way(world, forward_edge, back_edge, player, connected_edges) + + world.owsectors[player] = build_sectors(world, player) else: if world.owKeepSimilar[player] and world.owShuffle[player] in ['vanilla', 'parallel']: for exitname, destname in parallelsimilar_connections: @@ -280,50 +283,61 @@ def link_overworld(world, player): trimmed_groups = remove_reserved(world, trimmed_groups, connected_edges, player) groups = reorganize_groups(world, trimmed_groups, player) - if world.mode[player] == 'standard': - random.shuffle(groups[2:]) # keep first 2 groups (Standard) first - else: - random.shuffle(groups) + tries = 10 + valid_layout = False + connected_edge_cache = connected_edges.copy() + while not valid_layout and tries > 0: + connected_edges = connected_edge_cache.copy() - for (forward_edge_sets, back_edge_sets) in groups: - assert len(forward_edge_sets) == len(back_edge_sets) - random.shuffle(forward_edge_sets) - random.shuffle(back_edge_sets) - if len(forward_edge_sets) > 0: - f = 0 - b = 0 - while f < len(forward_edge_sets) and b < len(back_edge_sets): - forward_set = forward_edge_sets[f] - back_set = back_edge_sets[b] - while forward_set[0] in connected_edges: + if world.mode[player] == 'standard': + random.shuffle(groups[2:]) # keep first 2 groups (Standard) first + else: + random.shuffle(groups) + + for (forward_edge_sets, back_edge_sets) in groups: + assert len(forward_edge_sets) == len(back_edge_sets) + random.shuffle(forward_edge_sets) + random.shuffle(back_edge_sets) + if len(forward_edge_sets) > 0: + f = 0 + b = 0 + while f < len(forward_edge_sets) and b < len(back_edge_sets): + forward_set = forward_edge_sets[f] + back_set = back_edge_sets[b] + while forward_set[0] in connected_edges: + f += 1 + if f < len(forward_edge_sets): + forward_set = forward_edge_sets[f] + else: + forward_set = None + break f += 1 - if f < len(forward_edge_sets): - forward_set = forward_edge_sets[f] - else: - forward_set = None - break - f += 1 - while back_set[0] in connected_edges: + while back_set[0] in connected_edges: + b += 1 + if b < len(back_edge_sets): + back_set = back_edge_sets[b] + else: + back_set = None + break b += 1 - if b < len(back_edge_sets): - back_set = back_edge_sets[b] - else: - back_set = None - break - b += 1 - if forward_set is not None and back_set is not None: - assert len(forward_set) == len(back_set) - for (forward_edge, back_edge) in zip(forward_set, back_set): - connect_two_way(world, forward_edge, back_edge, player, connected_edges) - elif forward_set is not None: - logging.getLogger('').warning("Edge '%s' could not find a valid connection" % forward_set[0]) - elif back_set is not None: - logging.getLogger('').warning("Edge '%s' could not find a valid connection" % back_set[0]) - assert len(connected_edges) == len(default_connections) * 2, connected_edges + if forward_set is not None and back_set is not None: + assert len(forward_set) == len(back_set) + for (forward_edge, back_edge) in zip(forward_set, back_set): + connect_two_way(world, forward_edge, back_edge, player, connected_edges) + elif forward_set is not None: + logging.getLogger('').warning("Edge '%s' could not find a valid connection" % forward_set[0]) + elif back_set is not None: + logging.getLogger('').warning("Edge '%s' could not find a valid connection" % back_set[0]) + assert len(connected_edges) == len(default_connections) * 2, connected_edges + + world.owsectors[player] = build_sectors(world, player) + valid_layout = validate_layout(world, player) - # TODO: Reshuffle some areas if impossible to reach, exception if non-dungeon ER enabled or if area is LW with no portal and flute shuffle is enabled + tries -= 1 + assert valid_layout, 'Could not find a valid OW layout' # flute shuffle + logging.getLogger('').debug('Shuffling flute spots') def connect_flutes(flute_destinations): for o in range(0, len(flute_destinations)): owslot = flute_destinations[o] @@ -366,23 +380,37 @@ def link_overworld(world, player): flute_pool.remove(owid) new_spots.append(owid) return True + + # determine sectors (isolated groups of regions) to place flute spots + flute_regions = {(f[0][0] if f[1] not in world.owswaps[player][0] else f[0][1]) : o for o, f in flute_data.items()} + flute_sectors = [(len([r for l in s for r in l]), [r for l in s for r in l if r in flute_regions]) for s in world.owsectors[player]] + flute_sectors = [s for s in flute_sectors if len(s[1]) > 0] + region_total = sum([c for c,_ in flute_sectors]) + sector_total = len(flute_sectors) - # guarantee desert/mire access - addSpot(0x38) + # reserve a number of flute spots for each sector + flute_spots = 8 + for sector in flute_sectors: + sector_total -= 1 + spots_to_place = min(flute_spots - sector_total, max(1, round((sector[0] * (flute_spots - sector_total) / region_total) + 0.5))) + target_spots = len(new_spots) + spots_to_place + + if 'Desert Palace Teleporter Ledge' in sector[1] or 'Misery Mire Teleporter Ledge' in sector[1]: + addSpot(0x38) # guarantee desert/mire access - # guarantee mountain access - if world.owShuffle[player] == 'vanilla': - mountainIds = [0x0b, 0x0e, 0x07] - addSpot(mountainIds[random.randint(0, 2)]) + random.shuffle(sector[1]) + f = 0 + while len(new_spots) < target_spots: + if f >= len(sector[1]): + f = 0 + if sector[1][f] not in new_spots: + addSpot(flute_regions[sector[1][f]]) + f += 1 - random.shuffle(flute_pool) - f = 0 - while len(new_spots) < 8: - if f >= len(flute_pool): - f = 0 - if flute_pool[f] not in new_spots: - addSpot(flute_pool[f]) - f += 1 + region_total -= sector[0] + flute_spots -= spots_to_place + + # connect new flute spots new_spots.sort() world.owflutespots[player] = new_spots connect_flutes(new_spots) @@ -473,7 +501,7 @@ def shuffle_tiles(world, groups, result_list, player): exist_dw_regions.extend(dw_regions) # check whirlpool parity - 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 + valid_whirlpool_parity = world.owCrossed[player] not in ['none', 'grouped'] or len(OrderedDict.fromkeys(new_results[0] + [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]) @@ -782,7 +810,7 @@ def can_reach_smith(world, player): def explore_region(region_name, region=None): nonlocal found - explored_regions.add(region_name) + explored_regions.append(region_name) if not found: if not region: region = world.get_region(region_name, player) @@ -811,7 +839,7 @@ def can_reach_smith(world, player): blank_state.collect(ItemFactory('Titans Mitts', player), True) found = False - explored_regions = set() + explored_regions = list() explore_region('Links House') if not found: if not invFlag: @@ -820,6 +848,118 @@ def can_reach_smith(world, player): explore_region('Dark Sanctuary Hint') return found +def build_sectors(world, player): + from Main import copy_world + from OWEdges import OWTileRegions + + # perform accessibility check on duplicate world + for p in range(1, world.players + 1): + world.key_logic[p] = {} + base_world = copy_world(world) + world.key_logic = {} + + # build lists of contiguous regions accessible with full inventory (excl portals/mirror/flute/entrances) + regions = list(OWTileRegions.copy().keys()) + sectors = list() + while(len(regions) > 0): + explored_regions = build_accessible_region_list(base_world, regions[0], player, False, False, False, False) + regions = [r for r in regions if r not in explored_regions] + unique_regions = [_ for i in range(len(sectors)) for _ in sectors[i]] + if (any(r in unique_regions for r in explored_regions)): + for s in range(len(sectors)): + if (any(r in sectors[s] for r in explored_regions)): + sectors[s] = list(list(sectors[s]) + list(explored_regions)) + break + else: + sectors.append(explored_regions) + + # remove water regions if Flippers not in starting inventory + if not any(map(lambda i: i.name == 'Flippers', world.precollected_items)): + for s in range(len(sectors)): + terrains = list() + for regionname in sectors[s]: + region = world.get_region(regionname, player) + if region.terrain == Terrain.Land: + terrains.append(regionname) + sectors[s] = terrains + + # within each group, split into contiguous regions accessible only with starting inventory + for s in range(len(sectors)): + regions = list(sectors[s]).copy() + sectors2 = list() + while(len(regions) > 0): + explored_regions = build_accessible_region_list(base_world, regions[0], player, False, False, True, False) + regions = [r for r in regions if r not in explored_regions] + unique_regions = [_ for i in range(len(sectors2)) for _ in sectors2[i]] + if (any(r in unique_regions for r in explored_regions)): + for s2 in range(len(sectors2)): + if (any(r in sectors2[s2] for r in explored_regions)): + sectors2[s2] = list(sectors2[s2] + explored_regions) + break + else: + sectors2.append(explored_regions) + sectors[s] = sectors2 + + return sectors + +def build_accessible_region_list(world, start_region, player, build_copy_world=False, cross_world=False, region_rules=True, ignore_ledges = False): + from Main import copy_world + from BaseClasses import CollectionState + from Items import ItemFactory + from Utils import stack_size3a + + def explore_region(region_name, region=None): + if stack_size3a() > 500: + raise GenerationException(f'Infinite loop detected for "{start_region}" located at \'build_accessible_region_list\'') + + explored_regions.append(region_name) + if not region: + region = base_world.get_region(region_name, player) + for exit in region.exits: + if exit.connected_region is not None: + if any(map(lambda i: i.name == 'Ocarina', base_world.precollected_items)) and exit.spot_type == 'Flute': + fluteregion = exit.connected_region + for flutespot in fluteregion.exits: + if flutespot.connected_region and flutespot.connected_region.name not in explored_regions: + explore_region(flutespot.connected_region.name, flutespot.connected_region) + elif exit.connected_region.name not in explored_regions \ + and (exit.connected_region.type == region.type + or exit.name in OWExitTypes['OWEdge'] or (cross_world and exit.name in (OWExitTypes['Portal'] + OWExitTypes['Mirror']))) \ + and (not region_rules or exit.access_rule(blank_state)) and (not ignore_ledges or exit.name not in OWExitTypes['Ledge']): + explore_region(exit.connected_region.name, exit.connected_region) + + if build_copy_world: + for p in range(1, world.players + 1): + world.key_logic[p] = {} + base_world = copy_world(world) + base_world.override_bomb_check = True + world.key_logic = {} + else: + base_world = world + + connect_simple(base_world, 'Links House S&Q', start_region, player) + blank_state = CollectionState(base_world) + if base_world.mode[player] == 'standard': + blank_state.collect(ItemFactory('Zelda Delivered', player), True) + explored_regions = list() + explore_region(start_region) + + return explored_regions + +def validate_layout(world, player): + sectors = [[r for l in s for r in l] for s in world.owsectors[player]] + for sector in sectors: + entrances_present = False + for region_name in sector: + region = world.get_region(region_name, player) + if any(x.spot_type == 'Entrance' for x in region.exits): + entrances_present = True + break + if not entrances_present and not all(r in isolated_regions for r in sector): + return False + + return True + test_connections = [ #('Links House ES', 'Octoballoon WS'), #('Links House NE', 'Lost Woods Pass SW') @@ -860,6 +1000,10 @@ mandatory_connections = [# Intra-tile OW Connections ('Zora Waterfall Water Drop', 'Zora Waterfall Water'), #flippers ('Zora Waterfall Water Entry', 'Zora Waterfall Water'), #flippers ('Waterfall of Wishing Cave Entry', 'Waterfall of Wishing Cave'), #flippers + ('Lost Woods Pass Hammer (North)', 'Lost Woods Pass Portal Area'), #hammer + ('Lost Woods Pass Hammer (South)', 'Lost Woods Pass East Top Area'), #hammer + ('Lost Woods Pass Rock (North)', 'Lost Woods Pass East Bottom Area'), #mitts + ('Lost Woods Pass Rock (South)', 'Lost Woods Pass Portal Area'), #mitts ('Bonk Rock Ledge Drop', 'Sanctuary Area'), ('Graveyard Ledge Drop', 'Graveyard Area'), ('Kings Grave Outer Rocks', 'Kings Grave Area'), #mitts @@ -947,8 +1091,10 @@ mandatory_connections = [# Intra-tile OW Connections ('Bumper Cave Entrance Drop', 'Bumper Cave Area'), ('Skull Woods Pass Bush Row (West)', 'Skull Woods Pass East Top Area'), #pearl ('Skull Woods Pass Bush Row (East)', 'Skull Woods Pass West Area'), #pearl - ('Skull Woods Pass Rock (Top)', 'Skull Woods Pass East Bottom Area'), #mitts - ('Skull Woods Pass Rock (Bottom)', 'Skull Woods Pass East Top Area'), #mitts + ('Skull Woods Pass Bush (North)', 'Skull Woods Pass Portal Area'), #pearl + ('Skull Woods Pass Bush (South)', 'Skull Woods Pass East Top Area'), #pearl + ('Skull Woods Pass Rock (North)', 'Skull Woods Pass East Bottom Area'), #mitts + ('Skull Woods Pass Rock (South)', 'Skull Woods Pass Portal Area'), #mitts ('Dark Graveyard Bush (South)', 'Dark Graveyard North'), #pearl ('Dark Graveyard Bush (North)', 'Dark Graveyard Area'), #pearl ('Qirn Jump Water Drop', 'Qirn Jump Water'), #flippers @@ -1158,15 +1304,15 @@ ow_connections = { 0x10: ([ ('Lost Woods Pass West Mirror Spot', 'Lost Woods Pass West Area'), ('Lost Woods Pass East Top Mirror Spot', 'Lost Woods Pass East Top Area'), + ('Lost Woods Pass Portal Mirror Spot', 'Lost Woods Pass Portal Area'), ('Lost Woods Pass East Bottom Mirror Spot', 'Lost Woods Pass East Bottom Area'), - ('Kakariko Teleporter (Hammer)', 'Skull Woods Pass East Top Area'), - ('Kakariko Teleporter (Rock)', 'Skull Woods Pass East Top Area') + ('Kakariko Teleporter', 'Skull Woods Pass Portal Area') ], [ ('Skull Woods Pass West Mirror Spot', 'Skull Woods Pass West Area'), ('Skull Woods Pass East Top Mirror Spot', 'Skull Woods Pass East Top Area'), + ('Skull Woods Pass Portal Mirror Spot', 'Skull Woods Pass Portal Area'), ('Skull Woods Pass East Bottom Mirror Spot', 'Skull Woods Pass East Bottom Area'), - ('West Dark World Teleporter (Hammer)', 'Lost Woods Pass East Top Area'), - ('West Dark World Teleporter (Rock)', 'Lost Woods Pass East Bottom Area') + ('West Dark World Teleporter', 'Lost Woods Pass Portal Area') ]), 0x11: ([ ('Kakariko Fortune Mirror Spot', 'Kakariko Fortune Area') @@ -1570,6 +1716,11 @@ default_connections = [#('Lost Woods NW', 'Master Sword Meadow SC'), ('East Dark Death Mountain EN', 'Turtle Rock WN') ] +isolated_regions = [ + 'Death Mountain Floating Island', + 'Mimic Cave Ledge' +] + flute_data = { #Slot LW Region DW Region OWID VRAM BG Y BG X Link Y Link X Cam Y Cam X Unk1 Unk2 IconY IconX AltY AltX 0x09: (['Lost Woods East Area', 'Skull Woods Forest'], 0x00, 0x1042, 0x022e, 0x0202, 0x0290, 0x0288, 0x029b, 0x028f, 0xfff2, 0x000e, 0x0290, 0x0288, 0x0290, 0x0290), diff --git a/README.md b/README.md index 79a04f66..55bfdb98 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,8 @@ OW Transitions are shuffled within each world separately. This allows OW connections to be shuffled cross-world. +'None (Allowed)' allows entrance connectors and whirlpools to result in cross-world behavior, but edge transitions will not. This isn't a recommended option. + Polar and Grouped both are guaranteed to result in two separated planes of tiles. To navigate to the other plane, you have the following methods: 1) Normal portals 2) Mirroring on DW tiles 3) Fluting to a LW tile that was previously unreachable Limited and Chaos are not bound to follow a two-plane framework. This means that it could be possible to travel on foot to every tile without entering a normal portal. diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c1a7a62c..eaa45e2b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -15,6 +15,10 @@ CLI: ```--bombbag``` # Bug Fixes and Notes. +* 0.5.1.6 + * Rules fixes for TT (Boss and Cell) can now have TT Big Key if not otherwise required (boss shuffle + crossed dungeon) + * BUg fix for money balancing + * Add some bomb assumptions for bosses in bombbag mode * 0.5.1.5 * Fix for hard pool capacity upgrades missing * Bonk Fairy (Light) is no longer in logic for ER Standard and is forbidden to be a connector, so rain state isn't exitable diff --git a/Regions.py b/Regions.py index a5d3309d..f1d372cf 100644 --- a/Regions.py +++ b/Regions.py @@ -34,8 +34,9 @@ def create_regions(world, player): create_lw_region(player, 'Waterfall of Wishing Cave', None, ['Zora Waterfall Water Drop', 'Waterfall of Wishing']), create_lw_region(player, 'Zoras Domain', ['King Zora', 'Zora\'s Ledge'], ['Zoras Domain SW']), create_lw_region(player, 'Lost Woods Pass West Area', None, ['Skull Woods Pass West Mirror Spot', 'Lost Woods Pass NW', 'Lost Woods Pass SW']), - create_lw_region(player, 'Lost Woods Pass East Top Area', None, ['Skull Woods Pass East Top Mirror Spot', 'Kakariko Teleporter (Hammer)', 'Lost Woods Pass NE']), - create_lw_region(player, 'Lost Woods Pass East Bottom Area', None, ['Skull Woods Pass East Bottom Mirror Spot', 'Kakariko Teleporter (Rock)', 'Lost Woods Pass SE']), + create_lw_region(player, 'Lost Woods Pass East Top Area', None, ['Skull Woods Pass East Top Mirror Spot', 'Lost Woods Pass Hammer (North)', 'Lost Woods Pass NE']), + create_lw_region(player, 'Lost Woods Pass Portal Area', None, ['Skull Woods Pass Portal Mirror Spot', 'Kakariko Teleporter', 'Lost Woods Pass Hammer (South)', 'Lost Woods Pass Rock (North)']), + create_lw_region(player, 'Lost Woods Pass East Bottom Area', None, ['Skull Woods Pass East Bottom Mirror Spot', 'Lost Woods Pass Rock (South)', 'Lost Woods Pass SE']), create_lw_region(player, 'Kakariko Fortune Area', None, ['Fortune Teller (Light)', 'Outcast Fortune Mirror Spot', 'Kakariko Fortune NE', 'Kakariko Fortune EN', 'Kakariko Fortune ES', 'Kakariko Fortune SC']), create_lw_region(player, 'Kakariko Pond Area', None, ['Outcast Pond Mirror Spot', 'Kakariko Pond NE', 'Kakariko Pond WN', 'Kakariko Pond WS', 'Kakariko Pond SW', 'Kakariko Pond SE', 'Kakariko Pond EN', 'Kakariko Pond ES', 'Kakariko Pond Whirlpool']), create_lw_region(player, 'Sanctuary Area', None, ['Sanctuary', 'Dark Chapel Mirror Spot', 'Sanctuary WS', 'Sanctuary EC']), @@ -144,8 +145,9 @@ def create_regions(world, player): create_dw_region(player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'], ['Bumper Cave Entrance Drop', 'Bumper Cave (Top)', 'Mountain Entry Ledge Mirror Spot']), create_dw_region(player, 'Catfish Area', ['Catfish'], ['Zora Waterfall Mirror Spot', 'Catfish SE']), create_dw_region(player, 'Skull Woods Pass West Area', None, ['Skull Woods Pass Bush Row (West)', 'Lost Woods Pass West Mirror Spot', 'Skull Woods Pass NW', 'Skull Woods Pass SW']), - create_dw_region(player, 'Skull Woods Pass East Top Area', None, ['Skull Woods Pass Bush Row (East)', 'Skull Woods Pass Rock (Top)', 'West Dark World Teleporter (Hammer)', 'West Dark World Teleporter (Rock)', 'Lost Woods Pass East Top Mirror Spot', 'Skull Woods Pass NE']), - create_dw_region(player, 'Skull Woods Pass East Bottom Area', None, ['Skull Woods Pass Rock (Bottom)', 'Lost Woods Pass East Bottom Mirror Spot', 'Skull Woods Pass SE']), + create_dw_region(player, 'Skull Woods Pass East Top Area', None, ['Lost Woods Pass East Top Mirror Spot', 'Skull Woods Pass Bush Row (East)', 'Skull Woods Pass Bush (North)', 'Skull Woods Pass NE']), + create_dw_region(player, 'Skull Woods Pass Portal Area', None, ['Lost Woods Pass Portal Mirror Spot', 'West Dark World Teleporter', 'Skull Woods Pass Bush (South)', 'Skull Woods Pass Rock (North)']), + create_dw_region(player, 'Skull Woods Pass East Bottom Area', None, ['Lost Woods Pass East Bottom Mirror Spot', 'Skull Woods Pass Rock (South)', 'Skull Woods Pass SE']), create_dw_region(player, 'Dark Fortune Area', None, ['Fortune Teller (Dark)', 'Kakariko Fortune Mirror Spot', 'Dark Fortune NE', 'Dark Fortune EN', 'Dark Fortune ES', 'Dark Fortune SC']), create_dw_region(player, 'Outcast Pond Area', None, ['Kakariko Pond Mirror Spot', 'Outcast Pond NE', 'Outcast Pond WN', 'Outcast Pond WS', 'Outcast Pond SW', 'Outcast Pond SE', 'Outcast Pond EN', 'Outcast Pond ES']), create_dw_region(player, 'Dark Chapel Area', None, ['Dark Sanctuary Hint', 'Sanctuary Mirror Spot', 'Bonk Rock Ledge Mirror Spot', 'Dark Chapel WN', 'Dark Chapel WS', 'Dark Chapel EC']), @@ -1119,9 +1121,12 @@ def mark_dark_world_regions(world, player): def create_shops(world, player): world.shops[player] = [] for region_name, (room_id, type, shopkeeper, custom, locked, inventory, sram) in shop_table.items(): - if (world.mode[player] == 'inverted') != (0x35 in world.owswaps[player][0] and world.owMixed[player]) and region_name == 'Dark Lake Hylia Shop': - locked = True - inventory = [('Blue Potion', 160), ('Blue Shield', 50), ('Bombs (10)', 50)] + if world.mode[player] == 'inverted': + if (0x35 not in world.owswaps[player][0] and region_name == 'Dark Lake Hylia Shop') \ + or (0x35 in world.owswaps[player][0] and region_name == 'Cave Shop (Lake Hylia)'): + locked = True + custom = True + inventory = [('Blue Potion', 160), ('Blue Shield', 50), ('Bombs (10)', 50)] region = world.get_region(region_name, player) shop = Shop(region, room_id, type, shopkeeper, custom, locked, sram) region.shop = shop diff --git a/Rom.py b/Rom.py index 2aa4ab95..391a0e75 100644 --- a/Rom.py +++ b/Rom.py @@ -33,7 +33,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '7f7d8238e3a7254f21a1c6127521feac' +RANDOMIZERBASEHASH = '9bd966f2eabdfdaecf54c59d3921f470' class JsonRom(object): diff --git a/Rules.py b/Rules.py index ff878d1a..e6e73539 100644 --- a/Rules.py +++ b/Rules.py @@ -307,9 +307,10 @@ def global_rules(world, player): set_rule(world.get_location('Thieves\' Town - Big Chest', player), lambda state: state.has('Hammer', player)) for entrance in ['Thieves Basement Block Path', 'Thieves Blocked Entry Path', 'Thieves Conveyor Block Path', 'Thieves Conveyor Bridge Block Path']: set_rule(world.get_entrance(entrance, player), lambda state: state.can_lift_rocks(player)) - for location in ['Thieves\' Town - Blind\'s Cell', 'Thieves\' Town - Boss']: - forbid_item(world.get_location(location, player), 'Big Key (Thieves Town)', player) - forbid_item(world.get_location('Thieves\' Town - Blind\'s Cell', player), 'Big Key (Thieves Town)', player) + + # I think these rules are unnecessary now - testing needed + # for location in ['Thieves\' Town - Blind\'s Cell', 'Thieves\' Town - Boss']: + # forbid_item(world.get_location(location, player), 'Big Key (Thieves Town)', player) for location in ['Suspicious Maiden', 'Thieves\' Town - Blind\'s Cell']: set_rule(world.get_location(location, player), lambda state: state.has('Big Key (Thieves Town)', player)) set_rule(world.get_location('Revealing Light', player), lambda state: state.has('Shining Light', player) and state.has('Maiden Rescued', player)) @@ -730,7 +731,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('Magic Mirror', player)) # can erase block + set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: state.has_Mirror(player)) # can erase block, overwritten in noglitches 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 @@ -770,6 +771,10 @@ def default_rules(world, player): set_rule(world.get_entrance('TR Pegs Ledge Leave', player), lambda state: state.can_lift_heavy_rocks(player)) set_rule(world.get_entrance('Mountain Entry Entrance Rock (West)', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Mountain Entry Entrance Rock (East)', player), lambda state: state.can_lift_rocks(player)) + set_rule(world.get_entrance('Lost Woods Pass Hammer (North)', player), lambda state: state.has('Hammer', player)) + set_rule(world.get_entrance('Lost Woods Pass Hammer (South)', player), lambda state: state.has('Hammer', player)) + set_rule(world.get_entrance('Lost Woods Pass Rock (North)', player), lambda state: state.can_lift_heavy_rocks(player)) + set_rule(world.get_entrance('Lost Woods Pass Rock (South)', player), lambda state: state.can_lift_heavy_rocks(player)) set_rule(world.get_entrance('Kings Grave Outer Rocks', player), lambda state: state.can_lift_heavy_rocks(player)) set_rule(world.get_entrance('Kings Grave Inner Rocks', player), lambda state: state.can_lift_heavy_rocks(player)) set_rule(world.get_entrance('Potion Shop Rock (South)', player), lambda state: state.can_lift_rocks(player)) @@ -792,8 +797,8 @@ def default_rules(world, player): set_rule(world.get_entrance('Skull Woods Bush Rock (West)', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Skull Woods Bush Rock (East)', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Bumper Cave Entrance Rock', player), lambda state: state.can_lift_rocks(player)) - set_rule(world.get_entrance('Skull Woods Pass Rock (Top)', player), lambda state: state.can_lift_heavy_rocks(player)) - set_rule(world.get_entrance('Skull Woods Pass Rock (Bottom)', player), lambda state: state.can_lift_heavy_rocks(player)) + set_rule(world.get_entrance('Skull Woods Pass Rock (North)', player), lambda state: state.can_lift_heavy_rocks(player)) + set_rule(world.get_entrance('Skull Woods Pass Rock (South)', player), lambda state: state.can_lift_heavy_rocks(player)) set_rule(world.get_entrance('Dark Witch Rock (North)', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Dark Witch Rock (South)', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Catfish Approach Rocks (West)', player), lambda state: state.can_lift_heavy_rocks(player) or state.has_Boots(player)) @@ -949,15 +954,14 @@ def ow_rules(world, player): if (world.mode[player] == 'inverted') == (0x10 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Lost Woods Pass West Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Lost Woods Pass East Top Mirror Spot', player), lambda state: state.has_Mirror(player)) + set_rule(world.get_entrance('Lost Woods Pass Portal Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Lost Woods Pass East Bottom Mirror Spot', player), lambda state: state.has_Mirror(player)) - set_rule(world.get_entrance('Kakariko Teleporter (Hammer)', player), lambda state: state.has('Hammer', player) and state.can_lift_rocks(player) and state.has_Pearl(player)) # bunny cannot lift bushes - set_rule(world.get_entrance('Kakariko Teleporter (Rock)', player), lambda state: state.can_lift_heavy_rocks(player) and state.has_Pearl(player)) # bunny cannot lift bushes + set_rule(world.get_entrance('Kakariko Teleporter', player), lambda state: state.can_lift_rocks(player)) else: set_rule(world.get_entrance('Skull Woods Pass West Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Skull Woods Pass East Top Mirror Spot', player), lambda state: state.has_Mirror(player)) + set_rule(world.get_entrance('Skull Woods Pass Portal Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Skull Woods Pass East Bottom Mirror Spot', player), lambda state: state.has_Mirror(player)) - set_rule(world.get_entrance('West Dark World Teleporter (Hammer)', player), lambda state: state.has('Hammer', player) and state.can_lift_rocks(player) and state.has_Pearl(player)) - set_rule(world.get_entrance('West Dark World Teleporter (Rock)', player), lambda state: state.can_lift_heavy_rocks(player) and state.has_Pearl(player)) # bunny cannot lift bushes if (world.mode[player] == 'inverted') == (0x11 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Kakariko Fortune Mirror Spot', player), lambda state: state.has_Mirror(player)) @@ -1258,6 +1262,10 @@ def ow_bunny_rules(world, player): add_bunny_rule(world.get_entrance('TR Pegs Ledge Entry', player), player) add_bunny_rule(world.get_entrance('Mountain Entry Entrance Rock (West)', player), player) add_bunny_rule(world.get_entrance('Mountain Entry Entrance Rock (East)', player), player) + add_bunny_rule(world.get_entrance('Lost Woods Pass Hammer (North)', player), player) + add_bunny_rule(world.get_entrance('Lost Woods Pass Hammer (South)', player), player) + add_bunny_rule(world.get_entrance('Lost Woods Pass Rock (North)', player), player) + add_bunny_rule(world.get_entrance('Lost Woods Pass Rock (South)', player), player) add_bunny_rule(world.get_entrance('Kings Grave Outer Rocks', player), player) add_bunny_rule(world.get_entrance('Kings Grave Inner Rocks', player), player) add_bunny_rule(world.get_entrance('Potion Shop Rock (South)', player), player) @@ -1294,8 +1302,10 @@ def ow_bunny_rules(world, player): add_bunny_rule(world.get_entrance('Bumper Cave Entrance Rock', player), player) add_bunny_rule(world.get_entrance('Skull Woods Pass Bush Row (West)', player), player) add_bunny_rule(world.get_entrance('Skull Woods Pass Bush Row (East)', player), player) - add_bunny_rule(world.get_entrance('Skull Woods Pass Rock (Top)', player), player) - add_bunny_rule(world.get_entrance('Skull Woods Pass Rock (Bottom)', player), player) + add_bunny_rule(world.get_entrance('Skull Woods Pass Bush (North)', player), player) + add_bunny_rule(world.get_entrance('Skull Woods Pass Bush (South)', player), player) + add_bunny_rule(world.get_entrance('Skull Woods Pass Rock (North)', player), player) + add_bunny_rule(world.get_entrance('Skull Woods Pass Rock (South)', player), player) add_bunny_rule(world.get_entrance('Dark Graveyard Bush (South)', player), player) add_bunny_rule(world.get_entrance('Dark Graveyard Bush (North)', player), player) add_bunny_rule(world.get_entrance('Dark Witch Rock (North)', player), player) diff --git a/Utils.py b/Utils.py index 7d9f5ead..d81cdf01 100644 --- a/Utils.py +++ b/Utils.py @@ -7,6 +7,7 @@ import sys import xml.etree.ElementTree as ET from collections import defaultdict from functools import reduce +from itertools import count def int16_as_bytes(value): @@ -674,6 +675,21 @@ def extract_data_from_jp_rom(rom): print() +def stack_size3a(size=2): + # See reference: https://stackoverflow.com/questions/34115298/how-do-i-get-the-current-depth-of-the-python-interpreter-stack + """Get stack size for caller's frame.""" + frame = sys._getframe(size) + try: + for size in count(size, 8): + frame = frame.f_back.f_back.f_back.f_back.\ + f_back.f_back.f_back.f_back + except AttributeError: + while frame: + frame = frame.f_back + size += 1 + return size - 1 + + class bidict(dict): def __init__(self, *args, **kwargs): super(bidict, self).__init__(*args, **kwargs) diff --git a/asm/owrando.asm b/asm/owrando.asm index 0fc9716a..065cbfd8 100644 --- a/asm/owrando.asm +++ b/asm/owrando.asm @@ -148,14 +148,24 @@ 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 + lda InvertedMode : beq + + lda $7ef3ca : beq .deleteMirror + rtl + + lda $7ef3ca : bne .deleteMirror rtl .deleteMirror pla : lda #$de : pha ; in vanilla, if in dark world, jump to $05afdf rtl } +OWMirrorSpriteMove: +{ + lda.l OWMode+1 : and.b #!FLAG_OW_CROSSED : beq + + lda $1acf : eor #$80 : sta $1acf + + lda #$2c : jml.l $07A985 ; what we wrote over +} OWFluteCancel: { @@ -407,6 +417,14 @@ OWWorldUpdate: ; x = owid of destination screen { lda.l OWTileWorldAssoc,x : cmp.l $7ef3ca : beq .return sta.l $7ef3ca ; change world + + ; moving mirror portal off screen when in DW + cmp #0 : beq + : lda #1 + + cmp.l InvertedMode : bne + + lda $1acf : and #$0f : sta $1acf : bra .playSfx ; bring portal back into position + + lda $1acf : eor #$80 : sta $1acf ; move portal off screen + + .playSfx lda #$38 : sta $012f ; play sfx - #$3b is an alternative ; toggle bunny mode diff --git a/data/base2current.bps b/data/base2current.bps index 16ef5b3d..dd3449c3 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 92825856..e530d79c 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -125,6 +125,7 @@ "ow_crossed": { "choices": [ "none", + "allowed", "polar", "grouped", "limited", diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index dbf5b88c..b2b15464 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -213,6 +213,7 @@ "ow_crossed": [ "This allows cross-world connections to occur on the overworld.", "None: No transitions are cross-world connections.", + "Allowed: Only entrances/whirlpools can end up cross-world.", "Polar: Only used when Mixed is enabled. This retains original", " connections even when overworld tiles are swapped.", "Limited: Exactly nine transitions are randomly chosen as", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 0adb21ce..aaab4a5d 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -119,6 +119,7 @@ "randomizer.overworld.crossed": "Crossed", "randomizer.overworld.crossed.none": "None", + "randomizer.overworld.crossed.allowed": "None (Allowed)", "randomizer.overworld.crossed.polar": "Polar", "randomizer.overworld.crossed.grouped": "Grouped", "randomizer.overworld.crossed.limited": "Limited", diff --git a/resources/app/gui/randomize/overworld/widgets.json b/resources/app/gui/randomize/overworld/widgets.json index f6751bc2..43dc9394 100644 --- a/resources/app/gui/randomize/overworld/widgets.json +++ b/resources/app/gui/randomize/overworld/widgets.json @@ -14,6 +14,7 @@ "default": "vanilla", "options": [ "none", + "allowed", "polar", "grouped", "limited",