Pull in OR 0.7.0.1 (#2)

Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
2026-01-26 04:19:24 +00:00
39 changed files with 2750 additions and 454 deletions
+1
View File
@@ -31,6 +31,7 @@ weights/
/QUsb2Snes/ /QUsb2Snes/
/output/ /output/
/enemizer/ /enemizer/
visualizations/
resources/user/* resources/user/*
!resources/user/.gitkeep !resources/user/.gitkeep
+51 -49
View File
@@ -26,11 +26,12 @@ from Versions import DRVersion, GKVersion, ORVersion
class World(object): class World(object):
def __init__(self, players, owShuffle, owCrossed, owMixed, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments, def __init__(self, players, owLayout, owParallel, owCrossed, owMixed, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments,
timer, progressive, goal, algorithm, accessibility, shuffle_ganon, custom, customitemarray, hints, spoiler_mode): timer, progressive, goal, algorithm, accessibility, shuffle_ganon, custom, customitemarray, hints, spoiler_mode):
self.players = players self.players = players
self.teams = 1 self.teams = 1
self.owShuffle = owShuffle.copy() self.owLayout = owLayout.copy()
self.owParallel = owParallel.copy()
self.owTerrain = {} self.owTerrain = {}
self.owKeepSimilar = {} self.owKeepSimilar = {}
self.owMixed = owMixed.copy() self.owMixed = owMixed.copy()
@@ -38,6 +39,7 @@ class World(object):
self.owCrossed = self.owCrossed if self.owCrossed != 'polar' or self.owMixed else 'none' self.owCrossed = self.owCrossed if self.owCrossed != 'polar' or self.owMixed else 'none'
self.owWhirlpoolShuffle = {} self.owWhirlpoolShuffle = {}
self.owFluteShuffle = {} self.owFluteShuffle = {}
self.owFog = {}
self.shuffle = shuffle.copy() self.shuffle = shuffle.copy()
self.doorShuffle = doorShuffle.copy() self.doorShuffle = doorShuffle.copy()
self.intensity = {} self.intensity = {}
@@ -99,6 +101,9 @@ class World(object):
self.owswaps = {} self.owswaps = {}
self.owcrossededges = {} self.owcrossededges = {}
self.owwhirlpools = {} self.owwhirlpools = {}
self.owgrid = {}
self.owlayoutmap_lw = {}
self.owlayoutmap_dw = {}
self.owflutespots = {} self.owflutespots = {}
self.owsectors = {} self.owsectors = {}
self.allow_flip_sanc = {} self.allow_flip_sanc = {}
@@ -131,6 +136,7 @@ class World(object):
set_player_attr('owswaps', [[],[],[]]) set_player_attr('owswaps', [[],[],[]])
set_player_attr('owcrossededges', []) set_player_attr('owcrossededges', [])
set_player_attr('owwhirlpools', []) set_player_attr('owwhirlpools', [])
set_player_attr('owgrid', None)
set_player_attr('owsectors', None) set_player_attr('owsectors', None)
set_player_attr('allow_flip_sanc', False) set_player_attr('allow_flip_sanc', False)
set_player_attr('remote_items', False) set_player_attr('remote_items', False)
@@ -1747,12 +1753,12 @@ class Entrance(object):
def can_reach(self, state): def can_reach(self, state):
# Destination Pickup OW Only No Ledges Can S&Q Allow Mirror # Destination Pickup OW Only No Ledges Can S&Q Allow Mirror
multi_step_locations = { 'Pyramid Crack': ('Big Bomb', True, True, False, True), multi_step_locations = { 'Pyramid Crack': ('Pick Up Big Bomb', True, True, False, True),
'Missing Smith': ('Frog', True, False, True, True), 'Missing Smith': ('Get Frog', True, False, True, True),
'Middle Aged Man': ('Dark Blacksmith Ruins', True, False, True, True), 'Middle Aged Man': ('Pick Up Purple Chest', True, False, True, True),
'Dark Palace Button':('Kiki', True, False, False, False), 'Dark Palace Button': ('Pick Up Kiki', True, False, False, False),
'Old Man Drop Off': ('Lost Old Man', True, False, False, False), 'Old Man Drop Off': ('Escort Old Man', True, False, False, False),
'Revealing Light': ('Suspicious Maiden', False, False, False, False) 'Revealing Light': ('Maiden Rescued', False, False, False, False)
} }
if self.name in multi_step_locations: if self.name in multi_step_locations:
@@ -1761,7 +1767,10 @@ class Entrance(object):
multi_step_loc = multi_step_locations[self.name] multi_step_loc = multi_step_locations[self.name]
if world.shuffle_followers[self.player]: if world.shuffle_followers[self.player]:
multi_step_loc = (multi_step_loc[0], self.name == 'Pyramid Crack', multi_step_loc[2], True, True) 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 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: if not self in state.path:
path = state.path.get(step_location.parent_region, (step_location.parent_region.name, None)) 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): def __init__(self, player, name, owIndex, direction, terrain, edge_id, owSlotIndex=0xff):
self.player = player self.player = player
self.name = name self.name = name
self.type = DoorType.Open
self.direction = direction self.direction = direction
self.terrain = terrain self.terrain = terrain
self.parallel = None
self.specialEntrance = False self.specialEntrance = False
self.specialExit = False self.specialExit = False
self.deadEnd = False
# rom properties # rom properties
self.owIndex = owIndex self.owIndex = owIndex
@@ -2437,7 +2445,6 @@ class OWEdge(object):
self.worldType = WorldType.Dark self.worldType = WorldType.Dark
# logical properties # logical properties
# self.connected = False # combine with Dest?
self.dest = None self.dest = None
self.dependents = [] self.dependents = []
self.dead = False self.dead = False
@@ -2454,9 +2461,6 @@ class OWEdge(object):
def getTarget(self): def getTarget(self):
return self.dest.specialID if self.dest.specialExit else self.dest.edge_id 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): def coordInfo(self, midpoint, vram_loc):
self.midpoint = midpoint self.midpoint = midpoint
self.vramLoc = vram_loc self.vramLoc = vram_loc
@@ -3095,13 +3099,15 @@ class Spoiler(object):
'bow_mode': self.world.bow_mode, 'bow_mode': self.world.bow_mode,
'goal': self.world.goal, 'goal': self.world.goal,
'custom_goals': self.world.custom_goals, 'custom_goals': self.world.custom_goals,
'ow_shuffle': self.world.owShuffle, 'ow_layout': self.world.owLayout,
'ow_parallel': self.world.owParallel,
'ow_terrain': self.world.owTerrain, 'ow_terrain': self.world.owTerrain,
'ow_crossed': self.world.owCrossed, 'ow_crossed': self.world.owCrossed,
'ow_keepsimilar': self.world.owKeepSimilar, 'ow_keepsimilar': self.world.owKeepSimilar,
'ow_mixed': self.world.owMixed, 'ow_mixed': self.world.owMixed,
'ow_whirlpool': self.world.owWhirlpoolShuffle, 'ow_whirlpool': self.world.owWhirlpoolShuffle,
'ow_fluteshuffle': self.world.owFluteShuffle, 'ow_fluteshuffle': self.world.owFluteShuffle,
'ow_fog': self.world.owFog,
'bonk_drops': self.world.shuffle_bonk_drops, 'bonk_drops': self.world.shuffle_bonk_drops,
'shuffle_followers': self.world.shuffle_followers, 'shuffle_followers': self.world.shuffle_followers,
'shuffle': self.world.shuffle, '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('Enemy Drop Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['dropshuffle'][player])
outfile.write('Take Any Caves:'.ljust(line_width) + '%s\n' % self.metadata['take_any'][player]) outfile.write('Take Any Caves:'.ljust(line_width) + '%s\n' % self.metadata['take_any'][player])
outfile.write('\n') outfile.write('\n')
outfile.write('Overworld Layout Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_shuffle'][player]) outfile.write('Overworld Layout Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_layout'][player])
if self.metadata['ow_shuffle'][player] != 'vanilla': if self.metadata['ow_layout'][player] != 'vanilla':
outfile.write('Parallel OW:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_parallel'][player]))
outfile.write('Free Terrain:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_terrain'][player])) outfile.write('Free Terrain:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_terrain'][player]))
outfile.write('Crossed OW:'.ljust(line_width) + '%s\n' % self.metadata['ow_crossed'][player]) outfile.write('Crossed OW:'.ljust(line_width) + '%s\n' % self.metadata['ow_crossed'][player])
if self.metadata['ow_shuffle'][player] != 'vanilla' or self.metadata['ow_crossed'][player] != 'none': if self.metadata['ow_layout'][player] != 'vanilla' or self.metadata['ow_crossed'][player] != 'none':
outfile.write('Keep Similar OW Edges Together:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_keepsimilar'][player])) outfile.write('Keep Similar OW Edges Together:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_keepsimilar'][player]))
outfile.write('OW Tile Flip (Mixed):'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_mixed'][player])) outfile.write('OW Tile Flip (Mixed):'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_mixed'][player]))
outfile.write('Whirlpool Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_whirlpool'][player])) outfile.write('Whirlpool Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_whirlpool'][player]))
outfile.write('Flute Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_fluteshuffle'][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('\n')
outfile.write('Entrance Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['shuffle'][player]) outfile.write('Entrance Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['shuffle'][player])
if self.metadata['shuffle'][player] != 'vanilla': if self.metadata['shuffle'][player] != 'vanilla':
@@ -3506,40 +3515,31 @@ class Spoiler(object):
outfile.write(f'{fairy}: {bottle}\n') outfile.write(f'{fairy}: {bottle}\n')
if self.maps: if self.maps:
if 'all' in self.settings or 'flute' in self.settings: def write_map(type, title):
# flute shuffle
for player in range(1, self.world.players + 1): for player in range(1, self.world.players + 1):
if ('flute', player) in self.maps: if (type, player) in self.maps:
outfile.write('\n\nFlute Spots:\n\n') outfile.write('\n\n' + title + '\n\n')
break break
for player in range(1, self.world.players + 1): 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: if self.world.players > 1:
outfile.write(str('(Player ' + str(player) + ')\n')) # player name 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: if 'all' in self.settings or 'overworld' in self.settings:
# overworld tile flips # overworld tile flips
for player in range(1, self.world.players + 1): write_map('swaps', 'OW Tile Flips:')
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'])
# crossed groups # crossed groups
for player in range(1, self.world.players + 1): write_map('groups', 'OW Crossed Groups:')
if ('groups', player) in self.maps:
outfile.write('\n\nOW Crossed Groups:\n\n') # grid layout
break write_map('layout_grid_lw', 'Light World Layout:')
for player in range(1, self.world.players + 1): write_map('layout_grid_dw', 'Dark World Layout:')
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'])
if self.overworlds and ('all' in self.settings or 'overworld' in self.settings): if self.overworlds and ('all' in self.settings or 'overworld' in self.settings):
outfile.write('\n\nOverworld Edges:\n\n') 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 10: settings_version
# byte 11: OOOT WCCC (OWR layout, free terrain, whirlpools, OWR crossed) # byte 11: POOT WCCC (parallel, OWR layout, free terrain, whirlpools, OWR crossed)
or_mode = {"vanilla": 0, "parallel": 1, "full": 2} orlayout_mode = {"vanilla": 0, "grid": 1, "wild": 2}
orcrossed_mode = {"none": 0, "polar": 1, "grouped": 2, "unrestricted": 4} orcrossed_mode = {"none": 0, "polar": 1, "grouped": 2, "unrestricted": 4}
# byte 12: KMBQ FF?? (keep similar, mixed/tile flip, bonk drops, follower quests, flute spots) # byte 12: KMBQ FFO? (keep similar, mixed/tile flip, bonk drops, follower quests, flute spots, fog)
flutespot_mode = {"vanilla": 0, "balanced": 1, "random": 2} flutespot_mode = {"vanilla": 0, "balanced": 1, "random": 2}
# byte 13: FBBB TTPP (flute_mode, bow_mode, take_any, prize shuffle) # byte 13: FBBB TTPP (flute_mode, bow_mode, take_any, prize shuffle)
@@ -3871,12 +3871,12 @@ class Settings(object):
settings_version, settings_version,
(or_mode[w.owShuffle[p]] << 5) | (0x10 if w.owTerrain[p] else 0) (0x80 if w.owParallel[p] else 0) | (orlayout_mode[w.owLayout[p]] << 5) | (0x10 if w.owTerrain[p] else 0)
| (0x08 if w.owWhirlpoolShuffle[p] else 0) | orcrossed_mode[w.owCrossed[p]], | (0x08 if w.owWhirlpoolShuffle[p] else 0) | orcrossed_mode[w.owCrossed[p]],
(0x80 if w.owKeepSimilar[p] else 0) | (0x40 if w.owMixed[p] else 0) (0x80 if w.owKeepSimilar[p] else 0) | (0x40 if w.owMixed[p] else 0)
| (0x20 if w.shuffle_bonk_drops[p] else 0) | (0x10 if w.shuffle_followers[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 (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]]), | 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.algorithm = r(algo_mode)[(settings[9] & 0x38) >> 3]
args.shufflebosses[p] = r(boss_mode)[(settings[9] & 0x07)] args.shufflebosses[p] = r(boss_mode)[(settings[9] & 0x07)]
args.ow_shuffle[p] = r(or_mode)[(settings[11] & 0xE0) >> 5] args.ow_parallel[p] = True if settings[11] & 0x80 else False
args.ow_layout[p] = r(orlayout_mode)[(settings[11] & 0x60) >> 5]
args.ow_terrain[p] = True if settings[11] & 0x10 else False args.ow_terrain[p] = True if settings[11] & 0x10 else False
args.ow_whirlpool[p] = True if settings[11] & 0x08 else False args.ow_whirlpool[p] = True if settings[11] & 0x08 else False
args.ow_crossed[p] = r(orcrossed_mode)[(settings[11] & 0x07)] args.ow_crossed[p] = r(orcrossed_mode)[(settings[11] & 0x07)]
@@ -3963,6 +3964,7 @@ class Settings(object):
args.bonk_drops[p] = True if settings[12] & 0x20 else False args.bonk_drops[p] = True if settings[12] & 0x20 else False
args.shuffle_followers[p] = True if settings[12] & 0x10 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_fluteshuffle[p] = r(flutespot_mode)[(settings[12] & 0x0C) >> 2]
args.ow_fog[p] = True if settings[12] & 0x02 else False
if len(settings) > 13: if len(settings) > 13:
args.flute_mode[p] = r(flute_mode)[(settings[13] & 0x80) >> 7] args.flute_mode[p] = r(flute_mode)[(settings[13] & 0x80) >> 7]
+33
View File
@@ -1,5 +1,38 @@
# Changelog # 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 ## 0.6.1.7
- \~Merged in DR v1.5.2~ - \~Merged in DR v1.5.2~
- Reverted key count update - Reverted key count update
+54 -18
View File
@@ -120,6 +120,19 @@ def parse_cli(argv, no_defaults=False):
ret.take_any = 'random' if ret.take_any == 'none' else ret.take_any ret.take_any = 'random' if ret.take_any == 'none' else ret.take_any
ret.keyshuffle = 'universal' ret.keyshuffle = 'universal'
if ret.ow_unparallel:
ret.ow_parallel = False
if ret.ow_shuffle == 'parallel':
ret.ow_layout = 'wild'
ret.ow_parallel = True
elif ret.ow_shuffle == 'full':
ret.ow_layout = 'wild'
ret.ow_parallel = False
if ret.ow_no_fog:
ret.ow_fog = False
if player_num: if player_num:
defaults = copy.deepcopy(ret) defaults = copy.deepcopy(ret)
for player in range(1, player_num + 1): for player in range(1, player_num + 1):
@@ -130,23 +143,41 @@ def parse_cli(argv, no_defaults=False):
for k, v in playersettings.items(): for k, v in playersettings.items():
setattr(playerargs, k, v) setattr(playerargs, k, v)
for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', 'ow_shuffle', for name in ['logic', 'mode', 'swords', 'goal', 'difficulty',
'ow_terrain', 'ow_crossed', 'ow_keepsimilar', 'ow_mixed', 'ow_whirlpool', 'ow_fluteshuffle', 'item_functionality', 'ow_shuffle', 'ow_layout',
'flute_mode', 'bow_mode', 'take_any', 'boots_hint', 'shuffle_followers', 'ow_parallel', 'ow_terrain', 'ow_crossed',
'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'bosses_ganon', 'bosshunt_include_agas', 'ganon_item', 'openpyramid', 'ow_keepsimilar', 'ow_mixed', 'ow_whirlpool',
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'prizeshuffle', 'showloot', 'showmap', 'startinventory', 'ow_fluteshuffle', 'ow_fog', 'flute_mode', 'bow_mode',
'usestartinventory', 'bombbag', 'shuffleganon', 'overworld_map', 'restrict_boss_items', 'take_any', 'boots_hint', 'shuffle_followers',
'triforce_max_difference', 'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max', 'shuffle', 'door_shuffle', 'intensity',
'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'shuffletavern', 'crystals_ganon', 'crystals_gt', 'bosses_ganon',
'skullwoods', 'linked_drops', 'bosshunt_include_agas', 'ganon_item', 'openpyramid',
'pseudoboots', 'mirrorscroll', 'dark_rooms', 'damage_challenge', 'shuffle_damage_table', 'crystal_book', 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters', 'mapshuffle', 'compassshuffle', 'keyshuffle',
'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots', 'bigkeyshuffle', 'prizeshuffle', 'showloot', 'showmap',
'ow_palettes', 'uw_palettes', 'sprite', 'triforce_gfx', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'startinventory', 'usestartinventory', 'bombbag',
'heartbeep', 'remote_items', 'shopsanity', 'dropshuffle', 'pottery', 'keydropshuffle', 'shuffleganon', 'overworld_map', 'restrict_boss_items',
'mixed_travel', 'standardize_palettes', 'code', 'reduce_flashing', 'shuffle_sfx', 'shuffle_sfxinstruments', 'triforce_max_difference', 'triforce_pool_min',
'shuffle_songinstruments', 'msu_resume', 'collection_rate', 'colorizepots', 'decoupledoors', 'door_type_mode', 'triforce_pool_max', 'triforce_goal_min',
'bonk_drops', 'trap_door_mode', 'key_logic_algorithm', 'door_self_loops', 'any_enemy_logic', 'aga_randomness', 'triforce_goal_max', 'triforce_min_difference',
'money_balance']: '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) value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
if player == 1: if player == 1:
setattr(ret, name, {1: value}) setattr(ret, name, {1: value})
@@ -196,13 +227,18 @@ def parse_settings():
# Shuffle Ganon defaults to TRUE # Shuffle Ganon defaults to TRUE
"openpyramid": "auto", "openpyramid": "auto",
"shuffleganon": True, "shuffleganon": True,
"ow_shuffle": "vanilla", "ow_shuffle": "vanilla", # for backwards compatibility
"ow_layout": "vanilla",
"ow_parallel": True,
"ow_unparallel": False,
"ow_terrain": False, "ow_terrain": False,
"ow_crossed": "none", "ow_crossed": "none",
"ow_keepsimilar": False, "ow_keepsimilar": False,
"ow_mixed": False, "ow_mixed": False,
"ow_whirlpool": False, "ow_whirlpool": False,
"ow_fluteshuffle": "vanilla", "ow_fluteshuffle": "vanilla",
"ow_fog": True,
"ow_no_fog": False,
"shuffle_followers": False, "shuffle_followers": False,
"bonk_drops": False, "bonk_drops": False,
"shuffle": "vanilla", "shuffle": "vanilla",
+16 -6
View File
@@ -1030,7 +1030,10 @@ def balance_money_progression(world):
'Rupees (100)': 100, 'Rupees (300)': 300} 'Rupees (100)': 100, 'Rupees (300)': 300}
rupee_rooms = {'Eastern Rupees': 90, 'Mire Key Rupees': 45, 'Mire Shooter Rupees': 90, rupee_rooms = {'Eastern Rupees': 90, 'Mire Key Rupees': 45, 'Mire Shooter Rupees': 90,
'TR Rupees': 270, 'PoD Dark Basement': 270} '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()) base_value = sum(rupee_rooms.values())
available_money = {player: base_value for player in range(1, world.players+1)} 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)] 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): def interesting_item(location, item, world, player):
if location.event or location.locked:
return True
if item.advancement: if item.advancement:
return True return True
if item.type is not None or item.name.startswith('Rupee'): if item.type is not None or item.name.startswith('Rupee'):
@@ -1129,10 +1134,11 @@ def balance_money_progression(world):
unchecked_locations.remove(location) unchecked_locations.remove(location)
if location.item: if location.item:
if location.item.name.startswith('Rupee'): if location.item.name.startswith('Rupee'):
wallet[location.item.player] += rupee_chart[location.item.name] if not (location.item.name == 'Rupee (1)' and world.algorithm != 'district'):
if location.item.name != 'Rupees (300)': wallet[location.item.player] += rupee_chart[location.item.name]
balance_locations[location.item.player].add(location) if location.item.name != 'Rupees (300)':
if interesting_item(location, location.item, world, location.item.player): balance_locations[location.item.player].add(location)
elif interesting_item(location, location.item, world, location.item.player):
checked_locations.append(location) checked_locations.append(location)
elif location.item.name in acceptable_balancers: elif location.item.name in acceptable_balancers:
balance_locations[location.item.player].add(location) balance_locations[location.item.player].add(location)
@@ -1176,7 +1182,11 @@ def balance_money_progression(world):
if len(increase_targets) == 0: 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] 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: 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) 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 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) old_value = 0 if make_item_free else (rupee_chart[best_target.item.name] if best_target.item.name in rupee_chart else 0)
+32 -13
View File
@@ -1727,6 +1727,7 @@ def shuffle_event_items(world, player):
available_quests = follower_quests.copy() available_quests = follower_quests.copy()
available_pickups = [quests[0] for quests in available_quests.values()] available_pickups = [quests[0] for quests in available_quests.values()]
# finalize customizer followers first
for loc_name in follower_quests.keys(): for loc_name in follower_quests.keys():
loc = world.get_location(loc_name, player) loc = world.get_location(loc_name, player)
if loc.item: if loc.item:
@@ -1734,7 +1735,6 @@ def shuffle_event_items(world, player):
available_quests.pop(loc_name) available_quests.pop(loc_name)
available_pickups.remove(loc.item.name) available_pickups.remove(loc.item.name)
if world.mode[player] == 'standard' and 'Zelda Herself' in available_pickups: if world.mode[player] == 'standard' and 'Zelda Herself' in available_pickups:
zelda_dropoff = 'Zelda Pickup' zelda_dropoff = 'Zelda Pickup'
if world.default_zelda_region[player] == 'Thieves Blind\'s Cell': 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) available_pickups.remove(zelda_pickup)
set_event_item(world, player, zelda_dropoff, 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'} attempts = 10
for pickup in restricted_pickups: for attempt in range(attempts):
restricted_quests = [q for q in available_quests.keys() if q not in restricted_pickups[pickup]] try:
random.shuffle(restricted_quests) all_state = world.get_all_state(keys=True)
quest = restricted_quests.pop() if world.prizeshuffle[player] != 'wild':
available_quests.pop(quest) from Items import prize_item_table
available_pickups.remove(pickup) prizes = ItemFactory(list(prize_item_table.keys()), player)
set_event_item(world, player, quest, pickup) for prize in prizes:
all_state.collect(prize, True)
for pickup in available_pickups: # randomize the follower pickups, but ensure that the last items are the unrestrictive ones
quest, _ = available_quests.popitem() unrestrictive_pickups = ItemFactory([p for p in ['Zelda Herself', 'Sign Vandalized'] if p in available_pickups], player)
set_event_item(world, player, quest, pickup) 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): def get_item_and_event_flag(item, world, player, dungeon_pool, prize_set, prize_pool):
+6 -3
View File
@@ -503,7 +503,7 @@ def init_world(args, fish):
customized.load_yaml(args.customizer) customized.load_yaml(args.customizer)
customized.adjust_args(args, False) customized.adjust_args(args, False)
world = World(args.multi, args.ow_shuffle, args.ow_crossed, args.ow_mixed, args.shuffle, args.door_shuffle, args.logic, args.mode, args.swords, world = World(args.multi, args.ow_layout, args.ow_parallel, args.ow_crossed, args.ow_mixed, args.shuffle, args.door_shuffle, args.logic, args.mode, args.swords,
args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm, args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm,
args.accessibility, args.shuffleganon, args.custom, args.customitemarray, args.hints, args.spoiler) args.accessibility, args.shuffleganon, args.custom, args.customitemarray, args.hints, args.spoiler)
@@ -529,6 +529,7 @@ def init_world(args, fish):
world.owKeepSimilar = args.ow_keepsimilar.copy() world.owKeepSimilar = args.ow_keepsimilar.copy()
world.owWhirlpoolShuffle = args.ow_whirlpool.copy() world.owWhirlpoolShuffle = args.ow_whirlpool.copy()
world.owFluteShuffle = args.ow_fluteshuffle.copy() world.owFluteShuffle = args.ow_fluteshuffle.copy()
world.owFog = args.ow_fog.copy()
world.shuffle_followers = args.shuffle_followers.copy() world.shuffle_followers = args.shuffle_followers.copy()
world.shuffle_bonk_drops = args.bonk_drops.copy() world.shuffle_bonk_drops = args.bonk_drops.copy()
world.open_pyramid = args.openpyramid.copy() world.open_pyramid = args.openpyramid.copy()
@@ -806,7 +807,7 @@ def set_starting_inventory(world, args):
def copy_world(world): def copy_world(world):
# ToDo: Not good yet # ToDo: Not good yet
ret = World(world.players, world.owShuffle, world.owCrossed, world.owMixed, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords, ret = World(world.players, world.owLayout, world.owParallel, world.owCrossed, world.owMixed, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords,
world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm,
world.accessibility, world.shuffle_ganon, world.custom, world.customitemarray, world.hints, world.spoiler_mode) world.accessibility, world.shuffle_ganon, world.custom, world.customitemarray, world.hints, world.spoiler_mode)
ret.teams = world.teams ret.teams = world.teams
@@ -852,6 +853,7 @@ def copy_world(world):
ret.owKeepSimilar = world.owKeepSimilar.copy() ret.owKeepSimilar = world.owKeepSimilar.copy()
ret.owWhirlpoolShuffle = world.owWhirlpoolShuffle.copy() ret.owWhirlpoolShuffle = world.owWhirlpoolShuffle.copy()
ret.owFluteShuffle = world.owFluteShuffle.copy() ret.owFluteShuffle = world.owFluteShuffle.copy()
ret.owFog = world.owFog.copy()
ret.shuffle_followers = world.shuffle_followers.copy() ret.shuffle_followers = world.shuffle_followers.copy()
ret.shuffle_bonk_drops = world.shuffle_bonk_drops.copy() ret.shuffle_bonk_drops = world.shuffle_bonk_drops.copy()
ret.open_pyramid = world.open_pyramid.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): def copy_world_premature(world, player, create_flute_exits=True):
# ToDo: Not good yet # ToDo: Not good yet
ret = World(world.players, world.owShuffle, world.owCrossed, world.owMixed, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords, ret = World(world.players, world.owLayout, world.owParallel, world.owCrossed, world.owMixed, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords,
world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm,
world.accessibility, world.shuffle_ganon, world.custom, world.customitemarray, world.hints, world.spoiler_mode) world.accessibility, world.shuffle_ganon, world.custom, world.customitemarray, world.hints, world.spoiler_mode)
ret.teams = world.teams ret.teams = world.teams
@@ -1083,6 +1085,7 @@ def copy_world_premature(world, player, create_flute_exits=True):
ret.owKeepSimilar = world.owKeepSimilar.copy() ret.owKeepSimilar = world.owKeepSimilar.copy()
ret.owWhirlpoolShuffle = world.owWhirlpoolShuffle.copy() ret.owWhirlpoolShuffle = world.owWhirlpoolShuffle.copy()
ret.owFluteShuffle = world.owFluteShuffle.copy() ret.owFluteShuffle = world.owFluteShuffle.copy()
ret.owFog = world.owFog.copy()
ret.shuffle_followers = world.shuffle_followers.copy() ret.shuffle_followers = world.shuffle_followers.copy()
ret.shuffle_bonk_drops = world.shuffle_bonk_drops.copy() ret.shuffle_bonk_drops = world.shuffle_bonk_drops.copy()
ret.open_pyramid = world.open_pyramid.copy() ret.open_pyramid = world.open_pyramid.copy()
+8
View File
@@ -326,12 +326,20 @@ def create_owedges(world, player):
world.owedges += edges world.owedges += edges
world.initialize_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): def create_owedge(player, name, owIndex, direction, terrain, edge_id, owSlotIndex=0xff):
if name not in OWExitTypes['OWEdge']: if name not in OWExitTypes['OWEdge']:
OWExitTypes['OWEdge'].append(name) OWExitTypes['OWEdge'].append(name)
return OWEdge(player, name, owIndex, direction, terrain, edge_id, owSlotIndex) 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 = { OWEdgeGroups = {
#(IsStandard, World, EdgeAxis, Terrain, HasParallel, NumberInGroup, CustomizerGroup) #(IsStandard, World, EdgeAxis, Terrain, HasParallel, NumberInGroup, CustomizerGroup)
+178 -85
View File
@@ -127,37 +127,38 @@ def link_overworld(world, player):
# restructure Maze Race/Suburb/Frog/Dig Game manually due to NP/P relationship # 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) parallel_links_new = bidict(parallel_links) # shallow copy is enough (deep copy is broken)
if world.owKeepSimilar[player]: if world.owLayout[player] != 'grid':
del parallel_links_new['Maze Race ES'] if world.owKeepSimilar[player]:
del parallel_links_new['Kakariko Suburb WS'] del parallel_links_new['Maze Race ES']
for group in trimmed_groups.keys(): del parallel_links_new['Kakariko Suburb WS']
(std, region, axis, terrain, parallel, _, custom) = group for group in trimmed_groups.keys():
if parallel == IsParallel.Yes: (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] (forward_edges, back_edges) = trimmed_groups[group]
if ['Maze Race ES'] in forward_edges: if ['Dig Game EC', 'Dig Game ES'] in forward_edges:
forward_edges.remove(['Maze Race ES']) forward_edges.remove(['Dig Game EC', 'Dig Game ES'])
trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][0].append(['Maze Race ES']) trimmed_groups[(std, region, axis, terrain, IsParallel.Yes, 1, custom)][0].append(['Dig Game ES'])
if ['Kakariko Suburb WS'] in back_edges: trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][0].append(['Dig Game EC'])
back_edges.remove(['Kakariko Suburb WS']) if ['Frog WC', 'Frog WS'] in back_edges:
trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][1].append(['Kakariko Suburb WS']) 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) 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()})} parallel_links_new = {**dict(parallel_links_new), **dict({e:p[0] for e, p in parallel_links_new.inverse.items()})}
connected_edges = [] connected_edges = []
if world.owShuffle[player] != 'vanilla': if world.owLayout[player] != 'vanilla':
trimmed_groups = remove_reserved(world, trimmed_groups, connected_edges, player) trimmed_groups = remove_reserved(world, trimmed_groups, connected_edges, player)
trimmed_groups = reorganize_groups(world, trimmed_groups, player) trimmed_groups = reorganize_groups(world, trimmed_groups, player)
@@ -245,7 +246,7 @@ def link_overworld(world, player):
if 'undefined_chance' in custom_crossed: if 'undefined_chance' in custom_crossed:
undefined_chance = custom_crossed['undefined_chance'] 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 # connect forced crossed non-parallel edges based on previously determined tile flips
for edge in swapped_edges: for edge in swapped_edges:
if edge not in parallel_links_new: if edge not in parallel_links_new:
@@ -277,10 +278,10 @@ def link_overworld(world, player):
s[0x30], s[0x35], s[0x30], s[0x35],
s[0x41], s[0x3a],s[0x3b],s[0x3c], s[0x3f]) s[0x41], s[0x3a],s[0x3b],s[0x3c], s[0x3f])
world.spoiler.set_map('groups', text_output, ow_crossed_tiles, player) world.spoiler.set_map('groups', text_output, ow_crossed_tiles, player)
elif limited_crossed > -1 or (world.owShuffle[player] == 'vanilla' and world.owCrossed[player] == 'unrestricted'): elif (limited_crossed > -1 and world.owLayout[player] != 'grid') or (world.owLayout[player] == 'vanilla' and world.owCrossed[player] == 'unrestricted'):
crossed_candidates = list() crossed_candidates = list()
for group in trimmed_groups.keys(): for group in trimmed_groups.keys():
(mode, wrld, dir, terrain, parallel, count, _) = group (mode, wrld, _, terrain, parallel, _, _) = group
if wrld == WorldType.Light and mode != OpenStd.Standard: if wrld == WorldType.Light and mode != OpenStd.Standard:
for (forward_set, back_set) in zip(trimmed_groups[group][0], trimmed_groups[group][1]): for (forward_set, back_set) in zip(trimmed_groups[group][0], trimmed_groups[group][1]):
if forward_set[0] in parallel_links_new: if forward_set[0] in parallel_links_new:
@@ -291,7 +292,7 @@ def link_overworld(world, player):
combine_set = forward_combine+back_combine combine_set = forward_combine+back_combine
skip_forward = False skip_forward = False
if world.owShuffle[player] == 'vanilla': if world.owLayout[player] == 'vanilla':
if any(edge in force_crossed for edge in combine_set): if any(edge in force_crossed for edge in combine_set):
if not any(edge in force_noncrossed for edge in combine_set): if not any(edge in force_noncrossed for edge in combine_set):
if any(edge in force_crossed for edge in forward_combine): if any(edge in force_crossed for edge in forward_combine):
@@ -425,7 +426,7 @@ def link_overworld(world, player):
# layout shuffle # layout shuffle
logging.getLogger('').debug('Shuffling overworld layout') logging.getLogger('').debug('Shuffling overworld layout')
if world.owShuffle[player] == 'vanilla': if world.owLayout[player] == 'vanilla':
# apply outstanding flips # apply outstanding flips
trimmed_groups = performSwap(trimmed_groups, edges_to_swap) trimmed_groups = performSwap(trimmed_groups, edges_to_swap)
assert len(edges_to_swap) == 0, 'Not all edges were flipped successfully: ' + ', '.join(edges_to_swap) assert len(edges_to_swap) == 0, 'Not all edges were flipped successfully: ' + ', '.join(edges_to_swap)
@@ -438,8 +439,15 @@ def link_overworld(world, player):
assert len(forward_set) == len(back_set) assert len(forward_set) == len(back_set)
for (forward_edge, back_edge) in zip(forward_set, back_set): for (forward_edge, back_edge) in zip(forward_set, back_set):
connect_two_way(world, forward_edge, back_edge, player, connected_edges) connect_two_way(world, forward_edge, back_edge, player, connected_edges)
elif world.owLayout[player] == 'grid':
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: else:
if world.owKeepSimilar[player] and world.owShuffle[player] == 'parallel': if world.owKeepSimilar[player] and world.owParallel[player]:
for exitname, destname in parallelsimilar_connections: for exitname, destname in parallelsimilar_connections:
connect_two_way(world, exitname, destname, player, connected_edges) connect_two_way(world, exitname, destname, player, connected_edges)
@@ -589,7 +597,10 @@ def link_overworld(world, player):
connect_simple(world, 'Flute Spot ' + str(o + 1), regions[1], player) connect_simple(world, 'Flute Spot ' + str(o + 1), regions[1], player)
if world.owFluteShuffle[player] == 'vanilla': 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: else:
flute_spots = 8 flute_spots = 8
flute_pool = list(flute_data.keys()) flute_pool = list(flute_data.keys())
@@ -716,7 +727,7 @@ def link_overworld(world, player):
flute_spots -= spots_to_place flute_spots -= spots_to_place
# connect new flute spots # connect new flute spots
new_spots.sort() sort_flute_spots(world, player, new_spots)
world.owflutespots[player] = new_spots world.owflutespots[player] = new_spots
connect_flutes(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) remove_pair_from_pool(edge1.name, edge2.name, is_crossed)
connect_two_way(world, edge1.name, edge2.name, player, connected_edges) connect_two_way(world, edge1.name, edge2.name, player, connected_edges)
# resolve parallel # resolve parallel
if world.owShuffle[player] == 'parallel' and edge1.name in parallel_links_new: if world.owParallel[player] and edge1.name in parallel_links_new:
parallel_forward_edge = parallel_links_new[edge1.name] parallel_forward_edge = parallel_links_new[edge1.name]
parallel_back_edge = parallel_links_new[edge2.name] parallel_back_edge = parallel_links_new[edge2.name]
if validate_crossed_allowed(parallel_forward_edge, parallel_back_edge, is_crossed): if validate_crossed_allowed(parallel_forward_edge, parallel_back_edge, is_crossed):
@@ -851,13 +862,13 @@ def connect_custom(world, connected_edges, groups, forced, player):
connect_two_way(world, forward_edge, back_edge, player, connected_edges) connect_two_way(world, forward_edge, back_edge, player, connected_edges)
else: else:
raise GenerationException('Violation of force crossed rules on unresolved similars: \'%s\' <-> \'%s\'', forward_edge, back_edge) raise GenerationException('Violation of force crossed rules on unresolved similars: \'%s\' <-> \'%s\'', forward_edge, back_edge)
if world.owShuffle[player] == 'parallel' and forward_edge in parallel_links_new: if world.owParallel[player] and forward_edge in parallel_links_new:
parallel_forward_edge = parallel_links_new[forward_edge] parallel_forward_edge = parallel_links_new[forward_edge]
parallel_back_edge = parallel_links_new[back_edge] parallel_back_edge = parallel_links_new[back_edge]
if not validate_crossed_allowed(parallel_forward_edge, parallel_back_edge, is_crossed): if not validate_crossed_allowed(parallel_forward_edge, parallel_back_edge, is_crossed):
raise GenerationException('Violation of force crossed rules on parallel unresolved similars: \'%s\' <-> \'%s\'', forward_edge, back_edge) 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) edge1 = world.get_entrance(edgename1, player)
edge2 = world.get_entrance(edgename2, player) edge2 = world.get_entrance(edgename2, player)
x = world.get_owedge(edgename1, 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 x.dest = y
y.dest = x 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) world.spoiler.set_overworld(edgename2, edgename1, 'both', player)
if connected_edges is not None: 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) connected_edges.append(edgename2)
# connecting parallel connections # connecting parallel connections
if world.owShuffle[player] in ['vanilla', 'parallel']: if world.owLayout[player] == 'vanilla' or world.owParallel[player]:
if edgename1 in parallel_links_new: if edgename1 in parallel_links_new:
try: try:
parallel_forward_edge = parallel_links_new[edgename1] parallel_forward_edge = parallel_links_new[edgename1]
@@ -978,7 +989,7 @@ def determine_forced_flips(world, tile_ow_groups, do_grouped, player):
for whirl1, whirl2 in custom_whirlpools.items(): for whirl1, whirl2 in custom_whirlpools.items():
if [whirlpool_map[whirl1], whirlpool_map[whirl2]] not in merged_owids and should_merge_group(whirlpool_map[whirl1], whirlpool_map[whirl2]): if [whirlpool_map[whirl1], whirlpool_map[whirl2]] not in merged_owids and should_merge_group(whirlpool_map[whirl1], whirlpool_map[whirl2]):
merged_owids.append([whirlpool_map[whirl1], whirlpool_map[whirl2]]) merged_owids.append([whirlpool_map[whirl1], whirlpool_map[whirl2]])
if world.owShuffle[player] != 'vanilla': if world.owLayout[player] != 'vanilla':
custom_edges = world.customizer.get_owedges() custom_edges = world.customizer.get_owedges()
if custom_edges and player in custom_edges: if custom_edges and player in custom_edges:
custom_edges = custom_edges[player] custom_edges = custom_edges[player]
@@ -1084,6 +1095,9 @@ def shuffle_tiles(world, groups, result_list, do_grouped, forced_flips, player):
exist_dw_regions.extend(dw_regions) exist_dw_regions.extend(dw_regions)
parity = [sum(group_parity[group[0][0]][i] for group in groups if group not in removed) for i in range(6)] parity = [sum(group_parity[group[0][0]][i] for group in groups if group not in removed) for i in range(6)]
if world.owLayout[player] == 'grid':
parity[1] = 0
parity[2] = 0
if not world.owKeepSimilar[player]: if not world.owKeepSimilar[player]:
parity[1] += 2*parity[2] parity[1] += 2*parity[2]
parity[2] = 0 parity[2] = 0
@@ -1177,12 +1191,16 @@ def define_tile_groups(world, do_grouped, player):
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'simple', 'restricted', 'district']: if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'simple', 'restricted', 'district']:
merge_groups([[0x05, 0x07]]) merge_groups([[0x05, 0x07]])
# all non-parallel screens # special screens
if world.owShuffle[player] == 'vanilla' and (world.owCrossed[player] == 'none' or do_grouped): if world.owLayout[player] != 'wild' and (world.owCrossed[player] == 'none' or do_grouped):
merge_groups([[0x00, 0x2d, 0x80], [0x0f, 0x81], [0x1a, 0x1b], [0x28, 0x29], [0x30, 0x3a]]) merge_groups([[0x00, 0x2d, 0x80], [0x0f, 0x81]])
# remaining non-parallel edges
if world.owLayout[player] == 'vanilla' and (world.owCrossed[player] == 'none' or do_grouped):
merge_groups([[0x1a, 0x1b], [0x28, 0x29], [0x30, 0x3a]])
# special case: non-parallel keep similar # special case: non-parallel keep similar
if world.owShuffle[player] == 'parallel' and world.owKeepSimilar[player] and (world.owCrossed[player] == 'none' or do_grouped): if world.owLayout[player] == 'wild' and world.owParallel[player] and world.owKeepSimilar[player] and (world.owCrossed[player] == 'none' or do_grouped):
merge_groups([[0x28, 0x29]]) merge_groups([[0x28, 0x29]])
# whirlpool screens # whirlpool screens
@@ -1238,7 +1256,7 @@ def reorganize_groups(world, groups, player):
new_group[0] = None new_group[0] = None
if world.owTerrain[player]: if world.owTerrain[player]:
new_group[3] = None new_group[3] = None
if world.owShuffle[player] != 'parallel': if not world.owParallel[player]:
new_group[4] = None new_group[4] = None
if not world.owKeepSimilar[player]: if not world.owKeepSimilar[player]:
new_group[5] = None new_group[5] = None
@@ -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) groups[(mode, wrld, dir, terrain, parallel, count, group_name)][i].extend(matches)
return groups 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): 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) 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: if not flute_in_pool:
@@ -2347,6 +2374,11 @@ parallelsimilar_connections = [('Maze Race ES', 'Kakariko Suburb WS'),
('Dig Game ES', 'Frog 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 # non shuffled overworld
default_connections = [('Lost Woods NW', 'Master Sword Meadow SC'), default_connections = [('Lost Woods NW', 'Master Sword Meadow SC'),
('Lost Woods SW', 'Lost Woods Pass NW'), ('Lost Woods SW', 'Lost Woods Pass NW'),
@@ -2560,46 +2592,107 @@ isolated_regions = [
flute_data = { 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 #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), 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), 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, 0x035b, 0x0680, 0x0118, 0x0860, 0x05c0, 0x00b8, 0x07ec, 0x0127, 0x086b, 0xfff8, 0x0004, 0x0148, 0x0850), 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, 0x0388, 0x0da8), 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, 0x0160, 0x0f20), 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, 0x0280, 0x0488), 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, 0x02d0, 0x0f38), 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, 0x0448, 0x0058), 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, 0x0588, 0x0318), 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, 0x0578, 0x04f8), 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, 0x0508, 0x0758), 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, 0x0580, 0x0928, 0x0580, 0x0948), 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, 0x04f8, 0x0b60), 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, 0x0598, 0x0ccf), 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, 0x04e0, 0x0f68), 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, 0x07c0, 0x0210, 0x07c8, 0x01f8), 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, 0x0548), 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, 0x07d8, 0x07f8), 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, 0x0720, 0x0a80), 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, 0x09c0, 0x0c80), 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, 0x0908, 0x04d8), 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, 0x08b8, 0x0b08), 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, 0x0b88, 0x00b8), 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, 0x0ae0, 0x02c0), 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, 0x0b10, 0x04e8), 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, 0x0b30, 0x0770), 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, 0x0b20, 0x08b8), 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, 0x0bc4, 0x0ad0), 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, 0x0a78, 0x0c58), 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, 0x0b50, 0x0f30), 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, 0x0fb0, 0x0070), 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, 0x0cd6, 0x0568), 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, 0x0c80, 0x0628), 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, 0x0d60, 0x08d8), 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, 0x0d88, 0x0a88), #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, 0x0f90, 0x0da4), 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, 0x0d48, 0x0ed0), 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, 0x0e70, 0x0540), 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, 0x0f30, 0x0770), 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, 0x0f38, 0x0898), 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, 0x0f80, 0x0ef0) 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 = \ tile_swap_spoiler_table = \
+3 -3
View File
@@ -35,7 +35,7 @@ def main(args):
start_time = time.process_time() start_time = time.process_time()
# initialize the world # initialize the world
world = World(1, 'vanilla', 'vanilla', 'vanilla', 'vanilla', 'noglitches', 'standard', 'normal', 'none', 'on', 'ganon', 'freshness', False, False, False, False, False, False, None, False) world = World(1, 'vanilla', True, 'vanilla', 'vanilla', 'vanilla', 'noglitches', 'standard', 'normal', 'none', 'on', 'ganon', 'freshness', False, False, False, False, False, False, None, False)
world.player_names[1].append("Player 1") world.player_names[1].append("Player 1")
logger = logging.getLogger('') logger = logging.getLogger('')
@@ -168,9 +168,9 @@ def prefill_world(world, plando, text_patches):
elif line.startswith('!goal'): elif line.startswith('!goal'):
_, goalstr = line.split(':', 1) _, goalstr = line.split(':', 1)
world.goal = {1: goalstr.strip()} world.goal = {1: goalstr.strip()}
elif line.startswith('!owShuffle'): elif line.startswith('!owLayout'):
_, modestr = line.split(':', 1) _, modestr = line.split(':', 1)
world.owShuffle = {1: modestr.strip()} world.owLayout = {1: modestr.strip()}
elif line.startswith('!owCrossed'): elif line.startswith('!owCrossed'):
_, modestr = line.split(':', 1) _, modestr = line.split(':', 1)
world.owCrossed = {1: modestr.strip()} world.owCrossed = {1: modestr.strip()}
+1 -1
View File
@@ -244,7 +244,7 @@ Ganons Tower - Validation Chest: Nothing
Ganon: Triforce Ganon: Triforce
# set Overworld connections (lines starting with $, separate edges with =) # set Overworld connections (lines starting with $, separate edges with =)
!owShuffle: parallel !owLayout: wild
#!owMixed: true # Mixed OW not supported yet #!owMixed: true # Mixed OW not supported yet
!owCrossed: none !owCrossed: none
!owKeepSimilar: true !owKeepSimilar: true
+22 -6
View File
@@ -132,20 +132,24 @@ Note: These changes do impact the logic. If you use `CodeTracker`, these Inverte
Only settings specifically added by this Overworld Shuffle fork are found here. All door and entrance randomizer settings are supported. See their [readme](https://github.com/Aerinon/ALttPDoorRandomizer/blob/master/README.md) Only settings specifically added by this Overworld Shuffle fork are found here. All door and entrance randomizer settings are supported. See their [readme](https://github.com/Aerinon/ALttPDoorRandomizer/blob/master/README.md)
## Overworld Layout Shuffle (--ow_shuffle) ## Overworld Layout Shuffle (--ow_layout)
OW Edge Transitions are shuffled to create new world layouts. A brief visual representation of this can be viewed [here](https://zelda.codemann8.com/images/shared/ow-modes.gif). (This graphic also includes combinations of Crossed and Tile Flip) OW Edge Transitions are shuffled to create new world layouts. A brief visual representation of this can be viewed [here](https://zelda.codemann8.com/images/shared/ow-modes.gif). (This graphic also includes combinations of Crossed and Tile Flip)
### Vanilla ### Vanilla
OW Transitions are not shuffled. OW Transitions are not shuffled.
### Parallel ### Grid
OW Transitions are shuffled, but both worlds will have a matching layout, similar to that of vanilla. OW Screens are shuffled in such a way that they are still arranged on an 8x8 grid for each world like vanilla, and the OW Transitions are based on that arrangement.
### Full ### Wild
OW Transitions are shuffled within each world separately. OW Transitions are shuffled with no respect to geometric coherence.
## Parallel (--ow_unparallel to disable)
With OW Layout Shuffle, this forces both worlds to have a matching layout.
## Free Terrain (--ow_terrain) ## Free Terrain (--ow_terrain)
@@ -389,11 +393,17 @@ Districts are a concept originally conceived by Aerinon in the Door Randomizer,
Show the help message and exit. Show the help message and exit.
``` ```
--ow_shuffle <mode> --ow_layout <mode>
``` ```
For specifying the overworld layout shuffle you want as above. (default: vanilla) For specifying the overworld layout shuffle you want as above. (default: vanilla)
```
--ow_unparallel
```
With OW Layout Shuffle, this no longer forces both worlds to have a matching layout.
``` ```
--ow_terrain --ow_terrain
``` ```
@@ -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 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 --shuffle_followers
``` ```
+123 -72
View File
@@ -84,7 +84,7 @@ from Utils import int16_as_bytes, int32_as_bytes, local_path, snes_to_pc
from Versions import DRVersion, GKVersion, ORVersion from Versions import DRVersion, GKVersion, ORVersion
JAP10HASH = '03a63945398191337e896e5771f77173' JAP10HASH = '03a63945398191337e896e5771f77173'
RANDOMIZERBASEHASH = '2647cc28bca3675152576dd1f5ea0bab' RANDOMIZERBASEHASH = '33cd5e308266cf2273c80de1b1df3dac'
class JsonRom(object): class JsonRom(object):
@@ -530,14 +530,15 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
# patch flute spots # patch flute spots
owFlags = 0 owFlags = 0
if world.owFluteShuffle[player] == 'vanilla': owFog = 0
if world.owFluteShuffle[player] == 'vanilla' and world.owLayout[player] != 'grid':
flute_spots = default_flute_connections flute_spots = default_flute_connections
else: else:
flute_spots = world.owflutespots[player] flute_spots = world.owflutespots[player]
owFlags |= 0x0100 owFlags |= 0x0100
write_int16(rom, snes_to_pc(0x0AB7F7), 0xEAEA) 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)): for o in range(0, len(flute_writes)):
owid = flute_writes[o][0] owid = flute_writes[o][0]
offset = 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(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 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: base_index = 0
write_int16(rom, snes_to_pc(0x02E86B + (o * 2)), data[2]) # vram if not (offset == 0 or len(data) <= 15):
write_int16(rom, snes_to_pc(0x02E88D + (o * 2)), data[3]) # BG scroll Y base_index = 13 # use alternate flute data
write_int16(rom, snes_to_pc(0x02E8AF + (o * 2)), data[4]) # BG scroll X write_int16(rom, snes_to_pc(0x02E86B + (o * 2)), data[base_index + 2]) # vram
write_int16(rom, snes_to_pc(0x02E915 + (o * 2)), data[7]) # cam Y write_int16(rom, snes_to_pc(0x02E88D + (o * 2)), data[base_index + 3]) # BG scroll Y
write_int16(rom, snes_to_pc(0x02E937 + (o * 2)), data[8]) # cam X write_int16(rom, snes_to_pc(0x02E8AF + (o * 2)), data[base_index + 4]) # BG scroll X
write_int16(rom, snes_to_pc(0x02E959 + (o * 2)), data[9]) # unknown 1 if base_index > 0:
write_int16(rom, snes_to_pc(0x02E97B + (o * 2)), data[10]) # unknown 2 base_index -= 2
rom.write_byte(snes_to_pc(0x0AB783 + o), data[12] & 0xff) # flute menu blip - X low byte write_int16(rom, snes_to_pc(0x02E915 + (o * 2)), data[base_index + 7]) # cam Y
rom.write_byte(snes_to_pc(0x0AB78B + o), data[12] // 0x100) # flute menu blip - X high byte write_int16(rom, snes_to_pc(0x02E937 + (o * 2)), data[base_index + 8]) # cam X
rom.write_byte(snes_to_pc(0x0AB793 + o), data[11] & 0xff) # flute menu blip - Y low byte write_int16(rom, snes_to_pc(0x02E959 + (o * 2)), data[base_index + 9]) # unknown 1
rom.write_byte(snes_to_pc(0x0AB79B + o), data[11] // 0x100) # flute menu blip - Y high byte write_int16(rom, snes_to_pc(0x02E97B + (o * 2)), data[base_index + 10]) # unknown 2
else: # use alternate flute data map_x, map_y = adjust_ow_coordinates_to_layout(world, player, data[base_index + 12], data[base_index + 11], world.mode[player] == 'inverted')
write_int16(rom, snes_to_pc(0x02E86B + (o * 2)), data[15]) # vram rom.write_byte(snes_to_pc(0x0AB783 + o), map_x & 0xff) # flute menu blip - X low byte
write_int16(rom, snes_to_pc(0x02E88D + (o * 2)), data[16]) # BG scroll Y rom.write_byte(snes_to_pc(0x0AB78B + o), map_x // 0x100) # flute menu blip - X high byte
write_int16(rom, snes_to_pc(0x02E8AF + (o * 2)), data[17]) # BG scroll X rom.write_byte(snes_to_pc(0x0AB793 + o), map_y & 0xff) # flute menu blip - Y low byte
write_int16(rom, snes_to_pc(0x02E915 + (o * 2)), data[18]) # cam Y rom.write_byte(snes_to_pc(0x0AB79B + o), map_y // 0x100) # flute menu blip - Y high byte
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
# patch whirlpools # patch whirlpools
if world.owWhirlpoolShuffle[player]: if world.owWhirlpoolShuffle[player]:
owFlags |= 0x01 owFlags |= 0x01
write_int16s(rom, snes_to_pc(0x02EA5C), world.owwhirlpools[player]) 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 # patch overworld edges
inverted_buffer = [0] * 0x82 inverted_buffer = [0] * 0x82
owMode = 0 owMode = 0
if world.owShuffle[player] != 'vanilla' or world.owCrossed[player] not in ['none', 'polar'] or world.owMixed[player]: if world.owLayout[player] != 'vanilla' or world.owCrossed[player] not in ['none', 'polar'] or world.owMixed[player]:
if world.owShuffle[player] == 'parallel': if world.owLayout[player] != 'vanilla':
owMode = 1 owMode = 1 if world.owParallel[player] else 2
elif world.owShuffle[player] == 'full': if world.owKeepSimilar[player] and (world.owLayout[player] != 'vanilla' or world.owCrossed[player] == 'unrestricted'):
owMode = 2
if world.owKeepSimilar[player] and (world.owShuffle[player] != 'vanilla' or world.owCrossed[player] == 'unrestricted'):
owMode |= 0x0100 owMode |= 0x0100
if world.owCrossed[player] != 'none' and (world.owCrossed[player] != 'polar' or world.owMixed[player]): if world.owCrossed[player] != 'none' and (world.owCrossed[player] != 'polar' or world.owMixed[player]):
owMode |= 0x0200 owMode |= 0x0200
@@ -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) rom.write_byte(0x1539B0 + b + 9, world_flag)
for edge in world.owedges: 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) write_int16(rom, edge.getAddress() + 0x0a, edge.vramLoc)
if not edge.specialExit: 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 # patch bonk prizes
if world.shuffle_bonk_drops[player]: 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, 0x150002, owMode)
write_int16(rom, 0x150004, owFlags) write_int16(rom, 0x150004, owFlags)
write_int16(rom, 0x150008, owFog if world.owFog[player] else 0x00)
# patch entrance/exits/holes # patch entrance/exits/holes
for region in world.regions: for region in world.regions:
@@ -1317,8 +1338,8 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
else: else:
goal_bytes += int16_as_bytes(req['target']) goal_bytes += int16_as_bytes(req['target'])
elif req['condition'] & 0x80 == 0: elif req['condition'] & 0x80 == 0:
if req['condition'] & 0x7F == 0x06 or req['condition'] & 0x7F == 0x07: if req['condition'] & 0x7F in [0x00, 0x06, 0x07]:
# agahnims have no target value # no target value
pass pass
elif req['condition'] & 0x7F < 0x08: elif req['condition'] & 0x7F < 0x08:
goal_bytes += [req['target']] 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) | (0x04 if world.mapshuffle[player] != 'none' else 0x00)
| (0x08 if world.bigkeyshuffle[player] != 'none' else 0x00))) # free roaming item text boxes | (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 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' map_hud_mode = 0x00
or world.dropshuffle[player] != 'none' or world.pottery[player] not in ['none', 'cave']): if world.dungeon_counters[player] == 'on':
rom.write_byte(0x18003A, 0x01) # show key counts on map pickup 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 loot_source = 0x09
if world.prizeshuffle[player] != 'none': 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) rom.write_byte(0x18003C, compass_mode)
def get_entrance_coords(ent): def get_entrance_coords(ent):
if ent is None: if ent is None or isinstance(ent, int):
owid_map = [0x1E, 0x30, 0xFF, 0x7B, 0x5E, 0x70, 0x40, 0x75, 0x03, 0x58, 0x47] 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] coord_flags = 0x0000
y_map_position_generic = [0xff00, 0xff00, 0xff00, 0x0fc0, 0x0fc0, 0x0fc0, 0x0fc0, 0x0fc0, 0xff00, 0x0fc0, 0x0fc0] if ent is None:
world_indicator = 0x0000 # HUD-style dislocated icons
idx = int((map_index-2)/2) 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] owid = owid_map[idx]
map_x = x_map_position[idx]
map_y = y_map_position[idx]
if owid != 0xFF: if owid != 0xFF:
if (owid < 0x40) == (world.is_tile_swapped(owid, player)): if (owid < 0x40) == (world.is_tile_swapped(owid, player)):
world_indicator = 0x8000 coord_flags |= 0x8000 # world indicator flag
return [world_indicator | x_map_position_generic[idx], y_map_position_generic[idx]] if coord_flags & 0x4000 == 0:
if type(ent) is Location: map_x, map_y = adjust_ow_coordinates_to_layout(world, player, map_x, map_y, coord_flags & 0x8000 != 0)
from OverworldShuffle import OWTileRegions return (coord_flags | map_x, map_y)
if ent.name == 'Hobo': elif type(ent) is Location:
coords = (0xb80, 0xb80) from OverworldShuffle import OWTileRegions, ow_loc_prize_table
elif ent.name == 'Master Sword Pedestal': if ent.name in ow_loc_prize_table:
coords = (0x06d, 0x070) coords = ow_loc_prize_table[ent.name]
else: else:
owid = OWTileRegions[ent.parent_region.name] owid = OWTileRegions[ent.parent_region.name]
if owid == 0x81: if owid == 0x81:
coords = (0x220, 0xf40) coords = (0xF60, 0x280)
else: else:
owid = owid % 0x40 owid = owid % 0x40
coords = (0x200 * (owid % 0x08) + 0x100, 0x200 * int(owid / 0x08) + 0x100) 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) coords = (coords[0] + 0x100, coords[1] + 0x100)
else: else:
coords = ow_prize_table[ent.name] if ent.name in ow_prize_table:
coords = ((0x8000 if ent.parent_region.type == RegionType.DarkWorld else 0x0000) | coords[0], coords[1]) 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 return coords
if world.overworld_map[player] == 'default': if world.overworld_map[player] == 'default':
# disable HC/AT/GT icons # disable HC/AT/GT icons
if not world.owMixed[player]: if not world.owMixed[player]:
@@ -1591,16 +1635,16 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
# prize location # prize location
write_int16s(rom, snes_to_pc(0x0ABE2E)+(map_index*6)+8, coords) write_int16s(rom, snes_to_pc(0x0ABE2E)+(map_index*6)+8, coords)
if world.shuffle[player] == 'vanilla' or world.overworld_map[player] == 'default': 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 swap_entrances = { 'Agahnims Tower': 'Ganons Tower',
vanilla_entrances = { 'Hyrule Castle': 'Hyrule Castle Entrance (South)', 'Ganons Tower': 'Agahnims Tower' }
'Desert Palace': 'Desert Palace Entrance (North)', if dungeon in swap_entrances:
'Skull Woods': 'Skull Woods Final Section' } entrance_name = dungeon
entrance_name = vanilla_entrances[dungeon] if dungeon in vanilla_entrances else dungeon if world.is_atgt_swapped(player):
if world.is_atgt_swapped(player): entrance_name = swap_entrances[dungeon]
swap_entrances = { 'Agahnims Tower': 'Ganons Tower', entrance = world.get_entrance(entrance_name, player)
'Ganons Tower': 'Agahnims Tower' } coords = get_entrance_coords(entrance)
entrance_name = swap_entrances[dungeon] if dungeon in swap_entrances else entrance_name else:
entrance = world.get_entrance(entrance_name, player) coords = get_entrance_coords(int((map_index-2)/2))
else: else:
if len(portal_list) == 1: if len(portal_list) == 1:
portal_idx = 0 portal_idx = 0
@@ -1620,7 +1664,7 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
offset += 4 offset += 4
portal = world.get_portal(portal_list[portal_idx], player) portal = world.get_portal(portal_list[portal_idx], player)
entrance = portal.find_portal_entrance() entrance = portal.find_portal_entrance()
coords = get_entrance_coords(entrance) coords = get_entrance_coords(entrance)
# figure out compass entrances and what world (light/dark) # figure out compass entrances and what world (light/dark)
write_int16s(rom, snes_to_pc(0x0ABE2E)+(map_index*6), coords) write_int16s(rom, snes_to_pc(0x0ABE2E)+(map_index*6), coords)
@@ -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']: if world.is_tile_swapped(0x18, player) or world.flute_mode[player] in ['active', 'pseudo']:
items_to_hint.remove(flute_item) items_to_hint.remove(flute_item)
flute_item = 'Ocarina (Activated)' flute_item = 'Ocarina (Activated)'
if world.owShuffle[player] != 'vanilla' or world.owMixed[player]: if world.owLayout[player] != 'vanilla' or world.owMixed[player]:
# Adding a guaranteed hint for the Flute in overworld shuffle. # Adding a guaranteed hint for the Flute in overworld shuffle.
this_location = world.find_items_not_key_only(flute_item, player) this_location = world.find_items_not_key_only(flute_item, player)
if this_location and this_location not in hinted_locations: if this_location and this_location not in hinted_locations:
@@ -2560,7 +2604,7 @@ def write_strings(rom, world, player, team):
random.shuffle(items_to_hint) random.shuffle(items_to_hint)
hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'district', 'swapped'] else 8 hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'district', 'swapped'] else 8
hint_count += 2 if world.doorShuffle[player] not in ['vanilla', 'basic'] else 0 hint_count += 2 if world.doorShuffle[player] not in ['vanilla', 'basic'] else 0
hint_count += 1 if world.owShuffle[player] != 'vanilla' or world.owCrossed[player] != 'none' or world.owMixed[player] else 0 hint_count += 1 if world.owLayout[player] != 'vanilla' or world.owCrossed[player] != 'none' or world.owMixed[player] else 0
while hint_count > 0 and len(items_to_hint) > 0: while hint_count > 0 and len(items_to_hint) > 0:
this_item = items_to_hint.pop(0) this_item = items_to_hint.pop(0)
this_location = world.find_items_not_key_only(this_item, player) this_location = world.find_items_not_key_only(this_item, player)
@@ -3194,6 +3238,13 @@ def update_compasses(rom, dungeon_locations, world, player):
if not provided_dungeon: if not provided_dungeon:
rom.write_byte(0x186FFF, 0xff) 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', InconvenientDungeonEntrances = {'Turtle Rock': 'Turtle Rock Main',
+3 -1
View File
@@ -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)) add_rule(world.get_location('Central Bonk Rocks Tree', player), lambda state: state.has('Zelda Delivered', player))
if not world.is_premature_copied_world: 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 # 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)) 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))
+20
View File
@@ -356,6 +356,26 @@ def update_deprecated_args(args):
else: else:
args.shuffleganon = not args.no_shuffleganon in truthy 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 # Playthrough defaults to TRUE
# Don't do: Yes # Don't do: Yes
# Do: No # Do: No
+2 -2
View File
@@ -1,3 +1,3 @@
GKVersion = '1.0.0' GKVersion = '1.0.0'
ORVersion = '0.6.1.7' ORVersion = '0.7.0.1'
DRVersion = '1.5.0-u' DRVersion = '1.5.2-u'
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

+9 -1
View File
@@ -234,7 +234,7 @@ You may define a list of items and a list of locations. Those items will be cons
### ow-edges ### ow-edges
This must be defined by player. Each player number should be listed with the appropriate sections and each of these players MUST have either `ow_shuffle` or `ow_crossed` enabled in the `settings` section in order for any values here to take effect. This section has two primary subsections: `two-way` and `groups`. This must be defined by player. Each player number should be listed with the appropriate sections and each of these players MUST have either `ow_layout` or `ow_crossed` enabled in the `settings` section in order for any values here to take effect. This section has two primary subsections: `two-way` and `groups`.
#### two-way #### two-way
@@ -253,6 +253,14 @@ someDescription:
- Stone Bridge WS* - 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 ### 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 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
+2 -1
View File
@@ -20,7 +20,8 @@ settings:
shuffle_followers: true shuffle_followers: true
shuffle: crossed shuffle: crossed
shufflelinks: true shufflelinks: true
ow_shuffle: parallel ow_layout: wild
ow_parallel: true
ow_terrain: true ow_terrain: true
ow_crossed: grouped ow_crossed: grouped
ow_keepsimilar: true ow_keepsimilar: true
+2 -1
View File
@@ -1,6 +1,7 @@
settings: settings:
1: 1:
ow_shuffle: full ow_layout: wild
ow_parallel: false
ow_keepsimilar: false ow_keepsimilar: false
ow-edges: ow-edges:
1: 1:
@@ -1,6 +1,7 @@
settings: settings:
1: 1:
ow_shuffle: full ow_layout: wild
ow_parallel: false
ow_keepsimilar: false ow_keepsimilar: false
ow-edges: ow-edges:
1: 1:
@@ -1,6 +1,7 @@
settings: settings:
1: 1:
ow_shuffle: full ow_layout: wild
ow_parallel: false
ow_keepsimilar: false ow_keepsimilar: false
ow-edges: ow-edges:
1: 1:
@@ -1,6 +1,7 @@
settings: settings:
1: 1:
ow_shuffle: full ow_layout: wild
ow_parallel: false
ow_terrain: true ow_terrain: true
ow-edges: ow-edges:
1: 1:
+25
View File
@@ -177,6 +177,22 @@
"full" "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": { "ow_terrain": {
"action": "store_true", "action": "store_true",
"type": "bool" "type": "bool"
@@ -212,6 +228,15 @@
"random" "random"
] ]
}, },
"ow_fog": {
"action": "store_true",
"help": "suppress",
"type": "bool"
},
"ow_no_fog": {
"action": "store_true",
"dest": "ow_fog"
},
"shuffle": { "shuffle": {
"choices": [ "choices": [
"vanilla", "vanilla",
+11 -4
View File
@@ -235,14 +235,18 @@
"the entrances vanilla." "the entrances vanilla."
], ],
"ow_shuffle": [ "ow_shuffle": [
"Deprecated, use ow_layout and ow_unparallel instead."
],
"ow_layout": [
"This shuffles the layout of the overworld.", "This shuffles the layout of the overworld.",
"Vanilla: All overworld transitions are connected the same", "Vanilla: All overworld transitions are connected the same",
" way they were in the base game.", " way they were in the base game.",
"Parallel: Overworld transitions are shuffled, but both worlds", "Grid: OW Screens are arranged on 8x8 grids and OW Transitions",
" will have the same pattern/shape.", " work according to this arrangement.",
"Full: Overworld transitions are shuffled, but both worlds", "Wild: OW Transitions are shuffled with no respect to geometric coherence."
" will have an independent map shape."
], ],
"ow_unparallel": [
"With OW Layout Shuffle, this no longer forces both worlds to have a matching layout." ],
"ow_terrain": [ "ow_terrain": [
"With OW Layout Shuffle, this allows land and water edges to be connected." ], "With OW Layout Shuffle, this allows land and water edges to be connected." ],
"ow_crossed": [ "ow_crossed": [
@@ -277,6 +281,9 @@
" spots from being on any adjacent screen.", " spots from being on any adjacent screen.",
"Random: New flute spots will be generated with minimal bias." "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": [ "door_shuffle": [
"Select Door Shuffling Algorithm. (default: %(default)s)", "Select Door Shuffling Algorithm. (default: %(default)s)",
"Basic: Doors are mixed within a single dungeon.", "Basic: Doors are mixed within a single dungeon.",
+8 -4
View File
@@ -157,10 +157,12 @@
"randomizer.enemizer.enemylogic.allow_all": "Allow special enemies anywhere", "randomizer.enemizer.enemylogic.allow_all": "Allow special enemies anywhere",
"randomizer.overworld.overworldshuffle": "Layout Shuffle", "randomizer.overworld.layout": "Layout Shuffle",
"randomizer.overworld.overworldshuffle.vanilla": "Vanilla", "randomizer.overworld.layout.vanilla": "Vanilla",
"randomizer.overworld.overworldshuffle.parallel": "Parallel", "randomizer.overworld.layout.grid": "Grid",
"randomizer.overworld.overworldshuffle.full": "Full", "randomizer.overworld.layout.wild": "Wild",
"randomizer.overworld.parallel": "Keep Worlds Parallel",
"randomizer.overworld.terrain": "Free Terrain", "randomizer.overworld.terrain": "Free Terrain",
@@ -181,6 +183,8 @@
"randomizer.overworld.overworldflute.balanced": "Balanced", "randomizer.overworld.overworldflute.balanced": "Balanced",
"randomizer.overworld.overworldflute.random": "Random", "randomizer.overworld.overworldflute.random": "Random",
"randomizer.overworld.fog": "Overworld Map Fog",
"randomizer.entrance.openpyramid": "Pre-open Pyramid Hole", "randomizer.entrance.openpyramid": "Pre-open Pyramid Hole",
"randomizer.entrance.openpyramid.auto": "Auto", "randomizer.entrance.openpyramid.auto": "Auto",
@@ -1,13 +1,13 @@
{ {
"topOverworldFrame": {}, "topOverworldFrame": {},
"leftOverworldFrame": { "leftOverworldFrame": {
"overworldshuffle": { "layout": {
"type": "selectbox", "type": "selectbox",
"default": "vanilla", "default": "vanilla",
"options": [ "options": [
"vanilla", "vanilla",
"parallel", "grid",
"full" "wild"
] ]
}, },
"crossed": { "crossed": {
@@ -18,7 +18,10 @@
"grouped", "grouped",
"polar", "polar",
"unrestricted" "unrestricted"
] ],
"config": {
"pady": [16,0]
}
}, },
"mixed": { "mixed": {
"type": "checkbox", "type": "checkbox",
@@ -45,22 +48,27 @@
"config": { "config": {
"pady": [20,0] "pady": [20,0]
} }
},
"fog": {
"type": "checkbox",
"default": true,
"config": {
"pady": [20,0]
}
} }
}, },
"rightOverworldFrame": { "rightOverworldFrame": {
"parallel": {
"type": "checkbox",
"default": true
},
"terrain": { "terrain": {
"type": "checkbox", "type": "checkbox",
"default": false, "default": false
"config": {
"pady": [3,0]
}
}, },
"keepsimilar": { "keepsimilar": {
"type": "checkbox", "type": "checkbox",
"default": false, "default": false
"config": {
"pady": [6,0]
}
} }
} }
} }
+19 -2
View File
@@ -90,7 +90,8 @@ class CustomSettings(object):
args.mystery = True args.mystery = True
else: else:
settings = defaultdict(lambda: None, player_setting) settings = defaultdict(lambda: None, player_setting)
args.ow_shuffle[p] = get_setting(settings['ow_shuffle'], args.ow_shuffle[p]) args.ow_layout[p] = get_setting(settings['ow_layout'], args.ow_layout[p])
args.ow_parallel[p] = get_setting(settings['ow_parallel'], args.ow_parallel[p])
args.ow_terrain[p] = get_setting(settings['ow_terrain'], args.ow_terrain[p]) args.ow_terrain[p] = get_setting(settings['ow_terrain'], args.ow_terrain[p])
args.ow_crossed[p] = get_setting(settings['ow_crossed'], args.ow_crossed[p]) args.ow_crossed[p] = get_setting(settings['ow_crossed'], args.ow_crossed[p])
if args.ow_crossed[p] == 'chaos': if args.ow_crossed[p] == 'chaos':
@@ -101,6 +102,7 @@ class CustomSettings(object):
args.ow_mixed[p] = get_setting(settings['ow_mixed'], args.ow_mixed[p]) 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_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_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.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.bonk_drops[p] = get_setting(settings['bonk_drops'], args.bonk_drops[p])
args.shuffle[p] = get_setting(settings['shuffle'], args.shuffle[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.take_any[p] = 'random' if args.take_any[p] == 'none' else args.take_any[p]
args.keyshuffle[p] = 'universal' args.keyshuffle[p] = 'universal'
ow_shuffle = get_setting(settings['ow_shuffle'], args.ow_shuffle[p])
if ow_shuffle == 'parallel':
args.ow_layout = 'wild'
args.ow_parallel = True
elif ow_shuffle == 'full':
args.ow_layout = 'wild'
args.ow_parallel = False
args.mixed_travel[p] = get_setting(settings['mixed_travel'], args.mixed_travel[p]) args.mixed_travel[p] = get_setting(settings['mixed_travel'], args.mixed_travel[p])
args.standardize_palettes[p] = get_setting(settings['standardize_palettes'], args.standardize_palettes[p] = get_setting(settings['standardize_palettes'],
args.standardize_palettes[p]) args.standardize_palettes[p])
@@ -254,6 +264,11 @@ class CustomSettings(object):
return self.file_source['ow-edges'] return self.file_source['ow-edges']
return None 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): def get_owcrossed(self):
if 'ow-crossed' in self.file_source: if 'ow-crossed' in self.file_source:
return self.file_source['ow-crossed'] return self.file_source['ow-crossed']
@@ -357,13 +372,15 @@ class CustomSettings(object):
self.world_rep['start_inventory'] = start_inv = {} self.world_rep['start_inventory'] = start_inv = {}
for p in self.player_range: for p in self.player_range:
settings_dict[p] = {} settings_dict[p] = {}
settings_dict[p]['ow_shuffle'] = world.owShuffle[p] settings_dict[p]['ow_layout'] = world.owLayout[p]
settings_dict[p]['ow_parallel'] = world.owParallel[p]
settings_dict[p]['ow_terrain'] = world.owTerrain[p] settings_dict[p]['ow_terrain'] = world.owTerrain[p]
settings_dict[p]['ow_crossed'] = world.owCrossed[p] settings_dict[p]['ow_crossed'] = world.owCrossed[p]
settings_dict[p]['ow_keepsimilar'] = world.owKeepSimilar[p] settings_dict[p]['ow_keepsimilar'] = world.owKeepSimilar[p]
settings_dict[p]['ow_mixed'] = world.owMixed[p] settings_dict[p]['ow_mixed'] = world.owMixed[p]
settings_dict[p]['ow_whirlpool'] = world.owWhirlpoolShuffle[p] settings_dict[p]['ow_whirlpool'] = world.owWhirlpoolShuffle[p]
settings_dict[p]['ow_fluteshuffle'] = world.owFluteShuffle[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]['shuffle_followers'] = world.shuffle_followers[p]
settings_dict[p]['bonk_drops'] = world.shuffle_bonk_drops[p] settings_dict[p]['bonk_drops'] = world.shuffle_bonk_drops[p]
settings_dict[p]['shuffle'] = world.shuffle[p] settings_dict[p]['shuffle'] = world.shuffle[p]
+4 -2
View File
@@ -92,13 +92,15 @@ SETTINGSTOPROCESS = {
"bombbag": "bombbag" "bombbag": "bombbag"
}, },
"overworld": { "overworld": {
"overworldshuffle": "ow_shuffle", "layout": "ow_layout",
"parallel": "ow_parallel",
"terrain": "ow_terrain", "terrain": "ow_terrain",
"crossed": "ow_crossed", "crossed": "ow_crossed",
"keepsimilar": "ow_keepsimilar", "keepsimilar": "ow_keepsimilar",
"mixed": "ow_mixed", "mixed": "ow_mixed",
"whirlpool": "ow_whirlpool", "whirlpool": "ow_whirlpool",
"overworldflute": "ow_fluteshuffle" "overworldflute": "ow_fluteshuffle",
"fog": "ow_fog"
}, },
"entrance": { "entrance": {
"entranceshuffle": "shuffle", "entranceshuffle": "shuffle",
+7 -2
View File
@@ -2116,8 +2116,13 @@ class EnemyTable:
self.room_map = defaultdict(list) self.room_map = defaultdict(list)
self.special_bitmasks = None self.special_bitmasks = None
def write_sprite_data_to_rom(self, rom): def write_sprite_data_to_rom(self, rom, pointer_table):
pointer_address = snes_to_pc(0x09D62E) 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) data_pointer = snes_to_pc(0x288000)
empty_pointer = pc_to_snes(data_pointer) & 0xFFFF empty_pointer = pc_to_snes(data_pointer) & 0xFFFF
rom.write_bytes(data_pointer, [0x00, 0xff]) rom.write_bytes(data_pointer, [0x00, 0xff])
+9 -9
View File
@@ -183,9 +183,9 @@ def init_sprite_requirements():
SpriteRequirement(EnemySprite.Vulture).no_drop().sub_group(2, 0x12).exclude(NoFlyingRooms), SpriteRequirement(EnemySprite.Vulture).no_drop().sub_group(2, 0x12).exclude(NoFlyingRooms),
SpriteRequirement(EnemySprite.CorrectPullSwitch).affix().sub_group(3, [0x52, 0x53]), SpriteRequirement(EnemySprite.CorrectPullSwitch).affix().sub_group(3, [0x52, 0x53]),
SpriteRequirement(EnemySprite.WrongPullSwitch).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.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.Cucco).immune().sub_group(3, [0x15, 0x50]).exclude(NoFlyingRooms),
SpriteRequirement(EnemySprite.Buzzblob).sub_group(3, 0x11), SpriteRequirement(EnemySprite.Buzzblob).sub_group(3, 0x11),
SpriteRequirement(EnemySprite.Snapdragon).sub_group(0, 0x16).sub_group(2, 0x17), 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 .exclude(NoFlyingRooms).exclude({0x40}), # no anti-fairies in aga tower bridge room
SpriteRequirement(EnemySprite.Wiseman).affix().sub_group(2, 0x4c), SpriteRequirement(EnemySprite.Wiseman).affix().sub_group(2, 0x4c),
SpriteRequirement(EnemySprite.Hoarder).sub_group(3, 0x11).exclude({0x10c}), 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.Poe).no_drop().sub_group(3, 0x15).exclude(NoFlyingRooms),
SpriteRequirement(EnemySprite.Smithy).affix().sub_group(1, 0x1d).sub_group(3, 0x15), SpriteRequirement(EnemySprite.Smithy).affix().sub_group(1, 0x1d).sub_group(3, 0x15),
SpriteRequirement(EnemySprite.Statue).stasis().immune().sub_group(3, [0x52, 0x53]), 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.Hoarder2).sub_group(3, 0x11).exclude({0x10c}),
SpriteRequirement(EnemySprite.TutorialGuard).affix(), SpriteRequirement(EnemySprite.TutorialGuard).affix(),
SpriteRequirement(EnemySprite.LightningGate).affix().sub_group(3, 0x3f), SpriteRequirement(EnemySprite.LightningGate).affix().sub_group(3, 0x3f),
SpriteRequirement(EnemySprite.BlueGuard).aquaphobia().sub_group(1, [0xd, 0x49]).exclude(PitRooms), SpriteRequirement(EnemySprite.BlueGuard).sub_group(1, [0xd, 0x49]).exclude(PitRooms),
SpriteRequirement(EnemySprite.BlueGuard).aquaphobia().sub_group(1, [0xd, 0x49]).sub_group(2, [0x29, 0x13]), SpriteRequirement(EnemySprite.BlueGuard).sub_group(1, [0xd, 0x49]).sub_group(2, [0x29, 0x13]),
SpriteRequirement(EnemySprite.GreenGuard).aquaphobia().sub_group(1, 0x49).exclude(PitRooms), SpriteRequirement(EnemySprite.GreenGuard).sub_group(1, 0x49).exclude(PitRooms),
SpriteRequirement(EnemySprite.GreenGuard).aquaphobia().sub_group(1, 0x49).sub_group(2, 0x13), SpriteRequirement(EnemySprite.GreenGuard).sub_group(1, 0x49).sub_group(2, 0x13),
SpriteRequirement(EnemySprite.RedSpearGuard).aquaphobia().sub_group(1, [0xd, 0x49]).exclude(PitRooms), SpriteRequirement(EnemySprite.RedSpearGuard).sub_group(1, [0xd, 0x49]).exclude(PitRooms),
SpriteRequirement(EnemySprite.RedSpearGuard).aquaphobia().sub_group(1, [0xd, 0x49]).sub_group(2, [0x29, 0x13]), 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.BluesainBolt).aquaphobia().sub_group(0, 0x46).sub_group(1, [0xd, 0x49]),
SpriteRequirement(EnemySprite.UsainBolt).aquaphobia().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), SpriteRequirement(EnemySprite.BlueArcher).sub_group(0, 0x48).sub_group(1, 0x49),
+142 -132
View File
@@ -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), '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)} '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), ow_prize_table = {'Lost Woods Gamble': (0x2A0, 0x080),
'Desert Palace Entrance (South)': (0x108, 0xd70), 'Desert Palace Entrance (West)': (0x031, 0xca0), 'Lost Woods Hideout Drop': (0x338, 0x218),
'Desert Palace Entrance (North)': (0x0e1, 0xba0), 'Desert Palace Entrance (East)': (0x191, 0xca0), 'Lost Woods Hideout Stump': (0x2A0, 0x2E0),
'Eastern Palace': (0xf31, 0x620), 'Tower of Hera': (0x8D0, 0x080), 'Lumberjack House': (0x538, 0x158),
'Hyrule Castle Entrance (South)': (0x820, 0x730), 'Hyrule Castle Entrance (West)': (0x740, 0x5D0), 'Lumberjack Tree Tree': (0x4A0, 0x160),
'Hyrule Castle Entrance (East)': (0x8f0, 0x5D0), 'Lumberjack Tree Cave': (0x550, 0x004),
'Agahnims Tower': (0x820, 0x5D0), 'Tower of Hera': (0x900, 0x0E0),
'Thieves Town': (0x1d0, 0x780), 'Skull Woods First Section Door': (0x2e0, 0x280), 'Spectacle Rock Cave Peak': (0x758, 0x1B7),
'Skull Woods Second Section Door (East)': (0x200, 0x240), 'Spectacle Rock Cave (Bottom)': (0x6F0, 0x257),
'Skull Woods Second Section Door (West)': (0x0c0, 0x1c0), 'Spectacle Rock Cave': (0x7C8, 0x257),
'Skull Woods Final Section': (0x082, 0x0b0), 'Death Mountain Return Cave (East)': (0x648, 0x278),
'Skull Woods First Section Hole (West)': (0x200, 0x2b0), 'Old Man Cave (East)': (0x658, 0x387),
'Skull Woods First Section Hole (East)': (0x340, 0x2e0), 'Old Man House (Top)': (0x8C8, 0x2D8),
'Skull Woods First Section Hole (North)': (0x320, 0x1e0), 'Old Man House (Bottom)': (0x728, 0x3A7),
'Skull Woods Second Section Hole': (0x0f0, 0x0b0), 'Paradox Cave (Top)': (0xDE0, 0x100),
'Ice Palace': (0xca0, 0xda0), 'Paradox Cave (Bottom)': (0xDC8, 0x358),
'Misery Mire': (0x100, 0xca0), 'Paradox Cave (Middle)': (0xDDF, 0x3FF),
'Palace of Darkness': (0xf40, 0x620), 'Swamp Palace': (0x759, 0xED0), 'Fairy Ascension Cave (Top)': (0xCD8, 0x2B8),
'Turtle Rock': (0xf11, 0x103), 'Fairy Ascension Cave (Bottom)': (0xC78, 0x358),
'Dark Death Mountain Ledge (West)': (0xb80, 0x180), 'Spiral Cave': (0xC78, 0x207),
'Dark Death Mountain Ledge (East)': (0xc80, 0x180), 'Spiral Cave (Bottom)': (0xBC8, 0x2F7),
'Turtle Rock Isolated Ledge Entrance': (0xc00, 0x240), '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 Drop': (0x9D0, 0x680),
'Hyrule Castle Secret Entrance Stairs': (0x8D0, 0x700), 'Hyrule Castle Secret Entrance Stairs': (0x8C8, 0x718),
'Kakariko Well Drop': (0x030, 0x680), 'Inverted Pyramid Hole': (0x7F0, 0x6B0),
'Kakariko Well Cave': (0x060, 0x680), 'Inverted Pyramid Entrance': (0x6E8, 0x644),
'Bat Cave Drop': (0x520, 0x8f0), 'Sahasrahlas Hut': (0xCF0, 0x747),
'Bat Cave Cave': (0x560, 0x940), 'Eastern Palace': (0xF40, 0x680),
'Elder House (East)': (0x2b0, 0x6a0), 'Blacksmiths Hut': (0x4E8, 0x8C8),
'Elder House (West)': (0x230, 0x6a0), 'Bat Cave Drop': (0x538, 0x9A8),
'North Fairy Cave Drop': (0xa40, 0x500), 'Bat Cave Cave': (0x510, 0x930),
'North Fairy Cave': (0xa80, 0x440), 'Two Brothers House (West)': (0x1B8, 0xB88),
'Lost Woods Hideout Drop': (0x290, 0x200), 'Two Brothers House (East)': (0x238, 0xB88),
'Lost Woods Hideout Stump': (0x240, 0x280), 'Library': (0x2B8, 0xAA7),
'Lumberjack Tree Tree': (0x4e0, 0x140), 'Kakariko Gamble Game': (0x348, 0xB78),
'Lumberjack Tree Cave': (0x560, 0x004), 'Bonk Fairy (Light)': (0x788, 0xA87),
'Two Brothers House (East)': (0x200, 0x0b60), 'Links House': (0x8B8, 0xB58),
'Two Brothers House (West)': (0x180, 0x0b60), 'Lake Hylia Fairy': (0xD58, 0xA57),
'Sanctuary Grave': (0x820, 0x4c0), 'Long Fairy Cave': (0xF68, 0xB58),
'Sanctuary': (0x720, 0x4a0), 'Desert Palace Entrance (North)': (0x148, 0xC28),
'Old Man Cave (West)': (0x580, 0x2c0), 'Desert Palace Entrance (West)': (0x088, 0xCF8),
'Old Man Cave (East)': (0x620, 0x2c0), 'Desert Palace Entrance (East)': (0x1E8, 0xCF8),
'Old Man House (Bottom)': (0x720, 0x320), 'Desert Palace Entrance (South)': (0x138, 0xD18),
'Old Man House (Top)': (0x820, 0x220), 'Checkerboard Cave': (0x2E8, 0xCF7),
'Death Mountain Return Cave (East)': (0x600, 0x220), 'Aginahs Cave': (0x388, 0xDE8),
'Death Mountain Return Cave (West)': (0x500, 0x1c0), 'Cave 45': (0x448, 0xD86),
'Spectacle Rock Cave Peak': (0x720, 0x0a0), 'Light Hype Fairy': (0x988, 0xCF8),
'Spectacle Rock Cave': (0x790, 0x1a0), 'Lake Hylia Fortune Teller': (0xA68, 0xCC8),
'Spectacle Rock Cave (Bottom)': (0x710, 0x0a0), 'Lake Hylia Shop': (0xBF8, 0xC66),
'Paradox Cave (Bottom)': (0xd80, 0x180), 'Capacity Upgrade': (0xCC8, 0xDE8),
'Paradox Cave (Middle)': (0xd80, 0x380), 'Mini Moldorm Cave': (0xA88, 0xEF8),
'Paradox Cave (Top)': (0xd80, 0x020), 'Ice Rod Cave': (0xE68, 0xC37),
'Fairy Ascension Cave (Bottom)': (0xcc8, 0x2a0), 'Good Bee Cave': (0xEF8, 0xC36),
'Fairy Ascension Cave (Top)': (0xc00, 0x240), '20 Rupee Cave': (0xEB8, 0xCC6),
'Spiral Cave': (0xb80, 0x180), 'Desert Fairy': (0x488, 0xE76),
'Spiral Cave (Bottom)': (0xb80, 0x2c0), '50 Rupee Cave': (0x4F8, 0xF47),
'Bumper Cave (Bottom)': (0x580, 0x2c0), 'Dam': (0x778, 0xF68),
'Bumper Cave (Top)': (0x500, 0x1c0),
'Superbunny Cave (Top)': (0xd80, 0x020), 'Skull Woods Final Section': (0x078, 0x0A8),
'Superbunny Cave (Bottom)': (0xd00, 0x180), 'Skull Woods Second Section Door (West)': (0x0E0, 0x1C0),
'Hookshot Cave': (0xc80, 0x0c0), 'Skull Woods Second Section Hole': (0x150, 0x120),
'Hookshot Cave Back Entrance': (0xcf0, 0x004), 'Skull Woods Second Section Door (East)': (0x208, 0x208),
'Ganons Tower': (0x8D0, 0x080), 'Skull Woods First Section Door': (0x2C8, 0x2A8),
'Pyramid Hole': (0x820, 0x680), 'Skull Woods First Section Hole (North)': (0x320, 0x1e0),
'Inverted Pyramid Hole': (0x820, 0x680), 'Skull Woods First Section Hole (West)': (0x240, 0x380),
'Pyramid Entrance': (0x640, 0x7c0), 'Skull Woods First Section Hole (East)': (0x340, 0x380),
'Inverted Pyramid Entrance': (0x6C0, 0x5D0), 'Dark Lumberjack Shop': (0x538, 0x177),
'Waterfall of Wishing': (0xe80, 0x280), 'Ganons Tower': (0x900, 0x0E0),
'Dam': (0x759, 0xED0), 'Dark Death Mountain Fairy': (0x658, 0x387),
'Blinds Hideout': (0x190, 0x6c0), 'Spike Cave': (0x9F8, 0x268),
'Bonk Fairy (Light)': (0x740, 0xa80), 'Hookshot Cave Back Entrance': (0xD38, 0x038),
'Lake Hylia Fairy': (0xd40, 0x9f0), 'Hookshot Cave': (0xD48, 0x107),
'Light Hype Fairy': (0x940, 0xc80), 'Superbunny Cave (Top)': (0xDE0, 0x100),
'Desert Fairy': (0x420, 0xe00), 'Superbunny Cave (Bottom)': (0xD38, 0x358),
'Kings Grave': (0x920, 0x520), 'Dark Death Mountain Ledge (West)': (0xC78, 0x207),
'Tavern North': (0x270, 0x900), 'Dark Death Mountain Ledge (East)': (0xD38, 0x207),
'Chicken House': (0x120, 0x880), 'Turtle Rock Isolated Ledge Entrance': (0xCD8, 0x2B8),
'Aginahs Cave': (0x2e0, 0xd00), 'Dark Death Mountain Shop': (0xDC8, 0x358),
'Sahasrahlas Hut': (0xcf0, 0x6c0), 'Turtle Rock': (0xF48, 0x108),
'Lake Hylia Shop': (0xbc0, 0xc00), 'Bumper Cave (Top)': (0x5B8, 0x247),
'Capacity Upgrade': (0xca0, 0xda0), 'Bumper Cave (Bottom)': (0x5B8, 0x348),
'Blacksmiths Hut': (0x4a0, 0x880), 'Fortune Teller (Dark)': (0x308, 0x5A8),
'Sick Kids House': (0x220, 0x880), 'Dark Sanctuary Hint': (0x748, 0x4F4),
'Lost Woods Gamble': (0x240, 0x080), 'Dark Potion Shop': (0xCB8, 0x598),
'Fortune Teller (Light)': (0x2c0, 0x4c0), 'Chest Game': (0x0C8, 0x7E8),
'Snitch Lady (East)': (0x310, 0x7a0), 'Thieves Town': (0x1F0, 0x7E8),
'Snitch Lady (West)': (0x080, 0x7a0), 'C-Shaped House': (0x338, 0x7E8),
'Bush Covered House': (0x2e0, 0x880), 'Brewery': (0x1C8, 0x9D7),
'Tavern (Front)': (0x270, 0x980), 'Dark World Shop': (0x338, 0x8E8),
'Light World Bomb Hut': (0x070, 0x980), 'Red Shield Shop': (0x538, 0x778),
'Kakariko Shop': (0x170, 0x980), 'Pyramid Hole': (0x800, 0x680),
'Cave 45': (0x440, 0xca0), 'Graveyard Cave': (0x8f0, 0x430), 'Pyramid Entrance': (0x718, 0x7A8),
'Checkerboard Cave': (0x260, 0xc00), 'Pyramid Fairy': (0x7B8, 0x7A8),
'Mini Moldorm Cave': (0xa40, 0xe80), 'Palace of Darkness': (0xF40, 0x680),
'Long Fairy Cave': (0xf60, 0xb00), 'Palace of Darkness Hint': (0xD70, 0x878),
'Good Bee Cave': (0xec0, 0xc00), 'Hammer Peg Cave': (0x528, 0x9B6),
'20 Rupee Cave': (0xe80, 0xca0), 'Archery Game': (0x348, 0xB78),
'50 Rupee Cave': (0x4d0, 0xed0), 'Bonk Fairy (Dark)': (0x788, 0xA87),
'Ice Rod Cave': (0xe00, 0xc00), 'Big Bomb Shop': (0x8B8, 0xB58),
'Bonk Rock Cave': (0x5f0, 0x460), 'Dark Lake Hylia Fairy': (0xD58, 0xA57),
'Library': (0x270, 0xaa0), 'East Dark World Hint': (0xF68, 0xB58),
'Potion Shop': (0xc80, 0x4c0), 'Mire Shed': (0x0A8, 0xCC7),
'Hookshot Fairy': (0xd00, 0x180), 'Misery Mire': (0x148, 0xCC7),
'Pyramid Fairy': (0x740, 0x740), 'Mire Fairy': (0x1E8, 0xCC7),
'East Dark World Hint': (0xf60, 0xb00), 'Mire Hint': (0x388, 0xDE8),
'Palace of Darkness Hint': (0xd60, 0x7c0), 'Hype Cave': (0x988, 0xCF8),
'Dark Lake Hylia Fairy': (0xd40, 0x9f0), 'Dark Lake Hylia Shop': (0xA68, 0xCC8),
'Dark Lake Hylia Ledge Fairy': (0xe00, 0xc00), 'Ice Palace': (0xCA8, 0xE28),
'Dark Lake Hylia Ledge Spike Cave': (0xe80, 0xca0), 'Dark Lake Hylia Ledge Fairy': (0xE68, 0xC37),
'Dark Lake Hylia Ledge Hint': (0xec0, 0xc00), 'Dark Lake Hylia Ledge Hint': (0xEF8, 0xC36),
'Hype Cave': (0x940, 0xc80), 'Dark Lake Hylia Ledge Spike Cave': (0xEB8, 0xCC6),
'Bonk Fairy (Dark)': (0x740, 0xa80), 'Swamp Palace': (0x778, 0xF68)}
'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)}
default_connector_connections = [('Death Mountain Return Cave (West)', 'Death Mountain Return Cave Exit (West)'), 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)'), ('Death Mountain Return Cave (East)', 'Death Mountain Return Cave Exit (East)'),
+3 -3
View File
@@ -634,7 +634,7 @@ def do_dark_sanc(entrances, exits, avail):
forbidden.append('Links House') forbidden.append('Links House')
else: else:
forbidden.append('Big Bomb Shop') forbidden.append('Big Bomb Shop')
if avail.world.owShuffle[avail.player] == 'vanilla': if avail.world.owLayout[avail.player] == 'vanilla':
choices = [e for e in avail.world.districts[avail.player]['Northwest Dark World'].entrances if e not in forbidden and e in entrances] choices = [e for e in avail.world.districts[avail.player]['Northwest Dark World'].entrances if e not in forbidden and e in entrances]
else: else:
choices = [e for e in get_starting_entrances(avail) if e not in forbidden and e in entrances] choices = [e for e in get_starting_entrances(avail) if e not in forbidden and e in entrances]
@@ -678,7 +678,7 @@ def do_links_house(entrances, exits, avail, cross_world):
forbidden.append(links_house_vanilla) forbidden.append(links_house_vanilla)
forbidden.extend(Forbidden_Swap_Entrances) forbidden.extend(Forbidden_Swap_Entrances)
shuffle_mode = avail.world.shuffle[avail.player] shuffle_mode = avail.world.shuffle[avail.player]
if avail.world.owShuffle[avail.player] == 'vanilla': if avail.world.owLayout[avail.player] == 'vanilla':
# simple shuffle - # simple shuffle -
if shuffle_mode == 'simple': if shuffle_mode == 'simple':
avail.links_on_mountain = True # taken care of by the logic below avail.links_on_mountain = True # taken care of by the logic below
@@ -732,7 +732,7 @@ def do_links_house(entrances, exits, avail, cross_world):
# links on dm # links on dm
dm_spots = LH_DM_Connector_List.union(LH_DM_Exit_Forbidden) dm_spots = LH_DM_Connector_List.union(LH_DM_Exit_Forbidden)
if links_house in dm_spots and avail.world.owShuffle[avail.player] == 'vanilla': if links_house in dm_spots and avail.world.owLayout[avail.player] == 'vanilla':
if avail.links_on_mountain: if avail.links_on_mountain:
return # connector is fine return # connector is fine
logging.getLogger('').warning(f'Links House is placed in tight area and is now unhandled. Report any errors that occur from here.') logging.getLogger('').warning(f'Links House is placed in tight area and is now unhandled. Report any errors that occur from here.')
File diff suppressed because it is too large Load Diff
+473
View File
@@ -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}")
+22 -12
View File
@@ -44,6 +44,11 @@ class DataTables:
self.ow_enemy_table = None self.ow_enemy_table = None
self.pot_secret_table = None self.pot_secret_table = None
self.overworld_sprite_sheets = 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 # associated data
self.sprite_requirements = None self.sprite_requirements = None
@@ -92,13 +97,6 @@ class DataTables:
# bank 0A uses 372A bytes # bank 0A uses 372A bytes
# bank 1F uses 77CE bytes: total is about a bank and a half # bank 1F uses 77CE bytes: total is about a bank and a half
# probably should reuse bank 1F if writing all the rooms out # 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(): for area_id, sheet in self.overworld_sprite_sheets.items():
if area_id in [0x80, 0x81]: if area_id in [0x80, 0x81]:
offset = area_id - 0x80 # 02E575 for special areas? offset = area_id - 0x80 # 02E575 for special areas?
@@ -110,7 +108,14 @@ class DataTables:
# _00FAC1 is LW post-aga # _00FAC1 is LW post-aga
# _00FB01 is DW # _00FB01 is DW
# _00FA41 is rain state # _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) 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(): for sprite, stats in self.enemy_stats.items():
# write health to rom # write health to rom
if stats.health is not None: if stats.health is not None:
@@ -148,13 +153,14 @@ class DataTables:
def write_ow_sprite_data_to_rom(self, rom): def write_ow_sprite_data_to_rom(self, rom):
# calculate how big this table is going to be? # 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 # 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) pointer_address = snes_to_pc(self.pointer_addresses['ow_sprites'][2][0])
# currently borrowed 10 bytes, used 9 (2xMurah + TutorialGuard) self.pointer_addresses['ow_sprites'][0] = pointer_address + ((max_per_state[0] + max_per_state[1] + max_per_state[2]) * 2)
data_pointer = snes_to_pc(0x09CB38) # was originally 0x09CB41 - stealing space for a couple extra sprites (Murahdahla, extra tutorial guard) data_pointer = self.pointer_addresses['ow_sprites'][0]
empty_pointer = pc_to_snes(data_pointer) & 0xFFFF empty_pointer = pc_to_snes(data_pointer) & 0xFFFF
rom.write_byte(data_pointer, 0xff) rom.write_byte(data_pointer, 0xff)
cached_dark_world = {} cached_dark_world = {}
@@ -187,6 +193,10 @@ class DataTables:
data_pointer += len(data) data_pointer += len(data)
rom.write_byte(data_pointer, 0xff) rom.write_byte(data_pointer, 0xff)
data_pointer += 1 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 = { special_health_table = {
+10 -5
View File
@@ -121,8 +121,16 @@ def roll_settings(weights):
ret.accessibility = get_choice('accessibility') ret.accessibility = get_choice('accessibility')
ret.restrict_boss_items = get_choice('restrict_boss_items') ret.restrict_boss_items = get_choice('restrict_boss_items')
overworld_layout = get_choice('overworld_layout')
ret.ow_layout = overworld_layout if overworld_layout != 'none' else 'vanilla'
ret.ow_parallel = get_choice_bool('overworld_parallel')
overworld_shuffle = get_choice('overworld_shuffle') overworld_shuffle = get_choice('overworld_shuffle')
ret.ow_shuffle = overworld_shuffle if overworld_shuffle != 'none' else 'vanilla' if overworld_shuffle == 'parallel':
ret.ow_layout = 'wild'
ret.ow_parallel = True
elif overworld_shuffle == 'full':
ret.ow_layout = 'wild'
ret.ow_parallel = False
ret.ow_terrain = get_choice_bool('overworld_terrain') ret.ow_terrain = get_choice_bool('overworld_terrain')
valid_options = {'none': 'none', 'polar': 'polar', 'grouped': 'polar', 'chaos': 'unrestricted', 'unrestricted': 'unrestricted'} valid_options = {'none': 'none', 'polar': 'polar', 'grouped': 'polar', 'chaos': 'unrestricted', 'unrestricted': 'unrestricted'}
ret.ow_crossed = get_choice('overworld_crossed') ret.ow_crossed = get_choice('overworld_crossed')
@@ -132,6 +140,7 @@ def roll_settings(weights):
ret.ow_whirlpool = get_choice_bool('whirlpool_shuffle') ret.ow_whirlpool = get_choice_bool('whirlpool_shuffle')
overworld_flute = get_choice('flute_shuffle') overworld_flute = get_choice('flute_shuffle')
ret.ow_fluteshuffle = overworld_flute if overworld_flute != 'none' else 'vanilla' 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.shuffle_followers = get_choice_bool('shuffle_followers')
ret.bonk_drops = get_choice_bool('bonk_drops') ret.bonk_drops = get_choice_bool('bonk_drops')
entrance_shuffle = get_choice('entrance_shuffle') 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.door_self_loops = get_choice_bool('door_self_loops')
ret.experimental = get_choice_bool('experimental') ret.experimental = get_choice_bool('experimental')
ret.collection_rate = get_choice_bool('collection_rate') ret.collection_rate = get_choice_bool('collection_rate')
ret.dungeon_counters = get_choice_non_bool('dungeon_counters') if 'dungeon_counters' in weights else 'default' 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.pseudoboots = get_choice_bool('pseudoboots')
ret.mirrorscroll = get_choice_bool('mirrorscroll') ret.mirrorscroll = get_choice_bool('mirrorscroll')
ret.shopsanity = get_choice_bool('shopsanity') ret.shopsanity = get_choice_bool('shopsanity')