diff --git a/BaseClasses.py b/BaseClasses.py index 25aebbb1..3615d0e9 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -2784,6 +2784,7 @@ class Spoiler(object): self.world = world self.hashes = {} self.overworlds = {} + self.whirlpools = {} self.maps = {} self.entrances = {} self.doors = {} @@ -2808,6 +2809,12 @@ class Spoiler(object): else: self.overworlds[(entrance, direction, player)] = OrderedDict([('player', player), ('entrance', entrance), ('exit', exit), ('direction', direction)]) + def set_whirlpool(self, entrance, exit, direction, player): + if self.world.players == 1: + self.whirlpools[(entrance, direction, player)] = OrderedDict([('entrance', entrance), ('exit', exit), ('direction', direction)]) + else: + self.whirlpools[(entrance, direction, player)] = OrderedDict([('player', player), ('entrance', entrance), ('exit', exit), ('direction', direction)]) + def set_map(self, type, text, data, player): if self.world.players == 1: self.maps[(type, player)] = OrderedDict([('type', type), ('text', text), ('data', data)]) @@ -3010,6 +3017,7 @@ class Spoiler(object): self.parse_data() out = OrderedDict() out['Overworld'] = list(self.overworlds.values()) + out['Whirlpools'] = list(self.whirlpools.values()) out['Maps'] = list(self.maps.values()) out['Entrances'] = list(self.entrances.values()) out['Doors'] = list(self.doors.values()) @@ -3181,7 +3189,7 @@ class Spoiler(object): for fairy, bottle in self.bottles.items(): outfile.write(f'{fairy}: {bottle}\n') - if self.overworlds or self.maps: + if self.overworlds or self.whirlpools or self.maps: outfile.write('\n\nOverworld:\n\n') # flute shuffle @@ -3217,6 +3225,10 @@ class Spoiler(object): outfile.write(str('(Player ' + str(player) + ')\n')) # player name outfile.write(self.maps[('groups', player)]['text'] + '\n\n') + if self.whirlpools: + # whirlpools + 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","whirlpools",entry['entrance']), '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', self.world.fish.translate("meta","whirlpools",entry['exit'])) for entry in self.whirlpools.values()])) + if self.overworlds: # 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/OverworldShuffle.py b/OverworldShuffle.py index f1752dbd..8d242d2a 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -3,6 +3,7 @@ from collections import OrderedDict, defaultdict from DungeonGenerator import GenerationException from BaseClasses import OWEdge, WorldType, RegionType, Direction, Terrain, PolSlot, Entrance from Regions import mark_light_dark_world_regions +from source.overworld.EntranceShuffle2 import connect_simple from OWEdges import OWTileRegions, OWEdgeGroups, OWEdgeGroupsTerrain, OWExitTypes, OpenStd, parallel_links, IsParallel from OverworldGlitchRules import create_owg_connections from Utils import bidict @@ -278,16 +279,24 @@ def link_overworld(world, player): connect_simple(world, from_whirlpool, to_region, player) connect_simple(world, to_whirlpool, from_region, player) else: + def connect_whirlpool(from_whirlpool, to_whirlpool): + (from_owid, from_name, from_region) = from_whirlpool + (to_owid, to_name, to_region) = to_whirlpool + connect_simple(world, from_name, to_region, player) + connect_simple(world, to_name, from_region, player) + world.owwhirlpools[player][next(i for i, v in enumerate(whirlpool_map) if v == to_owid)] = from_owid + world.owwhirlpools[player][next(i for i, v in enumerate(whirlpool_map) if v == from_owid)] = to_owid + connected_whirlpools.append(tuple((from_name, to_name))) + world.spoiler.set_whirlpool(from_name, to_name, 'both', player) + + whirlpool_map = [ 0x35, 0x0f, 0x15, 0x33, 0x12, 0x3f, 0x55, 0x7f ] whirlpool_candidates = [[],[]] + connected_whirlpools = [] 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] == '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) + connect_whirlpool((from_owid, from_whirlpool, from_region), (to_owid, to_whirlpool, to_region)) else: 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'] \ @@ -304,19 +313,27 @@ def link_overworld(world, player): whirlpool_candidates[1].append(tuple((to_owid, to_whirlpool, to_region))) # shuffle happens here - whirlpool_map = [ 0x35, 0x0f, 0x15, 0x33, 0x12, 0x3f, 0x55, 0x7f ] + if world.customizer: + custom_whirlpools = world.customizer.get_whirlpools() + if custom_whirlpools and player in custom_whirlpools: + custom_whirlpools = custom_whirlpools[player] + if 'two-way' in custom_whirlpools: + for whirlpools in whirlpool_candidates: + for whirlname1, whirlname2 in custom_whirlpools['two-way'].items(): + whirl1 = next((w for w in whirlpools if w[1] == whirlname1), None) + whirl2 = next((w for w in whirlpools if w[1] == whirlname2), None) + if whirl1 and whirl2: + whirlpools.remove(whirl1) + whirlpools.remove(whirl2) + connect_whirlpool(whirl1, whirl2) + elif whirl1 != whirl2 or not any(w for w in connected_whirlpools if (whirlname1 in w) and (whirlname2 in w)): + raise GenerationException('Attempting to connect whirlpools not in same pool: \'%s\' <-> \'%s\'', whirl1, whirl2) for whirlpools in whirlpool_candidates: random.shuffle(whirlpools) while len(whirlpools): if len(whirlpools) % 2 == 1: x=0 - from_owid, from_whirlpool, from_region = whirlpools.pop() - to_owid, to_whirlpool, to_region = whirlpools.pop() - connect_simple(world, from_whirlpool, to_region, player) - connect_simple(world, to_whirlpool, from_region, player) - world.owwhirlpools[player][next(i for i, v in enumerate(whirlpool_map) if v == to_owid)] = from_owid - world.owwhirlpools[player][next(i for i, v in enumerate(whirlpool_map) if v == from_owid)] = to_owid - world.spoiler.set_overworld(from_whirlpool, to_whirlpool, 'both', player) + connect_whirlpool(whirlpools.pop(), whirlpools.pop()) # layout shuffle logging.getLogger('').debug('Shuffling overworld layout') @@ -337,7 +354,7 @@ def link_overworld(world, player): world.owsectors[player] = build_sectors(world, player) else: - if world.owKeepSimilar[player] and world.owShuffle[player] in ['vanilla', 'parallel']: + if world.owKeepSimilar[player] and world.owShuffle[player] == 'parallel': for exitname, destname in parallelsimilar_connections: connect_two_way(world, exitname, destname, player, connected_edges) @@ -345,10 +362,10 @@ def link_overworld(world, player): for exitname, destname in test_connections: connect_two_way(world, exitname, destname, player, connected_edges) - connect_custom(world, connected_edges, player) - # layout shuffle groups = adjust_edge_groups(world, trimmed_groups, edges_to_swap, player) + + connect_custom(world, connected_edges, groups, player) tries = 100 valid_layout = False @@ -564,21 +581,100 @@ def link_overworld(world, player): s[0x3a],s[0x3b],s[0x3c], s[0x3f]) world.spoiler.set_map('flute', text_output, new_spots, player) -def connect_custom(world, connected_edges, player): - if hasattr(world, 'custom_overworld') and world.custom_overworld[player]: - for edgename1, edgename2 in world.custom_overworld[player]: - if edgename1 in connected_edges or edgename2 in connected_edges: - owedge1 = world.check_for_owedge(edgename1, player) - owedge2 = world.check_for_owedge(edgename2, player) - if owedge1.dest is not None and owedge1.dest.name == owedge2.name: - continue # if attempting to connect a pair that was already connected earlier, allow it to continue - raise RuntimeError('Invalid plando connection: rule violation based on current settings') - connect_two_way(world, edgename1, edgename2, player, connected_edges) - if world.owKeepSimilar[player]: #TODO: If connecting an edge that belongs to a similar pair, the remaining edges need to get connected automatically +def connect_custom(world, connected_edges, groups, player): + def remove_pair_from_pool(edgename1, edgename2): + def add_to_unresolved(forward_set, back_set): + if len(forward_set) > 1: + if edgename1 in forward_set: + forward_set.remove(edgename1) + back_set.remove(edgename2) + else: + back_set.remove(edgename1) + forward_set.remove(edgename2) + unresolved_similars.append(tuple((forward_set, back_set))) + for forward_pool, back_pool in groups: continue + if len(forward_pool[0]) == 1: + if [edgename1] in forward_pool: + if [edgename2] in back_pool: + forward_pool.remove([edgename1]) + back_pool.remove([edgename2]) + return + else: + break + elif [edgename1] in back_pool: + if [edgename2] in forward_pool: + back_pool.remove([edgename1]) + forward_pool.remove([edgename2]) + return + else: + break + else: + forward_similar = next((x for x in forward_pool if edgename1 in x), None) + if forward_similar: + back_similar = next((x for x in back_pool if edgename2 in x), None) + if back_similar: + forward_pool.remove(forward_similar) + back_pool.remove(back_similar) + add_to_unresolved(forward_similar, back_similar) + return + else: + break + else: + back_similar = next((x for x in back_pool if edgename1 in x), None) + if back_similar: + forward_similar = next((x for x in forward_pool if edgename2 in x), None) + if forward_similar: + back_pool.remove(forward_similar) + forward_pool.remove(back_similar) + add_to_unresolved(forward_similar, back_similar) + return + else: + break + for pair in unresolved_similars: + forward_set, back_set = pair + if edgename1 in forward_set: + if edgename2 in back_set: + unresolved_similars.remove(pair) + add_to_unresolved(forward_set, back_set) + return + else: + break + else: + if edgename1 in back_set: + if edgename2 in forward_set: + unresolved_similars.remove(pair) + add_to_unresolved(forward_set, back_set) + return + else: + break + raise GenerationException('Could not find both OW edges in same pool: \'%s\' <-> \'%s\'', edgename1, edgename2) -def connect_simple(world, exitname, regionname, player): - world.get_entrance(exitname, player).connect(world.get_region(regionname, player)) + if world.customizer: + custom_edges = world.customizer.get_owedges() + if custom_edges and player in custom_edges: + custom_edges = custom_edges[player] + if 'two-way' in custom_edges: + unresolved_similars = [] + for edgename1, edgename2 in custom_edges['two-way'].items(): + edge1 = world.check_for_owedge(edgename1, player) + edge2 = world.check_for_owedge(edgename2, player) + if edgename1 not in connected_edges and edgename2 not in connected_edges: + # attempt connection + remove_pair_from_pool(edgename1, edgename2) + connect_two_way(world, edgename1, edgename2, player, connected_edges) + # resolve parallel + if (world.owShuffle[player] == 'parallel' and + (edgename1 in parallel_links.keys() or edgename1 in parallel_links.inverse.keys())): + parallel_forward_edge = parallel_links[edgename1] if edgename1 in parallel_links.keys() else parallel_links.inverse[edgename1][0] + parallel_back_edge = parallel_links[edgename2] if edgename2 in parallel_links.keys() else parallel_links.inverse[edgename2][0] + remove_pair_from_pool(parallel_forward_edge, parallel_back_edge) + elif not edge1.dest or not edge2.dest or edge1.dest.name != edgename2 or edge2.dest.name != edgename1: + raise GenerationException('OW Edge already connected: \'%s\' <-> \'%s\'', edgename1, edgename2) + # connect leftover similars + for forward_pool, back_pool in unresolved_similars: + for (forward_edge, back_edge) in zip(forward_pool, back_pool): + connect_two_way(world, forward_edge, back_edge, player, connected_edges) def connect_two_way(world, edgename1, edgename2, player, connected_edges=None): edge1 = world.get_entrance(edgename1, player) diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index 7eb8a31c..58c9494b 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -196,6 +196,16 @@ class CustomSettings(object): return self.file_source['advanced_placements'] return None + def get_owedges(self): + if 'ow-edges' in self.file_source: + return self.file_source['ow-edges'] + return None + + def get_whirlpools(self): + if 'ow-whirlpools' in self.file_source: + return self.file_source['ow-whirlpools'] + return None + def get_owtileflips(self): if 'ow-tileflips' in self.file_source: return self.file_source['ow-tileflips'] @@ -355,21 +365,40 @@ class CustomSettings(object): placements[location.player][location.name] = location.item.name def record_overworld(self, world): + self.world_rep['ow-edges'] = edges = {} + self.world_rep['ow-whirlpools'] = whirlpools = {} self.world_rep['ow-tileflips'] = flips = {} + self.world_rep['ow-flutespots'] = flute = {} for p in self.player_range: + connections = edges[p] = {} + connections['two-way'] = {} + connections['one-way'] = {} + whirlconnects = whirlpools[p] = {} + whirlconnects['two-way'] = {} + whirlconnects['one-way'] = {} + # tile flips if p in world.owswaps and len(world.owswaps[p][0]) > 0: flips[p] = {} flips[p]['force_flip'] = list(HexInt(f) for f in world.owswaps[p][0] if f < 0x40 or f >= 0x80) flips[p]['force_flip'].sort() flips[p]['undefined_chance'] = 0 - self.world_rep['ow-flutespots'] = flute = {} - for p in self.player_range: + # flute spots flute[p] = {} if p in world.owflutespots: flute[p]['force'] = list(HexInt(id) for id in sorted(world.owflutespots[p])) else: flute[p]['force'] = list(HexInt(id) for id in sorted(default_flute_connections)) flute[p]['forbid'] = [] + for key, data in world.spoiler.overworlds.items(): + player = data['player'] if 'player' in data else 1 + connections = edges[player] + sub = 'two-way' if data['direction'] == 'both' else 'one-way' + connections[sub][data['entrance']] = data['exit'] + for key, data in world.spoiler.whirlpools.items(): + player = data['player'] if 'player' in data else 1 + whirlconnects = whirlconnects[player] + sub = 'two-way' if data['direction'] == 'both' else 'one-way' + whirlconnects[sub][data['entrance']] = data['exit'] def record_entrances(self, world): self.world_rep['entrances'] = entrances = {}