diff --git a/BaseClasses.py b/BaseClasses.py index 4cfd41ca..a7c1f10c 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -86,6 +86,7 @@ class World(object): self._portal_cache = {} self.sanc_portal = {} self.fish = BabelFish() + self.pot_contents = {} for player in range(1, players + 1): # If World State is Retro, set to Open and set Retro flag @@ -381,6 +382,8 @@ class World(object): location.item = item item.location = location item.world = self + if location.player != item.player and location.type == LocationType.Pot: + self.pot_contents[location.player].multiworld_count += 1 if collect: self.state.collect(item, location.event, location) @@ -2123,8 +2126,15 @@ class Location(object): self.pot = None def can_fill(self, state, item, check_access=True): + if not self.valid_multiworld(state, item): + return False return self.always_allow(state, item) or (self.parent_region.can_fill(item) and self.item_rule(item) and (not check_access or self.can_reach(state))) + def valid_multiworld(self, state, item): + if self.type == LocationType.Pot and self.player != item.player: + return state.world.pot_contents[self.player].multiworld_count < 256 + return True + def can_reach(self, state): return self.parent_region.can_reach(state) and self.access_rule(state) @@ -2465,11 +2475,12 @@ class Spoiler(object): 'enemy_shuffle': self.world.enemy_shuffle, 'enemy_health': self.world.enemy_health, 'enemy_damage': self.world.enemy_damage, - 'potshuffle': self.world.potshuffle, 'players': self.world.players, 'teams': self.world.teams, 'experimental': self.world.experimental, - 'keydropshuffle': self.world.keydropshuffle, + 'dropshuffle': self.world.dropshuffle, + 'pottery': self.world.pottery, + 'potshuffle': self.world.potshuffle, 'shopsanity': self.world.shopsanity, 'pseudoboots': self.world.pseudoboots, 'triforcegoal': self.world.treasure_hunt_count, @@ -2530,7 +2541,9 @@ class Spoiler(object): outfile.write(f"Link's House Shuffled: {yn(self.metadata['shufflelinks'])}\n") outfile.write('Door Shuffle: %s\n' % self.metadata['door_shuffle'][player]) outfile.write('Intensity: %s\n' % self.metadata['intensity'][player]) - outfile.write(f"Drop Shuffle Mode: {self.metadata['keydropshuffle'][player]}\n") + outfile.write(f"Drop Shuffle: {yn(self.metadata['dropshuffle'][player])}\n") + outfile.write(f"Pottery Mode: {self.metadata['pottery'][player]}\n") + outfile.write(f"Pot Shuffle (Legacy): {yn(self.metadata['potshuffle'][player])}\n") addition = ' (Random)' if self.world.crystals_gt_orig[player] == 'random' else '' outfile.write('Crystals required for GT: %s\n' % (str(self.metadata['gt_crystals'][player]) + addition)) addition = ' (Random)' if self.world.crystals_ganon_orig[player] == 'random' else '' @@ -2546,7 +2559,6 @@ class Spoiler(object): outfile.write('Enemy shuffle: %s\n' % self.metadata['enemy_shuffle'][player]) outfile.write('Enemy health: %s\n' % self.metadata['enemy_health'][player]) outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'][player]) - outfile.write(f"Pot shuffle: {yn(self.metadata['potshuffle'][player])}\n") outfile.write(f"Hints: {yn(self.metadata['hints'][player])}\n") outfile.write(f"Experimental: {yn(self.metadata['experimental'][player])}\n") outfile.write(f"Shopsanity: {yn(self.metadata['shopsanity'][player])}\n") @@ -2737,28 +2749,33 @@ goal_mode = {"ganon": 0, "pedestal": 1, "dungeons": 2, "triforcehunt": 3, "cryst diff_mode = {"normal": 0, "hard": 1, "expert": 2} func_mode = {"normal": 0, "hard": 1, "expert": 2} -# byte 3: SKMM PIII (shop, keydrop, mixed, palettes, intensity) -# todo keydrop is not longer a switch +# byte 3: S?MM PIII (shop, unused, mixed, palettes, intensity) +# keydrop now has it's own byte mixed_travel_mode = {"prevent": 0, "allow": 1, "force": 2} # intensity is 3 bits (reserves 4-7 levels) -# byte 4: CCCC CTTX (crystals gt, ctr2, experimental) +# new byte 4: ?DDD PPPP (unused, drop, pottery) +# dropshuffle reserves 2 bits, pottery needs 2 but reserves 2 for future modes) +pottery_mode = {"none": 0, "shuffle": 1, "keys": 2, "lottery": 3} + +# byte 5: CCCC CTTX (crystals gt, ctr2, experimental) counter_mode = {"default": 0, "off": 1, "on": 2, "pickup": 3} -# byte 5: CCCC CPAA (crystals ganon, pyramid, access +# byte 6: CCCC CPAA (crystals ganon, pyramid, access access_mode = {"items": 0, "locations": 1, "none": 2} -# byte 6: BSMC BBEE (big, small, maps, compass, bosses, enemies) +# byte 7: BSMC BBEE (big, small, maps, compass, bosses, enemies) boss_mode = {"none": 0, "simple": 1, "full": 2, "random": 3, "chaos": 3} enemy_mode = {"none": 0, "shuffled": 1, "random": 2, "chaos": 2, "legacy": 3} -# byte 7: HHHD DPBS (enemy_health, enemy_dmg, potshuffle, bomb logic, shuffle links) +# byte 8: HHHD DPBS (enemy_health, enemy_dmg, potshuffle, bomb logic, shuffle links) +# potshuffle decprecated, now unused e_health = {"default": 0, "easy": 1, "normal": 2, "hard": 3, "expert": 4} e_dmg = {"default": 0, "shuffled": 1, "random": 2} -# byte 8: RRAA A??? (restrict boss mode, algorithm, ? = unused) +# byte 9: RRAA A??? (restrict boss mode, algorithm, ? = unused) rb_mode = {"none": 0, "mapcompass": 1, "dungeon": 2} -# algorithm: todo with "biased shuffles" +# algorithm: algo_mode = {"balanced": 0, "equitable": 1, "vanilla_fill": 2, "dungeon_only": 3, "district": 4} # additions @@ -2779,10 +2796,12 @@ class Settings(object): (goal_mode[w.goal[p]] << 5) | (diff_mode[w.difficulty[p]] << 3) | (func_mode[w.difficulty_adjustments[p]] << 1) | (1 if w.hints[p] else 0), - (0x80 if w.shopsanity[p] else 0) | (0x40 if w.keydropshuffle[p] else 0) - | (mixed_travel_mode[w.mixed_travel[p]] << 4) | (0x8 if w.standardize_palettes[p] == "original" else 0) + (0x80 if w.shopsanity[p] else 0) | (mixed_travel_mode[w.mixed_travel[p]] << 4) + | (0x8 if w.standardize_palettes[p] == "original" else 0) | (0 if w.intensity[p] == "random" else w.intensity[p]), + (0x10 if w.dropshuffle[p] else 0) | (pottery_mode[w.pottery[p]]), + ((8 if w.crystals_gt_orig[p] == "random" else int(w.crystals_gt_orig[p])) << 3) | (counter_mode[w.dungeon_counters[p]] << 1) | (1 if w.experimental[p] else 0), @@ -2819,31 +2838,39 @@ class Settings(object): args.hints[p] = True if settings[2] & 0x01 else False args.retro[p] = True if settings[1] & 0x01 else False args.shopsanity[p] = True if settings[3] & 0x80 else False - args.keydropshuffle[p] = True if settings[3] & 0x40 else False + # args.keydropshuffle[p] = True if settings[3] & 0x40 else False args.mixed_travel[p] = r(mixed_travel_mode)[(settings[3] & 0x30) >> 4] args.standardize_palettes[p] = "original" if settings[3] & 0x8 else "standardize" intensity = settings[3] & 0x7 args.intensity[p] = "random" if intensity == 0 else intensity - args.dungeon_counters[p] = r(counter_mode)[(settings[4] & 0x6) >> 1] - cgt = (settings[4] & 0xf8) >> 3 + + # args.shuffleswitches[p] = True if settings[4] & 0x80 else False + args.dropshuffle[p] = True if settings[4] & 0x10 else False + args.pottery[p] = r(pottery_mode)[settings[4] & 0x0F] + + args.dungeon_counters[p] = r(counter_mode)[(settings[5] & 0x6) >> 1] + cgt = (settings[5] & 0xf8) >> 3 args.crystals_gt[p] = "random" if cgt == 8 else cgt - args.experimental[p] = True if settings[4] & 0x1 else False - cgan = (settings[5] & 0xf8) >> 3 + args.experimental[p] = True if settings[5] & 0x1 else False + + cgan = (settings[6] & 0xf8) >> 3 args.crystals_ganon[p] = "random" if cgan == 8 else cgan - args.openpyramid[p] = True if settings[5] & 0x4 else False - args.bigkeyshuffle[p] = True if settings[6] & 0x80 else False - args.keyshuffle[p] = True if settings[6] & 0x40 else False - args.mapshuffle[p] = True if settings[6] & 0x20 else False - args.compassshuffle[p] = True if settings[6] & 0x10 else False - args.shufflebosses[p] = r(boss_mode)[(settings[6] & 0xc) >> 2] - args.shuffleenemies[p] = r(enemy_mode)[settings[6] & 0x3] - args.enemy_health[p] = r(e_health)[(settings[7] & 0xE0) >> 5] - args.enemy_damage[p] = r(e_dmg)[(settings[7] & 0x18) >> 3] + args.openpyramid[p] = True if settings[6] & 0x4 else False + + args.bigkeyshuffle[p] = True if settings[7] & 0x80 else False + args.keyshuffle[p] = True if settings[7] & 0x40 else False + args.mapshuffle[p] = True if settings[7] & 0x20 else False + args.compassshuffle[p] = True if settings[7] & 0x10 else False + args.shufflebosses[p] = r(boss_mode)[(settings[7] & 0xc) >> 2] + args.shuffleenemies[p] = r(enemy_mode)[settings[7] & 0x3] + + args.enemy_health[p] = r(e_health)[(settings[8] & 0xE0) >> 5] + args.enemy_damage[p] = r(e_dmg)[(settings[8] & 0x18) >> 3] args.shufflepots[p] = True if settings[7] & 0x4 else False - args.bombbag[p] = True if settings[7] & 0x2 else False - args.shufflelinks[p] = True if settings[7] & 0x1 else False - if len(settings) > 8: - args.restrict_boss_items[p] = True if r(rb_mode)[(settings[8] & 0x80) >> 6] else False + args.bombbag[p] = True if settings[8] & 0x2 else False + args.shufflelinks[p] = True if settings[8] & 0x1 else False + if len(settings) > 9: + args.restrict_boss_items[p] = r(rb_mode)[(settings[9] & 0x80) >> 6] class KeyRuleType(FastEnum): diff --git a/CLI.py b/CLI.py index 5c9d90db..fe7fdf29 100644 --- a/CLI.py +++ b/CLI.py @@ -88,6 +88,10 @@ def parse_cli(argv, no_defaults=False): if ret.keysanity: ret.mapshuffle, ret.compassshuffle, ret.keyshuffle, ret.bigkeyshuffle = [True] * 4 + if ret.keydropshuffle: + ret.dropshuffle = True + ret.pottery = 'keys' if ret.pottery == 'none' else ret.pottery + if multiargs.multi: defaults = copy.deepcopy(ret) for player in range(1, multiargs.multi + 1): @@ -101,9 +105,9 @@ def parse_cli(argv, no_defaults=False): 'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'pseudoboots', 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters', 'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots', - 'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep', - 'remote_items', 'shopsanity', 'keydropshuffle', 'mixed_travel', 'standardize_palettes', 'code', - 'reduce_flashing', 'shuffle_sfx']: + 'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', + 'heartbeep', 'remote_items', 'shopsanity', 'dropshuffle', 'pottery', + 'mixed_travel', 'standardize_palettes', 'code', 'reduce_flashing', 'shuffle_sfx']: value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) if player == 1: setattr(ret, name, {1: value}) @@ -150,7 +154,6 @@ def parse_settings(): "overworld_map": "default", "pseudoboots": False, - "shufflepots": False, "shuffleenemies": "none", "shufflebosses": "none", "enemy_damage": "default", @@ -158,7 +161,10 @@ def parse_settings(): "enemizercli": os.path.join(".", "EnemizerCLI", "EnemizerCLI.Core"), "shopsanity": False, - "keydropshuffle": 'none', + 'keydropshuffle': False, + 'dropshuffle': False, + 'pottery': 'none', + 'shufflepots': False, "mapshuffle": False, "compassshuffle": False, "keyshuffle": False, diff --git a/DoorShuffle.py b/DoorShuffle.py index 9c1bc540..708fbf8e 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -6,6 +6,7 @@ from enum import unique, Flag from typing import DefaultDict, Dict, List from BaseClasses import RegionType, Region, Door, DoorType, Direction, Sector, CrystalBarrier, DungeonInfo, dungeon_keys +from BaseClasses import PotFlags, LocationType from Doors import reset_portals from Dungeons import dungeon_regions, region_starts, standard_starts, split_region_starts from Dungeons import dungeon_bigs, dungeon_hints @@ -378,7 +379,7 @@ def choose_portals(world, player): if world.doorShuffle[player] in ['basic', 'crossed']: cross_flag = world.doorShuffle[player] == 'crossed' # key drops allow the big key in the right place in Desert Tiles 2 - bk_shuffle = world.bigkeyshuffle[player] or world.keydropshuffle[player] != 'none' + bk_shuffle = world.bigkeyshuffle[player] or world.dropshuffle[player] std_flag = world.mode[player] == 'standard' # roast incognito doors world.get_room(0x60, player).delete(5) @@ -994,10 +995,15 @@ def cross_dungeon(world, player): assign_cross_keys(dungeon_builders, world, player) all_dungeon_items_cnt = len(list(y for x in world.dungeons if x.player == player for y in x.all_items)) - if world.keydropshuffle[player] != 'none': - target_items = 35 if world.retro[player] else 96 + target_items = 34 + if world.retro[player]: + target_items += 1 if world.dropshuffle[player] else 0 # the hc big key else: - target_items = 34 if world.retro[player] else 63 + target_items += 29 # small keys in chests + if world.dropshuffle[player]: + target_items += 14 # 13 dropped smalls + 1 big + if world.pottery[player] != 'none': + target_items += 19 # 19 pot keys d_items = target_items - all_dungeon_items_cnt world.pool_adjustment[player] = d_items smooth_door_pairs(world, player) @@ -1055,7 +1061,11 @@ def assign_cross_keys(dungeon_builders, world, player): logging.getLogger('').info(world.fish.translate("cli", "cli", "shuffling.keydoors")) start = time.process_time() if world.retro[player]: - remaining = 61 if world.keydropshuffle[player] != 'none' else 29 + remaining = 29 + if world.dropshuffle[player]: + remaining += 13 + if world.pottery[player] in ['keys', 'lottery']: + remaining += 19 else: remaining = len(list(x for dgn in world.dungeons if dgn.player == player for x in dgn.small_keys)) total_keys = remaining @@ -1074,7 +1084,6 @@ def assign_cross_keys(dungeon_builders, world, player): total_candidates += builder.key_doors_num start_regions_map[name] = start_regions - # Step 2: Initial Key Number Assignment & Calculate Flexibility for name, builder in dungeon_builders.items(): calculated = int(round(builder.key_doors_num*total_keys/total_candidates)) @@ -1251,7 +1260,13 @@ def refine_hints(dungeon_builders): for region in builder.master_sector.regions: for location in region.locations: if not location.event and '- Boss' not in location.name and '- Prize' not in location.name and location.name != 'Sanctuary': - location.hint_text = dungeon_hints[name] + if location.type == LocationType.Pot and location.pot: + hint_text = ('under a block' if location.pot.flags & PotFlags.Block else 'in a pot') + location.hint_text = f'{hint_text} {dungeon_hints[name]}' + elif location.type == LocationType.Drop: + location.hint_text = f'dropped {dungeon_hints[name]}' + else: + location.hint_text = dungeon_hints[name] def refine_boss_exits(world, player): @@ -3015,7 +3030,8 @@ palette_map = { 'Tower of Hera': (0x6, None), 'Thieves Town': (0x17, None), # the attic uses 0x23 'Turtle Rock': (0x18, 0x19, 'TR Boss SW'), - 'Ganons Tower': (0x28, 0x1b, 'GT Agahnim 2 SW'), # other palettes: 0x1a (other) 0x24 (Gauntlet - Lanmo) 0x25 (conveyor-torch-wizzrode moldorm pit f5?) + 'Ganons Tower': (0x28, 0x1b, 'GT Agahnim 2 SW'), + # other palettes: 0x1a (other) 0x24 (Gauntlet - Lanmo) 0x25 (conveyor-torch-wizzrobe moldorm pit f5?) } # implications: diff --git a/Fill.py b/Fill.py index 071ec65a..4fe72e89 100644 --- a/Fill.py +++ b/Fill.py @@ -7,7 +7,7 @@ from BaseClasses import CollectionState, FillError, LocationType from Items import ItemFactory from Regions import shop_to_location_table, retro_shops from source.item.FillUtil import filter_locations, classify_major_items, replace_trash_item, vanilla_fallback -from source.item.FillUtil import filter_pot_locations +from source.item.FillUtil import filter_pot_locations, valid_pot_items def get_dungeon_item_pool(world): @@ -435,6 +435,8 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None fill_locations.remove(l) filtered_fill(world, placeholder_items, placeholder_locations) + if world.players > 1: + fast_fill_pot_for_multiworld(world, restitempool, fill_locations) if world.algorithm == 'vanilla_fill': fast_vanilla_fill(world, restitempool, fill_locations) else: @@ -445,11 +447,14 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None if unplaced or unfilled: logging.warning('Unplaced items: %s - Unfilled Locations: %s', unplaced, unfilled) - # convert Arrows 5 and Nothing when necessary - invalid_locations = [loc for loc in world.get_locations() if loc.item.name in {'Arrows (5)', 'Nothing'} and - (loc.type != LocationType.Pot or loc.item.player != loc.player)] - for i_loc in invalid_locations: - i_loc.item = ItemFactory(invalid_location_replacement[i_loc.item.name], i_loc.item.player) + for loc in world.get_locations(): + # convert Arrows 5 and Nothing when necessary + if (loc.item.name in {'Arrows (5)', 'Nothing'} + and (loc.type != LocationType.Pot or loc.item.player != loc.player)): + loc.item = ItemFactory(invalid_location_replacement[loc.item.name], loc.item.player) + # don't write out all pots to spoiler + if loc.type == LocationType.Pot and loc.item.name in valid_pot_items: + loc.skip = True invalid_location_replacement = {'Arrows (5)': 'Arrows (10)', 'Nothing': 'Rupees (5)'} @@ -470,6 +475,28 @@ def fast_fill(world, item_pool, fill_locations): world.push_item(spot_to_fill, item_to_place, False) +def fast_fill_pot_for_multiworld(world, item_pool, fill_locations): + pot_item_pool = collections.defaultdict(list) + pot_fill_locations = collections.defaultdict(list) + for item in item_pool: + if item.name in valid_pot_items: + pot_item_pool[item.player].append(item) + for loc in fill_locations: + if loc.type == LocationType.Pot: + pot_fill_locations[loc.player].append(loc) + for player in range(1, world.players+1): + flex = 256 - world.pot_contents[player].multiworld_count + fill_count = len(pot_fill_locations[player]) - flex + if fill_count > 0: + fill_spots = random.sample(pot_fill_locations[player], fill_count) + fill_items = random.sample(pot_item_pool[player], fill_count) + for x in fill_items: + item_pool.remove(x) + for x in fill_spots: + fill_locations.remove(x) + fast_fill(world, fill_items, fill_spots) + + def filtered_fill(world, item_pool, fill_locations): while item_pool and fill_locations: item_to_place = item_pool.pop() diff --git a/InvertedRegions.py b/InvertedRegions.py index 2465a841..b5625d1f 100644 --- a/InvertedRegions.py +++ b/InvertedRegions.py @@ -66,15 +66,15 @@ def create_inverted_regions(world, player): create_cave_region(player, 'Chicken House', 'a house with a chest', ['Chicken House']), create_cave_region(player, 'Aginahs Cave', 'a cave with a chest', ['Aginah\'s Cave']), create_cave_region(player, 'Sahasrahlas Hut', 'Sahasrahla', ['Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right', 'Sahasrahla']), - create_cave_region(player, 'Kakariko Well (top)', 'a drop\'s exit', + create_cave_region(player, 'Kakariko Well (top)', 'a drop', ['Kakariko Well - Left', 'Kakariko Well - Middle', 'Kakariko Well - Right', 'Kakariko Well - Bottom'], ['Kakariko Well (top to bottom)', 'Kakariko Well (top to back)']), - create_cave_region(player, 'Kakariko Well (back)', 'a drop\'s exit', ['Kakariko Well - Top']), + create_cave_region(player, 'Kakariko Well (back)', 'a drop', ['Kakariko Well - Top']), create_cave_region(player, 'Kakariko Well (bottom)', 'a drop\'s exit', None, ['Kakariko Well Exit']), create_cave_region(player, 'Blacksmiths Hut', 'the smith', ['Blacksmith', 'Missing Smith']), create_lw_region(player, 'Bat Cave Drop Ledge', None, ['Bat Cave Drop']), - create_cave_region(player, 'Bat Cave (right)', 'a drop\'s exit', ['Magic Bat'], ['Bat Cave Door']), + create_cave_region(player, 'Bat Cave (right)', 'a drop', ['Magic Bat'], ['Bat Cave Door']), create_cave_region(player, 'Bat Cave (left)', 'a drop\'s exit', None, ['Bat Cave Exit']), create_cave_region(player, 'Sick Kids House', 'the sick kid', ['Sick Kid']), create_lw_region(player, 'Hobo Bridge', ['Hobo']), diff --git a/ItemList.py b/ItemList.py index 1b262567..1c620ddf 100644 --- a/ItemList.py +++ b/ItemList.py @@ -389,12 +389,15 @@ def generate_itempool(world, player): if world.retro[player]: set_up_take_anys(world, player) - if world.keydropshuffle[player] != 'none': - world.itempool += [ItemFactory('Small Key (Universal)', player)] * 32 + if world.dropshuffle[player]: + world.itempool += [ItemFactory('Small Key (Universal)', player)] * 13 + if world.pottery[player] != 'none': + world.itempool += [ItemFactory('Small Key (Universal)', player)] * 19 + create_dynamic_shop_locations(world, player) - if world.keydropshuffle[player] == 'potsanity': + if world.pottery[player] == 'lottery': add_pot_contents(world, player) diff --git a/Main.py b/Main.py index 27294fb5..cff9eb16 100644 --- a/Main.py +++ b/Main.py @@ -97,10 +97,11 @@ def main(args, seed=None, fish=None): world.intensity = {player: random.randint(1, 3) if args.intensity[player] == 'random' else int(args.intensity[player]) for player in range(1, world.players + 1)} world.experimental = args.experimental.copy() world.dungeon_counters = args.dungeon_counters.copy() - world.potshuffle = args.shufflepots.copy() world.fish = fish world.shopsanity = args.shopsanity.copy() - world.keydropshuffle = args.keydropshuffle.copy() + world.dropshuffle = args.dropshuffle.copy() + world.pottery = args.pottery.copy() + 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() @@ -158,7 +159,7 @@ def main(args, seed=None, fish=None): logger.info(world.fish.translate("cli", "cli", "shuffling.pots")) for player in range(1, world.players + 1): if world.potshuffle[player]: - if world.keydropshuffle[player] != 'potsanity': + if world.pottery[player] != 'lottery': shuffle_pots(world, player) else: shuffle_pot_switches(world, player) @@ -401,7 +402,9 @@ def copy_world(world): ret.intensity = world.intensity.copy() ret.experimental = world.experimental.copy() ret.shopsanity = world.shopsanity.copy() - ret.keydropshuffle = world.keydropshuffle.copy() + ret.dropshuffle = world.dropshuffle.copy() + ret.pottery = world.pottery.copy() + ret.potshuffle = world.potshuffle.copy() ret.mixed_travel = world.mixed_travel.copy() ret.standardize_palettes = world.standardize_palettes.copy() ret.restrict_boss_items = world.restrict_boss_items.copy() diff --git a/MultiClient.py b/MultiClient.py index 501630e0..4504fed7 100644 --- a/MultiClient.py +++ b/MultiClient.py @@ -8,8 +8,10 @@ import shlex import urllib.parse import websockets +from BaseClasses import PotItem, PotFlags import Items import Regions +import PotShuffle class ReceivedItem: @@ -57,8 +59,13 @@ class Context: self.key_drop_mode = False self.shop_mode = False self.retro_mode = False + self.pottery_mode = False + self.mystery_mode = False self.ignore_count = 0 + self.lookup_name_to_id = {} + self.lookup_id_to_name = {} + def color_code(*args): codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37 , 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43, @@ -83,6 +90,10 @@ 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 +ITEM_SRAM_SIZE = 0x250 + RECV_PROGRESS_ADDR = SAVEDATA_START + 0x4D0 # 2 bytes RECV_ITEM_ADDR = SAVEDATA_START + 0x4D2 # 1 byte RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3 # 1 byte @@ -349,6 +360,8 @@ location_table_misc = {'Bottle Merchant': (0x3c9, 0x2), 'Purple Chest': (0x3c9, 0x10), "Link's Uncle": (0x3c6, 0x1), 'Hobo': (0x3c9, 0x1)} +location_table_pot_items = {} +location_table_sprite_items = {} SNES_DISCONNECTED = 0 SNES_CONNECTING = 1 @@ -676,7 +689,7 @@ async def process_server_cmd(ctx : Context, cmd, args): ctx.player_names = {p: n for p, n in args[1]} msgs = [] if ctx.locations_checked: - msgs.append(['LocationChecks', [Regions.lookup_name_to_id[loc] for loc in ctx.locations_checked]]) + msgs.append(['LocationChecks', [ctx.lookup_name_to_id[loc] for loc in ctx.locations_checked]]) if ctx.locations_scouted: msgs.append(['LocationScouts', list(ctx.locations_scouted)]) if msgs: @@ -689,7 +702,7 @@ async def process_server_cmd(ctx : Context, cmd, args): elif start_index != len(ctx.items_received): sync_msg = [['Sync']] if ctx.locations_checked: - sync_msg.append(['LocationChecks', [Regions.lookup_name_to_id[loc] for loc in ctx.locations_checked]]) + sync_msg.append(['LocationChecks', [ctx.lookup_name_to_id[loc] for loc in ctx.locations_checked]]) await send_msgs(ctx.socket, sync_msg) if start_index == len(ctx.items_received): for item in items: @@ -701,7 +714,7 @@ async def process_server_cmd(ctx : Context, cmd, args): if location not in ctx.locations_info: replacements = {0xA2: 'Small Key', 0x9D: 'Big Key', 0x8D: 'Compass', 0x7D: 'Map'} item_name = replacements.get(item, get_item_name_from_id(item)) - logging.info(f"Saw {color(item_name, 'red', 'bold')} at {list(Regions.lookup_id_to_name.keys())[location - 1]}") + logging.info(f"Saw {color(item_name, 'red', 'bold')} at {list(ctx.lookup_id_to_name.keys())[location - 1]}") ctx.locations_info[location] = (item, player) ctx.watcher_event.set() @@ -710,7 +723,8 @@ async def process_server_cmd(ctx : Context, cmd, args): item = color(get_item_name_from_id(item), 'cyan' if player_sent != ctx.slot else 'green') player_sent = color(ctx.player_names[player_sent], 'yellow' if player_sent != ctx.slot else 'magenta') player_recvd = color(ctx.player_names[player_recvd], 'yellow' if player_recvd != ctx.slot else 'magenta') - logging.info('%s sent %s to %s (%s)' % (player_sent, item, player_recvd, get_location_name_from_address(location))) + location_name = get_location_name_from_address(ctx, location) + logging.info('%s sent %s to %s (%s)' % (player_sent, item, player_recvd, location_name)) if cmd == 'Print': logging.info(args) @@ -779,10 +793,10 @@ async def console_loop(ctx : Context): for index, item in enumerate(ctx.items_received, 1): logging.info('%s from %s (%s) (%d/%d in list)' % ( color(get_item_name_from_id(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), - get_location_name_from_address(item.location), index, len(ctx.items_received))) + get_location_name_from_address(ctx, item.location), index, len(ctx.items_received))) if command[0] == '/missing': - for location in [k for k, v in Regions.lookup_name_to_id.items() + for location in [k for k, v in ctx.lookup_name_to_id.items() if type(v) is int and not filter_location(ctx, k)]: if location not in ctx.locations_checked: logging.info('Missing: ' + location) @@ -804,15 +818,19 @@ def get_item_name_from_id(code): return items[0] if items else f'Unknown item (ID:{code})' -def get_location_name_from_address(address): +def get_location_name_from_address(ctx, address): if type(address) is str: return address - return Regions.lookup_id_to_name.get(address, f'Unknown location (ID:{address})') + return ctx.lookup_id_to_name.get(address, f'Unknown location (ID:{address})') def filter_location(ctx, location): - if not ctx.key_drop_mode and ('Key Drop' in location or 'Pot Key' in location): + if (not ctx.key_drop_mode and location in PotShuffle.key_drop_data + and PotShuffle.key_drop_data[location][0] == 'Drop'): + return True + if (not ctx.pottery_mode and location in PotShuffle.key_drop_data + and PotShuffle.key_drop_data[location][0] == 'Pot'): return True if not ctx.shop_mode and location in Regions.flat_normal_shops: return True @@ -821,6 +839,31 @@ def filter_location(ctx, location): return False +def init_lookups(ctx): + ctx.lookup_id_to_name = {x: y for x, y in Regions.lookup_id_to_name.items()} + ctx.lookup_name_to_id = {x: y for x, y in Regions.lookup_name_to_id.items()} + for location, datum in PotShuffle.key_drop_data.items(): + type = datum[0] + if type == 'Drop': + location_id, super_tile, sprite_index = datum[1] + location_table_sprite_items[location] = (2 * super_tile, 0x8000 >> sprite_index) + ctx.lookup_name_to_id[location] = location_id + ctx.lookup_id_to_name[location_id] = location + for super_tile, pot_list in PotShuffle.vanilla_pots.items(): + for pot_index, pot in enumerate(pot_list): + if pot.item != PotItem.Hole: + if pot.item == PotItem.Key: + loc_name = next(loc for loc, datum in PotShuffle.key_drop_data.items() + if datum[1] == super_tile) + else: + descriptor = 'Large Block' if pot.flags & PotFlags.Block else f'Pot #{pot_index+1}' + loc_name = f'{pot.room} {descriptor}' + location_table_pot_items[loc_name] = (2 * super_tile, 0x8000 >> pot_index) + location_id = Regions.pot_address(pot_index, super_tile) + ctx.lookup_name_to_id[loc_name] = location_id + ctx.lookup_id_to_name[location_id] = loc_name + + async def track_locations(ctx : Context, roomid, roomdata): new_locations = [] @@ -832,9 +875,12 @@ async def track_locations(ctx : Context, roomid, roomdata): if ctx.mode_flags is None: flags = await snes_read(ctx, MODE_FLAGS, 1) + ctx.mode_flags = flags ctx.key_drop_mode = flags[0] & 0x1 ctx.shop_mode = flags[0] & 0x2 ctx.retro_mode = flags[0] & 0x4 + ctx.pottery_mode = flags[0] & 0x8 + ctx.mystery_mode = flags[0] & 0x10 def new_check(location): ctx.locations_checked.add(location) @@ -842,8 +888,9 @@ async def track_locations(ctx : Context, roomid, roomdata): if ignored: ctx.ignore_count += 1 else: - logging.info(f"New check: {location} ({len(ctx.locations_checked)-ctx.ignore_count}/{ctx.total_locations})") - new_locations.append(Regions.lookup_name_to_id[location]) + total = '???' if ctx.mystery_mode else ctx.total_locations + logging.info(f"New check: {location} ({len(ctx.locations_checked)-ctx.ignore_count}/{total})") + new_locations.append(ctx.lookup_name_to_id[location]) try: if ctx.shop_mode or ctx.retro_mode: @@ -908,6 +955,22 @@ async def track_locations(ctx : Context, roomid, roomdata): if misc_data[offset - 0x3c6] & mask != 0 and location not in ctx.locations_checked: new_check(location) + if not all([location in ctx.locations_checked for location in location_table_pot_items.keys()]): + pot_items_data = await snes_read(ctx, POT_ITEMS_SRAM_START, ITEM_SRAM_SIZE) + if pot_items_data is not None: + for location, (offset, mask) in location_table_pot_items.items(): + pot_value = pot_items_data[offset] | (pot_items_data[offset + 1] << 8) + if pot_value & mask != 0 and location not in ctx.locations_checked: + new_check(location) + + if not all([location in ctx.locations_checked for location in location_table_sprite_items.keys()]): + sprite_items_data = await snes_read(ctx, SPRITE_ITEMS_SRAM_START, ITEM_SRAM_SIZE) + if sprite_items_data is not None: + for location, (offset, mask) in location_table_sprite_items.items(): + sprite_value = sprite_items_data[offset] | (sprite_items_data[offset + 1] << 8) + if sprite_value & mask != 0 and location not in ctx.locations_checked: + new_check(location) + await send_msgs(ctx.socket, [['LocationChecks', new_locations]]) async def game_watcher(ctx : Context): @@ -955,7 +1018,7 @@ async def game_watcher(ctx : Context): item = ctx.items_received[recv_index] logging.info('Received %s from %s (%s) (%d/%d in list)' % ( color(get_item_name_from_id(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), - get_location_name_from_address(item.location), recv_index + 1, len(ctx.items_received))) + get_location_name_from_address(ctx, item.location), recv_index + 1, len(ctx.items_received))) recv_index += 1 snes_buffered_write(ctx, RECV_PROGRESS_ADDR, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF])) snes_buffered_write(ctx, RECV_ITEM_ADDR, bytes([item.item])) @@ -969,7 +1032,7 @@ async def game_watcher(ctx : Context): if scout_location > 0 and scout_location not in ctx.locations_scouted: ctx.locations_scouted.add(scout_location) - logging.info(f'Scouting item at {list(Regions.lookup_id_to_name.keys())[scout_location - 1]}') + logging.info(f'Scouting item at {list(ctx.lookup_id_to_name.keys())[scout_location - 1]}') await send_msgs(ctx.socket, [['LocationScouts', [scout_location]]]) await track_locations(ctx, roomid, roomdata) @@ -984,6 +1047,7 @@ async def main(): logging.basicConfig(format='%(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO)) ctx = Context(args.snes, args.connect, args.password) + init_lookups(ctx) input_task = asyncio.create_task(console_loop(ctx)) diff --git a/MultiServer.py b/MultiServer.py index cd333edf..b15390dd 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -10,8 +10,10 @@ import urllib.request import websockets import zlib +from BaseClasses import PotItem, PotFlags import Items import Regions +import PotShuffle from MultiClient import ReceivedItem, get_item_name_from_id, get_location_name_from_address class Client: @@ -40,6 +42,9 @@ class Context: self.clients = [] self.received_items = {} + self.lookup_name_to_id = {} + self.lookup_id_to_name = {} + async def send_msgs(websocket, msgs): if not websocket or not websocket.open or websocket.closed: return @@ -176,7 +181,8 @@ def register_location_checks(ctx : Context, team, slot, locations): recvd_items.append(new_item) if slot != target_player: broadcast_team(ctx, team, [['ItemSent', (slot, location, target_player, target_item)]]) - logging.info('(Team #%d) %s sent %s to %s (%s)' % (team+1, ctx.player_names[(team, slot)], get_item_name_from_id(target_item), ctx.player_names[(team, target_player)], get_location_name_from_address(location))) + loc_name = get_location_name_from_address(ctx, location) + logging.info('(Team #%d) %s sent %s to %s (%s)' % (team+1, ctx.player_names[(team, slot)], get_item_name_from_id(target_item), ctx.player_names[(team, target_player)], loc_name)) found_items = True send_new_items(ctx) @@ -249,11 +255,11 @@ async def process_client_cmd(ctx : Context, client : Client, cmd, args): return locs = [] for location in args: - if type(location) is not int or 0 >= location > len(Regions.lookup_id_to_name.keys()): + if type(location) is not int or 0 >= location > len(ctx.lookup_id_to_name.keys()): await send_msgs(client.socket, [['InvalidArguments', 'LocationScouts']]) return - loc_name = list(Regions.lookup_id_to_name.keys())[location - 1] - target_item, target_player = ctx.locations[(Regions.lookup_name_to_id[loc_name], client.slot)] + loc_name = list(ctx.lookup_id_to_name.keys())[location - 1] + target_item, target_player = ctx.locations[(ctx.lookup_name_to_id[loc_name], client.slot)] replacements = {'SmallKey': 0xA2, 'BigKey': 0x9D, 'Compass': 0x8D, 'Map': 0x7D} item_type = [i[2] for i in Items.item_table.values() if type(i[3]) is int and i[3] == target_item] @@ -339,6 +345,30 @@ async def console(ctx : Context): if command[0][0] != '/': notify_all(ctx, '[Server]: ' + input) + +def init_lookups(ctx): + ctx.lookup_id_to_name = {x: y for x, y in Regions.lookup_id_to_name.items()} + ctx.lookup_name_to_id = {x: y for x, y in Regions.lookup_name_to_id.items()} + for location, datum in PotShuffle.key_drop_data.items(): + type = datum[0] + if type == 'Drop': + location_id = datum[1][0] + ctx.lookup_name_to_id[location] = location_id + ctx.lookup_id_to_name[location_id] = location + for super_tile, pot_list in PotShuffle.vanilla_pots.items(): + for pot_index, pot in enumerate(pot_list): + if pot.item != PotItem.Hole: + if pot.item == PotItem.Key: + loc_name = next(loc for loc, datum in PotShuffle.key_drop_data.items() + if datum[1] == super_tile) + else: + descriptor = 'Large Block' if pot.flags & PotFlags.Block else f'Pot #{pot_index+1}' + loc_name = f'{pot.room} {descriptor}' + location_id = Regions.pot_address(pot_index, super_tile) + ctx.lookup_name_to_id[loc_name] = location_id + ctx.lookup_id_to_name[location_id] = loc_name + + async def main(): parser = argparse.ArgumentParser() parser.add_argument('--host', default=None) @@ -353,7 +383,7 @@ async def main(): logging.basicConfig(format='[%(asctime)s] %(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO)) ctx = Context(args.host, args.port, args.password) - + init_lookups(ctx) ctx.data_filename = args.multidata try: diff --git a/Mystery.py b/Mystery.py index 7b509cb9..e798c6da 100644 --- a/Mystery.py +++ b/Mystery.py @@ -173,7 +173,9 @@ def roll_settings(weights): ret.shufflelinks = get_choice('shufflelinks') == 'on' ret.pseudoboots = get_choice('pseudoboots') == 'on' ret.shopsanity = get_choice('shopsanity') == 'on' - ret.keydropshuffle = get_choice('keydropshuffle') + ret.dropshuffle = get_choice('dropshuffle') == 'on' + ret.pottery = get_choice('pottery') if 'pottery' in weights else 'none' + ret.shuffleswitches = get_choice('shuffleswitches') == 'on' ret.mixed_travel = get_choice('mixed_travel') if 'mixed_travel' in weights else 'prevent' ret.standardize_palettes = get_choice('standardize_palettes') if 'standardize_palettes' in weights else 'standardize' @@ -243,8 +245,6 @@ def roll_settings(weights): ret.enemy_health = get_choice('enemy_health') - ret.shufflepots = get_choice('pot_shuffle') == 'on' - ret.beemizer = get_choice('beemizer') if 'beemizer' in weights else '0' inventoryweights = weights.get('startinventory', {}) diff --git a/PotShuffle.py b/PotShuffle.py index 0da3c237..1d73c519 100644 --- a/PotShuffle.py +++ b/PotShuffle.py @@ -91,7 +91,7 @@ vanilla_pots = { 0x3F: [Pot(12, 25, PotItem.OneRupee, 'Ice Hammer Block'), Pot(20, 25, PotItem.OneRupee, 'Ice Hammer Block'), Pot(12, 26, PotItem.Bomb, 'Ice Hammer Block'), Pot(20, 26, PotItem.Bomb, 'Ice Hammer Block'), Pot(12, 27, PotItem.Switch, 'Ice Hammer Block'), Pot(20, 27, PotItem.Heart, 'Ice Hammer Block'), - Pot(28, 23, PotItem.Key, 'Ice Hammer Block')], + Pot(28, 23, PotItem.Key, 'Ice Hammer Block', PotFlags.Block)], 0x41: [Pot(100, 10, PotItem.Heart, 'Sewers Behind Tapestry'), Pot(52, 15, PotItem.OneRupee, 'Sewers Behind Tapestry'), Pot(52, 16, PotItem.SmallMagic, 'Sewers Behind Tapestry'), Pot(148, 22, PotItem.SmallMagic, 'Sewers Behind Tapestry')], 0x43: [Pot(66, 4, PotItem.FiveArrows, 'Desert Wall Slide'), Pot(78, 4, PotItem.SmallMagic, 'Desert Wall Slide'), Pot(66, 9, PotItem.Heart, 'Desert Wall Slide'), Pot(78, 9, PotItem.Heart, 'Desert Wall Slide'), Pot(112, 28, PotItem.Nothing, 'Desert Tiles 2'), Pot(76, 28, PotItem.Nothing, 'Desert Tiles 2'), Pot(76, 20, PotItem.Nothing, 'Desert Tiles 2'), Pot(112, 20, PotItem.Key, 'Desert Tiles 2')], @@ -467,45 +467,46 @@ def shuffle_pot_switches(world, player): key_drop_data = { - 'Hyrule Castle - Map Guard Key Drop': ['Drop', (0x09E20C, 0x72), 'in Hyrule Castle', 'Small Key (Escape)'], - 'Hyrule Castle - Boomerang Guard Key Drop': ['Drop', (0x09E204, 0x71), 'in Hyrule Castle', 'Small Key (Escape)'], - 'Hyrule Castle - Key Rat Key Drop': ['Drop', (0x09DB80, 0x21), 'in Hyrule Castle', 'Small Key (Escape)'], - 'Hyrule Castle - Big Key Drop': ['Drop', (0x09E327, 0x80), 'in Hyrule Castle', 'Big Key (Escape)'], - 'Eastern Palace - Dark Square Pot Key': ['Pot', 0xBA, 'in Eastern Palace', 'Small Key (Eastern Palace)'], - 'Eastern Palace - Dark Eyegore Key Drop': ['Drop', (0x09E4F8, 0x99), 'in Eastern Palace', 'Small Key (Eastern Palace)'], - 'Desert Palace - Desert Tiles 1 Pot Key': ['Pot', 0x63, 'in Desert Palace', 'Small Key (Desert Palace)'], - 'Desert Palace - Beamos Hall Pot Key': ['Pot', 0x53, 'in Desert Palace', 'Small Key (Desert Palace)'], - 'Desert Palace - Desert Tiles 2 Pot Key': ['Pot', 0x43, 'in Desert Palace', 'Small Key (Desert Palace)'], - 'Castle Tower - Dark Archer Key Drop': ['Drop', (0x09E7C9, 0xC0), 'in Castle Tower', 'Small Key (Agahnims Tower)'], - 'Castle Tower - Circle of Pots Key Drop': ['Drop', (0x09E688, 0xB0), 'in Castle Tower', 'Small Key (Agahnims Tower)'], - 'Swamp Palace - Pot Row Pot Key': ['Pot', 0x38, 'in Swamp Palace', 'Small Key (Swamp Palace)'], - 'Swamp Palace - Trench 1 Pot Key': ['Pot', 0x37, 'in Swamp Palace', 'Small Key (Swamp Palace)'], - 'Swamp Palace - Hookshot Pot Key': ['Pot', 0x36, 'in Swamp Palace', 'Small Key (Swamp Palace)'], - 'Swamp Palace - Trench 2 Pot Key': ['Pot', 0x35, 'in Swamp Palace', 'Small Key (Swamp Palace)'], - 'Swamp Palace - Waterway Pot Key': ['Pot', 0x16, 'in Swamp Palace', 'Small Key (Swamp Palace)'], - 'Skull Woods - West Lobby Pot Key': ['Pot', 0x56, 'in Skull Woods', 'Small Key (Skull Woods)'], - 'Skull Woods - Spike Corner Key Drop': ['Drop', (0x09DD74, 0x39), 'near Mothula', 'Small Key (Skull Woods)'], - "Thieves' Town - Hallway Pot Key": ['Pot', 0xBC, "in Thieves' Town", 'Small Key (Thieves Town)'], - "Thieves' Town - Spike Switch Pot Key": ['Pot', 0xAB, "in Thieves' Town", 'Small Key (Thieves Town)'], - 'Ice Palace - Jelly Key Drop': ['Drop', (0x09DA21, 0xE), 'in Ice Palace', 'Small Key (Ice Palace)'], - 'Ice Palace - Conveyor Key Drop': ['Drop', (0x09DE08, 0x3E), 'in Ice Palace', 'Small Key (Ice Palace)'], - 'Ice Palace - Hammer Block Key Drop': ['Pot', 0x3F, 'in Ice Palace', 'Small Key (Ice Palace)'], - 'Ice Palace - Many Pots Pot Key': ['Pot', 0x9F, 'in Ice Palace', 'Small Key (Ice Palace)'], - 'Misery Mire - Spikes Pot Key': ['Pot', 0xB3, 'in Misery Mire', 'Small Key (Misery Mire)'], - 'Misery Mire - Fishbone Pot Key': ['Pot', 0xA1, 'in forgotten Mire', 'Small Key (Misery Mire)'], - 'Misery Mire - Conveyor Crystal Key Drop': ['Drop', (0x09E7FB, 0xC1), 'in Misery Mire', 'Small Key (Misery Mire)'], - 'Turtle Rock - Pokey 1 Key Drop': ['Drop', (0x09E70D, 0xB6), 'in Turtle Rock', 'Small Key (Turtle Rock)'], - 'Turtle Rock - Pokey 2 Key Drop': ['Drop', (0x09DA5D, 0x13), 'in Turtle Rock', 'Small Key (Turtle Rock)'], - 'Ganons Tower - Conveyor Cross Pot Key': ['Pot', 0x8B, "in Ganon's Tower", 'Small Key (Ganons Tower)'], - 'Ganons Tower - Double Switch Pot Key': ['Pot', 0x9B, "in Ganon's Tower", 'Small Key (Ganons Tower)'], - 'Ganons Tower - Conveyor Star Pits Pot Key': ['Pot', 0x7B, "in Ganon's Tower", 'Small Key (Ganons Tower)'], - 'Ganons Tower - Mini Helmasaur Key Drop': ['Drop', (0x09DDC4, 0x3D), "atop Ganon's Tower", 'Small Key (Ganons Tower)'] + 'Hyrule Castle - Map Guard Key Drop': ['Drop', (0x09E20C, 0x72, 0), 'dropped in Hyrule Castle', 'Small Key (Escape)'], + 'Hyrule Castle - Boomerang Guard Key Drop': ['Drop', (0x09E204, 0x71, 1), 'dropped in Hyrule Castle', 'Small Key (Escape)'], + 'Hyrule Castle - Key Rat Key Drop': ['Drop', (0x09DB80, 0x21, 0), 'dropped in Hyrule Castle', 'Small Key (Escape)'], + 'Hyrule Castle - Big Key Drop': ['Drop', (0x09E327, 0x80, 2), 'dropped in Hyrule Castle', 'Big Key (Escape)'], + 'Eastern Palace - Dark Square Pot Key': ['Pot', 0xBA, 'in a pot in Eastern Palace', 'Small Key (Eastern Palace)'], + 'Eastern Palace - Dark Eyegore Key Drop': ['Drop', (0x09E4F8, 0x99, 3), 'dropped in Eastern Palace', 'Small Key (Eastern Palace)'], + 'Desert Palace - Desert Tiles 1 Pot Key': ['Pot', 0x63, 'in a pot in Desert Palace', 'Small Key (Desert Palace)'], + 'Desert Palace - Beamos Hall Pot Key': ['Pot', 0x53, 'in a pot in Desert Palace', 'Small Key (Desert Palace)'], + 'Desert Palace - Desert Tiles 2 Pot Key': ['Pot', 0x43, 'in a pot in Desert Palace', 'Small Key (Desert Palace)'], + 'Castle Tower - Dark Archer Key Drop': ['Drop', (0x09E7C9, 0xC0, 3), 'dropped in Castle Tower', 'Small Key (Agahnims Tower)'], + 'Castle Tower - Circle of Pots Key Drop': ['Drop', (0x09E688, 0xB0, 10), 'dropped in Castle Tower', 'Small Key (Agahnims Tower)'], + 'Swamp Palace - Pot Row Pot Key': ['Pot', 0x38, 'in a pot in Swamp Palace', 'Small Key (Swamp Palace)'], + 'Swamp Palace - Trench 1 Pot Key': ['Pot', 0x37, 'in a pot in Swamp Palace', 'Small Key (Swamp Palace)'], + 'Swamp Palace - Hookshot Pot Key': ['Pot', 0x36, 'in a pot in Swamp Palace', 'Small Key (Swamp Palace)'], + 'Swamp Palace - Trench 2 Pot Key': ['Pot', 0x35, 'in a pot in Swamp Palace', 'Small Key (Swamp Palace)'], + '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)'], + '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)'], + 'Ice Palace - Many Pots Pot Key': ['Pot', 0x9F, 'int a pot in Ice Palace', 'Small Key (Ice Palace)'], + 'Misery Mire - Spikes Pot Key': ['Pot', 0xB3, 'in a pot in Misery Mire', 'Small Key (Misery Mire)'], + 'Misery Mire - Fishbone Pot Key': ['Pot', 0xA1, 'in a pot in forgotten Mire', 'Small Key (Misery Mire)'], + 'Misery Mire - Conveyor Crystal Key Drop': ['Drop', (0x09E7FB, 0xC1, 9), 'dropped in Misery Mire', 'Small Key (Misery Mire)'], + 'Turtle Rock - Pokey 1 Key Drop': ['Drop', (0x09E70D, 0xB6, 5), 'dropped in Turtle Rock', 'Small Key (Turtle Rock)'], + 'Turtle Rock - Pokey 2 Key Drop': ['Drop', (0x09DA5D, 0x13, 6), 'dropped in Turtle Rock', 'Small Key (Turtle Rock)'], + 'Ganons Tower - Conveyor Cross Pot Key': ['Pot', 0x8B, "in a pot in Ganon's Tower", 'Small Key (Ganons Tower)'], + 'Ganons Tower - Double Switch Pot Key': ['Pot', 0x9B, "in a pot in Ganon's Tower", 'Small Key (Ganons Tower)'], + 'Ganons Tower - Conveyor Star Pits Pot Key': ['Pot', 0x7B, "in a pot in Ganon's Tower", 'Small Key (Ganons Tower)'], + 'Ganons Tower - Mini Helmasaur Key Drop': ['Drop', (0x09DDC4, 0x3D, 2), "dropped atop Ganon's Tower", 'Small Key (Ganons Tower)'] } class PotSecretTable(object): def __init__(self): self.room_map = defaultdict(list) + self.multiworld_count = 0 def write_pot_data_to_rom(self, rom): pointer_address = 0x140000 # pots currently in bank 28 diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8357f597..5c36c71b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,38 @@ ## New Features +## Pottery Lottery and Key Drop Shuffle Changes + +### Pottery + +New pottery option that control which pots are in the locations pool: + +* None: No pots are in the pool, like normal randomizer +* Key Pots: The pots that have keys are in the pool. This is about half of the old keydropshuffle option +* Lottery: All pots and large blocks are in the pool + +By default, switches remain in their vanilla location (unless you turn on the legacy option below) + +CLI `--pottery