From fefd790de6731576f2533bdf37763345e6356bb0 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Tue, 24 Feb 2026 11:43:42 -0600 Subject: [PATCH 01/53] ALTTP: remove `world: MultiWorld` and typing (#5974) --- worlds/alttp/EntranceShuffle.py | 1038 +++++++++++++------------ worlds/alttp/InvertedRegions.py | 493 ++++++------ worlds/alttp/ItemPool.py | 80 +- worlds/alttp/Items.py | 14 +- worlds/alttp/Options.py | 10 +- worlds/alttp/OverworldGlitchRules.py | 71 +- worlds/alttp/Regions.py | 508 ++++++------ worlds/alttp/Rom.py | 284 +++---- worlds/alttp/Rules.py | 920 +++++++++++----------- worlds/alttp/Shops.py | 12 +- worlds/alttp/UnderworldGlitchRules.py | 79 +- 11 files changed, 1767 insertions(+), 1742 deletions(-) diff --git a/worlds/alttp/EntranceShuffle.py b/worlds/alttp/EntranceShuffle.py index c062a17ea6..550e4878aa 100644 --- a/worlds/alttp/EntranceShuffle.py +++ b/worlds/alttp/EntranceShuffle.py @@ -1,74 +1,75 @@ # ToDo: With shuffle_ganon option, prevent gtower from linking to an exit only location through a 2 entrance cave. from collections import defaultdict +from BaseClasses import MultiWorld from .OverworldGlitchRules import overworld_glitch_connections from .UnderworldGlitchRules import underworld_glitch_connections from .Regions import mark_light_world_regions from .InvertedRegions import mark_dark_world_regions -def link_entrances(world, player): - connect_two_way(world, 'Links House', 'Links House Exit', player) # unshuffled. For now - connect_exit(world, 'Chris Houlihan Room Exit', 'Links House', player) # should always match link's house, except for plandos +def link_entrances(multiworld: MultiWorld, player: int): + connect_two_way(multiworld, 'Links House', 'Links House Exit', player) # unshuffled. For now + connect_exit(multiworld, 'Chris Houlihan Room Exit', 'Links House', player) # should always match link's house, except for plandos Dungeon_Exits = Dungeon_Exits_Base.copy() Cave_Exits = Cave_Exits_Base.copy() Old_Man_House = Old_Man_House_Base.copy() Cave_Three_Exits = Cave_Three_Exits_Base.copy() - unbias_some_entrances(world, Dungeon_Exits, Cave_Exits, Old_Man_House, Cave_Three_Exits) + unbias_some_entrances(multiworld, Dungeon_Exits, Cave_Exits, Old_Man_House, Cave_Three_Exits) # setup mandatory connections for exitname, regionname in mandatory_connections: - connect_simple(world, exitname, regionname, player) + connect_simple(multiworld, exitname, regionname, player) # if we do not shuffle, set default connections - if world.worlds[player].options.entrance_shuffle == 'vanilla': + if multiworld.worlds[player].options.entrance_shuffle == 'vanilla': for exitname, regionname in default_connections: - connect_simple(world, exitname, regionname, player) + connect_simple(multiworld, exitname, regionname, player) for exitname, regionname in default_dungeon_connections: - connect_simple(world, exitname, regionname, player) - elif world.worlds[player].options.entrance_shuffle == 'dungeons_simple': + connect_simple(multiworld, exitname, regionname, player) + elif multiworld.worlds[player].options.entrance_shuffle == 'dungeons_simple': for exitname, regionname in default_connections: - connect_simple(world, exitname, regionname, player) + connect_simple(multiworld, exitname, regionname, player) - simple_shuffle_dungeons(world, player) - elif world.worlds[player].options.entrance_shuffle == 'dungeons_full': + simple_shuffle_dungeons(multiworld, player) + elif multiworld.worlds[player].options.entrance_shuffle == 'dungeons_full': for exitname, regionname in default_connections: - connect_simple(world, exitname, regionname, player) + connect_simple(multiworld, exitname, regionname, player) - skull_woods_shuffle(world, player) + skull_woods_shuffle(multiworld, player) dungeon_exits = list(Dungeon_Exits) lw_entrances = list(LW_Dungeon_Entrances) dw_entrances = list(DW_Dungeon_Entrances) - if world.worlds[player].options.mode == 'standard': + if multiworld.worlds[player].options.mode == 'standard': # must connect front of hyrule castle to do escape - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) else: dungeon_exits.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) lw_entrances.append('Hyrule Castle Entrance (South)') - if not world.shuffle_ganon: - connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player) + if not multiworld.shuffle_ganon: + connect_two_way(multiworld, 'Ganons Tower', 'Ganons Tower Exit', player) else: dw_entrances.append('Ganons Tower') dungeon_exits.append('Ganons Tower Exit') - if world.worlds[player].options.mode == 'standard': + if multiworld.worlds[player].options.mode == 'standard': # rest of hyrule castle must be in light world, so it has to be the one connected to east exit of desert hyrule_castle_exits = [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')] - connect_mandatory_exits(world, lw_entrances, hyrule_castle_exits, list(LW_Dungeon_Entrances_Must_Exit), player) - connect_caves(world, lw_entrances, [], hyrule_castle_exits, player) + connect_mandatory_exits(multiworld, lw_entrances, hyrule_castle_exits, list(LW_Dungeon_Entrances_Must_Exit), player) + connect_caves(multiworld, lw_entrances, [], hyrule_castle_exits, player) else: - connect_mandatory_exits(world, lw_entrances, dungeon_exits, list(LW_Dungeon_Entrances_Must_Exit), player) - connect_mandatory_exits(world, dw_entrances, dungeon_exits, list(DW_Dungeon_Entrances_Must_Exit), player) - connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player) - elif world.worlds[player].options.entrance_shuffle == 'dungeons_crossed': - crossed_shuffle_dungeons(world, player) - elif world.worlds[player].options.entrance_shuffle == 'simple': - simple_shuffle_dungeons(world, player) + connect_mandatory_exits(multiworld, lw_entrances, dungeon_exits, list(LW_Dungeon_Entrances_Must_Exit), player) + connect_mandatory_exits(multiworld, dw_entrances, dungeon_exits, list(DW_Dungeon_Entrances_Must_Exit), player) + connect_caves(multiworld, lw_entrances, dw_entrances, dungeon_exits, player) + elif multiworld.worlds[player].options.entrance_shuffle == 'dungeons_crossed': + crossed_shuffle_dungeons(multiworld, player) + elif multiworld.worlds[player].options.entrance_shuffle == 'simple': + simple_shuffle_dungeons(multiworld, player) old_man_entrances = list(Old_Man_Entrances) caves = list(Cave_Exits) @@ -82,64 +83,64 @@ def link_entrances(world, player): # we shuffle all 2 entrance caves as pairs as a start # start with the ones that need to be directed two_door_caves = list(Two_Door_Caves_Directional) - world.random.shuffle(two_door_caves) - world.random.shuffle(caves) + multiworld.random.shuffle(two_door_caves) + multiworld.random.shuffle(caves) while two_door_caves: entrance1, entrance2 = two_door_caves.pop() exit1, exit2 = caves.pop() - connect_two_way(world, entrance1, exit1, player) - connect_two_way(world, entrance2, exit2, player) + connect_two_way(multiworld, entrance1, exit1, player) + connect_two_way(multiworld, entrance2, exit2, player) # now the remaining pairs two_door_caves = list(Two_Door_Caves) - world.random.shuffle(two_door_caves) + multiworld.random.shuffle(two_door_caves) while two_door_caves: entrance1, entrance2 = two_door_caves.pop() exit1, exit2 = caves.pop() - connect_two_way(world, entrance1, exit1, player) - connect_two_way(world, entrance2, exit2, player) + connect_two_way(multiworld, entrance1, exit1, player) + connect_two_way(multiworld, entrance2, exit2, player) # at this point only Light World death mountain entrances remain # place old man, has limited options remaining_entrances = ['Old Man Cave (West)', 'Old Man House (Bottom)', 'Death Mountain Return Cave (West)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'Paradox Cave (Top)', 'Fairy Ascension Cave (Bottom)', 'Fairy Ascension Cave (Top)', 'Spiral Cave', 'Spiral Cave (Bottom)'] - world.random.shuffle(old_man_entrances) + multiworld.random.shuffle(old_man_entrances) old_man_exit = old_man_entrances.pop() remaining_entrances.extend(old_man_entrances) - world.random.shuffle(remaining_entrances) + multiworld.random.shuffle(remaining_entrances) old_man_entrance = remaining_entrances.pop() - connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) + connect_two_way(multiworld, old_man_entrance, 'Old Man Cave Exit (West)', player) + connect_two_way(multiworld, old_man_exit, 'Old Man Cave Exit (East)', player) # add old man house to ensure it is always somewhere on light death mountain caves.extend(list(Old_Man_House)) caves.extend(list(three_exit_caves)) # connect rest - connect_caves(world, remaining_entrances, [], caves, player) + connect_caves(multiworld, remaining_entrances, [], caves, player) # scramble holes - scramble_holes(world, player) + scramble_holes(multiworld, player) # place blacksmith, has limited options - world.random.shuffle(blacksmith_doors) + multiworld.random.shuffle(blacksmith_doors) blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) + connect_entrance(multiworld, blacksmith_hut, 'Blacksmiths Hut', player) bomb_shop_doors.extend(blacksmith_doors) # place bomb shop, has limited options - world.random.shuffle(bomb_shop_doors) + multiworld.random.shuffle(bomb_shop_doors) bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) + connect_entrance(multiworld, bomb_shop, 'Big Bomb Shop', player) single_doors.extend(bomb_shop_doors) # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) + connect_doors(multiworld, ['Tavern North'], ['Tavern'], player) # place remaining doors - connect_doors(world, single_doors, door_targets, player) - elif world.worlds[player].options.entrance_shuffle == 'restricted': - simple_shuffle_dungeons(world, player) + connect_doors(multiworld, single_doors, door_targets, player) + elif multiworld.worlds[player].options.entrance_shuffle == 'restricted': + simple_shuffle_dungeons(multiworld, player) lw_entrances = list(LW_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances) dw_entrances = list(DW_Entrances + DW_Single_Cave_Doors) @@ -152,26 +153,26 @@ def link_entrances(world, player): door_targets = list(Single_Cave_Targets) # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) + connect_doors(multiworld, ['Tavern North'], ['Tavern'], player) # in restricted, the only mandatory exits are in dark world - connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player) + connect_mandatory_exits(multiworld, dw_entrances, caves, dw_must_exits, player) # place old man, has limited options # exit has to come from specific set of doors, the entrance is free to move about old_man_entrances = [door for door in old_man_entrances if door in lw_entrances] - world.random.shuffle(old_man_entrances) + multiworld.random.shuffle(old_man_entrances) old_man_exit = old_man_entrances.pop() - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) + connect_two_way(multiworld, old_man_exit, 'Old Man Cave Exit (East)', player) lw_entrances.remove(old_man_exit) # place blacksmith, has limited options all_entrances = lw_entrances + dw_entrances # cannot place it anywhere already taken (or that are otherwise not eligable for placement) blacksmith_doors = [door for door in blacksmith_doors if door in all_entrances] - world.random.shuffle(blacksmith_doors) + multiworld.random.shuffle(blacksmith_doors) blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) + connect_entrance(multiworld, blacksmith_hut, 'Blacksmiths Hut', player) if blacksmith_hut in lw_entrances: lw_entrances.remove(blacksmith_hut) if blacksmith_hut in dw_entrances: @@ -182,36 +183,36 @@ def link_entrances(world, player): all_entrances = lw_entrances + dw_entrances # cannot place it anywhere already taken (or that are otherwise not eligable for placement) bomb_shop_doors = [door for door in bomb_shop_doors if door in all_entrances] - world.random.shuffle(bomb_shop_doors) + multiworld.random.shuffle(bomb_shop_doors) bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) + connect_entrance(multiworld, bomb_shop, 'Big Bomb Shop', player) if bomb_shop in lw_entrances: lw_entrances.remove(bomb_shop) if bomb_shop in dw_entrances: dw_entrances.remove(bomb_shop) # place the old man cave's entrance somewhere in the light world - world.random.shuffle(lw_entrances) + multiworld.random.shuffle(lw_entrances) old_man_entrance = lw_entrances.pop() - connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) + connect_two_way(multiworld, old_man_entrance, 'Old Man Cave Exit (West)', player) # place Old Man House in Light World - connect_caves(world, lw_entrances, [], list(Old_Man_House), player) #for multiple seeds + connect_caves(multiworld, lw_entrances, [], list(Old_Man_House), player) #for multiple seeds # now scramble the rest - connect_caves(world, lw_entrances, dw_entrances, caves, player) + connect_caves(multiworld, lw_entrances, dw_entrances, caves, player) # scramble holes - scramble_holes(world, player) + scramble_holes(multiworld, player) doors = lw_entrances + dw_entrances # place remaining doors - connect_doors(world, doors, door_targets, player) + connect_doors(multiworld, doors, door_targets, player) - elif world.worlds[player].options.entrance_shuffle == 'full': - skull_woods_shuffle(world, player) + elif multiworld.worlds[player].options.entrance_shuffle == 'full': + skull_woods_shuffle(multiworld, player) lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances) dw_entrances = list(DW_Entrances + DW_Dungeon_Entrances + DW_Single_Cave_Doors) @@ -225,18 +226,18 @@ def link_entrances(world, player): old_man_house = list(Old_Man_House) # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) + connect_doors(multiworld, ['Tavern North'], ['Tavern'], player) - if world.worlds[player].options.mode == 'standard': + if multiworld.worlds[player].options.mode == 'standard': # must connect front of hyrule castle to do escape - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) else: - caves.append(tuple(world.random.sample( + caves.append(tuple(multiworld.random.sample( ['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'], 3))) lw_entrances.append('Hyrule Castle Entrance (South)') - if not world.shuffle_ganon: - connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player) + if not multiworld.shuffle_ganon: + connect_two_way(multiworld, 'Ganons Tower', 'Ganons Tower Exit', player) else: dw_entrances.append('Ganons Tower') caves.append('Ganons Tower Exit') @@ -244,45 +245,45 @@ def link_entrances(world, player): # we randomize which world requirements we fulfill first so we get better dungeon distribution #we also places the Old Man House at this time to make sure he can be connected to the desert one way - if world.random.randint(0, 1) == 0: + if multiworld.random.randint(0, 1) == 0: caves += old_man_house - connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player) + connect_mandatory_exits(multiworld, lw_entrances, caves, lw_must_exits, player) try: caves.remove(old_man_house[0]) except ValueError: pass else: # if the cave wasn't placed we get here - connect_caves(world, lw_entrances, [], old_man_house, player) - connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player) + connect_caves(multiworld, lw_entrances, [], old_man_house, player) + connect_mandatory_exits(multiworld, dw_entrances, caves, dw_must_exits, player) else: - connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player) + connect_mandatory_exits(multiworld, dw_entrances, caves, dw_must_exits, player) caves += old_man_house - connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player) + connect_mandatory_exits(multiworld, lw_entrances, caves, lw_must_exits, player) try: caves.remove(old_man_house[0]) except ValueError: pass else: #if the cave wasn't placed we get here - connect_caves(world, lw_entrances, [], old_man_house, player) - if world.worlds[player].options.mode == 'standard': + connect_caves(multiworld, lw_entrances, [], old_man_house, player) + if multiworld.worlds[player].options.mode == 'standard': # rest of hyrule castle must be in light world - connect_caves(world, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player) + connect_caves(multiworld, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player) # place old man, has limited options # exit has to come from specific set of doors, the entrance is free to move about old_man_entrances = [door for door in old_man_entrances if door in lw_entrances] - world.random.shuffle(old_man_entrances) + multiworld.random.shuffle(old_man_entrances) old_man_exit = old_man_entrances.pop() - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) + connect_two_way(multiworld, old_man_exit, 'Old Man Cave Exit (East)', player) lw_entrances.remove(old_man_exit) # place blacksmith, has limited options all_entrances = lw_entrances + dw_entrances # cannot place it anywhere already taken (or that are otherwise not eligable for placement) blacksmith_doors = [door for door in blacksmith_doors if door in all_entrances] - world.random.shuffle(blacksmith_doors) + multiworld.random.shuffle(blacksmith_doors) blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) + connect_entrance(multiworld, blacksmith_hut, 'Blacksmiths Hut', player) if blacksmith_hut in lw_entrances: lw_entrances.remove(blacksmith_hut) if blacksmith_hut in dw_entrances: @@ -293,9 +294,9 @@ def link_entrances(world, player): all_entrances = lw_entrances + dw_entrances # cannot place it anywhere already taken (or that are otherwise not eligable for placement) bomb_shop_doors = [door for door in bomb_shop_doors if door in all_entrances] - world.random.shuffle(bomb_shop_doors) + multiworld.random.shuffle(bomb_shop_doors) bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) + connect_entrance(multiworld, bomb_shop, 'Big Bomb Shop', player) if bomb_shop in lw_entrances: lw_entrances.remove(bomb_shop) if bomb_shop in dw_entrances: @@ -303,21 +304,21 @@ def link_entrances(world, player): # place the old man cave's entrance somewhere in the light world old_man_entrance = lw_entrances.pop() - connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) + connect_two_way(multiworld, old_man_entrance, 'Old Man Cave Exit (West)', player) # now scramble the rest - connect_caves(world, lw_entrances, dw_entrances, caves, player) + connect_caves(multiworld, lw_entrances, dw_entrances, caves, player) # scramble holes - scramble_holes(world, player) + scramble_holes(multiworld, player) doors = lw_entrances + dw_entrances # place remaining doors - connect_doors(world, doors, door_targets, player) - elif world.worlds[player].options.entrance_shuffle == 'crossed': - skull_woods_shuffle(world, player) + connect_doors(multiworld, doors, door_targets, player) + elif multiworld.worlds[player].options.entrance_shuffle == 'crossed': + skull_woods_shuffle(multiworld, player) entrances = list(LW_Entrances + LW_Dungeon_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances + DW_Entrances + DW_Dungeon_Entrances + DW_Single_Cave_Doors) must_exits = list(DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit + LW_Dungeon_Entrances_Must_Exit) @@ -329,43 +330,43 @@ def link_entrances(world, player): door_targets = list(Single_Cave_Targets) # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) + connect_doors(multiworld, ['Tavern North'], ['Tavern'], player) - if world.worlds[player].options.mode == 'standard': + if multiworld.worlds[player].options.mode == 'standard': # must connect front of hyrule castle to do escape - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) else: - caves.append(tuple(world.random.sample( + caves.append(tuple(multiworld.random.sample( ['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'], 3))) entrances.append('Hyrule Castle Entrance (South)') - if not world.shuffle_ganon: - connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player) + if not multiworld.shuffle_ganon: + connect_two_way(multiworld, 'Ganons Tower', 'Ganons Tower Exit', player) else: entrances.append('Ganons Tower') caves.append('Ganons Tower Exit') #place must-exit caves - connect_mandatory_exits(world, entrances, caves, must_exits, player) + connect_mandatory_exits(multiworld, entrances, caves, must_exits, player) - if world.worlds[player].options.mode == 'standard': + if multiworld.worlds[player].options.mode == 'standard': # rest of hyrule castle must be dealt with - connect_caves(world, entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player) + connect_caves(multiworld, entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player) # place old man, has limited options # exit has to come from specific set of doors, the entrance is free to move about old_man_entrances = [door for door in old_man_entrances if door in entrances] - world.random.shuffle(old_man_entrances) + multiworld.random.shuffle(old_man_entrances) old_man_exit = old_man_entrances.pop() - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) + connect_two_way(multiworld, old_man_exit, 'Old Man Cave Exit (East)', player) entrances.remove(old_man_exit) # place blacksmith, has limited options # cannot place it anywhere already taken (or that are otherwise not eligable for placement) blacksmith_doors = [door for door in blacksmith_doors if door in entrances] - world.random.shuffle(blacksmith_doors) + multiworld.random.shuffle(blacksmith_doors) blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) + connect_entrance(multiworld, blacksmith_hut, 'Blacksmiths Hut', player) entrances.remove(blacksmith_hut) bomb_shop_doors.extend(blacksmith_doors) @@ -373,28 +374,28 @@ def link_entrances(world, player): # cannot place it anywhere already taken (or that are otherwise not eligable for placement) bomb_shop_doors = [door for door in bomb_shop_doors if door in entrances] - world.random.shuffle(bomb_shop_doors) + multiworld.random.shuffle(bomb_shop_doors) bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) + connect_entrance(multiworld, bomb_shop, 'Big Bomb Shop', player) entrances.remove(bomb_shop) # place the old man cave's entrance somewhere - world.random.shuffle(entrances) + multiworld.random.shuffle(entrances) old_man_entrance = entrances.pop() - connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) + connect_two_way(multiworld, old_man_entrance, 'Old Man Cave Exit (West)', player) # now scramble the rest - connect_caves(world, entrances, [], caves, player) + connect_caves(multiworld, entrances, [], caves, player) # scramble holes - scramble_holes(world, player) + scramble_holes(multiworld, player) # place remaining doors - connect_doors(world, entrances, door_targets, player) + connect_doors(multiworld, entrances, door_targets, player) - elif world.worlds[player].options.entrance_shuffle == 'insanity': + elif multiworld.worlds[player].options.entrance_shuffle == 'insanity': # beware ye who enter here entrances = LW_Entrances + LW_Dungeon_Entrances + DW_Entrances + DW_Dungeon_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave'] @@ -412,7 +413,7 @@ def link_entrances(world, player): blacksmith_doors = list(Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors) door_targets = list(Single_Cave_Targets) - world.random.shuffle(doors) + multiworld.random.shuffle(doors) old_man_entrances = list(Old_Man_Entrances) + ['Tower of Hera'] @@ -429,13 +430,13 @@ def link_entrances(world, player): 'Skull Woods First Section (Left)', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Top)'] # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) + connect_doors(multiworld, ['Tavern North'], ['Tavern'], player) - if world.worlds[player].options.mode == 'standard': + if multiworld.worlds[player].options.mode == 'standard': # cannot move uncle cave - connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player) - connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player) - connect_entrance(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player) + connect_entrance(multiworld, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player) + connect_exit(multiworld, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player) + connect_entrance(multiworld, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player) else: hole_entrances.append('Hyrule Castle Secret Entrance Drop') hole_targets.append('Hyrule Castle Secret Entrance') @@ -443,10 +444,10 @@ def link_entrances(world, player): entrances.append('Hyrule Castle Secret Entrance Stairs') caves.append('Hyrule Castle Secret Entrance Exit') - if not world.shuffle_ganon: - connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player) - connect_two_way(world, 'Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Pyramid Hole', 'Pyramid', player) + if not multiworld.shuffle_ganon: + connect_two_way(multiworld, 'Ganons Tower', 'Ganons Tower Exit', player) + connect_two_way(multiworld, 'Pyramid Entrance', 'Pyramid Exit', player) + connect_entrance(multiworld, 'Pyramid Hole', 'Pyramid', player) else: entrances.append('Ganons Tower') caves.extend(['Ganons Tower Exit', 'Pyramid Exit']) @@ -455,19 +456,19 @@ def link_entrances(world, player): entrances_must_exits.append('Pyramid Entrance') doors.extend(['Ganons Tower', 'Pyramid Entrance']) - world.random.shuffle(hole_entrances) - world.random.shuffle(hole_targets) - world.random.shuffle(entrances) + multiworld.random.shuffle(hole_entrances) + multiworld.random.shuffle(hole_targets) + multiworld.random.shuffle(entrances) # fill up holes for hole in hole_entrances: - connect_entrance(world, hole, hole_targets.pop(), player) + connect_entrance(multiworld, hole, hole_targets.pop(), player) # hyrule castle handling - if world.worlds[player].options.mode == 'standard': + if multiworld.worlds[player].options.mode == 'standard': # must connect front of hyrule castle to do escape - connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player) + connect_entrance(multiworld, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) + connect_exit(multiworld, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player) caves.append(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) else: doors.append('Hyrule Castle Entrance (South)') @@ -476,7 +477,7 @@ def link_entrances(world, player): # now let's deal with mandatory reachable stuff def extract_reachable_exit(cavelist): - world.random.shuffle(cavelist) + multiworld.random.shuffle(cavelist) candidate = None for cave in cavelist: if isinstance(cave, tuple) and len(cave) > 1: @@ -496,8 +497,8 @@ def link_entrances(world, player): exit = cave[-1] cave = cave[:-1] - connect_exit(world, exit, entrance, player) - connect_entrance(world, doors.pop(), exit, player) + connect_exit(multiworld, exit, entrance, player) + connect_entrance(multiworld, doors.pop(), exit, player) # rest of cave now is forced to be in this world caves.append(cave) @@ -508,26 +509,26 @@ def link_entrances(world, player): # place old man, has limited options # exit has to come from specific set of doors, the entrance is free to move about old_man_entrances = [entrance for entrance in old_man_entrances if entrance in entrances] - world.random.shuffle(old_man_entrances) + multiworld.random.shuffle(old_man_entrances) old_man_exit = old_man_entrances.pop() entrances.remove(old_man_exit) - connect_exit(world, 'Old Man Cave Exit (East)', old_man_exit, player) - connect_entrance(world, doors.pop(), 'Old Man Cave Exit (East)', player) + connect_exit(multiworld, 'Old Man Cave Exit (East)', old_man_exit, player) + connect_entrance(multiworld, doors.pop(), 'Old Man Cave Exit (East)', player) caves.append('Old Man Cave Exit (West)') # place blacksmith, has limited options blacksmith_doors = [door for door in blacksmith_doors if door in doors] - world.random.shuffle(blacksmith_doors) + multiworld.random.shuffle(blacksmith_doors) blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) + connect_entrance(multiworld, blacksmith_hut, 'Blacksmiths Hut', player) doors.remove(blacksmith_hut) # place dam and pyramid fairy, have limited options bomb_shop_doors = [door for door in bomb_shop_doors if door in doors] - world.random.shuffle(bomb_shop_doors) + multiworld.random.shuffle(bomb_shop_doors) bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) + connect_entrance(multiworld, bomb_shop, 'Big Bomb Shop', player) doors.remove(bomb_shop) # handle remaining caves @@ -536,40 +537,40 @@ def link_entrances(world, player): cave = (cave,) for exit in cave: - connect_exit(world, exit, entrances.pop(), player) - connect_entrance(world, doors.pop(), exit, player) + connect_exit(multiworld, exit, entrances.pop(), player) + connect_entrance(multiworld, doors.pop(), exit, player) # place remaining doors - connect_doors(world, doors, door_targets, player) + connect_doors(multiworld, doors, door_targets, player) else: raise NotImplementedError( - f'{world.worlds[player].options.entrance_shuffle} Shuffling not supported yet. Player {world.get_player_name(player)}') + f'{multiworld.worlds[player].options.entrance_shuffle} Shuffling not supported yet. Player {multiworld.get_player_name(player)}') - if world.worlds[player].options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']: - overworld_glitch_connections(world, player) + if multiworld.worlds[player].options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']: + overworld_glitch_connections(multiworld, player) # mandatory hybrid major glitches connections - if world.worlds[player].options.glitches_required in ['hybrid_major_glitches', 'no_logic']: - underworld_glitch_connections(world, player) + if multiworld.worlds[player].options.glitches_required in ['hybrid_major_glitches', 'no_logic']: + underworld_glitch_connections(multiworld, player) # check for swamp palace fix - if world.get_entrance('Dam', player).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)': - world.worlds[player].swamp_patch_required = True + if multiworld.get_entrance('Dam', player).connected_region.name != 'Dam' or multiworld.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)': + multiworld.worlds[player].swamp_patch_required = True # check for potion shop location - if world.get_entrance('Potion Shop', player).connected_region.name != 'Potion Shop': - world.worlds[player].powder_patch_required = True + if multiworld.get_entrance('Potion Shop', player).connected_region.name != 'Potion Shop': + multiworld.worlds[player].powder_patch_required = True # check for ganon location - if world.get_entrance('Pyramid Hole', player).connected_region.name != 'Pyramid': - world.worlds[player].ganon_at_pyramid = False + if multiworld.get_entrance('Pyramid Hole', player).connected_region.name != 'Pyramid': + multiworld.worlds[player].ganon_at_pyramid = False # check for Ganon's Tower location - if world.get_entrance('Ganons Tower', player).connected_region.name != 'Ganons Tower (Entrance)': - world.worlds[player].ganonstower_vanilla = False + if multiworld.get_entrance('Ganons Tower', player).connected_region.name != 'Ganons Tower (Entrance)': + multiworld.worlds[player].ganonstower_vanilla = False -def link_inverted_entrances(world, player): +def link_inverted_entrances(multiworld: MultiWorld, player: int): # Link's house shuffled freely, Houlihan set in mandatory_connections Dungeon_Exits = Inverted_Dungeon_Exits_Base.copy() @@ -577,28 +578,28 @@ def link_inverted_entrances(world, player): Old_Man_House = Old_Man_House_Base.copy() Cave_Three_Exits = Cave_Three_Exits_Base.copy() - unbias_some_entrances(world, Dungeon_Exits, Cave_Exits, Old_Man_House, Cave_Three_Exits) + unbias_some_entrances(multiworld, Dungeon_Exits, Cave_Exits, Old_Man_House, Cave_Three_Exits) # setup mandatory connections for exitname, regionname in inverted_mandatory_connections: - connect_simple(world, exitname, regionname, player) + connect_simple(multiworld, exitname, regionname, player) # if we do not shuffle, set default connections - if world.worlds[player].options.entrance_shuffle == 'vanilla': + if multiworld.worlds[player].options.entrance_shuffle == 'vanilla': for exitname, regionname in inverted_default_connections: - connect_simple(world, exitname, regionname, player) + connect_simple(multiworld, exitname, regionname, player) for exitname, regionname in inverted_default_dungeon_connections: - connect_simple(world, exitname, regionname, player) - elif world.worlds[player].options.entrance_shuffle == 'dungeons_simple': + connect_simple(multiworld, exitname, regionname, player) + elif multiworld.worlds[player].options.entrance_shuffle == 'dungeons_simple': for exitname, regionname in inverted_default_connections: - connect_simple(world, exitname, regionname, player) + connect_simple(multiworld, exitname, regionname, player) - simple_shuffle_dungeons(world, player) - elif world.worlds[player].options.entrance_shuffle == 'dungeons_full': + simple_shuffle_dungeons(multiworld, player) + elif multiworld.worlds[player].options.entrance_shuffle == 'dungeons_full': for exitname, regionname in inverted_default_connections: - connect_simple(world, exitname, regionname, player) + connect_simple(multiworld, exitname, regionname, player) - skull_woods_shuffle(world, player) + skull_woods_shuffle(multiworld, player) dungeon_exits = list(Dungeon_Exits) lw_entrances = list(Inverted_LW_Dungeon_Entrances) @@ -606,7 +607,7 @@ def link_inverted_entrances(world, player): dw_entrances = list(Inverted_DW_Dungeon_Entrances) # randomize which desert ledge door is a must-exit - if world.random.randint(0, 1): + if multiworld.random.randint(0, 1): lw_dungeon_entrances_must_exit.append('Desert Palace Entrance (North)') lw_entrances.append('Desert Palace Entrance (West)') else: @@ -616,8 +617,8 @@ def link_inverted_entrances(world, player): dungeon_exits.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) lw_entrances.append('Hyrule Castle Entrance (South)') - if not world.shuffle_ganon: - connect_two_way(world, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player) + if not multiworld.shuffle_ganon: + connect_two_way(multiworld, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player) hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)'] else: lw_entrances.append('Inverted Ganons Tower') @@ -627,14 +628,14 @@ def link_inverted_entrances(world, player): # shuffle aga door first. If it's on HC ledge, remaining HC ledge door must be must-exit all_entrances_aga = lw_entrances + dw_entrances aga_doors = [i for i in all_entrances_aga] - world.random.shuffle(aga_doors) + multiworld.random.shuffle(aga_doors) aga_door = aga_doors.pop() if aga_door in hc_ledge_entrances: lw_entrances.remove(aga_door) hc_ledge_entrances.remove(aga_door) - world.random.shuffle(hc_ledge_entrances) + multiworld.random.shuffle(hc_ledge_entrances) hc_ledge_must_exit = hc_ledge_entrances.pop() lw_entrances.remove(hc_ledge_must_exit) lw_dungeon_entrances_must_exit.append(hc_ledge_must_exit) @@ -643,16 +644,16 @@ def link_inverted_entrances(world, player): else: dw_entrances.remove(aga_door) - connect_two_way(world, aga_door, 'Inverted Agahnims Tower Exit', player) + connect_two_way(multiworld, aga_door, 'Inverted Agahnims Tower Exit', player) dungeon_exits.remove('Inverted Agahnims Tower Exit') - connect_mandatory_exits(world, lw_entrances, dungeon_exits, lw_dungeon_entrances_must_exit, player) + connect_mandatory_exits(multiworld, lw_entrances, dungeon_exits, lw_dungeon_entrances_must_exit, player) - connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player) - elif world.worlds[player].options.entrance_shuffle == 'dungeons_crossed': - inverted_crossed_shuffle_dungeons(world, player) - elif world.worlds[player].options.entrance_shuffle == 'simple': - simple_shuffle_dungeons(world, player) + connect_caves(multiworld, lw_entrances, dw_entrances, dungeon_exits, player) + elif multiworld.worlds[player].options.entrance_shuffle == 'dungeons_crossed': + inverted_crossed_shuffle_dungeons(multiworld, player) + elif multiworld.worlds[player].options.entrance_shuffle == 'simple': + simple_shuffle_dungeons(multiworld, player) old_man_entrances = list(Inverted_Old_Man_Entrances) caves = list(Cave_Exits) @@ -666,28 +667,28 @@ def link_inverted_entrances(world, player): # we shuffle all 2 entrance caves as pairs as a start # start with the ones that need to be directed two_door_caves = list(Inverted_Two_Door_Caves_Directional) - world.random.shuffle(two_door_caves) - world.random.shuffle(caves) + multiworld.random.shuffle(two_door_caves) + multiworld.random.shuffle(caves) while two_door_caves: entrance1, entrance2 = two_door_caves.pop() exit1, exit2 = caves.pop() - connect_two_way(world, entrance1, exit1, player) - connect_two_way(world, entrance2, exit2, player) + connect_two_way(multiworld, entrance1, exit1, player) + connect_two_way(multiworld, entrance2, exit2, player) # now the remaining pairs two_door_caves = list(Inverted_Two_Door_Caves) - world.random.shuffle(two_door_caves) + multiworld.random.shuffle(two_door_caves) while two_door_caves: entrance1, entrance2 = two_door_caves.pop() exit1, exit2 = caves.pop() - connect_two_way(world, entrance1, exit1, player) - connect_two_way(world, entrance2, exit2, player) + connect_two_way(multiworld, entrance1, exit1, player) + connect_two_way(multiworld, entrance2, exit2, player) # place links house links_house_doors = [i for i in bomb_shop_doors + blacksmith_doors if i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors] - links_house = world.random.choice(list(links_house_doors)) - connect_two_way(world, links_house, 'Inverted Links House Exit', player) + links_house = multiworld.random.choice(list(links_house_doors)) + connect_two_way(multiworld, links_house, 'Inverted Links House Exit', player) if links_house in bomb_shop_doors: bomb_shop_doors.remove(links_house) if links_house in blacksmith_doors: @@ -697,11 +698,11 @@ def link_inverted_entrances(world, player): # place dark sanc sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in bomb_shop_doors] - sanc_door = world.random.choice(sanc_doors) + sanc_door = multiworld.random.choice(sanc_doors) bomb_shop_doors.remove(sanc_door) - connect_entrance(world, sanc_door, 'Inverted Dark Sanctuary', player) - world.get_entrance('Inverted Dark Sanctuary Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) + connect_entrance(multiworld, sanc_door, 'Inverted Dark Sanctuary', player) + multiworld.get_entrance('Inverted Dark Sanctuary Exit', player).connect(multiworld.get_entrance(sanc_door, player).parent_region) lw_dm_entrances = ['Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'Paradox Cave (Top)', 'Old Man House (Bottom)', 'Fairy Ascension Cave (Bottom)', 'Fairy Ascension Cave (Top)', 'Spiral Cave (Bottom)', 'Old Man Cave (East)', @@ -710,10 +711,10 @@ def link_inverted_entrances(world, player): # place old man, bumper cave bottom to DDM entrances not in east bottom - world.random.shuffle(old_man_entrances) + multiworld.random.shuffle(old_man_entrances) old_man_exit = old_man_entrances.pop() - connect_two_way(world, 'Bumper Cave (Bottom)', 'Old Man Cave Exit (West)', player) - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) + connect_two_way(multiworld, 'Bumper Cave (Bottom)', 'Old Man Cave Exit (West)', player) + connect_two_way(multiworld, old_man_exit, 'Old Man Cave Exit (East)', player) if old_man_exit == 'Spike Cave': bomb_shop_doors.remove('Spike Cave') bomb_shop_doors.extend(old_man_entrances) @@ -723,33 +724,33 @@ def link_inverted_entrances(world, player): caves.extend(list(three_exit_caves)) # connect rest - connect_caves(world, lw_dm_entrances, [], caves, player) + connect_caves(multiworld, lw_dm_entrances, [], caves, player) # scramble holes - scramble_inverted_holes(world, player) + scramble_inverted_holes(multiworld, player) # place blacksmith, has limited options blacksmith_doors = [door for door in blacksmith_doors[:]] - world.random.shuffle(blacksmith_doors) + multiworld.random.shuffle(blacksmith_doors) blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) + connect_entrance(multiworld, blacksmith_hut, 'Blacksmiths Hut', player) bomb_shop_doors.extend(blacksmith_doors) # place bomb shop, has limited options bomb_shop_doors = [door for door in bomb_shop_doors[:]] - world.random.shuffle(bomb_shop_doors) + multiworld.random.shuffle(bomb_shop_doors) bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Inverted Big Bomb Shop', player) + connect_entrance(multiworld, bomb_shop, 'Inverted Big Bomb Shop', player) single_doors.extend(bomb_shop_doors) # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) + connect_doors(multiworld, ['Tavern North'], ['Tavern'], player) # place remaining doors - connect_doors(world, single_doors, door_targets, player) + connect_doors(multiworld, single_doors, door_targets, player) - elif world.worlds[player].options.entrance_shuffle == 'restricted': - simple_shuffle_dungeons(world, player) + elif multiworld.worlds[player].options.entrance_shuffle == 'restricted': + simple_shuffle_dungeons(multiworld, player) lw_entrances = list(Inverted_LW_Entrances + Inverted_LW_Single_Cave_Doors) dw_entrances = list(Inverted_DW_Entrances + Inverted_DW_Single_Cave_Doors + Inverted_Old_Man_Entrances) @@ -764,8 +765,8 @@ def link_inverted_entrances(world, player): # place links house links_house_doors = [i for i in lw_entrances + dw_entrances + lw_must_exits if i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors] - links_house = world.random.choice(list(links_house_doors)) - connect_two_way(world, links_house, 'Inverted Links House Exit', player) + links_house = multiworld.random.choice(list(links_house_doors)) + connect_two_way(multiworld, links_house, 'Inverted Links House Exit', player) if links_house in lw_entrances: lw_entrances.remove(links_house) elif links_house in dw_entrances: @@ -775,32 +776,32 @@ def link_inverted_entrances(world, player): # place dark sanc sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in dw_entrances] - sanc_door = world.random.choice(sanc_doors) + sanc_door = multiworld.random.choice(sanc_doors) dw_entrances.remove(sanc_door) - connect_entrance(world, sanc_door, 'Inverted Dark Sanctuary', player) - world.get_entrance('Inverted Dark Sanctuary Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) + connect_entrance(multiworld, sanc_door, 'Inverted Dark Sanctuary', player) + multiworld.get_entrance('Inverted Dark Sanctuary Exit', player).connect(multiworld.get_entrance(sanc_door, player).parent_region) # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) + connect_doors(multiworld, ['Tavern North'], ['Tavern'], player) # place must exits - connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player) + connect_mandatory_exits(multiworld, lw_entrances, caves, lw_must_exits, player) # place old man, has limited options # exit has to come from specific set of doors, the entrance is free to move about old_man_entrances = [door for door in old_man_entrances if door in dw_entrances] - world.random.shuffle(old_man_entrances) + multiworld.random.shuffle(old_man_entrances) old_man_exit = old_man_entrances.pop() - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) + connect_two_way(multiworld, old_man_exit, 'Old Man Cave Exit (East)', player) dw_entrances.remove(old_man_exit) # place blacksmith, has limited options all_entrances = lw_entrances + dw_entrances # cannot place it anywhere already taken (or that are otherwise not eligible for placement) blacksmith_doors = [door for door in blacksmith_doors if door in all_entrances] - world.random.shuffle(blacksmith_doors) + multiworld.random.shuffle(blacksmith_doors) blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) + connect_entrance(multiworld, blacksmith_hut, 'Blacksmiths Hut', player) if blacksmith_hut in lw_entrances: lw_entrances.remove(blacksmith_hut) if blacksmith_hut in dw_entrances: @@ -811,30 +812,30 @@ def link_inverted_entrances(world, player): all_entrances = lw_entrances + dw_entrances # cannot place it anywhere already taken (or that are otherwise not eligible for placement) bomb_shop_doors = [door for door in bomb_shop_doors if door in all_entrances] - world.random.shuffle(bomb_shop_doors) + multiworld.random.shuffle(bomb_shop_doors) bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Inverted Big Bomb Shop', player) + connect_entrance(multiworld, bomb_shop, 'Inverted Big Bomb Shop', player) if bomb_shop in lw_entrances: lw_entrances.remove(bomb_shop) if bomb_shop in dw_entrances: dw_entrances.remove(bomb_shop) # place the old man cave's entrance somewhere in the dark world - world.random.shuffle(dw_entrances) + multiworld.random.shuffle(dw_entrances) old_man_entrance = dw_entrances.pop() - connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) + connect_two_way(multiworld, old_man_entrance, 'Old Man Cave Exit (West)', player) # now scramble the rest - connect_caves(world, lw_entrances, dw_entrances, caves, player) + connect_caves(multiworld, lw_entrances, dw_entrances, caves, player) # scramble holes - scramble_inverted_holes(world, player) + scramble_inverted_holes(multiworld, player) doors = lw_entrances + dw_entrances # place remaining doors - connect_doors(world, doors, door_targets, player) - elif world.worlds[player].options.entrance_shuffle == 'full': - skull_woods_shuffle(world, player) + connect_doors(multiworld, doors, door_targets, player) + elif multiworld.worlds[player].options.entrance_shuffle == 'full': + skull_woods_shuffle(multiworld, player) lw_entrances = list(Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_LW_Single_Cave_Doors) dw_entrances = list(Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_DW_Single_Cave_Doors + Inverted_Old_Man_Entrances) @@ -847,7 +848,7 @@ def link_inverted_entrances(world, player): old_man_house = list(Old_Man_House) # randomize which desert ledge door is a must-exit - if world.random.randint(0, 1) == 0: + if multiworld.random.randint(0, 1) == 0: lw_must_exits.append('Desert Palace Entrance (North)') lw_entrances.append('Desert Palace Entrance (West)') else: @@ -855,12 +856,12 @@ def link_inverted_entrances(world, player): lw_entrances.append('Desert Palace Entrance (North)') # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) + connect_doors(multiworld, ['Tavern North'], ['Tavern'], player) lw_entrances.append('Hyrule Castle Entrance (South)') - if not world.shuffle_ganon: - connect_two_way(world, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player) + if not multiworld.shuffle_ganon: + connect_two_way(multiworld, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player) hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)'] else: lw_entrances.append('Inverted Ganons Tower') @@ -870,14 +871,14 @@ def link_inverted_entrances(world, player): # shuffle aga door first. if it's on hc ledge, then one other hc ledge door has to be must_exit all_entrances_aga = lw_entrances + dw_entrances aga_doors = [i for i in all_entrances_aga] - world.random.shuffle(aga_doors) + multiworld.random.shuffle(aga_doors) aga_door = aga_doors.pop() if aga_door in hc_ledge_entrances: lw_entrances.remove(aga_door) hc_ledge_entrances.remove(aga_door) - world.random.shuffle(hc_ledge_entrances) + multiworld.random.shuffle(hc_ledge_entrances) hc_ledge_must_exit = hc_ledge_entrances.pop() lw_entrances.remove(hc_ledge_must_exit) lw_must_exits.append(hc_ledge_must_exit) @@ -886,14 +887,14 @@ def link_inverted_entrances(world, player): else: dw_entrances.remove(aga_door) - connect_two_way(world, aga_door, 'Inverted Agahnims Tower Exit', player) + connect_two_way(multiworld, aga_door, 'Inverted Agahnims Tower Exit', player) caves.remove('Inverted Agahnims Tower Exit') # place links house links_house_doors = [i for i in lw_entrances + dw_entrances + lw_must_exits if i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors] - links_house = world.random.choice(list(links_house_doors)) - connect_two_way(world, links_house, 'Inverted Links House Exit', player) + links_house = multiworld.random.choice(list(links_house_doors)) + connect_two_way(multiworld, links_house, 'Inverted Links House Exit', player) if links_house in lw_entrances: lw_entrances.remove(links_house) if links_house in dw_entrances: @@ -903,35 +904,35 @@ def link_inverted_entrances(world, player): # place dark sanc sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in dw_entrances] - sanc_door = world.random.choice(sanc_doors) + sanc_door = multiworld.random.choice(sanc_doors) dw_entrances.remove(sanc_door) - connect_entrance(world, sanc_door, 'Inverted Dark Sanctuary', player) - world.get_entrance('Inverted Dark Sanctuary Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) + connect_entrance(multiworld, sanc_door, 'Inverted Dark Sanctuary', player) + multiworld.get_entrance('Inverted Dark Sanctuary Exit', player).connect(multiworld.get_entrance(sanc_door, player).parent_region) # place old man house # no dw must exits in inverted, but we randomize whether cave is in light or dark world - if world.random.randint(0, 1) == 0: + if multiworld.random.randint(0, 1) == 0: caves += old_man_house - connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player) + connect_mandatory_exits(multiworld, lw_entrances, caves, lw_must_exits, player) try: caves.remove(old_man_house[0]) except ValueError: pass else: # if the cave wasn't placed we get here - connect_caves(world, lw_entrances, [], old_man_house, player) + connect_caves(multiworld, lw_entrances, [], old_man_house, player) else: - connect_caves(world, dw_entrances, [], old_man_house, player) - connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player) + connect_caves(multiworld, dw_entrances, [], old_man_house, player) + connect_mandatory_exits(multiworld, lw_entrances, caves, lw_must_exits, player) # put all HC exits in LW in inverted full shuffle - connect_caves(world, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)', 'Hyrule Castle Exit (South)')], player) + connect_caves(multiworld, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)', 'Hyrule Castle Exit (South)')], player) # place old man, has limited options # exit has to come from specific set of doors, the entrance is free to move about old_man_entrances = [door for door in old_man_entrances if door in dw_entrances + lw_entrances] - world.random.shuffle(old_man_entrances) + multiworld.random.shuffle(old_man_entrances) old_man_exit = old_man_entrances.pop() - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) + connect_two_way(multiworld, old_man_exit, 'Old Man Cave Exit (East)', player) if old_man_exit in dw_entrances: dw_entrances.remove(old_man_exit) old_man_world = 'dark' @@ -943,9 +944,9 @@ def link_inverted_entrances(world, player): all_entrances = lw_entrances + dw_entrances # cannot place it anywhere already taken (or that are otherwise not eligible for placement) blacksmith_doors = [door for door in blacksmith_doors if door in all_entrances] - world.random.shuffle(blacksmith_doors) + multiworld.random.shuffle(blacksmith_doors) blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) + connect_entrance(multiworld, blacksmith_hut, 'Blacksmiths Hut', player) if blacksmith_hut in lw_entrances: lw_entrances.remove(blacksmith_hut) if blacksmith_hut in dw_entrances: @@ -956,36 +957,36 @@ def link_inverted_entrances(world, player): all_entrances = lw_entrances + dw_entrances # cannot place it anywhere already taken (or that are otherwise not eligible for placement) bomb_shop_doors = [door for door in bomb_shop_doors if door in all_entrances] - world.random.shuffle(bomb_shop_doors) + multiworld.random.shuffle(bomb_shop_doors) bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Inverted Big Bomb Shop', player) + connect_entrance(multiworld, bomb_shop, 'Inverted Big Bomb Shop', player) if bomb_shop in lw_entrances: lw_entrances.remove(bomb_shop) if bomb_shop in dw_entrances: dw_entrances.remove(bomb_shop) - # place the old man cave's entrance somewhere in the same world he'll exit from + # place the old man cave's entrance somewhere in the same multiworld he'll exit from if old_man_world == 'light': - world.random.shuffle(lw_entrances) + multiworld.random.shuffle(lw_entrances) old_man_entrance = lw_entrances.pop() - connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) + connect_two_way(multiworld, old_man_entrance, 'Old Man Cave Exit (West)', player) elif old_man_world == 'dark': - world.random.shuffle(dw_entrances) + multiworld.random.shuffle(dw_entrances) old_man_entrance = dw_entrances.pop() - connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) + connect_two_way(multiworld, old_man_entrance, 'Old Man Cave Exit (West)', player) # now scramble the rest - connect_caves(world, lw_entrances, dw_entrances, caves, player) + connect_caves(multiworld, lw_entrances, dw_entrances, caves, player) # scramble holes - scramble_inverted_holes(world, player) + scramble_inverted_holes(multiworld, player) doors = lw_entrances + dw_entrances # place remaining doors - connect_doors(world, doors, door_targets, player) - elif world.worlds[player].options.entrance_shuffle == 'crossed': - skull_woods_shuffle(world, player) + connect_doors(multiworld, doors, door_targets, player) + elif multiworld.worlds[player].options.entrance_shuffle == 'crossed': + skull_woods_shuffle(multiworld, player) entrances = list(Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_LW_Single_Cave_Doors + Inverted_Old_Man_Entrances + Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_DW_Single_Cave_Doors) must_exits = list(Inverted_LW_Entrances_Must_Exit + Inverted_LW_Dungeon_Entrances_Must_Exit) @@ -997,19 +998,19 @@ def link_inverted_entrances(world, player): door_targets = list(Inverted_Single_Cave_Targets) # randomize which desert ledge door is a must-exit - if world.random.randint(0, 1) == 0: + if multiworld.random.randint(0, 1) == 0: must_exits.append('Desert Palace Entrance (North)') entrances.append('Desert Palace Entrance (West)') else: must_exits.append('Desert Palace Entrance (West)') entrances.append('Desert Palace Entrance (North)') - caves.append(tuple(world.random.sample( + caves.append(tuple(multiworld.random.sample( ['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'], 3))) entrances.append('Hyrule Castle Entrance (South)') - if not world.shuffle_ganon: - connect_two_way(world, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player) + if not multiworld.shuffle_ganon: + connect_two_way(multiworld, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player) hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)'] else: entrances.append('Inverted Ganons Tower') @@ -1017,26 +1018,26 @@ def link_inverted_entrances(world, player): hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)', 'Inverted Ganons Tower'] # shuffle aga door. if it's on hc ledge, then one other hc ledge door has to be must_exit - aga_door = world.random.choice(list(entrances)) + aga_door = multiworld.random.choice(list(entrances)) if aga_door in hc_ledge_entrances: hc_ledge_entrances.remove(aga_door) - world.random.shuffle(hc_ledge_entrances) + multiworld.random.shuffle(hc_ledge_entrances) hc_ledge_must_exit = hc_ledge_entrances.pop() entrances.remove(hc_ledge_must_exit) must_exits.append(hc_ledge_must_exit) entrances.remove(aga_door) - connect_two_way(world, aga_door, 'Inverted Agahnims Tower Exit', player) + connect_two_way(multiworld, aga_door, 'Inverted Agahnims Tower Exit', player) caves.remove('Inverted Agahnims Tower Exit') # place links house links_house_doors = [i for i in entrances + must_exits if i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors] - links_house = world.random.choice(list(links_house_doors)) - connect_two_way(world, links_house, 'Inverted Links House Exit', player) + links_house = multiworld.random.choice(list(links_house_doors)) + connect_two_way(multiworld, links_house, 'Inverted Links House Exit', player) if links_house in entrances: entrances.remove(links_house) elif links_house in must_exits: @@ -1044,58 +1045,58 @@ def link_inverted_entrances(world, player): # place dark sanc sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in entrances] - sanc_door = world.random.choice(sanc_doors) + sanc_door = multiworld.random.choice(sanc_doors) entrances.remove(sanc_door) - connect_entrance(world, sanc_door, 'Inverted Dark Sanctuary', player) - world.get_entrance('Inverted Dark Sanctuary Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) + connect_entrance(multiworld, sanc_door, 'Inverted Dark Sanctuary', player) + multiworld.get_entrance('Inverted Dark Sanctuary Exit', player).connect(multiworld.get_entrance(sanc_door, player).parent_region) # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) + connect_doors(multiworld, ['Tavern North'], ['Tavern'], player) #place must-exit caves - connect_mandatory_exits(world, entrances, caves, must_exits, player) + connect_mandatory_exits(multiworld, entrances, caves, must_exits, player) # place old man, has limited options # exit has to come from specific set of doors, the entrance is free to move about old_man_entrances = [door for door in old_man_entrances if door in entrances] - world.random.shuffle(old_man_entrances) + multiworld.random.shuffle(old_man_entrances) old_man_exit = old_man_entrances.pop() - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) + connect_two_way(multiworld, old_man_exit, 'Old Man Cave Exit (East)', player) entrances.remove(old_man_exit) # place blacksmith, has limited options # cannot place it anywhere already taken (or that are otherwise not eligible for placement) blacksmith_doors = [door for door in blacksmith_doors if door in entrances] - world.random.shuffle(blacksmith_doors) + multiworld.random.shuffle(blacksmith_doors) blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) + connect_entrance(multiworld, blacksmith_hut, 'Blacksmiths Hut', player) entrances.remove(blacksmith_hut) # place bomb shop, has limited options # cannot place it anywhere already taken (or that are otherwise not eligible for placement) bomb_shop_doors = [door for door in bomb_shop_doors if door in entrances] - world.random.shuffle(bomb_shop_doors) + multiworld.random.shuffle(bomb_shop_doors) bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Inverted Big Bomb Shop', player) + connect_entrance(multiworld, bomb_shop, 'Inverted Big Bomb Shop', player) entrances.remove(bomb_shop) # place the old man cave's entrance somewhere - world.random.shuffle(entrances) + multiworld.random.shuffle(entrances) old_man_entrance = entrances.pop() - connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) + connect_two_way(multiworld, old_man_entrance, 'Old Man Cave Exit (West)', player) # now scramble the rest - connect_caves(world, entrances, [], caves, player) + connect_caves(multiworld, entrances, [], caves, player) # scramble holes - scramble_inverted_holes(world, player) + scramble_inverted_holes(multiworld, player) # place remaining doors - connect_doors(world, entrances, door_targets, player) - elif world.worlds[player].options.entrance_shuffle == 'insanity': + connect_doors(multiworld, entrances, door_targets, player) + elif multiworld.worlds[player].options.entrance_shuffle == 'insanity': # beware ye who enter here entrances = Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_Old_Man_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave', 'Hyrule Castle Entrance (South)'] @@ -1106,7 +1107,7 @@ def link_inverted_entrances(world, player): Inverted_LW_Single_Cave_Doors + Inverted_DW_Single_Cave_Doors + ['Desert Palace Entrance (West)', 'Desert Palace Entrance (North)'] # randomize which desert ledge door is a must-exit - if world.random.randint(0, 1) == 0: + if multiworld.random.randint(0, 1) == 0: entrances_must_exits.append('Desert Palace Entrance (North)') entrances.append('Desert Palace Entrance (West)') else: @@ -1121,7 +1122,7 @@ def link_inverted_entrances(world, player): blacksmith_doors = list(Blacksmith_Single_Cave_Doors + Inverted_Blacksmith_Multi_Cave_Doors) door_targets = list(Inverted_Single_Cave_Targets) - world.random.shuffle(doors) + multiworld.random.shuffle(doors) old_man_entrances = list(Inverted_Old_Man_Entrances + Old_Man_Entrances) + ['Tower of Hera', 'Inverted Agahnims Tower'] @@ -1137,17 +1138,17 @@ def link_inverted_entrances(world, player): 'Skull Woods First Section (Left)', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Top)'] # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) + connect_doors(multiworld, ['Tavern North'], ['Tavern'], player) hole_entrances.append('Hyrule Castle Secret Entrance Drop') hole_targets.append('Hyrule Castle Secret Entrance') entrances.append('Hyrule Castle Secret Entrance Stairs') caves.append('Hyrule Castle Secret Entrance Exit') - if not world.shuffle_ganon: - connect_two_way(world, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player) - connect_two_way(world, 'Inverted Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Inverted Pyramid Hole', 'Pyramid', player) + if not multiworld.shuffle_ganon: + connect_two_way(multiworld, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player) + connect_two_way(multiworld, 'Inverted Pyramid Entrance', 'Pyramid Exit', player) + connect_entrance(multiworld, 'Inverted Pyramid Hole', 'Pyramid', player) else: entrances.append('Inverted Ganons Tower') caves.extend(['Inverted Ganons Tower Exit', 'Pyramid Exit']) @@ -1155,13 +1156,13 @@ def link_inverted_entrances(world, player): hole_targets.append('Pyramid') doors.extend(['Inverted Ganons Tower', 'Inverted Pyramid Entrance']) - world.random.shuffle(hole_entrances) - world.random.shuffle(hole_targets) - world.random.shuffle(entrances) + multiworld.random.shuffle(hole_entrances) + multiworld.random.shuffle(hole_targets) + multiworld.random.shuffle(entrances) # fill up holes for hole in hole_entrances: - connect_entrance(world, hole, hole_targets.pop(), player) + connect_entrance(multiworld, hole, hole_targets.pop(), player) doors.append('Hyrule Castle Entrance (South)') caves.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) @@ -1169,8 +1170,8 @@ def link_inverted_entrances(world, player): # place links house and dark sanc links_house_doors = [i for i in entrances + entrances_must_exits if i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors] - links_house = world.random.choice(list(links_house_doors)) - connect_two_way(world, links_house, 'Inverted Links House Exit', player) + links_house = multiworld.random.choice(list(links_house_doors)) + connect_two_way(multiworld, links_house, 'Inverted Links House Exit', player) if links_house in entrances: entrances.remove(links_house) elif links_house in entrances_must_exits: @@ -1178,15 +1179,15 @@ def link_inverted_entrances(world, player): doors.remove(links_house) sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in entrances] - sanc_door = world.random.choice(sanc_doors) + sanc_door = multiworld.random.choice(sanc_doors) entrances.remove(sanc_door) doors.remove(sanc_door) - connect_entrance(world, sanc_door, 'Inverted Dark Sanctuary', player) - world.get_entrance('Inverted Dark Sanctuary Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) + connect_entrance(multiworld, sanc_door, 'Inverted Dark Sanctuary', player) + multiworld.get_entrance('Inverted Dark Sanctuary Exit', player).connect(multiworld.get_entrance(sanc_door, player).parent_region) # now let's deal with mandatory reachable stuff def extract_reachable_exit(cavelist): - world.random.shuffle(cavelist) + multiworld.random.shuffle(cavelist) candidate = None for cave in cavelist: if isinstance(cave, tuple) and len(cave) > 1: @@ -1206,9 +1207,9 @@ def link_inverted_entrances(world, player): exit = cave[-1] cave = cave[:-1] - connect_exit(world, exit, entrance, player) - connect_entrance(world, doors.pop(), exit, player) - # rest of cave now is forced to be in this world + connect_exit(multiworld, exit, entrance, player) + connect_entrance(multiworld, doors.pop(), exit, player) + # rest of cave now is forced to be in this multiworld caves.append(cave) # connect mandatory exits @@ -1218,26 +1219,26 @@ def link_inverted_entrances(world, player): # place old man, has limited options # exit has to come from specific set of doors, the entrance is free to move about old_man_entrances = [entrance for entrance in old_man_entrances if entrance in entrances] - world.random.shuffle(old_man_entrances) + multiworld.random.shuffle(old_man_entrances) old_man_exit = old_man_entrances.pop() entrances.remove(old_man_exit) - connect_exit(world, 'Old Man Cave Exit (East)', old_man_exit, player) - connect_entrance(world, doors.pop(), 'Old Man Cave Exit (East)', player) + connect_exit(multiworld, 'Old Man Cave Exit (East)', old_man_exit, player) + connect_entrance(multiworld, doors.pop(), 'Old Man Cave Exit (East)', player) caves.append('Old Man Cave Exit (West)') # place blacksmith, has limited options blacksmith_doors = [door for door in blacksmith_doors if door in doors] - world.random.shuffle(blacksmith_doors) + multiworld.random.shuffle(blacksmith_doors) blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) + connect_entrance(multiworld, blacksmith_hut, 'Blacksmiths Hut', player) doors.remove(blacksmith_hut) # place dam and pyramid fairy, have limited options bomb_shop_doors = [door for door in bomb_shop_doors if door in doors] - world.random.shuffle(bomb_shop_doors) + multiworld.random.shuffle(bomb_shop_doors) bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Inverted Big Bomb Shop', player) + connect_entrance(multiworld, bomb_shop, 'Inverted Big Bomb Shop', player) doors.remove(bomb_shop) # handle remaining caves @@ -1246,49 +1247,49 @@ def link_inverted_entrances(world, player): cave = (cave,) for exit in cave: - connect_exit(world, exit, entrances.pop(), player) - connect_entrance(world, doors.pop(), exit, player) + connect_exit(multiworld, exit, entrances.pop(), player) + connect_entrance(multiworld, doors.pop(), exit, player) # place remaining doors - connect_doors(world, doors, door_targets, player) + connect_doors(multiworld, doors, door_targets, player) else: raise NotImplementedError('Shuffling not supported yet') - if world.worlds[player].options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']: - overworld_glitch_connections(world, player) + if multiworld.worlds[player].options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']: + overworld_glitch_connections(multiworld, player) # mandatory hybrid major glitches connections - if world.worlds[player].options.glitches_required in ['hybrid_major_glitches', 'no_logic']: - underworld_glitch_connections(world, player) + if multiworld.worlds[player].options.glitches_required in ['hybrid_major_glitches', 'no_logic']: + underworld_glitch_connections(multiworld, player) # patch swamp drain - if world.get_entrance('Dam', player).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)': - world.worlds[player].swamp_patch_required = True + if multiworld.get_entrance('Dam', player).connected_region.name != 'Dam' or multiworld.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)': + multiworld.worlds[player].swamp_patch_required = True # check for potion shop location - if world.get_entrance('Potion Shop', player).connected_region.name != 'Potion Shop': - world.worlds[player].powder_patch_required = True + if multiworld.get_entrance('Potion Shop', player).connected_region.name != 'Potion Shop': + multiworld.worlds[player].powder_patch_required = True # check for ganon location - if world.get_entrance('Inverted Pyramid Hole', player).connected_region.name != 'Pyramid': - world.worlds[player].ganon_at_pyramid = False + if multiworld.get_entrance('Inverted Pyramid Hole', player).connected_region.name != 'Pyramid': + multiworld.worlds[player].ganon_at_pyramid = False # check for Ganon's Tower location - if world.get_entrance('Inverted Ganons Tower', player).connected_region.name != 'Ganons Tower (Entrance)': - world.worlds[player].ganonstower_vanilla = False + if multiworld.get_entrance('Inverted Ganons Tower', player).connected_region.name != 'Ganons Tower (Entrance)': + multiworld.worlds[player].ganonstower_vanilla = False -def connect_simple(world, exitname, regionname, player): - world.get_entrance(exitname, player).connect(world.get_region(regionname, player)) +def connect_simple(multiworld: MultiWorld, exitname: str, regionname: str, player: int): + multiworld.get_entrance(exitname, player).connect(multiworld.get_region(regionname, player)) -def connect_entrance(world, entrancename: str, exitname: str, player: int): - entrance = world.get_entrance(entrancename, player) +def connect_entrance(multiworld: MultiWorld, entrancename: str, exitname: str, player: int): + entrance = multiworld.get_entrance(entrancename, player) # check if we got an entrance or a region to connect to try: - region = world.get_region(exitname, player) + region = multiworld.get_region(exitname, player) exit = None except KeyError: - exit = world.get_entrance(exitname, player) + exit = multiworld.get_entrance(exitname, player) region = exit.parent_region # if this was already connected somewhere, remove the backreference @@ -1299,24 +1300,24 @@ def connect_entrance(world, entrancename: str, exitname: str, player: int): addresses = door_addresses[entrance.name][0] entrance.connect(region, addresses, target) - world.spoiler.set_entrance(entrance.name, exit.name if exit is not None else region.name, 'entrance', player) + multiworld.spoiler.set_entrance(entrance.name, exit.name if exit is not None else region.name, 'entrance', player) -def connect_exit(world, exitname, entrancename, player): - entrance = world.get_entrance(entrancename, player) - exit = world.get_entrance(exitname, player) +def connect_exit(multiworld: MultiWorld, exitname: str, entrancename: str, player: int): + entrance = multiworld.get_entrance(entrancename, player) + exit = multiworld.get_entrance(exitname, player) # if this was already connected somewhere, remove the backreference if exit.connected_region is not None: exit.connected_region.entrances.remove(exit) exit.connect(entrance.parent_region, door_addresses[entrance.name][1], exit_ids[exit.name][1]) - world.spoiler.set_entrance(entrance.name, exit.name, 'exit', player) + multiworld.spoiler.set_entrance(entrance.name, exit.name, 'exit', player) -def connect_two_way(world, entrancename, exitname, player): - entrance = world.get_entrance(entrancename, player) - exit = world.get_entrance(exitname, player) +def connect_two_way(multiworld: MultiWorld, entrancename: str, exitname: str, player: int): + entrance = multiworld.get_entrance(entrancename, player) + exit = multiworld.get_entrance(exitname, player) # if these were already connected somewhere, remove the backreference if entrance.connected_region is not None: @@ -1326,10 +1327,10 @@ def connect_two_way(world, entrancename, exitname, player): entrance.connect(exit.parent_region, door_addresses[entrance.name][0], exit_ids[exit.name][0]) exit.connect(entrance.parent_region, door_addresses[entrance.name][1], exit_ids[exit.name][1]) - world.spoiler.set_entrance(entrance.name, exit.name, 'both', player) + multiworld.spoiler.set_entrance(entrance.name, exit.name, 'both', player) -def scramble_holes(world, player): +def scramble_holes(multiworld: MultiWorld, player: int): hole_entrances = [('Kakariko Well Cave', 'Kakariko Well Drop'), ('Bat Cave Cave', 'Bat Cave Drop'), ('North Fairy Cave', 'North Fairy Cave Drop'), @@ -1343,39 +1344,39 @@ def scramble_holes(world, player): ('Lost Woods Hideout Exit', 'Lost Woods Hideout (top)'), ('Lumberjack Tree Exit', 'Lumberjack Tree (top)')] - if not world.shuffle_ganon: - connect_two_way(world, 'Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Pyramid Hole', 'Pyramid', player) + if not multiworld.shuffle_ganon: + connect_two_way(multiworld, 'Pyramid Entrance', 'Pyramid Exit', player) + connect_entrance(multiworld, 'Pyramid Hole', 'Pyramid', player) else: hole_targets.append(('Pyramid Exit', 'Pyramid')) - if world.worlds[player].options.mode == 'standard': + if multiworld.worlds[player].options.mode == 'standard': # cannot move uncle cave - connect_two_way(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player) - connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player) + connect_two_way(multiworld, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player) + connect_entrance(multiworld, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player) else: hole_entrances.append(('Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Drop')) hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance')) # do not shuffle sanctuary into pyramid hole unless shuffle is crossed - if world.worlds[player].options.entrance_shuffle == 'crossed': + if multiworld.worlds[player].options.entrance_shuffle == 'crossed': hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) - if world.shuffle_ganon: - world.random.shuffle(hole_targets) + if multiworld.shuffle_ganon: + multiworld.random.shuffle(hole_targets) exit, target = hole_targets.pop() - connect_two_way(world, 'Pyramid Entrance', exit, player) - connect_entrance(world, 'Pyramid Hole', target, player) - if world.worlds[player].options.entrance_shuffle != 'crossed': + connect_two_way(multiworld, 'Pyramid Entrance', exit, player) + connect_entrance(multiworld, 'Pyramid Hole', target, player) + if multiworld.worlds[player].options.entrance_shuffle != 'crossed': hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) - world.random.shuffle(hole_targets) + multiworld.random.shuffle(hole_targets) for entrance, drop in hole_entrances: exit, target = hole_targets.pop() - connect_two_way(world, entrance, exit, player) - connect_entrance(world, drop, target, player) + connect_two_way(multiworld, entrance, exit, player) + connect_entrance(multiworld, drop, target, player) -def scramble_inverted_holes(world, player): +def scramble_inverted_holes(multiworld: MultiWorld, player: int): hole_entrances = [('Kakariko Well Cave', 'Kakariko Well Drop'), ('Bat Cave Cave', 'Bat Cave Drop'), ('North Fairy Cave', 'North Fairy Cave Drop'), @@ -1389,9 +1390,9 @@ def scramble_inverted_holes(world, player): ('Lost Woods Hideout Exit', 'Lost Woods Hideout (top)'), ('Lumberjack Tree Exit', 'Lumberjack Tree (top)')] - if not world.shuffle_ganon: - connect_two_way(world, 'Inverted Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Inverted Pyramid Hole', 'Pyramid', player) + if not multiworld.shuffle_ganon: + connect_two_way(multiworld, 'Inverted Pyramid Entrance', 'Pyramid Exit', player) + connect_entrance(multiworld, 'Inverted Pyramid Hole', 'Pyramid', player) else: hole_targets.append(('Pyramid Exit', 'Pyramid')) @@ -1400,58 +1401,59 @@ def scramble_inverted_holes(world, player): hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance')) # do not shuffle sanctuary into pyramid hole unless shuffle is crossed - if world.worlds[player].options.entrance_shuffle == 'crossed': + if multiworld.worlds[player].options.entrance_shuffle == 'crossed': hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) - if world.shuffle_ganon: - world.random.shuffle(hole_targets) + if multiworld.shuffle_ganon: + multiworld.random.shuffle(hole_targets) exit, target = hole_targets.pop() - connect_two_way(world, 'Inverted Pyramid Entrance', exit, player) - connect_entrance(world, 'Inverted Pyramid Hole', target, player) - if world.worlds[player].options.entrance_shuffle != 'crossed': + connect_two_way(multiworld, 'Inverted Pyramid Entrance', exit, player) + connect_entrance(multiworld, 'Inverted Pyramid Hole', target, player) + if multiworld.worlds[player].options.entrance_shuffle != 'crossed': hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) - world.random.shuffle(hole_targets) + multiworld.random.shuffle(hole_targets) for entrance, drop in hole_entrances: exit, target = hole_targets.pop() - connect_two_way(world, entrance, exit, player) - connect_entrance(world, drop, target, player) + connect_two_way(multiworld, entrance, exit, player) + connect_entrance(multiworld, drop, target, player) -def connect_random(world, exitlist, targetlist, player, two_way=False): +def connect_random(multiworld: MultiWorld, exitlist: list[str], targetlist: list[str], + player: int, two_way: bool = False): targetlist = list(targetlist) - world.random.shuffle(targetlist) + multiworld.random.shuffle(targetlist) for exit, target in zip(exitlist, targetlist): if two_way: - connect_two_way(world, exit, target, player) + connect_two_way(multiworld, exit, target, player) else: - connect_entrance(world, exit, target, player) + connect_entrance(multiworld, exit, target, player) -def connect_mandatory_exits(world, entrances, caves, must_be_exits, player): +def connect_mandatory_exits(multiworld: MultiWorld, entrances: list[str], caves: list[tuple[str, str]], must_be_exits: list[str], player): # Keeps track of entrances that cannot be used to access each exit / cave - if world.worlds[player].options.mode == 'inverted': + if multiworld.worlds[player].options.mode == 'inverted': invalid_connections = Inverted_Must_Exit_Invalid_Connections.copy() else: invalid_connections = Must_Exit_Invalid_Connections.copy() invalid_cave_connections = defaultdict(set) - if world.worlds[player].options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']: + if multiworld.worlds[player].options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']: from . import OverworldGlitchRules - for entrance in OverworldGlitchRules.get_non_mandatory_exits(world.worlds[player].options.mode == 'inverted'): + for entrance in OverworldGlitchRules.get_non_mandatory_exits(multiworld.worlds[player].options.mode == 'inverted'): invalid_connections[entrance] = set() if entrance in must_be_exits: must_be_exits.remove(entrance) entrances.append(entrance) """This works inplace""" - world.random.shuffle(entrances) - world.random.shuffle(caves) + multiworld.random.shuffle(entrances) + multiworld.random.shuffle(caves) # Handle inverted Aga Tower - if it depends on connections, then so does Hyrule Castle Ledge - if world.worlds[player].options.mode == 'inverted': + if multiworld.worlds[player].options.mode == 'inverted': for entrance in invalid_connections: - if world.get_entrance(entrance, player).connected_region == world.get_region('Inverted Agahnims Tower', + if multiworld.get_entrance(entrance, player).connected_region == multiworld.get_region('Inverted Agahnims Tower', player): for exit in invalid_connections[entrance]: invalid_connections[exit] = invalid_connections[exit].union( @@ -1473,11 +1475,11 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player): raise KeyError('No more caves left. Should not happen!') # all caves are sorted so that the last exit is always reachable - connect_two_way(world, exit, cave[-1], player) + connect_two_way(multiworld, exit, cave[-1], player) if len(cave) == 2: entrance = next(e for e in entrances[::-1] if e not in invalid_connections[exit] and e not in invalid_cave_connections[tuple(cave)]) entrances.remove(entrance) - connect_two_way(world, entrance, cave[0], player) + connect_two_way(multiworld, entrance, cave[0], player) if cave in used_caves: required_entrances -= 2 used_caves.remove(cave) @@ -1490,7 +1492,7 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player): entrance = next(e for e in entrances[::-1] if e not in invalid_connections[exit]) cave_entrances.append(entrance) entrances.remove(entrance) - connect_two_way(world, entrance, cave_exit, player) + connect_two_way(multiworld, entrance, cave_exit, player) if entrance not in invalid_connections: invalid_connections[exit] = set() if all(entrance in invalid_connections for entrance in cave_entrances): @@ -1504,7 +1506,7 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player): else: required_entrances += len(cave)-1 caves.append(cave[0:-1]) - world.random.shuffle(caves) + multiworld.random.shuffle(caves) used_caves.append(cave[0:-1]) invalid_cave_connections[tuple(cave[0:-1])] = invalid_cave_connections[tuple(cave)].union(invalid_connections[exit]) caves.remove(cave) @@ -1514,19 +1516,20 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player): entrance = next(e for e in entrances[::-1] if e not in invalid_cave_connections[tuple(cave)]) invalid_cave_connections[tuple(cave)] = set() entrances.remove(entrance) - connect_two_way(world, entrance, cave_exit, player) + connect_two_way(multiworld, entrance, cave_exit, player) caves.remove(cave) -def connect_caves(world, lw_entrances, dw_entrances, caves, player): +def connect_caves(multiworld: MultiWorld, lw_entrances: list[str], dw_entrances: list[str], + caves: list[tuple[str, str]], player: int): """This works inplace""" - world.random.shuffle(lw_entrances) - world.random.shuffle(dw_entrances) - world.random.shuffle(caves) + multiworld.random.shuffle(lw_entrances) + multiworld.random.shuffle(dw_entrances) + multiworld.random.shuffle(caves) # connect highest exit count caves first, prevent issue where we have 2 or 3 exits accross worlds left to fill caves.sort(key=lambda cave: 1 if isinstance(cave, str) else len(cave), reverse=True) for cave in caves: - target = lw_entrances if world.random.randint(0, 1) else dw_entrances + target = lw_entrances if multiworld.random.randint(0, 1) else dw_entrances if isinstance(cave, str): cave = (cave,) @@ -1536,37 +1539,37 @@ def connect_caves(world, lw_entrances, dw_entrances, caves, player): target = lw_entrances if target is dw_entrances else dw_entrances for exit in cave: - connect_two_way(world, target.pop(), exit, player) + connect_two_way(multiworld, target.pop(), exit, player) caves.clear() # emulating old behaviour of popping caves from the list in-place -def connect_doors(world, doors, targets, player): +def connect_doors(multiworld: MultiWorld, doors: list[str], targets: list[str], player: int): """This works inplace""" - world.random.shuffle(doors) - world.random.shuffle(targets) + multiworld.random.shuffle(doors) + multiworld.random.shuffle(targets) placing = min(len(doors), len(targets)) for door, target in zip(doors, targets): - connect_entrance(world, door, target, player) + connect_entrance(multiworld, door, target, player) doors[:] = doors[placing:] targets[:] = targets[placing:] -def skull_woods_shuffle(world, player): - connect_random(world, ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'], +def skull_woods_shuffle(multiworld: MultiWorld, player: int): + connect_random(multiworld, ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'], ['Skull Woods First Section (Left)', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Top)', 'Skull Woods Second Section (Drop)'], player) - connect_random(world, ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)'], + connect_random(multiworld, ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)'], ['Skull Woods First Section Exit', 'Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)'], player, True) -def simple_shuffle_dungeons(world, player): - skull_woods_shuffle(world, player) +def simple_shuffle_dungeons(multiworld: MultiWorld, player: int): + skull_woods_shuffle(multiworld, player) dungeon_entrances = ['Eastern Palace', 'Tower of Hera', 'Thieves Town', 'Skull Woods Final Section', 'Palace of Darkness', 'Ice Palace', 'Misery Mire', 'Swamp Palace'] dungeon_exits = ['Eastern Palace Exit', 'Tower of Hera Exit', 'Thieves Town Exit', 'Skull Woods Final Section Exit', 'Palace of Darkness Exit', 'Ice Palace Exit', 'Misery Mire Exit', 'Swamp Palace Exit'] - if world.worlds[player].options.mode != 'inverted': - if not world.shuffle_ganon: - connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player) + if multiworld.worlds[player].options.mode != 'inverted': + if not multiworld.shuffle_ganon: + connect_two_way(multiworld, 'Ganons Tower', 'Ganons Tower Exit', player) else: dungeon_entrances.append('Ganons Tower') dungeon_exits.append('Ganons Tower Exit') @@ -1575,17 +1578,17 @@ def simple_shuffle_dungeons(world, player): dungeon_exits.append('Inverted Agahnims Tower Exit') # shuffle up single entrance dungeons - connect_random(world, dungeon_entrances, dungeon_exits, player, True) + connect_random(multiworld, dungeon_entrances, dungeon_exits, player, True) # mix up 4 door dungeons multi_dungeons = ['Desert', 'Turtle Rock'] - if world.worlds[player].options.mode == 'open' or (world.worlds[player].options.mode == 'inverted' and world.shuffle_ganon): + if multiworld.worlds[player].options.mode == 'open' or (multiworld.worlds[player].options.mode == 'inverted' and multiworld.shuffle_ganon): multi_dungeons.append('Hyrule Castle') - world.random.shuffle(multi_dungeons) + multiworld.random.shuffle(multi_dungeons) dp_target = multi_dungeons[0] tr_target = multi_dungeons[1] - if world.worlds[player].options.mode not in ['open', 'inverted'] or (world.worlds[player].options.mode == 'inverted' and world.shuffle_ganon is False): + if multiworld.worlds[player].options.mode not in ['open', 'inverted'] or (multiworld.worlds[player].options.mode == 'inverted' and multiworld.shuffle_ganon is False): # place hyrule castle as intended hc_target = 'Hyrule Castle' else: @@ -1593,152 +1596,152 @@ def simple_shuffle_dungeons(world, player): # ToDo improve this? - if world.worlds[player].options.mode != 'inverted': + if multiworld.worlds[player].options.mode != 'inverted': if hc_target == 'Hyrule Castle': - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Exit (East)', player) - connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Hyrule Castle Exit (West)', player) - connect_two_way(world, 'Agahnims Tower', 'Agahnims Tower Exit', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Exit (East)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (West)', 'Hyrule Castle Exit (West)', player) + connect_two_way(multiworld, 'Agahnims Tower', 'Agahnims Tower Exit', player) elif hc_target == 'Desert': - connect_two_way(world, 'Desert Palace Entrance (South)', 'Hyrule Castle Exit (South)', player) - connect_two_way(world, 'Desert Palace Entrance (East)', 'Hyrule Castle Exit (East)', player) - connect_two_way(world, 'Desert Palace Entrance (West)', 'Hyrule Castle Exit (West)', player) - connect_two_way(world, 'Desert Palace Entrance (North)', 'Agahnims Tower Exit', player) + connect_two_way(multiworld, 'Desert Palace Entrance (South)', 'Hyrule Castle Exit (South)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (East)', 'Hyrule Castle Exit (East)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (West)', 'Hyrule Castle Exit (West)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (North)', 'Agahnims Tower Exit', player) elif hc_target == 'Turtle Rock': - connect_two_way(world, 'Turtle Rock', 'Hyrule Castle Exit (South)', player) - connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Hyrule Castle Exit (East)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Hyrule Castle Exit (West)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Agahnims Tower Exit', player) + connect_two_way(multiworld, 'Turtle Rock', 'Hyrule Castle Exit (South)', player) + connect_two_way(multiworld, 'Turtle Rock Isolated Ledge Entrance', 'Hyrule Castle Exit (East)', player) + connect_two_way(multiworld, 'Dark Death Mountain Ledge (West)', 'Hyrule Castle Exit (West)', player) + connect_two_way(multiworld, 'Dark Death Mountain Ledge (East)', 'Agahnims Tower Exit', player) if dp_target == 'Hyrule Castle': - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Desert Palace Exit (South)', player) - connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Desert Palace Exit (East)', player) - connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Desert Palace Exit (West)', player) - connect_two_way(world, 'Agahnims Tower', 'Desert Palace Exit (North)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (South)', 'Desert Palace Exit (South)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (East)', 'Desert Palace Exit (East)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (West)', 'Desert Palace Exit (West)', player) + connect_two_way(multiworld, 'Agahnims Tower', 'Desert Palace Exit (North)', player) elif dp_target == 'Desert': - connect_two_way(world, 'Desert Palace Entrance (South)', 'Desert Palace Exit (South)', player) - connect_two_way(world, 'Desert Palace Entrance (East)', 'Desert Palace Exit (East)', player) - connect_two_way(world, 'Desert Palace Entrance (West)', 'Desert Palace Exit (West)', player) - connect_two_way(world, 'Desert Palace Entrance (North)', 'Desert Palace Exit (North)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (South)', 'Desert Palace Exit (South)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (East)', 'Desert Palace Exit (East)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (West)', 'Desert Palace Exit (West)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (North)', 'Desert Palace Exit (North)', player) elif dp_target == 'Turtle Rock': - connect_two_way(world, 'Turtle Rock', 'Desert Palace Exit (South)', player) - connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Desert Palace Exit (East)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Desert Palace Exit (West)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Desert Palace Exit (North)', player) + connect_two_way(multiworld, 'Turtle Rock', 'Desert Palace Exit (South)', player) + connect_two_way(multiworld, 'Turtle Rock Isolated Ledge Entrance', 'Desert Palace Exit (East)', player) + connect_two_way(multiworld, 'Dark Death Mountain Ledge (West)', 'Desert Palace Exit (West)', player) + connect_two_way(multiworld, 'Dark Death Mountain Ledge (East)', 'Desert Palace Exit (North)', player) if tr_target == 'Hyrule Castle': - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Turtle Rock Exit (Front)', player) - connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Turtle Rock Ledge Exit (East)', player) - connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Turtle Rock Ledge Exit (West)', player) - connect_two_way(world, 'Agahnims Tower', 'Turtle Rock Isolated Ledge Exit', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (South)', 'Turtle Rock Exit (Front)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (East)', 'Turtle Rock Ledge Exit (East)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (West)', 'Turtle Rock Ledge Exit (West)', player) + connect_two_way(multiworld, 'Agahnims Tower', 'Turtle Rock Isolated Ledge Exit', player) elif tr_target == 'Desert': - connect_two_way(world, 'Desert Palace Entrance (South)', 'Turtle Rock Exit (Front)', player) - connect_two_way(world, 'Desert Palace Entrance (North)', 'Turtle Rock Ledge Exit (East)', player) - connect_two_way(world, 'Desert Palace Entrance (West)', 'Turtle Rock Ledge Exit (West)', player) - connect_two_way(world, 'Desert Palace Entrance (East)', 'Turtle Rock Isolated Ledge Exit', player) + connect_two_way(multiworld, 'Desert Palace Entrance (South)', 'Turtle Rock Exit (Front)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (North)', 'Turtle Rock Ledge Exit (East)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (West)', 'Turtle Rock Ledge Exit (West)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (East)', 'Turtle Rock Isolated Ledge Exit', player) elif tr_target == 'Turtle Rock': - connect_two_way(world, 'Turtle Rock', 'Turtle Rock Exit (Front)', player) - connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Turtle Rock Isolated Ledge Exit', player) - connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Turtle Rock Ledge Exit (West)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Turtle Rock Ledge Exit (East)', player) + connect_two_way(multiworld, 'Turtle Rock', 'Turtle Rock Exit (Front)', player) + connect_two_way(multiworld, 'Turtle Rock Isolated Ledge Entrance', 'Turtle Rock Isolated Ledge Exit', player) + connect_two_way(multiworld, 'Dark Death Mountain Ledge (West)', 'Turtle Rock Ledge Exit (West)', player) + connect_two_way(multiworld, 'Dark Death Mountain Ledge (East)', 'Turtle Rock Ledge Exit (East)', player) else: if hc_target == 'Hyrule Castle': - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Exit (East)', player) - connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Hyrule Castle Exit (West)', player) - connect_two_way(world, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Exit (East)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (West)', 'Hyrule Castle Exit (West)', player) + connect_two_way(multiworld, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player) elif hc_target == 'Desert': - connect_two_way(world, 'Desert Palace Entrance (South)', 'Hyrule Castle Exit (South)', player) - connect_two_way(world, 'Desert Palace Entrance (East)', 'Hyrule Castle Exit (East)', player) - connect_two_way(world, 'Desert Palace Entrance (West)', 'Hyrule Castle Exit (West)', player) - connect_two_way(world, 'Desert Palace Entrance (North)', 'Inverted Ganons Tower Exit', player) + connect_two_way(multiworld, 'Desert Palace Entrance (South)', 'Hyrule Castle Exit (South)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (East)', 'Hyrule Castle Exit (East)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (West)', 'Hyrule Castle Exit (West)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (North)', 'Inverted Ganons Tower Exit', player) elif hc_target == 'Turtle Rock': - connect_two_way(world, 'Turtle Rock', 'Hyrule Castle Exit (South)', player) - connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Inverted Ganons Tower Exit', player) - connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Hyrule Castle Exit (West)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Hyrule Castle Exit (East)', player) + connect_two_way(multiworld, 'Turtle Rock', 'Hyrule Castle Exit (South)', player) + connect_two_way(multiworld, 'Turtle Rock Isolated Ledge Entrance', 'Inverted Ganons Tower Exit', player) + connect_two_way(multiworld, 'Dark Death Mountain Ledge (West)', 'Hyrule Castle Exit (West)', player) + connect_two_way(multiworld, 'Dark Death Mountain Ledge (East)', 'Hyrule Castle Exit (East)', player) if dp_target == 'Hyrule Castle': - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Desert Palace Exit (South)', player) - connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Desert Palace Exit (East)', player) - connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Desert Palace Exit (West)', player) - connect_two_way(world, 'Inverted Ganons Tower', 'Desert Palace Exit (North)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (South)', 'Desert Palace Exit (South)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (East)', 'Desert Palace Exit (East)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (West)', 'Desert Palace Exit (West)', player) + connect_two_way(multiworld, 'Inverted Ganons Tower', 'Desert Palace Exit (North)', player) elif dp_target == 'Desert': - connect_two_way(world, 'Desert Palace Entrance (South)', 'Desert Palace Exit (South)', player) - connect_two_way(world, 'Desert Palace Entrance (East)', 'Desert Palace Exit (East)', player) - connect_two_way(world, 'Desert Palace Entrance (West)', 'Desert Palace Exit (West)', player) - connect_two_way(world, 'Desert Palace Entrance (North)', 'Desert Palace Exit (North)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (South)', 'Desert Palace Exit (South)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (East)', 'Desert Palace Exit (East)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (West)', 'Desert Palace Exit (West)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (North)', 'Desert Palace Exit (North)', player) elif dp_target == 'Turtle Rock': - connect_two_way(world, 'Turtle Rock', 'Desert Palace Exit (South)', player) - connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Desert Palace Exit (East)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Desert Palace Exit (West)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Desert Palace Exit (North)', player) + connect_two_way(multiworld, 'Turtle Rock', 'Desert Palace Exit (South)', player) + connect_two_way(multiworld, 'Turtle Rock Isolated Ledge Entrance', 'Desert Palace Exit (East)', player) + connect_two_way(multiworld, 'Dark Death Mountain Ledge (West)', 'Desert Palace Exit (West)', player) + connect_two_way(multiworld, 'Dark Death Mountain Ledge (East)', 'Desert Palace Exit (North)', player) if tr_target == 'Hyrule Castle': - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Turtle Rock Exit (Front)', player) - connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Turtle Rock Ledge Exit (East)', player) - connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Turtle Rock Ledge Exit (West)', player) - connect_two_way(world, 'Inverted Ganons Tower', 'Turtle Rock Isolated Ledge Exit', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (South)', 'Turtle Rock Exit (Front)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (East)', 'Turtle Rock Ledge Exit (East)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (West)', 'Turtle Rock Ledge Exit (West)', player) + connect_two_way(multiworld, 'Inverted Ganons Tower', 'Turtle Rock Isolated Ledge Exit', player) elif tr_target == 'Desert': - connect_two_way(world, 'Desert Palace Entrance (South)', 'Turtle Rock Exit (Front)', player) - connect_two_way(world, 'Desert Palace Entrance (North)', 'Turtle Rock Ledge Exit (East)', player) - connect_two_way(world, 'Desert Palace Entrance (West)', 'Turtle Rock Ledge Exit (West)', player) - connect_two_way(world, 'Desert Palace Entrance (East)', 'Turtle Rock Isolated Ledge Exit', player) + connect_two_way(multiworld, 'Desert Palace Entrance (South)', 'Turtle Rock Exit (Front)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (North)', 'Turtle Rock Ledge Exit (East)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (West)', 'Turtle Rock Ledge Exit (West)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (East)', 'Turtle Rock Isolated Ledge Exit', player) elif tr_target == 'Turtle Rock': - connect_two_way(world, 'Turtle Rock', 'Turtle Rock Exit (Front)', player) - connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Turtle Rock Isolated Ledge Exit', player) - connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Turtle Rock Ledge Exit (West)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Turtle Rock Ledge Exit (East)', player) + connect_two_way(multiworld, 'Turtle Rock', 'Turtle Rock Exit (Front)', player) + connect_two_way(multiworld, 'Turtle Rock Isolated Ledge Entrance', 'Turtle Rock Isolated Ledge Exit', player) + connect_two_way(multiworld, 'Dark Death Mountain Ledge (West)', 'Turtle Rock Ledge Exit (West)', player) + connect_two_way(multiworld, 'Dark Death Mountain Ledge (East)', 'Turtle Rock Ledge Exit (East)', player) -def crossed_shuffle_dungeons(world, player: int): +def crossed_shuffle_dungeons(multiworld: MultiWorld, player: int): lw_entrances = LW_Dungeon_Entrances.copy() dw_entrances = DW_Dungeon_Entrances.copy() for exitname, regionname in default_connections: - connect_simple(world, exitname, regionname, player) + connect_simple(multiworld, exitname, regionname, player) - skull_woods_shuffle(world, player) + skull_woods_shuffle(multiworld, player) dungeon_exits = Dungeon_Exits_Base.copy() dungeon_entrances = lw_entrances+dw_entrances - if not world.shuffle_ganon: - connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player) + if not multiworld.shuffle_ganon: + connect_two_way(multiworld, 'Ganons Tower', 'Ganons Tower Exit', player) else: dungeon_entrances.append('Ganons Tower') dungeon_exits.append('Ganons Tower Exit') - if world.worlds[player].options.mode == 'standard': + if multiworld.worlds[player].options.mode == 'standard': # must connect front of hyrule castle to do escape - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) else: dungeon_exits.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) dungeon_entrances.append('Hyrule Castle Entrance (South)') - connect_mandatory_exits(world, dungeon_entrances, dungeon_exits, + connect_mandatory_exits(multiworld, dungeon_entrances, dungeon_exits, LW_Dungeon_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit, player) - if world.worlds[player].options.mode == 'standard': - connect_caves(world, dungeon_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player) + if multiworld.worlds[player].options.mode == 'standard': + connect_caves(multiworld, dungeon_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player) - connect_caves(world, dungeon_entrances, [], dungeon_exits, player) + connect_caves(multiworld, dungeon_entrances, [], dungeon_exits, player) assert not dungeon_exits , "make sure all exits are accounted for" -def inverted_crossed_shuffle_dungeons(world, player: int): +def inverted_crossed_shuffle_dungeons(multiworld: MultiWorld, player: int): lw_entrances = Inverted_LW_Dungeon_Entrances.copy() dw_entrances = Inverted_DW_Dungeon_Entrances.copy() lw_dungeon_entrances_must_exit = list(Inverted_LW_Dungeon_Entrances_Must_Exit) for exitname, regionname in inverted_default_connections: - connect_simple(world, exitname, regionname, player) + connect_simple(multiworld, exitname, regionname, player) - skull_woods_shuffle(world, player) + skull_woods_shuffle(multiworld, player) dungeon_exits = Inverted_Dungeon_Exits_Base.copy() dungeon_entrances = lw_entrances+dw_entrances # randomize which desert ledge door is a must-exit - if world.random.randint(0, 1): + if multiworld.random.randint(0, 1): lw_dungeon_entrances_must_exit.append('Desert Palace Entrance (North)') dungeon_entrances.append('Desert Palace Entrance (West)') else: @@ -1748,8 +1751,8 @@ def inverted_crossed_shuffle_dungeons(world, player: int): dungeon_exits.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) dungeon_entrances.append('Hyrule Castle Entrance (South)') - if not world.shuffle_ganon: - connect_two_way(world, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player) + if not multiworld.shuffle_ganon: + connect_two_way(multiworld, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player) hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)'] else: dungeon_entrances.append('Inverted Ganons Tower') @@ -1757,29 +1760,34 @@ def inverted_crossed_shuffle_dungeons(world, player: int): hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)', 'Inverted Ganons Tower'] # shuffle aga door first. If it's on HC ledge, remaining HC ledge door must be must-exit - world.random.shuffle(dungeon_entrances) + multiworld.random.shuffle(dungeon_entrances) aga_door = dungeon_entrances.pop() if aga_door in hc_ledge_entrances: hc_ledge_entrances.remove(aga_door) - world.random.shuffle(hc_ledge_entrances) + multiworld.random.shuffle(hc_ledge_entrances) hc_ledge_must_exit = hc_ledge_entrances.pop() dungeon_entrances.remove(hc_ledge_must_exit) lw_dungeon_entrances_must_exit.append(hc_ledge_must_exit) - connect_two_way(world, aga_door, 'Inverted Agahnims Tower Exit', player) + connect_two_way(multiworld, aga_door, 'Inverted Agahnims Tower Exit', player) dungeon_exits.remove('Inverted Agahnims Tower Exit') - connect_mandatory_exits(world, dungeon_entrances, dungeon_exits, lw_dungeon_entrances_must_exit, player) + connect_mandatory_exits(multiworld, dungeon_entrances, dungeon_exits, lw_dungeon_entrances_must_exit, player) - connect_caves(world, dungeon_entrances, [], dungeon_exits, player) + connect_caves(multiworld, dungeon_entrances, [], dungeon_exits, player) assert not dungeon_exits, "make sure all exits are accounted for" -def unbias_some_entrances(world, Dungeon_Exits, Cave_Exits, Old_Man_House, Cave_Three_Exits): + +def unbias_some_entrances(multiworld: MultiWorld, + Dungeon_Exits: list[list[str] | str], + Cave_Exits: list[str], + Old_Man_House: list[str], + Cave_Three_Exits: list[tuple[str, str, str]]): def shuffle_lists_in_list(ls): for i, item in enumerate(ls): if isinstance(item, list): - ls[i] = world.random.sample(item, len(item)) + ls[i] = multiworld.random.sample(item, len(item)) def tuplize_lists_in_list(ls): for i, item in enumerate(ls): @@ -1793,7 +1801,7 @@ def unbias_some_entrances(world, Dungeon_Exits, Cave_Exits, Old_Man_House, Cave_ # paradox fixup if Cave_Three_Exits[1][0] == "Paradox Cave Exit (Bottom)": - i = world.random.randint(1, 2) + i = multiworld.random.randint(1, 2) Cave_Three_Exits[1][0] = Cave_Three_Exits[1][i] Cave_Three_Exits[1][i] = "Paradox Cave Exit (Bottom)" @@ -1822,18 +1830,18 @@ lookup = { } -def plando_connect(world, player: int): - if world.worlds[player].options.plando_connections: - for connection in world.worlds[player].options.plando_connections: +def plando_connect(multiworld: MultiWorld, player: int): + if multiworld.worlds[player].options.plando_connections: + for connection in multiworld.worlds[player].options.plando_connections: func = lookup[connection.direction] try: - func(world, connection.entrance, connection.exit, player) + func(multiworld, connection.entrance, connection.exit, player) except Exception as e: raise Exception(f"Could not connect using {connection}") from e - if world.worlds[player].options.mode != 'inverted': - mark_light_world_regions(world, player) + if multiworld.worlds[player].options.mode != 'inverted': + mark_light_world_regions(multiworld, player) else: - mark_dark_world_regions(world, player) + mark_dark_world_regions(multiworld, player) LW_Dungeon_Entrances = ['Desert Palace Entrance (South)', diff --git a/worlds/alttp/InvertedRegions.py b/worlds/alttp/InvertedRegions.py index 63a2d499e2..47e12d313f 100644 --- a/worlds/alttp/InvertedRegions.py +++ b/worlds/alttp/InvertedRegions.py @@ -1,15 +1,16 @@ import collections +from BaseClasses import MultiWorld from .Regions import create_lw_region, create_dw_region, create_cave_region, create_dungeon_region from .SubClasses import LTTPRegionType -def create_inverted_regions(world, player): +def create_inverted_regions(multiworld: MultiWorld, player: int): - world.regions += [ - create_dw_region(world, player, 'Menu', None, + multiworld.regions += [ + create_dw_region(multiworld, player, 'Menu', None, ['Links House S&Q', 'Dark Sanctuary S&Q', 'Old Man S&Q', 'Castle Ledge S&Q']), - create_lw_region(world, player, 'Light World', + create_lw_region(multiworld, player, 'Light World', ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure', 'Purple Chest', 'Bombos Tablet'], ["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Kings Grave Outer Rocks', 'Dam', @@ -35,184 +36,184 @@ def create_inverted_regions(world, player): 'Hyrule Castle Entrance (South)', 'Secret Passage Outer Bushes', 'Bush Covered Lawn Outer Bushes', 'Potion Shop Outer Bushes', 'Graveyard Cave Outer Bushes', 'Bomb Hut Outer Bushes']), - create_lw_region(world, player, 'Bush Covered Lawn', None, + create_lw_region(multiworld, player, 'Bush Covered Lawn', None, ['Bush Covered House', 'Bush Covered Lawn Inner Bushes', 'Bush Covered Lawn Mirror Spot']), - create_lw_region(world, player, 'Bomb Hut Area', None, + create_lw_region(multiworld, player, 'Bomb Hut Area', None, ['Light World Bomb Hut', 'Bomb Hut Inner Bushes', 'Bomb Hut Mirror Spot']), - create_lw_region(world, player, 'Hyrule Castle Secret Entrance Area', None, + create_lw_region(multiworld, player, 'Hyrule Castle Secret Entrance Area', None, ['Hyrule Castle Secret Entrance Stairs', 'Secret Passage Inner Bushes']), - create_lw_region(world, player, 'Death Mountain Entrance', None, + create_lw_region(multiworld, player, 'Death Mountain Entrance', None, ['Old Man Cave (West)', 'Death Mountain Entrance Drop', 'Bumper Cave Entrance Mirror Spot']), - create_lw_region(world, player, 'Lake Hylia Central Island', None, + create_lw_region(multiworld, player, 'Lake Hylia Central Island', None, ['Capacity Upgrade', 'Lake Hylia Central Island Mirror Spot']), - create_cave_region(world, player, 'Blinds Hideout', 'a bounty of five items', ["Blind\'s Hideout - Top", + create_cave_region(multiworld, player, 'Blinds Hideout', 'a bounty of five items', ["Blind\'s Hideout - Top", "Blind\'s Hideout - Left", "Blind\'s Hideout - Right", "Blind\'s Hideout - Far Left", "Blind\'s Hideout - Far Right"]), - create_lw_region(world, player, 'Northeast Light World', None, + create_lw_region(multiworld, player, 'Northeast Light World', None, ['Zoras River', 'Waterfall of Wishing Cave', 'Potion Shop Outer Rock', 'Catfish Mirror Spot', 'Northeast Light World Warp']), - create_lw_region(world, player, 'Waterfall of Wishing Cave', None, + create_lw_region(multiworld, player, 'Waterfall of Wishing Cave', None, ['Waterfall of Wishing', 'Northeast Light World Return']), - create_lw_region(world, player, 'Potion Shop Area', None, + create_lw_region(multiworld, player, 'Potion Shop Area', None, ['Potion Shop', 'Potion Shop Inner Bushes', 'Potion Shop Inner Rock', 'Potion Shop Mirror Spot', 'Potion Shop River Drop']), - create_lw_region(world, player, 'Graveyard Cave Area', None, + create_lw_region(multiworld, player, 'Graveyard Cave Area', None, ['Graveyard Cave', 'Graveyard Cave Inner Bushes', 'Graveyard Cave Mirror Spot']), - create_lw_region(world, player, 'River', None, ['Light World Pier', 'Potion Shop Pier']), - create_cave_region(world, player, 'Hyrule Castle Secret Entrance', 'a drop\'s exit', + create_lw_region(multiworld, player, 'River', None, ['Light World Pier', 'Potion Shop Pier']), + create_cave_region(multiworld, player, 'Hyrule Castle Secret Entrance', 'a drop\'s exit', ['Link\'s Uncle', 'Secret Passage'], ['Hyrule Castle Secret Entrance Exit']), - create_lw_region(world, player, 'Zoras River', ['King Zora', 'Zora\'s Ledge']), - create_cave_region(world, player, 'Waterfall of Wishing', 'a cave with two chests', + create_lw_region(multiworld, player, 'Zoras River', ['King Zora', 'Zora\'s Ledge']), + create_cave_region(multiworld, player, 'Waterfall of Wishing', 'a cave with two chests', ['Waterfall Fairy - Left', 'Waterfall Fairy - Right']), - create_lw_region(world, player, 'Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']), - create_cave_region(world, player, 'Kings Grave', 'a cave with a chest', ['King\'s Tomb']), - create_cave_region(world, player, 'North Fairy Cave', 'a drop\'s exit', None, ['North Fairy Cave Exit']), - create_cave_region(world, player, 'Dam', 'the dam', ['Floodgate', 'Floodgate Chest']), - create_cave_region(world, player, 'Inverted Links House', 'your house', ['Link\'s House'], + create_lw_region(multiworld, player, 'Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']), + create_cave_region(multiworld, player, 'Kings Grave', 'a cave with a chest', ['King\'s Tomb']), + create_cave_region(multiworld, player, 'North Fairy Cave', 'a drop\'s exit', None, ['North Fairy Cave Exit']), + create_cave_region(multiworld, player, 'Dam', 'the dam', ['Floodgate', 'Floodgate Chest']), + create_cave_region(multiworld, player, 'Inverted Links House', 'your house', ['Link\'s House'], ['Inverted Links House Exit']), - create_cave_region(world, player, 'Chris Houlihan Room', 'I AM ERROR', None, ['Chris Houlihan Room Exit']), - create_cave_region(world, player, 'Tavern', 'the tavern', ['Kakariko Tavern']), - create_cave_region(world, player, 'Elder House', 'a connector', None, + create_cave_region(multiworld, player, 'Chris Houlihan Room', 'I AM ERROR', None, ['Chris Houlihan Room Exit']), + create_cave_region(multiworld, player, 'Tavern', 'the tavern', ['Kakariko Tavern']), + create_cave_region(multiworld, player, 'Elder House', 'a connector', None, ['Elder House Exit (East)', 'Elder House Exit (West)']), - create_cave_region(world, player, 'Snitch Lady (East)', 'a boring house'), - create_cave_region(world, player, 'Snitch Lady (West)', 'a boring house'), - create_cave_region(world, player, 'Bush Covered House', 'the grass man'), - create_cave_region(world, player, 'Tavern (Front)', 'the tavern'), - create_cave_region(world, player, 'Light World Bomb Hut', 'a restock room'), - create_cave_region(world, player, 'Kakariko Shop', 'a common shop'), - create_cave_region(world, player, 'Fortune Teller (Light)', 'a fortune teller'), - create_cave_region(world, player, 'Lake Hylia Fortune Teller', 'a fortune teller'), - create_cave_region(world, player, 'Lumberjack House', 'a boring house'), - create_cave_region(world, player, 'Bonk Fairy (Light)', 'a fairy fountain'), - create_cave_region(world, player, 'Bonk Fairy (Dark)', 'a fairy fountain'), - create_cave_region(world, player, 'Lake Hylia Healer Fairy', 'a fairy fountain'), - create_cave_region(world, player, 'Swamp Healer Fairy', 'a fairy fountain'), - create_cave_region(world, player, 'Desert Healer Fairy', 'a fairy fountain'), - create_cave_region(world, player, 'Dark Lake Hylia Healer Fairy', 'a fairy fountain'), - create_cave_region(world, player, 'Dark Lake Hylia Ledge Healer Fairy', 'a fairy fountain'), - create_cave_region(world, player, 'Dark Desert Healer Fairy', 'a fairy fountain'), - create_cave_region(world, player, 'Dark Death Mountain Healer Fairy', 'a fairy fountain'), - create_cave_region(world, player, 'Chicken House', 'a house with a chest', ['Chicken House']), - create_cave_region(world, player, 'Aginahs Cave', 'a cave with a chest', ['Aginah\'s Cave']), - create_cave_region(world, player, 'Sahasrahlas Hut', 'Sahasrahla', + create_cave_region(multiworld, player, 'Snitch Lady (East)', 'a boring house'), + create_cave_region(multiworld, player, 'Snitch Lady (West)', 'a boring house'), + create_cave_region(multiworld, player, 'Bush Covered House', 'the grass man'), + create_cave_region(multiworld, player, 'Tavern (Front)', 'the tavern'), + create_cave_region(multiworld, player, 'Light World Bomb Hut', 'a restock room'), + create_cave_region(multiworld, player, 'Kakariko Shop', 'a common shop'), + create_cave_region(multiworld, player, 'Fortune Teller (Light)', 'a fortune teller'), + create_cave_region(multiworld, player, 'Lake Hylia Fortune Teller', 'a fortune teller'), + create_cave_region(multiworld, player, 'Lumberjack House', 'a boring house'), + create_cave_region(multiworld, player, 'Bonk Fairy (Light)', 'a fairy fountain'), + create_cave_region(multiworld, player, 'Bonk Fairy (Dark)', 'a fairy fountain'), + create_cave_region(multiworld, player, 'Lake Hylia Healer Fairy', 'a fairy fountain'), + create_cave_region(multiworld, player, 'Swamp Healer Fairy', 'a fairy fountain'), + create_cave_region(multiworld, player, 'Desert Healer Fairy', 'a fairy fountain'), + create_cave_region(multiworld, player, 'Dark Lake Hylia Healer Fairy', 'a fairy fountain'), + create_cave_region(multiworld, player, 'Dark Lake Hylia Ledge Healer Fairy', 'a fairy fountain'), + create_cave_region(multiworld, player, 'Dark Desert Healer Fairy', 'a fairy fountain'), + create_cave_region(multiworld, player, 'Dark Death Mountain Healer Fairy', 'a fairy fountain'), + create_cave_region(multiworld, player, 'Chicken House', 'a house with a chest', ['Chicken House']), + create_cave_region(multiworld, player, 'Aginahs Cave', 'a cave with a chest', ['Aginah\'s Cave']), + create_cave_region(multiworld, player, 'Sahasrahlas Hut', 'Sahasrahla', ['Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right', 'Sahasrahla']), - create_cave_region(world, player, 'Kakariko Well (top)', 'a drop\'s exit', + create_cave_region(multiworld, player, 'Kakariko Well (top)', 'a drop\'s exit', ['Kakariko Well - Top', 'Kakariko Well - Left', 'Kakariko Well - Middle', 'Kakariko Well - Right', 'Kakariko Well - Bottom'], ['Kakariko Well (top to bottom)']), - create_cave_region(world, player, 'Kakariko Well (bottom)', 'a drop\'s exit', None, ['Kakariko Well Exit']), - create_cave_region(world, player, 'Blacksmiths Hut', 'the smith', ['Blacksmith', 'Missing Smith']), - create_lw_region(world, player, 'Bat Cave Drop Ledge', None, ['Bat Cave Drop']), - create_cave_region(world, player, 'Bat Cave (right)', 'a drop\'s exit', ['Magic Bat'], ['Bat Cave Door']), - create_cave_region(world, player, 'Bat Cave (left)', 'a drop\'s exit', None, ['Bat Cave Exit']), - create_cave_region(world, player, 'Sick Kids House', 'the sick kid', ['Sick Kid']), - create_lw_region(world, player, 'Hobo Bridge', ['Hobo']), - create_cave_region(world, player, 'Lost Woods Hideout (top)', 'a drop\'s exit', ['Lost Woods Hideout'], + create_cave_region(multiworld, player, 'Kakariko Well (bottom)', 'a drop\'s exit', None, ['Kakariko Well Exit']), + create_cave_region(multiworld, player, 'Blacksmiths Hut', 'the smith', ['Blacksmith', 'Missing Smith']), + create_lw_region(multiworld, player, 'Bat Cave Drop Ledge', None, ['Bat Cave Drop']), + create_cave_region(multiworld, player, 'Bat Cave (right)', 'a drop\'s exit', ['Magic Bat'], ['Bat Cave Door']), + create_cave_region(multiworld, player, 'Bat Cave (left)', 'a drop\'s exit', None, ['Bat Cave Exit']), + create_cave_region(multiworld, player, 'Sick Kids House', 'the sick kid', ['Sick Kid']), + create_lw_region(multiworld, player, 'Hobo Bridge', ['Hobo']), + create_cave_region(multiworld, player, 'Lost Woods Hideout (top)', 'a drop\'s exit', ['Lost Woods Hideout'], ['Lost Woods Hideout (top to bottom)']), - create_cave_region(world, player, 'Lost Woods Hideout (bottom)', 'a drop\'s exit', None, + create_cave_region(multiworld, player, 'Lost Woods Hideout (bottom)', 'a drop\'s exit', None, ['Lost Woods Hideout Exit']), - create_cave_region(world, player, 'Lumberjack Tree (top)', 'a drop\'s exit', ['Lumberjack Tree'], + create_cave_region(multiworld, player, 'Lumberjack Tree (top)', 'a drop\'s exit', ['Lumberjack Tree'], ['Lumberjack Tree (top to bottom)']), - create_cave_region(world, player, 'Lumberjack Tree (bottom)', 'a drop\'s exit', None, ['Lumberjack Tree Exit']), - create_cave_region(world, player, 'Cave 45', 'a cave with an item', ['Cave 45']), - create_cave_region(world, player, 'Graveyard Cave', 'a cave with an item', ['Graveyard Cave']), - create_cave_region(world, player, 'Checkerboard Cave', 'a cave with an item', ['Checkerboard Cave']), - create_cave_region(world, player, 'Long Fairy Cave', 'a fairy fountain'), - create_cave_region(world, player, 'Mini Moldorm Cave', 'a bounty of five items', + create_cave_region(multiworld, player, 'Lumberjack Tree (bottom)', 'a drop\'s exit', None, ['Lumberjack Tree Exit']), + create_cave_region(multiworld, player, 'Cave 45', 'a cave with an item', ['Cave 45']), + create_cave_region(multiworld, player, 'Graveyard Cave', 'a cave with an item', ['Graveyard Cave']), + create_cave_region(multiworld, player, 'Checkerboard Cave', 'a cave with an item', ['Checkerboard Cave']), + create_cave_region(multiworld, player, 'Long Fairy Cave', 'a fairy fountain'), + create_cave_region(multiworld, player, 'Mini Moldorm Cave', 'a bounty of five items', ['Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right', 'Mini Moldorm Cave - Far Right', 'Mini Moldorm Cave - Generous Guy']), - create_cave_region(world, player, 'Ice Rod Cave', 'a cave with a chest', ['Ice Rod Cave']), - create_cave_region(world, player, 'Good Bee Cave', 'a cold bee'), - create_cave_region(world, player, '20 Rupee Cave', 'a cave with some cash'), - create_cave_region(world, player, 'Cave Shop (Lake Hylia)', 'a common shop'), - create_cave_region(world, player, 'Cave Shop (Dark Death Mountain)', 'a common shop'), - create_cave_region(world, player, 'Bonk Rock Cave', 'a cave with a chest', ['Bonk Rock Cave']), - create_cave_region(world, player, 'Library', 'the library', ['Library']), - create_cave_region(world, player, 'Kakariko Gamble Game', 'a game of chance'), - create_cave_region(world, player, 'Potion Shop', 'the potion shop', ['Potion Shop']), - create_lw_region(world, player, 'Lake Hylia Island', ['Lake Hylia Island']), - create_cave_region(world, player, 'Capacity Upgrade', 'the queen of fairies', ['Capacity Upgrade Shop']), - create_cave_region(world, player, 'Two Brothers House', 'a connector', None, + create_cave_region(multiworld, player, 'Ice Rod Cave', 'a cave with a chest', ['Ice Rod Cave']), + create_cave_region(multiworld, player, 'Good Bee Cave', 'a cold bee'), + create_cave_region(multiworld, player, '20 Rupee Cave', 'a cave with some cash'), + create_cave_region(multiworld, player, 'Cave Shop (Lake Hylia)', 'a common shop'), + create_cave_region(multiworld, player, 'Cave Shop (Dark Death Mountain)', 'a common shop'), + create_cave_region(multiworld, player, 'Bonk Rock Cave', 'a cave with a chest', ['Bonk Rock Cave']), + create_cave_region(multiworld, player, 'Library', 'the library', ['Library']), + create_cave_region(multiworld, player, 'Kakariko Gamble Game', 'a game of chance'), + create_cave_region(multiworld, player, 'Potion Shop', 'the potion shop', ['Potion Shop']), + create_lw_region(multiworld, player, 'Lake Hylia Island', ['Lake Hylia Island']), + create_cave_region(multiworld, player, 'Capacity Upgrade', 'the queen of fairies', ['Capacity Upgrade Shop']), + create_cave_region(multiworld, player, 'Two Brothers House', 'a connector', None, ['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)']), - create_lw_region(world, player, 'Maze Race Ledge', ['Maze Race'], + create_lw_region(multiworld, player, 'Maze Race Ledge', ['Maze Race'], ['Two Brothers House (West)', 'Maze Race Mirror Spot']), - create_cave_region(world, player, '50 Rupee Cave', 'a cave with some cash'), - create_lw_region(world, player, 'Desert Ledge', ['Desert Ledge'], + create_cave_region(multiworld, player, '50 Rupee Cave', 'a cave with some cash'), + create_lw_region(multiworld, player, 'Desert Ledge', ['Desert Ledge'], ['Desert Palace Entrance (North) Rocks', 'Desert Palace Entrance (West)', 'Desert Ledge Drop']), - create_lw_region(world, player, 'Desert Palace Stairs', None, + create_lw_region(multiworld, player, 'Desert Palace Stairs', None, ['Desert Palace Entrance (South)', 'Desert Palace Stairs Mirror Spot']), - create_lw_region(world, player, 'Desert Palace Lone Stairs', None, + create_lw_region(multiworld, player, 'Desert Palace Lone Stairs', None, ['Desert Palace Stairs Drop', 'Desert Palace Entrance (East)']), - create_lw_region(world, player, 'Desert Palace Entrance (North) Spot', None, + create_lw_region(multiworld, player, 'Desert Palace Entrance (North) Spot', None, ['Desert Palace Entrance (North)', 'Desert Ledge Return Rocks', 'Desert Palace North Mirror Spot']), - create_dungeon_region(world, player, 'Desert Palace Main (Outer)', 'Desert Palace', ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'], + create_dungeon_region(multiworld, player, 'Desert Palace Main (Outer)', 'Desert Palace', ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'], ['Desert Palace Pots (Outer)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)', 'Desert Palace East Wing']), - create_dungeon_region(world, player, 'Desert Palace Main (Inner)', 'Desert Palace', None, ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']), - create_dungeon_region(world, player, 'Desert Palace East', 'Desert Palace', ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']), - create_dungeon_region(world, player, 'Desert Palace North', 'Desert Palace', + create_dungeon_region(multiworld, player, 'Desert Palace Main (Inner)', 'Desert Palace', None, ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']), + create_dungeon_region(multiworld, player, 'Desert Palace East', 'Desert Palace', ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']), + create_dungeon_region(multiworld, player, 'Desert Palace North', 'Desert Palace', ['Desert Palace - Desert Tiles 1 Pot Key', 'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key', 'Desert Palace - Boss', 'Desert Palace - Prize'], ['Desert Palace Exit (North)']), - create_dungeon_region(world, player, 'Eastern Palace', 'Eastern Palace', + create_dungeon_region(multiworld, player, 'Eastern Palace', 'Eastern Palace', ['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest', 'Eastern Palace - Cannonball Chest', 'Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop', 'Eastern Palace - Big Key Chest', 'Eastern Palace - Map Chest', 'Eastern Palace - Boss', 'Eastern Palace - Prize'], ['Eastern Palace Exit']), - create_lw_region(world, player, 'Master Sword Meadow', ['Master Sword Pedestal']), - create_cave_region(world, player, 'Lost Woods Gamble', 'a game of chance'), - create_lw_region(world, player, 'Hyrule Castle Ledge', None, ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Inverted Ganons Tower', 'Hyrule Castle Ledge Courtyard Drop', 'Inverted Pyramid Hole']), - create_dungeon_region(world, player, 'Hyrule Castle', 'Hyrule Castle', + create_lw_region(multiworld, player, 'Master Sword Meadow', ['Master Sword Pedestal']), + create_cave_region(multiworld, player, 'Lost Woods Gamble', 'a game of chance'), + create_lw_region(multiworld, player, 'Hyrule Castle Ledge', None, ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Inverted Ganons Tower', 'Hyrule Castle Ledge Courtyard Drop', 'Inverted Pyramid Hole']), + create_dungeon_region(multiworld, player, 'Hyrule Castle', 'Hyrule Castle', ['Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', 'Hyrule Castle - Zelda\'s Chest', 'Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop', 'Hyrule Castle - Big Key Drop'], ['Hyrule Castle Exit (East)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (South)', 'Throne Room']), - create_dungeon_region(world, player, 'Sewer Drop', 'a drop\'s exit', None, ['Sewer Drop']), # This exists only to be referenced for access checks - create_dungeon_region(world, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross', 'Sewers - Key Rat Key Drop'], ['Sewers Door']), - create_dungeon_region(world, player, 'Sewers', 'a drop\'s exit', None, ['Sanctuary Push Door', 'Sewers Back Door', 'Sewers Secret Room']), - create_dungeon_region(world, player, 'Sewers Secret Room', 'a drop\'s exit', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle', + create_dungeon_region(multiworld, player, 'Sewer Drop', 'a drop\'s exit', None, ['Sewer Drop']), # This exists only to be referenced for access checks + create_dungeon_region(multiworld, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross', 'Sewers - Key Rat Key Drop'], ['Sewers Door']), + create_dungeon_region(multiworld, player, 'Sewers', 'a drop\'s exit', None, ['Sanctuary Push Door', 'Sewers Back Door', 'Sewers Secret Room']), + create_dungeon_region(multiworld, player, 'Sewers Secret Room', 'a drop\'s exit', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle', 'Sewers - Secret Room - Right']), - create_dungeon_region(world, player, 'Sanctuary', 'a drop\'s exit', ['Sanctuary'], ['Sanctuary Exit']), - create_dungeon_region(world, player, 'Inverted Agahnims Tower', 'Castle Tower', ['Castle Tower - Room 03', 'Castle Tower - Dark Maze', 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'], ['Agahnim 1', 'Inverted Agahnims Tower Exit']), - create_dungeon_region(world, player, 'Agahnim 1', 'Castle Tower', ['Agahnim 1'], None), - create_cave_region(world, player, 'Old Man Cave', 'a connector', ['Old Man'], + create_dungeon_region(multiworld, player, 'Sanctuary', 'a drop\'s exit', ['Sanctuary'], ['Sanctuary Exit']), + create_dungeon_region(multiworld, player, 'Inverted Agahnims Tower', 'Castle Tower', ['Castle Tower - Room 03', 'Castle Tower - Dark Maze', 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'], ['Agahnim 1', 'Inverted Agahnims Tower Exit']), + create_dungeon_region(multiworld, player, 'Agahnim 1', 'Castle Tower', ['Agahnim 1'], None), + create_cave_region(multiworld, player, 'Old Man Cave', 'a connector', ['Old Man'], ['Old Man Cave Exit (East)', 'Old Man Cave Exit (West)']), - create_cave_region(world, player, 'Old Man House', 'a connector', None, + create_cave_region(multiworld, player, 'Old Man House', 'a connector', None, ['Old Man House Exit (Bottom)', 'Old Man House Front to Back']), - create_cave_region(world, player, 'Old Man House Back', 'a connector', None, + create_cave_region(multiworld, player, 'Old Man House Back', 'a connector', None, ['Old Man House Exit (Top)', 'Old Man House Back to Front']), - create_lw_region(world, player, 'Death Mountain', None, + create_lw_region(multiworld, player, 'Death Mountain', None, ['Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)', 'Death Mountain Return Cave (East)', 'Spectacle Rock Cave', 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)', 'Broken Bridge (West)', 'Death Mountain Mirror Spot']), - create_cave_region(world, player, 'Death Mountain Return Cave', 'a connector', None, + create_cave_region(multiworld, player, 'Death Mountain Return Cave', 'a connector', None, ['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)']), - create_lw_region(world, player, 'Death Mountain Return Ledge', None, + create_lw_region(multiworld, player, 'Death Mountain Return Ledge', None, ['Death Mountain Return Ledge Drop', 'Death Mountain Return Cave (West)', 'Bumper Cave Ledge Mirror Spot']), - create_cave_region(world, player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'], + create_cave_region(multiworld, player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'], ['Spectacle Rock Cave Drop', 'Spectacle Rock Cave Exit (Top)']), - create_cave_region(world, player, 'Spectacle Rock Cave (Bottom)', 'a connector', None, + create_cave_region(multiworld, player, 'Spectacle Rock Cave (Bottom)', 'a connector', None, ['Spectacle Rock Cave Exit']), - create_cave_region(world, player, 'Spectacle Rock Cave (Peak)', 'a connector', None, + create_cave_region(multiworld, player, 'Spectacle Rock Cave (Peak)', 'a connector', None, ['Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Exit (Peak)']), - create_lw_region(world, player, 'East Death Mountain (Bottom)', None, + create_lw_region(multiworld, player, 'East Death Mountain (Bottom)', None, ['Broken Bridge (East)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'East Death Mountain Mirror Spot (Bottom)', 'Hookshot Fairy', 'Fairy Ascension Rocks', 'Spiral Cave (Bottom)']), - create_cave_region(world, player, 'Hookshot Fairy', 'fairies deep in a cave'), - create_cave_region(world, player, 'Paradox Cave Front', 'a connector', None, + create_cave_region(multiworld, player, 'Hookshot Fairy', 'fairies deep in a cave'), + create_cave_region(multiworld, player, 'Paradox Cave Front', 'a connector', None, ['Paradox Cave Push Block Reverse', 'Paradox Cave Exit (Bottom)', 'Light World Death Mountain Shop']), - create_cave_region(world, player, 'Paradox Cave Chest Area', 'a connector', ['Paradox Cave Lower - Far Left', + create_cave_region(multiworld, player, 'Paradox Cave Chest Area', 'a connector', ['Paradox Cave Lower - Far Left', 'Paradox Cave Lower - Left', 'Paradox Cave Lower - Right', 'Paradox Cave Lower - Far Right', @@ -220,273 +221,273 @@ def create_inverted_regions(world, player): 'Paradox Cave Upper - Left', 'Paradox Cave Upper - Right'], ['Paradox Cave Push Block', 'Paradox Cave Bomb Jump']), - create_cave_region(world, player, 'Paradox Cave', 'a connector', None, + create_cave_region(multiworld, player, 'Paradox Cave', 'a connector', None, ['Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Top)', 'Paradox Cave Drop']), - create_cave_region(world, player, 'Light World Death Mountain Shop', 'a common shop'), - create_lw_region(world, player, 'East Death Mountain (Top)', ['Floating Island'], + create_cave_region(multiworld, player, 'Light World Death Mountain Shop', 'a common shop'), + create_lw_region(multiworld, player, 'East Death Mountain (Top)', ['Floating Island'], ['Paradox Cave (Top)', 'Death Mountain (Top)', 'Spiral Cave Ledge Access', 'East Death Mountain Drop', 'East Death Mountain Mirror Spot (Top)', 'Fairy Ascension Ledge Access', 'Mimic Cave Ledge Access', 'Floating Island Mirror Spot']), - create_lw_region(world, player, 'Spiral Cave Ledge', None, + create_lw_region(multiworld, player, 'Spiral Cave Ledge', None, ['Spiral Cave', 'Spiral Cave Ledge Drop', 'Dark Death Mountain Ledge Mirror Spot (West)']), - create_lw_region(world, player, 'Mimic Cave Ledge', None, + create_lw_region(multiworld, player, 'Mimic Cave Ledge', None, ['Mimic Cave', 'Mimic Cave Ledge Drop', 'Dark Death Mountain Ledge Mirror Spot (East)']), - create_cave_region(world, player, 'Spiral Cave (Top)', 'a connector', ['Spiral Cave'], + create_cave_region(multiworld, player, 'Spiral Cave (Top)', 'a connector', ['Spiral Cave'], ['Spiral Cave (top to bottom)', 'Spiral Cave Exit (Top)']), - create_cave_region(world, player, 'Spiral Cave (Bottom)', 'a connector', None, ['Spiral Cave Exit']), - create_lw_region(world, player, 'Fairy Ascension Plateau', None, + create_cave_region(multiworld, player, 'Spiral Cave (Bottom)', 'a connector', None, ['Spiral Cave Exit']), + create_lw_region(multiworld, player, 'Fairy Ascension Plateau', None, ['Fairy Ascension Drop', 'Fairy Ascension Cave (Bottom)']), - create_cave_region(world, player, 'Fairy Ascension Cave (Bottom)', 'a connector', None, + create_cave_region(multiworld, player, 'Fairy Ascension Cave (Bottom)', 'a connector', None, ['Fairy Ascension Cave Climb', 'Fairy Ascension Cave Exit (Bottom)']), - create_cave_region(world, player, 'Fairy Ascension Cave (Drop)', 'a connector', None, + create_cave_region(multiworld, player, 'Fairy Ascension Cave (Drop)', 'a connector', None, ['Fairy Ascension Cave Pots']), - create_cave_region(world, player, 'Fairy Ascension Cave (Top)', 'a connector', None, + create_cave_region(multiworld, player, 'Fairy Ascension Cave (Top)', 'a connector', None, ['Fairy Ascension Cave Exit (Top)', 'Fairy Ascension Cave Drop']), - create_lw_region(world, player, 'Fairy Ascension Ledge', None, + create_lw_region(multiworld, player, 'Fairy Ascension Ledge', None, ['Fairy Ascension Ledge Drop', 'Fairy Ascension Cave (Top)', 'Laser Bridge Mirror Spot']), - create_lw_region(world, player, 'Death Mountain (Top)', ['Ether Tablet', 'Spectacle Rock'], + create_lw_region(multiworld, player, 'Death Mountain (Top)', ['Ether Tablet', 'Spectacle Rock'], ['East Death Mountain (Top)', 'Tower of Hera', 'Death Mountain Drop', 'Death Mountain (Top) Mirror Spot']), - create_dw_region(world, player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'], + create_dw_region(multiworld, player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'], ['Bumper Cave Ledge Drop', 'Bumper Cave (Top)']), - create_dungeon_region(world, player, 'Tower of Hera (Bottom)', 'Tower of Hera', ['Tower of Hera - Basement Cage', 'Tower of Hera - Map Chest'], ['Tower of Hera Small Key Door', 'Tower of Hera Big Key Door', 'Tower of Hera Exit']), - create_dungeon_region(world, player, 'Tower of Hera (Basement)', 'Tower of Hera', ['Tower of Hera - Big Key Chest']), - create_dungeon_region(world, player, 'Tower of Hera (Top)', 'Tower of Hera', ['Tower of Hera - Compass Chest', 'Tower of Hera - Big Chest', 'Tower of Hera - Boss', 'Tower of Hera - Prize']), + create_dungeon_region(multiworld, player, 'Tower of Hera (Bottom)', 'Tower of Hera', ['Tower of Hera - Basement Cage', 'Tower of Hera - Map Chest'], ['Tower of Hera Small Key Door', 'Tower of Hera Big Key Door', 'Tower of Hera Exit']), + create_dungeon_region(multiworld, player, 'Tower of Hera (Basement)', 'Tower of Hera', ['Tower of Hera - Big Key Chest']), + create_dungeon_region(multiworld, player, 'Tower of Hera (Top)', 'Tower of Hera', ['Tower of Hera - Compass Chest', 'Tower of Hera - Big Chest', 'Tower of Hera - Boss', 'Tower of Hera - Prize']), - create_dw_region(world, player, 'East Dark World', ['Pyramid'], + create_dw_region(multiworld, player, 'East Dark World', ['Pyramid'], ['Pyramid Fairy', 'South Dark World Bridge', 'Palace of Darkness', 'Dark Lake Hylia Drop (East)', 'Dark Lake Hylia Fairy', 'Palace of Darkness Hint', 'East Dark World Hint', 'Northeast Dark World Broken Bridge Pass', 'East Dark World Teleporter', 'EDW Flute']), - create_dw_region(world, player, 'Catfish', ['Catfish'], ['Catfish Exit Rock']), - create_dw_region(world, player, 'Northeast Dark World', None, + create_dw_region(multiworld, player, 'Catfish', ['Catfish'], ['Catfish Exit Rock']), + create_dw_region(multiworld, player, 'Northeast Dark World', None, ['West Dark World Gap', 'Dark World Potion Shop', 'East Dark World Broken Bridge Pass', 'NEDW Flute', 'Dark Lake Hylia Teleporter', 'Catfish Entrance Rock']), - create_cave_region(world, player, 'Palace of Darkness Hint', 'a storyteller'), - create_cave_region(world, player, 'East Dark World Hint', 'a storyteller'), - create_dw_region(world, player, 'South Dark World', ['Stumpy', 'Digging Game'], + create_cave_region(multiworld, player, 'Palace of Darkness Hint', 'a storyteller'), + create_cave_region(multiworld, player, 'East Dark World Hint', 'a storyteller'), + create_dw_region(multiworld, player, 'South Dark World', ['Stumpy', 'Digging Game'], ['Dark Lake Hylia Drop (South)', 'Hype Cave', 'Swamp Palace', 'Village of Outcasts Heavy Rock', 'East Dark World Bridge', 'Inverted Links House', 'Archery Game', 'Bonk Fairy (Dark)', 'Dark Lake Hylia Shop', 'South Dark World Teleporter', 'Post Aga Teleporter', 'SDW Flute']), - create_cave_region(world, player, 'Inverted Big Bomb Shop', 'the bomb shop'), - create_cave_region(world, player, 'Archery Game', 'a game of skill'), - create_dw_region(world, player, 'Dark Lake Hylia', None, + create_cave_region(multiworld, player, 'Inverted Big Bomb Shop', 'the bomb shop'), + create_cave_region(multiworld, player, 'Archery Game', 'a game of skill'), + create_dw_region(multiworld, player, 'Dark Lake Hylia', None, ['East Dark World Pier', 'Dark Lake Hylia Ledge Pier', 'Ice Palace Missing Wall']), - create_dw_region(world, player, 'Dark Lake Hylia Central Island', None, + create_dw_region(multiworld, player, 'Dark Lake Hylia Central Island', None, ['Dark Lake Hylia Shallows', 'Ice Palace', 'Dark Lake Hylia Central Island Teleporter']), - create_dw_region(world, player, 'Dark Lake Hylia Ledge', None, + create_dw_region(multiworld, player, 'Dark Lake Hylia Ledge', None, ['Dark Lake Hylia Ledge Drop', 'Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Ledge Hint', 'Dark Lake Hylia Ledge Spike Cave', 'DLHL Flute']), - create_cave_region(world, player, 'Dark Lake Hylia Ledge Hint', 'a storyteller'), - create_cave_region(world, player, 'Dark Lake Hylia Ledge Spike Cave', 'a spiky hint'), - create_cave_region(world, player, 'Hype Cave', 'a bounty of five items', + create_cave_region(multiworld, player, 'Dark Lake Hylia Ledge Hint', 'a storyteller'), + create_cave_region(multiworld, player, 'Dark Lake Hylia Ledge Spike Cave', 'a spiky hint'), + create_cave_region(multiworld, player, 'Hype Cave', 'a bounty of five items', ['Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left', 'Hype Cave - Bottom', 'Hype Cave - Generous Guy']), - create_dw_region(world, player, 'West Dark World', ['Frog', 'Flute Activation Spot'], + create_dw_region(multiworld, player, 'West Dark World', ['Frog', 'Flute Activation Spot'], ['Village of Outcasts Drop', 'East Dark World River Pier', 'Brewery', 'C-Shaped House', 'Chest Game', 'Thieves Town', 'Bumper Cave Entrance Rock', 'Skull Woods Forest', 'Village of Outcasts Pegs', 'Village of Outcasts Eastern Rocks', 'Red Shield Shop', 'Inverted Dark Sanctuary', 'Fortune Teller (Dark)', 'Dark World Lumberjack Shop', 'West Dark World Teleporter', 'WDW Flute']), - create_dw_region(world, player, 'Dark Grassy Lawn', None, + create_dw_region(multiworld, player, 'Dark Grassy Lawn', None, ['Grassy Lawn Pegs', 'Village of Outcasts Shop', 'Dark Grassy Lawn Flute']), - create_dw_region(world, player, 'Hammer Peg Area', ['Dark Blacksmith Ruins'], + create_dw_region(multiworld, player, 'Hammer Peg Area', ['Dark Blacksmith Ruins'], ['Dark World Hammer Peg Cave', 'Peg Area Rocks', 'Hammer Peg Area Flute']), - create_dw_region(world, player, 'Bumper Cave Entrance', None, + create_dw_region(multiworld, player, 'Bumper Cave Entrance', None, ['Bumper Cave (Bottom)', 'Bumper Cave Entrance Drop']), - create_cave_region(world, player, 'Fortune Teller (Dark)', 'a fortune teller'), - create_cave_region(world, player, 'Village of Outcasts Shop', 'a common shop'), - create_cave_region(world, player, 'Dark Lake Hylia Shop', 'a common shop'), - create_cave_region(world, player, 'Dark World Lumberjack Shop', 'a common shop'), - create_cave_region(world, player, 'Dark World Potion Shop', 'a common shop'), - create_cave_region(world, player, 'Dark World Hammer Peg Cave', 'a cave with an item', ['Peg Cave']), - create_cave_region(world, player, 'Pyramid Fairy', 'a cave with two chests', + create_cave_region(multiworld, player, 'Fortune Teller (Dark)', 'a fortune teller'), + create_cave_region(multiworld, player, 'Village of Outcasts Shop', 'a common shop'), + create_cave_region(multiworld, player, 'Dark Lake Hylia Shop', 'a common shop'), + create_cave_region(multiworld, player, 'Dark World Lumberjack Shop', 'a common shop'), + create_cave_region(multiworld, player, 'Dark World Potion Shop', 'a common shop'), + create_cave_region(multiworld, player, 'Dark World Hammer Peg Cave', 'a cave with an item', ['Peg Cave']), + create_cave_region(multiworld, player, 'Pyramid Fairy', 'a cave with two chests', ['Pyramid Fairy - Left', 'Pyramid Fairy - Right']), - create_cave_region(world, player, 'Brewery', 'a house with a chest', ['Brewery']), - create_cave_region(world, player, 'C-Shaped House', 'a house with a chest', ['C-Shaped House']), - create_cave_region(world, player, 'Chest Game', 'a game of 16 chests', ['Chest Game']), - create_cave_region(world, player, 'Red Shield Shop', 'the rare shop'), - create_cave_region(world, player, 'Inverted Dark Sanctuary', 'a storyteller', None, + create_cave_region(multiworld, player, 'Brewery', 'a house with a chest', ['Brewery']), + create_cave_region(multiworld, player, 'C-Shaped House', 'a house with a chest', ['C-Shaped House']), + create_cave_region(multiworld, player, 'Chest Game', 'a game of 16 chests', ['Chest Game']), + create_cave_region(multiworld, player, 'Red Shield Shop', 'the rare shop'), + create_cave_region(multiworld, player, 'Inverted Dark Sanctuary', 'a storyteller', None, ['Inverted Dark Sanctuary Exit']), - create_cave_region(world, player, 'Bumper Cave', 'a connector', None, + create_cave_region(multiworld, player, 'Bumper Cave', 'a connector', None, ['Bumper Cave Exit (Bottom)', 'Bumper Cave Exit (Top)']), - create_dw_region(world, player, 'Skull Woods Forest', None, + create_dw_region(multiworld, player, 'Skull Woods Forest', None, ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)']), - create_dw_region(world, player, 'Skull Woods Forest (West)', None, + create_dw_region(multiworld, player, 'Skull Woods Forest (West)', None, ['Skull Woods Second Section Hole', 'Skull Woods Second Section Door (West)', 'Skull Woods Final Section']), - create_dw_region(world, player, 'Dark Desert', None, + create_dw_region(multiworld, player, 'Dark Desert', None, ['Misery Mire', 'Mire Shed', 'Dark Desert Hint', 'Dark Desert Fairy', 'DD Flute']), - create_dw_region(world, player, 'Dark Desert Ledge', None, ['Dark Desert Drop', 'Dark Desert Teleporter']), - create_cave_region(world, player, 'Mire Shed', 'a cave with two chests', + create_dw_region(multiworld, player, 'Dark Desert Ledge', None, ['Dark Desert Drop', 'Dark Desert Teleporter']), + create_cave_region(multiworld, player, 'Mire Shed', 'a cave with two chests', ['Mire Shed - Left', 'Mire Shed - Right']), - create_cave_region(world, player, 'Dark Desert Hint', 'a storyteller'), - create_dw_region(world, player, 'Dark Death Mountain', None, + create_cave_region(multiworld, player, 'Dark Desert Hint', 'a storyteller'), + create_dw_region(multiworld, player, 'Dark Death Mountain', None, ['Dark Death Mountain Drop (East)', 'Inverted Agahnims Tower', 'Superbunny Cave (Top)', 'Hookshot Cave', 'Turtle Rock', 'Spike Cave', 'Dark Death Mountain Fairy', 'Dark Death Mountain Teleporter (West)', 'Turtle Rock Tail Drop', 'DDM Flute']), - create_dw_region(world, player, 'Dark Death Mountain Ledge', None, + create_dw_region(multiworld, player, 'Dark Death Mountain Ledge', None, ['Dark Death Mountain Ledge (East)', 'Dark Death Mountain Ledge (West)']), - create_dw_region(world, player, 'Turtle Rock (Top)', None, + create_dw_region(multiworld, player, 'Turtle Rock (Top)', None, ['Dark Death Mountain Teleporter (East)', 'Turtle Rock Drop']), - create_dw_region(world, player, 'Dark Death Mountain Isolated Ledge', None, + create_dw_region(multiworld, player, 'Dark Death Mountain Isolated Ledge', None, ['Turtle Rock Isolated Ledge Entrance']), - create_dw_region(world, player, 'Dark Death Mountain (East Bottom)', None, + create_dw_region(multiworld, player, 'Dark Death Mountain (East Bottom)', None, ['Superbunny Cave (Bottom)', 'Cave Shop (Dark Death Mountain)', 'Dark Death Mountain Teleporter (East Bottom)', 'EDDM Flute']), - create_cave_region(world, player, 'Superbunny Cave (Top)', 'a connector', + create_cave_region(multiworld, player, 'Superbunny Cave (Top)', 'a connector', ['Superbunny Cave - Top', 'Superbunny Cave - Bottom'], ['Superbunny Cave Exit (Top)']), - create_cave_region(world, player, 'Superbunny Cave (Bottom)', 'a connector', None, + create_cave_region(multiworld, player, 'Superbunny Cave (Bottom)', 'a connector', None, ['Superbunny Cave Climb', 'Superbunny Cave Exit (Bottom)']), - create_cave_region(world, player, 'Spike Cave', 'Spike Cave', ['Spike Cave']), - create_cave_region(world, player, 'Hookshot Cave', 'a connector', + create_cave_region(multiworld, player, 'Spike Cave', 'Spike Cave', ['Spike Cave']), + create_cave_region(multiworld, player, 'Hookshot Cave', 'a connector', ['Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left'], ['Hookshot Cave Exit (South)', 'Hookshot Cave Bomb Wall (South)']), - create_cave_region(world, player, 'Hookshot Cave (Upper)', 'a connector', None, ['Hookshot Cave Exit (North)', + create_cave_region(multiworld, player, 'Hookshot Cave (Upper)', 'a connector', None, ['Hookshot Cave Exit (North)', 'Hookshot Cave Bomb Wall (North)']), - create_dw_region(world, player, 'Death Mountain Floating Island (Dark World)', None, + create_dw_region(multiworld, player, 'Death Mountain Floating Island (Dark World)', None, ['Floating Island Drop', 'Hookshot Cave Back Entrance']), - create_cave_region(world, player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']), + create_cave_region(multiworld, player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']), - create_dungeon_region(world, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None, ['Swamp Palace Moat', 'Swamp Palace Exit']), - create_dungeon_region(world, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'], ['Swamp Palace Small Key Door']), - create_dungeon_region(world, player, 'Swamp Palace (Starting Area)', 'Swamp Palace', ['Swamp Palace - Map Chest', 'Swamp Palace - Pot Row Pot Key', + create_dungeon_region(multiworld, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None, ['Swamp Palace Moat', 'Swamp Palace Exit']), + create_dungeon_region(multiworld, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'], ['Swamp Palace Small Key Door']), + create_dungeon_region(multiworld, player, 'Swamp Palace (Starting Area)', 'Swamp Palace', ['Swamp Palace - Map Chest', 'Swamp Palace - Pot Row Pot Key', 'Swamp Palace - Trench 1 Pot Key'], ['Swamp Palace (Center)']), - create_dungeon_region(world, player, 'Swamp Palace (Center)', 'Swamp Palace', ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest', 'Swamp Palace - Hookshot Pot Key', + create_dungeon_region(multiworld, player, 'Swamp Palace (Center)', 'Swamp Palace', ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest', 'Swamp Palace - Hookshot Pot Key', 'Swamp Palace - Trench 2 Pot Key'], ['Swamp Palace (North)', 'Swamp Palace (West)']), - create_dungeon_region(world, player, 'Swamp Palace (West)', 'Swamp Palace', ['Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest']), - create_dungeon_region(world, player, 'Swamp Palace (North)', 'Swamp Palace', ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right', + create_dungeon_region(multiworld, player, 'Swamp Palace (West)', 'Swamp Palace', ['Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest']), + create_dungeon_region(multiworld, player, 'Swamp Palace (North)', 'Swamp Palace', ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right', 'Swamp Palace - Waterway Pot Key', 'Swamp Palace - Waterfall Room', 'Swamp Palace - Boss', 'Swamp Palace - Prize']), - create_dungeon_region(world, player, 'Thieves Town (Entrance)', 'Thieves\' Town', ['Thieves\' Town - Big Key Chest', + create_dungeon_region(multiworld, player, 'Thieves Town (Entrance)', 'Thieves\' Town', ['Thieves\' Town - Big Key Chest', 'Thieves\' Town - Map Chest', 'Thieves\' Town - Compass Chest', 'Thieves\' Town - Ambush Chest'], ['Thieves Town Big Key Door', 'Thieves Town Exit']), - create_dungeon_region(world, player, 'Thieves Town (Deep)', 'Thieves\' Town', ['Thieves\' Town - Attic', + create_dungeon_region(multiworld, player, 'Thieves Town (Deep)', 'Thieves\' Town', ['Thieves\' Town - Attic', 'Thieves\' Town - Big Chest', 'Thieves\' Town - Hallway Pot Key', 'Thieves\' Town - Spike Switch Pot Key', 'Thieves\' Town - Blind\'s Cell'], ['Blind Fight']), - create_dungeon_region(world, player, 'Blind Fight', 'Thieves\' Town', ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']), - create_dungeon_region(world, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'], ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump', 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']), - create_dungeon_region(world, player, 'Skull Woods First Section (Right)', 'Skull Woods', ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']), - create_dungeon_region(world, player, 'Skull Woods First Section (Left)', 'Skull Woods', ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'], ['Skull Woods First Section (Left) Door to Exit', 'Skull Woods First Section (Left) Door to Right']), - create_dungeon_region(world, player, 'Skull Woods First Section (Top)', 'Skull Woods', ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']), - create_dungeon_region(world, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None, ['Skull Woods Second Section (Drop)']), - create_dungeon_region(world, player, 'Skull Woods Second Section', 'Skull Woods', ['Skull Woods - Big Key Chest', 'Skull Woods - West Lobby Pot Key'], ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']), - create_dungeon_region(world, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']), - create_dungeon_region(world, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Spike Corner Key Drop', 'Skull Woods - Boss', 'Skull Woods - Prize']), - create_dungeon_region(world, player, 'Ice Palace (Entrance)', 'Ice Palace', ['Ice Palace - Jelly Key Drop', 'Ice Palace - Compass Chest'], ['Ice Palace (Second Section)', 'Ice Palace Exit']), - create_dungeon_region(world, player, 'Ice Palace (Second Section)', 'Ice Palace', ['Ice Palace - Conveyor Key Drop'], ['Ice Palace (Main)']), - create_dungeon_region(world, player, 'Ice Palace (Main)', 'Ice Palace', ['Ice Palace - Freezor Chest', + create_dungeon_region(multiworld, player, 'Blind Fight', 'Thieves\' Town', ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']), + create_dungeon_region(multiworld, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'], ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump', 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']), + create_dungeon_region(multiworld, player, 'Skull Woods First Section (Right)', 'Skull Woods', ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']), + create_dungeon_region(multiworld, player, 'Skull Woods First Section (Left)', 'Skull Woods', ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'], ['Skull Woods First Section (Left) Door to Exit', 'Skull Woods First Section (Left) Door to Right']), + create_dungeon_region(multiworld, player, 'Skull Woods First Section (Top)', 'Skull Woods', ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']), + create_dungeon_region(multiworld, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None, ['Skull Woods Second Section (Drop)']), + create_dungeon_region(multiworld, player, 'Skull Woods Second Section', 'Skull Woods', ['Skull Woods - Big Key Chest', 'Skull Woods - West Lobby Pot Key'], ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']), + create_dungeon_region(multiworld, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']), + create_dungeon_region(multiworld, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Spike Corner Key Drop', 'Skull Woods - Boss', 'Skull Woods - Prize']), + create_dungeon_region(multiworld, player, 'Ice Palace (Entrance)', 'Ice Palace', ['Ice Palace - Jelly Key Drop', 'Ice Palace - Compass Chest'], ['Ice Palace (Second Section)', 'Ice Palace Exit']), + create_dungeon_region(multiworld, player, 'Ice Palace (Second Section)', 'Ice Palace', ['Ice Palace - Conveyor Key Drop'], ['Ice Palace (Main)']), + create_dungeon_region(multiworld, player, 'Ice Palace (Main)', 'Ice Palace', ['Ice Palace - Freezor Chest', 'Ice Palace - Many Pots Pot Key', 'Ice Palace - Big Chest', 'Ice Palace - Iced T Room'], ['Ice Palace (East)', 'Ice Palace (Kholdstare)']), - create_dungeon_region(world, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'], ['Ice Palace (East Top)']), - create_dungeon_region(world, player, 'Ice Palace (East Top)', 'Ice Palace', ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest', 'Ice Palace - Hammer Block Key Drop']), - create_dungeon_region(world, player, 'Ice Palace (Kholdstare)', 'Ice Palace', ['Ice Palace - Boss', 'Ice Palace - Prize']), - create_dungeon_region(world, player, 'Misery Mire (Entrance)', 'Misery Mire', None, ['Misery Mire Entrance Gap', 'Misery Mire Exit']), - create_dungeon_region(world, player, 'Misery Mire (Main)', 'Misery Mire', ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby', + create_dungeon_region(multiworld, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'], ['Ice Palace (East Top)']), + create_dungeon_region(multiworld, player, 'Ice Palace (East Top)', 'Ice Palace', ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest', 'Ice Palace - Hammer Block Key Drop']), + create_dungeon_region(multiworld, player, 'Ice Palace (Kholdstare)', 'Ice Palace', ['Ice Palace - Boss', 'Ice Palace - Prize']), + create_dungeon_region(multiworld, player, 'Misery Mire (Entrance)', 'Misery Mire', None, ['Misery Mire Entrance Gap', 'Misery Mire Exit']), + create_dungeon_region(multiworld, player, 'Misery Mire (Main)', 'Misery Mire', ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby', 'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest', 'Misery Mire - Spikes Pot Key', 'Misery Mire - Fishbone Pot Key', 'Misery Mire - Conveyor Crystal Key Drop'], ['Misery Mire (West)', 'Misery Mire Big Key Door']), - create_dungeon_region(world, player, 'Misery Mire (West)', 'Misery Mire', ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']), - create_dungeon_region(world, player, 'Misery Mire (Final Area)', 'Misery Mire', None, ['Misery Mire (Vitreous)']), - create_dungeon_region(world, player, 'Misery Mire (Vitreous)', 'Misery Mire', ['Misery Mire - Boss', 'Misery Mire - Prize']), - create_dungeon_region(world, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None, ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']), - create_dungeon_region(world, player, 'Turtle Rock (First Section)', 'Turtle Rock', ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left', + create_dungeon_region(multiworld, player, 'Misery Mire (West)', 'Misery Mire', ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']), + create_dungeon_region(multiworld, player, 'Misery Mire (Final Area)', 'Misery Mire', None, ['Misery Mire (Vitreous)']), + create_dungeon_region(multiworld, player, 'Misery Mire (Vitreous)', 'Misery Mire', ['Misery Mire - Boss', 'Misery Mire - Prize']), + create_dungeon_region(multiworld, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None, ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']), + create_dungeon_region(multiworld, player, 'Turtle Rock (First Section)', 'Turtle Rock', ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left', 'Turtle Rock - Roller Room - Right'], ['Turtle Rock Entrance to Pokey Room', 'Turtle Rock Entrance Gap Reverse']), - create_dungeon_region(world, player, 'Turtle Rock (Pokey Room)', 'Turtle Rock', ['Turtle Rock - Pokey 1 Key Drop'], ['Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Pokey Room) (South)']), - create_dungeon_region(world, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'], + create_dungeon_region(multiworld, player, 'Turtle Rock (Pokey Room)', 'Turtle Rock', ['Turtle Rock - Pokey 1 Key Drop'], ['Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Pokey Room) (South)']), + create_dungeon_region(multiworld, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'], ['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']), - create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock', + create_dungeon_region(multiworld, player, 'Turtle Rock (Second Section)', 'Turtle Rock', ['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'], ['Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door', 'Turtle Rock Second Section Bomb Wall']), - create_dungeon_region(world, player, 'Turtle Rock (Second Section Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Second Section from Bomb Wall']), - create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']), - create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']), - create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']), - create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Isolated Ledge Exit', 'Turtle Rock Eye Bridge from Bomb Wall']), - create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right', + create_dungeon_region(multiworld, player, 'Turtle Rock (Second Section Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Second Section from Bomb Wall']), + create_dungeon_region(multiworld, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']), + create_dungeon_region(multiworld, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']), + create_dungeon_region(multiworld, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']), + create_dungeon_region(multiworld, player, 'Turtle Rock (Eye Bridge Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Isolated Ledge Exit', 'Turtle Rock Eye Bridge from Bomb Wall']), + create_dungeon_region(multiworld, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right', 'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'], ['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Eye Bridge Bomb Wall']), - create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']), - create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']), - create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'], + create_dungeon_region(multiworld, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']), + create_dungeon_region(multiworld, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']), + create_dungeon_region(multiworld, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'], ['Palace of Darkness Big Key Chest Staircase', 'Palace of Darkness (North)', 'Palace of Darkness Big Key Door']), - create_dungeon_region(world, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness', ['Palace of Darkness - Big Key Chest']), - create_dungeon_region(world, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'], ['Palace of Darkness Hammer Peg Drop']), - create_dungeon_region(world, player, 'Palace of Darkness (North)', 'Palace of Darkness', ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right'], + create_dungeon_region(multiworld, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness', ['Palace of Darkness - Big Key Chest']), + create_dungeon_region(multiworld, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'], ['Palace of Darkness Hammer Peg Drop']), + create_dungeon_region(multiworld, player, 'Palace of Darkness (North)', 'Palace of Darkness', ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right'], ['Palace of Darkness Spike Statue Room Door', 'Palace of Darkness Maze Door']), - create_dungeon_region(world, player, 'Palace of Darkness (Maze)', 'Palace of Darkness', ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Big Chest']), - create_dungeon_region(world, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness', ['Palace of Darkness - Harmless Hellway']), - create_dungeon_region(world, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness', ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']), - create_dungeon_region(world, player, 'Inverted Ganons Tower (Entrance)', 'Ganon\'s Tower', + create_dungeon_region(multiworld, player, 'Palace of Darkness (Maze)', 'Palace of Darkness', ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Big Chest']), + create_dungeon_region(multiworld, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness', ['Palace of Darkness - Harmless Hellway']), + create_dungeon_region(multiworld, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness', ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']), + create_dungeon_region(multiworld, player, 'Inverted Ganons Tower (Entrance)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Torch', 'Ganons Tower - Hope Room - Left', 'Ganons Tower - Hope Room - Right', 'Ganons Tower - Conveyor Cross Pot Key'], ['Ganons Tower (Tile Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower Big Key Door', 'Inverted Ganons Tower Exit']), - create_dungeon_region(world, player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'], + create_dungeon_region(multiworld, player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'], ['Ganons Tower (Tile Room) Key Door']), - create_dungeon_region(world, player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower', + create_dungeon_region(multiworld, player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower', ['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right', 'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Bottom Right', 'Ganons Tower - Conveyor Star Pits Pot Key'], ['Ganons Tower (Bottom) (East)']), - create_dungeon_region(world, player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower', + create_dungeon_region(multiworld, player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower', ['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right', 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right', 'Ganons Tower - Double Switch Pot Key'], ['Ganons Tower (Map Room)', 'Ganons Tower (Double Switch Room)']), - create_dungeon_region(world, player, 'Ganons Tower (Map Room)', 'Ganon\'s Tower', ['Ganons Tower - Map Chest']), - create_dungeon_region(world, player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower', + create_dungeon_region(multiworld, player, 'Ganons Tower (Map Room)', 'Ganon\'s Tower', ['Ganons Tower - Map Chest']), + create_dungeon_region(multiworld, player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower', ['Ganons Tower - Firesnake Room'], ['Ganons Tower (Firesnake Room)']), - create_dungeon_region(world, player, 'Ganons Tower (Teleport Room)', 'Ganon\'s Tower', + create_dungeon_region(multiworld, player, 'Ganons Tower (Teleport Room)', 'Ganon\'s Tower', ['Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right', 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right'], ['Ganons Tower (Bottom) (West)']), - create_dungeon_region(world, player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower', + create_dungeon_region(multiworld, player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest', 'Ganons Tower - Big Key Room - Left', 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Chest']), - create_dungeon_region(world, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None, ['Ganons Tower Torch Rooms']), - create_dungeon_region(world, player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower', + create_dungeon_region(multiworld, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None, ['Ganons Tower Torch Rooms']), + create_dungeon_region(multiworld, player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower', ['Ganons Tower - Mini Helmasaur Room - Left', 'Ganons Tower - Mini Helmasaur Room - Right', 'Ganons Tower - Pre-Moldorm Chest', 'Ganons Tower - Mini Helmasaur Key Drop'], ['Ganons Tower Moldorm Door']), - create_dungeon_region(world, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None, ['Ganons Tower Moldorm Gap']), + create_dungeon_region(multiworld, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None, ['Ganons Tower Moldorm Gap']), - create_dungeon_region(world, player, 'Agahnim 2', 'Ganon\'s Tower', ['Ganons Tower - Validation Chest', 'Agahnim 2'], None), - create_cave_region(world, player, 'Pyramid', 'a drop\'s exit', ['Ganon'], ['Ganon Drop']), - create_cave_region(world, player, 'Bottom of Pyramid', 'a drop\'s exit', None, ['Pyramid Exit']), - create_dw_region(world, player, 'Pyramid Ledge', None, ['Pyramid Drop']), # houlihan room exits here in inverted + create_dungeon_region(multiworld, player, 'Agahnim 2', 'Ganon\'s Tower', ['Ganons Tower - Validation Chest', 'Agahnim 2'], None), + create_cave_region(multiworld, player, 'Pyramid', 'a drop\'s exit', ['Ganon'], ['Ganon Drop']), + create_cave_region(multiworld, player, 'Bottom of Pyramid', 'a drop\'s exit', None, ['Pyramid Exit']), + create_dw_region(multiworld, player, 'Pyramid Ledge', None, ['Pyramid Drop']), # houlihan room exits here in inverted # to simplify flute connections - create_cave_region(world, player, 'The Sky', 'A Dark Sky', None, + create_cave_region(multiworld, player, 'The Sky', 'A Dark Sky', None, ['DDM Landing', 'NEDW Landing', 'WDW Landing', 'SDW Landing', 'EDW Landing', 'DD Landing', 'DLHL Landing']), - create_lw_region(world, player, 'Desert Northern Cliffs'), - create_lw_region(world, player, 'Death Mountain Bunny Descent Area') + create_lw_region(multiworld, player, 'Desert Northern Cliffs'), + create_lw_region(multiworld, player, 'Death Mountain Bunny Descent Area') ] -def mark_dark_world_regions(world, player): +def mark_dark_world_regions(multiworld: MultiWorld, player: int): # cross world caves may have some sections marked as both in_light_world, and in_dark_work. # That is ok. the bunny logic will check for this case and incorporate special rules. - queue = collections.deque(region for region in world.get_regions(player) if region.type == LTTPRegionType.DarkWorld) + queue = collections.deque(region for region in multiworld.get_regions(player) if region.type == LTTPRegionType.DarkWorld) seen = set(queue) while queue: current = queue.popleft() @@ -499,7 +500,7 @@ def mark_dark_world_regions(world, player): seen.add(exit.connected_region) queue.append(exit.connected_region) - queue = collections.deque(region for region in world.get_regions(player) if region.type == LTTPRegionType.LightWorld) + queue = collections.deque(region for region in multiworld.get_regions(player) if region.type == LTTPRegionType.LightWorld) seen = set(queue) while queue: current = queue.popleft() diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index 6b0968f6e5..c7dc7a6948 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -1,8 +1,9 @@ from collections import namedtuple import logging -from BaseClasses import ItemClassification +from BaseClasses import ItemClassification, MultiWorld from Options import OptionError +from typing import TYPE_CHECKING from .SubClasses import ALttPLocation, LTTPRegion, LTTPRegionType from .Shops import TakeAny, total_shop_slots, set_up_shops, shop_table_by_location, ShopType @@ -14,6 +15,9 @@ from .Options import small_key_shuffle, compass_shuffle, big_key_shuffle, map_sh from .StateHelpers import has_triforce_pieces, has_melee_weapon from .Regions import key_drop_data +if TYPE_CHECKING: + from . import ALTTPWorld + # This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space. # Some basic items that various modes require are placed here, including pendants and crystals. Medallion requirements for the two relevant entrances are also decided. @@ -222,7 +226,7 @@ items_reduction_table = ( ) -def generate_itempool(world): +def generate_itempool(world: "ALTTPWorld"): player: int = world.player multiworld = world.multiworld @@ -531,7 +535,7 @@ take_any_locations_inverted.sort() take_any_locations.sort() -def set_up_take_anys(multiworld, world, player): +def set_up_take_anys(multiworld: MultiWorld, world: "ALTTPWorld", player: int): # these are references, do not modify these lists in-place if world.options.mode == 'inverted': take_any_locs = take_any_locations_inverted @@ -585,15 +589,15 @@ def set_up_take_anys(multiworld, world, player): location.place_locked_item(item_factory("Boss Heart Container", world)) -def get_pool_core(world, player: int): - shuffle = world.worlds[player].options.entrance_shuffle.current_key - difficulty = world.worlds[player].options.item_pool.current_key - timer = world.worlds[player].options.timer.current_key - goal = world.worlds[player].options.goal.current_key - mode = world.worlds[player].options.mode.current_key - swordless = world.worlds[player].options.swordless - retro_bow = world.worlds[player].options.retro_bow - logic = world.worlds[player].options.glitches_required +def get_pool_core(multiworld: MultiWorld, player: int): + shuffle = multiworld.worlds[player].options.entrance_shuffle.current_key + difficulty = multiworld.worlds[player].options.item_pool.current_key + timer = multiworld.worlds[player].options.timer.current_key + goal = multiworld.worlds[player].options.goal.current_key + mode = multiworld.worlds[player].options.mode.current_key + swordless = multiworld.worlds[player].options.swordless + retro_bow = multiworld.worlds[player].options.retro_bow + logic = multiworld.worlds[player].options.glitches_required pool = [] placed_items = {} @@ -610,13 +614,13 @@ def get_pool_core(world, player: int): placed_items[loc] = item # provide boots to major glitch dependent seeds - if logic.current_key in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and world.worlds[player].options.glitch_boots: + if logic.current_key in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and multiworld.worlds[player].options.glitch_boots: precollected_items.append('Pegasus Boots') pool.remove('Pegasus Boots') pool.append('Rupees (20)') - want_progressives = world.worlds[player].options.progressive.want_progressives + want_progressives = multiworld.worlds[player].options.progressive.want_progressives - if want_progressives(world.random): + if want_progressives(multiworld.random): pool.extend(diff.progressiveglove) else: pool.extend(diff.basicglove) @@ -640,27 +644,27 @@ def get_pool_core(world, player: int): thisbottle = None for _ in range(diff.bottle_count): if not diff.same_bottle or not thisbottle: - thisbottle = world.random.choice(diff.bottles) + thisbottle = multiworld.random.choice(diff.bottles) pool.append(thisbottle) - if want_progressives(world.random): + if want_progressives(multiworld.random): pool.extend(diff.progressiveshield) else: pool.extend(diff.basicshield) - if want_progressives(world.random): + if want_progressives(multiworld.random): pool.extend(diff.progressivearmor) else: pool.extend(diff.basicarmor) - if want_progressives(world.random): + if want_progressives(multiworld.random): pool.extend(diff.progressivemagic) else: pool.extend(diff.basicmagic) - if want_progressives(world.random): + if want_progressives(multiworld.random): pool.extend(diff.progressivebow) - world.worlds[player].has_progressive_bows = True + multiworld.worlds[player].has_progressive_bows = True elif (swordless or logic == 'no_glitches'): swordless_bows = ['Bow', 'Silver Bow'] if difficulty == "easy": @@ -672,7 +676,7 @@ def get_pool_core(world, player: int): if swordless: pool.extend(diff.swordless) else: - progressive_swords = want_progressives(world.random) + progressive_swords = want_progressives(multiworld.random) pool.extend(diff.progressivesword if progressive_swords else diff.basicsword) extraitems = total_items_to_place - len(pool) - len(placed_items) @@ -688,29 +692,29 @@ def get_pool_core(world, player: int): additional_pieces_to_place = 0 if 'triforce_hunt' in goal: - if world.worlds[player].options.triforce_pieces_mode.value == TriforcePiecesMode.option_extra: - treasure_hunt_total = (world.worlds[player].options.triforce_pieces_required.value - + world.worlds[player].options.triforce_pieces_extra.value) - elif world.worlds[player].options.triforce_pieces_mode.value == TriforcePiecesMode.option_percentage: - percentage = float(world.worlds[player].options.triforce_pieces_percentage.value) / 100 - treasure_hunt_total = int(round(world.worlds[player].options.triforce_pieces_required.value * percentage, 0)) + if multiworld.worlds[player].options.triforce_pieces_mode.value == TriforcePiecesMode.option_extra: + treasure_hunt_total = (multiworld.worlds[player].options.triforce_pieces_required.value + + multiworld.worlds[player].options.triforce_pieces_extra.value) + elif multiworld.worlds[player].options.triforce_pieces_mode.value == TriforcePiecesMode.option_percentage: + percentage = float(multiworld.worlds[player].options.triforce_pieces_percentage.value) / 100 + treasure_hunt_total = int(round(multiworld.worlds[player].options.triforce_pieces_required.value * percentage, 0)) else: # available - treasure_hunt_total = world.worlds[player].options.triforce_pieces_available.value + treasure_hunt_total = multiworld.worlds[player].options.triforce_pieces_available.value - triforce_pieces = min(90, max(treasure_hunt_total, world.worlds[player].options.triforce_pieces_required.value)) + triforce_pieces = min(90, max(treasure_hunt_total, multiworld.worlds[player].options.triforce_pieces_required.value)) pieces_in_core = min(extraitems, triforce_pieces) additional_pieces_to_place = triforce_pieces - pieces_in_core pool.extend(["Triforce Piece"] * pieces_in_core) extraitems -= pieces_in_core - treasure_hunt_required = world.worlds[player].options.triforce_pieces_required.value + treasure_hunt_required = multiworld.worlds[player].options.triforce_pieces_required.value for extra in diff.extras: if extraitems >= len(extra): pool.extend(extra) extraitems -= len(extra) elif extraitems > 0: - pool.extend(world.random.sample(extra, extraitems)) + pool.extend(multiworld.random.sample(extra, extraitems)) break else: break @@ -729,25 +733,25 @@ def get_pool_core(world, player: int): else: break - if world.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal: + if multiworld.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal: pool.extend(diff.universal_keys) if mode == 'standard': - if world.worlds[player].options.key_drop_shuffle: + if multiworld.worlds[player].options.key_drop_shuffle: key_locations = ['Secret Passage', 'Hyrule Castle - Map Guard Key Drop'] - key_location = world.random.choice(key_locations) + key_location = multiworld.random.choice(key_locations) key_locations.remove(key_location) place_item(key_location, "Small Key (Universal)") key_locations += ['Hyrule Castle - Boomerang Guard Key Drop', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest'] - key_location = world.random.choice(key_locations) + key_location = multiworld.random.choice(key_locations) key_locations.remove(key_location) place_item(key_location, "Small Key (Universal)") key_locations += ['Hyrule Castle - Big Key Drop', 'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross'] - key_location = world.random.choice(key_locations) + key_location = multiworld.random.choice(key_locations) key_locations.remove(key_location) place_item(key_location, "Small Key (Universal)") key_locations += ['Sewers - Key Rat Key Drop'] - key_location = world.random.choice(key_locations) + key_location = multiworld.random.choice(key_locations) place_item(key_location, "Small Key (Universal)") pool = pool[:-3] diff --git a/worlds/alttp/Items.py b/worlds/alttp/Items.py index cbe6e99642..1c173c619e 100644 --- a/worlds/alttp/Items.py +++ b/worlds/alttp/Items.py @@ -1,24 +1,24 @@ import typing -from BaseClasses import ItemClassification as IC +from BaseClasses import MultiWorld, ItemClassification as IC from worlds.AutoWorld import World -def GetBeemizerItem(world, player: int, item): +def GetBeemizerItem(multiworld: MultiWorld, player: int, item): item_name = item if isinstance(item, str) else item.name - if item_name not in trap_replaceable or player in world.groups: + if item_name not in trap_replaceable or player in multiworld.groups: return item # first roll - replaceable item should be replaced, within beemizer_total_chance - if not world.worlds[player].options.beemizer_total_chance or world.random.random() > (world.worlds[player].options.beemizer_total_chance / 100): + if not multiworld.worlds[player].options.beemizer_total_chance or multiworld.random.random() > (multiworld.worlds[player].options.beemizer_total_chance / 100): return item # second roll - bee replacement should be trap, within beemizer_trap_chance - if not world.worlds[player].options.beemizer_trap_chance or world.random.random() > (world.worlds[player].options.beemizer_trap_chance / 100): - return "Bee" if isinstance(item, str) else world.create_item("Bee", player) + if not multiworld.worlds[player].options.beemizer_trap_chance or multiworld.random.random() > (multiworld.worlds[player].options.beemizer_trap_chance / 100): + return "Bee" if isinstance(item, str) else multiworld.create_item("Bee", player) else: - return "Bee Trap" if isinstance(item, str) else world.create_item("Bee Trap", player) + return "Bee Trap" if isinstance(item, str) else multiworld.create_item("Bee Trap", player) def item_factory(items: typing.Union[str, typing.Iterable[str]], world: World): diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index 519241d7f4..53bdbd6cba 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -154,13 +154,13 @@ class OpenPyramid(Choice): alias_true = option_open alias_false = option_closed - def to_bool(self, world: MultiWorld, player: int) -> bool: + def to_bool(self, multiworld: MultiWorld, player: int) -> bool: if self.value == self.option_goal: - return world.worlds[player].options.goal.current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'} + return multiworld.worlds[player].options.goal.current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'} elif self.value == self.option_auto: - return world.worlds[player].options.goal.current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'} \ - and (world.worlds[player].options.entrance_shuffle.current_key in {'vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed'} or not - world.shuffle_ganon) + return multiworld.worlds[player].options.goal.current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'} \ + and (multiworld.worlds[player].options.entrance_shuffle.current_key in {'vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed'} or not + multiworld.shuffle_ganon) elif self.value == self.option_open: return True else: diff --git a/worlds/alttp/OverworldGlitchRules.py b/worlds/alttp/OverworldGlitchRules.py index aeff9cb88e..a243569cca 100644 --- a/worlds/alttp/OverworldGlitchRules.py +++ b/worlds/alttp/OverworldGlitchRules.py @@ -2,6 +2,7 @@ Helper functions to deliver entrance/exit/region sets to OWG rules. """ +from BaseClasses import MultiWorld from .StateHelpers import can_lift_heavy_rocks, can_boots_clip_lw, can_boots_clip_dw, can_get_glitched_speed_dw @@ -200,7 +201,7 @@ def get_mirror_offset_spots_dw(): yield ('Dark Death Mountain Offset Mirror', 'Dark Death Mountain (West Bottom)', 'East Dark World') -def get_mirror_offset_spots_lw(player): +def get_mirror_offset_spots_lw(player: int): """ Mirror shenanigans placing a mirror portal with a broken camera """ @@ -218,54 +219,54 @@ def get_invalid_bunny_revival_dungeons(): yield 'Sanctuary' -def overworld_glitch_connections(world, player): +def overworld_glitch_connections(multiworld: MultiWorld, player: int): # Boots-accessible locations. - create_owg_connections(player, world, get_boots_clip_exits_lw(world.worlds[player].options.mode == 'inverted')) - create_owg_connections(player, world, get_boots_clip_exits_dw(world.worlds[player].options.mode == 'inverted', player)) + create_owg_connections(player, multiworld, get_boots_clip_exits_lw(multiworld.worlds[player].options.mode == 'inverted')) + create_owg_connections(player, multiworld, get_boots_clip_exits_dw(multiworld.worlds[player].options.mode == 'inverted', player)) # Glitched speed drops. - create_owg_connections(player, world, get_glitched_speed_drops_dw(world.worlds[player].options.mode == 'inverted')) + create_owg_connections(player, multiworld, get_glitched_speed_drops_dw(multiworld.worlds[player].options.mode == 'inverted')) # Mirror clip spots. - if world.worlds[player].options.mode != 'inverted': - create_owg_connections(player, world, get_mirror_clip_spots_dw()) - create_owg_connections(player, world, get_mirror_offset_spots_dw()) + if multiworld.worlds[player].options.mode != 'inverted': + create_owg_connections(player, multiworld, get_mirror_clip_spots_dw()) + create_owg_connections(player, multiworld, get_mirror_offset_spots_dw()) else: - create_owg_connections(player, world, get_mirror_offset_spots_lw(player)) + create_owg_connections(player, multiworld, get_mirror_offset_spots_lw(player)) -def overworld_glitches_rules(world, player): +def overworld_glitches_rules(multiworld: MultiWorld, player: int): # Boots-accessible locations. - set_owg_connection_rules(player, world, get_boots_clip_exits_lw(world.worlds[player].options.mode == 'inverted'), lambda state: can_boots_clip_lw(state, player)) - set_owg_connection_rules(player, world, get_boots_clip_exits_dw(world.worlds[player].options.mode == 'inverted', player), lambda state: can_boots_clip_dw(state, player)) + set_owg_connection_rules(player, multiworld, get_boots_clip_exits_lw(multiworld.worlds[player].options.mode == 'inverted'), lambda state: can_boots_clip_lw(state, player)) + set_owg_connection_rules(player, multiworld, get_boots_clip_exits_dw(multiworld.worlds[player].options.mode == 'inverted', player), lambda state: can_boots_clip_dw(state, player)) # Glitched speed drops. - set_owg_connection_rules(player, world, get_glitched_speed_drops_dw(world.worlds[player].options.mode == 'inverted'), lambda state: can_get_glitched_speed_dw(state, player)) + set_owg_connection_rules(player, multiworld, get_glitched_speed_drops_dw(multiworld.worlds[player].options.mode == 'inverted'), lambda state: can_get_glitched_speed_dw(state, player)) # Dark Death Mountain Ledge Clip Spot also accessible with mirror. - if world.worlds[player].options.mode != 'inverted': - add_alternate_rule(world.get_entrance('Dark Death Mountain Ledge Clip Spot', player), lambda state: state.has('Magic Mirror', player)) + if multiworld.worlds[player].options.mode != 'inverted': + add_alternate_rule(multiworld.get_entrance('Dark Death Mountain Ledge Clip Spot', player), lambda state: state.has('Magic Mirror', player)) # Mirror clip spots. - if world.worlds[player].options.mode != 'inverted': - set_owg_connection_rules(player, world, get_mirror_clip_spots_dw(), lambda state: state.has('Magic Mirror', player)) - set_owg_connection_rules(player, world, get_mirror_offset_spots_dw(), lambda state: state.has('Magic Mirror', player) and can_boots_clip_lw(state, player)) + if multiworld.worlds[player].options.mode != 'inverted': + set_owg_connection_rules(player, multiworld, get_mirror_clip_spots_dw(), lambda state: state.has('Magic Mirror', player)) + set_owg_connection_rules(player, multiworld, get_mirror_offset_spots_dw(), lambda state: state.has('Magic Mirror', player) and can_boots_clip_lw(state, player)) else: - set_owg_connection_rules(player, world, get_mirror_offset_spots_lw(player), lambda state: state.has('Magic Mirror', player) and can_boots_clip_dw(state, player)) + set_owg_connection_rules(player, multiworld, get_mirror_offset_spots_lw(player), lambda state: state.has('Magic Mirror', player) and can_boots_clip_dw(state, player)) # Regions that require the boots and some other stuff. - if world.worlds[player].options.mode != 'inverted': - world.get_entrance('Turtle Rock Teleporter', player).access_rule = lambda state: (can_boots_clip_lw(state, player) or can_lift_heavy_rocks(state, player)) and state.has('Hammer', player) - add_alternate_rule(world.get_entrance('Waterfall of Wishing', player), lambda state: state.has('Moon Pearl', player) or state.has('Pegasus Boots', player)) + if multiworld.worlds[player].options.mode != 'inverted': + multiworld.get_entrance('Turtle Rock Teleporter', player).access_rule = lambda state: (can_boots_clip_lw(state, player) or can_lift_heavy_rocks(state, player)) and state.has('Hammer', player) + add_alternate_rule(multiworld.get_entrance('Waterfall of Wishing', player), lambda state: state.has('Moon Pearl', player) or state.has('Pegasus Boots', player)) else: - add_alternate_rule(world.get_entrance('Waterfall of Wishing Cave', player), lambda state: state.has('Moon Pearl', player)) + add_alternate_rule(multiworld.get_entrance('Waterfall of Wishing Cave', player), lambda state: state.has('Moon Pearl', player)) - world.get_entrance('Dark Desert Teleporter', player).access_rule = lambda state: (state.has('Flute', player) or state.has('Pegasus Boots', player)) and can_lift_heavy_rocks(state, player) - add_alternate_rule(world.get_entrance('Catfish Exit Rock', player), lambda state: can_boots_clip_dw(state, player)) - add_alternate_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: can_boots_clip_dw(state, player)) + multiworld.get_entrance('Dark Desert Teleporter', player).access_rule = lambda state: (state.has('Flute', player) or state.has('Pegasus Boots', player)) and can_lift_heavy_rocks(state, player) + add_alternate_rule(multiworld.get_entrance('Catfish Exit Rock', player), lambda state: can_boots_clip_dw(state, player)) + add_alternate_rule(multiworld.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: can_boots_clip_dw(state, player)) # Zora's Ledge via waterwalk setup. - add_alternate_rule(world.get_location('Zora\'s Ledge', player), lambda state: state.has('Pegasus Boots', player)) + add_alternate_rule(multiworld.get_location('Zora\'s Ledge', player), lambda state: state.has('Pegasus Boots', player)) def add_alternate_rule(entrance, rule): @@ -273,22 +274,22 @@ def add_alternate_rule(entrance, rule): entrance.access_rule = lambda state: old_rule(state) or rule(state) -def create_no_logic_connections(player, world, connections): +def create_no_logic_connections(player: int, multiworld: MultiWorld, connections): for entrance, parent_region, target_region, *rule_override in connections: - parent = world.get_region(parent_region, player) - target = world.get_region(target_region, player) + parent = multiworld.get_region(parent_region, player) + target = multiworld.get_region(target_region, player) parent.connect(target, entrance) -def create_owg_connections(player, world, connections): +def create_owg_connections(player: int, multiworld: MultiWorld, connections): for entrance, parent_region, target_region, *rule_override in connections: - parent = world.get_region(parent_region, player) - target = world.get_region(target_region, player) + parent = multiworld.get_region(parent_region, player) + target = multiworld.get_region(target_region, player) parent.connect(target, entrance) -def set_owg_connection_rules(player, world, connections, default_rule): +def set_owg_connection_rules(player: int, multiworld: MultiWorld, connections, default_rule): for entrance, _, _, *rule_override in connections: - connection = world.get_entrance(entrance, player) + connection = multiworld.get_entrance(entrance, player) rule = rule_override[0] if len(rule_override) > 0 else default_rule connection.access_rule = rule diff --git a/worlds/alttp/Regions.py b/worlds/alttp/Regions.py index c2af795637..d3789f91ba 100644 --- a/worlds/alttp/Regions.py +++ b/worlds/alttp/Regions.py @@ -9,11 +9,11 @@ def is_main_entrance(entrance: LTTPEntrance) -> bool: return entrance.parent_region.type in {LTTPRegionType.DarkWorld, LTTPRegionType.LightWorld} if entrance.parent_region.type else True -def create_regions(world, player): +def create_regions(multiworld: MultiWorld, player: int): - world.regions += [ - create_lw_region(world, player, 'Menu', None, ['Links House S&Q', 'Sanctuary S&Q', 'Old Man S&Q']), - create_lw_region(world, player, 'Light World', ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure', + multiworld.regions += [ + create_lw_region(multiworld, player, 'Menu', None, ['Links House S&Q', 'Sanctuary S&Q', 'Old Man S&Q']), + create_lw_region(multiworld, player, 'Light World', ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure', 'Purple Chest', 'Flute Activation Spot'], ["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Zoras River', 'Kings Grave Outer Rocks', 'Dam', 'Links House', 'Tavern North', 'Chicken House', 'Aginahs Cave', 'Sahasrahlas Hut', 'Kakariko Well Drop', 'Kakariko Well Cave', @@ -24,122 +24,122 @@ def create_regions(world, player): 'Elder House (East)', 'Elder House (West)', 'North Fairy Cave', 'North Fairy Cave Drop', 'Lost Woods Gamble', 'Snitch Lady (East)', 'Snitch Lady (West)', 'Tavern (Front)', 'Bush Covered House', 'Light World Bomb Hut', 'Kakariko Shop', 'Long Fairy Cave', 'Good Bee Cave', '20 Rupee Cave', 'Cave Shop (Lake Hylia)', 'Waterfall of Wishing', 'Hyrule Castle Main Gate', 'Bonk Fairy (Light)', '50 Rupee Cave', 'Fortune Teller (Light)', 'Lake Hylia Fairy', 'Light Hype Fairy', 'Desert Fairy', 'Lumberjack House', 'Lake Hylia Fortune Teller', 'Kakariko Gamble Game', 'Top of Pyramid']), - create_lw_region(world, player, 'Death Mountain Entrance', None, ['Old Man Cave (West)', 'Death Mountain Entrance Drop']), - create_lw_region(world, player, 'Lake Hylia Central Island', None, ['Capacity Upgrade', 'Lake Hylia Central Island Teleporter']), - create_cave_region(world, player, 'Blinds Hideout', 'a bounty of five items', ["Blind\'s Hideout - Top", + create_lw_region(multiworld, player, 'Death Mountain Entrance', None, ['Old Man Cave (West)', 'Death Mountain Entrance Drop']), + create_lw_region(multiworld, player, 'Lake Hylia Central Island', None, ['Capacity Upgrade', 'Lake Hylia Central Island Teleporter']), + create_cave_region(multiworld, player, 'Blinds Hideout', 'a bounty of five items', ["Blind\'s Hideout - Top", "Blind\'s Hideout - Left", "Blind\'s Hideout - Right", "Blind\'s Hideout - Far Left", "Blind\'s Hideout - Far Right"]), - create_cave_region(world, player, 'Hyrule Castle Secret Entrance', 'a drop\'s exit', ['Link\'s Uncle', 'Secret Passage'], ['Hyrule Castle Secret Entrance Exit']), - create_lw_region(world, player, 'Zoras River', ['King Zora', 'Zora\'s Ledge']), - create_cave_region(world, player, 'Waterfall of Wishing', 'a cave with two chests', ['Waterfall Fairy - Left', 'Waterfall Fairy - Right']), - create_lw_region(world, player, 'Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']), - create_cave_region(world, player, 'Kings Grave', 'a cave with a chest', ['King\'s Tomb']), - create_cave_region(world, player, 'North Fairy Cave', 'a drop\'s exit', None, ['North Fairy Cave Exit']), - create_cave_region(world, player, 'Dam', 'the dam', ['Floodgate', 'Floodgate Chest']), - create_cave_region(world, player, 'Links House', 'your house', ['Link\'s House'], ['Links House Exit']), - create_cave_region(world, player, 'Chris Houlihan Room', 'I AM ERROR', None, ['Chris Houlihan Room Exit']), - create_cave_region(world, player, 'Tavern', 'the tavern', ['Kakariko Tavern']), - create_cave_region(world, player, 'Elder House', 'a connector', None, ['Elder House Exit (East)', 'Elder House Exit (West)']), - create_cave_region(world, player, 'Snitch Lady (East)', 'a boring house'), - create_cave_region(world, player, 'Snitch Lady (West)', 'a boring house'), - create_cave_region(world, player, 'Bush Covered House', 'the grass man'), - create_cave_region(world, player, 'Tavern (Front)', 'the tavern'), - create_cave_region(world, player, 'Light World Bomb Hut', 'a restock room'), - create_cave_region(world, player, 'Kakariko Shop', 'a common shop'), - create_cave_region(world, player, 'Fortune Teller (Light)', 'a fortune teller'), - create_cave_region(world, player, 'Lake Hylia Fortune Teller', 'a fortune teller'), - create_cave_region(world, player, 'Lumberjack House', 'a boring house'), - create_cave_region(world, player, 'Bonk Fairy (Light)', 'a fairy fountain'), - create_cave_region(world, player, 'Bonk Fairy (Dark)', 'a fairy fountain'), - create_cave_region(world, player, 'Lake Hylia Healer Fairy', 'a fairy fountain'), - create_cave_region(world, player, 'Swamp Healer Fairy', 'a fairy fountain'), - create_cave_region(world, player, 'Desert Healer Fairy', 'a fairy fountain'), - create_cave_region(world, player, 'Dark Lake Hylia Healer Fairy', 'a fairy fountain'), - create_cave_region(world, player, 'Dark Lake Hylia Ledge Healer Fairy', 'a fairy fountain'), - create_cave_region(world, player, 'Dark Desert Healer Fairy', 'a fairy fountain'), - create_cave_region(world, player, 'Dark Death Mountain Healer Fairy', 'a fairy fountain'), - create_cave_region(world, player, 'Chicken House', 'a house with a chest', ['Chicken House']), - create_cave_region(world, player, 'Aginahs Cave', 'a cave with a chest', ['Aginah\'s Cave']), - create_cave_region(world, player, 'Sahasrahlas Hut', 'Sahasrahla', ['Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right', 'Sahasrahla']), - create_cave_region(world, player, 'Kakariko Well (top)', 'a drop\'s exit', ['Kakariko Well - Top', 'Kakariko Well - Left', 'Kakariko Well - Middle', + create_cave_region(multiworld, player, 'Hyrule Castle Secret Entrance', 'a drop\'s exit', ['Link\'s Uncle', 'Secret Passage'], ['Hyrule Castle Secret Entrance Exit']), + create_lw_region(multiworld, player, 'Zoras River', ['King Zora', 'Zora\'s Ledge']), + create_cave_region(multiworld, player, 'Waterfall of Wishing', 'a cave with two chests', ['Waterfall Fairy - Left', 'Waterfall Fairy - Right']), + create_lw_region(multiworld, player, 'Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']), + create_cave_region(multiworld, player, 'Kings Grave', 'a cave with a chest', ['King\'s Tomb']), + create_cave_region(multiworld, player, 'North Fairy Cave', 'a drop\'s exit', None, ['North Fairy Cave Exit']), + create_cave_region(multiworld, player, 'Dam', 'the dam', ['Floodgate', 'Floodgate Chest']), + create_cave_region(multiworld, player, 'Links House', 'your house', ['Link\'s House'], ['Links House Exit']), + create_cave_region(multiworld, player, 'Chris Houlihan Room', 'I AM ERROR', None, ['Chris Houlihan Room Exit']), + create_cave_region(multiworld, player, 'Tavern', 'the tavern', ['Kakariko Tavern']), + create_cave_region(multiworld, player, 'Elder House', 'a connector', None, ['Elder House Exit (East)', 'Elder House Exit (West)']), + create_cave_region(multiworld, player, 'Snitch Lady (East)', 'a boring house'), + create_cave_region(multiworld, player, 'Snitch Lady (West)', 'a boring house'), + create_cave_region(multiworld, player, 'Bush Covered House', 'the grass man'), + create_cave_region(multiworld, player, 'Tavern (Front)', 'the tavern'), + create_cave_region(multiworld, player, 'Light World Bomb Hut', 'a restock room'), + create_cave_region(multiworld, player, 'Kakariko Shop', 'a common shop'), + create_cave_region(multiworld, player, 'Fortune Teller (Light)', 'a fortune teller'), + create_cave_region(multiworld, player, 'Lake Hylia Fortune Teller', 'a fortune teller'), + create_cave_region(multiworld, player, 'Lumberjack House', 'a boring house'), + create_cave_region(multiworld, player, 'Bonk Fairy (Light)', 'a fairy fountain'), + create_cave_region(multiworld, player, 'Bonk Fairy (Dark)', 'a fairy fountain'), + create_cave_region(multiworld, player, 'Lake Hylia Healer Fairy', 'a fairy fountain'), + create_cave_region(multiworld, player, 'Swamp Healer Fairy', 'a fairy fountain'), + create_cave_region(multiworld, player, 'Desert Healer Fairy', 'a fairy fountain'), + create_cave_region(multiworld, player, 'Dark Lake Hylia Healer Fairy', 'a fairy fountain'), + create_cave_region(multiworld, player, 'Dark Lake Hylia Ledge Healer Fairy', 'a fairy fountain'), + create_cave_region(multiworld, player, 'Dark Desert Healer Fairy', 'a fairy fountain'), + create_cave_region(multiworld, player, 'Dark Death Mountain Healer Fairy', 'a fairy fountain'), + create_cave_region(multiworld, player, 'Chicken House', 'a house with a chest', ['Chicken House']), + create_cave_region(multiworld, player, 'Aginahs Cave', 'a cave with a chest', ['Aginah\'s Cave']), + create_cave_region(multiworld, player, 'Sahasrahlas Hut', 'Sahasrahla', ['Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right', 'Sahasrahla']), + create_cave_region(multiworld, player, 'Kakariko Well (top)', 'a drop\'s exit', ['Kakariko Well - Top', 'Kakariko Well - Left', 'Kakariko Well - Middle', 'Kakariko Well - Right', 'Kakariko Well - Bottom'], ['Kakariko Well (top to bottom)']), - create_cave_region(world, player, 'Kakariko Well (bottom)', 'a drop\'s exit', None, ['Kakariko Well Exit']), - create_cave_region(world, player, 'Blacksmiths Hut', 'the smith', ['Blacksmith', 'Missing Smith']), - create_lw_region(world, player, 'Bat Cave Drop Ledge', None, ['Bat Cave Drop']), - create_cave_region(world, player, 'Bat Cave (right)', 'a drop\'s exit', ['Magic Bat'], ['Bat Cave Door']), - create_cave_region(world, player, 'Bat Cave (left)', 'a drop\'s exit', None, ['Bat Cave Exit']), - create_cave_region(world, player, 'Sick Kids House', 'the sick kid', ['Sick Kid']), - create_lw_region(world, player, 'Hobo Bridge', ['Hobo']), - create_cave_region(world, player, 'Lost Woods Hideout (top)', 'a drop\'s exit', ['Lost Woods Hideout'], ['Lost Woods Hideout (top to bottom)']), - create_cave_region(world, player, 'Lost Woods Hideout (bottom)', 'a drop\'s exit', None, ['Lost Woods Hideout Exit']), - create_cave_region(world, player, 'Lumberjack Tree (top)', 'a drop\'s exit', ['Lumberjack Tree'], ['Lumberjack Tree (top to bottom)']), - create_cave_region(world, player, 'Lumberjack Tree (bottom)', 'a drop\'s exit', None, ['Lumberjack Tree Exit']), - create_lw_region(world, player, 'Cave 45 Ledge', None, ['Cave 45']), - create_cave_region(world, player, 'Cave 45', 'a cave with an item', ['Cave 45']), - create_lw_region(world, player, 'Graveyard Ledge', None, ['Graveyard Cave']), - create_cave_region(world, player, 'Graveyard Cave', 'a cave with an item', ['Graveyard Cave']), - create_cave_region(world, player, 'Checkerboard Cave', 'a cave with an item', ['Checkerboard Cave']), - create_cave_region(world, player, 'Long Fairy Cave', 'a fairy fountain'), - create_cave_region(world, player, 'Mini Moldorm Cave', 'a bounty of five items', ['Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right', + create_cave_region(multiworld, player, 'Kakariko Well (bottom)', 'a drop\'s exit', None, ['Kakariko Well Exit']), + create_cave_region(multiworld, player, 'Blacksmiths Hut', 'the smith', ['Blacksmith', 'Missing Smith']), + create_lw_region(multiworld, player, 'Bat Cave Drop Ledge', None, ['Bat Cave Drop']), + create_cave_region(multiworld, player, 'Bat Cave (right)', 'a drop\'s exit', ['Magic Bat'], ['Bat Cave Door']), + create_cave_region(multiworld, player, 'Bat Cave (left)', 'a drop\'s exit', None, ['Bat Cave Exit']), + create_cave_region(multiworld, player, 'Sick Kids House', 'the sick kid', ['Sick Kid']), + create_lw_region(multiworld, player, 'Hobo Bridge', ['Hobo']), + create_cave_region(multiworld, player, 'Lost Woods Hideout (top)', 'a drop\'s exit', ['Lost Woods Hideout'], ['Lost Woods Hideout (top to bottom)']), + create_cave_region(multiworld, player, 'Lost Woods Hideout (bottom)', 'a drop\'s exit', None, ['Lost Woods Hideout Exit']), + create_cave_region(multiworld, player, 'Lumberjack Tree (top)', 'a drop\'s exit', ['Lumberjack Tree'], ['Lumberjack Tree (top to bottom)']), + create_cave_region(multiworld, player, 'Lumberjack Tree (bottom)', 'a drop\'s exit', None, ['Lumberjack Tree Exit']), + create_lw_region(multiworld, player, 'Cave 45 Ledge', None, ['Cave 45']), + create_cave_region(multiworld, player, 'Cave 45', 'a cave with an item', ['Cave 45']), + create_lw_region(multiworld, player, 'Graveyard Ledge', None, ['Graveyard Cave']), + create_cave_region(multiworld, player, 'Graveyard Cave', 'a cave with an item', ['Graveyard Cave']), + create_cave_region(multiworld, player, 'Checkerboard Cave', 'a cave with an item', ['Checkerboard Cave']), + create_cave_region(multiworld, player, 'Long Fairy Cave', 'a fairy fountain'), + create_cave_region(multiworld, player, 'Mini Moldorm Cave', 'a bounty of five items', ['Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right', 'Mini Moldorm Cave - Far Right', 'Mini Moldorm Cave - Generous Guy']), - create_cave_region(world, player, 'Ice Rod Cave', 'a cave with a chest', ['Ice Rod Cave']), - create_cave_region(world, player, 'Good Bee Cave', 'a cold bee'), - create_cave_region(world, player, '20 Rupee Cave', 'a cave with some cash'), - create_cave_region(world, player, 'Cave Shop (Lake Hylia)', 'a common shop'), - create_cave_region(world, player, 'Cave Shop (Dark Death Mountain)', 'a common shop'), - create_cave_region(world, player, 'Bonk Rock Cave', 'a cave with a chest', ['Bonk Rock Cave']), - create_cave_region(world, player, 'Library', 'the library', ['Library']), - create_cave_region(world, player, 'Kakariko Gamble Game', 'a game of chance'), - create_cave_region(world, player, 'Potion Shop', 'the potion shop', ['Potion Shop']), - create_lw_region(world, player, 'Lake Hylia Island', ['Lake Hylia Island']), - create_cave_region(world, player, 'Capacity Upgrade', 'the queen of fairies', ['Capacity Upgrade Shop']), - create_cave_region(world, player, 'Two Brothers House', 'a connector', None, ['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)']), - create_lw_region(world, player, 'Maze Race Ledge', ['Maze Race'], ['Two Brothers House (West)']), - create_cave_region(world, player, '50 Rupee Cave', 'a cave with some cash'), - create_lw_region(world, player, 'Desert Ledge', ['Desert Ledge'], ['Desert Palace Entrance (North) Rocks', 'Desert Palace Entrance (West)']), - create_lw_region(world, player, 'Desert Ledge (Northeast)', None, ['Checkerboard Cave']), - create_lw_region(world, player, 'Desert Palace Stairs', None, ['Desert Palace Entrance (South)']), - create_lw_region(world, player, 'Desert Palace Lone Stairs', None, ['Desert Palace Stairs Drop', 'Desert Palace Entrance (East)']), - create_lw_region(world, player, 'Desert Palace Entrance (North) Spot', None, ['Desert Palace Entrance (North)', 'Desert Ledge Return Rocks']), - create_dungeon_region(world, player, 'Desert Palace Main (Outer)', 'Desert Palace', ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'], + create_cave_region(multiworld, player, 'Ice Rod Cave', 'a cave with a chest', ['Ice Rod Cave']), + create_cave_region(multiworld, player, 'Good Bee Cave', 'a cold bee'), + create_cave_region(multiworld, player, '20 Rupee Cave', 'a cave with some cash'), + create_cave_region(multiworld, player, 'Cave Shop (Lake Hylia)', 'a common shop'), + create_cave_region(multiworld, player, 'Cave Shop (Dark Death Mountain)', 'a common shop'), + create_cave_region(multiworld, player, 'Bonk Rock Cave', 'a cave with a chest', ['Bonk Rock Cave']), + create_cave_region(multiworld, player, 'Library', 'the library', ['Library']), + create_cave_region(multiworld, player, 'Kakariko Gamble Game', 'a game of chance'), + create_cave_region(multiworld, player, 'Potion Shop', 'the potion shop', ['Potion Shop']), + create_lw_region(multiworld, player, 'Lake Hylia Island', ['Lake Hylia Island']), + create_cave_region(multiworld, player, 'Capacity Upgrade', 'the queen of fairies', ['Capacity Upgrade Shop']), + create_cave_region(multiworld, player, 'Two Brothers House', 'a connector', None, ['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)']), + create_lw_region(multiworld, player, 'Maze Race Ledge', ['Maze Race'], ['Two Brothers House (West)']), + create_cave_region(multiworld, player, '50 Rupee Cave', 'a cave with some cash'), + create_lw_region(multiworld, player, 'Desert Ledge', ['Desert Ledge'], ['Desert Palace Entrance (North) Rocks', 'Desert Palace Entrance (West)']), + create_lw_region(multiworld, player, 'Desert Ledge (Northeast)', None, ['Checkerboard Cave']), + create_lw_region(multiworld, player, 'Desert Palace Stairs', None, ['Desert Palace Entrance (South)']), + create_lw_region(multiworld, player, 'Desert Palace Lone Stairs', None, ['Desert Palace Stairs Drop', 'Desert Palace Entrance (East)']), + create_lw_region(multiworld, player, 'Desert Palace Entrance (North) Spot', None, ['Desert Palace Entrance (North)', 'Desert Ledge Return Rocks']), + create_dungeon_region(multiworld, player, 'Desert Palace Main (Outer)', 'Desert Palace', ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'], ['Desert Palace Pots (Outer)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)', 'Desert Palace East Wing']), - create_dungeon_region(world, player, 'Desert Palace Main (Inner)', 'Desert Palace', None, ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']), - create_dungeon_region(world, player, 'Desert Palace East', 'Desert Palace', ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']), - create_dungeon_region(world, player, 'Desert Palace North', 'Desert Palace', ['Desert Palace - Desert Tiles 1 Pot Key', 'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key', + create_dungeon_region(multiworld, player, 'Desert Palace Main (Inner)', 'Desert Palace', None, ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']), + create_dungeon_region(multiworld, player, 'Desert Palace East', 'Desert Palace', ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']), + create_dungeon_region(multiworld, player, 'Desert Palace North', 'Desert Palace', ['Desert Palace - Desert Tiles 1 Pot Key', 'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key', 'Desert Palace - Boss', 'Desert Palace - Prize'], ['Desert Palace Exit (North)']), - create_dungeon_region(world, player, 'Eastern Palace', 'Eastern Palace', ['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest', 'Eastern Palace - Cannonball Chest', + create_dungeon_region(multiworld, player, 'Eastern Palace', 'Eastern Palace', ['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest', 'Eastern Palace - Cannonball Chest', 'Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop', 'Eastern Palace - Big Key Chest', 'Eastern Palace - Map Chest', 'Eastern Palace - Boss', 'Eastern Palace - Prize'], ['Eastern Palace Exit']), - create_lw_region(world, player, 'Master Sword Meadow', ['Master Sword Pedestal']), - create_cave_region(world, player, 'Lost Woods Gamble', 'a game of chance'), - create_lw_region(world, player, 'Hyrule Castle Courtyard', None, ['Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Entrance (South)']), - create_lw_region(world, player, 'Hyrule Castle Ledge', None, ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Agahnims Tower', 'Hyrule Castle Ledge Courtyard Drop']), - create_dungeon_region(world, player, 'Hyrule Castle', 'Hyrule Castle', ['Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', 'Hyrule Castle - Zelda\'s Chest', + create_lw_region(multiworld, player, 'Master Sword Meadow', ['Master Sword Pedestal']), + create_cave_region(multiworld, player, 'Lost Woods Gamble', 'a game of chance'), + create_lw_region(multiworld, player, 'Hyrule Castle Courtyard', None, ['Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Entrance (South)']), + create_lw_region(multiworld, player, 'Hyrule Castle Ledge', None, ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Agahnims Tower', 'Hyrule Castle Ledge Courtyard Drop']), + create_dungeon_region(multiworld, player, 'Hyrule Castle', 'Hyrule Castle', ['Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', 'Hyrule Castle - Zelda\'s Chest', 'Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop', 'Hyrule Castle - Big Key Drop'], ['Hyrule Castle Exit (East)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (South)', 'Throne Room']), - create_dungeon_region(world, player, 'Sewer Drop', 'a drop\'s exit', None, ['Sewer Drop']), # This exists only to be referenced for access checks - create_dungeon_region(world, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross', 'Sewers - Key Rat Key Drop'], ['Sewers Door']), - create_dungeon_region(world, player, 'Sewers', 'a drop\'s exit', None, ['Sanctuary Push Door', 'Sewers Back Door', 'Sewers Secret Room']), - create_dungeon_region(world, player, 'Sewers Secret Room', 'a drop\'s exit', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle', + create_dungeon_region(multiworld, player, 'Sewer Drop', 'a drop\'s exit', None, ['Sewer Drop']), # This exists only to be referenced for access checks + create_dungeon_region(multiworld, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross', 'Sewers - Key Rat Key Drop'], ['Sewers Door']), + create_dungeon_region(multiworld, player, 'Sewers', 'a drop\'s exit', None, ['Sanctuary Push Door', 'Sewers Back Door', 'Sewers Secret Room']), + create_dungeon_region(multiworld, player, 'Sewers Secret Room', 'a drop\'s exit', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle', 'Sewers - Secret Room - Right']), - create_dungeon_region(world, player, 'Sanctuary', 'a drop\'s exit', ['Sanctuary'], ['Sanctuary Exit']), - create_dungeon_region(world, player, 'Agahnims Tower', 'Castle Tower', ['Castle Tower - Room 03', 'Castle Tower - Dark Maze', 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'], ['Agahnim 1', 'Agahnims Tower Exit']), - create_dungeon_region(world, player, 'Agahnim 1', 'Castle Tower', ['Agahnim 1'], None), - create_cave_region(world, player, 'Old Man Cave', 'a connector', ['Old Man'], ['Old Man Cave Exit (East)', 'Old Man Cave Exit (West)']), - create_cave_region(world, player, 'Old Man House', 'a connector', None, ['Old Man House Exit (Bottom)', 'Old Man House Front to Back']), - create_cave_region(world, player, 'Old Man House Back', 'a connector', None, ['Old Man House Exit (Top)', 'Old Man House Back to Front']), - create_lw_region(world, player, 'Death Mountain', None, ['Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)', 'Death Mountain Return Cave (East)', 'Spectacle Rock Cave', 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)', 'Broken Bridge (West)', 'Death Mountain Teleporter']), - create_cave_region(world, player, 'Death Mountain Return Cave', 'a connector', None, ['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)']), - create_lw_region(world, player, 'Death Mountain Return Ledge', None, ['Death Mountain Return Ledge Drop', 'Death Mountain Return Cave (West)']), - create_cave_region(world, player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'], ['Spectacle Rock Cave Drop', 'Spectacle Rock Cave Exit (Top)']), - create_cave_region(world, player, 'Spectacle Rock Cave (Bottom)', 'a connector', None, ['Spectacle Rock Cave Exit']), - create_cave_region(world, player, 'Spectacle Rock Cave (Peak)', 'a connector', None, ['Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Exit (Peak)']), - create_lw_region(world, player, 'East Death Mountain (Bottom)', None, ['Broken Bridge (East)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'East Death Mountain Teleporter', 'Hookshot Fairy', 'Fairy Ascension Rocks', 'Spiral Cave (Bottom)']), - create_cave_region(world, player, 'Hookshot Fairy', 'fairies deep in a cave'), - create_cave_region(world, player, 'Paradox Cave Front', 'a connector', None, ['Paradox Cave Push Block Reverse', 'Paradox Cave Exit (Bottom)', 'Light World Death Mountain Shop']), - create_cave_region(world, player, 'Paradox Cave Chest Area', 'a connector', ['Paradox Cave Lower - Far Left', + create_dungeon_region(multiworld, player, 'Sanctuary', 'a drop\'s exit', ['Sanctuary'], ['Sanctuary Exit']), + create_dungeon_region(multiworld, player, 'Agahnims Tower', 'Castle Tower', ['Castle Tower - Room 03', 'Castle Tower - Dark Maze', 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'], ['Agahnim 1', 'Agahnims Tower Exit']), + create_dungeon_region(multiworld, player, 'Agahnim 1', 'Castle Tower', ['Agahnim 1'], None), + create_cave_region(multiworld, player, 'Old Man Cave', 'a connector', ['Old Man'], ['Old Man Cave Exit (East)', 'Old Man Cave Exit (West)']), + create_cave_region(multiworld, player, 'Old Man House', 'a connector', None, ['Old Man House Exit (Bottom)', 'Old Man House Front to Back']), + create_cave_region(multiworld, player, 'Old Man House Back', 'a connector', None, ['Old Man House Exit (Top)', 'Old Man House Back to Front']), + create_lw_region(multiworld, player, 'Death Mountain', None, ['Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)', 'Death Mountain Return Cave (East)', 'Spectacle Rock Cave', 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)', 'Broken Bridge (West)', 'Death Mountain Teleporter']), + create_cave_region(multiworld, player, 'Death Mountain Return Cave', 'a connector', None, ['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)']), + create_lw_region(multiworld, player, 'Death Mountain Return Ledge', None, ['Death Mountain Return Ledge Drop', 'Death Mountain Return Cave (West)']), + create_cave_region(multiworld, player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'], ['Spectacle Rock Cave Drop', 'Spectacle Rock Cave Exit (Top)']), + create_cave_region(multiworld, player, 'Spectacle Rock Cave (Bottom)', 'a connector', None, ['Spectacle Rock Cave Exit']), + create_cave_region(multiworld, player, 'Spectacle Rock Cave (Peak)', 'a connector', None, ['Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Exit (Peak)']), + create_lw_region(multiworld, player, 'East Death Mountain (Bottom)', None, ['Broken Bridge (East)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'East Death Mountain Teleporter', 'Hookshot Fairy', 'Fairy Ascension Rocks', 'Spiral Cave (Bottom)']), + create_cave_region(multiworld, player, 'Hookshot Fairy', 'fairies deep in a cave'), + create_cave_region(multiworld, player, 'Paradox Cave Front', 'a connector', None, ['Paradox Cave Push Block Reverse', 'Paradox Cave Exit (Bottom)', 'Light World Death Mountain Shop']), + create_cave_region(multiworld, player, 'Paradox Cave Chest Area', 'a connector', ['Paradox Cave Lower - Far Left', 'Paradox Cave Lower - Left', 'Paradox Cave Lower - Right', 'Paradox Cave Lower - Far Right', @@ -147,267 +147,267 @@ def create_regions(world, player): 'Paradox Cave Upper - Left', 'Paradox Cave Upper - Right'], ['Paradox Cave Push Block', 'Paradox Cave Bomb Jump']), - create_cave_region(world, player, 'Paradox Cave', 'a connector', None, + create_cave_region(multiworld, player, 'Paradox Cave', 'a connector', None, ['Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Top)', 'Paradox Cave Drop']), - create_cave_region(world, player, 'Light World Death Mountain Shop', 'a common shop'), - create_lw_region(world, player, 'East Death Mountain (Top)', None, + create_cave_region(multiworld, player, 'Light World Death Mountain Shop', 'a common shop'), + create_lw_region(multiworld, player, 'East Death Mountain (Top)', None, ['Paradox Cave (Top)', 'Death Mountain (Top)', 'Spiral Cave Ledge Access', 'East Death Mountain Drop', 'Turtle Rock Teleporter', 'Fairy Ascension Ledge']), - create_lw_region(world, player, 'Spiral Cave Ledge', None, ['Spiral Cave', 'Spiral Cave Ledge Drop']), - create_cave_region(world, player, 'Spiral Cave (Top)', 'a connector', ['Spiral Cave'], + create_lw_region(multiworld, player, 'Spiral Cave Ledge', None, ['Spiral Cave', 'Spiral Cave Ledge Drop']), + create_cave_region(multiworld, player, 'Spiral Cave (Top)', 'a connector', ['Spiral Cave'], ['Spiral Cave (top to bottom)', 'Spiral Cave Exit (Top)']), - create_cave_region(world, player, 'Spiral Cave (Bottom)', 'a connector', None, ['Spiral Cave Exit']), - create_lw_region(world, player, 'Fairy Ascension Plateau', None, + create_cave_region(multiworld, player, 'Spiral Cave (Bottom)', 'a connector', None, ['Spiral Cave Exit']), + create_lw_region(multiworld, player, 'Fairy Ascension Plateau', None, ['Fairy Ascension Drop', 'Fairy Ascension Cave (Bottom)']), - create_cave_region(world, player, 'Fairy Ascension Cave (Bottom)', 'a connector', None, + create_cave_region(multiworld, player, 'Fairy Ascension Cave (Bottom)', 'a connector', None, ['Fairy Ascension Cave Climb', 'Fairy Ascension Cave Exit (Bottom)']), - create_cave_region(world, player, 'Fairy Ascension Cave (Drop)', 'a connector', None, + create_cave_region(multiworld, player, 'Fairy Ascension Cave (Drop)', 'a connector', None, ['Fairy Ascension Cave Pots']), - create_cave_region(world, player, 'Fairy Ascension Cave (Top)', 'a connector', None, + create_cave_region(multiworld, player, 'Fairy Ascension Cave (Top)', 'a connector', None, ['Fairy Ascension Cave Exit (Top)', 'Fairy Ascension Cave Drop']), - create_lw_region(world, player, 'Fairy Ascension Ledge', None, + create_lw_region(multiworld, player, 'Fairy Ascension Ledge', None, ['Fairy Ascension Ledge Drop', 'Fairy Ascension Cave (Top)']), - create_lw_region(world, player, 'Death Mountain (Top)', ['Ether Tablet'], + create_lw_region(multiworld, player, 'Death Mountain (Top)', ['Ether Tablet'], ['East Death Mountain (Top)', 'Tower of Hera', 'Death Mountain Drop']), - create_lw_region(world, player, 'Spectacle Rock', ['Spectacle Rock'], ['Spectacle Rock Drop']), - create_dungeon_region(world, player, 'Tower of Hera (Bottom)', 'Tower of Hera', + create_lw_region(multiworld, player, 'Spectacle Rock', ['Spectacle Rock'], ['Spectacle Rock Drop']), + create_dungeon_region(multiworld, player, 'Tower of Hera (Bottom)', 'Tower of Hera', ['Tower of Hera - Basement Cage', 'Tower of Hera - Map Chest'], ['Tower of Hera Small Key Door', 'Tower of Hera Big Key Door', 'Tower of Hera Exit']), - create_dungeon_region(world, player, 'Tower of Hera (Basement)', 'Tower of Hera', + create_dungeon_region(multiworld, player, 'Tower of Hera (Basement)', 'Tower of Hera', ['Tower of Hera - Big Key Chest']), - create_dungeon_region(world, player, 'Tower of Hera (Top)', 'Tower of Hera', + create_dungeon_region(multiworld, player, 'Tower of Hera (Top)', 'Tower of Hera', ['Tower of Hera - Compass Chest', 'Tower of Hera - Big Chest', 'Tower of Hera - Boss', 'Tower of Hera - Prize']), - create_dw_region(world, player, 'East Dark World', ['Pyramid'], + create_dw_region(multiworld, player, 'East Dark World', ['Pyramid'], ['Pyramid Fairy', 'South Dark World Bridge', 'Palace of Darkness', 'Dark Lake Hylia Drop (East)', 'Hyrule Castle Ledge Mirror Spot', 'Dark Lake Hylia Fairy', 'Palace of Darkness Hint', 'East Dark World Hint', 'Pyramid Hole', 'Northeast Dark World Broken Bridge Pass', ]), - create_dw_region(world, player, 'Catfish', ['Catfish'], ['Catfish Exit Rock']), - create_dw_region(world, player, 'Northeast Dark World', None, + create_dw_region(multiworld, player, 'Catfish', ['Catfish'], ['Catfish Exit Rock']), + create_dw_region(multiworld, player, 'Northeast Dark World', None, ['West Dark World Gap', 'Dark World Potion Shop', 'East Dark World Broken Bridge Pass', 'Catfish Entrance Rock', 'Dark Lake Hylia Teleporter']), - create_cave_region(world, player, 'Palace of Darkness Hint', 'a storyteller'), - create_cave_region(world, player, 'East Dark World Hint', 'a storyteller'), - create_dw_region(world, player, 'South Dark World', ['Stumpy', 'Digging Game'], + create_cave_region(multiworld, player, 'Palace of Darkness Hint', 'a storyteller'), + create_cave_region(multiworld, player, 'East Dark World Hint', 'a storyteller'), + create_dw_region(multiworld, player, 'South Dark World', ['Stumpy', 'Digging Game'], ['Dark Lake Hylia Drop (South)', 'Hype Cave', 'Swamp Palace', 'Village of Outcasts Heavy Rock', 'Maze Race Mirror Spot', 'Cave 45 Mirror Spot', 'East Dark World Bridge', 'Big Bomb Shop', 'Archery Game', 'Bonk Fairy (Dark)', 'Dark Lake Hylia Shop', 'Bombos Tablet Mirror Spot']), - create_lw_region(world, player, 'Bombos Tablet Ledge', ['Bombos Tablet']), - create_cave_region(world, player, 'Big Bomb Shop', 'the bomb shop'), - create_cave_region(world, player, 'Archery Game', 'a game of skill'), - create_dw_region(world, player, 'Dark Lake Hylia', None, + create_lw_region(multiworld, player, 'Bombos Tablet Ledge', ['Bombos Tablet']), + create_cave_region(multiworld, player, 'Big Bomb Shop', 'the bomb shop'), + create_cave_region(multiworld, player, 'Archery Game', 'a game of skill'), + create_dw_region(multiworld, player, 'Dark Lake Hylia', None, ['Lake Hylia Island Mirror Spot', 'East Dark World Pier', 'Dark Lake Hylia Ledge']), - create_dw_region(world, player, 'Dark Lake Hylia Central Island', None, + create_dw_region(multiworld, player, 'Dark Lake Hylia Central Island', None, ['Ice Palace', 'Lake Hylia Central Island Mirror Spot']), - create_dw_region(world, player, 'Dark Lake Hylia Ledge', None, + create_dw_region(multiworld, player, 'Dark Lake Hylia Ledge', None, ['Dark Lake Hylia Ledge Drop', 'Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Ledge Hint', 'Dark Lake Hylia Ledge Spike Cave']), - create_cave_region(world, player, 'Dark Lake Hylia Ledge Hint', 'a storyteller'), - create_cave_region(world, player, 'Dark Lake Hylia Ledge Spike Cave', 'a spiky hint'), - create_cave_region(world, player, 'Hype Cave', 'a bounty of five items', + create_cave_region(multiworld, player, 'Dark Lake Hylia Ledge Hint', 'a storyteller'), + create_cave_region(multiworld, player, 'Dark Lake Hylia Ledge Spike Cave', 'a spiky hint'), + create_cave_region(multiworld, player, 'Hype Cave', 'a bounty of five items', ['Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left', 'Hype Cave - Bottom', 'Hype Cave - Generous Guy']), - create_dw_region(world, player, 'West Dark World', ['Frog'], + create_dw_region(multiworld, player, 'West Dark World', ['Frog'], ['Village of Outcasts Drop', 'East Dark World River Pier', 'Brewery', 'C-Shaped House', 'Chest Game', 'Thieves Town', 'Graveyard Ledge Mirror Spot', 'Kings Grave Mirror Spot', 'Bumper Cave Entrance Rock', 'Skull Woods Forest', 'Village of Outcasts Pegs', 'Village of Outcasts Eastern Rocks', 'Red Shield Shop', 'Dark Sanctuary Hint', 'Fortune Teller (Dark)', 'Dark World Lumberjack Shop']), - create_dw_region(world, player, 'Dark Grassy Lawn', None, ['Grassy Lawn Pegs', 'Village of Outcasts Shop']), - create_dw_region(world, player, 'Hammer Peg Area', ['Dark Blacksmith Ruins'], + create_dw_region(multiworld, player, 'Dark Grassy Lawn', None, ['Grassy Lawn Pegs', 'Village of Outcasts Shop']), + create_dw_region(multiworld, player, 'Hammer Peg Area', ['Dark Blacksmith Ruins'], ['Bat Cave Drop Ledge Mirror Spot', 'Dark World Hammer Peg Cave', 'Peg Area Rocks']), - create_dw_region(world, player, 'Bumper Cave Entrance', None, + create_dw_region(multiworld, player, 'Bumper Cave Entrance', None, ['Bumper Cave (Bottom)', 'Bumper Cave Entrance Mirror Spot', 'Bumper Cave Entrance Drop']), - create_cave_region(world, player, 'Fortune Teller (Dark)', 'a fortune teller'), - create_cave_region(world, player, 'Village of Outcasts Shop', 'a common shop'), - create_cave_region(world, player, 'Dark Lake Hylia Shop', 'a common shop'), - create_cave_region(world, player, 'Dark World Lumberjack Shop', 'a common shop'), - create_cave_region(world, player, 'Dark World Potion Shop', 'a common shop'), - create_cave_region(world, player, 'Dark World Hammer Peg Cave', 'a cave with an item', ['Peg Cave']), - create_cave_region(world, player, 'Pyramid Fairy', 'a cave with two chests', + create_cave_region(multiworld, player, 'Fortune Teller (Dark)', 'a fortune teller'), + create_cave_region(multiworld, player, 'Village of Outcasts Shop', 'a common shop'), + create_cave_region(multiworld, player, 'Dark Lake Hylia Shop', 'a common shop'), + create_cave_region(multiworld, player, 'Dark World Lumberjack Shop', 'a common shop'), + create_cave_region(multiworld, player, 'Dark World Potion Shop', 'a common shop'), + create_cave_region(multiworld, player, 'Dark World Hammer Peg Cave', 'a cave with an item', ['Peg Cave']), + create_cave_region(multiworld, player, 'Pyramid Fairy', 'a cave with two chests', ['Pyramid Fairy - Left', 'Pyramid Fairy - Right']), - create_cave_region(world, player, 'Brewery', 'a house with a chest', ['Brewery']), - create_cave_region(world, player, 'C-Shaped House', 'a house with a chest', ['C-Shaped House']), - create_cave_region(world, player, 'Chest Game', 'a game of 16 chests', ['Chest Game']), - create_cave_region(world, player, 'Red Shield Shop', 'the rare shop'), - create_cave_region(world, player, 'Dark Sanctuary Hint', 'a storyteller'), - create_cave_region(world, player, 'Bumper Cave', 'a connector', None, + create_cave_region(multiworld, player, 'Brewery', 'a house with a chest', ['Brewery']), + create_cave_region(multiworld, player, 'C-Shaped House', 'a house with a chest', ['C-Shaped House']), + create_cave_region(multiworld, player, 'Chest Game', 'a game of 16 chests', ['Chest Game']), + create_cave_region(multiworld, player, 'Red Shield Shop', 'the rare shop'), + create_cave_region(multiworld, player, 'Dark Sanctuary Hint', 'a storyteller'), + create_cave_region(multiworld, player, 'Bumper Cave', 'a connector', None, ['Bumper Cave Exit (Bottom)', 'Bumper Cave Exit (Top)']), - create_dw_region(world, player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'], + create_dw_region(multiworld, player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'], ['Bumper Cave Ledge Drop', 'Bumper Cave (Top)', 'Bumper Cave Ledge Mirror Spot']), - create_dw_region(world, player, 'Skull Woods Forest', None, + create_dw_region(multiworld, player, 'Skull Woods Forest', None, ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)']), - create_dw_region(world, player, 'Skull Woods Forest (West)', None, + create_dw_region(multiworld, player, 'Skull Woods Forest (West)', None, ['Skull Woods Second Section Hole', 'Skull Woods Second Section Door (West)', 'Skull Woods Final Section']), - create_dw_region(world, player, 'Dark Desert', None, + create_dw_region(multiworld, player, 'Dark Desert', None, ['Misery Mire', 'Mire Shed', 'Desert Ledge (Northeast) Mirror Spot', 'Desert Ledge Mirror Spot', 'Desert Palace Stairs Mirror Spot', 'Desert Palace Entrance (North) Mirror Spot', 'Dark Desert Hint', 'Dark Desert Fairy']), - create_cave_region(world, player, 'Mire Shed', 'a cave with two chests', + create_cave_region(multiworld, player, 'Mire Shed', 'a cave with two chests', ['Mire Shed - Left', 'Mire Shed - Right']), - create_cave_region(world, player, 'Dark Desert Hint', 'a storyteller'), - create_dw_region(world, player, 'Dark Death Mountain (West Bottom)', None, + create_cave_region(multiworld, player, 'Dark Desert Hint', 'a storyteller'), + create_dw_region(multiworld, player, 'Dark Death Mountain (West Bottom)', None, ['Spike Cave', 'Spectacle Rock Mirror Spot', 'Dark Death Mountain Fairy']), - create_dw_region(world, player, 'Dark Death Mountain (Top)', None, + create_dw_region(multiworld, player, 'Dark Death Mountain (Top)', None, ['Dark Death Mountain Drop (East)', 'Dark Death Mountain Drop (West)', 'Ganons Tower', 'Superbunny Cave (Top)', 'Hookshot Cave', 'East Death Mountain (Top) Mirror Spot', 'Turtle Rock']), - create_dw_region(world, player, 'Dark Death Mountain Ledge', None, + create_dw_region(multiworld, player, 'Dark Death Mountain Ledge', None, ['Dark Death Mountain Ledge (East)', 'Dark Death Mountain Ledge (West)', 'Mimic Cave Mirror Spot', 'Spiral Cave Mirror Spot']), - create_dw_region(world, player, 'Dark Death Mountain Isolated Ledge', None, + create_dw_region(multiworld, player, 'Dark Death Mountain Isolated Ledge', None, ['Isolated Ledge Mirror Spot', 'Turtle Rock Isolated Ledge Entrance']), - create_dw_region(world, player, 'Dark Death Mountain (East Bottom)', None, + create_dw_region(multiworld, player, 'Dark Death Mountain (East Bottom)', None, ['Superbunny Cave (Bottom)', 'Cave Shop (Dark Death Mountain)', 'Fairy Ascension Mirror Spot']), - create_cave_region(world, player, 'Superbunny Cave (Top)', 'a connector', + create_cave_region(multiworld, player, 'Superbunny Cave (Top)', 'a connector', ['Superbunny Cave - Top', 'Superbunny Cave - Bottom'], ['Superbunny Cave Exit (Top)']), - create_cave_region(world, player, 'Superbunny Cave (Bottom)', 'a connector', None, + create_cave_region(multiworld, player, 'Superbunny Cave (Bottom)', 'a connector', None, ['Superbunny Cave Climb', 'Superbunny Cave Exit (Bottom)']), - create_cave_region(world, player, 'Spike Cave', 'Spike Cave', ['Spike Cave']), - create_cave_region(world, player, 'Hookshot Cave', 'a connector', + create_cave_region(multiworld, player, 'Spike Cave', 'Spike Cave', ['Spike Cave']), + create_cave_region(multiworld, player, 'Hookshot Cave', 'a connector', ['Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left'], ['Hookshot Cave Exit (South)', 'Hookshot Cave Bomb Wall (South)']), - create_cave_region(world, player, 'Hookshot Cave (Upper)', 'a connector', None, ['Hookshot Cave Exit (North)', + create_cave_region(multiworld, player, 'Hookshot Cave (Upper)', 'a connector', None, ['Hookshot Cave Exit (North)', 'Hookshot Cave Bomb Wall (North)']), - create_dw_region(world, player, 'Death Mountain Floating Island (Dark World)', None, + create_dw_region(multiworld, player, 'Death Mountain Floating Island (Dark World)', None, ['Floating Island Drop', 'Hookshot Cave Back Entrance', 'Floating Island Mirror Spot']), - create_lw_region(world, player, 'Death Mountain Floating Island (Light World)', ['Floating Island']), - create_dw_region(world, player, 'Turtle Rock (Top)', None, ['Turtle Rock Drop']), - create_lw_region(world, player, 'Mimic Cave Ledge', None, ['Mimic Cave']), - create_cave_region(world, player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']), + create_lw_region(multiworld, player, 'Death Mountain Floating Island (Light World)', ['Floating Island']), + create_dw_region(multiworld, player, 'Turtle Rock (Top)', None, ['Turtle Rock Drop']), + create_lw_region(multiworld, player, 'Mimic Cave Ledge', None, ['Mimic Cave']), + create_cave_region(multiworld, player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']), - create_dungeon_region(world, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None, ['Swamp Palace Moat', 'Swamp Palace Exit']), - create_dungeon_region(world, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'], ['Swamp Palace Small Key Door']), - create_dungeon_region(world, player, 'Swamp Palace (Starting Area)', 'Swamp Palace', ['Swamp Palace - Map Chest', 'Swamp Palace - Pot Row Pot Key', + create_dungeon_region(multiworld, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None, ['Swamp Palace Moat', 'Swamp Palace Exit']), + create_dungeon_region(multiworld, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'], ['Swamp Palace Small Key Door']), + create_dungeon_region(multiworld, player, 'Swamp Palace (Starting Area)', 'Swamp Palace', ['Swamp Palace - Map Chest', 'Swamp Palace - Pot Row Pot Key', 'Swamp Palace - Trench 1 Pot Key'], ['Swamp Palace (Center)']), - create_dungeon_region(world, player, 'Swamp Palace (Center)', 'Swamp Palace', ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest', 'Swamp Palace - Hookshot Pot Key', + create_dungeon_region(multiworld, player, 'Swamp Palace (Center)', 'Swamp Palace', ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest', 'Swamp Palace - Hookshot Pot Key', 'Swamp Palace - Trench 2 Pot Key'], ['Swamp Palace (North)', 'Swamp Palace (West)']), - create_dungeon_region(world, player, 'Swamp Palace (West)', 'Swamp Palace', ['Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest']), - create_dungeon_region(world, player, 'Swamp Palace (North)', 'Swamp Palace', ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right', + create_dungeon_region(multiworld, player, 'Swamp Palace (West)', 'Swamp Palace', ['Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest']), + create_dungeon_region(multiworld, player, 'Swamp Palace (North)', 'Swamp Palace', ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right', 'Swamp Palace - Waterway Pot Key', 'Swamp Palace - Waterfall Room', 'Swamp Palace - Boss', 'Swamp Palace - Prize']), - create_dungeon_region(world, player, 'Thieves Town (Entrance)', 'Thieves\' Town', ['Thieves\' Town - Big Key Chest', + create_dungeon_region(multiworld, player, 'Thieves Town (Entrance)', 'Thieves\' Town', ['Thieves\' Town - Big Key Chest', 'Thieves\' Town - Map Chest', 'Thieves\' Town - Compass Chest', 'Thieves\' Town - Ambush Chest'], ['Thieves Town Big Key Door', 'Thieves Town Exit']), - create_dungeon_region(world, player, 'Thieves Town (Deep)', 'Thieves\' Town', ['Thieves\' Town - Attic', + create_dungeon_region(multiworld, player, 'Thieves Town (Deep)', 'Thieves\' Town', ['Thieves\' Town - Attic', 'Thieves\' Town - Big Chest', 'Thieves\' Town - Hallway Pot Key', 'Thieves\' Town - Spike Switch Pot Key', 'Thieves\' Town - Blind\'s Cell'], ['Blind Fight']), - create_dungeon_region(world, player, 'Blind Fight', 'Thieves\' Town', ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']), - create_dungeon_region(world, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'], ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump', 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']), - create_dungeon_region(world, player, 'Skull Woods First Section (Right)', 'Skull Woods', ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']), - create_dungeon_region(world, player, 'Skull Woods First Section (Left)', 'Skull Woods', ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'], ['Skull Woods First Section (Left) Door to Exit', 'Skull Woods First Section (Left) Door to Right']), - create_dungeon_region(world, player, 'Skull Woods First Section (Top)', 'Skull Woods', ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']), - create_dungeon_region(world, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None, ['Skull Woods Second Section (Drop)']), - create_dungeon_region(world, player, 'Skull Woods Second Section', 'Skull Woods', ['Skull Woods - Big Key Chest', 'Skull Woods - West Lobby Pot Key'], ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']), - create_dungeon_region(world, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']), - create_dungeon_region(world, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Spike Corner Key Drop', 'Skull Woods - Boss', 'Skull Woods - Prize']), - create_dungeon_region(world, player, 'Ice Palace (Entrance)', 'Ice Palace', ['Ice Palace - Jelly Key Drop', 'Ice Palace - Compass Chest'], ['Ice Palace (Second Section)', 'Ice Palace Exit']), - create_dungeon_region(world, player, 'Ice Palace (Second Section)', 'Ice Palace', ['Ice Palace - Conveyor Key Drop'], ['Ice Palace (Main)']), - create_dungeon_region(world, player, 'Ice Palace (Main)', 'Ice Palace', ['Ice Palace - Freezor Chest', + create_dungeon_region(multiworld, player, 'Blind Fight', 'Thieves\' Town', ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']), + create_dungeon_region(multiworld, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'], ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump', 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']), + create_dungeon_region(multiworld, player, 'Skull Woods First Section (Right)', 'Skull Woods', ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']), + create_dungeon_region(multiworld, player, 'Skull Woods First Section (Left)', 'Skull Woods', ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'], ['Skull Woods First Section (Left) Door to Exit', 'Skull Woods First Section (Left) Door to Right']), + create_dungeon_region(multiworld, player, 'Skull Woods First Section (Top)', 'Skull Woods', ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']), + create_dungeon_region(multiworld, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None, ['Skull Woods Second Section (Drop)']), + create_dungeon_region(multiworld, player, 'Skull Woods Second Section', 'Skull Woods', ['Skull Woods - Big Key Chest', 'Skull Woods - West Lobby Pot Key'], ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']), + create_dungeon_region(multiworld, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']), + create_dungeon_region(multiworld, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Spike Corner Key Drop', 'Skull Woods - Boss', 'Skull Woods - Prize']), + create_dungeon_region(multiworld, player, 'Ice Palace (Entrance)', 'Ice Palace', ['Ice Palace - Jelly Key Drop', 'Ice Palace - Compass Chest'], ['Ice Palace (Second Section)', 'Ice Palace Exit']), + create_dungeon_region(multiworld, player, 'Ice Palace (Second Section)', 'Ice Palace', ['Ice Palace - Conveyor Key Drop'], ['Ice Palace (Main)']), + create_dungeon_region(multiworld, player, 'Ice Palace (Main)', 'Ice Palace', ['Ice Palace - Freezor Chest', 'Ice Palace - Many Pots Pot Key', 'Ice Palace - Big Chest', 'Ice Palace - Iced T Room'], ['Ice Palace (East)', 'Ice Palace (Kholdstare)']), - create_dungeon_region(world, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'], ['Ice Palace (East Top)']), - create_dungeon_region(world, player, 'Ice Palace (East Top)', 'Ice Palace', ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest', 'Ice Palace - Hammer Block Key Drop']), - create_dungeon_region(world, player, 'Ice Palace (Kholdstare)', 'Ice Palace', ['Ice Palace - Boss', 'Ice Palace - Prize']), - create_dungeon_region(world, player, 'Misery Mire (Entrance)', 'Misery Mire', None, ['Misery Mire Entrance Gap', 'Misery Mire Exit']), - create_dungeon_region(world, player, 'Misery Mire (Main)', 'Misery Mire', ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby', + create_dungeon_region(multiworld, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'], ['Ice Palace (East Top)']), + create_dungeon_region(multiworld, player, 'Ice Palace (East Top)', 'Ice Palace', ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest', 'Ice Palace - Hammer Block Key Drop']), + create_dungeon_region(multiworld, player, 'Ice Palace (Kholdstare)', 'Ice Palace', ['Ice Palace - Boss', 'Ice Palace - Prize']), + create_dungeon_region(multiworld, player, 'Misery Mire (Entrance)', 'Misery Mire', None, ['Misery Mire Entrance Gap', 'Misery Mire Exit']), + create_dungeon_region(multiworld, player, 'Misery Mire (Main)', 'Misery Mire', ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby', 'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest', 'Misery Mire - Spikes Pot Key', 'Misery Mire - Fishbone Pot Key', 'Misery Mire - Conveyor Crystal Key Drop'], ['Misery Mire (West)', 'Misery Mire Big Key Door']), - create_dungeon_region(world, player, 'Misery Mire (West)', 'Misery Mire', ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']), - create_dungeon_region(world, player, 'Misery Mire (Final Area)', 'Misery Mire', None, ['Misery Mire (Vitreous)']), - create_dungeon_region(world, player, 'Misery Mire (Vitreous)', 'Misery Mire', ['Misery Mire - Boss', 'Misery Mire - Prize']), - create_dungeon_region(world, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None, ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']), - create_dungeon_region(world, player, 'Turtle Rock (First Section)', 'Turtle Rock', ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left', + create_dungeon_region(multiworld, player, 'Misery Mire (West)', 'Misery Mire', ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']), + create_dungeon_region(multiworld, player, 'Misery Mire (Final Area)', 'Misery Mire', None, ['Misery Mire (Vitreous)']), + create_dungeon_region(multiworld, player, 'Misery Mire (Vitreous)', 'Misery Mire', ['Misery Mire - Boss', 'Misery Mire - Prize']), + create_dungeon_region(multiworld, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None, ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']), + create_dungeon_region(multiworld, player, 'Turtle Rock (First Section)', 'Turtle Rock', ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left', 'Turtle Rock - Roller Room - Right'], ['Turtle Rock Entrance to Pokey Room', 'Turtle Rock Entrance Gap Reverse']), - create_dungeon_region(world, player, 'Turtle Rock (Pokey Room)', 'Turtle Rock', ['Turtle Rock - Pokey 1 Key Drop'], ['Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Pokey Room) (South)']), - create_dungeon_region(world, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'], ['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']), - create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock', ['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'], ['Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door', 'Turtle Rock Second Section Bomb Wall']), - create_dungeon_region(world, player, 'Turtle Rock (Second Section Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Second Section from Bomb Wall']), - create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']), - create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']), - create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']), - create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Isolated Ledge Exit', 'Turtle Rock Eye Bridge from Bomb Wall']), - create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right', + create_dungeon_region(multiworld, player, 'Turtle Rock (Pokey Room)', 'Turtle Rock', ['Turtle Rock - Pokey 1 Key Drop'], ['Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Pokey Room) (South)']), + create_dungeon_region(multiworld, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'], ['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']), + create_dungeon_region(multiworld, player, 'Turtle Rock (Second Section)', 'Turtle Rock', ['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'], ['Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door', 'Turtle Rock Second Section Bomb Wall']), + create_dungeon_region(multiworld, player, 'Turtle Rock (Second Section Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Second Section from Bomb Wall']), + create_dungeon_region(multiworld, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']), + create_dungeon_region(multiworld, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']), + create_dungeon_region(multiworld, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']), + create_dungeon_region(multiworld, player, 'Turtle Rock (Eye Bridge Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Isolated Ledge Exit', 'Turtle Rock Eye Bridge from Bomb Wall']), + create_dungeon_region(multiworld, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right', 'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'], ['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Eye Bridge Bomb Wall']), - create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']), - create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']), - create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'], + create_dungeon_region(multiworld, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']), + create_dungeon_region(multiworld, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']), + create_dungeon_region(multiworld, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'], ['Palace of Darkness Big Key Chest Staircase', 'Palace of Darkness (North)', 'Palace of Darkness Big Key Door']), - create_dungeon_region(world, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness', ['Palace of Darkness - Big Key Chest']), - create_dungeon_region(world, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'], ['Palace of Darkness Hammer Peg Drop']), - create_dungeon_region(world, player, 'Palace of Darkness (North)', 'Palace of Darkness', ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right'], + create_dungeon_region(multiworld, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness', ['Palace of Darkness - Big Key Chest']), + create_dungeon_region(multiworld, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'], ['Palace of Darkness Hammer Peg Drop']), + create_dungeon_region(multiworld, player, 'Palace of Darkness (North)', 'Palace of Darkness', ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right'], ['Palace of Darkness Spike Statue Room Door', 'Palace of Darkness Maze Door']), - create_dungeon_region(world, player, 'Palace of Darkness (Maze)', 'Palace of Darkness', ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Big Chest']), - create_dungeon_region(world, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness', ['Palace of Darkness - Harmless Hellway']), - create_dungeon_region(world, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness', ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']), - create_dungeon_region(world, player, 'Ganons Tower (Entrance)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Torch', 'Ganons Tower - Hope Room - Left', + create_dungeon_region(multiworld, player, 'Palace of Darkness (Maze)', 'Palace of Darkness', ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Big Chest']), + create_dungeon_region(multiworld, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness', ['Palace of Darkness - Harmless Hellway']), + create_dungeon_region(multiworld, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness', ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']), + create_dungeon_region(multiworld, player, 'Ganons Tower (Entrance)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Torch', 'Ganons Tower - Hope Room - Left', 'Ganons Tower - Hope Room - Right', 'Ganons Tower - Conveyor Cross Pot Key'], ['Ganons Tower (Tile Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower Big Key Door', 'Ganons Tower Exit']), - create_dungeon_region(world, player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'], ['Ganons Tower (Tile Room) Key Door']), - create_dungeon_region(world, player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower', ['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right', + create_dungeon_region(multiworld, player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'], ['Ganons Tower (Tile Room) Key Door']), + create_dungeon_region(multiworld, player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower', ['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right', 'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Bottom Right', 'Ganons Tower - Conveyor Star Pits Pot Key'], ['Ganons Tower (Bottom) (East)']), - create_dungeon_region(world, player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower', ['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right', + create_dungeon_region(multiworld, player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower', ['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right', 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right', 'Ganons Tower - Double Switch Pot Key'], ['Ganons Tower (Map Room)', 'Ganons Tower (Double Switch Room)']), - create_dungeon_region(world, player, 'Ganons Tower (Map Room)', 'Ganon\'s Tower', ['Ganons Tower - Map Chest']), - create_dungeon_region(world, player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower', ['Ganons Tower - Firesnake Room'], ['Ganons Tower (Firesnake Room)']), - create_dungeon_region(world, player, 'Ganons Tower (Teleport Room)', 'Ganon\'s Tower', ['Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right', + create_dungeon_region(multiworld, player, 'Ganons Tower (Map Room)', 'Ganon\'s Tower', ['Ganons Tower - Map Chest']), + create_dungeon_region(multiworld, player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower', ['Ganons Tower - Firesnake Room'], ['Ganons Tower (Firesnake Room)']), + create_dungeon_region(multiworld, player, 'Ganons Tower (Teleport Room)', 'Ganon\'s Tower', ['Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right', 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right'], ['Ganons Tower (Bottom) (West)']), - create_dungeon_region(world, player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest', 'Ganons Tower - Big Key Room - Left', + create_dungeon_region(multiworld, player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest', 'Ganons Tower - Big Key Room - Left', 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Chest']), - create_dungeon_region(world, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None, ['Ganons Tower Torch Rooms']), - create_dungeon_region(world, player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower', ['Ganons Tower - Mini Helmasaur Room - Left', 'Ganons Tower - Mini Helmasaur Room - Right', + create_dungeon_region(multiworld, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None, ['Ganons Tower Torch Rooms']), + create_dungeon_region(multiworld, player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower', ['Ganons Tower - Mini Helmasaur Room - Left', 'Ganons Tower - Mini Helmasaur Room - Right', 'Ganons Tower - Pre-Moldorm Chest', 'Ganons Tower - Mini Helmasaur Key Drop'], ['Ganons Tower Moldorm Door']), - create_dungeon_region(world, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None, ['Ganons Tower Moldorm Gap']), - create_dungeon_region(world, player, 'Agahnim 2', 'Ganon\'s Tower', ['Ganons Tower - Validation Chest', 'Agahnim 2'], None), - create_cave_region(world, player, 'Pyramid', 'a drop\'s exit', ['Ganon'], ['Ganon Drop']), - create_cave_region(world, player, 'Bottom of Pyramid', 'a drop\'s exit', None, ['Pyramid Exit']), - create_dw_region(world, player, 'Pyramid Ledge', None, ['Pyramid Entrance', 'Pyramid Drop']), - create_lw_region(world, player, 'Desert Northern Cliffs'), - create_dw_region(world, player, 'Dark Death Mountain Bunny Descent Area') + create_dungeon_region(multiworld, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None, ['Ganons Tower Moldorm Gap']), + create_dungeon_region(multiworld, player, 'Agahnim 2', 'Ganon\'s Tower', ['Ganons Tower - Validation Chest', 'Agahnim 2'], None), + create_cave_region(multiworld, player, 'Pyramid', 'a drop\'s exit', ['Ganon'], ['Ganon Drop']), + create_cave_region(multiworld, player, 'Bottom of Pyramid', 'a drop\'s exit', None, ['Pyramid Exit']), + create_dw_region(multiworld, player, 'Pyramid Ledge', None, ['Pyramid Entrance', 'Pyramid Drop']), + create_lw_region(multiworld, player, 'Desert Northern Cliffs'), + create_dw_region(multiworld, player, 'Dark Death Mountain Bunny Descent Area') ] -def create_lw_region(world: MultiWorld, player: int, name: str, locations=None, exits=None): - return _create_region(world, player, name, LTTPRegionType.LightWorld, 'Light World', locations, exits) +def create_lw_region(multiworld: MultiWorld, player: int, name: str, locations=None, exits=None): + return _create_region(multiworld, player, name, LTTPRegionType.LightWorld, 'Light World', locations, exits) -def create_dw_region(world: MultiWorld, player: int, name: str, locations=None, exits=None): - return _create_region(world, player, name, LTTPRegionType.DarkWorld, 'Dark World', locations, exits) +def create_dw_region(multiworld: MultiWorld, player: int, name: str, locations=None, exits=None): + return _create_region(multiworld, player, name, LTTPRegionType.DarkWorld, 'Dark World', locations, exits) -def create_cave_region(world: MultiWorld, player: int, name: str, hint: str, locations=None, exits=None): - return _create_region(world, player, name, LTTPRegionType.Cave, hint, locations, exits) +def create_cave_region(multiworld: MultiWorld, player: int, name: str, hint: str, locations=None, exits=None): + return _create_region(multiworld, player, name, LTTPRegionType.Cave, hint, locations, exits) -def create_dungeon_region(world: MultiWorld, player: int, name: str, hint: str, locations=None, exits=None): - return _create_region(world, player, name, LTTPRegionType.Dungeon, hint, locations, exits) +def create_dungeon_region(multiworld: MultiWorld, player: int, name: str, hint: str, locations=None, exits=None): + return _create_region(multiworld, player, name, LTTPRegionType.Dungeon, hint, locations, exits) -def _create_region(world: MultiWorld, player: int, name: str, type: LTTPRegionType, hint: str, locations=None, +def _create_region(multiworld: MultiWorld, player: int, name: str, type: LTTPRegionType, hint: str, locations=None, exits=None): from .SubClasses import ALttPLocation - ret = LTTPRegion(name, type, hint, player, world) + ret = LTTPRegion(name, type, hint, player, multiworld) if exits: for exit in exits: ret.create_exit(exit) @@ -422,10 +422,10 @@ def _create_region(world: MultiWorld, player: int, name: str, type: LTTPRegionTy return ret -def mark_light_world_regions(world, player: int): +def mark_light_world_regions(multiworld: MultiWorld, player: int): # cross world caves may have some sections marked as both in_light_world, and in_dark_work. # That is ok. the bunny logic will check for this case and incorporate special rules. - queue = collections.deque(region for region in world.get_regions(player) if region.type == LTTPRegionType.LightWorld) + queue = collections.deque(region for region in multiworld.get_regions(player) if region.type == LTTPRegionType.LightWorld) seen = set(queue) while queue: current = queue.popleft() @@ -438,7 +438,7 @@ def mark_light_world_regions(world, player: int): seen.add(exit.connected_region) queue.append(exit.connected_region) - queue = collections.deque(region for region in world.get_regions(player) if region.type == LTTPRegionType.DarkWorld) + queue = collections.deque(region for region in multiworld.get_regions(player) if region.type == LTTPRegionType.DarkWorld) seen = set(queue) while queue: current = queue.popleft() diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index 2f62b37682..45b3ad39d9 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -19,7 +19,7 @@ import subprocess import threading import concurrent.futures import bsdiff4 -from typing import Collection, Optional, List, SupportsIndex +from typing import Collection, Optional, List, SupportsIndex, TYPE_CHECKING from BaseClasses import CollectionState, Region, Location, MultiWorld from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, parse_yaml, read_snes_rom @@ -39,6 +39,9 @@ from .Items import item_table, item_name_groups, progression_items from .EntranceShuffle import door_addresses from .Options import small_key_shuffle +if TYPE_CHECKING: + from . import ALTTPWorld + try: from maseya import z3pr from maseya.z3pr.palette_randomizer import build_offset_collections @@ -792,13 +795,13 @@ def get_nonnative_item_sprite(code: int) -> int: # https://discord.com/channels/731205301247803413/827141303330406408/852102450822905886 -def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): - local_random = world.worlds[player].random - local_world = world.worlds[player] +def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool): + local_random = multiworld.worlds[player].random + local_world = multiworld.worlds[player] # patch items - for location in world.get_locations(player): + for location in multiworld.get_locations(player): if location.address is None or location.shop_slot is not None: continue @@ -852,7 +855,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): rom.write_byte(0x155C9, local_random.choice([0x11, 0x16])) # Randomize GT music too with map shuffle # patch entrance/exits/holes - for region in world.get_regions(player): + for region in multiworld.get_regions(player): for exit in region.exits: if exit.target is not None: if isinstance(exit.addresses, tuple): @@ -885,7 +888,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): rom.write_int16(0x15DB5 + 2 * offset, 0x0640) elif room_id == 0x00d6 and local_world.fix_trock_exit: rom.write_int16(0x15DB5 + 2 * offset, 0x0134) - elif room_id == 0x000c and world.shuffle_ganon: # fix ganons tower exit point + elif room_id == 0x000c and multiworld.shuffle_ganon: # fix ganons tower exit point rom.write_int16(0x15DB5 + 2 * offset, 0x00A4) else: rom.write_int16(0x15DB5 + 2 * offset, link_y) @@ -905,9 +908,9 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): # patch door table rom.write_byte(0xDBB73 + exit.addresses, exit.target) if local_world.options.mode == 'inverted': - patch_shuffled_dark_sanc(world, rom, player) + patch_shuffled_dark_sanc(multiworld, rom, player) - write_custom_shops(rom, world, player) + write_custom_shops(rom, multiworld, player) def credits_digit(num): # top: $54 is 1, 55 2, etc , so 57=4, 5C=9 @@ -981,11 +984,11 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): if local_world.options.mode in ['open', 'inverted']: rom.write_byte(0x180032, 0x01) # open mode if local_world.options.mode == 'inverted': - set_inverted_mode(world, player, rom) + set_inverted_mode(multiworld, player, rom) elif local_world.options.mode == 'standard': rom.write_byte(0x180032, 0x00) # standard mode - uncle_location = world.get_location('Link\'s Uncle', player) + uncle_location = multiworld.get_location('Link\'s Uncle', player) if uncle_location.item is None or uncle_location.item.name not in ['Master Sword', 'Tempered Sword', 'Fighter Sword', 'Golden Sword', 'Progressive Sword']: @@ -1280,7 +1283,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): # set up goals for treasure hunt rom.write_int16(0x180163, max(0, local_world.treasure_hunt_required - - sum(1 for item in world.precollected_items[player] if item.name == "Triforce Piece"))) + sum(1 for item in multiworld.precollected_items[player] if item.name == "Triforce Piece"))) rom.write_bytes(0x180165, [0x0E, 0x28]) # Triforce Piece Sprite rom.write_byte(0x180194, 1) # Must turn in triforced pieces (instant win not enabled) @@ -1309,7 +1312,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): rom.write_bytes(0x50563, [0x3F, 0x14]) # disable below ganon chest rom.write_byte(0x50599, 0x00) # disable below ganon chest rom.write_bytes(0xE9A5, [0x7E, 0x00, 0x24]) # disable below ganon chest - rom.write_byte(0x18008B, 0x01 if local_world.options.open_pyramid.to_bool(world, player) else 0x00) # pre-open Pyramid Hole + rom.write_byte(0x18008B, 0x01 if local_world.options.open_pyramid.to_bool(multiworld, player) else 0x00) # pre-open Pyramid Hole rom.write_byte(0x18008C, 0x01 if local_world.options.crystals_needed_for_gt == 0 else 0x00) # GT pre-opened if crystal requirement is 0 rom.write_byte(0xF5D73, 0xF0) # bees are catchable rom.write_byte(0xF5F10, 0xF0) # bees are catchable @@ -1327,7 +1330,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): starting_max_bombs = 0 if local_world.options.bombless_start else 10 starting_max_arrows = 30 - startingstate = CollectionState(world) + startingstate = CollectionState(multiworld) if startingstate.has('Silver Bow', player): equip[0x340] = 1 @@ -1375,7 +1378,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): equip[0x37B] = 1 equip[0x36E] = 0x80 - for item in world.precollected_items[player]: + for item in multiworld.precollected_items[player]: if item.name in {'Bow', 'Silver Bow', 'Silver Arrows', 'Progressive Bow', 'Progressive Bow (Alt)', 'Titans Mitts', 'Power Glove', 'Progressive Glove', @@ -1590,7 +1593,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): } def get_reveal_bytes(itemName): - locations = world.find_item_locations(itemName, player) + locations = multiworld.find_item_locations(itemName, player) if len(locations) < 1: return 0x0000 location = locations[0] @@ -1667,7 +1670,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): rom.write_byte(0x18004C, 0x01) # set correct flag for hera basement item - hera_basement = world.get_location('Tower of Hera - Basement Cage', player) + hera_basement = multiworld.get_location('Tower of Hera - Basement Cage', player) if hera_basement.item is not None and hera_basement.item.name == 'Small Key (Tower of Hera)' and hera_basement.item.player == player: rom.write_byte(0x4E3BB, 0xE4) else: @@ -1684,12 +1687,12 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): rom.write_byte(0xFEE41, 0x2A) # bombable exit if local_world.options.tile_shuffle: - tile_set = TileSet.get_random_tile_set(world.worlds[player].random) + tile_set = TileSet.get_random_tile_set(multiworld.worlds[player].random) rom.write_byte(0x4BA21, tile_set.get_speed()) rom.write_byte(0x4BA1D, tile_set.get_len()) rom.write_bytes(0x4BA2A, tile_set.get_bytes()) - write_strings(rom, world, player) + write_strings(rom, multiworld, player) # remote items flag, does not currently work rom.write_byte(0x18637C, 0) @@ -1697,14 +1700,14 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): # set rom name # 21 bytes from Utils import __version__ - rom.name = bytearray(f'AP{__version__.replace(".", "")[0:3]}_{player}_{world.seed:11}\0', 'utf8')[:21] + rom.name = bytearray(f'AP{__version__.replace(".", "")[0:3]}_{player}_{multiworld.seed:11}\0', 'utf8')[:21] rom.name.extend([0] * (21 - len(rom.name))) rom.write_bytes(0x7FC0, rom.name) # set player names - encoded_players = world.players + len(world.groups) + encoded_players = multiworld.players + len(multiworld.groups) for p in range(1, min(encoded_players, ROM_PLAYER_LIMIT) + 1): - rom.write_bytes(0x195FFC + ((p - 1) * 32), hud_format_text(world.player_name[p])) + rom.write_bytes(0x195FFC + ((p - 1) * 32), hud_format_text(multiworld.player_name[p])) if encoded_players > ROM_PLAYER_LIMIT: rom.write_bytes(0x195FFC + ((ROM_PLAYER_LIMIT - 1) * 32), hud_format_text("Archipelago")) @@ -1723,9 +1726,9 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): return rom -def patch_race_rom(rom, world, player): +def patch_race_rom(rom: LocalRom, multiworld: MultiWorld, player: int): rom.write_bytes(0x180213, [0x01, 0x00]) # Tournament Seed - rom.encrypt(world, player) + rom.encrypt(multiworld, player) def get_price_data(price: int, price_type: int) -> List[int]: @@ -1738,8 +1741,8 @@ def get_price_data(price: int, price_type: int) -> List[int]: return int16_as_bytes(price) -def write_custom_shops(rom, world, player): - shops = sorted([shop for shop in world.worlds[player].shops if shop.custom], key=lambda shop: shop.sram_offset) +def write_custom_shops(rom: LocalRom, multiworld: MultiWorld, player: int): + shops = sorted([shop for shop in multiworld.worlds[player].shops if shop.custom], key=lambda shop: shop.sram_offset) shop_data = bytearray() items_data = bytearray() @@ -1758,9 +1761,9 @@ def write_custom_shops(rom, world, player): slot = 0 if shop.type == ShopType.TakeAny else index if item is None: break - if world.worlds[player].options.shop_item_slots or shop.type == ShopType.TakeAny: - count_shop = (shop.region.name != 'Potion Shop' or world.worlds[player].options.include_witch_hut) and \ - (shop.region.name != 'Capacity Upgrade' or world.worlds[player].options.shuffle_capacity_upgrades) + if multiworld.worlds[player].options.shop_item_slots or shop.type == ShopType.TakeAny: + count_shop = (shop.region.name != 'Potion Shop' or multiworld.worlds[player].options.include_witch_hut) and \ + (shop.region.name != 'Capacity Upgrade' or multiworld.worlds[player].options.shuffle_capacity_upgrades) rom.write_byte(0x186560 + shop.sram_offset + slot, 1 if count_shop else 0) if item['item'] == 'Single Arrow' and item['player'] == 0: arrow_mask |= 1 << index @@ -1773,11 +1776,11 @@ def write_custom_shops(rom, world, player): price_data = get_price_data(item['price'], item["price_type"]) replacement_price_data = get_price_data(item['replacement_price'], item['replacement_price_type']) slot = 0 if shop.type == ShopType.TakeAny else index - if item['player'] and world.game[item['player']] != "A Link to the Past": # item not native to ALTTP - item_code = get_nonnative_item_sprite(world.worlds[item['player']].item_name_to_id[item['item']]) + if item['player'] and multiworld.game[item['player']] != "A Link to the Past": # item not native to ALTTP + item_code = get_nonnative_item_sprite(multiworld.worlds[item['player']].item_name_to_id[item['item']]) else: item_code = item_table[item["item"]].item_code - if item['item'] == 'Single Arrow' and item['player'] == 0 and world.worlds[player].options.retro_bow: + if item['item'] == 'Single Arrow' and item['player'] == 0 and multiworld.worlds[player].options.retro_bow: rom.write_byte(0x186500 + shop.sram_offset + slot, arrow_mask) item_data = [shop_id, item_code] + price_data + \ @@ -1790,12 +1793,12 @@ def write_custom_shops(rom, world, player): items_data.extend([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) rom.write_bytes(0x184900, items_data) - if world.worlds[player].options.retro_bow: + if multiworld.worlds[player].options.retro_bow: retro_shop_slots.append(0xFF) rom.write_bytes(0x186540, retro_shop_slots) -def hud_format_text(text): +def hud_format_text(text: str): output = bytes() for char in text.lower(): if 'a' <= char <= 'z': @@ -1812,7 +1815,7 @@ def hud_format_text(text): output += b'\x7f\x00' return output[:32] -def apply_oof_sfx(rom, oof: str): +def apply_oof_sfx(rom: LocalRom, oof: str): with open(oof, 'rb') as stream: oof_bytes = bytearray(stream.read()) @@ -1862,9 +1865,10 @@ def apply_oof_sfx(rom, oof: str): rom.write_bytes(0x13000D, [0x00, 0x00, 0x00, 0x08]) -def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, sprite: str, oof: str, palettes_options, - world=None, player=1, allow_random_on_event=False, reduceflashing=False, - triforcehud: str = None, deathlink: bool = False, allowcollect: bool = False): +def apply_rom_settings(rom: LocalRom, beep: str, color: str, quickswap: bool, menuspeed: str, music: bool, sprite: str, + oof: str, palettes_options: dict[str, str], world: "ALTTPWorld | None" = None, player: int = 1, + allow_random_on_event: bool = False, reduceflashing: bool = False, triforcehud: str = None, + deathlink: bool = False, allowcollect: bool = False): local_random = random if not world else world.worlds[player].random disable_music: bool = not music # enable instant item menu @@ -1948,7 +1952,7 @@ def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, spri rom.write_byte(0x180167, triforce_flag) if z3pr: - def buildAndRandomize(option_name, mode): + def buildAndRandomize(option_name: str, mode: str): options = { option_name: True } @@ -2012,7 +2016,7 @@ def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, spri rom.write_crc() -def restore_maseya_colors(rom, offsets_array): +def restore_maseya_colors(rom: LocalRom, offsets_array: list[list[int]]): if not rom.orig_buffer: return for offsetC in offsets_array: @@ -2020,7 +2024,7 @@ def restore_maseya_colors(rom, offsets_array): rom.write_bytes(address, rom.orig_buffer[address:address + 2]) -def set_color(rom, address, color, shade): +def set_color(rom: LocalRom, address: int, color: tuple[int, int, int], shade: int): r = round(min(color[0], 0xFF) * pow(0.8, shade) * 0x1F / 0xFF) g = round(min(color[1], 0xFF) * pow(0.8, shade) * 0x1F / 0xFF) b = round(min(color[2], 0xFF) * pow(0.8, shade) * 0x1F / 0xFF) @@ -2028,7 +2032,7 @@ def set_color(rom, address, color, shade): rom.write_bytes(address, ((b << 10) | (g << 5) | (r << 0)).to_bytes(2, byteorder='little', signed=False)) -def default_ow_palettes(rom): +def default_ow_palettes(rom: LocalRom): if not rom.orig_buffer: return rom.write_bytes(0xDE604, rom.orig_buffer[0xDE604:0xDEBB4]) @@ -2037,7 +2041,7 @@ def default_ow_palettes(rom): rom.write_bytes(address, rom.orig_buffer[address:address + 2]) -def randomize_ow_palettes(rom, local_random): +def randomize_ow_palettes(rom: LocalRom, local_random: random.Random): grass, grass2, grass3, dirt, dirt2, water, clouds, dwdirt, \ dwgrass, dwwater, dwdmdirt, dwdmgrass, dwdmclouds1, dwdmclouds2 = [[local_random.randint(60, 215) for _ in range(3)] for _ in range(14)] @@ -2113,7 +2117,7 @@ def randomize_ow_palettes(rom, local_random): set_color(rom, address, color, shade) -def blackout_ow_palettes(rom): +def blackout_ow_palettes(rom: LocalRom): rom.write_bytes(0xDE604, [0] * 0xC4) for i in range(0xDE6C8, 0xDE86C, 70): rom.write_bytes(i, [0] * 64) @@ -2124,13 +2128,13 @@ def blackout_ow_palettes(rom): rom.write_bytes(address, [0, 0]) -def default_uw_palettes(rom): +def default_uw_palettes(rom: LocalRom): if not rom.orig_buffer: return rom.write_bytes(0xDD734, rom.orig_buffer[0xDD734:0xDE544]) -def randomize_uw_palettes(rom, local_random): +def randomize_uw_palettes(rom: LocalRom, local_random: random.Random): for dungeon in range(20): wall, pot, chest, floor1, floor2, floor3 = [[local_random.randint(60, 240) for _ in range(3)] for _ in range(6)] @@ -2177,7 +2181,7 @@ def randomize_uw_palettes(rom, local_random): set_color(rom, 0x0DD796 + (0xB4 * dungeon), floor3, 4) -def blackout_uw_palettes(rom): +def blackout_uw_palettes(rom: LocalRom): for i in range(0xDD734, 0xDE544, 180): rom.write_bytes(i, [0] * 38) rom.write_bytes(i + 44, [0] * 76) @@ -2188,25 +2192,25 @@ def get_hash_string(hash): return ", ".join([hash_alphabet[code & 0x1F] for code in hash]) -def write_string_to_rom(rom, target, string): +def write_string_to_rom(rom: LocalRom, target: str, string: str): address, maxbytes = text_addresses[target] rom.write_bytes(address, MultiByteTextMapper.convert(string, maxbytes)) -def write_strings(rom, world, player): +def write_strings(rom: LocalRom, multiworld: MultiWorld, player: int): from . import ALTTPWorld - local_random = world.worlds[player].random - w: ALTTPWorld = world.worlds[player] + local_random = multiworld.worlds[player].random + w: ALTTPWorld = multiworld.worlds[player] tt = TextTable() tt.removeUnwantedText() # Let's keep this guy's text accurate to the shuffle setting. - if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']: + if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']: tt['kakariko_flophouse_man_no_flippers'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.' tt['kakariko_flophouse_man'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.' - if world.worlds[player].options.mode == 'inverted': + if multiworld.worlds[player].options.mode == 'inverted': tt['sign_village_of_outcasts'] = 'attention\nferal ducks sighted\nhiding in statues\n\nflute players beware\n' def hint_text(dest, ped_hint=False): @@ -2218,45 +2222,45 @@ def write_strings(rom, world, player): hint = dest.hint_text if dest.player != player: if ped_hint: - hint += f" for {world.player_name[dest.player]}!" + hint += f" for {multiworld.player_name[dest.player]}!" elif isinstance(dest, (Region, Location)): - hint += f" in {world.player_name[dest.player]}'s world" + hint += f" in {multiworld.player_name[dest.player]}'s world" else: - hint += f" for {world.player_name[dest.player]}" + hint += f" for {multiworld.player_name[dest.player]}" return hint - if world.worlds[player].options.scams.gives_king_zora_hint: + if multiworld.worlds[player].options.scams.gives_king_zora_hint: # Zora hint - zora_location = world.get_location("King Zora", player) + zora_location = multiworld.get_location("King Zora", player) tt['zora_tells_cost'] = f"You got 500 rupees to buy {hint_text(zora_location.item)}" \ f"\n ≥ Duh\n Oh carp\n{{CHOICE}}" - if world.worlds[player].options.scams.gives_bottle_merchant_hint: + if multiworld.worlds[player].options.scams.gives_bottle_merchant_hint: # Bottle Vendor hint - vendor_location = world.get_location("Bottle Merchant", player) + vendor_location = multiworld.get_location("Bottle Merchant", player) tt['bottle_vendor_choice'] = f"I gots {hint_text(vendor_location.item)}\nYous gots 100 rupees?" \ f"\n ≥ I want\n no way!\n{{CHOICE}}" # First we write hints about entrances, some from the inconvenient list others from all reasonable entrances. - if world.worlds[player].options.hints: - if world.worlds[player].options.hints.value >= 2: - if world.worlds[player].options.hints == "full": + if multiworld.worlds[player].options.hints: + if multiworld.worlds[player].options.hints.value >= 2: + if multiworld.worlds[player].options.hints == "full": tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles have hints!' else: tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles can have hints!' hint_locations = HintLocations.copy() local_random.shuffle(hint_locations) - all_entrances = list(world.get_entrances(player)) + all_entrances = list(multiworld.get_entrances(player)) local_random.shuffle(all_entrances) # First we take care of the one inconvenient dungeon in the appropriately simple shuffles. entrances_to_hint = {} entrances_to_hint.update(InconvenientDungeonEntrances) - if world.shuffle_ganon: - if world.worlds[player].options.mode == 'inverted': + if multiworld.shuffle_ganon: + if multiworld.worlds[player].options.mode == 'inverted': entrances_to_hint.update({'Inverted Ganons Tower': 'The sealed castle door'}) else: entrances_to_hint.update({'Ganons Tower': 'Ganon\'s Tower'}) - if world.worlds[player].options.entrance_shuffle in ['simple', 'restricted']: + if multiworld.worlds[player].options.entrance_shuffle in ['simple', 'restricted']: for entrance in all_entrances: if entrance.name in entrances_to_hint: this_hint = entrances_to_hint[entrance.name] + ' leads to ' + hint_text( @@ -2266,9 +2270,9 @@ def write_strings(rom, world, player): break # Now we write inconvenient locations for most shuffles and finish taking care of the less chaotic ones. entrances_to_hint.update(InconvenientOtherEntrances) - if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: + if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: hint_count = 0 - elif world.worlds[player].options.entrance_shuffle in ['simple', 'restricted']: + elif multiworld.worlds[player].options.entrance_shuffle in ['simple', 'restricted']: hint_count = 2 else: hint_count = 4 @@ -2285,31 +2289,31 @@ def write_strings(rom, world, player): # Next we handle hints for randomly selected other entrances, # curating the selection intelligently based on shuffle. - if world.worlds[player].options.entrance_shuffle not in ['simple', 'restricted']: + if multiworld.worlds[player].options.entrance_shuffle not in ['simple', 'restricted']: entrances_to_hint.update(ConnectorEntrances) entrances_to_hint.update(DungeonEntrances) - if world.worlds[player].options.mode == 'inverted': + if multiworld.worlds[player].options.mode == 'inverted': entrances_to_hint.update({'Inverted Agahnims Tower': 'The dark mountain tower'}) else: entrances_to_hint.update({'Agahnims Tower': 'The sealed castle door'}) - elif world.worlds[player].options.entrance_shuffle == 'restricted': + elif multiworld.worlds[player].options.entrance_shuffle == 'restricted': entrances_to_hint.update(ConnectorEntrances) entrances_to_hint.update(OtherEntrances) - if world.worlds[player].options.mode == 'inverted': + if multiworld.worlds[player].options.mode == 'inverted': entrances_to_hint.update({'Inverted Dark Sanctuary': 'The dark sanctuary cave'}) entrances_to_hint.update({'Inverted Big Bomb Shop': 'The old hero\'s dark home'}) entrances_to_hint.update({'Inverted Links House': 'The old hero\'s light home'}) else: entrances_to_hint.update({'Dark Sanctuary Hint': 'The dark sanctuary cave'}) entrances_to_hint.update({'Big Bomb Shop': 'The old bomb shop'}) - if world.worlds[player].options.entrance_shuffle != 'insanity': + if multiworld.worlds[player].options.entrance_shuffle != 'insanity': entrances_to_hint.update(InsanityEntrances) - if world.shuffle_ganon: - if world.worlds[player].options.mode == 'inverted': + if multiworld.shuffle_ganon: + if multiworld.worlds[player].options.mode == 'inverted': entrances_to_hint.update({'Inverted Pyramid Entrance': 'The extra castle passage'}) else: entrances_to_hint.update({'Pyramid Ledge': 'The pyramid ledge'}) - hint_count = 4 if world.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full', + hint_count = 4 if multiworld.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed'] else 0 for entrance in all_entrances: if entrance.name in entrances_to_hint: @@ -2324,77 +2328,77 @@ def write_strings(rom, world, player): # Next we write a few hints for specific inconvenient locations. We don't make many because in entrance this is highly unpredictable. locations_to_hint = InconvenientLocations.copy() - if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: + if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: locations_to_hint.extend(InconvenientVanillaLocations) local_random.shuffle(locations_to_hint) - hint_count = 3 if world.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full', + hint_count = 3 if multiworld.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed'] else 5 for location in locations_to_hint[:hint_count]: if location == 'Swamp Left': if local_random.randint(0, 1): - first_item = hint_text(world.get_location('Swamp Palace - West Chest', player).item) - second_item = hint_text(world.get_location('Swamp Palace - Big Key Chest', player).item) + first_item = hint_text(multiworld.get_location('Swamp Palace - West Chest', player).item) + second_item = hint_text(multiworld.get_location('Swamp Palace - Big Key Chest', player).item) else: - second_item = hint_text(world.get_location('Swamp Palace - West Chest', player).item) - first_item = hint_text(world.get_location('Swamp Palace - Big Key Chest', player).item) + second_item = hint_text(multiworld.get_location('Swamp Palace - West Chest', player).item) + first_item = hint_text(multiworld.get_location('Swamp Palace - Big Key Chest', player).item) this_hint = ('The westmost chests in Swamp Palace contain ' + first_item + ' and ' + second_item + '.') tt[hint_locations.pop(0)] = this_hint elif location == 'Mire Left': if local_random.randint(0, 1): - first_item = hint_text(world.get_location('Misery Mire - Compass Chest', player).item) - second_item = hint_text(world.get_location('Misery Mire - Big Key Chest', player).item) + first_item = hint_text(multiworld.get_location('Misery Mire - Compass Chest', player).item) + second_item = hint_text(multiworld.get_location('Misery Mire - Big Key Chest', player).item) else: - second_item = hint_text(world.get_location('Misery Mire - Compass Chest', player).item) - first_item = hint_text(world.get_location('Misery Mire - Big Key Chest', player).item) + second_item = hint_text(multiworld.get_location('Misery Mire - Compass Chest', player).item) + first_item = hint_text(multiworld.get_location('Misery Mire - Big Key Chest', player).item) this_hint = ('The westmost chests in Misery Mire contain ' + first_item + ' and ' + second_item + '.') tt[hint_locations.pop(0)] = this_hint elif location == 'Tower of Hera - Big Key Chest': this_hint = 'Waiting in the Tower of Hera basement leads to ' + hint_text( - world.get_location(location, player).item) + '.' + multiworld.get_location(location, player).item) + '.' tt[hint_locations.pop(0)] = this_hint elif location == 'Ganons Tower - Big Chest': this_hint = 'The big chest in Ganon\'s Tower contains ' + hint_text( - world.get_location(location, player).item) + '.' + multiworld.get_location(location, player).item) + '.' tt[hint_locations.pop(0)] = this_hint elif location == 'Thieves\' Town - Big Chest': this_hint = 'The big chest in Thieves\' Town contains ' + hint_text( - world.get_location(location, player).item) + '.' + multiworld.get_location(location, player).item) + '.' tt[hint_locations.pop(0)] = this_hint elif location == 'Ice Palace - Big Chest': this_hint = 'The big chest in Ice Palace contains ' + hint_text( - world.get_location(location, player).item) + '.' + multiworld.get_location(location, player).item) + '.' tt[hint_locations.pop(0)] = this_hint elif location == 'Eastern Palace - Big Key Chest': this_hint = 'The antifairy guarded chest in Eastern Palace contains ' + hint_text( - world.get_location(location, player).item) + '.' + multiworld.get_location(location, player).item) + '.' tt[hint_locations.pop(0)] = this_hint elif location == 'Sahasrahla': this_hint = 'Sahasrahla seeks a green pendant for ' + hint_text( - world.get_location(location, player).item) + '.' + multiworld.get_location(location, player).item) + '.' tt[hint_locations.pop(0)] = this_hint elif location == 'Graveyard Cave': this_hint = 'The cave north of the graveyard contains ' + hint_text( - world.get_location(location, player).item) + '.' + multiworld.get_location(location, player).item) + '.' tt[hint_locations.pop(0)] = this_hint else: - this_hint = location + ' contains ' + hint_text(world.get_location(location, player).item) + '.' + this_hint = location + ' contains ' + hint_text(multiworld.get_location(location, player).item) + '.' tt[hint_locations.pop(0)] = this_hint # Lastly we write hints to show where certain interesting items are. items_to_hint = RelevantItems.copy() - if world.worlds[player].options.small_key_shuffle.hints_useful: + if multiworld.worlds[player].options.small_key_shuffle.hints_useful: items_to_hint |= item_name_groups["Small Keys"] - if world.worlds[player].options.big_key_shuffle.hints_useful: + if multiworld.worlds[player].options.big_key_shuffle.hints_useful: items_to_hint |= item_name_groups["Big Keys"] - if world.worlds[player].options.hints == "full": + if multiworld.worlds[player].options.hints == "full": hint_count = len(hint_locations) # fill all remaining hint locations with Item hints. else: - hint_count = 5 if world.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full', + hint_count = 5 if multiworld.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed'] else 8 hint_count = min(hint_count, len(items_to_hint), len(hint_locations)) if hint_count: - locations = world.find_items_in_locations(items_to_hint, player, True) + locations = multiworld.find_items_in_locations(items_to_hint, player, True) local_random.shuffle(locations) # make locked locations less likely to appear as hint, # chances are the lock means the player already knows. @@ -2414,15 +2418,15 @@ def write_strings(rom, world, player): # We still need the older hints of course. Those are done here. - silverarrows = world.find_item_locations('Silver Bow', player, True) + silverarrows = multiworld.find_item_locations('Silver Bow', player, True) local_random.shuffle(silverarrows) silverarrow_hint = ( ' %s?' % hint_text(silverarrows[0]).replace('Ganon\'s', 'my')) if silverarrows else '?\nI think not!' tt['ganon_phase_3_no_silvers'] = 'Did you find the silver arrows%s' % silverarrow_hint tt['ganon_phase_3_no_silvers_alt'] = 'Did you find the silver arrows%s' % silverarrow_hint - if world.worlds[player].has_progressive_bows and (w.difficulty_requirements.progressive_bow_limit >= 2 or ( - world.worlds[player].options.swordless or world.worlds[player].options.glitches_required == 'no_glitches')): - prog_bow_locs = world.find_item_locations('Progressive Bow', player, True) + if multiworld.worlds[player].has_progressive_bows and (w.difficulty_requirements.progressive_bow_limit >= 2 or ( + multiworld.worlds[player].options.swordless or multiworld.worlds[player].options.glitches_required == 'no_glitches')): + prog_bow_locs = multiworld.find_item_locations('Progressive Bow', player, True) local_random.shuffle(prog_bow_locs) found_bow = False found_bow_alt = False @@ -2437,34 +2441,34 @@ def write_strings(rom, world, player): silverarrow_hint = (' %s?' % hint_text(bow_loc).replace('Ganon\'s', 'my')) tt[target] = 'Did you find the silver arrows%s' % silverarrow_hint - crystal5 = world.find_item('Crystal 5', player) - crystal6 = world.find_item('Crystal 6', player) + crystal5 = multiworld.find_item('Crystal 5', player) + crystal6 = multiworld.find_item('Crystal 6', player) tt['bomb_shop'] = 'Big Bomb?\nMy supply is blocked until you clear %s and %s.' % ( crystal5.hint_text, crystal6.hint_text) - greenpendant = world.find_item('Green Pendant', player) + greenpendant = multiworld.find_item('Green Pendant', player) tt['sahasrahla_bring_courage'] = 'I lost my family heirloom in %s' % greenpendant.hint_text - if world.worlds[player].options.crystals_needed_for_gt == 1: + if multiworld.worlds[player].options.crystals_needed_for_gt == 1: tt['sign_ganons_tower'] = 'You need a crystal to enter.' else: - tt['sign_ganons_tower'] = f'You need {world.worlds[player].options.crystals_needed_for_gt} crystals to enter.' + tt['sign_ganons_tower'] = f'You need {multiworld.worlds[player].options.crystals_needed_for_gt} crystals to enter.' - if world.worlds[player].options.goal == 'bosses': + if multiworld.worlds[player].options.goal == 'bosses': tt['sign_ganon'] = 'You need to kill all bosses, Ganon last.' - elif world.worlds[player].options.goal == 'ganon_pedestal': + elif multiworld.worlds[player].options.goal == 'ganon_pedestal': tt['sign_ganon'] = 'You need to pull the pedestal to defeat Ganon.' - elif world.worlds[player].options.goal == "ganon": - if world.worlds[player].options.crystals_needed_for_ganon == 1: + elif multiworld.worlds[player].options.goal == "ganon": + if multiworld.worlds[player].options.crystals_needed_for_ganon == 1: tt['sign_ganon'] = 'You need a crystal to beat Ganon and have beaten Agahnim atop Ganons Tower.' else: - tt['sign_ganon'] = f'You need {world.worlds[player].options.crystals_needed_for_ganon} crystals to beat Ganon and ' \ + tt['sign_ganon'] = f'You need {multiworld.worlds[player].options.crystals_needed_for_ganon} crystals to beat Ganon and ' \ f'have beaten Agahnim atop Ganons Tower' else: - if world.worlds[player].options.crystals_needed_for_ganon == 1: + if multiworld.worlds[player].options.crystals_needed_for_ganon == 1: tt['sign_ganon'] = 'You need a crystal to beat Ganon.' else: - tt['sign_ganon'] = f'You need {world.worlds[player].options.crystals_needed_for_ganon} crystals to beat Ganon.' + tt['sign_ganon'] = f'You need {multiworld.worlds[player].options.crystals_needed_for_ganon} crystals to beat Ganon.' tt['uncle_leaving_text'] = Uncle_texts[local_random.randint(0, len(Uncle_texts) - 1)] tt['end_triforce'] = "{NOBORDER}\n" + Triforce_texts[local_random.randint(0, len(Triforce_texts) - 1)] @@ -2475,12 +2479,12 @@ def write_strings(rom, world, player): tt['blind_by_the_light'] = Blind_texts[local_random.randint(0, len(Blind_texts) - 1)] triforce_pieces_required = max(0, w.treasure_hunt_required - - sum(1 for item in world.precollected_items[player] if item.name == "Triforce Piece")) + sum(1 for item in multiworld.precollected_items[player] if item.name == "Triforce Piece")) - if world.worlds[player].options.goal in ['triforce_hunt', 'local_triforce_hunt']: + if multiworld.worlds[player].options.goal in ['triforce_hunt', 'local_triforce_hunt']: tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Get the Triforce Pieces.' tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.' - if world.worlds[player].options.goal == 'triforce_hunt' and world.players > 1: + if multiworld.worlds[player].options.goal == 'triforce_hunt' and multiworld.players > 1: tt['sign_ganon'] = 'Go find the Triforce pieces with your friends... Ganon is invincible!' else: tt['sign_ganon'] = 'Go find the Triforce pieces... Ganon is invincible!' @@ -2494,7 +2498,7 @@ def write_strings(rom, world, player): "invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \ "hidden in a hollow tree. If you bring\n%d Triforce piece out of %d, I can reassemble it." % \ (triforce_pieces_required, w.treasure_hunt_total) - elif world.worlds[player].options.goal in ['pedestal']: + elif multiworld.worlds[player].options.goal in ['pedestal']: tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Your goal is at the pedestal.' tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.' tt['sign_ganon'] = 'You need to get to the pedestal... Ganon is invincible!' @@ -2503,44 +2507,44 @@ def write_strings(rom, world, player): tt['ganon_fall_in_alt'] = 'You cannot defeat me until you finish your goal!' tt['ganon_phase_3_alt'] = 'Got wax in\nyour ears?\nI can not die!' if triforce_pieces_required > 1: - if world.worlds[player].options.goal == 'ganon_triforce_hunt' and world.players > 1: + if multiworld.worlds[player].options.goal == 'ganon_triforce_hunt' and multiworld.players > 1: tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d with your friends to defeat Ganon.' % \ (triforce_pieces_required, w.treasure_hunt_total) - elif world.worlds[player].options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']: + elif multiworld.worlds[player].options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']: tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d to defeat Ganon.' % \ (triforce_pieces_required, w.treasure_hunt_total) else: - if world.worlds[player].options.goal == 'ganon_triforce_hunt' and world.players > 1: + if multiworld.worlds[player].options.goal == 'ganon_triforce_hunt' and multiworld.players > 1: tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d with your friends to defeat Ganon.' % \ (triforce_pieces_required, w.treasure_hunt_total) - elif world.worlds[player].options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']: + elif multiworld.worlds[player].options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']: tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d to defeat Ganon.' % \ (triforce_pieces_required, w.treasure_hunt_total) tt['kakariko_tavern_fisherman'] = TavernMan_texts[local_random.randint(0, len(TavernMan_texts) - 1)] - pedestalitem = world.get_location('Master Sword Pedestal', player).item + pedestalitem = multiworld.get_location('Master Sword Pedestal', player).item pedestal_text = 'Some Hot Air' if pedestalitem is None else hint_text(pedestalitem, True) if pedestalitem.pedestal_hint_text is not None else 'Unknown Item' tt['mastersword_pedestal_translated'] = pedestal_text pedestal_credit_text = 'and the Hot Air' if pedestalitem is None else \ w.pedestal_credit_texts.get(pedestalitem.code, 'and the Unknown Item') - etheritem = world.get_location('Ether Tablet', player).item + etheritem = multiworld.get_location('Ether Tablet', player).item ether_text = 'Some Hot Air' if etheritem is None else hint_text(etheritem, True) if etheritem.pedestal_hint_text is not None else 'Unknown Item' tt['tablet_ether_book'] = ether_text - bombositem = world.get_location('Bombos Tablet', player).item + bombositem = multiworld.get_location('Bombos Tablet', player).item bombos_text = 'Some Hot Air' if bombositem is None else hint_text(bombositem, True) if bombositem.pedestal_hint_text is not None else 'Unknown Item' tt['tablet_bombos_book'] = bombos_text # inverted spawn menu changes - if world.worlds[player].options.mode == 'inverted': + if multiworld.worlds[player].options.mode == 'inverted': tt['menu_start_2'] = "{MENU}\n{SPEED0}\n≥@'s house\n Dark Chapel\n{CHOICE3}" tt['menu_start_3'] = "{MENU}\n{SPEED0}\n≥@'s house\n Dark Chapel\n Mountain Cave\n{CHOICE2}" - for at, text, _ in world.worlds[player].options.plando_texts: + for at, text, _ in multiworld.worlds[player].options.plando_texts: if at not in tt: raise Exception(f"No text target \"{at}\" found.") @@ -2551,22 +2555,22 @@ def write_strings(rom, world, player): credits = Credits() - sickkiditem = world.get_location('Sick Kid', player).item + sickkiditem = multiworld.get_location('Sick Kid', player).item sickkiditem_text = local_random.choice(SickKid_texts) \ if sickkiditem is None or sickkiditem.code not in w.sickkid_credit_texts \ else w.sickkid_credit_texts[sickkiditem.code] - zoraitem = world.get_location('King Zora', player).item + zoraitem = multiworld.get_location('King Zora', player).item zoraitem_text = local_random.choice(Zora_texts) \ if zoraitem is None or zoraitem.code not in w.zora_credit_texts \ else w.zora_credit_texts[zoraitem.code] - magicshopitem = world.get_location('Potion Shop', player).item + magicshopitem = multiworld.get_location('Potion Shop', player).item magicshopitem_text = local_random.choice(MagicShop_texts) \ if magicshopitem is None or magicshopitem.code not in w.magicshop_credit_texts \ else w.magicshop_credit_texts[magicshopitem.code] - fluteboyitem = world.get_location('Flute Spot', player).item + fluteboyitem = multiworld.get_location('Flute Spot', player).item fluteboyitem_text = local_random.choice(FluteBoy_texts) \ if fluteboyitem is None or fluteboyitem.code not in w.fluteboy_credit_texts \ else w.fluteboy_credit_texts[fluteboyitem.code] @@ -2595,7 +2599,7 @@ def write_strings(rom, world, player): rom.write_bytes(0x76CC0, [byte for p in pointers for byte in [p & 0xFF, p >> 8 & 0xFF]]) -def set_inverted_mode(world, player, rom): +def set_inverted_mode(multiworld: MultiWorld, player: int, rom: LocalRom): rom.write_byte(snes_to_pc(0x0283E0), 0xF0) # residual portals rom.write_byte(snes_to_pc(0x02B34D), 0xF0) rom.write_byte(snes_to_pc(0x06DB78), 0x8B) @@ -2613,12 +2617,12 @@ def set_inverted_mode(world, player, rom): rom.write_byte(snes_to_pc(0x08D40C), 0xD0) # morph proof # the following bytes should only be written in vanilla # or they'll overwrite the randomizer's shuffles - if world.worlds[player].options.entrance_shuffle == 'vanilla': + if multiworld.worlds[player].options.entrance_shuffle == 'vanilla': rom.write_byte(0xDBB73 + 0x23, 0x37) # switch AT and GT rom.write_byte(0xDBB73 + 0x36, 0x24) rom.write_int16(0x15AEE + 2 * 0x38, 0x00E0) rom.write_int16(0x15AEE + 2 * 0x25, 0x000C) - if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: + if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: rom.write_byte(0x15B8C, 0x6C) rom.write_byte(0xDBB73 + 0x00, 0x53) # switch bomb shop and links house rom.write_byte(0xDBB73 + 0x52, 0x01) @@ -2676,7 +2680,7 @@ def set_inverted_mode(world, player, rom): rom.write_int16(snes_to_pc(0x02D9A6), 0x005A) rom.write_byte(snes_to_pc(0x02D9B3), 0x12) # keep the old man spawn point at old man house unless shuffle is vanilla - if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']: + if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']: rom.write_bytes(snes_to_pc(0x308350), [0x00, 0x00, 0x01]) rom.write_int16(snes_to_pc(0x02D8DE), 0x00F1) rom.write_bytes(snes_to_pc(0x02D910), [0x1F, 0x1E, 0x1F, 0x1F, 0x03, 0x02, 0x03, 0x03]) @@ -2739,7 +2743,7 @@ def set_inverted_mode(world, player, rom): rom.write_int16s(snes_to_pc(0x1bb836), [0x001B, 0x001B, 0x001B]) rom.write_int16(snes_to_pc(0x308300), 0x0140) # new pyramid hole entrance rom.write_int16(snes_to_pc(0x308320), 0x001B) - if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: + if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: rom.write_byte(snes_to_pc(0x308340), 0x7B) rom.write_int16(snes_to_pc(0x1af504), 0x148B) rom.write_int16(snes_to_pc(0x1af50c), 0x149B) @@ -2776,10 +2780,10 @@ def set_inverted_mode(world, player, rom): rom.write_bytes(snes_to_pc(0x1BC85A), [0x50, 0x0F, 0x82]) rom.write_int16(0xDB96F + 2 * 0x35, 0x001B) # move pyramid exit door rom.write_int16(0xDBA71 + 2 * 0x35, 0x06A4) - if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: + if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: rom.write_byte(0xDBB73 + 0x35, 0x36) rom.write_byte(snes_to_pc(0x09D436), 0xF3) # remove castle gate warp - if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: + if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: rom.write_int16(0x15AEE + 2 * 0x37, 0x0010) # pyramid exit to new hc area rom.write_byte(0x15B8C + 0x37, 0x1B) rom.write_int16(0x15BDB + 2 * 0x37, 0x0418) diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index 18e2965d8c..bce75a157b 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -1,9 +1,9 @@ import collections import logging -from typing import Iterator, Set +from typing import Callable, Iterator, Set, TYPE_CHECKING from Options import ItemsAccessibility -from BaseClasses import MultiWorld +from BaseClasses import CollectionState, Item, Location, MultiWorld, Region from worlds.generic.Rules import (add_item_rule, add_rule, forbid_item, item_name_in_location_names, location_item_name, set_rule, allow_self_locking_items) @@ -21,112 +21,116 @@ from .StateHelpers import (can_extend_magic, can_kill_most_things, has_misery_mire_medallion, has_sword, has_turtle_rock_medallion, has_triforce_pieces, can_use_bombs, can_bomb_or_bonk, can_activate_crystal_switch, can_kill_standard_start) +from .SubClasses import ALttPLocation from .UnderworldGlitchRules import underworld_glitches_rules +if TYPE_CHECKING: + from . import ALTTPWorld -def set_rules(world): + +def set_rules(world: "ALTTPWorld"): player = world.player - world = world.multiworld - if world.worlds[player].options.glitches_required == 'no_logic': - if player == next(player_id for player_id in world.get_game_players("A Link to the Past") - if world.worlds[player_id].options.glitches_required == 'no_logic'): # only warn one time + multiworld = world.multiworld + if world.options.glitches_required == 'no_logic': + if player == next(player_id for player_id in multiworld.get_game_players("A Link to the Past") + if multiworld.worlds[player_id].options.glitches_required == 'no_logic'): # only warn one time logging.info( 'WARNING! Seeds generated under this logic often require major glitches and may be impossible!') - if world.players == 1: - for exit in world.get_region('Menu', player).exits: + if multiworld.players == 1: + for exit in multiworld.get_region('Menu', player).exits: exit.hide_path = True return else: # Set access rules according to max glitches for multiworld progression. # Set accessibility to none, and shuffle assuming the no logic players can always win - world.worlds[player].options.accessibility.value = ItemsAccessibility.option_minimal - world.worlds[player].options.progression_balancing.value = 0 + world.options.accessibility.value = ItemsAccessibility.option_minimal + world.options.progression_balancing.value = 0 else: - world.completion_condition[player] = lambda state: state.has('Triforce', player) + multiworld.completion_condition[player] = lambda state: state.has('Triforce', player) - dungeon_boss_rules(world, player) - global_rules(world, player) + dungeon_boss_rules(multiworld, player) + global_rules(multiworld, player) - if world.worlds[player].options.mode != 'inverted': - default_rules(world, player) + if world.options.mode != 'inverted': + default_rules(multiworld, player) - if world.worlds[player].options.mode == 'open': - open_rules(world, player) - elif world.worlds[player].options.mode == 'standard': - standard_rules(world, player) - elif world.worlds[player].options.mode == 'inverted': - open_rules(world, player) - inverted_rules(world, player) + if world.options.mode == 'open': + open_rules(multiworld, player) + elif world.options.mode == 'standard': + standard_rules(multiworld, player) + elif world.options.mode == 'inverted': + open_rules(multiworld, player) + inverted_rules(multiworld, player) else: - raise NotImplementedError(f'World state {world.worlds[player].options.mode} is not implemented yet') + raise NotImplementedError(f'World state {world.options.mode} is not implemented yet') - if world.worlds[player].options.glitches_required == 'no_glitches': - no_glitches_rules(world, player) - forbid_bomb_jump_requirements(world, player) - elif world.worlds[player].options.glitches_required == 'overworld_glitches': + if world.options.glitches_required == 'no_glitches': + no_glitches_rules(multiworld, player) + forbid_bomb_jump_requirements(multiworld, player) + elif world.options.glitches_required == 'overworld_glitches': # Initially setting no_glitches_rules to set the baseline rules for some # entrances. The overworld_glitches_rules set is primarily additive. - no_glitches_rules(world, player) - fake_flipper_rules(world, player) - overworld_glitches_rules(world, player) - forbid_bomb_jump_requirements(world, player) - elif world.worlds[player].options.glitches_required in ['hybrid_major_glitches', 'no_logic']: - no_glitches_rules(world, player) - fake_flipper_rules(world, player) - overworld_glitches_rules(world, player) - underworld_glitches_rules(world, player) - bomb_jump_requirements(world, player) - elif world.worlds[player].options.glitches_required == 'minor_glitches': - no_glitches_rules(world, player) - fake_flipper_rules(world, player) - forbid_bomb_jump_requirements(world, player) + no_glitches_rules(multiworld, player) + fake_flipper_rules(multiworld, player) + overworld_glitches_rules(multiworld, player) + forbid_bomb_jump_requirements(multiworld, player) + elif world.options.glitches_required in ['hybrid_major_glitches', 'no_logic']: + no_glitches_rules(multiworld, player) + fake_flipper_rules(multiworld, player) + overworld_glitches_rules(multiworld, player) + underworld_glitches_rules(multiworld, player) + bomb_jump_requirements(multiworld, player) + elif world.options.glitches_required == 'minor_glitches': + no_glitches_rules(multiworld, player) + fake_flipper_rules(multiworld, player) + forbid_bomb_jump_requirements(multiworld, player) else: - raise NotImplementedError(f'Not implemented yet: Logic - {world.worlds[player].options.glitches_required}') + raise NotImplementedError(f'Not implemented yet: Logic - {world.options.glitches_required}') - if world.worlds[player].options.goal == 'bosses': + if world.options.goal == 'bosses': # require all bosses to beat ganon - add_rule(world.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player) and state.has('Beat Agahnim 1', player) and state.has('Beat Agahnim 2', player) and has_crystals(state, 7, player)) - elif world.worlds[player].options.goal == 'ganon': + add_rule(multiworld.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player) and state.has('Beat Agahnim 1', player) and state.has('Beat Agahnim 2', player) and has_crystals(state, 7, player)) + elif world.options.goal == 'ganon': # require aga2 to beat ganon - add_rule(world.get_location('Ganon', player), lambda state: state.has('Beat Agahnim 2', player)) + add_rule(multiworld.get_location('Ganon', player), lambda state: state.has('Beat Agahnim 2', player)) - if world.worlds[player].options.mode != 'inverted': - set_big_bomb_rules(world, player) - if world.worlds[player].options.glitches_required.current_key in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and world.worlds[player].options.entrance_shuffle.current_key not in {'insanity', 'insanity_legacy', 'madness'}: - path_to_courtyard = mirrorless_path_to_castle_courtyard(world, player) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.multiworld.get_entrance('Dark Death Mountain Offset Mirror', player).can_reach(state) and all(rule(state) for rule in path_to_courtyard), 'or') + if world.options.mode != 'inverted': + set_big_bomb_rules(multiworld, player) + if world.options.glitches_required.current_key in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and world.options.entrance_shuffle.current_key not in {'insanity', 'insanity_legacy', 'madness'}: + path_to_courtyard = mirrorless_path_to_castle_courtyard(multiworld, player) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.multiworld.get_entrance('Dark Death Mountain Offset Mirror', player).can_reach(state) and all(rule(state) for rule in path_to_courtyard), 'or') else: - set_inverted_big_bomb_rules(world, player) + set_inverted_big_bomb_rules(multiworld, player) # if swamp and dam have not been moved we require mirror for swamp palace # however there is mirrorless swamp in hybrid MG, so we don't necessarily want this. HMG handles this requirement itself. - if not world.worlds[player].swamp_patch_required and world.worlds[player].options.glitches_required not in ['hybrid_major_glitches', 'no_logic']: - add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player)) + if not multiworld.worlds[player].swamp_patch_required and world.options.glitches_required not in ['hybrid_major_glitches', 'no_logic']: + add_rule(multiworld.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player)) # GT Entrance may be required for Turtle Rock for OWG and < 7 required - ganons_tower = world.get_entrance('Inverted Ganons Tower' if world.worlds[player].options.mode == 'inverted' else 'Ganons Tower', player) - if (world.worlds[player].options.crystals_needed_for_gt == 7 - and not (world.worlds[player].options.glitches_required + ganons_tower = multiworld.get_entrance('Inverted Ganons Tower' if world.options.mode == 'inverted' else 'Ganons Tower', player) + if (world.options.crystals_needed_for_gt == 7 + and not (world.options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic'] - and world.worlds[player].options.mode != 'inverted')): + and world.options.mode != 'inverted')): set_rule(ganons_tower, lambda state: False) - set_trock_key_rules(world, player) + set_trock_key_rules(multiworld, player) - set_rule(ganons_tower, lambda state: has_crystals(state, state.multiworld.worlds[player].options.crystals_needed_for_gt, player)) - if world.worlds[player].options.mode != 'inverted' and world.worlds[player].options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']: - add_rule(world.get_entrance('Ganons Tower', player), lambda state: state.multiworld.get_entrance('Ganons Tower Ascent', player).can_reach(state), 'or') + set_rule(ganons_tower, lambda state: has_crystals(state, world.options.crystals_needed_for_gt.value, player)) + if world.options.mode != 'inverted' and world.options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']: + add_rule(multiworld.get_entrance('Ganons Tower', player), lambda state: state.multiworld.get_entrance('Ganons Tower Ascent', player).can_reach(state), 'or') - set_bunny_rules(world, player, world.worlds[player].options.mode == 'inverted') + set_bunny_rules(multiworld, player, world.options.mode == 'inverted') -def mirrorless_path_to_castle_courtyard(world, player): +def mirrorless_path_to_castle_courtyard(multiworld: MultiWorld, player: int): # If Agahnim is defeated then the courtyard needs to be accessible without using the mirror for the mirror offset glitch. # Only considering the secret passage for now (in non-insanity shuffle). Basically, if it's Ganon you need the master sword. - start = world.get_entrance('Hyrule Castle Secret Entrance Drop', player) - target = world.get_region('Hyrule Castle Courtyard', player) + start = multiworld.get_entrance('Hyrule Castle Secret Entrance Drop', player) + target = multiworld.get_region('Hyrule Castle Courtyard', player) seen = {start.parent_region, start.connected_region} queue = collections.deque([(start.connected_region, [])]) while queue: @@ -139,37 +143,37 @@ def mirrorless_path_to_castle_courtyard(world, player): else: queue.append((entrance.connected_region, new_path)) - raise Exception(f"Could not find mirrorless path to castle courtyard for Player {player} ({world.get_player_name(player)})") + raise Exception(f"Could not find mirrorless path to castle courtyard for Player {player} ({multiworld.get_player_name(player)})") -def set_defeat_dungeon_boss_rule(location): +def set_defeat_dungeon_boss_rule(location: ALttPLocation): # Lambda required to defer evaluation of dungeon.boss since it will change later if boss shuffle is used add_rule(location, lambda state: location.parent_region.dungeon.boss.can_defeat(state)) -def set_always_allow(spot, rule): +def set_always_allow(spot: Location, rule: Callable[[CollectionState, Item], bool]): spot.always_allow = rule -def add_lamp_requirement(world: MultiWorld, spot, player: int, has_accessible_torch: bool = False): - if world.worlds[player].options.dark_room_logic == "lamp": +def add_lamp_requirement(multiworld: MultiWorld, spot, player: int, has_accessible_torch: bool = False): + if multiworld.worlds[player].options.dark_room_logic == "lamp": add_rule(spot, lambda state: state.has('Lamp', player)) - elif world.worlds[player].options.dark_room_logic == "torches": # implicitly lamp as well + elif multiworld.worlds[player].options.dark_room_logic == "torches": # implicitly lamp as well if has_accessible_torch: add_rule(spot, lambda state: state.has('Lamp', player) or state.has('Fire Rod', player)) else: add_rule(spot, lambda state: state.has('Lamp', player)) - elif world.worlds[player].options.dark_room_logic == "none": + elif multiworld.worlds[player].options.dark_room_logic == "none": pass else: - raise ValueError(f"Unknown Dark Room Logic: {world.worlds[player].options.dark_room_logic}") + raise ValueError(f"Unknown Dark Room Logic: {multiworld.worlds[player].options.dark_room_logic}") non_crossover_items = (item_name_groups["Small Keys"] | item_name_groups["Big Keys"] | progression_items) - { "Small Key (Universal)"} -def dungeon_boss_rules(world, player): +def dungeon_boss_rules(multiworld: MultiWorld, player: int): boss_locations = { 'Agahnim 1', 'Tower of Hera - Boss', @@ -190,7 +194,7 @@ def dungeon_boss_rules(world, player): 'Palace of Darkness - Prize', } for location in boss_locations: - set_defeat_dungeon_boss_rule(world.get_location(location, player)) + set_defeat_dungeon_boss_rule(multiworld.get_location(location, player)) def global_rules(multiworld: MultiWorld, player: int): @@ -616,330 +620,332 @@ def global_rules(multiworld: MultiWorld, player: int): set_rule(multiworld.get_location('Flute Activation Spot', player), lambda state: state.has('Flute', player)) -def default_rules(world, player): +def default_rules(multiworld: MultiWorld, player: int): """Default world rules when world state is not inverted.""" # overworld requirements - set_rule(world.get_entrance('Light World Bomb Hut', player), lambda state: can_use_bombs(state, player)) - set_rule(world.get_entrance('Light Hype Fairy', player), lambda state: can_use_bombs(state, player)) - set_rule(world.get_entrance('Mini Moldorm Cave', player), lambda state: can_use_bombs(state, player)) - set_rule(world.get_entrance('Ice Rod Cave', player), lambda state: can_use_bombs(state, player)) + set_rule(multiworld.get_entrance('Light World Bomb Hut', player), lambda state: can_use_bombs(state, player)) + set_rule(multiworld.get_entrance('Light Hype Fairy', player), lambda state: can_use_bombs(state, player)) + set_rule(multiworld.get_entrance('Mini Moldorm Cave', player), lambda state: can_use_bombs(state, player)) + set_rule(multiworld.get_entrance('Ice Rod Cave', player), lambda state: can_use_bombs(state, player)) - set_rule(world.get_entrance('Kings Grave', player), lambda state: state.has('Pegasus Boots', player)) - set_rule(world.get_entrance('Kings Grave Outer Rocks', player), lambda state: can_lift_heavy_rocks(state, player)) - set_rule(world.get_entrance('Kings Grave Inner Rocks', player), lambda state: can_lift_heavy_rocks(state, player)) - set_rule(world.get_entrance('Kings Grave Mirror Spot', player), lambda state: state.has('Moon Pearl', player) and state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Kings Grave', player), lambda state: state.has('Pegasus Boots', player)) + set_rule(multiworld.get_entrance('Kings Grave Outer Rocks', player), lambda state: can_lift_heavy_rocks(state, player)) + set_rule(multiworld.get_entrance('Kings Grave Inner Rocks', player), lambda state: can_lift_heavy_rocks(state, player)) + set_rule(multiworld.get_entrance('Kings Grave Mirror Spot', player), lambda state: state.has('Moon Pearl', player) and state.has('Magic Mirror', player)) # Caution: If king's grave is releaxed at all to account for reaching it via a two way cave's exit in insanity mode, then the bomb shop logic will need to be updated (that would involve create a small ledge-like Region for it) - set_rule(world.get_entrance('Bonk Fairy (Light)', player), lambda state: state.has('Pegasus Boots', player)) - set_rule(world.get_entrance('Lumberjack Tree Tree', player), lambda state: state.has('Pegasus Boots', player) and state.has('Beat Agahnim 1', player)) - set_rule(world.get_entrance('Bonk Rock Cave', player), lambda state: state.has('Pegasus Boots', player)) - set_rule(world.get_entrance('Desert Palace Stairs', player), lambda state: state.has('Book of Mudora', player)) - set_rule(world.get_entrance('Sanctuary Grave', player), lambda state: can_lift_rocks(state, player)) - set_rule(world.get_entrance('20 Rupee Cave', player), lambda state: can_lift_rocks(state, player)) - set_rule(world.get_entrance('50 Rupee Cave', player), lambda state: can_lift_rocks(state, player)) - set_rule(world.get_entrance('Death Mountain Entrance Rock', player), lambda state: can_lift_rocks(state, player)) - set_rule(world.get_entrance('Bumper Cave Entrance Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Flute Spot 1', player), lambda state: state.has('Activated Flute', player)) - set_rule(world.get_entrance('Lake Hylia Central Island Teleporter', player), lambda state: can_lift_heavy_rocks(state, player)) - set_rule(world.get_entrance('Dark Desert Teleporter', player), lambda state: state.has('Activated Flute', player) and can_lift_heavy_rocks(state, player)) - set_rule(world.get_entrance('East Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer - set_rule(world.get_entrance('South Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer - set_rule(world.get_entrance('Kakariko Teleporter', player), lambda state: ((state.has('Hammer', player) and can_lift_rocks(state, player)) or can_lift_heavy_rocks(state, player)) and state.has('Moon Pearl', player)) # bunny cannot lift bushes - set_rule(world.get_location('Flute Spot', player), lambda state: state.has('Shovel', player)) - set_rule(world.get_entrance('Bat Cave Drop Ledge', player), lambda state: state.has('Hammer', player)) + set_rule(multiworld.get_entrance('Bonk Fairy (Light)', player), lambda state: state.has('Pegasus Boots', player)) + set_rule(multiworld.get_entrance('Lumberjack Tree Tree', player), lambda state: state.has('Pegasus Boots', player) and state.has('Beat Agahnim 1', player)) + set_rule(multiworld.get_entrance('Bonk Rock Cave', player), lambda state: state.has('Pegasus Boots', player)) + set_rule(multiworld.get_entrance('Desert Palace Stairs', player), lambda state: state.has('Book of Mudora', player)) + set_rule(multiworld.get_entrance('Sanctuary Grave', player), lambda state: can_lift_rocks(state, player)) + set_rule(multiworld.get_entrance('20 Rupee Cave', player), lambda state: can_lift_rocks(state, player)) + set_rule(multiworld.get_entrance('50 Rupee Cave', player), lambda state: can_lift_rocks(state, player)) + set_rule(multiworld.get_entrance('Death Mountain Entrance Rock', player), lambda state: can_lift_rocks(state, player)) + set_rule(multiworld.get_entrance('Bumper Cave Entrance Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Flute Spot 1', player), lambda state: state.has('Activated Flute', player)) + set_rule(multiworld.get_entrance('Lake Hylia Central Island Teleporter', player), lambda state: can_lift_heavy_rocks(state, player)) + set_rule(multiworld.get_entrance('Dark Desert Teleporter', player), lambda state: state.has('Activated Flute', player) and can_lift_heavy_rocks(state, player)) + set_rule(multiworld.get_entrance('East Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer + set_rule(multiworld.get_entrance('South Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer + set_rule(multiworld.get_entrance('Kakariko Teleporter', player), lambda state: ((state.has('Hammer', player) and can_lift_rocks(state, player)) or can_lift_heavy_rocks(state, player)) and state.has('Moon Pearl', player)) # bunny cannot lift bushes + set_rule(multiworld.get_location('Flute Spot', player), lambda state: state.has('Shovel', player)) + set_rule(multiworld.get_entrance('Bat Cave Drop Ledge', player), lambda state: state.has('Hammer', player)) - set_rule(world.get_location('Zora\'s Ledge', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_entrance('Waterfall of Wishing', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_location('Frog', player), lambda state: can_lift_heavy_rocks(state, player)) # will get automatic moon pearl requirement - set_rule(world.get_location('Potion Shop', player), lambda state: state.has('Mushroom', player)) - set_rule(world.get_entrance('Desert Palace Entrance (North) Rocks', player), lambda state: can_lift_rocks(state, player)) - set_rule(world.get_entrance('Desert Ledge Return Rocks', player), lambda state: can_lift_rocks(state, player)) # should we decide to place something that is not a dungeon end up there at some point - set_rule(world.get_entrance('Checkerboard Cave', player), lambda state: can_lift_rocks(state, player)) - set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or has_beam_sword(state, player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle - set_rule(world.get_entrance('Top of Pyramid', player), lambda state: state.has('Beat Agahnim 1', player)) - set_rule(world.get_entrance('Old Man Cave Exit (West)', player), lambda state: False) # drop cannot be climbed up - set_rule(world.get_entrance('Broken Bridge (West)', player), lambda state: state.has('Hookshot', player)) - set_rule(world.get_entrance('Broken Bridge (East)', player), lambda state: state.has('Hookshot', player)) - set_rule(world.get_entrance('East Death Mountain Teleporter', player), lambda state: can_lift_heavy_rocks(state, player)) - set_rule(world.get_entrance('Fairy Ascension Rocks', player), lambda state: can_lift_heavy_rocks(state, player)) - set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: state.has('Mirror', player)) # can erase block - set_rule(world.get_entrance('Death Mountain (Top)', player), lambda state: state.has('Hammer', player)) - set_rule(world.get_entrance('Turtle Rock Teleporter', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) - set_rule(world.get_entrance('East Death Mountain (Top)', player), lambda state: state.has('Hammer', player)) + set_rule(multiworld.get_location('Zora\'s Ledge', player), lambda state: state.has('Flippers', player)) + set_rule(multiworld.get_entrance('Waterfall of Wishing', player), lambda state: state.has('Flippers', player)) + set_rule(multiworld.get_location('Frog', player), lambda state: can_lift_heavy_rocks(state, player)) # will get automatic moon pearl requirement + set_rule(multiworld.get_location('Potion Shop', player), lambda state: state.has('Mushroom', player)) + set_rule(multiworld.get_entrance('Desert Palace Entrance (North) Rocks', player), lambda state: can_lift_rocks(state, player)) + set_rule(multiworld.get_entrance('Desert Ledge Return Rocks', player), lambda state: can_lift_rocks(state, player)) # should we decide to place something that is not a dungeon end up there at some point + set_rule(multiworld.get_entrance('Checkerboard Cave', player), lambda state: can_lift_rocks(state, player)) + set_rule(multiworld.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or has_beam_sword(state, player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle + set_rule(multiworld.get_entrance('Top of Pyramid', player), lambda state: state.has('Beat Agahnim 1', player)) + set_rule(multiworld.get_entrance('Old Man Cave Exit (West)', player), lambda state: False) # drop cannot be climbed up + set_rule(multiworld.get_entrance('Broken Bridge (West)', player), lambda state: state.has('Hookshot', player)) + set_rule(multiworld.get_entrance('Broken Bridge (East)', player), lambda state: state.has('Hookshot', player)) + set_rule(multiworld.get_entrance('East Death Mountain Teleporter', player), lambda state: can_lift_heavy_rocks(state, player)) + set_rule(multiworld.get_entrance('Fairy Ascension Rocks', player), lambda state: can_lift_heavy_rocks(state, player)) + set_rule(multiworld.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: state.has('Mirror', player)) # can erase block + set_rule(multiworld.get_entrance('Death Mountain (Top)', player), lambda state: state.has('Hammer', player)) + set_rule(multiworld.get_entrance('Turtle Rock Teleporter', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) + set_rule(multiworld.get_entrance('East Death Mountain (Top)', player), lambda state: state.has('Hammer', player)) - set_rule(world.get_entrance('Catfish Exit Rock', player), lambda state: can_lift_rocks(state, player)) - set_rule(world.get_entrance('Catfish Entrance Rock', player), lambda state: can_lift_rocks(state, player)) - set_rule(world.get_entrance('Northeast Dark World Broken Bridge Pass', player), lambda state: state.has('Moon Pearl', player) and (can_lift_rocks(state, player) or state.has('Hammer', player) or state.has('Flippers', player))) - set_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: state.has('Moon Pearl', player) and (can_lift_rocks(state, player) or state.has('Hammer', player))) - set_rule(world.get_entrance('South Dark World Bridge', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Bonk Fairy (Dark)', player), lambda state: state.has('Moon Pearl', player) and state.has('Pegasus Boots', player)) - set_rule(world.get_entrance('West Dark World Gap', player), lambda state: state.has('Moon Pearl', player) and state.has('Hookshot', player)) - set_rule(world.get_entrance('Palace of Darkness', player), lambda state: state.has('Moon Pearl', player)) # kiki needs pearl - set_rule(world.get_entrance('Hyrule Castle Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Hyrule Castle Main Gate', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: (state.has('Moon Pearl', player) and state.has('Flippers', player) or state.has('Magic Mirror', player))) # Overworld Bunny Revival - set_rule(world.get_location('Bombos Tablet', player), lambda state: can_retrieve_tablet(state, player)) - set_rule(world.get_entrance('Dark Lake Hylia Drop (South)', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # ToDo any fake flipper set up? - set_rule(world.get_entrance('Dark Lake Hylia Ledge Fairy', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) - set_rule(world.get_entrance('Dark Lake Hylia Ledge Spike Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Village of Outcasts Heavy Rock', player), lambda state: state.has('Moon Pearl', player) and can_lift_heavy_rocks(state, player)) - set_rule(world.get_entrance('Hype Cave', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) - set_rule(world.get_entrance('Brewery', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) - set_rule(world.get_entrance('Thieves Town', player), lambda state: state.has('Moon Pearl', player)) # bunny cannot pull - set_rule(world.get_entrance('Skull Woods First Section Hole (North)', player), lambda state: state.has('Moon Pearl', player)) # bunny cannot lift bush - set_rule(world.get_entrance('Skull Woods Second Section Hole', player), lambda state: state.has('Moon Pearl', player)) # bunny cannot lift bush - set_rule(world.get_entrance('Maze Race Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Cave 45 Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Bombos Tablet Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('East Dark World Bridge', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player)) - set_rule(world.get_entrance('Lake Hylia Island Mirror Spot', player), lambda state: state.has('Moon Pearl', player) and state.has('Magic Mirror', player) and state.has('Flippers', player)) - set_rule(world.get_entrance('Lake Hylia Central Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('East Dark World River Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) - set_rule(world.get_entrance('Graveyard Ledge Mirror Spot', player), lambda state: state.has('Moon Pearl', player) and state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Bumper Cave Entrance Rock', player), lambda state: state.has('Moon Pearl', player) and can_lift_rocks(state, player)) - set_rule(world.get_entrance('Bumper Cave Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Bat Cave Drop Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Dark World Hammer Peg Cave', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player)) - set_rule(world.get_entrance('Village of Outcasts Eastern Rocks', player), lambda state: state.has('Moon Pearl', player) and can_lift_heavy_rocks(state, player)) - set_rule(world.get_entrance('Peg Area Rocks', player), lambda state: state.has('Moon Pearl', player) and can_lift_heavy_rocks(state, player)) - set_rule(world.get_entrance('Village of Outcasts Pegs', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player)) - set_rule(world.get_entrance('Grassy Lawn Pegs', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player)) - set_rule(world.get_entrance('Bumper Cave Exit (Top)', player), lambda state: state.has('Cape', player)) - set_rule(world.get_entrance('Bumper Cave Exit (Bottom)', player), lambda state: state.has('Cape', player) or state.has('Hookshot', player)) + set_rule(multiworld.get_entrance('Catfish Exit Rock', player), lambda state: can_lift_rocks(state, player)) + set_rule(multiworld.get_entrance('Catfish Entrance Rock', player), lambda state: can_lift_rocks(state, player)) + set_rule(multiworld.get_entrance('Northeast Dark World Broken Bridge Pass', player), lambda state: state.has('Moon Pearl', player) and (can_lift_rocks(state, player) or state.has('Hammer', player) or state.has('Flippers', player))) + set_rule(multiworld.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: state.has('Moon Pearl', player) and (can_lift_rocks(state, player) or state.has('Hammer', player))) + set_rule(multiworld.get_entrance('South Dark World Bridge', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Bonk Fairy (Dark)', player), lambda state: state.has('Moon Pearl', player) and state.has('Pegasus Boots', player)) + set_rule(multiworld.get_entrance('West Dark World Gap', player), lambda state: state.has('Moon Pearl', player) and state.has('Hookshot', player)) + set_rule(multiworld.get_entrance('Palace of Darkness', player), lambda state: state.has('Moon Pearl', player)) # kiki needs pearl + set_rule(multiworld.get_entrance('Hyrule Castle Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Hyrule Castle Main Gate', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: (state.has('Moon Pearl', player) and state.has('Flippers', player) or state.has('Magic Mirror', player))) # Overworld Bunny Revival + set_rule(multiworld.get_location('Bombos Tablet', player), lambda state: can_retrieve_tablet(state, player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Drop (South)', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # ToDo any fake flipper set up? + set_rule(multiworld.get_entrance('Dark Lake Hylia Ledge Fairy', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Ledge Spike Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Village of Outcasts Heavy Rock', player), lambda state: state.has('Moon Pearl', player) and can_lift_heavy_rocks(state, player)) + set_rule(multiworld.get_entrance('Hype Cave', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) + set_rule(multiworld.get_entrance('Brewery', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) + set_rule(multiworld.get_entrance('Thieves Town', player), lambda state: state.has('Moon Pearl', player)) # bunny cannot pull + set_rule(multiworld.get_entrance('Skull Woods First Section Hole (North)', player), lambda state: state.has('Moon Pearl', player)) # bunny cannot lift bush + set_rule(multiworld.get_entrance('Skull Woods Second Section Hole', player), lambda state: state.has('Moon Pearl', player)) # bunny cannot lift bush + set_rule(multiworld.get_entrance('Maze Race Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Cave 45 Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Bombos Tablet Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('East Dark World Bridge', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player)) + set_rule(multiworld.get_entrance('Lake Hylia Island Mirror Spot', player), lambda state: state.has('Moon Pearl', player) and state.has('Magic Mirror', player) and state.has('Flippers', player)) + set_rule(multiworld.get_entrance('Lake Hylia Central Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('East Dark World River Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) + set_rule(multiworld.get_entrance('Graveyard Ledge Mirror Spot', player), lambda state: state.has('Moon Pearl', player) and state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Bumper Cave Entrance Rock', player), lambda state: state.has('Moon Pearl', player) and can_lift_rocks(state, player)) + set_rule(multiworld.get_entrance('Bumper Cave Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Bat Cave Drop Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Dark World Hammer Peg Cave', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player)) + set_rule(multiworld.get_entrance('Village of Outcasts Eastern Rocks', player), lambda state: state.has('Moon Pearl', player) and can_lift_heavy_rocks(state, player)) + set_rule(multiworld.get_entrance('Peg Area Rocks', player), lambda state: state.has('Moon Pearl', player) and can_lift_heavy_rocks(state, player)) + set_rule(multiworld.get_entrance('Village of Outcasts Pegs', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player)) + set_rule(multiworld.get_entrance('Grassy Lawn Pegs', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player)) + set_rule(multiworld.get_entrance('Bumper Cave Exit (Top)', player), lambda state: state.has('Cape', player)) + set_rule(multiworld.get_entrance('Bumper Cave Exit (Bottom)', player), lambda state: state.has('Cape', player) or state.has('Hookshot', player)) - set_rule(world.get_entrance('Skull Woods Final Section', player), lambda state: state.has('Fire Rod', player) and state.has('Moon Pearl', player)) # bunny cannot use fire rod - set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has('Moon Pearl', player) and has_sword(state, player) and has_misery_mire_medallion(state, player)) # sword required to cast magic (!) - set_rule(world.get_entrance('Desert Ledge (Northeast) Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Skull Woods Final Section', player), lambda state: state.has('Fire Rod', player) and state.has('Moon Pearl', player)) # bunny cannot use fire rod + set_rule(multiworld.get_entrance('Misery Mire', player), lambda state: state.has('Moon Pearl', player) and has_sword(state, player) and has_misery_mire_medallion(state, player)) # sword required to cast magic (!) + set_rule(multiworld.get_entrance('Desert Ledge (Northeast) Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Desert Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Desert Palace Stairs Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Desert Palace Entrance (North) Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Spectacle Rock Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Hookshot Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Desert Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Desert Palace Stairs Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Desert Palace Entrance (North) Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Spectacle Rock Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Hookshot Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('East Death Mountain (Top) Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Mimic Cave Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Spiral Cave Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Fairy Ascension Mirror Spot', player), lambda state: state.has('Magic Mirror', player) and state.has('Moon Pearl', player)) # need to lift flowers - set_rule(world.get_entrance('Isolated Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Superbunny Cave Exit (Bottom)', player), lambda state: False) # Cannot get to bottom exit from top. Just exists for shuffling - set_rule(world.get_entrance('Floating Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and has_sword(state, player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!) + set_rule(multiworld.get_entrance('East Death Mountain (Top) Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Mimic Cave Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Spiral Cave Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Fairy Ascension Mirror Spot', player), lambda state: state.has('Magic Mirror', player) and state.has('Moon Pearl', player)) # need to lift flowers + set_rule(multiworld.get_entrance('Isolated Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Superbunny Cave Exit (Bottom)', player), lambda state: False) # Cannot get to bottom exit from top. Just exists for shuffling + set_rule(multiworld.get_entrance('Floating Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and has_sword(state, player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!) - set_rule(world.get_entrance('Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.worlds[player].options.open_pyramid.to_bool(world, player)) + set_rule(multiworld.get_entrance('Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or multiworld.worlds[player].options.open_pyramid.to_bool(multiworld, player)) - if world.worlds[player].options.swordless: - swordless_rules(world, player) + if multiworld.worlds[player].options.swordless: + swordless_rules(multiworld, player) -def inverted_rules(world, player): +def inverted_rules(multiworld: MultiWorld, player: int): # s&q regions. - set_rule(world.get_entrance('Castle Ledge S&Q', player), lambda state: state.has('Magic Mirror', player) and state.has('Beat Agahnim 1', player)) + set_rule(multiworld.get_entrance('Castle Ledge S&Q', player), lambda state: state.has('Magic Mirror', player) and state.has('Beat Agahnim 1', player)) # overworld requirements - set_rule(world.get_location('Maze Race', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Mini Moldorm Cave', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) - set_rule(world.get_entrance('Ice Rod Cave', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) - set_rule(world.get_entrance('Light Hype Fairy', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) - set_rule(world.get_entrance('Potion Shop Pier', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Light World Pier', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Kings Grave', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Kings Grave Outer Rocks', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Kings Grave Inner Rocks', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Potion Shop Inner Bushes', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Potion Shop Outer Bushes', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Potion Shop Outer Rock', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Potion Shop Inner Rock', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Graveyard Cave Inner Bushes', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Graveyard Cave Outer Bushes', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Secret Passage Inner Bushes', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Secret Passage Outer Bushes', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Bonk Fairy (Light)', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Bat Cave Drop Ledge', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Lumberjack Tree Tree', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player) and state.has('Beat Agahnim 1', player)) - set_rule(world.get_entrance('Bonk Rock Cave', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Desert Palace Stairs', player), lambda state: state.has('Book of Mudora', player)) # bunny can use book - set_rule(world.get_entrance('Sanctuary Grave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('20 Rupee Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('50 Rupee Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Death Mountain Entrance Rock', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Bumper Cave Entrance Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Lake Hylia Central Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Dark Lake Hylia Central Island Teleporter', player), lambda state: can_lift_heavy_rocks(state, player)) - set_rule(world.get_entrance('Dark Desert Teleporter', player), lambda state: state.has('Activated Flute', player) and can_lift_heavy_rocks(state, player)) - set_rule(world.get_entrance('East Dark World Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer - set_rule(world.get_entrance('South Dark World Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer - set_rule(world.get_entrance('West Dark World Teleporter', player), lambda state: ((state.has('Hammer', player) and can_lift_rocks(state, player)) or can_lift_heavy_rocks(state, player)) and state.has('Moon Pearl', player)) - set_rule(world.get_location('Flute Spot', player), lambda state: state.has('Shovel', player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_location('Maze Race', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Mini Moldorm Cave', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) + set_rule(multiworld.get_entrance('Ice Rod Cave', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) + set_rule(multiworld.get_entrance('Light Hype Fairy', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) + set_rule(multiworld.get_entrance('Potion Shop Pier', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Light World Pier', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Kings Grave', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Kings Grave Outer Rocks', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Kings Grave Inner Rocks', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Potion Shop Inner Bushes', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Potion Shop Outer Bushes', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Potion Shop Outer Rock', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Potion Shop Inner Rock', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Graveyard Cave Inner Bushes', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Graveyard Cave Outer Bushes', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Secret Passage Inner Bushes', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Secret Passage Outer Bushes', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Bonk Fairy (Light)', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Bat Cave Drop Ledge', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Lumberjack Tree Tree', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player) and state.has('Beat Agahnim 1', player)) + set_rule(multiworld.get_entrance('Bonk Rock Cave', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Desert Palace Stairs', player), lambda state: state.has('Book of Mudora', player)) # bunny can use book + set_rule(multiworld.get_entrance('Sanctuary Grave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('20 Rupee Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('50 Rupee Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Death Mountain Entrance Rock', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Bumper Cave Entrance Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Lake Hylia Central Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Central Island Teleporter', player), lambda state: can_lift_heavy_rocks(state, player)) + set_rule(multiworld.get_entrance('Dark Desert Teleporter', player), lambda state: state.has('Activated Flute', player) and can_lift_heavy_rocks(state, player)) + set_rule(multiworld.get_entrance('East Dark World Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer + set_rule(multiworld.get_entrance('South Dark World Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer + set_rule(multiworld.get_entrance('West Dark World Teleporter', player), lambda state: ((state.has('Hammer', player) and can_lift_rocks(state, player)) or can_lift_heavy_rocks(state, player)) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_location('Flute Spot', player), lambda state: state.has('Shovel', player) and state.has('Moon Pearl', player)) - set_rule(world.get_location('Zora\'s Ledge', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Waterfall of Wishing Cave', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Northeast Light World Return', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) - set_rule(world.get_location('Frog', player), lambda state: can_lift_heavy_rocks(state, player) and (state.has('Moon Pearl', player) or state.has('Beat Agahnim 1', player)) or (state.can_reach('Light World', 'Region', player) and state.has('Magic Mirror', player))) # Need LW access using Mirror or Portal - set_rule(world.get_location('Missing Smith', player), lambda state: state.has('Get Frog', player) and state.can_reach('Blacksmiths Hut', 'Region', player)) # Can't S&Q with smith - set_rule(world.get_location('Blacksmith', player), lambda state: state.has('Return Smith', player)) - set_rule(world.get_location('Magic Bat', player), lambda state: state.has('Magic Powder', player) and state.has('Moon Pearl', player)) - set_rule(world.get_location('Sick Kid', player), lambda state: state.has_group("Bottles", player)) - set_rule(world.get_location('Mushroom', player), lambda state: state.has('Moon Pearl', player)) # need pearl to pick up bushes - set_rule(world.get_entrance('Bush Covered Lawn Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Bush Covered Lawn Inner Bushes', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Bush Covered Lawn Outer Bushes', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Bomb Hut Inner Bushes', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Bomb Hut Outer Bushes', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Light World Bomb Hut', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) - set_rule(world.get_entrance('North Fairy Cave Drop', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Lost Woods Hideout Drop', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_location('Potion Shop', player), lambda state: state.has('Mushroom', player) and (state.can_reach('Potion Shop Area', 'Region', player))) # new inverted region, need pearl for bushes or access to potion shop door/waterfall fairy - set_rule(world.get_entrance('Desert Palace Entrance (North) Rocks', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Desert Ledge Return Rocks', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # should we decide to place something that is not a dungeon end up there at some point - set_rule(world.get_entrance('Checkerboard Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Hyrule Castle Secret Entrance Drop', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Old Man Cave Exit (West)', player), lambda state: False) # drop cannot be climbed up - set_rule(world.get_entrance('Broken Bridge (West)', player), lambda state: state.has('Hookshot', player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Broken Bridge (East)', player), lambda state: state.has('Hookshot', player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Dark Death Mountain Teleporter (East Bottom)', player), lambda state: can_lift_heavy_rocks(state, player)) - set_rule(world.get_entrance('Fairy Ascension Rocks', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: state.has('Mirror', player)) # can erase block - set_rule(world.get_entrance('Death Mountain (Top)', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Dark Death Mountain Teleporter (East)', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Hammer', player) and state.has('Moon Pearl', player)) # bunny cannot use hammer - set_rule(world.get_entrance('East Death Mountain (Top)', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player)) # bunny can not use hammer + set_rule(multiworld.get_location('Zora\'s Ledge', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Waterfall of Wishing Cave', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Northeast Light World Return', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_location('Frog', player), lambda state: can_lift_heavy_rocks(state, player) and (state.has('Moon Pearl', player) or state.has('Beat Agahnim 1', player)) or (state.can_reach('Light World', 'Region', player) and state.has('Magic Mirror', player))) # Need LW access using Mirror or Portal + set_rule(multiworld.get_location('Missing Smith', player), lambda state: state.has('Get Frog', player) and state.can_reach('Blacksmiths Hut', 'Region', player)) # Can't S&Q with smith + set_rule(multiworld.get_location('Blacksmith', player), lambda state: state.has('Return Smith', player)) + set_rule(multiworld.get_location('Magic Bat', player), lambda state: state.has('Magic Powder', player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_location('Sick Kid', player), lambda state: state.has_group("Bottles", player)) + set_rule(multiworld.get_location('Mushroom', player), lambda state: state.has('Moon Pearl', player)) # need pearl to pick up bushes + set_rule(multiworld.get_entrance('Bush Covered Lawn Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Bush Covered Lawn Inner Bushes', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Bush Covered Lawn Outer Bushes', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Bomb Hut Inner Bushes', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Bomb Hut Outer Bushes', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Light World Bomb Hut', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) + set_rule(multiworld.get_entrance('North Fairy Cave Drop', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Lost Woods Hideout Drop', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_location('Potion Shop', player), lambda state: state.has('Mushroom', player) and (state.can_reach('Potion Shop Area', 'Region', player))) # new inverted region, need pearl for bushes or access to potion shop door/waterfall fairy + set_rule(multiworld.get_entrance('Desert Palace Entrance (North) Rocks', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Desert Ledge Return Rocks', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # should we decide to place something that is not a dungeon end up there at some point + set_rule(multiworld.get_entrance('Checkerboard Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Hyrule Castle Secret Entrance Drop', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Old Man Cave Exit (West)', player), lambda state: False) # drop cannot be climbed up + set_rule(multiworld.get_entrance('Broken Bridge (West)', player), lambda state: state.has('Hookshot', player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Broken Bridge (East)', player), lambda state: state.has('Hookshot', player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Dark Death Mountain Teleporter (East Bottom)', player), lambda state: can_lift_heavy_rocks(state, player)) + set_rule(multiworld.get_entrance('Fairy Ascension Rocks', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: state.has('Mirror', player)) # can erase block + set_rule(multiworld.get_entrance('Death Mountain (Top)', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Dark Death Mountain Teleporter (East)', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Hammer', player) and state.has('Moon Pearl', player)) # bunny cannot use hammer + set_rule(multiworld.get_entrance('East Death Mountain (Top)', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player)) # bunny can not use hammer - set_rule(world.get_entrance('Catfish Entrance Rock', player), lambda state: can_lift_rocks(state, player)) - set_rule(world.get_entrance('Northeast Dark World Broken Bridge Pass', player), lambda state: ((can_lift_rocks(state, player) or state.has('Hammer', player)) or state.has('Flippers', player))) - set_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: (can_lift_rocks(state, player) or state.has('Hammer', player))) - set_rule(world.get_entrance('South Dark World Bridge', player), lambda state: state.has('Hammer', player)) - set_rule(world.get_entrance('Bonk Fairy (Dark)', player), lambda state: state.has('Pegasus Boots', player)) - set_rule(world.get_entrance('West Dark World Gap', player), lambda state: state.has('Hookshot', player)) - set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_location('Bombos Tablet', player), lambda state: can_retrieve_tablet(state, player)) - set_rule(world.get_entrance('Dark Lake Hylia Drop (South)', player), lambda state: state.has('Flippers', player)) # ToDo any fake flipper set up? - set_rule(world.get_entrance('Dark Lake Hylia Ledge Pier', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_entrance('Dark Lake Hylia Ledge Spike Cave', player), lambda state: can_lift_rocks(state, player)) - set_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Flippers', player)) # Fake Flippers - set_rule(world.get_entrance('Dark Lake Hylia Shallows', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_entrance('Village of Outcasts Heavy Rock', player), lambda state: can_lift_heavy_rocks(state, player)) - set_rule(world.get_entrance('East Dark World Bridge', player), lambda state: state.has('Hammer', player)) - set_rule(world.get_entrance('Lake Hylia Central Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('East Dark World River Pier', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_entrance('Bumper Cave Entrance Rock', player), lambda state: can_lift_rocks(state, player)) - set_rule(world.get_entrance('Bumper Cave Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Hammer Peg Area Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Dark World Hammer Peg Cave', player), lambda state: state.has('Hammer', player)) - set_rule(world.get_entrance('Village of Outcasts Eastern Rocks', player), lambda state: can_lift_heavy_rocks(state, player)) - set_rule(world.get_entrance('Peg Area Rocks', player), lambda state: can_lift_heavy_rocks(state, player)) - set_rule(world.get_entrance('Village of Outcasts Pegs', player), lambda state: state.has('Hammer', player)) - set_rule(world.get_entrance('Grassy Lawn Pegs', player), lambda state: state.has('Hammer', player)) - set_rule(world.get_entrance('Bumper Cave Exit (Top)', player), lambda state: state.has('Cape', player)) - set_rule(world.get_entrance('Bumper Cave Exit (Bottom)', player), lambda state: state.has('Cape', player) or state.has('Hookshot', player)) + set_rule(multiworld.get_entrance('Catfish Entrance Rock', player), lambda state: can_lift_rocks(state, player)) + set_rule(multiworld.get_entrance('Northeast Dark World Broken Bridge Pass', player), lambda state: ((can_lift_rocks(state, player) or state.has('Hammer', player)) or state.has('Flippers', player))) + set_rule(multiworld.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: (can_lift_rocks(state, player) or state.has('Hammer', player))) + set_rule(multiworld.get_entrance('South Dark World Bridge', player), lambda state: state.has('Hammer', player)) + set_rule(multiworld.get_entrance('Bonk Fairy (Dark)', player), lambda state: state.has('Pegasus Boots', player)) + set_rule(multiworld.get_entrance('West Dark World Gap', player), lambda state: state.has('Hookshot', player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Flippers', player)) + set_rule(multiworld.get_location('Bombos Tablet', player), lambda state: can_retrieve_tablet(state, player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Drop (South)', player), lambda state: state.has('Flippers', player)) # ToDo any fake flipper set up? + set_rule(multiworld.get_entrance('Dark Lake Hylia Ledge Pier', player), lambda state: state.has('Flippers', player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Ledge Spike Cave', player), lambda state: can_lift_rocks(state, player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Flippers', player)) # Fake Flippers + set_rule(multiworld.get_entrance('Dark Lake Hylia Shallows', player), lambda state: state.has('Flippers', player)) + set_rule(multiworld.get_entrance('Village of Outcasts Heavy Rock', player), lambda state: can_lift_heavy_rocks(state, player)) + set_rule(multiworld.get_entrance('East Dark World Bridge', player), lambda state: state.has('Hammer', player)) + set_rule(multiworld.get_entrance('Lake Hylia Central Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('East Dark World River Pier', player), lambda state: state.has('Flippers', player)) + set_rule(multiworld.get_entrance('Bumper Cave Entrance Rock', player), lambda state: can_lift_rocks(state, player)) + set_rule(multiworld.get_entrance('Bumper Cave Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Hammer Peg Area Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Dark World Hammer Peg Cave', player), lambda state: state.has('Hammer', player)) + set_rule(multiworld.get_entrance('Village of Outcasts Eastern Rocks', player), lambda state: can_lift_heavy_rocks(state, player)) + set_rule(multiworld.get_entrance('Peg Area Rocks', player), lambda state: can_lift_heavy_rocks(state, player)) + set_rule(multiworld.get_entrance('Village of Outcasts Pegs', player), lambda state: state.has('Hammer', player)) + set_rule(multiworld.get_entrance('Grassy Lawn Pegs', player), lambda state: state.has('Hammer', player)) + set_rule(multiworld.get_entrance('Bumper Cave Exit (Top)', player), lambda state: state.has('Cape', player)) + set_rule(multiworld.get_entrance('Bumper Cave Exit (Bottom)', player), lambda state: state.has('Cape', player) or state.has('Hookshot', player)) - set_rule(world.get_entrance('Hype Cave', player), lambda state: can_use_bombs(state, player)) - set_rule(world.get_entrance('Brewery', player), lambda state: can_use_bombs(state, player)) - set_rule(world.get_entrance('Dark Lake Hylia Ledge Fairy', player), lambda state: can_use_bombs(state, player)) + set_rule(multiworld.get_entrance('Hype Cave', player), lambda state: can_use_bombs(state, player)) + set_rule(multiworld.get_entrance('Brewery', player), lambda state: can_use_bombs(state, player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Ledge Fairy', player), lambda state: can_use_bombs(state, player)) - set_rule(world.get_entrance('Skull Woods Final Section', player), lambda state: state.has('Fire Rod', player)) - set_rule(world.get_entrance('Misery Mire', player), lambda state: has_sword(state, player) and has_misery_mire_medallion(state, player)) # sword required to cast magic (!) + set_rule(multiworld.get_entrance('Skull Woods Final Section', player), lambda state: state.has('Fire Rod', player)) + set_rule(multiworld.get_entrance('Misery Mire', player), lambda state: has_sword(state, player) and has_misery_mire_medallion(state, player)) # sword required to cast magic (!) - set_rule(world.get_entrance('Hookshot Cave', player), lambda state: can_lift_rocks(state, player)) + set_rule(multiworld.get_entrance('Hookshot Cave', player), lambda state: can_lift_rocks(state, player)) - set_rule(world.get_entrance('East Death Mountain Mirror Spot (Top)', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Death Mountain (Top) Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('East Death Mountain Mirror Spot (Top)', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Death Mountain (Top) Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('East Death Mountain Mirror Spot (Bottom)', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Dark Death Mountain Ledge Mirror Spot (East)', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Dark Death Mountain Ledge Mirror Spot (West)', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Laser Bridge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Floating Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Turtle Rock', player), lambda state: has_sword(state, player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!) + set_rule(multiworld.get_entrance('East Death Mountain Mirror Spot (Bottom)', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Dark Death Mountain Ledge Mirror Spot (East)', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Dark Death Mountain Ledge Mirror Spot (West)', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Laser Bridge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Floating Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Turtle Rock', player), lambda state: has_sword(state, player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!) # new inverted spots - set_rule(world.get_entrance('Post Aga Teleporter', player), lambda state: state.has('Beat Agahnim 1', player)) - set_rule(world.get_entrance('Mire Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Desert Palace Stairs Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Death Mountain Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('East Dark World Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('West Dark World Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('South Dark World Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Catfish Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Potion Shop Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Shopping Mall Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Maze Race Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Desert Palace North Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Death Mountain (Top) Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Graveyard Cave Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Bomb Hut Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Skull Woods Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Post Aga Teleporter', player), lambda state: state.has('Beat Agahnim 1', player)) + set_rule(multiworld.get_entrance('Mire Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Desert Palace Stairs Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Death Mountain Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('East Dark World Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('West Dark World Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('South Dark World Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Catfish Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Potion Shop Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Shopping Mall Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Maze Race Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Desert Palace North Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Death Mountain (Top) Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Graveyard Cave Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Bomb Hut Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Skull Woods Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) # inverted flute spots - set_rule(world.get_entrance('DDM Flute', player), lambda state: state.has('Activated Flute', player)) - set_rule(world.get_entrance('NEDW Flute', player), lambda state: state.has('Activated Flute', player)) - set_rule(world.get_entrance('WDW Flute', player), lambda state: state.has('Activated Flute', player)) - set_rule(world.get_entrance('SDW Flute', player), lambda state: state.has('Activated Flute', player)) - set_rule(world.get_entrance('EDW Flute', player), lambda state: state.has('Activated Flute', player)) - set_rule(world.get_entrance('DLHL Flute', player), lambda state: state.has('Activated Flute', player)) - set_rule(world.get_entrance('DD Flute', player), lambda state: state.has('Activated Flute', player)) - set_rule(world.get_entrance('EDDM Flute', player), lambda state: state.has('Activated Flute', player)) - set_rule(world.get_entrance('Dark Grassy Lawn Flute', player), lambda state: state.has('Activated Flute', player)) - set_rule(world.get_entrance('Hammer Peg Area Flute', player), lambda state: state.has('Activated Flute', player)) + set_rule(multiworld.get_entrance('DDM Flute', player), lambda state: state.has('Activated Flute', player)) + set_rule(multiworld.get_entrance('NEDW Flute', player), lambda state: state.has('Activated Flute', player)) + set_rule(multiworld.get_entrance('WDW Flute', player), lambda state: state.has('Activated Flute', player)) + set_rule(multiworld.get_entrance('SDW Flute', player), lambda state: state.has('Activated Flute', player)) + set_rule(multiworld.get_entrance('EDW Flute', player), lambda state: state.has('Activated Flute', player)) + set_rule(multiworld.get_entrance('DLHL Flute', player), lambda state: state.has('Activated Flute', player)) + set_rule(multiworld.get_entrance('DD Flute', player), lambda state: state.has('Activated Flute', player)) + set_rule(multiworld.get_entrance('EDDM Flute', player), lambda state: state.has('Activated Flute', player)) + set_rule(multiworld.get_entrance('Dark Grassy Lawn Flute', player), lambda state: state.has('Activated Flute', player)) + set_rule(multiworld.get_entrance('Hammer Peg Area Flute', player), lambda state: state.has('Activated Flute', player)) - set_rule(world.get_entrance('Inverted Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.worlds[player].options.open_pyramid) + set_rule(multiworld.get_entrance('Inverted Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or multiworld.worlds[player].options.open_pyramid) - if world.worlds[player].options.swordless: - swordless_rules(world, player) + if multiworld.worlds[player].options.swordless: + swordless_rules(multiworld, player) -def no_glitches_rules(world, player): + +def no_glitches_rules(multiworld: MultiWorld, player: int): """""" - if world.worlds[player].options.mode == 'inverted': - set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Moon Pearl', player) and (state.has('Flippers', player) or can_lift_rocks(state, player))) - set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to - set_rule(world.get_entrance('Lake Hylia Island Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to - set_rule(world.get_entrance('Lake Hylia Warp', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to - set_rule(world.get_entrance('Northeast Light World Warp', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to - set_rule(world.get_entrance('Hobo Bridge', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) - set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_entrance('East Dark World Pier', player), lambda state: state.has('Flippers', player)) + if multiworld.worlds[player].options.mode == 'inverted': + set_rule(multiworld.get_entrance('Zoras River', player), lambda state: state.has('Moon Pearl', player) and (state.has('Flippers', player) or can_lift_rocks(state, player))) + set_rule(multiworld.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to + set_rule(multiworld.get_entrance('Lake Hylia Island Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to + set_rule(multiworld.get_entrance('Lake Hylia Warp', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to + set_rule(multiworld.get_entrance('Northeast Light World Warp', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to + set_rule(multiworld.get_entrance('Hobo Bridge', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Flippers', player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Flippers', player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: state.has('Flippers', player)) + set_rule(multiworld.get_entrance('East Dark World Pier', player), lambda state: state.has('Flippers', player)) else: - set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Flippers', player) or can_lift_rocks(state, player)) - set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Flippers', player)) # can be fake flippered to - set_rule(world.get_entrance('Hobo Bridge', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) - set_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) - set_rule(world.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) + set_rule(multiworld.get_entrance('Zoras River', player), lambda state: state.has('Flippers', player) or can_lift_rocks(state, player)) + set_rule(multiworld.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Flippers', player)) # can be fake flippered to + set_rule(multiworld.get_entrance('Hobo Bridge', player), lambda state: state.has('Flippers', player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) - add_rule(world.get_entrance('Ganons Tower (Double Switch Room)', player), lambda state: state.has('Hookshot', player)) - set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: False) # no glitches does not require block override - add_conditional_lamps(world, player) + add_rule(multiworld.get_entrance('Ganons Tower (Double Switch Room)', player), lambda state: state.has('Hookshot', player)) + set_rule(multiworld.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: False) # no glitches does not require block override + add_conditional_lamps(multiworld, player) -def fake_flipper_rules(world, player): - if world.worlds[player].options.mode == 'inverted': - set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Lake Hylia Island Pier', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Lake Hylia Warp', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Northeast Light World Warp', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Hobo Bridge', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: True) - set_rule(world.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: True) - set_rule(world.get_entrance('East Dark World Pier', player), lambda state: True) + +def fake_flipper_rules(multiworld: MultiWorld, player: int): + if multiworld.worlds[player].options.mode == 'inverted': + set_rule(multiworld.get_entrance('Zoras River', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Lake Hylia Island Pier', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Lake Hylia Warp', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Northeast Light World Warp', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Hobo Bridge', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Flippers', player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: True) + set_rule(multiworld.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: True) + set_rule(multiworld.get_entrance('East Dark World Pier', player), lambda state: True) #qirn jump - set_rule(world.get_entrance('East Dark World River Pier', player), lambda state: True) + set_rule(multiworld.get_entrance('East Dark World River Pier', player), lambda state: True) else: - set_rule(world.get_entrance('Zoras River', player), lambda state: True) - set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: True) - set_rule(world.get_entrance('Hobo Bridge', player), lambda state: True) - set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) - set_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Zoras River', player), lambda state: True) + set_rule(multiworld.get_entrance('Lake Hylia Central Island Pier', player), lambda state: True) + set_rule(multiworld.get_entrance('Hobo Bridge', player), lambda state: True) + set_rule(multiworld.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: state.has('Moon Pearl', player)) #qirn jump - set_rule(world.get_entrance('East Dark World River Pier', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('East Dark World River Pier', player), lambda state: state.has('Moon Pearl', player)) -def bomb_jump_requirements(multiworld, player): +def bomb_jump_requirements(multiworld: MultiWorld, player: int): DMs_room_chests = ['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right', 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right'] for location in DMs_room_chests: add_rule(multiworld.get_location(location, player), lambda state: can_use_bombs(state, player), combine="or") @@ -947,7 +953,7 @@ def bomb_jump_requirements(multiworld, player): set_rule(multiworld.get_entrance('Skull Woods First Section Bomb Jump', player), lambda state: can_use_bombs(state, player)) -def forbid_bomb_jump_requirements(multiworld, player): +def forbid_bomb_jump_requirements(multiworld: MultiWorld, player: int): DMs_room_chests = ['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right', 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right'] for location in DMs_room_chests: add_rule(multiworld.get_location(location, player), lambda state: state.has('Hookshot', player)) @@ -972,14 +978,15 @@ DW_Entrances = ['Bumper Cave (Bottom)', 'Turtle Rock', 'Dark Death Mountain Ledge (West)'] -def check_is_dark_world(region): + +def check_is_dark_world(region: Region): for entrance in region.entrances: if entrance.name in DW_Entrances: return True return False -def add_conditional_lamps(multiworld, player): +def add_conditional_lamps(multiworld: MultiWorld, player: int): # Light cones in standard depend on which world we actually are in, not which one the location would normally be # We add Lamp requirements only to those locations which lie in the dark world (or everything if open local_world = multiworld.worlds[player] @@ -1030,7 +1037,7 @@ def add_conditional_lamps(multiworld, player): add_lamp_requirement(multiworld, local_world.get_entrance("Throne Room"), player) -def open_rules(world, player): +def open_rules(multiworld: MultiWorld, player: int): def basement_key_rule(state): if location_item_name(state, 'Sewers - Key Rat Key Drop', player) == ("Small Key (Hyrule Castle)", player): @@ -1038,96 +1045,95 @@ def open_rules(world, player): else: return state._lttp_has_key("Small Key (Hyrule Castle)", player, 3) - set_rule(world.get_location('Hyrule Castle - Boomerang Guard Key Drop', player), + set_rule(multiworld.get_location('Hyrule Castle - Boomerang Guard Key Drop', player), lambda state: basement_key_rule(state) and can_kill_most_things(state, player, 2)) - set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player), lambda state: basement_key_rule(state) and can_kill_most_things(state, player, 1)) + set_rule(multiworld.get_location('Hyrule Castle - Boomerang Chest', player), lambda state: basement_key_rule(state) and can_kill_most_things(state, player, 1)) - set_rule(world.get_location('Sewers - Key Rat Key Drop', player), + set_rule(multiworld.get_location('Sewers - Key Rat Key Drop', player), lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 3) and can_kill_most_things(state, player, 1)) - set_rule(world.get_location('Hyrule Castle - Big Key Drop', player), + set_rule(multiworld.get_location('Hyrule Castle - Big Key Drop', player), lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4) and can_kill_most_things(state, player, 1)) - set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player), + set_rule(multiworld.get_location('Hyrule Castle - Zelda\'s Chest', player), lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4) and state.has('Big Key (Hyrule Castle)', player) - and (world.worlds[player].options.enemy_health in ("easy", "default") + and (multiworld.worlds[player].options.enemy_health in ("easy", "default") or can_kill_most_things(state, player, 1))) -def swordless_rules(world, player): - set_rule(world.get_entrance('Agahnim 1', player), lambda state: (state.has('Hammer', player) or state.has('Fire Rod', player) or can_shoot_arrows(state, player) or state.has('Cane of Somaria', player)) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2)) - set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player)) # no curtain +def swordless_rules(multiworld: MultiWorld, player: int): + set_rule(multiworld.get_entrance('Agahnim 1', player), lambda state: (state.has('Hammer', player) or state.has('Fire Rod', player) or can_shoot_arrows(state, player) or state.has('Cane of Somaria', player)) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2)) + set_rule(multiworld.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player)) # no curtain - set_rule(world.get_location('Ice Palace - Jelly Key Drop', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player)) - set_rule(world.get_location('Ice Palace - Compass Chest', player), lambda state: (state.has('Fire Rod', player) or state.has('Bombos', player)) and state._lttp_has_key('Small Key (Ice Palace)', player)) - set_rule(world.get_entrance('Ice Palace (Second Section)', player), lambda state: (state.has('Fire Rod', player) or state.has('Bombos', player)) and state._lttp_has_key('Small Key (Ice Palace)', player)) + set_rule(multiworld.get_location('Ice Palace - Jelly Key Drop', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player)) + set_rule(multiworld.get_location('Ice Palace - Compass Chest', player), lambda state: (state.has('Fire Rod', player) or state.has('Bombos', player)) and state._lttp_has_key('Small Key (Ice Palace)', player)) + set_rule(multiworld.get_entrance('Ice Palace (Second Section)', player), lambda state: (state.has('Fire Rod', player) or state.has('Bombos', player)) and state._lttp_has_key('Small Key (Ice Palace)', player)) - set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has('Hammer', player)) # need to damage ganon to get tiles to drop + set_rule(multiworld.get_entrance('Ganon Drop', player), lambda state: state.has('Hammer', player)) # need to damage ganon to get tiles to drop - if world.worlds[player].options.mode != 'inverted': - set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has('Hammer', player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle - set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!) - set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has('Moon Pearl', player) and has_misery_mire_medallion(state, player)) # sword not required to use medallion for opening in swordless (!) + if multiworld.worlds[player].options.mode != 'inverted': + set_rule(multiworld.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has('Hammer', player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle + set_rule(multiworld.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!) + set_rule(multiworld.get_entrance('Misery Mire', player), lambda state: state.has('Moon Pearl', player) and has_misery_mire_medallion(state, player)) # sword not required to use medallion for opening in swordless (!) else: # only need ddm access for aga tower in inverted - set_rule(world.get_entrance('Turtle Rock', player), lambda state: has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!) - set_rule(world.get_entrance('Misery Mire', player), lambda state: has_misery_mire_medallion(state, player)) # sword not required to use medallion for opening in swordless (!) + set_rule(multiworld.get_entrance('Turtle Rock', player), lambda state: has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!) + set_rule(multiworld.get_entrance('Misery Mire', player), lambda state: has_misery_mire_medallion(state, player)) # sword not required to use medallion for opening in swordless (!) -def add_connection(parent_name, target_name, entrance_name, world, player): - parent = world.get_region(parent_name, player) - target = world.get_region(target_name, player) +def add_connection(parent_name: str, target_name: str, entrance_name: str, multiworld: MultiWorld, player: int): + parent = multiworld.get_region(parent_name, player) + target = multiworld.get_region(target_name, player) parent.connect(target, entrance_name) +def standard_rules(multiworld: MultiWorld, player: int): + add_connection('Menu', 'Hyrule Castle Secret Entrance', 'Uncle S&Q', multiworld, player) + multiworld.get_entrance('Uncle S&Q', player).hide_path = True + set_rule(multiworld.get_entrance('Throne Room', player), lambda state: state.can_reach('Hyrule Castle - Zelda\'s Chest', 'Location', player)) + set_rule(multiworld.get_entrance('Hyrule Castle Exit (East)', player), lambda state: state.can_reach('Sanctuary', 'Region', player)) + set_rule(multiworld.get_entrance('Hyrule Castle Exit (West)', player), lambda state: state.can_reach('Sanctuary', 'Region', player)) + set_rule(multiworld.get_entrance('Links House S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player)) + set_rule(multiworld.get_entrance('Sanctuary S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player)) -def standard_rules(world, player): - add_connection('Menu', 'Hyrule Castle Secret Entrance', 'Uncle S&Q', world, player) - world.get_entrance('Uncle S&Q', player).hide_path = True - set_rule(world.get_entrance('Throne Room', player), lambda state: state.can_reach('Hyrule Castle - Zelda\'s Chest', 'Location', player)) - set_rule(world.get_entrance('Hyrule Castle Exit (East)', player), lambda state: state.can_reach('Sanctuary', 'Region', player)) - set_rule(world.get_entrance('Hyrule Castle Exit (West)', player), lambda state: state.can_reach('Sanctuary', 'Region', player)) - set_rule(world.get_entrance('Links House S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player)) - set_rule(world.get_entrance('Sanctuary S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player)) - - if world.worlds[player].options.small_key_shuffle != small_key_shuffle.option_universal: - set_rule(world.get_location('Hyrule Castle - Boomerang Guard Key Drop', player), + if multiworld.worlds[player].options.small_key_shuffle != small_key_shuffle.option_universal: + set_rule(multiworld.get_location('Hyrule Castle - Boomerang Guard Key Drop', player), lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 1) and can_kill_standard_start(state, player, 2)) - set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player), + set_rule(multiworld.get_location('Hyrule Castle - Boomerang Chest', player), lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 1) and can_kill_standard_start(state, player, 1)) - set_rule(world.get_location('Hyrule Castle - Map Guard Key Drop', player), + set_rule(multiworld.get_location('Hyrule Castle - Map Guard Key Drop', player), lambda state: can_kill_standard_start(state, player, 1)) - set_rule(world.get_location('Hyrule Castle - Big Key Drop', player), + set_rule(multiworld.get_location('Hyrule Castle - Big Key Drop', player), lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 2)) - set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player), + set_rule(multiworld.get_location('Hyrule Castle - Zelda\'s Chest', player), lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 2) and state.has('Big Key (Hyrule Castle)', player) - and (world.worlds[player].options.enemy_health in ("easy", "default") + and (multiworld.worlds[player].options.enemy_health in ("easy", "default") or can_kill_standard_start(state, player, 1))) - set_rule(world.get_location('Sewers - Key Rat Key Drop', player), + set_rule(multiworld.get_location('Sewers - Key Rat Key Drop', player), lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 3) and can_kill_standard_start(state, player, 1)) else: - set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player), + set_rule(multiworld.get_location('Hyrule Castle - Zelda\'s Chest', player), lambda state: state.has('Big Key (Hyrule Castle)', player)) -def toss_junk_item(world, player): +def toss_junk_item(multiworld: MultiWorld, player: int): items = ['Rupees (20)', 'Bombs (3)', 'Arrows (10)', 'Rupees (5)', 'Rupee (1)', 'Bombs (10)', 'Single Arrow', 'Rupees (50)', 'Rupees (100)', 'Single Bomb', 'Bee', 'Bee Trap', 'Rupees (300)', 'Nothing'] for item in items: - big20 = next((i for i in world.itempool if i.name == item and i.player == player), None) + big20 = next((i for i in multiworld.itempool if i.name == item and i.player == player), None) if big20: - world.itempool.remove(big20) + multiworld.itempool.remove(big20) return raise Exception("Unable to find a junk item to toss to make room for a TR small key") -def set_trock_key_rules(multiworld, player): +def set_trock_key_rules(multiworld: MultiWorld, player: int): # First set all relevant locked doors to impassible. for entrance in ['Turtle Rock Dark Room Staircase', 'Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock Entrance to Pokey Room', 'Turtle Rock (Pokey Room) (South)', 'Turtle Rock (Pokey Room) (North)', 'Turtle Rock Big Key Door']: set_rule(multiworld.get_entrance(entrance, player), lambda state: False) @@ -1228,9 +1234,9 @@ def set_trock_key_rules(multiworld, player): and state.can_reach(state.multiworld.get_region('Turtle Rock (Second Section)', player))) -def set_big_bomb_rules(world, player): +def set_big_bomb_rules(multiworld: MultiWorld, player: int): # this is a mess - bombshop_entrance = world.get_region('Big Bomb Shop', player).entrances[0] + bombshop_entrance = multiworld.get_region('Big Bomb Shop', player).entrances[0] Normal_LW_entrances = ['Blinds Hideout', 'Bonk Fairy (Light)', 'Lake Hylia Fairy', @@ -1345,7 +1351,7 @@ def set_big_bomb_rules(world, player): 'Desert Palace Entrance (South)', 'Checkerboard Cave'] - set_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.can_reach('East Dark World', 'Region', player) and state.can_reach('Big Bomb Shop', 'Region', player) and state.has('Crystal 5', player) and state.has('Crystal 6', player)) + set_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.can_reach('East Dark World', 'Region', player) and state.can_reach('Big Bomb Shop', 'Region', player) and state.has('Crystal 5', player) and state.has('Crystal 6', player)) #crossing peg bridge starting from the southern dark world def cross_peg_bridge(state): @@ -1372,96 +1378,96 @@ def set_big_bomb_rules(world, player): #1. basic routes #2. Can reach Eastern dark world some other way, mirror, get bomb, return to mirror spot, walk to pyramid: Needs mirror # -> M or BR - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: basic_routes(state) or state.has('Magic Mirror', player)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: basic_routes(state) or state.has('Magic Mirror', player)) elif bombshop_entrance.name in LW_walkable_entrances: #1. Mirror then basic routes # -> M and BR - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and basic_routes(state)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and basic_routes(state)) elif bombshop_entrance.name in Northern_DW_entrances: #1. Mirror and basic routes #2. Go to south DW and then cross peg bridge: Need Mitts and hammer and moon pearl # -> (Mitts and CPB) or (M and BR) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (can_lift_heavy_rocks(state, player) and cross_peg_bridge(state)) or (state.has('Magic Mirror', player) and basic_routes(state))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: (can_lift_heavy_rocks(state, player) and cross_peg_bridge(state)) or (state.has('Magic Mirror', player) and basic_routes(state))) elif bombshop_entrance.name == 'Bumper Cave (Bottom)': #1. Mirror and Lift rock and basic_routes #2. Mirror and Flute and basic routes (can make difference if accessed via insanity or w/ mirror from connector, and then via hyrule castle gate, because no gloves are needed in that case) #3. Go to south DW and then cross peg bridge: Need Mitts and hammer and moon pearl # -> (Mitts and CPB) or (((G or Flute) and M) and BR)) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (can_lift_heavy_rocks(state, player) and cross_peg_bridge(state)) or (((can_lift_rocks(state, player) or state.has('Flute', player)) and state.has('Magic Mirror', player)) and basic_routes(state))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: (can_lift_heavy_rocks(state, player) and cross_peg_bridge(state)) or (((can_lift_rocks(state, player) or state.has('Flute', player)) and state.has('Magic Mirror', player)) and basic_routes(state))) elif bombshop_entrance.name in Southern_DW_entrances: #1. Mirror and enter via gate: Need mirror and Aga1 #2. cross peg bridge: Need hammer and moon pearl # -> CPB or (M and A) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: cross_peg_bridge(state) or (state.has('Magic Mirror', player) and state.has('Beat Agahnim 1', player))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: cross_peg_bridge(state) or (state.has('Magic Mirror', player) and state.has('Beat Agahnim 1', player))) elif bombshop_entrance.name in Isolated_DW_entrances: # 1. mirror then flute then basic routes # -> M and Flute and BR - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and state.has('Activated Flute', player) and basic_routes(state)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and state.has('Activated Flute', player) and basic_routes(state)) elif bombshop_entrance.name in Isolated_LW_entrances: # 1. flute then basic routes # Prexisting mirror spot is not permitted, because mirror might have been needed to reach these isolated locations. # -> Flute and BR - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and basic_routes(state)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and basic_routes(state)) elif bombshop_entrance.name in West_LW_DM_entrances: # 1. flute then basic routes or mirror # Prexisting mirror spot is permitted, because flute can be used to reach west DM directly. # -> Flute and (M or BR) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and (state.has('Magic Mirror', player) or basic_routes(state))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and (state.has('Magic Mirror', player) or basic_routes(state))) elif bombshop_entrance.name in East_LW_DM_entrances: # 1. flute then basic routes or mirror and hookshot # Prexisting mirror spot is permitted, because flute can be used to reach west DM directly and then east DM via Hookshot # -> Flute and ((M and Hookshot) or BR) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and ((state.has('Magic Mirror', player) and state.has('Hookshot', player)) or basic_routes(state))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and ((state.has('Magic Mirror', player) and state.has('Hookshot', player)) or basic_routes(state))) elif bombshop_entrance.name == 'Fairy Ascension Cave (Bottom)': # Same as East_LW_DM_entrances except navigation without BR requires Mitts # -> Flute and ((M and Hookshot and Mitts) or BR) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and ((state.has('Magic Mirror', player) and state.has('Hookshot', player) and can_lift_heavy_rocks(state, player)) or basic_routes(state))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and ((state.has('Magic Mirror', player) and state.has('Hookshot', player) and can_lift_heavy_rocks(state, player)) or basic_routes(state))) elif bombshop_entrance.name in Castle_ledge_entrances: # 1. mirror on pyramid to castle ledge, grab bomb, return through mirror spot: Needs mirror # 2. flute then basic routes # -> M or (Flute and BR) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) or (state.has('Activated Flute', player) and basic_routes(state))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) or (state.has('Activated Flute', player) and basic_routes(state))) elif bombshop_entrance.name in Desert_mirrorable_ledge_entrances: # Cases when you have mire access: Mirror to reach locations, return via mirror spot, move to center of desert, mirror anagin and: # 1. Have mire access, Mirror to reach locations, return via mirror spot, move to center of desert, mirror again and then basic routes # 2. flute then basic routes # -> (Mire access and M) or Flute) and BR - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: ((state.can_reach('Dark Desert', 'Region', player) and state.has('Magic Mirror', player)) or state.has('Activated Flute', player)) and basic_routes(state)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: ((state.can_reach('Dark Desert', 'Region', player) and state.has('Magic Mirror', player)) or state.has('Activated Flute', player)) and basic_routes(state)) elif bombshop_entrance.name == 'Old Man Cave (West)': # 1. Lift rock then basic_routes # 2. flute then basic_routes # -> (Flute or G) and BR - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or can_lift_rocks(state, player)) and basic_routes(state)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or can_lift_rocks(state, player)) and basic_routes(state)) elif bombshop_entrance.name == 'Graveyard Cave': # 1. flute then basic routes # 2. (has west dark world access) use existing mirror spot (required Pearl), mirror again off ledge # -> (Flute or (M and P and West Dark World access) and BR - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or (state.can_reach('West Dark World', 'Region', player) and state.has('Moon Pearl', player) and state.has('Magic Mirror', player))) and basic_routes(state)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or (state.can_reach('West Dark World', 'Region', player) and state.has('Moon Pearl', player) and state.has('Magic Mirror', player))) and basic_routes(state)) elif bombshop_entrance.name in Mirror_from_SDW_entrances: # 1. flute then basic routes # 2. (has South dark world access) use existing mirror spot, mirror again off ledge # -> (Flute or (M and South Dark World access) and BR - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or (state.can_reach('South Dark World', 'Region', player) and state.has('Magic Mirror', player))) and basic_routes(state)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or (state.can_reach('South Dark World', 'Region', player) and state.has('Magic Mirror', player))) and basic_routes(state)) elif bombshop_entrance.name == 'Dark World Potion Shop': # 1. walk down by lifting rock: needs gloves and pearl` # 2. walk down by hammering peg: needs hammer and pearl # 3. mirror and basic routes # -> (P and (H or Gloves)) or (M and BR) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Moon Pearl', player) and (state.has('Hammer', player) or can_lift_rocks(state, player))) or (state.has('Magic Mirror', player) and basic_routes(state))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Moon Pearl', player) and (state.has('Hammer', player) or can_lift_rocks(state, player))) or (state.has('Magic Mirror', player) and basic_routes(state))) elif bombshop_entrance.name == 'Kings Grave': # same as the Normal_LW_entrances case except that the pre-existing mirror is only possible if you have mitts # (because otherwise mirror was used to reach the grave, so would cancel a pre-existing mirror spot) # to account for insanity, must consider a way to escape without a cave for basic_routes # -> (M and Mitts) or ((Mitts or Flute or (M and P and West Dark World access)) and BR) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (can_lift_heavy_rocks(state, player) and state.has('Magic Mirror', player)) or ((can_lift_heavy_rocks(state, player) or state.has('Activated Flute', player) or (state.can_reach('West Dark World', 'Region', player) and state.has('Moon Pearl', player) and state.has('Magic Mirror', player))) and basic_routes(state))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: (can_lift_heavy_rocks(state, player) and state.has('Magic Mirror', player)) or ((can_lift_heavy_rocks(state, player) or state.has('Activated Flute', player) or (state.can_reach('West Dark World', 'Region', player) and state.has('Moon Pearl', player) and state.has('Magic Mirror', player))) and basic_routes(state))) elif bombshop_entrance.name == 'Waterfall of Wishing': # same as the Normal_LW_entrances case except in insanity it's possible you could be here without Flippers which # means you need an escape route of either Flippers or Flute - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Flippers', player) or state.has('Activated Flute', player)) and (basic_routes(state) or state.has('Magic Mirror', player))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Flippers', player) or state.has('Activated Flute', player)) and (basic_routes(state) or state.has('Magic Mirror', player))) -def set_inverted_big_bomb_rules(world, player): - bombshop_entrance = world.get_region('Inverted Big Bomb Shop', player).entrances[0] +def set_inverted_big_bomb_rules(multiworld: MultiWorld, player: int): + bombshop_entrance = multiworld.get_region('Inverted Big Bomb Shop', player).entrances[0] Normal_LW_entrances = ['Blinds Hideout', 'Bonk Fairy (Light)', 'Lake Hylia Fairy', @@ -1579,7 +1585,7 @@ def set_inverted_big_bomb_rules(world, player): 'Spectacle Rock Cave', 'Spectacle Rock Cave (Bottom)'] - set_rule(world.get_entrance('Pyramid Fairy', player), + set_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.can_reach('East Dark World', 'Region', player) and state.can_reach('Inverted Big Bomb Shop', 'Region', player) and state.has('Crystal 5', player) and state.has('Crystal 6', player)) # Key for below abbreviations: @@ -1593,64 +1599,64 @@ def set_inverted_big_bomb_rules(world, player): pass elif bombshop_entrance.name in Normal_LW_entrances: # Just walk to the castle and mirror. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player)) elif bombshop_entrance.name in Isolated_LW_entrances: # For these entrances, you cannot walk to the castle/pyramid and thus must use Mirror and then Flute. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and state.has('Magic Mirror', player)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and state.has('Magic Mirror', player)) elif bombshop_entrance.name in Northern_DW_entrances: # You can just fly with the Flute, you can take a long walk with Mitts and Hammer, # or you can leave a Mirror portal nearby and then walk to the castle to Mirror again. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player))) elif bombshop_entrance.name in Southern_DW_entrances: # This is the same as north DW without the Mitts rock present. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Hammer', player) or state.has('Activated Flute', player) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Hammer', player) or state.has('Activated Flute', player) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player))) elif bombshop_entrance.name in Isolated_DW_entrances: # There's just no way to escape these places with the bomb and no Flute. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player)) elif bombshop_entrance.name in LW_walkable_entrances: # You can fly with the flute, or leave a mirror portal and walk through the light world - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player))) elif bombshop_entrance.name in LW_bush_entrances: # These entrances are behind bushes in LW so you need either Pearl or the tools to solve NDW bomb shop locations. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and (state.has('Activated Flute', player) or state.has('Moon Pearl', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and (state.has('Activated Flute', player) or state.has('Moon Pearl', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)))) elif bombshop_entrance.name == 'Village of Outcasts Shop': # This is mostly the same as NDW but the Mirror path requires the Pearl, or using the Hammer - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player) and (state.has('Moon Pearl', player) or state.has('Hammer', player)))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player) and (state.has('Moon Pearl', player) or state.has('Hammer', player)))) elif bombshop_entrance.name == 'Bumper Cave (Bottom)': # This is mostly the same as NDW but the Mirror path requires being able to lift a rock. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and can_lift_rocks(state, player) and state.can_reach('Light World', 'Region', player))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and can_lift_rocks(state, player) and state.can_reach('Light World', 'Region', player))) elif bombshop_entrance.name == 'Old Man Cave (West)': # The three paths back are Mirror and DW walk, Mirror and Flute, or LW walk and then Mirror. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and ((can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (can_lift_rocks(state, player) and state.has('Moon Pearl', player)) or state.has('Activated Flute', player))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and ((can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (can_lift_rocks(state, player) and state.has('Moon Pearl', player)) or state.has('Activated Flute', player))) elif bombshop_entrance.name == 'Dark World Potion Shop': # You either need to Flute to 5 or cross the rock/hammer choice pass to the south. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or state.has('Hammer', player) or can_lift_rocks(state, player)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or state.has('Hammer', player) or can_lift_rocks(state, player)) elif bombshop_entrance.name == 'Kings Grave': # Either lift the rock and walk to the castle to Mirror or Mirror immediately and Flute. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or can_lift_heavy_rocks(state, player)) and state.has('Magic Mirror', player)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or can_lift_heavy_rocks(state, player)) and state.has('Magic Mirror', player)) elif bombshop_entrance.name == 'Waterfall of Wishing': # You absolutely must be able to swim to return it from here. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player) and state.has('Magic Mirror', player)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player) and state.has('Magic Mirror', player)) elif bombshop_entrance.name == 'Ice Palace': # You can swim to the dock or use the Flute to get off the island. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Flippers', player) or state.has('Activated Flute', player)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Flippers', player) or state.has('Activated Flute', player)) elif bombshop_entrance.name == 'Capacity Upgrade': # You must Mirror but then can use either Ice Palace return path. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Flippers', player) or state.has('Activated Flute', player)) and state.has('Magic Mirror', player)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Flippers', player) or state.has('Activated Flute', player)) and state.has('Magic Mirror', player)) elif bombshop_entrance.name == 'Two Brothers House (West)': # First you must Mirror. Then you can either Flute, cross the peg bridge, or use the Agah 1 portal to Mirror again. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or state.has('Hammer', player) or state.has('Beat Agahnim 1', player)) and state.has('Magic Mirror', player)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or state.has('Hammer', player) or state.has('Beat Agahnim 1', player)) and state.has('Magic Mirror', player)) elif bombshop_entrance.name in LW_inaccessible_entrances: # You can't get to the pyramid from these entrances without bomb duping. raise Exception('No valid path to open Pyramid Fairy. (Could not route from %s)' % bombshop_entrance.name) elif bombshop_entrance.name == 'Pyramid Fairy': # Self locking. The shuffles don't put the bomb shop here, but doesn't lock anything important. - set_rule(world.get_entrance('Pyramid Fairy', player), lambda state: False) + set_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: False) else: raise Exception('No logic found for routing from %s to the pyramid.' % bombshop_entrance.name) -def set_bunny_rules(world: MultiWorld, player: int, inverted: bool): +def set_bunny_rules(multiworld: MultiWorld, player: int, inverted: bool): # regions for the exits of multi-entrance caves/drops that bunny cannot pass # Note spiral cave and two brothers house are passable in superbunny state for glitch logic with extra requirements. @@ -1690,7 +1696,7 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool): def get_rule_to_add(region, location = None, connecting_entrance = None): # In OWG, a location can potentially be superbunny-mirror accessible or # bunny revival accessible. - if world.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic']: + if multiworld.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic']: if region.name == 'Swamp Palace (Entrance)': # Need to 0hp revive - not in logic return lambda state: state.has('Moon Pearl', player) if region.name == 'Tower of Hera (Bottom)': # Need to hit the crystal switch @@ -1730,7 +1736,7 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool): seen.add(new_region) if not is_link(new_region): # For glitch rulesets, establish superbunny and revival rules. - if world.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] and entrance.name not in OverworldGlitchRules.get_invalid_bunny_revival_dungeons(): + if multiworld.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] and entrance.name not in OverworldGlitchRules.get_invalid_bunny_revival_dungeons(): if region.name in OverworldGlitchRules.get_sword_required_superbunny_mirror_regions(): possible_options.append(lambda state: path_to_access_rule(new_path, entrance) and state.has('Magic Mirror', player) and has_sword(state, player)) elif (region.name in OverworldGlitchRules.get_boots_required_superbunny_mirror_regions() @@ -1753,29 +1759,29 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool): return options_to_access_rule(possible_options) # Add requirements for bunny-impassible caves if link is a bunny in them - for region in (world.get_region(name, player) for name in bunny_impassable_caves): + for region in (multiworld.get_region(name, player) for name in bunny_impassable_caves): if not is_bunny(region): continue rule = get_rule_to_add(region) for region_exit in region.exits: add_rule(region_exit, rule) - paradox_shop = world.get_region('Light World Death Mountain Shop', player) + paradox_shop = multiworld.get_region('Light World Death Mountain Shop', player) if is_bunny(paradox_shop): add_rule(paradox_shop.entrances[0], get_rule_to_add(paradox_shop)) # Add requirements for all locations that are actually in the dark world, except those available to the bunny, including dungeon revival - for entrance in world.get_entrances(player): + for entrance in multiworld.get_entrances(player): if is_bunny(entrance.connected_region): - if world.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] : + if multiworld.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] : if entrance.connected_region.type == LTTPRegionType.Dungeon: if entrance.parent_region.type != LTTPRegionType.Dungeon and entrance.connected_region.name in OverworldGlitchRules.get_invalid_bunny_revival_dungeons(): add_rule(entrance, get_rule_to_add(entrance.connected_region, None, entrance)) continue if entrance.connected_region.name == 'Turtle Rock (Entrance)': - add_rule(world.get_entrance('Turtle Rock Entrance Gap', player), get_rule_to_add(entrance.connected_region, None, entrance)) + add_rule(multiworld.get_entrance('Turtle Rock Entrance Gap', player), get_rule_to_add(entrance.connected_region, None, entrance)) for location in entrance.connected_region.locations: - if world.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] and entrance.name in OverworldGlitchRules.get_invalid_mirror_bunny_entrances(): + if multiworld.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] and entrance.name in OverworldGlitchRules.get_invalid_mirror_bunny_entrances(): continue if location.name in bunny_accessible_locations: continue diff --git a/worlds/alttp/Shops.py b/worlds/alttp/Shops.py index 89e43a1a04..182f25ddc2 100644 --- a/worlds/alttp/Shops.py +++ b/worlds/alttp/Shops.py @@ -7,7 +7,7 @@ from Utils import int16_as_bytes from worlds.generic.Rules import add_rule -from BaseClasses import CollectionState +from BaseClasses import CollectionState, Item, MultiWorld from .SubClasses import ALttPLocation from .Items import item_name_groups @@ -159,7 +159,7 @@ shop_class_mapping = {ShopType.UpgradeShop: UpgradeShop, ShopType.TakeAny: TakeAny} -def push_shop_inventories(multiworld): +def push_shop_inventories(multiworld: MultiWorld): all_shops = [] for world in multiworld.get_game_worlds(ALttPLocation.game): all_shops.extend(world.shops) @@ -183,7 +183,7 @@ def push_shop_inventories(multiworld): world.pushed_shop_inventories.set() -def create_shops(multiworld, player: int): +def create_shops(multiworld: MultiWorld, player: int): from .Options import RandomizeShopInventories player_shop_table = shop_table.copy() if multiworld.worlds[player].options.include_witch_hut: @@ -306,7 +306,7 @@ shop_generation_types = { } -def set_up_shops(multiworld, player: int): +def set_up_shops(multiworld: MultiWorld, player: int): from .Options import small_key_shuffle # TODO: move hard+ mode changes for shields here, utilizing the new shops @@ -408,7 +408,7 @@ price_rate_display = { } -def get_price_modifier(item) -> float: +def get_price_modifier(item: Item) -> float: if item.game == "A Link to the Past": if any(x in item.name for x in ['Compass', 'Map', 'Single Bomb', 'Single Arrow', 'Piece of Heart']): @@ -428,7 +428,7 @@ def get_price_modifier(item) -> float: return 0.25 -def get_price(multiworld, item, player: int, price_type=None): +def get_price(multiworld: MultiWorld, item: Item, player: int, price_type=None): """Converts a raw Rupee price into a special price type""" from .Options import small_key_shuffle if price_type: diff --git a/worlds/alttp/UnderworldGlitchRules.py b/worlds/alttp/UnderworldGlitchRules.py index 25511f320d..1b606a23af 100644 --- a/worlds/alttp/UnderworldGlitchRules.py +++ b/worlds/alttp/UnderworldGlitchRules.py @@ -1,3 +1,4 @@ +from BaseClasses import MultiWorld, CollectionState from worlds.generic.Rules import set_rule, add_rule from .StateHelpers import can_bomb_clip, has_sword, has_beam_sword, has_fire_source, can_melt_things, has_misery_mire_medallion from .SubClasses import LTTPEntrance @@ -5,27 +6,27 @@ from .SubClasses import LTTPEntrance # We actually need the logic to properly "mark" these regions as Light or Dark world. # Therefore we need to make these connections during the normal link_entrances stage, rather than during set_rules. -def underworld_glitch_connections(world, player): - specrock = world.get_region('Spectacle Rock Cave (Bottom)', player) - mire = world.get_region('Misery Mire (West)', player) +def underworld_glitch_connections(multiworld: MultiWorld, player: int): + specrock = multiworld.get_region('Spectacle Rock Cave (Bottom)', player) + mire = multiworld.get_region('Misery Mire (West)', player) kikiskip = specrock.create_exit('Kiki Skip') mire_to_hera = mire.create_exit('Mire to Hera Clip') mire_to_swamp = mire.create_exit('Hera to Swamp Clip') - if world.worlds[player].fix_fake_world: - kikiskip.connect(world.get_entrance('Palace of Darkness Exit', player).connected_region) - mire_to_hera.connect(world.get_entrance('Tower of Hera Exit', player).connected_region) - mire_to_swamp.connect(world.get_entrance('Swamp Palace Exit', player).connected_region) + if multiworld.worlds[player].fix_fake_world: + kikiskip.connect(multiworld.get_entrance('Palace of Darkness Exit', player).connected_region) + mire_to_hera.connect(multiworld.get_entrance('Tower of Hera Exit', player).connected_region) + mire_to_swamp.connect(multiworld.get_entrance('Swamp Palace Exit', player).connected_region) else: - kikiskip.connect(world.get_region('Palace of Darkness (Entrance)', player)) - mire_to_hera.connect(world.get_region('Tower of Hera (Bottom)', player)) - mire_to_swamp.connect(world.get_region('Swamp Palace (Entrance)', player)) + kikiskip.connect(multiworld.get_region('Palace of Darkness (Entrance)', player)) + mire_to_hera.connect(multiworld.get_region('Tower of Hera (Bottom)', player)) + mire_to_swamp.connect(multiworld.get_region('Swamp Palace (Entrance)', player)) # For some entrances, we need to fake having pearl, because we're in fake DW/LW. # This creates a copy of the input state that has Moon Pearl. -def fake_pearl_state(state, player): +def fake_pearl_state(state: CollectionState, player: int): if state.has('Moon Pearl', player): return state fake_state = state.copy() @@ -35,11 +36,11 @@ def fake_pearl_state(state, player): # Sets the rules on where we can actually go using this clip. # Behavior differs based on what type of ER shuffle we're playing. -def dungeon_reentry_rules(world, player, clip: LTTPEntrance, dungeon_region: str, dungeon_exit: str): - fix_dungeon_exits = world.worlds[player].fix_palaceofdarkness_exit - fix_fake_worlds = world.worlds[player].fix_fake_world +def dungeon_reentry_rules(multiworld: MultiWorld, player: int, clip: LTTPEntrance, dungeon_region: str, dungeon_exit: str): + fix_dungeon_exits = multiworld.worlds[player].fix_palaceofdarkness_exit + fix_fake_worlds = multiworld.worlds[player].fix_fake_world - dungeon_entrance = [r for r in world.get_region(dungeon_region, player).entrances if r.name != clip.name][0] + dungeon_entrance = [r for r in multiworld.get_region(dungeon_region, player).entrances if r.name != clip.name][0] if not fix_dungeon_exits: # vanilla, simple, restricted, dungeons_simple; should never have fake worlds fix # Dungeons are only shuffled among themselves. We need to check SW, MM, and AT because they can't be reentered trivially. if dungeon_entrance.name == 'Skull Woods Final Section': @@ -49,64 +50,64 @@ def dungeon_reentry_rules(world, player, clip: LTTPEntrance, dungeon_region: str elif dungeon_entrance.name == 'Agahnims Tower': add_rule(clip, lambda state: state.has('Cape', player) or has_beam_sword(state, player) or state.has('Beat Agahnim 1', player)) # kill/bypass barrier # Then we set a restriction on exiting the dungeon, so you can't leave unless you got in normally. - add_rule(world.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state)) + add_rule(multiworld.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state)) elif not fix_fake_worlds: # full, dungeons_full; fixed dungeon exits, but no fake worlds fix # Entry requires the entrance's requirements plus a fake pearl, but you don't gain logical access to the surrounding region. add_rule(clip, lambda state: dungeon_entrance.access_rule(fake_pearl_state(state, player))) # exiting restriction - add_rule(world.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state)) + add_rule(multiworld.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state)) # Otherwise, the shuffle type is crossed, dungeons_crossed, or insanity; all of these do not need additional rules on where we can go, # since the clip links directly to the exterior region. -def underworld_glitches_rules(world, player): +def underworld_glitches_rules(multiworld: MultiWorld, player: int): # Ice Palace Entrance Clip # This is the easiest one since it's a simple internal clip. # Need to also add melting to freezor chest since it's otherwise assumed. # Also can pick up the first jelly key from behind. - add_rule(world.get_entrance('Ice Palace (Main)', player), lambda state: can_bomb_clip(state, world.get_region('Ice Palace (Entrance)', player), player), combine='or') - add_rule(world.get_location('Ice Palace - Freezor Chest', player), lambda state: can_melt_things(state, player)) - add_rule(world.get_location('Ice Palace - Jelly Key Drop', player), lambda state: can_bomb_clip(state, world.get_region('Ice Palace (Entrance)', player), player), combine='or') + add_rule(multiworld.get_entrance('Ice Palace (Main)', player), lambda state: can_bomb_clip(state, multiworld.get_region('Ice Palace (Entrance)', player), player), combine='or') + add_rule(multiworld.get_location('Ice Palace - Freezor Chest', player), lambda state: can_melt_things(state, player)) + add_rule(multiworld.get_location('Ice Palace - Jelly Key Drop', player), lambda state: can_bomb_clip(state, multiworld.get_region('Ice Palace (Entrance)', player), player), combine='or') # Kiki Skip - kikiskip = world.get_entrance('Kiki Skip', player) + kikiskip = multiworld.get_entrance('Kiki Skip', player) set_rule(kikiskip, lambda state: can_bomb_clip(state, kikiskip.parent_region, player)) - dungeon_reentry_rules(world, player, kikiskip, 'Palace of Darkness (Entrance)', 'Palace of Darkness Exit') + dungeon_reentry_rules(multiworld, player, kikiskip, 'Palace of Darkness (Entrance)', 'Palace of Darkness Exit') # Mire -> Hera -> Swamp # Using mire keys on other dungeon doors - mire = world.get_region('Misery Mire (West)', player) + mire = multiworld.get_region('Misery Mire (West)', player) mire_clip = lambda state: state.can_reach('Misery Mire (West)', 'Region', player) and can_bomb_clip(state, mire, player) and has_fire_source(state, player) - hera_clip = lambda state: state.can_reach('Tower of Hera (Top)', 'Region', player) and can_bomb_clip(state, world.get_region('Tower of Hera (Top)', player), player) - add_rule(world.get_entrance('Tower of Hera Big Key Door', player), lambda state: mire_clip(state) and state.has('Big Key (Misery Mire)', player), combine='or') - add_rule(world.get_entrance('Swamp Palace Small Key Door', player), lambda state: mire_clip(state), combine='or') - add_rule(world.get_entrance('Swamp Palace (Center)', player), lambda state: mire_clip(state) or hera_clip(state), combine='or') + hera_clip = lambda state: state.can_reach('Tower of Hera (Top)', 'Region', player) and can_bomb_clip(state, multiworld.get_region('Tower of Hera (Top)', player), player) + add_rule(multiworld.get_entrance('Tower of Hera Big Key Door', player), lambda state: mire_clip(state) and state.has('Big Key (Misery Mire)', player), combine='or') + add_rule(multiworld.get_entrance('Swamp Palace Small Key Door', player), lambda state: mire_clip(state), combine='or') + add_rule(multiworld.get_entrance('Swamp Palace (Center)', player), lambda state: mire_clip(state) or hera_clip(state), combine='or') # Build the rule for SP moat. # We need to be able to s+q to old man, then go to either Mire or Hera at either Hera or GT. # First we require a certain type of entrance shuffle, then build the rule from its pieces. - if not world.worlds[player].swamp_patch_required: - if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: + if not multiworld.worlds[player].swamp_patch_required: + if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: rule_map = { 'Misery Mire (Entrance)': (lambda state: True), 'Tower of Hera (Bottom)': (lambda state: state.can_reach('Tower of Hera Big Key Door', 'Entrance', player)) } - inverted = world.worlds[player].options.mode == 'inverted' + inverted = multiworld.worlds[player].options.mode == 'inverted' hera_rule = lambda state: (state.has('Moon Pearl', player) or not inverted) and \ - rule_map.get(world.get_entrance('Tower of Hera', player).connected_region.name, lambda state: False)(state) + rule_map.get(multiworld.get_entrance('Tower of Hera', player).connected_region.name, lambda state: False)(state) gt_rule = lambda state: (state.has('Moon Pearl', player) or inverted) and \ - rule_map.get(world.get_entrance(('Ganons Tower' if not inverted else 'Inverted Ganons Tower'), player).connected_region.name, lambda state: False)(state) + rule_map.get(multiworld.get_entrance(('Ganons Tower' if not inverted else 'Inverted Ganons Tower'), player).connected_region.name, lambda state: False)(state) mirrorless_moat_rule = lambda state: state.can_reach('Old Man S&Q', 'Entrance', player) and mire_clip(state) and (hera_rule(state) or gt_rule(state)) - add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player) or mirrorless_moat_rule(state)) + add_rule(multiworld.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player) or mirrorless_moat_rule(state)) else: - add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player)) + add_rule(multiworld.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player)) # Using the entrances for various ER types. Hera -> Swamp never matters because you can only logically traverse with the mire keys - mire_to_hera = world.get_entrance('Mire to Hera Clip', player) - mire_to_swamp = world.get_entrance('Hera to Swamp Clip', player) + mire_to_hera = multiworld.get_entrance('Mire to Hera Clip', player) + mire_to_swamp = multiworld.get_entrance('Hera to Swamp Clip', player) set_rule(mire_to_hera, mire_clip) set_rule(mire_to_swamp, lambda state: mire_clip(state) and state.has('Flippers', player)) - dungeon_reentry_rules(world, player, mire_to_hera, 'Tower of Hera (Bottom)', 'Tower of Hera Exit') - dungeon_reentry_rules(world, player, mire_to_swamp, 'Swamp Palace (Entrance)', 'Swamp Palace Exit') + dungeon_reentry_rules(multiworld, player, mire_to_hera, 'Tower of Hera (Bottom)', 'Tower of Hera Exit') + dungeon_reentry_rules(multiworld, player, mire_to_swamp, 'Swamp Palace (Entrance)', 'Swamp Palace Exit') From 699ca8adf6bcf8dbb64ae34607bf0204ecce9446 Mon Sep 17 00:00:00 2001 From: DrAwesome4333 <22409322+DrAwesome4333@users.noreply.github.com> Date: Tue, 24 Feb 2026 18:47:54 -0700 Subject: [PATCH 02/53] WebHost: Add CORS headers to API Endpoints (#5777) --- WebHostLib/api/__init__.py | 10 ++++++++++ WebHostLib/requirements.txt | 1 + 2 files changed, 11 insertions(+) diff --git a/WebHostLib/api/__init__.py b/WebHostLib/api/__init__.py index 54eb5c1de1..63914a06ba 100644 --- a/WebHostLib/api/__init__.py +++ b/WebHostLib/api/__init__.py @@ -2,10 +2,20 @@ from typing import List, Tuple from flask import Blueprint +from flask_cors import CORS from ..models import Seed, Slot api_endpoints = Blueprint('api', __name__, url_prefix="/api") +cors = CORS(api_endpoints, resources={ + r"/api/datapackage/*": {"origins": "*"}, + r"/api/datapackage": {"origins": "*"}, + r"/api/datapackage_checksum/*": {"origins": "*"}, + r"/api/room_status/*": {"origins": "*"}, + r"/api/tracker/*": {"origins": "*"}, + r"/api/static_tracker/*": {"origins": "*"}, + r"/api/slot_data_tracker/*": {"origins": "*"} + }) def get_players(seed: Seed) -> List[Tuple[str, str]]: diff --git a/WebHostLib/requirements.txt b/WebHostLib/requirements.txt index c4267dc284..c9a923680a 100644 --- a/WebHostLib/requirements.txt +++ b/WebHostLib/requirements.txt @@ -6,6 +6,7 @@ waitress>=3.0.2 Flask-Caching>=2.3.0 Flask-Compress==1.18 # pkg_resources can't resolve the "backports.zstd" dependency of >1.18, breaking ModuleUpdate.py Flask-Limiter>=3.12 +Flask-Cors>=6.0.2 bokeh>=3.6.3 markupsafe>=3.0.2 setproctitle>=1.3.5 From b30b2ecb07552521d61046844a62cdf54a9b0d53 Mon Sep 17 00:00:00 2001 From: Duck <31627079+duckboycool@users.noreply.github.com> Date: Wed, 25 Feb 2026 12:52:34 -0700 Subject: [PATCH 03/53] Return new state man (Vi's note: I have chosen not to change this title) (#5978) --- docs/world api.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/world api.md b/docs/world api.md index 48e863fb26..2df7b12744 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -770,6 +770,7 @@ class MyGameState(LogicMixin): new_state.mygame_defeatable_enemies = { player: enemies.copy() for player, enemies in self.mygame_defeatable_enemies.items() } + return new_state ``` After doing this, you can now access `state.mygame_defeatable_enemies[player]` from your access rules. From eeb022fa0c69fa787cf6f771221d39232d646430 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Wed, 25 Feb 2026 19:24:50 -0600 Subject: [PATCH 04/53] The Messenger: minor maintenance (#5965) --- worlds/messenger/archipelago.json | 4 ++++ worlds/messenger/client_setup.py | 6 +++++- worlds/messenger/rules.py | 4 ++-- 3 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 worlds/messenger/archipelago.json diff --git a/worlds/messenger/archipelago.json b/worlds/messenger/archipelago.json new file mode 100644 index 0000000000..86aefc42a8 --- /dev/null +++ b/worlds/messenger/archipelago.json @@ -0,0 +1,4 @@ +{ + "game": "The Messenger", + "authors": ["alwaysintreble"] +} \ No newline at end of file diff --git a/worlds/messenger/client_setup.py b/worlds/messenger/client_setup.py index 3ef1df75cc..02fd299a6c 100644 --- a/worlds/messenger/client_setup.py +++ b/worlds/messenger/client_setup.py @@ -28,6 +28,8 @@ def create_yes_no_popup(title: str, text: str, callback: Callable[[str], None]) def launch_game(*args) -> None: """Check the game installation, then launch it""" + prompt: ButtonsPrompt | None = None + def courier_installed() -> bool: """Check if Courier is installed""" assembly_path = os.path.join(game_folder, "TheMessenger_Data", "Managed", "Assembly-CSharp.dll") @@ -190,7 +192,7 @@ def launch_game(*args) -> None: def launch(answer: str | None = None) -> None: """Launch the game.""" - nonlocal args + nonlocal args, prompt if prompt: prompt.dismiss() @@ -256,3 +258,5 @@ def launch_game(*args) -> None: prompt = create_yes_no_popup("Launch Game", "Mod installed and up to date. Would you like to launch the game now?", launch) + else: + launch() diff --git a/worlds/messenger/rules.py b/worlds/messenger/rules.py index 2d5ee1b8a9..7f17232cfb 100644 --- a/worlds/messenger/rules.py +++ b/worlds/messenger/rules.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING -from BaseClasses import CollectionState -from worlds.generic.Rules import CollectionRule, add_rule, allow_self_locking_items +from BaseClasses import CollectionState, CollectionRule +from worlds.generic.Rules import add_rule, allow_self_locking_items from .constants import NOTES, PHOBEKINS from .options import MessengerAccessibility From 2db5435474f3d722b60f8354a6b9c98fb0705840 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Thu, 26 Feb 2026 09:34:23 +0000 Subject: [PATCH 05/53] CI: upgrade InnoSetup to 6.7.0 (#5979) --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9ebe42307d..772a6c0be3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -51,7 +51,7 @@ jobs: run: | Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force - choco install innosetup --version=6.2.2 --allow-downgrade + choco install innosetup --version=6.7.0 --allow-downgrade - name: Build run: | python -m pip install --upgrade pip From fcccbfca65d4c86180748f036f1e46ae31da6b93 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Thu, 26 Feb 2026 18:31:39 +0000 Subject: [PATCH 06/53] MultiServer: don't keep multidata alive for race_mode (#5980) --- MultiServer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MultiServer.py b/MultiServer.py index 52c80c5540..d317e7b8fa 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -496,7 +496,8 @@ class Context: self.read_data = {} # there might be a better place to put this. - self.read_data["race_mode"] = lambda: decoded_obj.get("race_mode", 0) + race_mode = decoded_obj.get("race_mode", 0) + self.read_data["race_mode"] = lambda: race_mode mdata_ver = decoded_obj["minimum_versions"]["server"] if mdata_ver > version_tuple: raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver}, " From ff5402c410b570592cff0b8a6d1bc70e1f841f24 Mon Sep 17 00:00:00 2001 From: Chris W Date: Sat, 28 Feb 2026 23:56:28 +0100 Subject: [PATCH 07/53] Fix(undertale): prevent massive bounce msg spam for position updates (#5990) * fix(undertale): prevent massive bounce msg spam for position updates * make sure player is removed on leaving / timing out * do not check for tags: online, as bounce evaluation is or'd --- UndertaleClient.py | 100 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 82 insertions(+), 18 deletions(-) diff --git a/UndertaleClient.py b/UndertaleClient.py index 1c522fac92..9dc1136b77 100644 --- a/UndertaleClient.py +++ b/UndertaleClient.py @@ -1,6 +1,7 @@ from __future__ import annotations import os import sys +import time import asyncio import typing import bsdiff4 @@ -15,6 +16,9 @@ from CommonClient import CommonContext, server_loop, \ gui_enabled, ClientCommandProcessor, logger, get_base_parser from Utils import async_start +# Heartbeat for position sharing via bounces, in seconds +UNDERTALE_STATUS_INTERVAL = 30.0 +UNDERTALE_ONLINE_TIMEOUT = 60.0 class UndertaleCommandProcessor(ClientCommandProcessor): def __init__(self, ctx): @@ -109,6 +113,11 @@ class UndertaleContext(CommonContext): self.completed_routes = {"pacifist": 0, "genocide": 0, "neutral": 0} # self.save_game_folder: files go in this path to pass data between us and the actual game self.save_game_folder = os.path.expandvars(r"%localappdata%/UNDERTALE") + self.last_sent_position: typing.Optional[tuple] = None + self.last_room: typing.Optional[str] = None + self.last_status_write: float = 0.0 + self.other_undertale_status: dict[int, dict] = {} + def patch_game(self): with open(Utils.user_path("Undertale", "data.win"), "rb") as f: @@ -219,6 +228,9 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict): await ctx.send_msgs([{"cmd": "SetNotify", "keys": [str(ctx.slot)+" RoutesDone neutral", str(ctx.slot)+" RoutesDone pacifist", str(ctx.slot)+" RoutesDone genocide"]}]) + if any(info.game == "Undertale" and slot != ctx.slot + for slot, info in ctx.slot_info.items()): + ctx.set_notify("undertale_room_status") if args["slot_data"]["only_flakes"]: with open(os.path.join(ctx.save_game_folder, "GenoNoChest.flag"), "w") as f: f.close() @@ -263,6 +275,12 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict): if str(ctx.slot)+" RoutesDone pacifist" in args["keys"]: if args["keys"][str(ctx.slot) + " RoutesDone pacifist"] is not None: ctx.completed_routes["pacifist"] = args["keys"][str(ctx.slot)+" RoutesDone pacifist"] + if "undertale_room_status" in args["keys"] and args["keys"]["undertale_room_status"]: + status = args["keys"]["undertale_room_status"] + ctx.other_undertale_status = { + int(key): val for key, val in status.items() + if int(key) != ctx.slot + } elif cmd == "SetReply": if args["value"] is not None: if str(ctx.slot)+" RoutesDone pacifist" == args["key"]: @@ -271,6 +289,11 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict): ctx.completed_routes["genocide"] = args["value"] elif str(ctx.slot)+" RoutesDone neutral" == args["key"]: ctx.completed_routes["neutral"] = args["value"] + if args.get("key") == "undertale_room_status" and args.get("value"): + ctx.other_undertale_status = { + int(key): val for key, val in args["value"].items() + if int(key) != ctx.slot + } elif cmd == "ReceivedItems": start_index = args["index"] @@ -368,9 +391,8 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict): f.close() elif cmd == "Bounced": - tags = args.get("tags", []) - if "Online" in tags: - data = args.get("data", {}) + data = args.get("data", {}) + if "x" in data and "room" in data: if data["player"] != ctx.slot and data["player"] is not None: filename = f"FRISK" + str(data["player"]) + ".playerspot" with open(os.path.join(ctx.save_game_folder, filename), "w") as f: @@ -381,21 +403,63 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict): async def multi_watcher(ctx: UndertaleContext): while not ctx.exit_event.is_set(): - path = ctx.save_game_folder - for root, dirs, files in os.walk(path): - for file in files: - if "spots.mine" in file and "Online" in ctx.tags: - with open(os.path.join(root, file), "r") as mine: - this_x = mine.readline() - this_y = mine.readline() - this_room = mine.readline() - this_sprite = mine.readline() - this_frame = mine.readline() - mine.close() - message = [{"cmd": "Bounce", "tags": ["Online"], - "data": {"player": ctx.slot, "x": this_x, "y": this_y, "room": this_room, - "spr": this_sprite, "frm": this_frame}}] - await ctx.send_msgs(message) + if "Online" in ctx.tags and any( + info.game == "Undertale" and slot != ctx.slot + for slot, info in ctx.slot_info.items()): + now = time.time() + path = ctx.save_game_folder + for root, dirs, files in os.walk(path): + for file in files: + if "spots.mine" in file: + with open(os.path.join(root, file), "r") as mine: + this_x = mine.readline() + this_y = mine.readline() + this_room = mine.readline() + this_sprite = mine.readline() + this_frame = mine.readline() + + if this_room != ctx.last_room or \ + now - ctx.last_status_write >= UNDERTALE_STATUS_INTERVAL: + ctx.last_room = this_room + ctx.last_status_write = now + await ctx.send_msgs([{ + "cmd": "Set", + "key": "undertale_room_status", + "default": {}, + "want_reply": False, + "operations": [{"operation": "update", + "value": {str(ctx.slot): {"room": this_room, + "time": now}}}] + }]) + + # If player was visible but timed out (heartbeat) or left the room, remove them. + for slot, entry in ctx.other_undertale_status.items(): + if entry.get("room") != this_room or \ + now - entry.get("time", now) > UNDERTALE_ONLINE_TIMEOUT: + playerspot = os.path.join(ctx.save_game_folder, + f"FRISK{slot}.playerspot") + if os.path.exists(playerspot): + os.remove(playerspot) + + current_position = (this_x, this_y, this_room, this_sprite, this_frame) + if current_position == ctx.last_sent_position: + continue + + # Empty status dict = no data yet → send to bootstrap. + online_in_room = any( + entry.get("room") == this_room and + now - entry.get("time", now) <= UNDERTALE_ONLINE_TIMEOUT + for entry in ctx.other_undertale_status.values() + ) + if ctx.other_undertale_status and not online_in_room: + continue + + message = [{"cmd": "Bounce", "games": ["Undertale"], + "data": {"player": ctx.slot, "x": this_x, "y": this_y, + "room": this_room, "spr": this_sprite, + "frm": this_frame}}] + await ctx.send_msgs(message) + ctx.last_sent_position = current_position await asyncio.sleep(0.1) From 61d5120f66b87a683c8099d4a7b9cf4a0b62a583 Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Sat, 28 Feb 2026 15:14:33 -0800 Subject: [PATCH 08/53] Core: use typing_extensions `deprecated` (#5989) --- Utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Utils.py b/Utils.py index bf46d0832d..c18298559a 100644 --- a/Utils.py +++ b/Utils.py @@ -23,6 +23,7 @@ from time import sleep from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union, TypeGuard from yaml import load, load_all, dump from pathspec import PathSpec, GitIgnoreSpec +from typing_extensions import deprecated try: from yaml import CLoader as UnsafeLoader, CSafeLoader as SafeLoader, CDumper as Dumper @@ -315,6 +316,7 @@ def get_public_ipv6() -> str: return ip +@deprecated("Utils.get_options() is deprecated. Use the settings API instead.") def get_options() -> Settings: deprecate("Utils.get_options() is deprecated. Use the settings API instead.") return get_settings() @@ -1003,6 +1005,7 @@ def async_start(co: Coroutine[None, None, typing.Any], name: Optional[str] = Non def deprecate(message: str, add_stacklevels: int = 0): + """also use typing_extensions.deprecated wherever you use this""" if __debug__: raise Exception(message) warnings.warn(message, stacklevel=2 + add_stacklevels) @@ -1067,6 +1070,7 @@ def _extend_freeze_support() -> None: multiprocessing.freeze_support = multiprocessing.spawn.freeze_support = _freeze_support if is_frozen() else _noop +@deprecated("Use multiprocessing.freeze_support() instead") def freeze_support() -> None: """This now only calls multiprocessing.freeze_support since we are patching freeze_support on module load.""" import multiprocessing From e49ba2ff6fc849952889e344927a35e8db1fd7b0 Mon Sep 17 00:00:00 2001 From: Duck <31627079+duckboycool@users.noreply.github.com> Date: Sat, 28 Feb 2026 17:30:26 -0700 Subject: [PATCH 09/53] Undertale: Use check_locations helper to avoid redundant sends (#5993) --- UndertaleClient.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/UndertaleClient.py b/UndertaleClient.py index 9dc1136b77..b0efce206a 100644 --- a/UndertaleClient.py +++ b/UndertaleClient.py @@ -300,11 +300,8 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict): if start_index == 0: ctx.items_received = [] elif start_index != len(ctx.items_received): - sync_msg = [{"cmd": "Sync"}] - if ctx.locations_checked: - sync_msg.append({"cmd": "LocationChecks", - "locations": list(ctx.locations_checked)}) - await ctx.send_msgs(sync_msg) + await ctx.check_locations(ctx.locations_checked) + await ctx.send_msgs([{"cmd": "Sync"}]) if start_index == len(ctx.items_received): counter = -1 placedWeapon = 0 @@ -473,10 +470,9 @@ async def game_watcher(ctx: UndertaleContext): for file in files: if ".item" in file: os.remove(os.path.join(root, file)) - sync_msg = [{"cmd": "Sync"}] - if ctx.locations_checked: - sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)}) - await ctx.send_msgs(sync_msg) + await ctx.check_locations(ctx.locations_checked) + await ctx.send_msgs([{"cmd": "Sync"}]) + ctx.syncing = False if ctx.got_deathlink: ctx.got_deathlink = False @@ -511,7 +507,7 @@ async def game_watcher(ctx: UndertaleContext): for l in lines: sending = sending+[(int(l.rstrip('\n')))+12000] finally: - await ctx.send_msgs([{"cmd": "LocationChecks", "locations": sending}]) + await ctx.check_locations(sending) if "victory" in file and str(ctx.route) in file: victory = True if ".playerspot" in file and "Online" not in ctx.tags: From 922c7fe86aa580984584b8541e33b1a9a3962aca Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 1 Mar 2026 17:51:56 +0100 Subject: [PATCH 10/53] Core: allow async def functions as commands (#5859) --- MultiServer.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/MultiServer.py b/MultiServer.py index d317e7b8fa..ed50c98db6 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -1302,6 +1302,13 @@ class CommandMeta(type): commands.update(base.commands) commands.update({command_name[5:]: method for command_name, method in attrs.items() if command_name.startswith("_cmd_")}) + for command_name, method in commands.items(): + # wrap async def functions so they run on default asyncio loop + if inspect.iscoroutinefunction(method): + def _wrapper(self, *args, _method=method, **kwargs): + return async_start(_method(self, *args, **kwargs)) + functools.update_wrapper(_wrapper, method) + commands[command_name] = _wrapper return super(CommandMeta, cls).__new__(cls, name, bases, attrs) From a3e8f69909b3e77344033be88b2f87b7b54b939a Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 1 Mar 2026 17:53:41 +0100 Subject: [PATCH 11/53] Core: introduce finalize_multiworld and pre_output stages (#5700) Co-authored-by: Ishigh1 Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com> Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- Main.py | 3 +++ test/bases.py | 1 + test/general/test_ids.py | 1 + test/general/test_implemented.py | 3 +++ test/general/test_items.py | 1 + test/multiworld/test_multiworlds.py | 2 ++ worlds/AutoWorld.py | 17 +++++++++++++++++ 7 files changed, 28 insertions(+) diff --git a/Main.py b/Main.py index 47a28813fc..924def653b 100644 --- a/Main.py +++ b/Main.py @@ -207,6 +207,9 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None) else: logger.info("Progression balancing skipped.") + AutoWorld.call_all(multiworld, "finalize_multiworld") + AutoWorld.call_all(multiworld, "pre_output") + # we're about to output using multithreading, so we're removing the global random state to prevent accidental use multiworld.random.passthrough = False diff --git a/test/bases.py b/test/bases.py index dd93ca6452..19b19bea67 100644 --- a/test/bases.py +++ b/test/bases.py @@ -248,6 +248,7 @@ class WorldTestBase(unittest.TestCase): with self.subTest("Game", game=self.game, seed=self.multiworld.seed): distribute_items_restrictive(self.multiworld) call_all(self.multiworld, "post_fill") + call_all(self.multiworld, "finalize_multiworld") self.assertTrue(fulfills_accessibility(), "Collected all locations, but can't beat the game.") placed_items = [loc.item for loc in self.multiworld.get_locations() if loc.item and loc.item.code] self.assertLessEqual(len(self.multiworld.itempool), len(placed_items), diff --git a/test/general/test_ids.py b/test/general/test_ids.py index ad8aad11d1..08b4d0aa49 100644 --- a/test/general/test_ids.py +++ b/test/general/test_ids.py @@ -88,6 +88,7 @@ class TestIDs(unittest.TestCase): multiworld = setup_solo_multiworld(world_type) distribute_items_restrictive(multiworld) call_all(multiworld, "post_fill") + call_all(multiworld, "finalize_multiworld") datapackage = world_type.get_data_package_data() for item_group, item_names in datapackage["item_name_groups"].items(): self.assertIsInstance(item_group, str, diff --git a/test/general/test_implemented.py b/test/general/test_implemented.py index de432e3690..add6e5321e 100644 --- a/test/general/test_implemented.py +++ b/test/general/test_implemented.py @@ -46,6 +46,8 @@ class TestImplemented(unittest.TestCase): with self.subTest(game=game_name, seed=multiworld.seed): distribute_items_restrictive(multiworld) call_all(multiworld, "post_fill") + call_all(multiworld, "finalize_multiworld") + call_all(multiworld, "pre_output") for key, data in multiworld.worlds[1].fill_slot_data().items(): self.assertIsInstance(key, str, "keys in slot data must be a string") convert_to_base_types(data) # only put base data types into slot data @@ -93,6 +95,7 @@ class TestImplemented(unittest.TestCase): with self.subTest(game=game_name, seed=multiworld.seed): distribute_items_restrictive(multiworld) call_all(multiworld, "post_fill") + call_all(multiworld, "finalize_multiworld") # Note: `multiworld.get_spheres()` iterates a set of locations, so the order that locations are checked # is nondeterministic and may vary between runs with the same seed. diff --git a/test/general/test_items.py b/test/general/test_items.py index 694e0db406..9c300cf94e 100644 --- a/test/general/test_items.py +++ b/test/general/test_items.py @@ -123,6 +123,7 @@ class TestBase(unittest.TestCase): call_all(multiworld, "pre_fill") distribute_items_restrictive(multiworld) call_all(multiworld, "post_fill") + call_all(multiworld, "finalize_multiworld") self.assertTrue(multiworld.can_beat_game(CollectionState(multiworld)), f"seed = {multiworld.seed}") for game_name, world_type in AutoWorldRegister.world_types.items(): diff --git a/test/multiworld/test_multiworlds.py b/test/multiworld/test_multiworlds.py index 203af8b63a..d22013b4e0 100644 --- a/test/multiworld/test_multiworlds.py +++ b/test/multiworld/test_multiworlds.py @@ -61,6 +61,7 @@ class TestAllGamesMultiworld(MultiworldTestBase): with self.subTest("filling multiworld", seed=self.multiworld.seed): distribute_items_restrictive(self.multiworld) call_all(self.multiworld, "post_fill") + call_all(self.multiworld, "finalize_multiworld") self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game") @@ -78,4 +79,5 @@ class TestTwoPlayerMulti(MultiworldTestBase): with self.subTest("filling multiworld", games=world_type.game, seed=self.multiworld.seed): distribute_items_restrictive(self.multiworld) call_all(self.multiworld, "post_fill") + call_all(self.multiworld, "finalize_multiworld") self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game") diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 327e386c05..327746f1ce 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -430,6 +430,23 @@ class World(metaclass=AutoWorldRegister): This happens before progression balancing, so the items may not be in their final locations yet. """ + def finalize_multiworld(self) -> None: + """ + Optional Method that is called after fill and progression balancing. + This is the last stage of generation where worlds may change logically relevant data, + such as item placements and connections. To not break assumptions, + only ever increase accessibility, never decrease it. + """ + pass + + def pre_output(self): + """ + Optional method that is called before output generation. + Items and connections are not meant to be moved anymore, + anything that would affect logical spheres is forbidden at this point. + """ + pass + def generate_output(self, output_directory: str) -> None: """ This method gets called from a threadpool, do not use multiworld.random here. From f26313367e55ebf7555dfc97aad40682dfc71d13 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 3 Mar 2026 23:02:12 +0000 Subject: [PATCH 12/53] MultiServer: graceful shutdown for ctrl+c and sigterm (#5996) --- MultiServer.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/MultiServer.py b/MultiServer.py index ed50c98db6..0ba5adaf40 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -21,6 +21,7 @@ import time import typing import weakref import zlib +from signal import SIGINT, SIGTERM import ModuleUpdate @@ -2571,6 +2572,8 @@ async def console(ctx: Context): input_text = await queue.get() queue.task_done() ctx.commandprocessor(input_text) + except asyncio.exceptions.CancelledError: + ctx.logger.info("ConsoleTask cancelled") except: import traceback traceback.print_exc() @@ -2737,6 +2740,15 @@ async def main(args: argparse.Namespace): console_task = asyncio.create_task(console(ctx)) if ctx.auto_shutdown: ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, [console_task])) + + def stop(): + for remove_signal in [SIGINT, SIGTERM]: + asyncio.get_event_loop().remove_signal_handler(remove_signal) + ctx.commandprocessor._cmd_exit() + + for signal in [SIGINT, SIGTERM]: + asyncio.get_event_loop().add_signal_handler(signal, stop) + await ctx.exit_event.wait() console_task.cancel() if ctx.shutdown_task: From b372b02273436874dd7c5ce145387f96339eb5ed Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Wed, 4 Mar 2026 12:47:30 -0600 Subject: [PATCH 13/53] OptionCreator: 0.6.6 reported issues (#5949) --- OptionsCreator.py | 58 ++++++++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/OptionsCreator.py b/OptionsCreator.py index 4e56b680b8..94ca8ba7ac 100644 --- a/OptionsCreator.py +++ b/OptionsCreator.py @@ -29,7 +29,7 @@ import webbrowser import re from urllib.parse import urlparse from worlds.AutoWorld import AutoWorldRegister, World -from Options import (Option, Toggle, TextChoice, Choice, FreeText, NamedRange, Range, OptionSet, OptionList, Removed, +from Options import (Option, Toggle, TextChoice, Choice, FreeText, NamedRange, Range, OptionSet, OptionList, OptionCounter, Visibility) @@ -318,26 +318,28 @@ class OptionsCreator(ThemedApp): else: self.show_result_snack("Name cannot be longer than 16 characters.") - def create_range(self, option: typing.Type[Range], name: str): + def create_range(self, option: typing.Type[Range], name: str, bind=True): def update_text(range_box: VisualRange): self.options[name] = int(range_box.slider.value) range_box.tag.text = str(int(range_box.slider.value)) return box = VisualRange(option=option, name=name) - box.slider.bind(on_touch_move=lambda _, _1: update_text(box)) + if bind: + box.slider.bind(value=lambda _, _1: update_text(box)) self.options[name] = option.default return box def create_named_range(self, option: typing.Type[NamedRange], name: str): def set_to_custom(range_box: VisualNamedRange): - if (not self.options[name] == range_box.range.slider.value) \ - and (not self.options[name] in option.special_range_names or - range_box.range.slider.value != option.special_range_names[self.options[name]]): - # we should validate the touch here, - # but this is much cheaper + range_box.range.tag.text = str(int(range_box.range.slider.value)) + if range_box.range.slider.value in option.special_range_names.values(): + value = next(key for key, val in option.special_range_names.items() + if val == range_box.range.slider.value) + self.options[name] = value + set_button_text(box.choice, value.title()) + else: self.options[name] = int(range_box.range.slider.value) - range_box.range.tag.text = str(int(range_box.range.slider.value)) set_button_text(range_box.choice, "Custom") def set_button_text(button: MDButton, text: str): @@ -346,7 +348,7 @@ class OptionsCreator(ThemedApp): def set_value(text: str, range_box: VisualNamedRange): range_box.range.slider.value = min(max(option.special_range_names[text.lower()], option.range_start), option.range_end) - range_box.range.tag.text = str(int(range_box.range.slider.value)) + range_box.range.tag.text = str(option.special_range_names[text.lower()]) set_button_text(range_box.choice, text) self.options[name] = text.lower() range_box.range.slider.dropdown.dismiss() @@ -355,13 +357,18 @@ class OptionsCreator(ThemedApp): # for some reason this fixes an issue causing some to not open box.range.slider.dropdown.open() - box = VisualNamedRange(option=option, name=name, range_widget=self.create_range(option, name)) - if option.default in option.special_range_names: + box = VisualNamedRange(option=option, name=name, range_widget=self.create_range(option, name, bind=False)) + default: int | str = option.default + if default in option.special_range_names: # value can get mismatched in this case - box.range.slider.value = min(max(option.special_range_names[option.default], option.range_start), + box.range.slider.value = min(max(option.special_range_names[default], option.range_start), option.range_end) box.range.tag.text = str(int(box.range.slider.value)) - box.range.slider.bind(on_touch_move=lambda _, _2: set_to_custom(box)) + elif default in option.special_range_names.values(): + # better visual + default = next(key for key, val in option.special_range_names.items() if val == option.default) + set_button_text(box.choice, default.title()) + box.range.slider.bind(value=lambda _, _2: set_to_custom(box)) items = [ { "text": choice.title(), @@ -371,7 +378,7 @@ class OptionsCreator(ThemedApp): ] box.range.slider.dropdown = MDDropdownMenu(caller=box.choice, items=items) box.choice.bind(on_release=open_dropdown) - self.options[name] = option.default + self.options[name] = default return box def create_free_text(self, option: typing.Type[FreeText] | typing.Type[TextChoice], name: str): @@ -447,8 +454,12 @@ class OptionsCreator(ThemedApp): valid_keys = sorted(option.valid_keys) if option.verify_item_name: valid_keys += list(world.item_name_to_id.keys()) + if option.convert_name_groups: + valid_keys += list(world.item_name_groups.keys()) if option.verify_location_name: valid_keys += list(world.location_name_to_id.keys()) + if option.convert_name_groups: + valid_keys += list(world.location_name_groups.keys()) if not issubclass(option, OptionCounter): def apply_changes(button): @@ -470,14 +481,6 @@ class OptionsCreator(ThemedApp): dialog.scrollbox.layout.spacing = dp(5) dialog.scrollbox.layout.padding = [0, dp(5), 0, 0] - if name not in self.options: - # convert from non-mutable to mutable - # We use list syntax even for sets, set behavior is enforced through GUI - if issubclass(option, OptionCounter): - self.options[name] = deepcopy(option.default) - else: - self.options[name] = sorted(option.default) - if issubclass(option, OptionCounter): for value in sorted(self.options[name]): dialog.add_set_item(value, self.options[name].get(value, None)) @@ -491,6 +494,15 @@ class OptionsCreator(ThemedApp): def create_option_set_list_counter(self, option: typing.Type[OptionList] | typing.Type[OptionSet] | typing.Type[OptionCounter], name: str, world: typing.Type[World]): main_button = MDButton(MDButtonText(text="Edit"), on_release=lambda x: self.create_popup(option, name, world)) + + if name not in self.options: + # convert from non-mutable to mutable + # We use list syntax even for sets, set behavior is enforced through GUI + if issubclass(option, OptionCounter): + self.options[name] = deepcopy(option.default) + else: + self.options[name] = sorted(option.default) + return main_button def create_option(self, option: typing.Type[Option], name: str, world: typing.Type[World]) -> Widget: From 3ecd856e29a2ac58faf5335f765eee9445ddb93f Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Thu, 5 Mar 2026 18:41:48 -0600 Subject: [PATCH 14/53] MultiServer: fix Windows compatibility (#6010) --- MultiServer.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 0ba5adaf40..ed14b6506f 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -21,7 +21,7 @@ import time import typing import weakref import zlib -from signal import SIGINT, SIGTERM +from signal import SIGINT, SIGTERM, signal import ModuleUpdate @@ -2742,12 +2742,23 @@ async def main(args: argparse.Namespace): ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, [console_task])) def stop(): - for remove_signal in [SIGINT, SIGTERM]: - asyncio.get_event_loop().remove_signal_handler(remove_signal) + try: + for remove_signal in [SIGINT, SIGTERM]: + asyncio.get_event_loop().remove_signal_handler(remove_signal) + except NotImplementedError: + pass ctx.commandprocessor._cmd_exit() - for signal in [SIGINT, SIGTERM]: - asyncio.get_event_loop().add_signal_handler(signal, stop) + def shutdown(signum, frame): + stop() + + try: + for sig in [SIGINT, SIGTERM]: + asyncio.get_event_loop().add_signal_handler(sig, stop) + except NotImplementedError: + # add_signal_handler is only implemented for UNIX platforms + for sig in [SIGINT, SIGTERM]: + signal(sig, shutdown) await ctx.exit_event.wait() console_task.cancel() From b53f9d377302c26ae0a53bea6860f419dbd75dec Mon Sep 17 00:00:00 2001 From: qwint Date: Sat, 7 Mar 2026 17:51:42 -0600 Subject: [PATCH 15/53] Docs: Better document state.locations_checked (#6018) --- BaseClasses.py | 1 + 1 file changed, 1 insertion(+) diff --git a/BaseClasses.py b/BaseClasses.py index ccb8e0677f..69b900212c 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -727,6 +727,7 @@ class CollectionState(): advancements: Set[Location] path: Dict[Union[Region, Entrance], PathValue] locations_checked: Set[Location] + """Internal cache for Advancement Locations already checked by this CollectionState. Not for use in logic.""" stale: Dict[int, bool] allow_partial_entrances: bool additional_init_functions: List[Callable[[CollectionState, MultiWorld], None]] = [] From 366fd3712a2af9c42a02d0917417a828480d1187 Mon Sep 17 00:00:00 2001 From: Suyooo Date: Sun, 8 Mar 2026 21:28:44 +0100 Subject: [PATCH 16/53] MM2: Fix /request command help (#5805) --- worlds/mm2/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/mm2/client.py b/worlds/mm2/client.py index 96c477757d..725023c820 100644 --- a/worlds/mm2/client.py +++ b/worlds/mm2/client.py @@ -140,8 +140,8 @@ def cmd_pool(self: "BizHawkClientCommandProcessor") -> None: def cmd_request(self: "BizHawkClientCommandProcessor", amount: str, target: str) -> None: - from worlds._bizhawk.context import BizHawkClientContext """Request a refill from EnergyLink.""" + from worlds._bizhawk.context import BizHawkClientContext if self.ctx.game != "Mega Man 2": logger.warning("This command can only be used when playing Mega Man 2.") return From 9f298598107894f3ab495d0722f1262066dc787c Mon Sep 17 00:00:00 2001 From: jamesbrq Date: Sun, 8 Mar 2026 16:30:18 -0400 Subject: [PATCH 17/53] MLSS: Fix client auto-connect bug + Client cleanup (#5895) --- worlds/mlss/Client.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/worlds/mlss/Client.py b/worlds/mlss/Client.py index 7944442b2a..0334cc5abb 100644 --- a/worlds/mlss/Client.py +++ b/worlds/mlss/Client.py @@ -1,11 +1,11 @@ -from typing import TYPE_CHECKING, Optional, Set, List, Dict +import asyncio import struct +from typing import TYPE_CHECKING, Optional, Set, List, Dict from NetUtils import ClientStatus from .Locations import roomCount, nonBlock, beanstones, roomException, shop, badge, pants, eReward from .Items import items_by_id -import asyncio import worlds._bizhawk as bizhawk from worlds._bizhawk.client import BizHawkClient @@ -41,8 +41,6 @@ class MLSSClient(BizHawkClient): self.local_events = [] async def validate_rom(self, ctx: "BizHawkClientContext") -> bool: - from CommonClient import logger - try: # Check ROM name/patch version rom_name_bytes = await bizhawk.read(ctx.bizhawk_ctx, [(0xA0, 14, "ROM")]) @@ -72,20 +70,15 @@ class MLSSClient(BizHawkClient): async def set_auth(self, ctx: "BizHawkClientContext") -> None: ctx.auth = self.player_name - def on_package(self, ctx, cmd, args) -> None: - if cmd == "RoomInfo": - ctx.seed_name = args["seed_name"] - async def game_watcher(self, ctx: "BizHawkClientContext") -> None: from CommonClient import logger - try: - if ctx.seed_name is None: + if ctx.server_seed_name is None: return if not self.seed_verify: - seed = await bizhawk.read(ctx.bizhawk_ctx, [(0xDF00A0, len(ctx.seed_name), "ROM")]) + seed = await bizhawk.read(ctx.bizhawk_ctx, [(0xDF00A0, len(ctx.server_seed_name), "ROM")]) seed = seed[0].decode("UTF-8") - if seed not in ctx.seed_name: + if seed not in ctx.server_seed_name: logger.info( "ERROR: The ROM you loaded is for a different game of AP. " "Please make sure the host has sent you the correct patch file, " From 9efcba5323e938c58224db85840b1a0120fd0d99 Mon Sep 17 00:00:00 2001 From: Rosalie <61372066+Rosalie-A@users.noreply.github.com> Date: Sun, 8 Mar 2026 16:31:04 -0400 Subject: [PATCH 18/53] FF1: Added manifest (#5911) --- worlds/ff1/archipelago.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 worlds/ff1/archipelago.json diff --git a/worlds/ff1/archipelago.json b/worlds/ff1/archipelago.json new file mode 100644 index 0000000000..e3cf746a3b --- /dev/null +++ b/worlds/ff1/archipelago.json @@ -0,0 +1,5 @@ +{ + "game": "Final Fantasy", + "world_version": "1.0.0", + "authors": ["Rosalie"] +} \ No newline at end of file From fc2cb3c961cda23f62484e37d1f742556b3592d6 Mon Sep 17 00:00:00 2001 From: StripesOO7 <54711792+StripesOO7@users.noreply.github.com> Date: Sun, 8 Mar 2026 21:31:48 +0100 Subject: [PATCH 19/53] OoT: change setup-guides to have 2.10 be the minimum version recommended (#5799) --- worlds/oot/docs/setup_de.md | 7 +------ worlds/oot/docs/setup_en.md | 7 +------ worlds/oot/docs/setup_fr.md | 6 +----- 3 files changed, 3 insertions(+), 17 deletions(-) diff --git a/worlds/oot/docs/setup_de.md b/worlds/oot/docs/setup_de.md index f257ddc015..d456f69079 100644 --- a/worlds/oot/docs/setup_de.md +++ b/worlds/oot/docs/setup_de.md @@ -7,7 +7,7 @@ Da wir BizHawk benutzen, gilt diese Anleitung nur für Windows und Linux. ## Benötigte Software - BizHawk: [BizHawk Veröffentlichungen von TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory) - - Version 2.3.1 und später werden unterstützt. Version 2.10 ist empfohlen. + - Version 2.10 und neuer werden unterstützt. Version 2.10 ist empfohlen. - Detailierte Installtionsanweisungen für BizHawk können über den obrigen Link gefunden werden. - Windows-Benutzer müssen die Prerequisiten installiert haben. Diese können ebenfalls über den obrigen Link gefunden werden. @@ -19,11 +19,6 @@ Da wir BizHawk benutzen, gilt diese Anleitung nur für Windows und Linux. Sobald Bizhawk einmal installiert wurde, öffne **EmuHawk** und ändere die folgenen Einsteluungen: -- (≤ 2.8) Gehe zu `Config > Customize`. Wechlse zu dem `Advanced`-Reiter, wechsle dann den `Lua Core` von "NLua+KopiLua" zu - `"Lua+LuaInterface"`. Starte danach EmuHawk neu. Dies ist zwingend notwendig, damit die Lua-Scripts, mit denen man sich mit dem Client verbindet, ordnungsgemäß funktionieren. - **ANMERKUNG: Selbst wenn "Lua+LuaInterface" bereits ausgewählt ist, wechsle zwischen den beiden Optionen umher und** - **wähle es erneut aus. Neue Installationen oder Versionen von EmuHawk neigen dazu "Lua+LuaInterface" als die** - **Standard-Option anzuzeigen, aber laden dennoch "NLua+KopiLua", bis dieser Schritt getan ist.** - Unter `Config > Customize > Advanced`, gehe sicher dass der Haken bei `AutoSaveRAM` ausgeählt ist, und klicke dann den 5s-Knopf. Dies verringert die Wahrscheinlichkeit den Speicherfrotschritt zu verlieren, sollte der Emulator mal abstürzen. diff --git a/worlds/oot/docs/setup_en.md b/worlds/oot/docs/setup_en.md index 31b7137bd8..a09752f0d8 100644 --- a/worlds/oot/docs/setup_en.md +++ b/worlds/oot/docs/setup_en.md @@ -7,7 +7,7 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst ## Required Software - BizHawk: [BizHawk Releases from TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory) - - Version 2.3.1 and later are supported. Version 2.10 is recommended for stability. + - Version 2.10 and later are supported. Version 2.10 is recommended for stability. - Detailed installation instructions for BizHawk can be found at the above link. - Windows users must run the prereq installer first, which can also be found at the above link. - The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases). @@ -17,11 +17,6 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst Once BizHawk has been installed, open EmuHawk and change the following settings: -- (≤ 2.8) Go to Config > Customize. Switch to the Advanced tab, then switch the Lua Core from "NLua+KopiLua" to - "Lua+LuaInterface". Then restart EmuHawk. This is required for the Lua script to function correctly. - **NOTE: Even if "Lua+LuaInterface" is already selected, toggle between the two options and reselect it. Fresh installs** - **of newer versions of EmuHawk have a tendency to show "Lua+LuaInterface" as the default selected option but still load** - **"NLua+KopiLua" until this step is done.** - Under Config > Customize > Advanced, make sure the box for AutoSaveRAM is checked, and click the 5s button. This reduces the possibility of losing save data in emulator crashes. - Under Config > Customize, check the "Run in background" and "Accept background input" boxes. This will allow you to diff --git a/worlds/oot/docs/setup_fr.md b/worlds/oot/docs/setup_fr.md index eb2e97384a..8df50e05bd 100644 --- a/worlds/oot/docs/setup_fr.md +++ b/worlds/oot/docs/setup_fr.md @@ -7,7 +7,7 @@ Comme nous utilisons BizHawk, ce guide s'applique uniquement aux systèmes Windo ## Logiciel requis - BizHawk : [Sorties BizHawk de TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory) - - Les versions 2.3.1 et ultérieures sont prises en charge. La version 2.10 est recommandée pour des raisons de stabilité. + - Les versions 2.10 et ultérieures sont prises en charge. La version 2.10 est recommandée pour des raisons de stabilité. - Des instructions d'installation détaillées pour BizHawk peuvent être trouvées sur le lien ci-dessus. - Les utilisateurs Windows doivent d'abord exécuter le programme d'installation des prérequis, qui peut également être trouvé sur le lien ci-dessus. - Le client Archipelago intégré, qui peut être installé [ici](https://github.com/ArchipelagoMW/Archipelago/releases) @@ -18,10 +18,6 @@ Comme nous utilisons BizHawk, ce guide s'applique uniquement aux systèmes Windo Une fois BizHawk installé, ouvrez EmuHawk et modifiez les paramètres suivants : -- (≤ 2,8) Allez dans Config > Personnaliser. Passez à l'onglet Avancé, puis faites passer le Lua Core de "NLua+KopiLua" à - "Lua+LuaInterface". Puis redémarrez EmuHawk. Ceci est nécessaire pour que le script Lua fonctionne correctement. - **REMARQUE : Même si « Lua+LuaInterface » est déjà sélectionné, basculez entre les deux options et resélectionnez-la. Nouvelles installations** - **des versions plus récentes d'EmuHawk ont tendance à afficher "Lua+LuaInterface" comme option sélectionnée par défaut mais ce pendant refait l'épate juste au dessus par précautions** - Sous Config > Personnaliser > Avancé, assurez-vous que la case AutoSaveRAM est cochée et cliquez sur le bouton 5s. Cela réduit la possibilité de perdre des données de sauvegarde en cas de crash de l'émulateur. - Sous Config > Personnaliser, cochez les cases « Exécuter en arrière-plan » et « Accepter la saisie en arrière-plan ». Cela vous permettra continuez à jouer en arrière-plan, même si une autre fenêtre est sélectionnée. From a8ac828241066d4079e20123d043d417764bdce0 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Sun, 8 Mar 2026 13:32:40 -0700 Subject: [PATCH 20/53] Pokemon Emerald: Fix rare fuzzer errors (#5914) --- worlds/pokemon_emerald/opponents.py | 2 +- worlds/pokemon_emerald/pokemon.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/worlds/pokemon_emerald/opponents.py b/worlds/pokemon_emerald/opponents.py index 966d192054..00eceff9a7 100644 --- a/worlds/pokemon_emerald/opponents.py +++ b/worlds/pokemon_emerald/opponents.py @@ -63,7 +63,7 @@ def randomize_opponent_parties(world: "PokemonEmeraldWorld") -> None: if len(merged_blacklist) < NUM_REAL_SPECIES: break else: - raise RuntimeError("This should never happen") + merged_blacklist: Set[int] = set() candidates = [ species diff --git a/worlds/pokemon_emerald/pokemon.py b/worlds/pokemon_emerald/pokemon.py index b39f8c2abf..8f799ce611 100644 --- a/worlds/pokemon_emerald/pokemon.py +++ b/worlds/pokemon_emerald/pokemon.py @@ -245,7 +245,7 @@ def _rename_wild_events(world: "PokemonEmeraldWorld", map_data: MapData, new_slo for r, sc in _encounter_subcategory_ranges[encounter_type].items() if i in r ) - subcategory_species = [] + subcategory_species: list[int] = [] for k in subcategory_range: if new_slots[k] not in subcategory_species: subcategory_species.append(new_slots[k]) @@ -278,7 +278,7 @@ def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None: RandomizeWildPokemon.option_match_base_stats_and_type, } - already_placed = set() + already_placed: set[int] = set() num_placeable_species = NUM_REAL_SPECIES - len(world.blacklisted_wilds) priority_species = [data.constants["SPECIES_WAILORD"], data.constants["SPECIES_RELICANTH"]] @@ -349,7 +349,7 @@ def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None: if len(merged_blacklist) < NUM_REAL_SPECIES: break else: - raise RuntimeError("This should never happen") + merged_blacklist = set() candidates = [ species @@ -365,7 +365,7 @@ def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None: species_old_to_new_map[species_id] = new_species_id if world.options.dexsanity and encounter_type != EncounterType.ROCK_SMASH \ - and map_name not in OUT_OF_LOGIC_MAPS: + and map_name not in OUT_OF_LOGIC_MAPS and new_species_id not in world.blacklisted_wilds: already_placed.add(new_species_id) # Actually create the new list of slots and encounter table From b38548f89ba7bf73e970ed9da85290e200ab6ffa Mon Sep 17 00:00:00 2001 From: GodlFire <46984098+GodlFire@users.noreply.github.com> Date: Sun, 8 Mar 2026 14:33:16 -0600 Subject: [PATCH 21/53] Shivers: Adds Manifest File (#5918) --- worlds/shivers/archipelago.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 worlds/shivers/archipelago.json diff --git a/worlds/shivers/archipelago.json b/worlds/shivers/archipelago.json new file mode 100644 index 0000000000..d9838d3841 --- /dev/null +++ b/worlds/shivers/archipelago.json @@ -0,0 +1,6 @@ +{ + "game": "Shivers", + "minimum_ap_version": "0.6.0", + "world_version": "2.7.5", + "authors": ["GodlFire", "Cynbel_Terreus"] +} \ No newline at end of file From 53956b7d4d21356bd6bd6fbea9addca3231fd448 Mon Sep 17 00:00:00 2001 From: josephwhite <22449090+josephwhite@users.noreply.github.com> Date: Sun, 8 Mar 2026 16:34:19 -0400 Subject: [PATCH 22/53] OOT: UTC deprecation warning fix (#5983) --- worlds/oot/Patches.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/oot/Patches.py b/worlds/oot/Patches.py index db7be3d4dd..98bb44efc8 100644 --- a/worlds/oot/Patches.py +++ b/worlds/oot/Patches.py @@ -272,7 +272,7 @@ def patch_rom(world, rom): world_str = "" rom.write_bytes(rom.sym('WORLD_STRING_TXT'), makebytes(world_str, 12)) - time_str = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M") + " UTC" + time_str = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d %H:%M") + " UTC" rom.write_bytes(rom.sym('TIME_STRING_TXT'), makebytes(time_str, 25)) rom.write_byte(rom.sym('CFG_SHOW_SETTING_INFO'), 0x01) From 99601ccebc39dafedc1f6009f90ad543ddf46d4a Mon Sep 17 00:00:00 2001 From: LeonarthCG <33758848+LeonarthCG@users.noreply.github.com> Date: Sun, 8 Mar 2026 21:34:51 +0100 Subject: [PATCH 23/53] Saving Princess: add manifest (#6008) --- worlds/saving_princess/archipelago.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 worlds/saving_princess/archipelago.json diff --git a/worlds/saving_princess/archipelago.json b/worlds/saving_princess/archipelago.json new file mode 100644 index 0000000000..d4f65dabff --- /dev/null +++ b/worlds/saving_princess/archipelago.json @@ -0,0 +1,6 @@ +{ + "game": "Saving Princess", + "authors": [ "LeonarthCG" ], + "minimum_ap_version": "0.6.6", + "world_version": "1.0.0" +} From 4bb6cac7c4533aa19d336823a1bd6833592ff311 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Sun, 8 Mar 2026 16:35:12 -0400 Subject: [PATCH 24/53] Lingo: Add archipelago.json (#6017) --- worlds/lingo/archipelago.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 worlds/lingo/archipelago.json diff --git a/worlds/lingo/archipelago.json b/worlds/lingo/archipelago.json new file mode 100644 index 0000000000..d065ab5c0b --- /dev/null +++ b/worlds/lingo/archipelago.json @@ -0,0 +1,6 @@ +{ + "game": "Lingo", + "authors": ["hatkirby"], + "minimum_ap_version": "0.6.3", + "world_version": "5.0.0" +} From 5b99118dda7025d78aa457c4098d58aef69e6378 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sun, 8 Mar 2026 15:42:06 -0500 Subject: [PATCH 25/53] Mega Man 3: Implement new game (#5237) --- README.md | 1 + docs/CODEOWNERS | 3 + inno_setup.iss | 5 + worlds/mm3/.apignore | 1 + worlds/mm3/__init__.py | 275 +++++++++ worlds/mm3/archipelago.json | 6 + worlds/mm3/client.py | 783 ++++++++++++++++++++++++++ worlds/mm3/color.py | 331 +++++++++++ worlds/mm3/data/mm3_basepatch.bsdiff4 | Bin 0 -> 1235 bytes worlds/mm3/docs/en_Mega Man 3.md | 131 +++++ worlds/mm3/docs/setup_en.md | 53 ++ worlds/mm3/items.py | 80 +++ worlds/mm3/locations.py | 312 ++++++++++ worlds/mm3/names.py | 221 ++++++++ worlds/mm3/options.py | 164 ++++++ worlds/mm3/rom.py | 374 ++++++++++++ worlds/mm3/rules.py | 388 +++++++++++++ worlds/mm3/src/__init__.py | 0 worlds/mm3/src/mm3_basepatch.asm | 781 +++++++++++++++++++++++++ worlds/mm3/src/patch_mm3base.py | 8 + worlds/mm3/test/__init__.py | 0 worlds/mm3/test/bases.py | 5 + worlds/mm3/test/test_weakness.py | 105 ++++ worlds/mm3/text.py | 63 +++ 24 files changed, 4090 insertions(+) create mode 100644 worlds/mm3/.apignore create mode 100644 worlds/mm3/__init__.py create mode 100644 worlds/mm3/archipelago.json create mode 100644 worlds/mm3/client.py create mode 100644 worlds/mm3/color.py create mode 100644 worlds/mm3/data/mm3_basepatch.bsdiff4 create mode 100644 worlds/mm3/docs/en_Mega Man 3.md create mode 100644 worlds/mm3/docs/setup_en.md create mode 100644 worlds/mm3/items.py create mode 100644 worlds/mm3/locations.py create mode 100644 worlds/mm3/names.py create mode 100644 worlds/mm3/options.py create mode 100644 worlds/mm3/rom.py create mode 100644 worlds/mm3/rules.py create mode 100644 worlds/mm3/src/__init__.py create mode 100644 worlds/mm3/src/mm3_basepatch.asm create mode 100644 worlds/mm3/src/patch_mm3base.py create mode 100644 worlds/mm3/test/__init__.py create mode 100644 worlds/mm3/test/bases.py create mode 100644 worlds/mm3/test/test_weakness.py create mode 100644 worlds/mm3/text.py diff --git a/README.md b/README.md index efa18bc1ef..7a0c663db0 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ Currently, the following games are supported: * APQuest * Satisfactory * EarthBound +* Mega Man 3 For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 0e368386c5..46afd30456 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -134,6 +134,9 @@ # Mega Man 2 /worlds/mm2/ @Silvris +# Mega Man 3 +/worlds/mm3/ @Silvris + # MegaMan Battle Network 3 /worlds/mmbn3/ @digiholic diff --git a/inno_setup.iss b/inno_setup.iss index c396224c56..999070ad07 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -213,6 +213,11 @@ Root: HKCR; Subkey: "{#MyAppName}ebpatch"; ValueData: "Archi Root: HKCR; Subkey: "{#MyAppName}ebpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}ebpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: ".apmm3"; ValueData: "{#MyAppName}mm3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}mm3patch"; ValueData: "Archipelago Mega Man 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}mm3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}mm3patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: ""; + Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: ""; diff --git a/worlds/mm3/.apignore b/worlds/mm3/.apignore new file mode 100644 index 0000000000..4ae3da2695 --- /dev/null +++ b/worlds/mm3/.apignore @@ -0,0 +1 @@ +/src/* diff --git a/worlds/mm3/__init__.py b/worlds/mm3/__init__.py new file mode 100644 index 0000000000..5b349bc9c3 --- /dev/null +++ b/worlds/mm3/__init__.py @@ -0,0 +1,275 @@ +import hashlib +import logging +from copy import deepcopy +from typing import Any, Sequence, ClassVar + +from BaseClasses import Tutorial, ItemClassification, MultiWorld, Item, Location +from worlds.AutoWorld import World, WebWorld +from .names import (gamma, gemini_man_stage, needle_man_stage, hard_man_stage, magnet_man_stage, top_man_stage, + snake_man_stage, spark_man_stage, shadow_man_stage, rush_marine, rush_jet, rush_coil) +from .items import (item_table, item_names, MM3Item, filler_item_weights, robot_master_weapon_table, + stage_access_table, rush_item_table, lookup_item_to_id) +from .locations import (MM3Location, mm3_regions, MM3Region, lookup_location_to_id, + location_groups) +from .rom import patch_rom, MM3ProcedurePatch, MM3LCHASH, MM3VCHASH, PROTEUSHASH, MM3NESHASH +from .options import MM3Options, Consumables +from .client import MegaMan3Client +from .rules import set_rules, weapon_damage, robot_masters, weapons_to_name, minimum_weakness_requirement +import os +import threading +import base64 +import settings +logger = logging.getLogger("Mega Man 3") + + +class MM3Settings(settings.Group): + class RomFile(settings.UserFilePath): + """File name of the MM3 EN rom""" + description = "Mega Man 3 ROM File" + copy_to: str | None = "Mega Man 3 (USA).nes" + md5s = [MM3NESHASH, MM3LCHASH, PROTEUSHASH, MM3VCHASH] + + def browse(self: settings.T, + filetypes: Sequence[tuple[str, Sequence[str]]] | None = None, + **kwargs: Any) -> settings.T | None: + if not filetypes: + file_types = [("NES", [".nes"]), ("Program", [".exe"])] # LC1 is only a windows executable, no linux + return super().browse(file_types, **kwargs) + else: + return super().browse(filetypes, **kwargs) + + @classmethod + def validate(cls, path: str) -> None: + """Try to open and validate file against hashes""" + with open(path, "rb", buffering=0) as f: + try: + f.seek(0) + if f.read(4) == b"NES\x1A": + f.seek(16) + else: + f.seek(0) + cls._validate_stream_hashes(f) + base_rom_bytes = f.read() + basemd5 = hashlib.md5() + basemd5.update(base_rom_bytes) + if basemd5.hexdigest() == PROTEUSHASH: + # we need special behavior here + cls.copy_to = None + except ValueError: + raise ValueError(f"File hash does not match for {path}") + + rom_file: RomFile = RomFile(RomFile.copy_to) + + +class MM3WebWorld(WebWorld): + theme = "partyTime" + tutorials = [ + + Tutorial( + "Multiworld Setup Guide", + "A guide to setting up the Mega Man 3 randomizer connected to an Archipelago Multiworld.", + "English", + "setup_en.md", + "setup/en", + ["Silvris"] + ) + ] + + +class MM3World(World): + """ + Following his second defeat by Mega Man, Dr. Wily has finally come to his senses. He and Dr. Light begin work on + Gamma, a giant peacekeeping robot. However, Gamma's power source, the Energy Elements, are being guarded by the + Robot Masters sent to retrieve them. It's up to Mega Man to retrieve the Energy Elements and defeat the mastermind + behind the Robot Masters' betrayal. + """ + + game = "Mega Man 3" + settings: ClassVar[MM3Settings] + options_dataclass = MM3Options + options: MM3Options + item_name_to_id = lookup_item_to_id + location_name_to_id = lookup_location_to_id + item_name_groups = item_names + location_name_groups = location_groups + web = MM3WebWorld() + rom_name: bytearray + + def __init__(self, world: MultiWorld, player: int): + self.rom_name = bytearray() + self.rom_name_available_event = threading.Event() + super().__init__(world, player) + self.weapon_damage = deepcopy(weapon_damage) + self.wily_4_weapons: dict[int, list[int]] = {} + + def create_regions(self) -> None: + menu = MM3Region("Menu", self.player, self.multiworld) + self.multiworld.regions.append(menu) + location: MM3Location + for name, region in mm3_regions.items(): + stage = MM3Region(name, self.player, self.multiworld) + if not region.parent: + menu.connect(stage, f"To {name}", + lambda state, req=tuple(region.required_items): state.has_all(req, self.player)) + else: + old_stage = self.get_region(region.parent) + old_stage.connect(stage, f"To {name}", + lambda state, req=tuple(region.required_items): state.has_all(req, self.player)) + stage.add_locations({loc: data.location_id for loc, data in region.locations.items() + if (not data.energy or self.options.consumables.value in (Consumables.option_weapon_health, Consumables.option_all)) + and (not data.oneup_tank or self.options.consumables.value in (Consumables.option_1up_etank, Consumables.option_all))}) + for location in stage.get_locations(): + if location.address is None and location.name != gamma: + location.place_locked_item(MM3Item(location.name, ItemClassification.progression, + None, self.player)) + self.multiworld.regions.append(stage) + goal_location = self.get_location(gamma) + goal_location.place_locked_item(MM3Item("Victory", ItemClassification.progression, None, self.player)) + self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) + + def create_item(self, name: str, force_non_progression: bool = False) -> MM3Item: + item = item_table[name] + classification = ItemClassification.filler + if item.progression and not force_non_progression: + classification = ItemClassification.progression_skip_balancing \ + if item.skip_balancing else ItemClassification.progression + if item.useful: + classification |= ItemClassification.useful + return MM3Item(name, classification, item.code, self.player) + + def get_filler_item_name(self) -> str: + return self.random.choices(list(filler_item_weights.keys()), + weights=list(filler_item_weights.values()))[0] + + def create_items(self) -> None: + itempool = [] + # grab first robot master + robot_master = self.item_id_to_name[0x0101 + self.options.starting_robot_master.value] + self.multiworld.push_precollected(self.create_item(robot_master)) + itempool.extend([self.create_item(name) for name in stage_access_table.keys() + if name != robot_master]) + itempool.extend([self.create_item(name) for name in robot_master_weapon_table.keys()]) + itempool.extend([self.create_item(name) for name in rush_item_table.keys()]) + total_checks = 31 + if self.options.consumables in (Consumables.option_1up_etank, + Consumables.option_all): + total_checks += 33 + if self.options.consumables in (Consumables.option_weapon_health, + Consumables.option_all): + total_checks += 106 + remaining = total_checks - len(itempool) + itempool.extend([self.create_item(name) + for name in self.random.choices(list(filler_item_weights.keys()), + weights=list(filler_item_weights.values()), + k=remaining)]) + self.multiworld.itempool += itempool + + set_rules = set_rules + + def generate_early(self) -> None: + if (self.options.starting_robot_master.current_key == "gemini_man" + and not any(item in self.options.start_inventory for item in rush_item_table.keys())) or \ + (self.options.starting_robot_master.current_key == "hard_man" + and not any(item in self.options.start_inventory for item in [rush_coil, rush_jet])): + robot_master_pool = [0, 1, 4, 5, 6, 7, ] + if rush_marine in self.options.start_inventory: + robot_master_pool.append(2) + self.options.starting_robot_master.value = self.random.choice(robot_master_pool) + logger.warning( + f"Incompatible starting Robot Master, changing to " + f"{self.options.starting_robot_master.current_key.replace('_', ' ').title()}") + + def fill_hook(self, + prog_item_pool: list["Item"], + useful_item_pool: list["Item"], + filler_item_pool: list["Item"], + fill_locations: list["Location"]) -> None: + # on a solo gen, fill can try to force Wily into sphere 2, but for most generations this is impossible + # MM3 is worse than MM2 here, some of the RBMs can also require Rush + if self.multiworld.players > 1: + return # Don't need to change anything on a multi gen, fill should be able to solve it with a 4 sphere 1 + rbm_to_item = { + 0: needle_man_stage, + 1: magnet_man_stage, + 2: gemini_man_stage, + 3: hard_man_stage, + 4: top_man_stage, + 5: snake_man_stage, + 6: spark_man_stage, + 7: shadow_man_stage + } + affected_rbm = [2, 3] # Gemini and Hard will always have this happen + possible_rbm = [0, 7] # Needle and Shadow are always valid targets, due to Rush Marine/Jet receive + if self.options.consumables: + possible_rbm.extend([4, 5]) # every stage has at least one of each consumable + if self.options.consumables in (Consumables.option_weapon_health, Consumables.option_all): + possible_rbm.extend([1, 6]) + else: + affected_rbm.extend([1, 6]) + else: + affected_rbm.extend([1, 4, 5, 6]) # only two checks on non consumables + if self.options.starting_robot_master.value in affected_rbm: + rbm_names = list(map(lambda s: rbm_to_item[s], possible_rbm)) + valid_second = [item for item in prog_item_pool + if item.name in rbm_names + and item.player == self.player] + placed_item = self.random.choice(valid_second) + rbm_defeated = (f"{robot_masters[self.options.starting_robot_master.value].replace(' Defeated', '')}" + f" - Defeated") + rbm_location = self.get_location(rbm_defeated) + rbm_location.place_locked_item(placed_item) + prog_item_pool.remove(placed_item) + fill_locations.remove(rbm_location) + target_rbm = (placed_item.code & 0xF) - 1 + if self.options.strict_weakness or (self.options.random_weakness + and not (self.weapon_damage[0][target_rbm] > 0)): + # we need to find a weakness for this boss + weaknesses = [weapon for weapon in range(1, 9) + if self.weapon_damage[weapon][target_rbm] >= minimum_weakness_requirement[weapon]] + weapons = list(map(lambda s: weapons_to_name[s], weaknesses)) + valid_weapons = [item for item in prog_item_pool + if item.name in weapons + and item.player == self.player] + placed_weapon = self.random.choice(valid_weapons) + weapon_name = next(name for name, idx in lookup_location_to_id.items() + if idx == 0x0101 + self.options.starting_robot_master.value) + weapon_location = self.get_location(weapon_name) + weapon_location.place_locked_item(placed_weapon) + prog_item_pool.remove(placed_weapon) + fill_locations.remove(weapon_location) + + def generate_output(self, output_directory: str) -> None: + try: + patch = MM3ProcedurePatch(player=self.player, player_name=self.player_name) + patch_rom(self, patch) + + self.rom_name = patch.name + + patch.write(os.path.join(output_directory, + f"{self.multiworld.get_out_file_name_base(self.player)}{patch.patch_file_ending}")) + except Exception: + raise + finally: + self.rom_name_available_event.set() # make sure threading continues and errors are collected + + def fill_slot_data(self) -> dict[str, Any]: + return { + "death_link": self.options.death_link.value, + "weapon_damage": self.weapon_damage, + "wily_4_weapons": self.wily_4_weapons + } + + @staticmethod + def interpret_slot_data(slot_data: dict[str, Any]) -> dict[str, Any]: + local_weapon = {int(key): value for key, value in slot_data["weapon_damage"].items()} + local_wily = {int(key): value for key, value in slot_data["wily_4_weapons"].items()} + return {"weapon_damage": local_weapon, "wily_4_weapons": local_wily} + + def modify_multidata(self, multidata: dict[str, Any]) -> None: + # wait for self.rom_name to be available. + self.rom_name_available_event.wait() + rom_name = getattr(self, "rom_name", None) + # we skip in case of error, so that the original error in the output thread is the one that gets raised + if rom_name: + new_name = base64.b64encode(bytes(self.rom_name)).decode() + multidata["connect_names"][new_name] = multidata["connect_names"][self.player_name] diff --git a/worlds/mm3/archipelago.json b/worlds/mm3/archipelago.json new file mode 100644 index 0000000000..ed5ecffc6c --- /dev/null +++ b/worlds/mm3/archipelago.json @@ -0,0 +1,6 @@ +{ + "game": "Mega Man 3", + "authors": ["Silvris"], + "world_version": "0.1.7", + "minimum_ap_version": "0.6.4" +} diff --git a/worlds/mm3/client.py b/worlds/mm3/client.py new file mode 100644 index 0000000000..0e069043a7 --- /dev/null +++ b/worlds/mm3/client.py @@ -0,0 +1,783 @@ +import logging +import time +from enum import IntEnum +from base64 import b64encode +from typing import TYPE_CHECKING, Any +from NetUtils import ClientStatus, color, NetworkItem +from worlds._bizhawk.client import BizHawkClient + +if TYPE_CHECKING: + from worlds._bizhawk.context import BizHawkClientContext, BizHawkClientCommandProcessor + +nes_logger = logging.getLogger("NES") +logger = logging.getLogger("Client") + +MM3_CURRENT_STAGE = 0x22 +MM3_MEGAMAN_STATE = 0x30 +MM3_PROG_STATE = 0x60 +MM3_ROBOT_MASTERS_DEFEATED = 0x61 +MM3_DOC_STATUS = 0x62 +MM3_HEALTH = 0xA2 +MM3_WEAPON_ENERGY = 0xA3 +MM3_WEAPONS = { + 1: 1, + 2: 3, + 3: 0, + 4: 2, + 5: 4, + 6: 5, + 7: 7, + 8: 9, + 0x11: 6, + 0x12: 8, + 0x13: 10, +} + +MM3_DOC_REMAP = { + 0: 0, + 1: 1, + 2: 2, + 3: 3, + 4: 6, + 5: 7, + 6: 4, + 7: 5 +} +MM3_LIVES = 0xAE +MM3_E_TANKS = 0xAF +MM3_ENERGY_BAR = 0xB2 +MM3_CONSUMABLES = 0x150 +MM3_ROBOT_MASTERS_UNLOCKED = 0x680 +MM3_DOC_ROBOT_UNLOCKED = 0x681 +MM3_ENERGYLINK = 0x682 +MM3_LAST_WILY = 0x683 +MM3_RBM_STROBE = 0x684 +MM3_SFX_QUEUE = 0x685 +MM3_DOC_ROBOT_DEFEATED = 0x686 +MM3_COMPLETED_STAGES = 0x687 +MM3_RECEIVED_ITEMS = 0x688 +MM3_RUSH_RECEIVED = 0x689 + +MM3_CONSUMABLE_TABLE: dict[int, dict[int, tuple[int, int]]] = { + # Stage: + # Item: (byte offset, bit mask) + 0: { + 0x0200: (0, 5), + 0x0201: (3, 2), + }, + 1: { + 0x0202: (2, 6), + 0x0203: (2, 5), + 0x0204: (2, 4), + 0x0205: (2, 3), + 0x0206: (3, 6), + 0x0207: (3, 5), + 0x0208: (3, 7), + 0x0209: (4, 0) + }, + 2: { + 0x020A: (2, 7), + 0x020B: (3, 0), + 0x020C: (3, 1), + 0x020D: (3, 2), + 0x020E: (4, 2), + 0x020F: (4, 3), + 0x0210: (4, 7), + 0x0211: (5, 1), + 0x0212: (6, 1), + 0x0213: (7, 0) + }, + 3: { + 0x0214: (0, 6), + 0x0215: (1, 5), + 0x0216: (2, 3), + 0x0217: (2, 7), + 0x0218: (2, 6), + 0x0219: (2, 5), + 0x021A: (4, 5), + }, + 4: { + 0x021B: (1, 3), + 0x021C: (1, 5), + 0x021D: (1, 7), + 0x021E: (2, 0), + 0x021F: (1, 6), + 0x0220: (2, 4), + 0x0221: (2, 5), + 0x0222: (4, 5) + }, + 5: { + 0x0223: (3, 0), + 0x0224: (3, 2), + 0x0225: (4, 5), + 0x0226: (4, 6), + 0x0227: (6, 4), + }, + 6: { + 0x0228: (2, 0), + 0x0229: (2, 1), + 0x022A: (3, 1), + 0x022B: (3, 2), + 0x022C: (3, 3), + 0x022D: (3, 4), + }, + 7: { + 0x022E: (3, 5), + 0x022F: (3, 4), + 0x0230: (3, 3), + 0x0231: (3, 2), + }, + 8: { + 0x0232: (1, 4), + 0x0233: (2, 1), + 0x0234: (2, 2), + 0x0235: (2, 5), + 0x0236: (3, 5), + 0x0237: (4, 2), + 0x0238: (4, 4), + 0x0239: (5, 3), + 0x023A: (6, 0), + 0x023B: (6, 1), + 0x023C: (7, 5), + + }, + 9: { + 0x023D: (3, 2), + 0x023E: (3, 6), + 0x023F: (4, 5), + 0x0240: (5, 4), + }, + 10: { + 0x0241: (0, 2), + 0x0242: (2, 4) + }, + 11: { + 0x0243: (4, 1), + 0x0244: (6, 0), + 0x0245: (6, 1), + 0x0246: (6, 2), + 0x0247: (6, 3), + }, + 12: { + 0x0248: (0, 0), + 0x0249: (0, 3), + 0x024A: (0, 5), + 0x024B: (1, 6), + 0x024C: (2, 7), + 0x024D: (2, 3), + 0x024E: (2, 1), + 0x024F: (2, 2), + 0x0250: (3, 5), + 0x0251: (3, 4), + 0x0252: (3, 6), + 0x0253: (3, 7) + }, + 13: { + 0x0254: (0, 3), + 0x0255: (0, 6), + 0x0256: (1, 0), + 0x0257: (3, 0), + 0x0258: (3, 2), + 0x0259: (3, 3), + 0x025A: (3, 4), + 0x025B: (3, 5), + 0x025C: (3, 6), + 0x025D: (4, 0), + 0x025E: (3, 7), + 0x025F: (4, 1), + 0x0260: (4, 2), + }, + 14: { + 0x0261: (0, 3), + 0x0262: (0, 2), + 0x0263: (0, 6), + 0x0264: (1, 2), + 0x0265: (1, 7), + 0x0266: (2, 0), + 0x0267: (2, 1), + 0x0268: (2, 2), + 0x0269: (2, 3), + 0x026A: (5, 2), + 0x026B: (5, 3), + }, + 15: { + 0x026C: (0, 0), + 0x026D: (0, 1), + 0x026E: (0, 2), + 0x026F: (0, 3), + 0x0270: (0, 4), + 0x0271: (0, 6), + 0x0272: (1, 0), + 0x0273: (1, 2), + 0x0274: (1, 3), + 0x0275: (1, 1), + 0x0276: (0, 7), + 0x0277: (3, 2), + 0x0278: (2, 2), + 0x0279: (2, 3), + 0x027A: (2, 4), + 0x027B: (2, 5), + 0x027C: (3, 1), + 0x027D: (3, 0), + 0x027E: (2, 7), + 0x027F: (2, 6), + }, + 16: { + 0x0280: (0, 0), + 0x0281: (0, 3), + 0x0282: (0, 1), + 0x0283: (0, 2), + }, + 17: { + 0x0284: (0, 2), + 0x0285: (0, 6), + 0x0286: (0, 1), + 0x0287: (0, 5), + 0x0288: (0, 3), + 0x0289: (0, 0), + 0x028A: (0, 4) + } +} + + +def to_oneup_format(val: int) -> int: + return ((val // 10) * 0x10) + val % 10 + + +def from_oneup_format(val: int) -> int: + return ((val // 0x10) * 10) + val % 0x10 + + +class MM3EnergyLinkType(IntEnum): + Life = 0 + NeedleCannon = 1 + MagnetMissile = 2 + GeminiLaser = 3 + HardKnuckle = 4 + TopSpin = 5 + SearchSnake = 6 + SparkShot = 7 + ShadowBlade = 8 + OneUP = 12 + RushCoil = 0x11 + RushMarine = 0x12 + RushJet = 0x13 + + +request_to_name: dict[str, str] = { + "HP": "health", + "NE": "Needle Cannon energy", + "MA": "Magnet Missile energy", + "GE": "Gemini Laser energy", + "HA": "Hard Knuckle energy", + "TO": "Top Spin energy", + "SN": "Search Snake energy", + "SP": "Spark Shot energy", + "SH": "Shadow Blade energy", + "RC": "Rush Coil energy", + "RM": "Rush Marine energy", + "RJ": "Rush Jet energy", + "1U": "lives" +} + +HP_EXCHANGE_RATE = 500000000 +WEAPON_EXCHANGE_RATE = 250000000 +ONEUP_EXCHANGE_RATE = 14000000000 + + +def cmd_pool(self: "BizHawkClientCommandProcessor") -> None: + """Check the current pool of EnergyLink, and requestable refills from it.""" + if self.ctx.game != "Mega Man 3": + logger.warning("This command can only be used when playing Mega Man 3.") + return + if not self.ctx.server or not self.ctx.slot: + logger.warning("You must be connected to a server to use this command.") + return + energylink = self.ctx.stored_data.get(f"EnergyLink{self.ctx.team}", 0) + health_points = energylink // HP_EXCHANGE_RATE + weapon_points = energylink // WEAPON_EXCHANGE_RATE + lives = energylink // ONEUP_EXCHANGE_RATE + logger.info(f"Healing available: {health_points}\n" + f"Weapon refill available: {weapon_points}\n" + f"Lives available: {lives}") + + +def cmd_request(self: "BizHawkClientCommandProcessor", amount: str, target: str) -> None: + """Request a refill from EnergyLink.""" + from worlds._bizhawk.context import BizHawkClientContext + if self.ctx.game != "Mega Man 3": + logger.warning("This command can only be used when playing Mega Man 3.") + return + if not self.ctx.server or not self.ctx.slot: + logger.warning("You must be connected to a server to use this command.") + return + valid_targets: dict[str, MM3EnergyLinkType] = { + "HP": MM3EnergyLinkType.Life, + "NE": MM3EnergyLinkType.NeedleCannon, + "MA": MM3EnergyLinkType.MagnetMissile, + "GE": MM3EnergyLinkType.GeminiLaser, + "HA": MM3EnergyLinkType.HardKnuckle, + "TO": MM3EnergyLinkType.TopSpin, + "SN": MM3EnergyLinkType.SearchSnake, + "SP": MM3EnergyLinkType.SparkShot, + "SH": MM3EnergyLinkType.ShadowBlade, + "RC": MM3EnergyLinkType.RushCoil, + "RM": MM3EnergyLinkType.RushMarine, + "RJ": MM3EnergyLinkType.RushJet, + "1U": MM3EnergyLinkType.OneUP + } + if target.upper() not in valid_targets: + logger.warning(f"Unrecognized target {target.upper()}. Available targets: {', '.join(valid_targets.keys())}") + return + ctx = self.ctx + assert isinstance(ctx, BizHawkClientContext) + client = ctx.client_handler + assert isinstance(client, MegaMan3Client) + client.refill_queue.append((valid_targets[target.upper()], int(amount))) + logger.info(f"Restoring {amount} {request_to_name[target.upper()]}.") + + +def cmd_autoheal(self: "BizHawkClientCommandProcessor") -> None: + """Enable auto heal from EnergyLink.""" + if self.ctx.game != "Mega Man 3": + logger.warning("This command can only be used when playing Mega Man 3.") + return + if not self.ctx.server or not self.ctx.slot: + logger.warning("You must be connected to a server to use this command.") + return + else: + assert isinstance(self.ctx.client_handler, MegaMan3Client) + if self.ctx.client_handler.auto_heal: + self.ctx.client_handler.auto_heal = False + logger.info(f"Auto healing disabled.") + else: + self.ctx.client_handler.auto_heal = True + logger.info(f"Auto healing enabled.") + + +def get_sfx_writes(sfx: int) -> tuple[int, bytes, str]: + return MM3_SFX_QUEUE, sfx.to_bytes(1, 'little'), "RAM" + + +class MegaMan3Client(BizHawkClient): + game = "Mega Man 3" + system = "NES" + patch_suffix = ".apmm3" + item_queue: list[NetworkItem] = [] + pending_death_link: bool = False + # default to true, as we don't want to send a deathlink until Mega Man's HP is initialized once + sending_death_link: bool = True + death_link: bool = False + energy_link: bool = False + rom: bytes | None = None + weapon_energy: int = 0 + health_energy: int = 0 + auto_heal: bool = False + refill_queue: list[tuple[MM3EnergyLinkType, int]] = [] + last_wily: int | None = None # default to wily 1 + doc_status: int | None = None # default to no doc progress + + async def validate_rom(self, ctx: "BizHawkClientContext") -> bool: + from worlds._bizhawk import RequestFailedError, read, get_memory_size + from . import MM3World + + try: + + if (await get_memory_size(ctx.bizhawk_ctx, "PRG ROM")) < 0x3FFB0: + # not the entire size, but enough to check validation + if "pool" in ctx.command_processor.commands: + ctx.command_processor.commands.pop("pool") + if "request" in ctx.command_processor.commands: + ctx.command_processor.commands.pop("request") + if "autoheal" in ctx.command_processor.commands: + ctx.command_processor.commands.pop("autoheal") + return False + + game_name, version = (await read(ctx.bizhawk_ctx, [(0x3F320, 21, "PRG ROM"), + (0x3F33C, 3, "PRG ROM")])) + if game_name[:3] != b"MM3" or version != bytes(MM3World.world_version): + if game_name[:3] == b"MM3": + # I think this is an easier check than the other? + older_version = f"{version[0]}.{version[1]}.{version[2]}" + logger.warning(f"This Mega Man 3 patch was generated for an different version of the apworld. " + f"Please use that version to connect instead.\n" + f"Patch version: ({older_version})\n" + f"Client version: ({'.'.join([str(i) for i in MM3World.world_version])})") + if "pool" in ctx.command_processor.commands: + ctx.command_processor.commands.pop("pool") + if "request" in ctx.command_processor.commands: + ctx.command_processor.commands.pop("request") + if "autoheal" in ctx.command_processor.commands: + ctx.command_processor.commands.pop("autoheal") + return False + except UnicodeDecodeError: + return False + except RequestFailedError: + return False # Should verify on the next pass + + ctx.game = self.game + self.rom = game_name + ctx.items_handling = 0b111 + ctx.want_slot_data = False + deathlink = (await read(ctx.bizhawk_ctx, [(0x3F336, 1, "PRG ROM")]))[0][0] + if deathlink & 0x01: + self.death_link = True + await ctx.update_death_link(self.death_link) + if deathlink & 0x02: + self.energy_link = True + + if self.energy_link: + if "pool" not in ctx.command_processor.commands: + ctx.command_processor.commands["pool"] = cmd_pool + if "request" not in ctx.command_processor.commands: + ctx.command_processor.commands["request"] = cmd_request + if "autoheal" not in ctx.command_processor.commands: + ctx.command_processor.commands["autoheal"] = cmd_autoheal + + return True + + async def set_auth(self, ctx: "BizHawkClientContext") -> None: + if self.rom: + ctx.auth = b64encode(self.rom).decode() + + def on_package(self, ctx: "BizHawkClientContext", cmd: str, args: dict[str, Any]) -> None: + if cmd == "Bounced": + if "tags" in args: + assert ctx.slot is not None + if "DeathLink" in args["tags"] and args["data"]["source"] != ctx.slot_info[ctx.slot].name: + self.on_deathlink(ctx) + elif cmd == "Retrieved": + if f"MM3_LAST_WILY_{ctx.team}_{ctx.slot}" in args["keys"]: + self.last_wily = args["keys"][f"MM3_LAST_WILY_{ctx.team}_{ctx.slot}"] + if f"MM3_DOC_STATUS_{ctx.team}_{ctx.slot}" in args["keys"]: + self.doc_status = args["keys"][f"MM3_DOC_STATUS_{ctx.team}_{ctx.slot}"] + elif cmd == "Connected": + if self.energy_link: + ctx.set_notify(f"EnergyLink{ctx.team}") + if ctx.ui: + ctx.ui.enable_energy_link() + + async def send_deathlink(self, ctx: "BizHawkClientContext") -> None: + self.sending_death_link = True + ctx.last_death_link = time.time() + await ctx.send_death("Mega Man was defeated.") + + def on_deathlink(self, ctx: "BizHawkClientContext") -> None: + ctx.last_death_link = time.time() + self.pending_death_link = True + + async def game_watcher(self, ctx: "BizHawkClientContext") -> None: + from worlds._bizhawk import read, write + + if ctx.server is None: + return + + if ctx.slot is None: + return + + # get our relevant bytes + (prog_state, robot_masters_unlocked, robot_masters_defeated, doc_status, doc_robo_unlocked, doc_robo_defeated, + rush_acquired, received_items, completed_stages, consumable_checks, + e_tanks, lives, weapon_energy, health, state, bar_state, current_stage, + energy_link_packet, last_wily) = await read(ctx.bizhawk_ctx, [ + (MM3_PROG_STATE, 1, "RAM"), + (MM3_ROBOT_MASTERS_UNLOCKED, 1, "RAM"), + (MM3_ROBOT_MASTERS_DEFEATED, 1, "RAM"), + (MM3_DOC_STATUS, 1, "RAM"), + (MM3_DOC_ROBOT_UNLOCKED, 1, "RAM"), + (MM3_DOC_ROBOT_DEFEATED, 1, "RAM"), + (MM3_RUSH_RECEIVED, 1, "RAM"), + (MM3_RECEIVED_ITEMS, 1, "RAM"), + (MM3_COMPLETED_STAGES, 0x1, "RAM"), + (MM3_CONSUMABLES, 16, "RAM"), # Could be more but 16 definitely catches all current + (MM3_E_TANKS, 1, "RAM"), + (MM3_LIVES, 1, "RAM"), + (MM3_WEAPON_ENERGY, 11, "RAM"), + (MM3_HEALTH, 1, "RAM"), + (MM3_MEGAMAN_STATE, 1, "RAM"), + (MM3_ENERGY_BAR, 2, "RAM"), + (MM3_CURRENT_STAGE, 1, "RAM"), + (MM3_ENERGYLINK, 1, "RAM"), + (MM3_LAST_WILY, 1, "RAM"), + ]) + + if bar_state[0] not in (0x00, 0x80): + return # Game is not initialized + # Bit of a trick here, bar state can only be 0x00 or 0x80 (display health bar, or don't) + # This means it can double as init guard and in-stage tracker + + if not ctx.finished_game and completed_stages[0] & 0x20: + await ctx.send_msgs([{ + "cmd": "StatusUpdate", + "status": ClientStatus.CLIENT_GOAL + }]) + writes = [] + + # deathlink + # only handle deathlink in bar state 0x80 (in stage) + if bar_state[0] == 0x80: + if self.pending_death_link: + writes.append((MM3_MEGAMAN_STATE, bytes([0x0E]), "RAM")) + self.pending_death_link = False + self.sending_death_link = True + if "DeathLink" in ctx.tags and ctx.last_death_link + 1 < time.time(): + if state[0] == 0x0E and not self.sending_death_link: + await self.send_deathlink(ctx) + elif state[0] != 0x0E: + self.sending_death_link = False + + if self.last_wily != last_wily[0]: + if self.last_wily is None: + # revalidate last wily from data storage + await ctx.send_msgs([{"cmd": "Set", "key": f"MM3_LAST_WILY_{ctx.team}_{ctx.slot}", "operations": [ + {"operation": "default", "value": 0xC} + ]}]) + await ctx.send_msgs([{"cmd": "Get", "keys": [f"MM3_LAST_WILY_{ctx.team}_{ctx.slot}"]}]) + elif last_wily[0] == 0: + writes.append((MM3_LAST_WILY, self.last_wily.to_bytes(1, "little"), "RAM")) + else: + # correct our setting + self.last_wily = last_wily[0] + await ctx.send_msgs([{"cmd": "Set", "key": f"MM3_LAST_WILY_{ctx.team}_{ctx.slot}", "operations": [ + {"operation": "replace", "value": self.last_wily} + ]}]) + + if self.doc_status != doc_status[0]: + if self.doc_status is None: + # revalidate doc status from data storage + await ctx.send_msgs([{"cmd": "Set", "key": f"MM3_DOC_STATUS_{ctx.team}_{ctx.slot}", "operations": [ + {"operation": "default", "value": 0} + ]}]) + await ctx.send_msgs([{"cmd": "Get", "keys": [f"MM3_DOC_STATUS_{ctx.team}_{ctx.slot}"]}]) + elif doc_status[0] == 0: + writes.append((MM3_DOC_STATUS, self.doc_status.to_bytes(1, "little"), "RAM")) + else: + # correct our setting + # shouldn't be possible to desync, but we'll account for it anyways + self.doc_status |= doc_status[0] + await ctx.send_msgs([{"cmd": "Set", "key": f"MM3_DOC_STATUS_{ctx.team}_{ctx.slot}", "operations": [ + {"operation": "replace", "value": self.doc_status} + ]}]) + + weapon_energy = bytearray(weapon_energy) + # handle receiving items + recv_amount = received_items[0] + if recv_amount < len(ctx.items_received): + item = ctx.items_received[recv_amount] + logging.info('Received %s from %s (%s) (%d/%d in list)' % ( + color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'), + color(ctx.player_names[item.player], 'yellow'), + ctx.location_names.lookup_in_slot(item.location, item.player), recv_amount, len(ctx.items_received))) + + if item.item & 0x120 == 0: + # Robot Master Weapon, or Rush + new_weapons = item.item & 0xFF + weapon_energy[MM3_WEAPONS[new_weapons]] |= 0x9C + writes.append((MM3_WEAPON_ENERGY, weapon_energy, "RAM")) + writes.append(get_sfx_writes(0x32)) + elif item.item & 0x20 == 0: + # Robot Master Stage Access + # Catch the Doc Robo here + if item.item & 0x10: + ptr = MM3_DOC_ROBOT_UNLOCKED + unlocked = doc_robo_unlocked + else: + ptr = MM3_ROBOT_MASTERS_UNLOCKED + unlocked = robot_masters_unlocked + new_stages = unlocked[0] | (1 << ((item.item & 0xF) - 1)) + print(new_stages) + writes.append((ptr, new_stages.to_bytes(1, 'little'), "RAM")) + writes.append(get_sfx_writes(0x34)) + writes.append((MM3_RBM_STROBE, b"\x01", "RAM")) + else: + # append to the queue, so we handle it later + self.item_queue.append(item) + recv_amount += 1 + writes.append((MM3_RECEIVED_ITEMS, recv_amount.to_bytes(1, 'little'), "RAM")) + + if energy_link_packet[0]: + pickup = energy_link_packet[0] + if pickup in (0x64, 0x65): + # Health pickups + if pickup == 0x65: + value = 2 + else: + value = 10 + exchange_rate = HP_EXCHANGE_RATE + elif pickup in (0x66, 0x67): + # Weapon Energy + if pickup == 0x67: + value = 2 + else: + value = 10 + exchange_rate = WEAPON_EXCHANGE_RATE + elif pickup == 0x69: + # 1-Up + value = 1 + exchange_rate = ONEUP_EXCHANGE_RATE + else: + # if we managed to pickup something else, we should just fall through + value = 0 + exchange_rate = 0 + contribution = (value * exchange_rate) >> 1 + if contribution: + await ctx.send_msgs([{ + "cmd": "Set", "key": f"EnergyLink{ctx.team}", "slot": ctx.slot, "operations": + [{"operation": "add", "value": contribution}, + {"operation": "max", "value": 0}]}]) + logger.info(f"Deposited {contribution / HP_EXCHANGE_RATE} health into the pool.") + writes.append((MM3_ENERGYLINK, 0x00.to_bytes(1, "little"), "RAM")) + + if self.weapon_energy: + # Weapon Energy + # We parse the whole thing to spread it as thin as possible + current_energy = self.weapon_energy + for i, weapon in zip(range(len(weapon_energy)), weapon_energy): + if weapon & 0x80 and (weapon & 0x7F) < 0x1C: + missing = 0x1C - (weapon & 0x7F) + if missing > self.weapon_energy: + missing = self.weapon_energy + self.weapon_energy -= missing + weapon_energy[i] = weapon + missing + if not self.weapon_energy: + writes.append((MM3_WEAPON_ENERGY, weapon_energy, "RAM")) + break + else: + if current_energy != self.weapon_energy: + writes.append((MM3_WEAPON_ENERGY, weapon_energy, "RAM")) + + if self.health_energy or self.auto_heal: + # Health Energy + # We save this if the player has not taken any damage + current_health = health[0] + if 0 < (current_health & 0x7F) < 0x1C: + health_diff = 0x1C - (current_health & 0x7F) + if self.health_energy: + if health_diff > self.health_energy: + health_diff = self.health_energy + self.health_energy -= health_diff + else: + pool = ctx.stored_data.get(f"EnergyLink{ctx.team}", 0) + if health_diff * HP_EXCHANGE_RATE > pool: + health_diff = int(pool // HP_EXCHANGE_RATE) + await ctx.send_msgs([{ + "cmd": "Set", "key": f"EnergyLink{ctx.team}", "slot": ctx.slot, "operations": + [{"operation": "add", "value": -health_diff * HP_EXCHANGE_RATE}, + {"operation": "max", "value": 0}]}]) + current_health += health_diff + writes.append((MM3_HEALTH, current_health.to_bytes(1, 'little'), "RAM")) + + if self.refill_queue: + refill_type, refill_amount = self.refill_queue.pop() + if refill_type == MM3EnergyLinkType.Life: + exchange_rate = HP_EXCHANGE_RATE + elif refill_type == MM3EnergyLinkType.OneUP: + exchange_rate = ONEUP_EXCHANGE_RATE + else: + exchange_rate = WEAPON_EXCHANGE_RATE + pool = ctx.stored_data.get(f"EnergyLink{ctx.team}", 0) + request = exchange_rate * refill_amount + if request > pool: + logger.warning( + f"Not enough energy to fulfill the request. Maximum request: {pool // exchange_rate}") + else: + await ctx.send_msgs([{ + "cmd": "Set", "key": f"EnergyLink{ctx.team}", "slot": ctx.slot, "operations": + [{"operation": "add", "value": -request}, + {"operation": "max", "value": 0}]}]) + if refill_type == MM3EnergyLinkType.Life: + refill_ptr = MM3_HEALTH + elif refill_type == MM3EnergyLinkType.OneUP: + refill_ptr = MM3_LIVES + else: + refill_ptr = MM3_WEAPON_ENERGY + MM3_WEAPONS[refill_type] + current_value = (await read(ctx.bizhawk_ctx, [(refill_ptr, 1, "RAM")]))[0][0] + if refill_type == MM3EnergyLinkType.OneUP: + current_value = from_oneup_format(current_value) + new_value = min(0x9C if refill_type != MM3EnergyLinkType.OneUP else 99, current_value + refill_amount) + if refill_type == MM3EnergyLinkType.OneUP: + new_value = to_oneup_format(new_value) + writes.append((refill_ptr, new_value.to_bytes(1, "little"), "RAM")) + + if len(self.item_queue): + item = self.item_queue.pop(0) + idx = item.item & 0xF + if idx == 0: + # 1-Up + current_lives = from_oneup_format(lives[0]) + if current_lives > 99: + self.item_queue.append(item) + else: + current_lives += 1 + current_lives = to_oneup_format(current_lives) + writes.append((MM3_LIVES, current_lives.to_bytes(1, 'little'), "RAM")) + writes.append(get_sfx_writes(0x14)) + elif idx == 1: + self.weapon_energy += 0xE + writes.append(get_sfx_writes(0x1C)) + elif idx == 2: + self.health_energy += 0xE + writes.append(get_sfx_writes(0x1C)) + elif idx == 3: + current_tanks = from_oneup_format(e_tanks[0]) + if current_tanks > 99: + self.item_queue.append(item) + else: + current_tanks += 1 + current_tanks = to_oneup_format(current_tanks) + writes.append((MM3_E_TANKS, current_tanks.to_bytes(1, 'little'), "RAM")) + writes.append(get_sfx_writes(0x14)) + + await write(ctx.bizhawk_ctx, writes) + + new_checks = [] + # check for locations + for i in range(8): + flag = 1 << i + if robot_masters_defeated[0] & flag: + rbm_id = 0x0001 + i + if rbm_id not in ctx.checked_locations: + new_checks.append(rbm_id) + wep_id = 0x0101 + i + if wep_id not in ctx.checked_locations: + new_checks.append(wep_id) + if doc_robo_defeated[0] & flag: + doc_id = 0x0010 + MM3_DOC_REMAP[i] + if doc_id not in ctx.checked_locations: + new_checks.append(doc_id) + + for i in range(2): + flag = 1 << i + if rush_acquired[0] & flag: + itm_id = 0x0111 + i + if itm_id not in ctx.checked_locations: + new_checks.append(itm_id) + + for i in (0, 1, 2, 4): + # Wily 4 does not have a boss check + boss_id = 0x0009 + i + if completed_stages[0] & (1 << i) != 0: + if boss_id not in ctx.checked_locations: + new_checks.append(boss_id) + + if completed_stages[0] & 0x80 and 0x000F not in ctx.checked_locations: + new_checks.append(0x000F) + + if bar_state[0] == 0x80: # currently in stage + if (prog_state[0] > 0x00 and current_stage[0] >= 8) or prog_state[0] == 0x00: + # need to block the specific state of Break Man prog=0x12 stage=0x5 + # it doesn't clean the consumable table and he doesn't have any anyways + for consumable in MM3_CONSUMABLE_TABLE[current_stage[0]]: + consumable_info = MM3_CONSUMABLE_TABLE[current_stage[0]][consumable] + if consumable not in ctx.checked_locations: + is_checked = consumable_checks[consumable_info[0]] & (1 << consumable_info[1]) + if is_checked: + new_checks.append(consumable) + + for new_check_id in new_checks: + ctx.locations_checked.add(new_check_id) + location = ctx.location_names.lookup_in_game(new_check_id) + nes_logger.info( + f'New Check: {location} ({len(ctx.locations_checked)}/' + f'{len(ctx.missing_locations) + len(ctx.checked_locations)})') + await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}]) diff --git a/worlds/mm3/color.py b/worlds/mm3/color.py new file mode 100644 index 0000000000..0944026432 --- /dev/null +++ b/worlds/mm3/color.py @@ -0,0 +1,331 @@ +import sys +from typing import TYPE_CHECKING +from . import names +from zlib import crc32 +import struct +import logging + +if TYPE_CHECKING: + from . import MM3World + from .rom import MM3ProcedurePatch + +HTML_TO_NES: dict[str, int] = { + 'SNOW': 0x20, + 'LINEN': 0x36, + 'SEASHELL': 0x36, + 'AZURE': 0x3C, + 'LAVENDER': 0x33, + 'WHITE': 0x30, + 'BLACK': 0x0F, + 'GREY': 0x00, + 'GRAY': 0x00, + 'ROYALBLUE': 0x12, + 'BLUE': 0x11, + 'SKYBLUE': 0x21, + 'LIGHTBLUE': 0x31, + 'TURQUOISE': 0x2B, + 'CYAN': 0x2C, + 'AQUAMARINE': 0x3B, + 'DARKGREEN': 0x0A, + 'GREEN': 0x1A, + 'YELLOW': 0x28, + 'GOLD': 0x28, + 'WHEAT': 0x37, + 'TAN': 0x37, + 'CHOCOLATE': 0x07, + 'BROWN': 0x07, + 'SALMON': 0x26, + 'ORANGE': 0x27, + 'CORAL': 0x36, + 'TOMATO': 0x16, + 'RED': 0x16, + 'PINK': 0x25, + 'MAROON': 0x06, + 'MAGENTA': 0x24, + 'FUSCHIA': 0x24, + 'VIOLET': 0x24, + 'PLUM': 0x33, + 'PURPLE': 0x14, + 'THISTLE': 0x34, + 'DARKBLUE': 0x01, + 'SILVER': 0x10, + 'NAVY': 0x02, + 'TEAL': 0x1C, + 'OLIVE': 0x18, + 'LIME': 0x2A, + 'AQUA': 0x2C, + # can add more as needed +} + +MM3_COLORS: dict[str, tuple[int, int]] = { + names.gemini_laser: (0x30, 0x21), + names.needle_cannon: (0x30, 0x17), + names.hard_knuckle: (0x10, 0x01), + names.magnet_missile: (0x10, 0x16), + names.top_spin: (0x36, 0x00), + names.search_snake: (0x30, 0x19), + names.rush_coil: (0x30, 0x15), + names.spark_shock: (0x30, 0x26), + names.rush_marine: (0x30, 0x15), + names.shadow_blade: (0x34, 0x14), + names.rush_jet: (0x30, 0x15), + names.needle_man_stage: (0x3C, 0x11), + names.magnet_man_stage: (0x30, 0x15), + names.gemini_man_stage: (0x30, 0x21), + names.hard_man_stage: (0x10, 0xC), + names.top_man_stage: (0x30, 0x26), + names.snake_man_stage: (0x30, 0x29), + names.spark_man_stage: (0x30, 0x26), + names.shadow_man_stage: (0x30, 0x11), + names.doc_needle_stage: (0x27, 0x15), + names.doc_gemini_stage: (0x27, 0x15), + names.doc_spark_stage: (0x27, 0x15), + names.doc_shadow_stage: (0x27, 0x15), +} + +MM3_KNOWN_COLORS: dict[str, tuple[int, int]] = { + **MM3_COLORS, + # Metroid series + "Varia Suit": (0x27, 0x16), + "Gravity Suit": (0x14, 0x16), + "Phazon Suit": (0x06, 0x1D), + # Street Fighter, technically + "Hadouken": (0x3C, 0x11), + "Shoryuken": (0x38, 0x16), + # X Series + "Z-Saber": (0x20, 0x16), + "Helmet Upgrade": (0x20, 0x01), + "Body Upgrade": (0x20, 0x01), + "Arms Upgrade": (0x20, 0x01), + "Plasma Shot Upgrade": (0x20, 0x01), + "Stock Charge Upgrade": (0x20, 0x01), + "Legs Upgrade": (0x20, 0x01), + # X1 + "Homing Torpedo": (0x3D, 0x37), + "Chameleon Sting": (0x3B, 0x1A), + "Rolling Shield": (0x3A, 0x25), + "Fire Wave": (0x37, 0x26), + "Storm Tornado": (0x34, 0x14), + "Electric Spark": (0x3D, 0x28), + "Boomerang Cutter": (0x3B, 0x2D), + "Shotgun Ice": (0x28, 0x2C), + # X2 + "Crystal Hunter": (0x33, 0x21), + "Bubble Splash": (0x35, 0x28), + "Spin Wheel": (0x34, 0x1B), + "Silk Shot": (0x3B, 0x27), + "Sonic Slicer": (0x27, 0x01), + "Strike Chain": (0x30, 0x23), + "Magnet Mine": (0x28, 0x2D), + "Speed Burner": (0x31, 0x16), + # X3 + "Acid Burst": (0x28, 0x2A), + "Tornado Fang": (0x28, 0x2C), + "Triad Thunder": (0x2B, 0x23), + "Spinning Blade": (0x20, 0x16), + "Ray Splasher": (0x28, 0x17), + "Gravity Well": (0x38, 0x14), + "Parasitic Bomb": (0x31, 0x28), + "Frost Shield": (0x23, 0x2C), + # X4 + "Lightning Web": (0x3D, 0x28), + "Aiming Laser": (0x2C, 0x14), + "Double Cyclone": (0x28, 0x1A), + "Rising Fire": (0x20, 0x16), + "Ground Hunter": (0x2C, 0x15), + "Soul Body": (0x37, 0x27), + "Twin Slasher": (0x28, 0x00), + "Frost Tower": (0x3D, 0x2C), +} + +if "worlds.mm2" in sys.modules: + # is this the proper way to do this? who knows! + try: + mm2 = sys.modules["worlds.mm2"] + MM3_KNOWN_COLORS.update(mm2.color.MM2_COLORS) + for item in MM3_COLORS: + mm2.color.add_color_to_mm2(item, MM3_COLORS[item]) + except AttributeError: + # pass through if an old MM2 is found + pass + +palette_pointers: dict[str, list[int]] = { + "Mega Buster": [0x7C8A8, 0x4650], + "Gemini Laser": [0x4654], + "Needle Cannon": [0x4658], + "Hard Knuckle": [0x465C], + "Magnet Missile": [0x4660], + "Top Spin": [0x4664], + "Search Snake": [0x4668], + "Rush Coil": [0x466C], + "Spark Shock": [0x4670], + "Rush Marine": [0x4674], + "Shadow Blade": [0x4678], + "Rush Jet": [0x467C], + "Needle Man": [0x216C], + "Magnet Man": [0x215C], + "Gemini Man": [0x217C], + "Hard Man": [0x2164], + "Top Man": [0x2194], + "Snake Man": [0x2174], + "Spark Man": [0x2184], + "Shadow Man": [0x218C], + "Doc Robot": [0x20B8] +} + + +def add_color_to_mm3(name: str, color: tuple[int, int]) -> None: + """ + Add a color combo for Mega Man 3 to recognize as the color to display for a given item. + For information on available colors: https://www.nesdev.org/wiki/PPU_palettes#2C02 + """ + MM3_KNOWN_COLORS[name] = validate_colors(*color) + + +def extrapolate_color(color: int) -> tuple[int, int]: + if color > 0x1F: + color_1 = color + color_2 = color_1 - 0x10 + else: + color_2 = color + color_1 = color_2 + 0x10 + return color_1, color_2 + + +def validate_colors(color_1: int, color_2: int, allow_match: bool = False) -> tuple[int, int]: + # Black should be reserved for outlines, a gray should suffice + if color_1 in [0x0D, 0x0E, 0x0F, 0x1E, 0x2E, 0x3E, 0x1F, 0x2F, 0x3F]: + color_1 = 0x10 + if color_2 in [0x0D, 0x0E, 0x0F, 0x1E, 0x2E, 0x3E, 0x1F, 0x2F, 0x3F]: + color_2 = 0x10 + + # one final check, make sure we don't have two matching + if not allow_match and color_1 == color_2: + color_1 = 0x30 # color 1 to white works with about any paired color + + return color_1, color_2 + + +def expand_colors(color_1: int, color_2: int) -> tuple[tuple[int, int, int], tuple[int, int, int]]: + if color_2 >= 0x30: + color_a = color_b = color_2 + else: + color_a = color_2 + 0x10 + color_b = color_2 + + if color_1 < 0x10: + color_c = color_1 + 0x10 + color_d = color_1 + color_e = color_1 + 0x20 + elif color_1 >= 0x30: + color_c = color_1 - 0x10 + color_d = color_1 - 0x20 + color_e = color_1 + else: + color_c = color_1 + color_d = color_1 - 0x10 + color_e = color_1 + 0x10 + + return (0x30, color_a, color_b), (color_d, color_e, color_c) + + +def get_colors_for_item(name: str) -> tuple[tuple[int, int, int], tuple[int, int, int]]: + if name in MM3_KNOWN_COLORS: + return expand_colors(*MM3_KNOWN_COLORS[name]) + + check_colors = {color: color in name.upper().replace(" ", '') for color in HTML_TO_NES} + colors = [color for color in check_colors if check_colors[color]] + if colors: + # we have at least one color pattern matched + if len(colors) > 1: + # we have at least 2 + color_1 = HTML_TO_NES[colors[0]] + color_2 = HTML_TO_NES[colors[1]] + else: + color_1, color_2 = extrapolate_color(HTML_TO_NES[colors[0]]) + else: + # generate hash + crc_hash = crc32(name.encode('utf-8')) + hash_color = struct.pack("I", crc_hash) + color_1 = hash_color[0] % 0x3F + color_2 = hash_color[1] % 0x3F + + if color_1 < color_2: + temp = color_1 + color_1 = color_2 + color_2 = temp + + color_1, color_2 = validate_colors(color_1, color_2) + + return expand_colors(color_1, color_2) + + +def parse_color(colors: list[str]) -> tuple[int, int]: + color_a = colors[0] + if color_a.startswith("$"): + color_1 = int(color_a[1:], 16) + else: + # assume it's in our list of colors + color_1 = HTML_TO_NES[color_a.upper()] + + if len(colors) == 1: + color_1, color_2 = extrapolate_color(color_1) + else: + color_b = colors[1] + if color_b.startswith("$"): + color_2 = int(color_b[1:], 16) + else: + color_2 = HTML_TO_NES[color_b.upper()] + return color_1, color_2 + + +def write_palette_shuffle(world: "MM3World", rom: "MM3ProcedurePatch") -> None: + palette_shuffle: int | str = world.options.palette_shuffle.value + palettes_to_write: dict[str, tuple[int, int]] = {} + if isinstance(palette_shuffle, str): + color_sets = palette_shuffle.split(";") + if len(color_sets) == 1: + palette_shuffle = world.options.palette_shuffle.option_none + # singularity is more correct, but this is faster + else: + palette_shuffle = world.options.palette_shuffle.options[color_sets.pop()] + for color_set in color_sets: + if "-" in color_set: + character, color = color_set.split("-") + if character.title() not in palette_pointers: + logging.warning(f"Player {world.player_name} " + f"attempted to set color for unrecognized option {character}") + colors = color.split("|") + real_colors = validate_colors(*parse_color(colors), allow_match=True) + palettes_to_write[character.title()] = real_colors + else: + # If color is provided with no character, assume singularity + colors = color_set.split("|") + real_colors = validate_colors(*parse_color(colors), allow_match=True) + for character in palette_pointers: + palettes_to_write[character] = real_colors + # Now we handle the real values + if palette_shuffle != 0: + if palette_shuffle > 1: + if palette_shuffle == 3: + # singularity + real_colors = validate_colors(world.random.randint(0, 0x3F), world.random.randint(0, 0x3F)) + for character in palette_pointers: + if character not in palettes_to_write: + palettes_to_write[character] = real_colors + else: + for character in palette_pointers: + if character not in palettes_to_write: + real_colors = validate_colors(world.random.randint(0, 0x3F), world.random.randint(0, 0x3F)) + palettes_to_write[character] = real_colors + else: + shuffled_colors = list(MM3_COLORS.values())[:-3] # only include one Doc Robot + shuffled_colors.append((0x2C, 0x11)) # Mega Buster + world.random.shuffle(shuffled_colors) + for character in palette_pointers: + if character not in palettes_to_write: + palettes_to_write[character] = shuffled_colors.pop() + + for character in palettes_to_write: + for pointer in palette_pointers[character]: + rom.write_bytes(pointer + 2, bytes(palettes_to_write[character])) diff --git a/worlds/mm3/data/mm3_basepatch.bsdiff4 b/worlds/mm3/data/mm3_basepatch.bsdiff4 new file mode 100644 index 0000000000000000000000000000000000000000..f80cb76d67cfabb7966f20053e5fc7eee454d066 GIT binary patch literal 1235 zcmV;^1T6bPQ$$HdMl>+C000000001}0RR910000G015yA0000&T4*&fL0KkKS%F}> zEC2*-|MmU2gh&Jchy+0(z<|IY5CGr;zyKfwKmjTN00AHXiiDLX>QB;{4Lwb$4H^Sa zQIJU@ntB2TBO@WEnqe81zjLVq0#rTFg?qI9x#KPy?X%1?2tYM@B!nPJ-%davhF$Tl zIiQx9IRsK^@InFXApw4Wk((hP9HukLu*#@mW7;7BK!gT3(#}EbP{I%l&Sp1fuJ`R` z)1SrMkxmpOAXqMo076=5H8DY1CR154gXBH{004jg{`SR7$$$Vu_HSX?*R;Z^xg zN~RefD;61p@_)9BLve%jTr__4F;J8nW=~*lAr(pG&BZ)27qK_00000 z8UO$p27?l5rbv5K0(rmI4pd;$A>;sOqPkexARL4PSb#YCDB=L*w^7+8+^OF9^b%)!c zTe|0`3%i#3PAn3jp>W!T@T%g&6c3SJfAqDq(5 z5zZ~pgXl;67p-uDx`V;Oa>g~JbIfQ+CKtA&^X8nlQL07SqM z0uvIzMFzk_w|EUnwGHN zT(ZiIHw384c-|)-l=*9tHOx%iW1EfJxZuj8?X?gr;mM3+kH4=}*UJe<$7F6!Vl1Ip zTlcpSgA`JbuD*Xa0y2LyLHW&qkSM08tldQMOf%dD7z?yF{#(OguYm^gokIx_Cz>AN zc47huEyVFV%9i33j^bUngBQWyOJrwgoW8i4A5KL1;hXV literal 0 HcmV?d00001 diff --git a/worlds/mm3/docs/en_Mega Man 3.md b/worlds/mm3/docs/en_Mega Man 3.md new file mode 100644 index 0000000000..abb619858c --- /dev/null +++ b/worlds/mm3/docs/en_Mega Man 3.md @@ -0,0 +1,131 @@ +# Mega Man 3 + +## Where is the options page? + +The [player options page for this game](../player-options) contains all the options you need to configure and export a +config file. + +## What does randomization do to this game? + +Weapons received from Robot Masters, access to each individual stage (including Doc Robot stages), and Items from Dr. Light are randomized +into the multiworld. Access to the Wily Stages is locked behind clearing the 4 Doc Robot stages and defeating Break Man. The game is complete upon +viewing the ending sequence after defeating Gamma. + +## What Mega Man 3 items can appear in other players' worlds? +- Robot Master weapons +- Robot Master Access Codes (stage access) +- Doc Robot Access Codes (stage access) +- Rush Coil/Jet/Marine +- 1-Ups +- E-Tanks +- Health Energy (L) +- Weapon Energy (L) + +## What is considered a location check in Mega Man 3? +- The defeat of a Robot Master, Doc Robot, or Wily Boss +- Receiving a weapon or Rush item from Dr. Light +- Optionally, 1-Ups and E-Tanks present within stages +- Optionally, Weapon and Health Energy pickups present within stages + +## When the player receives an item, what happens? +A sound effect will play based on the type of item received, and the effects of the item will be immediately applied, +such as unlocking the use of a weapon mid-stage. If the effects of the item cannot be fully applied (such as receiving +Health Energy while at full health), the remaining are withheld until they can be applied. + +## How do I access the Doc Robot stages? +By pressing Select on the Robot Master screen, the screen will transition between Robot Masters and +Doc Robots. + +## Useful Information +* **NesHawk is the recommended core for this game!** Players using QuickNes (or QuickerNes) will experience graphical + glitches while in Gemini Man's stage and fighting Gamma. +* Pressing A+B+Start+Select while in a stage will take you to the Game Over screen, allowing you to leave the stage. + Your E-Tanks will be preserved. +* Your current progress through the Wily stages is saved to the multiworld, allowing you to return to the last stage you + reached should you need to leave and enter a Robot Master stage. If you need to return to an earlier Wily stage, holding + Select while entering Break Man's stage will take you to Wily 1. +* When Random Weaknesses are enabled, Break Man's weakness will be changed from Mega Buster to one random weapon. + + +## What is EnergyLink? +EnergyLink is an energy storage supported by certain games that is shared across all worlds in a multiworld. In Mega Man + 3, when enabled, drops from enemies are not applied directly to Mega Man and are instead deposited into the EnergyLink. +Half of the energy that would be gained is lost upon transfer to the EnergyLink. + +Energy from the EnergyLink storage can be converted into health, weapon energy, and lives at different conversion rates. +You can find out how much of each type you can pull using `/pool` in the client. Additionally, you can have it +automatically pull from the EnergyLink storage to keep Mega Man healed using the `/autoheal` command in the client. +Finally, you can use the `/request` command to request a certain type of energy from the storage. + +## Plando Palettes +The palette shuffle option supports specifying a specific palette for a given weapon/Robot Master. The format for doing +so is `Character-Color1|Color2;Option`. Character is the individual that this should apply to, and can only be one of +the following: +- Mega Buster +- Gemini Laser +- Needle Cannon +- Hard Knuckle +- Magnet Missile +- Top Spin +- Search Snake +- Spark Shot +- Shadow Blade +- Rush Coil +- Rush Jet +- Rush Marine +- Needle Man +- Magnet Man +- Gemini Man +- Hard Man +- Top Man +- Snake Man +- Spark Man +- Shadow Man +- Doc Robot + +Colors attempt to map a list of HTML-defined colors to what the NES can render. A full list of applicable colors can be +found [here](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/mm2/Color.py#L11). Alternatively, colors can +be supplied directly using `$xx` format. A full list of NES colors can be found [here](https://www.nesdev.org/wiki/PPU_palettes#2C02). + +You can also pass only one color (such as `Mega Buster-Red`) and it will interpret a second color based off of the color +given. Additionally, passing only colors (such as `Red|Blue`) and not any specific boss/weapon will apply that color to +all weapons/bosses that did not have a prior color specified. + +The option is the method to be used to set the palettes of the remaining bosses/weapons, and will not overwrite any +plando placements. + +## Plando Weaknesses +Plando Weaknesses allows you to override the amount of damage a boss should take from a given weapon, ignoring prior +weaknesses generated by strict/random weakness options. Formatting for this is as follows: +```yaml +plando_weakness: + Needle Man: + Top Spin: 0 + Hard Knuckle: 4 +``` +This would cause Air Man to take 4 damage from Hard Knuckle, and 0 from Top Spin. + +Note: it is possible that plando weakness is not be respected should the plando create a situation in which the game +becomes impossible to complete. In this situation, the damage would be boosted to the minimum required to defeat the +Robot Master. + + +## Unique Local Commands +- `/pool` Only present with EnergyLink, prints the max amount of each type of request that could be fulfilled. +- `/autoheal` Only present with EnergyLink, will automatically drain energy from the EnergyLink in order to +restore Mega Man's health. +- `/request ` Only present with EnergyLink, sends a request of a certain type of energy to be pulled from +the EnergyLink. Types are as follows: + - `HP` Health + - `NE` Needle Cannon + - `MA` Magnet Missile + - `GE` Gemini Laser + - `HA` Hard Knuckle + - `TO` Top Spin + - `SN` Search Snake + - `SP` Spark Shot + - `SH` Shadow Blade + - `RC` Rush Coil + - `RM` Rush Marine + - `RJ` Rush Jet + - `1U` Lives \ No newline at end of file diff --git a/worlds/mm3/docs/setup_en.md b/worlds/mm3/docs/setup_en.md new file mode 100644 index 0000000000..07cae74a8a --- /dev/null +++ b/worlds/mm3/docs/setup_en.md @@ -0,0 +1,53 @@ +# Mega Man 3 Setup Guide + +## Required Software + +- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) +- An English Mega Man 3 ROM. Alternatively, the [Mega Man Legacy Collection](https://store.steampowered.com/app/363440/Mega_Man_Legacy_Collection/) on Steam. +- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) 2.7 or later. Bizhawk 2.10 + +### Configuring Bizhawk + +Once you have installed BizHawk, open `EmuHawk.exe` and change the following settings: + +- If you're using BizHawk 2.7 or 2.8, go to `Config > Customize`. On the Advanced tab, switch the Lua Core from +`NLua+KopiLua` to `Lua+LuaInterface`, then restart EmuHawk. (If you're using BizHawk 2.9, you can skip this step.) +- Under `Config > Customize`, check the "Run in background" option to prevent disconnecting from the client while you're +tabbed out of EmuHawk. +- Open a `.nes` file in EmuHawk and go to `Config > Controllers…` to configure your inputs. If you can't click +`Controllers…`, load any `.nes` ROM first. +- Consider clearing keybinds in `Config > Hotkeys…` if you don't intend to use them. Select the keybind and press Esc to +clear it. + +## Generating and Patching a Game + +1. Create your options file (YAML). You can make one on the +[Mega Man 3 options page](../../../games/Mega%20Man%203/player-options). +2. Follow the general Archipelago instructions for [generating a game](../../Archipelago/setup/en#generating-a-game). +This will generate an output file for you. Your patch file will have the `.apmm3` file extension. +3. Open `ArchipelagoLauncher.exe` +4. Select "Open Patch" on the left side and select your patch file. +5. If this is your first time patching, you will be prompted to locate your vanilla ROM. If you are using the Legacy +Collection, provide `Proteus.exe` in place of your rom. +6. A patched `.nes` file will be created in the same place as the patch file. +7. On your first time opening a patch with BizHawk Client, you will also be asked to locate `EmuHawk.exe` in your +BizHawk install. + +## Connecting to a Server + +By default, opening a patch file will do steps 1-5 below for you automatically. Even so, keep them in your memory just +in case you have to close and reopen a window mid-game for some reason. + +1. Mega Man 3 uses Archipelago's BizHawk Client. If the client isn't still open from when you patched your game, +you can re-open it from the launcher. +2. Ensure EmuHawk is running the patched ROM. +3. In EmuHawk, go to `Tools > Lua Console`. This window must stay open while playing. +4. In the Lua Console window, go to `Script > Open Script…`. +5. Navigate to your Archipelago install folder and open `data/lua/connector_bizhawk_generic.lua`. +6. The emulator and client will eventually connect to each other. The BizHawk Client window should indicate that it +connected and recognized Mega Man 3. +7. To connect the client to the server, enter your room's address and port (e.g. `archipelago.gg:38281`) into the +top text field of the client and click Connect. + +You should now be able to receive and send items. You'll need to do these steps every time you want to reconnect. It is +perfectly safe to make progress offline; everything will re-sync when you reconnect. diff --git a/worlds/mm3/items.py b/worlds/mm3/items.py new file mode 100644 index 0000000000..40e6114fff --- /dev/null +++ b/worlds/mm3/items.py @@ -0,0 +1,80 @@ +from BaseClasses import Item +from typing import NamedTuple +from .names import (needle_cannon, magnet_missile, gemini_laser, hard_knuckle, top_spin, search_snake, spark_shock, + shadow_blade, rush_coil, rush_marine, rush_jet, needle_man_stage, magnet_man_stage, + gemini_man_stage, hard_man_stage, top_man_stage, snake_man_stage, spark_man_stage, shadow_man_stage, + doc_needle_stage, doc_gemini_stage, doc_spark_stage, doc_shadow_stage, e_tank, weapon_energy, + health_energy, one_up) + + +class ItemData(NamedTuple): + code: int + progression: bool + useful: bool = False # primarily use this for incredibly useful items of their class, like Metal Blade + skip_balancing: bool = False + + +class MM3Item(Item): + game = "Mega Man 3" + + +robot_master_weapon_table = { + needle_cannon: ItemData(0x0001, True), + magnet_missile: ItemData(0x0002, True, True), + gemini_laser: ItemData(0x0003, True), + hard_knuckle: ItemData(0x0004, True), + top_spin: ItemData(0x0005, True, True), + search_snake: ItemData(0x0006, True), + spark_shock: ItemData(0x0007, True), + shadow_blade: ItemData(0x0008, True, True), +} + +stage_access_table = { + needle_man_stage: ItemData(0x0101, True), + magnet_man_stage: ItemData(0x0102, True), + gemini_man_stage: ItemData(0x0103, True), + hard_man_stage: ItemData(0x0104, True), + top_man_stage: ItemData(0x0105, True), + snake_man_stage: ItemData(0x0106, True), + spark_man_stage: ItemData(0x0107, True), + shadow_man_stage: ItemData(0x0108, True), + doc_needle_stage: ItemData(0x0111, True, True), + doc_gemini_stage: ItemData(0x0113, True, True), + doc_spark_stage: ItemData(0x0117, True, True), + doc_shadow_stage: ItemData(0x0118, True, True), +} + +rush_item_table = { + rush_coil: ItemData(0x0011, True, True), + rush_marine: ItemData(0x0012, True), + rush_jet: ItemData(0x0013, True, True), +} + +filler_item_table = { + one_up: ItemData(0x0020, False), + weapon_energy: ItemData(0x0021, False), + health_energy: ItemData(0x0022, False), + e_tank: ItemData(0x0023, False, True), +} + +filler_item_weights = { + one_up: 1, + weapon_energy: 4, + health_energy: 1, + e_tank: 2, +} + +item_table = { + **robot_master_weapon_table, + **stage_access_table, + **rush_item_table, + **filler_item_table, +} + +item_names = { + "Weapons": {name for name in robot_master_weapon_table.keys()}, + "Stages": {name for name in stage_access_table.keys()}, + "Rush": {name for name in rush_item_table.keys()} +} + +lookup_item_to_id: dict[str, int] = {item_name: data.code for item_name, data in item_table.items() if data.code} diff --git a/worlds/mm3/locations.py b/worlds/mm3/locations.py new file mode 100644 index 0000000000..2504236bda --- /dev/null +++ b/worlds/mm3/locations.py @@ -0,0 +1,312 @@ +from BaseClasses import Location, Region +from typing import NamedTuple +from . import names + + +class MM3Location(Location): + game = "Mega Man 3" + + +class MM3Region(Region): + game = "Mega Man 3" + + +class LocationData(NamedTuple): + location_id: int | None + energy: bool = False + oneup_tank: bool = False + + +class RegionData(NamedTuple): + locations: dict[str, LocationData] + required_items: list[str] + parent: str = "" + +mm3_regions: dict[str, RegionData] = { + "Needle Man Stage": RegionData({ + names.needle_man: LocationData(0x0001), + names.get_needle_cannon: LocationData(0x0101), + names.get_rush_jet: LocationData(0x0111), + names.needle_man_c1: LocationData(0x0200, energy=True), + names.needle_man_c2: LocationData(0x0201, oneup_tank=True), + }, [names.needle_man_stage]), + + "Magnet Man Stage": RegionData({ + names.magnet_man: LocationData(0x0002), + names.get_magnet_missile: LocationData(0x0102), + names.magnet_man_c1: LocationData(0x0202, energy=True), + names.magnet_man_c2: LocationData(0x0203, energy=True), + names.magnet_man_c3: LocationData(0x0204, energy=True), + names.magnet_man_c4: LocationData(0x0205, energy=True), + names.magnet_man_c5: LocationData(0x0206, energy=True), + names.magnet_man_c6: LocationData(0x0207, energy=True), + names.magnet_man_c7: LocationData(0x0208, energy=True), + names.magnet_man_c8: LocationData(0x0209, energy=True), + }, [names.magnet_man_stage]), + + "Gemini Man Stage": RegionData({ + names.gemini_man: LocationData(0x0003), + names.get_gemini_laser: LocationData(0x0103), + names.gemini_man_c1: LocationData(0x020A, oneup_tank=True), + names.gemini_man_c2: LocationData(0x020B, energy=True), + names.gemini_man_c3: LocationData(0x020C, oneup_tank=True), + names.gemini_man_c4: LocationData(0x020D, energy=True), + names.gemini_man_c5: LocationData(0x020E, energy=True), + names.gemini_man_c6: LocationData(0x020F, oneup_tank=True), + names.gemini_man_c7: LocationData(0x0210, oneup_tank=True), + names.gemini_man_c8: LocationData(0x0211, energy=True), + names.gemini_man_c9: LocationData(0x0212, energy=True), + names.gemini_man_c10: LocationData(0x0213, oneup_tank=True), + }, [names.gemini_man_stage]), + + "Hard Man Stage": RegionData({ + names.hard_man: LocationData(0x0004), + names.get_hard_knuckle: LocationData(0x0104), + names.hard_man_c1: LocationData(0x0214, energy=True), + names.hard_man_c2: LocationData(0x0215, energy=True), + names.hard_man_c3: LocationData(0x0216, oneup_tank=True), + names.hard_man_c4: LocationData(0x0217, energy=True), + names.hard_man_c5: LocationData(0x0218, energy=True), + names.hard_man_c6: LocationData(0x0219, energy=True), + names.hard_man_c7: LocationData(0x021A, energy=True), + }, [names.hard_man_stage]), + + "Top Man Stage": RegionData({ + names.top_man: LocationData(0x0005), + names.get_top_spin: LocationData(0x0105), + names.top_man_c1: LocationData(0x021B, energy=True), + names.top_man_c2: LocationData(0x021C, energy=True), + names.top_man_c3: LocationData(0x021D, energy=True), + names.top_man_c4: LocationData(0x021E, energy=True), + names.top_man_c5: LocationData(0x021F, energy=True), + names.top_man_c6: LocationData(0x0220, oneup_tank=True), + names.top_man_c7: LocationData(0x0221, energy=True), + names.top_man_c8: LocationData(0x0222, energy=True), + }, [names.top_man_stage]), + + "Snake Man Stage": RegionData({ + names.snake_man: LocationData(0x0006), + names.get_search_snake: LocationData(0x0106), + names.snake_man_c1: LocationData(0x0223, energy=True), + names.snake_man_c2: LocationData(0x0224, energy=True), + names.snake_man_c3: LocationData(0x0225, oneup_tank=True), + names.snake_man_c4: LocationData(0x0226, oneup_tank=True), + names.snake_man_c5: LocationData(0x0227, energy=True), + }, [names.snake_man_stage]), + + "Spark Man Stage": RegionData({ + names.spark_man: LocationData(0x0007), + names.get_spark_shock: LocationData(0x0107), + names.spark_man_c1: LocationData(0x0228, energy=True), + names.spark_man_c2: LocationData(0x0229, energy=True), + names.spark_man_c3: LocationData(0x022A, energy=True), + names.spark_man_c4: LocationData(0x022B, energy=True), + names.spark_man_c5: LocationData(0x022C, energy=True), + names.spark_man_c6: LocationData(0x022D, energy=True), + }, [names.spark_man_stage]), + + "Shadow Man Stage": RegionData({ + names.shadow_man: LocationData(0x0008), + names.get_shadow_blade: LocationData(0x0108), + names.get_rush_marine: LocationData(0x0112), + names.shadow_man_c1: LocationData(0x022E, energy=True), + names.shadow_man_c2: LocationData(0x022F, energy=True), + names.shadow_man_c3: LocationData(0x0230, energy=True), + names.shadow_man_c4: LocationData(0x0231, energy=True), + }, [names.shadow_man_stage]), + + "Doc Robot (Needle) - Air": RegionData({ + names.doc_air: LocationData(0x0010), + names.doc_needle_c1: LocationData(0x0232, energy=True), + names.doc_needle_c2: LocationData(0x0233, oneup_tank=True), + names.doc_needle_c3: LocationData(0x0234, oneup_tank=True), + }, [names.doc_needle_stage]), + + "Doc Robot (Needle) - Crash": RegionData({ + names.doc_crash: LocationData(0x0011), + names.doc_needle: LocationData(None), + names.doc_needle_c4: LocationData(0x0235, energy=True), + names.doc_needle_c5: LocationData(0x0236, energy=True), + names.doc_needle_c6: LocationData(0x0237, energy=True), + names.doc_needle_c7: LocationData(0x0238, energy=True), + names.doc_needle_c8: LocationData(0x0239, energy=True), + names.doc_needle_c9: LocationData(0x023A, energy=True), + names.doc_needle_c10: LocationData(0x023B, energy=True), + names.doc_needle_c11: LocationData(0x023C, energy=True), + }, [], parent="Doc Robot (Needle) - Air"), + + "Doc Robot (Gemini) - Flash": RegionData({ + names.doc_flash: LocationData(0x0012), + names.doc_gemini_c1: LocationData(0x023D, oneup_tank=True), + names.doc_gemini_c2: LocationData(0x023E, oneup_tank=True), + }, [names.doc_gemini_stage]), + + "Doc Robot (Gemini) - Bubble": RegionData({ + names.doc_bubble: LocationData(0x0013), + names.doc_gemini: LocationData(None), + names.doc_gemini_c3: LocationData(0x023F, energy=True), + names.doc_gemini_c4: LocationData(0x0240, energy=True), + }, [], parent="Doc Robot (Gemini) - Flash"), + + "Doc Robot (Shadow) - Wood": RegionData({ + names.doc_wood: LocationData(0x0014), + }, [names.doc_shadow_stage]), + + "Doc Robot (Shadow) - Heat": RegionData({ + names.doc_heat: LocationData(0x0015), + names.doc_shadow: LocationData(None), + names.doc_shadow_c1: LocationData(0x0243, energy=True), + names.doc_shadow_c2: LocationData(0x0244, energy=True), + names.doc_shadow_c3: LocationData(0x0245, energy=True), + names.doc_shadow_c4: LocationData(0x0246, energy=True), + names.doc_shadow_c5: LocationData(0x0247, energy=True), + }, [], parent="Doc Robot (Shadow) - Wood"), + + "Doc Robot (Spark) - Metal": RegionData({ + names.doc_metal: LocationData(0x0016), + names.doc_spark_c1: LocationData(0x0241, energy=True), + }, [names.doc_spark_stage]), + + "Doc Robot (Spark) - Quick": RegionData({ + names.doc_quick: LocationData(0x0017), + names.doc_spark: LocationData(None), + names.doc_spark_c2: LocationData(0x0242, energy=True), + }, [], parent="Doc Robot (Spark) - Metal"), + + "Break Man": RegionData({ + names.break_man: LocationData(0x000F), + names.break_stage: LocationData(None), + }, [names.doc_needle, names.doc_gemini, names.doc_spark, names.doc_shadow]), + + "Wily Stage 1": RegionData({ + names.wily_1_boss: LocationData(0x0009), + names.wily_stage_1: LocationData(None), + names.wily_1_c1: LocationData(0x0248, oneup_tank=True), + names.wily_1_c2: LocationData(0x0249, oneup_tank=True), + names.wily_1_c3: LocationData(0x024A, energy=True), + names.wily_1_c4: LocationData(0x024B, oneup_tank=True), + names.wily_1_c5: LocationData(0x024C, energy=True), + names.wily_1_c6: LocationData(0x024D, energy=True), + names.wily_1_c7: LocationData(0x024E, energy=True), + names.wily_1_c8: LocationData(0x024F, oneup_tank=True), + names.wily_1_c9: LocationData(0x0250, energy=True), + names.wily_1_c10: LocationData(0x0251, energy=True), + names.wily_1_c11: LocationData(0x0252, energy=True), + names.wily_1_c12: LocationData(0x0253, energy=True), + }, [names.break_stage], parent="Break Man"), + + "Wily Stage 2": RegionData({ + names.wily_2_boss: LocationData(0x000A), + names.wily_stage_2: LocationData(None), + names.wily_2_c1: LocationData(0x0254, energy=True), + names.wily_2_c2: LocationData(0x0255, energy=True), + names.wily_2_c3: LocationData(0x0256, oneup_tank=True), + names.wily_2_c4: LocationData(0x0257, energy=True), + names.wily_2_c5: LocationData(0x0258, energy=True), + names.wily_2_c6: LocationData(0x0259, energy=True), + names.wily_2_c7: LocationData(0x025A, energy=True), + names.wily_2_c8: LocationData(0x025B, energy=True), + names.wily_2_c9: LocationData(0x025C, oneup_tank=True), + names.wily_2_c10: LocationData(0x025D, energy=True), + names.wily_2_c11: LocationData(0x025E, oneup_tank=True), + names.wily_2_c12: LocationData(0x025F, energy=True), + names.wily_2_c13: LocationData(0x0260, energy=True), + }, [names.wily_stage_1], parent="Wily Stage 1"), + + "Wily Stage 3": RegionData({ + names.wily_3_boss: LocationData(0x000B), + names.wily_stage_3: LocationData(None), + names.wily_3_c1: LocationData(0x0261, energy=True), + names.wily_3_c2: LocationData(0x0262, energy=True), + names.wily_3_c3: LocationData(0x0263, oneup_tank=True), + names.wily_3_c4: LocationData(0x0264, oneup_tank=True), + names.wily_3_c5: LocationData(0x0265, energy=True), + names.wily_3_c6: LocationData(0x0266, energy=True), + names.wily_3_c7: LocationData(0x0267, energy=True), + names.wily_3_c8: LocationData(0x0268, energy=True), + names.wily_3_c9: LocationData(0x0269, energy=True), + names.wily_3_c10: LocationData(0x026A, oneup_tank=True), + names.wily_3_c11: LocationData(0x026B, oneup_tank=True) + }, [names.wily_stage_2], parent="Wily Stage 2"), + + "Wily Stage 4": RegionData({ + names.wily_stage_4: LocationData(None), + names.wily_4_c1: LocationData(0x026C, energy=True), + names.wily_4_c2: LocationData(0x026D, energy=True), + names.wily_4_c3: LocationData(0x026E, energy=True), + names.wily_4_c4: LocationData(0x026F, energy=True), + names.wily_4_c5: LocationData(0x0270, energy=True), + names.wily_4_c6: LocationData(0x0271, energy=True), + names.wily_4_c7: LocationData(0x0272, energy=True), + names.wily_4_c8: LocationData(0x0273, energy=True), + names.wily_4_c9: LocationData(0x0274, energy=True), + names.wily_4_c10: LocationData(0x0275, oneup_tank=True), + names.wily_4_c11: LocationData(0x0276, energy=True), + names.wily_4_c12: LocationData(0x0277, oneup_tank=True), + names.wily_4_c13: LocationData(0x0278, energy=True), + names.wily_4_c14: LocationData(0x0279, energy=True), + names.wily_4_c15: LocationData(0x027A, energy=True), + names.wily_4_c16: LocationData(0x027B, energy=True), + names.wily_4_c17: LocationData(0x027C, energy=True), + names.wily_4_c18: LocationData(0x027D, energy=True), + names.wily_4_c19: LocationData(0x027E, energy=True), + names.wily_4_c20: LocationData(0x027F, energy=True), + }, [names.wily_stage_3], parent="Wily Stage 3"), + + "Wily Stage 5": RegionData({ + names.wily_5_boss: LocationData(0x000D), + names.wily_stage_5: LocationData(None), + names.wily_5_c1: LocationData(0x0280, energy=True), + names.wily_5_c2: LocationData(0x0281, energy=True), + names.wily_5_c3: LocationData(0x0282, oneup_tank=True), + names.wily_5_c4: LocationData(0x0283, oneup_tank=True), + }, [names.wily_stage_4], parent="Wily Stage 4"), + + "Wily Stage 6": RegionData({ + names.gamma: LocationData(None), + names.wily_6_c1: LocationData(0x0284, oneup_tank=True), + names.wily_6_c2: LocationData(0x0285, oneup_tank=True), + names.wily_6_c3: LocationData(0x0286, energy=True), + names.wily_6_c4: LocationData(0x0287, energy=True), + names.wily_6_c5: LocationData(0x0288, oneup_tank=True), + names.wily_6_c6: LocationData(0x0289, oneup_tank=True), + names.wily_6_c7: LocationData(0x028A, energy=True), + }, [names.wily_stage_5], parent="Wily Stage 5"), +} + + +def get_boss_locations(region: str) -> list[str]: + return [location for location, data in mm3_regions[region].locations.items() + if not data.energy and not data.oneup_tank] + + +def get_energy_locations(region: str) -> list[str]: + return [location for location, data in mm3_regions[region].locations.items() if data.energy] + + +def get_oneup_locations(region: str) -> list[str]: + return [location for location, data in mm3_regions[region].locations.items() if data.oneup_tank] + + +location_table: dict[str, int | None] = { + location: data.location_id for region in mm3_regions.values() for location, data in region.locations.items() +} + + +location_groups = { + "Get Equipped": { + names.get_needle_cannon, + names.get_magnet_missile, + names.get_gemini_laser, + names.get_hard_knuckle, + names.get_top_spin, + names.get_search_snake, + names.get_spark_shock, + names.get_shadow_blade, + names.get_rush_marine, + names.get_rush_jet, + }, + **{name: {location for location, data in region.locations.items() if data.location_id} for name, region in mm3_regions.items()} +} + +lookup_location_to_id: dict[str, int] = {location: idx for location, idx in location_table.items() if idx is not None} diff --git a/worlds/mm3/names.py b/worlds/mm3/names.py new file mode 100644 index 0000000000..dfad752676 --- /dev/null +++ b/worlds/mm3/names.py @@ -0,0 +1,221 @@ +# Robot Master Weapons +gemini_laser = "Gemini Laser" +needle_cannon = "Needle Cannon" +hard_knuckle = "Hard Knuckle" +magnet_missile = "Magnet Missile" +top_spin = "Top Spin" +search_snake = "Search Snake" +spark_shock = "Spark Shock" +shadow_blade = "Shadow Blade" + +# Rush +rush_coil = "Rush Coil" +rush_jet = "Rush Jet" +rush_marine = "Rush Marine" + +# Access Codes +needle_man_stage = "Needle Man Access Codes" +magnet_man_stage = "Magnet Man Access Codes" +gemini_man_stage = "Gemini Man Access Codes" +hard_man_stage = "Hard Man Access Codes" +top_man_stage = "Top Man Access Codes" +snake_man_stage = "Snake Man Access Codes" +spark_man_stage = "Spark Man Access Codes" +shadow_man_stage = "Shadow Man Access Codes" +doc_needle_stage = "Doc Robot (Needle) Access Codes" +doc_gemini_stage = "Doc Robot (Gemini) Access Codes" +doc_spark_stage = "Doc Robot (Spark) Access Codes" +doc_shadow_stage = "Doc Robot (Shadow) Access Codes" + +# Misc. Items +one_up = "1-Up" +weapon_energy = "Weapon Energy (L)" +health_energy = "Health Energy (L)" +e_tank = "E-Tank" + +needle_man = "Needle Man - Defeated" +magnet_man = "Magnet Man - Defeated" +gemini_man = "Gemini Man - Defeated" +hard_man = "Hard Man - Defeated" +top_man = "Top Man - Defeated" +snake_man = "Snake Man - Defeated" +spark_man = "Spark Man - Defeated" +shadow_man = "Shadow Man - Defeated" +doc_air = "Doc Robot (Air) - Defeated" +doc_crash = "Doc Robot (Crash) - Defeated" +doc_flash = "Doc Robot (Flash) - Defeated" +doc_bubble = "Doc Robot (Bubble) - Defeated" +doc_wood = "Doc Robot (Wood) - Defeated" +doc_heat = "Doc Robot (Heat) - Defeated" +doc_metal = "Doc Robot (Metal) - Defeated" +doc_quick = "Doc Robot (Quick) - Defeated" +break_man = "Break Man - Defeated" +wily_1_boss = "Kamegoro Maker - Defeated" +wily_2_boss = "Yellow Devil MK-II - Defeated" +wily_3_boss = "Holograph Mega Man - Defeated" +wily_5_boss = "Wily Machine 3 - Defeated" +gamma = "Gamma - Defeated" + +get_gemini_laser = "Gemini Laser - Received" +get_needle_cannon = "Needle Cannon - Received" +get_hard_knuckle = "Hard Knuckle - Received" +get_magnet_missile = "Magnet Missile - Received" +get_top_spin = "Top Spin - Received" +get_search_snake = "Search Snake - Received" +get_spark_shock = "Spark Shock - Received" +get_shadow_blade = "Shadow Blade - Received" +get_rush_jet = "Rush Jet - Received" +get_rush_marine = "Rush Marine - Received" + +# Wily Stage Event Items +doc_needle = "Doc Robot (Needle) - Completed" +doc_gemini = "Doc Robot (Gemini) - Completed" +doc_spark = "Doc Robot (Spark) - Completed" +doc_shadow = "Doc Robot (Shadow) - Completed" +break_stage = "Break Man" +wily_stage_1 = "Wily Stage 1 - Completed" +wily_stage_2 = "Wily Stage 2 - Completed" +wily_stage_3 = "Wily Stage 3 - Completed" +wily_stage_4 = "Wily Stage 4 - Completed" +wily_stage_5 = "Wily Stage 5 - Completed" + +# Consumable Locations +needle_man_c1 = "Needle Man Stage - Weapon Energy 1" +needle_man_c2 = "Needle Man Stage - E-Tank" +magnet_man_c1 = "Magnet Man Stage - Health Energy 1" +magnet_man_c2 = "Magnet Man Stage - Health Energy 2" +magnet_man_c3 = "Magnet Man Stage - Health Energy 3" +magnet_man_c4 = "Magnet Man Stage - Health Energy 4" +magnet_man_c5 = "Magnet Man Stage - Weapon Energy 1" +magnet_man_c6 = "Magnet Man Stage - Weapon Energy 2" +magnet_man_c7 = "Magnet Man Stage - Weapon Energy 3" +magnet_man_c8 = "Magnet Man Stage - Health Energy 5" +gemini_man_c1 = "Gemini Man Stage - 1-Up 1" +gemini_man_c2 = "Gemini Man Stage - Health Energy 1" +gemini_man_c3 = "Gemini Man Stage - Mystery Tank" +gemini_man_c4 = "Gemini Man Stage - Weapon Energy 1" +gemini_man_c5 = "Gemini Man Stage - Health Energy 2" +gemini_man_c6 = "Gemini Man Stage - 1-Up 2" +gemini_man_c7 = "Gemini Man Stage - E-Tank 1" +gemini_man_c8 = "Gemini Man Stage - Weapon Energy 2" +gemini_man_c9 = "Gemini Man Stage - Weapon Energy 3" +gemini_man_c10 = "Gemini Man Stage - E-Tank 2" +hard_man_c1 = "Hard Man Stage - Health Energy 1" +hard_man_c2 = "Hard Man Stage - Health Energy 2" +hard_man_c3 = "Hard Man Stage - E-Tank" +hard_man_c4 = "Hard Man Stage - Health Energy 3" +hard_man_c5 = "Hard Man Stage - Health Energy 4" +hard_man_c6 = "Hard Man Stage - Health Energy 5" +hard_man_c7 = "Hard Man Stage - Health Energy 6" +top_man_c1 = "Top Man Stage - Health Energy 1" +top_man_c2 = "Top Man Stage - Health Energy 2" +top_man_c3 = "Top Man Stage - Health Energy 3" +top_man_c4 = "Top Man Stage - Health Energy 4" +top_man_c5 = "Top Man Stage - Weapon Energy 1" +top_man_c6 = "Top Man Stage - 1-Up" +top_man_c7 = "Top Man Stage - Health Energy 5" +top_man_c8 = "Top Man Stage - Health Energy 6" +snake_man_c1 = "Snake Man Stage - Health Energy 1" +snake_man_c2 = "Snake Man Stage - Health Energy 2" +snake_man_c3 = "Snake Man Stage - Mystery Tank 1" +snake_man_c4 = "Snake Man Stage - Mystery Tank 2" +snake_man_c5 = "Snake Man Stage - Health Energy 3" +spark_man_c1 = "Spark Man Stage - Health Energy 1" +spark_man_c2 = "Spark Man Stage - Weapon Energy 1" +spark_man_c3 = "Spark Man Stage - Weapon Energy 2" +spark_man_c4 = "Spark Man Stage - Weapon Energy 3" +spark_man_c5 = "Spark Man Stage - Weapon Energy 4" +spark_man_c6 = "Spark Man Stage - Weapon Energy 5" +shadow_man_c1 = "Shadow Man Stage - Weapon Energy 1" +shadow_man_c2 = "Shadow Man Stage - Weapon Energy 2" +shadow_man_c3 = "Shadow Man Stage - Weapon Energy 3" +shadow_man_c4 = "Shadow Man Stage - Weapon Energy 4" +doc_needle_c1 = "Doc Robot (Needle) - Health Energy 1" +doc_needle_c2 = "Doc Robot (Needle) - 1-Up 1" +doc_needle_c3 = "Doc Robot (Needle) - E-Tank 1" +doc_needle_c4 = "Doc Robot (Needle) - Weapon Energy 1" +doc_needle_c5 = "Doc Robot (Needle) - Weapon Energy 2" +doc_needle_c6 = "Doc Robot (Needle) - Weapon Energy 3" +doc_needle_c7 = "Doc Robot (Needle) - Weapon Energy 4" +doc_needle_c8 = "Doc Robot (Needle) - Weapon Energy 5" +doc_needle_c9 = "Doc Robot (Needle) - Weapon Energy 6" +doc_needle_c10 = "Doc Robot (Needle) - Weapon Energy 7" +doc_needle_c11 = "Doc Robot (Needle) - Health Energy 2" +doc_gemini_c1 = "Doc Robot (Gemini) - Mystery Tank 1" +doc_gemini_c2 = "Doc Robot (Gemini) - Mystery Tank 2" +doc_gemini_c3 = "Doc Robot (Gemini) - Weapon Energy 1" +doc_gemini_c4 = "Doc Robot (Gemini) - Weapon Energy 2" +doc_spark_c1 = "Doc Robot (Spark) - Health Energy 1" +doc_spark_c2 = "Doc Robot (Spark) - Health Energy 2" +doc_shadow_c1 = "Doc Robot (Shadow) - Health Energy 1" +doc_shadow_c2 = "Doc Robot (Shadow) - Weapon Energy 1" +doc_shadow_c3 = "Doc Robot (Shadow) - Weapon Energy 2" +doc_shadow_c4 = "Doc Robot (Shadow) - Weapon Energy 3" +doc_shadow_c5 = "Doc Robot (Shadow) - Weapon Energy 4" +wily_1_c1 = "Wily Stage 1 - 1-Up 1" +wily_1_c2 = "Wily Stage 1 - E-Tank 1" +wily_1_c3 = "Wily Stage 1 - Weapon Energy 1" +wily_1_c4 = "Wily Stage 1 - 1-Up 2" # Hard Knuckle +wily_1_c5 = "Wily Stage 1 - Health Energy 1" # Hard Knuckle +wily_1_c6 = "Wily Stage 1 - Weapon Energy 2" # Hard Knuckle & Rush Vertical +wily_1_c7 = "Wily Stage 1 - Health Energy 2" # Hard Knuckle & Rush Vertical +wily_1_c8 = "Wily Stage 1 - E-Tank 2" # Hard Knuckle & Rush Vertical +wily_1_c9 = "Wily Stage 1 - Health Energy 3" +wily_1_c10 = "Wily Stage 1 - Health Energy 4" +wily_1_c11 = "Wily Stage 1 - Weapon Energy 3" # Rush Vertical +wily_1_c12 = "Wily Stage 1 - Weapon Energy 4" # Rush Vertical +wily_2_c1 = "Wily Stage 2 - Weapon Energy 1" +wily_2_c2 = "Wily Stage 2 - Weapon Energy 2" +wily_2_c3 = "Wily Stage 2 - 1-Up 1" +wily_2_c4 = "Wily Stage 2 - Weapon Energy 3" +wily_2_c5 = "Wily Stage 2 - Health Energy 1" +wily_2_c6 = "Wily Stage 2 - Health Energy 2" +wily_2_c7 = "Wily Stage 2 - Health Energy 3" +wily_2_c8 = "Wily Stage 2 - Weapon Energy 4" +wily_2_c9 = "Wily Stage 2 - E-Tank 1" +wily_2_c10 = "Wily Stage 2 - Weapon Energy 5" +wily_2_c11 = "Wily Stage 2 - E-Tank 2" +wily_2_c12 = "Wily Stage 2 - Weapon Energy 6" +wily_2_c13 = "Wily Stage 2 - Weapon Energy 7" +wily_3_c1 = "Wily Stage 3 - Weapon Energy 1" # Hard Knuckle +wily_3_c2 = "Wily Stage 3 - Weapon Energy 2" # Hard Knuckle +wily_3_c3 = "Wily Stage 3 - E-Tank 1" +wily_3_c4 = "Wily Stage 3 - 1-Up 1" +wily_3_c5 = "Wily Stage 3 - Health Energy 1" +wily_3_c6 = "Wily Stage 3 - Health Energy 2" +wily_3_c7 = "Wily Stage 3 - Health Energy 3" +wily_3_c8 = "Wily Stage 3 - Health Energy 4" +wily_3_c9 = "Wily Stage 3 - Weapon Energy 3" +wily_3_c10 = "Wily Stage 3 - Mystery Tank 1" # Hard Knuckle +wily_3_c11 = "Wily Stage 3 - Mystery Tank 2" # Hard Knuckle +wily_4_c1 = "Wily Stage 4 - Weapon Energy 1" +wily_4_c2 = "Wily Stage 4 - Weapon Energy 2" +wily_4_c3 = "Wily Stage 4 - Weapon Energy 3" +wily_4_c4 = "Wily Stage 4 - Weapon Energy 4" +wily_4_c5 = "Wily Stage 4 - Weapon Energy 5" +wily_4_c6 = "Wily Stage 4 - Health Energy 1" +wily_4_c7 = "Wily Stage 4 - Health Energy 2" +wily_4_c8 = "Wily Stage 4 - Health Energy 3" +wily_4_c9 = "Wily Stage 4 - Health Energy 4" +wily_4_c10 = "Wily Stage 4 - Mystery Tank" +wily_4_c11 = "Wily Stage 4 - Weapon Energy 6" +wily_4_c12 = "Wily Stage 4 - 1-Up" +wily_4_c13 = "Wily Stage 4 - Weapon Energy 7" +wily_4_c14 = "Wily Stage 4 - Weapon Energy 8" +wily_4_c15 = "Wily Stage 4 - Weapon Energy 9" +wily_4_c16 = "Wily Stage 4 - Weapon Energy 10" +wily_4_c17 = "Wily Stage 4 - Weapon Energy 11" +wily_4_c18 = "Wily Stage 4 - Weapon Energy 12" +wily_4_c19 = "Wily Stage 4 - Weapon Energy 13" +wily_4_c20 = "Wily Stage 4 - Weapon Energy 14" +wily_5_c1 = "Wily Stage 5 - Weapon Energy 1" +wily_5_c2 = "Wily Stage 5 - Weapon Energy 2" +wily_5_c3 = "Wily Stage 5 - Mystery Tank 1" +wily_5_c4 = "Wily Stage 5 - Mystery Tank 2" +wily_6_c1 = "Wily Stage 6 - Mystery Tank 1" +wily_6_c2 = "Wily Stage 6 - Mystery Tank 2" +wily_6_c3 = "Wily Stage 6 - Weapon Energy 1" +wily_6_c4 = "Wily Stage 6 - Weapon Energy 2" +wily_6_c5 = "Wily Stage 6 - 1-Up" +wily_6_c6 = "Wily Stage 6 - E-Tank" +wily_6_c7 = "Wily Stage 6 - Health Energy" diff --git a/worlds/mm3/options.py b/worlds/mm3/options.py new file mode 100644 index 0000000000..a1e9b24834 --- /dev/null +++ b/worlds/mm3/options.py @@ -0,0 +1,164 @@ +from dataclasses import dataclass + +from Options import Choice, Toggle, DeathLink, TextChoice, Range, OptionDict, PerGameCommonOptions +from schema import Schema, And, Use, Optional +from .rules import bosses, weapons_to_id + + +class EnergyLink(Toggle): + """ + Enables EnergyLink support. + When enabled, pickups dropped from enemies are sent to the EnergyLink pool, and healing/weapon energy/1-Ups can + be requested from the EnergyLink pool. + Some of the energy sent to the pool will be lost on transfer. + """ + display_name = "EnergyLink" + + +class StartingRobotMaster(Choice): + """ + The initial stage unlocked at the start. + """ + display_name = "Starting Robot Master" + option_needle_man = 0 + option_magnet_man = 1 + option_gemini_man = 2 + option_hard_man = 3 + option_top_man = 4 + option_snake_man = 5 + option_spark_man = 6 + option_shadow_man = 7 + default = "random" + + +class Consumables(Choice): + """ + When enabled, e-tanks/1-ups/health/weapon energy will be added to the pool of items and included as checks. + """ + display_name = "Consumables" + option_none = 0 + option_1up_etank = 1 + option_weapon_health = 2 + option_all = 3 + default = 1 + alias_true = 3 + alias_false = 0 + + @classmethod + def get_option_name(cls, value: int) -> str: + if value == 1: + return "1-Ups/E-Tanks" + elif value == 2: + return "Weapon/Health Energy" + return super().get_option_name(value) + + +class PaletteShuffle(TextChoice): + """ + Change the color of Mega Man and the Robot Masters. + None: The palettes are unchanged. + Shuffled: Palette colors are shuffled amongst the robot masters. + Randomized: Random (usually good) palettes are generated for each robot master. + Singularity: one palette is generated and used for all robot masters. + Supports custom palettes using HTML named colors in the + following format: Mega Buster-Lavender|Violet;randomized + The first value is the character whose palette you'd like to define, then separated by - is a set of 2 colors for + that character. separate every color with a pipe, and separate every character as well as the remaining shuffle with + a semicolon. + """ + display_name = "Palette Shuffle" + option_none = 0 + option_shuffled = 1 + option_randomized = 2 + option_singularity = 3 + + +class EnemyWeaknesses(Toggle): + """ + Randomizes the damage dealt to enemies by weapons. Certain enemies will always take damage from the buster. + """ + display_name = "Random Enemy Weaknesses" + + +class StrictWeaknesses(Toggle): + """ + Only your starting Robot Master will take damage from the Mega Buster, the rest must be defeated with weapons. + Weapons that only do 1-3 damage to bosses no longer deal damage (aside from Wily/Gamma). + """ + display_name = "Strict Boss Weaknesses" + + +class RandomWeaknesses(Choice): + """ + None: Bosses will have their regular weaknesses. + Shuffled: Weapon damage will be shuffled amongst the weapons, so Shadow Blade may do Top Spin damage. + Randomized: Weapon damage will be fully randomized. + """ + display_name = "Random Boss Weaknesses" + option_none = 0 + option_shuffled = 1 + option_randomized = 2 + alias_false = 0 + alias_true = 2 + + +class Wily4Requirement(Range): + """ + Change the amount of Robot Masters that are required to be defeated for + the door to the Wily Machine to open. + """ + display_name = "Wily 4 Requirement" + default = 8 + range_start = 1 + range_end = 8 + + +class WeaknessPlando(OptionDict): + """ + Specify specific damage numbers for boss damage. Can be used even without strict/random weaknesses. + plando_weakness: + Robot Master: + Weapon: Damage + """ + display_name = "Plando Weaknesses" + schema = Schema({ + Optional(And(str, Use(str.title), lambda s: s in bosses)): { + And(str, Use(str.title), lambda s: s in weapons_to_id): And(int, lambda i: i in range(0, 14)) + } + }) + default = {} + + +class ReduceFlashing(Toggle): + """ + Reduce flashing seen in gameplay, such as in stages and when defeating certain bosses. + """ + display_name = "Reduce Flashing" + + +class MusicShuffle(Choice): + """ + Shuffle the music that plays in every stage + """ + display_name = "Music Shuffle" + option_none = 0 + option_shuffled = 1 + option_randomized = 2 + option_no_music = 3 + default = 0 + + +@dataclass +class MM3Options(PerGameCommonOptions): + death_link: DeathLink + energy_link: EnergyLink + starting_robot_master: StartingRobotMaster + consumables: Consumables + enemy_weakness: EnemyWeaknesses + strict_weakness: StrictWeaknesses + random_weakness: RandomWeaknesses + wily_4_requirement: Wily4Requirement + plando_weakness: WeaknessPlando + palette_shuffle: PaletteShuffle + reduce_flashing: ReduceFlashing + music_shuffle: MusicShuffle diff --git a/worlds/mm3/rom.py b/worlds/mm3/rom.py new file mode 100644 index 0000000000..8803f38cc5 --- /dev/null +++ b/worlds/mm3/rom.py @@ -0,0 +1,374 @@ +import pkgutil +from typing import TYPE_CHECKING, Iterable +import hashlib +import Utils +import os + +from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes +from . import names +from .rules import bosses + +from .text import MM3TextEntry +from .color import get_colors_for_item, write_palette_shuffle +from .options import Consumables + +if TYPE_CHECKING: + from . import MM3World + +MM3LCHASH = "5266687de215e790b2008284402f3917" +PROTEUSHASH = "b69fff40212b80c94f19e786d1efbf61" +MM3NESHASH = "4a53b6f58067d62c9a43404fe835dd5c" +MM3VCHASH = "c50008f1ac86fae8d083232cdd3001a5" + +enemy_weakness_ptrs: dict[int, int] = { + 0: 0x14100, + 1: 0x14200, + 2: 0x14300, + 3: 0x14400, + 4: 0x14500, + 5: 0x14600, + 6: 0x14700, + 7: 0x14800, + 8: 0x14900, +} + +enemy_addresses: dict[str, int] = { + "Dada": 0x12, + "Potton": 0x13, + "New Shotman": 0x15, + "Hammer Joe": 0x16, + "Peterchy": 0x17, + "Bubukan": 0x18, + "Vault Pole": 0x19, # Capcom..., why did you name an enemy Pole? + "Bomb Flier": 0x1A, + "Yambow": 0x1D, + "Metall 2": 0x1E, + "Cannon": 0x22, + "Jamacy": 0x25, + "Jamacy 2": 0x26, # dunno what this is, but I won't question + "Jamacy 3": 0x27, + "Jamacy 4": 0x28, # tf is this Capcom + "Mag Fly": 0x2A, + "Egg": 0x2D, + "Gyoraibo 2": 0x2E, + "Junk Golem": 0x2F, + "Pickelman Bull": 0x30, + "Nitron": 0x35, + "Pole": 0x37, + "Gyoraibo": 0x38, + "Hari Harry": 0x3A, + "Penpen Maker": 0x3B, + "Returning Monking": 0x3C, + "Have 'Su' Bee": 0x3E, + "Hive": 0x3F, + "Bolton-Nutton": 0x40, + "Walking Bomb": 0x44, + "Elec'n": 0x45, + "Mechakkero": 0x47, + "Chibee": 0x4B, + "Swimming Penpen": 0x4D, + "Top": 0x52, + "Penpen": 0x56, + "Komasaburo": 0x57, + "Parasyu": 0x59, + "Hologran (Static)": 0x5A, + "Hologran (Moving)": 0x5B, + "Bomber Pepe": 0x5C, + "Metall DX": 0x5D, + "Petit Snakey": 0x5E, + "Proto Man": 0x62, + "Break Man": 0x63, + "Metall": 0x7D, + "Giant Springer": 0x83, + "Springer Missile": 0x85, + "Giant Snakey": 0x99, + "Tama": 0x9A, + "Doc Robot (Flash)": 0xB0, + "Doc Robot (Wood)": 0xB1, + "Doc Robot (Crash)": 0xB2, + "Doc Robot (Metal)": 0xB3, + "Doc Robot (Bubble)": 0xC0, + "Doc Robot (Heat)": 0xC1, + "Doc Robot (Quick)": 0xC2, + "Doc Robot (Air)": 0xC3, + "Snake": 0xCA, + "Needle Man": 0xD0, + "Magnet Man": 0xD1, + "Top Man": 0xD2, + "Shadow Man": 0xD3, + "Top Man's Top": 0xD5, + "Shadow Man (Sliding)": 0xD8, # Capcom I swear + "Hard Man": 0xE0, + "Spark Man": 0xE2, + "Snake Man": 0xE4, + "Gemini Man": 0xE6, + "Gemini Man (Clone)": 0xE7, # Capcom why + "Yellow Devil MK-II": 0xF1, + "Wily Machine 3": 0xF3, + "Gamma": 0xF8, + "Kamegoro": 0x101, + "Kamegoro Shell": 0x102, + "Holograph Mega Man": 0x105, + "Giant Metall": 0x10C, # This is technically FC but we're +16 from the rom header +} + +# addresses printed when assembling basepatch +wily_4_ptr: int = 0x7F570 +consumables_ptr: int = 0x7FDEA +energylink_ptr: int = 0x7FDF9 + + +class MM3ProcedurePatch(APProcedurePatch, APTokenMixin): + hash = [MM3LCHASH, MM3NESHASH, MM3VCHASH] + game = "Mega Man 3" + patch_file_ending = ".apmm3" + result_file_ending = ".nes" + name: bytearray + procedure = [ + ("apply_bsdiff4", ["mm3_basepatch.bsdiff4"]), + ("apply_tokens", ["token_patch.bin"]), + ] + + @classmethod + def get_source_data(cls) -> bytes: + return get_base_rom_bytes() + + def write_byte(self, offset: int, value: int) -> None: + self.write_token(APTokenTypes.WRITE, offset, value.to_bytes(1, "little")) + + def write_bytes(self, offset: int, value: Iterable[int]) -> None: + self.write_token(APTokenTypes.WRITE, offset, bytes(value)) + + +def patch_rom(world: "MM3World", patch: MM3ProcedurePatch) -> None: + patch.write_file("mm3_basepatch.bsdiff4", pkgutil.get_data(__name__, os.path.join("data", "mm3_basepatch.bsdiff4"))) + # text writing + + base_address = 0x3C000 + color_address = 0x31BC7 + for i, offset, location in zip([0, 8, 1, 2, + 3, 4, 5, 6, + 7, 9], + [0x10, 0x50, 0x91, 0xD2, + 0x113, 0x154, 0x195, 0x1D6, + 0x217, 0x257], + [ + names.get_needle_cannon, + names.get_rush_jet, + names.get_magnet_missile, + names.get_gemini_laser, + names.get_hard_knuckle, + names.get_top_spin, + names.get_search_snake, + names.get_spark_shock, + names.get_shadow_blade, + names.get_rush_marine, + ]): + item = world.get_location(location).item + if item: + if len(item.name) <= 13: + # we want to just place it in the center + first_str = "" + second_str = item.name + third_str = "" + elif len(item.name) <= 26: + # spread across second and third + first_str = "" + second_str = item.name[:13] + third_str = item.name[13:] + else: + # all three + first_str = item.name[:13] + second_str = item.name[13:26] + third_str = item.name[26:] + if len(third_str) > 13: + third_str = third_str[:13] + player_str = world.multiworld.get_player_name(item.player) + if len(player_str) > 13: + player_str = player_str[:13] + y_coords = 0xA5 + row = 0x21 + if location in [names.get_rush_marine, names.get_rush_jet]: + y_coords = 0x45 + row = 0x22 + patch.write_bytes(base_address + offset, MM3TextEntry(first_str, y_coords, row).resolve()) + patch.write_bytes(base_address + 16 + offset, MM3TextEntry(second_str, y_coords + 0x20, row).resolve()) + patch.write_bytes(base_address + 32 + offset, MM3TextEntry(third_str, y_coords + 0x40, row).resolve()) + if y_coords + 0x60 > 0xFF: + row += 1 + y_coords = 0x01 + patch.write_bytes(base_address + 48 + offset, MM3TextEntry(player_str, y_coords, row).resolve()) + colors_high, colors_low = get_colors_for_item(item.name) + patch.write_bytes(color_address + (i * 8) + 1, colors_high) + patch.write_bytes(color_address + (i * 8) + 5, colors_low) + else: + patch.write_bytes(base_address + 48 + offset, MM3TextEntry(player_str, y_coords + 0x60, row).resolve()) + + write_palette_shuffle(world, patch) + + enemy_weaknesses: dict[str, dict[int, int]] = {} + + if world.options.strict_weakness or world.options.random_weakness or world.options.plando_weakness: + # we need to write boss weaknesses + for boss in bosses: + if boss == "Kamegoro Maker": + enemy_weaknesses["Kamegoro"] = {i: world.weapon_damage[i][bosses[boss]] for i in world.weapon_damage} + enemy_weaknesses["Kamegoro Shell"] = {i: world.weapon_damage[i][bosses[boss]] + for i in world.weapon_damage} + elif boss == "Gemini Man": + enemy_weaknesses[boss] = {i: world.weapon_damage[i][bosses[boss]] for i in world.weapon_damage} + enemy_weaknesses["Gemini Man (Clone)"] = {i: world.weapon_damage[i][bosses[boss]] + for i in world.weapon_damage} + elif boss == "Shadow Man": + enemy_weaknesses[boss] = {i: world.weapon_damage[i][bosses[boss]] for i in world.weapon_damage} + enemy_weaknesses["Shadow Man (Sliding)"] = {i: world.weapon_damage[i][bosses[boss]] + for i in world.weapon_damage} + else: + enemy_weaknesses[boss] = {i: world.weapon_damage[i][bosses[boss]] for i in world.weapon_damage} + + if world.options.enemy_weakness: + for enemy in enemy_addresses: + if enemy in [*bosses.keys(), "Kamegoro", "Kamegoro Shell", "Gemini Man (Clone)", "Shadow Man (Sliding)"]: + continue + enemy_weaknesses[enemy] = {weapon: world.random.randint(-4, 4) for weapon in enemy_weakness_ptrs} + if enemy in ["Tama", "Giant Snakey", "Proto Man", "Giant Metall"] and enemy_weaknesses[enemy][0] <= 0: + enemy_weaknesses[enemy][0] = 1 + elif enemy == "Jamacy 2": + # bruh + if not enemy_weaknesses[enemy][8] > 0: + enemy_weaknesses[enemy][8] = 1 + if not enemy_weaknesses[enemy][3] > 0: + enemy_weaknesses[enemy][3] = 1 + + for enemy, damage in enemy_weaknesses.items(): + for weapon in enemy_weakness_ptrs: + if damage[weapon] < 0: + damage[weapon] = 256 + damage[weapon] + patch.write_byte(enemy_weakness_ptrs[weapon] + enemy_addresses[enemy], damage[weapon]) + + if world.options.consumables != Consumables.option_all: + value_a = 0x64 + value_b = 0x6A + if world.options.consumables in (Consumables.option_none, Consumables.option_1up_etank): + value_a = 0x68 + if world.options.consumables in (Consumables.option_none, Consumables.option_weapon_health): + value_b = 0x67 + patch.write_byte(consumables_ptr - 3, value_a) + patch.write_byte(consumables_ptr + 1, value_b) + + patch.write_byte(wily_4_ptr + 1, world.options.wily_4_requirement.value) + + patch.write_byte(energylink_ptr + 1, world.options.energy_link.value) + + if world.options.reduce_flashing: + # Spark Man + patch.write_byte(0x12649, 8) + patch.write_byte(0x1264E, 8) + patch.write_byte(0x12653, 8) + # Shadow Man + patch.write_byte(0x12658, 0x10) + # Gemini Man + patch.write_byte(0x12637, 0x20) + patch.write_byte(0x1263D, 0x20) + patch.write_byte(0x12643, 0x20) + # Gamma + patch.write_byte(0x7DA4A, 0xF) + + if world.options.music_shuffle: + if world.options.music_shuffle.current_key == "no_music": + pool = [0xF0] * 18 + elif world.options.music_shuffle.current_key == "randomized": + pool = world.random.choices(range(1, 0xC), k=18) + else: + pool = [1, 2, 3, 4, 5, 6, 7, 8, 1, 3, 7, 8, 9, 9, 10, 10, 11, 11] + world.random.shuffle(pool) + patch.write_bytes(0x7CD1C, pool) + + from Utils import __version__ + patch.name = bytearray(f'MM3{__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0', + 'utf8')[:21] + patch.name.extend([0] * (21 - len(patch.name))) + patch.write_bytes(0x3F330, patch.name) # We changed this section, but this pointer is still valid! + deathlink_byte = world.options.death_link.value | (world.options.energy_link.value << 1) + patch.write_byte(0x3F346, deathlink_byte) + + patch.write_bytes(0x3F34C, world.world_version) + + version_map = { + "0": 0x00, + "1": 0x01, + "2": 0x02, + "3": 0x03, + "4": 0x04, + "5": 0x05, + "6": 0x06, + "7": 0x07, + "8": 0x08, + "9": 0x09, + ".": 0x26 + } + patch.write_token(APTokenTypes.RLE, 0x653B, (11, 0x25)) + patch.write_token(APTokenTypes.RLE, 0x6549, (25, 0x25)) + + # BY SILVRIS + patch.write_bytes(0x653B, [0x0B, 0x22, 0x25, 0x1C, 0x12, 0x15, 0x1F, 0x1B, 0x12, 0x1C]) + # ARCHIPELAGO x.x.x + patch.write_bytes(0x654D, + [0x0A, 0x1B, 0x0C, 0x11, 0x12, 0x19, 0x0E, 0x15, 0x0A, 0x10, 0x18]) + patch.write_bytes(0x6559, list(map(lambda c: version_map[c], __version__))) + + patch.write_file("token_patch.bin", patch.get_token_binary()) + + +header = b"\x4E\x45\x53\x1A\x10\x10\x40\x00\x00\x00\x00\x00\x00\x00\x00\x00" + + +def read_headerless_nes_rom(rom: bytes) -> bytes: + if rom[:4] == b"NES\x1A": + return rom[16:] + else: + return rom + + +def get_base_rom_bytes(file_name: str = "") -> bytes: + base_rom_bytes: bytes | None = getattr(get_base_rom_bytes, "base_rom_bytes", None) + if not base_rom_bytes: + file_name = get_base_rom_path(file_name) + base_rom_bytes = read_headerless_nes_rom(bytes(open(file_name, "rb").read())) + + basemd5 = hashlib.md5() + basemd5.update(base_rom_bytes) + if basemd5.hexdigest() == PROTEUSHASH: + base_rom_bytes = extract_mm3(base_rom_bytes) + basemd5 = hashlib.md5() + basemd5.update(base_rom_bytes) + if basemd5.hexdigest() not in {MM3LCHASH, MM3NESHASH, MM3VCHASH}: + print(basemd5.hexdigest()) + raise Exception("Supplied Base Rom does not match known MD5 for US, LC, or US VC release. " + "Get the correct game and version, then dump it") + headered_rom = bytearray(base_rom_bytes) + headered_rom[0:0] = header + setattr(get_base_rom_bytes, "base_rom_bytes", bytes(headered_rom)) + return bytes(headered_rom) + return base_rom_bytes + + +def get_base_rom_path(file_name: str = "") -> str: + from . import MM3World + if not file_name: + file_name = MM3World.settings.rom_file + if not os.path.exists(file_name): + file_name = Utils.user_path(file_name) + return file_name + + +prg_offset = 0xCF1B0 +prg_size = 0x40000 +chr_offset = 0x10F1B0 +chr_size = 0x20000 + + +def extract_mm3(proteus: bytes) -> bytes: + mm3 = bytearray(proteus[prg_offset:prg_offset + prg_size]) + mm3.extend(proteus[chr_offset:chr_offset + chr_size]) + return bytes(mm3) diff --git a/worlds/mm3/rules.py b/worlds/mm3/rules.py new file mode 100644 index 0000000000..b43908f42c --- /dev/null +++ b/worlds/mm3/rules.py @@ -0,0 +1,388 @@ +from math import ceil +from typing import TYPE_CHECKING +from . import names +from .locations import get_boss_locations, get_oneup_locations, get_energy_locations +from worlds.generic.Rules import add_rule + +if TYPE_CHECKING: + from . import MM3World + from BaseClasses import CollectionState + +bosses: dict[str, int] = { + "Needle Man": 0, + "Magnet Man": 1, + "Gemini Man": 2, + "Hard Man": 3, + "Top Man": 4, + "Snake Man": 5, + "Spark Man": 6, + "Shadow Man": 7, + "Doc Robot (Metal)": 8, + "Doc Robot (Quick)": 9, + "Doc Robot (Air)": 10, + "Doc Robot (Crash)": 11, + "Doc Robot (Flash)": 12, + "Doc Robot (Bubble)": 13, + "Doc Robot (Wood)": 14, + "Doc Robot (Heat)": 15, + "Break Man": 16, + "Kamegoro Maker": 17, + "Yellow Devil MK-II": 18, + "Holograph Mega Man": 19, + "Wily Machine 3": 20, + "Gamma": 21 +} + +weapons_to_id: dict[str, int] = { + "Mega Buster": 0, + "Needle Cannon": 1, + "Magnet Missile": 2, + "Gemini Laser": 3, + "Hard Knuckle": 4, + "Top Spin": 5, + "Search Snake": 6, + "Spark Shot": 7, + "Shadow Blade": 8, +} + +weapon_damage: dict[int, list[int]] = { + 0: [1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 3, 1, 1, 1, 0, ], # Mega Buster + 1: [4, 1, 1, 0, 2, 4, 2, 1, 0, 1, 1, 2, 4, 2, 4, 2, 0, 3, 1, 1, 1, 0, ], # Needle Cannon + 2: [1, 4, 2, 4, 1, 0, 0, 1, 4, 2, 4, 1, 1, 0, 0, 1, 0, 3, 1, 0, 1, 0, ], # Magnet Missile + 3: [7, 2, 4, 1, 0, 1, 1, 1, 1, 4, 2, 0, 4, 1, 1, 1, 0, 3, 1, 1, 1, 0, ], # Gemini Laser + 4: [0, 2, 2, 4, 7, 2, 2, 2, 4, 1, 2, 7, 0, 2, 2, 2, 0, 1, 5, 4, 7, 4, ], # Hard Knuckle + 5: [1, 1, 2, 0, 4, 2, 1, 7, 0, 1, 1, 4, 1, 1, 2, 7, 0, 1, 0, 7, 0, 2, ], # Top Spin + 6: [1, 1, 5, 0, 1, 4, 0, 1, 0, 4, 1, 1, 1, 0, 4, 1, 0, 1, 0, 7, 4, 2, ], # Search Snake + 7: [0, 7, 1, 0, 1, 1, 4, 1, 2, 1, 4, 1, 0, 4, 1, 1, 0, 0, 0, 0, 7, 0, ], # Spark Shot + 8: [2, 7, 2, 0, 1, 2, 4, 4, 2, 2, 0, 1, 2, 4, 2, 4, 0, 1, 3, 2, 2, 2, ], # Shadow Blade +} + +weapons_to_name: dict[int, str] = { + 1: names.needle_cannon, + 2: names.magnet_missile, + 3: names.gemini_laser, + 4: names.hard_knuckle, + 5: names.top_spin, + 6: names.search_snake, + 7: names.spark_shock, + 8: names.shadow_blade +} + +minimum_weakness_requirement: dict[int, int] = { + 0: 1, # Mega Buster is free + 1: 1, # 112 shots of Needle Cannon + 2: 2, # 14 shots of Magnet Missile + 3: 2, # 14 shots of Gemini Laser + 4: 2, # 14 uses of Hard Knuckle + 5: 4, # an unknown amount of Top Spin (4 means you should be able to be fine) + 6: 1, # 56 uses of Search Snake + 7: 2, # 14 functional uses of Spark Shot (fires in twos) + 8: 1, # 56 uses of Shadow Blade +} + +robot_masters: dict[int, str] = { + 0: "Needle Man Defeated", + 1: "Magnet Man Defeated", + 2: "Gemini Man Defeated", + 3: "Hard Man Defeated", + 4: "Top Man Defeated", + 5: "Snake Man Defeated", + 6: "Spark Man Defeated", + 7: "Shadow Man Defeated" +} + +weapon_costs = { + 0: 0, + 1: 0.25, + 2: 2, + 3: 2, + 4: 2, + 5: 7, # Not really, but we can really only rely on Top for one RBM + 6: 0.5, + 7: 2, + 8: 0.5, +} + + +def can_defeat_enough_rbms(state: "CollectionState", player: int, + required: int, boss_requirements: dict[int, list[int]]) -> bool: + can_defeat = 0 + for boss, reqs in boss_requirements.items(): + if boss in robot_masters: + if state.has_all(map(lambda x: weapons_to_name[x], reqs), player): + can_defeat += 1 + if can_defeat >= required: + return True + return False + + +def has_rush_vertical(state: "CollectionState", player: int) -> bool: + return state.has_any([names.rush_coil, names.rush_jet], player) + + +def can_traverse_long_water(state: "CollectionState", player: int) -> bool: + return state.has_any([names.rush_marine, names.rush_jet], player) + + +def has_any_rush(state: "CollectionState", player: int) -> bool: + return state.has_any([names.rush_coil, names.rush_jet, names.rush_marine], player) + + +def has_rush_jet(state: "CollectionState", player: int) -> bool: + return state.has(names.rush_jet, player) + + +def set_rules(world: "MM3World") -> None: + # most rules are set on region, so we only worry about rules required within stage access + # or rules variable on settings + if hasattr(world.multiworld, "re_gen_passthrough"): + slot_data = getattr(world.multiworld, "re_gen_passthrough")["Mega Man 3"] + world.weapon_damage = slot_data["weapon_damage"] + else: + if world.options.random_weakness == world.options.random_weakness.option_shuffled: + weapon_tables = [table.copy() for weapon, table in weapon_damage.items() if weapon != 0] + world.random.shuffle(weapon_tables) + for i in range(1, 9): + world.weapon_damage[i] = weapon_tables.pop() + elif world.options.random_weakness == world.options.random_weakness.option_randomized: + world.weapon_damage = {i: [] for i in range(9)} + for boss in range(22): + for weapon in world.weapon_damage: + world.weapon_damage[weapon].append(min(14, max(0, int(world.random.normalvariate(3, 3))))) + if not any([world.weapon_damage[weapon][boss] >= 4 + for weapon in range(1, 9)]): + # failsafe, there should be at least one defined non-Buster weakness + weapon = world.random.randint(1, 7) + world.weapon_damage[weapon][boss] = world.random.randint(4, 14) # Force weakness + # handle Break Man + boss = 16 + for weapon in world.weapon_damage: + world.weapon_damage[weapon][boss] = 0 + weapon = world.random.choice(list(world.weapon_damage.keys())) + world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon] + + if world.options.strict_weakness: + for weapon in weapon_damage: + for i in range(22): + if i == 16: + continue # Break is only weak to buster on non-random, and minimal damage on random + elif weapon == 0: + world.weapon_damage[weapon][i] = 0 + elif i in (20, 21) and not world.options.random_weakness: + continue + # Gamma and Wily Machine need all weaknesses present, so allow + elif not world.options.random_weakness == world.options.random_weakness.option_randomized \ + and i == 17: + if 3 > world.weapon_damage[weapon][i] > 0: + # Kamegoros take 3 max from weapons on non-random + world.weapon_damage[weapon][i] = 0 + elif 4 > world.weapon_damage[weapon][i] > 0: + world.weapon_damage[weapon][i] = 0 + + for p_boss in world.options.plando_weakness: + for p_weapon in world.options.plando_weakness[p_boss]: + if not any(w for w in world.weapon_damage + if w != weapons_to_id[p_weapon] + and world.weapon_damage[w][bosses[p_boss]] > minimum_weakness_requirement[w]): + # we need to replace this weakness + weakness = world.random.choice([key for key in world.weapon_damage + if key != weapons_to_id[p_weapon]]) + world.weapon_damage[weakness][bosses[p_boss]] = minimum_weakness_requirement[weakness] + world.weapon_damage[weapons_to_id[p_weapon]][bosses[p_boss]] \ + = world.options.plando_weakness[p_boss][p_weapon] + + # handle special cases + for boss in range(22): + for weapon in range(1, 9): + if (0 < world.weapon_damage[weapon][boss] < minimum_weakness_requirement[weapon] and + not any(world.weapon_damage[i][boss] >= minimum_weakness_requirement[weapon] + for i in range(1, 8) if i != weapon)): + world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon] + + if world.weapon_damage[0][world.options.starting_robot_master.value] < 1: + world.weapon_damage[0][world.options.starting_robot_master.value] = 1 + + # weakness validation, it is better to confirm a completable seed than respect plando + boss_health = {boss: 0x1C for boss in range(8)} + + weapon_energy = {key: float(0x1C) for key in weapon_costs} + weapon_boss = {boss: {weapon: world.weapon_damage[weapon][boss] for weapon in world.weapon_damage} + for boss in range(8)} + flexibility = { + boss: ( + sum(damage_value > 0 for damage_value in + weapon_damages.values()) # Amount of weapons that hit this boss + * sum(weapon_damages.values()) # Overall damage that those weapons do + ) + for boss, weapon_damages in weapon_boss.items() + } + boss_flexibility = sorted(flexibility, key=flexibility.get) # Fast way to sort dict by value + used_weapons: dict[int, set[int]] = {i: set() for i in range(8)} + for boss in boss_flexibility: + boss_damage = weapon_boss[boss] + weapon_weight = {weapon: (weapon_energy[weapon] / damage) if damage else 0 for weapon, damage in + boss_damage.items() if weapon_energy[weapon] > 0} + while boss_health[boss] > 0: + if boss_damage[0] > 0: + boss_health[boss] = 0 # if we can buster, we should buster + continue + highest, wp = max(zip(weapon_weight.values(), weapon_weight.keys())) + uses = weapon_energy[wp] // weapon_costs[wp] + if int(uses * boss_damage[wp]) >= boss_health[boss]: + used = ceil(boss_health[boss] / boss_damage[wp]) + weapon_energy[wp] -= weapon_costs[wp] * used + boss_health[boss] = 0 + used_weapons[boss].add(wp) + elif highest <= 0: + # we are out of weapons that can actually damage the boss + # so find the weapon that has the most uses, and apply that as an additional weakness + # it should be impossible to be out of energy + max_uses, wp = max((weapon_energy[weapon] // weapon_costs[weapon], weapon) + for weapon in weapon_weight + if weapon != 0) + world.weapon_damage[wp][boss] = minimum_weakness_requirement[wp] + used = min(int(weapon_energy[wp] // weapon_costs[wp]), + ceil(boss_health[boss] / minimum_weakness_requirement[wp])) + weapon_energy[wp] -= weapon_costs[wp] * used + boss_health[boss] -= int(used * minimum_weakness_requirement[wp]) + weapon_weight.pop(wp) + used_weapons[boss].add(wp) + else: + # drain the weapon and continue + boss_health[boss] -= int(uses * boss_damage[wp]) + weapon_energy[wp] -= weapon_costs[wp] * uses + weapon_weight.pop(wp) + used_weapons[boss].add(wp) + + world.wily_4_weapons = {boss: sorted(weapons) for boss, weapons in used_weapons.items()} + + for i, boss_locations in zip(range(22), [ + get_boss_locations("Needle Man Stage"), + get_boss_locations("Magnet Man Stage"), + get_boss_locations("Gemini Man Stage"), + get_boss_locations("Hard Man Stage"), + get_boss_locations("Top Man Stage"), + get_boss_locations("Snake Man Stage"), + get_boss_locations("Spark Man Stage"), + get_boss_locations("Shadow Man Stage"), + get_boss_locations("Doc Robot (Spark) - Metal"), + get_boss_locations("Doc Robot (Spark) - Quick"), + get_boss_locations("Doc Robot (Needle) - Air"), + get_boss_locations("Doc Robot (Needle) - Crash"), + get_boss_locations("Doc Robot (Gemini) - Flash"), + get_boss_locations("Doc Robot (Gemini) - Bubble"), + get_boss_locations("Doc Robot (Shadow) - Wood"), + get_boss_locations("Doc Robot (Shadow) - Heat"), + get_boss_locations("Break Man"), + get_boss_locations("Wily Stage 1"), + get_boss_locations("Wily Stage 2"), + get_boss_locations("Wily Stage 3"), + get_boss_locations("Wily Stage 5"), + get_boss_locations("Wily Stage 6") + ]): + if world.weapon_damage[0][i] > 0: + continue # this can always be in logic + weapons = [] + for weapon in range(1, 9): + if world.weapon_damage[weapon][i] > 0: + if world.weapon_damage[weapon][i] < minimum_weakness_requirement[weapon]: + continue + weapons.append(weapons_to_name[weapon]) + if not weapons: + raise Exception(f"Attempted to have boss {i} with no weakness! Seed: {world.multiworld.seed}") + for location in boss_locations: + if i in (20, 21): + # multi-phase fights, get all potential weaknesses + # we should probably do this smarter, but this works for now + add_rule(world.get_location(location), + lambda state, weps=tuple(weapons): state.has_all(weps, world.player)) + else: + add_rule(world.get_location(location), + lambda state, weps=tuple(weapons): state.has_any(weps, world.player)) + + # Need to defeat x amount of robot masters for Wily 4 + add_rule(world.get_location(names.wily_stage_4), + lambda state: can_defeat_enough_rbms(state, world.player, world.options.wily_4_requirement.value, + world.wily_4_weapons)) + + # Handle Doc Robo stage connections + for entrance, location in (("To Doc Robot (Needle) - Crash", names.doc_air), + ("To Doc Robot (Gemini) - Bubble", names.doc_flash), + ("To Doc Robot (Shadow) - Heat", names.doc_wood), + ("To Doc Robot (Spark) - Quick", names.doc_metal)): + entrance_object = world.get_entrance(entrance) + add_rule(entrance_object, lambda state, loc=location: state.can_reach(loc, "Location", world.player)) + + # finally, real logic + for location in get_boss_locations("Hard Man Stage"): + add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player)) + + for location in get_boss_locations("Gemini Man Stage"): + add_rule(world.get_location(location), lambda state: has_any_rush(state, world.player)) + + add_rule(world.get_entrance("To Doc Robot (Spark) - Metal"), + lambda state: has_rush_vertical(state, world.player) and + state.has_any([names.shadow_blade, names.gemini_laser], world.player)) + add_rule(world.get_entrance("To Doc Robot (Needle) - Air"), + lambda state: has_rush_vertical(state, world.player)) + add_rule(world.get_entrance("To Doc Robot (Needle) - Crash"), + lambda state: has_rush_jet(state, world.player)) + add_rule(world.get_entrance("To Doc Robot (Gemini) - Bubble"), + lambda state: has_rush_vertical(state, world.player) and can_traverse_long_water(state, world.player)) + + for location in get_boss_locations("Wily Stage 1"): + add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player)) + + for location in get_boss_locations("Wily Stage 2"): + add_rule(world.get_location(location), lambda state: has_rush_jet(state, world.player)) + + # Wily 3 technically needs vertical + # However, Wily 3 requires beating Wily 2, and Wily 2 explicitly needs Jet + # So we can skip the additional rule on Wily 3 + + if world.options.consumables in (world.options.consumables.option_1up_etank, + world.options.consumables.option_all): + add_rule(world.get_location(names.needle_man_c2), lambda state: has_rush_jet(state, world.player)) + add_rule(world.get_location(names.gemini_man_c1), lambda state: has_rush_jet(state, world.player)) + add_rule(world.get_location(names.gemini_man_c3), + lambda state: has_rush_vertical(state, world.player) + or state.has_any([names.gemini_laser, names.shadow_blade], world.player)) + for location in (names.gemini_man_c6, names.gemini_man_c7, names.gemini_man_c10): + add_rule(world.get_location(location), lambda state: has_any_rush(state, world.player)) + for location in get_oneup_locations("Hard Man Stage"): + add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player)) + add_rule(world.get_location(names.top_man_c6), lambda state: has_rush_vertical(state, world.player)) + add_rule(world.get_location(names.doc_needle_c2), lambda state: has_rush_jet(state, world.player)) + add_rule(world.get_location(names.doc_needle_c3), lambda state: has_rush_jet(state, world.player)) + add_rule(world.get_location(names.doc_gemini_c1), lambda state: has_rush_vertical(state, world.player)) + add_rule(world.get_location(names.doc_gemini_c2), lambda state: has_rush_vertical(state, world.player)) + add_rule(world.get_location(names.wily_1_c8), lambda state: has_rush_vertical(state, world.player)) + for location in [names.wily_1_c4, names.wily_1_c8]: + add_rule(world.get_location(location), lambda state: state.has(names.hard_knuckle, world.player)) + for location in get_oneup_locations("Wily Stage 2"): + if location == names.wily_2_c3: + continue + add_rule(world.get_location(location), lambda state: has_rush_jet(state, world.player)) + if world.options.consumables in (world.options.consumables.option_weapon_health, + world.options.consumables.option_all): + add_rule(world.get_location(names.gemini_man_c2), lambda state: has_rush_vertical(state, world.player)) + add_rule(world.get_location(names.gemini_man_c4), lambda state: has_rush_vertical(state, world.player)) + add_rule(world.get_location(names.gemini_man_c5), lambda state: has_rush_vertical(state, world.player)) + for location in (names.gemini_man_c8, names.gemini_man_c9): + add_rule(world.get_location(location), lambda state: has_any_rush(state, world.player)) + for location in get_energy_locations("Hard Man Stage"): + if location == names.hard_man_c1: + continue + add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player)) + for location in (names.spark_man_c1, names.spark_man_c2): + add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player)) + for location in [names.top_man_c2, names.top_man_c3, names.top_man_c4, names.top_man_c7]: + add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player)) + for location in [names.wily_1_c5, names.wily_1_c6, names.wily_1_c7]: + add_rule(world.get_location(location), lambda state: state.has(names.hard_knuckle, world.player)) + for location in [names.wily_1_c6, names.wily_1_c7, names.wily_1_c11, names.wily_1_c12]: + add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player)) + for location in get_energy_locations("Wily Stage 2"): + if location in (names.wily_2_c1, names.wily_2_c2, names.wily_2_c4): + continue + add_rule(world.get_location(location), lambda state: has_rush_jet(state, world.player)) diff --git a/worlds/mm3/src/__init__.py b/worlds/mm3/src/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/worlds/mm3/src/mm3_basepatch.asm b/worlds/mm3/src/mm3_basepatch.asm new file mode 100644 index 0000000000..16e0567ff5 --- /dev/null +++ b/worlds/mm3/src/mm3_basepatch.asm @@ -0,0 +1,781 @@ +norom +!headersize = 16 + +!controller_flip = $14 ; only on first frame of input, used by crash man, etc +!controller_mirror = $16 +!current_stage = $22 +!current_state = $60 +!completed_rbm_stages = $61 +!completed_doc_stages = $62 +!current_wily = $75 +!received_rbm_stages = $680 +!received_doc_stages = $681 +; !deathlink = $30, set to $0E +!energylink_packet = $682 +!last_wily = $683 +!rbm_strobe = $684 +!sound_effect_strobe = $685 +!doc_robo_kills = $686 +!wily_stage_completion = $687 +;!received_items = $688 +!acquired_rush = $689 + +!current_weapon = $A0 +!current_health = $A2 +!received_weapons = $A3 + +'0' = $00 +'1' = $01 +'2' = $02 +'3' = $03 +'4' = $04 +'5' = $05 +'6' = $06 +'7' = $07 +'8' = $08 +'9' = $09 +'A' = $0A +'B' = $0B +'C' = $0C +'D' = $0D +'E' = $0E +'F' = $0F +'G' = $10 +'H' = $11 +'I' = $12 +'J' = $13 +'K' = $14 +'L' = $15 +'M' = $16 +'N' = $17 +'O' = $18 +'P' = $19 +'Q' = $1A +'R' = $1B +'S' = $1C +'T' = $1D +'U' = $1E +'V' = $1F +'W' = $20 +'X' = $21 +'Y' = $22 +'Z' = $23 +' ' = $25 +'.' = $26 +',' = $27 +'!' = $29 +'r' = $2A +':' = $2B + +; !consumable_checks = $0F80 ; have to find in-stage solutions for this, there's literally not enough ram + +!CONTROLLER_SELECT = #$20 +!CONTROLLER_SELECT_START = #$30 +!CONTROLLER_ALL_BUTTON = #$F0 + +!PpuControl_2000 = $2000 +!PpuMask_2001 = $2001 +!PpuAddr_2006 = $2006 +!PpuData_2007 = $2007 + +;!LOAD_BANK = $C000 + +macro org(address,bank) + if == $3E + org
-$C000+($2000*)+!headersize ; org sets the position in the output file to write to (in norom, at least) + base
; base sets the position that all labels are relative to - this is necessary so labels will still start from $8000, instead of $0000 or somewhere + else + if == $3F + org
-$E000+($2000*)+!headersize ; org sets the position in the output file to write to (in norom, at least) + base
; base sets the position that all labels are relative to - this is necessary so labels will still start from $8000, instead of $0000 or somewhere + else + if
>= $A000 + org
-$A000+($2000*)+!headersize + base
+ else + org
-$8000+($2000*)+!headersize + base
+ endif + endif + endif +endmacro + +; capcom..... +; i can't keep defending you like this + +;P +%org($BEBA, $13) +RemoveP: +db $25 +;A +%org($BD7D, $13) +RemoveA: +db $25 +;S +%org($BE7D, $13) +RemoveS1: +db $25 +;S +%org($BDD5, $13) +RemoveS2: +db $25 + +;W +%org($BDC7, $13) +RemoveW: +db $25 +;O +%org($BEC7, $13) +RemoveO: +db $25 +;R +%org($BDCF, $13) +RemoveR: +db $25 +;D +%org($BECF, $13) +RemoveD: +db $25 + +%org($A17C, $02) +AdjustWeaponRefill: + ; compare vs unreceived instead. Since the stage ends anyways, this just means you aren't granted the weapon if you don't have it already + CMP #$1C + BCS WeaponRefillJump + +%org($A18B, $02) +WeaponRefillJump: + ; just as a branch target + +%org($A3BF, $02) +FixPseudoSnake: + JMP CheckFirstWep + NOP + +%org($A3CB, $02) +FixPseudoRush: + JMP CheckRushWeapon + NOP + +%org($BF80, $02) +CheckRushWeapon: + AND #$01 + BNE .Rush + JMP $A3CF + .Rush: + LDA $A1 + CLC + ADC $B4 + TAY + LDA $00A2, Y + BNE .Skip + DEC $A1 + .Skip: + JMP $A477 + +; don't even try to go past this point + +%org($802F, $0B) +HookBreakMan: + JSR SetBreakMan + NOP + +%org($90BC, $18) +BlockPassword: + AND #$08 ; originally 0C, just block down inputs + +%org($9258, $18) +HookStageSelect: + JSR ChangeStageMode + NOP + +%org($92F2, $18) +AccessStageTarget: + +%org($9316, $18) +AccessStage: + JSR RewireDocRobotAccess + NOP #2 + BEQ AccessStageTarget + +%org($9468, $18) +HookWeaponGet: + JSR WeaponReceived + NOP #4 + +%org($9917, $18) +GameOverStageSelect: + ; fix it returning to Wily 1 + CMP #$16 + +%org($9966, $18) +SwapSelectTiles: + ; swaps when stage select face tiles should be shown + JMP InvertSelectTiles + NOP + +%org($9A54, $18) +SwapSelectSprites: + JMP InvertSelectSprites + NOP + +%org($9AFF, $18) +BreakManSelect: + JSR ApplyLastWily + NOP + +%org($BE22, $1D) +ConsumableHook: + JMP CheckConsumable + +%org($BE32, $1D) +EnergyLinkHook: + JSR EnergyLink + +%org($A000, $1E) +db $21, $A5, $0C, "PLACEHOLDER 1" +db $21, $C5, $0C, "PLACEHOLDER 2" +db $21, $E5, $0C, "PLACEHOLDER 3" +db $22, $05, $0C, "PLACEHOLDER P" +db $22, $45, $0C, "PLACEHOLDER 1" +db $22, $65, $0C, "PLACEHOLDER 2" +db $22, $85, $0C, "PLACEHOLDER 3" +db $22, $A5, $0C, "PLACEHOLDER P", $FF +db $21, $A5, $0C, "PLACEHOLDER 1" +db $21, $C5, $0C, "PLACEHOLDER 2" +db $21, $E5, $0C, "PLACEHOLDER 3" +db $22, $05, $0C, "PLACEHOLDER P", $FF +db $21, $A5, $0C, "PLACEHOLDER 1" +db $21, $C5, $0C, "PLACEHOLDER 2" +db $21, $E5, $0C, "PLACEHOLDER 3" +db $22, $05, $0C, "PLACEHOLDER P", $FF +db $21, $A5, $0C, "PLACEHOLDER 1" +db $21, $C5, $0C, "PLACEHOLDER 2" +db $21, $E5, $0C, "PLACEHOLDER 3" +db $22, $05, $0C, "PLACEHOLDER P", $FF +db $21, $A5, $0C, "PLACEHOLDER 1" +db $21, $C5, $0C, "PLACEHOLDER 2" +db $21, $E5, $0C, "PLACEHOLDER 3" +db $22, $05, $0C, "PLACEHOLDER P", $FF +db $21, $A5, $0C, "PLACEHOLDER 1" +db $21, $C5, $0C, "PLACEHOLDER 2" +db $21, $E5, $0C, "PLACEHOLDER 3" +db $22, $05, $0C, "PLACEHOLDER P", $FF +db $21, $A5, $0C, "PLACEHOLDER 1" +db $21, $C5, $0C, "PLACEHOLDER 2" +db $21, $E5, $0C, "PLACEHOLDER 3" +db $22, $05, $0C, "PLACEHOLDER P", $FF +db $21, $A5, $0C, "PLACEHOLDER 1" +db $21, $C5, $0C, "PLACEHOLDER 2" +db $21, $E5, $0C, "PLACEHOLDER 3" +db $22, $05, $0C, "PLACEHOLDER P" +db $22, $45, $0C, "PLACEHOLDER 1" +db $22, $65, $0C, "PLACEHOLDER 2" +db $22, $85, $0C, "PLACEHOLDER 3" +db $22, $A5, $0C, "PLACEHOLDER P", $FF + +ShowItemString: + STY $04 + LDA ItemLower,X + STA $02 + LDA ItemUpper,X + STA $03 + LDY #$00 + .LoadString: + LDA ($02),Y + ORA $10 + STA $0780,Y + BMI .Return + INY + LDA ($02),Y + STA $0780,Y + INY + LDA ($02),Y + STA $0780,Y + STA $00 + INY + .LoadCharacters: + LDA ($02),Y + STA $0780,Y + INY + DEC $00 + BPL .LoadCharacters + BMI .LoadString + .Return: + STA $19 + LDY $04 + RTS + +ItemUpper: + db $A0, $A0, $A0, $A1, $A1, $A1, $A1, $A2, $A2 + +ItemLower: + db $00, $81, $C2, $03, $44, $85, $C6, $07, $47 + +%org($C8F7, $3E) +RemoveRushCoil: + NOP #4 + +%org($CA73, $3E) +HookController: + JMP ControllerHook + NOP + +%org($DA18, $3E) +NullWeaponGet: + NOP #5 ; TODO: see if I can reroute this write instead for nicer timings + +%org($DB99, $3E) +HookMidDoc: + JSR SetMidDoc + NOP + +%org($DBB0, $3E) +HoodEndDoc: + JSR SetEndDoc + NOP + +%org($DC57, $3E) +RerouteStageComplete: + LDA $60 + JSR SetStageComplete + NOP #2 + +%org($DC6F, $3E) +RerouteRushMarine: + JMP SetRushMarine + NOP + +%org($DC6A, $3E) +RerouteRushJet: + JMP SetRushJet + NOP + +%org($DC78, $3E) +RerouteWilyComplete: + JMP SetEndWily + NOP + EndWilyReturn: + +%org($DF81, $3E) +NullBreak: + NOP #5 ; nop break man giving every weapon + +%org($E15F, $3F) +Wily4: + JMP Wily4Comparison + NOP + + +%org($F340, $3F) +RewireDocRobotAccess: + LDA !current_state + BNE .DocRobo + LDA !received_rbm_stages + SEC + BCS .Return + .DocRobo: + LDA !received_doc_stages + .Return: + AND $9DED,Y + RTS + +ChangeStageMode: + ; also handles hot reload of stage select + ; kinda broken, sprites don't disappear and palettes go wonky with Break Man access + ; but like, it functions! + LDA !sound_effect_strobe + BEQ .Continue + JSR $F89A + LDA #$00 + STA !sound_effect_strobe + .Continue: + LDA $14 + AND #$20 + BEQ .Next + LDA !current_state + BNE .Set + LDA !completed_doc_stages + CMP #$C5 + BEQ .BreakMan + LDA #$09 + SEC + BCS .Set + .EarlyReturn: + LDA $14 + AND #$90 + RTS + .BreakMan: + LDA #$12 + .Set: + EOR !current_state + STA !current_state + LDA #$01 + STA !rbm_strobe + .Next: + LDA !rbm_strobe + BEQ .EarlyReturn + LDA #$00 + STA !rbm_strobe + ; Clear the sprite buffer + LDX #$98 + .Loop: + LDA #$00 + STA $01FF, X + DEX + STA $01FF, X + DEX + STA $01FF, X + DEX + LDA #$F8 + STA $01FF, X + DEX + CPX #$00 + BNE .Loop + ; Break Man Sprites + LDX #$24 + .Loop2: + LDA #$00 + STA $02DB, X + DEX + STA $02DB, X + DEX + STA $02DB, X + DEX + LDA #$F8 + STA $02DB, X + DEX + CPX #$00 + BNE .Loop2 + ; Swap out the tilemap and write sprites + LDY #$10 + LDA $11 + BMI .B1 + LDA $FD + EOR #$01 + ASL A + ASL A + STA $10 + LDA #$01 + JSR $E8B4 + LDA #$00 + STA $70 + STA $EE + .B3: + LDA $10 + PHA + JSR $EF8C + PLA + STA $10 + JSR $FF21 + LDA $70 + BNE .B3 + JSR $995C + LDX #$03 + JSR $939E + JSR $FF21 + LDX #$04 + JSR $939E + LDA $FD + EOR #$01 + STA $FD + LDY #$00 + LDA #$7E + STA $E9 + JSR $FF3C + .B1: + LDX #$00 + ; palettes + .B2: + LDA $9C33,Y + STA $0600,X + LDA $9C23,Y + STA $0610,X + INY + INX + CPX #$10 + BNE .B2 + LDA #$FF + STA $18 + LDA #$01 + STA $12 + LDA #$03 + STA $13 + LDA $11 + JSR $99FA + LDA $14 + AND #$90 + RTS + +InvertSelectTiles: + LDY !current_state + BNE .DocRobo + AND !received_rbm_stages + SEC + BCS .Compare + .DocRobo: + AND !received_doc_stages + .Compare: + BNE .False + JMP $996A + .False: + JMP $99BA + +InvertSelectSprites: + LDY !current_state + BNE .DocRobo + AND !received_rbm_stages + SEC + BCS .Compare + .DocRobo: + AND !received_doc_stages + .Compare: + BNE .False + JMP $9A58 + .False: + JMP $9A6D + +SetStageComplete: + CMP #$00 + BNE .DocRobo + LDA !completed_rbm_stages + ORA $DEC2, Y + STA !completed_rbm_stages + SEC + BCS .Return + .DocRobo: + LDA !completed_doc_stages + ORA $DEC2, Y + STA !completed_doc_stages + .Return: + RTS + +ControllerHook: + ; Jump in here too for sfx + LDA !sound_effect_strobe + BEQ .Next + JSR $F89A + LDA #$00 + STA !sound_effect_strobe + .Next: + LDA !controller_mirror + CMP !CONTROLLER_ALL_BUTTON + BNE .Continue + JMP $CBB1 + .Continue: + LDA !controller_flip + AND #$10 ; start + JMP $CA77 + +SetRushMarine: + LDA #$01 + SEC + BCS SetRushAcquire + +SetRushJet: + LDA #$02 + SEC + BCS SetRushAcquire + +SetRushAcquire: + ORA !acquired_rush + STA !acquired_rush + RTS + +ApplyLastWily: + LDA !controller_mirror + AND !CONTROLLER_SELECT + BEQ .LastWily + .Default: + LDA #$00 + SEC + BCS .Set + .LastWily: + LDA !last_wily + BEQ .Default + SEC + SBC #$0C + .Set: + STA $75 ; wily index + LDA #$03 + STA !current_stage + RTS + +SetMidDoc: + LDA !current_stage + SEC + SBC #$08 + ASL + TAY + LDA #$01 + .Loop: + CPY #$00 + BEQ .Return + DEY + ASL + SEC + BCS .Loop + .Return: + ORA !doc_robo_kills + STA !doc_robo_kills + LDA #$00 + STA $30 + RTS + +SetEndDoc: + LDA !current_stage + SEC + SBC #$08 + ASL + TAY + INY + LDA #$01 + .Loop: + CPY #$00 + BEQ .Set + DEY + ASL + SEC + BCS .Loop + .Set: + ORA !doc_robo_kills + STA !doc_robo_kills + .Return: + LDA #$0D + STA $30 + RTS + +SetEndWily: + LDA !current_wily + PHA + CLC + ADC #$0C + STA !last_wily + PLA + TAX + LDA #$01 + .WLoop: + CPX #$00 + BEQ .WContinue + DEX + ASL A + SEC + BCS .WLoop + .WContinue: + ORA !wily_stage_completion + STA !wily_stage_completion + INC !current_wily + LDA #$9C + JMP EndWilyReturn + + +SetBreakMan: + LDA #$80 + ORA !wily_stage_completion + STA !wily_stage_completion + LDA #$16 + STA $22 + RTS + +CheckFirstWep: + LDA $B4 + BEQ .SetNone + TAY + .Loop: + LDA $00A2,Y + BMI .SetNew + INY + CPY #$0C + BEQ .SetSame + BCC .Loop + .SetSame: + LDA #$80 + STA $A1 + JMP $A3A1 + .SetNew: + TYA + SEC + SBC $B4 + BCS .Set + .SetNone: + LDA #$00 + .Set: + STA $A1 + JMP $A3DE + +Wily4Comparison: + TYA + PHA + TXA + PHA + LDY #$00 + LDX #$08 + LDA #$01 + .Loop: + PHA + AND $6E + BEQ .Skip + INY + .Skip: + PLA + ASL + DEX + BNE .Loop + print "Wily 4 Requirement:", hex(realbase()) + CPY #$08 + BCC .Return + LDA #$FF + STA $6E + .Return: + PLA + TAX + PLA + TAY + LDA #$0C + STA $EC + RTS + +; out of space here :( + +%org($FDBA, $3F) +WeaponReceived: + TAX + LDA $F5 + PHA + LDA #$1E + STA $F5 + JSR $FF6B + TXA + JSR ShowItemString + PLA + STA $F5 + JSR $FF6B + RTS + +CheckConsumable: + STA $0150, Y + LDA $0320, X + CMP #$64 + BMI .Return + print "Consumables (replace 67): ", hex(realbase()) + CMP #$6A + BPL .Return + LDA #$00 + STA $0300, X + JMP $BE49 + .Return: + JMP $BE25 + +EnergyLink: + print "Energylink: ", hex(realbase()) + LDA #$01 + BEQ .Return + TYA + STA !energylink_packet + LDA #$49 + STA $00 + .Return: + LDA $BDEC, Y + RTS + +; out of room here :( diff --git a/worlds/mm3/src/patch_mm3base.py b/worlds/mm3/src/patch_mm3base.py new file mode 100644 index 0000000000..c64c83c3c0 --- /dev/null +++ b/worlds/mm3/src/patch_mm3base.py @@ -0,0 +1,8 @@ +import os + +os.chdir(os.path.dirname(os.path.realpath(__file__))) + +mm3 = bytearray(open("Mega Man 3 (USA).nes", 'rb').read()) +mm3[0x3C010:0x3C010] = [0] * 0x40000 +mm3[0x4] = 0x20 # have to do it here, because we don't this in the basepatch itself +open("mm3_basepatch.nes", 'wb').write(mm3) diff --git a/worlds/mm3/test/__init__.py b/worlds/mm3/test/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/worlds/mm3/test/bases.py b/worlds/mm3/test/bases.py new file mode 100644 index 0000000000..38ea47ab2f --- /dev/null +++ b/worlds/mm3/test/bases.py @@ -0,0 +1,5 @@ +from test.bases import WorldTestBase + + +class MM3TestBase(WorldTestBase): + game = "Mega Man 3" diff --git a/worlds/mm3/test/test_weakness.py b/worlds/mm3/test/test_weakness.py new file mode 100644 index 0000000000..400eab1f4b --- /dev/null +++ b/worlds/mm3/test/test_weakness.py @@ -0,0 +1,105 @@ +from math import ceil + +from .bases import MM3TestBase +from ..rules import minimum_weakness_requirement, bosses + + +# Need to figure out how this test should work +def validate_wily_4(base: MM3TestBase) -> None: + world = base.multiworld.worlds[base.player] + weapon_damage = world.weapon_damage + weapon_costs = { + 0: 0, + 1: 0.25, + 2: 2, + 3: 1, + 4: 2, + 5: 7, # Not really, but we can really only rely on Top for one RBM + 6: 0.5, + 7: 2, + 8: 0.5, + } + boss_health = {boss: 0x1C for boss in range(8)} + weapon_energy = {key: float(0x1C) for key in weapon_costs} + weapon_boss = {boss: {weapon: world.weapon_damage[weapon][boss] for weapon in world.weapon_damage} + for boss in range(8)} + flexibility = { + boss: ( + sum(damage_value > 0 for damage_value in + weapon_damages.values()) # Amount of weapons that hit this boss + * sum(weapon_damages.values()) # Overall damage that those weapons do + ) + for boss, weapon_damages in weapon_boss.items() + } + boss_flexibility = sorted(flexibility, key=flexibility.get) # Fast way to sort dict by value + used_weapons: dict[int, set[int]] = {i: set() for i in range(8)} + for boss in boss_flexibility: + boss_damage = weapon_boss[boss] + weapon_weight = {weapon: (weapon_energy[weapon] / damage) if damage else 0 for weapon, damage in + boss_damage.items() if weapon_energy[weapon] > 0} + while boss_health[boss] > 0: + if boss_damage[0] > 0: + boss_health[boss] = 0 # if we can buster, we should buster + continue + highest, wp = max(zip(weapon_weight.values(), weapon_weight.keys())) + uses = weapon_energy[wp] // weapon_costs[wp] + used_weapons[boss].add(wp) + if int(uses * boss_damage[wp]) > boss_health[boss]: + used = ceil(boss_health[boss] / boss_damage[wp]) + weapon_energy[wp] -= weapon_costs[wp] * used + boss_health[boss] = 0 + elif highest <= 0: + # we are out of weapons that can actually damage the boss + base.fail(f"Ran out of weapon energy to damage " + f"{next(name for name in bosses if bosses[name] == boss)}\n" + f"Seed: {base.multiworld.seed}\n" + f"Damage Table: {weapon_damage}") + else: + # drain the weapon and continue + boss_health[boss] -= int(uses * boss_damage[wp]) + weapon_energy[wp] -= weapon_costs[wp] * uses + weapon_weight.pop(wp) + + +class WeaknessTests(MM3TestBase): + def test_that_every_boss_has_a_weakness(self) -> None: + world = self.multiworld.worlds[self.player] + weapon_damage = world.weapon_damage + for boss in range(22): + if not any(weapon_damage[weapon][boss] >= minimum_weakness_requirement[weapon] for weapon in range(9)): + self.fail(f"Boss {boss} generated without weakness! Seed: {self.multiworld.seed}") + + def test_wily_4(self) -> None: + validate_wily_4(self) + + +class StrictWeaknessTests(WeaknessTests): + options = { + "strict_weakness": True, + } + + +class RandomWeaknessTests(WeaknessTests): + options = { + "random_weakness": "randomized" + } + + +class ShuffledWeaknessTests(WeaknessTests): + options = { + "random_weakness": "shuffled" + } + + +class RandomStrictWeaknessTests(WeaknessTests): + options = { + "strict_weakness": True, + "random_weakness": "randomized", + } + + +class ShuffledStrictWeaknessTests(WeaknessTests): + options = { + "strict_weakness": True, + "random_weakness": "shuffled" + } diff --git a/worlds/mm3/text.py b/worlds/mm3/text.py new file mode 100644 index 0000000000..337837244c --- /dev/null +++ b/worlds/mm3/text.py @@ -0,0 +1,63 @@ +from collections import defaultdict +from typing import DefaultDict + +MM3_WEAPON_ENCODING: DefaultDict[str, int] = defaultdict(lambda: 0x25, { + '0': 0x00, + '1': 0x01, + '2': 0x02, + '3': 0x03, + '4': 0x04, + '5': 0x05, + '6': 0x06, + '7': 0x07, + '8': 0x08, + '9': 0x09, + 'A': 0x0A, + 'B': 0x0B, + 'C': 0x0C, + 'D': 0x0D, + 'E': 0x0E, + 'F': 0x0F, + 'G': 0x10, + 'H': 0x11, + 'I': 0x12, + 'J': 0x13, + 'K': 0x14, + 'L': 0x15, + 'M': 0x16, + 'N': 0x17, + 'O': 0x18, + 'P': 0x19, + 'Q': 0x1A, + 'R': 0x1B, + 'S': 0x1C, + 'T': 0x1D, + 'U': 0x1E, + 'V': 0x1F, + 'W': 0x20, + 'X': 0x21, + 'Y': 0x22, + 'Z': 0x23, + ' ': 0x25, + '.': 0x26, + ',': 0x27, + '\'': 0x28, + '!': 0x29, + ':': 0x2B +}) + + +class MM3TextEntry: + def __init__(self, text: str = "", y_coords: int = 0xA5, row: int = 0x21): + self.target_area: int = row # don't change + self.coords: int = y_coords # 0xYX, Y can only be increments of 0x20 + self.text: str = text + + def resolve(self) -> bytes: + data = bytearray() + data.append(self.target_area) + data.append(self.coords) + data.append(12) + data.extend([MM3_WEAPON_ENCODING[x] for x in self.text.upper()]) + data.extend([0x25] * (13 - len(self.text))) + return bytes(data) From 371db533717a1740d7f60224b380a6be21e8498d Mon Sep 17 00:00:00 2001 From: Noa Aarts Date: Sun, 8 Mar 2026 21:50:34 +0100 Subject: [PATCH 26/53] Stardew Valley: morel doesn't spawn in fall secret woods (#6003) --- worlds/stardew_valley/content/vanilla/pelican_town.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/stardew_valley/content/vanilla/pelican_town.py b/worlds/stardew_valley/content/vanilla/pelican_town.py index 3c2fb1a2da..72aa113543 100644 --- a/worlds/stardew_valley/content/vanilla/pelican_town.py +++ b/worlds/stardew_valley/content/vanilla/pelican_town.py @@ -144,7 +144,7 @@ pelican_town = ContentPack( ), Mushroom.morel: ( Tag(ItemTag.FORAGE), - ForagingSource(seasons=(Season.spring, Season.fall), regions=(Region.secret_woods,)), + ForagingSource(seasons=(Season.spring,), regions=(Region.secret_woods,)), ), Mushroom.red: ( Tag(ItemTag.FORAGE), From 44e424362e1f7b731a5d33268fe6ca43b0e6b219 Mon Sep 17 00:00:00 2001 From: Remy Jette Date: Mon, 9 Mar 2026 00:51:26 -0700 Subject: [PATCH 27/53] Docs: Don't serve non-static files in example_nginx.conf (#5971) * Docs: Don't serve non-static files in example_nginx.conf `try_files` will serve the file as long as it exists, which means I could `GET /autolauncher.py` and be served the file. * Use root instead of alias, add route for favicon * Update deploy/example_nginx.conf --- deploy/example_nginx.conf | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/deploy/example_nginx.conf b/deploy/example_nginx.conf index b0c0e8e5a0..a4f042739c 100644 --- a/deploy/example_nginx.conf +++ b/deploy/example_nginx.conf @@ -41,16 +41,8 @@ http { # server_name example.com www.example.com; keepalive_timeout 5; - - # path for static files - root /app/WebHostLib; - + location / { - # checks for static file, if not found proxy to app - try_files $uri @proxy_to_app; - } - - location @proxy_to_app { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $http_host; @@ -60,5 +52,14 @@ http { proxy_pass http://app_server; } + + location /static/ { + root /app/WebHostLib/; + autoindex off; + } + + location = /favicon.ico { + alias /app/WebHostLib/static/static/favicon.ico; + access_log off; } } From 123e1f5d95b844e0f2063526cdab18ca26cc10b5 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Mon, 9 Mar 2026 09:13:45 -0400 Subject: [PATCH 28/53] Lingo: Fix logic for Near Eight Painting (#6014) --- worlds/lingo/data/LL1.yaml | 6 +++++- worlds/lingo/data/generated.dat | Bin 149835 -> 149954 bytes 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index 4c41f3236f..613eedc4b6 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -4470,6 +4470,10 @@ panel: SEVEN (1) - room: Outside The Initiated panel: SEVEN (2) + First Eight: + event: True + panels: + - EIGHT Nines: id: - Count Up Room Area Doors/Door_nine_hider @@ -4612,7 +4616,7 @@ enter_only: True orientation: east required_door: - door: Eights + door: First Eight progression: Progressive Number Hunt: panel_doors: diff --git a/worlds/lingo/data/generated.dat b/worlds/lingo/data/generated.dat index f5eb3e069927f30aa069594bd3a98e6caa17e001..dff56b10d846da2142a4bfa267a225f160060160 100644 GIT binary patch delta 17553 zcmb_@d3;nwwm;QL-%e)*vJ*l&34ySNBrE}B=_H+`q0=2Z9U=%wfM~cvSR;z8DzX?i zZb&IA5I~lvB8ZBJEILm`2nge-fCz%isH39eo6&KB-&yYM?g+m3&rd%e`qZ~hZKuvT zb?Vg3u{G8=G;8S6(6ynDhGyDor{)cu+Hb&={sX7>pImTj?tp^a0aI%S6im&_&8@vP zf6DICp%EP-N~X=3JJ05rc3a)Nru&zmmQ>A*N+O^A8&&9TD*O=^X zm@&C_j;*Yrex9w5Ex3oq*tT^JXK9(QX+o1)4b9z+W-4nfN0VdB?rHV4bDQ*jzu2?? z{qaJHii7Smv<|5Wd-6YgOo+i+>*=?YQbfCIr%oqmJI^c@m0F*(i^W9k*|Uvenl|{O z6=I(D`bUez5^cn}m14DaB_cP;kQXHC|4?e$N$iFj?=XDfwG`{!qyMQ^S7 z^Lv#+@ziGI7x7yEFKU%SET(JACdX?pe(|R8Y773p5vG>%Ws6X?179|aJ2c-{Qrx3` z^VK5dL1fO7KP(r?+ODrdwZ^Zf3aS0@^)jUeYcjaz(SLlX?80)cc1n$thwc;6+9Th5 zDGq6OeS4ocy64KbqfCl90T{Yz_RDd5CVzjsNtEn4@y{tHWd*`nTJHbE?HT>A?}Rw4 z4f&}_v}*7Ev_+iO=KcJt_(IG1&*S1p?VbM|Hiac>5BzdKq-xGz4~SgN^4sHLnAY-J zi&BOI!A?b?ZU_@DOCvl(K(w*TrFW#y`s z6PmKMWmjX@=8FN!z1T;tw(O;pwX4J`@RzBzI%Vv=v<0D1P-ld^pkWj*spjD0}_&u|DLv)tSiugd}?ww%~ z%Y+!S_i~tcKqw24ucvk~v6GCpipafN!-Y$T^%81(?oEvp6NUJTTo@&O6{qE$(V|{l zl7EjD4dPolAx69?ev=*Bi>E`wlH0YDpR^ZQd*PquBJ>6ty;GksMa-b^07w;<`$EIh7tw7DCZER`&JyDDP~lpMSwbAuLE;D^-L8ojz5>$x*2^ zftpk?2B6v7MY=qbO8Qi@8EB=8Xs^#n=5!Gq4SHM`k~s0KW*+MLKo=1MJhKKs+wZ#2 zdVcOAZZM!RY1CcrJQ#W8Y7r|RN~5u;?Q3bG12Eb*i9~tjpy()nN~1vlNS0|fVS`Fz z8^_s17bxD?Rdj~MHFL?o-ZT=-B~_#Z3r4AS*u1Q1db{*=>Qpb?k}k6JK2xa!kNQTN z4nloZUbI)1;wR}OMZIxY1{q|Z400LIWr*BxrOe}Uv7tb@0J{e1TS7`5F6<;GlUU(r zsC+n+Y>%w{oVuF8iItMMRK|KQ)X$SZro*>JmzN>#lHfoHF zsw22@W2oEdCR+sQb&@A4M7-G6oJH->aOH?BVS(wOZOTbmA`EDn<~>;=325EQ_GZxn zvBcB-2}fGeW~8V}kIPeO%PyQ=Gv#)2QOF)a6JvHct{a&Knt*(yTR`3RcMGUt`0FA% z%&=f>);gbbcL&GspUSP-WblE$JBPqjK9;+(NoDj2|07#u>uq=jJ&@~1Ii!bSoaj8} z^&mlDn@F1H5ahC^%GY~PXNEfd(!)O{v>MV=qy^@4M^9o!lZXs+IZAwnH$%i~_EJ~{ z`k+rNtQYBIk6vUo;4-?&hi(;d&}~aEkq+JZAy(2$YF%oOGBk#kIg4pOq5BL=v3(pB zz)&+{>S|C)%x;g@SB4rmueVqM{r=XQR4l!Z=)qmqs86-+vc&17UT*6ndb4f)kwvL? zx_u7vaj+A!WsO7GD{-a9A-_oXjDg z19Yamnj_30Q)a3vGz6C}J^g5;!>_=oclINvrf<-eek3mH>g#?+SFpDzcw2TpX*Slz zM3mbrK`3Kw*|`LQlXb|)a;Y<-zs(hqF#57Qk*y!vL~<8I_NRtGqVpzXpn+tY2l+|a zFP{!_;26p6Bg=n*eh`Lc#gj0c>BxfZh?j#0lC#9_W&J>7HmLXB(f&C*#;{kU)|a{Ug&`tErVoP1 zkz~;cV}mCL(cmD&PFZyLtKp$w?sujI+ZUb8mAZ|k%oY700CL`hV_`e&rn-P+hkGG6 z6wo1I&`MyN)5AhboMTAgsMa=^W)JhqleY{epG!30VR|bl4>q$ZVO5mXQE4yoI;z=V z5DE)*RJjUqGSv3u5D^~KKtdVgB&Zw4KY7wKG%zgCK||%r)}e41tdp09iaGG}Xf5cL z-Inc}Ka=zh!Uvn&cmp*j`uGiG3`GBjX`@Z$v^%K{m7TV1PkpVwEEf)=I04&5S)tNb zH>Z}XsjI8QgxNB)VNPIBKrrCN&SdZ5Z5vnC-cei6tdEIK;N2Dv7rDSpU7Z|G>jyf$ zc?8+_?x&!S>=B|vTQe|X!txQM5$Pi-%+N-Vrvf!fkE&}x{SNUuC_?*e1bJ)ZpX<<`UJYuf%g66Ws@aq{h=CzlfB_zV#t zm)L17{+P!~-WFpX`L>-#-R&X)gcj1-geCUKOVyEbR3WSgqyiJ>$bcto3)b{&BLgF@ zn$YH4SHP^x*-68}jYkv(3%!fT!09Y6FCwQGT`UIbx=bWHIwIN80XYQP!%wfUP_M^B z?gC1Q^7M8H2%jz{Yu{HaZUn9f2W>}Rhp2)QIn;L?fjH`W2ZfV_mzGdNqBjQ7{|TbK zr2(F2N~tZ*Jto3e=CHY(gNd1qv;Ub{R9{P#YMyE4tG(%AX)Ok_qmvvO`z2!`iXUWe|JHK z0R~6)v{pL6b^7m_@)tKbGCEW`d*J2>8l?|h+I1+OO?CGGY0H6aAgej+{C(SEcxV(WXGhlqrJkaA0p{qNZRO8)B>+R^og)k zcp+iHff(@gl;^yp=X9;T0_~v@hazJN~w<(X2|tf~Opx+*^#3|&-ack<<)r&3l`Zl^04whLwukcjT@=#G_Yc-8ct-h`1BNlT|We&Jq;zFotB8T0@>jEg*TH&g4ml_Rp|DPlu8f#n}ljKgOF#*Dw4@nF8^H@5A3|Ejd zPT2DP#GBQfYBU!*N}cW?rS*qh<3tXe^Sj4UZ(v7!i~SS(zzZ08aTKBd?nMHryqTOn zMGDIUv?XL58wXXKznP?lbd=fHl!$aIK_f=tw zz?7p)zs$wNuz`0=}jX|s~ySo@}CuB6xPcN7_;_|>mQf`23k!}pi z%k6yiqyEZ0ReW>Ap&{ogw?rS#=T+kBnoLaP7-!l%Je$XRjywy!en#} zDPcklRr_kl9sZz#j{Fy--1G!mV<>E)Cs?)gTMO6>CKO91gE!nJyG^ERJ(dh5-XmvD z7RkVJYBKouZSpz%;gZ49iY)PF4t0<%P+^}!SFtfu;F89x2CG;)g>U1SoZB~rG=u`( z@21f1q2j@P$=C=e;@L!TwPP$?KRHo~T$T_gD-ssu{;AYFz{nG#y7qjl20Gm`3dBtI z0R`i_jZEQ6i5&tV(sbwqV^L~*uGW76u$n#K7)V{It7^N8JP#xwLiU;F2mdc2{C^B6jJdZBDB4_i`2Z-$@1IwY-)}&F+y$_uKJs0a!+up1 zg24LUE=HhwjD&1MPmcQK|IOI~Nlg*M2l{wk1psra$z+daSX^RYl zj8n0ZA;-fO4r9MNz_R>ix^Ylv$mAI!>iXg7kQo9WTF6(;njw_7T<};!&oQfKh|brq zd3A^zgK8m??{ z@}`+|DnVx6CRfj-paJLFA&LW7+gJ<({0}hl?99LoNX){+W^d9gvBK1Lih9s5_spUE zCb)BY9Qkn$jSE=~4D)8s6RC36T)L4##=d7vb{J=M3@b&g{SyVPaOkHPG(=^9zQ*&9 zN2WH=UPDc`bT^P&g6gg^dX5N}_cy?4PT_ElVaIJ9K&Vd-G_gH|g7CmaUMBwzcoJME zm);?&;Rh;vO4Iy6yw!I;DHY+9=2JEq5|wj-_4E1SF#K%26T}5t+Xu8Z=Pv3CoPte0 zc^5Imk&`Y@+(m{7;_A|VuG-0p;%E$kb4b_9v<`FC-i(GqoJmPHYFN8~%m5q8#~09s z65hIiIwuEzWdVr-?x>~Gb2n`?ODAnmoHuBt{yV2RFxTO?(w6pI1y~?TMoid(#(*1tt^*o*b_&;Vq&g zztBOk37`LNe)t2fbW4ec;{>dchnD*DPLD1V1LRXn@%|+ZIs-CS?jkRoG#o_cEu%CX zRM#+)a1DAoihUT$0s>65QkktTr+=&x_K)pVUO0KD`X+_4t8 z_)~ajAQiVl5FPSP59To}P<4o0&MviJc~ytD^JLUSWbWAOuqDaOf3jWCy; z50Im#P7tt~tL=C}gK_ps7v5L6p$1%Kdh(Zr4FSouB-%Tl3BCqB+V zZ+ZOFTP}W(VtTrhzWbnvw?Je}5w?No1N64|^ynH|_p)L-h2v4;qucDlhiD|Sf}IbM z;h>>g)Sm4j9m5vY{4fo77@rn;JWP88`HuuS=3#PIz@H%-A2!-%;G+mWgW!sny$msr zy||c#L!2>>kO^Ro9PtQAmbg5SSfoe}xj<|97EEjhnGGw#Lg7Y1(&Cb@(oS+GzG06B zx`Rtck-t0mk^uA$C&aBF+fHtQOO^V2LCc%4koIfXW38cskBnj|yhsOSEAL)IHc39} zxizBW^-rqz))+BonoQNm5x{8aa*RgGMH#h~emY(5*GMchhp#om`=-gXCiJX(`!+$? z2m4PM!AI7Tu<=9xML7mTn4IBDbMpKkDVa!Jb1Y zv^!l6&LILHCyJR*E^DSKV*57uYt0m$AWkAv&quUtC*hNpSlM2Zs$mr!!c?Uu=L8RF zVnqpbi_KN|C_w&UI33?F>57B=5Qh9)vdsM>e1olvf=5vM>Us@4oJhWwr!lm$Og|!l zifgh7nM;y}g({UY&o>lW=xz zlgd++??j8k<5sa& zcKT@K!e%gw;~U9o1%Rs?X<(W}&lWoHkdAD$z!zOU4=42zf$U%)DGWen`v)W08Cu@p-x7(lAgje7IGYHk^^;8vd`ViTp8{1|k0n3yLTS-r_9yD~clQTy+ z(OFyltA?6VerrZP`R3L@So`Z%Isg!d8-Ve=J!~6&P=hz6$gpCM$N$+F-JMQsqa`67 zuE5SXIl9pw$x^qI%|LCop6dn@-`ok|TnUl>tr4hd(1Mfm!Qij!&t7sK@tGgSG_sO>Kkv zrgBi<-~F*|(3*&MFwM)pQ*>8y`vuYbPR%&a1?lGJ=z9x>d5KK(6qx4WnPzD+%|kQo zA2!?UA64Hkn|x>|Y0=|5!Bn=%)}3@DT;3^i`H1=q%vyEk2OpFWQ?gl(d|vd<#4L__ z9WD$E@q-R>YN?_?zWqGq8;w_|tX=%wlviX%@&^R>Vi>SkF4)CMJulq<4(uXD#iN{2UuEhf-~u9bL6G(ivpqf$D$1P*E!d}KAa?CDczL49N?O&pGQgg_1SNNxgnQ#VpFqm(;eE21Hdh4O(D=!6N z?$2K`Ldgu7_A>RzL9C;+)Ch|yAMrAULUi1oc$uV+G@Wu|_@i2iy2ro;!Dt5&f}Vk; zs!HEjc8l215(3{;;cB_Zy+I?xCyM?aBEw*B%Z;oq1+x{ei1ybHk>|m82_P!`76udx z-U>*Q4!=S#fpkS_zmL=vPw{ZYbJN(;pofho_xaCm*}9L;XWH50`-G+WRrYka(^%HC z*&us=9(duaBoyu%6_V4)fs0{mRjUl`xw(91@N zx5`O&h^@nwa(j6>Kbw)_74ow=YGJxu@*ue|P%8}W=xkbefTl(C_dXG7=5MEsD{x2b z(5J_qeo%%Vq*ior!}qSqx(heT9V0AmN0raZ^Dxk4{opXl9p%RdNmvGe$xMUp2q}3Z zZX_YkJmgnh`NSa_9u+XcQs(j5ykq^BIQj2Gqz$B<*@px4xWoQNa^+z<(nu@!9rmZ% zz`2w`=HC%`5=@f%YbRdeWzOqj6wHz6M_#9(-LDezZ?99ZZEUK?>*gpK+z4I`U=bOX zQg0P~;$-v?2Ln3b{n_wt^1vHp?0DWmq>Z2Ilc|x`wEqj$CI5x20xtrPm-ZBr<{(3q zwE|Sv1`8nPh60F*06THoT{c)2J-8x>53WU{Z7xr#ABu^vk8#@FHvg9&z?T_S0-pqU zZN8xUa)+oAP*(U5wy3UV8h@)45WuVd?KizhosHcXPE|&{Nwu_+f&d+5)mixz#p;*{ zm%YN_vQ>g;C=UtSWEb1zr*Dc8$_IG9O_bShiQdX1oroUymguTH!SsD^i6Z4`rjH}O zi|L=<5*f;?OsBmqdMHisc{KFnep}d;4NR|nTVyGFnLhkB1O@mBHSm1&c4)j7wLVf792Gs4p3Jo2C}gph9~E)R06&ncRQTys`QuTMtl$@~Fl@(T zFy&!gh#qkadO6PY%wr-i0zU*)C*n}qnr(Zsyz97VFW)~VY>K2B`Rg$dD1HY7J@si7 z-ITLTk8Ks*Bi<#NCwg&LXDw&9Sw7n;5+k~#8{PKRP7O|$e{Y3H;E@E%=;LCDf?x4K zkM855OnHRqStlTGw(Gd)qdXY|{Di=E26~jp^i*u6<7ofLf|b`p(C?5?%c$IG23 zL9+Xp;rK~0&{6h|n-~%Jj!~ZH0m*7hRZ(ullk!Y)^ zsjresHHkA!^UoC2)cSBVn-RX&c z{U*QvD{S&h*)+Gr_rPY}V!H4>F-SSd^pf|$9?mj-;5|@;&>qD9JJSDVeAwTB{*viM ze-nkuD&~0;>BUS(y)SYi%6l7g%+wzLHbqW;UnDAbK!OT{v*dj-Sixi!&1pd+TvKLBZayQr%X6niv|N80-sU}L;48+jPK&IF%7$t4=GyFYYHOP0 z>@rh|+*M{ul;*3Zb$29VVFkk;a z@?l*l*A2K{otf(f{;^IDKMVUu{cn}EXF+^Az2z+UNTRpP*0Un%Hl}eL6ZI8Y0^hRE ztFx6%pE+}mKHcqsDZ*cB-g>=So@4~5?e%KS_iH1ckBbS6rQld{($65rv)xTG78c76 zx#1(xpQpD&p7}`R8FYtp@OOUHe1~+M6ZsKL4EmMHG**S*D){Urk+ZmhSV0*jq^KMTEEf?_#HF){N_jy+xin4`&c{v`xle z05`AGLodL=pwqWs03Sp2)AFGUqA%~k)Bb_wcOy(TeIbN~G=B{1IN9A49<6`U6tp*8 zqs{QaCC16rCGAm$tS>Yv{vCv!%97vxEG&Km_A9dkzd~}WKknN3LQ}Y(8@4S=j<_ho zdDc55>|JkzhHdL*(6D2fa&b{m(=RTHJ}w$!3l71y2d^OTrK>(+!dcJtzf=pbv!MA? z8(!WPdCMh{I3>WsO3u&A0hDdftbh0sz)8pTze5fl-$Hh#=j@nWp1CAacwwYpx_;gp|{XUfWnr%E*ueny2IQQFN_lXUh@2w4GAQ;-TrsE=fRQ1HL=rNTkzt5r5;B~Tk%)99#7;=I zT7*y*Kx%OulHHJWAk`hI5?SBhWE)XR+^Q4t&D4GrBQ8Yx5>n2H2ay~?DhSC|z4CZ} z(=e}(8O9((e`2U+WGo_s2pLC6wmKf60)W&BNDTq1L7k{qPC{rXK-z^qS=LdTsIq3b z+1QbovpjNFgN7(!MsvI>!ELRK$fs1c!?2zro_hY=Y|$RmubL1Y{u8Y62FxtWk= zMjk_CJRy%W@&qCi2zio_Y;`?C69H<*Uy$Dyn0nsuA|ms!?j@ueka`)ZJCNFo z)O@5~LF!JR8q|G2!eghpUyd1UvMqUyit0gRxEmPes)uxj*AZHXt=~Xu5w-pcRjO|y zvY6Q4V&n)SO9**~kzg-Azv}_4@4dy zm9@i{8Kv)tRAFwFP8Kc0SH`qZyZEw}2_ z*{V3AS&u(#4QUBkudEBnuouoOEG){)n>O&KfinxI=M5~HHluLR;QWEbgBtQ?<`oRu zQ!&(LR-ChEH7;t}yV{mQ)y0AnvYp+~*NVI4pK7CAS z#Woq*o{MqXZ=c?;Y{X)=HswmJw&mg`v19L`OZN(KK-+(Li8!hazVe9pqju)Xo#KM# z{%pJWv-aa>8^wQXD?Z<){LUTi{yb5u`(lN#X_vov2nc9_`6SZaEPFExr)2V3r?qjiY@8$2h3h}b`*YEFF4q;uEwtGSB-Zeku znM}XL?mhh1`%KEPIKX>q4S$Q=Z zi>H+bu#XHG6>UnCPYLm)*d%LB;vMBV!nK8W6P&F5eAOl=28+eYVXVm1rYuQ9{&4ww zus9*k?>iA98ieU;yv$R?1<_4Ln8im*b^`LsbzvfU->+sdTPRhC^p=P6Oi6NixUlV8 zYlU{o0)%^YfKzOuS}1J+?tQ=w?e`;+Ayd1EIN7C(xGYY}|LP*mLP0;t?Zv56s8lU+fOIRc?lxI z-~rkiTHtRVrj$9oPLI#u--QGbX7K*rp^wx=qwfU8%V{-Lx&nh#x$BLBoS7uLzzmy{L^}LT zsDw5Z*dWyHDRX*=Ek`7ac6mNYWB|vW7Lj0rrA8S+gq zgYS7=q+5&g1{Dk_#NygNhG1fdOf&j9OC-RI4)ZbV*HaYfC7#hwI99V<)stp~T%GXKI*#II z&i0F2RD{6W1p!1Yq81#(H&Lttxr#3BD zV%_vHGg1|R-yE`2z*38%#xf?U6+$f91@t@ z`}U@@MfAaYg(WcSrM*eaw)Gan0E9C>1UgE9^PrcyleB%NPVU}mqbglxv{BF_eh!?& zGB90FNu*=4Nw0N*8sd7g@l7u(kgQDy4mA& zkfNiN+1j5JUDQC4uITC=aciif$~Bfhw@_qAd!7h|uCXfI;`Mo)Ua!;WB;D-wIY{HL zL)J%`m3l07kn)9QcKLmth=MNkol4H9rJ^~k&KG9g7#d|XrbNGBr(*;VFl)>hed|D%E`Jx6dFUs z%F;p+-PsmBw4AM!S}1zrbPGu=;3qTGNva<|Aj0aZu66ia&KgtEnL^Oe=74Anhd3u+WZjLj zSt=daun;(~^LlhVG-O%5`oX!gNDS)G!O0?8x3prB9m=j0kZ7c#%QQK!SeOkua)_|# zEHIM-OGrso9iO0LF&vl>ll|U0!<0%8CM+N9`3$!esed{$%y|Zr% zG(E}ehewMfP+I>OF7k~jRE(h2!Rc5+Fp9)=+cOh}*g8UV>n#3;ul&{sR)^cbnSXvH z*wJM%Ny;|g;9^@;i4t>4zB!U~@0F3FFU(}YesDS>SAf$|gM+uhv)_Ufv*sUen2A7o!k<3>d-Bb!%KJu^F<=c z_MIhUW3E8aO0wP9q7IUWOTfc{%`(1$ZTfYl)5)f@%Kl9C$}y$n1(Ju?QcCKE4ZFA2 z2>n21gx z5Ba9yeyA~_M*f%&PTqS?5(`wkpfG$6m#5U>r9(%~$ELgCNU^p5ft3gIP1 z3v8Y{1&@IyvSE~nbN}DFZC3ZgP)picLrcJ~$1l51_wwi{l4{+KMvSIV)^Lu-a8W-1 z8%NX8)DQXvE`i^Yj53~HMaB)JnTm6`i0T|`U#_BlVWFw`3)kX-z}nJkkq6%3qH1x3 zyg_s#-ENZTb>l>0cxjaz0xB2F3kGy@y_?RTG5Gs#vV;Dk)JYvA*OE^MXEhI$$(&je z2P_#|IaxYX5{FL5*>KxBM;V>YqQT6GNs~{+!zpNZLS)L}J_=osHiH{R-2qd&C8TnEEtQ}uL%!i7 zb4qOWPIk+ft)xRq4y&V0$V0$Vm@>M?r6&upEDzREhs;^+;9!?ItKFUow$C*5 zh_S}d8J5brY6l%z zXaI6Uu^eszchgznW{pCbkVPRdukTeytwR_=#qZn_X}iGSN&a z3()=kWCM%&S@octeTi5cSNU@zw!ppYh)Or%dbpzUN_PQ!aVz4u5olFOg z+SJvQ>5&XoO_@v+K^jh0otN_hShP_#(8v7~?}Y$wgcZz?*HNk;JgkOPh|5!3<>0uS z8cd!-VG_}6ZsO1iIAJLqloF$vRyt}(7=bHQUY$az6;g)~w%dE#;V%k`cxvAm?Snc=*;eem<4V0`|)j;6!2GU-z?}^erja}ct%zDn?fbw_x^bWp|ZG82E+V!>#0 zPjE|p!{h%9aS(|F*5Ha77=uEFW)2--76yL)VBd? zONh(E>0YVMM$Ak#S7)%2gc_T< z9Mor}o|!|hztmmhgNzQV6k>0gN%DfUC8Qp*Y$yfbRHLtp$%lYdOPeL4JG)|$6GXgG zUoNN260x1@(^QdQ)cdXCE3=Fxzc%IOH-`V5Wh^;5ruIgX4+`I>Hd3Uo`waIq(q@2y zP7Y&7_#5?bwv3uBqV>H1;mg#0WXPDv$k`My=FS$GsEw2VF;s1+ckVfJMYmw4%0-d5)Qt%Nslaj&1rS%H$D%ajDLS^~QUpd%5()tJVr8_g{IQQiUmvNV2_JJ1(Ge>T$&Hr z*{%(Ki;SibQb?qY$hfYVE4p0Ya_pJQNjOmcNx`fHJ9?dv;-FMK+|FE+iZ^QV=Usb? zV$|F;45#C4RhXk&=8-YboAL`toqEYqAIWj zr!J<-l*JSj1}3(6vEg?mnrq#<$LIgx4;PDWW_=GS3u(Pgc%b8nw~0gW11Zd8S#-Oo z0~*qvH^C1^{ZUQorxprjLQ3k`AA=}7O*-!|%zvu9^A1X#f+Fl8pSgpUm3S`PLGvLa z8hIxP0OHW)%~tF(rxh-{D0NRKV6&5yJzTBe94O`Rk2?cBXWm6U(t{GDl#uXt%+LZegLF-t%Z`(=6I*YVo6iHf%Em~G$2CE^Yf42`qt=q01C zfF3S+GF+%}lCyxE7O;NotV0!Af~W`J%3{sEB2wOQ54hK-THx|xD zi8Jv2a^k(@78$_cPW}CQF9lHc`^3|LAG=S~^Ur8G_kJ?3_uMbma0|@uK_?Z`b+3rF zgczC%+VqiEAE0y{F!x2et87>%#sNRt28;- ziA!aU@f}~wNw^wXMUEw;TK5X*R2HuSFQC<2=5&?niBmre-&XG! z%WD8FamL(1iKB$V7C)yIuRcp?rofRaQ&zDPf(IK0z#Bj#tc;e+SCMxMjB)bPDiLR{ zuH&G_Kv0$V*`aH2nJHo15vz2uXJfR9ldD&AXsUM`D-W#>EdJ%yGzs$LYaMXiL90a? zpmt4%SYxz{V-(DoZ(Tz>4oq?K#2S*_D{Ck$hMx?Xx0bqJw@zeO9A)|~G%%+}SgYu! z8}3F?nt&loakF<%%qwZOK2w3qIvfmWd}~0>CqK;1Q57zGnD(E-RKHi+DW8$4+m=rGjAnJ5jTCEeIrmSR zDpUJU+1XJV&5m?fn@0MN*7$_Rnzap<^>;r_iU?;nwGGWEUh&%H#B5Y9`<`e6ClN5r z2!g??ba?pwbOYH>cuSFJHk@1Cd0e!C(!f}vKtY`j4_DML`*l3#N{ItfoWJwW1XYaNR)US zr9RT7ojt^d87(B*sWl(?qU1w_jz z4?@Q7K%Ud7mour>(cU5*rj&c&HkI50EL%b#^rt0<624i%k;0S`IHr2!Pi1c@T!K^N zO*1+9cwqFOAE(iwdA!6=3sb6`KE8eD2QUEQt1)W0Wjw6|(l+y0m%GYvx&Z(?UdI_* zs7{mJpI|9|f)*Th0TGUe_k*uc@uAlXyH4hn5lX_c#*t}AJ{yq$p+o3zsyI!&(J z$-)FZ$TxR(>tuDXo_4c9+fOG}LWs1h8} z&!6PT;92;Xq^D>i5Q>45i=iby1;etO{}e4I&~!P__7pja^uTHD({QcIm%IM5jC`8h zT0fxsf`KyM;TvVTAjQ{G)EQso8+B&=Ha}42EB$+(5xw+riR2^#kmx=W!ebCQkgi6F z%WplQm%cLCQLA=i-5$NYC^aAlFN5A8P zZ!ump5b0lyoKPN$Q2#pQ1eO3Fpi!lNq->?YKUB87^gQKuaMR!soW~8%0O%Y=z5p>H z-0HkQ_Il0!7vW%rMhxg^BST8`TfluUh(2s}m`ncf0(lo;1B&GE-ISO#ZjZL? zCSMOOJu<@isla{c2`rL7?xyq-RHVwnJ#^mntG%+iDpKHhQ6k&-kYk5c;g&L2joa&B z#fpeI*zF+oiL7A?+z8a__mog6YhNVU#5zmJcwHYu3B!6&zUb#eC+RYLFX=kc$M^sY z)izu!;*%;&yaA)gc-w>m_2rDW-G?L`ZAS6>?q7zH>sF(b0#6e%Vi3L-BP79cs)o%c(EZfyd_7P@fAfjmXGgtLI-1^aiw9 zUVfP*5_xUr3YW)G&M!y+NR@qG>Eus`hg%>htv0fA=(3uPED{x*6%~dJ$&lZ^LLWzd zuWsbg3%8bg>RfDUs0a#Xtp3mhJ>;$X*h2=h>CerQKR%w*^X!4c3n~008Wap_xlxb{ zUZqc`T=?c!NhOgxd-PQib^UbDx35xm39sX1zx^B_U@TF;pS~R;7z$#+UWO^|apNmE z^-lJ4|9=0yJCxJpg4g`YjJ9t6Yk}>aZ|LuT(R?si00JI7% z2dI0R=+g%%JwTTf9(W?^7Z+%Y+7+4L*%59WRzXaL=YrTv_?{aJ_{a%4L5@7Ir&Szq z!dR$FoMZK~fMz9-NTV3K_8?6KXnc-9b{5{0ChGdxD0h?J9;6L50DOUM(B0rV&Eu}+ zLtuc3`hpwfZt}pJenkPhS?hC^lDwk0*kIDNmHInylrlN;kRdLP=%+CTmWd-3)Kp|4W|3D(fOg24le zkrN*J(qmA(UW7CNeE^AY*jqGF`k11(jE_l|v)>|7CVV5~;3#IuV;%6H-{P6C6ghJ8 z+kT-V347sf5P$vR8!tks-hZ=_CU?J00yY(Nj6a^r`oupSPL*?R{~B z5|c(eBTtACwn0SWbbHEtczG%xJ0ZF&_&^4FdHaN@RF*LveG*puVWw|HdMnf0Pl}Ps zGfaPt_$y5J`v7+R2-8g;0Dh9`*FJ!q{)Fk^Q-FWPH1rcAC!c~(_!|RjvC;`?M3~6O zr$j&74x(|Y*_z|SSebQNB-&o^gP8yty)ftzP2Kwv6Wpr`CfuNby~#Q z*25JiZ&H?)-#tz{bjmCv{wTWIT>gqotdKYSQN%9AOH}3#gI9ZY7A9Sm@D`No!s&fr z=h}lLxj4A39G(hxs%&oj4D#RfE}1*9;Y+_xEGk|KsNG4L!r;|k=Sobm!dL$AN0Fdl zd<%5=8K5!R2fEi8Sc${gL|2^=!<1)ph+cgLj>`e2_n#4im7`4m^9=ZUe?0?ErB4?A zNeof`#rPtmzhn9(q%Si4_dkgeWoa(;IqXBA7cqVRhoZl&v=8F=;Tc-w56SY}ha%oK z2a-h$W@%Bk#>lQ8f$NGHBnIu;lz$~k??^ zzh!3g9NG7r=q_8%L4cTX4*1_VC%VXA&%%q27MCemUO6W!inkO61&tP{l3J|=c5m~% z#SOPCXj7j6Sfx4>*A2L~PBwfZ;)VzKLYQyhwS3rrDAx_TwobnM3B1jqJ|2-5J`u$R zoqQftF5&I+`->n?I^A#{RFh7(oDbkzJJ1KtizI{R!g*0>BmPbFopR5okm-&5Sy*LD zj;X7ezZ3p#+XYZ(%qM@n4EH9_T@aB5(Eke9En98}_Q_X(WoSoa9U>Dy6_FiAGvjEn za&4|D(Ld0xPeq~8Z|-N%uk~VJpdJ^EuiGpaeh!9g@<0bfCDE&Zaa^%r+_@$d z{}l#2Bec{&D$Y)MVq$re78P0K!c0?qjE!y>?U>htdnsm-#g|1dUKL^w=2cl!Vu}ja zm%sDYMEVyCb|qc@*JWc3HsRLv<%w>Ruq(L+4g2B8VJG_k`zBnzBDxveum_p)RH;ev z4+J~W�aBndrNeroPetb*=C`lhSYjoR5Y6;v@jmdbNh!==8uVu>fqvHDYB8|Dufk z9Ymm#PfjpAV=$Uj;K+PvLcRQaKESTfVG7k#`~lodxSo4uSk`_nlKI0(q6Qc=2$NkN z`&>jB07#U+0-I&*7h>q}#?y@vY6=F`3HT=y{xr^RoT2td;*G|Ijb~LmR#4m>gW#UV z6^#{*lhn~r*thx%u_AeWn(21y`W#mf{rB=_)9L)E(BX9Ds+!?UN>lznd)rRP>sMk3m{Eu7eaE>NQA5asZmIUA=ORh3^1jJM8IBKLOU_}gdlgLOiBC&*|5R#*&A{2++^*|~fDLYaLNTnf_gj70G$w+0$ z9)+d>OEUpj(x&!A+zxn?n$1WqB58#5Vx$iu>4fxSBoC2rLh>0IfJg=*g^UbBB$JTA zgyg8j2xS4J4v9gsCz3;v%0_CK{IbwwuegD@)e(q4PVGlBQi4c7LP{BNBGR9bazb*{ zN`&$N(iVcFG^30cO-Lx|i+$Z|$jA~J!HRz_AM zGLeuqjI2Xs5+M&0lB2dEG#Q}Q@Df>7WXh>pk4OU`>IS5yAtjNTj?_k^ZUU-V-2@~| z_Yp*90MevxAtXoLiqK4eaA}i9&BBI{BGyO^x9Mz;$sdYLMHP=@%^c*}fz({2oRF_kk=ljStw=qG z)MBKbN9s19n$;J8L??KUJW*`2FMW}U>PyIQConXrFY65Z5V{LnzlzinYQ3K-)z=ZZ zo7fL9auAWFguKbfVMJO8Il{L-B;+$fa?~$m z-cXZWe2E9PF6V1xco-O()jxBSZ~5m3{`rxA{w_}rHFaP56PJF5pHb_n*MBkcD>6MKx)tkpf(~UklF-PvuXkoWHAJh&D1cI5i=r>5Mp5@43RB>WHhNZh9VGZ TC#EipMDtJVzE#6a10((yk0o_E From 0b6ba103c5d8aa0323d8df9d1a679cda9aa57eda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Bolduc?= <16137441+Jouramie@users.noreply.github.com> Date: Mon, 9 Mar 2026 23:56:05 -0400 Subject: [PATCH 29/53] The Messenger: Universal Tracker support (#5344) --- worlds/messenger/__init__.py | 27 ++++++++++++++++-- worlds/messenger/subclasses.py | 3 +- worlds/messenger/transitions.py | 28 +++++++++++------- worlds/messenger/universal_tracker.py | 41 +++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 16 deletions(-) create mode 100644 worlds/messenger/universal_tracker.py diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index 88a0cec2ca..d1b672db7d 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -1,7 +1,8 @@ import logging from typing import Any, ClassVar, TextIO -from BaseClasses import CollectionState, Entrance, EntranceType, Item, ItemClassification, MultiWorld, Tutorial +from BaseClasses import CollectionState, Entrance, EntranceType, Item, ItemClassification, MultiWorld, Tutorial, \ + PlandoOptions from Options import Accessibility from Utils import output_path from settings import FilePath, Group @@ -18,6 +19,7 @@ from .rules import MessengerHardRules, MessengerOOBRules, MessengerRules from .shop import FIGURINES, PROG_SHOP_ITEMS, SHOP_ITEMS, USEFUL_SHOP_ITEMS, shuffle_shop_prices from .subclasses import MessengerItem, MessengerRegion, MessengerShopLocation from .transitions import disconnect_entrances, shuffle_transitions +from .universal_tracker import reverse_portal_exits_into_portal_plando, reverse_transitions_into_plando_connections components.append( Component( @@ -151,6 +153,10 @@ class MessengerWorld(World): reachable_locs: bool = False filler: dict[str, int] + @staticmethod + def interpret_slot_data(slot_data: dict[str, Any]) -> dict[str, Any]: + return slot_data + def generate_early(self) -> None: if self.options.goal == Goal.option_power_seal_hunt: self.total_seals = self.options.total_seals.value @@ -188,6 +194,11 @@ class MessengerWorld(World): self.spoiler_portal_mapping = {} self.transitions = [] + if hasattr(self.multiworld, "re_gen_passthrough"): + slot_data = self.multiworld.re_gen_passthrough.get(self.game) + if slot_data: + self.starting_portals = slot_data["starting_portals"] + def create_regions(self) -> None: # MessengerRegion adds itself to the multiworld # create simple regions @@ -279,6 +290,16 @@ class MessengerWorld(World): def connect_entrances(self) -> None: if self.options.shuffle_transitions: disconnect_entrances(self) + keep_entrance_logic = False + + if hasattr(self.multiworld, "re_gen_passthrough"): + slot_data = self.multiworld.re_gen_passthrough.get(self.game) + if slot_data: + self.multiworld.plando_options |= PlandoOptions.connections + self.options.portal_plando.value = reverse_portal_exits_into_portal_plando(slot_data["portal_exits"]) + self.options.plando_connections.value = reverse_transitions_into_plando_connections(slot_data["transitions"]) + keep_entrance_logic = True + add_closed_portal_reqs(self) # i need portal shuffle to happen after rules exist so i can validate it attempts = 20 @@ -295,7 +316,7 @@ class MessengerWorld(World): raise RuntimeError("Unable to generate valid portal output.") if self.options.shuffle_transitions: - shuffle_transitions(self) + shuffle_transitions(self, keep_entrance_logic) def write_spoiler_header(self, spoiler_handle: TextIO) -> None: if self.options.available_portals < 6: @@ -463,7 +484,7 @@ class MessengerWorld(World): "loc_data": {loc.address: {loc.item.name: [loc.item.code, loc.item.flags]} for loc in multiworld.get_filled_locations() if loc.address}, } - + output = orjson.dumps(data, option=orjson.OPT_NON_STR_KEYS) with open(out_path, "wb") as f: f.write(output) diff --git a/worlds/messenger/subclasses.py b/worlds/messenger/subclasses.py index 2e438fdbfd..8beed43027 100644 --- a/worlds/messenger/subclasses.py +++ b/worlds/messenger/subclasses.py @@ -1,8 +1,7 @@ from functools import cached_property from typing import TYPE_CHECKING -from BaseClasses import CollectionState, Entrance, EntranceType, Item, ItemClassification, Location, Region -from entrance_rando import ERPlacementState +from BaseClasses import CollectionState, Item, ItemClassification, Location, Region from .regions import LOCATIONS, MEGA_SHARDS from .shop import FIGURINES, SHOP_ITEMS diff --git a/worlds/messenger/transitions.py b/worlds/messenger/transitions.py index c0ae64c548..39ad591bf2 100644 --- a/worlds/messenger/transitions.py +++ b/worlds/messenger/transitions.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING -from BaseClasses import Entrance, Region +from BaseClasses import Region, CollectionRule from entrance_rando import EntranceType, randomize_entrances from .connections import RANDOMIZED_CONNECTIONS, TRANSITIONS from .options import ShuffleTransitions, TransitionPlando @@ -26,7 +26,6 @@ def disconnect_entrances(world: "MessengerWorld") -> None: entrance.randomization_type = er_type mock_entrance.randomization_type = er_type - for parent, child in RANDOMIZED_CONNECTIONS.items(): if child == "Corrupted Future": entrance = world.get_entrance("Artificer's Portal") @@ -36,8 +35,9 @@ def disconnect_entrances(world: "MessengerWorld") -> None: entrance = world.get_entrance(f"{parent} -> {child}") disconnect_entrance() -def connect_plando(world: "MessengerWorld", plando_connections: TransitionPlando) -> None: - def remove_dangling_exit(region: Region) -> None: + +def connect_plando(world: "MessengerWorld", plando_connections: TransitionPlando, keep_logic: bool = False) -> None: + def remove_dangling_exit(region: Region) -> CollectionRule: # find the disconnected exit and remove references to it for _exit in region.exits: if not _exit.connected_region: @@ -45,6 +45,7 @@ def connect_plando(world: "MessengerWorld", plando_connections: TransitionPlando else: raise ValueError(f"Unable to find randomized transition for {plando_connection}") region.exits.remove(_exit) + return _exit.access_rule def remove_dangling_entrance(region: Region) -> None: # find the disconnected entrance and remove references to it @@ -65,30 +66,35 @@ def connect_plando(world: "MessengerWorld", plando_connections: TransitionPlando else: dangling_exit = world.get_entrance("Artificer's Challenge") reg1.exits.remove(dangling_exit) + access_rule = dangling_exit.access_rule else: reg1 = world.get_region(plando_connection.entrance) - remove_dangling_exit(reg1) - + access_rule = remove_dangling_exit(reg1) + reg2 = world.get_region(plando_connection.exit) remove_dangling_entrance(reg2) # connect the regions - reg1.connect(reg2) + new_exit1 = reg1.connect(reg2) + if keep_logic: + new_exit1.access_rule = access_rule # pretend the user set the plando direction as "both" regardless of what they actually put on coupled if ((world.options.shuffle_transitions == ShuffleTransitions.option_coupled or plando_connection.direction == "both") and plando_connection.exit in RANDOMIZED_CONNECTIONS): - remove_dangling_exit(reg2) + access_rule = remove_dangling_exit(reg2) remove_dangling_entrance(reg1) - reg2.connect(reg1) + new_exit2 = reg2.connect(reg1) + if keep_logic: + new_exit2.access_rule = access_rule -def shuffle_transitions(world: "MessengerWorld") -> None: +def shuffle_transitions(world: "MessengerWorld", keep_logic: bool = False) -> None: coupled = world.options.shuffle_transitions == ShuffleTransitions.option_coupled plando = world.options.plando_connections if plando: - connect_plando(world, plando) + connect_plando(world, plando, keep_logic) result = randomize_entrances(world, coupled, {0: [0]}) diff --git a/worlds/messenger/universal_tracker.py b/worlds/messenger/universal_tracker.py new file mode 100644 index 0000000000..9d752031bf --- /dev/null +++ b/worlds/messenger/universal_tracker.py @@ -0,0 +1,41 @@ +from Options import PlandoConnection +from .connections import RANDOMIZED_CONNECTIONS +from .portals import REGION_ORDER, SHOP_POINTS, CHECKPOINTS +from .transitions import TRANSITIONS + +REVERSED_RANDOMIZED_CONNECTIONS = {v: k for k, v in RANDOMIZED_CONNECTIONS.items()} + + +def find_spot(portal_key: int) -> str: + """finds the spot associated with the portal key""" + parent = REGION_ORDER[portal_key // 100] + if portal_key % 100 == 0: + return f"{parent} Portal" + if portal_key % 100 // 10 == 1: + return SHOP_POINTS[parent][portal_key % 10] + return CHECKPOINTS[parent][portal_key % 10] + + +def reverse_portal_exits_into_portal_plando(portal_exits: list[int]) -> list[PlandoConnection]: + return [ + PlandoConnection("Autumn Hills", find_spot(portal_exits[0]), "both"), + PlandoConnection("Riviere Turquoise", find_spot(portal_exits[1]), "both"), + PlandoConnection("Howling Grotto", find_spot(portal_exits[2]), "both"), + PlandoConnection("Sunken Shrine", find_spot(portal_exits[3]), "both"), + PlandoConnection("Searing Crags", find_spot(portal_exits[4]), "both"), + PlandoConnection("Glacial Peak", find_spot(portal_exits[5]), "both"), + ] + + +def reverse_transitions_into_plando_connections(transitions: list[list[int]]) -> list[PlandoConnection]: + plando_connections = [] + + for connection in [ + PlandoConnection(REVERSED_RANDOMIZED_CONNECTIONS[TRANSITIONS[transition[0]]], TRANSITIONS[transition[1]], "both") + for transition in transitions + ]: + if connection.exit in {con.entrance for con in plando_connections}: + continue + plando_connections.append(connection) + + return plando_connections From 07a1ec0a1d1e958a24e9aeeb32cd5c1e8c8e4e5e Mon Sep 17 00:00:00 2001 From: josephwhite <22449090+josephwhite@users.noreply.github.com> Date: Tue, 10 Mar 2026 00:23:26 -0400 Subject: [PATCH 30/53] Test: Defaults for Options test (#5428) --- test/general/test_options.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/test/general/test_options.py b/test/general/test_options.py index e610e36794..6b08c8e9b0 100644 --- a/test/general/test_options.py +++ b/test/general/test_options.py @@ -1,7 +1,7 @@ import unittest from BaseClasses import PlandoOptions -from Options import Choice, ItemLinks, OptionSet, PlandoConnections, PlandoItems, PlandoTexts +from Options import Choice, TextChoice, ItemLinks, OptionSet, PlandoConnections, PlandoItems, PlandoTexts from Utils import restricted_dumps from worlds.AutoWorld import AutoWorldRegister @@ -16,6 +16,29 @@ class TestOptions(unittest.TestCase): with self.subTest(game=gamename, option=option_key): self.assertTrue(option.__doc__) + def test_option_defaults(self): + """Test that defaults for submitted options are valid.""" + for gamename, world_type in AutoWorldRegister.world_types.items(): + if not world_type.hidden: + for option_key, option in world_type.options_dataclass.type_hints.items(): + with self.subTest(game=gamename, option=option_key): + if issubclass(option, TextChoice): + self.assertTrue(option.default in option.name_lookup, + f"Default value {option.default} for TextChoice option {option.__name__} in" + f" {gamename} does not resolve to a listed value!" + ) + # Standard "can default generate" test + err_raised = None + try: + option.from_any(option.default) + except Exception as ex: + err_raised = ex + self.assertIsNone(err_raised, + f"Default value {option.default} for option {option.__name__} in {gamename}" + f" is not valid! Exception: {err_raised}" + ) + + def test_options_are_not_set_by_world(self): """Test that options attribute is not already set""" for gamename, world_type in AutoWorldRegister.world_types.items(): From 2c279cef09a53445aa4c25d4f5744ec6d45d2c24 Mon Sep 17 00:00:00 2001 From: Justus Lind Date: Tue, 10 Mar 2026 15:11:34 +1000 Subject: [PATCH 31/53] Muse Dash: Adds 3 new music packs plus fixes being able to roll songs without a legal way to obtain them (#5698) --- worlds/musedash/MuseDashCollection.py | 2 ++ worlds/musedash/MuseDashData.py | 28 ++++++++++++++++++++++++++- worlds/musedash/archipelago.json | 2 +- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/worlds/musedash/MuseDashCollection.py b/worlds/musedash/MuseDashCollection.py index 2a9f56750e..4c23a5c64f 100644 --- a/worlds/musedash/MuseDashCollection.py +++ b/worlds/musedash/MuseDashCollection.py @@ -28,6 +28,7 @@ class MuseDashCollections: "Miku in Museland", # Paid DLC not included in Muse Plus "Rin Len's Mirrorland", # Paid DLC not included in Muse Plus "MSR Anthology_Vol.02", # Goes away January 26, 2026. + "MD-level Tactical Training Blu-ray", # Goes away December 27, 2025. ] REMOVED_SONGS = [ @@ -38,6 +39,7 @@ class MuseDashCollections: "Tsukuyomi Ni Naru Replaced", "Heart Message feat. Aoi Tokimori Secret", "Meow Rock feat. Chun Ge, Yuan Shen", + "Stra Stella Secret", ] song_items = SONG_DATA diff --git a/worlds/musedash/MuseDashData.py b/worlds/musedash/MuseDashData.py index 4950c53e96..86486a4929 100644 --- a/worlds/musedash/MuseDashData.py +++ b/worlds/musedash/MuseDashData.py @@ -625,7 +625,7 @@ SONG_DATA: Dict[str, SongData] = { "Synthesis.": SongData(2900749, "83-1", "Cosmic Radio 2024", True, 6, 8, 10), "COSMiC FANFARE!!!!": SongData(2900750, "83-2", "Cosmic Radio 2024", False, 7, 9, 11), "Sharp Bubbles": SongData(2900751, "83-3", "Cosmic Radio 2024", True, 7, 9, 11), - "Replay": SongData(2900752, "83-4", "Cosmic Radio 2024", True, 5, 7, 9), + "Replay": SongData(2900752, "83-4", "Cosmic Radio 2024", False, 5, 7, 9), "Cosmic Dusty Girl": SongData(2900753, "83-5", "Cosmic Radio 2024", True, 5, 7, 9), "Meow Rock feat. Chun Ge, Yuan Shen": SongData(2900754, "84-0", "Muse Dash・Legend", True, None, None, None), "Even if you make an old radio song with AI": SongData(2900755, "84-1", "Muse Dash・Legend", False, 3, 6, 8), @@ -677,4 +677,30 @@ SONG_DATA: Dict[str, SongData] = { "City Lights": SongData(2900801, "90-3", "MEDIUM5 Echoes", True, 4, 6, 9), "Polaris Wandering Night": SongData(2900802, "90-4", "MEDIUM5 Echoes", True, 5, 8, 10), "Chasing the Moonlight": SongData(2900803, "90-5", "MEDIUM5 Echoes", True, 4, 6, 8), + "WILDCARD": SongData(2900804, "91-0", "48 Hours After Discharge", True, 3, 6, 9), + "It was all just a dream!": SongData(2900805, "91-1", "48 Hours After Discharge", True, 5, 7, 9), + "Science": SongData(2900806, "91-2", "48 Hours After Discharge", False, 4, 7, 9), + "Hit Maker": SongData(2900807, "91-3", "48 Hours After Discharge", False, 4, 6, 9), + "THX 4 playing": SongData(2900808, "91-4", "48 Hours After Discharge", True, 3, 5, 8), + "Theory of Existence": SongData(2900809, "91-5", "48 Hours After Discharge", True, 4, 6, 9), + "Kirakira Noel Story!!": SongData(2900810, "43-68", "MD Plus Project", False, 6, 8, 10), + "Fantasista LAST END": SongData(2900811, "92-0", "HARDCORE MOTTO TANO*C", True, 7, 9, 11), + "Colorful Universe": SongData(2900812, "92-1", "HARDCORE MOTTO TANO*C", True, 3, 6, 9), + "Future Flux": SongData(2900813, "92-2", "HARDCORE MOTTO TANO*C", True, 5, 8, 10), + "SOMEONE STOP ME!!!": SongData(2900814, "92-3", "HARDCORE MOTTO TANO*C", True, 6, 8, 10), + "Azathoth": SongData(2900815, "92-4", "HARDCORE MOTTO TANO*C", True, 6, 8, 10), + "Change the Game feat. Iori Matsunaga": SongData(2900816, "92-5", "HARDCORE MOTTO TANO*C", False, 6, 8, 10), + "Stra Stella Secret": SongData(2900817, "0-59", "Default Music", False, 6, 8, 10), + "Stra Stella": SongData(2900818, "0-60", "Default Music", False, 1, 4, None), + "Ultra-Digital Super Detox": SongData(2900819, "43-69", "MD Plus Project", False, 3, 6, 9), + "Otsukimi Koete Otsukiai": SongData(2900820, "43-70", "MD Plus Project", True, 6, 8, 10), + "Obenkyou Time": SongData(2900821, "43-71", "MD Plus Project", False, 6, 8, 11), + "Retry Now": SongData(2900822, "43-72", "MD Plus Project", False, 3, 6, 9), + "Master Bancho's Sushi Class ": SongData(2900823, "93-0", "Welcome to the Blue Hole!", False, None, None, None), + "CHAOTiC BATTLE": SongData(2900824, "94-0", "Cosmic Radio 2025", False, 7, 9, 11), + "FATAL GAME": SongData(2900825, "94-1", "Cosmic Radio 2025", False, 3, 6, 9), + "Aria": SongData(2900826, "94-2", "Cosmic Radio 2025", False, 4, 6, 9), + "+1 UNKNOWN -NUMBER": SongData(2900827, "94-3", "Cosmic Radio 2025", True, 4, 7, 10), + "To the Beyond, from the Nameless Seaside": SongData(2900828, "94-4", "Cosmic Radio 2025", False, 5, 8, 10), + "REK421": SongData(2900829, "94-5", "Cosmic Radio 2025", True, 7, 9, 11), } diff --git a/worlds/musedash/archipelago.json b/worlds/musedash/archipelago.json index 9b22a99605..dea7846b4f 100644 --- a/worlds/musedash/archipelago.json +++ b/worlds/musedash/archipelago.json @@ -1,6 +1,6 @@ { "game": "Muse Dash", "authors": ["DeamonHunter"], - "world_version": "1.5.26", + "world_version": "1.5.29", "minimum_ap_version": "0.6.3" } \ No newline at end of file From fd81553420297fbd74845bda76a9821d35d858ba Mon Sep 17 00:00:00 2001 From: Remy Jette Date: Tue, 10 Mar 2026 03:38:02 -0700 Subject: [PATCH 32/53] Fix missing } in example_nginx.conf (#6027) --- deploy/example_nginx.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/deploy/example_nginx.conf b/deploy/example_nginx.conf index a4f042739c..d44d1ffbd1 100644 --- a/deploy/example_nginx.conf +++ b/deploy/example_nginx.conf @@ -61,5 +61,6 @@ http { location = /favicon.ico { alias /app/WebHostLib/static/static/favicon.ico; access_log off; + } } } From c255ea8fc680236347485f2d92aa8d578070e9b1 Mon Sep 17 00:00:00 2001 From: Goo-Dang Date: Tue, 10 Mar 2026 13:52:59 -0400 Subject: [PATCH 33/53] Pokemon Emerald: Dexsanity Encounter Type Option (#6016) --------- Co-authored-by: Bryce Wilson --- worlds/pokemon_emerald/__init__.py | 4 +++- worlds/pokemon_emerald/locations.py | 2 +- worlds/pokemon_emerald/options.py | 14 +++++++++++++- worlds/pokemon_emerald/pokemon.py | 8 ++++++++ worlds/pokemon_emerald/rules.py | 2 +- 5 files changed, 26 insertions(+), 4 deletions(-) diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py index 4f2c2ef95c..10abed539f 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -123,6 +123,7 @@ class PokemonEmeraldWorld(World): blacklisted_wilds: Set[int] blacklisted_starters: Set[int] blacklisted_opponent_pokemon: Set[int] + allowed_dexsanity_species: set[int] hm_requirements: Dict[str, Union[int, List[str]]] auth: bytes @@ -142,6 +143,7 @@ class PokemonEmeraldWorld(World): self.blacklisted_wilds = set() self.blacklisted_starters = set() self.blacklisted_opponent_pokemon = set() + self.allowed_dexsanity_species = set() self.modified_maps = copy.deepcopy(emerald_data.maps) self.modified_species = copy.deepcopy(emerald_data.species) self.modified_tmhm_moves = [] @@ -265,6 +267,7 @@ class PokemonEmeraldWorld(World): from .regions import create_regions all_regions = create_regions(self) + randomize_wild_encounters(self) # Categories with progression items always included categories = { LocationCategory.BADGE, @@ -494,7 +497,6 @@ class PokemonEmeraldWorld(World): set_rules(self) def connect_entrances(self): - randomize_wild_encounters(self) self.shuffle_badges_hms() # For entrance randomization, disconnect entrances here, randomize map, then # undo badge/HM placement and re-shuffle them in the new map. diff --git a/worlds/pokemon_emerald/locations.py b/worlds/pokemon_emerald/locations.py index 49ce147041..fd8d0ebc7d 100644 --- a/worlds/pokemon_emerald/locations.py +++ b/worlds/pokemon_emerald/locations.py @@ -110,7 +110,7 @@ def create_locations_by_category(world: "PokemonEmeraldWorld", regions: Dict[str national_dex_id = int(location_name[-3:]) # Location names are formatted POKEDEX_REWARD_### # Don't create this pokedex location if player can't find it in the wild - if NATIONAL_ID_TO_SPECIES_ID[national_dex_id] in world.blacklisted_wilds: + if NATIONAL_ID_TO_SPECIES_ID[national_dex_id] in world.blacklisted_wilds or NATIONAL_ID_TO_SPECIES_ID[national_dex_id] not in world.allowed_dexsanity_species: continue location_id += POKEDEX_OFFSET + national_dex_id diff --git a/worlds/pokemon_emerald/options.py b/worlds/pokemon_emerald/options.py index 29929bd672..9529be877e 100644 --- a/worlds/pokemon_emerald/options.py +++ b/worlds/pokemon_emerald/options.py @@ -4,7 +4,7 @@ Option definitions for Pokemon Emerald from dataclasses import dataclass from Options import (Choice, DeathLink, DefaultOnToggle, OptionSet, NamedRange, Range, Toggle, FreeText, - PerGameCommonOptions, OptionGroup, StartInventory) + PerGameCommonOptions, OptionGroup, StartInventory, OptionList) from .data import data @@ -129,6 +129,17 @@ class Dexsanity(Toggle): display_name = "Dexsanity" +class DexsanityEncounterTypes(OptionList): + """ + Determines which Dexsanity encounter areas are in logic. + + Logic will only consider access to Pokemon at these encounter types, but they may still be found elsewhere. + """ + display_name = "Dexsanity Encounter Types" + valid_keys = {"Land", "Water", "Fishing"} + default = valid_keys.copy() + + class Trainersanity(Toggle): """ Defeating a trainer gives you an item. @@ -870,6 +881,7 @@ class PokemonEmeraldOptions(PerGameCommonOptions): npc_gifts: RandomizeNpcGifts berry_trees: RandomizeBerryTrees dexsanity: Dexsanity + dexsanity_encounter_types: DexsanityEncounterTypes trainersanity: Trainersanity item_pool_type: ItemPoolType diff --git a/worlds/pokemon_emerald/pokemon.py b/worlds/pokemon_emerald/pokemon.py index 8f799ce611..73af6c4658 100644 --- a/worlds/pokemon_emerald/pokemon.py +++ b/worlds/pokemon_emerald/pokemon.py @@ -264,6 +264,12 @@ def _rename_wild_events(world: "PokemonEmeraldWorld", map_data: MapData, new_slo def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None: + encounter_table = { + "Land": EncounterType.LAND, + "Water": EncounterType.WATER, + "Fishing": EncounterType.FISHING, + } + enabled_encounters = {encounter_table[encounter_type] for encounter_type in world.options.dexsanity_encounter_types.value} if world.options.wild_pokemon == RandomizeWildPokemon.option_vanilla: return @@ -370,6 +376,8 @@ def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None: # Actually create the new list of slots and encounter table new_slots: List[int] = [] + if encounter_type in enabled_encounters: + world.allowed_dexsanity_species.update(table.slots) for species_id in table.slots: new_slots.append(species_old_to_new_map[species_id]) diff --git a/worlds/pokemon_emerald/rules.py b/worlds/pokemon_emerald/rules.py index 828eb20f72..eeadb8bea2 100644 --- a/worlds/pokemon_emerald/rules.py +++ b/worlds/pokemon_emerald/rules.py @@ -1548,7 +1548,7 @@ def set_rules(world: "PokemonEmeraldWorld") -> None: for i in range(NUM_REAL_SPECIES): species = data.species[NATIONAL_ID_TO_SPECIES_ID[i + 1]] - if species.species_id in world.blacklisted_wilds: + if species.species_id in world.blacklisted_wilds or species.species_id not in world.allowed_dexsanity_species: continue set_rule( From 1a8a71f59343ce1c285624ef2faaf744fa7d3c30 Mon Sep 17 00:00:00 2001 From: Matthew Wells <91291346+richarm4@users.noreply.github.com> Date: Tue, 10 Mar 2026 10:54:24 -0700 Subject: [PATCH 34/53] Dark Souls 3: Update location descriptions for Red Tearstone Ring and Hood of Prayer (#5602) RTSR's description was incorrect and Hood of Prayer was missing its description --- worlds/dark_souls_3/Locations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/dark_souls_3/Locations.py b/worlds/dark_souls_3/Locations.py index b4e45fb577..569a0348d2 100644 --- a/worlds/dark_souls_3/Locations.py +++ b/worlds/dark_souls_3/Locations.py @@ -2025,13 +2025,13 @@ location_tables: Dict[str, List[DS3LocationData]] = { DS3LocationData("LC: Rusted Coin - chapel", "Rusted Coin x2"), DS3LocationData("LC: Braille Divine Tome of Lothric - wyvern room", "Braille Divine Tome of Lothric", hidden=True), # Hidden fall - DS3LocationData("LC: Red Tearstone Ring - chapel, drop onto roof", "Red Tearstone Ring"), + DS3LocationData("LC: Red Tearstone Ring - chapel, balcony before drop", "Red Tearstone Ring"), DS3LocationData("LC: Twinkling Titanite - moat, left side", "Twinkling Titanite x2"), DS3LocationData("LC: Large Soul of a Nameless Soldier - plaza left, by pillar", "Large Soul of a Nameless Soldier"), DS3LocationData("LC: Titanite Scale - altar", "Titanite Scale x3"), DS3LocationData("LC: Titanite Scale - chapel, chest", "Titanite Scale"), - DS3LocationData("LC: Hood of Prayer", "Hood of Prayer"), + DS3LocationData("LC: Hood of Prayer - ascent, chest at beginning", "Hood of Prayer"), DS3LocationData("LC: Robe of Prayer - ascent, chest at beginning", "Robe of Prayer"), DS3LocationData("LC: Skirt of Prayer - ascent, chest at beginning", "Skirt of Prayer"), DS3LocationData("LC: Spirit Tree Crest Shield - basement, chest", From c3659fb3ef43fa25a4b72fbb67564154a8407d31 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Tue, 10 Mar 2026 13:55:07 -0400 Subject: [PATCH 35/53] TUNIC: Refactor entrance hint generation (#5620) * Refactor hint generation * Remove debug print * Early out per qwint's comment --- worlds/tunic/__init__.py | 41 ++++++++++++++++++++++---------------- worlds/tunic/er_scripts.py | 8 ++++++++ 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 78c9dcdb67..2d7a632453 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -672,25 +672,29 @@ class TunicWorld(World): # Remove parentheses for better readability spoiler_handle.write(f'{ability[ability.find("(")+1:ability.find(")")]}: {self.ability_unlocks[ability]} Gold Questagons\n') - def extend_hint_information(self, hint_data: dict[int, dict[int, str]]) -> None: - if self.options.entrance_rando: - hint_data.update({self.player: {}}) - # all state seems to have efficient paths - all_state = self.multiworld.get_all_state(True) - all_state.update_reachable_regions(self.player) - paths = all_state.path - portal_names = {portal.name for portal in portal_mapping}.union({f"Shop Portal {i + 1}" for i in range(500)}) - for location in self.multiworld.get_locations(self.player): - # skipping event locations - if not location.address: + @classmethod + def stage_extend_hint_information(cls, multiworld: MultiWorld, hint_data: dict[int, dict[int, str]]) -> None: + tunic_er_worlds: list[TunicWorld] = [world for world in multiworld.get_game_worlds("TUNIC") + if world.options.entrance_rando] + if not tunic_er_worlds: + return + + hint_data.update({world.player: {} for world in tunic_er_worlds}) + all_state = multiworld.get_all_state() + paths = all_state.path + portal_names = {portal.name for portal in portal_mapping}.union({f"Shop Portal {i + 1}" for i in range(500)}) + for world in tunic_er_worlds: + all_state.update_reachable_regions(world.player) + for region in world.get_regions(): + if region.name == "Menu": continue - path_to_loc = [] + path_to_region = [] previous_name = "placeholder" try: - name, connection = paths[location.parent_region] + name, connection = paths[region] except KeyError: # logic bug, proceed with warning since it takes a long time to update AP - warning(f"{location.name} is not logically accessible for {self.player_name}. " + warning(f"{region.name} is not logically accessible for {world.player_name}. " "Creating entrance hint Inaccessible. Please report this to the TUNIC rando devs. " "If you are using Plando Items (excluding early locations), then this is likely the cause.") hint_text = "Inaccessible" @@ -703,11 +707,14 @@ class TunicWorld(World): # was getting some cases like Library Grave -> Library Grave -> other place if name in portal_names and name != previous_name: previous_name = name - path_to_loc.append(name) - hint_text = " -> ".join(reversed(path_to_loc)) + path_to_region.append(name) + hint_text = " -> ".join(reversed(path_to_region)) if hint_text: - hint_data[self.player][location.address] = hint_text + for location in region.get_locations(): + if location.address is None: + continue + hint_data[world.player][location.address] = hint_text def get_real_location(self, location: Location) -> tuple[str, int]: # if it's not in a group, it's not in an item link diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index a1b8b2fefd..60de72906d 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -29,6 +29,8 @@ def create_er_regions(world: "TunicWorld") -> dict[Portal, Portal]: world.used_shop_numbers = set() for region_name, region_data in world.er_regions.items(): + if region_name == "Zig Skip Exit": + continue if world.options.entrance_rando and region_name == "Zig Skip Exit": # need to check if there's a seed group for this first if world.options.entrance_rando.value not in EntranceRando.options.values(): @@ -773,11 +775,17 @@ def pair_portals(world: "TunicWorld", regions: dict[str, Region]) -> dict[Portal # loop through our list of paired portals and make two-way connections def create_randomized_entrances(world: "TunicWorld", portal_pairs: dict[Portal, Portal], regions: dict[str, Region]) -> None: for portal1, portal2 in portal_pairs.items(): + # this portal is completely inaccessible, so let's not make this connection + if portal1.region == "Zig Skip Exit": + continue # connect to the outlet region if there is one, if not connect to the actual region regions[portal1.region].connect( connecting_region=regions[get_portal_outlet_region(portal2, world)], name=portal1.name) if not world.options.decoupled or not world.options.entrance_rando: + # this portal is completely inaccessible, so let's not make this connection + if portal2.region == "Zig Skip Exit": + continue regions[portal2.region].connect( connecting_region=regions[get_portal_outlet_region(portal1, world)], name=portal2.name) From 4b37283d228e98586a58dbbf2a8423765af2969a Mon Sep 17 00:00:00 2001 From: josephwhite <22449090+josephwhite@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:57:48 -0400 Subject: [PATCH 36/53] WebHost: Update UTC datetime usage (timezone-naive) (#4906) --- Utils.py | 11 +++++++++++ WebHostLib/autolauncher.py | 8 ++++---- WebHostLib/customserver.py | 5 ++--- WebHostLib/landing.py | 7 ++++--- WebHostLib/misc.py | 12 +++++++----- WebHostLib/models.py | 8 +++++--- WebHostLib/tracker.py | 7 ++++--- test/hosting/webhost.py | 8 +++++--- 8 files changed, 42 insertions(+), 24 deletions(-) diff --git a/Utils.py b/Utils.py index c18298559a..627235f249 100644 --- a/Utils.py +++ b/Utils.py @@ -18,6 +18,8 @@ import logging import warnings from argparse import Namespace +from datetime import datetime, timezone + from settings import Settings, get_settings from time import sleep from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union, TypeGuard @@ -1291,6 +1293,15 @@ def is_iterable_except_str(obj: object) -> TypeGuard[typing.Iterable[typing.Any] return isinstance(obj, typing.Iterable) +def utcnow() -> datetime: + """ + Implementation of Python's datetime.utcnow() function for use after deprecation. + Needed for timezone-naive UTC datetimes stored in databases with PonyORM (upstream). + https://ponyorm.org/ponyorm-list/2014-August/000113.html + """ + return datetime.now(timezone.utc).replace(tzinfo=None) + + class DaemonThreadPoolExecutor(concurrent.futures.ThreadPoolExecutor): """ ThreadPoolExecutor that uses daemonic threads that do not keep the program alive. diff --git a/WebHostLib/autolauncher.py b/WebHostLib/autolauncher.py index 96ffbe9e95..b48c6a8cbb 100644 --- a/WebHostLib/autolauncher.py +++ b/WebHostLib/autolauncher.py @@ -4,14 +4,14 @@ import json import logging import multiprocessing import typing -from datetime import timedelta, datetime +from datetime import timedelta from threading import Event, Thread from typing import Any from uuid import UUID from pony.orm import db_session, select, commit, PrimaryKey -from Utils import restricted_loads +from Utils import restricted_loads, utcnow from .locker import Locker, AlreadyRunningException _stop_event = Event() @@ -129,10 +129,10 @@ def autohost(config: dict): with db_session: rooms = select( room for room in Room if - room.last_activity >= datetime.utcnow() - timedelta(days=3)) + room.last_activity >= utcnow() - timedelta(days=3)) for room in rooms: # we have to filter twice, as the per-room timeout can't currently be PonyORM transpiled. - if room.last_activity >= datetime.utcnow() - timedelta(seconds=room.timeout + 5): + if room.last_activity >= utcnow() - timedelta(seconds=room.timeout + 5): hosters[room.id.int % len(hosters)].start_room(room.id) except AlreadyRunningException: diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index e353cf2ab2..4257c6aff3 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -172,7 +172,7 @@ class WebHostContext(Context): room.multisave = pickle.dumps(self.get_save()) # saving only occurs on activity, so we can "abuse" this information to mark this as last_activity if not exit_save: # we don't want to count a shutdown as activity, which would restart the server again - room.last_activity = datetime.datetime.utcnow() + room.last_activity = Utils.utcnow() return True def get_save(self) -> dict: @@ -367,8 +367,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict, with db_session: # ensure the Room does not spin up again on its own, minute of safety buffer room = Room.get(id=room_id) - room.last_activity = datetime.datetime.utcnow() - \ - datetime.timedelta(minutes=1, seconds=room.timeout) + room.last_activity = Utils.utcnow() - datetime.timedelta(minutes=1, seconds=room.timeout) del room tear_down_logging(room_id) logging.info(f"Shutting down room {room_id} on {name}.") diff --git a/WebHostLib/landing.py b/WebHostLib/landing.py index 14e90cc28d..f1b8de21bf 100644 --- a/WebHostLib/landing.py +++ b/WebHostLib/landing.py @@ -1,8 +1,9 @@ -from datetime import timedelta, datetime +from datetime import timedelta from flask import render_template from pony.orm import count +from Utils import utcnow from WebHostLib import app, cache from .models import Room, Seed @@ -10,6 +11,6 @@ from .models import Room, Seed @app.route('/', methods=['GET', 'POST']) @cache.cached(timeout=300) # cache has to appear under app route for caching to work def landing(): - rooms = count(room for room in Room if room.creation_time >= datetime.utcnow() - timedelta(days=7)) - seeds = count(seed for seed in Seed if seed.creation_time >= datetime.utcnow() - timedelta(days=7)) + rooms = count(room for room in Room if room.creation_time >= utcnow() - timedelta(days=7)) + seeds = count(seed for seed in Seed if seed.creation_time >= utcnow() - timedelta(days=7)) return render_template("landing.html", rooms=rooms, seeds=seeds) diff --git a/WebHostLib/misc.py b/WebHostLib/misc.py index e30f1a6dd4..8d04fe984e 100644 --- a/WebHostLib/misc.py +++ b/WebHostLib/misc.py @@ -9,11 +9,12 @@ from flask import request, redirect, url_for, render_template, Response, session from pony.orm import count, commit, db_session from werkzeug.utils import secure_filename + from worlds.AutoWorld import AutoWorldRegister, World from . import app, cache from .markdown import render_markdown from .models import Seed, Room, Command, UUID, uuid4 -from Utils import title_sorted +from Utils import title_sorted, utcnow class WebWorldTheme(StrEnum): DIRT = "dirt" @@ -233,11 +234,12 @@ def host_room(room: UUID): if room is None: return abort(404) - now = datetime.datetime.utcnow() + now = utcnow() # indicate that the page should reload to get the assigned port - should_refresh = ((not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3)) - or room.last_activity < now - datetime.timedelta(seconds=room.timeout)) - + should_refresh = ( + (not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3)) + or room.last_activity < now - datetime.timedelta(seconds=room.timeout) + ) if now - room.last_activity > datetime.timedelta(minutes=1): # we only set last_activity if needed, otherwise parallel access on /room will cause an internal server error # due to "pony.orm.core.OptimisticCheckError: Object Room was updated outside of current transaction" diff --git a/WebHostLib/models.py b/WebHostLib/models.py index 7fa54f26a0..9060bc0ca4 100644 --- a/WebHostLib/models.py +++ b/WebHostLib/models.py @@ -2,6 +2,8 @@ from datetime import datetime from uuid import UUID, uuid4 from pony.orm import Database, PrimaryKey, Required, Set, Optional, buffer, LongStr +from Utils import utcnow + db = Database() STATE_QUEUED = 0 @@ -20,8 +22,8 @@ class Slot(db.Entity): class Room(db.Entity): id = PrimaryKey(UUID, default=uuid4) - last_activity = Required(datetime, default=lambda: datetime.utcnow(), index=True) - creation_time = Required(datetime, default=lambda: datetime.utcnow(), index=True) # index used by landing page + last_activity: datetime = Required(datetime, default=lambda: utcnow(), index=True) + creation_time: datetime = Required(datetime, default=lambda: utcnow(), index=True) # index used by landing page owner = Required(UUID, index=True) commands = Set('Command') seed = Required('Seed', index=True) @@ -38,7 +40,7 @@ class Seed(db.Entity): rooms = Set(Room) multidata = Required(bytes, lazy=True) owner = Required(UUID, index=True) - creation_time = Required(datetime, default=lambda: datetime.utcnow(), index=True) # index used by landing page + creation_time: datetime = Required(datetime, default=lambda: utcnow(), index=True) # index used by landing page slots = Set(Slot) spoiler = Optional(LongStr, lazy=True) meta = Required(LongStr, default=lambda: "{\"race\": false}") # additional meta information/tags diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index d1471aa658..cb40c8293f 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -10,7 +10,7 @@ from werkzeug.exceptions import abort from MultiServer import Context, get_saving_second from NetUtils import ClientStatus, Hint, NetworkItem, NetworkSlot, SlotType -from Utils import restricted_loads, KeyedDefaultDict +from Utils import restricted_loads, KeyedDefaultDict, utcnow from . import app, cache from .models import GameDataPackage, Room @@ -273,9 +273,10 @@ class TrackerData: Does not include players who have no activity recorded. """ last_activity: Dict[TeamPlayer, datetime.timedelta] = {} - now = datetime.datetime.utcnow() + now = utcnow() for (team, player), timestamp in self._multisave.get("client_activity_timers", []): - last_activity[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp) + from_timestamp = datetime.datetime.fromtimestamp(timestamp, datetime.timezone.utc).replace(tzinfo=None) + last_activity[team, player] = now - from_timestamp return last_activity diff --git a/test/hosting/webhost.py b/test/hosting/webhost.py index a8e70a50c2..286ef63a55 100644 --- a/test/hosting/webhost.py +++ b/test/hosting/webhost.py @@ -6,6 +6,7 @@ import zipfile from pathlib import Path from typing import TYPE_CHECKING, Iterable, Optional, cast +from Utils import utcnow from WebHostLib import to_python if TYPE_CHECKING: @@ -133,7 +134,7 @@ def stop_room(app_client: "FlaskClient", room_id: str, timeout: Optional[float] = None, simulate_idle: bool = True) -> None: - from datetime import datetime, timedelta + from datetime import timedelta from time import sleep from pony.orm import db_session @@ -151,10 +152,11 @@ def stop_room(app_client: "FlaskClient", with db_session: room: Room = Room.get(id=room_uuid) + now = utcnow() if simulate_idle: - new_last_activity = datetime.utcnow() - timedelta(seconds=room.timeout + 5) + new_last_activity = now - timedelta(seconds=room.timeout + 5) else: - new_last_activity = datetime.utcnow() - timedelta(days=3) + new_last_activity = now - timedelta(days=3) room.last_activity = new_last_activity address = f"localhost:{room.last_port}" if room.last_port > 0 else None if address: From 72ff9b1a7dace3063f95023a14f251362a9b9d29 Mon Sep 17 00:00:00 2001 From: LeonarthCG <33758848+LeonarthCG@users.noreply.github.com> Date: Tue, 10 Mar 2026 19:12:48 +0100 Subject: [PATCH 37/53] Saving Princess: Security fixes for issues detected by Bandit (#6013) * Saving Princess: absolute paths on suprocess.run * Saving Princess: more error handling for downloads * Saving Princess: rework launch_command setting Apparently subprocess.Popen requires a list for args instead of a string everywhere but in Windows, so the change was preventing the game from running on Linux. Additionally, the game is now launched using absolute paths. * Saving Princess: prevent bandit warnings * Saving Princess: remove unnecessary compare_digest * Saving Princess: fix Linux paths by using which * Saving Princess: rename launch command setting Previously, launch_command held a string. Now it holds a list of strings. Additionally, the defaults have changed. To prevent the wrong type from being used, the setting has been renamed, effectively abandoning the original launch_command setting. * Saving Princess: fix Linux default command return type Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- worlds/saving_princess/Client.py | 42 +++++++++++++++++++----------- worlds/saving_princess/__init__.py | 16 +++++++++--- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/worlds/saving_princess/Client.py b/worlds/saving_princess/Client.py index 29a97bb667..195a6a5703 100644 --- a/worlds/saving_princess/Client.py +++ b/worlds/saving_princess/Client.py @@ -1,4 +1,5 @@ import argparse +import ssl import zipfile from io import BytesIO @@ -8,12 +9,13 @@ import hashlib import json import logging import os + +import certifi import requests -import secrets import shutil -import subprocess +import subprocess # nosec from tkinter import messagebox -from typing import Any, Dict, Set +from typing import Any, Dict, Set, List import urllib import urllib.parse @@ -90,7 +92,7 @@ def get_timestamp(date: str) -> float: def send_request(request_url: str) -> UrlResponse: """Fetches status code and json response from given url""" - response = requests.get(request_url) + response = requests.get(request_url, timeout=10) if response.status_code == 200: # success try: data = response.json() @@ -129,13 +131,16 @@ def update(target_asset: str, url: str) -> bool: if update_available and messagebox.askyesnocancel(f"New {target_asset}", "Would you like to install the new version now?"): # unzip and patch - with urllib.request.urlopen(release_url) as download: + if not release_url.lower().startswith("https"): + raise ValueError(f'Unexpected scheme for url "{release_url}".') + context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=certifi.where()) + with urllib.request.urlopen(release_url, context=context) as download: # nosec with zipfile.ZipFile(BytesIO(download.read())) as zf: zf.extractall() patch_game() set_date(target_asset, newest_date) - except (ValueError, RuntimeError, urllib.error.HTTPError): - update_error = f"Failed to apply update." + except (ValueError, RuntimeError, urllib.error.HTTPError, urllib.error.URLError) as e: + update_error = f"Failed to apply update:\n{e}" messagebox.showerror("Failure", update_error) raise RuntimeError(update_error) return True @@ -158,8 +163,8 @@ def is_install_valid() -> bool: if not os.path.exists(file_name): return False with open(file_name, "rb") as clean: - current_hash = hashlib.md5(clean.read()).hexdigest() - if not secrets.compare_digest(current_hash, expected_hash): + current_hash = hashlib.md5(clean.read(), usedforsecurity=False).hexdigest() + if current_hash != expected_hash: return False return True @@ -189,12 +194,16 @@ def install() -> None: logging.info("Extracting files from cab archive.") if Utils.is_windows: - subprocess.run(["Extrac32", "/Y", "/E", "saving_princess.cab"]) + windows_path = os.environ["WINDIR"] + extractor_path = f"{windows_path}/System32/Extrac32" + subprocess.run([extractor_path, "/Y", "/E", "saving_princess.cab"]) #nosec else: - if shutil.which("wine") is not None: - subprocess.run(["wine", "Extrac32", "/Y", "/E", "saving_princess.cab"]) - elif shutil.which("7z") is not None: - subprocess.run(["7z", "e", "saving_princess.cab"]) + wine_path = shutil.which("wine") + p7zip_path = shutil.which("7z") + if wine_path is not None: + subprocess.run([wine_path, "Extrac32", "/Y", "/E", "saving_princess.cab"]) #nosec + elif p7zip_path is not None: + subprocess.run([p7zip_path, "e", "saving_princess.cab"]) #nosec else: error = "Could not find neither wine nor 7z.\n\nPlease install either the wine or the p7zip package." messagebox.showerror("Missing package!", f"Error: {error}") @@ -250,7 +259,10 @@ def launch(*args: str) -> Any: if SavingPrincessWorld.settings.launch_game: logging.info("Launching game.") try: - subprocess.Popen(f"{SavingPrincessWorld.settings.launch_command} {name} {password} {server}") + game: str = os.path.join(os.getcwd(), "Saving Princess v0_8.exe") + launch_command: List[str] = (SavingPrincessWorld.settings.launch_command_with_args + + [game, name, password, server]) + subprocess.Popen(launch_command) # nosec except FileNotFoundError: error = ("Could not run the game!\n\n" "Please check that launch_command in options.yaml or host.yaml is set up correctly.") diff --git a/worlds/saving_princess/__init__.py b/worlds/saving_princess/__init__.py index b4caf3828c..0c6208638a 100644 --- a/worlds/saving_princess/__init__.py +++ b/worlds/saving_princess/__init__.py @@ -1,3 +1,4 @@ +import shutil from typing import ClassVar, Dict, Any, Type, List, Union import Utils @@ -20,6 +21,15 @@ components.append( ) +def get_default_launch_command() -> List[str]: + """Returns platform-dependant default launch command for Saving Princess""" + if Utils.is_windows: + return [] + else: + wine_path = shutil.which("wine") + return [wine_path] if wine_path is not None else ["/usr/bin/wine"] + + class SavingPrincessSettings(Group): class GamePath(UserFilePath): """Path to the game executable from which files are extracted""" @@ -34,17 +44,17 @@ class SavingPrincessSettings(Group): class LaunchGame(Bool): """Set this to false to never autostart the game""" - class LaunchCommand(str): + class LaunchCommandWithArgs(List[str]): """ The console command that will be used to launch the game The command will be executed with the installation folder as the current directory + Additional items in the list will be passed in as arguments """ exe_path: GamePath = GamePath("Saving Princess.exe") install_folder: InstallFolder = InstallFolder("Saving Princess") launch_game: Union[LaunchGame, bool] = True - launch_command: LaunchCommand = LaunchCommand('"Saving Princess v0_8.exe"' if Utils.is_windows - else 'wine "Saving Princess v0_8.exe"') + launch_command_with_args: LaunchCommandWithArgs = LaunchCommandWithArgs(get_default_launch_command()) class SavingPrincessWeb(WebWorld): From 94136ac223b88a3b82d13c6205cb0603496f9f77 Mon Sep 17 00:00:00 2001 From: Duck <31627079+duckboycool@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:18:03 -0600 Subject: [PATCH 38/53] Docs: Add references to running from source (#6022) --- docs/adding games.md | 3 ++- docs/apworld specification.md | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/adding games.md b/docs/adding games.md index 9149e9f626..a977109bde 100644 --- a/docs/adding games.md +++ b/docs/adding games.md @@ -87,7 +87,8 @@ The world is your game integration for the Archipelago generator, webhost, and m information necessary for creating the items and locations to be randomized, the logic for item placement, the datapackage information so other game clients can recognize your game data, and documentation. Your world must be written as a Python package to be loaded by Archipelago. This is currently done by creating a fork of the Archipelago -repository and creating a new world package in `/worlds/`. +repository and creating a new world package in `/worlds/` (see [running from source](/docs/running%20from%20source.md) +for setup). The base World class can be found in [AutoWorld](/worlds/AutoWorld.py). Methods available for your world to call during generation can be found in [BaseClasses](/BaseClasses.py) and [Fill](/Fill.py). Some examples and documentation diff --git a/docs/apworld specification.md b/docs/apworld specification.md index 591ce51cae..7e13d6ccd3 100644 --- a/docs/apworld specification.md +++ b/docs/apworld specification.md @@ -46,8 +46,8 @@ which is the correct way to package your `.apworld` as a world developer. Do not ### "Build APWorlds" Launcher Component -In the Archipelago Launcher, there is a "Build APWorlds" component that will package all world folders to `.apworld`, -and add `archipelago.json` manifest files to them. +In the Archipelago Launcher (on [source only](/docs/running%20from%20source.md)), there is a "Build APWorlds" +component that will package all world folders to `.apworld`, and add `archipelago.json` manifest files to them. These .apworld files will be output to `build/apworlds` (relative to the Archipelago root directory). The `archipelago.json` file in each .apworld will automatically include the appropriate `version` and `compatible_version`. From d000c0f265c1f184df2cf44e193d98387dc34d05 Mon Sep 17 00:00:00 2001 From: Gryphonlady Date: Tue, 10 Mar 2026 13:23:25 -0500 Subject: [PATCH 39/53] Docs: Update plando_en.md with item group example (#6024) * Update plando_en.md with item group example Added example YAML block for item placement using an item group, including recommendation of use of `true` value with item groups to avoid unintended behaviors, with an example of the same. Adjustments more than welcome! * Made clarifying revision to description of Generator handling of item groups Clarified the behavior of the Generator regarding item creation when item groups are used in plando. --- worlds/generic/docs/plando_en.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/worlds/generic/docs/plando_en.md b/worlds/generic/docs/plando_en.md index 69f59c739e..625601d96b 100644 --- a/worlds/generic/docs/plando_en.md +++ b/worlds/generic/docs/plando_en.md @@ -216,6 +216,28 @@ dungeon major item chests. Because the from_pool value is `false`, a copy of the while the originals remain in the item pool to be shuffled. The second block will place the Kokiri Sword in the Deku Tree Slingshot Chest, again not from the pool. +```yaml + plando_items: + # Example block - Hollow Knight + - items: + Claw : true + world: + - BobsWitness + - BobsRogueLegacy +``` +This block will attempt to place all items in the Claw item group into any locations within the game slots named +"BobsWitness" and "BobsRogueLegacy." + +**NOTE:** As item groups may contain items that are not currently present in the item pool, use of `true` with +item groups, as shown here, is strongly recommended to avoid creation of unintended items. + +For example, the Claw item group for Hollow Knight includes Mantis_Claw, Left_Mantis_Claw, and Right_Mantis_Claw. +Depending on a different yaml setting, the Generator will create either one Mantis_Claw item, or one each of the +Left_Mantis_Claw and Right_Mantis_Claw items. By default, the Generator will create any missing item(s) in addition +to using the intended item(s), resulting in placement of all three items from the item group: Mantis_Claw, +Left_Mantis_Claw and Right_Mantis_Claw. Use of the true value, as shown in the example, restricts the Generator to +using only the items from the item group that are already present in the item pool. + ## Boss Plando This is currently only supported by A Link to the Past and Kirby's Dream Land 3. Boss plando allows a player to place a From f00d29e07203518bae5d87f463284830b36f8ff4 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 10 Mar 2026 18:56:23 +0000 Subject: [PATCH 40/53] Tests: fix race in test hosting shutdown (#5987) --- test/hosting/webhost.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/hosting/webhost.py b/test/hosting/webhost.py index 286ef63a55..0250445551 100644 --- a/test/hosting/webhost.py +++ b/test/hosting/webhost.py @@ -190,6 +190,7 @@ def stop_room(app_client: "FlaskClient", if address: room.timeout = original_timeout room.last_activity = new_last_activity + room.commands.clear() # make sure there is no leftover /exit print("timeout restored") From 3235863f2e759d7eeeaabdfba4edd96de42cd12b Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 10 Mar 2026 18:57:04 +0000 Subject: [PATCH 41/53] WebHost: add stats show cli command (#5995) Usage: flask -A "WebHost:get_app()" stats show Currently only shows sum and top10 biggest games packages. --- WebHostLib/__init__.py | 2 ++ WebHostLib/cli/__init__.py | 8 ++++++++ WebHostLib/cli/stats.py | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 WebHostLib/cli/__init__.py create mode 100644 WebHostLib/cli/stats.py diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index f856eea4c5..d10c17bff8 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -11,6 +11,7 @@ from pony.flask import Pony from werkzeug.routing import BaseConverter from Utils import title_sorted, get_file_safe_name +from .cli import CLI UPLOAD_FOLDER = os.path.relpath('uploads') LOGS_FOLDER = os.path.relpath('logs') @@ -64,6 +65,7 @@ app.config["ASSET_RIGHTS"] = False cache = Cache() Compress(app) +CLI(app) def to_python(value: str) -> uuid.UUID: diff --git a/WebHostLib/cli/__init__.py b/WebHostLib/cli/__init__.py new file mode 100644 index 0000000000..a210e1475c --- /dev/null +++ b/WebHostLib/cli/__init__.py @@ -0,0 +1,8 @@ +from flask import Flask + + +class CLI: + def __init__(self, app: Flask) -> None: + from .stats import stats_cli + + app.cli.add_command(stats_cli) diff --git a/WebHostLib/cli/stats.py b/WebHostLib/cli/stats.py new file mode 100644 index 0000000000..85edfb4348 --- /dev/null +++ b/WebHostLib/cli/stats.py @@ -0,0 +1,36 @@ +import click +from flask.cli import AppGroup +from pony.orm import raw_sql + +from Utils import format_SI_prefix + +stats_cli = AppGroup("stats") + + +@stats_cli.command("show") +def show() -> None: + from pony.orm import db_session, select + + from WebHostLib.models import GameDataPackage + + total_games_package_count: int = 0 + total_games_package_size: int + top_10_package_sizes: list[tuple[int, str]] = [] + + with db_session: + data_length = raw_sql("LENGTH(data)") + data_length_desc = raw_sql("LENGTH(data) DESC") + data_length_sum = raw_sql("SUM(LENGTH(data))") + total_games_package_count = GameDataPackage.select().count() + total_games_package_size = select(data_length_sum for _ in GameDataPackage).first() # type: ignore + top_10_package_sizes = list( + select((data_length, dp.checksum) for dp in GameDataPackage) # type: ignore + .order_by(lambda _, _2: data_length_desc) + .limit(10) + ) + + click.echo(f"Total number of games packages: {total_games_package_count}") + click.echo(f"Total size of games packages: {format_SI_prefix(total_games_package_size, power=1024)}B") + click.echo(f"Top {len(top_10_package_sizes)} biggest games packages:") + for size, checksum in top_10_package_sizes: + click.echo(f" {checksum}: {size:>8d}") From 47e581bc306e1ffd80ef0293cceb58a2372f6836 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 10 Mar 2026 20:04:27 +0100 Subject: [PATCH 42/53] LttP: add manifest (#6005) --- worlds/AutoWorld.py | 2 +- worlds/alttp/Rom.py | 3 +-- worlds/alttp/archipelago.json | 6 ++++++ 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 worlds/alttp/archipelago.json diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 327746f1ce..04f0b61ff8 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -363,7 +363,7 @@ class World(metaclass=AutoWorldRegister): def __getattr__(self, item: str) -> Any: if item == "settings": - return self.__class__.settings + return getattr(self.__class__, item) raise AttributeError # overridable methods that get called by Main.py, sorted by execution order diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index 45b3ad39d9..8acff214f2 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -1699,8 +1699,7 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool # set rom name # 21 bytes - from Utils import __version__ - rom.name = bytearray(f'AP{__version__.replace(".", "")[0:3]}_{player}_{multiworld.seed:11}\0', 'utf8')[:21] + rom.name = bytearray(f'AP{local_world.world_version.as_simple_string().replace(".", "")[0:3]}_{player}_{multiworld.seed:11}\0', 'utf8')[:21] rom.name.extend([0] * (21 - len(rom.name))) rom.write_bytes(0x7FC0, rom.name) diff --git a/worlds/alttp/archipelago.json b/worlds/alttp/archipelago.json new file mode 100644 index 0000000000..19bbc03213 --- /dev/null +++ b/worlds/alttp/archipelago.json @@ -0,0 +1,6 @@ +{ + "game": "A Link to the Past", + "minimum_ap_version": "0.6.6", + "world_version": "5.1.0", + "authors": ["Berserker"] +} From 56c2272bfd7921bd1f42577583295ffd053589e3 Mon Sep 17 00:00:00 2001 From: Rjosephson Date: Tue, 10 Mar 2026 13:05:59 -0600 Subject: [PATCH 43/53] RoR2: Seekers of the Storm (SOTS) DLC Support (#5569) --- worlds/ror2/__init__.py | 48 +++++++++++++--- worlds/ror2/archipelago.json | 6 ++ worlds/ror2/docs/en_Risk of Rain 2.md | 11 +++- worlds/ror2/docs/setup_en.md | 8 +++ worlds/ror2/locations.py | 14 ++++- worlds/ror2/options.py | 38 ++++++++++++- worlds/ror2/regions.py | 75 ++++++++++++++++++++---- worlds/ror2/ror2environments.py | 59 ++++++++++++++++--- worlds/ror2/rules.py | 79 ++++++++++++++------------ worlds/ror2/test/test_any_goal.py | 18 ++++-- worlds/ror2/test/test_falseson_goal.py | 17 ++++++ 11 files changed, 302 insertions(+), 71 deletions(-) create mode 100644 worlds/ror2/archipelago.json create mode 100644 worlds/ror2/test/test_falseson_goal.py diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index 7873ae54bb..f52ff789eb 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -4,7 +4,10 @@ from .items import RiskOfRainItem, item_table, item_pool_weights, offset, filler from .locations import RiskOfRainLocation, item_pickups, get_locations from .rules import set_rules from .ror2environments import environment_vanilla_table, environment_vanilla_orderedstages_table, \ - environment_sotv_orderedstages_table, environment_sotv_table, collapse_dict_list_vertical, shift_by_offset + environment_sotv_orderedstages_table, environment_sotv_table, environment_sost_orderedstages_table, \ + environment_sost_table, collapse_dict_list_vertical, shift_by_offset, environment_vanilla_variants_table, \ + environment_vanilla_variant_orderedstages_table, environment_sots_variants_table, \ + environment_sots_variants_orderedstages_table from BaseClasses import Item, ItemClassification, Tutorial from .options import ItemWeights, ROR2Options, ror2_option_groups @@ -46,7 +49,7 @@ class RiskOfRainWorld(World): } location_name_to_id = item_pickups - required_client_version = (0, 5, 0) + required_client_version = (0, 6, 4) web = RiskOfWeb() total_revivals: int @@ -62,7 +65,9 @@ class RiskOfRainWorld(World): scavengers=self.options.scavengers_per_stage.value, scanners=self.options.scanner_per_stage.value, altars=self.options.altars_per_stage.value, - dlc_sotv=bool(self.options.dlc_sotv.value) + dlc_sotv=bool(self.options.dlc_sotv.value), + dlc_sots=bool(self.options.dlc_sots.value), + stage_variants=bool(self.options.stage_variants) ) ) self.total_revivals = int(self.options.total_revivals.value / 100 * @@ -71,6 +76,8 @@ class RiskOfRainWorld(World): self.total_revivals -= 1 if self.options.victory == "voidling" and not self.options.dlc_sotv: self.options.victory.value = self.options.victory.option_any + if self.options.victory == "falseson" and not self.options.dlc_sots: + self.options.victory.value = self.options.victory.option_any def create_regions(self) -> None: @@ -105,16 +112,39 @@ class RiskOfRainWorld(World): # figure out all available ordered stages for each tier environment_available_orderedstages_table = environment_vanilla_orderedstages_table + environments_pool = shift_by_offset(environment_vanilla_table, environment_offset) + # Vanilla Variants + if self.options.stage_variants: + environment_available_orderedstages_table = \ + collapse_dict_list_vertical(environment_available_orderedstages_table, + environment_vanilla_variant_orderedstages_table) if self.options.dlc_sotv: environment_available_orderedstages_table = \ collapse_dict_list_vertical(environment_available_orderedstages_table, environment_sotv_orderedstages_table) + if self.options.dlc_sots: + environment_available_orderedstages_table = \ + collapse_dict_list_vertical(environment_available_orderedstages_table, + environment_sost_orderedstages_table) + if self.options.dlc_sots and self.options.stage_variants: + environment_available_orderedstages_table = \ + collapse_dict_list_vertical(environment_available_orderedstages_table, + environment_sots_variants_orderedstages_table) - environments_pool = shift_by_offset(environment_vanilla_table, environment_offset) - + if self.options.stage_variants: + environment_offset_table = shift_by_offset(environment_vanilla_variants_table, environment_offset) + environments_pool = {**environments_pool, **environment_offset_table} if self.options.dlc_sotv: environment_offset_table = shift_by_offset(environment_sotv_table, environment_offset) environments_pool = {**environments_pool, **environment_offset_table} + if self.options.dlc_sots: + environment_offset_table = shift_by_offset(environment_sost_table, environment_offset) + environments_pool = {**environments_pool, **environment_offset_table} + # SOTS Variant Environments + if self.options.dlc_sots and self.options.stage_variants: + environment_offset_table = shift_by_offset(environment_sots_variants_table, environment_offset) + environments_pool = {**environments_pool, **environment_offset_table} + # percollect starting environment for stage 1 unlock = self.random.choices(list(environment_available_orderedstages_table[0].keys()), k=1) self.multiworld.push_precollected(self.create_item(unlock[0])) @@ -146,7 +176,9 @@ class RiskOfRainWorld(World): scavengers=self.options.scavengers_per_stage.value, scanners=self.options.scanner_per_stage.value, altars=self.options.altars_per_stage.value, - dlc_sotv=bool(self.options.dlc_sotv.value) + dlc_sotv=bool(self.options.dlc_sotv.value), + dlc_sots=bool(self.options.dlc_sots.value), + stage_variants=bool(self.options.stage_variants) ) ) # Create junk items @@ -223,7 +255,7 @@ class RiskOfRainWorld(World): "chests_per_stage", "shrines_per_stage", "scavengers_per_stage", "scanner_per_stage", "altars_per_stage", "total_revivals", "start_with_revive", "final_stage_death", "death_link", "require_stages", - "progressive_stages", casing="camel") + "progressive_stages", "stage_variants", "show_seer_portals", casing="camel") return { **options_dict, "seed": "".join(self.random.choice(string.digits) for _ in range(16)), @@ -254,7 +286,7 @@ class RiskOfRainWorld(World): event_loc.place_locked_item(RiskOfRainItem("Stage 5", ItemClassification.progression, None, self.player)) event_loc.show_in_spoiler = False event_region.locations.append(event_loc) - event_loc.access_rule = lambda state: state.has("Sky Meadow", self.player) + event_loc.access_rule = lambda state: state.has("Sky Meadow", self.player) or state.has("Helminth Hatchery", self.player) victory_region = self.multiworld.get_region("Victory", self.player) victory_event = RiskOfRainLocation(self.player, "Victory", None, victory_region) diff --git a/worlds/ror2/archipelago.json b/worlds/ror2/archipelago.json new file mode 100644 index 0000000000..78c54c1420 --- /dev/null +++ b/worlds/ror2/archipelago.json @@ -0,0 +1,6 @@ +{ + "game": "Risk of Rain 2", + "minimum_ap_version": "0.6.4", + "world_version": "1.5.0", + "authors": ["Kindasneaki"] +} \ No newline at end of file diff --git a/worlds/ror2/docs/en_Risk of Rain 2.md b/worlds/ror2/docs/en_Risk of Rain 2.md index 651c89a339..2acd133e26 100644 --- a/worlds/ror2/docs/en_Risk of Rain 2.md +++ b/worlds/ror2/docs/en_Risk of Rain 2.md @@ -88,12 +88,21 @@ Explore Mode items are: * `Commencement` * `All the Hidden Realms` -Dlc_Sotv items +DLC Survivors of the Void (SOTV) items * `Siphoned Forest` * `Aphelian Sanctuary` * `Sulfur Pools` * `Void Locus` +DLC Seekers of the Storm (SOTS) items + +* `Shattered Abodes`, `Vicious Falls`, `Disturbed Impact` +* `Reformed Altar` +* `Treeborn Colony`, `Golden Dieback` +* `Prime Meridian` +* `Helminth Hatchery` + + When an explore item is granted, it will unlock that environment and will now be accessible! The game will still pick randomly which environment is next, but it will first check to see if they are available. If you have multiple of the next environments unlocked, it will weight the game to have a ***higher chance*** to go to one you diff --git a/worlds/ror2/docs/setup_en.md b/worlds/ror2/docs/setup_en.md index 6acf2654a8..cef0885970 100644 --- a/worlds/ror2/docs/setup_en.md +++ b/worlds/ror2/docs/setup_en.md @@ -23,6 +23,13 @@ all necessary dependencies as well. Click on the `Start modded` button in the top left in `r2modman` to start the game with the Archipelago mod installed. +### Troubleshooting + +* The mod doesn't show up in game! + * `r2modman` looks for the game at its default directory. If you have the game installed somewhere else, + you can update `r2modman` by going to `Settings > Change Risk of Rain 2 folder` + and selecting the correct directory. + ## Configuring your YAML File ### What is a YAML and why do I need one? You can see the [basic multiworld setup guide](/tutorial/Archipelago/setup/en) here on the Archipelago website to learn @@ -59,6 +66,7 @@ also optionally connect to the multiworld using the text client, which can be fo ### In-Game Commands These commands are to be used in-game by using ``Ctrl + Alt + ` `` and then typing the following: + - `archipelago_reconnect` Reconnect to AP. - `archipelago_connect [password]` example: "archipelago_connect archipelago.gg 38281 SlotName". - `archipelago_deathlink true/false` Toggle deathlink. - `archipelago_disconnect` Disconnect from AP. diff --git a/worlds/ror2/locations.py b/worlds/ror2/locations.py index 13077b3e14..3297231152 100644 --- a/worlds/ror2/locations.py +++ b/worlds/ror2/locations.py @@ -3,7 +3,8 @@ from BaseClasses import Location from .options import TotalLocations, ChestsPerEnvironment, ShrinesPerEnvironment, ScavengersPerEnvironment, \ ScannersPerEnvironment, AltarsPerEnvironment from .ror2environments import compress_dict_list_horizontal, environment_vanilla_orderedstages_table, \ - environment_sotv_orderedstages_table + environment_sotv_orderedstages_table, environment_sost_orderedstages_table, \ + environment_sots_variants_orderedstages_table, environment_vanilla_variant_orderedstages_table class RiskOfRainLocation(Location): @@ -57,13 +58,20 @@ def get_environment_locations(chests: int, shrines: int, scavengers: int, scanne return locations -def get_locations(chests: int, shrines: int, scavengers: int, scanners: int, altars: int, dlc_sotv: bool) \ +def get_locations(chests: int, shrines: int, scavengers: int, scanners: int, altars: int, dlc_sotv: bool, + dlc_sots: bool, stage_variants: bool) \ -> Dict[str, int]: """Get a dictionary of locations for the orderedstage environments with the locations from the parameters.""" locations = {} orderedstages = compress_dict_list_horizontal(environment_vanilla_orderedstages_table) + if stage_variants: + orderedstages.update(compress_dict_list_horizontal(environment_vanilla_variant_orderedstages_table)) if dlc_sotv: orderedstages.update(compress_dict_list_horizontal(environment_sotv_orderedstages_table)) + if dlc_sots: + orderedstages.update(compress_dict_list_horizontal(environment_sost_orderedstages_table)) + if dlc_sots and stage_variants: + orderedstages.update(compress_dict_list_horizontal(environment_sots_variants_orderedstages_table)) # for every environment, generate the respective locations for environment_name, environment_index in orderedstages.items(): locations.update(get_environment_locations( @@ -86,4 +94,6 @@ location_table.update(get_locations( scanners=ScannersPerEnvironment.range_end, altars=AltarsPerEnvironment.range_end, dlc_sotv=True, + dlc_sots=True, + stage_variants=True )) diff --git a/worlds/ror2/options.py b/worlds/ror2/options.py index 381c5942b0..876a67b7fb 100644 --- a/worlds/ror2/options.py +++ b/worlds/ror2/options.py @@ -22,8 +22,9 @@ class Goal(Choice): class Victory(Choice): """ Mithrix: Defeat Mithrix in Commencement - Voidling: Defeat the Voidling in The Planetarium (DLC required! Will select any if not enabled.) + Voidling: Defeat the Voidling in The Planetarium (SOTV DLC required! Will select any if not enabled.) Limbo: Defeat the Scavenger in Hidden Realm: A Moment, Whole + Falseson: Defeat False son and gift an item to the altar in Prime Meridian (SOTS DLC required! Will select any if not enabled.) Any: Any victory in the game will count. See Final Stage Death for additional ways. """ display_name = "Victory Condition" @@ -31,6 +32,7 @@ class Victory(Choice): option_mithrix = 1 option_voidling = 2 option_limbo = 3 + option_falseson = 4 default = 0 @@ -138,18 +140,26 @@ class FinalStageDeath(Toggle): If not use the following to tell if final stage death will count: Victory: mithrix - only dying in Commencement will count. Victory: voidling - only dying in The Planetarium will count. - Victory: limbo - Obliterating yourself will count.""" + Victory: limbo - Obliterating yourself will count. + Victory: falseson - only dying in Prime Meridian will count.""" display_name = "Final Stage Death is Win" class DLC_SOTV(Toggle): """ - Enable if you are using SOTV DLC. + Enable if you are using Survivors of the Void DLC. Affects environment availability for Explore Mode. Adds Void Items into the item pool """ display_name = "Enable DLC - SOTV" +class DLC_SOTS(Toggle): + """ + Enable if you are using Seekers of the Storm DLC. + Affects environment availability for Explore Mode. + """ + display_name = "Enable DLC - SOTS" + class RequireStages(DefaultOnToggle): """Add Stage items to the pool to block access to the next set of environments.""" @@ -162,6 +172,23 @@ class ProgressiveStages(DefaultOnToggle): display_name = "Progressive Stages" +class StageVariants(Toggle): + """Enable if you want to include stage variants in the environment pool. + Stages included are: + - Distant Roost (2) + - Titanic Plains (2) + SOTS DLC Enabled: + - Vicious Falls + - Shattered Abodes + - Golden Dieback""" + display_name = "Include Stage Variants" + + +class ShowSeerPortals(DefaultOnToggle): + """Shows Seer Portals at the teleporter to allow choosing the next environment.""" + display_name = "Show Seer Portals" + + class GreenScrap(Range): """Weight of Green Scraps in the item pool. @@ -384,6 +411,8 @@ ror2_option_groups = [ AltarsPerEnvironment, RequireStages, ProgressiveStages, + StageVariants, + ShowSeerPortals, ]), OptionGroup("Classic Mode Options", [ TotalLocations, @@ -427,8 +456,11 @@ class ROR2Options(PerGameCommonOptions): start_with_revive: StartWithRevive final_stage_death: FinalStageDeath dlc_sotv: DLC_SOTV + dlc_sots: DLC_SOTS require_stages: RequireStages progressive_stages: ProgressiveStages + stage_variants: StageVariants + show_seer_portals: ShowSeerPortals death_link: DeathLink item_pickup_step: ItemPickupStep shrine_use_step: ShrineUseStep diff --git a/worlds/ror2/regions.py b/worlds/ror2/regions.py index def29b4728..780f66bcac 100644 --- a/worlds/ror2/regions.py +++ b/worlds/ror2/regions.py @@ -18,13 +18,10 @@ def create_explore_regions(ror2_world: "RiskOfRainWorld") -> None: multiworld = ror2_world.multiworld # Default Locations non_dlc_regions: Dict[str, RoRRegionData] = { - "Menu": RoRRegionData(None, ["Distant Roost", "Distant Roost (2)", - "Titanic Plains", "Titanic Plains (2)", + "Menu": RoRRegionData(None, ["Distant Roost", "Titanic Plains", "Verdant Falls"]), "Distant Roost": RoRRegionData([], ["OrderedStage_1"]), - "Distant Roost (2)": RoRRegionData([], ["OrderedStage_1"]), "Titanic Plains": RoRRegionData([], ["OrderedStage_1"]), - "Titanic Plains (2)": RoRRegionData([], ["OrderedStage_1"]), "Verdant Falls": RoRRegionData([], ["OrderedStage_1"]), "Abandoned Aqueduct": RoRRegionData([], ["OrderedStage_2"]), "Wetland Aspect": RoRRegionData([], ["OrderedStage_2"]), @@ -35,12 +32,30 @@ def create_explore_regions(ror2_world: "RiskOfRainWorld") -> None: "Sundered Grove": RoRRegionData([], ["OrderedStage_4"]), "Sky Meadow": RoRRegionData([], ["Hidden Realm: Bulwark's Ambry", "OrderedStage_5"]), } + non_dlc_variant_regions: Dict[str, RoRRegionData] = { + "Distant Roost (2)": RoRRegionData([], ["OrderedStage_1"]), + "Titanic Plains (2)": RoRRegionData([], ["OrderedStage_1"]), + } # SOTV Regions - dlc_regions: Dict[str, RoRRegionData] = { + dlc_sotv_regions: Dict[str, RoRRegionData] = { "Siphoned Forest": RoRRegionData([], ["OrderedStage_1"]), "Aphelian Sanctuary": RoRRegionData([], ["OrderedStage_2"]), "Sulfur Pools": RoRRegionData([], ["OrderedStage_3"]) } + + dlc_sost_regions: Dict[str, RoRRegionData] = { + "Shattered Abodes": RoRRegionData([], ["OrderedStage_1"]), + "Reformed Altar": RoRRegionData([], ["OrderedStage_2", "Treeborn Colony"]), + "Treeborn Colony": RoRRegionData([], ["OrderedStage_3", "Prime Meridian"]), + "Helminth Hatchery": RoRRegionData([], ["Hidden Realm: Bulwark's Ambry", "OrderedStage_5"]), + } + + dlc_sots_variant_regions: Dict[str, RoRRegionData] = { + "Viscous Falls": RoRRegionData([], ["OrderedStage_1"]), + "Disturbed Impact": RoRRegionData([], ["OrderedStage_1"]), + "Golden Dieback": RoRRegionData([], ["OrderedStage_3", "Prime Meridian"]), + } + other_regions: Dict[str, RoRRegionData] = { "Commencement": RoRRegionData(None, ["Victory", "Petrichor V"]), "OrderedStage_5": RoRRegionData(None, ["Hidden Realm: A Moment, Fractured", @@ -61,10 +76,15 @@ def create_explore_regions(ror2_world: "RiskOfRainWorld") -> None: "Hidden Realm: Bazaar Between Time": RoRRegionData(None, ["Void Fields"]), "Hidden Realm: Gilded Coast": RoRRegionData(None, None) } - dlc_other_regions: Dict[str, RoRRegionData] = { + dlc_sotv_other_regions: Dict[str, RoRRegionData] = { "The Planetarium": RoRRegionData(None, ["Victory", "Petrichor V"]), "Void Locus": RoRRegionData(None, ["The Planetarium"]) } + + dlc_sost_other_regions: Dict[str, RoRRegionData] = { + "Prime Meridian": RoRRegionData(None, ["Victory", "Petrichor V"]), + } + # Totals of each item chests = int(ror2_options.chests_per_stage) shrines = int(ror2_options.shrines_per_stage) @@ -72,8 +92,14 @@ def create_explore_regions(ror2_world: "RiskOfRainWorld") -> None: scanners = int(ror2_options.scanner_per_stage) newt = int(ror2_options.altars_per_stage) all_location_regions = {**non_dlc_regions} + if ror2_options.stage_variants: + all_location_regions.update(non_dlc_variant_regions) if ror2_options.dlc_sotv: - all_location_regions = {**non_dlc_regions, **dlc_regions} + all_location_regions.update(dlc_sotv_regions) + if ror2_options.dlc_sots: + all_location_regions.update(dlc_sost_regions) + if ror2_options.dlc_sots and ror2_options.stage_variants: + all_location_regions.update(dlc_sots_variant_regions) # Locations for key in all_location_regions: @@ -99,25 +125,52 @@ def create_explore_regions(ror2_world: "RiskOfRainWorld") -> None: all_location_regions[key].locations.append(f"{key}: Newt Altar {i + 1}") regions_pool: Dict = {**all_location_regions, **other_regions} - # DLC Locations + # Non DLC Variant Locations + if ror2_options.stage_variants: + non_dlc_regions["Menu"].region_exits.append("Distant Roost (2)") + non_dlc_regions["Menu"].region_exits.append("Titanic Plains (2)") + # SOTV DLC Locations if ror2_options.dlc_sotv: non_dlc_regions["Menu"].region_exits.append("Siphoned Forest") other_regions["OrderedStage_1"].region_exits.append("Aphelian Sanctuary") other_regions["OrderedStage_2"].region_exits.append("Sulfur Pools") other_regions["Void Fields"].region_exits.append("Void Locus") other_regions["Commencement"].region_exits.append("The Planetarium") - regions_pool: Dict = {**all_location_regions, **other_regions, **dlc_other_regions} + + # SOTS DLC Locations + if ror2_options.dlc_sots: + non_dlc_regions["Menu"].region_exits.append("Shattered Abodes") + other_regions["OrderedStage_1"].region_exits.append("Reformed Altar") + other_regions["OrderedStage_4"].region_exits.append("Helminth Hatchery") + + # SOTS Variant Locations + if ror2_options.dlc_sots and ror2_options.stage_variants: + non_dlc_regions["Menu"].region_exits.append("Viscous Falls") + non_dlc_regions["Menu"].region_exits.append("Disturbed Impact") + dlc_sost_regions["Reformed Altar"].region_exits.append("Golden Dieback") + + if ror2_options.dlc_sotv: + regions_pool.update(dlc_sotv_other_regions) + if ror2_options.dlc_sots: + regions_pool.update(dlc_sost_other_regions) # Check to see if Victory needs to be removed from regions if ror2_options.victory == "mithrix": other_regions["Hidden Realm: A Moment, Whole"].region_exits.pop(0) - dlc_other_regions["The Planetarium"].region_exits.pop(0) + dlc_sotv_other_regions["The Planetarium"].region_exits.pop(0) + dlc_sost_other_regions["Prime Meridian"].region_exits.pop(0) elif ror2_options.victory == "voidling": other_regions["Commencement"].region_exits.pop(0) other_regions["Hidden Realm: A Moment, Whole"].region_exits.pop(0) + dlc_sost_other_regions["Prime Meridian"].region_exits.pop(0) elif ror2_options.victory == "limbo": other_regions["Commencement"].region_exits.pop(0) - dlc_other_regions["The Planetarium"].region_exits.pop(0) + dlc_sotv_other_regions["The Planetarium"].region_exits.pop(0) + dlc_sost_other_regions["Prime Meridian"].region_exits.pop(0) + elif ror2_options.victory == "falseson": + other_regions["Commencement"].region_exits.pop(0) + other_regions["Hidden Realm: A Moment, Whole"].region_exits.pop(0) + dlc_sotv_other_regions["The Planetarium"].region_exits.pop(0) # Create all the regions for name, data in regions_pool.items(): diff --git a/worlds/ror2/ror2environments.py b/worlds/ror2/ror2environments.py index 61707b3362..40e63a35b1 100644 --- a/worlds/ror2/ror2environments.py +++ b/worlds/ror2/ror2environments.py @@ -4,11 +4,14 @@ from typing import Dict, List, TypeVar environment_vanilla_orderedstage_1_table: Dict[str, int] = { "Distant Roost": 7, # blackbeach - "Distant Roost (2)": 8, # blackbeach2 "Titanic Plains": 15, # golemplains - "Titanic Plains (2)": 16, # golemplains2 "Verdant Falls": 28, # lakes } +environment_vanilla_variant_orderedstage_1_table: Dict[str, int] = { + "Distant Roost (2)": 8, # blackbeach2 + "Titanic Plains (2)": 16, # golemplains2 +} + environment_vanilla_orderedstage_2_table: Dict[str, int] = { "Abandoned Aqueduct": 17, # goolake "Wetland Aspect": 12, # foggyswamp @@ -54,6 +57,34 @@ environment_sotv_special_table: Dict[str, int] = { "The Planetarium": 45, # voidraid } +environment_sost_orderstage_1_table: Dict[str, int] = { + "Shattered Abodes": 54, # village + +} +environment_sost_variant_orderstage_1_table: Dict[str, int] = { + "Viscous Falls": 34, # lakesnight + "Disturbed Impact": 55, # villagenight +} + +environment_sost_orderstage_2_table: Dict[str, int] = { + "Reformed Altar": 36, # lemuriantemple +} + +environment_sost_orderstage_3_table: Dict[str, int] = { + "Treeborn Colony": 21, # habitat +} +environment_sost_variant_orderstage_3_table: Dict[str, int] = { + "Golden Dieback": 22, # habitatfall +} + +environment_sost_orderstage_5_table: Dict[str, int] = { + "Helminth Hatchery": 23, # helminthroost +} + +environment_sost_special_table: Dict[str, int] = { + "Prime Meridian": 40, # meridian +} + X = TypeVar("X") Y = TypeVar("Y") @@ -100,18 +131,32 @@ environment_vanilla_orderedstages_table = \ environment_vanilla_table = \ {**compress_dict_list_horizontal(environment_vanilla_orderedstages_table), **environment_vanilla_hidden_realm_table, **environment_vanilla_special_table} +# Vanilla Variants +environment_vanilla_variant_orderedstages_table = \ + [environment_vanilla_variant_orderedstage_1_table] +environment_vanilla_variants_table = \ + {**compress_dict_list_horizontal(environment_vanilla_variant_orderedstages_table)} +# SoTV environment_sotv_orderedstages_table = \ [environment_sotv_orderedstage_1_table, environment_sotv_orderedstage_2_table, environment_sotv_orderedstage_3_table] environment_sotv_table = \ {**compress_dict_list_horizontal(environment_sotv_orderedstages_table), **environment_sotv_special_table} +# SoST +environment_sost_orderedstages_table = \ + [environment_sost_orderstage_1_table, environment_sost_orderstage_2_table, + environment_sost_orderstage_3_table, {}, environment_sost_orderstage_5_table] # There is no new stage 4 in SoST +environment_sost_table = \ + {**compress_dict_list_horizontal(environment_sost_orderedstages_table), **environment_sost_special_table} +# SOTS Variants +environment_sots_variants_orderedstages_table = \ + [environment_sost_variant_orderstage_1_table, {}, environment_sost_variant_orderstage_3_table] +environment_sots_variants_table = \ + {**compress_dict_list_horizontal(environment_sots_variants_orderedstages_table)} -environment_non_orderedstages_table = \ - {**environment_vanilla_hidden_realm_table, **environment_vanilla_special_table, **environment_sotv_special_table} -environment_orderedstages_table = \ - collapse_dict_list_vertical(environment_vanilla_orderedstages_table, environment_sotv_orderedstages_table) -environment_all_table = {**environment_vanilla_table, **environment_sotv_table} +environment_all_table = {**environment_vanilla_table, **environment_sotv_table, **environment_sost_table, + **environment_vanilla_variants_table, **environment_sots_variants_table} def shift_by_offset(dictionary: Dict[str, int], offset: int) -> Dict[str, int]: diff --git a/worlds/ror2/rules.py b/worlds/ror2/rules.py index f0ab9f2831..d8d92ca270 100644 --- a/worlds/ror2/rules.py +++ b/worlds/ror2/rules.py @@ -1,7 +1,9 @@ from worlds.generic.Rules import set_rule, add_rule from BaseClasses import MultiWorld from .locations import get_locations -from .ror2environments import environment_vanilla_orderedstages_table, environment_sotv_orderedstages_table +from .ror2environments import environment_vanilla_orderedstages_table, environment_sotv_orderedstages_table, \ + environment_sost_orderedstages_table, environment_vanilla_variant_orderedstages_table, \ + environment_sots_variants_orderedstages_table from typing import Set, TYPE_CHECKING if TYPE_CHECKING: @@ -43,6 +45,24 @@ def has_location_access_rule(multiworld: MultiWorld, environment: str, player: i multiworld.get_location(location_name, player).access_rule = \ lambda state: state.has(environment, player) +def explore_environment_location_rules(table, multiworld, player, chests, shrines, newts, scavengers, scanners): + for i in range(len(table)): + for environment_name, _ in table[i].items(): + # Make sure to go through each location + if scavengers == 1: + has_location_access_rule(multiworld, environment_name, player, scavengers, "Scavenger") + if scanners == 1: + has_location_access_rule(multiworld, environment_name, player, scanners, "Radio Scanner") + for chest in range(1, chests + 1): + has_location_access_rule(multiworld, environment_name, player, chest, "Chest") + for shrine in range(1, shrines + 1): + has_location_access_rule(multiworld, environment_name, player, shrine, "Shrine") + if newts > 0: + for newt in range(1, newts + 1): + has_location_access_rule(multiworld, environment_name, player, newt, "Newt Altar") + if i > 0: + has_stage_access_rule(multiworld, f"Stage {i}", i, environment_name, player) + def set_rules(ror2_world: "RiskOfRainWorld") -> None: player = ror2_world.player @@ -60,7 +80,9 @@ def set_rules(ror2_world: "RiskOfRainWorld") -> None: scavengers=ror2_options.scavengers_per_stage.value, scanners=ror2_options.scanner_per_stage.value, altars=ror2_options.altars_per_stage.value, - dlc_sotv=bool(ror2_options.dlc_sotv.value) + dlc_sotv=bool(ror2_options.dlc_sotv.value), + dlc_sots=bool(ror2_options.dlc_sots.value), + stage_variants=bool(ror2_options.stage_variants) ) ) @@ -101,40 +123,25 @@ def set_rules(ror2_world: "RiskOfRainWorld") -> None: newts = ror2_options.altars_per_stage.value scavengers = ror2_options.scavengers_per_stage.value scanners = ror2_options.scanner_per_stage.value - for i in range(len(environment_vanilla_orderedstages_table)): - for environment_name, _ in environment_vanilla_orderedstages_table[i].items(): - # Make sure to go through each location - if scavengers == 1: - has_location_access_rule(multiworld, environment_name, player, scavengers, "Scavenger") - if scanners == 1: - has_location_access_rule(multiworld, environment_name, player, scanners, "Radio Scanner") - for chest in range(1, chests + 1): - has_location_access_rule(multiworld, environment_name, player, chest, "Chest") - for shrine in range(1, shrines + 1): - has_location_access_rule(multiworld, environment_name, player, shrine, "Shrine") - if newts > 0: - for newt in range(1, newts + 1): - has_location_access_rule(multiworld, environment_name, player, newt, "Newt Altar") - if i > 0: - has_stage_access_rule(multiworld, f"Stage {i}", i, environment_name, player) - + # Vanilla stages + explore_environment_location_rules(environment_vanilla_orderedstages_table, multiworld, player, chests, shrines, newts, + scavengers, scanners) + # Vanilla Variant stages + if ror2_options.stage_variants: + explore_environment_location_rules(environment_vanilla_variant_orderedstages_table, multiworld, player, chests, shrines, newts, + scavengers, scanners) + # SoTv stages if ror2_options.dlc_sotv: - for i in range(len(environment_sotv_orderedstages_table)): - for environment_name, _ in environment_sotv_orderedstages_table[i].items(): - # Make sure to go through each location - if scavengers == 1: - has_location_access_rule(multiworld, environment_name, player, scavengers, "Scavenger") - if scanners == 1: - has_location_access_rule(multiworld, environment_name, player, scanners, "Radio Scanner") - for chest in range(1, chests + 1): - has_location_access_rule(multiworld, environment_name, player, chest, "Chest") - for shrine in range(1, shrines + 1): - has_location_access_rule(multiworld, environment_name, player, shrine, "Shrine") - if newts > 0: - for newt in range(1, newts + 1): - has_location_access_rule(multiworld, environment_name, player, newt, "Newt Altar") - if i > 0: - has_stage_access_rule(multiworld, f"Stage {i}", i, environment_name, player) + explore_environment_location_rules(environment_sotv_orderedstages_table, multiworld, player, chests, shrines, + newts, scavengers, scanners) + # SoTS stages + if ror2_options.dlc_sots: + explore_environment_location_rules(environment_sost_orderedstages_table, multiworld, player, chests, shrines, + newts, scavengers, scanners) + if ror2_options.dlc_sots and ror2_options.stage_variants: + explore_environment_location_rules(environment_sots_variants_orderedstages_table, multiworld, player, chests, shrines, + newts, scavengers, scanners) + has_entrance_access_rule(multiworld, "Hidden Realm: A Moment, Fractured", "Hidden Realm: A Moment, Whole", player) has_stage_access_rule(multiworld, "Stage 1", 1, "Hidden Realm: Bazaar Between Time", player) @@ -147,6 +154,8 @@ def set_rules(ror2_world: "RiskOfRainWorld") -> None: has_entrance_access_rule(multiworld, "Stage 5", "Void Locus", player) if ror2_options.victory == "voidling": has_all_items(multiworld, {"Stage 5", "The Planetarium"}, "Commencement", player) + if ror2_options.dlc_sots: + has_entrance_access_rule(multiworld, "Stage 5", "Prime Meridian", player) # Win Condition multiworld.completion_condition[player] = lambda state: state.has("Victory", player) diff --git a/worlds/ror2/test/test_any_goal.py b/worlds/ror2/test/test_any_goal.py index 18d4994419..7dbd549c9f 100644 --- a/worlds/ror2/test/test_any_goal.py +++ b/worlds/ror2/test/test_any_goal.py @@ -4,23 +4,33 @@ from . import RoR2TestBase class DLCTest(RoR2TestBase): options = { "dlc_sotv": "true", - "victory": "any" + "victory": "any", + "dlc_sots": "true", } def test_commencement_victory(self) -> None: - self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Victory"]) + self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Prime Meridian", + "Victory"]) self.assertBeatable(False) self.collect_by_name("Commencement") self.assertBeatable(True) def test_planetarium_victory(self) -> None: - self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Victory"]) + self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Prime Meridian", + "Victory"]) self.assertBeatable(False) self.collect_by_name("The Planetarium") self.assertBeatable(True) def test_moment_whole_victory(self) -> None: - self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Victory"]) + self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Prime Meridian", + "Victory"]) self.assertBeatable(False) self.collect_by_name("Hidden Realm: A Moment, Whole") self.assertBeatable(True) + def test_false_son_victory(self) -> None: + self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Prime Meridian", + "Victory"]) + self.assertBeatable(False) + self.collect_by_name("Prime Meridian") + self.assertBeatable(True) diff --git a/worlds/ror2/test/test_falseson_goal.py b/worlds/ror2/test/test_falseson_goal.py new file mode 100644 index 0000000000..3cf815c942 --- /dev/null +++ b/worlds/ror2/test/test_falseson_goal.py @@ -0,0 +1,17 @@ +from . import RoR2TestBase + + +class FalseSonGoalTest(RoR2TestBase): + options = { + "dlc_sots": "true", + "victory": "falseson", + "stage_variants": "true" + } + + def test_false_son(self) -> None: + self.collect_all_but(["Prime Meridian", "Victory"]) + self.assertFalse(self.can_reach_region("Prime Meridian")) + self.assertBeatable(False) + self.collect_by_name("Prime Meridian") + self.assertTrue(self.can_reach_region("Prime Meridian")) + self.assertBeatable(True) From a8e926a1a9dad779599c855da3ed74d528638ba2 Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Tue, 10 Mar 2026 19:08:20 +0000 Subject: [PATCH 44/53] Core: Make Generic ER only consider the current world in isolation (#4680) --- entrance_rando.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/entrance_rando.py b/entrance_rando.py index a417767036..47664cb685 100644 --- a/entrance_rando.py +++ b/entrance_rando.py @@ -186,9 +186,20 @@ class ERPlacementState: self.pairings = [] self.world = world self.coupled = coupled - self.collection_state = world.multiworld.get_all_state(False, True) self.entrance_lookup = entrance_lookup + # Construct an 'all state', similar to MultiWorld.get_all_state(), but only for the world which is having its + # entrances randomized. + single_player_all_state = CollectionState(world.multiworld, True) + player = world.player + for item in world.multiworld.itempool: + if item.player == player: + world.collect(single_player_all_state, item) + for item in world.get_pre_fill_items(): + world.collect(single_player_all_state, item) + single_player_all_state.sweep_for_advancements(world.get_locations()) + self.collection_state = single_player_all_state + @property def placed_regions(self) -> set[Region]: return self.collection_state.reachable_regions[self.world.player] @@ -226,7 +237,7 @@ class ERPlacementState: copied_state.blocked_connections[self.world.player].remove(source_exit) copied_state.blocked_connections[self.world.player].update(target_entrance.connected_region.exits) copied_state.update_reachable_regions(self.world.player) - copied_state.sweep_for_advancements() + copied_state.sweep_for_advancements(self.world.get_locations()) # test that at there are newly reachable randomized exits that are ACTUALLY reachable available_randomized_exits = copied_state.blocked_connections[self.world.player] for _exit in available_randomized_exits: @@ -402,7 +413,7 @@ def randomize_entrances( placed_exits, paired_entrances = er_state.connect(source_exit, target_entrance) # propagate new connections er_state.collection_state.update_reachable_regions(world.player) - er_state.collection_state.sweep_for_advancements() + er_state.collection_state.sweep_for_advancements(world.get_locations()) if on_connect: change = on_connect(er_state, placed_exits, paired_entrances) if change: From 3c802d03a1423320ac454c4f05e23dd4e2a304fe Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:11:23 -0400 Subject: [PATCH 45/53] DS3: Use remaining_fill instead of custom fill (#4397) --------- Co-authored-by: Mysteryem --- Fill.py | 1 + worlds/dark_souls_3/__init__.py | 25 +++++++------------------ 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/Fill.py b/Fill.py index 48ed7253d9..7bd5756627 100644 --- a/Fill.py +++ b/Fill.py @@ -280,6 +280,7 @@ def remaining_fill(multiworld: MultiWorld, item_to_place = itempool.pop() spot_to_fill: typing.Optional[Location] = None + # going through locations in the same order as the provided `locations` argument for i, location in enumerate(locations): if location_can_fill_item(location, item_to_place): # popping by index is faster than removing by content, diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index 8c9716c03d..4ee972be9c 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -6,6 +6,7 @@ from logging import warning from typing import cast, Any, Callable, Dict, Set, List, Optional, TextIO, Union from BaseClasses import CollectionState, MultiWorld, Region, Location, LocationProgressType, Entrance, Tutorial, ItemClassification +from Fill import remaining_fill from worlds.AutoWorld import World, WebWorld from worlds.generic.Rules import CollectionRule, ItemRule, add_rule, add_item_rule @@ -1473,6 +1474,7 @@ class DarkSouls3World(World): f"contain smoothed items, but only {len(converted_item_order)} items to smooth." ) + sorted_spheres = [] for sphere in locations_by_sphere: locations = [loc for loc in sphere if loc.item.name in names] @@ -1480,12 +1482,12 @@ class DarkSouls3World(World): offworld = ds3_world._shuffle([loc for loc in locations if loc.game != "Dark Souls III"]) onworld = sorted((loc for loc in locations if loc.game == "Dark Souls III"), key=lambda loc: loc.data.region_value) - # Give offworld regions the last (best) items within a given sphere - for location in onworld + offworld: - new_item = ds3_world._pop_item(location, converted_item_order) - location.item = new_item - new_item.location = location + sorted_spheres.extend(onworld) + sorted_spheres.extend(offworld) + + converted_item_order.reverse() + remaining_fill(multiworld, sorted_spheres, converted_item_order, name="DS3 Smoothing", check_location_can_fill=True) if ds3_world.options.smooth_upgrade_items: base_names = { @@ -1518,19 +1520,6 @@ class DarkSouls3World(World): self.random.shuffle(copy) return copy - def _pop_item( - self, - location: Location, - items: List[DarkSouls3Item] - ) -> DarkSouls3Item: - """Returns the next item in items that can be assigned to location.""" - for i, item in enumerate(items): - if location.can_fill(self.multiworld.state, item, False): - return items.pop(i) - - # If we can't find a suitable item, give up and assign an unsuitable one. - return items.pop(0) - def _get_our_locations(self) -> List[DarkSouls3Location]: return cast(List[DarkSouls3Location], self.multiworld.get_locations(self.player)) From 03b638d027edc7a5844d05bd99242ba4c3b140e0 Mon Sep 17 00:00:00 2001 From: Ixrec Date: Tue, 10 Mar 2026 19:49:47 +0000 Subject: [PATCH 46/53] Docs: Reword 'could be generated from json' to avoid encouraging slow world loads (#5960) --- docs/world api.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/world api.md b/docs/world api.md index 2df7b12744..4e23549304 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -491,9 +491,10 @@ class MyGameWorld(World): base_id = 1234 # instead of dynamic numbering, IDs could be part of data - # The following two dicts are required for the generation to know which - # items exist. They could be generated from json or something else. They can - # include events, but don't have to since events will be placed manually. + # The following two dicts are required for the generation to know which items exist. + # They can be generated with arbitrary code during world load, but keep in mind that + # anything expensive (e.g. parsing non-python data files) will delay world loading. + # They can include events, but don't have to since events will be placed manually. item_name_to_id = {name: id for id, name in enumerate(mygame_items, base_id)} location_name_to_id = {name: id for From 3016379b85efdb336a25f82c146933476380f62d Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Tue, 10 Mar 2026 22:06:44 +0000 Subject: [PATCH 47/53] KH2: Fix nondeterministic generation when CasualBounties is enabled (#5967) When CasualBounties was enabled, the location names in `exclusion_table["HitlistCasual"]` would be iterated into `self.random_super_boss_list` in `generate_early`, but `exclusion_table["HitlistCasual"]` was a `set`, so its iteration order would vary on each generation, even with same seed. Random location names would be picked from `self.random_super_boss_list` to place Bounty items at, so different locations could be picked on each generation with the same seed. `exclusion_table["Hitlist"]` is similar and was already a `list`, avoiding the issue of nondeterministic iteration order, so `exclusion_table["HitlistCasual"]` has been changed to a `list` to match. --- worlds/kh2/Locations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/kh2/Locations.py b/worlds/kh2/Locations.py index 3b5a6e7e69..4b423d255a 100644 --- a/worlds/kh2/Locations.py +++ b/worlds/kh2/Locations.py @@ -1281,7 +1281,7 @@ exclusion_table = { LocationName.HadesCupTrophyParadoxCups, LocationName.MusicalOrichalcumPlus, ], - "HitlistCasual": { + "HitlistCasual": [ LocationName.FuturePete, LocationName.BetwixtandBetweenBondofFlame, LocationName.GrimReaper2, @@ -1299,7 +1299,7 @@ exclusion_table = { LocationName.MCP, LocationName.Lvl50, LocationName.Lvl99 - }, + ], "Cups": { LocationName.ProtectBeltPainandPanicCup, LocationName.SerenityGemPainandPanicCup, From 260bae359de479348974ad587d696c5bfa8482b5 Mon Sep 17 00:00:00 2001 From: qwint Date: Wed, 11 Mar 2026 15:37:00 -0500 Subject: [PATCH 48/53] Core: Update .gitignore to include an exe setup.py downloads (#6031) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index cbc33e5858..8f9ed6df14 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ EnemizerCLI/ /SNI/ /sni-*/ /appimagetool* +/VC_redist.x64.exe /host.yaml /options.yaml /config.yaml From d01c9577ab697855ff421fdc6a750fefdc30e4b0 Mon Sep 17 00:00:00 2001 From: Duck <31627079+duckboycool@users.noreply.github.com> Date: Wed, 11 Mar 2026 16:46:59 -0600 Subject: [PATCH 49/53] CommonClient: Add explicit message for connection timeout (#5842) * Change timeout and add timeout-specific message * Revert open_timeout --- CommonClient.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CommonClient.py b/CommonClient.py index 5fbc0f1b06..3f98a4eff1 100755 --- a/CommonClient.py +++ b/CommonClient.py @@ -773,7 +773,7 @@ class CommonContext: if len(parts) == 1: parts = title.split(', ', 1) if len(parts) > 1: - text = parts[1] + '\n\n' + text + text = f"{parts[1]}\n\n{text}" if text else parts[1] title = parts[0] # display error self._messagebox = MessageBox(title, text, error=True) @@ -896,6 +896,8 @@ async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None) "May not be running Archipelago on that address or port.") except websockets.InvalidURI: ctx.handle_connection_loss("Failed to connect to the multiworld server (invalid URI)") + except asyncio.TimeoutError: + ctx.handle_connection_loss("Failed to connect to the multiworld server. Connection timed out.") except OSError: ctx.handle_connection_loss("Failed to connect to the multiworld server") except Exception: From 70fc3e05fbacd1bb58d381d6820a9bdb8099d9db Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 12 Mar 2026 02:48:45 +0100 Subject: [PATCH 50/53] Webhost: port reuse fix & configurable max room timeout (#6033) * WebHost: make autolauncher max room timeout configurable * WebHost: launch rooms with assigned port first --- WebHostLib/__init__.py | 2 ++ WebHostLib/autolauncher.py | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index d10c17bff8..f1ac7ad558 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -46,6 +46,8 @@ app.config["SELFGEN"] = True # application process is in charge of scheduling G app.config["JOB_THRESHOLD"] = 1 # after what time in seconds should generation be aborted, freeing the queue slot. Can be set to None to disable. app.config["JOB_TIME"] = 600 +# maximum time in seconds since last activity for a room to be hosted +app.config["MAX_ROOM_TIMEOUT"] = 259200 # memory limit for generator processes in bytes app.config["GENERATOR_MEMORY_LIMIT"] = 4294967296 diff --git a/WebHostLib/autolauncher.py b/WebHostLib/autolauncher.py index b48c6a8cbb..1a61564500 100644 --- a/WebHostLib/autolauncher.py +++ b/WebHostLib/autolauncher.py @@ -9,7 +9,7 @@ from threading import Event, Thread from typing import Any from uuid import UUID -from pony.orm import db_session, select, commit, PrimaryKey +from pony.orm import db_session, select, commit, PrimaryKey, desc from Utils import restricted_loads, utcnow from .locker import Locker, AlreadyRunningException @@ -129,7 +129,8 @@ def autohost(config: dict): with db_session: rooms = select( room for room in Room if - room.last_activity >= utcnow() - timedelta(days=3)) + room.last_activity >= utcnow() - timedelta( + seconds=config["MAX_ROOM_TIMEOUT"])).order_by(desc(Room.last_port)) for room in rooms: # we have to filter twice, as the per-room timeout can't currently be PonyORM transpiled. if room.last_activity >= utcnow() - timedelta(seconds=room.timeout + 5): From 8457ff3e4bd3de154f9d191a2551eb1d916c9c41 Mon Sep 17 00:00:00 2001 From: lepideble <147614625+lepideble@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:17:54 +0100 Subject: [PATCH 51/53] Factorio: only show fluid boxes on assembling machine 1 when the selected recipe needs fluids (#5412) --- worlds/factorio/data/mod_template/data-final-fixes.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/factorio/data/mod_template/data-final-fixes.lua b/worlds/factorio/data/mod_template/data-final-fixes.lua index 2ddcd8d8ab..e459523019 100644 --- a/worlds/factorio/data/mod_template/data-final-fixes.lua +++ b/worlds/factorio/data/mod_template/data-final-fixes.lua @@ -130,6 +130,7 @@ end data.raw["assembling-machine"]["assembling-machine-1"].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories) data.raw["assembling-machine"]["assembling-machine-2"].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories) data.raw["assembling-machine"]["assembling-machine-1"].fluid_boxes = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-2"].fluid_boxes) +data.raw["assembling-machine"]["assembling-machine-1"].fluid_boxes_off_when_no_fluid_recipe = data.raw["assembling-machine"]["assembling-machine-2"].fluid_boxes_off_when_no_fluid_recipe if mods["factory-levels"] then -- Factory-Levels allows the assembling machines to get faster (and depending on settings), more productive at crafting products, the more the -- assembling machine crafts the product. If the machine crafts enough, it may auto-upgrade to the next tier. From 2e5356ad05aa0af7585ecd047b1992a4e3103218 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 18 Mar 2026 03:30:22 +0100 Subject: [PATCH 52/53] Core: other resources guide (#6043) Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: Emily <35015090+EmilyV99@users.noreply.github.com> --- WebHostLib/templates/supportedGames.html | 4 +- docs/CODEOWNERS | 2 - setup.py | 1 - test/general/test_implemented.py | 4 +- test/general/test_options.py | 2 +- worlds/apsudoku/__init__.py | 34 --------------- worlds/apsudoku/docs/en_Sudoku.md | 15 ------- worlds/apsudoku/docs/setup_en.md | 55 ------------------------ worlds/generic/__init__.py | 5 ++- worlds/generic/docs/other_en.md | 37 ++++++++++++++++ 10 files changed, 47 insertions(+), 112 deletions(-) delete mode 100644 worlds/apsudoku/__init__.py delete mode 100644 worlds/apsudoku/docs/en_Sudoku.md delete mode 100644 worlds/apsudoku/docs/setup_en.md create mode 100644 worlds/generic/docs/other_en.md diff --git a/WebHostLib/templates/supportedGames.html b/WebHostLib/templates/supportedGames.html index 759e748056..43028721b2 100644 --- a/WebHostLib/templates/supportedGames.html +++ b/WebHostLib/templates/supportedGames.html @@ -33,7 +33,9 @@

Currently Supported Games

Below are the games that are currently included with the Archipelago software. To play a game that is not on this page, please refer to the playing with - custom worlds section of the setup guide.

+ custom worlds section of the setup guide and the + other games and tools guide + to find more.


diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 46afd30456..30b61f5c85 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -19,8 +19,6 @@ # NewSoupVi is acting maintainer, but world belongs to core with the exception of the music /worlds/apquest/ @NewSoupVi -# Sudoku (APSudoku) -/worlds/apsudoku/ @EmilyV99 # Aquaria /worlds/aquaria/ @tioui diff --git a/setup.py b/setup.py index 949b1e3e30..ebef3880fc 100644 --- a/setup.py +++ b/setup.py @@ -71,7 +71,6 @@ non_apworlds: set[str] = { "Ocarina of Time", "Overcooked! 2", "Raft", - "Sudoku", "Super Mario 64", "VVVVVV", "Wargroove", diff --git a/test/general/test_implemented.py b/test/general/test_implemented.py index add6e5321e..0bc7b62d5b 100644 --- a/test/general/test_implemented.py +++ b/test/general/test_implemented.py @@ -11,7 +11,7 @@ class TestImplemented(unittest.TestCase): def test_completion_condition(self): """Ensure a completion condition is set that has requirements.""" for game_name, world_type in AutoWorldRegister.world_types.items(): - if not world_type.hidden and game_name not in {"Sudoku"}: + if not world_type.hidden: with self.subTest(game_name): multiworld = setup_solo_multiworld(world_type) self.assertFalse(multiworld.completion_condition[1](multiworld.state)) @@ -59,7 +59,7 @@ class TestImplemented(unittest.TestCase): def test_prefill_items(self): """Test that every world can reach every location from allstate before pre_fill.""" for gamename, world_type in AutoWorldRegister.world_types.items(): - if gamename not in ("Archipelago", "Sudoku", "Final Fantasy", "Test Game"): + if gamename not in ("Archipelago", "Final Fantasy", "Test Game"): with self.subTest(gamename): multiworld = setup_solo_multiworld(world_type, ("generate_early", "create_regions", "create_items", "set_rules", "connect_entrances", "generate_basic")) diff --git a/test/general/test_options.py b/test/general/test_options.py index 6b08c8e9b0..5d69b6820b 100644 --- a/test/general/test_options.py +++ b/test/general/test_options.py @@ -109,7 +109,7 @@ class TestOptions(unittest.TestCase): def test_option_set_keys_random(self): """Tests that option sets do not contain 'random' and its variants as valid keys""" for game_name, world_type in AutoWorldRegister.world_types.items(): - if game_name not in ("Archipelago", "Sudoku", "Super Metroid"): + if game_name not in ("Archipelago", "Super Metroid"): for option_key, option in world_type.options_dataclass.type_hints.items(): if issubclass(option, OptionSet): with self.subTest(game=game_name, option=option_key): diff --git a/worlds/apsudoku/__init__.py b/worlds/apsudoku/__init__.py deleted file mode 100644 index 04422ddb23..0000000000 --- a/worlds/apsudoku/__init__.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import Dict - -from BaseClasses import Tutorial -from ..AutoWorld import WebWorld, World - -class AP_SudokuWebWorld(WebWorld): - options_page = False - theme = 'partyTime' - - setup_en = Tutorial( - tutorial_name='Setup Guide', - description='A guide to playing APSudoku', - language='English', - file_name='setup_en.md', - link='setup/en', - authors=['EmilyV'] - ) - - tutorials = [setup_en] - -class AP_SudokuWorld(World): - """ - Play a little Sudoku while you're in BK mode to maybe get some useful hints - """ - game = "Sudoku" - web = AP_SudokuWebWorld() - - item_name_to_id: Dict[str, int] = {} - location_name_to_id: Dict[str, int] = {} - - @classmethod - def stage_assert_generate(cls, multiworld): - raise Exception("APSudoku cannot be used for generating worlds, the client can instead connect to any slot from any world") - diff --git a/worlds/apsudoku/docs/en_Sudoku.md b/worlds/apsudoku/docs/en_Sudoku.md deleted file mode 100644 index b56af0de79..0000000000 --- a/worlds/apsudoku/docs/en_Sudoku.md +++ /dev/null @@ -1,15 +0,0 @@ -# APSudoku - -## Hint Games - -HintGames do not need to be added at the start of a seed, and do not create a 'slot'- instead, you connect the HintGame client to a different game's slot. By playing a HintGame, you can earn hints for the connected slot. - -## What is this game? - -Play Sudoku puzzles of varying difficulties, earning a hint for each puzzle correctly solved. Harder puzzles are more likely to grant a hint towards a Progression item, though otherwise what hint is granted is random. - -## Where is the options page? - -There is no options page; this game cannot be used in your .yamls. Instead, the client can connect to any slot in a multiworld. - -By using the connected room's Admin Password on the Admin Panel tab, you can configure some settings at any time to affect the entire room. This allows disabling hints entirely, as well as altering the hint odds for each difficulty. diff --git a/worlds/apsudoku/docs/setup_en.md b/worlds/apsudoku/docs/setup_en.md deleted file mode 100644 index f80cd4333f..0000000000 --- a/worlds/apsudoku/docs/setup_en.md +++ /dev/null @@ -1,55 +0,0 @@ -# APSudoku Setup Guide - -## Required Software -- [APSudoku](https://github.com/APSudoku/APSudoku) - -## General Concept - -This is a HintGame client, which can connect to any multiworld slot, allowing you to play Sudoku to unlock random hints for that slot's locations. - -Does not need to be added at the start of a seed, as it does not create any slots of its own, nor does it have any YAML files. - -## Installation Procedures - -### Windows / Linux -Go to the latest release from the [github APSudoku Releases page](https://github.com/APSudoku/APSudoku/releases/latest). Download and extract the appropriate file for your platform. - -### Web -Go to the [github pages](apsudoku.github.io) or [itch.io](https://emilyv99.itch.io/apsudoku) site, and play in the browser. - -## Joining a MultiWorld Game - -1. Run the APSudoku executable. -2. Under `Settings` → `Connection` at the top-right: - - Enter the server address and port number - - Enter the name of the slot you wish to connect to - - Enter the room password (optional) - - Select DeathLink related settings (optional) - - Press `Connect` -4. Under the `Sudoku` tab - - Choose puzzle difficulty - - Click `Start` to generate a puzzle -5. Try to solve the Sudoku. Click `Check` when done - - A correct solution rewards you with 1 hint for a location in the world you are connected to - - An incorrect solution has no penalty, unless DeathLink is enabled (see below) - -Info: -- You can set various settings under `Settings` → `Sudoku`, and can change the colors used under `Settings` → `Theme`. -- While connected, you can view the `Console` and `Hints` tabs for standard TextClient-like features -- You can also use the `Tracking` tab to view either a basic tracker or a valid [GodotAP tracker pack](https://github.com/EmilyV99/GodotAP/blob/main/tracker_packs/GET_PACKS.md) -- While connected, the number of "unhinted" locations for your slot is shown in the upper-left of the the `Sudoku` tab. (If this reads 0, no further hints can be earned for this slot, as every locations is already hinted) -- Click the various `?` buttons for information on controls/how to play - -## Admin Settings - -By using the connected room's Admin Password on the Admin Panel tab, you can configure some settings at any time to affect the entire room. - -- You can disable APSudoku for the entire room, preventing any hints from being granted. -- You can customize the reward weights for each difficulty, making progression hints more or less likely, and/or adding a chance to get "no hint" after a solve. - -## DeathLink Support - -If `DeathLink` is enabled when you click `Connect`: -- Lose a life if you check an incorrect puzzle (not an _incomplete_ puzzle- if any cells are empty, you get off with a warning), or if you quit a puzzle without solving it (including disconnecting). -- Your life count is customizable (default 0). Dying with 0 lives left kills linked players AND resets your puzzle. -- On receiving a DeathLink from another player, your puzzle resets. diff --git a/worlds/generic/__init__.py b/worlds/generic/__init__.py index c4aef4f67b..4cd80556cc 100644 --- a/worlds/generic/__init__.py +++ b/worlds/generic/__init__.py @@ -26,7 +26,10 @@ class GenericWeb(WebWorld): 'English', 'setup_en.md', 'setup/en', ['alwaysintreble']) triggers = Tutorial('Archipelago Triggers Guide', 'A guide to setting up and using triggers in your game settings.', 'English', 'triggers_en.md', 'triggers/en', ['alwaysintreble']) - tutorials = [setup, mac, commands, advanced_settings, triggers, plando] + other_games = Tutorial('Other Games and Tools', + 'A guide to additional games and tools that can be used with Archipelago.', + 'English', 'other_en.md', 'other/en', ['Berserker']) + tutorials = [setup, mac, commands, advanced_settings, triggers, plando, other_games] class GenericWorld(World): diff --git a/worlds/generic/docs/other_en.md b/worlds/generic/docs/other_en.md new file mode 100644 index 0000000000..caf8372170 --- /dev/null +++ b/worlds/generic/docs/other_en.md @@ -0,0 +1,37 @@ +# Other Games And Tools + +This page provides information and links regarding various tools that may be of use with Archipelago, including additional playable games not supported by this website. + +You should only download and use files from sources you trust; sources listed here are not officially vetted for safety, so use your own judgement and caution. + +## Discord + +Currently, Discord is the primary hub for Archipelago; whether it be finding people to play with, developing new game implementations, or finding new playable games. + +The [Archipelago Official Discord](https://discord.gg/8Z65BR2) is the main hub, while the [Archipelago After Dark Discord](https://discord.gg/fqvNCCRsu4) houses additional games that may be unrated or 18+ in some territories. + +The `#apworld-index` channels in each of these servers contain lists of playable games which should be easily downloadable and playable with an Archipelago installation. + +## Wiki + +The community-maintained [Archipelago Wiki](https://archipelago.miraheze.org/) has information on many games as well, and acts as a great discord-free source of information. + +## Hint Games + +Hint Games are a special type of game which are not included as part of the multiworld generation process. Instead, they can log in to an ongoing multiworld, connecting to a slot designated for any game. Rather than earning items for other games in the multiworld, a Hint Game will allow you to earn hints for the slot you are connected to. + +Hint Games can be found from sources such as the Discord and the [Hint Game Category](https://archipelago.miraheze.org/wiki/Category:Hint_games) of the wiki, as detailed above. + +## Notable Tools + +### Options Creator + +The Options Creator is included in the Archipelago installation, and is accessible from the Archipelago Launcher. Using this simple GUI tool, you can easily create randomization options for any installed `.apworld` - perfect when using custom worlds you've installed that don't have options pages on the website. + +### PopTracker + +[PopTracker](https://poptracker.github.io) is a popular tool in Randomizer communities, which many games support via custom PopTracker Packs. Many Archipelago packs include the ability to directly connect to your slot for auto-tracking capabilities. (Check each game's setup guide or Discord channel to see if it has PopTracker compatibility!) + +### Universal Tracker + +[Universal Tracker](https://github.com/FarisTheAncient/Archipelago/releases?q=Tracker) is a custom tracker client that uses your .yaml files from generation (as well as the .apworld files) to attempt to provide a view of what locations are currently in-logic or not, using the actual generation logic. Specific steps may need to be taken depending on the game, or the use of randomness in your yaml. Support for UT can be found in the [#universal-tracker](https://discord.com/channels/731205301247803413/1367270230635839539) channel of the Archipelago Official Discord. From fb45a2f87e392a7ea69daa33905b8aee3e83e19d Mon Sep 17 00:00:00 2001 From: Ian Robinson Date: Wed, 18 Mar 2026 13:54:17 -0400 Subject: [PATCH 53/53] Rule Builder: Fix count resolution when Oring HasAnyCount (#6048) --- rule_builder/rules.py | 2 +- test/general/test_rule_builder.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/rule_builder/rules.py b/rule_builder/rules.py index 816ac9f0b7..77a89c96c2 100644 --- a/rule_builder/rules.py +++ b/rule_builder/rules.py @@ -527,7 +527,7 @@ class Or(NestedRule[TWorld], game="Archipelago"): items[item] = 1 elif isinstance(child, HasAnyCount.Resolved): for item, count in child.item_counts: - if item not in items or items[item] < count: + if item not in items or count < items[item]: items[item] = count else: clauses.append(child) diff --git a/test/general/test_rule_builder.py b/test/general/test_rule_builder.py index 52248b6047..81003dcd87 100644 --- a/test/general/test_rule_builder.py +++ b/test/general/test_rule_builder.py @@ -233,6 +233,14 @@ class CachedRuleBuilderTestCase(RuleBuilderTestCase): Or(Has("A"), HasAny("B", "C"), HasAnyCount({"D": 1, "E": 1})), HasAny.Resolved(("A", "B", "C", "D", "E"), player=1), ), + ( + And(HasAllCounts({"A": 1, "B": 2}), HasAllCounts({"A": 2, "B": 2})), + HasAllCounts.Resolved((("A", 2), ("B", 2)), player=1), + ), + ( + Or(HasAnyCount({"A": 1, "B": 2}), HasAnyCount({"A": 2, "B": 2})), + HasAnyCount.Resolved((("A", 1), ("B", 2)), player=1), + ), ) ) class TestSimplify(RuleBuilderTestCase):