diff --git a/DoorShuffle.py b/DoorShuffle.py index 701b3e4d..6e8c3e53 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -129,7 +129,7 @@ def link_doors_prep(world, player): vanilla_key_logic(world, player) -def link_doors_main(world, player): +def create_dungeon_pool(world, player): pool = None if world.doorShuffle[player] == 'basic': pool = [([name], regions) for name, regions in dungeon_regions.items()] @@ -143,6 +143,11 @@ def link_doors_main(world, player): elif world.doorShuffle[player] != 'vanilla': logging.getLogger('').error('Invalid door shuffle setting: %s' % world.doorShuffle[player]) raise Exception('Invalid door shuffle setting: %s' % world.doorShuffle[player]) + return pool + + +def link_doors_main(world, player): + pool = create_dungeon_pool(world, player) if pool: main_dungeon_pool(pool, world, player) if world.doorShuffle[player] != 'vanilla': @@ -558,7 +563,9 @@ def customizer_portals(master_door_list, world, player): custom_doors = world.customizer.get_doors()[player] if custom_doors and 'lobbies' in custom_doors: for portal, assigned_door in custom_doors['lobbies'].items(): - door = next(x for x in master_door_list if x.name == assigned_door) + door = next((x for x in master_door_list if x.name == assigned_door), None) + if door is None: + raise Exception(f'{assigned_door} not found. Check for typos') custom_portals[portal] = door assigned_doors.add(door) if custom_doors and 'doors' in custom_doors: @@ -571,6 +578,24 @@ def customizer_portals(master_door_list, world, player): elif 'dest' in dest: door = world.get_door(dest['dest'], player) assigned_doors.add(door) + # restricts connected doors to the customized portals + if assigned_doors: + pool = create_dungeon_pool(world, player) + if pool: + pool_map = {} + for pool, region_list in pool: + sector_pool = convert_to_sectors(region_list, world, player) + merge_sectors(sector_pool, world, player) + for p in pool: + pool_map[p] = sector_pool + for portal, assigned_door in custom_portals.items(): + portal_region = world.get_door(assigned_door, player).entrance.parent_region + portal_dungeon = world.get_region(f'{portal} Portal', player).dungeon.name + sector_pool = pool_map[portal_dungeon] + sector = next((s for s in sector_pool if portal_region in s.regions), None) + for door in sector.outstanding_doors: + if door.portalAble: + door.dungeonLink = portal_dungeon return custom_portals, assigned_doors @@ -1719,7 +1744,6 @@ def setup_custom_door_types(world, player): custom_doors = custom_doors[player] if 'doors' not in custom_doors: return - # todo: dash/bomb door pool specific customizeable_types = ['Key Door', 'Dash Door', 'Bomb Door', 'Trap Door', 'Big Key Door'] world.custom_door_types[player] = type_map = {x: defaultdict(list) for x in customizeable_types} for door, dest in custom_doors['doors'].items(): @@ -1732,7 +1756,7 @@ def setup_custom_door_types(world, player): type_map[door_kind][dungeon.name].append(d) else: # check if the dest is paired - if d.dest.type in [DoorType.Interior, DoorType.Normal] and door_kind != 'Trap Door': + if d.dest and d.dest.type in [DoorType.Interior, DoorType.Normal] and door_kind != 'Trap Door': type_map[door_kind][dungeon.name].append((d, d.dest)) else: type_map[door_kind][dungeon.name].append(d) @@ -1784,18 +1808,24 @@ def shuffle_door_types(door_type_pools, paths, world, player): start_regions_map[name] = start_regions builder.candidates = BuilderDoorCandidates() + all_custom = defaultdict(list) + if player in world.custom_door_types: + for custom_dict in world.custom_door_types[player].values(): + for dungeon, doors in custom_dict.items(): + all_custom[dungeon].extend(doors) + world.paired_doors[player].clear() - used_doors = shuffle_trap_doors(door_type_pools, paths, start_regions_map, world, player) + used_doors = shuffle_trap_doors(door_type_pools, paths, start_regions_map, all_custom, world, player) # big keys - used_doors = shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world, player) + used_doors = shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, all_custom, world, player) # small keys - used_doors = shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, world, player) + used_doors = shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, all_custom, world, player) # bombable / dashable - used_doors = shuffle_bomb_dash_doors(door_type_pools, used_doors, start_regions_map, world, player) + used_doors = shuffle_bomb_dash_doors(door_type_pools, used_doors, start_regions_map, all_custom, world, player) # handle paired list -def shuffle_trap_doors(door_type_pools, paths, start_regions_map, world, player): +def shuffle_trap_doors(door_type_pools, paths, start_regions_map, all_custom, world, player): used_doors = set() for pool, door_type_pool in door_type_pools: if world.trap_door_mode[player] != 'oneway': @@ -1806,15 +1836,14 @@ def shuffle_trap_doors(door_type_pools, paths, start_regions_map, world, player) custom_trap_doors = world.custom_door_types[player]['Trap Door'] else: custom_trap_doors = defaultdict(list) - for dungeon in pool: builder = world.dungeon_layouts[player][dungeon] if 'Mire Warping Pool' in builder.master_sector.region_set(): custom_trap_doors[dungeon].append(world.get_door('Mire Warping Pool ES', player)) world.custom_door_types[player]['Trap Door'] = custom_trap_doors find_trappable_candidates(builder, world, player) - if custom_trap_doors[dungeon]: - builder.candidates.trap = filter_key_door_pool(builder.candidates.trap, custom_trap_doors[dungeon]) + if all_custom[dungeon]: + builder.candidates.trap = filter_key_door_pool(builder.candidates.trap, all_custom[dungeon]) remaining -= len(custom_trap_doors[dungeon]) ttl += len(builder.candidates.trap) if ttl == 0: @@ -1866,7 +1895,7 @@ def shuffle_trap_doors(door_type_pools, paths, start_regions_map, world, player) return used_doors -def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world, player): +def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, all_custom, world, player): for pool, door_type_pool in door_type_pools: ttl = 0 suggestion_map, bk_map, flex_map = {}, {}, {} @@ -1879,8 +1908,8 @@ def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world, for dungeon in pool: builder = world.dungeon_layouts[player][dungeon] find_big_key_candidates(builder, start_regions_map[dungeon], used_doors, world, player) - if custom_bk_doors[dungeon]: - builder.candidates.big = filter_key_door_pool(builder.candidates.big, custom_bk_doors[dungeon]) + if all_custom[dungeon]: + builder.candidates.big = filter_key_door_pool(builder.candidates.big, all_custom[dungeon]) remaining -= len(custom_bk_doors[dungeon]) ttl += len(builder.candidates.big) if ttl == 0: @@ -1926,7 +1955,7 @@ def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world, return used_doors -def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, world, player): +def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, all_custom, world, player): max_computation = 11 # this is around 6 billion worse case factorial don't want to exceed this much for pool, door_type_pool in door_type_pools: ttl = 0 @@ -1944,8 +1973,8 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, worl builder.total_keys = total_keys find_small_key_door_candidates(builder, start_regions_map[dungeon], used_doors, world, player) custom_doors = 0 - if custom_key_doors[dungeon]: - builder.candidates.small = filter_key_door_pool(builder.candidates.small, custom_key_doors[dungeon]) + if all_custom[dungeon]: + builder.candidates.small = filter_key_door_pool(builder.candidates.small, all_custom[dungeon]) custom_doors = len(custom_key_doors[dungeon]) remaining -= custom_doors builder.key_doors_num = max(0, len(builder.candidates.small) - builder.key_drop_cnt) + custom_doors @@ -1961,13 +1990,10 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, worl suggested = min(calculated, limit) key_door_num = min(suggested + builder.key_drop_cnt, max_computation) combo_size = ncr(len(builder.candidates.small), key_door_num) - while combo_size > 500000 and suggested > 0: - suggested -= 1 - combo_size = ncr(len(builder.candidates.small), key_door_num) suggestion_map[dungeon] = builder.key_doors_num = key_door_num - remaining -= suggested + builder.key_drop_cnt + remaining -= key_door_num + builder.key_drop_cnt builder.combo_size = combo_size - flex_map[dungeon] = (limit - suggested) if suggested < limit else 0 + flex_map[dungeon] = (limit - key_door_num) if key_door_num < limit else 0 for dungeon in pool: builder = world.dungeon_layouts[player][dungeon] if total_adjustable: @@ -2026,7 +2052,7 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, worl return used_doors -def shuffle_bomb_dash_doors(door_type_pools, used_doors, start_regions_map, world, player): +def shuffle_bomb_dash_doors(door_type_pools, used_doors, start_regions_map, all_custom, world, player): for pool, door_type_pool in door_type_pools: ttl = 0 suggestion_map, bd_map = {}, {} @@ -2043,11 +2069,9 @@ def shuffle_bomb_dash_doors(door_type_pools, used_doors, start_regions_map, worl for dungeon in pool: builder = world.dungeon_layouts[player][dungeon] find_bd_candidates(builder, start_regions_map[dungeon], used_doors, world, player) - if custom_bomb_doors[dungeon]: - builder.candidates.bomb_dash = filter_key_door_pool(builder.candidates.bomb_dash, custom_bomb_doors[dungeon]) + if all_custom[dungeon]: + builder.candidates.bomb_dash = filter_key_door_pool(builder.candidates.bomb_dash, all_custom[dungeon]) remaining_bomb -= len(custom_bomb_doors[dungeon]) - if custom_dash_doors[dungeon]: - builder.candidates.bomb_dash = filter_key_door_pool(builder.candidates.bomb_dash, custom_dash_doors[dungeon]) remaining_dash -= len(custom_dash_doors[dungeon]) ttl += len(builder.candidates.bomb_dash) if ttl == 0: diff --git a/Doors.py b/Doors.py index edc51ac0..0bd13742 100644 --- a/Doors.py +++ b/Doors.py @@ -66,7 +66,7 @@ def create_doors(world, player): create_door(player, 'Hyrule Castle Back Hall Down Stairs', Sprl).dir(Dn, 0x01, 0, HTL).ss(A, 0x2a, 0x00), create_door(player, 'Hyrule Castle Throne Room Tapestry', Lgcl), create_door(player, 'Hyrule Castle Tapestry Backwards', Lgcl), - create_door(player, 'Hyrule Castle Throne Room N', Nrml).dir(No, 0x51, Mid, High).pos(1), + create_door(player, 'Hyrule Castle Throne Room N', Nrml).dir(No, 0x51, Mid, High).pos(0), create_door(player, 'Hyrule Castle Throne Room South Stairs', StrS).dir(So, 0x51, Mid, Low), # hyrule dungeon level diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 9be244f8..e429719a 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -1824,6 +1824,7 @@ def ensure_crystal_switches_reachable(dungeon_map, crystal_switches, polarized_s for name, builder in dungeon_map.items(): if builder.c_switch_present and builder.c_switch_required and not builder.c_locked: invalid_builders.append(builder) + random.shuffle(invalid_builders) while len(invalid_builders) > 0: valid_builders = [] for builder in invalid_builders: @@ -1849,6 +1850,7 @@ def ensure_crystal_switches_reachable(dungeon_map, crystal_switches, polarized_s if eq.c_switch: reachable_crystals[hook_from_door(eq.door)] = True valid_ent_sectors = [] + random.shuffle(entrance_sectors) for entrance_sector in entrance_sectors: other_sectors = [x for x in builder.sectors if x != entrance_sector] reachable, access = is_c_switch_reachable(entrance_sector, reachable_crystals, other_sectors) @@ -1866,7 +1868,12 @@ def ensure_crystal_switches_reachable(dungeon_map, crystal_switches, polarized_s while not valid: if len(candidates) <= 0: raise GenerationException(f'need to provide more sophisticated crystal connection for {entrance_sector}') - sector, which_list = random.choice(list(candidates.items())) + # prioritize candidates + if any(x == 'Crystals' for x in candidates.values()): + cand_list = [x for x in candidates.items() if x[1] == 'Crystals'] + else: + cand_list = list(candidates.items()) + sector, which_list = random.choice(cand_list) del candidates[sector] valid = global_pole.is_valid_choice(dungeon_map, builder, [sector]) if which_list == 'Polarized': diff --git a/Main.py b/Main.py index 4a7407be..117e75f1 100644 --- a/Main.py +++ b/Main.py @@ -36,7 +36,7 @@ from source.overworld.EntranceShuffle2 import link_entrances_new from source.tools.BPS import create_bps_from_data from source.classes.CustomSettings import CustomSettings -__version__ = '1.2.0.8-u' +__version__ = '1.2.0.9-u' from source.classes.BabelFish import BabelFish diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0328487c..d5a492b1 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -108,6 +108,19 @@ These are now independent of retro mode and have three options: None, Random, an * Bonk Fairy (Dark) # Bug Fixes and Notes +* 1.2.0.9-u + * Disallowed standard exits (due to ER) are now graphically half blocked instead of missing + * Graphical issues with Sanctuary and Swamp Hub lobbies are fixed + * Fixes an issue surrounding door state and decoupled doors leading to blocked doors + * Customizer improvements: + * Better logic around customized lobbies + * Better logic around customized door types + * Fix to key doors that was causing extra key doors + * Generation improvement around crystal switches + * Fix bug in dungeon_only that wasn't using pot key locations (known issue still exists in pottery modes) + * Fixes for multiworld: + * Fixes an issue when keys are found in own dungeon for another player when using the bizhawk plugin. + * Fixes an issue with absorbables for another player also being received by the player picking it up. * 1.2.0.8-u * New Features: trap_door_mode and key_logic_algorithm * Change S&Q in door shuffle + standard during escape to spawn as Uncle diff --git a/Rom.py b/Rom.py index 5041f030..0b6fd1af 100644 --- a/Rom.py +++ b/Rom.py @@ -38,7 +38,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '1694ba41bf6ab7086f05e914e8d08433' +RANDOMIZERBASEHASH = 'e9a22882bb59523c19845274d76b3761' class JsonRom(object): @@ -573,7 +573,7 @@ class Sprite(object): def handle_native_dungeon(location, itemid): # Keys in their native dungeon should use the original item code for keys - if location.parent_region.dungeon: + if location.parent_region.dungeon and location.player == location.item.player: if location.parent_region.dungeon.name == location.item.dungeon: if location.item.bigkey: return 0x32 diff --git a/RoomData.py b/RoomData.py index d8d3a82a..13339364 100644 --- a/RoomData.py +++ b/RoomData.py @@ -244,7 +244,9 @@ def create_rooms(world, player): # Room(player, 0xff, 0x52c9a).door(Position.InteriorW, DoorKind.Bombable).door(Position.InteriorE, DoorKind.Bombable).door(Position.SouthE, DoorKind.CaveEntrance), ] # fix some wonky things - world.get_room(0x51, player).change(1, DoorKind.Normal) # fix the dungeon changer + # should I put back the dungeon changer for certain logic - like no logic? maybe in basic + if world.doorShuffle[player] != 'vanilla': + world.get_room(0x51, player).delete(1) # remove the dungeon changer world.get_room(0x60, player).swap(2, 4) # puts the exit at pos 2 - enables pos 3 world.get_room(0x61, player).swap(1, 6) # puts the WN door at pos 1 - enables it world.get_room(0x61, player).swap(5, 6) # puts the Incognito Entrance at the end, so it can be deleted diff --git a/asm/owrando.asm b/asm/owrando.asm index 363d9587..09fe010d 100644 --- a/asm/owrando.asm +++ b/asm/owrando.asm @@ -608,16 +608,18 @@ OWEdgeTransition: } OWSpecialExit: { - PHY - LDY.b #$00 - LDA.w $0418 : LSR : BNE + - LDY.w $0704 : BRA ++ - + - LDA.w $0704 : BNE ++ - LDY.b #$02 - ++ - JSR OWWorldTerrainUpdate - PLY + LDA.l OWMode : ORA.l OWMode+1 : BEQ .vanilla + PHY + LDY.b #$00 + LDA.w $0418 : LSR : BNE + + LDY.w $0704 : BRA ++ + + + LDA.w $0704 : BNE ++ + LDY.b #$02 + ++ + JSR OWWorldTerrainUpdate + PLY + .vanilla LDA.l $7EFD40,X ; what we wrote over RTL } diff --git a/data/base2current.bps b/data/base2current.bps index d629c08c..71f15e40 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py index a0e8a01a..3e00d38a 100644 --- a/source/item/FillUtil.py +++ b/source/item/FillUtil.py @@ -163,7 +163,7 @@ def create_item_pool_config(world): dungeon_set = (mode_grouping['Big Chests'] + mode_grouping['Dungeon Trash'] + mode_grouping['Big Keys'] + mode_grouping['Heart Containers'] + mode_grouping['GT Trash'] + mode_grouping['Small Keys'] + mode_grouping['Compasses'] + mode_grouping['Maps'] + mode_grouping['Key Drops'] + - mode_grouping['Big Key Drops']) + mode_grouping['Pot Keys'] + mode_grouping['Big Key Drops']) for player in range(1, world.players + 1): config.item_pool[player] = determine_major_items(world, player) config.location_groups[0].locations = set(dungeon_set) diff --git a/test/customizer/multi_test.yaml b/test/customizer/multi_test.yaml new file mode 100644 index 00000000..360499ef --- /dev/null +++ b/test/customizer/multi_test.yaml @@ -0,0 +1,23 @@ +meta: + players: 2 +settings: + 1: + pottery: cavekeys + keysanity: True + 2: + keysanity: True +placements: + 1: + Sanctuary: Small Key (Escape)#2 + 'Links House Pot #3': Rupees (20)#2 +start_inventory: + 1: + - Pegasus Boots + - Ocarina (Activated) + - Magic Mirror + - Boss Heart Container + - Boss Heart Container + - Boss Heart Container + - Boss Heart Container + - Red Mail + - Golden Sword \ No newline at end of file diff --git a/test/customizer/test_stuff.yaml b/test/customizer/test_stuff.yaml index 552d5490..313d35d2 100644 --- a/test/customizer/test_stuff.yaml +++ b/test/customizer/test_stuff.yaml @@ -1,28 +1,46 @@ meta: players: 1 - race: true + algorithm: dungeon_only settings: 1: - shopsanity: true + goal: dungeons pseudoboots: true - goal: crystals - crystals_gt: random - keysanity: true door_shuffle: crossed + decoupledoors: true intensity: 3 - door_type_mode: big - pottery: keys - dropshuffle: true + door_type_mode: all experimental: true dungeon_counters: 'on' - hints: true - msu_resume: true - collection_rate: true - quickswap: true -start_inventory: + compassshuffle: true + mapshuffle: true + keydropshuffle: true +doors: 1: - - Pegasus Boots - - Ocarina (Activated) - - Magic Mirror - - Boss Heart Container - - Blue Mail \ No newline at end of file + lobbies: + Hyrule Castle South: Thieves Lobby S + doors: + Thieves Lobby NE Edge: + dest: Hyrule Castle Throne Room South Stairs + one-way: True + Hyrule Castle Throne Room South Stairs: + dest: Desert Beamos Hall NE + one-way: True + Desert Beamos Hall NE: + dest: Ice Spike Cross SE + type: Key Door + one-way: True + Ice Spike Cross SE: + dest: Thieves Lobby NE Edge + one-way: True + Hyrule Castle Throne Room N: + dest: Ice Firebar Down Ladder + type: Key Door + Swamp Lobby S: + dest: Thieves Lobby N Edge + type: Bomb Door + Thieves Lobby E: Thieves Compass Room W + Thieves Compass Room NW Edge: Ice Tall Hint SE + Thieves Compass Room N Edge: PoD Bow Statue Down Ladder + + +