Re-worked settings for pottery lottery

Fixed multiworld support for lottery
This commit is contained in:
aerinon
2022-01-21 16:10:33 -07:00
parent d6e74c638f
commit 5211b305e6
22 changed files with 472 additions and 220 deletions

View File

@@ -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):

16
CLI.py
View File

@@ -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,

View File

@@ -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,6 +1260,12 @@ 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':
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]
@@ -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:

37
Fill.py
View File

@@ -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)
for loc in world.get_locations():
# 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)
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()

View File

@@ -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']),

View File

@@ -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)

11
Main.py
View File

@@ -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()

View File

@@ -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))

View File

@@ -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:

View File

@@ -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', {})

View File

@@ -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

View File

@@ -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 <option>` from `none, keys, lottery`
Note for multiworld: due to the design of the pottery lottery, only 256 items for other players can be under pots in your world.
### Shuffle key drops
Enemies that drop keys can have their drop shuffled into the pool. This is the other half of the keydropshuffle option.
CLI `--dropshuffle`
#### Legacy options
"Drop and Pot Keys" or `--keydropshuffle` is still availabe for use. This simply sets the pottery to keys and turns dropshuffle on as well to have the same behavior as the
The old "Pot Shuffle" option is available under "Shuffle Pot Switches" or `--shuffleswitches` and works the same by shuffling all pots on a supertile. It works with the lottery option as well to move the switches while having every pot in the pool. I have no justification for the name change on the CLI/Mystery, it just kind of happened and I was too lazy to change it back.
#### Tracking Notes
The sram locations for pots and sprite drops have been moved, please reach out for assistance or investigate the rom changes.
## Restricted Item Placement Algorithm

View File

@@ -58,15 +58,15 @@ def create_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']),
@@ -993,16 +993,19 @@ def adjust_locations(world, player):
world.pot_contents[player] = PotSecretTable()
for location, datum in key_drop_data.items():
loc = world.get_location(location, player)
if 'Drop' == datum[0]:
drop_location = 'Drop' == datum[0]
if drop_location:
loc.type = LocationType.Drop
snes_address, room = datum[1]
snes_address, room, sprite_idx = datum[1]
loc.address = snes_address
else:
loc.type = LocationType.Pot
pot = next(p for p in vanilla_pots[datum[1]] if p.item == PotItem.Key).copy()
pot, pot_index = next((p, i) for i, p in enumerate(vanilla_pots[datum[1]]) if p.item == PotItem.Key)
pot = pot.copy()
loc.address = pot_address(pot_index, datum[1])
loc.pot = pot
world.pot_contents[player].room_map[datum[1]].append(pot)
if world.keydropshuffle[player] == 'none':
if (not world.dropshuffle[player] and drop_location)\
or (not drop_location and world.pottery[player] == 'none'):
loc.skip = True
else:
key_item = loc.item
@@ -1019,23 +1022,14 @@ def adjust_locations(world, player):
dungeon.big_key = key_item
for super_tile, pot_list in vanilla_pots.items():
for pot_index, pot_orig in enumerate(pot_list):
if pot_orig.item == PotItem.Key:
loc = next(location for location, datum in key_drop_data.items() if datum[1] == super_tile)
pot = world.get_location(loc, player).pot
else:
pot = pot_orig.copy()
if pot.item != PotItem.Key:
world.pot_contents[player].room_map[super_tile].append(pot)
if world.keydropshuffle[player] == 'potsanity':
if (pot.item not in [PotItem.Key, PotItem.Hole]
and (pot.item != PotItem.Switch or world.potshuffle[player])):
parent = world.get_region(pot.room, player)
descriptor = 'Large Block' if pot.flags & PotFlags.Block else f'Pot #{pot_index+1}'
# todo: better hints
hint_text = 'under a block' if pot.flags & PotFlags.Block else 'in a pot'
pot_location = Location(player, f'{pot.room} {descriptor}', player, hint_text=hint_text,
parent=parent)
world.dynamic_locations.append(pot_location)
pot_location.pot = pot
pot_location.type = LocationType.Pot
parent.locations.append(pot_location)
if world.pottery[player] == 'lottery':
create_pot_location(pot, pot_index, super_tile, world, player)
if world.shopsanity[player]:
index = 0
for shop, location_list in shop_to_location_table.items():
@@ -1057,6 +1051,28 @@ def adjust_locations(world, player):
location.skip = True
def create_pot_location(pot, pot_index, super_tile, world, player):
if (pot.item not in [PotItem.Key, PotItem.Hole]
and (pot.item != PotItem.Switch or world.potshuffle[player])):
address = pot_address(pot_index, super_tile)
parent = world.get_region(pot.room, player)
descriptor = 'Large Block' if pot.flags & PotFlags.Block else f'Pot #{pot_index+1}'
hint_text = ('under a block' if pot.flags & PotFlags.Block else 'in a pot')
modifier = parent.hint_text not in {'a storyteller', 'fairies deep in a cave', 'a spiky hint',
'a bounty of five items', 'the sick kid', 'Sahasrahla'}
hint_text = f'{hint_text} {"in" if modifier else "near"} {parent.hint_text}'
pot_location = Location(player, f'{pot.room} {descriptor}', address, hint_text=hint_text,
parent=parent)
world.dynamic_locations.append(pot_location)
pot_location.pot = pot
pot_location.type = LocationType.Pot
parent.locations.append(pot_location)
def pot_address(pot_index, super_tile):
return 0x7f6600 + super_tile * 2 + (pot_index << 24)
# (type, room_id, shopkeeper, custom, locked, [items])
# item = (item, price, max=0, replacement=None, replacement_price=0)
@@ -1401,8 +1417,6 @@ location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'),
}
lookup_id_to_name = {data[0]: name for name, data in location_table.items() if type(data[0]) == int}
lookup_id_to_name = {**lookup_id_to_name, **{data[1]: name for name, data in key_drop_data.items()}}
lookup_id_to_name.update(shop_table_by_location_id)
lookup_name_to_id = {name: data[0] for name, data in location_table.items() if type(data[0]) == int}
lookup_name_to_id = {**lookup_name_to_id, **{name: data[1] for name, data in key_drop_data.items()}}
lookup_name_to_id.update(shop_table_by_location)

37
Rom.py
View File

@@ -35,7 +35,7 @@ from source.item.FillUtil import valid_pot_items
JAP10HASH = '03a63945398191337e896e5771f77173'
RANDOMIZERBASEHASH = '8d13470d5a3127c6705846674cfb6209'
RANDOMIZERBASEHASH = 'dea3a3bc1435aa0895181c5d46dc6d43'
class JsonRom(object):
@@ -554,7 +554,7 @@ class Sprite(object):
def handle_native_dungeon(location, itemid):
# Keys in their native dungeon should use the original item code for keys
if location.parent_region.dungeon:
if location.parent_region.dungeon.is_dungeon_item(location.item):
if location.parent_region.dungeon.name == location.item.dungeon:
if location.item.bigkey:
return 0x32
if location.item.smallkey:
@@ -604,7 +604,6 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
else:
code = 0xF8
sprite_pointer = snes_to_pc(location.address)
if code != 0xE4:
rom.write_byte(sprite_pointer, handle_native_dungeon(location, itemid))
if code == 0xF9:
rom.write_byte(sprite_pointer+1, location.item.player)
@@ -650,6 +649,13 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
if world.pot_contents[player].size() > 0x2800:
raise Exception('Pot table is too big for current area')
world.pot_contents[player].write_pot_data_to_rom(rom)
# fix for swamp drains if necessary
swamp1location = world.get_location('Swamp Palace - Trench 1 Pot Key', player)
if not swamp1location.pot.indicator:
rom.write_byte(0x142A53, 0)
swamp2location = world.get_location('Swamp Palace - Trench 2 Pot Key', player)
if not swamp2location.pot.indicator:
rom.write_byte(0x142A54, 0)
# patch entrance/exits/holes
for region in world.regions:
@@ -740,7 +746,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
valid_loc_by_dungeon = valid_dungeon_locations(valid_locations)
# fix hc big key problems (map and compass too)
if world.doorShuffle[player] == 'crossed' or world.keydropshuffle[player] != 'none':
if world.doorShuffle[player] == 'crossed' or world.dropshuffle[player]:
rom.write_byte(0x151f1, 2)
rom.write_byte(0x15270, 2)
sanctuary = world.get_region('Sanctuary', player)
@@ -868,13 +874,14 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
credits_total = len(valid_locations)
if world.keydropshuffle[player] != 'none':
if world.dropshuffle[player] or world.pottery[player] != 'none':
rom.write_byte(0x142A50, 1)
rom.write_byte(0x142A51, 1)
if world.keydropshuffle[player] == 'potsanity':
rom.write_bytes(0x04DFD8, [0x18, 0x0B, 0x1C])
rom.write_byte(0x04E002, 0xFF)
# write_int16(0x142A52, credits_total - 216) # todo: this is now a delta
multiClientFlags = ((0x1 if world.dropshuffle[player] else 0)
| (0x2 if world.shopsanity[player] else 0)
| (0x4 if world.retro[player] else 0)
| (0x8 if world.pottery[player] in ['keys', 'lottery'] else 0)
| (0x10 if is_mystery else 0))
rom.write_byte(0x142A51, multiClientFlags)
write_int16(rom, 0x187010, credits_total) # dynamic credits
if credits_total != 216:
@@ -1202,7 +1209,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
rom.write_bytes(0x180213, [0x00, 0x01]) # Not a Tournament Seed
gametype = 0x04 # item
if world.shuffle[player] != 'vanilla' or world.doorShuffle[player] != 'vanilla' or world.keydropshuffle[player] != 'none':
if (world.shuffle[player] != 'vanilla' or world.doorShuffle[player] != 'vanilla' or world.dropshuffle[player]
or world.pottery[player] != 'none'):
gametype |= 0x02 # entrance/door
if enemized:
gametype |= 0x01 # enemizer
@@ -1452,8 +1460,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
rom.write_byte(0x18003C, 0x00)
elif world.dungeon_counters[player] == 'on':
compass_mode = 0x02 # always on
elif (world.compassshuffle[player] or world.doorShuffle[player] != 'vanilla'
or world.dungeon_counters[player] == 'pickup' or world.keydropshuffle[player] != 'none'):
elif (world.compassshuffle[player] or world.doorShuffle[player] != 'vanilla' or world.dropshuffle[player]
or world.dungeon_counters[player] == 'pickup' or world.pottery[player] != 'none'):
compass_mode = 0x01 # show on pickup
if world.shuffle[player] != 'vanilla' and world.overworld_map[player] != 'default':
compass_mode |= 0x80 # turn on locating dungeons
@@ -1628,7 +1636,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
rom.write_byte(0xFED31, 0x0E) # preopen bombable exit
rom.write_byte(0xFEE41, 0x0E) # preopen bombable exit
if world.doorShuffle[player] != 'vanilla' or world.keydropshuffle[player] != 'none':
if (world.doorShuffle[player] != 'vanilla' or world.dropshuffle[player]
or world.pottery[player] != 'none'):
for room in world.rooms:
if room.player == player and room.modified:
rom.write_bytes(room.address(), room.rom_data())

View File

@@ -711,7 +711,7 @@ def bomb_rules(world, player):
def pot_rules(world, player):
if world.keydropshuffle[player] == 'potsanity':
if world.pottery[player] == 'lottery':
blocks = [l for l in world.get_locations() if l.type == LocationType.Pot and l.pot.flags & PotFlags.Block]
for block_pot in blocks:
add_rule(block_pot, lambda state: state.can_lift_rocks(player))

Binary file not shown.

View File

@@ -65,12 +65,24 @@
"type": "bool"
},
"keydropshuffle" : {
"choices": [
"action": "store_true",
"type": "bool"
},
"dropshuffle" : {
"action": "store_true",
"type": "bool"
},
"pottery" : {
"choices" : [
"none",
"keydrops",
"potsanity"
"keys",
"lottery"
]
},
"shufflepots": {
"action": "store_true",
"type": "bool"
},
"mixed_travel" : {
"choices": [
"prevent",
@@ -388,10 +400,6 @@
"random"
]
},
"shufflepots": {
"action": "store_true",
"type": "bool"
},
"remote_items": {
"action": "store_true",
"type": "bool"

View File

@@ -254,11 +254,13 @@
"keyshuffle": [ "Small Keys are no longer restricted to their dungeons, but can be anywhere. (default: %(default)s)" ],
"bigkeyshuffle": [ "Big Keys are no longer restricted to their dungeons, but can be anywhere. (default: %(default)s)" ],
"shopsanity": ["Shop contents are shuffle in the main item pool and other items can take their place. (default: %(default)s)"],
"keydropshuffle": [ "Key Drops (Pots and Enemies) are shuffled and other items can take their place:",
"None: No pots or enemies are included",
"Keydrops: Only key drops are included",
"Potsanity: All key drops and all normal pots are included"
"dropshuffle": [ "Keys dropped by enemies are shuffled and other items can take their place. (default: %(default)s)"],
"pottery": [ "Controls how items under pots are shuffled and if other items can take their place:",
"None: No pots are changed",
"Keys: Key pots are included in the location pool and other items can take their place",
"Lottery: All pots are part of the location pool"
],
"shufflepots": [ "Pots and switches are shuffled on the supertile (legacy potshuffle) (default: %(default)s)"],
"mixed_travel": [
"How to handle potential traversal between dungeon in Crossed door shuffle",
"Prevent: Rails are placed to prevent bombs jump and hovering from changing dungeon except with glitched logic settings",

View File

@@ -53,10 +53,13 @@
"randomizer.dungeon.compassshuffle": "Compasses",
"randomizer.dungeon.smallkeyshuffle": "Small Keys",
"randomizer.dungeon.bigkeyshuffle": "Big Keys",
"randomizer.dungeon.keydropshuffle": "Drops and Pots",
"randomizer.dungeon.keydropshuffle.none": "None",
"randomizer.dungeon.keydropshuffle.keydrops": "Only Key Drops",
"randomizer.dungeon.keydropshuffle.potsanity": "All Pots and Key Drops",
"randomizer.dungeon.keydropshuffle": "Drop and Pot Keys",
"randomizer.dungeon.dropshuffle": "Shuffle Key Drops",
"randomizer.dungeon.potshuffle": "Pot Shuffle (Legacy)",
"randomizer.dungeon.pottery": "Pottery",
"randomizer.dungeon.pottery.none": "None",
"randomizer.dungeon.pottery.keys": "Key Pots",
"randomizer.dungeon.pottery.lottery": "Lottery (All Pots and Large Blocks)",
"randomizer.dungeon.dungeondoorshuffle": "Dungeon Door Shuffle",
"randomizer.dungeon.dungeondoorshuffle.vanilla": "Vanilla",
@@ -69,7 +72,6 @@
"randomizer.dungeon.dungeonintensity.3": "3: Dungeon Lobbies",
"randomizer.dungeon.dungeonintensity.random": "Random",
"randomizer.dungeon.potshuffle": "Pot Shuffle",
"randomizer.dungeon.experimental": "Enable Experimental Features",
"randomizer.dungeon.dungeon_counters": "Dungeon Chest Counters",

View File

@@ -22,15 +22,20 @@
"width": 45
}
},
"keydropshuffle": {
"keydropshuffle": { "type": "checkbox" },
"pottery": {
"type": "selectbox",
"default": "none",
"options": [
"none",
"keydrops",
"potsanity"
]
"keys",
"lottery"
],
"config": {
"width": 35
}
},
"dropshuffle": { "type": "checkbox" },
"potshuffle": { "type": "checkbox" },
"experimental": { "type": "checkbox" },
"dungeon_counters": {

View File

@@ -93,9 +93,11 @@ SETTINGSTOPROCESS = {
"compassshuffle": "compassshuffle",
"smallkeyshuffle": "keyshuffle",
"bigkeyshuffle": "bigkeyshuffle",
"keydropshuffle": "keydropshuffle",
"dungeondoorshuffle": "door_shuffle",
"dungeonintensity": "intensity",
"keydropshuffle": "keydropshuffle",
"dropshuffle": "dropshuffle",
"pottery": "pottery",
"potshuffle": "shufflepots",
"experimental": "experimental",
"dungeon_counters": "dungeon_counters",

View File

@@ -65,45 +65,40 @@ def create_item_pool_config(world):
config.static_placement = {}
config.location_groups = {}
for player in range(1, world.players + 1):
config.static_placement[player] = vanilla_mapping.copy()
if world.keydropshuffle[player] != 'none':
config.static_placement[player] = defaultdict(list)
config.static_placement[player].update(vanilla_mapping)
if world.dropshuffle[player]:
for item, locs in keydrop_vanilla_mapping.items():
if item in config.static_placement[player]:
config.static_placement[player][item].extend(locs)
else:
config.static_placement[player][item] = list(locs)
if world.keydropshuffle[player] == 'potsanity':
if world.pottery[player] != 'none':
for item, locs in potkeys_vanilla_mapping.items():
config.static_placement[player][item].extend(locs)
if world.pottery[player] == 'lottery':
for super_tile, pot_list in vanilla_pots.items():
for pot_index, pot in enumerate(pot_list):
if pot.item not in [PotItem.Key, PotItem.Hole, PotItem.Switch]:
item = pot_items[pot.item]
descriptor = 'Large Block' if pot.flags & PotFlags.Block else f'Pot #{pot_index+1}'
location = f'{pot.room} {descriptor}'
if item in config.static_placement[player]:
config.static_placement[player][item].append(location)
else:
config.static_placement[player][item] = list([location])
if world.shopsanity[player]:
for item, locs in shop_vanilla_mapping.items():
if item in config.static_placement[player]:
config.static_placement[player][item].extend(locs)
else:
config.static_placement[player][item] = list(locs)
if world.retro[player]:
for item, locs in retro_vanilla_mapping.items():
if item in config.static_placement[player]:
config.static_placement[player][item].extend(locs)
else:
config.static_placement[player][item] = list(locs)
# universal keys
universal_key_locations = []
for item, locs in vanilla_mapping.items():
if 'Small Key' in item:
universal_key_locations.extend(locs)
if world.keydropshuffle[player] != 'none':
if world.dropshuffle[player]:
for item, locs in keydrop_vanilla_mapping.items():
if 'Small Key' in item:
universal_key_locations.extend(locs)
if world.pottery[player] != 'none':
for item, locs in potkeys_vanilla_mapping.items():
universal_key_locations.extend(locs)
if world.shopsanity[player]:
single_arrow_placement = list(shop_vanilla_mapping['Red Potion'])
single_arrow_placement.append('Red Shield Shop - Right')
@@ -677,25 +672,31 @@ vanilla_mapping = {
keydrop_vanilla_mapping = {
'Small Key (Desert Palace)': ['Desert Palace - Desert Tiles 1 Pot Key',
'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key'],
'Small Key (Eastern Palace)': ['Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop'],
'Small Key (Eastern Palace)': ['Eastern Palace - Dark Eyegore Key Drop'],
'Small Key (Escape)': ['Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop',
'Hyrule Castle - Key Rat Key Drop'],
'Big Key (Escape)': ['Hyrule Castle - Big Key Drop'],
'Small Key (Agahnims Tower)': ['Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'],
'Small Key (Skull Woods)': ['Skull Woods - Spike Corner Key Drop'],
'Small Key (Ice Palace)': ['Ice Palace - Jelly Key Drop', 'Ice Palace - Conveyor Key Drop'],
'Small Key (Misery Mire)': ['Misery Mire - Conveyor Crystal Key Drop'],
'Small Key (Turtle Rock)': ['Turtle Rock - Pokey 1 Key Drop', 'Turtle Rock - Pokey 2 Key Drop'],
'Small Key (Ganons Tower)': ['Ganons Tower - Mini Helmasaur Key Drop'],
}
potkeys_vanilla_mapping = {
'Small Key (Desert Palace)': ['Desert Palace - Desert Tiles 1 Pot Key',
'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key'],
'Small Key (Eastern Palace)': ['Eastern Palace - Dark Square Pot Key'],
'Small Key (Thieves Town)': ["Thieves' Town - Hallway Pot Key", "Thieves' Town - Spike Switch Pot Key"],
'Small Key (Skull Woods)': ['Skull Woods - West Lobby Pot Key', 'Skull Woods - Spike Corner Key Drop'],
'Small Key (Skull Woods)': ['Skull Woods - West Lobby Pot Key'],
'Small Key (Swamp Palace)': ['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'],
'Small Key (Ice Palace)': ['Ice Palace - Jelly Key Drop', 'Ice Palace - Conveyor Key Drop',
'Ice Palace - Hammer Block Key Drop', 'Ice Palace - Many Pots Pot Key'],
'Small Key (Misery Mire)': ['Misery Mire - Spikes Pot Key',
'Misery Mire - Fishbone Pot Key', 'Misery Mire - Conveyor Crystal Key Drop'],
'Small Key (Turtle Rock)': ['Turtle Rock - Pokey 1 Key Drop', 'Turtle Rock - Pokey 2 Key Drop'],
'Small Key (Ice Palace)': ['Ice Palace - Hammer Block Key Drop', 'Ice Palace - Many Pots Pot Key'],
'Small Key (Misery Mire)': ['Misery Mire - Spikes Pot Key', 'Misery Mire - Fishbone Pot Key'],
'Small Key (Ganons Tower)': ['Ganons Tower - Conveyor Cross Pot Key', 'Ganons Tower - Double Switch Pot Key',
'Ganons Tower - Conveyor Star Pits Pot Key', 'Ganons Tower - Mini Helmasaur Key Drop'],
'Ganons Tower - Conveyor Star Pits Pot Key'],
}
shop_vanilla_mapping = {