diff --git a/DoorShuffle.py b/DoorShuffle.py index 676546b4..88b489f3 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1967,8 +1967,10 @@ ohko_forbidden = { def filter_dashable_candidates(candidates, world): - forbidden_set = dashable_forbidden if world.timer in ['ohko', 'timed-ohko'] else ohko_forbidden - return [x for x in candidates if x not in forbidden_set and x.dest not in forbidden_set] + forbidden_set = dashable_forbidden + if world.timer in ['ohko', 'timed-ohko']: + forbidden_set = ohko_forbidden.union(dashable_forbidden) + return [x for x in candidates if x.name not in forbidden_set and x.dest.name not in forbidden_set] def shuffle_bombable_dashable(bd_candidates, world, player): diff --git a/InitialSram.py b/InitialSram.py new file mode 100644 index 00000000..772e1d46 --- /dev/null +++ b/InitialSram.py @@ -0,0 +1,253 @@ +from dataclasses import dataclass, field +from typing import List + +from BaseClasses import CollectionState +from Utils import count_set_bits + +SRAM_SIZE = 0x500 +ROOM_DATA = 0x000 +OVERWORLD_DATA = 0x280 + +def _new_default_sram(): + sram_buf = [0x00] * 0x500 + sram_buf[ROOM_DATA+0x20D] = 0xF0 + sram_buf[ROOM_DATA+0x20F] = 0xF0 + sram_buf[0x379] = 0x68 + sram_buf[0x401] = 0xFF + sram_buf[0x402] = 0xFF + return sram_buf + +@dataclass +class InitialSram: + _initial_sram_bytes: List[int] = field(default_factory=_new_default_sram) + + def _set_value(self, idx: int, val:int): + if idx > SRAM_SIZE: + raise IndexError('SRAM index out of bounds: {idx}') + if not (-1 < val < 256): + raise ValueError('SRAM value must be between 0 and 255: {val}') + self._initial_sram_bytes[idx] = val + + def _or_value(self, idx: int, val:int): + if idx > SRAM_SIZE: + raise IndexError('SRAM index out of bounds: {idx}') + if not (-1 < val < 256): + raise ValueError('SRAM value must be between 0 and 255: {val}') + self._initial_sram_bytes[idx] |= val + + def pre_open_aga_curtains(self): + self._or_value(ROOM_DATA+0x61, 0x80) + + def pre_open_skullwoods_curtains(self): + self._or_value(ROOM_DATA+0x93, 0x80) + + def pre_open_lumberjack(self): + self._or_value(OVERWORLD_DATA+0x02, 0x20) + + def pre_open_castle_gate(self): + self._or_value(OVERWORLD_DATA+0x1B, 0x20) + + def pre_open_ganons_tower(self): + self._or_value(OVERWORLD_DATA+0x43, 0x20) + + def pre_open_pyramid_hole(self): + self._or_value(OVERWORLD_DATA+0x5B, 0x20) + + def set_starting_equipment(self, world: object, player: int): + equip = [0] * (0x340 + 0x4F) + equip[0x36C] = 0x18 + equip[0x36D] = 0x18 + equip[0x379] = 0x68 + if world.bombbag[player]: + starting_max_bombs = 0 + else: + starting_max_bombs = 10 + starting_max_arrows = 30 + starting_bomb_cap_upgrades = 0 + starting_arrow_cap_upgrades = 0 + starting_bombs = 0 + starting_arrows = 0 + + startingstate = CollectionState(world) + + if startingstate.has('Bow', player): + equip[0x340] = 3 if startingstate.has('Silver Arrows', player) else 1 + equip[0x38E] |= 0x20 # progressive flag to get the correct hint in all cases + if not world.retro[player]: + equip[0x38E] |= 0x80 + if startingstate.has('Silver Arrows', player): + equip[0x38E] |= 0x40 + + if startingstate.has('Titans Mitts', player): + equip[0x354] = 2 + elif startingstate.has('Power Glove', player): + equip[0x354] = 1 + + if startingstate.has('Golden Sword', player): + equip[0x359] = 4 + elif startingstate.has('Tempered Sword', player): + equip[0x359] = 3 + elif startingstate.has('Master Sword', player): + equip[0x359] = 2 + elif startingstate.has('Fighter Sword', player): + equip[0x359] = 1 + + if startingstate.has('Mirror Shield', player): + equip[0x35A] = 3 + elif startingstate.has('Red Shield', player): + equip[0x35A] = 2 + elif startingstate.has('Blue Shield', player): + equip[0x35A] = 1 + + if startingstate.has('Red Mail', player): + equip[0x35B] = 2 + elif startingstate.has('Blue Mail', player): + equip[0x35B] = 1 + + if startingstate.has('Magic Upgrade (1/4)', player): + equip[0x37B] = 2 + equip[0x36E] = 0x80 + elif startingstate.has('Magic Upgrade (1/2)', player): + equip[0x37B] = 1 + equip[0x36E] = 0x80 + + for item in world.precollected_items: + if item.player != player: + continue + + if item.name in ['Bow', 'Silver Arrows', 'Progressive Bow', 'Progressive Bow (Alt)', + 'Titans Mitts', 'Power Glove', 'Progressive Glove', + 'Golden Sword', 'Tempered Sword', 'Master Sword', 'Fighter Sword', 'Progressive Sword', + 'Mirror Shield', 'Red Shield', 'Blue Shield', 'Progressive Shield', + 'Red Mail', 'Blue Mail', 'Progressive Armor', + 'Magic Upgrade (1/4)', 'Magic Upgrade (1/2)']: + continue + + set_table = {'Book of Mudora': (0x34E, 1), 'Hammer': (0x34B, 1), 'Bug Catching Net': (0x34D, 1), 'Hookshot': (0x342, 1), 'Magic Mirror': (0x353, 2), + 'Cape': (0x352, 1), 'Lamp': (0x34A, 1), 'Moon Pearl': (0x357, 1), 'Cane of Somaria': (0x350, 1), 'Cane of Byrna': (0x351, 1), + 'Fire Rod': (0x345, 1), 'Ice Rod': (0x346, 1), 'Bombos': (0x347, 1), 'Ether': (0x348, 1), 'Quake': (0x349, 1)} + or_table = {'Green Pendant': (0x374, 0x04), 'Red Pendant': (0x374, 0x01), 'Blue Pendant': (0x374, 0x02), + 'Crystal 1': (0x37A, 0x02), 'Crystal 2': (0x37A, 0x10), 'Crystal 3': (0x37A, 0x40), 'Crystal 4': (0x37A, 0x20), + 'Crystal 5': (0x37A, 0x04), 'Crystal 6': (0x37A, 0x01), 'Crystal 7': (0x37A, 0x08), + 'Big Key (Eastern Palace)': (0x367, 0x20), 'Compass (Eastern Palace)': (0x365, 0x20), 'Map (Eastern Palace)': (0x369, 0x20), + 'Big Key (Desert Palace)': (0x367, 0x10), 'Compass (Desert Palace)': (0x365, 0x10), 'Map (Desert Palace)': (0x369, 0x10), + 'Big Key (Tower of Hera)': (0x366, 0x20), 'Compass (Tower of Hera)': (0x364, 0x20), 'Map (Tower of Hera)': (0x368, 0x20), + 'Big Key (Escape)': (0x367, 0xC0), 'Compass (Escape)': (0x365, 0xC0), 'Map (Escape)': (0x369, 0xC0), + 'Big Key (Agahnims Tower)': (0x367, 0x08), 'Compass (Agahnims Tower)': (0x365, 0x08), 'Map (Agahnims Tower)': (0x369, 0x08), + 'Big Key (Palace of Darkness)': (0x367, 0x02), 'Compass (Palace of Darkness)': (0x365, 0x02), 'Map (Palace of Darkness)': (0x369, 0x02), + 'Big Key (Thieves Town)': (0x366, 0x10), 'Compass (Thieves Town)': (0x364, 0x10), 'Map (Thieves Town)': (0x368, 0x10), + 'Big Key (Skull Woods)': (0x366, 0x80), 'Compass (Skull Woods)': (0x364, 0x80), 'Map (Skull Woods)': (0x368, 0x80), + 'Big Key (Swamp Palace)': (0x367, 0x04), 'Compass (Swamp Palace)': (0x365, 0x04), 'Map (Swamp Palace)': (0x369, 0x04), + 'Big Key (Ice Palace)': (0x366, 0x40), 'Compass (Ice Palace)': (0x364, 0x40), 'Map (Ice Palace)': (0x368, 0x40), + 'Big Key (Misery Mire)': (0x367, 0x01), 'Compass (Misery Mire)': (0x365, 0x01), 'Map (Misery Mire)': (0x369, 0x01), + 'Big Key (Turtle Rock)': (0x366, 0x08), 'Compass (Turtle Rock)': (0x364, 0x08), 'Map (Turtle Rock)': (0x368, 0x08), + 'Big Key (Ganons Tower)': (0x366, 0x04), 'Compass (Ganons Tower)': (0x364, 0x04), 'Map (Ganons Tower)': (0x368, 0x04)} + set_or_table = {'Flippers': (0x356, 1, 0x379, 0x02),'Pegasus Boots': (0x355, 1, 0x379, 0x04), + 'Shovel': (0x34C, 1, 0x38C, 0x04), 'Ocarina': (0x34C, 3, 0x38C, 0x01), + 'Mushroom': (0x344, 1, 0x38C, 0x20 | 0x08), 'Magic Powder': (0x344, 2, 0x38C, 0x10), + 'Blue Boomerang': (0x341, 1, 0x38C, 0x80), 'Red Boomerang': (0x341, 2, 0x38C, 0x40)} + keys = {'Small Key (Eastern Palace)': [0x37E], 'Small Key (Desert Palace)': [0x37F], + 'Small Key (Tower of Hera)': [0x386], + 'Small Key (Agahnims Tower)': [0x380], 'Small Key (Palace of Darkness)': [0x382], + 'Small Key (Thieves Town)': [0x387], + 'Small Key (Skull Woods)': [0x384], 'Small Key (Swamp Palace)': [0x381], + 'Small Key (Ice Palace)': [0x385], + 'Small Key (Misery Mire)': [0x383], 'Small Key (Turtle Rock)': [0x388], + 'Small Key (Ganons Tower)': [0x389], + 'Small Key (Universal)': [0x38B], 'Small Key (Escape)': [0x37C, 0x37D]} + bottles = {'Bottle': 2, 'Bottle (Red Potion)': 3, 'Bottle (Green Potion)': 4, 'Bottle (Blue Potion)': 5, + 'Bottle (Fairy)': 6, 'Bottle (Bee)': 7, 'Bottle (Good Bee)': 8} + rupees = {'Rupee (1)': 1, 'Rupees (5)': 5, 'Rupees (20)': 20, 'Rupees (50)': 50, 'Rupees (100)': 100, 'Rupees (300)': 300} + bomb_caps = {'Bomb Upgrade (+5)': 5, 'Bomb Upgrade (+10)': 10} + arrow_caps = {'Arrow Upgrade (+5)': 5, 'Arrow Upgrade (+10)': 10} + bombs = {'Single Bomb': 1, 'Bombs (3)': 3, 'Bombs (10)': 10} + arrows = {'Single Arrow': 1, 'Arrows (10)': 10} + + if item.name in set_table: + equip[set_table[item.name][0]] = set_table[item.name][1] + elif item.name in or_table: + equip[or_table[item.name][0]] |= or_table[item.name][1] + elif item.name in set_or_table: + equip[set_or_table[item.name][0]] = set_or_table[item.name][1] + equip[set_or_table[item.name][2]] |= set_or_table[item.name][3] + elif item.name in keys: + for address in keys[item.name]: + equip[address] = min(equip[address] + 1, 99) + elif item.name in bottles: + if equip[0x34F] < world.difficulty_requirements[player].progressive_bottle_limit: + equip[0x35C + equip[0x34F]] = bottles[item.name] + equip[0x34F] += 1 + elif item.name in rupees: + equip[0x360:0x362] = list(min(equip[0x360] + (equip[0x361] << 8) + rupees[item.name], 9999).to_bytes(2, byteorder='little', signed=False)) + equip[0x362:0x364] = list(min(equip[0x362] + (equip[0x363] << 8) + rupees[item.name], 9999).to_bytes(2, byteorder='little', signed=False)) + elif item.name in bomb_caps: + starting_bomb_cap_upgrades += bomb_caps[item.name] + elif item.name in arrow_caps: + starting_arrow_cap_upgrades += arrow_caps[item.name] + elif item.name in bombs: + starting_bombs += bombs[item.name] + elif item.name in arrows: + if world.retro[player]: + equip[0x38E] |= 0x80 + starting_arrows = 1 + else: + starting_arrows += arrows[item.name] + elif item.name in ['Piece of Heart', 'Boss Heart Container', 'Sanctuary Heart Container']: + if item.name == 'Piece of Heart': + equip[0x36B] = (equip[0x36B] + 1) % 4 + if item.name != 'Piece of Heart' or equip[0x36B] == 0: + equip[0x36C] = min(equip[0x36C] + 0x08, 0xA0) + equip[0x36D] = min(equip[0x36D] + 0x08, 0xA0) + else: + raise RuntimeError(f'Unsupported item in starting equipment: {item.name}') + + equip[0x370] = min(starting_bomb_cap_upgrades, 50) + equip[0x371] = min(starting_arrow_cap_upgrades, 70) + equip[0x343] = min(starting_bombs, (equip[0x370] + starting_max_bombs)) + equip[0x377] = min(starting_arrows, (equip[0x371] + starting_max_arrows)) + + # Assertion and copy equip to initial_sram_bytes + assert equip[:0x340] == [0] * 0x340 + self._initial_sram_bytes[0x340:0x38F] = equip[0x340:0x38F] + + # Set counters and highest equipment values + self._initial_sram_bytes[0x471] = count_set_bits(self._initial_sram_bytes[0x37A]) + self._initial_sram_bytes[0x429] = count_set_bits(self._initial_sram_bytes[0x374]) + self._initial_sram_bytes[0x417] = self._initial_sram_bytes[0x359] + self._initial_sram_bytes[0x422] = self._initial_sram_bytes[0x35A] + self._initial_sram_bytes[0x46E] = self._initial_sram_bytes[0x35B] + + if world.swords[player] == "swordless": + self._initial_sram_bytes[0x359] = 0xFF + self._initial_sram_bytes[0x417] = 0x00 + + def set_starting_rupees(self, rupees: int): + if not (-1 < rupees < 10000): + raise ValueError("Starting rupees must be between 0 and 9999") + self._initial_sram_bytes[0x362] = self._initial_sram_bytes[0x360] = rupees & 0xFF + self._initial_sram_bytes[0x363] = self._initial_sram_bytes[0x361] = rupees >> 8 + + def set_progress_indicator(self, indicator: int): + self._set_value(0x3C5, indicator) + + def set_progress_flags(self, flags: int): + self._set_value(0x3C6, flags) + + def set_starting_entrance(self, entrance: int): + self._set_value(0x3C8, entrance) + + def set_starting_timer(self, seconds: int): + timer = (seconds * 60).to_bytes(4, "little") + self._initial_sram_bytes[0x454] = timer[0] + self._initial_sram_bytes[0x455] = timer[1] + self._initial_sram_bytes[0x456] = timer[2] + self._initial_sram_bytes[0x457] = timer[3] + + def set_swordless_curtains(self): + self._or_value(ROOM_DATA+0x61, 0x80) + self._or_value(ROOM_DATA+0x93, 0x80) + + def get_initial_sram(self): + assert len(self._initial_sram_bytes) == SRAM_SIZE + + return self._initial_sram_bytes[:] diff --git a/ItemList.py b/ItemList.py index 2d647c5c..f2500e07 100644 --- a/ItemList.py +++ b/ItemList.py @@ -44,6 +44,7 @@ Difficulty = namedtuple('Difficulty', 'progressive_bow_limit', 'heart_piece_limit', 'boss_heart_container_limit']) total_items_to_place = 153 +max_goal = 850 difficulties = { 'normal': Difficulty( @@ -203,6 +204,7 @@ def generate_itempool(world, player): world.push_item(loc, ItemFactory('Triforce', player), False) loc.event = True loc.locked = True + loc.forced_item = loc.item world.get_location('Ganon', player).event = True world.get_location('Ganon', player).locked = True @@ -795,7 +797,8 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, if goal in ['triforcehunt', 'trinity']: if treasure_hunt_total == 0: treasure_hunt_total = 30 if goal == 'triforcehunt' else 10 - triforcepool = ['Triforce Piece'] * int(treasure_hunt_total) + # triforce pieces max out + triforcepool = ['Triforce Piece'] * min(treasure_hunt_total, max_goal) pool.extend(alwaysitems) @@ -984,8 +987,8 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s pool.append(thisbottle) if customitemarray["triforcepieces"] > 0 or customitemarray["triforcepiecesgoal"] > 0: - # To display, count must be between 1 and 254 - larger values are not yet supported - treasure_hunt_count = max(min(customitemarray["triforcepiecesgoal"], 254), 1) + # Location pool doesn't support larger values + treasure_hunt_count = max(min(customitemarray["triforcepiecesgoal"], max_goal), 1) treasure_hunt_icon = 'Triforce Piece' # Ensure game is always possible to complete here, force sufficient pieces if the player is unwilling. if ((customitemarray["triforcepieces"] < treasure_hunt_count) and (goal in ['triforcehunt', 'trinity']) @@ -1176,7 +1179,7 @@ def get_player_dungeon_item_pool(world, player): if dungeon.player == player and item.location is None] -# To display, count must be between 1 and 254 - larger values are not yet supported +# location pool doesn't support larger values at this time def set_default_triforce(goal, custom_goal, custom_total): triforce_goal, triforce_total = 0, 0 if goal == 'triforcehunt': @@ -1184,9 +1187,9 @@ def set_default_triforce(goal, custom_goal, custom_total): elif goal == 'trinity': triforce_goal, triforce_total = 8, 10 if custom_goal > 0: - triforce_goal = max(min(custom_goal, 254), 1) + triforce_goal = max(min(custom_goal, max_goal), 1) if custom_total > 0: - triforce_total = max(min(custom_total, 254), triforce_goal) + triforce_total = max(min(custom_total, max_goal), triforce_goal) return triforce_goal, triforce_total diff --git a/Items.py b/Items.py index 8857e302..22a0ad8a 100644 --- a/Items.py +++ b/Items.py @@ -26,23 +26,23 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'You have\nchosen the\narche 'Progressive Bow': (True, False, None, 0x64, 150, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'), 'Progressive Bow (Alt)': (True, False, None, 0x65, 150, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'), 'Book of Mudora': (True, False, None, 0x1D, 150, 'This is a\nparadox?!', 'and the story book', 'the scholarly kid', 'moon runes for sale', 'drugs for literacy', 'book-worm boy can read again', 'the Book'), - 'Hammer': (True, False, None, 0x09, 250, 'stop\nhammer time!', 'and m c hammer', 'hammer-smashing kid', 'm c hammer for sale', 'stop... hammer time', 'stop, hammer time', 'the hammer'), + 'Hammer': (True, False, None, 0x09, 250, 'stop\nhammer time!', 'and m c hammer', 'hammer-smashing kid', 'm c hammer for sale', 'stop... hammer time', 'stop, hammer time', 'the Hammer'), 'Hookshot': (True, False, None, 0x0A, 250, 'BOING!!!\nBOING!!!\nBOING!!!', 'and the tickle beam', 'tickle-monster kid', 'tickle beam for sale', 'witch and tickle boy', 'beam boy tickles again', 'the Hookshot'), 'Magic Mirror': (True, False, None, 0x1A, 250, 'Isn\'t your\nreflection so\npretty?', 'the face reflector', 'the narcissistic kid', 'your face for sale', 'trades looking-glass', 'narcissistic boy is happy again', 'the Mirror'), 'Ocarina': (True, False, None, 0x14, 250, 'Save the duck\nand fly to\nfreedom!', 'and the duck call', 'the duck-call kid', 'duck call for sale', 'duck-calls for trade', 'ocarina boy plays again', 'the Flute'), 'Pegasus Boots': (True, False, None, 0x4B, 250, 'Gotta go fast!', 'and the sprint shoes', 'the running-man kid', 'sprint shoe for sale', 'shrooms for speed', 'gotta-go-fast boy runs again', 'the Boots'), - 'Power Glove': (True, False, None, 0x1B, 100, 'Now you can\nlift weak\nstuff!', 'and the grey mittens', 'body-building kid', 'lift glove for sale', 'fungus for gloves', 'body-building boy lifts again', 'the glove'), - 'Cape': (True, False, None, 0x19, 50, 'Wear this to\nbecome\ninvisible!', 'the camouflage cape', 'red riding-hood kid', 'red hood for sale', 'hood from a hood', 'dapper boy hides again', 'the cape'), - 'Mushroom': (True, False, None, 0x29, 50, 'I\'m a fun guy!\n\nI\'m a funghi!', 'and the legal drugs', 'the drug-dealing kid', 'legal drugs for sale', 'shroom swap', 'shroom boy sells drugs again', 'the mushroom'), - 'Shovel': (True, False, None, 0x13, 50, 'Can\n You\n Dig it?', 'and the spade', 'archaeologist kid', 'dirt spade for sale', 'can you dig it', 'shovel boy digs again', 'the shovel'), - 'Lamp': (True, False, None, 0x12, 150, 'Baby, baby,\nbaby.\nLight my way!', 'and the flashlight', 'light-shining kid', 'flashlight for sale', 'fungus for illumination', 'illuminated boy can see again', 'the lamp'), - 'Magic Powder': (True, False, None, 0x0D, 50, 'you can turn\nanti-faeries\ninto faeries', 'and the magic sack', 'the sack-holding kid', 'magic sack for sale', 'the witch and assistant', 'magic boy plays marbles again', 'the powder'), - 'Moon Pearl': (True, False, None, 0x1F, 200, ' Bunny Link\n be\n gone!', 'and the jaw breaker', 'fortune-telling kid', 'lunar orb for sale', 'shrooms for moon rock', 'moon boy plays ball again', 'the moon pearl'), - 'Cane of Somaria': (True, False, None, 0x15, 250, 'I make blocks\nto hold down\nswitches!', 'and the red blocks', 'the block-making kid', 'block stick for sale', 'block stick for trade', 'cane boy makes blocks again', 'the red cane'), - 'Fire Rod': (True, False, None, 0x07, 250, 'I\'m the hot\nrod. I make\nthings burn!', 'and the flamethrower', 'fire-starting kid', 'rage rod for sale', 'fungus for rage-rod', 'firestarter boy burns again', 'the fire rod'), - 'Flippers': (True, False, None, 0x1E, 250, 'fancy a swim?', 'and the toewebs', 'the swimming kid', 'finger webs for sale', 'shrooms let you swim', 'swimming boy swims again', 'the flippers'), - 'Ice Rod': (True, False, None, 0x08, 250, 'I\'m the cold\nrod. I make\nthings freeze!', 'and the freeze ray', 'the ice-bending kid', 'freeze ray for sale', 'fungus for ice-rod', 'ice-cube boy freezes again', 'the ice rod'), - 'Titans Mitts': (True, False, None, 0x1C, 200, 'Now you can\nlift heavy\nstuff!', 'and the golden glove', 'body-building kid', 'carry glove for sale', 'fungus for bling-gloves', 'body-building boy has gold again', 'the mitts'), + 'Power Glove': (True, False, None, 0x1B, 100, 'Now you can\nlift weak\nstuff!', 'and the grey mittens', 'body-building kid', 'lift glove for sale', 'fungus for gloves', 'body-building boy lifts again', 'the Glove'), + 'Cape': (True, False, None, 0x19, 50, 'Wear this to\nbecome\ninvisible!', 'the camouflage cape', 'red riding-hood kid', 'red hood for sale', 'hood from a hood', 'dapper boy hides again', 'the Cape'), + 'Mushroom': (True, False, None, 0x29, 50, 'I\'m a fun guy!\n\nI\'m a funghi!', 'and the legal drugs', 'the drug-dealing kid', 'legal drugs for sale', 'shroom swap', 'shroom boy sells drugs again', 'the Mushroom'), + 'Shovel': (True, False, None, 0x13, 50, 'Can\n You\n Dig it?', 'and the spade', 'archaeologist kid', 'dirt spade for sale', 'can you dig it', 'shovel boy digs again', 'the Shovel'), + 'Lamp': (True, False, None, 0x12, 150, 'Baby, baby,\nbaby.\nLight my way!', 'and the flashlight', 'light-shining kid', 'flashlight for sale', 'fungus for illumination', 'illuminated boy can see again', 'the Lamp'), + 'Magic Powder': (True, False, None, 0x0D, 50, 'you can turn\nanti-faeries\ninto faeries', 'and the magic sack', 'the sack-holding kid', 'magic sack for sale', 'the witch and assistant', 'magic boy plays marbles again', 'the Powder'), + 'Moon Pearl': (True, False, None, 0x1F, 200, ' Bunny Link\n be\n gone!', 'and the jaw breaker', 'fortune-telling kid', 'lunar orb for sale', 'shrooms for moon rock', 'moon boy plays ball again', 'the Moon Pearl'), + 'Cane of Somaria': (True, False, None, 0x15, 250, 'I make blocks\nto hold down\nswitches!', 'and the red blocks', 'the block-making kid', 'block stick for sale', 'block stick for trade', 'cane boy makes blocks again', 'the red Cane'), + 'Fire Rod': (True, False, None, 0x07, 250, 'I\'m the hot\nrod. I make\nthings burn!', 'and the flamethrower', 'fire-starting kid', 'rage rod for sale', 'fungus for rage-rod', 'firestarter boy burns again', 'the Fire Rod'), + 'Flippers': (True, False, None, 0x1E, 250, 'fancy a swim?', 'and the toewebs', 'the swimming kid', 'finger webs for sale', 'shrooms let you swim', 'swimming boy swims again', 'the Flippers'), + 'Ice Rod': (True, False, None, 0x08, 250, 'I\'m the cold\nrod. I make\nthings freeze!', 'and the freeze ray', 'the ice-bending kid', 'freeze ray for sale', 'fungus for ice-rod', 'ice-cube boy freezes again', 'the Ice Rod'), + 'Titans Mitts': (True, False, None, 0x1C, 200, 'Now you can\nlift heavy\nstuff!', 'and the golden glove', 'body-building kid', 'carry glove for sale', 'fungus for bling-gloves', 'body-building boy has gold again', 'the Mitts'), 'Bombos': (True, False, None, 0x0F, 100, 'Burn, baby,\nburn! Fear my\nring of fire!', 'and the swirly coin', 'coin-collecting kid', 'swirly coin for sale', 'shrooms for swirly-coin', 'medallion boy melts room again', 'Bombos'), 'Ether': (True, False, None, 0x10, 100, 'This magic\ncoin freezes\neverything!', 'and the bolt coin', 'coin-collecting kid', 'bolt coin for sale', 'shrooms for bolt-coin', 'medallion boy sees floor again', 'Ether'), 'Quake': (True, False, None, 0x11, 100, 'Maxing out the\nRichter scale\nis what I do!', 'and the wavy coin', 'coin-collecting kid', 'wavy coin for sale', 'shrooms for wavy-coin', 'medallion boy shakes dirt again', 'Quake'), @@ -56,23 +56,24 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'You have\nchosen the\narche 'Master Sword': (True, False, 'Sword', 0x50, 100, 'I beat barries and pigs alike', 'and the master sword', 'sword-wielding kid', 'glow sword for sale', 'fungus for blue slasher', 'sword boy fights again', 'the Master Sword'), 'Tempered Sword': (True, False, 'Sword', 0x02, 150, 'I stole the\nblacksmith\'s\njob!', 'the tempered sword', 'sword-wielding kid', 'flame sword for sale', 'fungus for red slasher', 'sword boy fights again', 'the Tempered Sword'), 'Fighter Sword': (True, False, 'Sword', 0x49, 50, 'A pathetic\nsword rests\nhere!', 'the tiny sword', 'sword-wielding kid', 'tiny sword for sale', 'fungus for tiny slasher', 'sword boy fights again', 'the small sword'), + 'Sword and Shield': (True, False, 'Sword', 0x00, 'An uncle\nsword rests\nhere!', 'the sword and shield', 'sword and shield-wielding kid', 'training set for sale', 'fungus for training set', 'sword and shield boy fights again', 'the small sword and shield'), 'Golden Sword': (True, False, 'Sword', 0x03, 200, 'The butter\nsword rests\nhere!', 'and the butter sword', 'sword-wielding kid', 'butter for sale', 'cap churned to butter', 'sword boy fights again', 'the Golden Sword'), 'Progressive Sword': (True, False, 'Sword', 0x5E, 150, 'a better copy\nof your sword\nfor your time', 'the unknown sword', 'sword-wielding kid', 'sword for sale', 'fungus for some slasher', 'sword boy fights again', 'a sword'), 'Progressive Glove': (True, False, None, 0x61, 150, 'a way to lift\nheavier things', 'and the lift upgrade', 'body-building kid', 'some glove for sale', 'fungus for gloves', 'body-building boy lifts again', 'a glove'), 'Silver Arrows': (True, False, None, 0x58, 100, 'Do you fancy\nsilver tipped\narrows?', 'and the ganonsbane', 'ganon-killing kid', 'ganon doom for sale', 'fungus for pork', 'archer boy shines again', 'the silver arrows'), - 'Green Pendant': (True, False, 'Crystal', [0x04, 0x38, 0x62, 0x00, 0x69, 0x01, 0x01], 999, None, None, None, None, None, None, None), - 'Blue Pendant': (True, False, 'Crystal', [0x02, 0x34, 0x60, 0x00, 0x69, 0x02, 0x02], 999, None, None, None, None, None, None, None), - 'Red Pendant': (True, False, 'Crystal', [0x01, 0x32, 0x60, 0x00, 0x69, 0x03, 0x02], 999, None, None, None, None, None, None, None), + 'Green Pendant': (True, False, 'Crystal', [0x04, 0x38, 0x62, 0x00, 0x69, 0x01, 0x08], 999, None, None, None, None, None, None, None), + 'Blue Pendant': (True, False, 'Crystal', [0x02, 0x34, 0x60, 0x00, 0x69, 0x02, 0x09], 999, None, None, None, None, None, None, None), + 'Red Pendant': (True, False, 'Crystal', [0x01, 0x32, 0x60, 0x00, 0x69, 0x03, 0x0a], 999, None, None, None, None, None, None, None), 'Triforce': (True, False, None, 0x6A, 777, '\n YOU WIN!', 'and the triforce', 'victorious kid', 'victory for sale', 'fungus for the win', 'greedy boy wins game again', 'the Triforce'), 'Power Star': (True, False, None, 0x6B, 100, 'a small victory', 'and the power star', 'star-struck kid', 'star for sale', 'see stars with shroom', 'mario powers up again', 'a Power Star'), 'Triforce Piece': (True, False, None, 0x6C, 100, 'a small victory', 'and the thirdforce', 'triangular kid', 'triangle for sale', 'fungus for triangle', 'wise boy has triangle again', 'a Triforce Piece'), - 'Crystal 1': (True, False, 'Crystal', [0x02, 0x34, 0x64, 0x40, 0x7F, 0x06, 0x04], 999, None, None, None, None, None, None, None), - 'Crystal 2': (True, False, 'Crystal', [0x10, 0x34, 0x64, 0x40, 0x79, 0x06, 0x04], 999, None, None, None, None, None, None, None), - 'Crystal 3': (True, False, 'Crystal', [0x40, 0x34, 0x64, 0x40, 0x6C, 0x06, 0x04], 999, None, None, None, None, None, None, None), + 'Crystal 1': (True, False, 'Crystal', [0x02, 0x34, 0x64, 0x40, 0x7F, 0x06, 0x01], 999, None, None, None, None, None, None, None), + 'Crystal 2': (True, False, 'Crystal', [0x10, 0x34, 0x64, 0x40, 0x79, 0x06, 0x02], 999, None, None, None, None, None, None, None), + 'Crystal 3': (True, False, 'Crystal', [0x40, 0x34, 0x64, 0x40, 0x6C, 0x06, 0x03], 999, None, None, None, None, None, None, None), 'Crystal 4': (True, False, 'Crystal', [0x20, 0x34, 0x64, 0x40, 0x6D, 0x06, 0x04], 999, None, None, None, None, None, None, None), - 'Crystal 5': (True, False, 'Crystal', [0x04, 0x32, 0x64, 0x40, 0x6E, 0x06, 0x03], 999, None, None, None, None, None, None, None), - 'Crystal 6': (True, False, 'Crystal', [0x01, 0x32, 0x64, 0x40, 0x6F, 0x06, 0x03], 999, None, None, None, None, None, None, None), - 'Crystal 7': (True, False, 'Crystal', [0x08, 0x34, 0x64, 0x40, 0x7C, 0x06, 0x04], 999, None, None, None, None, None, None, None), + 'Crystal 5': (True, False, 'Crystal', [0x04, 0x32, 0x64, 0x40, 0x6E, 0x06, 0x05], 999, None, None, None, None, None, None, None), + 'Crystal 6': (True, False, 'Crystal', [0x01, 0x32, 0x64, 0x40, 0x6F, 0x06, 0x06], 999, None, None, None, None, None, None, None), + 'Crystal 7': (True, False, 'Crystal', [0x08, 0x34, 0x64, 0x40, 0x7C, 0x06, 0x07], 999, None, None, None, None, None, None, None), 'Single Arrow': (False, False, None, 0x43, 3, 'a lonely arrow\nsits here.', 'and the arrow', 'stick-collecting kid', 'sewing needle for sale', 'fungus for arrow', 'archer boy sews again', 'an arrow'), 'Arrows (10)': (False, False, None, 0x44, 30, 'This will give\nyou ten shots\nwith your bow!', 'and the arrow pack', 'stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'archer boy sews again', 'ten arrows'), 'Arrow Upgrade (+10)': (False, False, None, 0x54, 100, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'), @@ -91,12 +92,12 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'You have\nchosen the\narche 'Progressive Armor': (False, True, None, 0x60, 50, 'time for a\nchange of\nclothes?', 'and the unknown hat', 'the protected kid', 'new hat for sale', 'the clothing store', 'tailor boy has threads again', 'some armor'), 'Blue Boomerang': (True, False, None, 0x0C, 50, 'No matter what\nyou do, blue\nreturns to you', 'and the bluemarang', 'the bat-throwing kid', 'bent stick for sale', 'fungus for puma-stick', 'throwing boy plays fetch again', 'the blue boomerang'), 'Red Boomerang': (True, False, None, 0x2A, 50, 'No matter what\nyou do, red\nreturns to you', 'and the badmarang', 'the bat-throwing kid', 'air foil for sale', 'fungus for return-stick', 'magical boy plays fetch again', 'the red boomerang'), - 'Blue Shield': (False, True, None, 0x04, 50, 'Now you can\ndefend against\npebbles!', 'and the stone blocker', 'shield-wielding kid', 'shield for sale', 'fungus for shield', 'shield boy defends again', 'the blue shield'), - 'Red Shield': (False, True, None, 0x05, 500, 'Now you can\ndefend against\nfireballs!', 'and the shot blocker', 'shield-wielding kid', 'fire shield for sale', 'fungus for fire shield', 'shield boy defends again', 'the red shield'), - 'Mirror Shield': (True, False, None, 0x06, 200, 'Now you can\ndefend against\nlasers!', 'and the laser blocker', 'shield-wielding kid', 'face shield for sale', 'fungus for face shield', 'shield boy defends again', 'the mirror shield'), + 'Blue Shield': (False, True, None, 0x04, 50, 'Now you can\ndefend against\npebbles!', 'and the stone blocker', 'shield-wielding kid', 'shield for sale', 'fungus for shield', 'shield boy defends again', 'a blue shield'), + 'Red Shield': (False, True, None, 0x05, 500, 'Now you can\ndefend against\nfireballs!', 'and the shot blocker', 'shield-wielding kid', 'fire shield for sale', 'fungus for fire shield', 'shield boy defends again', 'a red shield'), + 'Mirror Shield': (True, False, None, 0x06, 200, 'Now you can\ndefend against\nlasers!', 'and the laser blocker', 'shield-wielding kid', 'face shield for sale', 'fungus for face shield', 'shield boy defends again', 'the Mirror Shield'), 'Progressive Shield': (True, False, None, 0x5F, 50, 'have a better\nblocker in\nfront of you', 'and the new shield', 'shield-wielding kid', 'shield for sale', 'fungus for shield', 'shield boy defends again', 'a shield'), 'Bug Catching Net': (True, False, None, 0x21, 50, 'Let\'s catch\nsome bees and\nfaeries!', 'and the bee catcher', 'the bug-catching kid', 'stick web for sale', 'fungus for butterflies', 'wrong boy catches bees again', 'the bug net'), - 'Cane of Byrna': (True, False, None, 0x18, 50, 'Use this to\nbecome\ninvincible!', 'and the bad cane', 'the spark-making kid', 'spark stick for sale', 'spark-stick for trade', 'cane boy encircles again', 'the blue cane'), + 'Cane of Byrna': (True, False, None, 0x18, 50, 'Use this to\nbecome\ninvincible!', 'and the bad cane', 'the spark-making kid', 'spark stick for sale', 'spark-stick for trade', 'cane boy encircles again', 'the blue Cane'), 'Boss Heart Container': (False, True, None, 0x3E, 40, 'Maximum health\nincreased!\nYeah!', 'and the full heart', 'the life-giving kid', 'love for sale', 'fungus for life', 'life boy feels love again', 'a heart'), 'Sanctuary Heart Container': (False, True, None, 0x3F, 50, 'Maximum health\nincreased!\nYeah!', 'and the full heart', 'the life-giving kid', 'love for sale', 'fungus for life', 'life boy feels love again', 'a heart'), 'Piece of Heart': (False, False, None, 0x17, 10, 'Just a little\npiece of love!', 'and the broken heart', 'the life-giving kid', 'little love for sale', 'fungus for life', 'life boy feels some love again', 'a heart piece'), @@ -112,8 +113,8 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'You have\nchosen the\narche 'Green Clock': (False, True, None, 0x5D, 200, 'a lot of time', 'the emerald clock', 'the emerald-time kid', 'green time for sale', 'for emerald time', 'moment boy adjusts time again', 'a red clock'), 'Single RNG': (False, True, None, 0x62, 300, 'something you don\'t yet have', None, None, None, None, 'unknown boy somethings again', 'a new mystery'), 'Multi RNG': (False, True, None, 0x63, 100, 'something you may already have', None, None, None, None, 'unknown boy somethings again', 'a total mystery'), - 'Magic Upgrade (1/2)': (True, False, None, 0x4E, 50, 'Your magic\npower has been\ndoubled!', 'and the spell power', 'the magic-saving kid', 'wizardry for sale', 'mekalekahi mekahiney ho', 'magic boy saves magic again', 'half magic'), # can be required to beat mothula in an open seed in very very rare circumstance - 'Magic Upgrade (1/4)': (True, False, None, 0x4F, 100, 'Your magic\npower has been\nquadrupled!', 'and the spell power', 'the magic-saving kid', 'wizardry for sale', 'mekalekahi mekahiney ho', 'magic boy saves magic again', 'quarter magic'), # can be required to beat mothula in an open seed in very very rare circumstance + 'Magic Upgrade (1/2)': (True, False, None, 0x4E, 50, 'Your magic\npower has been\ndoubled!', 'and the spell power', 'the magic-saving kid', 'wizardry for sale', 'mekalekahi mekahiney ho', 'magic boy saves magic again', 'Half Magic'), # can be required to beat mothula in an open seed in very very rare circumstance + 'Magic Upgrade (1/4)': (True, False, None, 0x4F, 100, 'Your magic\npower has been\nquadrupled!', 'and the spell power', 'the magic-saving kid', 'wizardry for sale', 'mekalekahi mekahiney ho', 'magic boy saves magic again', 'Quarter Magic'), # can be required to beat mothula in an open seed in very very rare circumstance 'Small Key (Eastern Palace)': (False, False, 'SmallKey', 0xA2, 40, 'A small key to Armos Knights', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Eastern Palace'), 'Big Key (Eastern Palace)': (False, False, 'BigKey', 0x9D, 60, 'A big key to Armos Knights', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Eastern Palace'), 'Compass (Eastern Palace)': (False, True, 'Compass', 0x8D, 10, 'Now you can find the Armos Knights!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Eastern Palace'), diff --git a/Main.py b/Main.py index af0b1c9e..1c2357bb 100644 --- a/Main.py +++ b/Main.py @@ -33,7 +33,7 @@ from source.overworld.EntranceShuffle2 import link_entrances_new from source.tools.BPS import create_bps_from_data from source.classes.CustomSettings import CustomSettings -__version__ = '1.0.1.13w' +__version__ = '1.0.2.4-w' from source.classes.BabelFish import BabelFish @@ -115,8 +115,8 @@ def main(args, seed=None, fish=None): world.potshuffle = args.shufflepots.copy() world.mixed_travel = args.mixed_travel.copy() world.standardize_palettes = args.standardize_palettes.copy() - world.treasure_hunt_count = args.triforce_goal.copy() - world.treasure_hunt_total = args.triforce_pool.copy() + world.treasure_hunt_count = {k: int(v) for k, v in args.triforce_goal.items()} + world.treasure_hunt_total = {k: int(v) for k, v in args.triforce_pool.items()} world.shufflelinks = args.shufflelinks.copy() world.pseudoboots = args.pseudoboots.copy() world.overworld_map = args.overworld_map.copy() diff --git a/MultiClient.py b/MultiClient.py index 4504fed7..fbde673d 100644 --- a/MultiClient.py +++ b/MultiClient.py @@ -90,9 +90,11 @@ INGAME_MODES = {0x07, 0x09, 0x0b} SAVEDATA_START = WRAM_START + 0xF000 SAVEDATA_SIZE = 0x500 -POT_ITEMS_SRAM_START = WRAM_START + 0x016600 -SPRITE_ITEMS_SRAM_START = WRAM_START + 0x016850 +POT_ITEMS_SRAM_START = WRAM_START + 0x016018 # 2 bytes per room +SPRITE_ITEMS_SRAM_START = WRAM_START + 0x016268 # 2 bytes per room +SHOP_SRAM_START = WRAM_START + 0x0164B8 # 2 bytes? ITEM_SRAM_SIZE = 0x250 +SHOP_SRAM_LEN = 0x29 # 41 tracked items RECV_PROGRESS_ADDR = SAVEDATA_START + 0x4D0 # 2 bytes RECV_ITEM_ADDR = SAVEDATA_START + 0x4D2 # 1 byte @@ -103,11 +105,9 @@ SCOUT_LOCATION_ADDR = SAVEDATA_START + 0x4D7 # 1 byte SCOUTREPLY_LOCATION_ADDR = SAVEDATA_START + 0x4D8 # 1 byte SCOUTREPLY_ITEM_ADDR = SAVEDATA_START + 0x4D9 # 1 byte SCOUTREPLY_PLAYER_ADDR = SAVEDATA_START + 0x4DA # 1 byte -SHOP_ADDR = SAVEDATA_START + 0x302 # 2 bytes? DYNAMIC_TOTAL_ADDR = SAVEDATA_START + 0x33E # 2 bytes MODE_FLAGS = SAVEDATA_START + 0x33D # 1 byte -SHOP_SRAM_LEN = 0x29 # 41 tracked items location_shop_order = [Regions.shop_to_location_table.keys()] + [Regions.retro_shops.keys()] location_shop_ids = {0x0112, 0x0110, 0x010F, 0x00FF, 0x011F, 0x0109, 0x0115} @@ -894,7 +894,7 @@ async def track_locations(ctx : Context, roomid, roomdata): try: if ctx.shop_mode or ctx.retro_mode: - misc_data = await snes_read(ctx, SHOP_ADDR, SHOP_SRAM_LEN) + misc_data = await snes_read(ctx, SHOP_SRAM_START, SHOP_SRAM_LEN) for cnt, b in enumerate(misc_data): my_check = Regions.shop_table_by_location_id[0x400000 + cnt] if int(b) > 0 and my_check not in ctx.locations_checked: diff --git a/PotShuffle.py b/PotShuffle.py index 17107bae..a1c96783 100644 --- a/PotShuffle.py +++ b/PotShuffle.py @@ -1015,8 +1015,8 @@ key_drop_data = { 'Swamp Palace - Waterway Pot Key': ['Pot', 0x16, 'in a pot in Swamp Palace', 'Small Key (Swamp Palace)'], 'Skull Woods - West Lobby Pot Key': ['Pot', 0x56, 'in a pot in Skull Woods', 'Small Key (Skull Woods)'], 'Skull Woods - Spike Corner Key Drop': ['Drop', (0x09DD74, 0x39, 1), 'dropped near Mothula', 'Small Key (Skull Woods)'], - "Thieves' Town - Hallway Pot Key": ['Pot', 0xBC, "in a pot in Thieves' Town", 'Small Key (Thieves Town)'], - "Thieves' Town - Spike Switch Pot Key": ['Pot', 0xAB, "in a pot in Thieves' Town", 'Small Key (Thieves Town)'], + "Thieves' Town - Hallway Pot Key": ['Pot', 0xBC, "in a pot in Thieves Town", 'Small Key (Thieves Town)'], + "Thieves' Town - Spike Switch Pot Key": ['Pot', 0xAB, "in a pot in Thieves Town", 'Small Key (Thieves Town)'], 'Ice Palace - Jelly Key Drop': ['Drop', (0x09DA21, 0xE, 3), 'dropped in Ice Palace', 'Small Key (Ice Palace)'], 'Ice Palace - Conveyor Key Drop': ['Drop', (0x09DE08, 0x3E, 8), 'dropped in Ice Palace', 'Small Key (Ice Palace)'], 'Ice Palace - Hammer Block Key Drop': ['Pot', 0x3F, 'under a block in Ice Palace', 'Small Key (Ice Palace)'], @@ -1052,7 +1052,7 @@ class PotSecretTable(object): rom.write_bytes(pointer_address + room * 2, int16_as_bytes(data_address)) for pot in self.room_map[room]: rom.write_bytes(data_pointer + list_idx * 3, pot.pot_data()) - if pot.location is not None: + if pot.location is not None and not pot.location.forced_item: collection_rate_mask |= 1 << (15 - list_idx) if colorize and pot.obj_ref: pot.obj_ref.change_type(Shuffled_Pot) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d5f3bfe4..35e9c5d4 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -107,7 +107,7 @@ These districts are chosen at random and then filled with major items. If a loca In entrance shuffle, what is shuffled to the entrances is considered instead of where the interior was originally. For example, if Blind's Hut is shuffled to the Dam, then the 5 chests in Blind's Hut are part of Central Hyrule instead of Kakariko. -Bombos Table, Lake Hylia Island, Bumper Cave Ledge, the Floating Island, Cave 45, the Graveyard Cave, Checkerboard Cave and Mimic Cave are considered part of the dark world region that you mirror from to get there (except in inverted where these are only accessible in the Light World). Note that Spectacle Rock is always part of light Death Mountain. +Note: Bombos Tablet, Lake Hylia Island, Bumper Cave Ledge, the Floating Island, Cave 45, the Graveyard Cave, Checkerboard Cave and Mimic Cave are considered part of the light world region rather than the dark world region you mirror from. In multiworld, the districts chosen apply to all players. @@ -170,6 +170,27 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o #### Volatile +* 1.0.2.4 + * Updated tourney winners (included Doors Async League winners) + * Fixed a couple issues with dungeon counters and the DungeonCompletion field for autotracking +* 1.0.2.3 + * Fix MultiClient for new shop data location in SRAM + * Some minor text updates +* 1.0.2.2 + * Change to all key pots and enemy key drops: always use the same address + * Don't colorize key pots in mystery if the item is "forced" +* 1.0.2.1 + * Fix for paired doors + * Fix for forbidding certain dashable doors (it actually does something this time) +* 1.0.2.0 + * Updated baserom to bleeding edge + * Pottery and enemy SRAM re-located to final destination + * Bulk of work on new font + * Updated TFH to support up to 850 pieces + * Fix for major item algorithm and pottery + * Updated map display on keysanity menu to work better with overworld_amp option + * Minor bug in crossed doors + * Minor bug in MultiClient which would count switches * 1.0.1.13 * New pottery modes * Trinity goal added @@ -260,6 +281,10 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o #### Unstable +* 1.0.0.3 + * overworld_map=map mode fixed. Location of dungeons with maps are not shown until map is retrieved. (Dungeon that do not have map like Castle Tower are simply never shown) + * Aga2 completion on overworld_map now tied to boss defeat flag instead of pyramid hole being opened (fast ganon fix) + * Minor issue in dungeon_only algorithm fixed (minorly affected major_only keyshuffle and vanilla fallbacks) * 1.0.0.2 * Include 1.0.1 fixes * District hint rework diff --git a/Regions.py b/Regions.py index 331795dc..29d61bf9 100644 --- a/Regions.py +++ b/Regions.py @@ -1097,7 +1097,7 @@ def create_pot_location(pot, pot_index, super_tile, world, player): def pot_address(pot_index, super_tile): - return 0x7f6600 + super_tile * 2 + (pot_index << 24) + return 0x7f6018 + super_tile * 2 + (pot_index << 24) # (type, room_id, shopkeeper, custom, locked, [items]) @@ -1251,7 +1251,7 @@ location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'), 'Paradox Cave Lower - Middle': (0xeb36, 0x18630a, False, 'in a cave with seven chests'), 'Paradox Cave Upper - Left': (0xeb39, 0x18630d, False, 'in a cave with seven chests'), 'Paradox Cave Upper - Right': (0xeb3c, 0x186310, False, 'in a cave with seven chests'), - 'Spiral Cave': (0xe9bf, 0x186193, False, 'in spiral cave'), + 'Spiral Cave': (0xe9bf, 0x186193, False, 'in Spiral Cave'), 'Ether Tablet': (0x180016, 0x18633b, False, 'at a monolith'), 'Spectacle Rock': (0x180140, 0x18634f, False, 'atop a rock'), 'Tower of Hera - Basement Cage': (0x180162, 0x18633a, False, 'in Tower of Hera'), @@ -1260,7 +1260,7 @@ location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'), 'Tower of Hera - Compass Chest': (0xe9fb, 0x1861cf, False, 'in Tower of Hera'), 'Tower of Hera - Big Chest': (0xe9f8, 0x1861cc, False, 'in Tower of Hera'), 'Tower of Hera - Boss': (0x180152, 0x186340, False, 'with Moldorm'), - 'Pyramid': (0x180147, 0x186356, False, 'on the pyramid'), + 'Pyramid': (0x180147, 0x186356, False, 'on the Pyramid'), 'Catfish': (0xee185, 0x186361, False, 'with a catfish'), 'Stumpy': (0x330c7, 0x18636a, False, 'with tree boy'), 'Digging Game': (0x180148, 0x186357, False, 'underground'), @@ -1298,13 +1298,13 @@ location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'), 'Swamp Palace - Flooded Room - Right': (0xeaac, 0x186280, False, 'in Swamp Palace'), 'Swamp Palace - Waterfall Room': (0xeaaf, 0x186283, False, 'in Swamp Palace'), 'Swamp Palace - Boss': (0x180154, 0x186342, False, 'with Arrghus'), - "Thieves' Town - Big Key Chest": (0xea04, 0x1861d8, False, "in Thieves' Town"), - "Thieves' Town - Map Chest": (0xea01, 0x1861d5, False, "in Thieves' Town"), - "Thieves' Town - Compass Chest": (0xea07, 0x1861db, False, "in Thieves' Town"), - "Thieves' Town - Ambush Chest": (0xea0a, 0x1861de, False, "in Thieves' Town"), - "Thieves' Town - Attic": (0xea0d, 0x1861e1, False, "in Thieves' Town"), - "Thieves' Town - Big Chest": (0xea10, 0x1861e4, False, "in Thieves' Town"), - "Thieves' Town - Blind's Cell": (0xea13, 0x1861e7, False, "in Thieves' Town"), + "Thieves' Town - Big Key Chest": (0xea04, 0x1861d8, False, "in Thieves Town"), + "Thieves' Town - Map Chest": (0xea01, 0x1861d5, False, "in Thieves Town"), + "Thieves' Town - Compass Chest": (0xea07, 0x1861db, False, "in Thieves Town"), + "Thieves' Town - Ambush Chest": (0xea0a, 0x1861de, False, "in Thieves Town"), + "Thieves' Town - Attic": (0xea0d, 0x1861e1, False, "in Thieves Town"), + "Thieves' Town - Big Chest": (0xea10, 0x1861e4, False, "in Thieves Town"), + "Thieves' Town - Blind's Cell": (0xea13, 0x1861e7, False, "in Thieves Town"), "Thieves' Town - Boss": (0x180156, 0x186344, False, 'with Blind'), 'Skull Woods - Compass Chest': (0xe992, 0x186166, False, 'in Skull Woods'), 'Skull Woods - Map Chest': (0xe99b, 0x18616f, False, 'in Skull Woods'), @@ -1405,7 +1405,7 @@ location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'), 'Tower of Hera - Prize': ([0x120A5, 0x53E78, 0x53E79, 0x18005A, 0x180071, 0xC706, 0x186FEA], None, True, 'Tower of Hera'), 'Palace of Darkness - Prize': ([0x120A1, 0x53E7C, 0x53E7D, 0x180056, 0x180073, 0xC702, 0x186FE6], None, True, 'Palace of Darkness'), 'Swamp Palace - Prize': ([0x120A0, 0x53E88, 0x53E89, 0x180055, 0x180079, 0xC701, 0x186FE5], None, True, 'Swamp Palace'), - 'Thieves\' Town - Prize': ([0x120A6, 0x53E82, 0x53E83, 0x18005B, 0x180076, 0xC707, 0x186FEB], None, True, 'Thieves\' Town'), + 'Thieves\' Town - Prize': ([0x120A6, 0x53E82, 0x53E83, 0x18005B, 0x180076, 0xC707, 0x186FEB], None, True, 'Thieves Town'), 'Skull Woods - Prize': ([0x120A3, 0x53E7E, 0x53E7F, 0x180058, 0x180074, 0xC704, 0x186FE8], None, True, 'Skull Woods'), 'Ice Palace - Prize': ([0x120A4, 0x53E86, 0x53E87, 0x180059, 0x180078, 0xC705, 0x186FE9], None, True, 'Ice Palace'), 'Misery Mire - Prize': ([0x120A2, 0x53E84, 0x53E85, 0x180057, 0x180077, 0xC703, 0x186FE7], None, True, 'Misery Mire'), diff --git a/Rom.py b/Rom.py index 5d0214d2..616b7978 100644 --- a/Rom.py +++ b/Rom.py @@ -15,7 +15,7 @@ try: except ImportError: raise Exception('Could not load BPS module') -from BaseClasses import CollectionState, ShopType, Region, Location, Door, DoorType, RegionType, PotItem, LocationType +from BaseClasses import ShopType, Region, Location, Door, DoorType, RegionType, LocationType from DoorShuffle import compass_data, DROptions, boss_indicator, dungeon_portals from Dungeons import dungeon_music_addresses, dungeon_table from Regions import location_table, shop_to_location_table, retro_shops @@ -29,6 +29,7 @@ from Text import Lumberjacks_texts, SickKid_texts, FluteBoy_texts, Zora_texts, M from Utils import output_path, local_path, int16_as_bytes, int32_as_bytes, snes_to_pc from Items import ItemFactory from EntranceShuffle import door_addresses, exit_ids, ow_prize_table +from InitialSram import InitialSram from source.classes.SFX import randomize_sfx from source.item.FillUtil import valid_pot_items @@ -36,7 +37,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'd143684aa6a8e4560eb3ac912d600525' +RANDOMIZERBASEHASH = '01166fb16b38b49ef79acc9993dc4f02' class JsonRom(object): @@ -47,6 +48,7 @@ class JsonRom(object): self.orig_buffer = None self.patches = {} self.addresses = [] + self.initial_sram = InitialSram() def write_byte(self, address, value): self.write_bytes(address, [value]) @@ -76,6 +78,9 @@ class JsonRom(object): del self.patches[str(intervalstart)] del self.addresses[pos] + def write_initial_sram(self): + self.write_bytes(0x183000, self.initial_sram.get_initial_sram()) + def write_to_file(self, file): with open(file, 'w') as stream: json.dump([self.patches], stream) @@ -93,6 +98,7 @@ class LocalRom(object): self.hash = hash self.orig_buffer = None self.file = file + self.initial_sram = InitialSram() self.has_smc_header = False if not os.path.isfile(file): raise RuntimeError("Could not find valid local base rom for patching at expected path %s." % file) @@ -108,6 +114,9 @@ class LocalRom(object): def write_bytes(self, startaddress, values): self.buffer[startaddress:startaddress + len(values)] = values + def write_initial_sram(self): + self.write_bytes(0x183000, self.initial_sram.get_initial_sram()) + def write_to_file(self, file): with open(file, 'wb') as outfile: outfile.write(self.buffer) @@ -958,11 +967,11 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): # set open mode: if world.mode[player] in ['open', 'inverted']: - rom.write_byte(0x180032, 0x01) # open mode + init_open_mode_sram(rom) if world.mode[player] == 'inverted': set_inverted_mode(world, player, rom) elif world.mode[player] == 'standard': - rom.write_byte(0x180032, 0x00) # standard mode + init_standard_mode_sram(rom) uncle_location = world.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']: @@ -1186,10 +1195,10 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): # set swordless mode settings rom.write_byte(0x18003F, 0x01 if world.swords[player] == 'swordless' else 0x00) # hammer can harm ganon - rom.write_byte(0x180040, 0x01 if world.swords[player] == 'swordless' else 0x00) # open curtains rom.write_byte(0x180041, 0x01 if world.swords[player] == 'swordless' else 0x00) # swordless medallions - rom.write_byte(0x180043, 0xFF if world.swords[player] == 'swordless' else 0x00) # starting sword for link rom.write_byte(0x180044, 0x01 if world.swords[player] == 'swordless' else 0x00) # hammer activates tablets + if world.swords[player] == 'swordless': + rom.initial_sram.set_swordless_curtains() # open curtains # set up clocks for timed modes if world.shuffle[player] == 'vanilla': @@ -1205,39 +1214,39 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): write_int32(rom, 0x180200, 0) # red clock adjustment time (in frames, sint32) write_int32(rom, 0x180204, 0) # blue clock adjustment time (in frames, sint32) write_int32(rom, 0x180208, 0) # green clock adjustment time (in frames, sint32) - write_int32(rom, 0x18020C, 0) # starting time (in frames, sint32) + rom.initial_sram.set_starting_timer(0) # starting time (in frames, sint32) elif world.clock_mode == 'ohko': rom.write_bytes(0x180190, [0x01, 0x02, 0x01]) # ohko timer with resetable timer functionality write_int32(rom, 0x180200, 0) # red clock adjustment time (in frames, sint32) write_int32(rom, 0x180204, 0) # blue clock adjustment time (in frames, sint32) write_int32(rom, 0x180208, 0) # green clock adjustment time (in frames, sint32) - write_int32(rom, 0x18020C, 0) # starting time (in frames, sint32) + rom.initial_sram.set_starting_timer(0) # starting time (in frames, sint32) elif world.clock_mode == 'countdown-ohko': rom.write_bytes(0x180190, [0x01, 0x02, 0x01]) # ohko timer with resetable timer functionality write_int32(rom, 0x180200, -100 * 60 * 60 * 60) # red clock adjustment time (in frames, sint32) write_int32(rom, 0x180204, 2 * 60 * 60) # blue clock adjustment time (in frames, sint32) write_int32(rom, 0x180208, 4 * 60 * 60) # green clock adjustment time (in frames, sint32) - if world.difficulty_adjustments[player] == 'normal': - write_int32(rom, 0x18020C, (10 + ERtimeincrease) * 60 * 60) # starting time (in frames, sint32) + if world.difficulty_adjustments == 'normal': + rom.initial_sram.set_starting_timer((10 + ERtimeincrease) * 60) # starting time (in seconds) else: - write_int32(rom, 0x18020C, int((5 + ERtimeincrease / 2) * 60 * 60)) # starting time (in frames, sint32) + rom.initial_sram.set_starting_timer(int((5 + ERtimeincrease / 2) * 60)) # starting time (in seconds) if world.clock_mode == 'stopwatch': rom.write_bytes(0x180190, [0x02, 0x01, 0x00]) # set stopwatch mode write_int32(rom, 0x180200, -2 * 60 * 60) # red clock adjustment time (in frames, sint32) write_int32(rom, 0x180204, 2 * 60 * 60) # blue clock adjustment time (in frames, sint32) write_int32(rom, 0x180208, 4 * 60 * 60) # green clock adjustment time (in frames, sint32) - write_int32(rom, 0x18020C, 0) # starting time (in frames, sint32) + rom.initial_sram.set_starting_timer(0) # starting time (in frames, sint32) if world.clock_mode == 'countdown': rom.write_bytes(0x180190, [0x01, 0x01, 0x00]) # set countdown, with no reset available write_int32(rom, 0x180200, -2 * 60 * 60) # red clock adjustment time (in frames, sint32) write_int32(rom, 0x180204, 2 * 60 * 60) # blue clock adjustment time (in frames, sint32) write_int32(rom, 0x180208, 4 * 60 * 60) # green clock adjustment time (in frames, sint32) - write_int32(rom, 0x18020C, (40 + ERtimeincrease) * 60 * 60) # starting time (in frames, sint32) + rom.initial_sram.set_starting_timer((40 + ERtimeincrease) * 60) # starting time (in seconds) # set up goals for treasure hunt rom.write_bytes(0x180165, [0x0E, 0x28] if world.treasure_hunt_icon[player] == 'Triforce Piece' else [0x0D, 0x28]) if world.goal[player] in ['triforcehunt', 'trinity']: - rom.write_byte(0x180167, int(world.treasure_hunt_count[player]) % 256) + rom.write_bytes(0x180167, int16_as_bytes(world.treasure_hunt_count[player])) rom.write_byte(0x180194, 1) # Must turn in triforced pieces (instant win not enabled) rom.write_bytes(0x180213, [0x00, 0x01]) # Not a Tournament Seed @@ -1257,14 +1266,16 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_byte(0x180169, 0x02) # lock aga/ganon tower door with crystals in inverted rom.write_byte(0x180171, 0x01 if world.ganon_at_pyramid[player] else 0x00) # Enable respawning on pyramid after ganon death rom.write_byte(0x180173, 0x01) # Bob is enabled - rom.write_byte(0x180168, 0x08) # Spike Cave Damage + rom.write_byte(0x180195, 0x08) # Spike Cave Damage rom.write_bytes(0x18016B, [0x04, 0x02, 0x01]) # Set spike cave and MM spike room Byrna usage rom.write_bytes(0x18016E, [0x04, 0x08, 0x10]) # Set spike cave and MM spike room Cape usage 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 world.open_pyramid[player] or world.goal[player] == 'trinity' else 0x00) # pre-open Pyramid Hole - rom.write_byte(0x18008C, 0x01 if world.crystals_needed_for_gt[player] == 0 else 0x00) # GT pre-opened if crystal requirement is 0 + if world.open_pyramid[player] or world.goal[player] == 'trinity': + rom.initial_sram.pre_open_pyramid_hole() + if world.crystals_needed_for_gt == 0: + rom.initial_sram.pre_open_ganons_tower() rom.write_byte(0xF5D73, 0xF0) # bees are catchable rom.write_byte(0xF5F10, 0xF0) # bees are catchable rom.write_byte(0x180086, 0x00 if world.aga_randomness else 0x01) # set blue ball and ganon warp randomness @@ -1276,161 +1287,9 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): # Starting equipment if world.pseudoboots[player]: rom.write_byte(0x18008E, 0x01) - - equip = [0] * (0x340 + 0x4F) - equip[0x36C] = 0x18 - equip[0x36D] = 0x18 - equip[0x379] = 0x68 - if world.bombbag[player]: - starting_max_bombs = 0 - else: - starting_max_bombs = 10 - starting_max_arrows = 30 - - startingstate = CollectionState(world) - - if startingstate.has('Bow', player): - equip[0x340] = 3 if startingstate.has('Silver Arrows', player) else 1 - equip[0x38E] |= 0x20 # progressive flag to get the correct hint in all cases - if not world.retro[player]: - equip[0x38E] |= 0x80 - if startingstate.has('Silver Arrows', player): - equip[0x38E] |= 0x40 - - if startingstate.has('Titans Mitts', player): - equip[0x354] = 2 - elif startingstate.has('Power Glove', player): - equip[0x354] = 1 - - if startingstate.has('Golden Sword', player): - equip[0x359] = 4 - elif startingstate.has('Tempered Sword', player): - equip[0x359] = 3 - elif startingstate.has('Master Sword', player): - equip[0x359] = 2 - elif startingstate.has('Fighter Sword', player): - equip[0x359] = 1 - - if startingstate.has('Mirror Shield', player): - equip[0x35A] = 3 - elif startingstate.has('Red Shield', player): - equip[0x35A] = 2 - elif startingstate.has('Blue Shield', player): - equip[0x35A] = 1 - - if startingstate.has('Red Mail', player): - equip[0x35B] = 2 - elif startingstate.has('Blue Mail', player): - equip[0x35B] = 1 - - if startingstate.has('Magic Upgrade (1/4)', player): - equip[0x37B] = 2 - equip[0x36E] = 0x80 - elif startingstate.has('Magic Upgrade (1/2)', player): - equip[0x37B] = 1 - equip[0x36E] = 0x80 - - for item in world.precollected_items: - if item.player != player: - continue - - if item.name in ['Bow', 'Silver Arrows', 'Progressive Bow', 'Progressive Bow (Alt)', - 'Titans Mitts', 'Power Glove', 'Progressive Glove', - 'Golden Sword', 'Tempered Sword', 'Master Sword', 'Fighter Sword', 'Progressive Sword', - 'Mirror Shield', 'Red Shield', 'Blue Shield', 'Progressive Shield', - 'Red Mail', 'Blue Mail', 'Progressive Armor', - 'Magic Upgrade (1/4)', 'Magic Upgrade (1/2)']: - continue - - set_table = {'Book of Mudora': (0x34E, 1), 'Hammer': (0x34B, 1), 'Bug Catching Net': (0x34D, 1), 'Hookshot': (0x342, 1), 'Magic Mirror': (0x353, 2), - 'Cape': (0x352, 1), 'Lamp': (0x34A, 1), 'Moon Pearl': (0x357, 1), 'Cane of Somaria': (0x350, 1), 'Cane of Byrna': (0x351, 1), - 'Fire Rod': (0x345, 1), 'Ice Rod': (0x346, 1), 'Bombos': (0x347, 1), 'Ether': (0x348, 1), 'Quake': (0x349, 1)} - or_table = {'Green Pendant': (0x374, 0x04), 'Red Pendant': (0x374, 0x01), 'Blue Pendant': (0x374, 0x02), - 'Crystal 1': (0x37A, 0x02), 'Crystal 2': (0x37A, 0x10), 'Crystal 3': (0x37A, 0x40), 'Crystal 4': (0x37A, 0x20), - 'Crystal 5': (0x37A, 0x04), 'Crystal 6': (0x37A, 0x01), 'Crystal 7': (0x37A, 0x08), - 'Big Key (Eastern Palace)': (0x367, 0x20), 'Compass (Eastern Palace)': (0x365, 0x20), 'Map (Eastern Palace)': (0x369, 0x20), - 'Big Key (Desert Palace)': (0x367, 0x10), 'Compass (Desert Palace)': (0x365, 0x10), 'Map (Desert Palace)': (0x369, 0x10), - 'Big Key (Tower of Hera)': (0x366, 0x20), 'Compass (Tower of Hera)': (0x364, 0x20), 'Map (Tower of Hera)': (0x368, 0x20), - 'Big Key (Escape)': (0x367, 0xC0), 'Compass (Escape)': (0x365, 0xC0), 'Map (Escape)': (0x369, 0xC0), - 'Big Key (Agahnims Tower)': (0x367, 0x08), 'Compass (Agahnims Tower)': (0x365, 0x08), 'Map (Agahnims Tower)': (0x369, 0x08), - 'Big Key (Palace of Darkness)': (0x367, 0x02), 'Compass (Palace of Darkness)': (0x365, 0x02), 'Map (Palace of Darkness)': (0x369, 0x02), - 'Big Key (Thieves Town)': (0x366, 0x10), 'Compass (Thieves Town)': (0x364, 0x10), 'Map (Thieves Town)': (0x368, 0x10), - 'Big Key (Skull Woods)': (0x366, 0x80), 'Compass (Skull Woods)': (0x364, 0x80), 'Map (Skull Woods)': (0x368, 0x80), - 'Big Key (Swamp Palace)': (0x367, 0x04), 'Compass (Swamp Palace)': (0x365, 0x04), 'Map (Swamp Palace)': (0x369, 0x04), - 'Big Key (Ice Palace)': (0x366, 0x40), 'Compass (Ice Palace)': (0x364, 0x40), 'Map (Ice Palace)': (0x368, 0x40), - 'Big Key (Misery Mire)': (0x367, 0x01), 'Compass (Misery Mire)': (0x365, 0x01), 'Map (Misery Mire)': (0x369, 0x01), - 'Big Key (Turtle Rock)': (0x366, 0x08), 'Compass (Turtle Rock)': (0x364, 0x08), 'Map (Turtle Rock)': (0x368, 0x08), - 'Big Key (Ganons Tower)': (0x366, 0x04), 'Compass (Ganons Tower)': (0x364, 0x04), 'Map (Ganons Tower)': (0x368, 0x04)} - set_or_table = {'Flippers': (0x356, 1, 0x379, 0x02),'Pegasus Boots': (0x355, 1, 0x379, 0x04), - 'Shovel': (0x34C, 1, 0x38C, 0x04), 'Ocarina': (0x34C, 3, 0x38C, 0x01), - 'Mushroom': (0x344, 1, 0x38C, 0x20 | 0x08), 'Magic Powder': (0x344, 2, 0x38C, 0x10), - 'Blue Boomerang': (0x341, 1, 0x38C, 0x80), 'Red Boomerang': (0x341, 2, 0x38C, 0x40)} - keys = {'Small Key (Eastern Palace)': [0x37E], 'Small Key (Desert Palace)': [0x37F], - 'Small Key (Tower of Hera)': [0x386], - 'Small Key (Agahnims Tower)': [0x380], 'Small Key (Palace of Darkness)': [0x382], - 'Small Key (Thieves Town)': [0x387], - 'Small Key (Skull Woods)': [0x384], 'Small Key (Swamp Palace)': [0x381], - 'Small Key (Ice Palace)': [0x385], - 'Small Key (Misery Mire)': [0x383], 'Small Key (Turtle Rock)': [0x388], - 'Small Key (Ganons Tower)': [0x389], - 'Small Key (Universal)': [0x38B], 'Small Key (Escape)': [0x37C, 0x37D]} - bottles = {'Bottle': 2, 'Bottle (Red Potion)': 3, 'Bottle (Green Potion)': 4, 'Bottle (Blue Potion)': 5, - 'Bottle (Fairy)': 6, 'Bottle (Bee)': 7, 'Bottle (Good Bee)': 8} - rupees = {'Rupee (1)': 1, 'Rupees (5)': 5, 'Rupees (20)': 20, 'Rupees (50)': 50, 'Rupees (100)': 100, 'Rupees (300)': 300} - bomb_caps = {'Bomb Upgrade (+5)': 5, 'Bomb Upgrade (+10)': 10} - arrow_caps = {'Arrow Upgrade (+5)': 5, 'Arrow Upgrade (+10)': 10} - bombs = {'Single Bomb': 1, 'Bombs (3)': 3, 'Bombs (10)': 10} - arrows = {'Single Arrow': 1, 'Arrows (10)': 10} - - if item.name in set_table: - equip[set_table[item.name][0]] = set_table[item.name][1] - elif item.name in or_table: - equip[or_table[item.name][0]] |= or_table[item.name][1] - elif item.name in set_or_table: - equip[set_or_table[item.name][0]] = set_or_table[item.name][1] - equip[set_or_table[item.name][2]] |= set_or_table[item.name][3] - elif item.name in keys: - for address in keys[item.name]: - equip[address] = min(equip[address] + 1, 99) - elif item.name in bottles: - if equip[0x34F] < world.difficulty_requirements[player].progressive_bottle_limit: - equip[0x35C + equip[0x34F]] = bottles[item.name] - equip[0x34F] += 1 - elif item.name in rupees: - equip[0x360:0x362] = list(min(equip[0x360] + (equip[0x361] << 8) + rupees[item.name], 9999).to_bytes(2, byteorder='little', signed=False)) - equip[0x362:0x364] = list(min(equip[0x362] + (equip[0x363] << 8) + rupees[item.name], 9999).to_bytes(2, byteorder='little', signed=False)) - elif item.name in bomb_caps: - starting_max_bombs = min(starting_max_bombs + bomb_caps[item.name], 50) - elif item.name in arrow_caps: - starting_max_arrows = min(starting_max_arrows + arrow_caps[item.name], 70) - elif item.name in bombs: - equip[0x343] += bombs[item.name] - elif item.name in arrows: - if world.retro[player]: - equip[0x38E] |= 0x80 - equip[0x377] = 1 - else: - equip[0x377] += arrows[item.name] - elif item.name in ['Piece of Heart', 'Boss Heart Container', 'Sanctuary Heart Container']: - if item.name == 'Piece of Heart': - equip[0x36B] = (equip[0x36B] + 1) % 4 - if item.name != 'Piece of Heart' or equip[0x36B] == 0: - equip[0x36C] = min(equip[0x36C] + 0x08, 0xA0) - equip[0x36D] = min(equip[0x36D] + 0x08, 0xA0) - else: - raise RuntimeError(f'Unsupported item in starting equipment: {item.name}') - - equip[0x343] = min(equip[0x343], starting_max_bombs) - rom.write_byte(0x180034, starting_max_bombs) - equip[0x377] = min(equip[0x377], starting_max_arrows) - rom.write_byte(0x180035, starting_max_arrows) - rom.write_bytes(0x180046, equip[0x360:0x362]) - if equip[0x359]: - rom.write_byte(0x180043, equip[0x359]) - - assert equip[:0x340] == [0] * 0x340 - rom.write_bytes(0x183000, equip[0x340:]) - rom.write_bytes(0x271A6, equip[0x340:0x340+60]) + rom.initial_sram.set_starting_equipment(world, player) + rom.write_byte(0x180034, 10 if not world.bombbag[player] else 0) # starting max bombs + rom.write_byte(0x180035, 30) # starting max arrows rom.write_byte(0x18004A, 0x00 if world.mode[player] != 'inverted' else 0x01) # Inverted mode rom.write_byte(0x18005D, 0x00) # Hammer always breaks barrier @@ -1503,8 +1362,13 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): for idx, x_map in enumerate(x_map_position_generic): rom.write_bytes(0x53df6+idx*2, int16_as_bytes(x_map)) rom.write_bytes(0x53e16+idx*2, int16_as_bytes(0xFC0)) - if world.compassshuffle[player] and world.overworld_map[player] == 'compass': - compass_mode |= 0x40 # compasses are wild + if world.overworld_map[player] == 'compass': + compass_mode |= 0x20 # check for compass + if world.compassshuffle[player]: + compass_mode |= 0x40 # compasses are wild + elif world.overworld_map[player] == 'map': + if world.mapshuffle[player]: + compass_mode |= 0x40 # maps are wild for dungeon, portal_list in dungeon_portals.items(): ow_map_index = dungeon_table[dungeon].map_index if len(portal_list) == 1: @@ -1680,6 +1544,9 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): write_strings(rom, world, player, team) + # write initial sram + rom.write_initial_sram() + rom.write_byte(0x18636C, 1 if world.remote_items[player] else 0) # set rom name @@ -2451,8 +2318,8 @@ def write_strings(rom, world, player, team): # inverted spawn menu changes if world.mode[player] == '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}" + 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}" tt['intro_main'] = CompressedTextMapper.convert( "{INTRO}\n Episode III\n{PAUSE3}\n A Link to\n the Past\n" + "{PAUSE3}\nInverted\n Randomizer\n{PAUSE3}\nAfter mostly disregarding what happened in the first two games.\n" @@ -2517,6 +2384,16 @@ def text_for_item(item, world, player, team): else: return f'{item.hint_text} for {world.player_names[item.player][team]}' +def init_open_mode_sram(rom): + rom.initial_sram.pre_open_castle_gate() + rom.initial_sram.set_progress_indicator(0x02) + rom.initial_sram.set_progress_flags(0x14) + rom.initial_sram.set_starting_entrance(0x01) + +def init_standard_mode_sram(rom): + rom.initial_sram.set_progress_indicator(0x00) + rom.initial_sram.set_progress_flags(0x0) + rom.initial_sram.set_starting_entrance(0x00) def set_inverted_mode(world, player, rom): rom.write_byte(snes_to_pc(0x0283E0), 0xF0) # residual portals diff --git a/Text.py b/Text.py index c84a202c..07b076f9 100644 --- a/Text.py +++ b/Text.py @@ -45,7 +45,7 @@ Uncle_texts = [ "I am leaving\nforever.\nGoodbye.", "Don't worry.\nI got this\ncovered.", "Race you to\nthe castle!", - "\n hi", + "\n Hi", "I'M JUST GOING\nOUT FOR A\nPACK OF SMOKES", "It's dangerous\nto go alone.\nSee ya!", "ARE YOU A BAD\nENOUGH DUDE TO\nRESCUE ZELDA?", @@ -89,49 +89,49 @@ Triforce_texts = [ 'Product has Hole in center. Bad seller, 0 out of 5.', 'Who stole the fourth triangle?', 'Trifource?\nMore Like Tritrice, am I right?' - '\n Well Done!', + '\n Well Done!', 'You just wasted 2 hours of your life.', 'This was meant to be a trapezoid', # these ones are from web randomizer - "\n G G", - "All your base\nare belong\nto us.", - "You have ended\nthe domination\nof Dr. Wily", - " thanks for\n playing!!!", - "\n You Win!", - " Thank you!\n your quest\n is over.", - " A winner\n is\n you!", - "\n WINNER!!", - "\n I'm sorry\n\n but your\nprincess is in\nanother castle", - "\n success!", + "\n G G", + " All your base\n are belong\n to us.", + " You have ended\n the domination\n of Dr. Wily", + " Thanks for\n playing!!!", + "\n You Win!", + " Thank you!\n Your quest\n is over.", + " A winner\n is you!", + "\n WINNER!!", + "\n I'm sorry\n\nbut our princess is\n in another castle", + "\n Success!", " Whelp…\n that just\n happened", - " Oh hey…\n it's you", - "\n Wheeeeee!!", - " Time for\n another one?", - "and\n\n scene", - "\n GOT EM!!", - "\nTHE VALUUUE!!!", - "Cool seed,\n\nright?", - "\n We did it!", - " Spam those\n emotes in\n wilds chat", - "\n O M G", - " Hello. Will\n you be my\n friend?", - " Beetorp\n was\n here!", - "The Wind Fish\nwill wake\nsoon. Hoot!", - "meow meow meow\nmeow meow meow\n oh my god!", - "Ahhhhhhhhh\nYa ya yaaaah\nYa ya yaaah", - ".done\n\n.comment lol", - "You get to\ndrink from\nthe firehose", - "Do you prefer\n bacon, pork,\n or ham?", - "You get one\nwish. Choose\nwisely, hero!", - "Can you please\nbreak us three\nup? Thanks.", - " Pick us up\n before we\n get dizzy!", - "Thank you,\nMikey. You’re\n2 minutes late", - "This was a\n7000 series\ntrain.", - " I'd buy\n that for\n a rupee!", - " Did you like\n that bow\n placement?", - "I promise the\nnext seed will\nbe better.", - "\n Honk.", - "Breakfast\nis served!", + " Oh hey…\n it's you", + "\n Wheeeeee!!", + " Time for\n another one?", + " And\n\n scene", + "\n GOT EM!!", + "\n THE VALUUUE!!!", + " Cool seed,\n\n right?", + "\n We did it!", + " Spam those\n emotes in\n wilds chat", + "\n O M G", + " Hello. Will you\n you be my friend?", + " Beetorp\n was\n here!", + " The Wind Fish\n will wake soon.\n Hoot!", + " Meow Meow Meow\n Meow Meow Meow\n Oh my god!", + " Ahhhhhhhhh\n Ya ya yaaaah\n Ya ya yaaah", + " .done\n\n .comment lol", + " You get to\n drink from\n the firehose", + " Do you prefer\n bacon, pork,\n or ham?", + " You get one\n wish. Choose\n wisely, hero!", + " Can you please\n break us three\n up? Thanks.", + " Pick us up\n before we\n get dizzy!", + " Thank you,\n Mikey. You’re\n 2 minutes late", + " This was a\n 7000 series\n train.", + " I'd buy\n that for\n a rupee!", + " Did you like\n that bow\n placement?", + " I promise the\n next seed will\n be better.", + "\n Honk.", + " Breakfast\n is served!", ] BombShop2_texts = ['Bombs!\nBombs!\nBiggest!\nBestest!\nGreatest!\nBoomest!'] Sahasrahla2_texts = ['You already got my item, idiot.', 'Why are you still talking to me?', 'This text won\'t change.', 'Have you met my brother, Hasarahshla?'] @@ -217,7 +217,7 @@ Ganon1_texts = [ "The Hemiptera\nor true bugs\nare an order\nof insects\ncovering 50k\nto 80k species\nlike aphids,\ncicadas, and\nshield bugs.", "Thanks for\ndropping in.\nThe first\npassengers\nin a hot\nair balloon\nwere a duck,\na sheep,\nand a rooster.", "You think you\nare so smart?\n\nI bet you\ndidn't know\nyou can't hum\nwhile holding\nyour nose\nclosed.", - "grumble,\n\ngrumble…\ngrumble,\n\ngrumble…\nSeriously, you\nwere supposed\nto bring food.", + "Grumble,\n\ngrumble…\nGrumble,\n\ngrumble…\nSeriously, you\nwere supposed\nto bring food.", "Join me hero,\nand I shall\nmake your face\nthe greatest\nin the Dark\nWorld!\n\nOr else you\nwill die!", "Why rule over\na desert full\nof stereotypes\nwhen I can\ncorrupt a\nworld into\npure evil and\nrule over\nthat instead?", "When I conquer\nthe Light\nWorld, I'll\nhold a parade\nof all my\nmonsters to\ndemonstrate my\nmight to the\npeople!", @@ -627,8 +627,7 @@ class MultiByteCoreTextMapper(object): } @classmethod - def convert(cls, text, pause=True, wrap=14): - text = text.upper() + def convert(cls, text, pause=True, wrap=19): lines = text.split('\n') outbuf = bytearray() lineindex = 0 @@ -655,7 +654,8 @@ class MultiByteCoreTextMapper(object): pending_space = False while words: word = words.pop(0) - # sanity check: if the word we have is more than 14 characters, we take as much as we can still fit and push the rest back for later + # sanity check: if the word we have is more than 19 characters, + # we take as much as we can still fit and push the rest back for later if cls.wordlen(word) > wrap: (word_first, word_rest) = cls.splitword(word, linespace) words.insert(0, word_rest) @@ -736,7 +736,7 @@ class CompressedTextMapper(object): } @classmethod - def convert(cls, text, pause=True, max_bytes_expanded=0x800, wrap=14): + def convert(cls, text, pause=True, max_bytes_expanded=0x800, wrap=19): inbuf = MultiByteCoreTextMapper.convert(text, pause, wrap) # Links name will need 8 bytes in the target buffer @@ -772,20 +772,23 @@ class CompressedTextMapper(object): class CharTextMapper(object): number_offset = None alpha_offset = 0 + alpha_lower_offset = 0 char_map = {} @classmethod def map_char(cls, char): if cls.number_offset is not None: - if 0x30 <= ord(char) <= 0x39: + if 0x30 <= ord(char) <= 0x39: return ord(char) + cls.number_offset + if 0x41 <= ord(char) <= 0x5A: + return ord(char) + 0x20 + cls.alpha_offset if 0x61 <= ord(char) <= 0x7A: - return ord(char) + cls.alpha_offset + return ord(char) + cls.alpha_lower_offset return cls.char_map.get(char, cls.char_map[' ']) @classmethod def convert(cls, text): buf = bytearray() - for char in text.lower(): + for char in text: buf.append(cls.map_char(char)) return buf @@ -1240,6 +1243,7 @@ class RawMBTextMapper(CharTextMapper): "月": 0xFE, "姫": 0xFF} alpha_offset = 0x49 + alpha_lower_offset = -0x31 number_offset = 0x70 @classmethod @@ -1251,7 +1255,7 @@ class RawMBTextMapper(CharTextMapper): @classmethod def convert(cls, text): buf = bytearray() - for char in text.lower(): + for char in text: res = cls.map_char(char) if isinstance(res, int): buf.extend([0x00, res]) @@ -1267,16 +1271,19 @@ class GoldCreditMapper(CharTextMapper): '-': 0x36, '.': 0x37,} alpha_offset = -0x47 + alpha_lower_offset = -0x47 class GreenCreditMapper(CharTextMapper): char_map = {' ': 0x9F, '·': 0x52} alpha_offset = -0x29 + alpha_lower_offset = -0x29 class RedCreditMapper(CharTextMapper): char_map = {' ': 0x9F} alpha_offset = -0x61 + alpha_lower_offset = -0x61 class LargeCreditTopMapper(CharTextMapper): char_map = {' ': 0x9F, @@ -1296,6 +1303,7 @@ class LargeCreditTopMapper(CharTextMapper): '◢': 0xAA, '◣': 0xAB,} alpha_offset = -0x04 + alpha_lower_offset = -0x04 number_offset = 0x23 @@ -1317,6 +1325,7 @@ class LargeCreditBottomMapper(CharTextMapper): '◢': 0xCA, '◣': 0xCB,} alpha_offset = 0x22 + alpha_lower_offset = 0x22 number_offset = 0x49 class TextTable(object): @@ -1549,7 +1558,7 @@ class TextTable(object): text['tutorial_guard_7'] = CompressedTextMapper.convert("Jeeze! There really are a lot of things.") text['priest_sanctuary_before_leave'] = CompressedTextMapper.convert("Go be a hero!") text['sanctuary_enter'] = CompressedTextMapper.convert("YAY!\nYou saved Zelda!") - text['zelda_sanctuary_story'] = CompressedTextMapper.convert("Do you want to hear me say this again?\n{HARP}\n ≥ no\n yes\n{CHOICE}") + text['zelda_sanctuary_story'] = CompressedTextMapper.convert("Do you want to hear me say this again?\n{HARP}\n ≥ No\n Yes\n{CHOICE}") text['priest_sanctuary_before_pendants'] = CompressedTextMapper.convert("Go'on and get them pendants so you can beat up Agahnim.") text['priest_sanctuary_after_pendants_before_master_sword'] = CompressedTextMapper.convert("Kudos! But seriously, you should be getting the master sword, not having a kegger in here.") text['priest_sanctuary_dying'] = CompressedTextMapper.convert("They took her to the castle! Take your sword and save her!") @@ -1563,14 +1572,14 @@ class TextTable(object): text['zelda_push_throne'] = CompressedTextMapper.convert("Let's push it from the left!") text['zelda_switch_room_pull'] = CompressedTextMapper.convert("Pull this lever using A.") text['zelda_save_lets_go'] = CompressedTextMapper.convert("Let's get out of here!") - text['zelda_save_repeat'] = CompressedTextMapper.convert("I like talking, do you?\n ≥ no\n yes\n{CHOICE}") + text['zelda_save_repeat'] = CompressedTextMapper.convert("I like talking, do you?\n ≥ No\n Yes\n{CHOICE}") text['zelda_before_pendants'] = CompressedTextMapper.convert("You need to find all the pendants…\n\n\nNumpty.") text['zelda_after_pendants_before_master_sword'] = CompressedTextMapper.convert("Very pretty pendants, but really you should be getting that sword in the forest!") text['telepathic_zelda_right_after_master_sword'] = CompressedTextMapper.convert("{NOBORDER}\n{SPEED6}\nHi @,\nHave you been thinking about me?\narrrrrgghh…\n… … …") text['zelda_sewers'] = CompressedTextMapper.convert("Just a little further to the Sanctuary.") text['zelda_switch_room'] = CompressedTextMapper.convert("The Sanctuary!\n\nPull my finger") text['kakariko_saharalasa_wife'] = CompressedTextMapper.convert("Heya, @!\nLong time no see.\nYou want a master sword?\n\nWell good luck with that.") - text['kakariko_saharalasa_wife_sword_story'] = CompressedTextMapper.convert("It occurs to me that I like toast and jam, but cheese and crackers is better.\nYou like?\n ≥ cheese\n jam\n{CHOICE}") + text['kakariko_saharalasa_wife_sword_story'] = CompressedTextMapper.convert("It occurs to me that I like toast and jam, but cheese and crackers is better.\nYou like?\n ≥ Cheese\n Jam\n{CHOICE}") text['kakariko_saharalasa_wife_closing'] = CompressedTextMapper.convert("Anywho, I have things to do. You see those 2 ovens?\n\nYeah 2!\nWho has 2 ovens nowadays?") text['kakariko_saharalasa_after_master_sword'] = CompressedTextMapper.convert("Cool sword!\n\n\n…\n\n\n…\n\n\nPlease save us") text['kakariko_alert_guards'] = CompressedTextMapper.convert("GUARDS! HELP!\nThe creeper\n@ is here!") @@ -1580,7 +1589,7 @@ class TextTable(object): text['sahasrahla_quest_information'] = CompressedTextMapper.convert( "{BOTTOM}\n" + "Sahasrahla, I am. You would do well to find the 3 pendants from the 3 dungeons in the Light World.\n" - + "Understand?\n ≥ yes\n no\n{CHOICE}") + + "Understand?\n ≥ Yes\n No\n{CHOICE}") text['sahasrahla_bring_courage'] = CompressedTextMapper.convert( "{BOTTOM}\n" + "While you're here, could you do me a solid and get the green pendant from that dungeon?\n" @@ -1671,15 +1680,15 @@ class TextTable(object): text['talking_tree_other'] = CompressedTextMapper.convert("I can breathe!") text['item_get_pendant_power_alt'] = CompressedTextMapper.convert("We have the Pendant of Power! How robust!") text['item_get_pendant_wisdom_alt'] = CompressedTextMapper.convert("We have the Pendant of Wisdom! How astute!") - text['game_shooting_choice'] = CompressedTextMapper.convert("20 rupees.\n5 arrows.\nWin rupees!\nWant to play?\n ≥ yes\n no\n{CHOICE}") + text['game_shooting_choice'] = CompressedTextMapper.convert("20 rupees.\n5 arrows.\nWin rupees!\nWant to play?\n ≥ Yes\n No\n{CHOICE}") text['game_shooting_yes'] = CompressedTextMapper.convert("Let's do this!") text['game_shooting_no'] = CompressedTextMapper.convert("Where are you going? Straight up!") text['game_shooting_continue'] = CompressedTextMapper.convert("Keep playing?\n ≥ yes\n no\n{CHOICE}") text['pond_of_wishing'] = CompressedTextMapper.convert("-Wishing Pond-\n\n On Vacation") text['pond_item_select'] = CompressedTextMapper.convert("Pick something\nto throw in.\n{ITEMSELECT}") - text['pond_item_test'] = CompressedTextMapper.convert("You toss this?\n ≥ yup\n wrong\n{CHOICE}") + text['pond_item_test'] = CompressedTextMapper.convert("You toss this?\n ≥ Yup\n Wrong\n{CHOICE}") text['pond_will_upgrade'] = CompressedTextMapper.convert("You're honest, so I'll give you a present.") - text['pond_item_test_no'] = CompressedTextMapper.convert("You sure?\n ≥ oh yeah\n um\n{CHOICE}") + text['pond_item_test_no'] = CompressedTextMapper.convert("You sure?\n ≥ Oh yeah\n Um\n{CHOICE}") text['pond_item_test_no_no'] = CompressedTextMapper.convert("Well, I don't want it, so take it back.") text['pond_item_boomerang'] = CompressedTextMapper.convert("I don't much like you, so have this worse Boomerang.") # 90 @@ -1688,7 +1697,7 @@ class TextTable(object): text['pond_item_bottle_filled'] = CompressedTextMapper.convert("Bottle Filled!\nMoney Saved!") text['pond_item_sword'] = CompressedTextMapper.convert("Thank you for the sword, here is a stick of butter.") text['pond_of_wishing_happiness'] = CompressedTextMapper.convert("Happiness up!\nYou are now\nᚌᚋ happy!") - text['pond_of_wishing_choice'] = CompressedTextMapper.convert("Your wish?\n ≥more bombs\n more arrows\n{CHOICE}") + text['pond_of_wishing_choice'] = CompressedTextMapper.convert("Your wish?\n ≥More bombs\n More arrows\n{CHOICE}") text['pond_of_wishing_bombs'] = CompressedTextMapper.convert("Woo-hoo!\nYou can now\ncarry ᚌᚋ bombs") text['pond_of_wishing_arrows'] = CompressedTextMapper.convert("Woo-hoo!\nYou can now\nhold ᚌᚋ arrows") text['pond_of_wishing_full_upgrades'] = CompressedTextMapper.convert("Youhave all I can give you, here are your rupees back.") @@ -1709,7 +1718,7 @@ class TextTable(object): text['running_man'] = CompressedTextMapper.convert("Hi, Do you\nknow Veetorp?\n\nYou really\nshould. And\nall the other great guys who made this possible.\nGo thank them.\n\n\nIf you can catch them…") text['game_race_sign'] = CompressedTextMapper.convert("Why are you reading this sign? Run!!!") text['sign_bumper_cave'] = CompressedTextMapper.convert("You need Cape, but not Hookshot") - text['sign_catfish'] = CompressedTextMapper.convert("toss rocks\ntoss items\ntoss cookies") + text['sign_catfish'] = CompressedTextMapper.convert("Toss rocks\nToss items\nToss cookies") text['sign_north_village_of_outcasts'] = CompressedTextMapper.convert("↑ Skull Woods\n\n↓ Steve's Town") text['sign_south_of_bumper_cave'] = CompressedTextMapper.convert("\n→ Karkats cave") text['sign_east_of_pyramid'] = CompressedTextMapper.convert("\n→ Dark Palace") @@ -1717,7 +1726,7 @@ class TextTable(object): text['sign_east_of_mire'] = CompressedTextMapper.convert("\n← Misery Mire\n no way in.\n no way out.") text['sign_village_of_outcasts'] = CompressedTextMapper.convert("Have a Trulie Awesome Day!") # B0 - text['sign_before_wishing_pond'] = CompressedTextMapper.convert("waterfall\nup ahead\nmake wishes") + text['sign_before_wishing_pond'] = CompressedTextMapper.convert("Waterfall\nup ahead\nMake wishes") text['sign_before_catfish_area'] = CompressedTextMapper.convert("→↑ Have you met Woeful Ike?") text['castle_wall_guard'] = CompressedTextMapper.convert("Looking for a Princess? Look downstairs.") text['gate_guard'] = CompressedTextMapper.convert("No Lonks Allowed!") @@ -1731,7 +1740,7 @@ class TextTable(object): text['telepathic_tile_misery_mire'] = CompressedTextMapper.convert("{NOBORDER}\nLighting 4 torches will open your way forward!") text['hylian_text_2'] = CompressedTextMapper.convert("%%^= %==%\n ^ =%^=\n==%= ^^%^") text['desert_entry_translated'] = CompressedTextMapper.convert("Kneel before this stone, and magic will move around you.") - text['telepathic_tile_under_ganon'] = CompressedTextMapper.convert("Secondary tournament winners\n{HARP}\n ~~~2017~~~\nA: Zaen") + text['telepathic_tile_under_ganon'] = CompressedTextMapper.convert("Doors Async League winners\n{HARP}\n ~~~2022~~~\nAndy\n\n ~~~2021~~~\nprdwong") text['telepathic_tile_palace_of_darkness'] = CompressedTextMapper.convert("{NOBORDER}\nThis is a funny looking Enemizer") # C0 text['telepathic_tile_desert_bonk_torch_room'] = CompressedTextMapper.convert("{NOBORDER}\nThings can be knocked down, if you fancy yourself a dashing dude.") @@ -1741,9 +1750,9 @@ class TextTable(object): text['telepathic_tile_ice_entrance'] = CompressedTextMapper.convert("{NOBORDER}\nYou can use Fire Rod or Bombos to pass.") text['telepathic_tile_ice_stalfos_knights_room'] = CompressedTextMapper.convert("{NOBORDER}\nKnock 'em down and then bomb them dead.") text['telepathic_tile_tower_of_hera_entrance'] = CompressedTextMapper.convert("{NOBORDER}\nThis is a bad place, with a guy who will make you fall…\n\n\na lot.") - text['houlihan_room'] = CompressedTextMapper.convert("Randomizer tournament winners\n{HARP}\n ~~~2018~~~\nS: Andy\n\n ~~~2017~~~\nA: ajneb174\nS: ajneb174") - text['caught_a_bee'] = CompressedTextMapper.convert("Caught a Bee\n ≥ keep\n release\n{CHOICE}") - text['caught_a_fairy'] = CompressedTextMapper.convert("Caught Fairy!\n ≥ keep\n release\n{CHOICE}") + text['houlihan_room'] = CompressedTextMapper.convert("Randomizer tournament winners\n{HARP}\n ~~~2021~~~\nDaaanty\n\n ~~~2019~~~\nJet082\n\n ~~~2018~~~\nAndy\n\n ~~~2017~~~\nA: ajneb174\nS: ajneb174") + text['caught_a_bee'] = CompressedTextMapper.convert("Caught a Bee\n ≥ Keep\n Release\n{CHOICE}") + text['caught_a_fairy'] = CompressedTextMapper.convert("Caught Fairy!\n ≥ Keep\n Release\n{CHOICE}") text['no_empty_bottles'] = CompressedTextMapper.convert("Whoa, bucko!\nNo empty bottles.") text['game_race_boy_time'] = CompressedTextMapper.convert("Your time was\nᚎᚍ min ᚌᚋ sec.") text['game_race_girl'] = CompressedTextMapper.convert("You have 15 seconds,\nGo… Go… Go…") @@ -1773,7 +1782,7 @@ class TextTable(object): text['blacksmiths_still_working'] = CompressedTextMapper.convert("Something this precious takes time… Come back later.") text['blacksmiths_saving_bows'] = CompressedTextMapper.convert("Thanks!\n\nThanks!") text['blacksmiths_hammer_anvil'] = CompressedTextMapper.convert("Dernt Take Er Jerbs!") - text['dark_flute_boy_storytime'] = CompressedTextMapper.convert("Hi!\nI'm Stumpy\nI've been chillin' in this world for a while now, but I miss my flute. If I gave you a shovel, would you go digging for it?\n ≥ sure\n nahh\n{CHOICE}") + text['dark_flute_boy_storytime'] = CompressedTextMapper.convert("Hi!\nI'm Stumpy\nI've been chillin' in this world for a while now, but I miss my flute. If I gave you a shovel, would you go digging for it?\n ≥ Sure\n Nahh\n{CHOICE}") text['dark_flute_boy_get_shovel'] = CompressedTextMapper.convert("Schaweet! Here you go. Happy digging!") text['dark_flute_boy_no_get_shovel'] = CompressedTextMapper.convert("Oh I see, not good enough for you… FINE!") text['dark_flute_boy_flute_not_found'] = CompressedTextMapper.convert("Still haven't found the item? Dig in the Light World around here, dingus!") @@ -1787,7 +1796,7 @@ class TextTable(object): text['shop_fortune_teller_lw_hint_6'] = CompressedTextMapper.convert("{BOTTOM}\nBy the black cats, Spin, Hammer, or Net to hurt Agahnim") text['shop_fortune_teller_lw_hint_7'] = CompressedTextMapper.convert("{BOTTOM}\nBy the black cats, You can jump in the well by the blacksmiths") text['shop_fortune_teller_lw_no_rupees'] = CompressedTextMapper.convert("{BOTTOM}\nThe black cats are hungry, come back with rupees") - text['shop_fortune_teller_lw'] = CompressedTextMapper.convert("{BOTTOM}\nWelcome to the Fortune Shoppe!\nFancy a read?\n ≥I must know\n negative\n{CHOICE}") + text['shop_fortune_teller_lw'] = CompressedTextMapper.convert("{BOTTOM}\nWelcome to the Fortune Shoppe!\nFancy a read?\n ≥I must know\n Negative\n{CHOICE}") text['shop_fortune_teller_lw_post_hint'] = CompressedTextMapper.convert("{BOTTOM}\nFor ᚋᚌ rupees\nIt is done.\nBe gone!") text['shop_fortune_teller_lw_no'] = CompressedTextMapper.convert("{BOTTOM}\nWell then, why did you even come in here?") text['shop_fortune_teller_lw_hint_8'] = CompressedTextMapper.convert("{BOTTOM}\nBy the black cats, why you do?") @@ -1798,7 +1807,7 @@ class TextTable(object): text['shop_fortune_teller_lw_hint_13'] = CompressedTextMapper.convert("{BOTTOM}\nBy the black cats, big bombs blow up cracked walls in pyramids") text['shop_fortune_teller_lw_hint_14'] = CompressedTextMapper.convert("{BOTTOM}\nBy the black cats, you need all the crystals to open Ganon's Tower") text['shop_fortune_teller_lw_hint_15'] = CompressedTextMapper.convert("{BOTTOM}\nBy the black cats, Silver Arrows will defeat Ganon in his final phase") - text['dark_sanctuary'] = CompressedTextMapper.convert("For 20 rupees I'll tell you something?\nHow about it?\n ≥ yes\n no\n{CHOICE}") + text['dark_sanctuary'] = CompressedTextMapper.convert("For 20 rupees I'll tell you something?\nHow about it?\n ≥ Yes\n No\n{CHOICE}") text['dark_sanctuary_hint_0'] = CompressedTextMapper.convert("I once was a tea kettle, but then I moved up in the world, and now you can see me as this. Makes you wonder. What I could be next time.") # 100 text['dark_sanctuary_no'] = CompressedTextMapper.convert("Then go away!") @@ -1814,8 +1823,8 @@ class TextTable(object): text['sick_kid_trade'] = CompressedTextMapper.convert("{BOTTOM}\nCool Bottle! Here's something for you.") text['sick_kid_post_trade'] = CompressedTextMapper.convert("{BOTTOM}\nLeave me alone\nI'm sick. You have my item.") text['desert_thief_sitting'] = CompressedTextMapper.convert("………………………") - text['desert_thief_following'] = CompressedTextMapper.convert("why……………") - text['desert_thief_question'] = CompressedTextMapper.convert("I was a thief, I open purple chests!\nKeep secret?\n ≥ sure thing\n never!\n{CHOICE}") + text['desert_thief_following'] = CompressedTextMapper.convert("Why……………") + text['desert_thief_question'] = CompressedTextMapper.convert("I was a thief, I open purple chests!\nKeep secret?\n ≥ Sure thing\n Never!\n{CHOICE}") text['desert_thief_question_yes'] = CompressedTextMapper.convert("Cool, bring me any purple chests you find.") text['desert_thief_after_item_get'] = CompressedTextMapper.convert("You tell anyone and I will give you such a pinch!") text['desert_thief_reassure'] = CompressedTextMapper.convert("Bring chests. It's a secret to everyone.") @@ -1841,10 +1850,10 @@ class TextTable(object): text['bomb_shop_big_bomb'] = CompressedTextMapper.convert("30 bombs for 100 rupees, 100 rupees 1 BIG bomb. Good deals all day!") text['bomb_shop_big_bomb_buy'] = CompressedTextMapper.convert("Thanks!\nBoom goes the dynamite!") text['item_get_big_bomb'] = CompressedTextMapper.convert("YAY! press A to splode it!") - text['kiki_second_extortion'] = CompressedTextMapper.convert("For 100 more, I'll open this place.\nHow about it?\n ≥ open\n nah\n{CHOICE}") + text['kiki_second_extortion'] = CompressedTextMapper.convert("For 100 more, I'll open this place.\nHow about it?\n ≥ Open\n Nah\n{CHOICE}") text['kiki_second_extortion_no'] = CompressedTextMapper.convert("Heh, good luck getting in.") text['kiki_second_extortion_yes'] = CompressedTextMapper.convert("Yay! Rupees!\nOkay, let's do this!") - text['kiki_first_extortion'] = CompressedTextMapper.convert("I'm Kiki, I like rupees, may I have 10?\nHow about it?\n ≥ yes\n no\n{CHOICE}") + text['kiki_first_extortion'] = CompressedTextMapper.convert("I'm Kiki, I like rupees, may I have 10?\nHow about it?\n ≥ Yes\n No\n{CHOICE}") text['kiki_first_extortion_yes'] = CompressedTextMapper.convert("Nice. I'll tag along with you for a bit.") # 120 text['kiki_first_extortion_no'] = CompressedTextMapper.convert("Pfft. I have no reason to hang. See ya!") @@ -1903,7 +1912,7 @@ class TextTable(object): text['pond_of_wishing_good_luck'] = CompressedTextMapper.convert("\n is good luck") text['pond_of_wishing_meh_luck'] = CompressedTextMapper.convert("\n is meh luck") # Repurposed to no items in Randomizer - text['pond_of_wishing_bad_luck'] = CompressedTextMapper.convert("Why you come in here and pretend like you have something this fountain wants? Come back with bottles!") + text['pond_of_wishing_bad_luck'] = CompressedTextMapper.convert("Why come in here and pretend like you have something this fountain wants? Come back with bottles!") text['pond_of_wishing_fortune'] = CompressedTextMapper.convert("by the way, your fortune,") text['item_get_14_heart'] = CompressedTextMapper.convert("3 more to go\n ¼\nYay!") text['item_get_24_heart'] = CompressedTextMapper.convert("2 more to go\n ½\nWhee!") @@ -1917,7 +1926,7 @@ class TextTable(object): text['death_mountain_bully_with_pearl'] = CompressedTextMapper.convert("I think I forgot how to smile…") text['shop_darkworld_enter'] = CompressedTextMapper.convert("It's dangerous outside, buy my crap for safety.") # 160 - text['game_chest_village_of_outcasts'] = CompressedTextMapper.convert("Pay 30 rupees, open 2 chests. Are you lucky?\nSo, Play game?\n ≥ play\n never!\n{CHOICE}") + text['game_chest_village_of_outcasts'] = CompressedTextMapper.convert("Pay 30 rupees, open 2 chests. Are you lucky?\nSo, Play game?\n ≥ Play\n Never!\n{CHOICE}") text['game_chest_no_cash'] = CompressedTextMapper.convert("So, like, you need 30 rupees.\nSilly!") text['game_chest_not_played'] = CompressedTextMapper.convert("You want to play a game?\nTalk to me.") text['game_chest_played'] = CompressedTextMapper.convert("You've opened the chests!\nTime to go.") @@ -1948,28 +1957,28 @@ class TextTable(object): text['cukeman_2'] = CompressedTextMapper.convert("You found Shabadoo, huh?\nNiiiiice.") text['potion_shop_no_cash'] = CompressedTextMapper.convert("Yo! I'm not running a charity here.") text['kakariko_powdered_chicken'] = CompressedTextMapper.convert("Smallhacker…\n\n\nWas hiding, you found me!\n\n\nOkay, you can leave now.") - text['game_chest_south_of_kakariko'] = CompressedTextMapper.convert("Pay 20 rupees, open 1 chest. Are you lucky?\nSo, Play game?\n ≥ play\n never!\n{CHOICE}") + text['game_chest_south_of_kakariko'] = CompressedTextMapper.convert("Pay 20 rupees, open 1 chest. Are you lucky?\nSo, Play game?\n ≥ Play\n Never!\n{CHOICE}") text['game_chest_play_yes'] = CompressedTextMapper.convert("Good luck then") # 180 text['game_chest_play_no'] = CompressedTextMapper.convert("Well fine, I didn't want your rupees.") - text['game_chest_lost_woods'] = CompressedTextMapper.convert("Pay 100 rupees open 1 chest. Are you lucky?\nSo, Play game?\n ≥ play\n never!\n{CHOICE}") + text['game_chest_lost_woods'] = CompressedTextMapper.convert("Pay 100 rupees open 1 chest. Are you lucky?\nSo, Play game?\n ≥ Play\n Never!\n{CHOICE}") text['kakariko_flophouse_man_no_flippers'] = CompressedTextMapper.convert("I really hate mowing my yard.\nI moved my house and everyone else's to avoid it.\n{PAGEBREAK}\nI hope you don't mind.") text['kakariko_flophouse_man'] = CompressedTextMapper.convert("I really hate mowing my yard.\nI moved my house and everyone else's to avoid it.\n{PAGEBREAK}\nI hope you don't mind.") - text['menu_start_2'] = CompressedTextMapper.convert("{MENU}\n{SPEED0}\n≥@'s house\n Sanctuary\n{CHOICE3}", False) - text['menu_start_3'] = CompressedTextMapper.convert("{MENU}\n{SPEED0}\n≥@'s house\n Sanctuary\n Mountain Cave\n{CHOICE2}", False) - text['menu_pause'] = CompressedTextMapper.convert("{SPEED0}\n≥continue\n save and quit\n{CHOICE3}", False) - text['game_digging_choice'] = CompressedTextMapper.convert("Have 80 Rupees? Want to play digging game?\n ≥yes\n no\n{CHOICE}") + text['menu_start_2'] = CompressedTextMapper.convert("{MENU}\n{SPEED0}\n≥@'s House\n Sanctuary\n{CHOICE3}", False) + text['menu_start_3'] = CompressedTextMapper.convert("{MENU}\n{SPEED0}\n≥@'s House\n Sanctuary\n Mountain Cave\n{CHOICE2}", False) + text['menu_pause'] = CompressedTextMapper.convert("{SPEED0}\n≥Continue\n Save and Quit\n{CHOICE3}", False) + text['game_digging_choice'] = CompressedTextMapper.convert("Have 80 Rupees? Want to play digging game?\n ≥Yes\n No\n{CHOICE}") text['game_digging_start'] = CompressedTextMapper.convert("Okay, use the shovel with Y!") text['game_digging_no_cash'] = CompressedTextMapper.convert("Shovel rental is 80 rupees.\nI have all day") text['game_digging_end_time'] = CompressedTextMapper.convert("Time's up!\nTime for you to go.") text['game_digging_come_back_later'] = CompressedTextMapper.convert("Come back later, I have to bury things.") text['game_digging_no_follower'] = CompressedTextMapper.convert("Something is following you. I don't like.") - text['menu_start_4'] = CompressedTextMapper.convert("{MENU}\n{SPEED0}\n≥@'s house\n Mountain Cave\n{CHOICE3}", False) + text['menu_start_4'] = CompressedTextMapper.convert("{MENU}\n{SPEED0}\n≥@'s House\n Mountain Cave\n{CHOICE3}", False) # Start of new text data - text['ganon_fall_in_alt'] = CompressedTextMapper.convert("You think you\nare ready to\nface me?\n\nI will not die\n\nunless you\ncomplete your\ngoals. Dingus!") - text['ganon_phase_3_alt'] = CompressedTextMapper.convert("Got wax in\nyour ears?\nI cannot die!") + text['ganon_fall_in_alt'] = CompressedTextMapper.convert("You think you are ready to face me?\n\nI will not die unless you complete your goals. Dingus!") + text['ganon_phase_3_alt'] = CompressedTextMapper.convert("Got wax in your ears? I cannot die!") # 190 - text['sign_east_death_mountain_bridge'] = CompressedTextMapper.convert("How did you get up here?") + text['sign_east_death_mountain_bridge'] = CompressedTextMapper.convert("Glitched\ntournament\nwinners\n{HARP}\n~~~HMG 2021~~~\nKrithel\n\n~~~OWG 2019~~~\nGlan\n\n~~~OWG 2018~~~\nChristosOwen\nthe numpty") text['fish_money'] = CompressedTextMapper.convert("It's a secret to everyone.") text['sign_ganons_tower'] = CompressedTextMapper.convert("You need all 7 crystals to enter.") text['sign_ganon'] = CompressedTextMapper.convert("You need all 7 crystals to beat Ganon.") @@ -1979,4 +1988,4 @@ class TextTable(object): text['ganon_phase_3_silvers'] = CompressedTextMapper.convert("Oh no! Silver! My one true weakness!") text['murahdahla'] = CompressedTextMapper.convert("Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\ninvisibility.\n{PAUSE3}\n… … …\nWait! you can see me? I knew I should have\nhidden in a hollow tree.") text['end_pad_data'] = bytearray([0xfb]) - text['terminator'] = bytearray([0xFF, 0xFF]) + text['terminator'] = bytearray([0xFF, 0xFF]) diff --git a/Utils.py b/Utils.py index 75b83d14..d0bbb5fc 100644 --- a/Utils.py +++ b/Utils.py @@ -678,6 +678,11 @@ def extract_data_from_jp_rom(rom): # print_data_block(secretdata) # print() +def count_set_bits(val): + if val == 0: + return 0 + else: + return (val & 1) + count_set_bits(val >> 1) def check_pots(): from PotShuffle import vanilla_pots diff --git a/asm/hudadditions.asm b/asm/hudadditions.asm index 14844ad9..575e189f 100644 --- a/asm/hudadditions.asm +++ b/asm/hudadditions.asm @@ -196,8 +196,8 @@ OldHudToNewHudTable: dw 1, 2, 3, 10, 4, 6, 5, 8, 11, 9, 7, 12, 13 IndicatorCharacters: - ; check G P R C - dw $2426, $2590, $2599, $259B, $258C + ; check 1 2 3 4 5 6 7 G B R + dw $2426, $2817, $2818, $2819, $281A, $281B, $281C, $281D, $2590, $258B, $259B MapIndicator: LDA.l CrystalPendantFlags_3, X : AND #$00FF @@ -223,7 +223,7 @@ ConvertToDisplay: ConvertToDisplay2: and.w #$00ff : beq ++ cmp #$000a : !blt + - !add #$2553 : rts + !add #$2553 : rts ; todo: use 2580 with 258A as "A" for non transparent digits + !add #$2816 : rts ++ lda #$2827 : rts ; 0/O for 0 or placeholder digit ;2483 diff --git a/data/base2current.bps b/data/base2current.bps index ff88366f..bd6417a4 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ diff --git a/mystery_example.yml b/mystery_example.yml index a7f26ddb..b51c58c5 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -5,6 +5,10 @@ major_only: 1 dungeon_only: 1 district: 1 + restrict_boss_items: + none: 1 +# mapcompass: 1 Bug exists, not recommended + dungeon: 1 door_shuffle: vanilla: 0 basic: 2 @@ -17,11 +21,15 @@ on: 1 off: 1 pottery: - none: 4 + none: 8 keys: 1 cave: 1 dungeon: 1 lottery: 1 + cavekeys: 1 + reduced: 1 + clustered: 1 + nonempty: 1 shopsanity: on: 1 off: 1 diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 6f361835..4e805b12 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -261,8 +261,8 @@ "Keys: Key pots are included in the location pool and other items can take their place", "Cave: Only pots in houses and caves are included in the location pool", "CaveKeys: Both pots in houses and caves and keys pots are included in the location pool", - "Reduced: Same as KeyCaves + 25% of Pots in dungeons (dynamic mode)", - "Clustered: Same as KeyCaves + 50% of Pots in dungeons, chosen by logical group (dynamic mode)", + "Reduced: Same as KeyCaves + 25%% of Pots in dungeons (dynamic mode)", + "Clustered: Same as KeyCaves + 50%% of Pots in dungeons, chosen by logical group (dynamic mode)", "NonEmpty: All pots that are not originally empty are included in the location pool", "Dungeon: Only pots in dungeons are included in the location pool", "Lottery: All pots are part of the location pool" diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py index 4be12667..9614eeef 100644 --- a/source/item/FillUtil.py +++ b/source/item/FillUtil.py @@ -29,7 +29,6 @@ class LocationGroup(object): # flags self.keyshuffle = False - self.keydropshuffle = False self.shopsanity = False self.retro = False @@ -37,9 +36,8 @@ class LocationGroup(object): self.locations = list(locs) return self - def flags(self, k, d=False, s=False, r=False): + def flags(self, k, s=False, r=False): self.keyshuffle = k - self.keydropshuffle = d self.shopsanity = s self.retro = r return self @@ -124,12 +122,14 @@ def create_item_pool_config(world): groups = LocationGroup('Major').locs(init_set) if world.bigkeyshuffle[player]: groups.locations.extend(mode_grouping['Big Keys']) - if world.keydropshuffle[player] != 'none': + if world.dropshuffle[player] != 'none': groups.locations.extend(mode_grouping['Big Key Drops']) if world.keyshuffle[player]: groups.locations.extend(mode_grouping['Small Keys']) - if world.keydropshuffle[player] != 'none': + if world.dropshuffle[player] != 'none': groups.locations.extend(mode_grouping['Key Drops']) + if world.pottery[player] not in ['none', 'cave']: + groups.locations.extend(mode_grouping['Pot Keys']) if world.compassshuffle[player]: groups.locations.extend(mode_grouping['Compasses']) if world.mapshuffle[player]: @@ -275,7 +275,7 @@ def massage_item_pool(world): world.itempool.remove(deleted) discrepancy -= 1 if discrepancy > 0: - logging.getLogger('').warning(f'Too many good items in pool, something will be removed at random') + raise Exception(f'Too many required items in pool, {discrepancy} items cannot be placed') if world.item_pool_config.placeholders is not None: removed = 0 single_rupees = [item for item in world.itempool if item.name == 'Rupee (1)'] @@ -329,49 +329,6 @@ def count_major_items(config, world, player): return sum(1 for x in world.itempool if x.name in config.item_pool[player] and x.player == player) -def calc_dungeon_limits(world, player): - b, s, c, m, k, r, bi = (world.bigkeyshuffle[player], world.keyshuffle[player], world.compassshuffle[player], - world.mapshuffle[player], world.keydropshuffle[player], world.retro[player], - world.restrict_boss_items[player]) - if world.doorShuffle[player] in ['vanilla', 'basic']: - limits = {} - for dungeon, info in dungeon_table.items(): - val = info.free_items - if bi != 'none' and info.prize: - if bi == 'mapcompass' and (not c or not m): - val -= 1 - if bi == 'dungeon' and (not c or not m or not (s or r) or not b): - val -= 1 - if b: - val += 1 if info.bk_present else 0 - if k != 'none': - val += 1 if info.bk_drops else 0 - if s or r: - val += info.key_num - if k != 'none': - val += info.key_drops - if c: - val += 1 if info.compass_present else 0 - if m: - val += 1 if info.map_present else 0 - limits[dungeon] = val - else: - limits = 60 - if world.bigkeyshuffle[player]: - limits += 11 - if world.keydropshuffle[player] != 'none': - limits += 1 - if world.keyshuffle[player] or world.retro[player]: - limits += 29 - if world.keydropshuffle[player] != 'none': - limits += 32 - if world.compassshuffle[player]: - limits += 11 - if world.mapshuffle[player]: - limits += 12 - return limits - - def determine_major_items(world, player): major_item_set = set(major_items) if world.progressive == 'off': @@ -731,7 +688,7 @@ mode_grouping = { 'Sewers - Dark Cross', 'Desert Palace - Torch', 'Tower of Hera - Basement Cage', 'Castle Tower - Room 03', 'Castle Tower - Dark Maze', 'Palace of Darkness - Stalfos Basement', 'Palace of Darkness - Dark Basement - Right', - 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Shooter Room', + 'Palace of Darkness - Harmless Hellway', 'Palace of Darkness - Shooter Room', 'Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - The Arena - Ledge', "Thieves' Town - Blind's Cell", 'Skull Woods - Bridge Room', 'Ice Palace - Spike Room', 'Skull Woods - Pot Prison', 'Skull Woods - Pinball Room', 'Misery Mire - Spike Chest', @@ -776,19 +733,22 @@ mode_grouping = { ], 'Key Drops': [ 'Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop', - 'Hyrule Castle - Key Rat Key Drop', 'Eastern Palace - Dark Square Pot Key', - 'Eastern Palace - Dark Eyegore Key Drop', 'Desert Palace - Desert Tiles 1 Pot Key', - 'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key', + 'Hyrule Castle - Key Rat Key Drop', 'Eastern Palace - Dark Eyegore Key Drop', 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop', + 'Skull Woods - Spike Corner Key Drop', 'Ice Palace - Jelly Key Drop', 'Ice Palace - Conveyor Key Drop', + 'Misery Mire - Conveyor Crystal Key Drop', 'Turtle Rock - Pokey 1 Key Drop', + 'Turtle Rock - Pokey 2 Key Drop', 'Ganons Tower - Mini Helmasuar Key Drop', + ], + 'Pot Keys': [ + 'Eastern Palace - Dark Square Pot Key', 'Desert Palace - Desert Tiles 1 Pot Key', + 'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key', 'Swamp Palace - Pot Row Pot Key', 'Swamp Palace - Trench 1 Pot Key', 'Swamp Palace - Hookshot Pot Key', 'Swamp Palace - Trench 2 Pot Key', 'Swamp Palace - Waterway Pot Key', 'Skull Woods - West Lobby Pot Key', - 'Skull Woods - Spike Corner Key Drop', "Thieves' Town - Hallway Pot Key", - "Thieves' Town - Spike Switch Pot Key", 'Ice Palace - Jelly Key Drop', 'Ice Palace - Conveyor Key Drop', + "Thieves' Town - Hallway Pot Key", "Thieves' Town - Spike Switch Pot Key", 'Ice Palace - Hammer Block Key Drop', 'Ice Palace - Many Pots Pot Key', 'Misery Mire - Spikes Pot Key', - 'Misery Mire - Fishbone Pot Key', 'Misery Mire - Conveyor Crystal Key Drop', 'Turtle Rock - Pokey 1 Key Drop', - 'Turtle Rock - Pokey 2 Key Drop', 'Ganons Tower - Conveyor Cross Pot Key', + 'Misery Mire - Fishbone Pot Key', 'Ganons Tower - Conveyor Cross Pot Key', 'Ganons Tower - Double Switch Pot Key', 'Ganons Tower - Conveyor Star Pits Pot Key', - 'Ganons Tower - Mini Helmasuar Key Drop', + ], 'Big Key Drops': ['Hyrule Castle - Big Key Drop'], 'Shops': [