From 83254f4c781d216dbfcc0803af2166e5b13423b0 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sun, 25 Jan 2026 00:06:19 -0600 Subject: [PATCH 1/9] Moved Flute Shuffle --- Main.py | 3 +- OverworldShuffle.py | 256 +---------------------------- Rom.py | 2 +- source/classes/CustomSettings.py | 2 +- source/overworld/FluteShuffle.py | 266 +++++++++++++++++++++++++++++++ 5 files changed, 273 insertions(+), 256 deletions(-) create mode 100644 source/overworld/FluteShuffle.py diff --git a/Main.py b/Main.py index bf5fc65c..eab4a3e5 100644 --- a/Main.py +++ b/Main.py @@ -17,7 +17,8 @@ from OverworldGlitchRules import create_owg_connections 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 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 Doors import create_doors from DoorShuffle import link_doors, connect_portal, link_doors_prep diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 2f56676e..2e4203ca 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -4,6 +4,7 @@ from DungeonGenerator import GenerationException from BaseClasses import OWEdge, WorldType, RegionType, Direction, Terrain, PolSlot, Entrance from Regions import mark_light_dark_world_regions from source.overworld.EntranceShuffle2 import connect_simple +from source.overworld.FluteShuffle import shuffle_flute_spots, default_flute_connections, flute_data from OWEdges import OWTileRegions, OWEdgeGroups, OWEdgeGroupsTerrain, OWExitTypes, OpenStd, parallel_links, IsParallel from OverworldGlitchRules import create_owg_connections from Utils import bidict @@ -336,6 +337,7 @@ def link_overworld(world, player): world.owcrossededges[player].extend(edge_set) 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]): if edge in parallel_links_new: if parallel_links_new[edge] not in world.owcrossededges[player]: @@ -574,169 +576,8 @@ def link_overworld(world, player): # flute shuffle logging.getLogger('').debug('Shuffling flute spots') - 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: - flute_spots = 8 - flute_pool = list(flute_data.keys()) - new_spots = list() - ignored_regions = set() - used_flute_regions = [] - forbidden_spots = [] - forbidden_regions = [] + shuffle_flute_spots(world, player) - 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): 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) 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): if from_region in mirror_connections and to_region in mirror_connections[from_region]: 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')), ((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')) -] - -default_flute_connections = [ - 0x03, 0x16, 0x18, 0x2c, 0x2f, 0x30, 0x3b, 0x3f ] ow_connections = { @@ -2569,51 +2384,6 @@ isolated_regions = [ '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 = { 'Master Sword Pedestal': (0x06d, 0x070), 'Hobo': (0xb80, 0xb90), @@ -2694,23 +2464,3 @@ H(38)| sss 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| - +---+-+-+-+---+-+""" diff --git a/Rom.py b/Rom.py index 4a768c28..b0b9335d 100644 --- a/Rom.py +++ b/Rom.py @@ -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 source.overworld.EntranceData import door_addresses, ow_prize_table 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 source.classes.SFX import randomize_sfx, randomize_sfxinstruments, randomize_songinstruments diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index 653f8758..0e66b2de 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -10,7 +10,7 @@ from pathlib import Path import RaceRandom as random 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 diff --git a/source/overworld/FluteShuffle.py b/source/overworld/FluteShuffle.py new file mode 100644 index 00000000..a499598b --- /dev/null +++ b/source/overworld/FluteShuffle.py @@ -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| + +---+-+-+-+---+-+""" From b16777ede723a5ad2ca074b9a68adf41af5c8c6a Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 28 Jan 2026 18:13:58 -0600 Subject: [PATCH 2/9] Added new PlasmaKappa tribute TF text --- Text.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Text.py b/Text.py index b21eb57c..e7bbe072 100644 --- a/Text.py +++ b/Text.py @@ -134,6 +134,7 @@ Triforce_texts = [ " I promise the\n next seed will\n be better.", "\n Honk.", " Breakfast\n is served!", + "\n send help", ] 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?'] From daa54e8aadb79416f8db5a7193a8535a7aa75e78 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 28 Jan 2026 18:40:51 -0600 Subject: [PATCH 3/9] Fixed palette issue with map checks when Link stands in grass/water --- Rom.py | 2 +- data/base2current.bps | Bin 139141 -> 139153 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index b0b9335d..4606e9e5 100644 --- a/Rom.py +++ b/Rom.py @@ -43,7 +43,7 @@ from source.enemizer.Enemizer import write_enemy_shuffle_settings JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '01d81a1ad16b1838b27a057b8f8936d4' +RANDOMIZERBASEHASH = '67b4921f9233b14736beead10ac5ec18' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index 8acc79e8bc9f031852a13bd96dd3ca168ee4869e..d447dd55eb49b00305bfedb5bdf215f0b9b9dff5 100644 GIT binary patch delta 417 zcmV;S0bc%vzX*}P2(U5(1jyqz9J4k9Iv)X&vvDE=1OW}RbtY{M0ll;1Ggbl%P>ZT8 zfQ=&LimH&4J~kv0>x!!C>ZFNj>7+n`fr|8vBKxy+HXi{HIg6@ffRQ4ph^p#PfQ=$t zv!gd)NdaZE1c8D_7@i~qq?&Oyk*x-3X!KY0YiMZnUG`PdAp@CgtGWOAty zg&b>_ouL6pSOPrjkqV`9@CJ|CnZs+P8YvKKmQoUil^|q`!XRss5SE2xi=XqYP_GNY zekl%0g#fP!k63__3JPTag_|*j)usR`K=1~?K)tGf1it{is;Vgm3WWfZ7MDGu0Ve_r zN0(-z0U--qj*t=Py#TvNAe)zxq5)(WUQ_{qh0qkrP*~H!YrW77&;o!5TAL!fmky%= zF%Mjhumen5j_ADryV^(~o0m?b0T~zWjml`r(9i{`c}$ay$wJTvB3P}4OqZ8_qX9_) z$d|~Y0W=a_Zw!DffcmKmBFK)aXz&9}SeFi@0i6MAm#m}#R|k8C3kal>54Q@X0dsl? LRkX(;r7=cGUbe0d delta 432 zcmV;h0Z;yszX*lD2(U5(1PbiD~JiK!JgZ^o=6)vwAik0T42as$_tXBB_X~>QI1v!uxR*aeO72@nRCsi6T#K6E?l zkqV`9@CJ|CnZs+P8YvKKmQoUil^|VUyml~u2odH~zxTFDB a2N#Ha2c(l7w-%)Vb9x9Fzxy>(9p5#BR<%0- From 31f634396abb91e3553aaf78c01e9404071433ad Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 28 Jan 2026 19:49:20 -0600 Subject: [PATCH 4/9] Fixed issue with map checks showing opposite world icons in Inverted --- Rom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index 4606e9e5..756e3d33 100644 --- a/Rom.py +++ b/Rom.py @@ -1457,7 +1457,7 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): map_x = x_map_position[idx] map_y = y_map_position[idx] 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 if coord_flags & 0x4000 == 0: map_x, map_y = adjust_ow_coordinates_to_layout(world, player, map_x, map_y, coord_flags & 0x8000 != 0) From 09254850d3909b6ad0ec3520611ec3692b228ead Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 28 Jan 2026 20:03:56 -0600 Subject: [PATCH 5/9] Fixed error with bombbag + non-shopsanity --- Fill.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Fill.py b/Fill.py index 52021036..0dd28574 100644 --- a/Fill.py +++ b/Fill.py @@ -1106,7 +1106,7 @@ def balance_money_progression(world): slot = shop_to_location_table[location.parent_region.name].index(location.name) shop = location.parent_region.shop 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 shop_item['price'] < rupee_chart[location.item.name]: wallet[loc_player] -= shop_item['price'] # will get picked up in the location_free block From afe888a87a5c19ccb6d3d1475cadf5c0f9d108d1 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 28 Jan 2026 22:50:40 -0600 Subject: [PATCH 6/9] Fix Link sprite using vanilla coords on map check in Special OW --- Rom.py | 5 +++-- data/base2current.bps | Bin 139153 -> 139233 bytes 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Rom.py b/Rom.py index 756e3d33..9c63dd1d 100644 --- a/Rom.py +++ b/Rom.py @@ -43,7 +43,7 @@ from source.enemizer.Enemizer import write_enemy_shuffle_settings JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '67b4921f9233b14736beead10ac5ec18' +RANDOMIZERBASEHASH = '630ea96d0b0c1acdc3f4d158480cd951' class JsonRom(object): @@ -518,7 +518,8 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): else: flute_spots = world.owflutespots[player] 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] for o in range(0, len(flute_writes)): diff --git a/data/base2current.bps b/data/base2current.bps index d447dd55eb49b00305bfedb5bdf215f0b9b9dff5..1aae81c450a9c74593252d5993d3367e0f2acadb 100644 GIT binary patch delta 682 zcmV;b0#*HyzX;*K2(U5(1jWBPK(jUjIv)YxvvDE=1OZ#KbtY{M0t<|@kP%UT%Yh zB7dI%gfM`Ciken|fr_-B6o8YeHt8*mBxK~I(<7XLKKp{AEP;_CsfeoTP=JjhQj4mT zflR1@ikOuFATGs#2AeBAle{-d1AhRIqX{@k0kg9Tfuq+Ho*71mr2_B|rO7%F0~l$m zU{Vo)m%vy7dILQF$L zAf$`%3Z-)J254yXo5O3R8YvKKj|stkAf$_%0zB)j0Pq*$ARxetAp46*msLIet?S|- zXzRitYp)2we!qA={ekk6f`JOFf`JO7f{E}G2$zc8j|;(mAp47t3Bi6SV6O|oekl%0 zg#fP!kK_rF3JPTag_~K1moTCM7f_cxhMfTg0APwKI7**_kJ6>38}J6LwGT*u2bK^@ zf`LezHAt0NfHl1!4$0Zj2dM*{AP;~B$+GYQXltI32a8LqTALuMj*t=Py#TvNAnBK$ zq5)(XdQ|~{h0qkrP*~H!YrW77&;o!5TALyXjh7pv0Wd$Rj<5qvsE+8p0K3{qAnA@s zum!0bjV}SC1FDlRfsZ7D34rhg%4keHj>>4s(9i{`c}(Gr$wJTvB3P}4Oy!q~qX9_* z7LS+JqX9D#d2kGXEr9x|3nCVes%Y>7Os1C_qye1*1{s&Rqybh6LxnL*ij!$i6M(lE Qr2%nz2>Mg=;myEqQBgG{iU0rr delta 592 zcmV-W0fSulSDRI6C8x6UT%X}fPspdR)K+vw4Wq} zle{+RDV-!_hRRks_&xs_IaHjUrr&s+56DsDX-@l>s0w#efEzV_B2ZH%bFR zevYFbI7tCzvmk+^*At&EU8to3@DHWQIuHXGX{#c1=a<)50eS;P0IIiCSpn%rJGJl& zrSi!@&;_Xhjrs;_jzWN&#+f9g5y=YB9+DuUr5LGxg$=1Rg$!hp76^coeM9|`7-yZkja_|O^+L^;^r5Y&^YnD>i^3pl zk`R`KWQ(8ktx&HE!G0+YN`(Ng36EHSkqQcB0EL?|h1I42DM0WBzd*gJfCRq)y{f7y z2MUD%lNN=k0R;e$3&DPhDL6`>f{)O(3Bi6~r5o@Dt+fpgNPq{HX Date: Thu, 29 Jan 2026 21:26:32 -0600 Subject: [PATCH 7/9] Fix buffered sword issue when transitioning to water with sword out --- Rom.py | 2 +- data/base2current.bps | Bin 139233 -> 139239 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index 9c63dd1d..74063ec2 100644 --- a/Rom.py +++ b/Rom.py @@ -43,7 +43,7 @@ from source.enemizer.Enemizer import write_enemy_shuffle_settings JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '630ea96d0b0c1acdc3f4d158480cd951' +RANDOMIZERBASEHASH = 'da081467317d8f6902eb1cf57304dbff' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index 1aae81c450a9c74593252d5993d3367e0f2acadb..ff23c010dc7ac1586bd7447755edfa3d967ca84d 100644 GIT binary patch delta 266 zcmV+l0rmdjzX<2Q2(U5(1eUvLM6)&nSqTB0vycoQ0s+30unt54_Ota42L=KJmxKNk zxBe6Xf|LO`mw~APmj+>$fNUUPmmaDCHvwUnR;mFWf9}k&_ShL z$pr8_lgX2B4hevd9KeYe@D87BP6&XRgm+h(n5D0sg?6V4zGv=2Cs{u9;9!r#&11Nw2D5V9WVds}ymrJVwQUrU(24uIP Qs{uH42>8wgndDnRB0{!mO#lD@ delta 240 zcmVa+C@2L=N8mV^Ej zxBe6Xf|LO)mw~APmj+dqfNUUDmmaDCHvv_bR;mFWWetF>0|)RVzyt6FWJ#Pw1Dpc~ z$pX*|I*mmGoInGq1b_y~1JDGI)Tv;=0?-eoUC9K{NSjVjr>$gNzyZ)ZrCrGc@HLak zlWz_QfPoH@JA$ Date: Fri, 30 Jan 2026 12:35:42 -0600 Subject: [PATCH 8/9] Minor efficiency --- ItemList.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ItemList.py b/ItemList.py index a6d0f899..a95604c7 100644 --- a/ItemList.py +++ b/ItemList.py @@ -341,9 +341,10 @@ def generate_itempool(world, player): world.escape_assist[player].append('bombs') for (location, item) in placed_items.items(): - world.push_item(world.get_location(location, player), ItemFactory(item, player), False) - world.get_location(location, player).event = True - world.get_location(location, player).locked = True + loc = world.get_location(location, player) + world.push_item(loc, ItemFactory(item, player), False) + loc.event = True + loc.locked = True if world.shopsanity[player] and not skip_pool_adjustments: for shop in world.shops[player]: From ad8dff3b2dfcd12ee5d5a98a2c9953eff4dd4c04 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 30 Jan 2026 12:41:26 -0600 Subject: [PATCH 9/9] Version bump 0.7.0.2 --- CHANGELOG.md | 8 ++++++++ OverworldShuffle.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bef8bcc..5fe8b5e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # 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 - Fixed buggy sprites in post-Aga Zora's Domain - Fixed L/R map switch when in special OW screens diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 2e4203ca..b1eca427 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -9,7 +9,7 @@ from OWEdges import OWTileRegions, OWEdgeGroups, OWEdgeGroupsTerrain, OWExitType from OverworldGlitchRules import create_owg_connections from Utils import bidict -version_number = '0.7.0.1' +version_number = '0.7.0.2' # branch indicator is intentionally different across branches version_branch = '-u'