Merge branch 'OverworldShuffleDev' into OverworldShuffle

This commit is contained in:
codemann8
2026-01-30 12:41:38 -06:00
10 changed files with 292 additions and 264 deletions

View File

@@ -1,5 +1,13 @@
# Changelog # Changelog
# 0.7.0.2
- Fixed money error for bombbag/take-anys
- Fixed palette issue for map check/flute menu
- Fixed issue with dungeon icons showing in opposite world in Inverted
- Fixed Links position in map checks when in Special OW areas
- Fixed buffered sword issue in OW Shuffle water transitions
- Added PlasmaKappa tribute TF room text
# 0.7.0.1 # 0.7.0.1
- Fixed buggy sprites in post-Aga Zora's Domain - Fixed buggy sprites in post-Aga Zora's Domain
- Fixed L/R map switch when in special OW screens - Fixed L/R map switch when in special OW screens

View File

@@ -1106,7 +1106,7 @@ def balance_money_progression(world):
slot = shop_to_location_table[location.parent_region.name].index(location.name) slot = shop_to_location_table[location.parent_region.name].index(location.name)
shop = location.parent_region.shop shop = location.parent_region.shop
shop_item = shop.inventory[slot] shop_item = shop.inventory[slot]
if location.item and interesting_item(location, location.item, world, location.item.player): if shop_item and location.item and interesting_item(location, location.item, world, location.item.player):
if location.item.name.startswith('Rupee') and loc_player == location.item.player: if location.item.name.startswith('Rupee') and loc_player == location.item.player:
if shop_item['price'] < rupee_chart[location.item.name]: if shop_item['price'] < rupee_chart[location.item.name]:
wallet[loc_player] -= shop_item['price'] # will get picked up in the location_free block wallet[loc_player] -= shop_item['price'] # will get picked up in the location_free block

View File

@@ -341,9 +341,10 @@ def generate_itempool(world, player):
world.escape_assist[player].append('bombs') world.escape_assist[player].append('bombs')
for (location, item) in placed_items.items(): for (location, item) in placed_items.items():
world.push_item(world.get_location(location, player), ItemFactory(item, player), False) loc = world.get_location(location, player)
world.get_location(location, player).event = True world.push_item(loc, ItemFactory(item, player), False)
world.get_location(location, player).locked = True loc.event = True
loc.locked = True
if world.shopsanity[player] and not skip_pool_adjustments: if world.shopsanity[player] and not skip_pool_adjustments:
for shop in world.shops[player]: for shop in world.shops[player]:

View File

@@ -17,7 +17,8 @@ from OverworldGlitchRules import create_owg_connections
from PotShuffle import shuffle_pots, shuffle_pot_switches from PotShuffle import shuffle_pots, shuffle_pot_switches
from Regions import create_regions, create_shops, mark_light_dark_world_regions, create_dungeon_regions, adjust_locations from Regions import create_regions, create_shops, mark_light_dark_world_regions, create_dungeon_regions, adjust_locations
from OWEdges import create_owedges from OWEdges import create_owedges
from OverworldShuffle import link_overworld, update_world_regions, create_dynamic_flute_exits, create_dynamic_mirror_exits from OverworldShuffle import link_overworld, update_world_regions, create_dynamic_mirror_exits
from source.overworld.FluteShuffle import create_dynamic_flute_exits
from Rom import patch_rom, patch_race_rom, apply_rom_settings, LocalRom, JsonRom, get_hash_string from Rom import patch_rom, patch_race_rom, apply_rom_settings, LocalRom, JsonRom, get_hash_string
from Doors import create_doors from Doors import create_doors
from DoorShuffle import link_doors, connect_portal, link_doors_prep from DoorShuffle import link_doors, connect_portal, link_doors_prep

View File

@@ -4,11 +4,12 @@ from DungeonGenerator import GenerationException
from BaseClasses import OWEdge, WorldType, RegionType, Direction, Terrain, PolSlot, Entrance from BaseClasses import OWEdge, WorldType, RegionType, Direction, Terrain, PolSlot, Entrance
from Regions import mark_light_dark_world_regions from Regions import mark_light_dark_world_regions
from source.overworld.EntranceShuffle2 import connect_simple from source.overworld.EntranceShuffle2 import connect_simple
from source.overworld.FluteShuffle import shuffle_flute_spots, default_flute_connections, flute_data
from OWEdges import OWTileRegions, OWEdgeGroups, OWEdgeGroupsTerrain, OWExitTypes, OpenStd, parallel_links, IsParallel from OWEdges import OWTileRegions, OWEdgeGroups, OWEdgeGroupsTerrain, OWExitTypes, OpenStd, parallel_links, IsParallel
from OverworldGlitchRules import create_owg_connections from OverworldGlitchRules import create_owg_connections
from Utils import bidict from Utils import bidict
version_number = '0.7.0.1' version_number = '0.7.0.2'
# branch indicator is intentionally different across branches # branch indicator is intentionally different across branches
version_branch = '' version_branch = ''
@@ -336,6 +337,7 @@ def link_overworld(world, player):
world.owcrossededges[player].extend(edge_set) world.owcrossededges[player].extend(edge_set)
assert len(world.owcrossededges[player]) == len(set(world.owcrossededges[player])), "Same edge candidate added to crossed edges" assert len(world.owcrossededges[player]) == len(set(world.owcrossededges[player])), "Same edge candidate added to crossed edges"
# TODO: Someday don't force parallels also? Keeping for now to ensure full reachability
for edge in copy.deepcopy(world.owcrossededges[player]): for edge in copy.deepcopy(world.owcrossededges[player]):
if edge in parallel_links_new: if edge in parallel_links_new:
if parallel_links_new[edge] not in world.owcrossededges[player]: if parallel_links_new[edge] not in world.owcrossededges[player]:
@@ -574,169 +576,8 @@ def link_overworld(world, player):
# flute shuffle # flute shuffle
logging.getLogger('').debug('Shuffling flute spots') logging.getLogger('').debug('Shuffling flute spots')
def connect_flutes(flute_destinations): shuffle_flute_spots(world, player)
for o in range(0, len(flute_destinations)):
owid = flute_destinations[o]
regions = flute_data[owid][0]
if not world.is_tile_swapped(owid, player):
connect_simple(world, 'Flute Spot ' + str(o + 1), regions[0], player)
else:
connect_simple(world, 'Flute Spot ' + str(o + 1), regions[1], player)
if world.owFluteShuffle[player] == 'vanilla':
flute_spots = default_flute_connections.copy()
sort_flute_spots(world, player, flute_spots)
world.owflutespots[player] = flute_spots
connect_flutes(flute_spots)
else:
flute_spots = 8
flute_pool = list(flute_data.keys())
new_spots = list()
ignored_regions = set()
used_flute_regions = []
forbidden_spots = []
forbidden_regions = []
def addSpot(owid, ignore_proximity, forced):
if world.owFluteShuffle[player] == 'balanced':
def getIgnored(regionname, base_owid, owid):
region = world.get_region(regionname, player)
for exit in region.exits:
if exit.connected_region is not None and exit.connected_region.type in [RegionType.LightWorld, RegionType.DarkWorld] and exit.connected_region.name not in new_ignored:
if exit.connected_region.name in OWTileRegions and (OWTileRegions[exit.connected_region.name] in [base_owid, owid] or OWTileRegions[regionname] == base_owid):
new_ignored.add(exit.connected_region.name)
getIgnored(exit.connected_region.name, base_owid, OWTileRegions[exit.connected_region.name])
if regionname in one_way_ledges:
for ledge_region in one_way_ledges[regionname]:
if ledge_region not in new_ignored:
new_ignored.add(ledge_region)
getIgnored(ledge_region, base_owid, OWTileRegions[ledge_region])
if not world.is_tile_swapped(owid, player):
new_region = flute_data[owid][0][0]
else:
new_region = flute_data[owid][0][1]
if new_region in ignored_regions and not forced:
return False
new_ignored = {new_region}
getIgnored(new_region, OWTileRegions[new_region], OWTileRegions[new_region])
if not ignore_proximity and not forced and random.randint(0, 31) != 0 and new_ignored.intersection(ignored_regions):
return False
ignored_regions.update(new_ignored)
if owid in flute_pool:
flute_pool.remove(owid)
if ignore_proximity and not forced:
logging.getLogger('').warning(f'Warning: Adding flute spot within proximity: {hex(owid)}')
logging.getLogger('').debug(f'Placing flute at: {hex(owid)}')
new_spots.append(owid)
else:
# TODO: Inspect later, seems to happen only with 'random' flute shuffle
logging.getLogger('').warning(f'Warning: Attempted to place flute spot not in pool: {hex(owid)}')
return True
if world.customizer:
custom_spots = world.customizer.get_owflutespots()
if custom_spots and player in custom_spots:
if 'force' in custom_spots[player]:
for id in custom_spots[player]['force']:
owid = id & 0xBF
addSpot(owid, True, True)
flute_spots -= 1
if not world.is_tile_swapped(owid, player):
used_flute_regions.append(flute_data[owid][0][0])
else:
used_flute_regions.append(flute_data[owid][0][1])
if 'forbid' in custom_spots[player]:
for id in custom_spots[player]['forbid']:
owid = id & 0xBF
if owid not in new_spots:
forbidden_spots.append(owid)
if not world.is_tile_swapped(owid, player):
forbidden_regions.append(flute_data[owid][0][0])
else:
forbidden_regions.append(flute_data[owid][0][1])
# determine sectors (isolated groups of regions) to place flute spots
flute_regions = {(f[0][0] if (o not in world.owswaps[player][0]) != (world.mode[player] == 'inverted') else f[0][1]) : o for o, f in flute_data.items() if o not in new_spots and o not in forbidden_spots}
flute_sectors = [(len([r for l in s for r in l]), [r for l in s for r in l if r in flute_regions]) for s in world.owsectors[player]]
flute_sectors = [s for s in flute_sectors if len(s[1]) > 0]
region_total = sum([c for c,_ in flute_sectors])
sector_total = len(flute_sectors)
empty_sector_total = 0
sector_has_spot = []
# determine which sectors still need a flute spot
for sector in flute_sectors:
already_has_spot = any(region in sector for region in used_flute_regions)
sector_has_spot.append(already_has_spot)
if not already_has_spot:
empty_sector_total += 1
if flute_spots < empty_sector_total:
logging.getLogger('').warning(f'Warning: Not every sector can have a flute spot, generation might fail')
# pretend like some of the empty sectors already have a flute spot, don't know if they will be reachable
for i in range(len(flute_sectors)):
if not sector_has_spot[i]:
sector_has_spot[i] = True
empty_sector_total -= 1
if flute_spots == empty_sector_total:
break
# distribute flute spots for each sector
for i in range(len(flute_sectors)):
sector = flute_sectors[i]
sector_total -= 1
if not sector_has_spot[i]:
empty_sector_total -= 1
spots_to_place = min(flute_spots - empty_sector_total, max(0 if sector_has_spot[i] else 1, round((sector[0] * (flute_spots - sector_total) / region_total) + 0.5)))
target_spots = len(new_spots) + spots_to_place
logging.getLogger('').debug(f'Sector of {sector[0]} regions gets {spots_to_place} spot(s)')
if 0x30 in flute_pool and 0x30 not in forbidden_spots and len(new_spots) < target_spots and ('Desert Teleporter Ledge' in sector[1] or 'Mire Teleporter Ledge' in sector[1]):
addSpot(0x30, True, True) # guarantee desert/mire access
random.shuffle(sector[1])
f = 0
t = 0
while len(new_spots) < target_spots:
if f >= len(sector[1]):
f = 0
t += 1
if t > 5:
raise GenerationException('Infinite loop detected in flute shuffle')
owid = flute_regions[sector[1][f]]
if owid not in new_spots and owid not in forbidden_spots:
addSpot(owid, t > 0, False)
f += 1
region_total -= sector[0]
flute_spots -= spots_to_place
# connect new flute spots
sort_flute_spots(world, player, new_spots)
world.owflutespots[player] = new_spots
connect_flutes(new_spots)
# update spoiler
s = list(map(lambda x: ' ' if x not in new_spots else 'F', [i for i in range(0x40)]))
text_output = flute_spoiler_table.replace('s', '%s') % ( s[0x02], s[0x07],
s[0x00], s[0x03], s[0x05],
s[0x00], s[0x02],s[0x03], s[0x05], s[0x07], s[0x0a], s[0x0f],
s[0x0a], s[0x0f],
s[0x10],s[0x11],s[0x12],s[0x13],s[0x14],s[0x15],s[0x16],s[0x17], s[0x10],s[0x11],s[0x12],s[0x13],s[0x14],s[0x15],s[0x16],s[0x17],
s[0x18], s[0x1a],s[0x1b], s[0x1d],s[0x1e],
s[0x22], s[0x25], s[0x1a], s[0x1d],
s[0x28],s[0x29],s[0x2a],s[0x2b],s[0x2c],s[0x2d],s[0x2e],s[0x2f], s[0x18], s[0x1b], s[0x1e],
s[0x30], s[0x32],s[0x33],s[0x34],s[0x35], s[0x37], s[0x22], s[0x25],
s[0x3a],s[0x3b],s[0x3c], s[0x3f],
s[0x28],s[0x29],s[0x2a],s[0x2b],s[0x2c],s[0x2d],s[0x2e],s[0x2f],
s[0x32],s[0x33],s[0x34], s[0x37],
s[0x30], s[0x35],
s[0x3a],s[0x3b],s[0x3c], s[0x3f])
world.spoiler.set_map('flute', text_output, new_spots, player)
create_dynamic_flute_exits(world, player)
def connect_custom(world, connected_edges, groups, forced, player): def connect_custom(world, connected_edges, groups, forced, player):
forced_crossed, forced_noncrossed = forced forced_crossed, forced_noncrossed = forced
@@ -1312,28 +1153,6 @@ 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):
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:
return
for region in (r for r in world.regions if r.player == player and r.terrain == Terrain.Land and r.name not in ['Zoras Domain', 'Master Sword Meadow', 'Hobo Bridge']):
if region.type == (RegionType.LightWorld if world.mode[player] != 'inverted' else RegionType.DarkWorld):
exitname = 'Flute From ' + region.name
exit = Entrance(region.player, exitname, region)
exit.spot_type = 'Flute'
exit.connect(world.get_region('Flute Sky', player))
region.exits.append(exit)
world.initialize_regions()
def get_mirror_exit_name(from_region, to_region): def get_mirror_exit_name(from_region, to_region):
if from_region in mirror_connections and to_region in mirror_connections[from_region]: if from_region in mirror_connections and to_region in mirror_connections[from_region]:
if len(mirror_connections[from_region]) == 1: if len(mirror_connections[from_region]) == 1:
@@ -1898,10 +1717,6 @@ default_whirlpool_connections = [
((0x35, 'Lake Hylia Whirlpool', 'Lake Hylia Water'), (0x0f, 'Zora Whirlpool', 'Zora Waterfall Water')), ((0x35, 'Lake Hylia Whirlpool', 'Lake Hylia Water'), (0x0f, 'Zora Whirlpool', 'Zora Waterfall Water')),
((0x12, 'Kakariko Pond Whirlpool', 'Kakariko Pond Area'), (0x3f, 'Octoballoon Whirlpool', 'Octoballoon Water')), ((0x12, 'Kakariko Pond Whirlpool', 'Kakariko Pond Area'), (0x3f, 'Octoballoon Whirlpool', 'Octoballoon Water')),
((0x55, 'Qirn Jump Whirlpool', 'Qirn Jump Water'), (0x7f, 'Bomber Corner Whirlpool', 'Bomber Corner Water')) ((0x55, 'Qirn Jump Whirlpool', 'Qirn Jump Water'), (0x7f, 'Bomber Corner Whirlpool', 'Bomber Corner Water'))
]
default_flute_connections = [
0x03, 0x16, 0x18, 0x2c, 0x2f, 0x30, 0x3b, 0x3f
] ]
ow_connections = { ow_connections = {
@@ -2569,51 +2384,6 @@ isolated_regions = [
'Pyramid Water' 'Pyramid Water'
] ]
flute_data = {
#OWID LW Region DW Region Slot VRAM BG Y BG X Link Y Link X Cam Y Cam X Unk1 Unk2 IconY IconX AltY AltX AltVRAM AltBGY AltBGX AltCamY AltCamX AltUnk1 AltUnk2 AltIconY AltIconX
0x00: (['Lost Woods East Area', 'Skull Woods Forest'], 0x09, 0x1042, 0x022e, 0x0202, 0x0290, 0x0288, 0x029b, 0x028f, 0xfff2, 0x000e, 0x0290, 0x0288, 0x0290, 0x0290),
0x02: (['Lumberjack Area', 'Dark Lumberjack Area'], 0x02, 0x059c, 0x00d6, 0x04e6, 0x0138, 0x0558, 0x0143, 0x0563, 0xfffa, 0xfffa, 0x01d8, 0x0518),
0x03: (['West Death Mountain (Bottom)', 'West Dark Death Mountain (Top)'], 0x0b, 0x1600, 0x02ca, 0x060e, 0x0328, 0x0678, 0x0337, 0x0683, 0xfff6, 0xfff2, 0x03bb, 0x0680, 0x0118, 0x0860, 0x05c0, 0x00b8, 0x07ec, 0x0127, 0x086b, 0xfff8, 0x0004, 0x0148, 0x0850),
0x05: (['East Death Mountain (Bottom)', 'East Dark Death Mountain (Bottom)'], 0x0e, 0x1860, 0x031e, 0x0d00, 0x0388, 0x0da8, 0x038d, 0x0d7d, 0x0000, 0x0000, 0x03c8, 0x0d98),
0x07: (['Death Mountain TR Pegs Area', 'Turtle Rock Area'], 0x07, 0x0804, 0x0102, 0x0e1a, 0x0160, 0x0e90, 0x016f, 0x0e97, 0xfffe, 0x0006, 0x0150, 0x0ea0),
0x0a: (['Mountain Pass Area', 'Bumper Cave Area'], 0x0a, 0x0180, 0x0220, 0x0406, 0x0280, 0x0488, 0x028f, 0x0493, 0x0000, 0xfffa, 0x0390, 0x04d8),
0x0f: (['Zora Waterfall Area', 'Catfish Area'], 0x0f, 0x0316, 0x025c, 0x0eb2, 0x02c0, 0x0f28, 0x02cb, 0x0f2f, 0x0002, 0xfffe, 0x0360, 0x0f58),
0x10: (['Lost Woods Pass West Area', 'Skull Woods Pass West Area'], 0x10, 0x0080, 0x0400, 0x0000, 0x0448, 0x0058, 0x046f, 0x0085, 0x0000, 0x0000, 0x04f8, 0x0088),
0x11: (['Kakariko Fortune Area', 'Dark Fortune Area'], 0x11, 0x0912, 0x051e, 0x0292, 0x0588, 0x0318, 0x058d, 0x031f, 0x0000, 0xfffe, 0x05f8, 0x0318),
0x12: (['Kakariko Pond Area', 'Outcast Pond Area'], 0x12, 0x0890, 0x051a, 0x0476, 0x0578, 0x04f8, 0x0587, 0x0503, 0xfff6, 0x000a, 0x05b8, 0x04f8),
0x13: (['Sanctuary Area', 'Dark Chapel Area'], 0x13, 0x051c, 0x04aa, 0x06de, 0x0508, 0x0758, 0x0517, 0x0763, 0xfff6, 0x0002, 0x05b8, 0x0738),
0x14: (['Graveyard Area', 'Dark Graveyard Area'], 0x14, 0x089c, 0x051e, 0x08e6, 0x0580, 0x0958, 0x058b, 0x0963, 0x0000, 0xfffa, 0x05f0, 0x0918, 0x0580, 0x0948),
0x15: (['River Bend East Bank', 'Qirn Jump East Bank'], 0x15, 0x041a, 0x0486, 0x0ad2, 0x04e8, 0x0b48, 0x04f3, 0x0b4f, 0x0008, 0xfffe, 0x0548, 0x0b78),
0x16: (['Potion Shop Area', 'Dark Witch Area'], 0x16, 0x0888, 0x0516, 0x0c4e, 0x0578, 0x0cc8, 0x0583, 0x0cd3, 0xfffa, 0xfff2, 0x05e8, 0x0c9f),
0x17: (['Zora Approach Ledge', 'Catfish Approach Ledge'], 0x17, 0x039e, 0x047e, 0x0ef2, 0x04e0, 0x0f68, 0x04eb, 0x0f6f, 0x0000, 0xfffe, 0x0580, 0x0f48),
0x18: (['Kakariko Village', 'Village of Outcasts'], 0x18, 0x0b30, 0x0759, 0x017e, 0x07b7, 0x0200, 0x07c6, 0x020b, 0x0007, 0x0002, 0x0830, 0x0240, 0x07c8, 0x01f8),
0x1a: (['Forgotten Forest Area', 'Shield Shop Fence'], 0x1a, 0x081a, 0x070f, 0x04d2, 0x0770, 0x0548, 0x077c, 0x054f, 0xffff, 0xfffe, 0x0770, 0x0518),
0x1b: (['Hyrule Castle Courtyard', 'Pyramid Area'], 0x1b, 0x0c30, 0x077a, 0x0786, 0x07d8, 0x07f8, 0x07e7, 0x0803, 0x0006, 0xfffa, 0x07f8, 0x07f8),
0x1d: (['Wooden Bridge Area', 'Broken Bridge Northeast'], 0x1d, 0x0602, 0x06c2, 0x0a0e, 0x0720, 0x0a80, 0x072f, 0x0a8b, 0xfffe, 0x0002, 0x0750, 0x0a70),
0x1e: (['Eastern Palace Area', 'Palace of Darkness Area'], 0x26, 0x1802, 0x091e, 0x0c0e, 0x09c0, 0x0c80, 0x098b, 0x0c8b, 0x0000, 0x0002, 0x09a0, 0x0cb0),
0x22: (['Blacksmith Area', 'Hammer Pegs Area'], 0x22, 0x058c, 0x08aa, 0x0462, 0x0908, 0x04d8, 0x0917, 0x04df, 0x0006, 0xfffe, 0x0978, 0x04e8),
0x25: (['Sand Dunes Area', 'Dark Dunes Area'], 0x25, 0x030e, 0x085a, 0x0a76, 0x08b8, 0x0ae8, 0x08c7, 0x0af3, 0x0006, 0xfffa, 0x0918, 0x0b18),
0x28: (['Maze Race Area', 'Dig Game Area'], 0x28, 0x0908, 0x0b1e, 0x003a, 0x0b88, 0x00b8, 0x0b8d, 0x00bf, 0x0000, 0x0006, 0x0ba8, 0x00b8),
0x29: (['Kakariko Suburb Area', 'Frog Area'], 0x29, 0x0408, 0x0a7c, 0x0242, 0x0ae0, 0x02c0, 0x0aeb, 0x02c7, 0x0002, 0xfffe, 0x0b30, 0x02e0),
0x2a: (['Flute Boy Area', 'Stumpy Area'], 0x2a, 0x058e, 0x0aac, 0x046e, 0x0b10, 0x04e8, 0x0b1b, 0x04f3, 0x0002, 0x0002, 0x0b60, 0x04f8),
0x2b: (['Central Bonk Rocks Area', 'Dark Bonk Rocks Area'], 0x2b, 0x0620, 0x0acc, 0x0700, 0x0b30, 0x0790, 0x0b3b, 0x0785, 0xfff2, 0x0000, 0x0b80, 0x0760),
0x2c: (['Links House Area', 'Big Bomb Shop Area'], 0x2c, 0x0588, 0x0ab9, 0x0840, 0x0b17, 0x08b8, 0x0b26, 0x08bf, 0xfff7, 0x0000, 0x0bb0, 0x08a8),
0x2d: (['Stone Bridge South Area', 'Hammer Bridge South Area'], 0x2d, 0x0886, 0x0b1e, 0x0a2a, 0x0ba0, 0x0aa8, 0x0b8b, 0x0aaf, 0x0000, 0x0006, 0x0bf0, 0x0ab8),
0x2e: (['Tree Line Area', 'Dark Tree Line Area'], 0x2e, 0x0100, 0x0a1a, 0x0c00, 0x0a78, 0x0c30, 0x0a87, 0x0c7d, 0x0006, 0x0000, 0x0ac8, 0x0c70),
0x2f: (['Eastern Nook Area', 'Darkness Nook Area'], 0x2f, 0x0798, 0x0afa, 0x0eb2, 0x0b58, 0x0f30, 0x0b67, 0x0f37, 0xfff6, 0x000e, 0x0bc0, 0x0f00),
0x30: (['Desert Teleporter Ledge', 'Mire Teleporter Ledge'], 0x38, 0x1880, 0x0f1e, 0x0000, 0x0fa8, 0x0078, 0x0f8d, 0x008d, 0x0000, 0x0000, 0x0ff0, 0x0070),
0x32: (['Flute Boy Approach Area', 'Stumpy Approach Area'], 0x32, 0x03a0, 0x0c6c, 0x0500, 0x0cd0, 0x05a8, 0x0cdb, 0x0585, 0x0002, 0x0000, 0x0d00, 0x0528),
0x33: (['C Whirlpool Outer Area', 'Dark C Whirlpool Outer Area'], 0x33, 0x0180, 0x0c20, 0x0600, 0x0c80, 0x0628, 0x0c8f, 0x067d, 0x0000, 0x0000, 0x0ce0, 0x0688),
0x34: (['Statues Area', 'Hype Cave Area'], 0x34, 0x088e, 0x0d00, 0x0866, 0x0d60, 0x08d8, 0x0d6f, 0x08e3, 0x0000, 0x000a, 0x0dd0, 0x08e8),
#0x35: (['Lake Hylia Northwest Bank', 'Ice Lake Northwest Bank'], 0x35, 0x0d00, 0x0da6, 0x0a06, 0x0e08, 0x0a80, 0x0e13, 0x0a8b, 0xfffa, 0xfffa, 0x0dc8, 0x0a90),
0x35: (['Lake Hylia South Shore', 'Ice Lake Southeast Ledge'], 0x3e, 0x1860, 0x0f1e, 0x0d00, 0x0f98, 0x0da8, 0x0f8b, 0x0d85, 0x0000, 0x0000, 0x0fd8, 0x0da8),
0x37: (['Ice Cave Area', 'Shopping Mall Area'], 0x37, 0x0786, 0x0cf6, 0x0e2e, 0x0d58, 0x0ea0, 0x0d63, 0x0eab, 0x000a, 0x0002, 0x0d98, 0x0ed0),
0x3a: (['Desert Pass Area', 'Swamp Nook Area'], 0x3a, 0x001a, 0x0e08, 0x04c6, 0x0e70, 0x0540, 0x0e7d, 0x054b, 0x0006, 0x000a, 0x0ee0, 0x0570),
0x3b: (['Dam Area', 'Swamp Area'], 0x3b, 0x069e, 0x0edf, 0x06f2, 0x0f3d, 0x0778, 0x0f4c, 0x077f, 0xfff1, 0xfffe, 0x0fd0, 0x0770),
0x3c: (['South Pass Area', 'Dark South Pass Area'], 0x3c, 0x0584, 0x0ed0, 0x081e, 0x0f38, 0x0898, 0x0f45, 0x08a3, 0xfffe, 0x0002, 0x0fa8, 0x0898),
0x3f: (['Octoballoon Area', 'Bomber Corner Area'], 0x3f, 0x0810, 0x0f05, 0x0e75, 0x0f67, 0x0ef3, 0x0f72, 0x0efa, 0xfffb, 0x000b, 0x0fd0, 0x0ef0)
}
ow_loc_prize_table = { ow_loc_prize_table = {
'Master Sword Pedestal': (0x06d, 0x070), 'Master Sword Pedestal': (0x06d, 0x070),
'Hobo': (0xb80, 0xb90), 'Hobo': (0xb80, 0xb90),
@@ -2694,23 +2464,3 @@ H(38)| sss s| +-+-+-+-+-+-+-+-+
+-+ | s +-+-+-+ s +-+ +-+ | s +-+-+-+ s +-+
Zora: |s| H(38)| |s|s|s| |s| Zora: |s| H(38)| |s|s|s| |s|
+-+ +---+-+-+-+---+-+""" +-+ +---+-+-+-+---+-+"""
flute_spoiler_table = \
""" 0 1 2 3 4 5 6 7
+---+-+---+---+-+
01234567 A(00)| |s| | |s|
+--------+ | s +-+ s | s +-+
A(00)|s ss s s| B(08)| |s| | |s|
B(08)| s s| +-+-+-+-+-+-+-+-+
C(10)|ssssssss| C(10)|s|s|s|s|s|s|s|s|
D(18)|s ss ss | +-+-+-+-+-+-+-+-+
E(20)| s s | D(18)| |s| |s| |
F(28)|ssssssss| | s +-+ s +-+ s |
G(30)|s ssss s| E(20)| |s| |s| |
H(38)| sss s| +-+-+-+-+-+-+-+-+
+--------+ F(28)|s|s|s|s|s|s|s|s|
+-+-+-+-+-+-+-+-+
G(30)| |s|s|s| |s|
| s +-+-+-+ s +-+
H(38)| |s|s|s| |s|
+---+-+-+-+---+-+"""

9
Rom.py
View File

@@ -31,7 +31,7 @@ from Utils import local_path, int16_as_bytes, int32_as_bytes, snes_to_pc
from Items import ItemFactory, prize_item_table from Items import ItemFactory, prize_item_table
from source.overworld.EntranceData import door_addresses, ow_prize_table from source.overworld.EntranceData import door_addresses, ow_prize_table
from source.overworld.EntranceShuffle2 import exit_ids from source.overworld.EntranceShuffle2 import exit_ids
from OverworldShuffle import default_flute_connections, flute_data from source.overworld.FluteShuffle import default_flute_connections, flute_data
from InitialSram import InitialSram from InitialSram import InitialSram
from source.classes.SFX import randomize_sfx, randomize_sfxinstruments, randomize_songinstruments from source.classes.SFX import randomize_sfx, randomize_sfxinstruments, randomize_songinstruments
@@ -43,7 +43,7 @@ from source.enemizer.Enemizer import write_enemy_shuffle_settings
JAP10HASH = '03a63945398191337e896e5771f77173' JAP10HASH = '03a63945398191337e896e5771f77173'
RANDOMIZERBASEHASH = '01d81a1ad16b1838b27a057b8f8936d4' RANDOMIZERBASEHASH = 'da081467317d8f6902eb1cf57304dbff'
class JsonRom(object): class JsonRom(object):
@@ -518,7 +518,8 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
else: else:
flute_spots = world.owflutespots[player] flute_spots = world.owflutespots[player]
owFlags |= 0x0100 owFlags |= 0x0100
write_int16(rom, snes_to_pc(0x0AB7F7), 0xEAEA) if world.owFluteShuffle[player] != 'vanilla':
write_int16(rom, snes_to_pc(0x0AB80B), 0xEAEA)
flute_writes = [(f, flute_data[f][1]) for f in flute_spots] 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)):
@@ -1457,7 +1458,7 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
map_x = x_map_position[idx] map_x = x_map_position[idx]
map_y = y_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) == (owid in world.owswaps[player][0]):
coord_flags |= 0x8000 # world indicator flag coord_flags |= 0x8000 # world indicator flag
if coord_flags & 0x4000 == 0: if coord_flags & 0x4000 == 0:
map_x, map_y = adjust_ow_coordinates_to_layout(world, player, map_x, map_y, coord_flags & 0x8000 != 0) map_x, map_y = adjust_ow_coordinates_to_layout(world, player, map_x, map_y, coord_flags & 0x8000 != 0)

View File

@@ -134,6 +134,7 @@ Triforce_texts = [
" I promise the\n next seed will\n be better.", " I promise the\n next seed will\n be better.",
"\n Honk.", "\n Honk.",
" Breakfast\n is served!", " Breakfast\n is served!",
"\n send help",
] ]
BombShop2_texts = ['Bombs!\nBombs!\nBiggest!\nBestest!\nGreatest!\nBoomest!'] BombShop2_texts = ['Bombs!\nBombs!\nBiggest!\nBestest!\nGreatest!\nBoomest!']
Sahasrahla2_texts = ['You already got my item, idiot.', 'Why are you still talking to me?', 'This text won\'t change.', 'Have you met my brother, Hasarahshla?'] Sahasrahla2_texts = ['You already got my item, idiot.', 'Why are you still talking to me?', 'This text won\'t change.', 'Have you met my brother, Hasarahshla?']

Binary file not shown.

View File

@@ -10,7 +10,7 @@ from pathlib import Path
import RaceRandom as random import RaceRandom as random
from BaseClasses import LocationType, DoorType from BaseClasses import LocationType, DoorType
from OverworldShuffle import default_flute_connections, flute_data from source.overworld.FluteShuffle import default_flute_connections, flute_data
from source.tools.MysteryUtils import roll_settings, get_weights from source.tools.MysteryUtils import roll_settings, get_weights

View File

@@ -0,0 +1,266 @@
import RaceRandom as random, logging, copy
from BaseClasses import Entrance, RegionType, Terrain
from source.overworld.EntranceShuffle2 import connect_simple
from OWEdges import OWTileRegions
from DungeonGenerator import GenerationException
def shuffle_flute_spots(world, player):
def connect_flutes(flute_destinations):
for o in range(0, len(flute_destinations)):
owid = flute_destinations[o]
regions = flute_data[owid][0]
if not world.is_tile_swapped(owid, player):
connect_simple(world, 'Flute Spot ' + str(o + 1), regions[0], player)
else:
connect_simple(world, 'Flute Spot ' + str(o + 1), regions[1], player)
if world.owFluteShuffle[player] == 'vanilla':
flute_spots = default_flute_connections.copy()
sort_flute_spots(world, player, flute_spots)
world.owflutespots[player] = flute_spots
connect_flutes(flute_spots)
else:
from OverworldShuffle import one_way_ledges
flute_spots = 8
flute_pool = list(flute_data.keys())
new_spots = list()
ignored_regions = set()
used_flute_regions = []
forbidden_spots = []
forbidden_regions = []
def addSpot(owid, ignore_proximity, forced):
if world.owFluteShuffle[player] == 'balanced':
def getIgnored(regionname, base_owid, owid):
region = world.get_region(regionname, player)
for exit in region.exits:
if exit.connected_region is not None and exit.connected_region.type in [RegionType.LightWorld, RegionType.DarkWorld] and exit.connected_region.name not in new_ignored:
if exit.connected_region.name in OWTileRegions and (OWTileRegions[exit.connected_region.name] in [base_owid, owid] or OWTileRegions[regionname] == base_owid):
new_ignored.add(exit.connected_region.name)
getIgnored(exit.connected_region.name, base_owid, OWTileRegions[exit.connected_region.name])
if regionname in one_way_ledges:
for ledge_region in one_way_ledges[regionname]:
if ledge_region not in new_ignored:
new_ignored.add(ledge_region)
getIgnored(ledge_region, base_owid, OWTileRegions[ledge_region])
if not world.is_tile_swapped(owid, player):
new_region = flute_data[owid][0][0]
else:
new_region = flute_data[owid][0][1]
if new_region in ignored_regions and not forced:
return False
new_ignored = {new_region}
getIgnored(new_region, OWTileRegions[new_region], OWTileRegions[new_region])
if not ignore_proximity and not forced and random.randint(0, 31) != 0 and new_ignored.intersection(ignored_regions):
return False
ignored_regions.update(new_ignored)
if owid in flute_pool:
flute_pool.remove(owid)
if ignore_proximity and not forced:
logging.getLogger('').warning(f'Warning: Adding flute spot within proximity: {hex(owid)}')
logging.getLogger('').debug(f'Placing flute at: {hex(owid)}')
new_spots.append(owid)
else:
# TODO: Inspect later, seems to happen only with 'random' flute shuffle
logging.getLogger('').warning(f'Warning: Attempted to place flute spot not in pool: {hex(owid)}')
return True
if world.customizer:
custom_spots = world.customizer.get_owflutespots()
if custom_spots and player in custom_spots:
if 'force' in custom_spots[player]:
for id in custom_spots[player]['force']:
owid = id & 0xBF
addSpot(owid, True, True)
flute_spots -= 1
if not world.is_tile_swapped(owid, player):
used_flute_regions.append(flute_data[owid][0][0])
else:
used_flute_regions.append(flute_data[owid][0][1])
if 'forbid' in custom_spots[player]:
for id in custom_spots[player]['forbid']:
owid = id & 0xBF
if owid not in new_spots:
forbidden_spots.append(owid)
if not world.is_tile_swapped(owid, player):
forbidden_regions.append(flute_data[owid][0][0])
else:
forbidden_regions.append(flute_data[owid][0][1])
# determine sectors (isolated groups of regions) to place flute spots
flute_regions = {(f[0][0] if (o not in world.owswaps[player][0]) != (world.mode[player] == 'inverted') else f[0][1]) : o for o, f in flute_data.items() if o not in new_spots and o not in forbidden_spots}
flute_sectors = [(len([r for l in s for r in l]), [r for l in s for r in l if r in flute_regions]) for s in world.owsectors[player]]
flute_sectors = [s for s in flute_sectors if len(s[1]) > 0]
region_total = sum([c for c,_ in flute_sectors])
sector_total = len(flute_sectors)
empty_sector_total = 0
sector_has_spot = []
# determine which sectors still need a flute spot
for sector in flute_sectors:
already_has_spot = any(region in sector for region in used_flute_regions)
sector_has_spot.append(already_has_spot)
if not already_has_spot:
empty_sector_total += 1
if flute_spots < empty_sector_total:
logging.getLogger('').warning(f'Warning: Not every sector can have a flute spot, generation might fail')
# pretend like some of the empty sectors already have a flute spot, don't know if they will be reachable
for i in range(len(flute_sectors)):
if not sector_has_spot[i]:
sector_has_spot[i] = True
empty_sector_total -= 1
if flute_spots == empty_sector_total:
break
# distribute flute spots for each sector
for i in range(len(flute_sectors)):
sector = flute_sectors[i]
sector_total -= 1
if not sector_has_spot[i]:
empty_sector_total -= 1
spots_to_place = min(flute_spots - empty_sector_total, max(0 if sector_has_spot[i] else 1, round((sector[0] * (flute_spots - sector_total) / region_total) + 0.5)))
target_spots = len(new_spots) + spots_to_place
logging.getLogger('').debug(f'Sector of {sector[0]} regions gets {spots_to_place} spot(s)')
if 0x30 in flute_pool and 0x30 not in forbidden_spots and len(new_spots) < target_spots and ('Desert Teleporter Ledge' in sector[1] or 'Mire Teleporter Ledge' in sector[1]):
addSpot(0x30, True, True) # guarantee desert/mire access
random.shuffle(sector[1])
f = 0
t = 0
while len(new_spots) < target_spots:
if f >= len(sector[1]):
f = 0
t += 1
if t > 5:
raise GenerationException('Infinite loop detected in flute shuffle')
owid = flute_regions[sector[1][f]]
if owid not in new_spots and owid not in forbidden_spots:
addSpot(owid, t > 0, False)
f += 1
region_total -= sector[0]
flute_spots -= spots_to_place
# connect new flute spots
sort_flute_spots(world, player, new_spots)
world.owflutespots[player] = new_spots
connect_flutes(new_spots)
# update spoiler
#new_spots = list(map(lambda o: flute_data[o][1], new_spots))
s = list(map(lambda x: ' ' if x not in new_spots else 'F', [i for i in range(0x40)]))
text_output = flute_spoiler_table.replace('s', '%s') % ( s[0x02], s[0x07],
s[0x00], s[0x03], s[0x05],
s[0x00], s[0x02],s[0x03], s[0x05], s[0x07], s[0x0a], s[0x0f],
s[0x0a], s[0x0f],
s[0x10],s[0x11],s[0x12],s[0x13],s[0x14],s[0x15],s[0x16],s[0x17], s[0x10],s[0x11],s[0x12],s[0x13],s[0x14],s[0x15],s[0x16],s[0x17],
s[0x18], s[0x1a],s[0x1b], s[0x1d],s[0x1e],
s[0x22], s[0x25], s[0x1a], s[0x1d],
s[0x28],s[0x29],s[0x2a],s[0x2b],s[0x2c],s[0x2d],s[0x2e],s[0x2f], s[0x18], s[0x1b], s[0x1e],
s[0x30], s[0x32],s[0x33],s[0x34],s[0x35], s[0x37], s[0x22], s[0x25],
s[0x3a],s[0x3b],s[0x3c], s[0x3f],
s[0x28],s[0x29],s[0x2a],s[0x2b],s[0x2c],s[0x2d],s[0x2e],s[0x2f],
s[0x32],s[0x33],s[0x34], s[0x37],
s[0x30], s[0x35],
s[0x3a],s[0x3b],s[0x3c], s[0x3f])
world.spoiler.set_map('flute', text_output, new_spots, player)
create_dynamic_flute_exits(world, player)
def sort_flute_spots(world, player, flute_spots):
if world.owLayout[player] != 'grid':
flute_spots.sort(key=lambda id: flute_data[id][1] if id != 0x03 or not world.is_tile_swapped(0x03, player) else 0x04)
else:
world_layout = world.owgrid[player][0] if world.mode[player] != 'inverted' else world.owgrid[player][1]
layout_list = sum(world_layout, [])
layout_map = {id & 0xBF: i for i, id in enumerate(layout_list)}
flute_spots.sort(key=lambda id: layout_map[flute_data[id][1] if id != 0x03 or not world.is_tile_swapped(0x03, player) else 0x04])
def create_dynamic_flute_exits(world, player):
flute_in_pool = True if player not in world.customitemarray else any(i for i, n in world.customitemarray[player].items() if i == 'flute' and n > 0)
if not flute_in_pool:
return
for region in (r for r in world.regions if r.player == player and r.terrain == Terrain.Land and r.name not in ['Zoras Domain', 'Master Sword Meadow', 'Hobo Bridge']):
if region.type == (RegionType.LightWorld if world.mode[player] != 'inverted' else RegionType.DarkWorld):
exitname = 'Flute From ' + region.name
exit = Entrance(region.player, exitname, region)
exit.spot_type = 'Flute'
exit.connect(world.get_region('Flute Sky', player))
region.exits.append(exit)
world.initialize_regions()
default_flute_connections = [
0x03, 0x16, 0x18, 0x2c, 0x2f, 0x30, 0x3b, 0x3f
]
flute_data = {
#OWID LW Region DW Region Slot VRAM BG Y BG X Link Y Link X Cam Y Cam X Unk1 Unk2 IconY IconX AltY AltX AltVRAM AltBGY AltBGX AltCamY AltCamX AltUnk1 AltUnk2 AltIconY AltIconX
0x00: (['Lost Woods East Area', 'Skull Woods Forest'], 0x09, 0x1042, 0x022e, 0x0202, 0x0290, 0x0288, 0x029b, 0x028f, 0xfff2, 0x000e, 0x0290, 0x0288, 0x0290, 0x0290),
0x02: (['Lumberjack Area', 'Dark Lumberjack Area'], 0x02, 0x059c, 0x00d6, 0x04e6, 0x0138, 0x0558, 0x0143, 0x0563, 0xfffa, 0xfffa, 0x01d8, 0x0518),
0x03: (['West Death Mountain (Bottom)', 'West Dark Death Mountain (Top)'], 0x0b, 0x1600, 0x02ca, 0x060e, 0x0328, 0x0678, 0x0337, 0x0683, 0xfff6, 0xfff2, 0x03bb, 0x0680, 0x0118, 0x0860, 0x05c0, 0x00b8, 0x07ec, 0x0127, 0x086b, 0xfff8, 0x0004, 0x0148, 0x0850),
0x05: (['East Death Mountain (Bottom)', 'East Dark Death Mountain (Bottom)'], 0x0e, 0x1860, 0x031e, 0x0d00, 0x0388, 0x0da8, 0x038d, 0x0d7d, 0x0000, 0x0000, 0x03c8, 0x0d98),
0x07: (['Death Mountain TR Pegs Area', 'Turtle Rock Area'], 0x07, 0x0804, 0x0102, 0x0e1a, 0x0160, 0x0e90, 0x016f, 0x0e97, 0xfffe, 0x0006, 0x0150, 0x0ea0),
0x0a: (['Mountain Pass Area', 'Bumper Cave Area'], 0x0a, 0x0180, 0x0220, 0x0406, 0x0280, 0x0488, 0x028f, 0x0493, 0x0000, 0xfffa, 0x0390, 0x04d8),
0x0f: (['Zora Waterfall Area', 'Catfish Area'], 0x0f, 0x0316, 0x025c, 0x0eb2, 0x02c0, 0x0f28, 0x02cb, 0x0f2f, 0x0002, 0xfffe, 0x0360, 0x0f58),
0x10: (['Lost Woods Pass West Area', 'Skull Woods Pass West Area'], 0x10, 0x0080, 0x0400, 0x0000, 0x0448, 0x0058, 0x046f, 0x0085, 0x0000, 0x0000, 0x04f8, 0x0088),
0x11: (['Kakariko Fortune Area', 'Dark Fortune Area'], 0x11, 0x0912, 0x051e, 0x0292, 0x0588, 0x0318, 0x058d, 0x031f, 0x0000, 0xfffe, 0x05f8, 0x0318),
0x12: (['Kakariko Pond Area', 'Outcast Pond Area'], 0x12, 0x0890, 0x051a, 0x0476, 0x0578, 0x04f8, 0x0587, 0x0503, 0xfff6, 0x000a, 0x05b8, 0x04f8),
0x13: (['Sanctuary Area', 'Dark Chapel Area'], 0x13, 0x051c, 0x04aa, 0x06de, 0x0508, 0x0758, 0x0517, 0x0763, 0xfff6, 0x0002, 0x05b8, 0x0738),
0x14: (['Graveyard Area', 'Dark Graveyard Area'], 0x14, 0x089c, 0x051e, 0x08e6, 0x0580, 0x0958, 0x058b, 0x0963, 0x0000, 0xfffa, 0x05f0, 0x0918, 0x0580, 0x0948),
0x15: (['River Bend East Bank', 'Qirn Jump East Bank'], 0x15, 0x041a, 0x0486, 0x0ad2, 0x04e8, 0x0b48, 0x04f3, 0x0b4f, 0x0008, 0xfffe, 0x0548, 0x0b78),
0x16: (['Potion Shop Area', 'Dark Witch Area'], 0x16, 0x0888, 0x0516, 0x0c4e, 0x0578, 0x0cc8, 0x0583, 0x0cd3, 0xfffa, 0xfff2, 0x05e8, 0x0c9f),
0x17: (['Zora Approach Ledge', 'Catfish Approach Ledge'], 0x17, 0x039e, 0x047e, 0x0ef2, 0x04e0, 0x0f68, 0x04eb, 0x0f6f, 0x0000, 0xfffe, 0x0580, 0x0f48),
0x18: (['Kakariko Village', 'Village of Outcasts'], 0x18, 0x0b30, 0x0759, 0x017e, 0x07b7, 0x0200, 0x07c6, 0x020b, 0x0007, 0x0002, 0x0830, 0x0240, 0x07c8, 0x01f8),
0x1a: (['Forgotten Forest Area', 'Shield Shop Fence'], 0x1a, 0x081a, 0x070f, 0x04d2, 0x0770, 0x0548, 0x077c, 0x054f, 0xffff, 0xfffe, 0x0770, 0x0518),
0x1b: (['Hyrule Castle Courtyard', 'Pyramid Area'], 0x1b, 0x0c30, 0x077a, 0x0786, 0x07d8, 0x07f8, 0x07e7, 0x0803, 0x0006, 0xfffa, 0x07f8, 0x07f8),
0x1d: (['Wooden Bridge Area', 'Broken Bridge Northeast'], 0x1d, 0x0602, 0x06c2, 0x0a0e, 0x0720, 0x0a80, 0x072f, 0x0a8b, 0xfffe, 0x0002, 0x0750, 0x0a70),
0x1e: (['Eastern Palace Area', 'Palace of Darkness Area'], 0x26, 0x1802, 0x091e, 0x0c0e, 0x09c0, 0x0c80, 0x098b, 0x0c8b, 0x0000, 0x0002, 0x09a0, 0x0cb0),
0x22: (['Blacksmith Area', 'Hammer Pegs Area'], 0x22, 0x058c, 0x08aa, 0x0462, 0x0908, 0x04d8, 0x0917, 0x04df, 0x0006, 0xfffe, 0x0978, 0x04e8),
0x25: (['Sand Dunes Area', 'Dark Dunes Area'], 0x25, 0x030e, 0x085a, 0x0a76, 0x08b8, 0x0ae8, 0x08c7, 0x0af3, 0x0006, 0xfffa, 0x0918, 0x0b18),
0x28: (['Maze Race Area', 'Dig Game Area'], 0x28, 0x0908, 0x0b1e, 0x003a, 0x0b88, 0x00b8, 0x0b8d, 0x00bf, 0x0000, 0x0006, 0x0ba8, 0x00b8),
0x29: (['Kakariko Suburb Area', 'Frog Area'], 0x29, 0x0408, 0x0a7c, 0x0242, 0x0ae0, 0x02c0, 0x0aeb, 0x02c7, 0x0002, 0xfffe, 0x0b30, 0x02e0),
0x2a: (['Flute Boy Area', 'Stumpy Area'], 0x2a, 0x058e, 0x0aac, 0x046e, 0x0b10, 0x04e8, 0x0b1b, 0x04f3, 0x0002, 0x0002, 0x0b60, 0x04f8),
0x2b: (['Central Bonk Rocks Area', 'Dark Bonk Rocks Area'], 0x2b, 0x0620, 0x0acc, 0x0700, 0x0b30, 0x0790, 0x0b3b, 0x0785, 0xfff2, 0x0000, 0x0b80, 0x0760),
0x2c: (['Links House Area', 'Big Bomb Shop Area'], 0x2c, 0x0588, 0x0ab9, 0x0840, 0x0b17, 0x08b8, 0x0b26, 0x08bf, 0xfff7, 0x0000, 0x0bb0, 0x08a8),
0x2d: (['Stone Bridge South Area', 'Hammer Bridge South Area'], 0x2d, 0x0886, 0x0b1e, 0x0a2a, 0x0ba0, 0x0aa8, 0x0b8b, 0x0aaf, 0x0000, 0x0006, 0x0bf0, 0x0ab8),
0x2e: (['Tree Line Area', 'Dark Tree Line Area'], 0x2e, 0x0100, 0x0a1a, 0x0c00, 0x0a78, 0x0c30, 0x0a87, 0x0c7d, 0x0006, 0x0000, 0x0ac8, 0x0c70),
0x2f: (['Eastern Nook Area', 'Darkness Nook Area'], 0x2f, 0x0798, 0x0afa, 0x0eb2, 0x0b58, 0x0f30, 0x0b67, 0x0f37, 0xfff6, 0x000e, 0x0bc0, 0x0f00),
0x30: (['Desert Teleporter Ledge', 'Mire Teleporter Ledge'], 0x38, 0x1880, 0x0f1e, 0x0000, 0x0fa8, 0x0078, 0x0f8d, 0x008d, 0x0000, 0x0000, 0x0ff0, 0x0070),
0x32: (['Flute Boy Approach Area', 'Stumpy Approach Area'], 0x32, 0x03a0, 0x0c6c, 0x0500, 0x0cd0, 0x05a8, 0x0cdb, 0x0585, 0x0002, 0x0000, 0x0d00, 0x0528),
0x33: (['C Whirlpool Outer Area', 'Dark C Whirlpool Outer Area'], 0x33, 0x0180, 0x0c20, 0x0600, 0x0c80, 0x0628, 0x0c8f, 0x067d, 0x0000, 0x0000, 0x0ce0, 0x0688),
0x34: (['Statues Area', 'Hype Cave Area'], 0x34, 0x088e, 0x0d00, 0x0866, 0x0d60, 0x08d8, 0x0d6f, 0x08e3, 0x0000, 0x000a, 0x0dd0, 0x08e8),
#0x35: (['Lake Hylia Northwest Bank', 'Ice Lake Northwest Bank'], 0x35, 0x0d00, 0x0da6, 0x0a06, 0x0e08, 0x0a80, 0x0e13, 0x0a8b, 0xfffa, 0xfffa, 0x0dc8, 0x0a90),
0x35: (['Lake Hylia South Shore', 'Ice Lake Southeast Ledge'], 0x3e, 0x1860, 0x0f1e, 0x0d00, 0x0f98, 0x0da8, 0x0f8b, 0x0d85, 0x0000, 0x0000, 0x0fd8, 0x0da8),
0x37: (['Ice Cave Area', 'Shopping Mall Area'], 0x37, 0x0786, 0x0cf6, 0x0e2e, 0x0d58, 0x0ea0, 0x0d63, 0x0eab, 0x000a, 0x0002, 0x0d98, 0x0ed0),
0x3a: (['Desert Pass Area', 'Swamp Nook Area'], 0x3a, 0x001a, 0x0e08, 0x04c6, 0x0e70, 0x0540, 0x0e7d, 0x054b, 0x0006, 0x000a, 0x0ee0, 0x0570),
0x3b: (['Dam Area', 'Swamp Area'], 0x3b, 0x069e, 0x0edf, 0x06f2, 0x0f3d, 0x0778, 0x0f4c, 0x077f, 0xfff1, 0xfffe, 0x0fd0, 0x0770),
0x3c: (['South Pass Area', 'Dark South Pass Area'], 0x3c, 0x0584, 0x0ed0, 0x081e, 0x0f38, 0x0898, 0x0f45, 0x08a3, 0xfffe, 0x0002, 0x0fa8, 0x0898),
0x3f: (['Octoballoon Area', 'Bomber Corner Area'], 0x3f, 0x0810, 0x0f05, 0x0e75, 0x0f67, 0x0ef3, 0x0f72, 0x0efa, 0xfffb, 0x000b, 0x0fd0, 0x0ef0)
}
flute_spoiler_table = \
""" 0 1 2 3 4 5 6 7
+---+-+---+---+-+
01234567 A(00)| |s| | |s|
+--------+ | s +-+ s | s +-+
A(00)|s ss s s| B(08)| |s| | |s|
B(08)| s s| +-+-+-+-+-+-+-+-+
C(10)|ssssssss| C(10)|s|s|s|s|s|s|s|s|
D(18)|s ss ss | +-+-+-+-+-+-+-+-+
E(20)| s s | D(18)| |s| |s| |
F(28)|ssssssss| | s +-+ s +-+ s |
G(30)|s ssss s| E(20)| |s| |s| |
H(38)| sss s| +-+-+-+-+-+-+-+-+
+--------+ F(28)|s|s|s|s|s|s|s|s|
+-+-+-+-+-+-+-+-+
G(30)| |s|s|s| |s|
| s +-+-+-+ s +-+
H(38)| |s|s|s| |s|
+---+-+-+-+---+-+"""