diff --git a/.gitignore b/.gitignore index 907ddf87..fddeb322 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ weights/ /QUsb2Snes/ /output/ /enemizer/ +visualizations/ resources/user/* !resources/user/.gitkeep diff --git a/BaseClasses.py b/BaseClasses.py index 65f7e789..eda1e5df 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -26,11 +26,12 @@ from Versions import DRVersion, GKVersion, ORVersion 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): self.players = players self.teams = 1 - self.owShuffle = owShuffle.copy() + self.owLayout = owLayout.copy() + self.owParallel = owParallel.copy() self.owTerrain = {} self.owKeepSimilar = {} self.owMixed = owMixed.copy() @@ -38,6 +39,7 @@ class World(object): self.owCrossed = self.owCrossed if self.owCrossed != 'polar' or self.owMixed else 'none' self.owWhirlpoolShuffle = {} self.owFluteShuffle = {} + self.owFog = {} self.shuffle = shuffle.copy() self.doorShuffle = doorShuffle.copy() self.intensity = {} @@ -99,6 +101,9 @@ class World(object): self.owswaps = {} self.owcrossededges = {} self.owwhirlpools = {} + self.owgrid = {} + self.owlayoutmap_lw = {} + self.owlayoutmap_dw = {} self.owflutespots = {} self.owsectors = {} self.allow_flip_sanc = {} @@ -131,6 +136,7 @@ class World(object): set_player_attr('owswaps', [[],[],[]]) set_player_attr('owcrossededges', []) set_player_attr('owwhirlpools', []) + set_player_attr('owgrid', None) set_player_attr('owsectors', None) set_player_attr('allow_flip_sanc', False) set_player_attr('remote_items', False) @@ -1747,12 +1753,12 @@ class Entrance(object): def can_reach(self, state): # Destination Pickup OW Only No Ledges Can S&Q Allow Mirror - multi_step_locations = { 'Pyramid Crack': ('Big Bomb', True, True, False, True), - 'Missing Smith': ('Frog', True, False, True, True), - 'Middle Aged Man': ('Dark Blacksmith Ruins', True, False, True, True), - 'Dark Palace Button':('Kiki', True, False, False, False), - 'Old Man Drop Off': ('Lost Old Man', True, False, False, False), - 'Revealing Light': ('Suspicious Maiden', False, False, False, False) + multi_step_locations = { 'Pyramid Crack': ('Pick Up Big Bomb', True, True, False, True), + 'Missing Smith': ('Get Frog', True, False, True, True), + 'Middle Aged Man': ('Pick Up Purple Chest', True, False, True, True), + 'Dark Palace Button': ('Pick Up Kiki', True, False, False, False), + 'Old Man Drop Off': ('Escort Old Man', True, False, False, False), + 'Revealing Light': ('Maiden Rescued', False, False, False, False) } if self.name in multi_step_locations: @@ -1761,7 +1767,10 @@ class Entrance(object): multi_step_loc = multi_step_locations[self.name] if world.shuffle_followers[self.player]: multi_step_loc = (multi_step_loc[0], self.name == 'Pyramid Crack', multi_step_loc[2], True, True) - step_location = world.get_location(multi_step_loc[0], self.player) + step_location = world.find_items(multi_step_loc[0], self.player) + if len(step_location) == 0: + return False + step_location = step_location[0] if step_location.can_reach(state) and self.can_reach_thru(state, step_location, multi_step_loc[1], multi_step_loc[2], multi_step_loc[3], multi_step_loc[4]) and self.access_rule(state): if not self in state.path: path = state.path.get(step_location.parent_region, (step_location.parent_region.name, None)) @@ -2401,12 +2410,11 @@ class OWEdge(object): def __init__(self, player, name, owIndex, direction, terrain, edge_id, owSlotIndex=0xff): self.player = player self.name = name - self.type = DoorType.Open self.direction = direction self.terrain = terrain + self.parallel = None self.specialEntrance = False self.specialExit = False - self.deadEnd = False # rom properties self.owIndex = owIndex @@ -2437,7 +2445,6 @@ class OWEdge(object): self.worldType = WorldType.Dark # logical properties - # self.connected = False # combine with Dest? self.dest = None self.dependents = [] self.dead = False @@ -2454,9 +2461,6 @@ class OWEdge(object): def getTarget(self): return self.dest.specialID if self.dest.specialExit else self.dest.edge_id - def dead_end(self): - self.deadEnd = True - def coordInfo(self, midpoint, vram_loc): self.midpoint = midpoint self.vramLoc = vram_loc @@ -3095,13 +3099,15 @@ class Spoiler(object): 'bow_mode': self.world.bow_mode, 'goal': self.world.goal, '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_crossed': self.world.owCrossed, 'ow_keepsimilar': self.world.owKeepSimilar, 'ow_mixed': self.world.owMixed, 'ow_whirlpool': self.world.owWhirlpoolShuffle, 'ow_fluteshuffle': self.world.owFluteShuffle, + 'ow_fog': self.world.owFog, 'bonk_drops': self.world.shuffle_bonk_drops, 'shuffle_followers': self.world.shuffle_followers, 'shuffle': self.world.shuffle, @@ -3381,15 +3387,18 @@ class Spoiler(object): 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('\n') - outfile.write('Overworld Layout Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_shuffle'][player]) - if self.metadata['ow_shuffle'][player] != 'vanilla': + outfile.write('Overworld Layout Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_layout'][player]) + 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('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('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('Flute Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_fluteshuffle'][player]) + if self.metadata['ow_layout'][player] == 'grid' or self.metadata['ow_mixed'][player]: + outfile.write('Overworld Fog:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_fog'][player])) outfile.write('\n') outfile.write('Entrance Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['shuffle'][player]) if self.metadata['shuffle'][player] != 'vanilla': @@ -3506,40 +3515,31 @@ class Spoiler(object): outfile.write(f'{fairy}: {bottle}\n') if self.maps: - if 'all' in self.settings or 'flute' in self.settings: - # flute shuffle + def write_map(type, title): for player in range(1, self.world.players + 1): - if ('flute', player) in self.maps: - outfile.write('\n\nFlute Spots:\n\n') + if (type, player) in self.maps: + outfile.write('\n\n' + title + '\n\n') break for player in range(1, self.world.players + 1): - if ('flute', player) in self.maps: + if (type, player) in self.maps: if self.world.players > 1: outfile.write(str('(Player ' + str(player) + ')\n')) # player name - outfile.write(self.maps[('flute', player)]['text']) + outfile.write(self.maps[(type, player)]['text']) + + if 'all' in self.settings or 'flute' in self.settings: + # flute shuffle + write_map('flute', 'Flute Spots:') if 'all' in self.settings or 'overworld' in self.settings: # overworld tile flips - for player in range(1, self.world.players + 1): - if ('swaps', player) in self.maps: - outfile.write('\n\nOW Tile Flips:\n\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']) + write_map('swaps', 'OW Tile Flips:') # crossed groups - for player in range(1, self.world.players + 1): - if ('groups', player) in self.maps: - outfile.write('\n\nOW Crossed Groups:\n\n') - break - for player in range(1, self.world.players + 1): - if ('groups', player) in self.maps: - if self.world.players > 1: - outfile.write(str('(Player ' + str(player) + ')\n')) # player name - outfile.write(self.maps[('groups', player)]['text']) + write_map('groups', 'OW Crossed Groups:') + + # grid layout + write_map('layout_grid_lw', 'Light World Layout:') + write_map('layout_grid_dw', 'Dark World Layout:') if self.overworlds and ('all' in self.settings or 'overworld' in self.settings): outfile.write('\n\nOverworld Edges:\n\n') @@ -3804,11 +3804,11 @@ boss_mode = {"none": 0, "simple": 1, "full": 2, "chaos": 3, 'random': 3, 'unique # byte 10: settings_version -# byte 11: OOOT WCCC (OWR layout, free terrain, whirlpools, OWR crossed) -or_mode = {"vanilla": 0, "parallel": 1, "full": 2} +# byte 11: POOT WCCC (parallel, OWR layout, free terrain, whirlpools, OWR crossed) +orlayout_mode = {"vanilla": 0, "grid": 1, "wild": 2} 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 FFO? (keep similar, mixed/tile flip, bonk drops, follower quests, flute spots, fog) flutespot_mode = {"vanilla": 0, "balanced": 1, "random": 2} # byte 13: FBBB TTPP (flute_mode, bow_mode, take_any, prize shuffle) @@ -3871,12 +3871,12 @@ class Settings(object): 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]], (0x80 if w.owKeepSimilar[p] else 0) | (0x40 if w.owMixed[p] else 0) | (0x20 if w.shuffle_bonk_drops[p] else 0) | (0x10 if w.shuffle_followers[p] else 0) - | (flutespot_mode[w.owFluteShuffle[p]] << 4), + | (flutespot_mode[w.owFluteShuffle[p]] << 4) | (0x02 if w.owFog[p] else 0), (flute_mode[w.flute_mode[p]] << 7 | bow_mode[w.bow_mode[p]] << 4 | take_any_mode[w.take_any[p]] << 2 | prizeshuffle_mode[w.prizeshuffle[p]]), @@ -3953,7 +3953,8 @@ class Settings(object): args.algorithm = r(algo_mode)[(settings[9] & 0x38) >> 3] 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_whirlpool[p] = True if settings[11] & 0x08 else False args.ow_crossed[p] = r(orcrossed_mode)[(settings[11] & 0x07)] @@ -3963,6 +3964,7 @@ class Settings(object): args.bonk_drops[p] = True if settings[12] & 0x20 else False args.shuffle_followers[p] = True if settings[12] & 0x10 else False args.ow_fluteshuffle[p] = r(flutespot_mode)[(settings[12] & 0x0C) >> 2] + args.ow_fog[p] = True if settings[12] & 0x02 else False if len(settings) > 13: args.flute_mode[p] = r(flute_mode)[(settings[13] & 0x80) >> 7] diff --git a/CHANGELOG.md b/CHANGELOG.md index 277596e9..3bef8bcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,38 @@ # Changelog +# 0.7.0.1 +- Fixed buggy sprites in post-Aga Zora's Domain +- Fixed L/R map switch when in special OW screens +- Fixes issue not able to screen transition if bumped by enemy in water + +# 0.7.0.0 +- New OW Layout Shuffle Mode: Grid +- Implemented Fog of War for Tile Flip + +# 0.6.1.11 +- Fixed bonk drops duplicate counting and potentially overwriting arbitrary values +- Fixed boss icons on dungeon map check +- Fixed dungeon counters to autotrack correctly +- Key and chest counts in menu now display consistently (must have dungeon item or have visited dungeon to see the HUD) +- Money balancing will fail in less scenarios +- Fixed issue with Sanc pots not collecting +- Enemizer now allows more enemies on water +- Fix infinite pit fall issue with Old Man follower location +- Fix bad overworld tilemap drawing on HC and Pyramid screens in OW Layout Shuffle + +## 0.6.1.10 +- Emergency fix for bonk functionality + +## 0.6.1.9 +- Fixed follower shuffle placement errors +- Fixed pseudoboots ability to open Kings Tomb +- Implemented better accurate coordinates on map check locations +- Fixed janky icons on zoomed-in map check screen + +## 0.6.1.8 +- Fixed follower placement and logic +- Fixed error with HC Courtyard Tree Pull + ## 0.6.1.7 - \~Merged in DR v1.5.2~ - Reverted key count update diff --git a/CLI.py b/CLI.py index 809a90c6..8a1bcc70 100644 --- a/CLI.py +++ b/CLI.py @@ -120,33 +120,64 @@ def parse_cli(argv, no_defaults=False): ret.take_any = 'random' if ret.take_any == 'none' else ret.take_any 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 ret.ow_no_fog: + ret.ow_fog = False + if player_num: defaults = copy.deepcopy(ret) for player in range(1, player_num + 1): playerargs = parse_cli(shlex.split(getattr(ret, f"p{player}")), True) - + if playerargs.filename: playersettings = apply_settings_file({}, playerargs.filename) for k, v in playersettings.items(): setattr(playerargs, k, v) - for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', 'ow_shuffle', - 'ow_terrain', 'ow_crossed', 'ow_keepsimilar', 'ow_mixed', 'ow_whirlpool', 'ow_fluteshuffle', - 'flute_mode', 'bow_mode', 'take_any', 'boots_hint', 'shuffle_followers', - 'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'bosses_ganon', 'bosshunt_include_agas', 'ganon_item', 'openpyramid', - 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'prizeshuffle', 'showloot', 'showmap', 'startinventory', - 'usestartinventory', 'bombbag', 'shuffleganon', 'overworld_map', 'restrict_boss_items', - 'triforce_max_difference', 'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max', - 'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'shuffletavern', - 'skullwoods', 'linked_drops', - 'pseudoboots', 'mirrorscroll', 'dark_rooms', 'damage_challenge', 'shuffle_damage_table', 'crystal_book', 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters', - 'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots', - 'ow_palettes', 'uw_palettes', 'sprite', 'triforce_gfx', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', - 'heartbeep', 'remote_items', 'shopsanity', 'dropshuffle', 'pottery', 'keydropshuffle', - 'mixed_travel', 'standardize_palettes', 'code', 'reduce_flashing', 'shuffle_sfx', 'shuffle_sfxinstruments', - 'shuffle_songinstruments', 'msu_resume', 'collection_rate', 'colorizepots', 'decoupledoors', 'door_type_mode', - 'bonk_drops', 'trap_door_mode', 'key_logic_algorithm', 'door_self_loops', 'any_enemy_logic', 'aga_randomness', - 'money_balance']: + for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', + 'item_functionality', 'ow_shuffle', 'ow_layout', + 'ow_parallel', 'ow_terrain', 'ow_crossed', + 'ow_keepsimilar', 'ow_mixed', 'ow_whirlpool', + 'ow_fluteshuffle', 'ow_fog', 'flute_mode', 'bow_mode', + 'take_any', 'boots_hint', 'shuffle_followers', + 'shuffle', 'door_shuffle', 'intensity', + 'crystals_ganon', 'crystals_gt', 'bosses_ganon', + 'bosshunt_include_agas', 'ganon_item', 'openpyramid', + 'mapshuffle', 'compassshuffle', 'keyshuffle', + 'bigkeyshuffle', 'prizeshuffle', 'showloot', 'showmap', + 'startinventory', 'usestartinventory', 'bombbag', + 'shuffleganon', 'overworld_map', 'restrict_boss_items', + 'triforce_max_difference', 'triforce_pool_min', + 'triforce_pool_max', 'triforce_goal_min', + 'triforce_goal_max', 'triforce_min_difference', + 'triforce_goal', 'triforce_pool', 'shufflelinks', + 'shuffletavern', 'skullwoods', 'linked_drops', + 'pseudoboots', 'mirrorscroll', 'dark_rooms', + 'damage_challenge', 'shuffle_damage_table', + 'crystal_book', 'retro', 'accessibility', 'hints', + 'beemizer', 'experimental', 'dungeon_counters', + 'shufflebosses', 'shuffleenemies', 'enemy_health', + 'enemy_damage', 'shufflepots', 'ow_palettes', + 'uw_palettes', 'sprite', 'triforce_gfx', 'disablemusic', + 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep', + 'remote_items', 'shopsanity', 'dropshuffle', 'pottery', + 'keydropshuffle', 'mixed_travel', + 'standardize_palettes', 'code', 'reduce_flashing', + 'shuffle_sfx', 'shuffle_sfxinstruments', + 'shuffle_songinstruments', 'msu_resume', + 'collection_rate', 'colorizepots', 'decoupledoors', + 'door_type_mode', 'bonk_drops', 'trap_door_mode', + 'key_logic_algorithm', 'door_self_loops', + 'any_enemy_logic', 'aga_randomness', 'money_balance']: value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) if player == 1: setattr(ret, name, {1: value}) @@ -196,13 +227,18 @@ def parse_settings(): # Shuffle Ganon defaults to TRUE "openpyramid": "auto", "shuffleganon": True, - "ow_shuffle": "vanilla", + "ow_shuffle": "vanilla", # for backwards compatibility + "ow_layout": "vanilla", + "ow_parallel": True, + "ow_unparallel": False, "ow_terrain": False, "ow_crossed": "none", "ow_keepsimilar": False, "ow_mixed": False, "ow_whirlpool": False, "ow_fluteshuffle": "vanilla", + "ow_fog": True, + "ow_no_fog": False, "shuffle_followers": False, "bonk_drops": False, "shuffle": "vanilla", diff --git a/Fill.py b/Fill.py index 0643e509..9db0eef0 100644 --- a/Fill.py +++ b/Fill.py @@ -1030,7 +1030,10 @@ def balance_money_progression(world): 'Rupees (100)': 100, 'Rupees (300)': 300} rupee_rooms = {'Eastern Rupees': 90, 'Mire Key Rupees': 45, 'Mire Shooter Rupees': 90, 'TR Rupees': 270, 'PoD Dark Basement': 270} - acceptable_balancers = ['Bombs (3)', 'Arrows (10)', 'Bombs (10)'] + acceptable_balancers = ['Single Bomb', 'Bombs (3)', 'Bombs (10)', + 'Single Arrow', 'Arrows (5)', 'Arrows (10)', + 'Small Magic', 'Big Magic', 'Small Heart', + 'Fairy', 'Chicken', 'Nothing'] base_value = sum(rupee_rooms.values()) available_money = {player: base_value for player in range(1, world.players+1)} @@ -1060,6 +1063,8 @@ def balance_money_progression(world): return [loc for loc in locations if sphere_state.can_reach(loc) and sphere_state.not_flooding_a_key(sphere_state.world, loc)] def interesting_item(location, item, world, player): + if location.event or location.locked: + return True if item.advancement: return True if item.type is not None or item.name.startswith('Rupee'): @@ -1129,10 +1134,11 @@ def balance_money_progression(world): unchecked_locations.remove(location) if location.item: if location.item.name.startswith('Rupee'): - wallet[location.item.player] += rupee_chart[location.item.name] - if location.item.name != 'Rupees (300)': - balance_locations[location.item.player].add(location) - if interesting_item(location, location.item, world, location.item.player): + if not (location.item.name == 'Rupee (1)' and world.algorithm != 'district'): + wallet[location.item.player] += rupee_chart[location.item.name] + if location.item.name != 'Rupees (300)': + balance_locations[location.item.player].add(location) + elif interesting_item(location, location.item, world, location.item.player): checked_locations.append(location) elif location.item.name in acceptable_balancers: balance_locations[location.item.player].add(location) @@ -1176,7 +1182,11 @@ def balance_money_progression(world): if len(increase_targets) == 0: increase_targets = [x for x in balance_locations[target_player] if (rupee_chart[x.item.name] if x.item.name in rupee_chart else 0) < best_value] if len(increase_targets) == 0: - raise Exception('No early sphere swaps for rupees - money grind would be required - bailing for now') + if state.can_farm_rupees(target_player): + logger.warning(f'No more swap targets available. Short by {difference} rupees, but continuing (player can farm)') + break + else: + raise Exception(f'No early sphere swaps for rupees - money grind would be required - bailing for now') best_target = min(increase_targets, key=lambda t: rupee_chart[t.item.name] if t.item.name in rupee_chart else 0) make_item_free = wallet[target_player] < 20 old_value = 0 if make_item_free else (rupee_chart[best_target.item.name] if best_target.item.name in rupee_chart else 0) diff --git a/ItemList.py b/ItemList.py index cf76538f..710a4520 100644 --- a/ItemList.py +++ b/ItemList.py @@ -1727,6 +1727,7 @@ def shuffle_event_items(world, player): available_quests = follower_quests.copy() available_pickups = [quests[0] for quests in available_quests.values()] + # finalize customizer followers first for loc_name in follower_quests.keys(): loc = world.get_location(loc_name, player) if loc.item: @@ -1734,7 +1735,6 @@ def shuffle_event_items(world, player): available_quests.pop(loc_name) available_pickups.remove(loc.item.name) - if world.mode[player] == 'standard' and 'Zelda Herself' in available_pickups: zelda_dropoff = 'Zelda Pickup' if world.default_zelda_region[player] == 'Thieves Blind\'s Cell': @@ -1744,20 +1744,39 @@ def shuffle_event_items(world, player): available_pickups.remove(zelda_pickup) set_event_item(world, player, zelda_dropoff, zelda_pickup) - random.shuffle(available_pickups) + follower_locations = [world.get_location(loc_name, player) for loc_name in available_quests.keys()] - restricted_pickups = { 'Get Frog': 'Dark Blacksmith Ruins'} - for pickup in restricted_pickups: - restricted_quests = [q for q in available_quests.keys() if q not in restricted_pickups[pickup]] - random.shuffle(restricted_quests) - quest = restricted_quests.pop() - available_quests.pop(quest) - available_pickups.remove(pickup) - set_event_item(world, player, quest, pickup) + attempts = 10 + for attempt in range(attempts): + try: + all_state = world.get_all_state(keys=True) + if world.prizeshuffle[player] != 'wild': + from Items import prize_item_table + prizes = ItemFactory(list(prize_item_table.keys()), player) + for prize in prizes: + all_state.collect(prize, True) - for pickup in available_pickups: - quest, _ = available_quests.popitem() - set_event_item(world, player, quest, pickup) + # randomize the follower pickups, but ensure that the last items are the unrestrictive ones + unrestrictive_pickups = ItemFactory([p for p in ['Zelda Herself', 'Sign Vandalized'] if p in available_pickups], player) + restrictive_pickups = ItemFactory([p for p in available_pickups if p not in unrestrictive_pickups], player) + random.shuffle(restrictive_pickups) + random.shuffle(unrestrictive_pickups) + pickup_items = unrestrictive_pickups + restrictive_pickups + random.shuffle(follower_locations) + + fill_restrictive(world, all_state, follower_locations, pickup_items, single_player_placement=True) + for loc_name in available_quests.keys(): + loc = world.get_location(loc_name, player) + if loc.item: + set_event_item(world, player, loc_name) + except FillError as e: + logging.getLogger('').info("Failed to place followers (%s). Will retry %s more times", e, attempts - attempt - 1) + for loc in follower_locations: + loc.item = None + continue + break + else: + raise FillError(f'Unable to place followers: {", ".join(list(map(lambda d: d.hint_text, follower_locations)))}') def get_item_and_event_flag(item, world, player, dungeon_pool, prize_set, prize_pool): diff --git a/Main.py b/Main.py index 4c68d0a1..7a4ae50b 100644 --- a/Main.py +++ b/Main.py @@ -503,7 +503,7 @@ def init_world(args, fish): customized.load_yaml(args.customizer) 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.accessibility, args.shuffleganon, args.custom, args.customitemarray, args.hints, args.spoiler) @@ -529,6 +529,7 @@ def init_world(args, fish): world.owKeepSimilar = args.ow_keepsimilar.copy() world.owWhirlpoolShuffle = args.ow_whirlpool.copy() world.owFluteShuffle = args.ow_fluteshuffle.copy() + world.owFog = args.ow_fog.copy() world.shuffle_followers = args.shuffle_followers.copy() world.shuffle_bonk_drops = args.bonk_drops.copy() world.open_pyramid = args.openpyramid.copy() @@ -806,7 +807,7 @@ def set_starting_inventory(world, args): def copy_world(world): # 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.accessibility, world.shuffle_ganon, world.custom, world.customitemarray, world.hints, world.spoiler_mode) ret.teams = world.teams @@ -852,6 +853,7 @@ def copy_world(world): ret.owKeepSimilar = world.owKeepSimilar.copy() ret.owWhirlpoolShuffle = world.owWhirlpoolShuffle.copy() ret.owFluteShuffle = world.owFluteShuffle.copy() + ret.owFog = world.owFog.copy() ret.shuffle_followers = world.shuffle_followers.copy() ret.shuffle_bonk_drops = world.shuffle_bonk_drops.copy() ret.open_pyramid = world.open_pyramid.copy() @@ -1037,7 +1039,7 @@ def copy_world(world): def copy_world_premature(world, player, create_flute_exits=True): # 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.accessibility, world.shuffle_ganon, world.custom, world.customitemarray, world.hints, world.spoiler_mode) ret.teams = world.teams @@ -1083,6 +1085,7 @@ def copy_world_premature(world, player, create_flute_exits=True): ret.owKeepSimilar = world.owKeepSimilar.copy() ret.owWhirlpoolShuffle = world.owWhirlpoolShuffle.copy() ret.owFluteShuffle = world.owFluteShuffle.copy() + ret.owFog = world.owFog.copy() ret.shuffle_followers = world.shuffle_followers.copy() ret.shuffle_bonk_drops = world.shuffle_bonk_drops.copy() ret.open_pyramid = world.open_pyramid.copy() diff --git a/OWEdges.py b/OWEdges.py index aa198675..d2710715 100644 --- a/OWEdges.py +++ b/OWEdges.py @@ -326,12 +326,20 @@ def create_owedges(world, player): world.owedges += edges world.initialize_owedges(edges) + set_parallel_owedge_links(world, player, 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) +def set_parallel_owedge_links(world, player, edges): + for edge in edges: + if edge.name in parallel_links: + dw_edge = world.get_owedge(parallel_links[edge.name], player) + edge.parallel = dw_edge + dw_edge.parallel = edge + OWEdgeGroups = { #(IsStandard, World, EdgeAxis, Terrain, HasParallel, NumberInGroup, CustomizerGroup) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 72b79b9d..e144faee 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -127,37 +127,38 @@ def link_overworld(world, player): # restructure Maze Race/Suburb/Frog/Dig Game manually due to NP/P relationship parallel_links_new = bidict(parallel_links) # shallow copy is enough (deep copy is broken) - if world.owKeepSimilar[player]: - del parallel_links_new['Maze Race ES'] - del parallel_links_new['Kakariko Suburb WS'] - for group in trimmed_groups.keys(): - (std, region, axis, terrain, parallel, _, custom) = group - if parallel == IsParallel.Yes: + if world.owLayout[player] != 'grid': + if world.owKeepSimilar[player]: + del parallel_links_new['Maze Race ES'] + del parallel_links_new['Kakariko Suburb WS'] + for group in trimmed_groups.keys(): + (std, region, axis, terrain, parallel, _, custom) = group + if parallel == IsParallel.Yes: + (forward_edges, back_edges) = trimmed_groups[group] + if ['Maze Race ES'] in forward_edges: + forward_edges.remove(['Maze Race ES']) + trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][0].append(['Maze Race ES']) + if ['Kakariko Suburb WS'] in back_edges: + back_edges.remove(['Kakariko Suburb WS']) + trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][1].append(['Kakariko Suburb WS']) + trimmed_groups[group] = (forward_edges, back_edges) + else: + for group in trimmed_groups.keys(): + (std, region, axis, terrain, _, _, custom) = group (forward_edges, back_edges) = trimmed_groups[group] - if ['Maze Race ES'] in forward_edges: - forward_edges.remove(['Maze Race ES']) - trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][0].append(['Maze Race ES']) - if ['Kakariko Suburb WS'] in back_edges: - back_edges.remove(['Kakariko Suburb WS']) - trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][1].append(['Kakariko Suburb WS']) + if ['Dig Game EC', 'Dig Game ES'] in forward_edges: + forward_edges.remove(['Dig Game EC', 'Dig Game ES']) + trimmed_groups[(std, region, axis, terrain, IsParallel.Yes, 1, custom)][0].append(['Dig Game ES']) + trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][0].append(['Dig Game EC']) + if ['Frog WC', 'Frog WS'] in back_edges: + back_edges.remove(['Frog WC', 'Frog WS']) + trimmed_groups[(std, region, axis, terrain, IsParallel.Yes, 1, custom)][1].append(['Frog WS']) + trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][1].append(['Frog WC']) trimmed_groups[group] = (forward_edges, back_edges) - else: - for group in trimmed_groups.keys(): - (std, region, axis, terrain, _, _, custom) = group - (forward_edges, back_edges) = trimmed_groups[group] - if ['Dig Game EC', 'Dig Game ES'] in forward_edges: - forward_edges.remove(['Dig Game EC', 'Dig Game ES']) - trimmed_groups[(std, region, axis, terrain, IsParallel.Yes, 1, custom)][0].append(['Dig Game ES']) - trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][0].append(['Dig Game EC']) - if ['Frog WC', 'Frog WS'] in back_edges: - back_edges.remove(['Frog WC', 'Frog WS']) - trimmed_groups[(std, region, axis, terrain, IsParallel.Yes, 1, custom)][1].append(['Frog WS']) - trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][1].append(['Frog WC']) - trimmed_groups[group] = (forward_edges, back_edges) parallel_links_new = {**dict(parallel_links_new), **dict({e:p[0] for e, p in parallel_links_new.inverse.items()})} connected_edges = [] - if world.owShuffle[player] != 'vanilla': + if world.owLayout[player] != 'vanilla': trimmed_groups = remove_reserved(world, trimmed_groups, connected_edges, player) trimmed_groups = reorganize_groups(world, trimmed_groups, player) @@ -245,7 +246,7 @@ def link_overworld(world, player): if 'undefined_chance' in custom_crossed: undefined_chance = custom_crossed['undefined_chance'] - if limited_crossed > -1: + if limited_crossed > -1 and world.owLayout[player] != 'grid': # connect forced crossed non-parallel edges based on previously determined tile flips for edge in swapped_edges: if edge not in parallel_links_new: @@ -277,10 +278,10 @@ def link_overworld(world, player): s[0x30], s[0x35], s[0x41], s[0x3a],s[0x3b],s[0x3c], s[0x3f]) 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 and world.owLayout[player] != 'grid') or (world.owLayout[player] == 'vanilla' and world.owCrossed[player] == 'unrestricted'): crossed_candidates = list() 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: for (forward_set, back_set) in zip(trimmed_groups[group][0], trimmed_groups[group][1]): if forward_set[0] in parallel_links_new: @@ -291,7 +292,7 @@ def link_overworld(world, player): combine_set = forward_combine+back_combine 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 not any(edge in force_noncrossed for edge in combine_set): if any(edge in force_crossed for edge in forward_combine): @@ -425,7 +426,7 @@ def link_overworld(world, player): # layout shuffle logging.getLogger('').debug('Shuffling overworld layout') - if world.owShuffle[player] == 'vanilla': + if world.owLayout[player] == 'vanilla': # apply outstanding flips trimmed_groups = performSwap(trimmed_groups, edges_to_swap) assert len(edges_to_swap) == 0, 'Not all edges were flipped successfully: ' + ', '.join(edges_to_swap) @@ -438,8 +439,15 @@ 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) + elif world.owLayout[player] == 'grid': + from source.overworld.LayoutGenerator import generate_random_grid_layout + + for exitname, destname in special_screen_connections: + connect_two_way(world, exitname, destname, player, connected_edges) + + generate_random_grid_layout(world, player, connected_edges, ow_crossed_tiles if world.owCrossed[player] == 'grouped' else [], force_noncrossed, force_crossed, limited_crossed, undefined_chance / 100) else: - if world.owKeepSimilar[player] and world.owShuffle[player] == 'parallel': + if world.owKeepSimilar[player] and world.owParallel[player]: for exitname, destname in parallelsimilar_connections: connect_two_way(world, exitname, destname, player, connected_edges) @@ -589,7 +597,10 @@ def link_overworld(world, player): connect_simple(world, 'Flute Spot ' + str(o + 1), regions[1], player) if world.owFluteShuffle[player] == 'vanilla': - connect_flutes(default_flute_connections) + flute_spots = default_flute_connections.copy() + sort_flute_spots(world, player, flute_spots) + world.owflutespots[player] = flute_spots + connect_flutes(flute_spots) else: flute_spots = 8 flute_pool = list(flute_data.keys()) @@ -716,7 +727,7 @@ def link_overworld(world, player): flute_spots -= spots_to_place # connect new flute spots - new_spots.sort() + sort_flute_spots(world, player, new_spots) world.owflutespots[player] = new_spots connect_flutes(new_spots) @@ -835,7 +846,7 @@ def connect_custom(world, connected_edges, groups, forced, player): remove_pair_from_pool(edge1.name, edge2.name, is_crossed) connect_two_way(world, edge1.name, edge2.name, player, connected_edges) # 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_back_edge = parallel_links_new[edge2.name] if validate_crossed_allowed(parallel_forward_edge, parallel_back_edge, is_crossed): @@ -851,13 +862,13 @@ def connect_custom(world, connected_edges, groups, forced, player): connect_two_way(world, forward_edge, back_edge, player, connected_edges) else: 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_back_edge = parallel_links_new[back_edge] if not validate_crossed_allowed(parallel_forward_edge, parallel_back_edge, is_crossed): raise GenerationException('Violation of force crossed rules on parallel unresolved similars: \'%s\' <-> \'%s\'', forward_edge, back_edge) -def connect_two_way(world, edgename1, edgename2, player, connected_edges=None): +def connect_two_way(world, edgename1, edgename2, player, connected_edges=None, set_spoiler=True): edge1 = world.get_entrance(edgename1, player) edge2 = world.get_entrance(edgename2, player) x = world.get_owedge(edgename1, player) @@ -881,7 +892,7 @@ def connect_two_way(world, edgename1, edgename2, player, connected_edges=None): x.dest = y y.dest = x - if world.owShuffle[player] != 'vanilla' or world.owMixed[player] or world.owCrossed[player] != 'none': + if set_spoiler and (world.owLayout[player] != 'vanilla' or world.owMixed[player] or world.owCrossed[player] != 'none'): world.spoiler.set_overworld(edgename2, edgename1, 'both', player) if connected_edges is not None: @@ -889,7 +900,7 @@ def connect_two_way(world, edgename1, edgename2, player, connected_edges=None): connected_edges.append(edgename2) # 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: try: parallel_forward_edge = parallel_links_new[edgename1] @@ -978,7 +989,7 @@ def determine_forced_flips(world, tile_ow_groups, do_grouped, player): 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]): 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() if custom_edges and player in custom_edges: custom_edges = custom_edges[player] @@ -1084,6 +1095,9 @@ def shuffle_tiles(world, groups, result_list, do_grouped, forced_flips, player): 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)] + if world.owLayout[player] == 'grid': + parity[1] = 0 + parity[2] = 0 if not world.owKeepSimilar[player]: parity[1] += 2*parity[2] parity[2] = 0 @@ -1177,12 +1191,16 @@ def define_tile_groups(world, do_grouped, player): if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'simple', 'restricted', 'district']: merge_groups([[0x05, 0x07]]) - # all non-parallel screens - if world.owShuffle[player] == 'vanilla' and (world.owCrossed[player] == 'none' or do_grouped): - merge_groups([[0x00, 0x2d, 0x80], [0x0f, 0x81], [0x1a, 0x1b], [0x28, 0x29], [0x30, 0x3a]]) + # special screens + if world.owLayout[player] != 'wild' and (world.owCrossed[player] == 'none' or do_grouped): + 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 - 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]]) # whirlpool screens @@ -1238,7 +1256,7 @@ def reorganize_groups(world, groups, player): new_group[0] = None if world.owTerrain[player]: new_group[3] = None - if world.owShuffle[player] != 'parallel': + if not world.owParallel[player]: new_group[4] = None if not world.owKeepSimilar[player]: new_group[5] = None @@ -1307,6 +1325,15 @@ def adjust_edge_groups(world, trimmed_groups, edges_to_swap, player): groups[(mode, wrld, dir, terrain, parallel, count, group_name)][i].extend(matches) return groups +def sort_flute_spots(world, player, flute_spots): + if world.owLayout[player] != 'grid': + flute_spots.sort(key=lambda id: flute_data[id][1] if id != 0x03 or not world.is_tile_swapped(0x03, player) else 0x04) + else: + world_layout = world.owgrid[player][0] if world.mode[player] != 'inverted' else world.owgrid[player][1] + layout_list = sum(world_layout, []) + layout_map = {id & 0xBF: i for i, id in enumerate(layout_list)} + flute_spots.sort(key=lambda id: layout_map[flute_data[id][1] if id != 0x03 or not world.is_tile_swapped(0x03, player) else 0x04]) + def create_dynamic_flute_exits(world, player): flute_in_pool = True if player not in world.customitemarray else any(i for i, n in world.customitemarray[player].items() if i == 'flute' and n > 0) if not flute_in_pool: @@ -2347,6 +2374,11 @@ parallelsimilar_connections = [('Maze Race ES', 'Kakariko Suburb WS'), ('Dig Game ES', 'Frog WS') ] +special_screen_connections = [('Lost Woods NW', 'Master Sword Meadow SC'), + ('Stone Bridge WC', 'Hobo EC'), + ('Zora Waterfall NE', 'Zoras Domain SW') + ] + # non shuffled overworld default_connections = [('Lost Woods NW', 'Master Sword Meadow SC'), ('Lost Woods SW', 'Lost Woods Pass NW'), @@ -2560,46 +2592,107 @@ isolated_regions = [ flute_data = { #OWID LW Region DW Region Slot VRAM BG Y BG X Link Y Link X Cam Y Cam X Unk1 Unk2 IconY IconX AltY AltX AltVRAM AltBGY AltBGX AltCamY AltCamX AltUnk1 AltUnk2 AltIconY AltIconX 0x00: (['Lost Woods East Area', 'Skull Woods Forest'], 0x09, 0x1042, 0x022e, 0x0202, 0x0290, 0x0288, 0x029b, 0x028f, 0xfff2, 0x000e, 0x0290, 0x0288, 0x0290, 0x0290), - 0x02: (['Lumberjack Area', 'Dark Lumberjack Area'], 0x02, 0x059c, 0x00d6, 0x04e6, 0x0138, 0x0558, 0x0143, 0x0563, 0xfffa, 0xfffa, 0x0138, 0x0550), - 0x03: (['West Death Mountain (Bottom)', 'West Dark Death Mountain (Top)'], 0x0b, 0x1600, 0x02ca, 0x060e, 0x0328, 0x0678, 0x0337, 0x0683, 0xfff6, 0xfff2, 0x035b, 0x0680, 0x0118, 0x0860, 0x05c0, 0x00b8, 0x07ec, 0x0127, 0x086b, 0xfff8, 0x0004, 0x0148, 0x0850), - 0x05: (['East Death Mountain (Bottom)', 'East Dark Death Mountain (Bottom)'], 0x0e, 0x1860, 0x031e, 0x0d00, 0x0388, 0x0da8, 0x038d, 0x0d7d, 0x0000, 0x0000, 0x0388, 0x0da8), - 0x07: (['Death Mountain TR Pegs Area', 'Turtle Rock Area'], 0x07, 0x0804, 0x0102, 0x0e1a, 0x0160, 0x0e90, 0x016f, 0x0e97, 0xfffe, 0x0006, 0x0160, 0x0f20), - 0x0a: (['Mountain Pass Area', 'Bumper Cave Area'], 0x0a, 0x0180, 0x0220, 0x0406, 0x0280, 0x0488, 0x028f, 0x0493, 0x0000, 0xfffa, 0x0280, 0x0488), - 0x0f: (['Zora Waterfall Area', 'Catfish Area'], 0x0f, 0x0316, 0x025c, 0x0eb2, 0x02c0, 0x0f28, 0x02cb, 0x0f2f, 0x0002, 0xfffe, 0x02d0, 0x0f38), - 0x10: (['Lost Woods Pass West Area', 'Skull Woods Pass West Area'], 0x10, 0x0080, 0x0400, 0x0000, 0x0448, 0x0058, 0x046f, 0x0085, 0x0000, 0x0000, 0x0448, 0x0058), - 0x11: (['Kakariko Fortune Area', 'Dark Fortune Area'], 0x11, 0x0912, 0x051e, 0x0292, 0x0588, 0x0318, 0x058d, 0x031f, 0x0000, 0xfffe, 0x0588, 0x0318), - 0x12: (['Kakariko Pond Area', 'Outcast Pond Area'], 0x12, 0x0890, 0x051a, 0x0476, 0x0578, 0x04f8, 0x0587, 0x0503, 0xfff6, 0x000a, 0x0578, 0x04f8), - 0x13: (['Sanctuary Area', 'Dark Chapel Area'], 0x13, 0x051c, 0x04aa, 0x06de, 0x0508, 0x0758, 0x0517, 0x0763, 0xfff6, 0x0002, 0x0508, 0x0758), - 0x14: (['Graveyard Area', 'Dark Graveyard Area'], 0x14, 0x089c, 0x051e, 0x08e6, 0x0580, 0x0958, 0x058b, 0x0963, 0x0000, 0xfffa, 0x0580, 0x0928, 0x0580, 0x0948), - 0x15: (['River Bend East Bank', 'Qirn Jump East Bank'], 0x15, 0x041a, 0x0486, 0x0ad2, 0x04e8, 0x0b48, 0x04f3, 0x0b4f, 0x0008, 0xfffe, 0x04f8, 0x0b60), - 0x16: (['Potion Shop Area', 'Dark Witch Area'], 0x16, 0x0888, 0x0516, 0x0c4e, 0x0578, 0x0cc8, 0x0583, 0x0cd3, 0xfffa, 0xfff2, 0x0598, 0x0ccf), - 0x17: (['Zora Approach Ledge', 'Catfish Approach Ledge'], 0x17, 0x039e, 0x047e, 0x0ef2, 0x04e0, 0x0f68, 0x04eb, 0x0f6f, 0x0000, 0xfffe, 0x04e0, 0x0f68), - 0x18: (['Kakariko Village', 'Village of Outcasts'], 0x18, 0x0b30, 0x0759, 0x017e, 0x07b7, 0x0200, 0x07c6, 0x020b, 0x0007, 0x0002, 0x07c0, 0x0210, 0x07c8, 0x01f8), - 0x1a: (['Forgotten Forest Area', 'Shield Shop Fence'], 0x1a, 0x081a, 0x070f, 0x04d2, 0x0770, 0x0548, 0x077c, 0x054f, 0xffff, 0xfffe, 0x0770, 0x0548), - 0x1b: (['Hyrule Castle Courtyard', 'Pyramid Area'], 0x1b, 0x0c30, 0x077a, 0x0786, 0x07d8, 0x07f8, 0x07e7, 0x0803, 0x0006, 0xfffa, 0x07d8, 0x07f8), - 0x1d: (['Wooden Bridge Area', 'Broken Bridge Northeast'], 0x1d, 0x0602, 0x06c2, 0x0a0e, 0x0720, 0x0a80, 0x072f, 0x0a8b, 0xfffe, 0x0002, 0x0720, 0x0a80), - 0x1e: (['Eastern Palace Area', 'Palace of Darkness Area'], 0x26, 0x1802, 0x091e, 0x0c0e, 0x09c0, 0x0c80, 0x098b, 0x0c8b, 0x0000, 0x0002, 0x09c0, 0x0c80), - 0x22: (['Blacksmith Area', 'Hammer Pegs Area'], 0x22, 0x058c, 0x08aa, 0x0462, 0x0908, 0x04d8, 0x0917, 0x04df, 0x0006, 0xfffe, 0x0908, 0x04d8), - 0x25: (['Sand Dunes Area', 'Dark Dunes Area'], 0x25, 0x030e, 0x085a, 0x0a76, 0x08b8, 0x0ae8, 0x08c7, 0x0af3, 0x0006, 0xfffa, 0x08b8, 0x0b08), - 0x28: (['Maze Race Area', 'Dig Game Area'], 0x28, 0x0908, 0x0b1e, 0x003a, 0x0b88, 0x00b8, 0x0b8d, 0x00bf, 0x0000, 0x0006, 0x0b88, 0x00b8), - 0x29: (['Kakariko Suburb Area', 'Frog Area'], 0x29, 0x0408, 0x0a7c, 0x0242, 0x0ae0, 0x02c0, 0x0aeb, 0x02c7, 0x0002, 0xfffe, 0x0ae0, 0x02c0), - 0x2a: (['Flute Boy Area', 'Stumpy Area'], 0x2a, 0x058e, 0x0aac, 0x046e, 0x0b10, 0x04e8, 0x0b1b, 0x04f3, 0x0002, 0x0002, 0x0b10, 0x04e8), - 0x2b: (['Central Bonk Rocks Area', 'Dark Bonk Rocks Area'], 0x2b, 0x0620, 0x0acc, 0x0700, 0x0b30, 0x0790, 0x0b3b, 0x0785, 0xfff2, 0x0000, 0x0b30, 0x0770), - 0x2c: (['Links House Area', 'Big Bomb Shop Area'], 0x2c, 0x0588, 0x0ab9, 0x0840, 0x0b17, 0x08b8, 0x0b26, 0x08bf, 0xfff7, 0x0000, 0x0b20, 0x08b8), - 0x2d: (['Stone Bridge South Area', 'Hammer Bridge South Area'], 0x2d, 0x0886, 0x0b1e, 0x0a2a, 0x0ba0, 0x0aa8, 0x0b8b, 0x0aaf, 0x0000, 0x0006, 0x0bc4, 0x0ad0), - 0x2e: (['Tree Line Area', 'Dark Tree Line Area'], 0x2e, 0x0100, 0x0a1a, 0x0c00, 0x0a78, 0x0c30, 0x0a87, 0x0c7d, 0x0006, 0x0000, 0x0a78, 0x0c58), - 0x2f: (['Eastern Nook Area', 'Darkness Nook Area'], 0x2f, 0x0798, 0x0afa, 0x0eb2, 0x0b58, 0x0f30, 0x0b67, 0x0f37, 0xfff6, 0x000e, 0x0b50, 0x0f30), - 0x30: (['Desert Teleporter Ledge', 'Mire Teleporter Ledge'], 0x38, 0x1880, 0x0f1e, 0x0000, 0x0fa8, 0x0078, 0x0f8d, 0x008d, 0x0000, 0x0000, 0x0fb0, 0x0070), - 0x32: (['Flute Boy Approach Area', 'Stumpy Approach Area'], 0x32, 0x03a0, 0x0c6c, 0x0500, 0x0cd0, 0x05a8, 0x0cdb, 0x0585, 0x0002, 0x0000, 0x0cd6, 0x0568), - 0x33: (['C Whirlpool Outer Area', 'Dark C Whirlpool Outer Area'], 0x33, 0x0180, 0x0c20, 0x0600, 0x0c80, 0x0628, 0x0c8f, 0x067d, 0x0000, 0x0000, 0x0c80, 0x0628), - 0x34: (['Statues Area', 'Hype Cave Area'], 0x34, 0x088e, 0x0d00, 0x0866, 0x0d60, 0x08d8, 0x0d6f, 0x08e3, 0x0000, 0x000a, 0x0d60, 0x08d8), - #0x35: (['Lake Hylia Northwest Bank', 'Ice Lake Northwest Bank'], 0x35, 0x0d00, 0x0da6, 0x0a06, 0x0e08, 0x0a80, 0x0e13, 0x0a8b, 0xfffa, 0xfffa, 0x0d88, 0x0a88), - 0x35: (['Lake Hylia South Shore', 'Ice Lake Southeast Ledge'], 0x3e, 0x1860, 0x0f1e, 0x0d00, 0x0f98, 0x0da8, 0x0f8b, 0x0d85, 0x0000, 0x0000, 0x0f90, 0x0da4), - 0x37: (['Ice Cave Area', 'Shopping Mall Area'], 0x37, 0x0786, 0x0cf6, 0x0e2e, 0x0d58, 0x0ea0, 0x0d63, 0x0eab, 0x000a, 0x0002, 0x0d48, 0x0ed0), - 0x3a: (['Desert Pass Area', 'Swamp Nook Area'], 0x3a, 0x001a, 0x0e08, 0x04c6, 0x0e70, 0x0540, 0x0e7d, 0x054b, 0x0006, 0x000a, 0x0e70, 0x0540), - 0x3b: (['Dam Area', 'Swamp Area'], 0x3b, 0x069e, 0x0edf, 0x06f2, 0x0f3d, 0x0778, 0x0f4c, 0x077f, 0xfff1, 0xfffe, 0x0f30, 0x0770), - 0x3c: (['South Pass Area', 'Dark South Pass Area'], 0x3c, 0x0584, 0x0ed0, 0x081e, 0x0f38, 0x0898, 0x0f45, 0x08a3, 0xfffe, 0x0002, 0x0f38, 0x0898), - 0x3f: (['Octoballoon Area', 'Bomber Corner Area'], 0x3f, 0x0810, 0x0f05, 0x0e75, 0x0f67, 0x0ef3, 0x0f72, 0x0efa, 0xfffb, 0x000b, 0x0f80, 0x0ef0) + 0x02: (['Lumberjack Area', 'Dark Lumberjack Area'], 0x02, 0x059c, 0x00d6, 0x04e6, 0x0138, 0x0558, 0x0143, 0x0563, 0xfffa, 0xfffa, 0x01d8, 0x0518), + 0x03: (['West Death Mountain (Bottom)', 'West Dark Death Mountain (Top)'], 0x0b, 0x1600, 0x02ca, 0x060e, 0x0328, 0x0678, 0x0337, 0x0683, 0xfff6, 0xfff2, 0x03bb, 0x0680, 0x0118, 0x0860, 0x05c0, 0x00b8, 0x07ec, 0x0127, 0x086b, 0xfff8, 0x0004, 0x0148, 0x0850), + 0x05: (['East Death Mountain (Bottom)', 'East Dark Death Mountain (Bottom)'], 0x0e, 0x1860, 0x031e, 0x0d00, 0x0388, 0x0da8, 0x038d, 0x0d7d, 0x0000, 0x0000, 0x03c8, 0x0d98), + 0x07: (['Death Mountain TR Pegs Area', 'Turtle Rock Area'], 0x07, 0x0804, 0x0102, 0x0e1a, 0x0160, 0x0e90, 0x016f, 0x0e97, 0xfffe, 0x0006, 0x0150, 0x0ea0), + 0x0a: (['Mountain Pass Area', 'Bumper Cave Area'], 0x0a, 0x0180, 0x0220, 0x0406, 0x0280, 0x0488, 0x028f, 0x0493, 0x0000, 0xfffa, 0x0390, 0x04d8), + 0x0f: (['Zora Waterfall Area', 'Catfish Area'], 0x0f, 0x0316, 0x025c, 0x0eb2, 0x02c0, 0x0f28, 0x02cb, 0x0f2f, 0x0002, 0xfffe, 0x0360, 0x0f58), + 0x10: (['Lost Woods Pass West Area', 'Skull Woods Pass West Area'], 0x10, 0x0080, 0x0400, 0x0000, 0x0448, 0x0058, 0x046f, 0x0085, 0x0000, 0x0000, 0x04f8, 0x0088), + 0x11: (['Kakariko Fortune Area', 'Dark Fortune Area'], 0x11, 0x0912, 0x051e, 0x0292, 0x0588, 0x0318, 0x058d, 0x031f, 0x0000, 0xfffe, 0x05f8, 0x0318), + 0x12: (['Kakariko Pond Area', 'Outcast Pond Area'], 0x12, 0x0890, 0x051a, 0x0476, 0x0578, 0x04f8, 0x0587, 0x0503, 0xfff6, 0x000a, 0x05b8, 0x04f8), + 0x13: (['Sanctuary Area', 'Dark Chapel Area'], 0x13, 0x051c, 0x04aa, 0x06de, 0x0508, 0x0758, 0x0517, 0x0763, 0xfff6, 0x0002, 0x05b8, 0x0738), + 0x14: (['Graveyard Area', 'Dark Graveyard Area'], 0x14, 0x089c, 0x051e, 0x08e6, 0x0580, 0x0958, 0x058b, 0x0963, 0x0000, 0xfffa, 0x05f0, 0x0918, 0x0580, 0x0948), + 0x15: (['River Bend East Bank', 'Qirn Jump East Bank'], 0x15, 0x041a, 0x0486, 0x0ad2, 0x04e8, 0x0b48, 0x04f3, 0x0b4f, 0x0008, 0xfffe, 0x0548, 0x0b78), + 0x16: (['Potion Shop Area', 'Dark Witch Area'], 0x16, 0x0888, 0x0516, 0x0c4e, 0x0578, 0x0cc8, 0x0583, 0x0cd3, 0xfffa, 0xfff2, 0x05e8, 0x0c9f), + 0x17: (['Zora Approach Ledge', 'Catfish Approach Ledge'], 0x17, 0x039e, 0x047e, 0x0ef2, 0x04e0, 0x0f68, 0x04eb, 0x0f6f, 0x0000, 0xfffe, 0x0580, 0x0f48), + 0x18: (['Kakariko Village', 'Village of Outcasts'], 0x18, 0x0b30, 0x0759, 0x017e, 0x07b7, 0x0200, 0x07c6, 0x020b, 0x0007, 0x0002, 0x0830, 0x0240, 0x07c8, 0x01f8), + 0x1a: (['Forgotten Forest Area', 'Shield Shop Fence'], 0x1a, 0x081a, 0x070f, 0x04d2, 0x0770, 0x0548, 0x077c, 0x054f, 0xffff, 0xfffe, 0x0770, 0x0518), + 0x1b: (['Hyrule Castle Courtyard', 'Pyramid Area'], 0x1b, 0x0c30, 0x077a, 0x0786, 0x07d8, 0x07f8, 0x07e7, 0x0803, 0x0006, 0xfffa, 0x07f8, 0x07f8), + 0x1d: (['Wooden Bridge Area', 'Broken Bridge Northeast'], 0x1d, 0x0602, 0x06c2, 0x0a0e, 0x0720, 0x0a80, 0x072f, 0x0a8b, 0xfffe, 0x0002, 0x0750, 0x0a70), + 0x1e: (['Eastern Palace Area', 'Palace of Darkness Area'], 0x26, 0x1802, 0x091e, 0x0c0e, 0x09c0, 0x0c80, 0x098b, 0x0c8b, 0x0000, 0x0002, 0x09a0, 0x0cb0), + 0x22: (['Blacksmith Area', 'Hammer Pegs Area'], 0x22, 0x058c, 0x08aa, 0x0462, 0x0908, 0x04d8, 0x0917, 0x04df, 0x0006, 0xfffe, 0x0978, 0x04e8), + 0x25: (['Sand Dunes Area', 'Dark Dunes Area'], 0x25, 0x030e, 0x085a, 0x0a76, 0x08b8, 0x0ae8, 0x08c7, 0x0af3, 0x0006, 0xfffa, 0x0918, 0x0b18), + 0x28: (['Maze Race Area', 'Dig Game Area'], 0x28, 0x0908, 0x0b1e, 0x003a, 0x0b88, 0x00b8, 0x0b8d, 0x00bf, 0x0000, 0x0006, 0x0ba8, 0x00b8), + 0x29: (['Kakariko Suburb Area', 'Frog Area'], 0x29, 0x0408, 0x0a7c, 0x0242, 0x0ae0, 0x02c0, 0x0aeb, 0x02c7, 0x0002, 0xfffe, 0x0b30, 0x02e0), + 0x2a: (['Flute Boy Area', 'Stumpy Area'], 0x2a, 0x058e, 0x0aac, 0x046e, 0x0b10, 0x04e8, 0x0b1b, 0x04f3, 0x0002, 0x0002, 0x0b60, 0x04f8), + 0x2b: (['Central Bonk Rocks Area', 'Dark Bonk Rocks Area'], 0x2b, 0x0620, 0x0acc, 0x0700, 0x0b30, 0x0790, 0x0b3b, 0x0785, 0xfff2, 0x0000, 0x0b80, 0x0760), + 0x2c: (['Links House Area', 'Big Bomb Shop Area'], 0x2c, 0x0588, 0x0ab9, 0x0840, 0x0b17, 0x08b8, 0x0b26, 0x08bf, 0xfff7, 0x0000, 0x0bb0, 0x08a8), + 0x2d: (['Stone Bridge South Area', 'Hammer Bridge South Area'], 0x2d, 0x0886, 0x0b1e, 0x0a2a, 0x0ba0, 0x0aa8, 0x0b8b, 0x0aaf, 0x0000, 0x0006, 0x0bf0, 0x0ab8), + 0x2e: (['Tree Line Area', 'Dark Tree Line Area'], 0x2e, 0x0100, 0x0a1a, 0x0c00, 0x0a78, 0x0c30, 0x0a87, 0x0c7d, 0x0006, 0x0000, 0x0ac8, 0x0c70), + 0x2f: (['Eastern Nook Area', 'Darkness Nook Area'], 0x2f, 0x0798, 0x0afa, 0x0eb2, 0x0b58, 0x0f30, 0x0b67, 0x0f37, 0xfff6, 0x000e, 0x0bc0, 0x0f00), + 0x30: (['Desert Teleporter Ledge', 'Mire Teleporter Ledge'], 0x38, 0x1880, 0x0f1e, 0x0000, 0x0fa8, 0x0078, 0x0f8d, 0x008d, 0x0000, 0x0000, 0x0ff0, 0x0070), + 0x32: (['Flute Boy Approach Area', 'Stumpy Approach Area'], 0x32, 0x03a0, 0x0c6c, 0x0500, 0x0cd0, 0x05a8, 0x0cdb, 0x0585, 0x0002, 0x0000, 0x0d00, 0x0528), + 0x33: (['C Whirlpool Outer Area', 'Dark C Whirlpool Outer Area'], 0x33, 0x0180, 0x0c20, 0x0600, 0x0c80, 0x0628, 0x0c8f, 0x067d, 0x0000, 0x0000, 0x0ce0, 0x0688), + 0x34: (['Statues Area', 'Hype Cave Area'], 0x34, 0x088e, 0x0d00, 0x0866, 0x0d60, 0x08d8, 0x0d6f, 0x08e3, 0x0000, 0x000a, 0x0dd0, 0x08e8), + #0x35: (['Lake Hylia Northwest Bank', 'Ice Lake Northwest Bank'], 0x35, 0x0d00, 0x0da6, 0x0a06, 0x0e08, 0x0a80, 0x0e13, 0x0a8b, 0xfffa, 0xfffa, 0x0dc8, 0x0a90), + 0x35: (['Lake Hylia South Shore', 'Ice Lake Southeast Ledge'], 0x3e, 0x1860, 0x0f1e, 0x0d00, 0x0f98, 0x0da8, 0x0f8b, 0x0d85, 0x0000, 0x0000, 0x0fd8, 0x0da8), + 0x37: (['Ice Cave Area', 'Shopping Mall Area'], 0x37, 0x0786, 0x0cf6, 0x0e2e, 0x0d58, 0x0ea0, 0x0d63, 0x0eab, 0x000a, 0x0002, 0x0d98, 0x0ed0), + 0x3a: (['Desert Pass Area', 'Swamp Nook Area'], 0x3a, 0x001a, 0x0e08, 0x04c6, 0x0e70, 0x0540, 0x0e7d, 0x054b, 0x0006, 0x000a, 0x0ee0, 0x0570), + 0x3b: (['Dam Area', 'Swamp Area'], 0x3b, 0x069e, 0x0edf, 0x06f2, 0x0f3d, 0x0778, 0x0f4c, 0x077f, 0xfff1, 0xfffe, 0x0fd0, 0x0770), + 0x3c: (['South Pass Area', 'Dark South Pass Area'], 0x3c, 0x0584, 0x0ed0, 0x081e, 0x0f38, 0x0898, 0x0f45, 0x08a3, 0xfffe, 0x0002, 0x0fa8, 0x0898), + 0x3f: (['Octoballoon Area', 'Bomber Corner Area'], 0x3f, 0x0810, 0x0f05, 0x0e75, 0x0f67, 0x0ef3, 0x0f72, 0x0efa, 0xfffb, 0x000b, 0x0fd0, 0x0ef0) +} + +ow_loc_prize_table = { + 'Master Sword Pedestal': (0x06d, 0x070), + 'Hobo': (0xb80, 0xb90), + 'Mushroom': (0x180, 0x140), + 'Lost Woods Hideout Tree': (0x200, 0x320), + 'Ether Tablet': (0x728, 0x017), + 'Spectacle Rock': (0x7F8, 0x128), + 'Old Man': (0x6A8, 0x3FF), + 'Floating Island': (0xD38, 0x038), + 'Death Mountain Bonk Rocks': (0xD48, 0x107), + 'Mountain Pass Pull Tree': (0x4E0, 0x380), + 'Mountain Pass Southeast Tree': (0x5A0, 0x3FF), + 'Lost Woods Pass West Tree': (0x080, 0x540), + 'Kakariko Portal Tree': (0x190, 0x540), + 'Fortune Bonk Rocks': (0x288, 0x558), + 'Bonk Rocks Tree': (0x680, 0x520), + 'Sanctuary Tree': (0x788, 0x594), + 'River Bend West Tree': (0xA38, 0x5FF), + 'River Bend East Tree': (0xB88, 0x558), + 'Bottle Merchant': (0x178, 0x7E8), + 'Blinds Hideout Tree': (0x118, 0x6F7), + 'Kakariko Welcome Tree': (0x358, 0x988), + 'Hyrule Castle Tree': (0x730, 0x780), + 'Wooden Bridge Tree': (0xA78, 0x6F0), + 'Eastern Palace Tree': (0xEC0, 0x968), + 'Maze Race': (0x0C0, 0xB00), + 'Flute Spot': (0x480, 0xAC0), + 'Flute Boy East Tree': (0x540, 0xB80), + 'Flute Boy South Tree': (0x540, 0xB80), + 'Central Bonk Rocks Tree': (0x768, 0xB37), + 'Tree Line Tree 2': (0xCD8, 0xB00), + 'Tree Line Tree 4': (0xCD8, 0xB00), + 'Desert Ledge': (0x018, 0xE88), + 'Bombos Tablet': (0x3E8, 0xEF8), + 'Flute Boy Approach North Tree': (0x588, 0xD07), + 'Flute Boy Approach South Tree': (0x588, 0xD07), + 'Lake Hylia Island': (0xB98, 0xD88), + 'Purple Chest': (0x588, 0xE96), + 'Sunken Treasure': (0x6F8, 0xF48), + + 'Dark Lumberjack Tree': (0x438, 0x1F7), + 'Bumper Cave Ledge': (0x538, 0x277), + 'Catfish': (0xE80, 0x368), + 'Dark Fortune Bonk Rocks (Drop 1)': (0x288, 0x558), + 'Dark Fortune Bonk Rocks (Drop 2)': (0x288, 0x558), + 'Dark Graveyard West Bonk Rocks': (0x900, 0x580), + 'Dark Graveyard North Bonk Rocks': (0x900, 0x580), + 'Dark Graveyard Tomb Bonk Rocks': (0x900, 0x580), + 'Qirn Jump West Tree': (0xA98, 0x5FF), + 'Qirn Jump East Tree': (0xB88, 0x558), + 'Dark Witch Tree': (0xC28, 0x558), + 'Pyramid': (0x998, 0x778), + 'Pyramid Tree': (0x738, 0x908), + 'Palace of Darkness Tree': (0xEC0, 0x968), + 'Digging Game': (0x0C0, 0xB00), + 'Dark Tree Line Tree 2': (0xCD8, 0xB00), + 'Dark Tree Line Tree 3': (0xCD8, 0xB00), + 'Dark Tree Line Tree 4': (0xCD8, 0xB00), + 'Hype Cave Statue': (0x900, 0xDB0) } tile_swap_spoiler_table = \ diff --git a/Plando.py b/Plando.py index 13a41bda..8f844f19 100755 --- a/Plando.py +++ b/Plando.py @@ -35,7 +35,7 @@ def main(args): start_time = time.process_time() # 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") logger = logging.getLogger('') @@ -168,9 +168,9 @@ def prefill_world(world, plando, text_patches): elif line.startswith('!goal'): _, goalstr = line.split(':', 1) world.goal = {1: goalstr.strip()} - elif line.startswith('!owShuffle'): + elif line.startswith('!owLayout'): _, modestr = line.split(':', 1) - world.owShuffle = {1: modestr.strip()} + world.owLayout = {1: modestr.strip()} elif line.startswith('!owCrossed'): _, modestr = line.split(':', 1) world.owCrossed = {1: modestr.strip()} diff --git a/Plandomizer_Template.txt b/Plandomizer_Template.txt index 41782f89..68b46544 100644 --- a/Plandomizer_Template.txt +++ b/Plandomizer_Template.txt @@ -244,7 +244,7 @@ Ganons Tower - Validation Chest: Nothing Ganon: Triforce # set Overworld connections (lines starting with $, separate edges with =) -!owShuffle: parallel +!owLayout: wild #!owMixed: true # Mixed OW not supported yet !owCrossed: none !owKeepSimilar: true diff --git a/README.md b/README.md index f5c6572a..86d65eee 100644 --- a/README.md +++ b/README.md @@ -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) -## 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) ### Vanilla 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) @@ -389,11 +393,17 @@ Districts are a concept originally conceived by Aerinon in the Door Randomizer, Show the help message and exit. ``` ---ow_shuffle +--ow_layout ``` 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 ``` @@ -424,6 +434,12 @@ This gives each OW tile a random chance to be flipped to the opposite world For randomizing the flute spots around the overworld +``` +--ow_no_fog +``` + +With OW Grid Layout Shuffle or Mixed, this disables the fog that prevents you from seeing unvisited screens on the overworld map. + ``` --shuffle_followers ``` diff --git a/Rom.py b/Rom.py index eccb1463..ccf664f4 100644 --- a/Rom.py +++ b/Rom.py @@ -84,7 +84,7 @@ from Utils import int16_as_bytes, int32_as_bytes, local_path, snes_to_pc from Versions import DRVersion, GKVersion, ORVersion JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '2647cc28bca3675152576dd1f5ea0bab' +RANDOMIZERBASEHASH = '33cd5e308266cf2273c80de1b1df3dac' class JsonRom(object): @@ -530,14 +530,15 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): # patch flute spots owFlags = 0 - if world.owFluteShuffle[player] == 'vanilla': + owFog = 0 + if world.owFluteShuffle[player] == 'vanilla' and world.owLayout[player] != 'grid': flute_spots = default_flute_connections else: flute_spots = world.owflutespots[player] owFlags |= 0x0100 write_int16(rom, snes_to_pc(0x0AB7F7), 0xEAEA) - flute_writes = sorted([(f, flute_data[f][1]) for f in flute_spots], key = lambda f: f[1]) + flute_writes = [(f, flute_data[f][1]) for f in flute_spots] for o in range(0, len(flute_writes)): owid = flute_writes[o][0] offset = 0 @@ -550,46 +551,64 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): write_int16(rom, snes_to_pc(0x02E8D1 + (o * 2)), data[13] if offset > 0 and len(data) > 13 else data[5]) # link Y write_int16(rom, snes_to_pc(0x02E8F3 + (o * 2)), data[14] if offset > 0 and len(data) > 13 else data[6]) # link X - if offset == 0 or len(data) <= 15: - write_int16(rom, snes_to_pc(0x02E86B + (o * 2)), data[2]) # vram - write_int16(rom, snes_to_pc(0x02E88D + (o * 2)), data[3]) # BG scroll Y - write_int16(rom, snes_to_pc(0x02E8AF + (o * 2)), data[4]) # BG scroll X - write_int16(rom, snes_to_pc(0x02E915 + (o * 2)), data[7]) # cam Y - write_int16(rom, snes_to_pc(0x02E937 + (o * 2)), data[8]) # cam X - write_int16(rom, snes_to_pc(0x02E959 + (o * 2)), data[9]) # unknown 1 - write_int16(rom, snes_to_pc(0x02E97B + (o * 2)), data[10]) # unknown 2 - rom.write_byte(snes_to_pc(0x0AB783 + o), data[12] & 0xff) # flute menu blip - X low byte - rom.write_byte(snes_to_pc(0x0AB78B + o), data[12] // 0x100) # flute menu blip - X high byte - rom.write_byte(snes_to_pc(0x0AB793 + o), data[11] & 0xff) # flute menu blip - Y low byte - rom.write_byte(snes_to_pc(0x0AB79B + o), data[11] // 0x100) # flute menu blip - Y high byte - else: # use alternate flute data - write_int16(rom, snes_to_pc(0x02E86B + (o * 2)), data[15]) # vram - write_int16(rom, snes_to_pc(0x02E88D + (o * 2)), data[16]) # BG scroll Y - write_int16(rom, snes_to_pc(0x02E8AF + (o * 2)), data[17]) # BG scroll X - write_int16(rom, snes_to_pc(0x02E915 + (o * 2)), data[18]) # cam Y - write_int16(rom, snes_to_pc(0x02E937 + (o * 2)), data[19]) # cam X - write_int16(rom, snes_to_pc(0x02E959 + (o * 2)), data[20]) # unknown 1 - write_int16(rom, snes_to_pc(0x02E97B + (o * 2)), data[21]) # unknown 2 - rom.write_byte(snes_to_pc(0x0AB783 + o), data[23] & 0xff) # flute menu blip - X low byte - rom.write_byte(snes_to_pc(0x0AB78B + o), data[23] // 0x100) # flute menu blip - X high byte - rom.write_byte(snes_to_pc(0x0AB793 + o), data[22] & 0xff) # flute menu blip - Y low byte - rom.write_byte(snes_to_pc(0x0AB79B + o), data[22] // 0x100) # flute menu blip - Y high byte + base_index = 0 + if not (offset == 0 or len(data) <= 15): + base_index = 13 # use alternate flute data + write_int16(rom, snes_to_pc(0x02E86B + (o * 2)), data[base_index + 2]) # vram + write_int16(rom, snes_to_pc(0x02E88D + (o * 2)), data[base_index + 3]) # BG scroll Y + write_int16(rom, snes_to_pc(0x02E8AF + (o * 2)), data[base_index + 4]) # BG scroll X + if base_index > 0: + base_index -= 2 + write_int16(rom, snes_to_pc(0x02E915 + (o * 2)), data[base_index + 7]) # cam Y + write_int16(rom, snes_to_pc(0x02E937 + (o * 2)), data[base_index + 8]) # cam X + write_int16(rom, snes_to_pc(0x02E959 + (o * 2)), data[base_index + 9]) # unknown 1 + write_int16(rom, snes_to_pc(0x02E97B + (o * 2)), data[base_index + 10]) # unknown 2 + map_x, map_y = adjust_ow_coordinates_to_layout(world, player, data[base_index + 12], data[base_index + 11], world.mode[player] == 'inverted') + rom.write_byte(snes_to_pc(0x0AB783 + o), map_x & 0xff) # flute menu blip - X low byte + rom.write_byte(snes_to_pc(0x0AB78B + o), map_x // 0x100) # flute menu blip - X high byte + rom.write_byte(snes_to_pc(0x0AB793 + o), map_y & 0xff) # flute menu blip - Y low byte + rom.write_byte(snes_to_pc(0x0AB79B + o), map_y // 0x100) # flute menu blip - Y high byte # patch whirlpools if world.owWhirlpoolShuffle[player]: owFlags |= 0x01 write_int16s(rom, snes_to_pc(0x02EA5C), world.owwhirlpools[player]) + # set custom overworld map layout and fog + if world.owLayout[player] == 'grid': + owFlags |= 0x06 + owFog = 1 if world.owParallel[player] else 2 + grid = world.owgrid[player] + all_rows = grid[0] + grid[1] + all_cells = sum(all_rows, []) + rom.write_bytes(0x153C80, all_cells) + for pos, cell_id in enumerate(sum(grid[0], [])): + rom.write_byte(0x153D00 + cell_id % 0x40, pos) + for pos, cell_id in enumerate(sum(grid[1], [])): + rom.write_byte(0x153D40 + cell_id % 0x40, pos) + elif world.owMixed[player]: + owFlags |= 0x02 + owFog = 1 + large_screen_ids = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35, 0x40, 0x43, 0x45, 0x58, 0x5B, 0x5E, 0x70, 0x75] + for cell_id in range(0x80): + if cell_id - 0x01 in large_screen_ids: + screen_id = cell_id - 0x01 + elif cell_id - 0x08 in large_screen_ids: + screen_id = cell_id - 0x08 + elif cell_id - 0x09 in large_screen_ids: + screen_id = cell_id - 0x09 + else: + screen_id = cell_id + world_flag = 0x40 if screen_id in world.owswaps[player][0] else 0x00 + rom.write_byte(0x153C80 + cell_id, cell_id ^ world_flag) + # patch overworld edges inverted_buffer = [0] * 0x82 owMode = 0 - if world.owShuffle[player] != 'vanilla' or world.owCrossed[player] not in ['none', 'polar'] or world.owMixed[player]: - if world.owShuffle[player] == 'parallel': - owMode = 1 - elif world.owShuffle[player] == 'full': - owMode = 2 - - if world.owKeepSimilar[player] and (world.owShuffle[player] != 'vanilla' or world.owCrossed[player] == 'unrestricted'): + if world.owLayout[player] != 'vanilla' or world.owCrossed[player] not in ['none', 'polar'] or world.owMixed[player]: + if world.owLayout[player] != 'vanilla': + owMode = 1 if world.owParallel[player] else 2 + if world.owKeepSimilar[player] and (world.owLayout[player] != 'vanilla' or world.owCrossed[player] == 'unrestricted'): owMode |= 0x0100 if world.owCrossed[player] != 'none' and (world.owCrossed[player] != 'polar' or world.owMixed[player]): owMode |= 0x0200 @@ -624,10 +643,11 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): rom.write_byte(0x1539B0 + b + 9, world_flag) for edge in world.owedges: - if edge.dest is not None and isinstance(edge.dest, OWEdge) and edge.player == player: + if edge.player == player: write_int16(rom, edge.getAddress() + 0x0a, edge.vramLoc) if not edge.specialExit: - rom.write_byte(0x1539A0 + (edge.specialID - 0x80) * 2 if edge.specialEntrance else edge.getAddress() + 0x0e, edge.getTarget()) + destination = edge.getTarget() if edge.dest is not None and isinstance(edge.dest, OWEdge) else 0xFF + rom.write_byte(0x1539A0 + (edge.specialID - 0x80) * 2 if edge.specialEntrance else edge.getAddress() + 0x0e, destination) # patch bonk prizes if world.shuffle_bonk_drops[player]: @@ -656,6 +676,7 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): write_int16(rom, 0x150002, owMode) write_int16(rom, 0x150004, owFlags) + write_int16(rom, 0x150008, owFog if world.owFog[player] else 0x00) # patch entrance/exits/holes for region in world.regions: @@ -1317,8 +1338,8 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): else: goal_bytes += int16_as_bytes(req['target']) elif req['condition'] & 0x80 == 0: - if req['condition'] & 0x7F == 0x06 or req['condition'] & 0x7F == 0x07: - # agahnims have no target value + if req['condition'] & 0x7F in [0x00, 0x06, 0x07]: + # no target value pass elif req['condition'] & 0x7F < 0x08: goal_bytes += [req['target']] @@ -1459,9 +1480,15 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): | (0x04 if world.mapshuffle[player] != 'none' else 0x00) | (0x08 if world.bigkeyshuffle[player] != 'none' else 0x00))) # free roaming item text boxes rom.write_byte(0x18003B, 0x01 if world.mapshuffle[player] not in ['none', 'nearby'] else 0x00) # maps showing crystals on overworld - if world.keyshuffle[player] != 'universal' and (world.mapshuffle[player] not in ['none', 'nearby'] or world.doorShuffle[player] != 'vanilla' - or world.dropshuffle[player] != 'none' or world.pottery[player] not in ['none', 'cave']): - rom.write_byte(0x18003A, 0x01) # show key counts on map pickup + map_hud_mode = 0x00 + if world.dungeon_counters[player] == 'on': + map_hud_mode = 0x02 # always on + elif world.dungeon_counters[player] == 'off': + pass + elif world.keyshuffle[player] != 'universal' and (world.mapshuffle[player] not in ['none', 'nearby'] or world.doorShuffle[player] != 'vanilla' + or world.dropshuffle[player] != 'none' or world.pottery[player] not in ['none', 'cave'] or world.dungeon_counters[player] == 'pickup'): + map_hud_mode = 0x01 # show on pickup + rom.write_byte(0x18003A, map_hud_mode) loot_source = 0x09 if world.prizeshuffle[player] != 'none': @@ -1535,36 +1562,53 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): rom.write_byte(0x18003C, compass_mode) def get_entrance_coords(ent): - if ent is None: - owid_map = [0x1E, 0x30, 0xFF, 0x7B, 0x5E, 0x70, 0x40, 0x75, 0x03, 0x58, 0x47] - x_map_position_generic = [0x03c0, 0x0740, 0xff00, 0x03c0, 0x01c0, 0x0bc0, 0x05c0, 0x09c0, 0x0ac0, 0x07c0, 0x0dc0] - y_map_position_generic = [0xff00, 0xff00, 0xff00, 0x0fc0, 0x0fc0, 0x0fc0, 0x0fc0, 0x0fc0, 0xff00, 0x0fc0, 0x0fc0] - world_indicator = 0x0000 - idx = int((map_index-2)/2) + if ent is None or isinstance(ent, int): + owid_map = [ 0x1E, 0x30, 0xFF, 0x7B, 0x5E, 0x70, 0x40, 0x75, 0x03, 0x58, 0x47 ] + coord_flags = 0x0000 + if ent is None: + # HUD-style dislocated icons + x_map_position = [0x0050, 0x0080, 0xFF00, 0x0040, 0x0020, 0x00C0, 0x0060, 0x00A0, 0x00B0, 0x0080, 0x00E0] + y_map_position = [0x000C, 0x000C, 0xFF00, 0x00D4, 0x00D4, 0x00D4, 0x00D4, 0x00D4, 0x000C, 0x00D4, 0x00D4] + coord_flags = 0x4000 # raw OAM coord flag + idx = int((map_index-2)/2) + elif isinstance(ent, int): + # vanilla icon positions + x_map_position = [0x0F30, 0x0170, 0xFF00, 0x0790, 0x0F30, 0x0160, 0x00F0, 0x0CB0, 0x0900, 0x0240, 0x0F30] + y_map_position = [0x06E0, 0x0E50, 0xFF00, 0x0FD0, 0x06E0, 0x0D80, 0x0160, 0x0E80, 0x0130, 0x0840, 0x01B0] + idx = ent owid = owid_map[idx] + map_x = x_map_position[idx] + map_y = y_map_position[idx] if owid != 0xFF: if (owid < 0x40) == (world.is_tile_swapped(owid, player)): - world_indicator = 0x8000 - return [world_indicator | x_map_position_generic[idx], y_map_position_generic[idx]] - if type(ent) is Location: - from OverworldShuffle import OWTileRegions - if ent.name == 'Hobo': - coords = (0xb80, 0xb80) - elif ent.name == 'Master Sword Pedestal': - coords = (0x06d, 0x070) + coord_flags |= 0x8000 # world indicator flag + if coord_flags & 0x4000 == 0: + map_x, map_y = adjust_ow_coordinates_to_layout(world, player, map_x, map_y, coord_flags & 0x8000 != 0) + return (coord_flags | map_x, map_y) + elif type(ent) is Location: + from OverworldShuffle import OWTileRegions, ow_loc_prize_table + if ent.name in ow_loc_prize_table: + coords = ow_loc_prize_table[ent.name] else: owid = OWTileRegions[ent.parent_region.name] if owid == 0x81: - coords = (0x220, 0xf40) + coords = (0xF60, 0x280) else: owid = owid % 0x40 coords = (0x200 * (owid % 0x08) + 0x100, 0x200 * int(owid / 0x08) + 0x100) - if owid in [0x00, 0x03, 0x05, 0x18, 0x1b, 0x1e, 0x30, 0x35]: + if owid in [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35]: coords = (coords[0] + 0x100, coords[1] + 0x100) else: - coords = ow_prize_table[ent.name] - coords = ((0x8000 if ent.parent_region.type == RegionType.DarkWorld else 0x0000) | coords[0], coords[1]) + if ent.name in ow_prize_table: + coords = ow_prize_table[ent.name] + elif door_addresses[ent.name][1] is not None: + coords = (door_addresses[ent.name][1][6], door_addresses[ent.name][1][5]) + else: + raise Exception(f"No overworld map coordinates for entrance {ent.name}") + map_x, map_y = adjust_ow_coordinates_to_layout(world, player, coords[0], coords[1], ent.parent_region.type == RegionType.DarkWorld) + coords = ((0x8000 if ent.parent_region.type == RegionType.DarkWorld else 0x0000) | map_x, map_y) return coords + if world.overworld_map[player] == 'default': # disable HC/AT/GT icons if not world.owMixed[player]: @@ -1591,16 +1635,16 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): # prize location write_int16s(rom, snes_to_pc(0x0ABE2E)+(map_index*6)+8, coords) if world.shuffle[player] == 'vanilla' or world.overworld_map[player] == 'default': - # TODO: I think this is logically the same as some of the vanilla stuff below - vanilla_entrances = { 'Hyrule Castle': 'Hyrule Castle Entrance (South)', - 'Desert Palace': 'Desert Palace Entrance (North)', - 'Skull Woods': 'Skull Woods Final Section' } - entrance_name = vanilla_entrances[dungeon] if dungeon in vanilla_entrances else dungeon - if world.is_atgt_swapped(player): - swap_entrances = { 'Agahnims Tower': 'Ganons Tower', - 'Ganons Tower': 'Agahnims Tower' } - entrance_name = swap_entrances[dungeon] if dungeon in swap_entrances else entrance_name - entrance = world.get_entrance(entrance_name, player) + swap_entrances = { 'Agahnims Tower': 'Ganons Tower', + 'Ganons Tower': 'Agahnims Tower' } + if dungeon in swap_entrances: + entrance_name = dungeon + if world.is_atgt_swapped(player): + entrance_name = swap_entrances[dungeon] + entrance = world.get_entrance(entrance_name, player) + coords = get_entrance_coords(entrance) + else: + coords = get_entrance_coords(int((map_index-2)/2)) else: if len(portal_list) == 1: portal_idx = 0 @@ -1620,8 +1664,8 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): offset += 4 portal = world.get_portal(portal_list[portal_idx], player) entrance = portal.find_portal_entrance() - coords = get_entrance_coords(entrance) - + coords = get_entrance_coords(entrance) + # figure out compass entrances and what world (light/dark) write_int16s(rom, snes_to_pc(0x0ABE2E)+(map_index*6), coords) if world.prizeshuffle[player] in ['none', 'dungeon', 'nearby'] and dungeon_table[dungeon].prize: @@ -2542,7 +2586,7 @@ def write_strings(rom, world, player, team): if world.is_tile_swapped(0x18, player) or world.flute_mode[player] in ['active', 'pseudo']: items_to_hint.remove(flute_item) 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. this_location = world.find_items_not_key_only(flute_item, player) if this_location and this_location not in hinted_locations: @@ -2560,7 +2604,7 @@ def write_strings(rom, world, player, team): random.shuffle(items_to_hint) hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'district', 'swapped'] else 8 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: this_item = items_to_hint.pop(0) this_location = world.find_items_not_key_only(this_item, player) @@ -3194,6 +3238,13 @@ def update_compasses(rom, dungeon_locations, world, player): if not provided_dungeon: rom.write_byte(0x186FFF, 0xff) +def adjust_ow_coordinates_to_layout(world, player, x, y, dw_flag): + if world.owLayout[player] != 'grid': + return (x, y) + layout_map = world.owlayoutmap_dw[player] if dw_flag else world.owlayoutmap_lw[player] + original_slot_id = ((y // 0x0200) % 0x08) * 0x08 + ((x // 0x0200) % 0x08) + new_slot_id = layout_map[original_slot_id] + return ((new_slot_id % 0x08) * 0x0200 + x % 0x0200, ((new_slot_id // 0x08) % 0x08) * 0x0200 + y % 0x0200) InconvenientDungeonEntrances = {'Turtle Rock': 'Turtle Rock Main', diff --git a/Rules.py b/Rules.py index 8707151e..1c9ba3f6 100644 --- a/Rules.py +++ b/Rules.py @@ -1832,7 +1832,9 @@ def standard_rules(world, player): add_rule(world.get_location('Central Bonk Rocks Tree', player), lambda state: state.has('Zelda Delivered', player)) if not world.is_premature_copied_world: - add_rule(world.get_location('Hyrule Castle Courtyard Tree Pull', player), lambda state: state.has('Zelda Delivered', player)) + loc = world.get_location_unsafe('Hyrule Castle Courtyard Tree Pull', player) + if loc: + add_rule(loc, lambda state: state.has('Zelda Delivered', player)) # don't allow bombs to get past here before zelda is rescued set_rule(world.get_entrance('GT Hookshot South Entry to Ranged Crystal', player), lambda state: (state.can_use_bombs(player) and state.has('Zelda Delivered', player)) or state.has('Blue Boomerang', player) or state.has('Red Boomerang', player)) # or state.has('Cane of Somaria', player)) diff --git a/Utils.py b/Utils.py index 1f3663e4..83ece04a 100644 --- a/Utils.py +++ b/Utils.py @@ -356,6 +356,26 @@ def update_deprecated_args(args): else: args.shuffleganon = not args.no_shuffleganon in truthy + # OW Parallel defaults to TRUE + # Don't do: Yes + # Do: No + if "ow_unparallel" in argVars: + if isinstance(args.ow_parallel, dict): + for player in range(1, players + 1): + args.ow_parallel[player] = not args.ow_unparallel in truthy + else: + args.ow_parallel = not args.ow_unparallel in truthy + + # OW Fog defaults to TRUE + # Don't do: Yes + # Do: No + if "ow_no_fog" in argVars: + if isinstance(args.ow_fog, dict): + for player in range(1, players + 1): + args.ow_fog[player] = not args.ow_no_fog in truthy + else: + args.ow_fog = not args.ow_no_fog in truthy + # Playthrough defaults to TRUE # Don't do: Yes # Do: No diff --git a/Versions.py b/Versions.py index 639ac093..7dca9f70 100644 --- a/Versions.py +++ b/Versions.py @@ -1,3 +1,3 @@ GKVersion = '1.0.0' -ORVersion = '0.6.1.7' -DRVersion = '1.5.0-u' +ORVersion = '0.7.0.1' +DRVersion = '1.5.2-u' diff --git a/data/base2current.bps b/data/base2current.bps index 79683ba6..cac30eb8 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ diff --git a/data/overworld/darkworld.png b/data/overworld/darkworld.png new file mode 100644 index 00000000..0c5c45fb Binary files /dev/null and b/data/overworld/darkworld.png differ diff --git a/data/overworld/lightworld.png b/data/overworld/lightworld.png new file mode 100644 index 00000000..9f2b3d7e Binary files /dev/null and b/data/overworld/lightworld.png differ diff --git a/docs/Customizer.md b/docs/Customizer.md index 17c5c337..5d5e083d 100644 --- a/docs/Customizer.md +++ b/docs/Customizer.md @@ -234,7 +234,7 @@ You may define a list of items and a list of locations. Those items will be cons ### 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 @@ -253,6 +253,14 @@ someDescription: - Stone Bridge WS* ``` +### ow-grid + +`grid` contains additional options that only have an effect when `ow_layout` is set to `grid`. + +#### wrap_horizontal / wrap_vertical + +Set these to `true` to allow for overworld edge transitions to wrap from one side of a world to the opposite side. With `wrap_horizontal`, there can be east transitions on the eastern edge of the world map that send the player to the western edge of the world. With `wrap_vertical`, there can be south transitions on the southern edge of the world map that send the player to the northern edge of the world. + ### ow-crossed This must be defined by player. Each player number should be listed with the appropriate sections and each of these players MUST have `ow_crossed` enabled in the `settings` section in order for any values here to take effect. This section has four primary subsections: `force_crossed`, `force_noncrossed`, `limit_crossed`, and `undefined_chance`. There are also diff --git a/docs/customizer_example.yaml b/docs/customizer_example.yaml index 5390eeb3..c2f3943c 100644 --- a/docs/customizer_example.yaml +++ b/docs/customizer_example.yaml @@ -20,7 +20,8 @@ settings: shuffle_followers: true shuffle: crossed shufflelinks: true - ow_shuffle: parallel + ow_layout: wild + ow_parallel: true ow_terrain: true ow_crossed: grouped ow_keepsimilar: true diff --git a/presets/world/owr_districtshuffle-full.yaml b/presets/world/owr_districtshuffle-full.yaml index e9e62916..8903fb58 100644 --- a/presets/world/owr_districtshuffle-full.yaml +++ b/presets/world/owr_districtshuffle-full.yaml @@ -1,6 +1,7 @@ settings: 1: - ow_shuffle: full + ow_layout: wild + ow_parallel: false ow_keepsimilar: false ow-edges: 1: diff --git a/presets/world/owr_districtshuffle-pangea.yaml b/presets/world/owr_districtshuffle-pangea.yaml index e4d1ff3c..7f47e4f0 100644 --- a/presets/world/owr_districtshuffle-pangea.yaml +++ b/presets/world/owr_districtshuffle-pangea.yaml @@ -1,6 +1,7 @@ settings: 1: - ow_shuffle: full + ow_layout: wild + ow_parallel: false ow_keepsimilar: false ow-edges: 1: diff --git a/presets/world/owr_districtshuffle-vanillaborders.yaml b/presets/world/owr_districtshuffle-vanillaborders.yaml index 016dd9da..8f3f29d5 100644 --- a/presets/world/owr_districtshuffle-vanillaborders.yaml +++ b/presets/world/owr_districtshuffle-vanillaborders.yaml @@ -1,6 +1,7 @@ settings: 1: - ow_shuffle: full + ow_layout: wild + ow_parallel: false ow_keepsimilar: false ow-edges: 1: diff --git a/presets/world/owr_vanilla-mirroredsimilar.yaml b/presets/world/owr_vanilla-mirroredsimilar.yaml index f9411f08..9f302899 100644 --- a/presets/world/owr_vanilla-mirroredsimilar.yaml +++ b/presets/world/owr_vanilla-mirroredsimilar.yaml @@ -1,6 +1,7 @@ settings: 1: - ow_shuffle: full + ow_layout: wild + ow_parallel: false ow_terrain: true ow-edges: 1: diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 591bc892..425fcb18 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -177,6 +177,22 @@ "full" ] }, + "ow_layout": { + "choices": [ + "vanilla", + "grid", + "wild" + ] + }, + "ow_parallel": { + "action": "store_true", + "help": "suppress", + "type": "bool" + }, + "ow_unparallel": { + "action": "store_true", + "dest": "ow_parallel" + }, "ow_terrain": { "action": "store_true", "type": "bool" @@ -212,6 +228,15 @@ "random" ] }, + "ow_fog": { + "action": "store_true", + "help": "suppress", + "type": "bool" + }, + "ow_no_fog": { + "action": "store_true", + "dest": "ow_fog" + }, "shuffle": { "choices": [ "vanilla", diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index fa4f015e..c63ef213 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -235,14 +235,18 @@ "the entrances vanilla." ], "ow_shuffle": [ + "Deprecated, use ow_layout and ow_unparallel instead." + ], + "ow_layout": [ "This shuffles the layout of the overworld.", "Vanilla: All overworld transitions are connected the same", " way they were in the base game.", - "Parallel: Overworld transitions are shuffled, but both worlds", - " will have the same pattern/shape.", - "Full: Overworld transitions are shuffled, but both worlds", - " will have an independent map shape." + "Grid: OW Screens are arranged on 8x8 grids and OW Transitions", + " work according to this arrangement.", + "Wild: OW Transitions are shuffled with no respect to geometric coherence." ], + "ow_unparallel": [ + "With OW Layout Shuffle, this no longer forces both worlds to have a matching layout." ], "ow_terrain": [ "With OW Layout Shuffle, this allows land and water edges to be connected." ], "ow_crossed": [ @@ -277,6 +281,9 @@ " spots from being on any adjacent screen.", "Random: New flute spots will be generated with minimal bias." ], + "ow_no_fog": [ + "With OW Grid Layout Shuffle or Mixed, this disables the fog that prevents", + "you from seeing unvisited screens on the overworld map." ], "door_shuffle": [ "Select Door Shuffling Algorithm. (default: %(default)s)", "Basic: Doors are mixed within a single dungeon.", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 0ad57757..c43a383e 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -157,10 +157,12 @@ "randomizer.enemizer.enemylogic.allow_all": "Allow special enemies anywhere", - "randomizer.overworld.overworldshuffle": "Layout Shuffle", - "randomizer.overworld.overworldshuffle.vanilla": "Vanilla", - "randomizer.overworld.overworldshuffle.parallel": "Parallel", - "randomizer.overworld.overworldshuffle.full": "Full", + "randomizer.overworld.layout": "Layout Shuffle", + "randomizer.overworld.layout.vanilla": "Vanilla", + "randomizer.overworld.layout.grid": "Grid", + "randomizer.overworld.layout.wild": "Wild", + + "randomizer.overworld.parallel": "Keep Worlds Parallel", "randomizer.overworld.terrain": "Free Terrain", @@ -181,6 +183,8 @@ "randomizer.overworld.overworldflute.balanced": "Balanced", "randomizer.overworld.overworldflute.random": "Random", + "randomizer.overworld.fog": "Overworld Map Fog", + "randomizer.entrance.openpyramid": "Pre-open Pyramid Hole", "randomizer.entrance.openpyramid.auto": "Auto", diff --git a/resources/app/gui/randomize/overworld/widgets.json b/resources/app/gui/randomize/overworld/widgets.json index c3c49c94..9a58ad66 100644 --- a/resources/app/gui/randomize/overworld/widgets.json +++ b/resources/app/gui/randomize/overworld/widgets.json @@ -1,13 +1,13 @@ { "topOverworldFrame": {}, "leftOverworldFrame": { - "overworldshuffle": { + "layout": { "type": "selectbox", "default": "vanilla", "options": [ "vanilla", - "parallel", - "full" + "grid", + "wild" ] }, "crossed": { @@ -18,7 +18,10 @@ "grouped", "polar", "unrestricted" - ] + ], + "config": { + "pady": [16,0] + } }, "mixed": { "type": "checkbox", @@ -45,22 +48,27 @@ "config": { "pady": [20,0] } + }, + "fog": { + "type": "checkbox", + "default": true, + "config": { + "pady": [20,0] + } } }, "rightOverworldFrame": { + "parallel": { + "type": "checkbox", + "default": true + }, "terrain": { "type": "checkbox", - "default": false, - "config": { - "pady": [3,0] - } + "default": false }, "keepsimilar": { "type": "checkbox", - "default": false, - "config": { - "pady": [6,0] - } + "default": false } } } \ No newline at end of file diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index 780b4969..c5695cfe 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -90,7 +90,8 @@ class CustomSettings(object): args.mystery = True else: 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_crossed[p] = get_setting(settings['ow_crossed'], args.ow_crossed[p]) if args.ow_crossed[p] == 'chaos': @@ -101,6 +102,7 @@ class CustomSettings(object): args.ow_mixed[p] = get_setting(settings['ow_mixed'], args.ow_mixed[p]) args.ow_whirlpool[p] = get_setting(settings['ow_whirlpool'], args.ow_whirlpool[p]) args.ow_fluteshuffle[p] = get_setting(settings['ow_fluteshuffle'], args.ow_fluteshuffle[p]) + args.ow_fog[p] = get_setting(settings['ow_fog'], args.ow_fog[p]) args.shuffle_followers[p] = get_setting(settings['shuffle_followers'], args.shuffle_followers[p]) args.bonk_drops[p] = get_setting(settings['bonk_drops'], args.bonk_drops[p]) args.shuffle[p] = get_setting(settings['shuffle'], args.shuffle[p]) @@ -136,6 +138,14 @@ class CustomSettings(object): args.take_any[p] = 'random' if args.take_any[p] == 'none' else args.take_any[p] 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.standardize_palettes[p] = get_setting(settings['standardize_palettes'], args.standardize_palettes[p]) @@ -254,6 +264,11 @@ class CustomSettings(object): return self.file_source['ow-edges'] return None + def get_owgrid(self): + if 'ow-grid' in self.file_source: + return self.file_source['ow-grid'] + return None + def get_owcrossed(self): if 'ow-crossed' in self.file_source: return self.file_source['ow-crossed'] @@ -357,13 +372,15 @@ class CustomSettings(object): self.world_rep['start_inventory'] = start_inv = {} for p in self.player_range: 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_crossed'] = world.owCrossed[p] settings_dict[p]['ow_keepsimilar'] = world.owKeepSimilar[p] settings_dict[p]['ow_mixed'] = world.owMixed[p] settings_dict[p]['ow_whirlpool'] = world.owWhirlpoolShuffle[p] settings_dict[p]['ow_fluteshuffle'] = world.owFluteShuffle[p] + settings_dict[p]['ow_fog'] = world.owFog[p] settings_dict[p]['shuffle_followers'] = world.shuffle_followers[p] settings_dict[p]['bonk_drops'] = world.shuffle_bonk_drops[p] settings_dict[p]['shuffle'] = world.shuffle[p] diff --git a/source/classes/constants.py b/source/classes/constants.py index c8e017ef..069afeae 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -92,13 +92,15 @@ SETTINGSTOPROCESS = { "bombbag": "bombbag" }, "overworld": { - "overworldshuffle": "ow_shuffle", + "layout": "ow_layout", + "parallel": "ow_parallel", "terrain": "ow_terrain", "crossed": "ow_crossed", "keepsimilar": "ow_keepsimilar", "mixed": "ow_mixed", "whirlpool": "ow_whirlpool", - "overworldflute": "ow_fluteshuffle" + "overworldflute": "ow_fluteshuffle", + "fog": "ow_fog" }, "entrance": { "entranceshuffle": "shuffle", diff --git a/source/dungeon/EnemyList.py b/source/dungeon/EnemyList.py index 2511361f..f86f5f53 100644 --- a/source/dungeon/EnemyList.py +++ b/source/dungeon/EnemyList.py @@ -2116,8 +2116,13 @@ class EnemyTable: self.room_map = defaultdict(list) self.special_bitmasks = None - def write_sprite_data_to_rom(self, rom): - pointer_address = snes_to_pc(0x09D62E) + def write_sprite_data_to_rom(self, rom, pointer_table): + ow_data_end = pointer_table['ow_sprites'][0] + pointer_table['ow_sprites'][1] + if ow_data_end > pointer_table['uw_sprites'][2]: + # moving pointer table down + pointer_table['uw_sprites'][2] = ow_data_end + rom.write_bytes(snes_to_pc(pointer_table['uw_sprites'][3]), int16_as_bytes(ow_data_end & 0xFFFF)) + pointer_address = snes_to_pc(pointer_table['uw_sprites'][2]) data_pointer = snes_to_pc(0x288000) empty_pointer = pc_to_snes(data_pointer) & 0xFFFF rom.write_bytes(data_pointer, [0x00, 0xff]) diff --git a/source/enemizer/SpriteSheets.py b/source/enemizer/SpriteSheets.py index 47b79e92..0cb77433 100644 --- a/source/enemizer/SpriteSheets.py +++ b/source/enemizer/SpriteSheets.py @@ -183,9 +183,9 @@ def init_sprite_requirements(): SpriteRequirement(EnemySprite.Vulture).no_drop().sub_group(2, 0x12).exclude(NoFlyingRooms), SpriteRequirement(EnemySprite.CorrectPullSwitch).affix().sub_group(3, [0x52, 0x53]), SpriteRequirement(EnemySprite.WrongPullSwitch).affix().sub_group(3, [0x52, 0x53]), - SpriteRequirement(EnemySprite.Octorok).aquaphobia().sub_group(2, [0xc, 0x18]), + SpriteRequirement(EnemySprite.Octorok).sub_group(2, [0xc, 0x18]), SpriteRequirement(EnemySprite.Moldorm).exalt().sub_group(2, 0x30), - SpriteRequirement(EnemySprite.Octorok4Way).aquaphobia().sub_group(2, 0xc), + SpriteRequirement(EnemySprite.Octorok4Way).sub_group(2, 0xc), SpriteRequirement(EnemySprite.Cucco).immune().sub_group(3, [0x15, 0x50]).exclude(NoFlyingRooms), SpriteRequirement(EnemySprite.Buzzblob).sub_group(3, 0x11), SpriteRequirement(EnemySprite.Snapdragon).sub_group(0, 0x16).sub_group(2, 0x17), @@ -197,7 +197,7 @@ def init_sprite_requirements(): .exclude(NoFlyingRooms).exclude({0x40}), # no anti-fairies in aga tower bridge room SpriteRequirement(EnemySprite.Wiseman).affix().sub_group(2, 0x4c), SpriteRequirement(EnemySprite.Hoarder).sub_group(3, 0x11).exclude({0x10c}), - SpriteRequirement(EnemySprite.MiniMoldorm).aquaphobia().sub_group(1, 0x1e), + SpriteRequirement(EnemySprite.MiniMoldorm).sub_group(1, 0x1e), SpriteRequirement(EnemySprite.Poe).no_drop().sub_group(3, 0x15).exclude(NoFlyingRooms), SpriteRequirement(EnemySprite.Smithy).affix().sub_group(1, 0x1d).sub_group(3, 0x15), SpriteRequirement(EnemySprite.Statue).stasis().immune().sub_group(3, [0x52, 0x53]), @@ -231,12 +231,12 @@ def init_sprite_requirements(): SpriteRequirement(EnemySprite.Hoarder2).sub_group(3, 0x11).exclude({0x10c}), SpriteRequirement(EnemySprite.TutorialGuard).affix(), SpriteRequirement(EnemySprite.LightningGate).affix().sub_group(3, 0x3f), - SpriteRequirement(EnemySprite.BlueGuard).aquaphobia().sub_group(1, [0xd, 0x49]).exclude(PitRooms), - SpriteRequirement(EnemySprite.BlueGuard).aquaphobia().sub_group(1, [0xd, 0x49]).sub_group(2, [0x29, 0x13]), - SpriteRequirement(EnemySprite.GreenGuard).aquaphobia().sub_group(1, 0x49).exclude(PitRooms), - SpriteRequirement(EnemySprite.GreenGuard).aquaphobia().sub_group(1, 0x49).sub_group(2, 0x13), - SpriteRequirement(EnemySprite.RedSpearGuard).aquaphobia().sub_group(1, [0xd, 0x49]).exclude(PitRooms), - SpriteRequirement(EnemySprite.RedSpearGuard).aquaphobia().sub_group(1, [0xd, 0x49]).sub_group(2, [0x29, 0x13]), + SpriteRequirement(EnemySprite.BlueGuard).sub_group(1, [0xd, 0x49]).exclude(PitRooms), + SpriteRequirement(EnemySprite.BlueGuard).sub_group(1, [0xd, 0x49]).sub_group(2, [0x29, 0x13]), + SpriteRequirement(EnemySprite.GreenGuard).sub_group(1, 0x49).exclude(PitRooms), + SpriteRequirement(EnemySprite.GreenGuard).sub_group(1, 0x49).sub_group(2, 0x13), + SpriteRequirement(EnemySprite.RedSpearGuard).sub_group(1, [0xd, 0x49]).exclude(PitRooms), + SpriteRequirement(EnemySprite.RedSpearGuard).sub_group(1, [0xd, 0x49]).sub_group(2, [0x29, 0x13]), SpriteRequirement(EnemySprite.BluesainBolt).aquaphobia().sub_group(0, 0x46).sub_group(1, [0xd, 0x49]), SpriteRequirement(EnemySprite.UsainBolt).aquaphobia().sub_group(1, [0xd, 0x49]), SpriteRequirement(EnemySprite.BlueArcher).sub_group(0, 0x48).sub_group(1, 0x49), diff --git a/source/overworld/EntranceData.py b/source/overworld/EntranceData.py index a9fe0b2a..095615e1 100644 --- a/source/overworld/EntranceData.py +++ b/source/overworld/EntranceData.py @@ -147,139 +147,149 @@ door_addresses = {'Links House': (0x00, (0x0104, 0x2c 'Lake Hylia Fortune Teller': (0x72, (0x0122, 0x35, 0x0380, 0x0c6a, 0x0a00, 0x0cb8, 0x0a58, 0x0cd7, 0x0a85, 0x06, 0xfa, 0x0000, 0x0000), 0x00), 'Kakariko Gamble Game': (0x66, (0x0118, 0x29, 0x069e, 0x0ac4, 0x02ea, 0x0b18, 0x0368, 0x0b33, 0x036f, 0x0a, 0xf6, 0x09AC, 0x0000), 0x00)} -ow_prize_table = {'Links House': (0x8b1, 0xb2d), - 'Desert Palace Entrance (South)': (0x108, 0xd70), 'Desert Palace Entrance (West)': (0x031, 0xca0), - 'Desert Palace Entrance (North)': (0x0e1, 0xba0), 'Desert Palace Entrance (East)': (0x191, 0xca0), - 'Eastern Palace': (0xf31, 0x620), 'Tower of Hera': (0x8D0, 0x080), - 'Hyrule Castle Entrance (South)': (0x820, 0x730), 'Hyrule Castle Entrance (West)': (0x740, 0x5D0), - 'Hyrule Castle Entrance (East)': (0x8f0, 0x5D0), - 'Agahnims Tower': (0x820, 0x5D0), - 'Thieves Town': (0x1d0, 0x780), 'Skull Woods First Section Door': (0x2e0, 0x280), - 'Skull Woods Second Section Door (East)': (0x200, 0x240), - 'Skull Woods Second Section Door (West)': (0x0c0, 0x1c0), - 'Skull Woods Final Section': (0x082, 0x0b0), - 'Skull Woods First Section Hole (West)': (0x200, 0x2b0), - 'Skull Woods First Section Hole (East)': (0x340, 0x2e0), - 'Skull Woods First Section Hole (North)': (0x320, 0x1e0), - 'Skull Woods Second Section Hole': (0x0f0, 0x0b0), - 'Ice Palace': (0xca0, 0xda0), - 'Misery Mire': (0x100, 0xca0), - 'Palace of Darkness': (0xf40, 0x620), 'Swamp Palace': (0x759, 0xED0), - 'Turtle Rock': (0xf11, 0x103), - 'Dark Death Mountain Ledge (West)': (0xb80, 0x180), - 'Dark Death Mountain Ledge (East)': (0xc80, 0x180), - 'Turtle Rock Isolated Ledge Entrance': (0xc00, 0x240), +ow_prize_table = {'Lost Woods Gamble': (0x2A0, 0x080), + 'Lost Woods Hideout Drop': (0x338, 0x218), + 'Lost Woods Hideout Stump': (0x2A0, 0x2E0), + 'Lumberjack House': (0x538, 0x158), + 'Lumberjack Tree Tree': (0x4A0, 0x160), + 'Lumberjack Tree Cave': (0x550, 0x004), + 'Tower of Hera': (0x900, 0x0E0), + 'Spectacle Rock Cave Peak': (0x758, 0x1B7), + 'Spectacle Rock Cave (Bottom)': (0x6F0, 0x257), + 'Spectacle Rock Cave': (0x7C8, 0x257), + 'Death Mountain Return Cave (East)': (0x648, 0x278), + 'Old Man Cave (East)': (0x658, 0x387), + 'Old Man House (Top)': (0x8C8, 0x2D8), + 'Old Man House (Bottom)': (0x728, 0x3A7), + 'Paradox Cave (Top)': (0xDE0, 0x100), + 'Paradox Cave (Bottom)': (0xDC8, 0x358), + 'Paradox Cave (Middle)': (0xDDF, 0x3FF), + 'Fairy Ascension Cave (Top)': (0xCD8, 0x2B8), + 'Fairy Ascension Cave (Bottom)': (0xC78, 0x358), + 'Spiral Cave': (0xC78, 0x207), + 'Spiral Cave (Bottom)': (0xBC8, 0x2F7), + 'Mimic Cave': (0xD38, 0x207), + 'Hookshot Fairy': (0xD38, 0x358), + 'Death Mountain Return Cave (West)': (0x5B8, 0x247), + 'Old Man Cave (West)': (0x5B8, 0x348), + 'Waterfall of Wishing': (0xE80, 0x2C7), + 'Fortune Teller (Light)': (0x308, 0x5A8), + 'Bonk Rock Cave': (0x600, 0x517), + 'Sanctuary': (0x748, 0x4F4), + 'Sanctuary Grave': (0x860, 0x550), + 'Graveyard Cave': (0x8F8, 0x4A6), + 'Kings Grave': (0x978, 0x566), + 'North Fairy Cave Drop': (0xA40, 0x580), + 'North Fairy Cave': (0xAA8, 0x4A8), + 'Potion Shop': (0xCB8, 0x598), + 'Kakariko Well Drop': (0x030, 0x6C0), + 'Kakariko Well Cave': (0x0D8, 0x6F7), + 'Blinds Hideout': (0x1B8, 0x6F7), + 'Elder House (West)': (0x258, 0x6F7), + 'Elder House (East)': (0x2E8, 0x6F7), + 'Snitch Lady (West)': (0x0C8, 0x7E8), + 'Snitch Lady (East)': (0x338, 0x7E8), + 'Chicken House': (0x188, 0x8E8), + 'Sick Kids House': (0x278, 0x8E8), + 'Bush Covered House': (0x338, 0x8E8), + 'Light World Bomb Hut': (0x068, 0x9D7), + 'Kakariko Shop': (0x1C8, 0x9D7), + 'Tavern North': (0x2B8, 0x977), + 'Tavern (Front)': (0x2B8, 0x9F7), + 'Agahnims Tower': (0x7F0, 0x630), + 'Hyrule Castle Entrance (South)': (0x7F0, 0x750), + 'Hyrule Castle Entrance (West)': (0x758, 0x644), + 'Hyrule Castle Entrance (East)': (0x8E8, 0x644), 'Hyrule Castle Secret Entrance Drop': (0x9D0, 0x680), - 'Hyrule Castle Secret Entrance Stairs': (0x8D0, 0x700), - 'Kakariko Well Drop': (0x030, 0x680), - 'Kakariko Well Cave': (0x060, 0x680), - 'Bat Cave Drop': (0x520, 0x8f0), - 'Bat Cave Cave': (0x560, 0x940), - 'Elder House (East)': (0x2b0, 0x6a0), - 'Elder House (West)': (0x230, 0x6a0), - 'North Fairy Cave Drop': (0xa40, 0x500), - 'North Fairy Cave': (0xa80, 0x440), - 'Lost Woods Hideout Drop': (0x290, 0x200), - 'Lost Woods Hideout Stump': (0x240, 0x280), - 'Lumberjack Tree Tree': (0x4e0, 0x140), - 'Lumberjack Tree Cave': (0x560, 0x004), - 'Two Brothers House (East)': (0x200, 0x0b60), - 'Two Brothers House (West)': (0x180, 0x0b60), - 'Sanctuary Grave': (0x820, 0x4c0), - 'Sanctuary': (0x720, 0x4a0), - 'Old Man Cave (West)': (0x580, 0x2c0), - 'Old Man Cave (East)': (0x620, 0x2c0), - 'Old Man House (Bottom)': (0x720, 0x320), - 'Old Man House (Top)': (0x820, 0x220), - 'Death Mountain Return Cave (East)': (0x600, 0x220), - 'Death Mountain Return Cave (West)': (0x500, 0x1c0), - 'Spectacle Rock Cave Peak': (0x720, 0x0a0), - 'Spectacle Rock Cave': (0x790, 0x1a0), - 'Spectacle Rock Cave (Bottom)': (0x710, 0x0a0), - 'Paradox Cave (Bottom)': (0xd80, 0x180), - 'Paradox Cave (Middle)': (0xd80, 0x380), - 'Paradox Cave (Top)': (0xd80, 0x020), - 'Fairy Ascension Cave (Bottom)': (0xcc8, 0x2a0), - 'Fairy Ascension Cave (Top)': (0xc00, 0x240), - 'Spiral Cave': (0xb80, 0x180), - 'Spiral Cave (Bottom)': (0xb80, 0x2c0), - 'Bumper Cave (Bottom)': (0x580, 0x2c0), - 'Bumper Cave (Top)': (0x500, 0x1c0), - 'Superbunny Cave (Top)': (0xd80, 0x020), - 'Superbunny Cave (Bottom)': (0xd00, 0x180), - 'Hookshot Cave': (0xc80, 0x0c0), - 'Hookshot Cave Back Entrance': (0xcf0, 0x004), - 'Ganons Tower': (0x8D0, 0x080), - 'Pyramid Hole': (0x820, 0x680), - 'Inverted Pyramid Hole': (0x820, 0x680), - 'Pyramid Entrance': (0x640, 0x7c0), - 'Inverted Pyramid Entrance': (0x6C0, 0x5D0), - 'Waterfall of Wishing': (0xe80, 0x280), - 'Dam': (0x759, 0xED0), - 'Blinds Hideout': (0x190, 0x6c0), - 'Bonk Fairy (Light)': (0x740, 0xa80), - 'Lake Hylia Fairy': (0xd40, 0x9f0), - 'Light Hype Fairy': (0x940, 0xc80), - 'Desert Fairy': (0x420, 0xe00), - 'Kings Grave': (0x920, 0x520), - 'Tavern North': (0x270, 0x900), - 'Chicken House': (0x120, 0x880), - 'Aginahs Cave': (0x2e0, 0xd00), - 'Sahasrahlas Hut': (0xcf0, 0x6c0), - 'Lake Hylia Shop': (0xbc0, 0xc00), - 'Capacity Upgrade': (0xca0, 0xda0), - 'Blacksmiths Hut': (0x4a0, 0x880), - 'Sick Kids House': (0x220, 0x880), - 'Lost Woods Gamble': (0x240, 0x080), - 'Fortune Teller (Light)': (0x2c0, 0x4c0), - 'Snitch Lady (East)': (0x310, 0x7a0), - 'Snitch Lady (West)': (0x080, 0x7a0), - 'Bush Covered House': (0x2e0, 0x880), - 'Tavern (Front)': (0x270, 0x980), - 'Light World Bomb Hut': (0x070, 0x980), - 'Kakariko Shop': (0x170, 0x980), - 'Cave 45': (0x440, 0xca0), 'Graveyard Cave': (0x8f0, 0x430), - 'Checkerboard Cave': (0x260, 0xc00), - 'Mini Moldorm Cave': (0xa40, 0xe80), - 'Long Fairy Cave': (0xf60, 0xb00), - 'Good Bee Cave': (0xec0, 0xc00), - '20 Rupee Cave': (0xe80, 0xca0), - '50 Rupee Cave': (0x4d0, 0xed0), - 'Ice Rod Cave': (0xe00, 0xc00), - 'Bonk Rock Cave': (0x5f0, 0x460), - 'Library': (0x270, 0xaa0), - 'Potion Shop': (0xc80, 0x4c0), - 'Hookshot Fairy': (0xd00, 0x180), - 'Pyramid Fairy': (0x740, 0x740), - 'East Dark World Hint': (0xf60, 0xb00), - 'Palace of Darkness Hint': (0xd60, 0x7c0), - 'Dark Lake Hylia Fairy': (0xd40, 0x9f0), - 'Dark Lake Hylia Ledge Fairy': (0xe00, 0xc00), - 'Dark Lake Hylia Ledge Spike Cave': (0xe80, 0xca0), - 'Dark Lake Hylia Ledge Hint': (0xec0, 0xc00), - 'Hype Cave': (0x940, 0xc80), - 'Bonk Fairy (Dark)': (0x740, 0xa80), - 'Brewery': (0x170, 0x980), 'C-Shaped House': (0x310, 0x7a0), 'Chest Game': (0x080, 0x7a0), - 'Hammer Peg Cave': (0x4c0, 0x940), - 'Red Shield Shop': (0x500, 0x680), - 'Dark Sanctuary Hint': (0x720, 0x4a0), - 'Fortune Teller (Dark)': (0x2c0, 0x4c0), - 'Dark World Shop': (0x2e0, 0x880), - 'Dark Lumberjack Shop': (0x4e0, 0x0d0), - 'Dark Potion Shop': (0xc80, 0x4c0), - 'Archery Game': (0x2f0, 0xaf0), - 'Mire Shed': (0x060, 0xc90), - 'Mire Hint': (0x2e0, 0xd00), - 'Mire Fairy': (0x1c0, 0xc90), - 'Spike Cave': (0x860, 0x180), - 'Dark Death Mountain Shop': (0xd80, 0x180), - 'Dark Death Mountain Fairy': (0x620, 0x2c0), - 'Mimic Cave': (0xc80, 0x180), - 'Big Bomb Shop': (0x8b1, 0xb2d), - 'Dark Lake Hylia Shop': (0xa40, 0xc40), - 'Lumberjack House': (0x580, 0x100), - 'Lake Hylia Fortune Teller': (0xa40, 0xc40), - 'Kakariko Gamble Game': (0x2f0, 0xaf0)} + 'Hyrule Castle Secret Entrance Stairs': (0x8C8, 0x718), + 'Inverted Pyramid Hole': (0x7F0, 0x6B0), + 'Inverted Pyramid Entrance': (0x6E8, 0x644), + 'Sahasrahlas Hut': (0xCF0, 0x747), + 'Eastern Palace': (0xF40, 0x680), + 'Blacksmiths Hut': (0x4E8, 0x8C8), + 'Bat Cave Drop': (0x538, 0x9A8), + 'Bat Cave Cave': (0x510, 0x930), + 'Two Brothers House (West)': (0x1B8, 0xB88), + 'Two Brothers House (East)': (0x238, 0xB88), + 'Library': (0x2B8, 0xAA7), + 'Kakariko Gamble Game': (0x348, 0xB78), + 'Bonk Fairy (Light)': (0x788, 0xA87), + 'Links House': (0x8B8, 0xB58), + 'Lake Hylia Fairy': (0xD58, 0xA57), + 'Long Fairy Cave': (0xF68, 0xB58), + 'Desert Palace Entrance (North)': (0x148, 0xC28), + 'Desert Palace Entrance (West)': (0x088, 0xCF8), + 'Desert Palace Entrance (East)': (0x1E8, 0xCF8), + 'Desert Palace Entrance (South)': (0x138, 0xD18), + 'Checkerboard Cave': (0x2E8, 0xCF7), + 'Aginahs Cave': (0x388, 0xDE8), + 'Cave 45': (0x448, 0xD86), + 'Light Hype Fairy': (0x988, 0xCF8), + 'Lake Hylia Fortune Teller': (0xA68, 0xCC8), + 'Lake Hylia Shop': (0xBF8, 0xC66), + 'Capacity Upgrade': (0xCC8, 0xDE8), + 'Mini Moldorm Cave': (0xA88, 0xEF8), + 'Ice Rod Cave': (0xE68, 0xC37), + 'Good Bee Cave': (0xEF8, 0xC36), + '20 Rupee Cave': (0xEB8, 0xCC6), + 'Desert Fairy': (0x488, 0xE76), + '50 Rupee Cave': (0x4F8, 0xF47), + 'Dam': (0x778, 0xF68), + + 'Skull Woods Final Section': (0x078, 0x0A8), + 'Skull Woods Second Section Door (West)': (0x0E0, 0x1C0), + 'Skull Woods Second Section Hole': (0x150, 0x120), + 'Skull Woods Second Section Door (East)': (0x208, 0x208), + 'Skull Woods First Section Door': (0x2C8, 0x2A8), + 'Skull Woods First Section Hole (North)': (0x320, 0x1e0), + 'Skull Woods First Section Hole (West)': (0x240, 0x380), + 'Skull Woods First Section Hole (East)': (0x340, 0x380), + 'Dark Lumberjack Shop': (0x538, 0x177), + 'Ganons Tower': (0x900, 0x0E0), + 'Dark Death Mountain Fairy': (0x658, 0x387), + 'Spike Cave': (0x9F8, 0x268), + 'Hookshot Cave Back Entrance': (0xD38, 0x038), + 'Hookshot Cave': (0xD48, 0x107), + 'Superbunny Cave (Top)': (0xDE0, 0x100), + 'Superbunny Cave (Bottom)': (0xD38, 0x358), + 'Dark Death Mountain Ledge (West)': (0xC78, 0x207), + 'Dark Death Mountain Ledge (East)': (0xD38, 0x207), + 'Turtle Rock Isolated Ledge Entrance': (0xCD8, 0x2B8), + 'Dark Death Mountain Shop': (0xDC8, 0x358), + 'Turtle Rock': (0xF48, 0x108), + 'Bumper Cave (Top)': (0x5B8, 0x247), + 'Bumper Cave (Bottom)': (0x5B8, 0x348), + 'Fortune Teller (Dark)': (0x308, 0x5A8), + 'Dark Sanctuary Hint': (0x748, 0x4F4), + 'Dark Potion Shop': (0xCB8, 0x598), + 'Chest Game': (0x0C8, 0x7E8), + 'Thieves Town': (0x1F0, 0x7E8), + 'C-Shaped House': (0x338, 0x7E8), + 'Brewery': (0x1C8, 0x9D7), + 'Dark World Shop': (0x338, 0x8E8), + 'Red Shield Shop': (0x538, 0x778), + 'Pyramid Hole': (0x800, 0x680), + 'Pyramid Entrance': (0x718, 0x7A8), + 'Pyramid Fairy': (0x7B8, 0x7A8), + 'Palace of Darkness': (0xF40, 0x680), + 'Palace of Darkness Hint': (0xD70, 0x878), + 'Hammer Peg Cave': (0x528, 0x9B6), + 'Archery Game': (0x348, 0xB78), + 'Bonk Fairy (Dark)': (0x788, 0xA87), + 'Big Bomb Shop': (0x8B8, 0xB58), + 'Dark Lake Hylia Fairy': (0xD58, 0xA57), + 'East Dark World Hint': (0xF68, 0xB58), + 'Mire Shed': (0x0A8, 0xCC7), + 'Misery Mire': (0x148, 0xCC7), + 'Mire Fairy': (0x1E8, 0xCC7), + 'Mire Hint': (0x388, 0xDE8), + 'Hype Cave': (0x988, 0xCF8), + 'Dark Lake Hylia Shop': (0xA68, 0xCC8), + 'Ice Palace': (0xCA8, 0xE28), + 'Dark Lake Hylia Ledge Fairy': (0xE68, 0xC37), + 'Dark Lake Hylia Ledge Hint': (0xEF8, 0xC36), + 'Dark Lake Hylia Ledge Spike Cave': (0xEB8, 0xCC6), + 'Swamp Palace': (0x778, 0xF68)} default_connector_connections = [('Death Mountain Return Cave (West)', 'Death Mountain Return Cave Exit (West)'), ('Death Mountain Return Cave (East)', 'Death Mountain Return Cave Exit (East)'), diff --git a/source/overworld/EntranceShuffle2.py b/source/overworld/EntranceShuffle2.py index b0779e18..17e5bf08 100644 --- a/source/overworld/EntranceShuffle2.py +++ b/source/overworld/EntranceShuffle2.py @@ -634,7 +634,7 @@ def do_dark_sanc(entrances, exits, avail): forbidden.append('Links House') else: 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] else: choices = [e for e in get_starting_entrances(avail) if e not in forbidden and e in entrances] @@ -678,7 +678,7 @@ def do_links_house(entrances, exits, avail, cross_world): forbidden.append(links_house_vanilla) forbidden.extend(Forbidden_Swap_Entrances) shuffle_mode = avail.world.shuffle[avail.player] - if avail.world.owShuffle[avail.player] == 'vanilla': + if avail.world.owLayout[avail.player] == 'vanilla': # simple shuffle - if shuffle_mode == 'simple': avail.links_on_mountain = True # taken care of by the logic below @@ -732,7 +732,7 @@ def do_links_house(entrances, exits, avail, cross_world): # links on dm 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: 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.') diff --git a/source/overworld/LayoutGenerator.py b/source/overworld/LayoutGenerator.py new file mode 100644 index 00000000..467941ab --- /dev/null +++ b/source/overworld/LayoutGenerator.py @@ -0,0 +1,1423 @@ +import copy +import logging +import RaceRandom as random +import random as _random +from typing import List, Dict, Optional, Set, Tuple +from BaseClasses import OWEdge, World, Direction, Terrain +from OverworldShuffle import connect_two_way, validate_layout + +ENABLE_KEEP_SIMILAR_SPECIAL_HANDLING = False +PREVENT_WRAPPED_LARGE_SCREENS = False +DRAW_IMAGE = False + +large_screen_ids = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35] + [0x40, 0x43, 0x45, 0x58, 0x5B, 0x5E, 0x70, 0x75] + +# ============================================================================ +# DATA STRUCTURES +# ============================================================================ + +class Screen: + """ + Represents a game map screen. + """ + __slots__ = ('id', 'big', 'dark_world', 'parallel', + 'edges', 'mixed_state') + + def __init__( + self, + id: int, + big: bool = False, + dark_world: bool = False, + parallel: Optional['Screen'] = None, + edges: Optional[Dict[str, OWEdge]] = None, + mixed_state: str = "normal" + ): + self.id = id + self.big = big + self.dark_world = dark_world + self.parallel = parallel + self.edges = edges if edges is not None else {} + self.mixed_state = mixed_state # "normal" or "swapped" + +class WorldPiece: + """ + Represents a piece within a world containing screens to be placed on the grid. + """ + __slots__ = ('screens', 'grid', 'width', 'height', 'north_edges', 'south_edges', + 'west_edges', 'east_edges', 'north_edges_water', 'south_edges_water', + 'west_edges_water', 'east_edges_water') + + def __init__( + self, + screens: List[List[Optional[Screen]]], + grid: Optional[List[List[int]]] = None, + width: int = 0, + height: int = 0, + north_edges: Optional[List[List[List[OWEdge]]]] = None, + south_edges: Optional[List[List[List[OWEdge]]]] = None, + west_edges: Optional[List[List[List[OWEdge]]]] = None, + east_edges: Optional[List[List[List[OWEdge]]]] = None, + north_edges_water: Optional[List[List[List[OWEdge]]]] = None, + south_edges_water: Optional[List[List[List[OWEdge]]]] = None, + west_edges_water: Optional[List[List[List[OWEdge]]]] = None, + east_edges_water: Optional[List[List[List[OWEdge]]]] = None + ): + self.screens = screens + self.grid = grid if grid is not None else [] + self.width = width + self.height = height + self.north_edges = north_edges if north_edges is not None else [] + self.south_edges = south_edges if south_edges is not None else [] + self.west_edges = west_edges if west_edges is not None else [] + self.east_edges = east_edges if east_edges is not None else [] + self.north_edges_water = north_edges_water if north_edges_water is not None else [] + self.south_edges_water = south_edges_water if south_edges_water is not None else [] + self.west_edges_water = west_edges_water if west_edges_water is not None else [] + self.east_edges_water = east_edges_water if east_edges_water is not None else [] + +class Piece: + """ + Represents a piece consisting of a main and optionally a parallel world piece. + """ + __slots__ = ('main', 'parallel', 'world', 'width', 'height', + 'invalid_wrap_row', 'invalid_wrap_column', 'restriction', + 'crossed_groups', 'delay', 'order', 'edge_sides', 'max_edges_per_side') + + def __init__( + self, + main: WorldPiece, + parallel: Optional[WorldPiece] = None, + world: int = 0, + width: int = 0, + height: int = 0, + invalid_wrap_row: Optional[List[int]] = None, + invalid_wrap_column: Optional[List[int]] = None, + restriction: Optional[List[int]] = None, + crossed_groups: Optional[List[List[int]]] = None, + delay: int = 0, + order: float = 0.0, + edge_sides: int = 0, + max_edges_per_side: int = 0 + ): + self.main = main + self.parallel = parallel + self.world = world # 0 or 1 + self.width = width + self.height = height + self.invalid_wrap_row = invalid_wrap_row if invalid_wrap_row is not None else [] + self.invalid_wrap_column = invalid_wrap_column if invalid_wrap_column is not None else [] + self.restriction = restriction + self.crossed_groups = crossed_groups if crossed_groups is not None else [] + self.delay = delay + self.order = order + self.edge_sides = edge_sides + self.max_edges_per_side = max_edges_per_side + +class GridInfo: + """ + Container for grid layout information during placement runs. + Stores screen IDs and edge information for both Light and Dark worlds. + """ + __slots__ = ( + 'grid', 'north_edges_grid', 'south_edges_grid', 'west_edges_grid', 'east_edges_grid', + 'north_edges_water_grid', 'south_edges_water_grid', 'west_edges_water_grid', 'east_edges_water_grid', + 'crossed_groups', 'edge_connection_seed' + ) + + def __init__( + self, + grid: List[List[List[int]]], + north_edges_grid: List[List[List[List[OWEdge]]]], + south_edges_grid: List[List[List[List[OWEdge]]]], + west_edges_grid: List[List[List[List[OWEdge]]]], + east_edges_grid: List[List[List[List[OWEdge]]]], + north_edges_water_grid: List[List[List[List[OWEdge]]]], + south_edges_water_grid: List[List[List[List[OWEdge]]]], + west_edges_water_grid: List[List[List[List[OWEdge]]]], + east_edges_water_grid: List[List[List[List[OWEdge]]]], + crossed_groups: List[List[int]], + edge_connection_seed: float + ): + self.grid = grid + self.north_edges_grid = north_edges_grid + self.south_edges_grid = south_edges_grid + self.west_edges_grid = west_edges_grid + self.east_edges_grid = east_edges_grid + self.north_edges_water_grid = north_edges_water_grid + self.south_edges_water_grid = south_edges_water_grid + self.west_edges_water_grid = west_edges_water_grid + self.east_edges_water_grid = east_edges_water_grid + self.crossed_groups = crossed_groups + self.edge_connection_seed = edge_connection_seed + +class LayoutGeneratorOptions: + """ + Configuration options for layout generation. + """ + __slots__ = ('horizontal_wrap', 'vertical_wrap', + 'large_screen_pool', 'distortion_chance', 'random_order', + 'multi_choice', 'max_delay', 'first_ignore_bonus_points', + 'penalty_full_edge_mismatch', 'penalty_partial_edge_mismatch', 'bonus_partial_edge_match', + 'bonus_full_edge_match', 'bonus_crossed_group_match', 'bonus_fill_parallel', + 'forced_non_crossed_edges', 'forced_crossed_edges', 'check_reachability', + 'crossed_chance', 'crossed_limit', + 'sort_by_edge_sides', 'sort_by_max_edges_per_side', 'sort_by_piece_size', + 'min_runs', 'max_runs', 'target_runs_times_successes') + + def __init__( + self, + horizontal_wrap: bool = True, + vertical_wrap: bool = True, + large_screen_pool: bool = False, + distortion_chance: float = 0.0, + random_order: int = 0, + multi_choice: int = 1, + max_delay: int = 10, + first_ignore_bonus_points: int = 0, + penalty_full_edge_mismatch: float = 1, + penalty_partial_edge_mismatch: float = 0, + bonus_partial_edge_match: float = 1, + bonus_full_edge_match: float = 1, + bonus_crossed_group_match: float = 1, + bonus_fill_parallel: float = 0, + forced_non_crossed_edges: Set[str] = [], + forced_crossed_edges: Set[str] = [], + crossed_chance: float = 0.5, + crossed_limit: int = -1, + check_reachability: bool = True, + sort_by_edge_sides: bool = False, + sort_by_max_edges_per_side: bool = False, + sort_by_piece_size: bool = False, + min_runs: int = 100, + max_runs: int = 10000, + target_runs_times_successes: int = 5000 + ): + self.horizontal_wrap = horizontal_wrap + self.vertical_wrap = vertical_wrap + self.large_screen_pool = large_screen_pool + self.distortion_chance = distortion_chance + self.random_order = random_order + self.multi_choice = multi_choice + self.max_delay = max_delay + self.first_ignore_bonus_points = first_ignore_bonus_points + self.penalty_full_edge_mismatch = penalty_full_edge_mismatch + self.penalty_partial_edge_mismatch = penalty_partial_edge_mismatch + self.bonus_partial_edge_match = bonus_partial_edge_match + self.bonus_full_edge_match = bonus_full_edge_match + self.bonus_crossed_group_match = bonus_crossed_group_match + self.bonus_fill_parallel = bonus_fill_parallel + self.forced_non_crossed_edges = forced_non_crossed_edges + self.forced_crossed_edges = forced_crossed_edges + self.check_reachability = check_reachability + self.crossed_chance = crossed_chance + self.crossed_limit = crossed_limit + self.sort_by_edge_sides = sort_by_edge_sides + self.sort_by_max_edges_per_side = sort_by_max_edges_per_side + self.sort_by_piece_size = sort_by_piece_size + self.min_runs = min_runs + self.max_runs = max_runs + self.target_runs_times_successes = target_runs_times_successes + +class LayoutGeneratorResult: + """ + Result object for the layout generation. + """ + __slots__ = ('grid_info', 'score', 'worst_score', 'average_score', 'successes', 'failures') + + def __init__( + self, + grid_info: Optional[GridInfo] = None, + score: int = 0, + worst_score: int = 0, + average_score: float = 0.0, + successes: int = 0, + failures: int = 0 + ): + self.grid_info = grid_info + self.score = score + self.worst_score = worst_score + self.average_score = average_score + self.successes = successes + self.failures = failures + +class PiecePlacementResult: + """ + Result object for the layout generator placement operations. + """ + __slots__ = ('success', 'piece', 'score_major', 'score_minor') + + def __init__( + self, + success: bool = False, + piece: Optional[Piece] = None, + score_major: float = 0, + score_minor: float = 0 + ): + self.success = success + self.piece = piece + self.score_major = score_major + self.score_minor = score_minor + +# ============================================================================ +# GRID INITIALIZATION +# ============================================================================ + +def create_empty_grid_info(edge_connection_seed: float) -> GridInfo: + return GridInfo( + grid=[[[-1] * 8 for _ in range(8)] for _ in range(2)], + north_edges_grid=[[[[] for _ in range(8)] for _ in range(8)] for _ in range(2)], + south_edges_grid=[[[[] for _ in range(8)] for _ in range(8)] for _ in range(2)], + west_edges_grid=[[[[] for _ in range(8)] for _ in range(8)] for _ in range(2)], + east_edges_grid=[[[[] for _ in range(8)] for _ in range(8)] for _ in range(2)], + north_edges_water_grid=[[[[] for _ in range(8)] for _ in range(8)] for _ in range(2)], + south_edges_water_grid=[[[[] for _ in range(8)] for _ in range(8)] for _ in range(2)], + west_edges_water_grid=[[[[] for _ in range(8)] for _ in range(8)] for _ in range(2)], + east_edges_water_grid=[[[[] for _ in range(8)] for _ in range(8)] for _ in range(2)], + crossed_groups=[[-1] * 8 for _ in range(8)], + edge_connection_seed=edge_connection_seed + ) + +def initialize_screens(world: World, player: int) -> Dict[int, Screen]: + overworld_screens: Dict[int, Screen] = {} + screen_edges_map = group_owedges_by_screens(world, player) + + for screen_id in range(0x80): + if screen_id - 0x01 not in large_screen_ids and screen_id - 0x08 not in large_screen_ids and screen_id - 0x09 not in large_screen_ids: + is_vanilla_dark = screen_id >= 0x40 + is_big = screen_id in large_screen_ids + is_flipped = world.owMixed[player] and screen_id in world.owswaps[player][0] + + screen = Screen( + id=screen_id, + big=is_big, + dark_world=not is_vanilla_dark if is_flipped else is_vanilla_dark, + mixed_state="swapped" if is_flipped else "normal" + ) + + if screen_id in screen_edges_map: + for edge in screen_edges_map[screen_id]: + screen.edges[edge.name] = edge + + overworld_screens[screen_id] = screen + + for light_id in range(0x40): + dark_id = light_id + 0x40 + if light_id in overworld_screens: + overworld_screens[light_id].parallel = overworld_screens[dark_id] + overworld_screens[dark_id].parallel = overworld_screens[light_id] + + return overworld_screens + +def group_owedges_by_screens(world: World, player: int) -> Dict[int, List[OWEdge]]: + screen_edges: Dict[int, List[OWEdge]] = {} + edges: List[OWEdge] = world.owedges + + for edge in edges: + # Skip edges that lead to/from special screens + if edge.player == player and not edge.specialEntrance and not edge.specialExit: + owIndex = edge.owIndex + if owIndex not in screen_edges: + screen_edges[owIndex] = [] + screen_edges[owIndex].append(edge) + + return screen_edges + +def initialize_large_screen_data(overworld_screens: Dict[int, Screen]) -> Tuple[Dict[int, Dict], Dict[int, Dict], Dict[int, Dict]]: + i: Dict[int, Dict] = {} + il: Dict[int, Dict] = {} + iw: Dict[int, Dict] = {} + define_large_screen_quadrants(overworld_screens, i, il, iw, 0x00, [], [], [], [], ["Lost Woods EN"], [], ["Lost Woods SW", "Lost Woods SC"], ["Lost Woods SE"]) + define_large_screen_quadrants(overworld_screens, i, il, iw, 0x40, [], [], [], [], ["Skull Woods EN"], [], ["Skull Woods SW", "Skull Woods SC"], ["Skull Woods SE"]) + define_large_screen_quadrants(overworld_screens, i, il, iw, 0x03, [], [], [], [], ["West Death Mountain EN"], ["West Death Mountain ES"], [], []) + define_large_screen_quadrants(overworld_screens, i, il, iw, 0x43, [], [], [], [], ["West Dark Death Mountain EN"], ["West Dark Death Mountain ES"], [], []) + define_large_screen_quadrants(overworld_screens, i, il, iw, 0x05, [], [], ["East Death Mountain WN"], ["East Death Mountain WS"], ["East Death Mountain EN"], [], [], []) + define_large_screen_quadrants(overworld_screens, i, il, iw, 0x45, [], [], ["East Dark Death Mountain WN"], ["East Dark Death Mountain WS"], ["East Dark Death Mountain EN"], [], [], []) + define_large_screen_quadrants(overworld_screens, i, il, iw, 0x18, ["Kakariko NW", "Kakariko NC"], ["Kakariko NE"], [], [], [], ["Kakariko ES"], [], ["Kakariko SE"]) + define_large_screen_quadrants(overworld_screens, i, il, iw, 0x58, ["Village of Outcasts NW", "Village of Outcasts NC"], ["Village of Outcasts NE"], [], [], [], ["Village of Outcasts ES"], [], ["Village of Outcasts SE"]) + define_large_screen_quadrants(overworld_screens, i, il, iw, 0x1B, [], [], ["Hyrule Castle WN"], [], [], ["Hyrule Castle ES"], ["Hyrule Castle SW"], ["Hyrule Castle SE"]) + define_large_screen_quadrants(overworld_screens, i, il, iw, 0x5B, [], [], [], [], [], ["Pyramid ES"], ["Pyramid SW"], ["Pyramid SE"]) + define_large_screen_quadrants(overworld_screens, i, il, iw, 0x1E, [], [], [], [], [], [], ["Eastern Palace SW"], ["Eastern Palace SE"]) + define_large_screen_quadrants(overworld_screens, i, il, iw, 0x5E, [], [], [], [], [], [], ["Palace of Darkness SW"], ["Palace of Darkness SE"]) + define_large_screen_quadrants(overworld_screens, i, il, iw, 0x30, [], [], [], [], [], ["Desert EC", "Desert ES"], [], []) + define_large_screen_quadrants(overworld_screens, i, il, iw, 0x70, [], [], [], [], [], [], [], []) + define_large_screen_quadrants(overworld_screens, i, il, iw, 0x35, ["Lake Hylia NW"], ["Lake Hylia NC", "Lake Hylia NE"], [], ["Lake Hylia WS"], [], ["Lake Hylia EC", "Lake Hylia ES"], [], []) + define_large_screen_quadrants(overworld_screens, i, il, iw, 0x75, ["Ice Lake NW"], ["Ice Lake NC", "Ice Lake NE"], [], ["Ice Lake WS"], [], ["Ice Lake EC", "Ice Lake ES"], [], []) + return (i, il, iw) + +def define_large_screen_quadrants( + overworld_screens: Dict[int, Screen], + large_screen_quadrant_info: Dict[int, Dict], + large_screen_quadrant_info_land: Dict[int, Dict], + large_screen_quadrant_info_water: Dict[int, Dict], + screen_id: int, + north1: List[str], north2: List[str], + west1: List[str], west2: List[str], + east1: List[str], east2: List[str], + south1: List[str], south2: List[str] +) -> None: + """ + Define edge info for large screens + Maps edge names to quadrants (NW, NE, SW, SE) + + Edge names are the actual edge names from OWEdges.py like "Lost Woods SW", "Kakariko NW", etc. + """ + edges = overworld_screens[screen_id].edges + info = { + "NW": {Direction.North: [], Direction.West: [], Direction.East: [], Direction.South: []}, + "NE": {Direction.North: [], Direction.West: [], Direction.East: [], Direction.South: []}, + "SW": {Direction.North: [], Direction.West: [], Direction.East: [], Direction.South: []}, + "SE": {Direction.North: [], Direction.West: [], Direction.East: [], Direction.South: []} + } + info["NW"][Direction.North] = [edges[name] for name in north1] + info["NE"][Direction.North] = [edges[name] for name in north2] + info["NW"][Direction.West] = [edges[name] for name in west1] + info["SW"][Direction.West] = [edges[name] for name in west2] + info["NE"][Direction.East] = [edges[name] for name in east1] + info["SE"][Direction.East] = [edges[name] for name in east2] + info["SW"][Direction.South] = [edges[name] for name in south1] + info["SE"][Direction.South] = [edges[name] for name in south2] + + large_screen_quadrant_info[screen_id] = info + + large_screen_quadrant_info_land[screen_id] = { + "NW": {}, "NE": {}, "SW": {}, "SE": {} + } + large_screen_quadrant_info_water[screen_id] = { + "NW": {}, "NE": {}, "SW": {}, "SE": {} + } + + for quadrant_name in ["NW", "NE", "SW", "SE"]: + for direction in [Direction.North, Direction.West, Direction.East, Direction.South]: + large_screen_quadrant_info_land[screen_id][quadrant_name][direction] = \ + [edge for edge in info[quadrant_name][direction] if edge.terrain != Terrain.Water] + large_screen_quadrant_info_water[screen_id][quadrant_name][direction] = \ + [edge for edge in info[quadrant_name][direction] if edge.terrain == Terrain.Water] + +# ============================================================================ +# PIECE CREATION +# ============================================================================ + +def create_piece_list(world: World, player: int, options: LayoutGeneratorOptions, crossed_group_b: List[int], overworld_screens: Dict[int, Screen], large_screen_quadrant_info: Dict[int, Dict], large_screen_quadrant_info_land: Dict[int, Dict], large_screen_quadrant_info_water: Dict[int, Dict]) -> List[Piece]: + piece_list: List[Piece] = [] + used_screens_set = set() + + all_large_screens = [s for s in overworld_screens.values() if s.big] + all_small_screens = [s for s in overworld_screens.values() if not s.big] + + if world.owParallel[player]: + # In Parallel, only use light world screens + # Each piece will automatically handle both worlds through parallel mechanism + all_large_screens = [s for s in all_large_screens if not s.dark_world] + all_small_screens = [s for s in all_small_screens if not s.dark_world] + + # In Standard mode, screens 0x1B, 0x2B, 0x2C are glued together as a single piece + if world.mode[player] == 'standard': + castle_screen = overworld_screens.get(0x1B) + central_bonk_screen = overworld_screens.get(0x2B) + links_house_screen = overworld_screens.get(0x2C) + + if castle_screen and central_bonk_screen and links_house_screen: + piece = create_piece(world, player, [ + [0x1B, 0x1B], + [0x1B, 0x1B], + [0x2B, 0x2C] + ], overworld_screens) + + if options.large_screen_pool: + piece.restriction = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35] + + piece_list.append(piece) + used_screens_set.add(castle_screen) + used_screens_set.add(central_bonk_screen) + used_screens_set.add(links_house_screen) + + if world.owParallel[player]: + used_screens_set.add(castle_screen.parallel) + used_screens_set.add(central_bonk_screen.parallel) + used_screens_set.add(links_house_screen.parallel) + + # Add large screens + for screen in all_large_screens: + if screen not in used_screens_set: + piece = create_piece(world, player, [[screen.id, screen.id], [screen.id, screen.id]], overworld_screens) + if options.large_screen_pool: + piece.restriction = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35] + piece_list.append(piece) + used_screens_set.add(screen) + if world.owParallel[player]: + used_screens_set.add(screen.parallel) + + # Add small screens + for screen in all_small_screens: + if screen not in used_screens_set: + piece = create_piece(world, player, [[screen.id]], overworld_screens) + if options.large_screen_pool: + piece.restriction = [s.id for s in overworld_screens.values() if not s.big] + piece_list.append(piece) + used_screens_set.add(screen) + if world.owParallel[player]: + used_screens_set.add(screen.parallel) + + # Add piece data + for piece in piece_list: + add_piece_data(world, player, piece, large_screen_quadrant_info, large_screen_quadrant_info_land, large_screen_quadrant_info_water) + # Handle crossed groups + if world.owCrossed[player] == 'polar' and world.owMixed[player]: + piece.crossed_groups = [[] for _ in range(8)] + for k in range(piece.height): + for l in range(piece.width): + piece.crossed_groups[k].append(-1) + screen = piece.main.screens[k][l] + if screen: + piece.crossed_groups[k][l] = 1 if screen.mixed_state == "swapped" else 0 + else: + if piece.parallel and piece.parallel.screens[k][l]: + piece.crossed_groups[k][l] = 1 if piece.parallel.screens[k][l].mixed_state == "swapped" else 0 + if world.owCrossed[player] == 'grouped': + piece.crossed_groups = [[] for _ in range(8)] + for k in range(piece.height): + for l in range(piece.width): + piece.crossed_groups[k].append(-1) + screen_id = piece.main.grid[k][l] + if screen_id != -1: + piece.crossed_groups[k][l] = 1 if screen_id in crossed_group_b else 0 + else: + if piece.parallel and piece.parallel.screens[k][l]: + piece.crossed_groups[k][l] = 1 if piece.parallel.grid[k][l] in crossed_group_b else 0 + + return piece_list + +def create_piece(world: World, player: int, grid: List[List[int]], overworld_screens: Dict[int, Screen]) -> Piece: + """ + Create piece from grid of screen IDs + Takes 2D array of screen IDs and creates main and parallel pieces + """ + piece = Piece( + main=WorldPiece(screens=[]), + width=len(grid[0]), + height=len(grid) + ) + + if world.owParallel[player]: + piece.parallel = WorldPiece(screens=[]) + + found_screens = set() + + for i in range(piece.height): + new_row = [] + new_row_parallel = [] + piece.main.screens.append(new_row) + if world.owParallel[player]: + piece.parallel.screens.append(new_row_parallel) + + for j in range(piece.width): + screen = overworld_screens.get(grid[i][j]) + new_row.append(screen) + if world.owParallel[player]: + new_row_parallel.append(screen.parallel if screen else None) + + if screen and screen not in found_screens: + found_screens.add(screen) + piece.world = 1 if screen.dark_world else 0 + if screen.big and PREVENT_WRAPPED_LARGE_SCREENS: + # For large screens, prevent wrapping at the second row/column + # This ensures the 2x2 piece doesn't split across the grid boundary + if (i + 1) not in piece.invalid_wrap_row: + piece.invalid_wrap_row.append(i + 1) + if (j + 1) not in piece.invalid_wrap_column: + piece.invalid_wrap_column.append(j + 1) + + return piece + +def add_piece_data(world: World, player: int, piece: Piece, large_screen_quadrant_info: Dict[int, Dict], large_screen_quadrant_info_land: Dict[int, Dict], large_screen_quadrant_info_water: Dict[int, Dict]) -> None: + """ + Add computed data to piece + Calls add_piece_grid_info for main and parallel pieces + """ + num_pieces = 2 if piece.parallel else 1 + for p in range(num_pieces): + world_piece = piece.main if p == 0 else piece.parallel + world_piece.width = len(world_piece.screens[0]) + world_piece.height = len(world_piece.screens) + add_piece_grid_info(world, player, world_piece, large_screen_quadrant_info, large_screen_quadrant_info_land, large_screen_quadrant_info_water) + + piece.width = piece.main.width + piece.height = piece.main.height + + # Calculate edge_sides and max_edges_per_side: 0 for multi-cell pieces + if piece.width == 1 and piece.height == 1: + edge_sides = 0 + max_edges_per_side = 0 + # Count edge sides and max edges for main piece and parallel piece (if exists) + for world_piece in ([piece.main, piece.parallel] if piece.parallel else [piece.main]): + north_count = len(world_piece.north_edges[0][0]) + (len(world_piece.north_edges_water[0][0]) if world_piece.north_edges_water else 0) + if north_count > 0: + edge_sides += 1 + max_edges_per_side = max(max_edges_per_side, north_count) + south_count = len(world_piece.south_edges[0][0]) + (len(world_piece.south_edges_water[0][0]) if world_piece.south_edges_water else 0) + if south_count > 0: + edge_sides += 1 + max_edges_per_side = max(max_edges_per_side, south_count) + west_count = len(world_piece.west_edges[0][0]) + (len(world_piece.west_edges_water[0][0]) if world_piece.west_edges_water else 0) + if west_count > 0: + edge_sides += 1 + max_edges_per_side = max(max_edges_per_side, west_count) + east_count = len(world_piece.east_edges[0][0]) + (len(world_piece.east_edges_water[0][0]) if world_piece.east_edges_water else 0) + if east_count > 0: + edge_sides += 1 + max_edges_per_side = max(max_edges_per_side, east_count) + piece.edge_sides = edge_sides + piece.max_edges_per_side = max_edges_per_side + else: + piece.edge_sides = 0 + piece.max_edges_per_side = 0 + +def add_piece_grid_info(world: World, player: int, piece: WorldPiece, large_screen_quadrant_info: Dict[int, Dict], large_screen_quadrant_info_land: Dict[int, Dict], large_screen_quadrant_info_water: Dict[int, Dict]) -> None: + """ + Populate piece edge information + Initializes 8x8 edge arrays and extracts edges from screens + """ + piece.grid = [[] for _ in range(8)] + piece.north_edges = [[] for _ in range(8)] + piece.south_edges = [[] for _ in range(8)] + piece.west_edges = [[] for _ in range(8)] + piece.east_edges = [[] for _ in range(8)] + + if not world.owTerrain[player]: + piece.north_edges_water = [[] for _ in range(8)] + piece.south_edges_water = [[] for _ in range(8)] + piece.west_edges_water = [[] for _ in range(8)] + piece.east_edges_water = [[] for _ in range(8)] + + for k in range(piece.height): + for l in range(piece.width): + piece.grid[k].append(piece.screens[k][l].id if piece.screens[k][l] else -1) + piece.north_edges[k].append([]) + piece.south_edges[k].append([]) + piece.west_edges[k].append([]) + piece.east_edges[k].append([]) + + if not world.owTerrain[player]: + piece.north_edges_water[k].append([]) + piece.south_edges_water[k].append([]) + piece.west_edges_water[k].append([]) + piece.east_edges_water[k].append([]) + + done_large = set() + for k in range(piece.height): + for l in range(piece.width): + screen = piece.screens[k][l] + if not screen: + continue + + if screen.big: + if screen.id not in done_large: + done_large.add(screen.id) + quadrant_info = (large_screen_quadrant_info[screen.id] if world.owTerrain[player] + else large_screen_quadrant_info_land[screen.id]) + + piece.north_edges[k][l] = [e for e in quadrant_info["NW"][Direction.North] if not e.dest] + piece.north_edges[k][l + 1] = [e for e in quadrant_info["NE"][Direction.North] if not e.dest] + piece.south_edges[k + 1][l] = [e for e in quadrant_info["SW"][Direction.South] if not e.dest] + piece.south_edges[k + 1][l + 1] = [e for e in quadrant_info["SE"][Direction.South] if not e.dest] + piece.west_edges[k][l] = [e for e in quadrant_info["NW"][Direction.West] if not e.dest] + piece.west_edges[k + 1][l] = [e for e in quadrant_info["SW"][Direction.West] if not e.dest] + piece.east_edges[k][l + 1] = [e for e in quadrant_info["NE"][Direction.East] if not e.dest] + piece.east_edges[k + 1][l + 1] = [e for e in quadrant_info["SE"][Direction.East] if not e.dest] + + if not world.owTerrain[player]: + quadrant_info = large_screen_quadrant_info_water[screen.id] + piece.north_edges_water[k][l] = [e for e in quadrant_info["NW"][Direction.North] if not e.dest] + piece.north_edges_water[k][l + 1] = [e for e in quadrant_info["NE"][Direction.North] if not e.dest] + piece.south_edges_water[k + 1][l] = [e for e in quadrant_info["SW"][Direction.South] if not e.dest] + piece.south_edges_water[k + 1][l + 1] = [e for e in quadrant_info["SE"][Direction.South] if not e.dest] + piece.west_edges_water[k][l] = [e for e in quadrant_info["NW"][Direction.West] if not e.dest] + piece.west_edges_water[k + 1][l] = [e for e in quadrant_info["SW"][Direction.West] if not e.dest] + piece.east_edges_water[k][l + 1] = [e for e in quadrant_info["NE"][Direction.East] if not e.dest] + piece.east_edges_water[k + 1][l + 1] = [e for e in quadrant_info["SE"][Direction.East] if not e.dest] + else: + for edge in sorted(screen.edges.values(), key=lambda e: e.midpoint): + if not edge.dest: + if edge.direction == Direction.North: + target = piece.north_edges[k][l] if world.owTerrain[player] or edge.terrain != Terrain.Water else piece.north_edges_water[k][l] + target.append(edge) + elif edge.direction == Direction.South: + target = piece.south_edges[k][l] if world.owTerrain[player] or edge.terrain != Terrain.Water else piece.south_edges_water[k][l] + target.append(edge) + elif edge.direction == Direction.West: + target = piece.west_edges[k][l] if world.owTerrain[player] or edge.terrain != Terrain.Water else piece.west_edges_water[k][l] + target.append(edge) + elif edge.direction == Direction.East: + target = piece.east_edges[k][l] if world.owTerrain[player] or edge.terrain != Terrain.Water else piece.east_edges_water[k][l] + target.append(edge) + +# ============================================================================ +# PLACEMENT ALGORITHM +# ============================================================================ + +def random_place_piece( + world: World, + player: int, + grid_info: GridInfo, + options: LayoutGeneratorOptions, + pieces: List[Piece], + ignore_bonus_points: bool +) -> PiecePlacementResult: + """ + Core placement algorithm + Evaluates all valid positions and scores each based on edge compatibility + Performance is critical within these deeply nested loops, every optimization matters + """ + use_crossed_groups = (world.owCrossed[player] == 'polar' and world.owMixed[player]) or world.owCrossed[player] == 'grouped' + is_unrestricted_crossed = world.owCrossed[player] == 'unrestricted' + keep_similar = ENABLE_KEEP_SIMILAR_SPECIAL_HANDLING and world.owKeepSimilar[player] + + width = 8 + height = 8 + horizontal_wrap = options.horizontal_wrap + vertical_wrap = options.vertical_wrap + distortion_chance = options.distortion_chance + use_distortion = distortion_chance > 0 + crossed_chance = options.crossed_chance + crossworld_weights = (1 - crossed_chance, crossed_chance) if is_unrestricted_crossed else (1, 0) + if not is_unrestricted_crossed: + crossed_score_weight = 1 + penalty_full_edge_mismatch = options.penalty_full_edge_mismatch + penalty_partial_edge_mismatch = options.penalty_partial_edge_mismatch + bonus_partial_edge_match = 0 if ignore_bonus_points else options.bonus_partial_edge_match + bonus_full_edge_match = 0 if ignore_bonus_points else options.bonus_full_edge_match + bonus_crossed_group_match = 0 if ignore_bonus_points else options.bonus_crossed_group_match + bonus_fill_parallel = 0 if ignore_bonus_points else options.bonus_fill_parallel + can_stop_early = penalty_full_edge_mismatch >= 0 and penalty_partial_edge_mismatch >= 0 + + grid = grid_info.grid + crossed_groups = grid_info.crossed_groups + north_edges_grid = grid_info.north_edges_grid + south_edges_grid = grid_info.south_edges_grid + west_edges_grid = grid_info.west_edges_grid + east_edges_grid = grid_info.east_edges_grid + north_edges_water_grid = grid_info.north_edges_water_grid + south_edges_water_grid = grid_info.south_edges_water_grid + west_edges_water_grid = grid_info.west_edges_water_grid + east_edges_water_grid = grid_info.east_edges_water_grid + + best_choices = [] + max_score_major = -1000000 + max_score_minor = -1000000 + + for c, piece in enumerate(pieces): + piece_main = piece.main + piece_parallel = piece.parallel + wrld = piece.world + invalid_wrap_row = piece.invalid_wrap_row + invalid_wrap_column = piece.invalid_wrap_column + restriction = piece.restriction + piece_width = piece.width + piece_height = piece.height + piece_crossed_groups = piece.crossed_groups + + grid_main_world = grid[wrld] + grid_other_world = grid[1 - wrld] + + i_range = height if vertical_wrap else height - piece_height + 1 + for i in range(i_range): + if i >= height - piece_height + 1 and (height - i) in invalid_wrap_row: + continue + + j_range = width if horizontal_wrap else width - piece_width + 1 + for j in range(j_range): + if j >= width - piece_width + 1 and (width - j) in invalid_wrap_column: + continue + if restriction and (i * 8 + j) not in restriction: + continue + + # Check for overlap + overlap = False + for k in range(piece_height): + row_idx = (i + k) % height + for l in range(piece_width): + col_idx = (j + l) % width + + if grid_main_world[row_idx][col_idx] != -1 and piece_main.screens[k][l]: + overlap = True + break + + if use_crossed_groups and crossed_groups[row_idx][col_idx] != -1 and crossed_groups[row_idx][col_idx] != piece_crossed_groups[k][l]: + overlap = True + break + + if piece_parallel and grid_other_world[row_idx][col_idx] != -1 and piece_parallel.screens[k][l]: + overlap = True + break + + if not overlap: + score_major = 0 + score_minor = 0 + + # Calculate scores based on edge compatibility + for k in range(piece_height): + row_idx = (i + k) % height + row_above = (i + k + height - 1) % height + row_below = (i + k + 1) % height + i_plus_k = i + k + + for l in range(piece_width): + col_idx = (j + l) % width + col_left = (j + l + width - 1) % width + col_right = (j + l + 1) % width + j_plus_l = j + l + + num_pieces = 2 if piece_parallel else 1 + for p in range(num_pieces): + world_piece = piece_main if p == 0 else piece_parallel + cw = wrld if p == 0 else 1 - wrld + + if not world_piece.screens[k][l]: + continue + + # Add small bias when the crossed group is already determined and matches the piece to avoid issues later on + if use_crossed_groups and not piece_parallel and crossed_groups[row_idx][col_idx] == piece_crossed_groups[k][l]: + score_minor += bonus_crossed_group_match + + if not piece_parallel and grid_other_world[row_idx][col_idx] != -1: + score_minor += bonus_fill_parallel + + for terrain in range(1 if world.owTerrain[player] else 2): + north_piece = world_piece.north_edges if terrain == 0 else world_piece.north_edges_water + south_piece = world_piece.south_edges if terrain == 0 else world_piece.south_edges_water + west_piece = world_piece.west_edges if terrain == 0 else world_piece.west_edges_water + east_piece = world_piece.east_edges if terrain == 0 else world_piece.east_edges_water + + north_edges = north_edges_grid if terrain == 0 else north_edges_water_grid + south_edges = south_edges_grid if terrain == 0 else south_edges_water_grid + west_edges = west_edges_grid if terrain == 0 else west_edges_water_grid + east_edges = east_edges_grid if terrain == 0 else east_edges_water_grid + + # Check boundary edges + if not vertical_wrap and i_plus_k == 0 and (not use_distortion or distortion_chance <= random.random()): + if north_piece[k][l]: + score_major -= penalty_full_edge_mismatch + else: + score_minor += bonus_full_edge_match + + if not vertical_wrap and i_plus_k == height - 1 and (not use_distortion or distortion_chance <= random.random()): + if south_piece[k][l]: + score_major -= penalty_full_edge_mismatch + else: + score_minor += bonus_full_edge_match + + if not horizontal_wrap and j_plus_l == 0 and (not use_distortion or distortion_chance <= random.random()): + if west_piece[k][l]: + score_major -= penalty_full_edge_mismatch + else: + score_minor += bonus_full_edge_match + + if not horizontal_wrap and j_plus_l == width - 1 and (not use_distortion or distortion_chance <= random.random()): + if east_piece[k][l]: + score_major -= penalty_full_edge_mismatch + else: + score_minor += bonus_full_edge_match + + for other_world_index in range(2 if is_unrestricted_crossed else 1): + # Check neighbor compatibility (north) + if is_unrestricted_crossed: + w = cw if other_world_index == 0 else 1 - cw + crossed_score_weight = crossworld_weights[other_world_index] + elif use_crossed_groups and crossed_groups[row_above][col_idx] != piece_crossed_groups[k][l]: + w = 1 - cw + else: + w = cw + + if (i_plus_k != 0 or vertical_wrap) and grid[w][row_above][col_idx] != -1 and (not use_distortion or distortion_chance <= random.random()): + piece_edges = len(north_piece[k][l]) + grid_edges = len(south_edges[w][row_above][col_idx]) + if piece_edges == grid_edges: + score_minor += bonus_full_edge_match * crossed_score_weight + elif not keep_similar and ((piece_edges == 0) == (grid_edges == 0)): + score_minor += bonus_partial_edge_match * crossed_score_weight + score_major -= penalty_partial_edge_mismatch * crossed_score_weight + else: + score_major -= penalty_full_edge_mismatch * crossed_score_weight + + # Check south neighbor + if is_unrestricted_crossed: + w = cw if other_world_index == 0 else 1 - cw + crossed_score_weight = crossworld_weights[other_world_index] + elif use_crossed_groups and crossed_groups[row_below][col_idx] != piece_crossed_groups[k][l]: + w = 1 - cw + else: + w = cw + + if (i_plus_k != height - 1 or vertical_wrap) and grid[w][row_below][col_idx] != -1 and (not use_distortion or distortion_chance <= random.random()): + piece_edges = len(south_piece[k][l]) + grid_edges = len(north_edges[w][row_below][col_idx]) + if piece_edges == grid_edges: + score_minor += bonus_full_edge_match * crossed_score_weight + elif not keep_similar and ((piece_edges == 0) == (grid_edges == 0)): + score_minor += bonus_partial_edge_match * crossed_score_weight + score_major -= penalty_partial_edge_mismatch * crossed_score_weight + else: + score_major -= penalty_full_edge_mismatch * crossed_score_weight + + # Check west neighbor + if is_unrestricted_crossed: + w = cw if other_world_index == 0 else 1 - cw + crossed_score_weight = crossworld_weights[other_world_index] + elif use_crossed_groups and crossed_groups[row_idx][col_left] != piece_crossed_groups[k][l]: + w = 1 - cw + else: + w = cw + + if (j_plus_l != 0 or horizontal_wrap) and grid[w][row_idx][col_left] != -1 and (not use_distortion or distortion_chance <= random.random()): + piece_edges = len(west_piece[k][l]) + grid_edges = len(east_edges[w][row_idx][col_left]) + if piece_edges == grid_edges: + score_minor += bonus_full_edge_match * crossed_score_weight + elif not keep_similar and ((piece_edges == 0) == (grid_edges == 0)): + score_minor += bonus_partial_edge_match * crossed_score_weight + score_major -= penalty_partial_edge_mismatch * crossed_score_weight + else: + score_major -= penalty_full_edge_mismatch * crossed_score_weight + + # Check east neighbor + if is_unrestricted_crossed: + w = cw if other_world_index == 0 else 1 - cw + crossed_score_weight = crossworld_weights[other_world_index] + elif use_crossed_groups and crossed_groups[row_idx][col_right] != piece_crossed_groups[k][l]: + w = 1 - cw + else: + w = cw + + if (j_plus_l != width - 1 or horizontal_wrap) and grid[w][row_idx][col_right] != -1 and (not use_distortion or distortion_chance <= random.random()): + piece_edges = len(east_piece[k][l]) + grid_edges = len(west_edges[w][row_idx][col_right]) + if piece_edges == grid_edges: + score_minor += bonus_full_edge_match * crossed_score_weight + elif not keep_similar and ((piece_edges == 0) == (grid_edges == 0)): + score_minor += bonus_partial_edge_match * crossed_score_weight + score_major -= penalty_partial_edge_mismatch * crossed_score_weight + else: + score_major -= penalty_full_edge_mismatch * crossed_score_weight + + if can_stop_early and score_major < max_score_major: + break + # This is so an we can break out of all remaining checks for the current placement option + else: + continue + break + else: + continue + break + else: + continue + break + else: + continue + break + + if score_major == max_score_major and score_minor == max_score_minor: + best_choices.append((c, i, j)) + + if score_major > max_score_major or (score_major == max_score_major and score_minor > max_score_minor): + max_score_major = score_major + max_score_minor = score_minor + best_choices = [(c, i, j)] + + if not best_choices: + return PiecePlacementResult(success=False, piece=None, score_major=0, score_minor=0) + + # Select random best choice + piece_index, row, column = random.choice(best_choices) + used_score_major = max_score_major + used_score_minor = max_score_minor + + piece = pieces[piece_index] + wrld = piece.world + + # Place the piece on the grid + for k in range(piece.height): + row_idx = (row + k) % height + for l in range(piece.width): + col_idx = (column + l) % width + num_pieces = 2 if piece.parallel else 1 + for p in range(num_pieces): + world_piece = piece.main if p == 0 else piece.parallel + w = wrld if p == 0 else 1 - wrld + + grid[w][row_idx][col_idx] = world_piece.grid[k][l] + north_edges_grid[w][row_idx][col_idx] = world_piece.north_edges[k][l] + south_edges_grid[w][row_idx][col_idx] = world_piece.south_edges[k][l] + west_edges_grid[w][row_idx][col_idx] = world_piece.west_edges[k][l] + east_edges_grid[w][row_idx][col_idx] = world_piece.east_edges[k][l] + + if not world.owTerrain[player]: + north_edges_water_grid[w][row_idx][col_idx] = world_piece.north_edges_water[k][l] + south_edges_water_grid[w][row_idx][col_idx] = world_piece.south_edges_water[k][l] + west_edges_water_grid[w][row_idx][col_idx] = world_piece.west_edges_water[k][l] + east_edges_water_grid[w][row_idx][col_idx] = world_piece.east_edges_water[k][l] + + if use_crossed_groups: + crossed_groups[row_idx][col_idx] = piece_crossed_groups[k][l] + + return PiecePlacementResult(success=True, piece=piece, score_major=used_score_major, score_minor=used_score_minor) + +def get_random_layout(world: World, player: int, connected_edges_cache: List[str], pieces_to_place: List[Piece], options: LayoutGeneratorOptions, prio_edges: List[str], overworld_screens: Dict[int, Screen]) -> LayoutGeneratorResult: + total_score = 0 + best_score = -1000000 + worst_score = 1000000 + best_grid_info = None + + successes = 0 + failures = 0 + run = 0 + while run < options.min_runs or (run * successes < options.target_runs_times_successes and run < options.max_runs): + run += 1 + connected_edges = connected_edges_cache.copy() + piece_list = pieces_to_place.copy() + + grid_info = create_empty_grid_info(random.random()) + for piece in piece_list: + piece.delay = 0 + + major_score = 0 + + # Order pieces by size, max_edges_per_side, edge_sides, and randomness + random.shuffle(piece_list) + if options.sort_by_edge_sides: + piece_list.sort(key=lambda p: p.edge_sides) + if options.sort_by_max_edges_per_side: + piece_list.sort(key=lambda p: p.max_edges_per_side, reverse=True) + if options.sort_by_piece_size: + piece_list.sort(key=lambda p: p.width * p.height, reverse=True) + if options.random_order > 0: + for i, piece in enumerate(piece_list): + piece.order = i + random.random() * (options.random_order + 1) + piece_list.sort(key=lambda p: p.order) + + # Place pieces + placed_pieces = set() + while piece_list: + pieces = [piece_list[0]] + if piece_list[0].delay < options.max_delay: + for i in range(1, min(options.multi_choice, len(piece_list))): + pieces.append(piece_list[i]) + + result = random_place_piece(world, player, grid_info, options, pieces, len(placed_pieces) < options.first_ignore_bonus_points) + + if not result.success: + failures += 1 + break + + if result.piece != piece_list[0]: + piece_list[0].delay += 1 + + placed_pieces.add(result.piece) + piece_list.remove(result.piece) + major_score += result.score_major + else: + # Successfully placed all pieces + if options.check_reachability: + disabled_count = connect_edges_for_screen_layout(world, player, grid_info, options, connected_edges, prio_edges, overworld_screens, False) + valid_layout = validate_layout(world, player) + # Clean up connected entrances and edges + for edge_name in connected_edges: + if edge_name not in connected_edges_cache: + entrance = world.get_entrance(edge_name, player) + entrance.connected_region.entrances.remove(entrance) + entrance.connected_region = None + edge = world.get_owedge(edge_name, player) + edge.dest = None + if not valid_layout: + failures += 1 + continue + logging.getLogger('').debug("Found valid layout with " + str(disabled_count)+ " disabled edges") + successes += 1 + score = -disabled_count + else: + successes += 1 + score = major_score + + total_score += score + + if score > best_score: + best_score = score + best_grid_info = grid_info + + if score < worst_score: + worst_score = score + + if best_grid_info is None: + return LayoutGeneratorResult( + successes=successes, + failures=failures + ) + + return LayoutGeneratorResult( + grid_info=best_grid_info, + score=best_score, + worst_score=worst_score, + average_score=total_score / successes, + successes=successes, + failures=failures + ) + +def get_prioritized_edges(world: World, player: int) -> List[str]: + prio_edges = [] + if world.accessibility[player] != 'none': + prio_edges += ['Desert EC'] + if not world.is_tile_swapped(0x3A, player): + prio_edges += ['Desert Pass WC'] + if world.is_tile_swapped(0x13, player): + prio_edges += ['Sanctuary WN'] + if world.owParallel[player]: + prio_edges += ['Dark Chapel WN'] + if world.owParallel[player]: + prio_edges += ['Flute Boy SC', 'Stumpy SC'] + else: + if world.is_tile_swapped(0x2A, player): + prio_edges += ['Flute Boy SC'] + else: + prio_edges += ['Stumpy SC'] + if world.owTerrain[player]: + prio_edges += ['Octoballoon NW', 'Bomber Corner NW'] + if world.is_tile_swapped(0x2D, player): + prio_edges += ['Stone Bridge EC'] + if world.owParallel[player]: + prio_edges += ['Hammer Bridge EC'] + if not world.is_tile_swapped(0x35, player): + prio_edges += ['Ice Lake ES'] + if world.owParallel[player]: + prio_edges += ['Lake Hylia ES'] + return prio_edges + +escape_screen_ids = set([0x1B, 0x2B, 0x2C]) + +def connect_edges_for_screen_layout(world: World, player: int, grid_info: GridInfo, options: LayoutGeneratorOptions, connected_edges: List[str], prio_edges: List[str], overworld_screens: Dict[int, Screen], final_placement: bool) -> int: + use_crossed_groups = (world.owCrossed[player] == 'polar' and world.owMixed[player]) or world.owCrossed[player] == 'grouped' + is_unrestricted_crossed = world.owCrossed[player] == 'unrestricted' + is_standard = world.mode[player] == 'standard' + edge_random = _random.Random(grid_info.edge_connection_seed) + left_to_connect: List[Direction, int, int, int, int] = [] + make_non_crossed = set() + make_crossed = set() + make_disabled = set() + undecided = [] + + # Collect information about all edge sets to connect + for dir in [Direction.East, Direction.South]: + for i in range(7 if dir == Direction.South and not options.vertical_wrap else 8): + for j in range(7 if dir == Direction.East and not options.horizontal_wrap else 8): + forced_escape = False + forced_non_crossed = False + forced_crossed = False + has_edges_1 = [False, False] + has_edges_2 = [False, False] + for w in range(2): + for terrain in range(1 if world.owTerrain[player] else 2): + left_to_connect.append((dir, w, i, j, terrain)) + + if is_unrestricted_crossed: + if dir == Direction.East: + west_edges = grid_info.west_edges_grid if terrain == 0 else grid_info.west_edges_water_grid + east_edges = grid_info.east_edges_grid if terrain == 0 else grid_info.east_edges_water_grid + edge_set_1 = east_edges[w][i][j] + edge_set_2 = west_edges[w][i][(j + 1) % 8] + if is_standard and grid_info.grid[w][i][j] in escape_screen_ids and grid_info.grid[w][i][(j + 1) % 8] in escape_screen_ids: + forced_escape = True + else: + north_edges = grid_info.north_edges_grid if terrain == 0 else grid_info.north_edges_water_grid + south_edges = grid_info.south_edges_grid if terrain == 0 else grid_info.south_edges_water_grid + edge_set_1 = south_edges[w][i][j] + edge_set_2 = north_edges[w][(i + 1) % 8][j] + if is_standard and grid_info.grid[w][i][j] in escape_screen_ids and grid_info.grid[w][(i + 1) % 8][j] in escape_screen_ids: + forced_escape = True + if any(edge for edge in edge_set_1 if edge.name in options.forced_non_crossed_edges) or any(edge for edge in edge_set_2 if edge.name in options.forced_non_crossed_edges): + forced_non_crossed = True + if any(edge for edge in edge_set_1 if edge.name in options.forced_crossed_edges) or any(edge for edge in edge_set_2 if edge.name in options.forced_crossed_edges): + forced_crossed = True + if edge_set_1: + has_edges_1[w] = True + if edge_set_2: + has_edges_2[w] = True + if is_unrestricted_crossed: + if forced_escape: + make_non_crossed.add((dir, i, j)) + elif forced_non_crossed and forced_crossed: + make_disabled.add((dir, i, j)) + elif forced_non_crossed: + make_non_crossed.add((dir, i, j)) + elif forced_crossed: + make_crossed.add((dir, i, j)) + elif has_edges_1[0] != has_edges_1[1] and has_edges_2[0] != has_edges_2[1]: + # On both sides of the transition only one world has any edges, so make sure we can connect those + (make_non_crossed if has_edges_1[0] == has_edges_2[0] else make_crossed).add((dir, i, j)) + else: + undecided.append((dir, i, j)) + + if is_unrestricted_crossed: + # Make outstanding crossed choices + if options.crossed_limit > 0: + edge_random.shuffle(undecided) + remaining_crossed_edges = len(undecided) if options.crossed_limit < 0 else max(0, options.crossed_limit - len(make_crossed)) + if remaining_crossed_edges > 0: + for x in undecided: + if edge_random.random() < options.crossed_chance: + make_crossed.add(x) + remaining_crossed_edges -= 1 + if remaining_crossed_edges == 0: + break + + # Connect the edge sets + for dir, w, i, j, terrain in left_to_connect: + if not is_unrestricted_crossed or not (dir, i, j) in make_disabled: + world_idx = w + if dir == Direction.East: + edges_1 = grid_info.east_edges_grid if terrain == 0 else grid_info.east_edges_water_grid + edges_2 = grid_info.west_edges_grid if terrain == 0 else grid_info.west_edges_water_grid + if use_crossed_groups and grid_info.crossed_groups[i][j] != grid_info.crossed_groups[i][(j + 1) % 8]: + world_idx = 1 - w + elif is_unrestricted_crossed and (dir, i, j) in make_crossed: + world_idx = 1 - w + connect_edge_sets(world, player, edges_1[w][i][j], edges_2[world_idx][i][(j + 1) % 8], edge_random, connected_edges, prio_edges, final_placement) + else: + edges_1 = grid_info.south_edges_grid if terrain == 0 else grid_info.south_edges_water_grid + edges_2 = grid_info.north_edges_grid if terrain == 0 else grid_info.north_edges_water_grid + if use_crossed_groups and grid_info.crossed_groups[i][j] != grid_info.crossed_groups[(i + 1) % 8][j]: + world_idx = 1 - w + elif is_unrestricted_crossed and (dir, i, j) in make_crossed: + world_idx = 1 - w + connect_edge_sets(world, player, edges_1[w][i][j], edges_2[world_idx][(i + 1) % 8][j], edge_random, connected_edges, prio_edges, final_placement) + + # Count disabled edges + disabled_count = 0 + for screen in overworld_screens.values(): + for edge in screen.edges.values(): + if not edge.dest: + disabled_count += 1 + return disabled_count + +def connect_edge_sets(world: World, player: int, edge_set_1: List[OWEdge], edge_set_2: List[OWEdge], edge_random: _random.Random, connected_edges: List[str], prio_edges: List[str], final_placement: bool) -> None: + if edge_set_1 and edge_set_2: + if world.owParallel[player]: + # Make sure that we do not connect parallel with non-parallel edges + parallel_edge_set_1 = [edge for edge in edge_set_1 if edge.parallel] + parallel_edge_set_2 = [edge for edge in edge_set_2 if edge.parallel] + if any(parallel_edge_set_1) and any(parallel_edge_set_2): + # Special case for screens that have both types of edges in the same direction (Dig Game and Frog) + if len(edge_set_1) == 2 and len(edge_set_2) == 2 and not edge_set_1[0].parallel and edge_set_1[1].parallel and not edge_set_2[0].parallel and edge_set_2[1].parallel: + connect_two_way(world, edge_set_1[0].name, edge_set_2[0].name, player, connected_edges, final_placement) + # Check if the edges already got connected when handling the other world + if any(edge for edge in parallel_edge_set_1 if edge.dest) or any(edge for edge in parallel_edge_set_2 if edge.dest): + return + # Special case for Maze Race and Kakariko Suburb with Keep Similar, only connect those when handling the other world + if ENABLE_KEEP_SIMILAR_SPECIAL_HANDLING and world.owKeepSimilar[player] and ((len(edge_set_1) == 1 and (edge_set_1[0].name == 'Maze Race ES' or edge_set_1[0].name == 'Kakariko Suburb WS')) or (len(edge_set_2) == 1 and (edge_set_2[0].name == 'Maze Race ES' or edge_set_2[0].name == 'Kakariko Suburb WS'))): + return + edge_set_1 = parallel_edge_set_1 + edge_set_2 = parallel_edge_set_2 + else: + non_parallel_edge_set_1 = [edge for edge in edge_set_1 if not edge.parallel] + non_parallel_edge_set_2 = [edge for edge in edge_set_2 if not edge.parallel] + if not any(non_parallel_edge_set_1) or not any(non_parallel_edge_set_2): + return + edge_set_1 = non_parallel_edge_set_1 + edge_set_2 = non_parallel_edge_set_2 + if len(edge_set_1) == len(edge_set_2): + for k in range(len(edge_set_1)): + connect_two_way(world, edge_set_1[k].name, edge_set_2[k].name, player, connected_edges, final_placement) + elif not ENABLE_KEEP_SIMILAR_SPECIAL_HANDLING or not world.owKeepSimilar[player]: + if len(edge_set_1) < len(edge_set_2): + edge_set_1, edge_set_2 = edge_set_2, edge_set_1 + # Not all edges from edge_set_1 can get connected + prio_set = [edge for edge in edge_set_1 if edge.name in prio_edges] + if len(prio_set) == len(edge_set_2): + for k in range(len(prio_set)): + connect_two_way(world, prio_set[k].name, edge_set_2[k].name, player, connected_edges, final_placement) + elif len(prio_set) < len(edge_set_2): + unconnected_edges = edge_random.sample([edge.name for edge in edge_set_1 if edge.name not in prio_edges], len(edge_set_1) - len(edge_set_2)) + edges_to_connect = [edge for edge in edge_set_1 if edge.name not in unconnected_edges] + for k in range(len(edge_set_2)): + connect_two_way(world, edges_to_connect[k].name, edge_set_2[k].name, player, connected_edges, final_placement) + else: + raise Exception("There should never be multiple edges with high priority in an edge set") + +# ============================================================================ +# GRID FORMATTING +# ============================================================================ + +def format_grid_for_spoiler(grid: List[List[int]]) -> str: + lines = [] + header = " " + for col in range(8): + header += f" {col} " + lines.append(header) + + for row in range(8): + border_line = " +" + for col in range(8): + if row > 0 and is_same_large_screen(grid, row, col, row - 1, col): + border_line += " " + else: + border_line += "--" + + # Check if we need a corner or continuation + if col < 7: + has_horizontal_left = row == 0 or not is_same_large_screen(grid, row, col, row - 1, col) + has_horizontal_right = row == 0 or not is_same_large_screen(grid, row, col + 1, row - 1, col + 1) + has_vertical_top = row == 0 or not is_same_large_screen(grid, row - 1, col, row - 1, col + 1) + has_vertical_bottom = not is_same_large_screen(grid, row, col, row, col + 1) + + if has_vertical_bottom or has_vertical_top: + if has_horizontal_left or has_horizontal_right: + border_line += "+" + else: + border_line += "|" + else: + if has_horizontal_left or has_horizontal_right: + border_line += "-" + else: + border_line += " " + else: + border_line += "+" + + lines.append(border_line) + + row_name = "ABCDEFGH"[row] + content_line = f"{row_name}({row * 8:02X})|" + for col in range(8): + screen_id = grid[row][col] + if screen_id == -1: + content_line += "--" + else: + content_line += f"{screen_id:02X}" + + # Check if we need a vertical separator after this cell + if col < 7: + if is_same_large_screen(grid, row, col, row, col + 1): + content_line += " " + else: + content_line += "|" + else: + content_line += "|" + + lines.append(content_line) + + bottom_border = " +" + for col in range(8): + bottom_border += "--" + if col < 7: + # Check if the bottom cells are part of the same large screen + if is_same_large_screen(grid, 7, col, 7, col + 1): + bottom_border += "-" + else: + bottom_border += "+" + else: + bottom_border += "+" + lines.append(bottom_border) + return "\n".join(lines) + +def is_same_large_screen(grid: List[List[int]], row1: int, col1: int, row2: int, col2: int) -> bool: + id1 = grid[row1 % 8][col1 % 8] + id2 = grid[row2 % 8][col2 % 8] + if id1 == -1 or id2 == -1: + return False + return id1 == id2 and id1 in large_screen_ids + +# ============================================================================ +# MAIN EXECUTION +# ============================================================================ + +def generate_random_grid_layout(world: World, player: int, connected_edges: List[str], crossed_group_b: List[int], forced_non_crossed: Set[str], forced_crossed: Set[str], crossed_limit: int, crossed_chance: float): + """Main execution function""" + import time + + horizontal_wrap = False + vertical_wrap = False + if world.customizer: + grid_options = world.customizer.get_owgrid() + if grid_options and player in grid_options: + grid_options = grid_options[player] + horizontal_wrap = 'wrap_horizontal' in grid_options and grid_options['wrap_horizontal'] == True + vertical_wrap = 'wrap_vertical' in grid_options and grid_options['wrap_vertical'] == True + + first_ignore_bonus = 2 + if not world.owParallel[player]: + first_ignore_bonus *= 2 + if world.owCrossed[player] == 'unrestricted': + first_ignore_bonus *= 2 + options = LayoutGeneratorOptions( + horizontal_wrap=horizontal_wrap, + vertical_wrap=vertical_wrap, + large_screen_pool=False, + distortion_chance=0.0, + random_order=6 if world.owParallel[player] else 12, + multi_choice=1, + max_delay=10, + penalty_full_edge_mismatch=1, + penalty_partial_edge_mismatch=1, + bonus_partial_edge_match=1, + bonus_full_edge_match=1, + bonus_crossed_group_match=1, + bonus_fill_parallel=1 if world.owCrossed[player] == 'unrestricted' else 0, + first_ignore_bonus_points=first_ignore_bonus, + forced_non_crossed_edges=forced_non_crossed, + forced_crossed_edges=forced_crossed, + crossed_chance=crossed_chance, + crossed_limit=crossed_limit, + check_reachability=True, + sort_by_edge_sides=world.owParallel[player] or not world.owTerrain[player], + sort_by_max_edges_per_side=False, + sort_by_piece_size=True, + min_runs=100, + max_runs=10000, + target_runs_times_successes=5000 + ) + + overworld_screens = initialize_screens(world, player) + large_screen_quadrant_info, large_screen_quadrant_info_land, large_screen_quadrant_info_water = initialize_large_screen_data(overworld_screens) + prio_edges = get_prioritized_edges(world, player) + pieces_to_place = create_piece_list(world, player, options, crossed_group_b, overworld_screens, large_screen_quadrant_info, large_screen_quadrant_info_land, large_screen_quadrant_info_water) + + start_time = time.time() + result = get_random_layout(world, player, connected_edges, pieces_to_place, options, prio_edges, overworld_screens) + elapsed_time = time.time() - start_time + + if result.grid_info: + connect_edges_for_screen_layout(world, player, result.grid_info, options, connected_edges, prio_edges, overworld_screens, True) + grid = result.grid_info.grid + + # Make new grid containing cell IDs for the overworld map + map_grid = copy.deepcopy(grid) + for w in range(2): + for i in range(8): + for j in range(8): + screen_id = map_grid[w][i][j] + if screen_id in large_screen_ids and map_grid[w][i][(j + 1) % 8] == screen_id and map_grid[w][(i + 1) % 8][j] == screen_id and map_grid[w][(i + 1) % 8][(j + 1) % 8] == screen_id: + map_grid[w][i][(j + 1) % 8] = screen_id + 0x01 + map_grid[w][(i + 1) % 8][j] = screen_id + 0x08 + map_grid[w][(i + 1) % 8][(j + 1) % 8] = screen_id + 0x09 + world.owgrid[player] = map_grid + world.owlayoutmap_lw[player] = {id & 0xBF: i for i, id in enumerate(sum(map_grid[0], []))} + world.owlayoutmap_dw[player] = {id & 0xBF: i for i, id in enumerate(sum(map_grid[1], []))} + + world.spoiler.set_map('layout_grid_lw', format_grid_for_spoiler(grid[0]), grid[0], player) + if not world.owParallel[player]: + world.spoiler.set_map('layout_grid_dw', format_grid_for_spoiler(grid[1]), grid[1], player) + + logger = logging.getLogger('') + logger.debug(f"\nLayout generation statistics:") + logger.debug(f" Best score: {result.score}") + logger.debug(f" Worst score: {result.worst_score}") + logger.debug(f" Average score: {result.average_score:.2f}") + logger.debug(f" Successes: {result.successes}") + logger.debug(f" Failures: {result.failures}") + logger.debug(f" Generation time: {elapsed_time:.3f}s") + + if DRAW_IMAGE: + logger.debug("Creating layout visualization...") + try: + from source.overworld.LayoutVisualizer import visualize_layout + visualize_layout(grid, "visualizations", overworld_screens, large_screen_quadrant_info) + except Exception as e: + logger.warning(f"Warning: Could not create visualization: {e}") + else: + raise Exception(f"Layout generation FAILED after {result.failures} attempts and {elapsed_time:.3f} seconds") \ No newline at end of file diff --git a/source/overworld/LayoutVisualizer.py b/source/overworld/LayoutVisualizer.py new file mode 100644 index 00000000..8dbdea34 --- /dev/null +++ b/source/overworld/LayoutVisualizer.py @@ -0,0 +1,473 @@ +import logging +import os +from datetime import datetime +from typing import Dict, List +from PIL import Image, ImageDraw +from BaseClasses import Direction, OWEdge +from source.overworld.LayoutGenerator import Screen + +def get_edge_lists(grid: List[List[List[int]]], + overworld_screens: Dict[int, Screen], + large_screen_quadrant_info: Dict[int, Dict]) -> Dict: + """ + Get list of edges for each cell and direction. + + Args: + grid: 3D list [world][row][col] containing screen IDs + overworld_screens: Dict of screen_id -> Screen objects + large_screen_quadrant_info: Dict of screen_id -> quadrant info for large screens + + Returns: + Dict mapping (world, row, col, direction) -> list of edges + Each edge has a .dest property (None if unconnected) + """ + GRID_SIZE = 8 + edge_lists = {} + + # Large screen base IDs + large_screen_base_ids = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35, + 0x40, 0x43, 0x45, 0x58, 0x5B, 0x5E, 0x70, 0x75] + + for world_idx in range(2): + # Build a map of screen_id -> list of (row, col) positions for large screens + large_screen_positions = {} + for row in range(GRID_SIZE): + for col in range(GRID_SIZE): + screen_id = grid[world_idx][row][col] + if screen_id != -1 and screen_id in large_screen_base_ids: + if screen_id not in large_screen_positions: + large_screen_positions[screen_id] = [] + large_screen_positions[screen_id].append((row, col)) + + for row in range(GRID_SIZE): + for col in range(GRID_SIZE): + screen_id = grid[world_idx][row][col] + + if screen_id == -1: + # Empty cell - no edges + for direction in [Direction.North, Direction.South, Direction.East, Direction.West]: + edge_lists[(world_idx, row, col, direction)] = [] + continue + + screen = overworld_screens.get(screen_id) + if not screen: + for direction in [Direction.North, Direction.South, Direction.East, Direction.West]: + edge_lists[(world_idx, row, col, direction)] = [] + continue + + is_large = screen_id in large_screen_base_ids + + if is_large: + # For large screens, determine which quadrant this cell is + # Find all positions of this large screen and determine quadrant + positions = large_screen_positions.get(screen_id, [(row, col)]) + + # Determine quadrant by finding relative position + # The quadrant is determined by which cells are adjacent + quadrant = determine_large_screen_quadrant(row, col, positions, GRID_SIZE) + + # Get edges for this quadrant + if screen_id in large_screen_quadrant_info: + quad_info = large_screen_quadrant_info[screen_id] + + for direction in [Direction.North, Direction.South, Direction.East, Direction.West]: + edges = quad_info.get(quadrant, {}).get(direction, []) + edge_lists[(world_idx, row, col, direction)] = edges + else: + # No quadrant info - no edges + for direction in [Direction.North, Direction.South, Direction.East, Direction.West]: + edge_lists[(world_idx, row, col, direction)] = [] + else: + # Small screen - get edges directly + for direction in [Direction.North, Direction.South, Direction.East, Direction.West]: + edges_in_dir = [e for e in screen.edges.values() if e.direction == direction] + edge_lists[(world_idx, row, col, direction)] = edges_in_dir + + return edge_lists + +def determine_large_screen_quadrant(row: int, col: int, positions: List[tuple], grid_size: int) -> str: + """ + Determine which quadrant (NW, NE, SW, SE) a cell is in for a large screen. + Handles wrapping correctly by checking adjacency patterns. + + Args: + row: Current cell row + col: Current cell column + positions: List of all (row, col) positions for this large screen + grid_size: Size of the grid (8) + + Returns: + Quadrant string: "NW", "NE", "SW", or "SE" + """ + positions_set = set(positions) + + # Check which adjacent cells also belong to this large screen + has_right = ((row, (col + 1) % grid_size) in positions_set) + has_below = (((row + 1) % grid_size, col) in positions_set) + has_left = ((row, (col - 1) % grid_size) in positions_set) + has_above = (((row - 1) % grid_size, col) in positions_set) + + # Determine quadrant based on adjacency + # NW: has right and below neighbors + # NE: has left and below neighbors + # SW: has right and above neighbors + # SE: has left and above neighbors + + if has_right and has_below: + return "NW" + elif has_left and has_below: + return "NE" + elif has_right and has_above: + return "SW" + elif has_left and has_above: + return "SE" + else: + raise Exception("?") + +def is_crossed_edge(edge: OWEdge, overworld_screens: Dict[int, Screen]) -> bool: + if edge.dest is None: + return False + + source_screen = overworld_screens.get(edge.owIndex) + dest_screen = overworld_screens.get(edge.dest.owIndex) + return source_screen.dark_world != dest_screen.dark_world + +def visualize_layout(grid: List[List[List[int]]], output_dir: str, + overworld_screens: Dict[int, Screen], + large_screen_quadrant_info: Dict[int, Dict]) -> None: + # Constants + GRID_SIZE = 8 + BORDER_WIDTH = 1 + OUTPUT_CELL_SIZE = 64 # Each cell in output is always 64x64 pixels + + # Load the world images + try: + lightworld_img = Image.open("data/overworld/lightworld.png") + darkworld_img = Image.open("data/overworld/darkworld.png") + except FileNotFoundError as e: + raise FileNotFoundError(f"World image not found: {e}. Ensure lightworld.png and darkworld.png are in the data/overworld directory.") + + # Calculate source cell size from the base images + # Each world image is 8x8 screens, so divide by 8 to get source cell size + img_width, _ = lightworld_img.size + SOURCE_CELL_SIZE = img_width // GRID_SIZE # Size of each cell in the source image + + # Calculate dimensions for the output (always based on 64x64 cells) + world_width = GRID_SIZE * OUTPUT_CELL_SIZE + world_height = GRID_SIZE * OUTPUT_CELL_SIZE + + # Create output image (two worlds side by side with a small gap) + gap = 32 + output_width = world_width * 2 + gap + output_height = world_height + output_img = Image.new('RGB', (output_width, output_height), color='black') + + # Large screen base IDs (defined once for reuse) + large_screen_base_ids = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35, + 0x40, 0x43, 0x45, 0x58, 0x5B, 0x5E, 0x70, 0x75] + + # Process both worlds + for world_idx in range(2): + x_offset = 0 if world_idx == 0 else (world_width + gap) + + # Build a map of screen_id -> list of (row, col) positions for large screens + large_screen_positions = {} + for row in range(GRID_SIZE): + for col in range(GRID_SIZE): + screen_id = grid[world_idx][row][col] + if screen_id != -1 and screen_id in large_screen_base_ids: + if screen_id not in large_screen_positions: + large_screen_positions[screen_id] = [] + large_screen_positions[screen_id].append((row, col)) + + # Process each cell in the grid individually + # This handles wrapped large screens correctly by drawing each quadrant separately + for row in range(GRID_SIZE): + for col in range(GRID_SIZE): + screen_id = grid[world_idx][row][col] + + if screen_id == -1: + # Empty cell - fill with black (already black from initialization) + continue + + is_large = screen_id in large_screen_base_ids + + # Calculate source position in the world image + source_row = (screen_id % 0x40) >> 3 + source_col = screen_id % 0x08 + world_img = lightworld_img if screen_id < 0x40 else darkworld_img + + if is_large: + # For large screens, determine which quadrant this cell represents + positions = large_screen_positions.get(screen_id, [(row, col)]) + quadrant = determine_large_screen_quadrant(row, col, positions, GRID_SIZE) + + # Map quadrant to source offset within the 2x2 large screen + quadrant_offsets = { + "NW": (0, 0), + "NE": (1, 0), + "SW": (0, 1), + "SE": (1, 1) + } + q_col_offset, q_row_offset = quadrant_offsets[quadrant] + + # Calculate source position for this quadrant + source_x = (source_col + q_col_offset) * SOURCE_CELL_SIZE + source_y = (source_row + q_row_offset) * SOURCE_CELL_SIZE + + # Crop single cell from source (the specific quadrant) + cropped = world_img.crop(( + source_x, + source_y, + source_x + SOURCE_CELL_SIZE, + source_y + SOURCE_CELL_SIZE + )) + else: + # Small screen (1x1) + source_x = source_col * SOURCE_CELL_SIZE + source_y = source_row * SOURCE_CELL_SIZE + + # Crop single cell from source + cropped = world_img.crop(( + source_x, + source_y, + source_x + SOURCE_CELL_SIZE, + source_y + SOURCE_CELL_SIZE + )) + + # Resize to output size (64x64 pixels) + resized = cropped.resize( + (OUTPUT_CELL_SIZE, OUTPUT_CELL_SIZE), + Image.LANCZOS + ) + + # Paste into output at grid position + dest_x = x_offset + col * OUTPUT_CELL_SIZE + dest_y = row * OUTPUT_CELL_SIZE + output_img.paste(resized, (dest_x, dest_y)) + + edge_lists = get_edge_lists(grid, overworld_screens, large_screen_quadrant_info) + + # Draw borders and edge connection indicators after all screens are placed + draw = ImageDraw.Draw(output_img) + + # Size of the indicator squares + INDICATOR_SIZE = 12 + + for world_idx in range(2): + x_offset = 0 if world_idx == 0 else (world_width + gap) + + # Build large screen positions map for this world + large_screen_positions = {} + for row in range(GRID_SIZE): + for col in range(GRID_SIZE): + screen_id = grid[world_idx][row][col] + if screen_id != -1 and screen_id in large_screen_base_ids: + if screen_id not in large_screen_positions: + large_screen_positions[screen_id] = [] + large_screen_positions[screen_id].append((row, col)) + + # Draw borders for each cell + for row in range(GRID_SIZE): + for col in range(GRID_SIZE): + screen_id = grid[world_idx][row][col] + + if screen_id == -1: + continue + + is_large = screen_id in large_screen_base_ids + + dest_x = x_offset + col * OUTPUT_CELL_SIZE + dest_y = row * OUTPUT_CELL_SIZE + + if is_large: + # For large screens, determine which quadrant this cell is + positions = large_screen_positions.get(screen_id, [(row, col)]) + quadrant = determine_large_screen_quadrant(row, col, positions, GRID_SIZE) + + # Draw border only on the outer edges of the large screen + # (not on internal edges between quadrants) + # NW: draw top and left borders + # NE: draw top and right borders + # SW: draw bottom and left borders + # SE: draw bottom and right borders + + if quadrant in ["NW", "NE"]: + # Draw top border + draw.line([(dest_x, dest_y), (dest_x + OUTPUT_CELL_SIZE - 1, dest_y)], fill='black', width=BORDER_WIDTH) + if quadrant in ["SW", "SE"]: + # Draw bottom border + draw.line([(dest_x, dest_y + OUTPUT_CELL_SIZE - 1), (dest_x + OUTPUT_CELL_SIZE - 1, dest_y + OUTPUT_CELL_SIZE - 1)], fill='black', width=BORDER_WIDTH) + if quadrant in ["NW", "SW"]: + # Draw left border + draw.line([(dest_x, dest_y), (dest_x, dest_y + OUTPUT_CELL_SIZE - 1)], fill='black', width=BORDER_WIDTH) + if quadrant in ["NE", "SE"]: + # Draw right border + draw.line([(dest_x + OUTPUT_CELL_SIZE - 1, dest_y), (dest_x + OUTPUT_CELL_SIZE - 1, dest_y + OUTPUT_CELL_SIZE - 1)], fill='black', width=BORDER_WIDTH) + else: + # Small screen - draw border around single cell + draw.rectangle( + [dest_x, dest_y, dest_x + OUTPUT_CELL_SIZE - 1, dest_y + OUTPUT_CELL_SIZE - 1], + outline='black', + width=BORDER_WIDTH + ) + + # Draw edge connection indicators for each cell + for row in range(GRID_SIZE): + for col in range(GRID_SIZE): + screen_id = grid[world_idx][row][col] + if screen_id == -1: + continue + + dest_x = x_offset + col * OUTPUT_CELL_SIZE + dest_y = row * OUTPUT_CELL_SIZE + + # Draw indicator for each direction (only if edges exist) + # Use bright colors for visibility + GREEN = (0, 255, 0) # Bright green + YELLOW = (255, 255, 0) # Bright yellow + RED = (255, 0, 0) # Bright red + + # North indicators - positioned based on edge midpoint + north_edges = edge_lists.get((world_idx, row, col, Direction.North), []) + if north_edges: + north_y = dest_y # Touch the top border + + for edge in north_edges: + # For north/south edges, midpoint gives the X coordinate + # Take midpoint modulo 0x0200, range 0-0x01FF maps to full side + midpoint = edge.midpoint % 0x0200 + # Map from game coordinate range to pixel position + edge_x_offset = (midpoint * OUTPUT_CELL_SIZE) // 0x0200 + edge_x = dest_x + edge_x_offset - INDICATOR_SIZE // 2 + + edge_color = GREEN if edge.dest is not None else YELLOW if any(e for e in north_edges if e.dest) else RED + draw.rectangle( + [edge_x, north_y, edge_x + INDICATOR_SIZE - 1, north_y + INDICATOR_SIZE - 1], + fill=edge_color, + outline='black' + ) + + # Draw diagonal cross if edge crosses between worlds + if edge.dest is not None and is_crossed_edge(edge, overworld_screens): + draw.line( + [edge_x, north_y, edge_x + INDICATOR_SIZE - 1, north_y + INDICATOR_SIZE - 1], + fill='black', + width=1 + ) + draw.line( + [edge_x + INDICATOR_SIZE - 1, north_y, edge_x, north_y + INDICATOR_SIZE - 1], + fill='black', + width=1 + ) + + # South indicators - positioned based on edge midpoint + south_edges = edge_lists.get((world_idx, row, col, Direction.South), []) + if south_edges: + south_y = dest_y + OUTPUT_CELL_SIZE - INDICATOR_SIZE # Touch the bottom border + + for edge in south_edges: + # For north/south edges, midpoint gives the X coordinate + # Take midpoint modulo 0x0200, range 0-0x01FF maps to full side + midpoint = edge.midpoint % 0x0200 + # Map from game coordinate range to pixel position + edge_x_offset = (midpoint * OUTPUT_CELL_SIZE) // 0x0200 + edge_x = dest_x + edge_x_offset - INDICATOR_SIZE // 2 + + edge_color = GREEN if edge.dest is not None else YELLOW if any(e for e in south_edges if e.dest) else RED + draw.rectangle( + [edge_x, south_y, edge_x + INDICATOR_SIZE - 1, south_y + INDICATOR_SIZE - 1], + fill=edge_color, + outline='black' + ) + + # Draw diagonal cross if edge crosses between worlds + if edge.dest is not None and is_crossed_edge(edge, overworld_screens): + draw.line( + [edge_x, south_y, edge_x + INDICATOR_SIZE - 1, south_y + INDICATOR_SIZE - 1], + fill='black', + width=1 + ) + draw.line( + [edge_x + INDICATOR_SIZE - 1, south_y, edge_x, south_y + INDICATOR_SIZE - 1], + fill='black', + width=1 + ) + + # West indicators - positioned based on edge midpoint + west_edges = edge_lists.get((world_idx, row, col, Direction.West), []) + if west_edges: + west_x = dest_x # Touch the left border + + for edge in west_edges: + # For west/east edges, midpoint gives the Y coordinate + # Take midpoint modulo 0x0200, range 0-0x01FF maps to full side + midpoint = edge.midpoint % 0x0200 + # Map from game coordinate range to pixel position + edge_y_offset = (midpoint * OUTPUT_CELL_SIZE) // 0x0200 + edge_y = dest_y + edge_y_offset - INDICATOR_SIZE // 2 + + edge_color = GREEN if edge.dest is not None else YELLOW if any(e for e in west_edges if e.dest) else RED + draw.rectangle( + [west_x, edge_y, west_x + INDICATOR_SIZE - 1, edge_y + INDICATOR_SIZE - 1], + fill=edge_color, + outline='black' + ) + + # Draw diagonal cross if edge crosses between worlds + if edge.dest is not None and is_crossed_edge(edge, overworld_screens): + draw.line( + [west_x, edge_y, west_x + INDICATOR_SIZE - 1, edge_y + INDICATOR_SIZE - 1], + fill='black', + width=1 + ) + draw.line( + [west_x + INDICATOR_SIZE - 1, edge_y, west_x, edge_y + INDICATOR_SIZE - 1], + fill='black', + width=1 + ) + + # East indicators - positioned based on edge midpoint + east_edges = edge_lists.get((world_idx, row, col, Direction.East), []) + if east_edges: + east_x = dest_x + OUTPUT_CELL_SIZE - INDICATOR_SIZE # Touch the right border + + for edge in east_edges: + # For west/east edges, midpoint gives the Y coordinate + # Take midpoint modulo 0x0200, range 0-0x01FF maps to full side + midpoint = edge.midpoint % 0x0200 + # Map from game coordinate range to pixel position + edge_y_offset = (midpoint * OUTPUT_CELL_SIZE) // 0x0200 + edge_y = dest_y + edge_y_offset - INDICATOR_SIZE // 2 + + edge_color = GREEN if edge.dest is not None else YELLOW if any(e for e in east_edges if e.dest) else RED + draw.rectangle( + [east_x, edge_y, east_x + INDICATOR_SIZE - 1, edge_y + INDICATOR_SIZE - 1], + fill=edge_color, + outline='black' + ) + + # Draw diagonal cross if edge crosses between worlds + if edge.dest is not None and is_crossed_edge(edge, overworld_screens): + draw.line( + [east_x, edge_y, east_x + INDICATOR_SIZE - 1, edge_y + INDICATOR_SIZE - 1], + fill='black', + width=1 + ) + draw.line( + [east_x + INDICATOR_SIZE - 1, edge_y, east_x, edge_y + INDICATOR_SIZE - 1], + fill='black', + width=1 + ) + + # Create output directory if it doesn't exist + os.makedirs(output_dir, exist_ok=True) + + # Generate filename with timestamp + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f"layout_{timestamp}.png" + filepath = os.path.join(output_dir, filename) + + # Save the image + output_img.save(filepath, "PNG") + logging.getLogger('').info(f"Layout visualization saved to {filepath}") \ No newline at end of file diff --git a/source/rom/DataTables.py b/source/rom/DataTables.py index 0ba42bab..8523cf77 100644 --- a/source/rom/DataTables.py +++ b/source/rom/DataTables.py @@ -44,6 +44,11 @@ class DataTables: self.ow_enemy_table = None self.pot_secret_table = None self.overworld_sprite_sheets = None + self.pointer_addresses = { + # table: [data_start, data_size, pointer_table, references] + 'ow_sprites': [ 0x09CB41, None, (0x09C881, 0x09C901, 0x09CA21), None ], + 'uw_sprites': [ 0x09D92E, None, 0x09D62E, 0x09C298 ], + } # associated data self.sprite_requirements = None @@ -92,13 +97,6 @@ class DataTables: # bank 0A uses 372A bytes # bank 1F uses 77CE bytes: total is about a bank and a half # probably should reuse bank 1F if writing all the rooms out - for sheet in self.sprite_sheets.values(): - sheet.write_to_rom(rom, snes_to_pc(0x00DB97)) # bank 00, SheetsTable_AA3 - if self.uw_enemy_table.size() > 0x2800: - raise Exception('Sprite table is too big for current area') - self.uw_enemy_table.write_sprite_data_to_rom(rom) - self.uw_enemy_table.check_special_bitmasks_size() - self.uw_enemy_table.write_special_bitmask_table(rom) for area_id, sheet in self.overworld_sprite_sheets.items(): if area_id in [0x80, 0x81]: offset = area_id - 0x80 # 02E575 for special areas? @@ -110,7 +108,14 @@ class DataTables: # _00FAC1 is LW post-aga # _00FB01 is DW # _00FA41 is rain state + for sheet in self.sprite_sheets.values(): + sheet.write_to_rom(rom, snes_to_pc(0x00DB97)) # bank 00, SheetsTable_AA3 self.write_ow_sprite_data_to_rom(rom) + if self.uw_enemy_table.size() > 0x2800: + raise Exception('Sprite table is too big for current area') + self.uw_enemy_table.write_sprite_data_to_rom(rom, self.pointer_addresses) + self.uw_enemy_table.check_special_bitmasks_size() + self.uw_enemy_table.write_special_bitmask_table(rom) for sprite, stats in self.enemy_stats.items(): # write health to rom if stats.health is not None: @@ -148,13 +153,14 @@ class DataTables: def write_ow_sprite_data_to_rom(self, rom): # calculate how big this table is going to be? - # bytes = sum(1+len(x)*3 for x in self.ow_enemy_table.values() if len(x) > 0)+1 + bytes = sum(1+len(x)*3 for x in self.ow_enemy_table.values() if len(x) > 0)+1 + self.pointer_addresses['ow_sprites'][1] = bytes # ending_byte = 0x09CB3B + bytes - max_per_state = {0: 0x40, 1: 0x90, 2: 0x8B} # dropped max on state 2 to steal space for a couple extra sprites (Murahdahla, extra tutorial guard) + max_per_state = {0: 0x40, 1: 0x90, 2: 0x82} # dropped max on state 2 to steal space for extra sprites (Murahdahla, extra tutorial guard) - pointer_address = snes_to_pc(0x09C881) - # currently borrowed 10 bytes, used 9 (2xMurah + TutorialGuard) - data_pointer = snes_to_pc(0x09CB38) # was originally 0x09CB41 - stealing space for a couple extra sprites (Murahdahla, extra tutorial guard) + pointer_address = snes_to_pc(self.pointer_addresses['ow_sprites'][2][0]) + self.pointer_addresses['ow_sprites'][0] = pointer_address + ((max_per_state[0] + max_per_state[1] + max_per_state[2]) * 2) + data_pointer = self.pointer_addresses['ow_sprites'][0] empty_pointer = pc_to_snes(data_pointer) & 0xFFFF rom.write_byte(data_pointer, 0xff) cached_dark_world = {} @@ -187,6 +193,10 @@ class DataTables: data_pointer += len(data) rom.write_byte(data_pointer, 0xff) data_pointer += 1 + # Check if OW sprite data has overwritten the UW sprite pointer table + max_allowed_address = snes_to_pc(0x09D62E) + if data_pointer > max_allowed_address: + raise Exception(f'OW sprite data will cause the UW sprite pointer table to overwrite the pots pointer table. Data end: {hex(pc_to_snes(data_pointer))}, Max allowed: $09D62E') special_health_table = { diff --git a/source/tools/MysteryUtils.py b/source/tools/MysteryUtils.py index 441d4fdc..670cc62c 100644 --- a/source/tools/MysteryUtils.py +++ b/source/tools/MysteryUtils.py @@ -121,8 +121,16 @@ def roll_settings(weights): ret.accessibility = get_choice('accessibility') 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') - 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') valid_options = {'none': 'none', 'polar': 'polar', 'grouped': 'polar', 'chaos': 'unrestricted', 'unrestricted': 'unrestricted'} ret.ow_crossed = get_choice('overworld_crossed') @@ -132,6 +140,7 @@ def roll_settings(weights): ret.ow_whirlpool = get_choice_bool('whirlpool_shuffle') overworld_flute = get_choice('flute_shuffle') ret.ow_fluteshuffle = overworld_flute if overworld_flute != 'none' else 'vanilla' + ret.ow_fog = get_choice_bool('overworld_fog') ret.shuffle_followers = get_choice_bool('shuffle_followers') ret.bonk_drops = get_choice_bool('bonk_drops') entrance_shuffle = get_choice('entrance_shuffle') @@ -148,11 +157,7 @@ def roll_settings(weights): ret.door_self_loops = get_choice_bool('door_self_loops') ret.experimental = get_choice_bool('experimental') ret.collection_rate = get_choice_bool('collection_rate') - ret.dungeon_counters = get_choice_non_bool('dungeon_counters') if 'dungeon_counters' in weights else 'default' - if ret.dungeon_counters == 'default': - ret.dungeon_counters = 'pickup' if ret.door_shuffle != 'vanilla' or ret.compassshuffle != 'none' else 'off' - ret.pseudoboots = get_choice_bool('pseudoboots') ret.mirrorscroll = get_choice_bool('mirrorscroll') ret.shopsanity = get_choice_bool('shopsanity')