diff --git a/BaseClasses.py b/BaseClasses.py index 30be74e8..340b08ab 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -83,7 +83,7 @@ class World(object): set_player_attr('ganonstower_vanilla', True) set_player_attr('sewer_light_cone', self.mode[player] == 'standard') set_player_attr('fix_trock_doors', self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted') - set_player_attr('fix_skullwoods_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple']) + set_player_attr('fix_skullwoods_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'] or self.doorShuffle[player] not in ['vanilla']) set_player_attr('fix_palaceofdarkness_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple']) set_player_attr('fix_trock_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple']) set_player_attr('can_access_trock_eyebridge', None) @@ -450,17 +450,17 @@ class CollectionState(object): blocked.add(entrance.parent_region) if color_type: ccr[candidate] = color_type - for ext in candidate.exits: - connect = ext.connected_region - if connect in rrp and connect in ccr: - door = self.world.check_for_door(ext.name, player) - if door is not None and not door.blocked: - if ext.can_reach(self): - new_color = ccr[connect] | (ccr[candidate] & (door.crystal or CrystalBarrier.Either)) - if new_color != ccr[connect]: - self.spread_crystal_access(candidate, new_color, rrp, ccr, player) - else: - blocked.add(candidate) + for ext in candidate.exits: + connect = ext.connected_region + if connect in rrp and connect in ccr: + door = self.world.check_for_door(ext.name, player) + if door is not None and not door.blocked: + if ext.can_reach(self): + new_color = ccr[connect] | (ccr[candidate] & (door.crystal or CrystalBarrier.Either)) + if new_color != ccr[connect]: + self.spread_crystal_access(candidate, new_color, rrp, ccr, player) + else: + blocked.add(candidate) new_regions = len(rrp) > reachable_regions_count reachable_regions_count = len(rrp) diff --git a/DoorShuffle.py b/DoorShuffle.py index ac81c5e9..45083d72 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -55,6 +55,9 @@ def link_doors(world, player): cross_dungeon(world, player) elif world.doorShuffle[player] == 'experimental': experiment(world, player) + else: + logging.getLogger('').error('Invalid door shuffle setting: %s' % world.doorShuffle[player]) + raise Exception('Invalid door shuffle setting: %s' % world.doorShuffle[player]) if world.doorShuffle[player] != 'vanilla': create_door_spoiler(world, player) @@ -146,7 +149,8 @@ def vanilla_key_logic(world, player): analyze_dungeon(key_layout, world, player) world.key_logic[player][builder.name] = key_layout.key_logic last_key = None - validate_vanilla_key_logic(world, player) + if world.shuffle[player] == 'vanilla': + validate_vanilla_key_logic(world, player) # some useful functions @@ -2023,18 +2027,19 @@ default_one_way_connections = [ ] # For crossed -compass_data = { # offset from 0x122e17, sram storage, write offset from compass_w_addr, 0 = jmp or # of nops - 'Hyrule Castle': (0x1, 0xc0, 0x16, 0), - 'Eastern Palace': (0x1C, 0xc1, 0x28, 0), - 'Desert Palace': (0x35, 0xc2, 0x4a, 0), - 'Agahnims Tower': (0x51, 0xc3, 0x5c, 0), - 'Swamp Palace': (0x6A, 0xc4, 0x7e, 0), - 'Palace of Darkness': (0x83, 0xc5, 0xa4, 0), - 'Misery Mire': (0x9C, 0xc6, 0xca, 0), - 'Skull Woods': (0xB5, 0xc7, 0xf0, 0), - 'Ice Palace': (0xD0, 0xc8, 0x102, 0), - 'Tower of Hera': (0xEB, 0xc9, 0x114, 0), - 'Thieves Town': (0x106, 0xca, 0x138, 0), - 'Turtle Rock': (0x11F, 0xcb, 0x15e, 0), - 'Ganons Tower': (0x13A, 0xcc, 0x170, 2) +# offset from 0x122e17, sram storage, write offset from compass_w_addr, 0 = jmp or # of nops, dungeon_id +compass_data = { + 'Hyrule Castle': (0x1, 0xc0, 0x16, 0, 0x02), + 'Eastern Palace': (0x1C, 0xc1, 0x28, 0, 0x04), + 'Desert Palace': (0x35, 0xc2, 0x4a, 0, 0x06), + 'Agahnims Tower': (0x51, 0xc3, 0x5c, 0, 0x08), + 'Swamp Palace': (0x6A, 0xc4, 0x7e, 0, 0x0a), + 'Palace of Darkness': (0x83, 0xc5, 0xa4, 0, 0x0c), + 'Misery Mire': (0x9C, 0xc6, 0xca, 0, 0x0e), + 'Skull Woods': (0xB5, 0xc7, 0xf0, 0, 0x10), + 'Ice Palace': (0xD0, 0xc8, 0x102, 0, 0x12), + 'Tower of Hera': (0xEB, 0xc9, 0x114, 0, 0x14), + 'Thieves Town': (0x106, 0xca, 0x138, 0, 0x16), + 'Turtle Rock': (0x11F, 0xcb, 0x15e, 0, 0x18), + 'Ganons Tower': (0x13A, 0xcc, 0x170, 2, 0x1a) } diff --git a/DungeonRandomizer.py b/DungeonRandomizer.py index 68858c9e..d04d6fb5 100755 --- a/DungeonRandomizer.py +++ b/DungeonRandomizer.py @@ -218,8 +218,8 @@ def parse_arguments(argv, no_defaults=False): Select the rate at which the menu opens and closes. (default: %(default)s) ''') - parser.add_argument('--quickswap', help='Enable quick item swapping with L and R.', action='store_true') - parser.add_argument('--disablemusic', help='Disables game music.', action='store_true') + parser.add_argument('--quickswap', default=defval(False), help='Enable quick item swapping with L and R.', action='store_true') + parser.add_argument('--disablemusic', default=defval(False), help='Disables game music.', action='store_true') parser.add_argument('--mapshuffle', default=defval(False), help='Maps are no longer restricted to their dungeons, but can be anywhere', action='store_true') parser.add_argument('--compassshuffle', default=defval(False), help='Compasses are no longer restricted to their dungeons, but can be anywhere', action='store_true') parser.add_argument('--keyshuffle', default=defval(False), help='Small Keys are no longer restricted to their dungeons, but can be anywhere', action='store_true') diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 3cdc9054..f00f02c6 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -550,7 +550,7 @@ def link_entrances(world, player): ('North Fairy Cave Exit', 'North Fairy Cave'), ('Lost Woods Hideout Exit', 'Lost Woods Hideout (top)'), ('Lumberjack Tree Exit', 'Lumberjack Tree (top)'), - (('Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)'), 'Skull Woods Second Section (Drop)')] + (('Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)'), 'Skull Back Drop')] if world.mode[player] == 'standard': # cannot move uncle cave @@ -587,7 +587,7 @@ def link_entrances(world, player): else: sw_hole_pool = dw_hole_entrances mandatory_dark_world.append('Skull Woods First Section Exit') - for target in ['Skull Woods First Section (Left)', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Top)']: + for target in ['Skull Left Drop', 'Skull Pinball', 'Skull Pot Circle']: connect_entrance(world, sw_hole_pool.pop(), target, player) # sanctuary has to be in light world @@ -786,8 +786,8 @@ def link_entrances(world, player): hole_entrances = ['Kakariko Well Drop', 'Bat Cave Drop', 'North Fairy Cave Drop', 'Lost Woods Hideout Drop', 'Lumberjack Tree Tree', 'Sanctuary Grave', 'Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'] - hole_targets = ['Kakariko Well (top)', 'Bat Cave (right)', 'North Fairy Cave', 'Lost Woods Hideout (top)', 'Lumberjack Tree (top)', 'Sewer Drop', 'Skull Woods Second Section (Drop)', - 'Skull Woods First Section (Left)', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Top)'] + hole_targets = ['Kakariko Well (top)', 'Bat Cave (right)', 'North Fairy Cave', 'Lost Woods Hideout (top)', 'Lumberjack Tree (top)', 'Sewer Drop', 'Skull Back Drop', + 'Skull Left Drop', 'Skull Pinball', 'Skull Pot Circle'] # tavern back door cannot be shuffled yet connect_doors(world, ['Tavern North'], ['Tavern'], player) @@ -924,8 +924,8 @@ def link_entrances(world, player): hole_entrances = ['Kakariko Well Drop', 'Bat Cave Drop', 'North Fairy Cave Drop', 'Lost Woods Hideout Drop', 'Lumberjack Tree Tree', 'Sanctuary Grave', 'Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'] - hole_targets = ['Kakariko Well (top)', 'Bat Cave (right)', 'North Fairy Cave', 'Lost Woods Hideout (top)', 'Lumberjack Tree (top)', 'Sewer Drop', 'Skull Woods Second Section (Drop)', - 'Skull Woods First Section (Left)', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Top)'] + hole_targets = ['Kakariko Well (top)', 'Bat Cave (right)', 'North Fairy Cave', 'Lost Woods Hideout (top)', 'Lumberjack Tree (top)', 'Sewer Drop', 'Skull Back Drop', + 'Skull Left Drop', 'Skull Pinball', 'Skull Pot Circle'] if world.mode[player] == 'standard': # cannot move uncle cave @@ -1627,8 +1627,8 @@ def link_inverted_entrances(world, player): hole_entrances = ['Kakariko Well Drop', 'Bat Cave Drop', 'North Fairy Cave Drop', 'Lost Woods Hideout Drop', 'Lumberjack Tree Tree', 'Sanctuary Grave', 'Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'] - hole_targets = ['Kakariko Well (top)', 'Bat Cave (right)', 'North Fairy Cave', 'Lost Woods Hideout (top)', 'Lumberjack Tree (top)', 'Sewer Drop', 'Skull Woods Second Section (Drop)', - 'Skull Woods First Section (Left)', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Top)'] + hole_targets = ['Kakariko Well (top)', 'Bat Cave (right)', 'North Fairy Cave', 'Lost Woods Hideout (top)', 'Lumberjack Tree (top)', 'Sewer Drop', 'Skull Back Drop', + 'Skull Left Drop', 'Skull Pinball', 'Skull Pot Circle'] # tavern back door cannot be shuffled yet connect_doors(world, ['Tavern North'], ['Tavern'], player) @@ -3780,8 +3780,8 @@ exit_ids = {'Links House Exit': (0x01, 0x00), 'Lost Woods Hideout (top)': 0x7A, 'Lumberjack Tree (top)': 0x7F, 'Sewer Drop': 0x81, - 'Skull Woods Second Section (Drop)': 0x79, - 'Skull Woods First Section (Left)': 0x77, - 'Skull Woods First Section (Right)': 0x78, - 'Skull Woods First Section (Top)': 0x76, + 'Skull Back Drop': 0x79, + 'Skull Left Drop': 0x77, + 'Skull Pinball': 0x78, + 'Skull Pot Circle': 0x76, 'Pyramid': 0x7B} diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 8e89ec26..794bbf57 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -864,7 +864,8 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa def cnt_avail_small_locations(free_locations, key_only, state, world, player): if not world.keyshuffle[player] and not world.retro[player]: - avail_chest_keys = min(free_locations - state.used_locations + state.used_smalls, state.key_locations - key_only) + bk_adj = 1 if state.big_key_opened and not state.big_key_special else 0 + avail_chest_keys = min(free_locations - bk_adj, state.key_locations - key_only) return max(0, avail_chest_keys + key_only - state.used_smalls) return state.key_locations - state.used_smalls @@ -1146,20 +1147,22 @@ def val_mire(key_logic, world, player): def val_turtle(key_logic, world, player): - val_rule(key_logic.door_rules['TR Hub NW'], 1) - val_rule(key_logic.door_rules['TR Pokey 1 NW'], 2) - val_rule(key_logic.door_rules['TR Chain Chomps Down Stairs'], 3) - val_rule(key_logic.door_rules['TR Pokey 2 ES'], 6, True, 'Turtle Rock - Big Key Chest', 4, {'Turtle Rock - Big Key Chest'}) - val_rule(key_logic.door_rules['TR Crystaroller Down Stairs'], 5) - val_rule(key_logic.door_rules['TR Dash Bridge WS'], 6) - assert world.get_location('Turtle Rock - Eye Bridge - Bottom Right', player) in key_logic.bk_restricted - assert world.get_location('Turtle Rock - Eye Bridge - Top Left', player) in key_logic.bk_restricted - assert world.get_location('Turtle Rock - Eye Bridge - Top Right', player) in key_logic.bk_restricted - assert world.get_location('Turtle Rock - Eye Bridge - Bottom Left', player) in key_logic.bk_restricted - assert world.get_location('Turtle Rock - Boss', player) in key_logic.bk_restricted - assert world.get_location('Turtle Rock - Crystaroller Room', player) in key_logic.bk_restricted - assert world.get_location('Turtle Rock - Big Chest', player) in key_logic.bk_restricted - assert len(key_logic.bk_restricted) == 7 + # todo: check vanilla key logic when TR back doors are accessible + if world.shuffle[player] == 'vanilla' and world.mode[player] != 'inverted': + val_rule(key_logic.door_rules['TR Hub NW'], 1) + val_rule(key_logic.door_rules['TR Pokey 1 NW'], 2) + val_rule(key_logic.door_rules['TR Chain Chomps Down Stairs'], 3) + val_rule(key_logic.door_rules['TR Pokey 2 ES'], 6, True, 'Turtle Rock - Big Key Chest', 4, {'Turtle Rock - Big Key Chest'}) + val_rule(key_logic.door_rules['TR Crystaroller Down Stairs'], 5) + val_rule(key_logic.door_rules['TR Dash Bridge WS'], 6) + assert world.get_location('Turtle Rock - Eye Bridge - Bottom Right', player) in key_logic.bk_restricted + assert world.get_location('Turtle Rock - Eye Bridge - Top Left', player) in key_logic.bk_restricted + assert world.get_location('Turtle Rock - Eye Bridge - Top Right', player) in key_logic.bk_restricted + assert world.get_location('Turtle Rock - Eye Bridge - Bottom Left', player) in key_logic.bk_restricted + assert world.get_location('Turtle Rock - Boss', player) in key_logic.bk_restricted + assert world.get_location('Turtle Rock - Crystaroller Room', player) in key_logic.bk_restricted + assert world.get_location('Turtle Rock - Big Chest', player) in key_logic.bk_restricted + assert len(key_logic.bk_restricted) == 7 def val_ganons(key_logic, world, player): diff --git a/Main.py b/Main.py index c97d99af..448ec551 100644 --- a/Main.py +++ b/Main.py @@ -23,7 +23,8 @@ from Fill import distribute_items_cutoff, distribute_items_staleness, distribute from ItemList import generate_itempool, difficulties, fill_prizes from Utils import output_path, parse_player_names -__version__ = '0.0.7-pre' +__version__ = '0.0.9-pre' + def main(args, seed=None): if args.outputpath: diff --git a/Mystery.py b/Mystery.py index 7bc43f37..9937b5ad 100644 --- a/Mystery.py +++ b/Mystery.py @@ -3,6 +3,7 @@ import logging import random import urllib.request import urllib.parse +import re from DungeonRandomizer import parse_arguments from Main import main as DRMain @@ -14,6 +15,7 @@ def parse_yaml(txt): ret = {} indents = {len(txt) - len(txt.lstrip(' ')): ret} for line in txt.splitlines(): + line = re.sub(r'#.*', '', line) if not line: continue name, val = line.split(':', 1) diff --git a/Rom.py b/Rom.py index 8de2d0ec..45d6c38f 100644 --- a/Rom.py +++ b/Rom.py @@ -557,7 +557,7 @@ def patch_rom(world, rom, player, team, enemized): # todo fix screen scrolling if world.shuffle[player] not in ['insanity', 'insanity_legacy', 'madness_legacy'] and \ - exit.name in ['Eastern Palace Exit', 'Tower of Hera Exit', 'Thieves Town Exit', 'Skull Woods Final Section Exit', 'Ice Palace Exit', 'Misery Mire Exit', + exit.name in ['Eastern Palace Exit', 'Tower of Hera Exit', 'Thieves Town Exit', 'Ice Palace Exit', 'Misery Mire Exit', 'Palace of Darkness Exit', 'Swamp Palace Exit', 'Ganons Tower Exit', 'Desert Palace Exit (North)', 'Agahnims Tower Exit', 'Spiral Cave Exit (Top)', 'Superbunny Cave Exit (Bottom)', 'Turtle Rock Ledge Exit (East)']: # For exits that connot be reached from another, no need to apply offset fixes. @@ -608,16 +608,10 @@ def patch_rom(world, rom, player, team, enemized): for paired_door in world.paired_doors[player]: rom.write_bytes(paired_door.address_a(world, player), paired_door.rom_data_a(world, player)) rom.write_bytes(paired_door.address_b(world, player), paired_door.rom_data_b(world, player)) - if world.fix_skullwoods_exit and world.shuffle in ['vanilla', 'simple', 'restricted', 'dungeonssimple']: - connect = world.get_entrance('Skull Woods Final Section', player).connected_region - exit_name = None - for ext in connect.exits: - if ext.connected_region.name == 'Skull Woods Forest (West)': - exit_name = ext.name - break - if exit_name is not None and exit_name == 'Skull Woods Final Section Exit': - rom.write_int16(0x15DB5 + 2 * exit_ids['Skull Woods Final Section Exit'][1], 0x00F8) - # todo: fix other exits if ER enabled and similar situation happens + + # fix skull woods exit, if not fixed during exit patching + if world.fix_skullwoods_exit[player] and world.shuffle[player] == 'vanilla': + write_int16(rom, 0x15DB5 + 2 * exit_ids['Skull Woods Final Section Exit'][1], 0x00F8) write_custom_shops(rom, world, player) @@ -2102,8 +2096,19 @@ def compass_code_good(rom): def update_compasses(rom, world, player): layouts = world.dungeon_layouts[player] + # cmp XX : bne escape + # cpy 32 : bne escape + # brl .itemCounts + # c9 02 d0 eastern + # nop #2 + new_code = [0x07, 0xC0, 0x32, 0xD0, 0x03, 0x82, 0xB9, 0x01, 0xC9, 0x02, 0xD0, 0x10, 0xEA, 0xEA] + rom.write_bytes(compass_w_addr + 8, new_code) + # 05 06 07 08 + # C9 00 D0 02 80 04 C9 02 D0 15 C0 32 D0 03 82 B3 01 + # C9 XX D0 07 C0 32 D0 03 82 B9 01 C9 02 D0 10 EA EA + for name, builder in layouts.items(): - digit_offset, sram_byte, write_offset, jmp_nop_flag = compass_data[name] + digit_offset, sram_byte, write_offset, jmp_nop_flag, dungeon_id = compass_data[name] digit1 = builder.location_cnt // 10 digit2 = builder.location_cnt % 10 rom.write_byte(compass_r_addr+digit_offset, 0x90+digit1) @@ -2128,6 +2133,8 @@ def update_compasses(rom, world, player): else: for i in range(0, jmp_nop_flag): rom.write_byte(write_address+9+i, 0xea) # nop + if builder.bk_provided: + rom.write_byte(compass_w_addr+6, dungeon_id) InconvenientDungeonEntrances = {'Turtle Rock': 'Turtle Rock Main', diff --git a/RoomData.py b/RoomData.py index add738e6..e80fb087 100644 --- a/RoomData.py +++ b/RoomData.py @@ -239,6 +239,8 @@ def create_rooms(world, player): world.get_room(0x61, player).swap(5, 6) # puts the Incognito Entrance at the end, so it can be deleted world.get_room(0x62, player).swap(1, 4) # puts the exit at pos 1 - enables pos 3 world.get_room(0x77, player).swap(0, 1) # fixes Hera Lobby Key Stairs - entrance now at pos 0 + if world.enemy_shuffle[player] != 'none': + world.get_room(0xc0, player).change(0, DoorKind.Normal) # fix this kill room if enemizer is on class Room(object): diff --git a/Rules.py b/Rules.py index a29ec51d..c3367c72 100644 --- a/Rules.py +++ b/Rules.py @@ -131,12 +131,12 @@ def global_rules(world, player): set_rule(world.get_location('Spike Cave', player), lambda state: - state.has('Hammer', player) and state.can_lift_rocks(player) and - ((state.has('Cape', player) and state.can_extend_magic(player, 16, True)) or - (state.has('Cane of Byrna', player) and - (state.can_extend_magic(player, 12, True) or - (state.world.can_take_damage and (state.has_Boots(player) or state.has_hearts(player, 4)))))) - ) + state.has('Hammer', player) and state.can_lift_rocks(player) and + ((state.has('Cape', player) and state.can_extend_magic(player, 16, True)) or + (state.has('Cane of Byrna', player) and + (state.can_extend_magic(player, 12, True) or + (state.world.can_take_damage and (state.has_Boots(player) or state.has_hearts(player, 4)))))) + ) set_rule(world.get_location('Hookshot Cave - Top Right', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_location('Hookshot Cave - Top Left', player), lambda state: state.has('Hookshot', player)) @@ -217,6 +217,7 @@ def global_rules(world, player): set_defeat_dungeon_boss_rule(world.get_location('Skull Woods - Prize', player)) # blind can't have the small key? - not necessarily true anymore - but likely still + set_rule(world.get_location('Thieves\' Town - Big Chest', player), lambda state: state.has('Hammer', player)) for entrance in ['Thieves Basement Block Path', 'Thieves Blocked Entry Path', 'Thieves Conveyor Block Path', 'Thieves Conveyor Bridge Block Path']: set_rule(world.get_entrance(entrance, player), lambda state: state.can_lift_rocks(player)) for location in ['Thieves\' Town - Blind\'s Cell', 'Thieves\' Town - Boss']: @@ -363,7 +364,7 @@ def global_rules(world, player): add_rule(world.get_location('Sunken Treasure', player), lambda state: state.has('Open Floodgate', player)) set_rule(world.get_location('Ganon', player), lambda state: state.has_beam_sword(player) and state.has_fire_source(player) and state.has_crystals(world.crystals_needed_for_ganon[player], player) - and (state.has('Tempered Sword', player) or state.has('Golden Sword', player) or (state.has('Silver Arrows', player) and state.can_shoot_arrows(player)) or state.has('Lamp', player) or state.can_extend_magic(player, 12))) # need to light torch a sufficient amount of times + and (state.has('Tempered Sword', player) or state.has('Golden Sword', player) or (state.has('Silver Arrows', player) and state.can_shoot_arrows(player)) or state.has('Lamp', player) or state.can_extend_magic(player, 12))) # need to light torch a sufficient amount of times set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has_beam_sword(player)) # need to damage ganon to get tiles to drop