Add ow_layout and ow_parallel settings

This commit is contained in:
Catobat
2025-12-23 15:01:52 +01:00
parent 452981ffe2
commit 080f3b1cca
22 changed files with 164 additions and 84 deletions

View File

@@ -20,11 +20,12 @@ from source.overworld.EntranceData import door_addresses
class World(object): class World(object):
def __init__(self, players, owShuffle, owCrossed, owMixed, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments, def __init__(self, players, owLayout, owParallel, owCrossed, owMixed, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments,
timer, progressive, goal, algorithm, accessibility, shuffle_ganon, custom, customitemarray, hints, spoiler_mode): timer, progressive, goal, algorithm, accessibility, shuffle_ganon, custom, customitemarray, hints, spoiler_mode):
self.players = players self.players = players
self.teams = 1 self.teams = 1
self.owShuffle = owShuffle.copy() self.owLayout = owLayout.copy()
self.owParallel = owParallel.copy()
self.owTerrain = {} self.owTerrain = {}
self.owKeepSimilar = {} self.owKeepSimilar = {}
self.owMixed = owMixed.copy() self.owMixed = owMixed.copy()
@@ -3041,7 +3042,8 @@ class Spoiler(object):
'bow_mode': self.world.bow_mode, 'bow_mode': self.world.bow_mode,
'goal': self.world.goal, 'goal': self.world.goal,
'custom_goals': self.world.custom_goals, 'custom_goals': self.world.custom_goals,
'ow_shuffle': self.world.owShuffle, 'ow_layout': self.world.owLayout,
'ow_parallel': self.world.owParallel,
'ow_terrain': self.world.owTerrain, 'ow_terrain': self.world.owTerrain,
'ow_crossed': self.world.owCrossed, 'ow_crossed': self.world.owCrossed,
'ow_keepsimilar': self.world.owKeepSimilar, 'ow_keepsimilar': self.world.owKeepSimilar,
@@ -3312,11 +3314,12 @@ class Spoiler(object):
outfile.write('Enemy Drop Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['dropshuffle'][player]) outfile.write('Enemy Drop Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['dropshuffle'][player])
outfile.write('Take Any Caves:'.ljust(line_width) + '%s\n' % self.metadata['take_any'][player]) outfile.write('Take Any Caves:'.ljust(line_width) + '%s\n' % self.metadata['take_any'][player])
outfile.write('\n') outfile.write('\n')
outfile.write('Overworld Layout Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_shuffle'][player]) outfile.write('Overworld Layout Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_layout'][player])
if self.metadata['ow_shuffle'][player] != 'vanilla': if self.metadata['ow_layout'][player] != 'vanilla':
outfile.write('Parallel OW:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_parallel'][player]))
outfile.write('Free Terrain:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_terrain'][player])) outfile.write('Free Terrain:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_terrain'][player]))
outfile.write('Crossed OW:'.ljust(line_width) + '%s\n' % self.metadata['ow_crossed'][player]) outfile.write('Crossed OW:'.ljust(line_width) + '%s\n' % self.metadata['ow_crossed'][player])
if self.metadata['ow_shuffle'][player] != 'vanilla' or self.metadata['ow_crossed'][player] != 'none': if self.metadata['ow_layout'][player] != 'vanilla' or self.metadata['ow_crossed'][player] != 'none':
outfile.write('Keep Similar OW Edges Together:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_keepsimilar'][player])) outfile.write('Keep Similar OW Edges Together:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_keepsimilar'][player]))
outfile.write('OW Tile Flip (Mixed):'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_mixed'][player])) outfile.write('OW Tile Flip (Mixed):'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_mixed'][player]))
outfile.write('Whirlpool Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_whirlpool'][player])) outfile.write('Whirlpool Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_whirlpool'][player]))
@@ -3728,8 +3731,8 @@ boss_mode = {"none": 0, "simple": 1, "full": 2, "chaos": 3, 'random': 3, 'unique
# byte 10: settings_version # byte 10: settings_version
# byte 11: OOOT WCCC (OWR layout, free terrain, whirlpools, OWR crossed) # byte 11: POOT WCCC (parallel, OWR layout, free terrain, whirlpools, OWR crossed)
or_mode = {"vanilla": 0, "parallel": 1, "full": 2} orlayout_mode = {"vanilla": 0, "grid": 1, "wild": 2}
orcrossed_mode = {"none": 0, "polar": 1, "grouped": 2, "unrestricted": 4} orcrossed_mode = {"none": 0, "polar": 1, "grouped": 2, "unrestricted": 4}
# byte 12: KMBQ FF?? (keep similar, mixed/tile flip, bonk drops, follower quests, flute spots) # byte 12: KMBQ FF?? (keep similar, mixed/tile flip, bonk drops, follower quests, flute spots)
@@ -3795,7 +3798,7 @@ class Settings(object):
settings_version, settings_version,
(or_mode[w.owShuffle[p]] << 5) | (0x10 if w.owTerrain[p] else 0) (0x80 if w.owParallel[p] else 0) | (orlayout_mode[w.owLayout[p]] << 5) | (0x10 if w.owTerrain[p] else 0)
| (0x08 if w.owWhirlpoolShuffle[p] else 0) | orcrossed_mode[w.owCrossed[p]], | (0x08 if w.owWhirlpoolShuffle[p] else 0) | orcrossed_mode[w.owCrossed[p]],
(0x80 if w.owKeepSimilar[p] else 0) | (0x40 if w.owMixed[p] else 0) (0x80 if w.owKeepSimilar[p] else 0) | (0x40 if w.owMixed[p] else 0)
@@ -3877,7 +3880,8 @@ class Settings(object):
args.algorithm = r(algo_mode)[(settings[9] & 0x38) >> 3] args.algorithm = r(algo_mode)[(settings[9] & 0x38) >> 3]
args.shufflebosses[p] = r(boss_mode)[(settings[9] & 0x07)] args.shufflebosses[p] = r(boss_mode)[(settings[9] & 0x07)]
args.ow_shuffle[p] = r(or_mode)[(settings[11] & 0xE0) >> 5] args.ow_parallel[p] = True if settings[11] & 0x80 else False
args.ow_layout[p] = r(orlayout_mode)[(settings[11] & 0x60) >> 5]
args.ow_terrain[p] = True if settings[11] & 0x10 else False args.ow_terrain[p] = True if settings[11] & 0x10 else False
args.ow_whirlpool[p] = True if settings[11] & 0x08 else False args.ow_whirlpool[p] = True if settings[11] & 0x08 else False
args.ow_crossed[p] = r(orcrossed_mode)[(settings[11] & 0x07)] args.ow_crossed[p] = r(orcrossed_mode)[(settings[11] & 0x07)]

19
CLI.py
View File

@@ -120,6 +120,16 @@ def parse_cli(argv, no_defaults=False):
ret.take_any = 'random' if ret.take_any == 'none' else ret.take_any ret.take_any = 'random' if ret.take_any == 'none' else ret.take_any
ret.keyshuffle = 'universal' ret.keyshuffle = 'universal'
if ret.ow_unparallel:
ret.ow_parallel = False
if ret.ow_shuffle == 'parallel':
ret.ow_layout = 'wild'
ret.ow_parallel = True
elif ret.ow_shuffle == 'full':
ret.ow_layout = 'wild'
ret.ow_parallel = False
if player_num: if player_num:
defaults = copy.deepcopy(ret) defaults = copy.deepcopy(ret)
for player in range(1, player_num + 1): for player in range(1, player_num + 1):
@@ -130,8 +140,8 @@ def parse_cli(argv, no_defaults=False):
for k, v in playersettings.items(): for k, v in playersettings.items():
setattr(playerargs, k, v) setattr(playerargs, k, v)
for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', 'ow_shuffle', for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', 'ow_shuffle', 'ow_layout',
'ow_terrain', 'ow_crossed', 'ow_keepsimilar', 'ow_mixed', 'ow_whirlpool', 'ow_fluteshuffle', 'ow_parallel', 'ow_terrain', 'ow_crossed', 'ow_keepsimilar', 'ow_mixed', 'ow_whirlpool', 'ow_fluteshuffle',
'flute_mode', 'bow_mode', 'take_any', 'boots_hint', 'shuffle_followers', 'flute_mode', 'bow_mode', 'take_any', 'boots_hint', 'shuffle_followers',
'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid', 'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid',
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'prizeshuffle', 'startinventory', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'prizeshuffle', 'startinventory',
@@ -193,7 +203,10 @@ def parse_settings():
# Shuffle Ganon defaults to TRUE # Shuffle Ganon defaults to TRUE
"openpyramid": "auto", "openpyramid": "auto",
"shuffleganon": True, "shuffleganon": True,
"ow_shuffle": "vanilla", "ow_shuffle": "vanilla", # for backwards compatibility
"ow_layout": "vanilla",
"ow_parallel": True,
"ow_unparallel": False,
"ow_terrain": False, "ow_terrain": False,
"ow_crossed": "none", "ow_crossed": "none",
"ow_keepsimilar": False, "ow_keepsimilar": False,

View File

@@ -432,7 +432,7 @@ def init_world(args, fish):
customized.load_yaml(args.customizer) customized.load_yaml(args.customizer)
customized.adjust_args(args, False) customized.adjust_args(args, False)
world = World(args.multi, args.ow_shuffle, args.ow_crossed, args.ow_mixed, args.shuffle, args.door_shuffle, args.logic, args.mode, args.swords, world = World(args.multi, args.ow_layout, args.ow_parallel, args.ow_crossed, args.ow_mixed, args.shuffle, args.door_shuffle, args.logic, args.mode, args.swords,
args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm, args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm,
args.accessibility, args.shuffleganon, args.custom, args.customitemarray, args.hints, args.spoiler) args.accessibility, args.shuffleganon, args.custom, args.customitemarray, args.hints, args.spoiler)
@@ -725,7 +725,7 @@ def set_starting_inventory(world, args):
def copy_world(world): def copy_world(world):
# ToDo: Not good yet # ToDo: Not good yet
ret = World(world.players, world.owShuffle, world.owCrossed, world.owMixed, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords, ret = World(world.players, world.owLayout, world.owParallel, world.owCrossed, world.owMixed, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords,
world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm,
world.accessibility, world.shuffle_ganon, world.custom, world.customitemarray, world.hints, world.spoiler_mode) world.accessibility, world.shuffle_ganon, world.custom, world.customitemarray, world.hints, world.spoiler_mode)
ret.teams = world.teams ret.teams = world.teams
@@ -946,7 +946,7 @@ def copy_world(world):
def copy_world_premature(world, player, create_flute_exits=True): def copy_world_premature(world, player, create_flute_exits=True):
# ToDo: Not good yet # ToDo: Not good yet
ret = World(world.players, world.owShuffle, world.owCrossed, world.owMixed, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords, ret = World(world.players, world.owLayout, world.owParallel, world.owCrossed, world.owMixed, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords,
world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm,
world.accessibility, world.shuffle_ganon, world.custom, world.customitemarray, world.hints, world.spoiler_mode) world.accessibility, world.shuffle_ganon, world.custom, world.customitemarray, world.hints, world.spoiler_mode)
ret.teams = world.teams ret.teams = world.teams

View File

@@ -144,7 +144,7 @@ def link_overworld(world, player):
parallel_links_new = {**dict(parallel_links_new), **dict({e:p[0] for e, p in parallel_links_new.inverse.items()})} parallel_links_new = {**dict(parallel_links_new), **dict({e:p[0] for e, p in parallel_links_new.inverse.items()})}
connected_edges = [] connected_edges = []
if world.owShuffle[player] != 'vanilla': if world.owLayout[player] != 'vanilla':
trimmed_groups = remove_reserved(world, trimmed_groups, connected_edges, player) trimmed_groups = remove_reserved(world, trimmed_groups, connected_edges, player)
trimmed_groups = reorganize_groups(world, trimmed_groups, player) trimmed_groups = reorganize_groups(world, trimmed_groups, player)
@@ -264,10 +264,10 @@ def link_overworld(world, player):
s[0x30], s[0x35], s[0x30], s[0x35],
s[0x41], s[0x3a],s[0x3b],s[0x3c], s[0x3f]) s[0x41], s[0x3a],s[0x3b],s[0x3c], s[0x3f])
world.spoiler.set_map('groups', text_output, ow_crossed_tiles, player) world.spoiler.set_map('groups', text_output, ow_crossed_tiles, player)
elif limited_crossed > -1 or (world.owShuffle[player] == 'vanilla' and world.owCrossed[player] == 'unrestricted'): elif limited_crossed > -1 or (world.owLayout[player] == 'vanilla' and world.owCrossed[player] == 'unrestricted'):
crossed_candidates = list() crossed_candidates = list()
for group in trimmed_groups.keys(): for group in trimmed_groups.keys():
(mode, wrld, dir, terrain, parallel, count, _) = group (mode, wrld, _, terrain, parallel, _, _) = group
if wrld == WorldType.Light and mode != OpenStd.Standard: if wrld == WorldType.Light and mode != OpenStd.Standard:
for (forward_set, back_set) in zip(trimmed_groups[group][0], trimmed_groups[group][1]): for (forward_set, back_set) in zip(trimmed_groups[group][0], trimmed_groups[group][1]):
if forward_set[0] in parallel_links_new: if forward_set[0] in parallel_links_new:
@@ -278,7 +278,7 @@ def link_overworld(world, player):
combine_set = forward_combine+back_combine combine_set = forward_combine+back_combine
skip_forward = False skip_forward = False
if world.owShuffle[player] == 'vanilla': if world.owLayout[player] == 'vanilla':
if any(edge in force_crossed for edge in combine_set): if any(edge in force_crossed for edge in combine_set):
if not any(edge in force_noncrossed for edge in combine_set): if not any(edge in force_noncrossed for edge in combine_set):
if any(edge in force_crossed for edge in forward_combine): if any(edge in force_crossed for edge in forward_combine):
@@ -412,7 +412,7 @@ def link_overworld(world, player):
# layout shuffle # layout shuffle
logging.getLogger('').debug('Shuffling overworld layout') logging.getLogger('').debug('Shuffling overworld layout')
if world.owShuffle[player] == 'vanilla': if world.owLayout[player] == 'vanilla':
# apply outstanding flips # apply outstanding flips
trimmed_groups = performSwap(trimmed_groups, edges_to_swap) trimmed_groups = performSwap(trimmed_groups, edges_to_swap)
assert len(edges_to_swap) == 0, 'Not all edges were flipped successfully: ' + ', '.join(edges_to_swap) assert len(edges_to_swap) == 0, 'Not all edges were flipped successfully: ' + ', '.join(edges_to_swap)
@@ -425,8 +425,10 @@ def link_overworld(world, player):
assert len(forward_set) == len(back_set) assert len(forward_set) == len(back_set)
for (forward_edge, back_edge) in zip(forward_set, back_set): for (forward_edge, back_edge) in zip(forward_set, back_set):
connect_two_way(world, forward_edge, back_edge, player, connected_edges) connect_two_way(world, forward_edge, back_edge, player, connected_edges)
elif world.owLayout[player] == 'grid':
raise NotImplementedError()
else: else:
if world.owKeepSimilar[player] and world.owShuffle[player] == 'parallel': if world.owKeepSimilar[player] and world.owParallel[player]:
for exitname, destname in parallelsimilar_connections: for exitname, destname in parallelsimilar_connections:
connect_two_way(world, exitname, destname, player, connected_edges) connect_two_way(world, exitname, destname, player, connected_edges)
@@ -822,7 +824,7 @@ def connect_custom(world, connected_edges, groups, forced, player):
remove_pair_from_pool(edge1.name, edge2.name, is_crossed) remove_pair_from_pool(edge1.name, edge2.name, is_crossed)
connect_two_way(world, edge1.name, edge2.name, player, connected_edges) connect_two_way(world, edge1.name, edge2.name, player, connected_edges)
# resolve parallel # resolve parallel
if world.owShuffle[player] == 'parallel' and edge1.name in parallel_links_new: if world.owParallel[player] and edge1.name in parallel_links_new:
parallel_forward_edge = parallel_links_new[edge1.name] parallel_forward_edge = parallel_links_new[edge1.name]
parallel_back_edge = parallel_links_new[edge2.name] parallel_back_edge = parallel_links_new[edge2.name]
if validate_crossed_allowed(parallel_forward_edge, parallel_back_edge, is_crossed): if validate_crossed_allowed(parallel_forward_edge, parallel_back_edge, is_crossed):
@@ -838,7 +840,7 @@ def connect_custom(world, connected_edges, groups, forced, player):
connect_two_way(world, forward_edge, back_edge, player, connected_edges) connect_two_way(world, forward_edge, back_edge, player, connected_edges)
else: else:
raise GenerationException('Violation of force crossed rules on unresolved similars: \'%s\' <-> \'%s\'', forward_edge, back_edge) raise GenerationException('Violation of force crossed rules on unresolved similars: \'%s\' <-> \'%s\'', forward_edge, back_edge)
if world.owShuffle[player] == 'parallel' and forward_edge in parallel_links_new: if world.owParallel[player] and forward_edge in parallel_links_new:
parallel_forward_edge = parallel_links_new[forward_edge] parallel_forward_edge = parallel_links_new[forward_edge]
parallel_back_edge = parallel_links_new[back_edge] parallel_back_edge = parallel_links_new[back_edge]
if not validate_crossed_allowed(parallel_forward_edge, parallel_back_edge, is_crossed): if not validate_crossed_allowed(parallel_forward_edge, parallel_back_edge, is_crossed):
@@ -868,7 +870,7 @@ def connect_two_way(world, edgename1, edgename2, player, connected_edges=None):
x.dest = y x.dest = y
y.dest = x y.dest = x
if world.owShuffle[player] != 'vanilla' or world.owMixed[player] or world.owCrossed[player] != 'none': if world.owLayout[player] != 'vanilla' or world.owMixed[player] or world.owCrossed[player] != 'none':
world.spoiler.set_overworld(edgename2, edgename1, 'both', player) world.spoiler.set_overworld(edgename2, edgename1, 'both', player)
if connected_edges is not None: if connected_edges is not None:
@@ -876,7 +878,7 @@ def connect_two_way(world, edgename1, edgename2, player, connected_edges=None):
connected_edges.append(edgename2) connected_edges.append(edgename2)
# connecting parallel connections # connecting parallel connections
if world.owShuffle[player] in ['vanilla', 'parallel']: if world.owLayout[player] == 'vanilla' or world.owParallel[player]:
if edgename1 in parallel_links_new: if edgename1 in parallel_links_new:
try: try:
parallel_forward_edge = parallel_links_new[edgename1] parallel_forward_edge = parallel_links_new[edgename1]
@@ -965,7 +967,7 @@ def determine_forced_flips(world, tile_ow_groups, do_grouped, player):
for whirl1, whirl2 in custom_whirlpools.items(): for whirl1, whirl2 in custom_whirlpools.items():
if [whirlpool_map[whirl1], whirlpool_map[whirl2]] not in merged_owids and should_merge_group(whirlpool_map[whirl1], whirlpool_map[whirl2]): if [whirlpool_map[whirl1], whirlpool_map[whirl2]] not in merged_owids and should_merge_group(whirlpool_map[whirl1], whirlpool_map[whirl2]):
merged_owids.append([whirlpool_map[whirl1], whirlpool_map[whirl2]]) merged_owids.append([whirlpool_map[whirl1], whirlpool_map[whirl2]])
if world.owShuffle[player] != 'vanilla': if world.owLayout[player] != 'vanilla':
custom_edges = world.customizer.get_owedges() custom_edges = world.customizer.get_owedges()
if custom_edges and player in custom_edges: if custom_edges and player in custom_edges:
custom_edges = custom_edges[player] custom_edges = custom_edges[player]
@@ -1071,6 +1073,9 @@ def shuffle_tiles(world, groups, result_list, do_grouped, forced_flips, player):
exist_dw_regions.extend(dw_regions) exist_dw_regions.extend(dw_regions)
parity = [sum(group_parity[group[0][0]][i] for group in groups if group not in removed) for i in range(6)] parity = [sum(group_parity[group[0][0]][i] for group in groups if group not in removed) for i in range(6)]
if world.owLayout[player] == 'grid':
parity[1] = 0
parity[2] = 0
if not world.owKeepSimilar[player]: if not world.owKeepSimilar[player]:
parity[1] += 2*parity[2] parity[1] += 2*parity[2]
parity[2] = 0 parity[2] = 0
@@ -1164,12 +1169,16 @@ def define_tile_groups(world, do_grouped, player):
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'simple', 'restricted', 'district']: if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'simple', 'restricted', 'district']:
merge_groups([[0x05, 0x07]]) merge_groups([[0x05, 0x07]])
# all non-parallel screens # special screens
if world.owShuffle[player] == 'vanilla' and (world.owCrossed[player] == 'none' or do_grouped): if world.owLayout[player] != 'wild' and (world.owCrossed[player] == 'none' or do_grouped):
merge_groups([[0x00, 0x2d, 0x80], [0x0f, 0x81], [0x1a, 0x1b], [0x28, 0x29], [0x30, 0x3a]]) merge_groups([[0x00, 0x2d, 0x80], [0x0f, 0x81]])
# remaining non-parallel edges
if world.owLayout[player] == 'vanilla' and (world.owCrossed[player] == 'none' or do_grouped):
merge_groups([[0x1a, 0x1b], [0x28, 0x29], [0x30, 0x3a]])
# special case: non-parallel keep similar # special case: non-parallel keep similar
if world.owShuffle[player] == 'parallel' and world.owKeepSimilar[player] and (world.owCrossed[player] == 'none' or do_grouped): if world.owLayout[player] == 'wild' and world.owParallel[player] and world.owKeepSimilar[player] and (world.owCrossed[player] == 'none' or do_grouped):
merge_groups([[0x28, 0x29]]) merge_groups([[0x28, 0x29]])
# whirlpool screens # whirlpool screens
@@ -1225,7 +1234,7 @@ def reorganize_groups(world, groups, player):
new_group[0] = None new_group[0] = None
if world.owTerrain[player]: if world.owTerrain[player]:
new_group[3] = None new_group[3] = None
if world.owShuffle[player] != 'parallel': if not world.owParallel[player]:
new_group[4] = None new_group[4] = None
if not world.owKeepSimilar[player]: if not world.owKeepSimilar[player]:
new_group[5] = None new_group[5] = None

View File

@@ -24,7 +24,7 @@ def main(args):
start_time = time.process_time() start_time = time.process_time()
# initialize the world # initialize the world
world = World(1, 'vanilla', 'vanilla', 'vanilla', 'vanilla', 'noglitches', 'standard', 'normal', 'none', 'on', 'ganon', 'freshness', False, False, False, False, False, False, None, False) world = World(1, 'vanilla', True, 'vanilla', 'vanilla', 'vanilla', 'noglitches', 'standard', 'normal', 'none', 'on', 'ganon', 'freshness', False, False, False, False, False, False, None, False)
world.player_names[1].append("Player 1") world.player_names[1].append("Player 1")
logger = logging.getLogger('') logger = logging.getLogger('')
@@ -157,9 +157,9 @@ def prefill_world(world, plando, text_patches):
elif line.startswith('!goal'): elif line.startswith('!goal'):
_, goalstr = line.split(':', 1) _, goalstr = line.split(':', 1)
world.goal = {1: goalstr.strip()} world.goal = {1: goalstr.strip()}
elif line.startswith('!owShuffle'): elif line.startswith('!owLayout'):
_, modestr = line.split(':', 1) _, modestr = line.split(':', 1)
world.owShuffle = {1: modestr.strip()} world.owLayout = {1: modestr.strip()}
elif line.startswith('!owCrossed'): elif line.startswith('!owCrossed'):
_, modestr = line.split(':', 1) _, modestr = line.split(':', 1)
world.owCrossed = {1: modestr.strip()} world.owCrossed = {1: modestr.strip()}

View File

@@ -244,7 +244,7 @@ Ganons Tower - Validation Chest: Nothing
Ganon: Triforce Ganon: Triforce
# set Overworld connections (lines starting with $, separate edges with =) # set Overworld connections (lines starting with $, separate edges with =)
!owShuffle: parallel !owLayout: wild
#!owMixed: true # Mixed OW not supported yet #!owMixed: true # Mixed OW not supported yet
!owCrossed: none !owCrossed: none
!owKeepSimilar: true !owKeepSimilar: true

View File

@@ -132,20 +132,24 @@ Note: These changes do impact the logic. If you use `CodeTracker`, these Inverte
Only settings specifically added by this Overworld Shuffle fork are found here. All door and entrance randomizer settings are supported. See their [readme](https://github.com/Aerinon/ALttPDoorRandomizer/blob/master/README.md) Only settings specifically added by this Overworld Shuffle fork are found here. All door and entrance randomizer settings are supported. See their [readme](https://github.com/Aerinon/ALttPDoorRandomizer/blob/master/README.md)
## Overworld Layout Shuffle (--ow_shuffle) ## Overworld Layout Shuffle (--ow_layout)
OW Edge Transitions are shuffled to create new world layouts. A brief visual representation of this can be viewed [here](https://zelda.codemann8.com/images/shared/ow-modes.gif). (This graphic also includes combinations of Crossed and Tile Flip) OW Edge Transitions are shuffled to create new world layouts. A brief visual representation of this can be viewed [here](https://zelda.codemann8.com/images/shared/ow-modes.gif). (This graphic also includes combinations of Crossed and Tile Flip)
### Vanilla ### Vanilla
OW Transitions are not shuffled. OW Transitions are not shuffled.
### Parallel ### Grid
OW Transitions are shuffled, but both worlds will have a matching layout, similar to that of vanilla. OW Screens are shuffled in such a way that they are still arranged on an 8x8 grid for each world like vanilla, and the OW Transitions are based on that arrangement.
### Full ### Wild
OW Transitions are shuffled within each world separately. OW Transitions are shuffled with no respect to geometric coherence.
## Parallel (--ow_unparallel to disable)
With OW Layout Shuffle, this forces both worlds to have a matching layout.
## Free Terrain (--ow_terrain) ## Free Terrain (--ow_terrain)
@@ -389,11 +393,17 @@ Districts are a concept originally conceived by Aerinon in the Door Randomizer,
Show the help message and exit. Show the help message and exit.
``` ```
--ow_shuffle <mode> --ow_layout <mode>
``` ```
For specifying the overworld layout shuffle you want as above. (default: vanilla) For specifying the overworld layout shuffle you want as above. (default: vanilla)
```
--ow_unparallel
```
With OW Layout Shuffle, this no longer forces both worlds to have a matching layout.
``` ```
--ow_terrain --ow_terrain
``` ```

15
Rom.py
View File

@@ -557,13 +557,10 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
# patch overworld edges # patch overworld edges
inverted_buffer = [0] * 0x82 inverted_buffer = [0] * 0x82
owMode = 0 owMode = 0
if world.owShuffle[player] != 'vanilla' or world.owCrossed[player] not in ['none', 'polar'] or world.owMixed[player]: if world.owLayout[player] != 'vanilla' or world.owCrossed[player] not in ['none', 'polar'] or world.owMixed[player]:
if world.owShuffle[player] == 'parallel': if world.owLayout[player] != 'vanilla':
owMode = 1 owMode = 1 if world.owParallel[player] else 2
elif world.owShuffle[player] == 'full': if world.owKeepSimilar[player] and (world.owLayout[player] != 'vanilla' or world.owCrossed[player] == 'unrestricted'):
owMode = 2
if world.owKeepSimilar[player] and (world.owShuffle[player] != 'vanilla' or world.owCrossed[player] == 'unrestricted'):
owMode |= 0x0100 owMode |= 0x0100
if world.owCrossed[player] != 'none' and (world.owCrossed[player] != 'polar' or world.owMixed[player]): if world.owCrossed[player] != 'none' and (world.owCrossed[player] != 'polar' or world.owMixed[player]):
owMode |= 0x0200 owMode |= 0x0200
@@ -2393,7 +2390,7 @@ def write_strings(rom, world, player, team):
if world.is_tile_swapped(0x18, player) or world.flute_mode[player] == 'active': if world.is_tile_swapped(0x18, player) or world.flute_mode[player] == 'active':
items_to_hint.remove(flute_item) items_to_hint.remove(flute_item)
flute_item = 'Ocarina (Activated)' flute_item = 'Ocarina (Activated)'
if world.owShuffle[player] != 'vanilla' or world.owMixed[player]: if world.owLayout[player] != 'vanilla' or world.owMixed[player]:
# Adding a guaranteed hint for the Flute in overworld shuffle. # Adding a guaranteed hint for the Flute in overworld shuffle.
this_location = world.find_items_not_key_only(flute_item, player) this_location = world.find_items_not_key_only(flute_item, player)
if this_location and this_location not in hinted_locations: if this_location and this_location not in hinted_locations:
@@ -2411,7 +2408,7 @@ def write_strings(rom, world, player, team):
random.shuffle(items_to_hint) random.shuffle(items_to_hint)
hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'district', 'swapped'] else 8 hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'district', 'swapped'] else 8
hint_count += 2 if world.doorShuffle[player] not in ['vanilla', 'basic'] else 0 hint_count += 2 if world.doorShuffle[player] not in ['vanilla', 'basic'] else 0
hint_count += 1 if world.owShuffle[player] != 'vanilla' or world.owCrossed[player] != 'none' or world.owMixed[player] else 0 hint_count += 1 if world.owLayout[player] != 'vanilla' or world.owCrossed[player] != 'none' or world.owMixed[player] else 0
while hint_count > 0 and len(items_to_hint) > 0: while hint_count > 0 and len(items_to_hint) > 0:
this_item = items_to_hint.pop(0) this_item = items_to_hint.pop(0)
this_location = world.find_items_not_key_only(this_item, player) this_location = world.find_items_not_key_only(this_item, player)

View File

@@ -234,7 +234,7 @@ You may define a list of items and a list of locations. Those items will be cons
### ow-edges ### ow-edges
This must be defined by player. Each player number should be listed with the appropriate sections and each of these players MUST have either `ow_shuffle` or `ow_crossed` enabled in the `settings` section in order for any values here to take effect. This section has two primary subsections: `two-way` and `groups`. This must be defined by player. Each player number should be listed with the appropriate sections and each of these players MUST have either `ow_layout` or `ow_crossed` enabled in the `settings` section in order for any values here to take effect. This section has two primary subsections: `two-way` and `groups`.
#### two-way #### two-way

View File

@@ -20,7 +20,8 @@ settings:
shuffle_followers: true shuffle_followers: true
shuffle: crossed shuffle: crossed
shufflelinks: true shufflelinks: true
ow_shuffle: parallel ow_layout: wild
ow_parallel: true
ow_terrain: true ow_terrain: true
ow_crossed: grouped ow_crossed: grouped
ow_keepsimilar: true ow_keepsimilar: true

View File

@@ -1,6 +1,7 @@
settings: settings:
1: 1:
ow_shuffle: full ow_layout: wild
ow_parallel: false
ow_keepsimilar: false ow_keepsimilar: false
ow-edges: ow-edges:
1: 1:

View File

@@ -1,6 +1,7 @@
settings: settings:
1: 1:
ow_shuffle: full ow_layout: wild
ow_parallel: false
ow_keepsimilar: false ow_keepsimilar: false
ow-edges: ow-edges:
1: 1:

View File

@@ -1,6 +1,7 @@
settings: settings:
1: 1:
ow_shuffle: full ow_layout: wild
ow_parallel: false
ow_keepsimilar: false ow_keepsimilar: false
ow-edges: ow-edges:
1: 1:

View File

@@ -1,6 +1,7 @@
settings: settings:
1: 1:
ow_shuffle: full ow_layout: wild
ow_parallel: false
ow_terrain: true ow_terrain: true
ow-edges: ow-edges:
1: 1:

View File

@@ -173,6 +173,22 @@
"full" "full"
] ]
}, },
"ow_layout": {
"choices": [
"vanilla",
"grid",
"wild"
]
},
"ow_parallel": {
"action": "store_true",
"help": "suppress",
"type": "bool"
},
"ow_unparallel": {
"action": "store_true",
"type": "bool"
},
"ow_terrain": { "ow_terrain": {
"action": "store_true", "action": "store_true",
"type": "bool" "type": "bool"

View File

@@ -234,14 +234,18 @@
"the entrances vanilla." "the entrances vanilla."
], ],
"ow_shuffle": [ "ow_shuffle": [
"Deprecated, use ow_layout and ow_unparallel instead."
],
"ow_layout": [
"This shuffles the layout of the overworld.", "This shuffles the layout of the overworld.",
"Vanilla: All overworld transitions are connected the same", "Vanilla: All overworld transitions are connected the same",
" way they were in the base game.", " way they were in the base game.",
"Parallel: Overworld transitions are shuffled, but both worlds", "Grid: OW Screens are arranged on 8x8 grids and OW Transitions",
" will have the same pattern/shape.", " work according to this arrangement.",
"Full: Overworld transitions are shuffled, but both worlds", "Wild: OW Transitions are shuffled with no respect to geometric coherence."
" will have an independent map shape."
], ],
"ow_unparallel": [
"With OW Layout Shuffle, this no longer forces both worlds to have a matching layout." ],
"ow_terrain": [ "ow_terrain": [
"With OW Layout Shuffle, this allows land and water edges to be connected." ], "With OW Layout Shuffle, this allows land and water edges to be connected." ],
"ow_crossed": [ "ow_crossed": [

View File

@@ -157,10 +157,12 @@
"randomizer.enemizer.enemylogic.allow_all": "Allow special enemies anywhere", "randomizer.enemizer.enemylogic.allow_all": "Allow special enemies anywhere",
"randomizer.overworld.overworldshuffle": "Layout Shuffle", "randomizer.overworld.layout": "Layout Shuffle",
"randomizer.overworld.overworldshuffle.vanilla": "Vanilla", "randomizer.overworld.layout.vanilla": "Vanilla",
"randomizer.overworld.overworldshuffle.parallel": "Parallel", "randomizer.overworld.layout.grid": "Grid",
"randomizer.overworld.overworldshuffle.full": "Full", "randomizer.overworld.layout.wild": "Wild",
"randomizer.overworld.parallel": "Keep Worlds Parallel",
"randomizer.overworld.terrain": "Free Terrain", "randomizer.overworld.terrain": "Free Terrain",

View File

@@ -1,13 +1,13 @@
{ {
"topOverworldFrame": {}, "topOverworldFrame": {},
"leftOverworldFrame": { "leftOverworldFrame": {
"overworldshuffle": { "layout": {
"type": "selectbox", "type": "selectbox",
"default": "vanilla", "default": "vanilla",
"options": [ "options": [
"vanilla", "vanilla",
"parallel", "grid",
"full" "wild"
] ]
}, },
"crossed": { "crossed": {
@@ -18,7 +18,10 @@
"grouped", "grouped",
"polar", "polar",
"unrestricted" "unrestricted"
] ],
"config": {
"pady": [16,0]
}
}, },
"mixed": { "mixed": {
"type": "checkbox", "type": "checkbox",
@@ -48,19 +51,17 @@
} }
}, },
"rightOverworldFrame": { "rightOverworldFrame": {
"parallel": {
"type": "checkbox",
"default": true
},
"terrain": { "terrain": {
"type": "checkbox", "type": "checkbox",
"default": false, "default": false
"config": {
"pady": [3,0]
}
}, },
"keepsimilar": { "keepsimilar": {
"type": "checkbox", "type": "checkbox",
"default": false, "default": false
"config": {
"pady": [6,0]
}
} }
} }
} }

View File

@@ -89,7 +89,8 @@ class CustomSettings(object):
args.mystery = True args.mystery = True
else: else:
settings = defaultdict(lambda: None, player_setting) settings = defaultdict(lambda: None, player_setting)
args.ow_shuffle[p] = get_setting(settings['ow_shuffle'], args.ow_shuffle[p]) args.ow_layout[p] = get_setting(settings['ow_layout'], args.ow_layout[p])
args.ow_parallel[p] = get_setting(settings['ow_parallel'], args.ow_parallel[p])
args.ow_terrain[p] = get_setting(settings['ow_terrain'], args.ow_terrain[p]) args.ow_terrain[p] = get_setting(settings['ow_terrain'], args.ow_terrain[p])
args.ow_crossed[p] = get_setting(settings['ow_crossed'], args.ow_crossed[p]) args.ow_crossed[p] = get_setting(settings['ow_crossed'], args.ow_crossed[p])
if args.ow_crossed[p] == 'chaos': if args.ow_crossed[p] == 'chaos':
@@ -135,6 +136,14 @@ class CustomSettings(object):
args.take_any[p] = 'random' if args.take_any[p] == 'none' else args.take_any[p] args.take_any[p] = 'random' if args.take_any[p] == 'none' else args.take_any[p]
args.keyshuffle[p] = 'universal' args.keyshuffle[p] = 'universal'
ow_shuffle = get_setting(settings['ow_shuffle'], args.ow_shuffle[p])
if ow_shuffle == 'parallel':
args.ow_layout = 'wild'
args.ow_parallel = True
elif ow_shuffle == 'full':
args.ow_layout = 'wild'
args.ow_parallel = False
args.mixed_travel[p] = get_setting(settings['mixed_travel'], args.mixed_travel[p]) args.mixed_travel[p] = get_setting(settings['mixed_travel'], args.mixed_travel[p])
args.standardize_palettes[p] = get_setting(settings['standardize_palettes'], args.standardize_palettes[p] = get_setting(settings['standardize_palettes'],
args.standardize_palettes[p]) args.standardize_palettes[p])
@@ -356,7 +365,8 @@ class CustomSettings(object):
self.world_rep['start_inventory'] = start_inv = {} self.world_rep['start_inventory'] = start_inv = {}
for p in self.player_range: for p in self.player_range:
settings_dict[p] = {} settings_dict[p] = {}
settings_dict[p]['ow_shuffle'] = world.owShuffle[p] settings_dict[p]['ow_layout'] = world.owLayout[p]
settings_dict[p]['ow_parallel'] = world.owParallel[p]
settings_dict[p]['ow_terrain'] = world.owTerrain[p] settings_dict[p]['ow_terrain'] = world.owTerrain[p]
settings_dict[p]['ow_crossed'] = world.owCrossed[p] settings_dict[p]['ow_crossed'] = world.owCrossed[p]
settings_dict[p]['ow_keepsimilar'] = world.owKeepSimilar[p] settings_dict[p]['ow_keepsimilar'] = world.owKeepSimilar[p]

View File

@@ -92,7 +92,8 @@ SETTINGSTOPROCESS = {
"bombbag": "bombbag" "bombbag": "bombbag"
}, },
"overworld": { "overworld": {
"overworldshuffle": "ow_shuffle", "layout": "ow_layout",
"parallel": "ow_parallel",
"terrain": "ow_terrain", "terrain": "ow_terrain",
"crossed": "ow_crossed", "crossed": "ow_crossed",
"keepsimilar": "ow_keepsimilar", "keepsimilar": "ow_keepsimilar",

View File

@@ -635,7 +635,7 @@ def do_dark_sanc(entrances, exits, avail):
forbidden.append('Links House') forbidden.append('Links House')
else: else:
forbidden.append('Big Bomb Shop') forbidden.append('Big Bomb Shop')
if avail.world.owShuffle[avail.player] == 'vanilla': if avail.world.owLayout[avail.player] == 'vanilla':
choices = [e for e in avail.world.districts[avail.player]['Northwest Dark World'].entrances if e not in forbidden and e in entrances] choices = [e for e in avail.world.districts[avail.player]['Northwest Dark World'].entrances if e not in forbidden and e in entrances]
else: else:
choices = [e for e in get_starting_entrances(avail) if e not in forbidden and e in entrances] choices = [e for e in get_starting_entrances(avail) if e not in forbidden and e in entrances]
@@ -679,7 +679,7 @@ def do_links_house(entrances, exits, avail, cross_world):
forbidden.append(links_house_vanilla) forbidden.append(links_house_vanilla)
forbidden.extend(Forbidden_Swap_Entrances) forbidden.extend(Forbidden_Swap_Entrances)
shuffle_mode = avail.world.shuffle[avail.player] shuffle_mode = avail.world.shuffle[avail.player]
if avail.world.owShuffle[avail.player] == 'vanilla': if avail.world.owLayout[avail.player] == 'vanilla':
# simple shuffle - # simple shuffle -
if shuffle_mode == 'simple': if shuffle_mode == 'simple':
avail.links_on_mountain = True # taken care of by the logic below avail.links_on_mountain = True # taken care of by the logic below
@@ -733,7 +733,7 @@ def do_links_house(entrances, exits, avail, cross_world):
# links on dm # links on dm
dm_spots = LH_DM_Connector_List.union(LH_DM_Exit_Forbidden) dm_spots = LH_DM_Connector_List.union(LH_DM_Exit_Forbidden)
if links_house in dm_spots and avail.world.owShuffle[avail.player] == 'vanilla': if links_house in dm_spots and avail.world.owLayout[avail.player] == 'vanilla':
if avail.links_on_mountain: if avail.links_on_mountain:
return # connector is fine return # connector is fine
logging.getLogger('').warning(f'Links House is placed in tight area and is now unhandled. Report any errors that occur from here.') logging.getLogger('').warning(f'Links House is placed in tight area and is now unhandled. Report any errors that occur from here.')

View File

@@ -120,8 +120,16 @@ def roll_settings(weights):
ret.accessibility = get_choice('accessibility') ret.accessibility = get_choice('accessibility')
ret.restrict_boss_items = get_choice('restrict_boss_items') ret.restrict_boss_items = get_choice('restrict_boss_items')
overworld_layout = get_choice('overworld_layout')
ret.ow_layout = overworld_layout if overworld_layout != 'none' else 'vanilla'
ret.ow_parallel = get_choice_bool('overworld_parallel')
overworld_shuffle = get_choice('overworld_shuffle') overworld_shuffle = get_choice('overworld_shuffle')
ret.ow_shuffle = overworld_shuffle if overworld_shuffle != 'none' else 'vanilla' if overworld_shuffle == 'parallel':
ret.ow_layout = 'wild'
ret.ow_parallel = True
elif overworld_shuffle == 'full':
ret.ow_layout = 'wild'
ret.ow_parallel = False
ret.ow_terrain = get_choice_bool('overworld_terrain') ret.ow_terrain = get_choice_bool('overworld_terrain')
valid_options = {'none': 'none', 'polar': 'polar', 'grouped': 'polar', 'chaos': 'unrestricted', 'unrestricted': 'unrestricted'} valid_options = {'none': 'none', 'polar': 'polar', 'grouped': 'polar', 'chaos': 'unrestricted', 'unrestricted': 'unrestricted'}
ret.ow_crossed = get_choice('overworld_crossed') ret.ow_crossed = get_choice('overworld_crossed')