Multiworld merge

This commit is contained in:
aerinon
2020-01-14 15:07:13 -07:00
54 changed files with 4319 additions and 2321 deletions
+3
View File
@@ -15,5 +15,8 @@ README.html
*multisave *multisave
EnemizerCLI/ EnemizerCLI/
.mypy_cache/ .mypy_cache/
RaceRom.py
weights/
venv venv
test test
+9 -4
View File
@@ -6,6 +6,7 @@ import textwrap
import sys import sys
from AdjusterMain import adjust from AdjusterMain import adjust
from Rom import get_sprite_from_name
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter): class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
@@ -15,7 +16,8 @@ class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
def main(): def main():
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument('--rom', default='ER_base.sfc', help='Path to an ALttP JAP(1.0) rom to use as a base.') parser.add_argument('--rom', default='ER_base.sfc', help='Path to an ALttPR rom to adjust.')
parser.add_argument('--baserom', default='Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', help='Path to an ALttP JAP(1.0) rom to use as a base.')
parser.add_argument('--loglevel', default='info', const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.') parser.add_argument('--loglevel', default='info', const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
parser.add_argument('--fastmenu', default='normal', const='normal', nargs='?', choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'], parser.add_argument('--fastmenu', default='normal', const='normal', nargs='?', choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
help='''\ help='''\
@@ -31,6 +33,8 @@ def main():
''') ''')
parser.add_argument('--heartcolor', default='red', const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow', 'random'], parser.add_argument('--heartcolor', default='red', const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow', 'random'],
help='Select the color of Link\'s heart meter. (default: %(default)s)') help='Select the color of Link\'s heart meter. (default: %(default)s)')
parser.add_argument('--ow_palettes', default='default', choices=['default', 'random', 'blackout'])
parser.add_argument('--uw_palettes', default='default', choices=['default', 'random', 'blackout'])
parser.add_argument('--sprite', help='''\ parser.add_argument('--sprite', help='''\
Path to a sprite sheet to use for Link. Needs to be in Path to a sprite sheet to use for Link. Needs to be in
binary format and have a length of 0x7000 (28672) bytes, binary format and have a length of 0x7000 (28672) bytes,
@@ -38,14 +42,15 @@ def main():
Alternatively, can be a ALttP Rom patched with a Link Alternatively, can be a ALttP Rom patched with a Link
sprite that will be extracted. sprite that will be extracted.
''') ''')
parser.add_argument('--names', default='', type=str)
args = parser.parse_args() args = parser.parse_args()
# ToDo: Validate files further than mere existance # ToDo: Validate files further than mere existance
if not os.path.isfile(args.rom): if not os.path.isfile(args.rom):
input('Could not find valid base rom for patching at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.rom) input('Could not find valid rom for patching at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.rom)
sys.exit(1) sys.exit(1)
if args.sprite is not None and not os.path.isfile(args.sprite): if args.sprite is not None and not os.path.isfile(args.sprite) and not get_sprite_from_name(args.sprite):
input('Could not find link sprite sheet at given location. \nPress Enter to exit.' % args.sprite) input('Could not find link sprite sheet at given location. \nPress Enter to exit.')
sys.exit(1) sys.exit(1)
# set up logger # set up logger
+6 -11
View File
@@ -2,8 +2,8 @@ import os
import time import time
import logging import logging
from Utils import output_path from Utils import output_path, parse_names_string
from Rom import LocalRom, Sprite, apply_rom_settings from Rom import LocalRom, apply_rom_settings
def adjust(args): def adjust(args):
@@ -11,22 +11,17 @@ def adjust(args):
logger = logging.getLogger('') logger = logging.getLogger('')
logger.info('Patching ROM.') logger.info('Patching ROM.')
if args.sprite is not None:
if isinstance(args.sprite, Sprite):
sprite = args.sprite
else:
sprite = Sprite(args.sprite)
else:
sprite = None
outfilebase = os.path.basename(args.rom)[:-4] + '_adjusted' outfilebase = os.path.basename(args.rom)[:-4] + '_adjusted'
if os.stat(args.rom).st_size in (0x200000, 0x400000) and os.path.splitext(args.rom)[-1].lower() == '.sfc': if os.stat(args.rom).st_size in (0x200000, 0x400000) and os.path.splitext(args.rom)[-1].lower() == '.sfc':
rom = LocalRom(args.rom, False) rom = LocalRom(args.rom, False)
if os.path.isfile(args.baserom):
baserom = LocalRom(args.baserom, True)
rom.orig_buffer = baserom.orig_buffer
else: else:
raise RuntimeError('Provided Rom is not a valid Link to the Past Randomizer Rom. Please provide one for adjusting.') raise RuntimeError('Provided Rom is not a valid Link to the Past Randomizer Rom. Please provide one for adjusting.')
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, sprite) apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, args.sprite, args.ow_palettes, args.uw_palettes, parse_names_string(args.names))
rom.write_to_file(output_path('%s.sfc' % outfilebase)) rom.write_to_file(output_path('%s.sfc' % outfilebase))
+160 -98
View File
@@ -12,18 +12,18 @@ from RoomData import Room
class World(object): class World(object):
def __init__(self, players, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments, timer, progressive, goal, algorithm, place_dungeon_items, accessibility, shuffle_ganon, quickswap, fastmenu, disable_music, keysanity, retro, custom, customitemarray, boss_shuffle, hints): def __init__(self, players, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments, timer, progressive, goal, algorithm, accessibility, shuffle_ganon, retro, custom, customitemarray, hints):
self.players = players self.players = players
self.shuffle = shuffle self.shuffle = shuffle.copy()
self.doorShuffle = doorShuffle self.doorShuffle = doorShuffle.copy()
self.logic = logic self.logic = logic.copy()
self.mode = mode self.mode = mode.copy()
self.swords = swords self.swords = swords.copy()
self.difficulty = difficulty self.difficulty = difficulty.copy()
self.difficulty_adjustments = difficulty_adjustments self.difficulty_adjustments = difficulty_adjustments.copy()
self.timer = timer self.timer = timer
self.progressive = progressive self.progressive = progressive
self.goal = goal self.goal = goal.copy()
self.algorithm = algorithm self.algorithm = algorithm
self.dungeons = [] self.dungeons = []
self.regions = [] self.regions = []
@@ -32,55 +32,30 @@ class World(object):
self.seed = None self.seed = None
self.precollected_items = [] self.precollected_items = []
self.state = CollectionState(self) self.state = CollectionState(self)
self.required_medallions = dict([(player, ['Ether', 'Quake']) for player in range(1, players + 1)])
self._cached_entrances = None self._cached_entrances = None
self._cached_locations = None self._cached_locations = None
self._entrance_cache = {} self._entrance_cache = {}
self._region_cache = {}
self._entrance_cache = {}
self._location_cache = {} self._location_cache = {}
self.required_locations = [] self.required_locations = []
self.place_dungeon_items = place_dungeon_items # configurable in future
self.shuffle_bonk_prizes = False self.shuffle_bonk_prizes = False
self.swamp_patch_required = {player: False for player in range(1, players + 1)}
self.powder_patch_required = {player: False for player in range(1, players + 1)}
self.ganon_at_pyramid = {player: True for player in range(1, players + 1)}
self.ganonstower_vanilla = {player: True for player in range(1, players + 1)}
self.sewer_light_cone = mode == 'standard'
self.light_world_light_cone = False self.light_world_light_cone = False
self.dark_world_light_cone = False self.dark_world_light_cone = False
self.treasure_hunt_count = 0
self.treasure_hunt_icon = 'Triforce Piece'
self.clock_mode = 'off' self.clock_mode = 'off'
self.rupoor_cost = 10 self.rupoor_cost = 10
self.aga_randomness = True self.aga_randomness = True
self.lock_aga_door_in_escape = False self.lock_aga_door_in_escape = False
self.fix_trock_doors = self.shuffle != 'vanilla' or self.mode == 'inverted'
self.save_and_quit_from_boss = True self.save_and_quit_from_boss = True
self.accessibility = accessibility self.accessibility = accessibility.copy()
self.fix_skullwoods_exit = self.shuffle not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'] or self.doorShuffle not in ['vanilla'] self.fix_skullwoods_exit = {}
self.fix_palaceofdarkness_exit = self.shuffle not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'] self.fix_palaceofdarkness_exit = {}
self.fix_trock_exit = self.shuffle not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'] self.fix_trock_exit = {}
self.shuffle_ganon = shuffle_ganon self.shuffle_ganon = shuffle_ganon
self.fix_gtower_exit = self.shuffle_ganon self.fix_gtower_exit = self.shuffle_ganon
self.can_access_trock_eyebridge = None self.retro = retro.copy()
self.can_access_trock_front = None
self.can_access_trock_big_chest = None
self.can_access_trock_middle = None
self.quickswap = quickswap
self.fastmenu = fastmenu
self.disable_music = disable_music
self.keysanity = keysanity
self.retro = retro
self.custom = custom self.custom = custom
self.customitemarray = customitemarray self.customitemarray = customitemarray
self.can_take_damage = True self.can_take_damage = True
self.difficulty_requirements = None self.hints = hints.copy()
self.fix_fake_world = True
self.boss_shuffle = boss_shuffle
self.hints = hints
self.crystals_needed_for_ganon = 7
self.crystals_needed_for_gt = 7
self.dynamic_regions = [] self.dynamic_regions = []
self.dynamic_locations = [] self.dynamic_locations = []
self.spoiler = Spoiler(self) self.spoiler = Spoiler(self)
@@ -94,19 +69,59 @@ class World(object):
self.inaccessible_regions = {} self.inaccessible_regions = {}
self.key_logic = {} self.key_logic = {}
def intialize_regions(self): for player in range(1, players + 1):
for region in self.regions: def set_player_attr(attr, val):
self.__dict__.setdefault(attr, {})[player] = val
set_player_attr('_region_cache', {})
set_player_attr('required_medallions', ['Ether', 'Quake'])
set_player_attr('swamp_patch_required', False)
set_player_attr('powder_patch_required', False)
set_player_attr('ganon_at_pyramid', True)
set_player_attr('ganonstower_vanilla', True)
set_player_attr('sewer_light_cone', self.mode[player] == 'standard')
set_player_attr('fix_trock_doors', self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted')
set_player_attr('fix_skullwoods_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
set_player_attr('fix_palaceofdarkness_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
set_player_attr('fix_trock_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
set_player_attr('can_access_trock_eyebridge', None)
set_player_attr('can_access_trock_front', None)
set_player_attr('can_access_trock_big_chest', None)
set_player_attr('can_access_trock_middle', None)
set_player_attr('fix_fake_world', True)
set_player_attr('mapshuffle', False)
set_player_attr('compassshuffle', False)
set_player_attr('keyshuffle', False)
set_player_attr('bigkeyshuffle', False)
set_player_attr('difficulty_requirements', None)
set_player_attr('boss_shuffle', 'none')
set_player_attr('enemy_shuffle', 'none')
set_player_attr('enemy_health', 'default')
set_player_attr('enemy_damage', 'default')
set_player_attr('beemizer', 0)
set_player_attr('escape_assist', [])
set_player_attr('crystals_needed_for_ganon', 7)
set_player_attr('crystals_needed_for_gt', 7)
set_player_attr('open_pyramid', False)
set_player_attr('treasure_hunt_icon', 'Triforce Piece')
set_player_attr('treasure_hunt_count', 0)
def initialize_regions(self, regions=None):
for region in regions if regions else self.regions:
region.world = self region.world = self
self._region_cache[region.player][region.name] = region
def get_regions(self, player=None):
return self.regions if player is None else self._region_cache[player].values()
def get_region(self, regionname, player): def get_region(self, regionname, player):
if isinstance(regionname, Region): if isinstance(regionname, Region):
return regionname return regionname
try: try:
return self._region_cache[(regionname, player)] return self._region_cache[player][regionname]
except KeyError: except KeyError:
for region in self.regions: for region in self.regions:
if region.name == regionname and region.player == player: if region.name == regionname and region.player == player:
self._region_cache[(regionname, player)] = region assert not region.world # this should only happen before initialization
return region return region
raise RuntimeError('No such region %s for player %d' % (regionname, player)) raise RuntimeError('No such region %s for player %d' % (regionname, player))
@@ -189,13 +204,13 @@ class World(object):
if 'Sword' in item.name: if 'Sword' in item.name:
if ret.has('Golden Sword', item.player): if ret.has('Golden Sword', item.player):
pass pass
elif ret.has('Tempered Sword', item.player) and self.difficulty_requirements.progressive_sword_limit >= 4: elif ret.has('Tempered Sword', item.player) and self.difficulty_requirements[item.player].progressive_sword_limit >= 4:
ret.prog_items.add(('Golden Sword', item.player)) ret.prog_items.add(('Golden Sword', item.player))
elif ret.has('Master Sword', item.player) and self.difficulty_requirements.progressive_sword_limit >= 3: elif ret.has('Master Sword', item.player) and self.difficulty_requirements[item.player].progressive_sword_limit >= 3:
ret.prog_items.add(('Tempered Sword', item.player)) ret.prog_items.add(('Tempered Sword', item.player))
elif ret.has('Fighter Sword', item.player) and self.difficulty_requirements.progressive_sword_limit >= 2: elif ret.has('Fighter Sword', item.player) and self.difficulty_requirements[item.player].progressive_sword_limit >= 2:
ret.prog_items.add(('Master Sword', item.player)) ret.prog_items.add(('Master Sword', item.player))
elif self.difficulty_requirements.progressive_sword_limit >= 1: elif self.difficulty_requirements[item.player].progressive_sword_limit >= 1:
ret.prog_items.add(('Fighter Sword', item.player)) ret.prog_items.add(('Fighter Sword', item.player))
elif 'Glove' in item.name: elif 'Glove' in item.name:
if ret.has('Titans Mitts', item.player): if ret.has('Titans Mitts', item.player):
@@ -207,23 +222,23 @@ class World(object):
elif 'Shield' in item.name: elif 'Shield' in item.name:
if ret.has('Mirror Shield', item.player): if ret.has('Mirror Shield', item.player):
pass pass
elif ret.has('Red Shield', item.player) and self.difficulty_requirements.progressive_shield_limit >= 3: elif ret.has('Red Shield', item.player) and self.difficulty_requirements[item.player].progressive_shield_limit >= 3:
ret.prog_items.add(('Mirror Shield', item.player)) ret.prog_items.add(('Mirror Shield', item.player))
elif ret.has('Blue Shield', item.player) and self.difficulty_requirements.progressive_shield_limit >= 2: elif ret.has('Blue Shield', item.player) and self.difficulty_requirements[item.player].progressive_shield_limit >= 2:
ret.prog_items.add(('Red Shield', item.player)) ret.prog_items.add(('Red Shield', item.player))
elif self.difficulty_requirements.progressive_shield_limit >= 1: elif self.difficulty_requirements[item.player].progressive_shield_limit >= 1:
ret.prog_items.add(('Blue Shield', item.player)) ret.prog_items.add(('Blue Shield', item.player))
elif 'Bow' in item.name: elif 'Bow' in item.name:
if ret.has('Silver Arrows', item.player): if ret.has('Silver Arrows', item.player):
pass pass
elif ret.has('Bow', item.player) and self.difficulty_requirements.progressive_bow_limit >= 2: elif ret.has('Bow', item.player) and self.difficulty_requirements[item.player].progressive_bow_limit >= 2:
ret.prog_items.add(('Silver Arrows', item.player)) ret.prog_items.add(('Silver Arrows', item.player))
elif self.difficulty_requirements.progressive_bow_limit >= 1: elif self.difficulty_requirements[item.player].progressive_bow_limit >= 1:
ret.prog_items.add(('Bow', item.player)) ret.prog_items.add(('Bow', item.player))
elif item.name.startswith('Bottle'): elif item.name.startswith('Bottle'):
if ret.bottle_count(item.player) < self.difficulty_requirements.progressive_bottle_limit: if ret.bottle_count(item.player) < self.difficulty_requirements[item.player].progressive_bottle_limit:
ret.prog_items.add((item.name, item.player)) ret.prog_items.add((item.name, item.player))
elif item.advancement or item.key: elif item.advancement or item.smallkey or item.bigkey:
ret.prog_items.add((item.name, item.player)) ret.prog_items.add((item.name, item.player))
for item in self.itempool: for item in self.itempool:
@@ -251,6 +266,8 @@ class World(object):
return [location for location in self.get_locations() if location.item is not None and location.item.name == item and location.item.player == player] return [location for location in self.get_locations() if location.item is not None and location.item.name == item and location.item.player == player]
def push_precollected(self, item): def push_precollected(self, item):
if (item.smallkey and self.keyshuffle[item.player]) or (item.bigkey and self.bigkeyshuffle[item.player]):
item.advancement = True
self.precollected_items.append(item) self.precollected_items.append(item)
self.state.collect(item, True) self.state.collect(item, True)
@@ -366,15 +383,15 @@ class CollectionState(object):
self.collect(item, True) self.collect(item, True)
def update_reachable_regions(self, player): def update_reachable_regions(self, player):
player_regions = [region for region in self.world.regions if region.player == player] player_regions = self.world.get_regions(player)
self.stale[player] = False self.stale[player] = False
rrp = self.reachable_regions[player] rrp = self.reachable_regions[player]
ccr = self.colored_regions[player] ccr = self.colored_regions[player]
new_regions = True new_regions = True
reachable_regions_count = len(rrp) reachable_regions_count = len(rrp)
while new_regions: while new_regions:
possible = [region for region in player_regions if region not in rrp] player_regions = [region for region in player_regions if region not in rrp]
for candidate in possible: for candidate in player_regions:
if candidate.can_reach_private(self): if candidate.can_reach_private(self):
rrp.add(candidate) rrp.add(candidate)
if candidate.type == RegionType.Dungeon: if candidate.type == RegionType.Dungeon:
@@ -455,12 +472,14 @@ class CollectionState(object):
def sweep_for_events(self, key_only=False, locations=None): def sweep_for_events(self, key_only=False, locations=None):
# this may need improvement # this may need improvement
if locations is None:
locations = self.world.get_filled_locations()
new_locations = True new_locations = True
checked_locations = 0 checked_locations = 0
while new_locations: while new_locations:
if locations is None: reachable_events = [location for location in locations if location.event and
locations = self.world.get_filled_locations() (not key_only or (not self.world.keyshuffle[location.item.player] and location.item.smallkey) or (not self.world.bigkeyshuffle[location.item.player] and location.item.bigkey))
reachable_events = [location for location in locations if location.event and (not key_only or location.item.key) and location.can_reach(self)] and location.can_reach(self)]
reachable_events = self._do_not_flood_the_keys(reachable_events) reachable_events = self._do_not_flood_the_keys(reachable_events)
for event in reachable_events: for event in reachable_events:
if (event.name, event.player) not in self.events: if (event.name, event.player) not in self.events:
@@ -501,7 +520,7 @@ class CollectionState(object):
return self.prog_items.count((item, player)) >= count return self.prog_items.count((item, player)) >= count
def has_key(self, item, player, count=1): def has_key(self, item, player, count=1):
if self.world.retro: if self.world.retro[player]:
return self.can_buy_unlimited('Small Key (Universal)', player) return self.can_buy_unlimited('Small Key (Universal)', player)
if count == 1: if count == 1:
return (item, player) in self.prog_items return (item, player) in self.prog_items
@@ -535,7 +554,7 @@ class CollectionState(object):
def heart_count(self, player): def heart_count(self, player):
# Warning: This only considers items that are marked as advancement items # Warning: This only considers items that are marked as advancement items
diff = self.world.difficulty_requirements diff = self.world.difficulty_requirements[player]
return ( return (
min(self.item_count('Boss Heart Container', player), diff.boss_heart_container_limit) min(self.item_count('Boss Heart Container', player), diff.boss_heart_container_limit)
+ self.item_count('Sanctuary Heart Container', player) + self.item_count('Sanctuary Heart Container', player)
@@ -553,9 +572,9 @@ class CollectionState(object):
elif self.has('Half Magic', player): elif self.has('Half Magic', player):
basemagic = 16 basemagic = 16
if self.can_buy_unlimited('Green Potion', player) or self.can_buy_unlimited('Blue Potion', player): if self.can_buy_unlimited('Green Potion', player) or self.can_buy_unlimited('Blue Potion', player):
if self.world.difficulty_adjustments == 'hard' and not fullrefill: if self.world.difficulty_adjustments[player] == 'hard' and not fullrefill:
basemagic = basemagic + int(basemagic * 0.5 * self.bottle_count(player)) basemagic = basemagic + int(basemagic * 0.5 * self.bottle_count(player))
elif self.world.difficulty_adjustments == 'expert' and not fullrefill: elif self.world.difficulty_adjustments[player] == 'expert' and not fullrefill:
basemagic = basemagic + int(basemagic * 0.25 * self.bottle_count(player)) basemagic = basemagic + int(basemagic * 0.25 * self.bottle_count(player))
else: else:
basemagic = basemagic + basemagic * self.bottle_count(player) basemagic = basemagic + basemagic * self.bottle_count(player)
@@ -570,7 +589,7 @@ class CollectionState(object):
) )
def can_shoot_arrows(self, player): def can_shoot_arrows(self, player):
if self.world.retro: if self.world.retro[player]:
#TODO: need to decide how we want to handle wooden arrows longer-term (a can-buy-a check, or via dynamic shop location) #TODO: need to decide how we want to handle wooden arrows longer-term (a can-buy-a check, or via dynamic shop location)
#FIXME: Should do something about hard+ ganon only silvers. For the moment, i believe they effective grant wooden, so we are safe #FIXME: Should do something about hard+ ganon only silvers. For the moment, i believe they effective grant wooden, so we are safe
return self.has('Bow', player) and (self.has('Silver Arrows', player) or self.can_buy_unlimited('Single Arrow', player)) return self.has('Bow', player) and (self.has('Silver Arrows', player) or self.can_buy_unlimited('Single Arrow', player))
@@ -621,7 +640,7 @@ class CollectionState(object):
if self.has_Pearl(player): if self.has_Pearl(player):
return True return True
return region.is_light_world if self.world.mode != 'inverted' else region.is_dark_world return region.is_light_world if self.world.mode[player] != 'inverted' else region.is_dark_world
def can_reach_light_world(self, player): def can_reach_light_world(self, player):
if True in [i.is_light_world for i in self.reachable_regions[player]]: if True in [i.is_light_world for i in self.reachable_regions[player]]:
@@ -647,16 +666,16 @@ class CollectionState(object):
if 'Sword' in item.name: if 'Sword' in item.name:
if self.has('Golden Sword', item.player): if self.has('Golden Sword', item.player):
pass pass
elif self.has('Tempered Sword', item.player) and self.world.difficulty_requirements.progressive_sword_limit >= 4: elif self.has('Tempered Sword', item.player) and self.world.difficulty_requirements[item.player].progressive_sword_limit >= 4:
self.prog_items.add(('Golden Sword', item.player)) self.prog_items.add(('Golden Sword', item.player))
changed = True changed = True
elif self.has('Master Sword', item.player) and self.world.difficulty_requirements.progressive_sword_limit >= 3: elif self.has('Master Sword', item.player) and self.world.difficulty_requirements[item.player].progressive_sword_limit >= 3:
self.prog_items.add(('Tempered Sword', item.player)) self.prog_items.add(('Tempered Sword', item.player))
changed = True changed = True
elif self.has('Fighter Sword', item.player) and self.world.difficulty_requirements.progressive_sword_limit >= 2: elif self.has('Fighter Sword', item.player) and self.world.difficulty_requirements[item.player].progressive_sword_limit >= 2:
self.prog_items.add(('Master Sword', item.player)) self.prog_items.add(('Master Sword', item.player))
changed = True changed = True
elif self.world.difficulty_requirements.progressive_sword_limit >= 1: elif self.world.difficulty_requirements[item.player].progressive_sword_limit >= 1:
self.prog_items.add(('Fighter Sword', item.player)) self.prog_items.add(('Fighter Sword', item.player))
changed = True changed = True
elif 'Glove' in item.name: elif 'Glove' in item.name:
@@ -671,13 +690,13 @@ class CollectionState(object):
elif 'Shield' in item.name: elif 'Shield' in item.name:
if self.has('Mirror Shield', item.player): if self.has('Mirror Shield', item.player):
pass pass
elif self.has('Red Shield', item.player) and self.world.difficulty_requirements.progressive_shield_limit >= 3: elif self.has('Red Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 3:
self.prog_items.add(('Mirror Shield', item.player)) self.prog_items.add(('Mirror Shield', item.player))
changed = True changed = True
elif self.has('Blue Shield', item.player) and self.world.difficulty_requirements.progressive_shield_limit >= 2: elif self.has('Blue Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 2:
self.prog_items.add(('Red Shield', item.player)) self.prog_items.add(('Red Shield', item.player))
changed = True changed = True
elif self.world.difficulty_requirements.progressive_shield_limit >= 1: elif self.world.difficulty_requirements[item.player].progressive_shield_limit >= 1:
self.prog_items.add(('Blue Shield', item.player)) self.prog_items.add(('Blue Shield', item.player))
changed = True changed = True
elif 'Bow' in item.name: elif 'Bow' in item.name:
@@ -690,7 +709,7 @@ class CollectionState(object):
self.prog_items.add(('Bow', item.player)) self.prog_items.add(('Bow', item.player))
changed = True changed = True
elif item.name.startswith('Bottle'): elif item.name.startswith('Bottle'):
if self.bottle_count(item.player) < self.world.difficulty_requirements.progressive_bottle_limit: if self.bottle_count(item.player) < self.world.difficulty_requirements[item.player].progressive_bottle_limit:
self.prog_items.add((item.name, item.player)) self.prog_items.add((item.name, item.player))
changed = True changed = True
elif event or item.advancement: elif event or item.advancement:
@@ -757,6 +776,8 @@ class CollectionState(object):
return self.can_reach(item[10]) return self.can_reach(item[10])
#elif item.startswith('has_'): #elif item.startswith('has_'):
# return self.has(item[4]) # return self.has(item[4])
if item == '__len__':
return
raise RuntimeError('Cannot parse %s.' % item) raise RuntimeError('Cannot parse %s.' % item)
@@ -805,9 +826,12 @@ class Region(object):
return False return False
def can_fill(self, item): def can_fill(self, item):
is_dungeon_item = item.key or item.map or item.compass inside_dungeon_item = ((item.smallkey and not self.world.keyshuffle[item.player])
sewer_hack = self.world.mode == 'standard' and item.name == 'Small Key (Escape)' or (item.bigkey and not self.world.bigkeyshuffle[item.player])
if sewer_hack or (is_dungeon_item and not self.world.keysanity): or (item.map and not self.world.mapshuffle[item.player])
or (item.compass and not self.world.compassshuffle[item.player]))
sewer_hack = self.world.mode[item.player] == 'standard' and item.name == 'Small Key (Escape)'
if sewer_hack or inside_dungeon_item:
return self.dungeon and self.dungeon.is_dungeon_item(item) and item.player == self.player return self.dungeon and self.dungeon.is_dungeon_item(item) and item.player == self.player
return True return True
@@ -1211,7 +1235,7 @@ class Boss(object):
return self.defeat_rule(state, self.player) return self.defeat_rule(state, self.player)
class Location(object): class Location(object):
def __init__(self, player, name='', address=None, crystal=False, hint_text=None, parent=None, forced_item=None): def __init__(self, player, name='', address=None, crystal=False, hint_text=None, parent=None, forced_item=None, player_address=None):
self.name = name self.name = name
self.parent_region = parent self.parent_region = parent
if forced_item is not None: if forced_item is not None:
@@ -1226,11 +1250,12 @@ class Location(object):
self.event = False self.event = False
self.crystal = crystal self.crystal = crystal
self.address = address self.address = address
self.player_address = player_address
self.spot_type = 'Location' self.spot_type = 'Location'
self.hint_text = hint_text if hint_text is not None else 'Hyrule' self.hint_text = hint_text if hint_text is not None else 'Hyrule'
self.recursion_count = 0 self.recursion_count = 0
self.staleness_count = 0 self.staleness_count = 0
self.locked = True self.locked = False
self.always_allow = lambda item, state: False self.always_allow = lambda item, state: False
self.access_rule = lambda state: True self.access_rule = lambda state: True
self.item_rule = lambda item: True self.item_rule = lambda item: True
@@ -1272,14 +1297,18 @@ class Item(object):
self.location = None self.location = None
self.player = player self.player = player
@property
def key(self):
return self.type == 'SmallKey' or self.type == 'BigKey'
@property @property
def crystal(self): def crystal(self):
return self.type == 'Crystal' return self.type == 'Crystal'
@property
def smallkey(self):
return self.type == 'SmallKey'
@property
def bigkey(self):
return self.type == 'BigKey'
@property @property
def map(self): def map(self):
return self.type == 'Map' return self.type == 'Map'
@@ -1309,14 +1338,14 @@ class ShopType(Enum):
UpgradeShop = 2 UpgradeShop = 2
class Shop(object): class Shop(object):
def __init__(self, region, room_id, type, shopkeeper_config, replaceable): def __init__(self, region, room_id, type, shopkeeper_config, custom, locked):
self.region = region self.region = region
self.room_id = room_id self.room_id = room_id
self.type = type self.type = type
self.inventory = [None, None, None] self.inventory = [None, None, None]
self.shopkeeper_config = shopkeeper_config self.shopkeeper_config = shopkeeper_config
self.replaceable = replaceable self.custom = custom
self.active = False self.locked = locked
@property @property
def item_count(self): def item_count(self):
@@ -1373,6 +1402,8 @@ class Spoiler(object):
self.doorTypes = OrderedDict() self.doorTypes = OrderedDict()
self.medallions = {} self.medallions = {}
self.playthrough = {} self.playthrough = {}
self.unreachables = []
self.startinventory = []
self.locations = {} self.locations = {}
self.paths = {} self.paths = {}
self.metadata = {} self.metadata = {}
@@ -1407,6 +1438,8 @@ class Spoiler(object):
self.medallions['Misery Mire (Player %d)' % player] = self.world.required_medallions[player][0] self.medallions['Misery Mire (Player %d)' % player] = self.world.required_medallions[player][0]
self.medallions['Turtle Rock (Player %d)' % player] = self.world.required_medallions[player][1] self.medallions['Turtle Rock (Player %d)' % player] = self.world.required_medallions[player][1]
self.startinventory = list(map(str, self.world.precollected_items))
self.locations = OrderedDict() self.locations = OrderedDict()
listed_locations = set() listed_locations = set()
@@ -1434,7 +1467,7 @@ class Spoiler(object):
self.shops = [] self.shops = []
for shop in self.world.shops: for shop in self.world.shops:
if not shop.active: if not shop.custom:
continue continue
shopdata = {'location': str(shop.region), shopdata = {'location': str(shop.region),
'type': 'Take Any' if shop.type == ShopType.TakeAny else 'Shop' 'type': 'Take Any' if shop.type == ShopType.TakeAny else 'Shop'
@@ -1472,14 +1505,27 @@ class Spoiler(object):
self.metadata = {'version': ERVersion, self.metadata = {'version': ERVersion,
'logic': self.world.logic, 'logic': self.world.logic,
'mode': self.world.mode, 'mode': self.world.mode,
'retro': self.world.retro,
'weapons': self.world.swords, 'weapons': self.world.swords,
'goal': self.world.goal, 'goal': self.world.goal,
'shuffle': self.world.shuffle, 'shuffle': self.world.shuffle,
'door_shuffle': self.world.doorShuffle,
'item_pool': self.world.difficulty, 'item_pool': self.world.difficulty,
'item_functionality': self.world.difficulty_adjustments, 'item_functionality': self.world.difficulty_adjustments,
'gt_crystals': self.world.crystals_needed_for_gt,
'ganon_crystals': self.world.crystals_needed_for_ganon,
'open_pyramid': self.world.open_pyramid,
'accessibility': self.world.accessibility, 'accessibility': self.world.accessibility,
'hints': self.world.hints, 'hints': self.world.hints,
'keysanity': self.world.keysanity, 'mapshuffle': self.world.mapshuffle,
'compassshuffle': self.world.compassshuffle,
'keyshuffle': self.world.keyshuffle,
'bigkeyshuffle': self.world.bigkeyshuffle,
'boss_shuffle': self.world.boss_shuffle,
'enemy_shuffle': self.world.enemy_shuffle,
'enemy_health': self.world.enemy_health,
'enemy_damage': self.world.enemy_damage,
'players': self.world.players
} }
def to_json(self): def to_json(self):
@@ -1489,13 +1535,13 @@ class Spoiler(object):
out['Doors'] = list(self.doors.values()) out['Doors'] = list(self.doors.values())
out['DoorTypes'] = list(self.doorTypes.values()) out['DoorTypes'] = list(self.doorTypes.values())
out.update(self.locations) out.update(self.locations)
out['Starting Inventory'] = self.startinventory
out['Special'] = self.medallions out['Special'] = self.medallions
if self.shops: if self.shops:
out['Shops'] = self.shops out['Shops'] = self.shops
out['playthrough'] = self.playthrough out['playthrough'] = self.playthrough
out['paths'] = self.paths out['paths'] = self.paths
if self.world.boss_shuffle != 'none': out['Bosses'] = self.bosses
out['Bosses'] = self.bosses
out['meta'] = self.metadata out['meta'] = self.metadata
return json.dumps(out) return json.dumps(out)
@@ -1504,19 +1550,30 @@ class Spoiler(object):
self.parse_data() self.parse_data()
with open(filename, 'w') as outfile: with open(filename, 'w') as outfile:
outfile.write('ALttP Entrance Randomizer Version %s - Seed: %s\n\n' % (self.metadata['version'], self.world.seed)) outfile.write('ALttP Entrance Randomizer Version %s - Seed: %s\n\n' % (self.metadata['version'], self.world.seed))
outfile.write('Players: %d\n' % self.world.players)
outfile.write('Filling Algorithm: %s\n' % self.world.algorithm)
outfile.write('Logic: %s\n' % self.metadata['logic']) outfile.write('Logic: %s\n' % self.metadata['logic'])
outfile.write('Mode: %s\n' % self.metadata['mode']) outfile.write('Mode: %s\n' % self.metadata['mode'])
outfile.write('Retro: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['retro'].items()})
outfile.write('Swords: %s\n' % self.metadata['weapons'])
outfile.write('Goal: %s\n' % self.metadata['goal']) outfile.write('Goal: %s\n' % self.metadata['goal'])
outfile.write('Difficulty: %s\n' % self.metadata['item_pool']) outfile.write('Difficulty: %s\n' % self.metadata['item_pool'])
outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality']) outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'])
outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle']) outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'])
outfile.write('Filling Algorithm: %s\n' % self.world.algorithm) outfile.write('Door Shuffle: %s\n' % self.metadata['door_shuffle'])
outfile.write('Crystals required for GT: %s\n' % self.metadata['gt_crystals'])
outfile.write('Crystals required for Ganon: %s\n' % self.metadata['ganon_crystals'])
outfile.write('Pyramid hole pre-opened: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['open_pyramid'].items()})
outfile.write('Accessibility: %s\n' % self.metadata['accessibility']) outfile.write('Accessibility: %s\n' % self.metadata['accessibility'])
outfile.write('Maps and Compasses in Dungeons: %s\n' % ('Yes' if self.world.place_dungeon_items else 'No')) outfile.write('Map shuffle: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['mapshuffle'].items()})
outfile.write('L\\R Quickswap enabled: %s\n' % ('Yes' if self.world.quickswap else 'No')) outfile.write('Compass shuffle: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['compassshuffle'].items()})
outfile.write('Menu speed: %s\n' % self.world.fastmenu) outfile.write('Small Key shuffle: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['keyshuffle'].items()})
outfile.write('Keysanity enabled: %s\n' % ('Yes' if self.metadata['keysanity'] else 'No')) outfile.write('Big Key shuffle: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['bigkeyshuffle'].items()})
outfile.write('Players: %d' % self.world.players) outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'])
outfile.write('Enemy shuffle: %s\n' % self.metadata['enemy_shuffle'])
outfile.write('Enemy health: %s\n' % self.metadata['enemy_health'])
outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'])
outfile.write('Hints: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['hints'].items()})
if self.doors: if self.doors:
outfile.write('\n\nDoors:\n\n') outfile.write('\n\nDoors:\n\n')
outfile.write('\n'.join(['%s%s %s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '', entry['entrance'], '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', entry['exit']) for entry in self.doors.values()])) outfile.write('\n'.join(['%s%s %s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '', entry['entrance'], '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', entry['exit']) for entry in self.doors.values()]))
@@ -1534,12 +1591,17 @@ class Spoiler(object):
for player in range(1, self.world.players + 1): for player in range(1, self.world.players + 1):
outfile.write('\nMisery Mire Medallion (Player %d): %s' % (player, self.medallions['Misery Mire (Player %d)' % player])) outfile.write('\nMisery Mire Medallion (Player %d): %s' % (player, self.medallions['Misery Mire (Player %d)' % player]))
outfile.write('\nTurtle Rock Medallion (Player %d): %s' % (player, self.medallions['Turtle Rock (Player %d)' % player])) outfile.write('\nTurtle Rock Medallion (Player %d): %s' % (player, self.medallions['Turtle Rock (Player %d)' % player]))
outfile.write('\n\nStarting Inventory:\n\n')
outfile.write('\n'.join(self.startinventory))
outfile.write('\n\nLocations:\n\n') outfile.write('\n\nLocations:\n\n')
outfile.write('\n'.join(['%s: %s' % (location, item) for grouping in self.locations.values() for (location, item) in grouping.items()])) outfile.write('\n'.join(['%s: %s' % (location, item) for grouping in self.locations.values() for (location, item) in grouping.items()]))
outfile.write('\n\nShops:\n\n') outfile.write('\n\nShops:\n\n')
outfile.write('\n'.join("{} [{}]\n {}".format(shop['location'], shop['type'], "\n ".join(item for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if item)) for shop in self.shops)) outfile.write('\n'.join("{} [{}]\n {}".format(shop['location'], shop['type'], "\n ".join(item for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if item)) for shop in self.shops))
outfile.write('\n\nPlaythrough:\n\n') outfile.write('\n\nPlaythrough:\n\n')
outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join([' %s: %s' % (location, item) for (location, item) in sphere.items()])) for (sphere_nr, sphere) in self.playthrough.items()])) outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join([' %s: %s' % (location, item) for (location, item) in sphere.items()] if sphere_nr != '0' else [f' {item}' for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()]))
if self.unreachables:
outfile.write('\n\nUnreachable Items:\n\n')
outfile.write('\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables]))
outfile.write('\n\nPaths:\n\n') outfile.write('\n\nPaths:\n\n')
path_listings = [] path_listings = []
+12 -12
View File
@@ -73,7 +73,7 @@ def KholdstareDefeatRule(state, player):
( (
state.has('Bombos', player) and state.has('Bombos', player) and
# FIXME: the following only actually works for the vanilla location for swordless # FIXME: the following only actually works for the vanilla location for swordless
(state.has_sword(player) or state.world.swords == 'swordless') (state.has_sword(player) or state.world.swords[player] == 'swordless')
) )
) and ) and
( (
@@ -83,7 +83,7 @@ def KholdstareDefeatRule(state, player):
( (
state.has('Fire Rod', player) and state.has('Fire Rod', player) and
state.has('Bombos', player) and state.has('Bombos', player) and
state.world.swords == 'swordless' and state.world.swords[player] == 'swordless' and
state.can_extend_magic(player, 16) state.can_extend_magic(player, 16)
) )
) )
@@ -115,8 +115,8 @@ boss_table = {
'Agahnim2': ('Agahnim2', AgahnimDefeatRule) 'Agahnim2': ('Agahnim2', AgahnimDefeatRule)
} }
def can_place_boss(world, boss, dungeon_name, level=None): def can_place_boss(world, player, boss, dungeon_name, level=None):
if world.swords in ['swordless'] and boss == 'Kholdstare' and dungeon_name != 'Ice Palace': if world.swords[player] in ['swordless'] and boss == 'Kholdstare' and dungeon_name != 'Ice Palace':
return False return False
if dungeon_name in ['Ganons Tower', 'Inverted Ganons Tower'] and level == 'top': if dungeon_name in ['Ganons Tower', 'Inverted Ganons Tower'] and level == 'top':
@@ -138,10 +138,10 @@ def can_place_boss(world, boss, dungeon_name, level=None):
return True return True
def place_bosses(world, player): def place_bosses(world, player):
if world.boss_shuffle == 'none': if world.boss_shuffle[player] == 'none':
return return
# Most to least restrictive order # Most to least restrictive order
if world.mode != 'inverted': if world.mode[player] != 'inverted':
boss_locations = [ boss_locations = [
['Ganons Tower', 'top'], ['Ganons Tower', 'top'],
['Tower of Hera', None], ['Tower of Hera', None],
@@ -177,15 +177,15 @@ def place_bosses(world, player):
all_bosses = sorted(boss_table.keys()) #s orted to be deterministic on older pythons all_bosses = sorted(boss_table.keys()) #s orted to be deterministic on older pythons
placeable_bosses = [boss for boss in all_bosses if boss not in ['Agahnim', 'Agahnim2', 'Ganon']] placeable_bosses = [boss for boss in all_bosses if boss not in ['Agahnim', 'Agahnim2', 'Ganon']]
if world.boss_shuffle in ["basic", "normal"]: if world.boss_shuffle[player] in ["basic", "normal"]:
# temporary hack for swordless kholdstare: # temporary hack for swordless kholdstare:
if world.swords == 'swordless': if world.swords[player] == 'swordless':
world.get_dungeon('Ice Palace', player).boss = BossFactory('Kholdstare', player) world.get_dungeon('Ice Palace', player).boss = BossFactory('Kholdstare', player)
logging.getLogger('').debug('Placing boss Kholdstare at Ice Palace') logging.getLogger('').debug('Placing boss Kholdstare at Ice Palace')
boss_locations.remove(['Ice Palace', None]) boss_locations.remove(['Ice Palace', None])
placeable_bosses.remove('Kholdstare') placeable_bosses.remove('Kholdstare')
if world.boss_shuffle == "basic": # vanilla bosses shuffled if world.boss_shuffle[player] == "basic": # vanilla bosses shuffled
bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm'] bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm']
else: # all bosses present, the three duplicates chosen at random else: # all bosses present, the three duplicates chosen at random
bosses = all_bosses + [random.choice(placeable_bosses) for _ in range(3)] bosses = all_bosses + [random.choice(placeable_bosses) for _ in range(3)]
@@ -195,18 +195,18 @@ def place_bosses(world, player):
random.shuffle(bosses) random.shuffle(bosses)
for [loc, level] in boss_locations: for [loc, level] in boss_locations:
loc_text = loc + (' ('+level+')' if level else '') loc_text = loc + (' ('+level+')' if level else '')
boss = next((b for b in bosses if can_place_boss(world, b, loc, level)), None) boss = next((b for b in bosses if can_place_boss(world, player, b, loc, level)), None)
if not boss: if not boss:
raise FillError('Could not place boss for location %s' % loc_text) raise FillError('Could not place boss for location %s' % loc_text)
bosses.remove(boss) bosses.remove(boss)
logging.getLogger('').debug('Placing boss %s at %s', boss, loc_text) logging.getLogger('').debug('Placing boss %s at %s', boss, loc_text)
world.get_dungeon(loc, player).bosses[level] = BossFactory(boss, player) world.get_dungeon(loc, player).bosses[level] = BossFactory(boss, player)
elif world.boss_shuffle == "chaos": #all bosses chosen at random elif world.boss_shuffle[player] == "chaos": #all bosses chosen at random
for [loc, level] in boss_locations: for [loc, level] in boss_locations:
loc_text = loc + (' ('+level+')' if level else '') loc_text = loc + (' ('+level+')' if level else '')
try: try:
boss = random.choice([b for b in placeable_bosses if can_place_boss(world, b, loc, level)]) boss = random.choice([b for b in placeable_bosses if can_place_boss(world, player, b, loc, level)])
except IndexError: except IndexError:
raise FillError('Could not place boss for location %s' % loc_text) raise FillError('Could not place boss for location %s' % loc_text)
+12 -12
View File
@@ -39,7 +39,7 @@ def link_doors(world, player):
for ent, ext in ladders: for ent, ext in ladders:
connect_two_way(world, ent, ext, player) connect_two_way(world, ent, ext, player)
if world.doorShuffle == 'vanilla': if world.doorShuffle[player] == 'vanilla':
for exitName, regionName in vanilla_logical_connections: for exitName, regionName in vanilla_logical_connections:
connect_simple_door(world, exitName, regionName, player) connect_simple_door(world, exitName, regionName, player)
for entrance, ext in spiral_staircases: for entrance, ext in spiral_staircases:
@@ -49,14 +49,14 @@ def link_doors(world, player):
for ent, ext in default_one_way_connections: for ent, ext in default_one_way_connections:
connect_one_way(world, ent, ext, player) connect_one_way(world, ent, ext, player)
vanilla_key_logic(world, player) vanilla_key_logic(world, player)
elif world.doorShuffle == 'basic': elif world.doorShuffle[player] == 'basic':
within_dungeon(world, player) within_dungeon(world, player)
elif world.doorShuffle == 'crossed': elif world.doorShuffle[player] == 'crossed':
cross_dungeon(world, player) cross_dungeon(world, player)
elif world.doorShuffle == 'experimental': elif world.doorShuffle[player] == 'experimental':
experiment(world, player) experiment(world, player)
if world.doorShuffle != 'vanilla': if world.doorShuffle[player] != 'vanilla':
create_door_spoiler(world, player) create_door_spoiler(world, player)
@@ -83,7 +83,7 @@ def mark_regions(world, player):
def create_door_spoiler(world, player): def create_door_spoiler(world, player):
logger = logging.getLogger('') logger = logging.getLogger('')
queue = collections.deque(world.doors) queue = collections.deque((door for door in world.doors if door.player == player))
while len(queue) > 0: while len(queue) > 0:
door_a = queue.popleft() door_a = queue.popleft()
if door_a.type in [DoorType.Normal, DoorType.SpiralStairs]: if door_a.type in [DoorType.Normal, DoorType.SpiralStairs]:
@@ -283,7 +283,7 @@ def within_dungeon(world, player):
handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map) handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map)
main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, world, player) main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, world, player)
paths = determine_required_paths(world) paths = determine_required_paths(world, player)
check_required_paths(paths, world, player) check_required_paths(paths, world, player)
# shuffle_key_doors for dungeons # shuffle_key_doors for dungeons
@@ -637,7 +637,7 @@ def cross_dungeon(world, player):
main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, world, player) main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, world, player)
paths = determine_required_paths(world) paths = determine_required_paths(world, player)
check_required_paths(paths, world, player) check_required_paths(paths, world, player)
hc = world.get_dungeon('Hyrule Castle', player) hc = world.get_dungeon('Hyrule Castle', player)
@@ -1106,7 +1106,7 @@ def change_door_to_small_key(d, world, player):
room.change(d.doorListPos, DoorKind.SmallKey) room.change(d.doorListPos, DoorKind.SmallKey)
def determine_required_paths(world): def determine_required_paths(world, player):
paths = { paths = {
'Hyrule Castle': [], 'Hyrule Castle': [],
'Eastern Palace': ['Eastern Boss'], 'Eastern Palace': ['Eastern Boss'],
@@ -1122,7 +1122,7 @@ def determine_required_paths(world):
'Turtle Rock': ['TR Boss'], 'Turtle Rock': ['TR Boss'],
'Ganons Tower': ['GT Agahnim 2'] 'Ganons Tower': ['GT Agahnim 2']
} }
if world.shuffle == 'vanilla': if world.shuffle[player] == 'vanilla':
paths['Skull Woods'].insert(0, 'Skull 2 West Lobby') paths['Skull Woods'].insert(0, 'Skull 2 West Lobby')
paths['Turtle Rock'].insert(0, 'TR Eye Bridge') paths['Turtle Rock'].insert(0, 'TR Eye Bridge')
paths['Turtle Rock'].insert(0, 'TR Big Chest Entrance') paths['Turtle Rock'].insert(0, 'TR Big Chest Entrance')
@@ -1131,7 +1131,7 @@ def determine_required_paths(world):
paths['Hyrule Castle'].append('Hyrule Dungeon Cellblock') paths['Hyrule Castle'].append('Hyrule Dungeon Cellblock')
# noinspection PyTypeChecker # noinspection PyTypeChecker
paths['Hyrule Castle'].append(('Hyrule Dungeon Cellblock', 'Sanctuary')) paths['Hyrule Castle'].append(('Hyrule Dungeon Cellblock', 'Sanctuary'))
if world.doorShuffle in ['basic']: if world.doorShuffle[player] in ['basic']:
paths['Thieves Town'].append('Thieves Attic Window') paths['Thieves Town'].append('Thieves Attic Window')
return paths return paths
@@ -1194,7 +1194,7 @@ def add_inaccessible_doors(world, player):
create_door(world, player, 'Death Mountain Return Cave (West)', 'Death Mountain Return Ledge') create_door(world, player, 'Death Mountain Return Cave (West)', 'Death Mountain Return Ledge')
if 'Desert Palace Lone Stairs' in world.inaccessible_regions[player]: if 'Desert Palace Lone Stairs' in world.inaccessible_regions[player]:
create_door(world, player, 'Desert Palace Entrance (East)', 'Desert Palace Lone Stairs') create_door(world, player, 'Desert Palace Entrance (East)', 'Desert Palace Lone Stairs')
if world.mode == 'standard' and 'Hyrule Castle Ledge' in world.inaccessible_regions[player]: if world.mode[player] == 'standard' and 'Hyrule Castle Ledge' in world.inaccessible_regions[player]:
create_door(world, player, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Ledge') create_door(world, player, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Ledge')
create_door(world, player, 'Hyrule Castle Entrance (West)', 'Hyrule Castle Ledge') create_door(world, player, 'Hyrule Castle Entrance (West)', 'Hyrule Castle Ledge')
+90 -50
View File
@@ -1,13 +1,16 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import argparse import argparse
import copy
import os import os
import logging import logging
import random import random
import textwrap import textwrap
import shlex
import sys import sys
from Main import main from Main import main
from Utils import is_bundled, close_console, output_path from Rom import get_sprite_from_name
from Utils import is_bundled, close_console
from Fill import FillError from Fill import FillError
@@ -16,11 +19,18 @@ class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
def _get_help_string(self, action): def _get_help_string(self, action):
return textwrap.dedent(action.help) return textwrap.dedent(action.help)
def parse_arguments(argv, no_defaults=False):
def defval(value):
return value if not no_defaults else None
# we need to know how many players we have first
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('--multi', default=defval(1), type=lambda value: min(max(int(value), 1), 255))
multiargs, _ = parser.parse_known_args(argv)
def start():
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument('--create_spoiler', help='Output a Spoiler File', action='store_true') parser.add_argument('--create_spoiler', help='Output a Spoiler File', action='store_true')
parser.add_argument('--logic', default='noglitches', const='noglitches', nargs='?', choices=['noglitches', 'minorglitches', 'nologic'], parser.add_argument('--logic', default=defval('noglitches'), const='noglitches', nargs='?', choices=['noglitches', 'minorglitches', 'nologic'],
help='''\ help='''\
Select Enforcement of Item Requirements. (default: %(default)s) Select Enforcement of Item Requirements. (default: %(default)s)
No Glitches: No Glitches:
@@ -29,7 +39,7 @@ def start():
No Logic: Distribute items without regard for No Logic: Distribute items without regard for
item requirements. item requirements.
''') ''')
parser.add_argument('--mode', default='open', const='open', nargs='?', choices=['standard', 'open', 'inverted'], parser.add_argument('--mode', default=defval('open'), const='open', nargs='?', choices=['standard', 'open', 'inverted'],
help='''\ help='''\
Select game mode. (default: %(default)s) Select game mode. (default: %(default)s)
Open: World starts with Zelda rescued. Open: World starts with Zelda rescued.
@@ -42,7 +52,7 @@ def start():
Requires the moon pearl to be Link in the Light World Requires the moon pearl to be Link in the Light World
instead of a bunny. instead of a bunny.
''') ''')
parser.add_argument('--swords', default='random', const='random', nargs='?', choices= ['random', 'assured', 'swordless', 'vanilla'], parser.add_argument('--swords', default=defval('random'), const='random', nargs='?', choices= ['random', 'assured', 'swordless', 'vanilla'],
help='''\ help='''\
Select sword placement. (default: %(default)s) Select sword placement. (default: %(default)s)
Random: All swords placed randomly. Random: All swords placed randomly.
@@ -56,7 +66,7 @@ def start():
Palace, to allow for an alternative to firerod. Palace, to allow for an alternative to firerod.
Vanilla: Swords are in vanilla locations. Vanilla: Swords are in vanilla locations.
''') ''')
parser.add_argument('--goal', default='ganon', const='ganon', nargs='?', choices=['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'crystals'], parser.add_argument('--goal', default=defval('ganon'), const='ganon', nargs='?', choices=['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'crystals'],
help='''\ help='''\
Select completion goal. (default: %(default)s) Select completion goal. (default: %(default)s)
Ganon: Collect all crystals, beat Agahnim 2 then Ganon: Collect all crystals, beat Agahnim 2 then
@@ -68,21 +78,21 @@ def start():
Triforce Hunt: Places 30 Triforce Pieces in the world, collect Triforce Hunt: Places 30 Triforce Pieces in the world, collect
20 of them to beat the game. 20 of them to beat the game.
''') ''')
parser.add_argument('--difficulty', default='normal', const='normal', nargs='?', choices=['normal', 'hard', 'expert'], parser.add_argument('--difficulty', default=defval('normal'), const='normal', nargs='?', choices=['normal', 'hard', 'expert'],
help='''\ help='''\
Select game difficulty. Affects available itempool. (default: %(default)s) Select game difficulty. Affects available itempool. (default: %(default)s)
Normal: Normal difficulty. Normal: Normal difficulty.
Hard: A harder setting with less equipment and reduced health. Hard: A harder setting with less equipment and reduced health.
Expert: A harder yet setting with minimum equipment and health. Expert: A harder yet setting with minimum equipment and health.
''') ''')
parser.add_argument('--item_functionality', default='normal', const='normal', nargs='?', choices=['normal', 'hard', 'expert'], parser.add_argument('--item_functionality', default=defval('normal'), const='normal', nargs='?', choices=['normal', 'hard', 'expert'],
help='''\ help='''\
Select limits on item functionality to increase difficulty. (default: %(default)s) Select limits on item functionality to increase difficulty. (default: %(default)s)
Normal: Normal functionality. Normal: Normal functionality.
Hard: Reduced functionality. Hard: Reduced functionality.
Expert: Greatly reduced functionality. Expert: Greatly reduced functionality.
''') ''')
parser.add_argument('--timer', default='none', const='normal', nargs='?', choices=['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'], parser.add_argument('--timer', default=defval('none'), const='normal', nargs='?', choices=['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'],
help='''\ help='''\
Select game timer setting. Affects available itempool. (default: %(default)s) Select game timer setting. Affects available itempool. (default: %(default)s)
None: No timer. None: No timer.
@@ -102,7 +112,7 @@ def start():
Timed mode. If time runs out, you lose (but can Timed mode. If time runs out, you lose (but can
still keep playing). still keep playing).
''') ''')
parser.add_argument('--progressive', default='on', const='normal', nargs='?', choices=['on', 'off', 'random'], parser.add_argument('--progressive', default=defval('on'), const='normal', nargs='?', choices=['on', 'off', 'random'],
help='''\ help='''\
Select progressive equipment setting. Affects available itempool. (default: %(default)s) Select progressive equipment setting. Affects available itempool. (default: %(default)s)
On: Swords, Shields, Armor, and Gloves will On: Swords, Shields, Armor, and Gloves will
@@ -116,7 +126,7 @@ def start():
category, be randomly progressive or not. category, be randomly progressive or not.
Link will die in one hit. Link will die in one hit.
''') ''')
parser.add_argument('--algorithm', default='balanced', const='balanced', nargs='?', choices=['freshness', 'flood', 'vt21', 'vt22', 'vt25', 'vt26', 'balanced'], parser.add_argument('--algorithm', default=defval('balanced'), const='balanced', nargs='?', choices=['freshness', 'flood', 'vt21', 'vt22', 'vt25', 'vt26', 'balanced'],
help='''\ help='''\
Select item filling algorithm. (default: %(default)s Select item filling algorithm. (default: %(default)s
balanced: vt26 derivative that aims to strike a balance between balanced: vt26 derivative that aims to strike a balance between
@@ -139,7 +149,7 @@ def start():
slightly biased to placing progression items with slightly biased to placing progression items with
less restrictions. less restrictions.
''') ''')
parser.add_argument('--shuffle', default='full', const='full', nargs='?', choices=['vanilla', 'simple', 'restricted', 'full', 'crossed', 'insanity', 'restricted_legacy', 'full_legacy', 'madness_legacy', 'insanity_legacy', 'dungeonsfull', 'dungeonssimple'], parser.add_argument('--shuffle', default=defval('vanilla'), const='full', nargs='?', choices=['vanilla', 'simple', 'restricted', 'full', 'crossed', 'insanity', 'restricted_legacy', 'full_legacy', 'madness_legacy', 'insanity_legacy', 'dungeonsfull', 'dungeonssimple'],
help='''\ help='''\
Select Entrance Shuffling Algorithm. (default: %(default)s) Select Entrance Shuffling Algorithm. (default: %(default)s)
Full: Mix cave and dungeon entrances freely while limiting Full: Mix cave and dungeon entrances freely while limiting
@@ -163,7 +173,7 @@ def start():
The dungeon variants only mix up dungeons and keep the rest of The dungeon variants only mix up dungeons and keep the rest of
the overworld vanilla. the overworld vanilla.
''') ''')
parser.add_argument('--door_shuffle', default='vanilla', const='vanilla', nargs='?', choices=['vanilla', 'basic', 'crossed', 'experimental'], parser.add_argument('--door_shuffle', default=defval('basic'), const='vanilla', nargs='?', choices=['vanilla', 'basic', 'crossed', 'experimental'],
help='''\ help='''\
Select Door Shuffling Algorithm. (default: %(default)s) Select Door Shuffling Algorithm. (default: %(default)s)
Basic: Doors are mixed within a single dungeon. Basic: Doors are mixed within a single dungeon.
@@ -174,7 +184,7 @@ def start():
base game. base game.
Experimental: Experimental mixes live here. Use at your own risk. Experimental: Experimental mixes live here. Use at your own risk.
''') ''')
parser.add_argument('--crystals_ganon', default='7', const='7', nargs='?', choices=['random', '0', '1', '2', '3', '4', '5', '6', '7'], parser.add_argument('--crystals_ganon', default=defval('7'), const='7', nargs='?', choices=['random', '0', '1', '2', '3', '4', '5', '6', '7'],
help='''\ help='''\
How many crystals are needed to defeat ganon. Any other How many crystals are needed to defeat ganon. Any other
requirements for ganon for the selected goal still apply. requirements for ganon for the selected goal still apply.
@@ -183,16 +193,18 @@ def start():
Random: Picks a random value between 0 and 7 (inclusive). Random: Picks a random value between 0 and 7 (inclusive).
0-7: Number of crystals needed 0-7: Number of crystals needed
''') ''')
parser.add_argument('--crystals_gt', default='7', const='7', nargs='?', choices=['random', '0', '1', '2', '3', '4', '5', '6', '7'], parser.add_argument('--crystals_gt', default=defval('7'), const='7', nargs='?', choices=['random', '0', '1', '2', '3', '4', '5', '6', '7'],
help='''\ help='''\
How many crystals are needed to open GT. For inverted mode How many crystals are needed to open GT. For inverted mode
this applies to the castle tower door instead. (default: %(default)s) this applies to the castle tower door instead. (default: %(default)s)
Random: Picks a random value between 0 and 7 (inclusive). Random: Picks a random value between 0 and 7 (inclusive).
0-7: Number of crystals needed 0-7: Number of crystals needed
''') ''')
parser.add_argument('--openpyramid', default=defval(False), help='''\
parser.add_argument('--rom', default='Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', help='Path to an ALttP JAP(1.0) rom to use as a base.') Pre-opens the pyramid hole, this removes the Agahnim 2 requirement for it
parser.add_argument('--loglevel', default='info', const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.') ''', action='store_true')
parser.add_argument('--rom', default=defval('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc'), help='Path to an ALttP JAP(1.0) rom to use as a base.')
parser.add_argument('--loglevel', default=defval('info'), const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
parser.add_argument('--seed', help='Define seed number to generate.', type=int) parser.add_argument('--seed', help='Define seed number to generate.', type=int)
parser.add_argument('--count', help='''\ parser.add_argument('--count', help='''\
Use to batch generate multiple seeds with same settings. Use to batch generate multiple seeds with same settings.
@@ -201,50 +213,50 @@ def start():
--seed given will produce the same 10 (different) roms each --seed given will produce the same 10 (different) roms each
time). time).
''', type=int) ''', type=int)
parser.add_argument('--fastmenu', default='normal', const='normal', nargs='?', choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'], parser.add_argument('--fastmenu', default=defval('normal'), const='normal', nargs='?', choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
help='''\ help='''\
Select the rate at which the menu opens and closes. Select the rate at which the menu opens and closes.
(default: %(default)s) (default: %(default)s)
''') ''')
parser.add_argument('--quickswap', help='Enable quick item swapping with L and R.', action='store_true') parser.add_argument('--quickswap', help='Enable quick item swapping with L and R.', action='store_true')
parser.add_argument('--disablemusic', help='Disables game music.', action='store_true') parser.add_argument('--disablemusic', help='Disables game music.', action='store_true')
parser.add_argument('--keysanity', help='''\ parser.add_argument('--mapshuffle', default=defval(False), help='Maps are no longer restricted to their dungeons, but can be anywhere', action='store_true')
Keys (and other dungeon items) are no longer restricted to parser.add_argument('--compassshuffle', default=defval(False), help='Compasses are no longer restricted to their dungeons, but can be anywhere', action='store_true')
their dungeons, but can be anywhere parser.add_argument('--keyshuffle', default=defval(False), help='Small Keys are no longer restricted to their dungeons, but can be anywhere', action='store_true')
''', action='store_true') parser.add_argument('--bigkeyshuffle', default=defval(False), help='Big Keys are no longer restricted to their dungeons, but can be anywhere', action='store_true')
parser.add_argument('--retro', help='''\ parser.add_argument('--keysanity', default=defval(False), help=argparse.SUPPRESS, action='store_true')
parser.add_argument('--retro', default=defval(False), help='''\
Keys are universal, shooting arrows costs rupees, Keys are universal, shooting arrows costs rupees,
and a few other little things make this more like Zelda-1. and a few other little things make this more like Zelda-1.
''', action='store_true') ''', action='store_true')
parser.add_argument('--custom', default=False, help='Not supported.') parser.add_argument('--startinventory', default=defval(''), help='Specifies a list of items that will be in your starting inventory (separated by commas)')
parser.add_argument('--customitemarray', default=False, help='Not supported.') parser.add_argument('--custom', default=defval(False), help='Not supported.')
parser.add_argument('--nodungeonitems', help='''\ parser.add_argument('--customitemarray', default=defval(False), help='Not supported.')
Remove Maps and Compasses from Itempool, replacing them by parser.add_argument('--accessibility', default=defval('items'), const='items', nargs='?', choices=['items', 'locations', 'none'], help='''\
empty slots.
''', action='store_true')
parser.add_argument('--accessibility', default='items', const='items', nargs='?', choices=['items', 'locations', 'none'], help='''\
Select Item/Location Accessibility. (default: %(default)s) Select Item/Location Accessibility. (default: %(default)s)
Items: You can reach all unique inventory items. No guarantees about Items: You can reach all unique inventory items. No guarantees about
reaching all locations or all keys. reaching all locations or all keys.
Locations: You will be able to reach every location in the game. Locations: You will be able to reach every location in the game.
None: You will be able to reach enough locations to beat the game. None: You will be able to reach enough locations to beat the game.
''') ''')
parser.add_argument('--hints', help='''\ parser.add_argument('--hints', default=defval(False), help='''\
Make telepathic tiles and storytellers give helpful hints. Make telepathic tiles and storytellers give helpful hints.
''', action='store_true') ''', action='store_true')
# included for backwards compatibility # included for backwards compatibility
parser.add_argument('--shuffleganon', help=argparse.SUPPRESS, action='store_true', default=True) parser.add_argument('--shuffleganon', help=argparse.SUPPRESS, action='store_true', default=defval(True))
parser.add_argument('--no-shuffleganon', help='''\ parser.add_argument('--no-shuffleganon', help='''\
If set, the Pyramid Hole and Ganon's Tower are not If set, the Pyramid Hole and Ganon's Tower are not
included entrance shuffle pool. included entrance shuffle pool.
''', action='store_false', dest='shuffleganon') ''', action='store_false', dest='shuffleganon')
parser.add_argument('--heartbeep', default='normal', const='normal', nargs='?', choices=['double', 'normal', 'half', 'quarter', 'off'], parser.add_argument('--heartbeep', default=defval('normal'), const='normal', nargs='?', choices=['double', 'normal', 'half', 'quarter', 'off'],
help='''\ help='''\
Select the rate at which the heart beep sound is played at Select the rate at which the heart beep sound is played at
low health. (default: %(default)s) low health. (default: %(default)s)
''') ''')
parser.add_argument('--heartcolor', default='red', const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow', 'random'], parser.add_argument('--heartcolor', default=defval('red'), const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow', 'random'],
help='Select the color of Link\'s heart meter. (default: %(default)s)') help='Select the color of Link\'s heart meter. (default: %(default)s)')
parser.add_argument('--ow_palettes', default=defval('default'), choices=['default', 'random', 'blackout'])
parser.add_argument('--uw_palettes', default=defval('default'), choices=['default', 'random', 'blackout'])
parser.add_argument('--sprite', help='''\ parser.add_argument('--sprite', help='''\
Path to a sprite sheet to use for Link. Needs to be in Path to a sprite sheet to use for Link. Needs to be in
binary format and have a length of 0x7000 (28672) bytes, binary format and have a length of 0x7000 (28672) bytes,
@@ -258,21 +270,49 @@ def start():
Output .json patch to stdout instead of a patched rom. Used Output .json patch to stdout instead of a patched rom. Used
for VT site integration, do not use otherwise. for VT site integration, do not use otherwise.
''') ''')
parser.add_argument('--skip_playthrough', action='store_true', default=False) parser.add_argument('--skip_playthrough', action='store_true', default=defval(False))
parser.add_argument('--enemizercli', default='') parser.add_argument('--enemizercli', default=defval('EnemizerCLI/EnemizerCLI.Core'))
parser.add_argument('--shufflebosses', default='none', choices=['none', 'basic', 'normal', 'chaos']) parser.add_argument('--shufflebosses', default=defval('none'), choices=['none', 'basic', 'normal', 'chaos'])
parser.add_argument('--shuffleenemies', default=False, action='store_true') parser.add_argument('--shuffleenemies', default=defval('none'), choices=['none', 'shuffled', 'chaos'])
parser.add_argument('--enemy_health', default='default', choices=['default', 'easy', 'normal', 'hard', 'expert']) parser.add_argument('--enemy_health', default=defval('default'), choices=['default', 'easy', 'normal', 'hard', 'expert'])
parser.add_argument('--enemy_damage', default='default', choices=['default', 'shuffled', 'chaos']) parser.add_argument('--enemy_damage', default=defval('default'), choices=['default', 'shuffled', 'chaos'])
parser.add_argument('--shufflepalette', default=False, action='store_true') parser.add_argument('--shufflepots', default=defval(False), action='store_true')
parser.add_argument('--shufflepots', default=False, action='store_true') parser.add_argument('--beemizer', default=defval(0), type=lambda value: min(max(int(value), 0), 4))
parser.add_argument('--multi', default=1, type=lambda value: min(max(int(value), 1), 255)) parser.add_argument('--multi', default=defval(1), type=lambda value: min(max(int(value), 1), 255))
parser.add_argument('--names', default=defval(''))
parser.add_argument('--outputpath') parser.add_argument('--outputpath')
args = parser.parse_args() parser.add_argument('--race', default=defval(False), action='store_true')
parser.add_argument('--outputname')
if args.outputpath and os.path.isdir(args.outputpath): if multiargs.multi:
output_path.cached_path = args.outputpath for player in range(1, multiargs.multi + 1):
parser.add_argument(f'--p{player}', default=defval(''), help=argparse.SUPPRESS)
ret = parser.parse_args(argv)
if ret.keysanity:
ret.mapshuffle, ret.compassshuffle, ret.keyshuffle, ret.bigkeyshuffle = [True] * 4
if multiargs.multi:
defaults = copy.deepcopy(ret)
for player in range(1, multiargs.multi + 1):
playerargs = parse_arguments(shlex.split(getattr(ret,f"p{player}")), True)
for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality',
'shuffle', 'door_shuffle', 'crystals_ganon', 'crystals_gt', 'openpyramid',
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
'retro', 'accessibility', 'hints', 'beemizer',
'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots',
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep']:
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
if player == 1:
setattr(ret, name, {1: value})
else:
getattr(ret, name)[player] = value
return ret
def start():
args = parse_arguments(None)
if is_bundled() and len(sys.argv) == 1: if is_bundled() and len(sys.argv) == 1:
# for the bundled builds, if we have no arguments, the user # for the bundled builds, if we have no arguments, the user
@@ -288,9 +328,9 @@ def start():
if not args.jsonout and not os.path.isfile(args.rom): if not args.jsonout and not os.path.isfile(args.rom):
input('Could not find valid base rom for patching at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.rom) input('Could not find valid base rom for patching at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.rom)
sys.exit(1) sys.exit(1)
if args.sprite is not None and not os.path.isfile(args.sprite): if any([sprite is not None and not os.path.isfile(sprite) and not get_sprite_from_name(sprite) for sprite in args.sprite.values()]):
if not args.jsonout: if not args.jsonout:
input('Could not find link sprite sheet at given location. \nPress Enter to exit.' % args.sprite) input('Could not find link sprite sheet at given location. \nPress Enter to exit.')
sys.exit(1) sys.exit(1)
else: else:
raise IOError('Cannot find sprite file at %s' % args.sprite) raise IOError('Cannot find sprite file at %s' % args.sprite)
+20 -19
View File
@@ -8,7 +8,7 @@ from Items import ItemFactory
def create_dungeons(world, player): def create_dungeons(world, player):
def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dungeon_items): def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dungeon_items):
dungeon = Dungeon(name, dungeon_regions, big_key, [] if world.retro else small_keys, dungeon_items, player) dungeon = Dungeon(name, dungeon_regions, big_key, [] if world.retro[player] else small_keys, dungeon_items, player)
dungeon.boss = BossFactory(default_boss, player) dungeon.boss = BossFactory(default_boss, player)
for region in dungeon.regions: for region in dungeon.regions:
world.get_region(region, player).dungeon = dungeon world.get_region(region, player).dungeon = dungeon
@@ -27,7 +27,7 @@ def create_dungeons(world, player):
MM = make_dungeon('Misery Mire', 'Vitreous', mire_regions, ItemFactory('Big Key (Misery Mire)', player), ItemFactory(['Small Key (Misery Mire)'] * 3, player), ItemFactory(['Map (Misery Mire)', 'Compass (Misery Mire)'], player)) MM = make_dungeon('Misery Mire', 'Vitreous', mire_regions, ItemFactory('Big Key (Misery Mire)', player), ItemFactory(['Small Key (Misery Mire)'] * 3, player), ItemFactory(['Map (Misery Mire)', 'Compass (Misery Mire)'], player))
TR = make_dungeon('Turtle Rock', 'Trinexx', tr_regions, ItemFactory('Big Key (Turtle Rock)', player), ItemFactory(['Small Key (Turtle Rock)'] * 4, player), ItemFactory(['Map (Turtle Rock)', 'Compass (Turtle Rock)'], player)) TR = make_dungeon('Turtle Rock', 'Trinexx', tr_regions, ItemFactory('Big Key (Turtle Rock)', player), ItemFactory(['Small Key (Turtle Rock)'] * 4, player), ItemFactory(['Map (Turtle Rock)', 'Compass (Turtle Rock)'], player))
if world.mode != 'inverted': if world.mode[player] != 'inverted':
AT = make_dungeon('Agahnims Tower', 'Agahnim', tower_regions, None, ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), []) AT = make_dungeon('Agahnims Tower', 'Agahnim', tower_regions, None, ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), [])
GT = make_dungeon('Ganons Tower', 'Agahnim2', gt_regions, ItemFactory('Big Key (Ganons Tower)', player), ItemFactory(['Small Key (Ganons Tower)'] * 4, player), ItemFactory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], player)) GT = make_dungeon('Ganons Tower', 'Agahnim2', gt_regions, ItemFactory('Big Key (Ganons Tower)', player), ItemFactory(['Small Key (Ganons Tower)'] * 4, player), ItemFactory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], player))
else: else:
@@ -47,7 +47,7 @@ def fill_dungeons(world):
for player in range(1, world.players + 1): for player in range(1, world.players + 1):
pinball_room = world.get_location('Skull Woods - Pinball Room', player) pinball_room = world.get_location('Skull Woods - Pinball Room', player)
if world.retro: if world.retro[player]:
world.push_item(pinball_room, ItemFactory('Small Key (Universal)', player), False) world.push_item(pinball_room, ItemFactory('Small Key (Universal)', player), False)
else: else:
world.push_item(pinball_room, ItemFactory('Small Key (Skull Woods)', player), False) world.push_item(pinball_room, ItemFactory('Small Key (Skull Woods)', player), False)
@@ -113,21 +113,20 @@ def fill_dungeons(world):
continue continue
# next place dungeon items # next place dungeon items
if world.place_dungeon_items: for dungeon_item in dungeon_items:
for dungeon_item in dungeon_items: di_location = dungeon_locations.pop()
di_location = dungeon_locations.pop() world.push_item(di_location, dungeon_item, False)
world.push_item(di_location, dungeon_item, False)
def get_dungeon_item_pool(world): def get_dungeon_item_pool(world):
return [item for dungeon in world.dungeons for item in dungeon.all_items if item.key or world.place_dungeon_items] return [item for dungeon in world.dungeons for item in dungeon.all_items]
def fill_dungeons_restrictive(world, shuffled_locations): def fill_dungeons_restrictive(world, shuffled_locations):
all_state_base = world.get_all_state() all_state_base = world.get_all_state()
# for player in range(1, world.players + 1): # for player in range(1, world.players + 1):
# pinball_room = world.get_location('Skull Woods - Pinball Room', player) # pinball_room = world.get_location('Skull Woods - Pinball Room', player)
# if world.retro: # if world.retro[player]:
# world.push_item(pinball_room, ItemFactory('Small Key (Universal)', player), False) # world.push_item(pinball_room, ItemFactory('Small Key (Universal)', player), False)
# else: # else:
# world.push_item(pinball_room, ItemFactory('Small Key (Skull Woods)', player), False) # world.push_item(pinball_room, ItemFactory('Small Key (Skull Woods)', player), False)
@@ -135,22 +134,24 @@ def fill_dungeons_restrictive(world, shuffled_locations):
# pinball_room.locked = True # pinball_room.locked = True
# shuffled_locations.remove(pinball_room) # shuffled_locations.remove(pinball_room)
if world.keysanity: # with shuffled dungeon items they are distributed as part of the normal item pool
#in keysanity dungeon items are distributed as part of the normal item pool for item in world.get_items():
for item in world.get_items(): if (item.smallkey and world.keyshuffle[item.player]) or (item.bigkey and world.bigkeyshuffle[item.player]):
if item.key: all_state_base.collect(item, True)
item.advancement = True item.advancement = True
elif item.map or item.compass: elif (item.map and world.mapshuffle[item.player]) or (item.compass and world.compassshuffle[item.player]):
item.priority = True item.priority = True
return
dungeon_items = get_dungeon_item_pool(world) dungeon_items = [item for item in get_dungeon_item_pool(world) if ((item.smallkey and not world.keyshuffle[item.player])
or (item.bigkey and not world.bigkeyshuffle[item.player])
or (item.map and not world.mapshuffle[item.player])
or (item.compass and not world.compassshuffle[item.player]))]
# sort in the order Big Key, Small Key, Other before placing dungeon items # sort in the order Big Key, Small Key, Other before placing dungeon items
sort_order = {"BigKey": 3, "SmallKey": 2} sort_order = {"BigKey": 3, "SmallKey": 2}
dungeon_items.sort(key=lambda item: sort_order.get(item.type, 1)) dungeon_items.sort(key=lambda item: sort_order.get(item.type, 1))
fill_restrictive(world, all_state_base, shuffled_locations, dungeon_items) fill_restrictive(world, all_state_base, shuffled_locations, dungeon_items, True)
dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A], dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A],
+23 -3
View File
@@ -8,6 +8,21 @@ Hints will appear in the following ratios across the 15 telepathic tiles that ha
5 hints for valuable items. 5 hints for valuable items.
4 junk hints. 4 junk hints.
In the vanilla, dungeonssimple, and dungeonsfull shuffles, the following ratios will be used instead:
5 hints for inconvenient item locations.
8 hints for valuable items.
7 junk hints.
In the simple, restricted, and restricted legacy shuffles, these are the ratios:
2 hints for inconvenient entrances.
1 hint for an inconvenient dungeon entrance.
4 hints for random entrances (this can by coincidence pick inconvenient entrances that aren't used for the first set of hints).
3 hints for inconvenient item locations.
5 hints for valuable items.
5 junk hints.
These hints will use the following format: These hints will use the following format:
Entrance hints go "[Entrance on overworld] leads to [interior]". Entrance hints go "[Entrance on overworld] leads to [interior]".
@@ -65,7 +80,12 @@ Spike Cave
Magic Bat Magic Bat
Sahasrahla (Green Pendant) Sahasrahla (Green Pendant)
Valuable Items are simply all items that are shown on the pause subscreen (Y, B, or A sections) minus Silver Arrows and plus Triforce Pieces, Magic Upgrades (1/2 or 1/4), and the Single Arrow. If keysanity is being used, you can additionally get hints for Small Keys or Big Keys but not hints for Maps or Compasses. In the vanilla, dungeonssimple, and dungeonsfull shuffles, the following two locations are added to the inconvenient locations list:
Graveyard Cave
Mimic Cave
Valuable Items are simply all items that are shown on the pause subscreen (Y, B, or A sections) minus Silver Arrows and plus Triforce Pieces, Magic Upgrades (1/2 or 1/4), and the Single Arrow. If key shuffle is being used, you can additionally get hints for Small Keys or Big Keys but not hints for Maps or Compasses.
While the exact verbage of location names and item names can be found in the source code, here's a copy for reference: While the exact verbage of location names and item names can be found in the source code, here's a copy for reference:
@@ -103,8 +123,8 @@ Death Mountain Return Cave (East): The westmost cave on west DM
Spectacle Rock Cave Peak: The highest cave on west DM Spectacle Rock Cave Peak: The highest cave on west DM
Spectacle Rock Cave: The right ledge on west DM Spectacle Rock Cave: The right ledge on west DM
Spectacle Rock Cave (Bottom): The left ledge on west DM Spectacle Rock Cave (Bottom): The left ledge on west DM
Paradox Cave (Bottom): The southmost cave on east DM Paradox Cave (Bottom): The right paired cave on east DM
Paradox Cave (Middle): The right paired cave on east DM Paradox Cave (Middle): The southmost cave on east DM
Paradox Cave (Top): The east DM summit cave Paradox Cave (Top): The east DM summit cave
Fairy Ascension Cave (Bottom): The east DM cave behind rocks Fairy Ascension Cave (Bottom): The east DM cave behind rocks
Fairy Ascension Cave (Top): The central ledge on east DM Fairy Ascension Cave (Top): The central ledge on east DM
+57 -87
View File
@@ -19,17 +19,17 @@ def link_entrances(world, player):
connect_simple(world, exitname, regionname, player) connect_simple(world, exitname, regionname, player)
# if we do not shuffle, set default connections # if we do not shuffle, set default connections
if world.shuffle == 'vanilla': if world.shuffle[player] == 'vanilla':
for exitname, regionname in default_connections: for exitname, regionname in default_connections:
connect_simple(world, exitname, regionname, player) connect_simple(world, exitname, regionname, player)
for exitname, regionname in default_dungeon_connections: for exitname, regionname in default_dungeon_connections:
connect_simple(world, exitname, regionname, player) connect_simple(world, exitname, regionname, player)
elif world.shuffle == 'dungeonssimple': elif world.shuffle[player] == 'dungeonssimple':
for exitname, regionname in default_connections: for exitname, regionname in default_connections:
connect_simple(world, exitname, regionname, player) connect_simple(world, exitname, regionname, player)
simple_shuffle_dungeons(world, player) simple_shuffle_dungeons(world, player)
elif world.shuffle == 'dungeonsfull': elif world.shuffle[player] == 'dungeonsfull':
for exitname, regionname in default_connections: for exitname, regionname in default_connections:
connect_simple(world, exitname, regionname, player) connect_simple(world, exitname, regionname, player)
@@ -39,7 +39,7 @@ def link_entrances(world, player):
lw_entrances = list(LW_Dungeon_Entrances) lw_entrances = list(LW_Dungeon_Entrances)
dw_entrances = list(DW_Dungeon_Entrances) dw_entrances = list(DW_Dungeon_Entrances)
if world.mode == 'standard': if world.mode[player] == 'standard':
# must connect front of hyrule castle to do escape # must connect front of hyrule castle to do escape
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
else: else:
@@ -52,14 +52,14 @@ def link_entrances(world, player):
dw_entrances.append('Ganons Tower') dw_entrances.append('Ganons Tower')
dungeon_exits.append('Ganons Tower Exit') dungeon_exits.append('Ganons Tower Exit')
if world.mode == 'standard': if world.mode[player] == 'standard':
# rest of hyrule castle must be in light world, so it has to be the one connected to east exit of desert # rest of hyrule castle must be in light world, so it has to be the one connected to east exit of desert
connect_mandatory_exits(world, lw_entrances, [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], list(LW_Dungeon_Entrances_Must_Exit), player) connect_mandatory_exits(world, lw_entrances, [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], list(LW_Dungeon_Entrances_Must_Exit), player)
else: else:
connect_mandatory_exits(world, lw_entrances, dungeon_exits, list(LW_Dungeon_Entrances_Must_Exit), player) connect_mandatory_exits(world, lw_entrances, dungeon_exits, list(LW_Dungeon_Entrances_Must_Exit), player)
connect_mandatory_exits(world, dw_entrances, dungeon_exits, list(DW_Dungeon_Entrances_Must_Exit), player) connect_mandatory_exits(world, dw_entrances, dungeon_exits, list(DW_Dungeon_Entrances_Must_Exit), player)
connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player) connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player)
elif world.shuffle == 'simple': elif world.shuffle[player] == 'simple':
simple_shuffle_dungeons(world, player) simple_shuffle_dungeons(world, player)
old_man_entrances = list(Old_Man_Entrances) old_man_entrances = list(Old_Man_Entrances)
@@ -130,7 +130,7 @@ def link_entrances(world, player):
# place remaining doors # place remaining doors
connect_doors(world, single_doors, door_targets, player) connect_doors(world, single_doors, door_targets, player)
elif world.shuffle == 'restricted': elif world.shuffle[player] == 'restricted':
simple_shuffle_dungeons(world, player) simple_shuffle_dungeons(world, player)
lw_entrances = list(LW_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances) lw_entrances = list(LW_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances)
@@ -201,7 +201,7 @@ def link_entrances(world, player):
# place remaining doors # place remaining doors
connect_doors(world, doors, door_targets, player) connect_doors(world, doors, door_targets, player)
elif world.shuffle == 'restricted_legacy': elif world.shuffle[player] == 'restricted_legacy':
simple_shuffle_dungeons(world, player) simple_shuffle_dungeons(world, player)
lw_entrances = list(LW_Entrances) lw_entrances = list(LW_Entrances)
@@ -256,7 +256,7 @@ def link_entrances(world, player):
# place remaining doors # place remaining doors
connect_doors(world, single_doors, door_targets, player) connect_doors(world, single_doors, door_targets, player)
elif world.shuffle == 'full': elif world.shuffle[player] == 'full':
skull_woods_shuffle(world, player) skull_woods_shuffle(world, player)
lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances) lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances)
@@ -273,7 +273,7 @@ def link_entrances(world, player):
# tavern back door cannot be shuffled yet # tavern back door cannot be shuffled yet
connect_doors(world, ['Tavern North'], ['Tavern'], player) connect_doors(world, ['Tavern North'], ['Tavern'], player)
if world.mode == 'standard': if world.mode[player] == 'standard':
# must connect front of hyrule castle to do escape # must connect front of hyrule castle to do escape
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
else: else:
@@ -309,7 +309,7 @@ def link_entrances(world, player):
pass pass
else: #if the cave wasn't placed we get here else: #if the cave wasn't placed we get here
connect_caves(world, lw_entrances, [], old_man_house, player) connect_caves(world, lw_entrances, [], old_man_house, player)
if world.mode == 'standard': if world.mode[player] == 'standard':
# rest of hyrule castle must be in light world # rest of hyrule castle must be in light world
connect_caves(world, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player) connect_caves(world, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player)
@@ -361,7 +361,7 @@ def link_entrances(world, player):
# place remaining doors # place remaining doors
connect_doors(world, doors, door_targets, player) connect_doors(world, doors, door_targets, player)
elif world.shuffle == 'crossed': elif world.shuffle[player] == 'crossed':
skull_woods_shuffle(world, player) skull_woods_shuffle(world, player)
entrances = list(LW_Entrances + LW_Dungeon_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances + DW_Entrances + DW_Dungeon_Entrances + DW_Single_Cave_Doors) entrances = list(LW_Entrances + LW_Dungeon_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances + DW_Entrances + DW_Dungeon_Entrances + DW_Single_Cave_Doors)
@@ -376,7 +376,7 @@ def link_entrances(world, player):
# tavern back door cannot be shuffled yet # tavern back door cannot be shuffled yet
connect_doors(world, ['Tavern North'], ['Tavern'], player) connect_doors(world, ['Tavern North'], ['Tavern'], player)
if world.mode == 'standard': if world.mode[player] == 'standard':
# must connect front of hyrule castle to do escape # must connect front of hyrule castle to do escape
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
else: else:
@@ -392,7 +392,7 @@ def link_entrances(world, player):
#place must-exit caves #place must-exit caves
connect_mandatory_exits(world, entrances, caves, must_exits, player) connect_mandatory_exits(world, entrances, caves, must_exits, player)
if world.mode == 'standard': if world.mode[player] == 'standard':
# rest of hyrule castle must be dealt with # rest of hyrule castle must be dealt with
connect_caves(world, entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player) connect_caves(world, entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player)
@@ -437,7 +437,7 @@ def link_entrances(world, player):
# place remaining doors # place remaining doors
connect_doors(world, entrances, door_targets, player) connect_doors(world, entrances, door_targets, player)
elif world.shuffle == 'full_legacy': elif world.shuffle[player] == 'full_legacy':
skull_woods_shuffle(world, player) skull_woods_shuffle(world, player)
lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + Old_Man_Entrances) lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + Old_Man_Entrances)
@@ -451,7 +451,7 @@ def link_entrances(world, player):
blacksmith_doors = list(Blacksmith_Single_Cave_Doors) blacksmith_doors = list(Blacksmith_Single_Cave_Doors)
door_targets = list(Single_Cave_Targets) door_targets = list(Single_Cave_Targets)
if world.mode == 'standard': if world.mode[player] == 'standard':
# must connect front of hyrule castle to do escape # must connect front of hyrule castle to do escape
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
else: else:
@@ -471,7 +471,7 @@ def link_entrances(world, player):
else: else:
connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player) connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player)
connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player) connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player)
if world.mode == 'standard': if world.mode[player] == 'standard':
# rest of hyrule castle must be in light world # rest of hyrule castle must be in light world
connect_caves(world, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player) connect_caves(world, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player)
@@ -513,7 +513,7 @@ def link_entrances(world, player):
# place remaining doors # place remaining doors
connect_doors(world, single_doors, door_targets, player) connect_doors(world, single_doors, door_targets, player)
elif world.shuffle == 'madness_legacy': elif world.shuffle[player] == 'madness_legacy':
# here lie dragons, connections are no longer two way # here lie dragons, connections are no longer two way
lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + Old_Man_Entrances) lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + Old_Man_Entrances)
dw_entrances = list(DW_Entrances + DW_Dungeon_Entrances) dw_entrances = list(DW_Entrances + DW_Dungeon_Entrances)
@@ -552,7 +552,7 @@ def link_entrances(world, player):
('Lumberjack Tree Exit', 'Lumberjack Tree (top)'), ('Lumberjack Tree Exit', 'Lumberjack Tree (top)'),
(('Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)'), 'Skull Woods Second Section (Drop)')] (('Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)'), 'Skull Woods Second Section (Drop)')]
if world.mode == 'standard': if world.mode[player] == 'standard':
# cannot move uncle cave # cannot move uncle cave
connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player) connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player)
connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player) connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player)
@@ -606,7 +606,7 @@ def link_entrances(world, player):
connect_entrance(world, hole, target, player) connect_entrance(world, hole, target, player)
# hyrule castle handling # hyrule castle handling
if world.mode == 'standard': if world.mode[player] == 'standard':
# must connect front of hyrule castle to do escape # must connect front of hyrule castle to do escape
connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player) connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player)
@@ -755,7 +755,7 @@ def link_entrances(world, player):
# place remaining doors # place remaining doors
connect_doors(world, single_doors, door_targets, player) connect_doors(world, single_doors, door_targets, player)
elif world.shuffle == 'insanity': elif world.shuffle[player] == 'insanity':
# beware ye who enter here # beware ye who enter here
entrances = LW_Entrances + LW_Dungeon_Entrances + DW_Entrances + DW_Dungeon_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave'] entrances = LW_Entrances + LW_Dungeon_Entrances + DW_Entrances + DW_Dungeon_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave']
@@ -792,7 +792,7 @@ def link_entrances(world, player):
# tavern back door cannot be shuffled yet # tavern back door cannot be shuffled yet
connect_doors(world, ['Tavern North'], ['Tavern'], player) connect_doors(world, ['Tavern North'], ['Tavern'], player)
if world.mode == 'standard': if world.mode[player] == 'standard':
# cannot move uncle cave # cannot move uncle cave
connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player) connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player)
connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player) connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player)
@@ -825,7 +825,7 @@ def link_entrances(world, player):
connect_entrance(world, hole, hole_targets.pop(), player) connect_entrance(world, hole, hole_targets.pop(), player)
# hyrule castle handling # hyrule castle handling
if world.mode == 'standard': if world.mode[player] == 'standard':
# must connect front of hyrule castle to do escape # must connect front of hyrule castle to do escape
connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player) connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player)
@@ -902,8 +902,8 @@ def link_entrances(world, player):
# place remaining doors # place remaining doors
connect_doors(world, doors, door_targets, player) connect_doors(world, doors, door_targets, player)
elif world.shuffle == 'insanity_legacy': elif world.shuffle[player] == 'insanity_legacy':
world.fix_fake_world = False world.fix_fake_world[player] = False
# beware ye who enter here # beware ye who enter here
entrances = LW_Entrances + LW_Dungeon_Entrances + DW_Entrances + DW_Dungeon_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave'] entrances = LW_Entrances + LW_Dungeon_Entrances + DW_Entrances + DW_Dungeon_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave']
@@ -927,7 +927,7 @@ def link_entrances(world, player):
hole_targets = ['Kakariko Well (top)', 'Bat Cave (right)', 'North Fairy Cave', 'Lost Woods Hideout (top)', 'Lumberjack Tree (top)', 'Sewer Drop', 'Skull Woods Second Section (Drop)', hole_targets = ['Kakariko Well (top)', 'Bat Cave (right)', 'North Fairy Cave', 'Lost Woods Hideout (top)', 'Lumberjack Tree (top)', 'Sewer Drop', 'Skull Woods Second Section (Drop)',
'Skull Woods First Section (Left)', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Top)'] 'Skull Woods First Section (Left)', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Top)']
if world.mode == 'standard': if world.mode[player] == 'standard':
# cannot move uncle cave # cannot move uncle cave
connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player) connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player)
connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player) connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player)
@@ -960,7 +960,7 @@ def link_entrances(world, player):
connect_entrance(world, hole, hole_targets.pop(), player) connect_entrance(world, hole, hole_targets.pop(), player)
# hyrule castle handling # hyrule castle handling
if world.mode == 'standard': if world.mode[player] == 'standard':
# must connect front of hyrule castle to do escape # must connect front of hyrule castle to do escape
connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player) connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player)
@@ -1079,17 +1079,17 @@ def link_inverted_entrances(world, player):
connect_simple(world, exitname, regionname, player) connect_simple(world, exitname, regionname, player)
# if we do not shuffle, set default connections # if we do not shuffle, set default connections
if world.shuffle == 'vanilla': if world.shuffle[player] == 'vanilla':
for exitname, regionname in inverted_default_connections: for exitname, regionname in inverted_default_connections:
connect_simple(world, exitname, regionname, player) connect_simple(world, exitname, regionname, player)
for exitname, regionname in inverted_default_dungeon_connections: for exitname, regionname in inverted_default_dungeon_connections:
connect_simple(world, exitname, regionname, player) connect_simple(world, exitname, regionname, player)
elif world.shuffle == 'dungeonssimple': elif world.shuffle[player] == 'dungeonssimple':
for exitname, regionname in inverted_default_connections: for exitname, regionname in inverted_default_connections:
connect_simple(world, exitname, regionname, player) connect_simple(world, exitname, regionname, player)
simple_shuffle_dungeons(world, player) simple_shuffle_dungeons(world, player)
elif world.shuffle == 'dungeonsfull': elif world.shuffle[player] == 'dungeonsfull':
for exitname, regionname in inverted_default_connections: for exitname, regionname in inverted_default_connections:
connect_simple(world, exitname, regionname, player) connect_simple(world, exitname, regionname, player)
@@ -1151,7 +1151,7 @@ def link_inverted_entrances(world, player):
remaining_lw_entrances = [i for i in all_dungeon_entrances if i in lw_entrances] remaining_lw_entrances = [i for i in all_dungeon_entrances if i in lw_entrances]
connect_caves(world, remaining_lw_entrances, remaining_dw_entrances, dungeon_exits, player) connect_caves(world, remaining_lw_entrances, remaining_dw_entrances, dungeon_exits, player)
elif world.shuffle == 'simple': elif world.shuffle[player] == 'simple':
simple_shuffle_dungeons(world, player) simple_shuffle_dungeons(world, player)
old_man_entrances = list(Inverted_Old_Man_Entrances) old_man_entrances = list(Inverted_Old_Man_Entrances)
@@ -1160,7 +1160,7 @@ def link_inverted_entrances(world, player):
single_doors = list(Single_Cave_Doors) single_doors = list(Single_Cave_Doors)
bomb_shop_doors = list(Inverted_Bomb_Shop_Single_Cave_Doors) bomb_shop_doors = list(Inverted_Bomb_Shop_Single_Cave_Doors)
blacksmith_doors = list(Inverted_Blacksmith_Single_Cave_Doors) blacksmith_doors = list(Blacksmith_Single_Cave_Doors)
door_targets = list(Inverted_Single_Cave_Targets) door_targets = list(Inverted_Single_Cave_Targets)
# we shuffle all 2 entrance caves as pairs as a start # we shuffle all 2 entrance caves as pairs as a start
@@ -1191,6 +1191,8 @@ def link_inverted_entrances(world, player):
bomb_shop_doors.remove(links_house) bomb_shop_doors.remove(links_house)
if links_house in blacksmith_doors: if links_house in blacksmith_doors:
blacksmith_doors.remove(links_house) blacksmith_doors.remove(links_house)
if links_house in old_man_entrances:
old_man_entrances.remove(links_house)
# place dark sanc # place dark sanc
sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in bomb_shop_doors] sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in bomb_shop_doors]
@@ -1243,7 +1245,7 @@ def link_inverted_entrances(world, player):
# place remaining doors # place remaining doors
connect_doors(world, single_doors, door_targets, player) connect_doors(world, single_doors, door_targets, player)
elif world.shuffle == 'restricted': elif world.shuffle[player] == 'restricted':
simple_shuffle_dungeons(world, player) simple_shuffle_dungeons(world, player)
lw_entrances = list(Inverted_LW_Entrances + Inverted_LW_Single_Cave_Doors) lw_entrances = list(Inverted_LW_Entrances + Inverted_LW_Single_Cave_Doors)
@@ -1253,7 +1255,7 @@ def link_inverted_entrances(world, player):
caves = list(Cave_Exits + Cave_Three_Exits + Old_Man_House) caves = list(Cave_Exits + Cave_Three_Exits + Old_Man_House)
single_doors = list(Single_Cave_Doors) single_doors = list(Single_Cave_Doors)
bomb_shop_doors = list(Inverted_Bomb_Shop_Single_Cave_Doors + Inverted_Bomb_Shop_Multi_Cave_Doors) bomb_shop_doors = list(Inverted_Bomb_Shop_Single_Cave_Doors + Inverted_Bomb_Shop_Multi_Cave_Doors)
blacksmith_doors = list(Inverted_Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors) blacksmith_doors = list(Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors)
door_targets = list(Inverted_Single_Cave_Targets) door_targets = list(Inverted_Single_Cave_Targets)
# place links house # place links house
@@ -1326,7 +1328,7 @@ def link_inverted_entrances(world, player):
doors = lw_entrances + dw_entrances doors = lw_entrances + dw_entrances
# place remaining doors # place remaining doors
connect_doors(world, doors, door_targets, player) connect_doors(world, doors, door_targets, player)
elif world.shuffle == 'full': elif world.shuffle[player] == 'full':
skull_woods_shuffle(world, player) skull_woods_shuffle(world, player)
lw_entrances = list(Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_LW_Single_Cave_Doors) lw_entrances = list(Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_LW_Single_Cave_Doors)
@@ -1335,7 +1337,7 @@ def link_inverted_entrances(world, player):
old_man_entrances = list(Inverted_Old_Man_Entrances + Old_Man_Entrances + ['Inverted Agahnims Tower', 'Tower of Hera']) old_man_entrances = list(Inverted_Old_Man_Entrances + Old_Man_Entrances + ['Inverted Agahnims Tower', 'Tower of Hera'])
caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits) # don't need to consider three exit caves, have one exit caves to avoid parity issues caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits) # don't need to consider three exit caves, have one exit caves to avoid parity issues
bomb_shop_doors = list(Inverted_Bomb_Shop_Single_Cave_Doors + Inverted_Bomb_Shop_Multi_Cave_Doors) bomb_shop_doors = list(Inverted_Bomb_Shop_Single_Cave_Doors + Inverted_Bomb_Shop_Multi_Cave_Doors)
blacksmith_doors = list(Inverted_Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors) blacksmith_doors = list(Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors)
door_targets = list(Inverted_Single_Cave_Targets) door_targets = list(Inverted_Single_Cave_Targets)
old_man_house = list(Old_Man_House) old_man_house = list(Old_Man_House)
@@ -1477,7 +1479,7 @@ def link_inverted_entrances(world, player):
# place remaining doors # place remaining doors
connect_doors(world, doors, door_targets, player) connect_doors(world, doors, door_targets, player)
elif world.shuffle == 'crossed': elif world.shuffle[player] == 'crossed':
skull_woods_shuffle(world, player) skull_woods_shuffle(world, player)
entrances = list(Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_LW_Single_Cave_Doors + Inverted_Old_Man_Entrances + Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_DW_Single_Cave_Doors) entrances = list(Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_LW_Single_Cave_Doors + Inverted_Old_Man_Entrances + Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_DW_Single_Cave_Doors)
@@ -1486,7 +1488,7 @@ def link_inverted_entrances(world, player):
old_man_entrances = list(Inverted_Old_Man_Entrances + Old_Man_Entrances + ['Inverted Agahnims Tower', 'Tower of Hera']) old_man_entrances = list(Inverted_Old_Man_Entrances + Old_Man_Entrances + ['Inverted Agahnims Tower', 'Tower of Hera'])
caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits + Old_Man_House) # don't need to consider three exit caves, have one exit caves to avoid parity issues caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits + Old_Man_House) # don't need to consider three exit caves, have one exit caves to avoid parity issues
bomb_shop_doors = list(Inverted_Bomb_Shop_Single_Cave_Doors + Inverted_Bomb_Shop_Multi_Cave_Doors) bomb_shop_doors = list(Inverted_Bomb_Shop_Single_Cave_Doors + Inverted_Bomb_Shop_Multi_Cave_Doors)
blacksmith_doors = list(Inverted_Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors) blacksmith_doors = list(Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors)
door_targets = list(Inverted_Single_Cave_Targets) door_targets = list(Inverted_Single_Cave_Targets)
# randomize which desert ledge door is a must-exit # randomize which desert ledge door is a must-exit
@@ -1587,7 +1589,7 @@ def link_inverted_entrances(world, player):
# place remaining doors # place remaining doors
connect_doors(world, entrances, door_targets, player) connect_doors(world, entrances, door_targets, player)
elif world.shuffle == 'insanity': elif world.shuffle[player] == 'insanity':
# beware ye who enter here # beware ye who enter here
entrances = Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_Old_Man_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave', 'Hyrule Castle Entrance (South)'] entrances = Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_Old_Man_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave', 'Hyrule Castle Entrance (South)']
@@ -1609,8 +1611,8 @@ def link_inverted_entrances(world, player):
# and rentering to find bomb shop. However appended list here is all those that we currently have # and rentering to find bomb shop. However appended list here is all those that we currently have
# bomb shop logic for. # bomb shop logic for.
# Specifically we could potentially add: 'Dark Death Mountain Ledge (East)' and doors associated with pits # Specifically we could potentially add: 'Dark Death Mountain Ledge (East)' and doors associated with pits
bomb_shop_doors = list(Inverted_Bomb_Shop_Single_Cave_Doors + Inverted_Bomb_Shop_Multi_Cave_Doors + ['Desert Palace Entrance (East)', 'Turtle Rock Isolated Ledge Entrance', 'Bumper Cave (Top)', 'Hookshot Cave Back Entrance']) bomb_shop_doors = list(Inverted_Bomb_Shop_Single_Cave_Doors + Inverted_Bomb_Shop_Multi_Cave_Doors + ['Turtle Rock Isolated Ledge Entrance', 'Bumper Cave (Top)', 'Hookshot Cave Back Entrance'])
blacksmith_doors = list(Inverted_Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors) blacksmith_doors = list(Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors)
door_targets = list(Inverted_Single_Cave_Targets) door_targets = list(Inverted_Single_Cave_Targets)
random.shuffle(doors) random.shuffle(doors)
@@ -1831,7 +1833,7 @@ def scramble_holes(world, player):
else: else:
hole_targets.append(('Pyramid Exit', 'Pyramid')) hole_targets.append(('Pyramid Exit', 'Pyramid'))
if world.mode == 'standard': if world.mode[player] == 'standard':
# cannot move uncle cave # cannot move uncle cave
connect_two_way(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player) connect_two_way(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player)
connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player) connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player)
@@ -1840,14 +1842,14 @@ def scramble_holes(world, player):
hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance')) hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance'))
# do not shuffle sanctuary into pyramid hole unless shuffle is crossed # do not shuffle sanctuary into pyramid hole unless shuffle is crossed
if world.shuffle == 'crossed': if world.shuffle[player] == 'crossed':
hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
if world.shuffle_ganon: if world.shuffle_ganon:
random.shuffle(hole_targets) random.shuffle(hole_targets)
exit, target = hole_targets.pop() exit, target = hole_targets.pop()
connect_two_way(world, 'Pyramid Entrance', exit, player) connect_two_way(world, 'Pyramid Entrance', exit, player)
connect_entrance(world, 'Pyramid Hole', target, player) connect_entrance(world, 'Pyramid Hole', target, player)
if world.shuffle != 'crossed': if world.shuffle[player] != 'crossed':
hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
random.shuffle(hole_targets) random.shuffle(hole_targets)
@@ -1882,14 +1884,14 @@ def scramble_inverted_holes(world, player):
hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance')) hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance'))
# do not shuffle sanctuary into pyramid hole unless shuffle is crossed # do not shuffle sanctuary into pyramid hole unless shuffle is crossed
if world.shuffle == 'crossed': if world.shuffle[player] == 'crossed':
hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
if world.shuffle_ganon: if world.shuffle_ganon:
random.shuffle(hole_targets) random.shuffle(hole_targets)
exit, target = hole_targets.pop() exit, target = hole_targets.pop()
connect_two_way(world, 'Inverted Pyramid Entrance', exit, player) connect_two_way(world, 'Inverted Pyramid Entrance', exit, player)
connect_entrance(world, 'Inverted Pyramid Hole', target, player) connect_entrance(world, 'Inverted Pyramid Hole', target, player)
if world.shuffle != 'crossed': if world.shuffle[player] != 'crossed':
hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
random.shuffle(hole_targets) random.shuffle(hole_targets)
@@ -1931,11 +1933,11 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player, dp_m
if len(cave) == 2: if len(cave) == 2:
entrance = entrances.pop() entrance = entrances.pop()
# ToDo Better solution, this is a hot fix. Do not connect both sides of trock/desert ledge only to each other # ToDo Better solution, this is a hot fix. Do not connect both sides of trock/desert ledge only to each other
if world.mode != 'inverted' and entrance == 'Dark Death Mountain Ledge (West)': if world.mode[player] != 'inverted' and entrance == 'Dark Death Mountain Ledge (West)':
new_entrance = entrances.pop() new_entrance = entrances.pop()
entrances.append(entrance) entrances.append(entrance)
entrance = new_entrance entrance = new_entrance
if world.mode == 'inverted' and entrance == dp_must_exit: if world.mode[player] == 'inverted' and entrance == dp_must_exit:
new_entrance = entrances.pop() new_entrance = entrances.pop()
entrances.append(entrance) entrances.append(entrance)
entrance = new_entrance entrance = new_entrance
@@ -2006,7 +2008,7 @@ def simple_shuffle_dungeons(world, player):
dungeon_entrances = ['Eastern Palace', 'Tower of Hera', 'Thieves Town', 'Skull Woods Final Section', 'Palace of Darkness', 'Ice Palace', 'Misery Mire', 'Swamp Palace'] dungeon_entrances = ['Eastern Palace', 'Tower of Hera', 'Thieves Town', 'Skull Woods Final Section', 'Palace of Darkness', 'Ice Palace', 'Misery Mire', 'Swamp Palace']
dungeon_exits = ['Eastern Palace Exit', 'Tower of Hera Exit', 'Thieves Town Exit', 'Skull Woods Final Section Exit', 'Palace of Darkness Exit', 'Ice Palace Exit', 'Misery Mire Exit', 'Swamp Palace Exit'] dungeon_exits = ['Eastern Palace Exit', 'Tower of Hera Exit', 'Thieves Town Exit', 'Skull Woods Final Section Exit', 'Palace of Darkness Exit', 'Ice Palace Exit', 'Misery Mire Exit', 'Swamp Palace Exit']
if world.mode != 'inverted': if world.mode[player] != 'inverted':
if not world.shuffle_ganon: if not world.shuffle_ganon:
connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player) connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player)
else: else:
@@ -2021,13 +2023,13 @@ def simple_shuffle_dungeons(world, player):
# mix up 4 door dungeons # mix up 4 door dungeons
multi_dungeons = ['Desert', 'Turtle Rock'] multi_dungeons = ['Desert', 'Turtle Rock']
if world.mode == 'open' or (world.mode == 'inverted' and world.shuffle_ganon): if world.mode[player] == 'open' or (world.mode[player] == 'inverted' and world.shuffle_ganon):
multi_dungeons.append('Hyrule Castle') multi_dungeons.append('Hyrule Castle')
random.shuffle(multi_dungeons) random.shuffle(multi_dungeons)
dp_target = multi_dungeons[0] dp_target = multi_dungeons[0]
tr_target = multi_dungeons[1] tr_target = multi_dungeons[1]
if world.mode not in ['open', 'inverted'] or (world.mode == 'inverted' and world.shuffle_ganon is False): if world.mode[player] not in ['open', 'inverted'] or (world.mode[player] == 'inverted' and world.shuffle_ganon is False):
# place hyrule castle as intended # place hyrule castle as intended
hc_target = 'Hyrule Castle' hc_target = 'Hyrule Castle'
else: else:
@@ -2035,7 +2037,7 @@ def simple_shuffle_dungeons(world, player):
# ToDo improve this? # ToDo improve this?
if world.mode != 'inverted': if world.mode[player] != 'inverted':
if hc_target == 'Hyrule Castle': if hc_target == 'Hyrule Castle':
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Exit (East)', player) connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Exit (East)', player)
@@ -2648,8 +2650,6 @@ Inverted_Bomb_Shop_Multi_Cave_Doors = ['Hyrule Castle Entrance (South)',
'Desert Palace Entrance (West)', 'Desert Palace Entrance (West)',
'Desert Palace Entrance (North)'] 'Desert Palace Entrance (North)']
Inverted_Blacksmith_Multi_Cave_Doors = [] # same as non-inverted
Inverted_LW_Single_Cave_Doors = LW_Single_Cave_Doors + ['Inverted Big Bomb Shop'] Inverted_LW_Single_Cave_Doors = LW_Single_Cave_Doors + ['Inverted Big Bomb Shop']
Inverted_DW_Single_Cave_Doors = ['Bonk Fairy (Dark)', Inverted_DW_Single_Cave_Doors = ['Bonk Fairy (Dark)',
@@ -2717,39 +2717,8 @@ Inverted_Bomb_Shop_Single_Cave_Doors = ['Waterfall of Wishing',
'Bumper Cave (Top)', 'Bumper Cave (Top)',
'Mimic Cave', 'Mimic Cave',
'Dark Lake Hylia Shop', 'Dark Lake Hylia Shop',
'Inverted Links House'] 'Inverted Links House',
'Inverted Big Bomb Shop']
Inverted_Blacksmith_Single_Cave_Doors = ['Blinds Hideout',
'Lake Hylia Fairy',
'Light Hype Fairy',
'Desert Fairy',
'Chicken House',
'Aginahs Cave',
'Sahasrahlas Hut',
'Cave Shop (Lake Hylia)',
'Blacksmiths Hut',
'Sick Kids House',
'Lost Woods Gamble',
'Fortune Teller (Light)',
'Snitch Lady (East)',
'Snitch Lady (West)',
'Bush Covered House',
'Tavern (Front)',
'Light World Bomb Hut',
'Kakariko Shop',
'Mini Moldorm Cave',
'Long Fairy Cave',
'Good Bee Cave',
'20 Rupee Cave',
'50 Rupee Cave',
'Ice Rod Cave',
'Library',
'Potion Shop',
'Dam',
'Lumberjack House',
'Lake Hylia Fortune Teller',
'Kakariko Gamble Game',
'Inverted Big Bomb Shop']
Inverted_Single_Cave_Targets = ['Blinds Hideout', Inverted_Single_Cave_Targets = ['Blinds Hideout',
@@ -2951,6 +2920,7 @@ mandatory_connections = [('Lake Hylia Central Island Pier', 'Lake Hylia Central
] ]
inverted_mandatory_connections = [('Lake Hylia Central Island Pier', 'Lake Hylia Central Island'), inverted_mandatory_connections = [('Lake Hylia Central Island Pier', 'Lake Hylia Central Island'),
('Lake Hylia Island', 'Lake Hylia Island'),
('Zoras River', 'Zoras River'), ('Zoras River', 'Zoras River'),
('Kings Grave Outer Rocks', 'Kings Grave Area'), ('Kings Grave Outer Rocks', 'Kings Grave Area'),
('Kings Grave Inner Rocks', 'Light World'), ('Kings Grave Inner Rocks', 'Light World'),
+84 -70
View File
@@ -161,7 +161,7 @@ def distribute_items_staleness(world):
logging.getLogger('').debug('Unplaced items: %s - Unfilled Locations: %s', [item.name for item in itempool], [location.name for location in fill_locations]) logging.getLogger('').debug('Unplaced items: %s - Unfilled Locations: %s', [item.name for item in itempool], [location.name for location in fill_locations])
def fill_restrictive(world, base_state, locations, itempool): def fill_restrictive(world, base_state, locations, itempool, single_player_placement = False):
def sweep_from_pool(): def sweep_from_pool():
new_state = base_state.copy() new_state = base_state.copy()
for item in itempool: for item in itempool:
@@ -169,53 +169,60 @@ def fill_restrictive(world, base_state, locations, itempool):
new_state.sweep_for_events() new_state.sweep_for_events()
return new_state return new_state
while itempool and locations: unplaced_items = []
items_to_place = []
nextpool = []
placing_players = set()
for item in reversed(itempool):
if item.player not in placing_players:
placing_players.add(item.player)
items_to_place.append(item)
else:
nextpool.insert(0, item)
itempool = nextpool
maximum_exploration_state = sweep_from_pool() no_access_checks = {}
reachable_items = {}
for item in itempool:
if world.accessibility[item.player] == 'none':
no_access_checks.setdefault(item.player, []).append(item)
else:
reachable_items.setdefault(item.player, []).append(item)
perform_access_check = True for player_items in [no_access_checks, reachable_items]:
if world.accessibility == 'none': while any(player_items.values()) and locations:
perform_access_check = not world.has_beaten_game(maximum_exploration_state) items_to_place = [[itempool.remove(items[-1]), items.pop()][-1] for items in player_items.values() if items]
for item_to_place in items_to_place: maximum_exploration_state = sweep_from_pool()
spot_to_fill = None has_beaten_game = world.has_beaten_game(maximum_exploration_state)
for location in locations:
if item_to_place.key: # a better test to see if a key can go there
location.item = item_to_place
test_state = maximum_exploration_state.copy()
test_state.stale[item_to_place.player] = True
else:
test_state = maximum_exploration_state
if location.can_fill(test_state, item_to_place, perform_access_check):
spot_to_fill = location
break
elif item_to_place.key:
location.item = None
if spot_to_fill is None: for item_to_place in items_to_place:
# we filled all reachable spots. Maybe the game can be beaten anyway? perform_access_check = True
if world.can_beat_game(): if world.accessibility[item_to_place.player] == 'none':
if world.accessibility != 'none': perform_access_check = not world.has_beaten_game(maximum_exploration_state, item_to_place.player) if single_player_placement else not has_beaten_game
logging.getLogger('').warning('Not all items placed. Game beatable anyway. (Could not place %s)' % item_to_place)
continue
raise FillError('No more spots to place %s' % item_to_place)
world.push_item(spot_to_fill, item_to_place, False) spot_to_fill = None
locations.remove(spot_to_fill)
spot_to_fill.event = True
for location in locations:
if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there
location.item = item_to_place
test_state = maximum_exploration_state.copy()
test_state.stale[item_to_place.player] = True
else:
test_state = maximum_exploration_state
if (not single_player_placement or location.player == item_to_place.player)\
and location.can_fill(test_state, item_to_place, perform_access_check):
spot_to_fill = location
break
elif item_to_place.smallkey or item_to_place.bigkey:
location.item = None
def distribute_items_restrictive(world, gftower_trash_count=0, fill_locations=None): if spot_to_fill is None:
# we filled all reachable spots. Maybe the game can be beaten anyway?
unplaced_items.insert(0, item_to_place)
if world.can_beat_game():
if world.accessibility[item_to_place.player] != 'none':
logging.getLogger('').warning('Not all items placed. Game beatable anyway. (Could not place %s)' % item_to_place)
continue
raise FillError('No more spots to place %s' % item_to_place)
world.push_item(spot_to_fill, item_to_place, False)
locations.remove(spot_to_fill)
spot_to_fill.event = True
itempool.extend(unplaced_items)
def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None):
# If not passed in, then get a shuffled list of locations to fill in # If not passed in, then get a shuffled list of locations to fill in
if not fill_locations: if not fill_locations:
fill_locations = world.get_unfilled_locations() fill_locations = world.get_unfilled_locations()
@@ -229,23 +236,26 @@ def distribute_items_restrictive(world, gftower_trash_count=0, fill_locations=No
# fill in gtower locations with trash first # fill in gtower locations with trash first
for player in range(1, world.players + 1): for player in range(1, world.players + 1):
if world.ganonstower_vanilla[player]: if not gftower_trash or not world.ganonstower_vanilla[player]:
gtower_locations = [location for location in fill_locations if 'Ganons Tower' in location.name and location.player == player] continue
random.shuffle(gtower_locations)
trashcnt = 0 gftower_trash_count = (random.randint(15, 50) if world.goal[player] == 'triforcehunt' else random.randint(0, 15))
while gtower_locations and restitempool and trashcnt < gftower_trash_count:
spot_to_fill = gtower_locations.pop() gtower_locations = [location for location in fill_locations if 'Ganons Tower' in location.name and location.player == player]
item_to_place = restitempool.pop() random.shuffle(gtower_locations)
world.push_item(spot_to_fill, item_to_place, False) trashcnt = 0
fill_locations.remove(spot_to_fill) while gtower_locations and restitempool and trashcnt < gftower_trash_count:
trashcnt += 1 spot_to_fill = gtower_locations.pop()
item_to_place = restitempool.pop()
world.push_item(spot_to_fill, item_to_place, False)
fill_locations.remove(spot_to_fill)
trashcnt += 1
random.shuffle(fill_locations) random.shuffle(fill_locations)
fill_locations.reverse() fill_locations.reverse()
# Make sure the escape small key is placed first in standard keysanity to prevent running out of spots # Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots
if world.keysanity and world.mode == 'standard': progitempool.sort(key=lambda item: 1 if item.name == 'Small Key (Escape)' and world.mode[item.player] == 'standard' and world.keyshuffle[item.player] else 0)
progitempool.sort(key=lambda item: 1 if item.name == 'Small Key (Escape)' else 0)
fill_restrictive(world, world.state, fill_locations, progitempool) fill_restrictive(world, world.state, fill_locations, progitempool)
@@ -315,7 +325,7 @@ def flood_items(world):
location_list = world.get_reachable_locations() location_list = world.get_reachable_locations()
random.shuffle(location_list) random.shuffle(location_list)
for location in location_list: for location in location_list:
if location.item is not None and not location.item.advancement and not location.item.priority and not location.item.key: if location.item is not None and not location.item.advancement and not location.item.priority and not location.item.smallkey and not location.item.bigkey:
# safe to replace # safe to replace
replace_item = location.item replace_item = location.item
replace_item.location = None replace_item.location = None
@@ -335,8 +345,7 @@ def balance_multiworld_progression(world):
reachable_locations_count[player] = 0 reachable_locations_count[player] = 0
def get_sphere_locations(sphere_state, locations): def get_sphere_locations(sphere_state, locations):
if not world.keysanity: sphere_state.sweep_for_events(key_only=True, locations=locations)
sphere_state.sweep_for_events(key_only=True, locations=locations)
return [loc for loc in locations if sphere_state.can_reach(loc)] return [loc for loc in locations if sphere_state.can_reach(loc)]
while True: while True:
@@ -346,8 +355,7 @@ def balance_multiworld_progression(world):
reachable_locations_count[location.player] += 1 reachable_locations_count[location.player] += 1
if checked_locations: if checked_locations:
average_reachable_locations = sum(reachable_locations_count.values()) / world.players threshold = max(reachable_locations_count.values()) - 20
threshold = ((average_reachable_locations + max(reachable_locations_count.values())) / 2) * 0.8 #todo: probably needs some tweaking
balancing_players = [player for player, reachables in reachable_locations_count.items() if reachables < threshold] balancing_players = [player for player, reachables in reachable_locations_count.items() if reachables < threshold]
if balancing_players: if balancing_players:
@@ -358,9 +366,9 @@ def balance_multiworld_progression(world):
candidate_items = [] candidate_items = []
while True: while True:
for location in balancing_sphere: for location in balancing_sphere:
if location.event: if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey):
balancing_state.collect(location.item, True, location) balancing_state.collect(location.item, True, location)
if location.item.player in balancing_players: if location.item.player in balancing_players and not location.locked:
candidate_items.append(location) candidate_items.append(location)
balancing_sphere = get_sphere_locations(balancing_state, balancing_unchecked_locations) balancing_sphere = get_sphere_locations(balancing_state, balancing_unchecked_locations)
for location in balancing_sphere: for location in balancing_sphere:
@@ -368,11 +376,14 @@ def balance_multiworld_progression(world):
balancing_reachables[location.player] += 1 balancing_reachables[location.player] += 1
if world.has_beaten_game(balancing_state) or all([reachables >= threshold for reachables in balancing_reachables.values()]): if world.has_beaten_game(balancing_state) or all([reachables >= threshold for reachables in balancing_reachables.values()]):
break break
elif not balancing_sphere:
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.')
unlocked_locations = [l for l in unchecked_locations if l not in balancing_unchecked_locations] unlocked_locations = [l for l in unchecked_locations if l not in balancing_unchecked_locations]
items_to_replace = [] items_to_replace = []
for player in balancing_players: for player in balancing_players:
locations_to_test = [l for l in unlocked_locations if l.player == player] locations_to_test = [l for l in unlocked_locations if l.player == player]
# only replace items that end up in another player's world
items_to_test = [l for l in candidate_items if l.item.player == player and l.player != player] items_to_test = [l for l in candidate_items if l.item.player == player and l.player != player]
while items_to_test: while items_to_test:
testing = items_to_test.pop() testing = items_to_test.pop()
@@ -382,9 +393,6 @@ def balance_multiworld_progression(world):
reducing_state.sweep_for_events(locations=locations_to_test) reducing_state.sweep_for_events(locations=locations_to_test)
if testing.locked:
continue
if world.has_beaten_game(balancing_state): if world.has_beaten_game(balancing_state):
if not world.has_beaten_game(reducing_state): if not world.has_beaten_game(reducing_state):
items_to_replace.append(testing) items_to_replace.append(testing)
@@ -394,13 +402,17 @@ def balance_multiworld_progression(world):
items_to_replace.append(testing) items_to_replace.append(testing)
replaced_items = False replaced_items = False
locations_for_replacing = [l for l in checked_locations if not l.event and not l.locked] replacement_locations = [l for l in checked_locations if not l.event and not l.locked]
while locations_for_replacing and items_to_replace: while replacement_locations and items_to_replace:
new_location = locations_for_replacing.pop() new_location = replacement_locations.pop()
old_location = items_to_replace.pop() old_location = items_to_replace.pop()
while not new_location.can_fill(state, old_location.item, False) or (new_location.item and not old_location.can_fill(state, new_location.item, False)):
replacement_locations.insert(0, new_location)
new_location = replacement_locations.pop()
new_location.item, old_location.item = old_location.item, new_location.item new_location.item, old_location.item = old_location.item, new_location.item
new_location.event = True new_location.event, old_location.event = True, False
old_location.event = False
state.collect(new_location.item, True, new_location) state.collect(new_location.item, True, new_location)
replaced_items = True replaced_items = True
if replaced_items: if replaced_items:
@@ -410,9 +422,11 @@ def balance_multiworld_progression(world):
sphere_locations.append(location) sphere_locations.append(location)
for location in sphere_locations: for location in sphere_locations:
if location.event: if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey):
state.collect(location.item, True, location) state.collect(location.item, True, location)
checked_locations.extend(sphere_locations) checked_locations.extend(sphere_locations)
if world.has_beaten_game(state): if world.has_beaten_game(state):
break break
elif not sphere_locations:
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.')
+276 -164
View File
@@ -2,6 +2,7 @@
from argparse import Namespace from argparse import Namespace
from glob import glob from glob import glob
import json import json
import logging
import random import random
import os import os
import shutil import shutil
@@ -10,10 +11,11 @@ from urllib.parse import urlparse
from urllib.request import urlopen from urllib.request import urlopen
from AdjusterMain import adjust from AdjusterMain import adjust
from DungeonRandomizer import parse_arguments
from GuiUtils import ToolTips, set_icon, BackgroundTaskProgress from GuiUtils import ToolTips, set_icon, BackgroundTaskProgress
from Main import main, __version__ as ESVersion from Main import main, __version__ as ESVersion
from Rom import Sprite from Rom import Sprite
from Utils import is_bundled, local_path, output_path, open_file from Utils import is_bundled, local_path, output_path, open_file, parse_names_string
def guiMain(args=None): def guiMain(args=None):
@@ -58,16 +60,20 @@ def guiMain(args=None):
createSpoilerCheckbutton = Checkbutton(checkBoxFrame, text="Create Spoiler Log", variable=createSpoilerVar) createSpoilerCheckbutton = Checkbutton(checkBoxFrame, text="Create Spoiler Log", variable=createSpoilerVar)
suppressRomVar = IntVar() suppressRomVar = IntVar()
suppressRomCheckbutton = Checkbutton(checkBoxFrame, text="Do not create patched Rom", variable=suppressRomVar) suppressRomCheckbutton = Checkbutton(checkBoxFrame, text="Do not create patched Rom", variable=suppressRomVar)
quickSwapVar = IntVar() openpyramidVar = IntVar()
quickSwapCheckbutton = Checkbutton(checkBoxFrame, text="Enabled L/R Item quickswapping", variable=quickSwapVar) openpyramidCheckbutton = Checkbutton(checkBoxFrame, text="Pre-open Pyramid Hole", variable=openpyramidVar)
keysanityVar = IntVar() mcsbshuffleFrame = Frame(checkBoxFrame)
keysanityCheckbutton = Checkbutton(checkBoxFrame, text="Keysanity (keys anywhere)", variable=keysanityVar) mcsbLabel = Label(mcsbshuffleFrame, text="Shuffle: ")
mapshuffleVar = IntVar()
mapshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="Maps", variable=mapshuffleVar)
compassshuffleVar = IntVar()
compassshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="Compasses", variable=compassshuffleVar)
keyshuffleVar = IntVar()
keyshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="Keys", variable=keyshuffleVar)
bigkeyshuffleVar = IntVar()
bigkeyshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="BigKeys", variable=bigkeyshuffleVar)
retroVar = IntVar() retroVar = IntVar()
retroCheckbutton = Checkbutton(checkBoxFrame, text="Retro mode (universal keys)", variable=retroVar) retroCheckbutton = Checkbutton(checkBoxFrame, text="Retro mode (universal keys)", variable=retroVar)
dungeonItemsVar = IntVar()
dungeonItemsCheckbutton = Checkbutton(checkBoxFrame, text="Place Dungeon Items (Compasses/Maps)", onvalue=0, offvalue=1, variable=dungeonItemsVar)
disableMusicVar = IntVar()
disableMusicCheckbutton = Checkbutton(checkBoxFrame, text="Disable game music", variable=disableMusicVar)
shuffleGanonVar = IntVar() shuffleGanonVar = IntVar()
shuffleGanonVar.set(1) #set default shuffleGanonVar.set(1) #set default
shuffleGanonCheckbutton = Checkbutton(checkBoxFrame, text="Include Ganon's Tower and Pyramid Hole in shuffle pool", variable=shuffleGanonVar) shuffleGanonCheckbutton = Checkbutton(checkBoxFrame, text="Include Ganon's Tower and Pyramid Hole in shuffle pool", variable=shuffleGanonVar)
@@ -79,61 +85,31 @@ def guiMain(args=None):
createSpoilerCheckbutton.pack(expand=True, anchor=W) createSpoilerCheckbutton.pack(expand=True, anchor=W)
suppressRomCheckbutton.pack(expand=True, anchor=W) suppressRomCheckbutton.pack(expand=True, anchor=W)
quickSwapCheckbutton.pack(expand=True, anchor=W) openpyramidCheckbutton.pack(expand=True, anchor=W)
keysanityCheckbutton.pack(expand=True, anchor=W) mcsbshuffleFrame.pack(expand=True, anchor=W)
mcsbLabel.grid(row=0, column=0)
mapshuffleCheckbutton.grid(row=0, column=1)
compassshuffleCheckbutton.grid(row=0, column=2)
keyshuffleCheckbutton.grid(row=0, column=3)
bigkeyshuffleCheckbutton.grid(row=0, column=4)
retroCheckbutton.pack(expand=True, anchor=W) retroCheckbutton.pack(expand=True, anchor=W)
dungeonItemsCheckbutton.pack(expand=True, anchor=W)
disableMusicCheckbutton.pack(expand=True, anchor=W)
shuffleGanonCheckbutton.pack(expand=True, anchor=W) shuffleGanonCheckbutton.pack(expand=True, anchor=W)
hintsCheckbutton.pack(expand=True, anchor=W) hintsCheckbutton.pack(expand=True, anchor=W)
customCheckbutton.pack(expand=True, anchor=W) customCheckbutton.pack(expand=True, anchor=W)
fileDialogFrame = Frame(rightHalfFrame) romOptionsFrame = LabelFrame(rightHalfFrame, text="Rom options")
romOptionsFrame.columnconfigure(0, weight=1)
romOptionsFrame.columnconfigure(1, weight=1)
for i in range(5):
romOptionsFrame.rowconfigure(i, weight=1)
heartbeepFrame = Frame(fileDialogFrame) disableMusicVar = IntVar()
heartbeepVar = StringVar() disableMusicCheckbutton = Checkbutton(romOptionsFrame, text="Disable music", variable=disableMusicVar)
heartbeepVar.set('normal') disableMusicCheckbutton.grid(row=0, column=0, sticky=E)
heartbeepOptionMenu = OptionMenu(heartbeepFrame, heartbeepVar, 'double', 'normal', 'half', 'quarter', 'off')
heartbeepOptionMenu.pack(side=RIGHT)
heartbeepLabel = Label(heartbeepFrame, text='Heartbeep sound rate')
heartbeepLabel.pack(side=LEFT, padx=(0,52))
heartcolorFrame = Frame(fileDialogFrame) spriteDialogFrame = Frame(romOptionsFrame)
heartcolorVar = StringVar() spriteDialogFrame.grid(row=0, column=1)
heartcolorVar.set('red') baseSpriteLabel = Label(spriteDialogFrame, text='Sprite:')
heartcolorOptionMenu = OptionMenu(heartcolorFrame, heartcolorVar, 'red', 'blue', 'green', 'yellow', 'random')
heartcolorOptionMenu.pack(side=RIGHT)
heartcolorLabel = Label(heartcolorFrame, text='Heart color')
heartcolorLabel.pack(side=LEFT, padx=(0,127))
fastMenuFrame = Frame(fileDialogFrame)
fastMenuVar = StringVar()
fastMenuVar.set('normal')
fastMenuOptionMenu = OptionMenu(fastMenuFrame, fastMenuVar, 'normal', 'instant', 'double', 'triple', 'quadruple', 'half')
fastMenuOptionMenu.pack(side=RIGHT)
fastMenuLabel = Label(fastMenuFrame, text='Menu speed')
fastMenuLabel.pack(side=LEFT, padx=(0,100))
heartbeepFrame.pack(expand=True, anchor=E)
heartcolorFrame.pack(expand=True, anchor=E)
fastMenuFrame.pack(expand=True, anchor=E)
romDialogFrame = Frame(fileDialogFrame)
baseRomLabel = Label(romDialogFrame, text='Base Rom')
romVar = StringVar()
romEntry = Entry(romDialogFrame, textvariable=romVar)
def RomSelect():
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")])
romVar.set(rom)
romSelectButton = Button(romDialogFrame, text='Select Rom', command=RomSelect)
baseRomLabel.pack(side=LEFT)
romEntry.pack(side=LEFT)
romSelectButton.pack(side=LEFT)
spriteDialogFrame = Frame(fileDialogFrame)
baseSpriteLabel = Label(spriteDialogFrame, text='Link Sprite:')
spriteNameVar = StringVar() spriteNameVar = StringVar()
sprite = None sprite = None
@@ -153,17 +129,79 @@ def guiMain(args=None):
def SpriteSelect(): def SpriteSelect():
SpriteSelector(mainWindow, set_sprite) SpriteSelector(mainWindow, set_sprite)
spriteSelectButton = Button(spriteDialogFrame, text='Open Sprite Picker', command=SpriteSelect) spriteSelectButton = Button(spriteDialogFrame, text='...', command=SpriteSelect)
baseSpriteLabel.pack(side=LEFT) baseSpriteLabel.pack(side=LEFT)
spriteEntry.pack(side=LEFT) spriteEntry.pack(side=LEFT)
spriteSelectButton.pack(side=LEFT) spriteSelectButton.pack(side=LEFT)
romDialogFrame.pack() quickSwapVar = IntVar()
spriteDialogFrame.pack() quickSwapCheckbutton = Checkbutton(romOptionsFrame, text="L/R Quickswapping", variable=quickSwapVar)
quickSwapCheckbutton.grid(row=1, column=0, sticky=E)
checkBoxFrame.pack() fastMenuFrame = Frame(romOptionsFrame)
fileDialogFrame.pack() fastMenuFrame.grid(row=1, column=1, sticky=E)
fastMenuLabel = Label(fastMenuFrame, text='Menu speed')
fastMenuLabel.pack(side=LEFT)
fastMenuVar = StringVar()
fastMenuVar.set('normal')
fastMenuOptionMenu = OptionMenu(fastMenuFrame, fastMenuVar, 'normal', 'instant', 'double', 'triple', 'quadruple', 'half')
fastMenuOptionMenu.pack(side=LEFT)
heartcolorFrame = Frame(romOptionsFrame)
heartcolorFrame.grid(row=2, column=0, sticky=E)
heartcolorLabel = Label(heartcolorFrame, text='Heart color')
heartcolorLabel.pack(side=LEFT)
heartcolorVar = StringVar()
heartcolorVar.set('red')
heartcolorOptionMenu = OptionMenu(heartcolorFrame, heartcolorVar, 'red', 'blue', 'green', 'yellow', 'random')
heartcolorOptionMenu.pack(side=LEFT)
heartbeepFrame = Frame(romOptionsFrame)
heartbeepFrame.grid(row=2, column=1, sticky=E)
heartbeepLabel = Label(heartbeepFrame, text='Heartbeep')
heartbeepLabel.pack(side=LEFT)
heartbeepVar = StringVar()
heartbeepVar.set('normal')
heartbeepOptionMenu = OptionMenu(heartbeepFrame, heartbeepVar, 'double', 'normal', 'half', 'quarter', 'off')
heartbeepOptionMenu.pack(side=LEFT)
owPalettesFrame = Frame(romOptionsFrame)
owPalettesFrame.grid(row=3, column=0, sticky=E)
owPalettesLabel = Label(owPalettesFrame, text='Overworld palettes')
owPalettesLabel.pack(side=LEFT)
owPalettesVar = StringVar()
owPalettesVar.set('default')
owPalettesOptionMenu = OptionMenu(owPalettesFrame, owPalettesVar, 'default', 'random', 'blackout')
owPalettesOptionMenu.pack(side=LEFT)
uwPalettesFrame = Frame(romOptionsFrame)
uwPalettesFrame.grid(row=3, column=1, sticky=E)
uwPalettesLabel = Label(uwPalettesFrame, text='Dungeon palettes')
uwPalettesLabel.pack(side=LEFT)
uwPalettesVar = StringVar()
uwPalettesVar.set('default')
uwPalettesOptionMenu = OptionMenu(uwPalettesFrame, uwPalettesVar, 'default', 'random', 'blackout')
uwPalettesOptionMenu.pack(side=LEFT)
romDialogFrame = Frame(romOptionsFrame)
romDialogFrame.grid(row=4, column=0, columnspan=2, sticky=W+E)
baseRomLabel = Label(romDialogFrame, text='Base Rom: ')
romVar = StringVar(value="Zelda no Densetsu - Kamigami no Triforce (Japan).sfc")
romEntry = Entry(romDialogFrame, textvariable=romVar)
def RomSelect():
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")])
romVar.set(rom)
romSelectButton = Button(romDialogFrame, text='Select Rom', command=RomSelect)
baseRomLabel.pack(side=LEFT)
romEntry.pack(side=LEFT, expand=True, fill=X)
romSelectButton.pack(side=LEFT)
checkBoxFrame.pack(side=TOP, anchor=W, padx=5, pady=10)
romOptionsFrame.pack(expand=True, fill=BOTH, padx=3)
drowDownFrame = Frame(topFrame) drowDownFrame = Frame(topFrame)
@@ -183,46 +221,6 @@ def guiMain(args=None):
logicLabel = Label(logicFrame, text='Game logic') logicLabel = Label(logicFrame, text='Game logic')
logicLabel.pack(side=LEFT) logicLabel.pack(side=LEFT)
swordsFrame = Frame(drowDownFrame)
swordsVar = StringVar()
swordsVar.set('random')
swordsOptionMenu = OptionMenu(swordsFrame, swordsVar, 'random', 'assured', 'swordless', 'vanilla')
swordsOptionMenu.pack(side=RIGHT)
swordsLabel = Label(swordsFrame, text='Swords')
swordsLabel.pack(side=LEFT)
itemFuncFrame = Frame(drowDownFrame)
itemFuncVar = StringVar()
itemFuncVar.set('normal')
itemFuncOptionMenu = OptionMenu(itemFuncFrame, itemFuncVar, 'normal', 'hard', 'expert')
itemFuncOptionMenu.pack(side=RIGHT)
itemFuncLabel = Label(itemFuncFrame, text='Item Functionality')
itemFuncLabel.pack(side=LEFT)
accessibilityFrame = Frame(drowDownFrame)
accessibilityVar = StringVar()
accessibilityVar.set('items')
accessibilityOptionMenu = OptionMenu(accessibilityFrame, accessibilityVar, 'items', 'locations', 'none')
accessibilityOptionMenu.pack(side=RIGHT)
accessibilityLabel = Label(accessibilityFrame, text='Accessibility')
accessibilityLabel.pack(side=LEFT)
crystalsGanonFrame = Frame(drowDownFrame)
crystalsGanonVar = StringVar()
crystalsGanonVar.set('7')
crystalsGanonOptionMenu = OptionMenu(crystalsGanonFrame, crystalsGanonVar, 'random', '0', '1', '2', '3', '4', '5', '6', '7')
crystalsGanonOptionMenu.pack(side=RIGHT)
crystalsGanonLabel = Label(crystalsGanonFrame, text='Ganon Vulnerable')
crystalsGanonLabel.pack(side=LEFT)
crystalsGTFrame = Frame(drowDownFrame)
crystalsGTVar = StringVar()
crystalsGTVar.set('7')
crystalsGTOptionMenu = OptionMenu(crystalsGTFrame, crystalsGTVar, 'random', '0', '1', '2', '3', '4', '5', '6', '7')
crystalsGTOptionMenu.pack(side=RIGHT)
crystalsGTLabel = Label(crystalsGTFrame, text='Open Tower')
crystalsGTLabel.pack(side=LEFT)
goalFrame = Frame(drowDownFrame) goalFrame = Frame(drowDownFrame)
goalVar = StringVar() goalVar = StringVar()
goalVar.set('ganon') goalVar.set('ganon')
@@ -231,6 +229,30 @@ def guiMain(args=None):
goalLabel = Label(goalFrame, text='Game goal') goalLabel = Label(goalFrame, text='Game goal')
goalLabel.pack(side=LEFT) goalLabel.pack(side=LEFT)
crystalsGTFrame = Frame(drowDownFrame)
crystalsGTVar = StringVar()
crystalsGTVar.set('7')
crystalsGTOptionMenu = OptionMenu(crystalsGTFrame, crystalsGTVar, '0', '1', '2', '3', '4', '5', '6', '7', 'random')
crystalsGTOptionMenu.pack(side=RIGHT)
crystalsGTLabel = Label(crystalsGTFrame, text='Crystals to open Ganon\'s Tower')
crystalsGTLabel.pack(side=LEFT)
crystalsGanonFrame = Frame(drowDownFrame)
crystalsGanonVar = StringVar()
crystalsGanonVar.set('7')
crystalsGanonOptionMenu = OptionMenu(crystalsGanonFrame, crystalsGanonVar, '0', '1', '2', '3', '4', '5', '6', '7', 'random')
crystalsGanonOptionMenu.pack(side=RIGHT)
crystalsGanonLabel = Label(crystalsGanonFrame, text='Crystals to fight Ganon')
crystalsGanonLabel.pack(side=LEFT)
swordFrame = Frame(drowDownFrame)
swordVar = StringVar()
swordVar.set('random')
swordOptionMenu = OptionMenu(swordFrame, swordVar, 'random', 'assured', 'swordless', 'vanilla')
swordOptionMenu.pack(side=RIGHT)
swordLabel = Label(swordFrame, text='Sword availability')
swordLabel.pack(side=LEFT)
difficultyFrame = Frame(drowDownFrame) difficultyFrame = Frame(drowDownFrame)
difficultyVar = StringVar() difficultyVar = StringVar()
difficultyVar.set('normal') difficultyVar.set('normal')
@@ -239,6 +261,14 @@ def guiMain(args=None):
difficultyLabel = Label(difficultyFrame, text='Difficulty: item pool') difficultyLabel = Label(difficultyFrame, text='Difficulty: item pool')
difficultyLabel.pack(side=LEFT) difficultyLabel.pack(side=LEFT)
itemfunctionFrame = Frame(drowDownFrame)
itemfunctionVar = StringVar()
itemfunctionVar.set('normal')
itemfunctionOptionMenu = OptionMenu(itemfunctionFrame, itemfunctionVar, 'normal', 'hard', 'expert')
itemfunctionOptionMenu.pack(side=RIGHT)
itemfunctionLabel = Label(itemfunctionFrame, text='Difficulty: item functionality')
itemfunctionLabel.pack(side=LEFT)
timerFrame = Frame(drowDownFrame) timerFrame = Frame(drowDownFrame)
timerVar = StringVar() timerVar = StringVar()
timerVar.set('none') timerVar.set('none')
@@ -255,6 +285,14 @@ def guiMain(args=None):
progressiveLabel = Label(progressiveFrame, text='Progressive equipment') progressiveLabel = Label(progressiveFrame, text='Progressive equipment')
progressiveLabel.pack(side=LEFT) progressiveLabel.pack(side=LEFT)
accessibilityFrame = Frame(drowDownFrame)
accessibilityVar = StringVar()
accessibilityVar.set('items')
accessibilityOptionMenu = OptionMenu(accessibilityFrame, accessibilityVar, 'items', 'locations', 'none')
accessibilityOptionMenu.pack(side=RIGHT)
accessibilityLabel = Label(accessibilityFrame, text='Item accessibility')
accessibilityLabel.pack(side=LEFT)
algorithmFrame = Frame(drowDownFrame) algorithmFrame = Frame(drowDownFrame)
algorithmVar = StringVar() algorithmVar = StringVar()
algorithmVar.set('balanced') algorithmVar.set('balanced')
@@ -314,25 +352,26 @@ def guiMain(args=None):
shuffleFrame.pack(expand=True, anchor=E) shuffleFrame.pack(expand=True, anchor=E)
doorShuffleFrame.pack(expand=True, anchor=E) doorShuffleFrame.pack(expand=True, anchor=E)
swordsFrame.pack(expand=True, anchor=E)
difficultyFrame.pack(expand=True, anchor=E) difficultyFrame.pack(expand=True, anchor=E)
itemFuncFrame.pack(expand=True, anchor=E) itemfunctionFrame.pack(expand=True, anchor=E)
timerFrame.pack(expand=True, anchor=E) timerFrame.pack(expand=True, anchor=E)
progressiveFrame.pack(expand=True, anchor=E) progressiveFrame.pack(expand=True, anchor=E)
accessibilityFrame.pack(expand=True, anchor=E)
algorithmFrame.pack(expand=True, anchor=E) algorithmFrame.pack(expand=True, anchor=E)
enemizerFrame = LabelFrame(randomizerWindow, text="Enemizer", padx=5, pady=5) enemizerFrame = LabelFrame(randomizerWindow, text="Enemizer", padx=5, pady=2)
enemizerFrame.columnconfigure(0, weight=1) enemizerFrame.columnconfigure(0, weight=1)
enemizerFrame.columnconfigure(1, weight=1) enemizerFrame.columnconfigure(1, weight=1)
enemizerFrame.columnconfigure(2, weight=1) enemizerFrame.columnconfigure(2, weight=1)
enemizerFrame.columnconfigure(3, weight=1)
enemizerPathFrame = Frame(enemizerFrame) enemizerPathFrame = Frame(enemizerFrame)
enemizerPathFrame.grid(row=0, column=0, columnspan=3, sticky=W) enemizerPathFrame.grid(row=0, column=0, columnspan=3, sticky=W+E, padx=3)
enemizerCLIlabel = Label(enemizerPathFrame, text="EnemizerCLI path: ") enemizerCLIlabel = Label(enemizerPathFrame, text="EnemizerCLI path: ")
enemizerCLIlabel.pack(side=LEFT) enemizerCLIlabel.pack(side=LEFT)
enemizerCLIpathVar = StringVar() enemizerCLIpathVar = StringVar(value="EnemizerCLI/EnemizerCLI.Core")
enemizerCLIpathEntry = Entry(enemizerPathFrame, textvariable=enemizerCLIpathVar, width=80) enemizerCLIpathEntry = Entry(enemizerPathFrame, textvariable=enemizerCLIpathVar)
enemizerCLIpathEntry.pack(side=LEFT) enemizerCLIpathEntry.pack(side=LEFT, expand=True, fill=X)
def EnemizerSelectPath(): def EnemizerSelectPath():
path = filedialog.askopenfilename(filetypes=[("EnemizerCLI executable", "*EnemizerCLI*")]) path = filedialog.askopenfilename(filetypes=[("EnemizerCLI executable", "*EnemizerCLI*")])
if path: if path:
@@ -340,18 +379,21 @@ def guiMain(args=None):
enemizerCLIbrowseButton = Button(enemizerPathFrame, text='...', command=EnemizerSelectPath) enemizerCLIbrowseButton = Button(enemizerPathFrame, text='...', command=EnemizerSelectPath)
enemizerCLIbrowseButton.pack(side=LEFT) enemizerCLIbrowseButton.pack(side=LEFT)
enemyShuffleVar = IntVar()
enemyShuffleButton = Checkbutton(enemizerFrame, text="Enemy shuffle", variable=enemyShuffleVar)
enemyShuffleButton.grid(row=1, column=0)
paletteShuffleVar = IntVar()
paletteShuffleButton = Checkbutton(enemizerFrame, text="Palette shuffle", variable=paletteShuffleVar)
paletteShuffleButton.grid(row=1, column=1)
potShuffleVar = IntVar() potShuffleVar = IntVar()
potShuffleButton = Checkbutton(enemizerFrame, text="Pot shuffle", variable=potShuffleVar) potShuffleButton = Checkbutton(enemizerFrame, text="Pot shuffle", variable=potShuffleVar)
potShuffleButton.grid(row=1, column=2) potShuffleButton.grid(row=0, column=3)
enemizerEnemyFrame = Frame(enemizerFrame)
enemizerEnemyFrame.grid(row=1, column=0, pady=5)
enemizerEnemyLabel = Label(enemizerEnemyFrame, text='Enemy shuffle')
enemizerEnemyLabel.pack(side=LEFT)
enemyShuffleVar = StringVar()
enemyShuffleVar.set('none')
enemizerEnemyOption = OptionMenu(enemizerEnemyFrame, enemyShuffleVar, 'none', 'shuffled', 'chaos')
enemizerEnemyOption.pack(side=LEFT)
enemizerBossFrame = Frame(enemizerFrame) enemizerBossFrame = Frame(enemizerFrame)
enemizerBossFrame.grid(row=2, column=0) enemizerBossFrame.grid(row=1, column=1)
enemizerBossLabel = Label(enemizerBossFrame, text='Boss shuffle') enemizerBossLabel = Label(enemizerBossFrame, text='Boss shuffle')
enemizerBossLabel.pack(side=LEFT) enemizerBossLabel.pack(side=LEFT)
enemizerBossVar = StringVar() enemizerBossVar = StringVar()
@@ -360,7 +402,7 @@ def guiMain(args=None):
enemizerBossOption.pack(side=LEFT) enemizerBossOption.pack(side=LEFT)
enemizerDamageFrame = Frame(enemizerFrame) enemizerDamageFrame = Frame(enemizerFrame)
enemizerDamageFrame.grid(row=2, column=1) enemizerDamageFrame.grid(row=1, column=2)
enemizerDamageLabel = Label(enemizerDamageFrame, text='Enemy damage') enemizerDamageLabel = Label(enemizerDamageFrame, text='Enemy damage')
enemizerDamageLabel.pack(side=LEFT) enemizerDamageLabel.pack(side=LEFT)
enemizerDamageVar = StringVar() enemizerDamageVar = StringVar()
@@ -369,7 +411,7 @@ def guiMain(args=None):
enemizerDamageOption.pack(side=LEFT) enemizerDamageOption.pack(side=LEFT)
enemizerHealthFrame = Frame(enemizerFrame) enemizerHealthFrame = Frame(enemizerFrame)
enemizerHealthFrame.grid(row=2, column=2) enemizerHealthFrame.grid(row=1, column=3)
enemizerHealthLabel = Label(enemizerHealthFrame, text='Enemy health') enemizerHealthLabel = Label(enemizerHealthFrame, text='Enemy health')
enemizerHealthLabel.pack(side=LEFT) enemizerHealthLabel.pack(side=LEFT)
enemizerHealthVar = StringVar() enemizerHealthVar = StringVar()
@@ -382,6 +424,9 @@ def guiMain(args=None):
worldLabel = Label(bottomFrame, text='Worlds') worldLabel = Label(bottomFrame, text='Worlds')
worldVar = StringVar() worldVar = StringVar()
worldSpinbox = Spinbox(bottomFrame, from_=1, to=100, width=5, textvariable=worldVar) worldSpinbox = Spinbox(bottomFrame, from_=1, to=100, width=5, textvariable=worldVar)
namesLabel = Label(bottomFrame, text='Player names')
namesVar = StringVar()
namesEntry = Entry(bottomFrame, textvariable=namesVar)
seedLabel = Label(bottomFrame, text='Seed #') seedLabel = Label(bottomFrame, text='Seed #')
seedVar = StringVar() seedVar = StringVar()
seedEntry = Entry(bottomFrame, width=15, textvariable=seedVar) seedEntry = Entry(bottomFrame, width=15, textvariable=seedVar)
@@ -390,21 +435,23 @@ def guiMain(args=None):
countSpinbox = Spinbox(bottomFrame, from_=1, to=100, width=5, textvariable=countVar) countSpinbox = Spinbox(bottomFrame, from_=1, to=100, width=5, textvariable=countVar)
def generateRom(): def generateRom():
guiargs = Namespace guiargs = Namespace()
guiargs.multi = int(worldVar.get()) guiargs.multi = int(worldVar.get())
guiargs.names = namesVar.get()
guiargs.seed = int(seedVar.get()) if seedVar.get() else None guiargs.seed = int(seedVar.get()) if seedVar.get() else None
guiargs.count = int(countVar.get()) if countVar.get() != '1' else None guiargs.count = int(countVar.get()) if countVar.get() != '1' else None
guiargs.mode = modeVar.get() guiargs.mode = modeVar.get()
guiargs.logic = logicVar.get() guiargs.logic = logicVar.get()
guiargs.swords = swordsVar.get()
guiargs.item_functionality = itemFuncVar.get()
guiargs.accessibility = accessibilityVar.get()
guiargs.crystals_ganon = crystalsGanonVar.get()
guiargs.crystals_gt = crystalsGTVar.get()
guiargs.goal = goalVar.get() guiargs.goal = goalVar.get()
guiargs.crystals_gt = crystalsGTVar.get()
guiargs.crystals_ganon = crystalsGanonVar.get()
guiargs.swords = swordVar.get()
guiargs.difficulty = difficultyVar.get() guiargs.difficulty = difficultyVar.get()
guiargs.item_functionality = itemfunctionVar.get()
guiargs.timer = timerVar.get() guiargs.timer = timerVar.get()
guiargs.progressive = progressiveVar.get() guiargs.progressive = progressiveVar.get()
guiargs.accessibility = accessibilityVar.get()
guiargs.algorithm = algorithmVar.get() guiargs.algorithm = algorithmVar.get()
guiargs.shuffle = shuffleVar.get() guiargs.shuffle = shuffleVar.get()
guiargs.door_shuffle = doorShuffleVar.get() guiargs.door_shuffle = doorShuffleVar.get()
@@ -413,19 +460,23 @@ def guiMain(args=None):
guiargs.fastmenu = fastMenuVar.get() guiargs.fastmenu = fastMenuVar.get()
guiargs.create_spoiler = bool(createSpoilerVar.get()) guiargs.create_spoiler = bool(createSpoilerVar.get())
guiargs.suppress_rom = bool(suppressRomVar.get()) guiargs.suppress_rom = bool(suppressRomVar.get())
guiargs.keysanity = bool(keysanityVar.get()) guiargs.openpyramid = bool(openpyramidVar.get())
guiargs.mapshuffle = bool(mapshuffleVar.get())
guiargs.compassshuffle = bool(compassshuffleVar.get())
guiargs.keyshuffle = bool(keyshuffleVar.get())
guiargs.bigkeyshuffle = bool(bigkeyshuffleVar.get())
guiargs.retro = bool(retroVar.get()) guiargs.retro = bool(retroVar.get())
guiargs.nodungeonitems = bool(dungeonItemsVar.get())
guiargs.quickswap = bool(quickSwapVar.get()) guiargs.quickswap = bool(quickSwapVar.get())
guiargs.disablemusic = bool(disableMusicVar.get()) guiargs.disablemusic = bool(disableMusicVar.get())
guiargs.ow_palettes = owPalettesVar.get()
guiargs.uw_palettes = uwPalettesVar.get()
guiargs.shuffleganon = bool(shuffleGanonVar.get()) guiargs.shuffleganon = bool(shuffleGanonVar.get())
guiargs.hints = bool(hintsVar.get()) guiargs.hints = bool(hintsVar.get())
guiargs.enemizercli = enemizerCLIpathVar.get() guiargs.enemizercli = enemizerCLIpathVar.get()
guiargs.shufflebosses = enemizerBossVar.get() guiargs.shufflebosses = enemizerBossVar.get()
guiargs.shuffleenemies = bool(enemyShuffleVar.get()) guiargs.shuffleenemies = enemyShuffleVar.get()
guiargs.enemy_health = enemizerHealthVar.get() guiargs.enemy_health = enemizerHealthVar.get()
guiargs.enemy_damage = enemizerDamageVar.get() guiargs.enemy_damage = enemizerDamageVar.get()
guiargs.shufflepalette = bool(paletteShuffleVar.get())
guiargs.shufflepots = bool(potShuffleVar.get()) guiargs.shufflepots = bool(potShuffleVar.get())
guiargs.custom = bool(customVar.get()) guiargs.custom = bool(customVar.get())
guiargs.customitemarray = [int(bowVar.get()), int(silverarrowVar.get()), int(boomerangVar.get()), int(magicboomerangVar.get()), int(hookshotVar.get()), int(mushroomVar.get()), int(magicpowderVar.get()), int(firerodVar.get()), guiargs.customitemarray = [int(bowVar.get()), int(silverarrowVar.get()), int(boomerangVar.get()), int(magicboomerangVar.get()), int(hookshotVar.get()), int(mushroomVar.get()), int(magicpowderVar.get()), int(firerodVar.get()),
@@ -435,13 +486,16 @@ def guiMain(args=None):
int(sword3Var.get()), int(sword4Var.get()), int(progswordVar.get()), int(shield1Var.get()), int(shield2Var.get()), int(shield3Var.get()), int(progshieldVar.get()), int(bluemailVar.get()), int(sword3Var.get()), int(sword4Var.get()), int(progswordVar.get()), int(shield1Var.get()), int(shield2Var.get()), int(shield3Var.get()), int(progshieldVar.get()), int(bluemailVar.get()),
int(redmailVar.get()), int(progmailVar.get()), int(halfmagicVar.get()), int(quartermagicVar.get()), int(bcap5Var.get()), int(bcap10Var.get()), int(acap5Var.get()), int(acap10Var.get()), int(redmailVar.get()), int(progmailVar.get()), int(halfmagicVar.get()), int(quartermagicVar.get()), int(bcap5Var.get()), int(bcap10Var.get()), int(acap5Var.get()), int(acap10Var.get()),
int(arrow1Var.get()), int(arrow10Var.get()), int(bomb1Var.get()), int(bomb3Var.get()), int(rupee1Var.get()), int(rupee5Var.get()), int(rupee20Var.get()), int(rupee50Var.get()), int(rupee100Var.get()), int(arrow1Var.get()), int(arrow10Var.get()), int(bomb1Var.get()), int(bomb3Var.get()), int(rupee1Var.get()), int(rupee5Var.get()), int(rupee20Var.get()), int(rupee50Var.get()), int(rupee100Var.get()),
int(rupee300Var.get()), int(rupoorVar.get()), int(blueclockVar.get()), int(greenclockVar.get()), int(redclockVar.get()), int(triforcepieceVar.get()), int(triforcecountVar.get()), int(rupee300Var.get()), int(rupoorVar.get()), int(blueclockVar.get()), int(greenclockVar.get()), int(redclockVar.get()), int(progbowVar.get()), int(bomb10Var.get()), int(triforcepieceVar.get()),
int(triforceVar.get()), int(rupoorcostVar.get()), int(universalkeyVar.get())] int(triforcecountVar.get()), int(triforceVar.get()), int(rupoorcostVar.get()), int(universalkeyVar.get())]
guiargs.rom = romVar.get() guiargs.rom = romVar.get()
guiargs.jsonout = None
guiargs.sprite = sprite guiargs.sprite = sprite
guiargs.skip_playthrough = False # get default values for missing parameters
guiargs.outputpath = None for k,v in vars(parse_arguments(['--multi', str(guiargs.multi)])).items():
if k not in vars(guiargs):
setattr(guiargs, k, v)
elif type(v) is dict: # use same settings for every player
setattr(guiargs, k, {player: getattr(guiargs, k) for player in range(1, guiargs.multi + 1)})
try: try:
if guiargs.count is not None: if guiargs.count is not None:
seed = guiargs.seed seed = guiargs.seed
@@ -451,14 +505,21 @@ def guiMain(args=None):
else: else:
main(seed=guiargs.seed, args=guiargs) main(seed=guiargs.seed, args=guiargs)
except Exception as e: except Exception as e:
logging.exception(e)
messagebox.showerror(title="Error while creating seed", message=str(e)) messagebox.showerror(title="Error while creating seed", message=str(e))
else: else:
messagebox.showinfo(title="Success", message="Rom patched successfully") msgtxt = "Rom patched successfully"
if guiargs.names:
for player, name in parse_names_string(guiargs.names).items():
msgtxt += "\nPlayer %d => %s" % (player, name)
messagebox.showinfo(title="Success", message=msgtxt)
generateButton = Button(bottomFrame, text='Generate Patched Rom', command=generateRom) generateButton = Button(bottomFrame, text='Generate Patched Rom', command=generateRom)
worldLabel.pack(side=LEFT) worldLabel.pack(side=LEFT)
worldSpinbox.pack(side=LEFT) worldSpinbox.pack(side=LEFT)
namesLabel.pack(side=LEFT)
namesEntry.pack(side=LEFT)
seedLabel.pack(side=LEFT, padx=(5, 0)) seedLabel.pack(side=LEFT, padx=(5, 0))
seedEntry.pack(side=LEFT) seedEntry.pack(side=LEFT)
countLabel.pack(side=LEFT, padx=(5, 0)) countLabel.pack(side=LEFT, padx=(5, 0))
@@ -539,28 +600,59 @@ def guiMain(args=None):
fastMenuLabel2 = Label(fastMenuFrame2, text='Menu speed') fastMenuLabel2 = Label(fastMenuFrame2, text='Menu speed')
fastMenuLabel2.pack(side=LEFT) fastMenuLabel2.pack(side=LEFT)
owPalettesFrame2 = Frame(drowDownFrame2)
owPalettesOptionMenu2 = OptionMenu(owPalettesFrame2, owPalettesVar, 'default', 'random', 'blackout')
owPalettesOptionMenu2.pack(side=RIGHT)
owPalettesLabel2 = Label(owPalettesFrame2, text='Overworld palettes')
owPalettesLabel2.pack(side=LEFT)
uwPalettesFrame2 = Frame(drowDownFrame2)
uwPalettesOptionMenu2 = OptionMenu(uwPalettesFrame2, uwPalettesVar, 'default', 'random', 'blackout')
uwPalettesOptionMenu2.pack(side=RIGHT)
uwPalettesLabel2 = Label(uwPalettesFrame2, text='Dungeon palettes')
uwPalettesLabel2.pack(side=LEFT)
namesFrame2 = Frame(drowDownFrame2)
namesLabel2 = Label(namesFrame2, text='Player names')
namesVar2 = StringVar()
namesEntry2 = Entry(namesFrame2, textvariable=namesVar2)
namesLabel2.pack(side=LEFT)
namesEntry2.pack(side=LEFT)
heartbeepFrame2.pack(expand=True, anchor=E) heartbeepFrame2.pack(expand=True, anchor=E)
heartcolorFrame2.pack(expand=True, anchor=E) heartcolorFrame2.pack(expand=True, anchor=E)
fastMenuFrame2.pack(expand=True, anchor=E) fastMenuFrame2.pack(expand=True, anchor=E)
owPalettesFrame2.pack(expand=True, anchor=E)
uwPalettesFrame2.pack(expand=True, anchor=E)
namesFrame2.pack(expand=True, anchor=E)
bottomFrame2 = Frame(topFrame2) bottomFrame2 = Frame(topFrame2)
def adjustRom(): def adjustRom():
guiargs = Namespace guiargs = Namespace()
guiargs.heartbeep = heartbeepVar.get() guiargs.heartbeep = heartbeepVar.get()
guiargs.heartcolor = heartcolorVar.get() guiargs.heartcolor = heartcolorVar.get()
guiargs.fastmenu = fastMenuVar.get() guiargs.fastmenu = fastMenuVar.get()
guiargs.ow_palettes = owPalettesVar.get()
guiargs.uw_palettes = uwPalettesVar.get()
guiargs.quickswap = bool(quickSwapVar.get()) guiargs.quickswap = bool(quickSwapVar.get())
guiargs.disablemusic = bool(disableMusicVar.get()) guiargs.disablemusic = bool(disableMusicVar.get())
guiargs.rom = romVar2.get() guiargs.rom = romVar2.get()
guiargs.baserom = romVar.get()
guiargs.sprite = sprite guiargs.sprite = sprite
guiargs.names = namesEntry2.get()
try: try:
adjust(args=guiargs) adjust(args=guiargs)
except Exception as e: except Exception as e:
logging.exception(e)
messagebox.showerror(title="Error while creating seed", message=str(e)) messagebox.showerror(title="Error while creating seed", message=str(e))
else: else:
messagebox.showinfo(title="Success", message="Rom patched successfully") msgtxt = "Rom patched successfully"
if guiargs.names:
for player, name in parse_names_string(guiargs.names).items():
msgtxt += "\nPlayer %d => %s" % (player, name)
messagebox.showinfo(title="Success", message=msgtxt)
adjustButton = Button(bottomFrame2, text='Adjust Rom', command=adjustRom) adjustButton = Button(bottomFrame2, text='Adjust Rom', command=adjustRom)
@@ -590,19 +682,19 @@ def guiMain(args=None):
bowFrame = Frame(itemList1) bowFrame = Frame(itemList1)
bowLabel = Label(bowFrame, text='Bow') bowLabel = Label(bowFrame, text='Bow')
bowVar = StringVar(value='1') bowVar = StringVar(value='0')
bowEntry = Entry(bowFrame, textvariable=bowVar, width=3, validate='all', vcmd=vcmd) bowEntry = Entry(bowFrame, textvariable=bowVar, width=3, validate='all', vcmd=vcmd)
bowFrame.pack() bowFrame.pack()
bowLabel.pack(anchor=W, side=LEFT, padx=(0,53)) bowLabel.pack(anchor=W, side=LEFT, padx=(0,53))
bowEntry.pack(anchor=E) bowEntry.pack(anchor=E)
silverarrowFrame = Frame(itemList1) progbowFrame = Frame(itemList1)
silverarrowLabel = Label(silverarrowFrame, text='Silver Arrow') progbowLabel = Label(progbowFrame, text='Prog.Bow')
silverarrowVar = StringVar(value='1') progbowVar = StringVar(value='2')
silverarrowEntry = Entry(silverarrowFrame, textvariable=silverarrowVar, width=3, validate='all', vcmd=vcmd) progbowEntry = Entry(progbowFrame, textvariable=progbowVar, width=3, validate='all', vcmd=vcmd)
silverarrowFrame.pack() progbowFrame.pack()
silverarrowLabel.pack(anchor=W, side=LEFT, padx=(0,13)) progbowLabel.pack(anchor=W, side=LEFT, padx=(0,25))
silverarrowEntry.pack(anchor=E) progbowEntry.pack(anchor=E)
boomerangFrame = Frame(itemList1) boomerangFrame = Frame(itemList1)
boomerangLabel = Label(boomerangFrame, text='Boomerang') boomerangLabel = Label(boomerangFrame, text='Boomerang')
@@ -958,7 +1050,7 @@ def guiMain(args=None):
bcap5Frame = Frame(itemList3) bcap5Frame = Frame(itemList3)
bcap5Label = Label(bcap5Frame, text='Bomb C.+5') bcap5Label = Label(bcap5Frame, text='Bomb C.+5')
bcap5Var = StringVar(value='6') bcap5Var = StringVar(value='0')
bcap5Entry = Entry(bcap5Frame, textvariable=bcap5Var, width=3, validate='all', vcmd=vcmd) bcap5Entry = Entry(bcap5Frame, textvariable=bcap5Var, width=3, validate='all', vcmd=vcmd)
bcap5Frame.pack() bcap5Frame.pack()
bcap5Label.pack(anchor=W, side=LEFT, padx=(0,16)) bcap5Label.pack(anchor=W, side=LEFT, padx=(0,16))
@@ -966,7 +1058,7 @@ def guiMain(args=None):
bcap10Frame = Frame(itemList3) bcap10Frame = Frame(itemList3)
bcap10Label = Label(bcap10Frame, text='Bomb C.+10') bcap10Label = Label(bcap10Frame, text='Bomb C.+10')
bcap10Var = StringVar(value='1') bcap10Var = StringVar(value='0')
bcap10Entry = Entry(bcap10Frame, textvariable=bcap10Var, width=3, validate='all', vcmd=vcmd) bcap10Entry = Entry(bcap10Frame, textvariable=bcap10Var, width=3, validate='all', vcmd=vcmd)
bcap10Frame.pack() bcap10Frame.pack()
bcap10Label.pack(anchor=W, side=LEFT, padx=(0,10)) bcap10Label.pack(anchor=W, side=LEFT, padx=(0,10))
@@ -974,7 +1066,7 @@ def guiMain(args=None):
acap5Frame = Frame(itemList4) acap5Frame = Frame(itemList4)
acap5Label = Label(acap5Frame, text='Arrow C.+5') acap5Label = Label(acap5Frame, text='Arrow C.+5')
acap5Var = StringVar(value='6') acap5Var = StringVar(value='0')
acap5Entry = Entry(acap5Frame, textvariable=acap5Var, width=3, validate='all', vcmd=vcmd) acap5Entry = Entry(acap5Frame, textvariable=acap5Var, width=3, validate='all', vcmd=vcmd)
acap5Frame.pack() acap5Frame.pack()
acap5Label.pack(anchor=W, side=LEFT, padx=(0,7)) acap5Label.pack(anchor=W, side=LEFT, padx=(0,7))
@@ -982,7 +1074,7 @@ def guiMain(args=None):
acap10Frame = Frame(itemList4) acap10Frame = Frame(itemList4)
acap10Label = Label(acap10Frame, text='Arrow C.+10') acap10Label = Label(acap10Frame, text='Arrow C.+10')
acap10Var = StringVar(value='1') acap10Var = StringVar(value='0')
acap10Entry = Entry(acap10Frame, textvariable=acap10Var, width=3, validate='all', vcmd=vcmd) acap10Entry = Entry(acap10Frame, textvariable=acap10Var, width=3, validate='all', vcmd=vcmd)
acap10Frame.pack() acap10Frame.pack()
acap10Label.pack(anchor=W, side=LEFT, padx=(0,1)) acap10Label.pack(anchor=W, side=LEFT, padx=(0,1))
@@ -998,7 +1090,7 @@ def guiMain(args=None):
arrow10Frame = Frame(itemList4) arrow10Frame = Frame(itemList4)
arrow10Label = Label(arrow10Frame, text='Arrows (10)') arrow10Label = Label(arrow10Frame, text='Arrows (10)')
arrow10Var = StringVar(value='5') arrow10Var = StringVar(value='12')
arrow10Entry = Entry(arrow10Frame, textvariable=arrow10Var, width=3, validate='all', vcmd=vcmd) arrow10Entry = Entry(arrow10Frame, textvariable=arrow10Var, width=3, validate='all', vcmd=vcmd)
arrow10Frame.pack() arrow10Frame.pack()
arrow10Label.pack(anchor=W, side=LEFT, padx=(0,7)) arrow10Label.pack(anchor=W, side=LEFT, padx=(0,7))
@@ -1014,12 +1106,20 @@ def guiMain(args=None):
bomb3Frame = Frame(itemList4) bomb3Frame = Frame(itemList4)
bomb3Label = Label(bomb3Frame, text='Bombs (3)') bomb3Label = Label(bomb3Frame, text='Bombs (3)')
bomb3Var = StringVar(value='10') bomb3Var = StringVar(value='16')
bomb3Entry = Entry(bomb3Frame, textvariable=bomb3Var, width=3, validate='all', vcmd=vcmd) bomb3Entry = Entry(bomb3Frame, textvariable=bomb3Var, width=3, validate='all', vcmd=vcmd)
bomb3Frame.pack() bomb3Frame.pack()
bomb3Label.pack(anchor=W, side=LEFT, padx=(0,13)) bomb3Label.pack(anchor=W, side=LEFT, padx=(0,13))
bomb3Entry.pack(anchor=E) bomb3Entry.pack(anchor=E)
bomb10Frame = Frame(itemList4)
bomb10Label = Label(bomb10Frame, text='Bombs (10)')
bomb10Var = StringVar(value='1')
bomb10Entry = Entry(bomb10Frame, textvariable=bomb10Var, width=3, validate='all', vcmd=vcmd)
bomb10Frame.pack()
bomb10Label.pack(anchor=W, side=LEFT, padx=(0,7))
bomb10Entry.pack(anchor=E)
rupee1Frame = Frame(itemList4) rupee1Frame = Frame(itemList4)
rupee1Label = Label(rupee1Frame, text='Rupee (1)') rupee1Label = Label(rupee1Frame, text='Rupee (1)')
rupee1Var = StringVar(value='2') rupee1Var = StringVar(value='2')
@@ -1068,14 +1168,6 @@ def guiMain(args=None):
rupee300Label.pack(anchor=W, side=LEFT, padx=(0,0)) rupee300Label.pack(anchor=W, side=LEFT, padx=(0,0))
rupee300Entry.pack(anchor=E) rupee300Entry.pack(anchor=E)
rupoorFrame = Frame(itemList4)
rupoorLabel = Label(rupoorFrame, text='Rupoor')
rupoorVar = StringVar(value='0')
rupoorEntry = Entry(rupoorFrame, textvariable=rupoorVar, width=3, validate='all', vcmd=vcmd)
rupoorFrame.pack()
rupoorLabel.pack(anchor=W, side=LEFT, padx=(0,28))
rupoorEntry.pack(anchor=E)
blueclockFrame = Frame(itemList4) blueclockFrame = Frame(itemList4)
blueclockLabel = Label(blueclockFrame, text='Blue Clock') blueclockLabel = Label(blueclockFrame, text='Blue Clock')
blueclockVar = StringVar(value='0') blueclockVar = StringVar(value='0')
@@ -1100,6 +1192,14 @@ def guiMain(args=None):
redclockLabel.pack(anchor=W, side=LEFT, padx=(0,14)) redclockLabel.pack(anchor=W, side=LEFT, padx=(0,14))
redclockEntry.pack(anchor=E) redclockEntry.pack(anchor=E)
silverarrowFrame = Frame(itemList5)
silverarrowLabel = Label(silverarrowFrame, text='Silver Arrow')
silverarrowVar = StringVar(value='0')
silverarrowEntry = Entry(silverarrowFrame, textvariable=silverarrowVar, width=3, validate='all', vcmd=vcmd)
silverarrowFrame.pack()
silverarrowLabel.pack(anchor=W, side=LEFT, padx=(0,64))
silverarrowEntry.pack(anchor=E)
universalkeyFrame = Frame(itemList5) universalkeyFrame = Frame(itemList5)
universalkeyLabel = Label(universalkeyFrame, text='Universal Key') universalkeyLabel = Label(universalkeyFrame, text='Universal Key')
universalkeyVar = StringVar(value='0') universalkeyVar = StringVar(value='0')
@@ -1132,6 +1232,14 @@ def guiMain(args=None):
triforceLabel.pack(anchor=W, side=LEFT, padx=(0,23)) triforceLabel.pack(anchor=W, side=LEFT, padx=(0,23))
triforceEntry.pack(anchor=E) triforceEntry.pack(anchor=E)
rupoorFrame = Frame(itemList5)
rupoorLabel = Label(rupoorFrame, text='Rupoor')
rupoorVar = StringVar(value='0')
rupoorEntry = Entry(rupoorFrame, textvariable=rupoorVar, width=3, validate='all', vcmd=vcmd)
rupoorFrame.pack()
rupoorLabel.pack(anchor=W, side=LEFT, padx=(0,87))
rupoorEntry.pack(anchor=E)
rupoorcostFrame = Frame(itemList5) rupoorcostFrame = Frame(itemList5)
rupoorcostLabel = Label(rupoorcostFrame, text='Rupoor Cost') rupoorcostLabel = Label(rupoorcostFrame, text='Rupoor Cost')
rupoorcostVar = StringVar(value='10') rupoorcostVar = StringVar(value='10')
@@ -1148,13 +1256,17 @@ def guiMain(args=None):
topFrame3.pack(side=TOP, pady=(17,0)) topFrame3.pack(side=TOP, pady=(17,0))
if args is not None: if args is not None:
for k,v in vars(args).items():
if type(v) is dict:
setattr(args, k, v[1]) # only get values for player 1 for now
# load values from commandline args # load values from commandline args
createSpoilerVar.set(int(args.create_spoiler)) createSpoilerVar.set(int(args.create_spoiler))
suppressRomVar.set(int(args.suppress_rom)) suppressRomVar.set(int(args.suppress_rom))
keysanityVar.set(args.keysanity) mapshuffleVar.set(args.mapshuffle)
compassshuffleVar.set(args.compassshuffle)
keyshuffleVar.set(args.keyshuffle)
bigkeyshuffleVar.set(args.bigkeyshuffle)
retroVar.set(args.retro) retroVar.set(args.retro)
if args.nodungeonitems:
dungeonItemsVar.set(int(not args.nodungeonitems))
quickSwapVar.set(int(args.quickswap)) quickSwapVar.set(int(args.quickswap))
disableMusicVar.set(int(args.disablemusic)) disableMusicVar.set(int(args.disablemusic))
if args.count: if args.count:
@@ -1162,9 +1274,9 @@ def guiMain(args=None):
if args.seed: if args.seed:
seedVar.set(str(args.seed)) seedVar.set(str(args.seed))
modeVar.set(args.mode) modeVar.set(args.mode)
swordsVar.set(args.swords) swordVar.set(args.swords)
difficultyVar.set(args.difficulty) difficultyVar.set(args.difficulty)
itemFuncVar.set(args.item_functionality) itemfunctionVar.set(args.item_functionality)
timerVar.set(args.timer) timerVar.set(args.timer)
progressiveVar.set(args.progressive) progressiveVar.set(args.progressive)
accessibilityVar.set(args.accessibility) accessibilityVar.set(args.accessibility)
+239 -311
View File
@@ -1,5 +1,6 @@
import collections import collections
from BaseClasses import Region, Location, Entrance, RegionType, Shop, ShopType from BaseClasses import RegionType
from Regions import create_lw_region, create_dw_region, create_cave_region, create_dungeon_region
def create_inverted_regions(world, player): def create_inverted_regions(world, player):
@@ -9,7 +10,7 @@ def create_inverted_regions(world, player):
["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Kings Grave Outer Rocks', 'Dam', ["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Kings Grave Outer Rocks', 'Dam',
'Inverted Big Bomb Shop', 'Tavern North', 'Chicken House', 'Aginahs Cave', 'Sahasrahlas Hut', 'Kakariko Well Drop', 'Kakariko Well Cave', 'Inverted Big Bomb Shop', 'Tavern North', 'Chicken House', 'Aginahs Cave', 'Sahasrahlas Hut', 'Kakariko Well Drop', 'Kakariko Well Cave',
'Blacksmiths Hut', 'Bat Cave Drop Ledge', 'Bat Cave Cave', 'Sick Kids House', 'Hobo Bridge', 'Lost Woods Hideout Drop', 'Lost Woods Hideout Stump', 'Blacksmiths Hut', 'Bat Cave Drop Ledge', 'Bat Cave Cave', 'Sick Kids House', 'Hobo Bridge', 'Lost Woods Hideout Drop', 'Lost Woods Hideout Stump',
'Lumberjack Tree Tree', 'Lumberjack Tree Cave', 'Mini Moldorm Cave', 'Ice Rod Cave', 'Lake Hylia Central Island Pier', 'Lumberjack Tree Tree', 'Lumberjack Tree Cave', 'Mini Moldorm Cave', 'Ice Rod Cave', 'Lake Hylia Central Island Pier', 'Lake Hylia Island',
'Bonk Rock Cave', 'Library', 'Two Brothers House (East)', 'Desert Palace Stairs', 'Eastern Palace', 'Master Sword Meadow', 'Bonk Rock Cave', 'Library', 'Two Brothers House (East)', 'Desert Palace Stairs', 'Eastern Palace', 'Master Sword Meadow',
'Sanctuary', 'Sanctuary Grave', 'Death Mountain Entrance Rock', 'Light World River Drop', 'Sanctuary', 'Sanctuary Grave', 'Death Mountain Entrance Rock', 'Light World River Drop',
'Elder House (East)', 'Elder House (West)', 'North Fairy Cave', 'North Fairy Cave Drop', 'Lost Woods Gamble', 'Snitch Lady (East)', 'Snitch Lady (West)', 'Tavern (Front)', 'Elder House (East)', 'Elder House (West)', 'North Fairy Cave', 'North Fairy Cave Drop', 'Lost Woods Gamble', 'Snitch Lady (East)', 'Snitch Lady (West)', 'Tavern (Front)',
@@ -302,52 +303,11 @@ def create_inverted_regions(world, player):
create_cave_region(player, 'The Sky', 'A Dark Sky', None, ['DDM Landing','NEDW Landing', 'WDW Landing', 'SDW Landing', 'EDW Landing', 'DD Landing', 'DLHL Landing']) create_cave_region(player, 'The Sky', 'A Dark Sky', None, ['DDM Landing','NEDW Landing', 'WDW Landing', 'SDW Landing', 'EDW Landing', 'DD Landing', 'DLHL Landing'])
] ]
for region_name, (room_id, shopkeeper, replaceable) in shop_table.items():
region = world.get_region(region_name, player)
shop = Shop(region, room_id, ShopType.Shop, shopkeeper, replaceable)
region.shop = shop
world.shops.append(shop)
for index, (item, price) in enumerate(default_shop_contents[region_name]):
shop.add_inventory(index, item, price)
region = world.get_region('Capacity Upgrade', player) def mark_dark_world_regions(world, player):
shop = Shop(region, 0x0115, ShopType.UpgradeShop, 0x04, True)
region.shop = shop
world.shops.append(shop)
shop.add_inventory(0, 'Bomb Upgrade (+5)', 100, 7)
shop.add_inventory(1, 'Arrow Upgrade (+5)', 100, 7)
world.intialize_regions()
def create_lw_region(player, name, locations=None, exits=None):
return _create_region(player, name, RegionType.LightWorld, 'Light World', locations, exits)
def create_dw_region(player, name, locations=None, exits=None):
return _create_region(player, name, RegionType.DarkWorld, 'Dark World', locations, exits)
def create_cave_region(player, name, hint='Hyrule', locations=None, exits=None):
return _create_region(player, name, RegionType.Cave, hint, locations, exits)
def create_dungeon_region(player, name, hint='Hyrule', locations=None, exits=None):
return _create_region(player, name, RegionType.Dungeon, hint, locations, exits)
def _create_region(player, name, type, hint='Hyrule', locations=None, exits=None):
ret = Region(name, type, hint, player)
if locations is None:
locations = []
if exits is None:
exits = []
for exit in exits:
ret.exits.append(Entrance(player, exit, ret))
for location in locations:
address, crystal, hint_text = location_table[location]
ret.locations.append(Location(player, location, address, crystal, hint_text, ret))
return ret
def mark_dark_world_regions(world):
# cross world caves may have some sections marked as both in_light_world, and in_dark_work. # cross world caves may have some sections marked as both in_light_world, and in_dark_work.
# That is ok. the bunny logic will check for this case and incorporate special rules. # That is ok. the bunny logic will check for this case and incorporate special rules.
queue = collections.deque(region for region in world.regions if region.type == RegionType.DarkWorld) queue = collections.deque(region for region in world.get_regions(player) if region.type == RegionType.DarkWorld)
seen = set(queue) seen = set(queue)
while queue: while queue:
current = queue.popleft() current = queue.popleft()
@@ -360,7 +320,7 @@ def mark_dark_world_regions(world):
seen.add(exit.connected_region) seen.add(exit.connected_region)
queue.append(exit.connected_region) queue.append(exit.connected_region)
queue = collections.deque(region for region in world.regions if region.type == RegionType.LightWorld) queue = collections.deque(region for region in world.get_regions(player) if region.type == RegionType.LightWorld)
seen = set(queue) seen = set(queue)
while queue: while queue:
current = queue.popleft() current = queue.popleft()
@@ -373,269 +333,237 @@ def mark_dark_world_regions(world):
seen.add(exit.connected_region) seen.add(exit.connected_region)
queue.append(exit.connected_region) queue.append(exit.connected_region)
# (room_id, shopkeeper, replaceable)
shop_table = {
'Cave Shop (Dark Death Mountain)': (0x0112, 0xC1, True),
'Red Shield Shop': (0x0110, 0xC1, True),
'Dark Lake Hylia Shop': (0x010F, 0xC1, True),
'Dark World Lumberjack Shop': (0x010F, 0xC1, True),
'Village of Outcasts Shop': (0x010F, 0xC1, True),
'Dark World Potion Shop': (0x010F, 0xC1, True),
'Light World Death Mountain Shop': (0x00FF, 0xA0, True),
'Kakariko Shop': (0x011F, 0xA0, True),
'Cave Shop (Lake Hylia)': (0x0112, 0xA0, True),
'Potion Shop': (0x0109, 0xFF, False),
# Bomb Shop not currently modeled as a shop, due to special nature of items
}
# region, [item]
# slot, item, price, max=0, replacement=None, replacement_price=0
# item = (item, price)
_basic_shop_defaults = [('Red Potion', 150), ('Small Heart', 10), ('Bombs (10)', 50)] location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'),
_dark_world_shop_defaults = [('Red Potion', 150), ('Blue Shield', 50), ('Bombs (10)', 50)] 'Bottle Merchant': (0x2eb18, 0x186339, False, 'with a merchant'),
default_shop_contents = { 'Flute Spot': (0x18014a, 0x18633d, False, 'underground'),
'Cave Shop (Dark Death Mountain)': _basic_shop_defaults, 'Sunken Treasure': (0x180145, 0x186354, False, 'underwater'),
'Red Shield Shop': [('Red Shield', 500), ('Bee', 10), ('Arrows (10)', 30)], 'Purple Chest': (0x33d68, 0x186359, False, 'from a box'),
'Dark Lake Hylia Shop': _dark_world_shop_defaults, "Blind's Hideout - Top": (0xeb0f, 0x1862e3, False, 'in a basement'),
'Dark World Lumberjack Shop': _dark_world_shop_defaults, "Blind's Hideout - Left": (0xeb12, 0x1862e6, False, 'in a basement'),
'Village of Outcasts Shop': _dark_world_shop_defaults, "Blind's Hideout - Right": (0xeb15, 0x1862e9, False, 'in a basement'),
'Dark World Potion Shop': _dark_world_shop_defaults, "Blind's Hideout - Far Left": (0xeb18, 0x1862ec, False, 'in a basement'),
'Light World Death Mountain Shop': _basic_shop_defaults, "Blind's Hideout - Far Right": (0xeb1b, 0x1862ef, False, 'in a basement'),
'Kakariko Shop': _basic_shop_defaults, "Link's Uncle": (0x2df45, 0x18635f, False, 'with your uncle'),
'Cave Shop (Lake Hylia)': _basic_shop_defaults, 'Secret Passage': (0xe971, 0x186145, False, 'near your uncle'),
'Potion Shop': [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)], 'King Zora': (0xee1c3, 0x186360, False, 'at a high price'),
} "Zora's Ledge": (0x180149, 0x186358, False, 'near Zora'),
'Waterfall Fairy - Left': (0xe9b0, 0x186184, False, 'near a fairy'),
location_table = {'Mushroom': (0x180013, False, 'in the woods'), 'Waterfall Fairy - Right': (0xe9d1, 0x1861a5, False, 'near a fairy'),
'Bottle Merchant': (0x2EB18, False, 'with a merchant'), "King's Tomb": (0xe97a, 0x18614e, False, 'alone in a cave'),
'Flute Spot': (0x18014A, False, 'underground'), 'Floodgate Chest': (0xe98c, 0x186160, False, 'in the dam'),
'Sunken Treasure': (0x180145, False, 'underwater'), "Link's House": (0xe9bc, 0x186190, False, 'in your home'),
'Purple Chest': (0x33D68, False, 'from a box'), 'Kakariko Tavern': (0xe9ce, 0x1861a2, False, 'in the bar'),
'Blind\'s Hideout - Top': (0xEB0F, False, 'in a basement'), 'Chicken House': (0xe9e9, 0x1861bd, False, 'near poultry'),
'Blind\'s Hideout - Left': (0xEB12, False, 'in a basement'), "Aginah's Cave": (0xe9f2, 0x1861c6, False, 'with Aginah'),
'Blind\'s Hideout - Right': (0xEB15, False, 'in a basement'), "Sahasrahla's Hut - Left": (0xea82, 0x186256, False, 'near the elder'),
'Blind\'s Hideout - Far Left': (0xEB18, False, 'in a basement'), "Sahasrahla's Hut - Middle": (0xea85, 0x186259, False, 'near the elder'),
'Blind\'s Hideout - Far Right': (0xEB1B, False, 'in a basement'), "Sahasrahla's Hut - Right": (0xea88, 0x18625c, False, 'near the elder'),
'Link\'s Uncle': (0x2DF45, False, 'with your uncle'), 'Sahasrahla': (0x2f1fc, 0x186365, False, 'with the elder'),
'Secret Passage': (0xE971, False, 'near your uncle'), 'Kakariko Well - Top': (0xea8e, 0x186262, False, 'in a well'),
'King Zora': (0xEE1C3, False, 'at a high price'), 'Kakariko Well - Left': (0xea91, 0x186265, False, 'in a well'),
'Zora\'s Ledge': (0x180149, False, 'near Zora'), 'Kakariko Well - Middle': (0xea94, 0x186268, False, 'in a well'),
'Waterfall Fairy - Left': (0xE9B0, False, 'near a fairy'), 'Kakariko Well - Right': (0xea97, 0x18626b, False, 'in a well'),
'Waterfall Fairy - Right': (0xE9D1, False, 'near a fairy'), 'Kakariko Well - Bottom': (0xea9a, 0x18626e, False, 'in a well'),
'King\'s Tomb': (0xE97A, False, 'alone in a cave'), 'Blacksmith': (0x18002a, 0x186366, False, 'with the smith'),
'Floodgate Chest': (0xE98C, False, 'in the dam'), 'Magic Bat': (0x180015, 0x18635e, False, 'with the bat'),
'Link\'s House': (0xE9BC, False, 'in your home'), 'Sick Kid': (0x339cf, 0x186367, False, 'with the sick'),
'Kakariko Tavern': (0xE9CE, False, 'in the bar'), 'Hobo': (0x33e7d, 0x186368, False, 'with the hobo'),
'Chicken House': (0xE9E9, False, 'near poultry'), 'Lost Woods Hideout': (0x180000, 0x186348, False, 'near a thief'),
'Aginah\'s Cave': (0xE9F2, False, 'with Aginah'), 'Lumberjack Tree': (0x180001, 0x186349, False, 'in a hole'),
'Sahasrahla\'s Hut - Left': (0xEA82, False, 'near the elder'), 'Cave 45': (0x180003, 0x18634b, False, 'alone in a cave'),
'Sahasrahla\'s Hut - Middle': (0xEA85, False, 'near the elder'), 'Graveyard Cave': (0x180004, 0x18634c, False, 'alone in a cave'),
'Sahasrahla\'s Hut - Right': (0xEA88, False, 'near the elder'), 'Checkerboard Cave': (0x180005, 0x18634d, False, 'alone in a cave'),
'Sahasrahla': (0x2F1FC, False, 'with the elder'), 'Mini Moldorm Cave - Far Left': (0xeb42, 0x186316, False, 'near Moldorms'),
'Kakariko Well - Top': (0xEA8E, False, 'in a well'), 'Mini Moldorm Cave - Left': (0xeb45, 0x186319, False, 'near Moldorms'),
'Kakariko Well - Left': (0xEA91, False, 'in a well'), 'Mini Moldorm Cave - Right': (0xeb48, 0x18631c, False, 'near Moldorms'),
'Kakariko Well - Middle': (0xEA94, False, 'in a well'), 'Mini Moldorm Cave - Far Right': (0xeb4b, 0x18631f, False, 'near Moldorms'),
'Kakariko Well - Right': (0xEA97, False, 'in a well'), 'Mini Moldorm Cave - Generous Guy': (0x180010, 0x18635a, False, 'near Moldorms'),
'Kakariko Well - Bottom': (0xEA9A, False, 'in a well'), 'Ice Rod Cave': (0xeb4e, 0x186322, False, 'in a frozen cave'),
'Blacksmith': (0x18002A, False, 'with the smith'), 'Bonk Rock Cave': (0xeb3f, 0x186313, False, 'alone in a cave'),
'Magic Bat': (0x180015, False, 'with the bat'), 'Library': (0x180012, 0x18635c, False, 'near books'),
'Sick Kid': (0x339CF, False, 'with the sick'), 'Potion Shop': (0x180014, 0x18635d, False, 'near potions'),
'Hobo': (0x33E7D, False, 'with the hobo'), 'Lake Hylia Island': (0x180144, 0x186353, False, 'on an island'),
'Lost Woods Hideout': (0x180000, False, 'near a thief'), 'Maze Race': (0x180142, 0x186351, False, 'at the race'),
'Lumberjack Tree': (0x180001, False, 'in a hole'), 'Desert Ledge': (0x180143, 0x186352, False, 'in the desert'),
'Cave 45': (0x180003, False, 'alone in a cave'), 'Desert Palace - Big Chest': (0xe98f, 0x186163, False, 'in Desert Palace'),
'Graveyard Cave': (0x180004, False, 'alone in a cave'), 'Desert Palace - Torch': (0x180160, 0x186362, False, 'in Desert Palace'),
'Checkerboard Cave': (0x180005, False, 'alone in a cave'), 'Desert Palace - Map Chest': (0xe9b6, 0x18618a, False, 'in Desert Palace'),
'Mini Moldorm Cave - Far Left': (0xEB42, False, 'near Moldorms'), 'Desert Palace - Compass Chest': (0xe9cb, 0x18619f, False, 'in Desert Palace'),
'Mini Moldorm Cave - Left': (0xEB45, False, 'near Moldorms'), 'Desert Palace - Big Key Chest': (0xe9c2, 0x186196, False, 'in Desert Palace'),
'Mini Moldorm Cave - Right': (0xEB48, False, 'near Moldorms'), 'Desert Palace - Boss': (0x180151, 0x18633f, False, 'with Lanmolas'),
'Mini Moldorm Cave - Far Right': (0xEB4B, False, 'near Moldorms'), 'Eastern Palace - Compass Chest': (0xe977, 0x18614b, False, 'in Eastern Palace'),
'Mini Moldorm Cave - Generous Guy': (0x180010, False, 'near Moldorms'), 'Eastern Palace - Big Chest': (0xe97d, 0x186151, False, 'in Eastern Palace'),
'Ice Rod Cave': (0xEB4E, False, 'in a frozen cave'), 'Eastern Palace - Cannonball Chest': (0xe9b3, 0x186187, False, 'in Eastern Palace'),
'Bonk Rock Cave': (0xEB3F, False, 'alone in a cave'), 'Eastern Palace - Big Key Chest': (0xe9b9, 0x18618d, False, 'in Eastern Palace'),
'Library': (0x180012, False, 'near books'), 'Eastern Palace - Map Chest': (0xe9f5, 0x1861c9, False, 'in Eastern Palace'),
'Potion Shop': (0x180014, False, 'near potions'), 'Eastern Palace - Boss': (0x180150, 0x18633e, False, 'with the Armos'),
'Lake Hylia Island': (0x180144, False, 'on an island'), 'Master Sword Pedestal': (0x289b0, 0x186369, False, 'at the pedestal'),
'Maze Race': (0x180142, False, 'at the race'), 'Hyrule Castle - Boomerang Chest': (0xe974, 0x186148, False, 'in Hyrule Castle'),
'Desert Ledge': (0x180143, False, 'in the desert'), 'Hyrule Castle - Map Chest': (0xeb0c, 0x1862e0, False, 'in Hyrule Castle'),
'Desert Palace - Big Chest': (0xE98F, False, 'in Desert Palace'), "Hyrule Castle - Zelda's Chest": (0xeb09, 0x1862dd, False, 'in Hyrule Castle'),
'Desert Palace - Torch': (0x180160, False, 'in Desert Palace'), 'Sewers - Dark Cross': (0xe96e, 0x186142, False, 'in the sewers'),
'Desert Palace - Map Chest': (0xE9B6, False, 'in Desert Palace'), 'Sewers - Secret Room - Left': (0xeb5d, 0x186331, False, 'in the sewers'),
'Desert Palace - Compass Chest': (0xE9CB, False, 'in Desert Palace'), 'Sewers - Secret Room - Middle': (0xeb60, 0x186334, False, 'in the sewers'),
'Desert Palace - Big Key Chest': (0xE9C2, False, 'in Desert Palace'), 'Sewers - Secret Room - Right': (0xeb63, 0x186337, False, 'in the sewers'),
'Desert Palace - Boss': (0x180151, False, 'with Lanmolas'), 'Sanctuary': (0xea79, 0x18624d, False, 'in Sanctuary'),
'Eastern Palace - Compass Chest': (0xE977, False, 'in Eastern Palace'), 'Castle Tower - Room 03': (0xeab5, 0x186289, False, 'in Castle Tower'),
'Eastern Palace - Big Chest': (0xE97D, False, 'in Eastern Palace'), 'Castle Tower - Dark Maze': (0xeab2, 0x186286, False, 'in Castle Tower'),
'Eastern Palace - Cannonball Chest': (0xE9B3, False, 'in Eastern Palace'), 'Old Man': (0xf69fa, 0x186364, False, 'with the old man'),
'Eastern Palace - Big Key Chest': (0xE9B9, False, 'in Eastern Palace'), 'Spectacle Rock Cave': (0x180002, 0x18634a, False, 'alone in a cave'),
'Eastern Palace - Map Chest': (0xE9F5, False, 'in Eastern Palace'), 'Paradox Cave Lower - Far Left': (0xeb2a, 0x1862fe, False, 'in a cave with seven chests'),
'Eastern Palace - Boss': (0x180150, False, 'with the Armos'), 'Paradox Cave Lower - Left': (0xeb2d, 0x186301, False, 'in a cave with seven chests'),
'Master Sword Pedestal': (0x289B0, False, 'at the pedestal'), 'Paradox Cave Lower - Right': (0xeb30, 0x186304, False, 'in a cave with seven chests'),
'Hyrule Castle - Boomerang Chest': (0xE974, False, 'in Hyrule Castle'), 'Paradox Cave Lower - Far Right': (0xeb33, 0x186307, False, 'in a cave with seven chests'),
'Hyrule Castle - Map Chest': (0xEB0C, False, 'in Hyrule Castle'), 'Paradox Cave Lower - Middle': (0xeb36, 0x18630a, False, 'in a cave with seven chests'),
'Hyrule Castle - Zelda\'s Chest': (0xEB09, False, 'in Hyrule Castle'), 'Paradox Cave Upper - Left': (0xeb39, 0x18630d, False, 'in a cave with seven chests'),
'Sewers - Dark Cross': (0xE96E, False, 'in the sewers'), 'Paradox Cave Upper - Right': (0xeb3c, 0x186310, False, 'in a cave with seven chests'),
'Sewers - Secret Room - Left': (0xEB5D, False, 'in the sewers'), 'Spiral Cave': (0xe9bf, 0x186193, False, 'in spiral cave'),
'Sewers - Secret Room - Middle': (0xEB60, False, 'in the sewers'), 'Ether Tablet': (0x180016, 0x18633b, False, 'at a monolith'),
'Sewers - Secret Room - Right': (0xEB63, False, 'in the sewers'), 'Spectacle Rock': (0x180140, 0x18634f, False, 'atop a rock'),
'Sanctuary': (0xEA79, False, 'in Sanctuary'), 'Tower of Hera - Basement Cage': (0x180162, 0x18633a, False, 'in Tower of Hera'),
'Castle Tower - Room 03': (0xEAB5, False, 'in Castle Tower'), 'Tower of Hera - Map Chest': (0xe9ad, 0x186181, False, 'in Tower of Hera'),
'Castle Tower - Dark Maze': (0xEAB2, False, 'in Castle Tower'), 'Tower of Hera - Big Key Chest': (0xe9e6, 0x1861ba, False, 'in Tower of Hera'),
'Old Man': (0xF69FA, False, 'with the old man'), 'Tower of Hera - Compass Chest': (0xe9fb, 0x1861cf, False, 'in Tower of Hera'),
'Spectacle Rock Cave': (0x180002, False, 'alone in a cave'), 'Tower of Hera - Big Chest': (0xe9f8, 0x1861cc, False, 'in Tower of Hera'),
'Paradox Cave Lower - Far Left': (0xEB2A, False, 'in a cave with seven chests'), 'Tower of Hera - Boss': (0x180152, 0x186340, False, 'with Moldorm'),
'Paradox Cave Lower - Left': (0xEB2D, False, 'in a cave with seven chests'), 'Pyramid': (0x180147, 0x186356, False, 'on the pyramid'),
'Paradox Cave Lower - Right': (0xEB30, False, 'in a cave with seven chests'), 'Catfish': (0xee185, 0x186361, False, 'with a catfish'),
'Paradox Cave Lower - Far Right': (0xEB33, False, 'in a cave with seven chests'), 'Stumpy': (0x330c7, 0x18636a, False, 'with tree boy'),
'Paradox Cave Lower - Middle': (0xEB36, False, 'in a cave with seven chests'), 'Digging Game': (0x180148, 0x186357, False, 'underground'),
'Paradox Cave Upper - Left': (0xEB39, False, 'in a cave with seven chests'), 'Bombos Tablet': (0x180017, 0x18633c, False, 'at a monolith'),
'Paradox Cave Upper - Right': (0xEB3C, False, 'in a cave with seven chests'), 'Hype Cave - Top': (0xeb1e, 0x1862f2, False, 'near a bat-like man'),
'Spiral Cave': (0xE9BF, False, 'in spiral cave'), 'Hype Cave - Middle Right': (0xeb21, 0x1862f5, False, 'near a bat-like man'),
'Ether Tablet': (0x180016, False, 'at a monolith'), 'Hype Cave - Middle Left': (0xeb24, 0x1862f8, False, 'near a bat-like man'),
'Spectacle Rock': (0x180140, False, 'atop a rock'), 'Hype Cave - Bottom': (0xeb27, 0x1862fb, False, 'near a bat-like man'),
'Tower of Hera - Basement Cage': (0x180162, False, 'in Tower of Hera'), 'Hype Cave - Generous Guy': (0x180011, 0x18635b, False, 'with a bat-like man'),
'Tower of Hera - Map Chest': (0xE9AD, False, 'in Tower of Hera'), 'Peg Cave': (0x180006, 0x18634e, False, 'alone in a cave'),
'Tower of Hera - Big Key Chest': (0xE9E6, False, 'in Tower of Hera'), 'Pyramid Fairy - Left': (0xe980, 0x186154, False, 'near a fairy'),
'Tower of Hera - Compass Chest': (0xE9FB, False, 'in Tower of Hera'), 'Pyramid Fairy - Right': (0xe983, 0x186157, False, 'near a fairy'),
'Tower of Hera - Big Chest': (0xE9F8, False, 'in Tower of Hera'), 'Brewery': (0xe9ec, 0x1861c0, False, 'alone in a home'),
'Tower of Hera - Boss': (0x180152, False, 'with Moldorm'), 'C-Shaped House': (0xe9ef, 0x1861c3, False, 'alone in a home'),
'Pyramid': (0x180147, False, 'on the pyramid'), 'Chest Game': (0xeda8, 0x18636b, False, 'as a prize'),
'Catfish': (0xEE185, False, 'with a catfish'), 'Bumper Cave Ledge': (0x180146, 0x186355, False, 'on a ledge'),
'Stumpy': (0x330C7, False, 'with tree boy'), 'Mire Shed - Left': (0xea73, 0x186247, False, 'near sparks'),
'Digging Game': (0x180148, False, 'underground'), 'Mire Shed - Right': (0xea76, 0x18624a, False, 'near sparks'),
'Bombos Tablet': (0x180017, False, 'at a monolith'), 'Superbunny Cave - Top': (0xea7c, 0x186250, False, 'in a connection'),
'Hype Cave - Top': (0xEB1E, False, 'near a bat-like man'), 'Superbunny Cave - Bottom': (0xea7f, 0x186253, False, 'in a connection'),
'Hype Cave - Middle Right': (0xEB21, False, 'near a bat-like man'), 'Spike Cave': (0xea8b, 0x18625f, False, 'beyond spikes'),
'Hype Cave - Middle Left': (0xEB24, False, 'near a bat-like man'), 'Hookshot Cave - Top Right': (0xeb51, 0x186325, False, 'across pits'),
'Hype Cave - Bottom': (0xEB27, False, 'near a bat-like man'), 'Hookshot Cave - Top Left': (0xeb54, 0x186328, False, 'across pits'),
'Hype Cave - Generous Guy': (0x180011, False, 'with a bat-like man'), 'Hookshot Cave - Bottom Right': (0xeb5a, 0x18632e, False, 'across pits'),
'Peg Cave': (0x180006, False, 'alone in a cave'), 'Hookshot Cave - Bottom Left': (0xeb57, 0x18632b, False, 'across pits'),
'Pyramid Fairy - Left': (0xE980, False, 'near a fairy'), 'Floating Island': (0x180141, 0x186350, False, 'on an island'),
'Pyramid Fairy - Right': (0xE983, False, 'near a fairy'), 'Mimic Cave': (0xe9c5, 0x186199, False, 'in a cave of mimicry'),
'Brewery': (0xE9EC, False, 'alone in a home'), 'Swamp Palace - Entrance': (0xea9d, 0x186271, False, 'in Swamp Palace'),
'C-Shaped House': (0xE9EF, False, 'alone in a home'), 'Swamp Palace - Map Chest': (0xe986, 0x18615a, False, 'in Swamp Palace'),
'Chest Game': (0xEDA8, False, 'as a prize'), 'Swamp Palace - Big Chest': (0xe989, 0x18615d, False, 'in Swamp Palace'),
'Bumper Cave Ledge': (0x180146, False, 'on a ledge'), 'Swamp Palace - Compass Chest': (0xeaa0, 0x186274, False, 'in Swamp Palace'),
'Mire Shed - Left': (0xEA73, False, 'near sparks'), 'Swamp Palace - Big Key Chest': (0xeaa6, 0x18627a, False, 'in Swamp Palace'),
'Mire Shed - Right': (0xEA76, False, 'near sparks'), 'Swamp Palace - West Chest': (0xeaa3, 0x186277, False, 'in Swamp Palace'),
'Superbunny Cave - Top': (0xEA7C, False, 'in a connection'), 'Swamp Palace - Flooded Room - Left': (0xeaa9, 0x18627d, False, 'in Swamp Palace'),
'Superbunny Cave - Bottom': (0xEA7F, False, 'in a connection'), 'Swamp Palace - Flooded Room - Right': (0xeaac, 0x186280, False, 'in Swamp Palace'),
'Spike Cave': (0xEA8B, False, 'beyond spikes'), 'Swamp Palace - Waterfall Room': (0xeaaf, 0x186283, False, 'in Swamp Palace'),
'Hookshot Cave - Top Right': (0xEB51, False, 'across pits'), 'Swamp Palace - Boss': (0x180154, 0x186342, False, 'with Arrghus'),
'Hookshot Cave - Top Left': (0xEB54, False, 'across pits'), "Thieves' Town - Big Key Chest": (0xea04, 0x1861d8, False, "in Thieves' Town"),
'Hookshot Cave - Bottom Right': (0xEB5A, False, 'across pits'), "Thieves' Town - Map Chest": (0xea01, 0x1861d5, False, "in Thieves' Town"),
'Hookshot Cave - Bottom Left': (0xEB57, False, 'across pits'), "Thieves' Town - Compass Chest": (0xea07, 0x1861db, False, "in Thieves' Town"),
'Floating Island': (0x180141, False, 'on an island'), "Thieves' Town - Ambush Chest": (0xea0a, 0x1861de, False, "in Thieves' Town"),
'Mimic Cave': (0xE9C5, False, 'in a cave of mimicry'), "Thieves' Town - Attic": (0xea0d, 0x1861e1, False, "in Thieves' Town"),
'Swamp Palace - Entrance': (0xEA9D, False, 'in Swamp Palace'), "Thieves' Town - Big Chest": (0xea10, 0x1861e4, False, "in Thieves' Town"),
'Swamp Palace - Map Chest': (0xE986, False, 'in Swamp Palace'), "Thieves' Town - Blind's Cell": (0xea13, 0x1861e7, False, "in Thieves' Town"),
'Swamp Palace - Big Chest': (0xE989, False, 'in Swamp Palace'), "Thieves' Town - Boss": (0x180156, 0x186344, False, 'with Blind'),
'Swamp Palace - Compass Chest': (0xEAA0, False, 'in Swamp Palace'), 'Skull Woods - Compass Chest': (0xe992, 0x186166, False, 'in Skull Woods'),
'Swamp Palace - Big Key Chest': (0xEAA6, False, 'in Swamp Palace'), 'Skull Woods - Map Chest': (0xe99b, 0x18616f, False, 'in Skull Woods'),
'Swamp Palace - West Chest': (0xEAA3, False, 'in Swamp Palace'), 'Skull Woods - Big Chest': (0xe998, 0x18616c, False, 'in Skull Woods'),
'Swamp Palace - Flooded Room - Left': (0xEAA9, False, 'in Swamp Palace'), 'Skull Woods - Pot Prison': (0xe9a1, 0x186175, False, 'in Skull Woods'),
'Swamp Palace - Flooded Room - Right': (0xEAAC, False, 'in Swamp Palace'), 'Skull Woods - Pinball Room': (0xe9c8, 0x18619c, False, 'in Skull Woods'),
'Swamp Palace - Waterfall Room': (0xEAAF, False, 'in Swamp Palace'), 'Skull Woods - Big Key Chest': (0xe99e, 0x186172, False, 'in Skull Woods'),
'Swamp Palace - Boss': (0x180154, False, 'with Arrghus'), 'Skull Woods - Bridge Room': (0xe9fe, 0x1861d2, False, 'near Mothula'),
'Thieves\' Town - Big Key Chest': (0xEA04, False, 'in Thieves\' Town'), 'Skull Woods - Boss': (0x180155, 0x186343, False, 'with Mothula'),
'Thieves\' Town - Map Chest': (0xEA01, False, 'in Thieves\' Town'), 'Ice Palace - Compass Chest': (0xe9d4, 0x1861a8, False, 'in Ice Palace'),
'Thieves\' Town - Compass Chest': (0xEA07, False, 'in Thieves\' Town'), 'Ice Palace - Freezor Chest': (0xe995, 0x186169, False, 'in Ice Palace'),
'Thieves\' Town - Ambush Chest': (0xEA0A, False, 'in Thieves\' Town'), 'Ice Palace - Big Chest': (0xe9aa, 0x18617e, False, 'in Ice Palace'),
'Thieves\' Town - Attic': (0xEA0D, False, 'in Thieves\' Town'), 'Ice Palace - Iced T Room': (0xe9e3, 0x1861b7, False, 'in Ice Palace'),
'Thieves\' Town - Big Chest': (0xEA10, False, 'in Thieves\' Town'), 'Ice Palace - Spike Room': (0xe9e0, 0x1861b4, False, 'in Ice Palace'),
'Thieves\' Town - Blind\'s Cell': (0xEA13, False, 'in Thieves\' Town'), 'Ice Palace - Big Key Chest': (0xe9a4, 0x186178, False, 'in Ice Palace'),
'Thieves\' Town - Boss': (0x180156, False, 'with Blind'), 'Ice Palace - Map Chest': (0xe9dd, 0x1861b1, False, 'in Ice Palace'),
'Skull Woods - Compass Chest': (0xE992, False, 'in Skull Woods'), 'Ice Palace - Boss': (0x180157, 0x186345, False, 'with Kholdstare'),
'Skull Woods - Map Chest': (0xE99B, False, 'in Skull Woods'), 'Misery Mire - Big Chest': (0xea67, 0x18623b, False, 'in Misery Mire'),
'Skull Woods - Big Chest': (0xE998, False, 'in Skull Woods'), 'Misery Mire - Map Chest': (0xea6a, 0x18623e, False, 'in Misery Mire'),
'Skull Woods - Pot Prison': (0xE9A1, False, 'in Skull Woods'), 'Misery Mire - Main Lobby': (0xea5e, 0x186232, False, 'in Misery Mire'),
'Skull Woods - Pinball Room': (0xE9C8, False, 'in Skull Woods'), 'Misery Mire - Bridge Chest': (0xea61, 0x186235, False, 'in Misery Mire'),
'Skull Woods - Big Key Chest': (0xE99E, False, 'in Skull Woods'), 'Misery Mire - Spike Chest': (0xe9da, 0x1861ae, False, 'in Misery Mire'),
'Skull Woods - Bridge Room': (0xE9FE, False, 'near Mothula'), 'Misery Mire - Compass Chest': (0xea64, 0x186238, False, 'in Misery Mire'),
'Skull Woods - Boss': (0x180155, False, 'with Mothula'), 'Misery Mire - Big Key Chest': (0xea6d, 0x186241, False, 'in Misery Mire'),
'Ice Palace - Compass Chest': (0xE9D4, False, 'in Ice Palace'), 'Misery Mire - Boss': (0x180158, 0x186346, False, 'with Vitreous'),
'Ice Palace - Freezor Chest': (0xE995, False, 'in Ice Palace'), 'Turtle Rock - Compass Chest': (0xea22, 0x1861f6, False, 'in Turtle Rock'),
'Ice Palace - Big Chest': (0xE9AA, False, 'in Ice Palace'), 'Turtle Rock - Roller Room - Left': (0xea1c, 0x1861f0, False, 'in Turtle Rock'),
'Ice Palace - Iced T Room': (0xE9E3, False, 'in Ice Palace'), 'Turtle Rock - Roller Room - Right': (0xea1f, 0x1861f3, False, 'in Turtle Rock'),
'Ice Palace - Spike Room': (0xE9E0, False, 'in Ice Palace'), 'Turtle Rock - Chain Chomps': (0xea16, 0x1861ea, False, 'in Turtle Rock'),
'Ice Palace - Big Key Chest': (0xE9A4, False, 'in Ice Palace'), 'Turtle Rock - Big Key Chest': (0xea25, 0x1861f9, False, 'in Turtle Rock'),
'Ice Palace - Map Chest': (0xE9DD, False, 'in Ice Palace'), 'Turtle Rock - Big Chest': (0xea19, 0x1861ed, False, 'in Turtle Rock'),
'Ice Palace - Boss': (0x180157, False, 'with Kholdstare'), 'Turtle Rock - Crystaroller Room': (0xea34, 0x186208, False, 'in Turtle Rock'),
'Misery Mire - Big Chest': (0xEA67, False, 'in Misery Mire'), 'Turtle Rock - Eye Bridge - Bottom Left': (0xea31, 0x186205, False, 'in Turtle Rock'),
'Misery Mire - Map Chest': (0xEA6A, False, 'in Misery Mire'), 'Turtle Rock - Eye Bridge - Bottom Right': (0xea2e, 0x186202, False, 'in Turtle Rock'),
'Misery Mire - Main Lobby': (0xEA5E, False, 'in Misery Mire'), 'Turtle Rock - Eye Bridge - Top Left': (0xea2b, 0x1861ff, False, 'in Turtle Rock'),
'Misery Mire - Bridge Chest': (0xEA61, False, 'in Misery Mire'), 'Turtle Rock - Eye Bridge - Top Right': (0xea28, 0x1861fc, False, 'in Turtle Rock'),
'Misery Mire - Spike Chest': (0xE9DA, False, 'in Misery Mire'), 'Turtle Rock - Boss': (0x180159, 0x186347, False, 'with Trinexx'),
'Misery Mire - Compass Chest': (0xEA64, False, 'in Misery Mire'), 'Palace of Darkness - Shooter Room': (0xea5b, 0x18622f, False, 'in Palace of Darkness'),
'Misery Mire - Big Key Chest': (0xEA6D, False, 'in Misery Mire'), 'Palace of Darkness - The Arena - Bridge': (0xea3d, 0x186211, False, 'in Palace of Darkness'),
'Misery Mire - Boss': (0x180158, False, 'with Vitreous'), 'Palace of Darkness - Stalfos Basement': (0xea49, 0x18621d, False, 'in Palace of Darkness'),
'Turtle Rock - Compass Chest': (0xEA22, False, 'in Turtle Rock'), 'Palace of Darkness - Big Key Chest': (0xea37, 0x18620b, False, 'in Palace of Darkness'),
'Turtle Rock - Roller Room - Left': (0xEA1C, False, 'in Turtle Rock'), 'Palace of Darkness - The Arena - Ledge': (0xea3a, 0x18620e, False, 'in Palace of Darkness'),
'Turtle Rock - Roller Room - Right': (0xEA1F, False, 'in Turtle Rock'), 'Palace of Darkness - Map Chest': (0xea52, 0x186226, False, 'in Palace of Darkness'),
'Turtle Rock - Chain Chomps': (0xEA16, False, 'in Turtle Rock'), 'Palace of Darkness - Compass Chest': (0xea43, 0x186217, False, 'in Palace of Darkness'),
'Turtle Rock - Big Key Chest': (0xEA25, False, 'in Turtle Rock'), 'Palace of Darkness - Dark Basement - Left': (0xea4c, 0x186220, False, 'in Palace of Darkness'),
'Turtle Rock - Big Chest': (0xEA19, False, 'in Turtle Rock'), 'Palace of Darkness - Dark Basement - Right': (0xea4f, 0x186223, False, 'in Palace of Darkness'),
'Turtle Rock - Crystaroller Room': (0xEA34, False, 'in Turtle Rock'), 'Palace of Darkness - Dark Maze - Top': (0xea55, 0x186229, False, 'in Palace of Darkness'),
'Turtle Rock - Eye Bridge - Bottom Left': (0xEA31, False, 'in Turtle Rock'), 'Palace of Darkness - Dark Maze - Bottom': (0xea58, 0x18622c, False, 'in Palace of Darkness'),
'Turtle Rock - Eye Bridge - Bottom Right': (0xEA2E, False, 'in Turtle Rock'), 'Palace of Darkness - Big Chest': (0xea40, 0x186214, False, 'in Palace of Darkness'),
'Turtle Rock - Eye Bridge - Top Left': (0xEA2B, False, 'in Turtle Rock'), 'Palace of Darkness - Harmless Hellway': (0xea46, 0x18621a, False, 'in Palace of Darkness'),
'Turtle Rock - Eye Bridge - Top Right': (0xEA28, False, 'in Turtle Rock'), 'Palace of Darkness - Boss': (0x180153, 0x186341, False, 'with Helmasaur King'),
'Turtle Rock - Boss': (0x180159, False, 'with Trinexx'), "Ganons Tower - Bob's Torch": (0x180161, 0x186363, False, "in Ganon's Tower"),
'Palace of Darkness - Shooter Room': (0xEA5B, False, 'in Palace of Darkness'), 'Ganons Tower - Hope Room - Left': (0xead9, 0x1862ad, False, "in Ganon's Tower"),
'Palace of Darkness - The Arena - Bridge': (0xEA3D, False, 'in Palace of Darkness'), 'Ganons Tower - Hope Room - Right': (0xeadc, 0x1862b0, False, "in Ganon's Tower"),
'Palace of Darkness - Stalfos Basement': (0xEA49, False, 'in Palace of Darkness'), 'Ganons Tower - Tile Room': (0xeae2, 0x1862b6, False, "in Ganon's Tower"),
'Palace of Darkness - Big Key Chest': (0xEA37, False, 'in Palace of Darkness'), 'Ganons Tower - Compass Room - Top Left': (0xeae5, 0x1862b9, False, "in Ganon's Tower"),
'Palace of Darkness - The Arena - Ledge': (0xEA3A, False, 'in Palace of Darkness'), 'Ganons Tower - Compass Room - Top Right': (0xeae8, 0x1862bc, False, "in Ganon's Tower"),
'Palace of Darkness - Map Chest': (0xEA52, False, 'in Palace of Darkness'), 'Ganons Tower - Compass Room - Bottom Left': (0xeaeb, 0x1862bf, False, "in Ganon's Tower"),
'Palace of Darkness - Compass Chest': (0xEA43, False, 'in Palace of Darkness'), 'Ganons Tower - Compass Room - Bottom Right': (0xeaee, 0x1862c2, False, "in Ganon's Tower"),
'Palace of Darkness - Dark Basement - Left': (0xEA4C, False, 'in Palace of Darkness'), 'Ganons Tower - DMs Room - Top Left': (0xeab8, 0x18628c, False, "in Ganon's Tower"),
'Palace of Darkness - Dark Basement - Right': (0xEA4F, False, 'in Palace of Darkness'), 'Ganons Tower - DMs Room - Top Right': (0xeabb, 0x18628f, False, "in Ganon's Tower"),
'Palace of Darkness - Dark Maze - Top': (0xEA55, False, 'in Palace of Darkness'), 'Ganons Tower - DMs Room - Bottom Left': (0xeabe, 0x186292, False, "in Ganon's Tower"),
'Palace of Darkness - Dark Maze - Bottom': (0xEA58, False, 'in Palace of Darkness'), 'Ganons Tower - DMs Room - Bottom Right': (0xeac1, 0x186295, False, "in Ganon's Tower"),
'Palace of Darkness - Big Chest': (0xEA40, False, 'in Palace of Darkness'), 'Ganons Tower - Map Chest': (0xead3, 0x1862a7, False, "in Ganon's Tower"),
'Palace of Darkness - Harmless Hellway': (0xEA46, False, 'in Palace of Darkness'), 'Ganons Tower - Firesnake Room': (0xead0, 0x1862a4, False, "in Ganon's Tower"),
'Palace of Darkness - Boss': (0x180153, False, 'with Helmasaur King'), 'Ganons Tower - Randomizer Room - Top Left': (0xeac4, 0x186298, False, "in Ganon's Tower"),
'Ganons Tower - Bob\'s Torch': (0x180161, False, 'in Ganon\'s Tower'), 'Ganons Tower - Randomizer Room - Top Right': (0xeac7, 0x18629b, False, "in Ganon's Tower"),
'Ganons Tower - Hope Room - Left': (0xEAD9, False, 'in Ganon\'s Tower'), 'Ganons Tower - Randomizer Room - Bottom Left': (0xeaca, 0x18629e, False, "in Ganon's Tower"),
'Ganons Tower - Hope Room - Right': (0xEADC, False, 'in Ganon\'s Tower'), 'Ganons Tower - Randomizer Room - Bottom Right': (0xeacd, 0x1862a1, False, "in Ganon's Tower"),
'Ganons Tower - Tile Room': (0xEAE2, False, 'in Ganon\'s Tower'), "Ganons Tower - Bob's Chest": (0xeadf, 0x1862b3, False, "in Ganon's Tower"),
'Ganons Tower - Compass Room - Top Left': (0xEAE5, False, 'in Ganon\'s Tower'), 'Ganons Tower - Big Chest': (0xead6, 0x1862aa, False, "in Ganon's Tower"),
'Ganons Tower - Compass Room - Top Right': (0xEAE8, False, 'in Ganon\'s Tower'), 'Ganons Tower - Big Key Room - Left': (0xeaf4, 0x1862c8, False, "in Ganon's Tower"),
'Ganons Tower - Compass Room - Bottom Left': (0xEAEB, False, 'in Ganon\'s Tower'), 'Ganons Tower - Big Key Room - Right': (0xeaf7, 0x1862cb, False, "in Ganon's Tower"),
'Ganons Tower - Compass Room - Bottom Right': (0xEAEE, False, 'in Ganon\'s Tower'), 'Ganons Tower - Big Key Chest': (0xeaf1, 0x1862c5, False, "in Ganon's Tower"),
'Ganons Tower - DMs Room - Top Left': (0xEAB8, False, 'in Ganon\'s Tower'), 'Ganons Tower - Mini Helmasaur Room - Left': (0xeafd, 0x1862d1, False, "atop Ganon's Tower"),
'Ganons Tower - DMs Room - Top Right': (0xEABB, False, 'in Ganon\'s Tower'), 'Ganons Tower - Mini Helmasaur Room - Right': (0xeb00, 0x1862d4, False, "atop Ganon's Tower"),
'Ganons Tower - DMs Room - Bottom Left': (0xEABE, False, 'in Ganon\'s Tower'), 'Ganons Tower - Pre-Moldorm Chest': (0xeb03, 0x1862d7, False, "atop Ganon's Tower"),
'Ganons Tower - DMs Room - Bottom Right': (0xEAC1, False, 'in Ganon\'s Tower'), 'Ganons Tower - Validation Chest': (0xeb06, 0x1862da, False, "atop Ganon's Tower"),
'Ganons Tower - Map Chest': (0xEAD3, False, 'in Ganon\'s Tower'), 'Ganon': (None, None, False, 'from me'),
'Ganons Tower - Firesnake Room': (0xEAD0, False, 'in Ganon\'s Tower'), 'Agahnim 1': (None, None, False, 'from Ganon\'s wizardry form'),
'Ganons Tower - Randomizer Room - Top Left': (0xEAC4, False, 'in Ganon\'s Tower'), 'Agahnim 2': (None, None, False, 'from Ganon\'s wizardry form'),
'Ganons Tower - Randomizer Room - Top Right': (0xEAC7, False, 'in Ganon\'s Tower'), 'Floodgate': (None, None, False, None),
'Ganons Tower - Randomizer Room - Bottom Left': (0xEACA, False, 'in Ganon\'s Tower'), 'Frog': (None, None, False, None),
'Ganons Tower - Randomizer Room - Bottom Right': (0xEACD, False, 'in Ganon\'s Tower'), 'Missing Smith': (None, None, False, None),
'Ganons Tower - Bob\'s Chest': (0xEADF, False, 'in Ganon\'s Tower'), 'Dark Blacksmith Ruins': (None, None, False, None),
'Ganons Tower - Big Chest': (0xEAD6, False, 'in Ganon\'s Tower'), 'Eastern Palace - Prize': ([0x1209D, 0x53EF8, 0x53EF9, 0x180052, 0x18007C, 0xC6FE], None, True, 'Eastern Palace'),
'Ganons Tower - Big Key Room - Left': (0xEAF4, False, 'in Ganon\'s Tower'), 'Desert Palace - Prize': ([0x1209E, 0x53F1C, 0x53F1D, 0x180053, 0x180078, 0xC6FF], None, True, 'Desert Palace'),
'Ganons Tower - Big Key Room - Right': (0xEAF7, False, 'in Ganon\'s Tower'), 'Tower of Hera - Prize': ([0x120A5, 0x53F0A, 0x53F0B, 0x18005A, 0x18007A, 0xC706], None, True, 'Tower of Hera'),
'Ganons Tower - Big Key Chest': (0xEAF1, False, 'in Ganon\'s Tower'), 'Palace of Darkness - Prize': ([0x120A1, 0x53F00, 0x53F01, 0x180056, 0x18007D, 0xC702], None, True, 'Palace of Darkness'),
'Ganons Tower - Mini Helmasaur Room - Left': (0xEAFD, False, 'atop Ganon\'s Tower'), 'Swamp Palace - Prize': ([0x120A0, 0x53F6C, 0x53F6D, 0x180055, 0x180071, 0xC701], None, True, 'Swamp Palace'),
'Ganons Tower - Mini Helmasaur Room - Right': (0xEB00, False, 'atop Ganon\'s Tower'), 'Thieves\' Town - Prize': ([0x120A6, 0x53F36, 0x53F37, 0x18005B, 0x180077, 0xC707], None, True, 'Thieves\' Town'),
'Ganons Tower - Pre-Moldorm Chest': (0xEB03, False, 'atop Ganon\'s Tower'), 'Skull Woods - Prize': ([0x120A3, 0x53F12, 0x53F13, 0x180058, 0x18007B, 0xC704], None, True, 'Skull Woods'),
'Ganons Tower - Validation Chest': (0xEB06, False, 'atop Ganon\'s Tower'), 'Ice Palace - Prize': ([0x120A4, 0x53F5A, 0x53F5B, 0x180059, 0x180073, 0xC705], None, True, 'Ice Palace'),
'Ganon': (None, False, 'from me'), 'Misery Mire - Prize': ([0x120A2, 0x53F48, 0x53F49, 0x180057, 0x180075, 0xC703], None, True, 'Misery Mire'),
'Agahnim 1': (None, False, 'from Ganon\'s wizardry form'), 'Turtle Rock - Prize': ([0x120A7, 0x53F24, 0x53F25, 0x18005C, 0x180079, 0xC708], None, True, 'Turtle Rock')}
'Agahnim 2': (None, False, 'from Ganon\'s wizardry form'),
'Floodgate': (None, False, None),
'Frog': (None, False, None),
'Missing Smith': (None, False, None),
'Dark Blacksmith Ruins': (None, False, None),
'Eastern Palace - Prize': ([0x1209D, 0x53EF8, 0x53EF9, 0x180052, 0x18007C, 0xC6FE], True, 'Eastern Palace'),
'Desert Palace - Prize': ([0x1209E, 0x53F1C, 0x53F1D, 0x180053, 0x180078, 0xC6FF], True, 'Desert Palace'),
'Tower of Hera - Prize': ([0x120A5, 0x53F0A, 0x53F0B, 0x18005A, 0x18007A, 0xC706], True, 'Tower of Hera'),
'Palace of Darkness - Prize': ([0x120A1, 0x53F00, 0x53F01, 0x180056, 0x18007D, 0xC702], True, 'Palace of Darkness'),
'Swamp Palace - Prize': ([0x120A0, 0x53F6C, 0x53F6D, 0x180055, 0x180071, 0xC701], True, 'Swamp Palace'),
'Thieves\' Town - Prize': ([0x120A6, 0x53F36, 0x53F37, 0x18005B, 0x180077, 0xC707], True, 'Thieves\' Town'),
'Skull Woods - Prize': ([0x120A3, 0x53F12, 0x53F13, 0x180058, 0x18007B, 0xC704], True, 'Skull Woods'),
'Ice Palace - Prize': ([0x120A4, 0x53F5A, 0x53F5B, 0x180059, 0x180073, 0xC705], True, 'Ice Palace'),
'Misery Mire - Prize': ([0x120A2, 0x53F48, 0x53F49, 0x180057, 0x180075, 0xC703], True, 'Misery Mire'),
'Turtle Rock - Prize': ([0x120A7, 0x53F24, 0x53F25, 0x18005C, 0x180079, 0xC708], True, 'Turtle Rock')}
+139 -111
View File
@@ -51,8 +51,8 @@ difficulties = {
progressivearmor = ['Progressive Armor'] * 2, progressivearmor = ['Progressive Armor'] * 2,
basicarmor = ['Blue Mail', 'Red Mail'], basicarmor = ['Blue Mail', 'Red Mail'],
swordless = ['Rupees (20)'] * 4, swordless = ['Rupees (20)'] * 4,
progressivesword = ['Progressive Sword'] * 3, progressivesword = ['Progressive Sword'] * 4,
basicsword = ['Master Sword', 'Tempered Sword', 'Golden Sword'], basicsword = ['Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'],
basicbow = ['Bow', 'Silver Arrows'], basicbow = ['Bow', 'Silver Arrows'],
timedohko = ['Green Clock'] * 25, timedohko = ['Green Clock'] * 25,
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10, timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
@@ -78,8 +78,8 @@ difficulties = {
progressivearmor = ['Progressive Armor'] * 2, progressivearmor = ['Progressive Armor'] * 2,
basicarmor = ['Progressive Armor'] * 2, # neither will count basicarmor = ['Progressive Armor'] * 2, # neither will count
swordless = ['Rupees (20)'] * 4, swordless = ['Rupees (20)'] * 4,
progressivesword = ['Progressive Sword'] * 3, progressivesword = ['Progressive Sword'] * 4,
basicsword = ['Master Sword', 'Master Sword', 'Tempered Sword'], basicsword = ['Fighter Sword', 'Master Sword', 'Master Sword', 'Tempered Sword'],
basicbow = ['Bow'] * 2, basicbow = ['Bow'] * 2,
timedohko = ['Green Clock'] * 25, timedohko = ['Green Clock'] * 25,
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10, timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
@@ -105,8 +105,8 @@ difficulties = {
progressivearmor = ['Progressive Armor'] * 2, # neither will count progressivearmor = ['Progressive Armor'] * 2, # neither will count
basicarmor = ['Progressive Armor'] * 2, # neither will count basicarmor = ['Progressive Armor'] * 2, # neither will count
swordless = ['Rupees (20)'] * 4, swordless = ['Rupees (20)'] * 4,
progressivesword = ['Progressive Sword'] * 3, progressivesword = ['Progressive Sword'] * 4,
basicsword = ['Fighter Sword', 'Master Sword', 'Master Sword'], basicsword = ['Fighter Sword', 'Fighter Sword', 'Master Sword', 'Master Sword'],
basicbow = ['Bow'] * 2, basicbow = ['Bow'] * 2,
timedohko = ['Green Clock'] * 20 + ['Red Clock'] * 5, timedohko = ['Green Clock'] * 20 + ['Red Clock'] * 5,
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10, timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
@@ -125,26 +125,23 @@ difficulties = {
} }
def generate_itempool(world, player): def generate_itempool(world, player):
if (world.difficulty not in ['normal', 'hard', 'expert'] or world.goal not in ['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'crystals'] if (world.difficulty[player] not in ['normal', 'hard', 'expert'] or world.goal[player] not in ['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'crystals']
or world.mode not in ['open', 'standard', 'inverted'] or world.timer not in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'] or world.progressive not in ['on', 'off', 'random']): or world.mode[player] not in ['open', 'standard', 'inverted'] or world.timer not in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'] or world.progressive not in ['on', 'off', 'random']):
raise NotImplementedError('Not supported yet') raise NotImplementedError('Not supported yet')
if world.timer in ['ohko', 'timed-ohko']: if world.timer in ['ohko', 'timed-ohko']:
world.can_take_damage = False world.can_take_damage = False
if world.goal in ['pedestal', 'triforcehunt']: if world.goal[player] in ['pedestal', 'triforcehunt']:
world.push_item(world.get_location('Ganon', player), ItemFactory('Nothing', player), False) world.push_item(world.get_location('Ganon', player), ItemFactory('Nothing', player), False)
else: else:
world.push_item(world.get_location('Ganon', player), ItemFactory('Triforce', player), False) world.push_item(world.get_location('Ganon', player), ItemFactory('Triforce', player), False)
if world.goal in ['triforcehunt']: if world.goal[player] in ['triforcehunt']:
if world.mode == 'inverted': region = world.get_region('Light World',player)
region = world.get_region('Light World',player)
else:
region = world.get_region('Hyrule Castle Courtyard', player)
loc = Location(player, "Murahdahla", parent=region) loc = Location(player, "Murahdahla", parent=region)
loc.access_rule = lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) > state.world.treasure_hunt_count loc.access_rule = lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) > state.world.treasure_hunt_count[player]
region.locations.append(loc) region.locations.append(loc)
world.dynamic_locations.append(loc) world.dynamic_locations.append(loc)
@@ -198,38 +195,82 @@ def generate_itempool(world, player):
# set up item pool # set up item pool
if world.custom: if world.custom:
(pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = make_custom_item_pool(world.progressive, world.shuffle, world.difficulty, world.timer, world.goal, world.mode, world.swords, world.retro, world.customitemarray) (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = make_custom_item_pool(world.progressive, world.shuffle[player], world.difficulty[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.customitemarray)
world.rupoor_cost = min(world.customitemarray[67], 9999) world.rupoor_cost = min(world.customitemarray[69], 9999)
else: else:
(pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = get_pool_core(world.progressive, world.shuffle, world.difficulty, world.timer, world.goal, world.mode, world.swords, world.retro) (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = get_pool_core(world.progressive, world.shuffle[player], world.difficulty[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player])
world.itempool += ItemFactory(pool, player)
for item in precollected_items: for item in precollected_items:
world.push_precollected(ItemFactory(item, player)) world.push_precollected(ItemFactory(item, player))
for (location, item) in placed_items:
if world.mode[player] == 'standard' and not world.state.has_blunt_weapon(player):
if "Link's Uncle" not in placed_items:
found_sword = False
found_bow = False
possible_weapons = []
for item in pool:
if item in ['Progressive Sword', 'Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword']:
if not found_sword and world.swords[player] != 'swordless':
found_sword = True
possible_weapons.append(item)
if item in ['Progressive Bow', 'Bow'] and not found_bow:
found_bow = True
possible_weapons.append(item)
if item in ['Hammer', 'Bombs (10)', 'Fire Rod', 'Cane of Somaria', 'Cane of Byrna']:
if item not in possible_weapons:
possible_weapons.append(item)
starting_weapon = random.choice(possible_weapons)
placed_items["Link's Uncle"] = starting_weapon
pool.remove(starting_weapon)
if placed_items["Link's Uncle"] in ['Bow', 'Progressive Bow', 'Bombs (10)', 'Cane of Somaria', 'Cane of Byrna'] and world.enemy_health[player] not in ['default', 'easy']:
world.escape_assist[player].append('bombs')
for (location, item) in placed_items.items():
world.push_item(world.get_location(location, player), ItemFactory(item, player), False) world.push_item(world.get_location(location, player), ItemFactory(item, player), False)
world.get_location(location, player).event = True world.get_location(location, player).event = True
world.get_location(location, player).locked = True world.get_location(location, player).locked = True
items = ItemFactory(pool, player)
world.lamps_needed_for_dark_rooms = lamps_needed_for_dark_rooms world.lamps_needed_for_dark_rooms = lamps_needed_for_dark_rooms
if clock_mode is not None: if clock_mode is not None:
world.clock_mode = clock_mode world.clock_mode = clock_mode
if treasure_hunt_count is not None:
world.treasure_hunt_count = treasure_hunt_count
if treasure_hunt_icon is not None:
world.treasure_hunt_icon = treasure_hunt_icon
if world.keysanity: if treasure_hunt_count is not None:
world.itempool.extend([item for item in get_dungeon_item_pool(world) if item.player == player]) world.treasure_hunt_count[player] = treasure_hunt_count
if treasure_hunt_icon is not None:
world.treasure_hunt_icon[player] = treasure_hunt_icon
world.itempool.extend([item for item in get_dungeon_item_pool(world) if item.player == player
and ((item.smallkey and world.keyshuffle[player])
or (item.bigkey and world.bigkeyshuffle[player])
or (item.map and world.mapshuffle[player])
or (item.compass and world.compassshuffle[player]))])
# logic has some branches where having 4 hearts is one possible requirement (of several alternatives) # logic has some branches where having 4 hearts is one possible requirement (of several alternatives)
# rather than making all hearts/heart pieces progression items (which slows down generation considerably) # rather than making all hearts/heart pieces progression items (which slows down generation considerably)
# We mark one random heart container as an advancement item (or 4 heart pieces in expert mode) # We mark one random heart container as an advancement item (or 4 heart pieces in expert mode)
if world.difficulty in ['normal', 'hard'] and not (world.custom and world.customitemarray[30] == 0): if world.difficulty[player] in ['normal', 'hard'] and not (world.custom and world.customitemarray[30] == 0):
[item for item in world.itempool if item.name == 'Boss Heart Container' and item.player == player][0].advancement = True [item for item in items if item.name == 'Boss Heart Container'][0].advancement = True
elif world.difficulty in ['expert'] and not (world.custom and world.customitemarray[29] < 4): elif world.difficulty[player] in ['expert'] and not (world.custom and world.customitemarray[29] < 4):
adv_heart_pieces = [item for item in world.itempool if item.name == 'Piece of Heart' and item.player == player][0:4] adv_heart_pieces = [item for item in items if item.name == 'Piece of Heart'][0:4]
for hp in adv_heart_pieces: for hp in adv_heart_pieces:
hp.advancement = True hp.advancement = True
beeweights = {0: {None: 100},
1: {None: 75, 'trap': 25},
2: {None: 40, 'trap': 40, 'bee': 20},
3: {'trap': 50, 'bee': 50},
4: {'trap': 100}}
def beemizer(item):
if world.beemizer[item.player] and not item.advancement and not item.priority and not item.type:
choice = random.choices(list(beeweights[world.beemizer[item.player]].keys()), weights=list(beeweights[world.beemizer[item.player]].values()))[0]
return item if not choice else ItemFactory("Bee Trap", player) if choice == 'trap' else ItemFactory("Bee", player)
return item
world.itempool += [beemizer(item) for item in items]
# shuffle medallions # shuffle medallions
mm_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)] mm_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)]
tr_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)] tr_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)]
@@ -238,7 +279,7 @@ def generate_itempool(world, player):
place_bosses(world, player) place_bosses(world, player)
set_up_shops(world, player) set_up_shops(world, player)
if world.retro: if world.retro[player]:
set_up_take_anys(world, player) set_up_take_anys(world, player)
create_dynamic_shop_locations(world, player) create_dynamic_shop_locations(world, player)
@@ -254,7 +295,7 @@ take_any_locations = [
'Dark Lake Hylia Ledge Spike Cave', 'Fortune Teller (Dark)', 'Dark Sanctuary Hint', 'Dark Desert Hint'] 'Dark Lake Hylia Ledge Spike Cave', 'Fortune Teller (Dark)', 'Dark Sanctuary Hint', 'Dark Desert Hint']
def set_up_take_anys(world, player): def set_up_take_anys(world, player):
if world.mode == 'inverted' and 'Dark Sanctuary Hint' in take_any_locations: if world.mode[player] == 'inverted' and 'Dark Sanctuary Hint' in take_any_locations:
take_any_locations.remove('Dark Sanctuary Hint') take_any_locations.remove('Dark Sanctuary Hint')
regions = random.sample(take_any_locations, 5) regions = random.sample(take_any_locations, 5)
@@ -267,9 +308,8 @@ def set_up_take_anys(world, player):
entrance = world.get_region(reg, player).entrances[0] entrance = world.get_region(reg, player).entrances[0]
connect_entrance(world, entrance, old_man_take_any, player) connect_entrance(world, entrance, old_man_take_any, player)
entrance.target = 0x58 entrance.target = 0x58
old_man_take_any.shop = Shop(old_man_take_any, 0x0112, ShopType.TakeAny, 0xE2, True) old_man_take_any.shop = Shop(old_man_take_any, 0x0112, ShopType.TakeAny, 0xE2, True, True)
world.shops.append(old_man_take_any.shop) world.shops.append(old_man_take_any.shop)
old_man_take_any.shop.active = True
swords = [item for item in world.itempool if item.type == 'Sword' and item.player == player] swords = [item for item in world.itempool if item.type == 'Sword' and item.player == player]
if swords: if swords:
@@ -290,13 +330,12 @@ def set_up_take_anys(world, player):
entrance = world.get_region(reg, player).entrances[0] entrance = world.get_region(reg, player).entrances[0]
connect_entrance(world, entrance, take_any, player) connect_entrance(world, entrance, take_any, player)
entrance.target = target entrance.target = target
take_any.shop = Shop(take_any, room_id, ShopType.TakeAny, 0xE3, True) take_any.shop = Shop(take_any, room_id, ShopType.TakeAny, 0xE3, True, True)
world.shops.append(take_any.shop) world.shops.append(take_any.shop)
take_any.shop.active = True
take_any.shop.add_inventory(0, 'Blue Potion', 0, 0) take_any.shop.add_inventory(0, 'Blue Potion', 0, 0)
take_any.shop.add_inventory(1, 'Boss Heart Container', 0, 0) take_any.shop.add_inventory(1, 'Boss Heart Container', 0, 0)
world.intialize_regions() world.initialize_regions()
def create_dynamic_shop_locations(world, player): def create_dynamic_shop_locations(world, player):
for shop in world.shops: for shop in world.shops:
@@ -333,9 +372,9 @@ def fill_prizes(world, attempts=15):
prize_locs = list(empty_crystal_locations) prize_locs = list(empty_crystal_locations)
random.shuffle(prizepool) random.shuffle(prizepool)
random.shuffle(prize_locs) random.shuffle(prize_locs)
fill_restrictive(world, all_state, prize_locs, prizepool) fill_restrictive(world, all_state, prize_locs, prizepool, True)
except FillError as e: except FillError as e:
logging.getLogger('').info("Failed to place dungeon prizes (%s). Will retry %s more times", e, attempts - attempt) logging.getLogger('').info("Failed to place dungeon prizes (%s). Will retry %s more times", e, attempts - attempt - 1)
for location in empty_crystal_locations: for location in empty_crystal_locations:
location.item = None location.item = None
continue continue
@@ -345,30 +384,23 @@ def fill_prizes(world, attempts=15):
def set_up_shops(world, player): def set_up_shops(world, player):
# Changes to basic Shops
# TODO: move hard+ mode changes for sheilds here, utilizing the new shops # TODO: move hard+ mode changes for sheilds here, utilizing the new shops
for shop in world.shops: if world.retro[player]:
shop.active = True
if world.retro:
rss = world.get_region('Red Shield Shop', player).shop rss = world.get_region('Red Shield Shop', player).shop
rss.active = True if not rss.locked:
rss.add_inventory(2, 'Single Arrow', 80) rss.add_inventory(2, 'Single Arrow', 80)
for shop in random.sample([s for s in world.shops if s.custom and not s.locked and s.region.player == player], 5):
# Randomized changes to Shops shop.locked = True
if world.retro:
for shop in random.sample([s for s in world.shops if s.replaceable and s.type == ShopType.Shop and s.region.player == player], 5):
shop.active = True
shop.add_inventory(0, 'Single Arrow', 80) shop.add_inventory(0, 'Single Arrow', 80)
shop.add_inventory(1, 'Small Key (Universal)', 100) shop.add_inventory(1, 'Small Key (Universal)', 100)
shop.add_inventory(2, 'Bombs (10)', 50) shop.add_inventory(2, 'Bombs (10)', 50)
rss.locked = True
#special shop types
def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, retro): def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, retro):
pool = [] pool = []
placed_items = [] placed_items = {}
precollected_items = [] precollected_items = []
clock_mode = None clock_mode = None
treasure_hunt_count = None treasure_hunt_count = None
@@ -376,6 +408,10 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, r
pool.extend(alwaysitems) pool.extend(alwaysitems)
def place_item(loc, item):
assert loc not in placed_items
placed_items[loc] = item
def want_progressives(): def want_progressives():
return random.choice([True, False]) if progressive == 'random' else progressive == 'on' return random.choice([True, False]) if progressive == 'random' else progressive == 'on'
@@ -388,8 +424,8 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, r
# insanity shuffle doesn't have fake LW/DW logic so for now guaranteed Mirror and Moon Pearl at the start # insanity shuffle doesn't have fake LW/DW logic so for now guaranteed Mirror and Moon Pearl at the start
if shuffle == 'insanity_legacy': if shuffle == 'insanity_legacy':
placed_items.append(('Link\'s House', 'Magic Mirror')) place_item('Link\'s House', 'Magic Mirror')
placed_items.append(('Sanctuary', 'Moon Pearl')) place_item('Sanctuary', 'Moon Pearl')
else: else:
pool.extend(['Magic Mirror', 'Moon Pearl']) pool.extend(['Magic Mirror', 'Moon Pearl'])
@@ -420,50 +456,36 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, r
else: else:
pool.extend(diff.basicarmor) pool.extend(diff.basicarmor)
if swords != 'swordless': if want_progressives():
if want_progressives(): pool.extend(['Progressive Bow'] * 2)
pool.extend(['Progressive Bow'] * 2) elif swords != 'swordless':
else: pool.extend(diff.basicbow)
pool.extend(diff.basicbow) else:
pool.extend(['Bow', 'Silver Arrows'])
if swords == 'swordless': if swords == 'swordless':
pool.extend(diff.swordless) pool.extend(diff.swordless)
if want_progressives():
pool.extend(['Progressive Bow'] * 2)
else:
pool.extend(['Bow', 'Silver Arrows'])
elif swords == 'assured':
precollected_items.append('Fighter Sword')
if want_progressives():
pool.extend(diff.progressivesword)
pool.extend(['Rupees (100)'])
else:
pool.extend(diff.basicsword)
pool.extend(['Rupees (100)'])
elif swords == 'vanilla': elif swords == 'vanilla':
swords_to_use = [] swords_to_use = diff.progressivesword.copy() if want_progressives() else diff.basicsword.copy()
if want_progressives():
swords_to_use.extend(diff.progressivesword)
swords_to_use.extend(['Progressive Sword'])
else:
swords_to_use.extend(diff.basicsword)
swords_to_use.extend(['Fighter Sword'])
random.shuffle(swords_to_use) random.shuffle(swords_to_use)
placed_items.append(('Link\'s Uncle', swords_to_use.pop())) place_item('Link\'s Uncle', swords_to_use.pop())
placed_items.append(('Blacksmith', swords_to_use.pop())) place_item('Blacksmith', swords_to_use.pop())
placed_items.append(('Pyramid Fairy - Left', swords_to_use.pop())) place_item('Pyramid Fairy - Left', swords_to_use.pop())
if goal != 'pedestal': if goal != 'pedestal':
placed_items.append(('Master Sword Pedestal', swords_to_use.pop())) place_item('Master Sword Pedestal', swords_to_use.pop())
else: else:
placed_items.append(('Master Sword Pedestal', 'Triforce')) place_item('Master Sword Pedestal', 'Triforce')
else: else:
if want_progressives(): pool.extend(diff.progressivesword if want_progressives() else diff.basicsword)
pool.extend(diff.progressivesword) if swords == 'assured':
pool.extend(['Progressive Sword']) if want_progressives():
else: precollected_items.append('Progressive Sword')
pool.extend(diff.basicsword) pool.remove('Progressive Sword')
pool.extend(['Fighter Sword']) else:
precollected_items.append('Fighter Sword')
pool.remove('Fighter Sword')
pool.extend(['Rupees (50)'])
extraitems = total_items_to_place - len(pool) - len(placed_items) extraitems = total_items_to_place - len(pool) - len(placed_items)
@@ -487,7 +509,7 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, r
extraitems -= len(extra) extraitems -= len(extra)
if goal == 'pedestal' and swords != 'vanilla': if goal == 'pedestal' and swords != 'vanilla':
placed_items.append(('Master Sword Pedestal', 'Triforce')) place_item('Master Sword Pedestal', 'Triforce')
if retro: if retro:
pool = [item.replace('Single Arrow','Rupees (5)') for item in pool] pool = [item.replace('Single Arrow','Rupees (5)') for item in pool]
pool = [item.replace('Arrows (10)','Rupees (5)') for item in pool] pool = [item.replace('Arrows (10)','Rupees (5)') for item in pool]
@@ -496,30 +518,34 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, r
pool.extend(diff.retro) pool.extend(diff.retro)
if mode == 'standard': if mode == 'standard':
key_location = random.choice(['Secret Passage', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', 'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross']) key_location = random.choice(['Secret Passage', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', 'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross'])
placed_items.append((key_location, 'Small Key (Universal)')) place_item(key_location, 'Small Key (Universal)')
else: else:
pool.extend(['Small Key (Universal)']) pool.extend(['Small Key (Universal)'])
return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms)
def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, swords, retro, customitemarray): def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, swords, retro, customitemarray):
pool = [] pool = []
placed_items = [] placed_items = {}
precollected_items = [] precollected_items = []
clock_mode = None clock_mode = None
treasure_hunt_count = None treasure_hunt_count = None
treasure_hunt_icon = None treasure_hunt_icon = None
def place_item(loc, item):
assert loc not in placed_items
placed_items[loc] = item
# Correct for insanely oversized item counts and take initial steps to handle undersized pools. # Correct for insanely oversized item counts and take initial steps to handle undersized pools.
for x in range(0, 64): for x in range(0, 66):
if customitemarray[x] > total_items_to_place: if customitemarray[x] > total_items_to_place:
customitemarray[x] = total_items_to_place customitemarray[x] = total_items_to_place
if customitemarray[66] > total_items_to_place: if customitemarray[68] > total_items_to_place:
customitemarray[66] = total_items_to_place customitemarray[68] = total_items_to_place
itemtotal = 0 itemtotal = 0
for x in range(0, 65): for x in range(0, 66):
itemtotal = itemtotal + customitemarray[x] itemtotal = itemtotal + customitemarray[x]
itemtotal = itemtotal + customitemarray[66]
itemtotal = itemtotal + customitemarray[68] itemtotal = itemtotal + customitemarray[68]
itemtotal = itemtotal + customitemarray[70]
pool.extend(['Bow'] * customitemarray[0]) pool.extend(['Bow'] * customitemarray[0])
pool.extend(['Silver Arrows']* customitemarray[1]) pool.extend(['Silver Arrows']* customitemarray[1])
@@ -580,8 +606,10 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s
pool.extend(['Blue Clock'] * customitemarray[61]) pool.extend(['Blue Clock'] * customitemarray[61])
pool.extend(['Green Clock'] * customitemarray[62]) pool.extend(['Green Clock'] * customitemarray[62])
pool.extend(['Red Clock'] * customitemarray[63]) pool.extend(['Red Clock'] * customitemarray[63])
pool.extend(['Triforce Piece'] * customitemarray[64]) pool.extend(['Progressive Bow'] * customitemarray[64])
pool.extend(['Triforce'] * customitemarray[66]) pool.extend(['Bombs (10)'] * customitemarray[65])
pool.extend(['Triforce Piece'] * customitemarray[66])
pool.extend(['Triforce'] * customitemarray[68])
diff = difficulties[difficulty] diff = difficulties[difficulty]
@@ -596,12 +624,12 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s
thisbottle = random.choice(diff.bottles) thisbottle = random.choice(diff.bottles)
pool.append(thisbottle) pool.append(thisbottle)
if customitemarray[64] > 0 or customitemarray[65] > 0: if customitemarray[66] > 0 or customitemarray[67] > 0:
treasure_hunt_count = max(min(customitemarray[65], 99), 1) #To display, count must be between 1 and 99. treasure_hunt_count = max(min(customitemarray[67], 99), 1) #To display, count must be between 1 and 99.
treasure_hunt_icon = 'Triforce Piece' treasure_hunt_icon = 'Triforce Piece'
# Ensure game is always possible to complete here, force sufficient pieces if the player is unwilling. # Ensure game is always possible to complete here, force sufficient pieces if the player is unwilling.
if (customitemarray[64] < treasure_hunt_count) and (goal == 'triforcehunt') and (customitemarray[66] == 0): if (customitemarray[66] < treasure_hunt_count) and (goal == 'triforcehunt') and (customitemarray[68] == 0):
extrapieces = treasure_hunt_count - customitemarray[64] extrapieces = treasure_hunt_count - customitemarray[66]
pool.extend(['Triforce Piece'] * extrapieces) pool.extend(['Triforce Piece'] * extrapieces)
itemtotal = itemtotal + extrapieces itemtotal = itemtotal + extrapieces
@@ -613,25 +641,25 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s
clock_mode = 'ohko' clock_mode = 'ohko'
if goal == 'pedestal': if goal == 'pedestal':
placed_items.append(('Master Sword Pedestal', 'Triforce')) place_item('Master Sword Pedestal', 'Triforce')
itemtotal = itemtotal + 1 itemtotal = itemtotal + 1
if mode == 'standard': if mode == 'standard':
if retro: if retro:
key_location = random.choice(['Secret Passage', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', 'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross']) key_location = random.choice(['Secret Passage', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', 'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross'])
placed_items.append((key_location, 'Small Key (Universal)')) place_item(key_location, 'Small Key (Universal)')
pool.extend(['Small Key (Universal)'] * max((customitemarray[68] - 1), 0)) pool.extend(['Small Key (Universal)'] * max((customitemarray[70] - 1), 0))
else: else:
pool.extend(['Small Key (Universal)'] * customitemarray[68]) pool.extend(['Small Key (Universal)'] * customitemarray[70])
else: else:
pool.extend(['Small Key (Universal)'] * customitemarray[68]) pool.extend(['Small Key (Universal)'] * customitemarray[70])
pool.extend(['Fighter Sword'] * customitemarray[32]) pool.extend(['Fighter Sword'] * customitemarray[32])
pool.extend(['Progressive Sword'] * customitemarray[36]) pool.extend(['Progressive Sword'] * customitemarray[36])
if shuffle == 'insanity_legacy': if shuffle == 'insanity_legacy':
placed_items.append(('Link\'s House', 'Magic Mirror')) place_item('Link\'s House', 'Magic Mirror')
placed_items.append(('Sanctuary', 'Moon Pearl')) place_item('Sanctuary', 'Moon Pearl')
pool.extend(['Magic Mirror'] * max((customitemarray[22] -1 ), 0)) pool.extend(['Magic Mirror'] * max((customitemarray[22] -1 ), 0))
pool.extend(['Moon Pearl'] * max((customitemarray[28] - 1), 0)) pool.extend(['Moon Pearl'] * max((customitemarray[28] - 1), 0))
else: else:
+8 -6
View File
@@ -25,6 +25,7 @@ def ItemFactory(items, player):
# Format: Name: (Advancement, Priority, Type, ItemCode, Pedestal Hint Text, Pedestal Credit Text, Sick Kid Credit Text, Zora Credit Text, Witch Credit Text, Flute Boy Credit Text, Hint Text) # Format: Name: (Advancement, Priority, Type, ItemCode, Pedestal Hint Text, Pedestal Credit Text, Sick Kid Credit Text, Zora Credit Text, Witch Credit Text, Flute Boy Credit Text, Hint Text)
item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'the Bow'), item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'the Bow'),
'Progressive Bow': (True, False, None, 0x64, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'), 'Progressive Bow': (True, False, None, 0x64, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'),
'Progressive Bow (Alt)': (True, False, None, 0x65, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'),
'Book of Mudora': (True, False, None, 0x1D, 'This is a\nparadox?!', 'and the story book', 'the scholarly kid', 'moon runes for sale', 'drugs for literacy', 'book-worm boy can read again', 'the Book'), 'Book of Mudora': (True, False, None, 0x1D, 'This is a\nparadox?!', 'and the story book', 'the scholarly kid', 'moon runes for sale', 'drugs for literacy', 'book-worm boy can read again', 'the Book'),
'Hammer': (True, False, None, 0x09, 'stop\nhammer time!', 'and m c hammer', 'hammer-smashing kid', 'm c hammer for sale', 'stop... hammer time', 'stop, hammer time', 'the hammer'), 'Hammer': (True, False, None, 0x09, 'stop\nhammer time!', 'and m c hammer', 'hammer-smashing kid', 'm c hammer for sale', 'stop... hammer time', 'stop, hammer time', 'the hammer'),
'Hookshot': (True, False, None, 0x0A, 'BOING!!!\nBOING!!!\nBOING!!!', 'and the tickle beam', 'tickle-monster kid', 'tickle beam for sale', 'witch and tickle boy', 'beam boy tickles again', 'the Hookshot'), 'Hookshot': (True, False, None, 0x0A, 'BOING!!!\nBOING!!!\nBOING!!!', 'and the tickle beam', 'tickle-monster kid', 'tickle beam for sale', 'witch and tickle boy', 'beam boy tickles again', 'the Hookshot'),
@@ -43,8 +44,8 @@ item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher cla
'Flippers': (True, False, None, 0x1E, 'fancy a swim?', 'and the toewebs', 'the swimming kid', 'finger webs for sale', 'shrooms let you swim', 'swimming boy swims again', 'the flippers'), 'Flippers': (True, False, None, 0x1E, 'fancy a swim?', 'and the toewebs', 'the swimming kid', 'finger webs for sale', 'shrooms let you swim', 'swimming boy swims again', 'the flippers'),
'Ice Rod': (True, False, None, 0x08, 'I\'m the cold\nrod. I make\nthings freeze!', 'and the freeze ray', 'the ice-bending kid', 'freeze ray for sale', 'fungus for ice-rod', 'ice-cube boy freezes again', 'the ice rod'), 'Ice Rod': (True, False, None, 0x08, 'I\'m the cold\nrod. I make\nthings freeze!', 'and the freeze ray', 'the ice-bending kid', 'freeze ray for sale', 'fungus for ice-rod', 'ice-cube boy freezes again', 'the ice rod'),
'Titans Mitts': (True, False, None, 0x1C, 'Now you can\nlift heavy\nstuff!', 'and the golden glove', 'body-building kid', 'carry glove for sale', 'fungus for bling-gloves', 'body-building boy has gold again', 'the mitts'), 'Titans Mitts': (True, False, None, 0x1C, 'Now you can\nlift heavy\nstuff!', 'and the golden glove', 'body-building kid', 'carry glove for sale', 'fungus for bling-gloves', 'body-building boy has gold again', 'the mitts'),
'Ether': (True, False, None, 0x10, 'This magic\ncoin freezes\neverything!', 'and the bolt coin', 'coin-collecting kid', 'bolt coin for sale', 'shrooms for bolt-coin', 'medallion boy sees floor again', 'Ether'),
'Bombos': (True, False, None, 0x0F, 'Burn, baby,\nburn! Fear my\nring of fire!', 'and the swirly coin', 'coin-collecting kid', 'swirly coin for sale', 'shrooms for swirly-coin', 'medallion boy melts room again', 'Bombos'), 'Bombos': (True, False, None, 0x0F, 'Burn, baby,\nburn! Fear my\nring of fire!', 'and the swirly coin', 'coin-collecting kid', 'swirly coin for sale', 'shrooms for swirly-coin', 'medallion boy melts room again', 'Bombos'),
'Ether': (True, False, None, 0x10, 'This magic\ncoin freezes\neverything!', 'and the bolt coin', 'coin-collecting kid', 'bolt coin for sale', 'shrooms for bolt-coin', 'medallion boy sees floor again', 'Ether'),
'Quake': (True, False, None, 0x11, 'Maxing out the\nRichter scale\nis what I do!', 'and the wavy coin', 'coin-collecting kid', 'wavy coin for sale', 'shrooms for wavy-coin', 'medallion boy shakes dirt again', 'Quake'), 'Quake': (True, False, None, 0x11, 'Maxing out the\nRichter scale\nis what I do!', 'and the wavy coin', 'coin-collecting kid', 'wavy coin for sale', 'shrooms for wavy-coin', 'medallion boy shakes dirt again', 'Quake'),
'Bottle': (True, False, None, 0x16, 'Now you can\nstore potions\nand stuff!', 'and the terrarium', 'the terrarium kid', 'terrarium for sale', 'special promotion', 'bottle boy has terrarium again', 'a Bottle'), 'Bottle': (True, False, None, 0x16, 'Now you can\nstore potions\nand stuff!', 'and the terrarium', 'the terrarium kid', 'terrarium for sale', 'special promotion', 'bottle boy has terrarium again', 'a Bottle'),
'Bottle (Red Potion)': (True, False, None, 0x2B, 'Hearty red goop!', 'and the red goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has red goo again', 'a Bottle'), 'Bottle (Red Potion)': (True, False, None, 0x2B, 'Hearty red goop!', 'and the red goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has red goo again', 'a Bottle'),
@@ -163,11 +164,12 @@ item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher cla
'Map (Ganons Tower)': (False, True, 'Map', 0x72, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Ganon\'s Tower'), 'Map (Ganons Tower)': (False, True, 'Map', 0x72, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Ganon\'s Tower'),
'Small Key (Universal)': (False, True, None, 0xAF, 'A small key for any door', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key'), 'Small Key (Universal)': (False, True, None, 0xAF, 'A small key for any door', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key'),
'Nothing': (False, False, None, 0x5A, 'Some Hot Air', 'and the Nothing', 'the zen kid', 'outright theft', 'shroom theft', 'empty boy is bored again', 'nothing'), 'Nothing': (False, False, None, 0x5A, 'Some Hot Air', 'and the Nothing', 'the zen kid', 'outright theft', 'shroom theft', 'empty boy is bored again', 'nothing'),
'Red Potion': (False, False, None, 0x2E, None, None, None, None, None, None, None), 'Bee Trap': (False, False, None, 0xB0, 'We will sting your face a whole lot!', 'and the sting buddies', 'the beekeeper kid', 'insects for sale', 'shroom pollenation', 'bottle boy has mad bees again', 'friendship'),
'Green Potion': (False, False, None, 0x2F, None, None, None, None, None, None, None), 'Red Potion': (False, False, None, 0x2E, 'Hearty red goop!', 'and the red goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has red goo again', 'a red potion'),
'Blue Potion': (False, False, None, 0x30, None, None, None, None, None, None, None), 'Green Potion': (False, False, None, 0x2F, 'Refreshing green goop!', 'and the green goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has green goo again', 'a green potion'),
'Bee': (False, False, None, 0x0E, None, None, None, None, None, None, None), 'Blue Potion': (False, False, None, 0x30, 'Delicious blue goop!', 'and the blue goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has blue goo again', 'a blue potion'),
'Small Heart': (False, False, None, 0x42, None, None, None, None, None, None, None), 'Bee': (False, False, None, 0x0E, 'I will sting your foes a few times', 'and the sting buddy', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has mad bee again', 'a bee'),
'Small Heart': (False, False, None, 0x42, 'Just a little\npiece of love!', 'and the heart', 'the life-giving kid', 'little love for sale', 'fungus for life', 'life boy feels some love again', 'a heart'),
'Beat Agahnim 1': (True, False, 'Event', None, None, None, None, None, None, None, None), 'Beat Agahnim 1': (True, False, 'Event', None, None, None, None, None, None, None, None),
'Beat Agahnim 2': (True, False, 'Event', None, None, None, None, None, None, None, None), 'Beat Agahnim 2': (True, False, 'Event', None, None, None, None, None, None, None, None),
'Get Frog': (True, False, 'Event', None, None, None, None, None, None, None, None), 'Get Frog': (True, False, 'Event', None, None, None, None, None, None, None, None),
+44 -44
View File
@@ -94,7 +94,7 @@ def build_key_layout(builder, start_regions, proposal, world, player):
def calc_max_chests(builder, key_layout, world, player): def calc_max_chests(builder, key_layout, world, player):
if world.doorShuffle != 'crossed': if world.doorShuffle[player] != 'crossed':
return len(world.get_dungeon(key_layout.sector.name, player).small_keys) return len(world.get_dungeon(key_layout.sector.name, player).small_keys)
return builder.key_doors_num - key_layout.max_drops return builder.key_doors_num - key_layout.max_drops
@@ -115,7 +115,7 @@ def analyze_dungeon(key_layout, world, player):
while len(queue) > 0: while len(queue) > 0:
queue = deque(sorted(queue, key=queue_sorter)) queue = deque(sorted(queue, key=queue_sorter))
parent_door, key_counter = queue.popleft() parent_door, key_counter = queue.popleft()
chest_keys = available_chest_small_keys(key_counter, world) chest_keys = available_chest_small_keys(key_counter, world, player)
raw_avail = chest_keys + len(key_counter.key_only_locations) raw_avail = chest_keys + len(key_counter.key_only_locations)
available = raw_avail - key_counter.used_keys available = raw_avail - key_counter.used_keys
possible_smalls = count_unique_small_doors(key_counter, key_layout.flat_prop) possible_smalls = count_unique_small_doors(key_counter, key_layout.flat_prop)
@@ -132,7 +132,7 @@ def analyze_dungeon(key_layout, world, player):
smallest_rule = None smallest_rule = None
for child in key_counter.child_doors.keys(): for child in key_counter.child_doors.keys():
if not child.bigKey or not key_layout.big_key_special or key_counter.big_key_opened: if not child.bigKey or not key_layout.big_key_special or key_counter.big_key_opened:
odd_counter = create_odd_key_counter(child, key_counter, key_layout, world) odd_counter = create_odd_key_counter(child, key_counter, key_layout, world, player)
empty_flag = empty_counter(odd_counter) empty_flag = empty_counter(odd_counter)
child_queue.append((child, odd_counter, empty_flag)) child_queue.append((child, odd_counter, empty_flag))
if child in doors_completed and child in key_logic.door_rules.keys(): if child in doors_completed and child in key_logic.door_rules.keys():
@@ -142,12 +142,12 @@ def analyze_dungeon(key_layout, world, player):
while len(child_queue) > 0: while len(child_queue) > 0:
child, odd_counter, empty_flag = child_queue.popleft() child, odd_counter, empty_flag = child_queue.popleft()
if not child.bigKey and child not in doors_completed: if not child.bigKey and child not in doors_completed:
best_counter = find_best_counter(child, odd_counter, key_counter, key_layout, world, False, empty_flag) best_counter = find_best_counter(child, odd_counter, key_counter, key_layout, world, player, False, empty_flag)
rule = create_rule(best_counter, key_counter, key_layout, world) rule = create_rule(best_counter, key_counter, key_layout, world, player)
if smallest_rule is None or rule.small_key_num < smallest_rule: if smallest_rule is None or rule.small_key_num < smallest_rule:
smallest_rule = rule.small_key_num smallest_rule = rule.small_key_num
check_for_self_lock_key(rule, child, best_counter, key_layout, world) check_for_self_lock_key(rule, child, best_counter, key_layout, world, player)
bk_restricted_rules(rule, child, odd_counter, empty_flag, key_counter, key_layout, world) bk_restricted_rules(rule, child, odd_counter, empty_flag, key_counter, key_layout, world, player)
key_logic.door_rules[child.name] = rule key_logic.door_rules[child.name] = rule
doors_completed.add(child) doors_completed.add(child)
next_counter = find_next_counter(child, key_counter, key_layout) next_counter = find_next_counter(child, key_counter, key_layout)
@@ -161,7 +161,7 @@ def analyze_dungeon(key_layout, world, player):
key_logic.bk_restricted.update(filter_big_chest(key_counter.free_locations)) key_logic.bk_restricted.update(filter_big_chest(key_counter.free_locations))
if not key_counter.big_key_opened and big_chest_in_locations(key_counter.free_locations): if not key_counter.big_key_opened and big_chest_in_locations(key_counter.free_locations):
key_logic.sm_restricted.update(find_big_chest_locations(key_counter.free_locations)) key_logic.sm_restricted.update(find_big_chest_locations(key_counter.free_locations))
check_rules(original_key_counter, key_layout, world) check_rules(original_key_counter, key_layout, world, player)
def count_key_drops(sector): def count_key_drops(sector):
@@ -237,7 +237,7 @@ def unique_child_door(child, key_counter):
return True return True
def find_best_counter(door, odd_counter, key_counter, key_layout, world, skip_bk, empty_flag): # try to waste as many keys as possible? def find_best_counter(door, odd_counter, key_counter, key_layout, world, player, skip_bk, empty_flag): # try to waste as many keys as possible?
ignored_doors = {door, door.dest} if door is not None else {} ignored_doors = {door, door.dest} if door is not None else {}
finished = False finished = False
opened_doors = dict(key_counter.open_doors) opened_doors = dict(key_counter.open_doors)
@@ -257,7 +257,7 @@ def find_best_counter(door, odd_counter, key_counter, key_layout, world, skip_bk
# this means the new_door invalidates the door / leads to the same stuff # this means the new_door invalidates the door / leads to the same stuff
if not empty_flag and relative_empty_counter(odd_counter, new_counter): if not empty_flag and relative_empty_counter(odd_counter, new_counter):
ignored_doors.add(new_door) ignored_doors.add(new_door)
elif empty_flag or key_wasted(new_door, door, last_counter, new_counter, key_layout, world): elif empty_flag or key_wasted(new_door, door, last_counter, new_counter, key_layout, world, player):
last_counter = new_counter last_counter = new_counter
opened_doors = proposed_doors opened_doors = proposed_doors
bk_opened = bk_open bk_opened = bk_open
@@ -285,13 +285,13 @@ def find_potential_open_doors(key_counter, ignored_doors, key_layout, skip_bk):
return small_doors + big_doors return small_doors + big_doors
def key_wasted(new_door, old_door, old_counter, new_counter, key_layout, world): def key_wasted(new_door, old_door, old_counter, new_counter, key_layout, world, player):
if new_door.bigKey: # big keys are not wastes - it uses up a location if new_door.bigKey: # big keys are not wastes - it uses up a location
return True return True
chest_keys = available_chest_small_keys(old_counter, world) chest_keys = available_chest_small_keys(old_counter, world, player)
old_key_diff = len(old_counter.key_only_locations) - old_counter.used_keys old_key_diff = len(old_counter.key_only_locations) - old_counter.used_keys
old_avail = chest_keys + old_key_diff old_avail = chest_keys + old_key_diff
new_chest_keys = available_chest_small_keys(new_counter, world) new_chest_keys = available_chest_small_keys(new_counter, world, player)
new_key_diff = len(new_counter.key_only_locations) - new_counter.used_keys new_key_diff = len(new_counter.key_only_locations) - new_counter.used_keys
new_avail = new_chest_keys + new_key_diff new_avail = new_chest_keys + new_key_diff
if new_key_diff < old_key_diff or new_avail < old_avail: if new_key_diff < old_key_diff or new_avail < old_avail:
@@ -306,7 +306,7 @@ def key_wasted(new_door, old_door, old_counter, new_counter, key_layout, world):
proposed_doors = {**opened_doors, **dict.fromkeys([new_child, new_child.dest])} proposed_doors = {**opened_doors, **dict.fromkeys([new_child, new_child.dest])}
bk_open = bk_opened or new_door.bigKey bk_open = bk_opened or new_door.bigKey
new_counter = find_counter(proposed_doors, bk_open, key_layout) new_counter = find_counter(proposed_doors, bk_open, key_layout)
if key_wasted(new_child, old_door, current_counter, new_counter, key_layout, world): if key_wasted(new_child, old_door, current_counter, new_counter, key_layout, world, player):
return True # waste is possible return True # waste is possible
return False return False
@@ -324,16 +324,16 @@ def check_special_locations(locations):
return False return False
def calc_avail_keys(key_counter, world): def calc_avail_keys(key_counter, world, player):
chest_keys = available_chest_small_keys(key_counter, world) chest_keys = available_chest_small_keys(key_counter, world, player)
raw_avail = chest_keys + len(key_counter.key_only_locations) raw_avail = chest_keys + len(key_counter.key_only_locations)
return raw_avail - key_counter.used_keys return raw_avail - key_counter.used_keys
def create_rule(key_counter, prev_counter, key_layout, world): def create_rule(key_counter, prev_counter, key_layout, world, player):
# prev_chest_keys = available_chest_small_keys(prev_counter, world) # prev_chest_keys = available_chest_small_keys(prev_counter, world)
# prev_avail = prev_chest_keys + len(prev_counter.key_only_locations) # prev_avail = prev_chest_keys + len(prev_counter.key_only_locations)
chest_keys = available_chest_small_keys(key_counter, world) chest_keys = available_chest_small_keys(key_counter, world, player)
key_gain = len(key_counter.key_only_locations) - len(prev_counter.key_only_locations) key_gain = len(key_counter.key_only_locations) - len(prev_counter.key_only_locations)
# previous method # previous method
# raw_avail = chest_keys + len(key_counter.key_only_locations) # raw_avail = chest_keys + len(key_counter.key_only_locations)
@@ -348,9 +348,9 @@ def create_rule(key_counter, prev_counter, key_layout, world):
return DoorRules(rule_num) return DoorRules(rule_num)
def check_for_self_lock_key(rule, door, parent_counter, key_layout, world): def check_for_self_lock_key(rule, door, parent_counter, key_layout, world, player):
if world.accessibility != 'locations': if world.accessibility != 'locations':
counter = find_inverted_counter(door, parent_counter, key_layout, world) counter = find_inverted_counter(door, parent_counter, key_layout, world, player)
if not self_lock_possible(counter): if not self_lock_possible(counter):
return return
if len(counter.free_locations) == 1 and len(counter.key_only_locations) == 0 and not counter.important_location: if len(counter.free_locations) == 1 and len(counter.key_only_locations) == 0 and not counter.important_location:
@@ -358,7 +358,7 @@ def check_for_self_lock_key(rule, door, parent_counter, key_layout, world):
rule.small_location = next(iter(counter.free_locations)) rule.small_location = next(iter(counter.free_locations))
def find_inverted_counter(door, parent_counter, key_layout, world): def find_inverted_counter(door, parent_counter, key_layout, world, player):
# open all doors in counter # open all doors in counter
counter = open_all_counter(parent_counter, key_layout, door=door) counter = open_all_counter(parent_counter, key_layout, door=door)
max_counter = find_max_counter(key_layout) max_counter = find_max_counter(key_layout)
@@ -371,7 +371,7 @@ def find_inverted_counter(door, parent_counter, key_layout, world):
inverted_counter.open_doors = dict_difference(max_counter.open_doors, counter.open_doors) inverted_counter.open_doors = dict_difference(max_counter.open_doors, counter.open_doors)
inverted_counter.other_locations = dict_difference(max_counter.other_locations, counter.other_locations) inverted_counter.other_locations = dict_difference(max_counter.other_locations, counter.other_locations)
for loc in inverted_counter.other_locations: for loc in inverted_counter.other_locations:
if important_location(loc, world): if important_location(loc, world, player):
inverted_counter.important_location = True inverted_counter.important_location = True
return inverted_counter return inverted_counter
@@ -425,8 +425,8 @@ def self_lock_possible(counter):
return len(counter.free_locations) <= 1 and len(counter.key_only_locations) == 0 and not counter.important_location return len(counter.free_locations) <= 1 and len(counter.key_only_locations) == 0 and not counter.important_location
def available_chest_small_keys(key_counter, world): def available_chest_small_keys(key_counter, world, player):
if not world.keysanity and world.mode != 'retro': if not world.keyshuffle[player] and not world.retro[player]:
cnt = 0 cnt = 0
for loc in key_counter.free_locations: for loc in key_counter.free_locations:
if key_counter.big_key_opened or '- Big Chest' not in loc.name: if key_counter.big_key_opened or '- Big Chest' not in loc.name:
@@ -436,8 +436,8 @@ def available_chest_small_keys(key_counter, world):
return key_counter.max_chests return key_counter.max_chests
def available_chest_small_keys_logic(key_counter, world, sm_restricted): def available_chest_small_keys_logic(key_counter, world, player, sm_restricted):
if not world.keysanity and world.mode != 'retro': if not world.keyshuffle[player] and not world.retro[player]:
cnt = 0 cnt = 0
for loc in key_counter.free_locations: for loc in key_counter.free_locations:
if loc not in sm_restricted and (key_counter.big_key_opened or '- Big Chest' not in loc.name): if loc not in sm_restricted and (key_counter.big_key_opened or '- Big Chest' not in loc.name):
@@ -447,11 +447,11 @@ def available_chest_small_keys_logic(key_counter, world, sm_restricted):
return key_counter.max_chests return key_counter.max_chests
def bk_restricted_rules(rule, door, odd_counter, empty_flag, key_counter, key_layout, world): def bk_restricted_rules(rule, door, odd_counter, empty_flag, key_counter, key_layout, world, player):
if key_counter.big_key_opened: if key_counter.big_key_opened:
return return
best_counter = find_best_counter(door, odd_counter, key_counter, key_layout, world, True, empty_flag) best_counter = find_best_counter(door, odd_counter, key_counter, key_layout, world, player, True, empty_flag)
bk_number = create_rule(best_counter, key_counter, key_layout, world).small_key_num bk_number = create_rule(best_counter, key_counter, key_layout, world, player).small_key_num
if bk_number == rule.small_key_num: if bk_number == rule.small_key_num:
return return
door_open = find_next_counter(door, best_counter, key_layout) door_open = find_next_counter(door, best_counter, key_layout)
@@ -586,7 +586,7 @@ def flatten_pair_list(paired_list):
return flat_list return flat_list
def check_rules(original_counter, key_layout, world): def check_rules(original_counter, key_layout, world, player):
all_key_only = set() all_key_only = set()
key_only_map = {} key_only_map = {}
queue = deque([(None, original_counter, original_counter.key_only_locations)]) queue = deque([(None, original_counter, original_counter.key_only_locations)])
@@ -639,7 +639,7 @@ def check_rules(original_counter, key_layout, world):
if check_non_bk: if check_non_bk:
adjust_key_location_mins(key_layout, min_rule_non_bk, lambda r: r.small_key_num if r.alternate_small_key is None else r.alternate_small_key, adjust_key_location_mins(key_layout, min_rule_non_bk, lambda r: r.small_key_num if r.alternate_small_key is None else r.alternate_small_key,
lambda r, v: r if r.alternate_small_key is None else setattr(r, 'alternate_small_key', v)) lambda r, v: r if r.alternate_small_key is None else setattr(r, 'alternate_small_key', v))
check_rules_deep(original_counter, key_layout, world) check_rules_deep(original_counter, key_layout, world, player)
def adjust_key_location_mins(key_layout, min_rules, getter, setter): def adjust_key_location_mins(key_layout, min_rules, getter, setter):
@@ -666,7 +666,7 @@ def adjust_key_location_mins(key_layout, min_rules, getter, setter):
setter(rule, collected_keys) setter(rule, collected_keys)
def check_rules_deep(original_counter, key_layout, world): def check_rules_deep(original_counter, key_layout, world, player):
key_logic = key_layout.key_logic key_logic = key_layout.key_logic
big_locations = {x for x in key_layout.all_chest_locations if x not in key_logic.bk_restricted} big_locations = {x for x in key_layout.all_chest_locations if x not in key_logic.bk_restricted}
queue = deque([original_counter]) queue = deque([original_counter])
@@ -683,7 +683,7 @@ def check_rules_deep(original_counter, key_layout, world):
else: else:
bail = 0 bail = 0
last_counter = counter last_counter = counter
chest_keys = available_chest_small_keys_logic(counter, world, key_logic.sm_restricted) chest_keys = available_chest_small_keys_logic(counter, world, player, key_logic.sm_restricted)
big_avail = counter.big_key_opened big_avail = counter.big_key_opened
big_maybe_not_found = not counter.big_key_opened big_maybe_not_found = not counter.big_key_opened
if not key_layout.big_key_special and not big_avail: if not key_layout.big_key_special and not big_avail:
@@ -787,8 +787,8 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa
if not smalls_avail and num_bigs == 0: if not smalls_avail and num_bigs == 0:
return True # I think that's the end return True # I think that's the end
ttl_locations = state.ttl_locations if state.big_key_opened else count_locations_exclude_big_chest(state) ttl_locations = state.ttl_locations if state.big_key_opened else count_locations_exclude_big_chest(state)
available_small_locations = cnt_avail_small_locations(key_layout, ttl_locations, state, world) available_small_locations = cnt_avail_small_locations(key_layout, ttl_locations, state, world, player)
available_big_locations = cnt_avail_big_locations(ttl_locations, state, world) available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player)
if (not smalls_avail or available_small_locations == 0) and (state.big_key_opened or num_bigs == 0 or available_big_locations == 0): if (not smalls_avail or available_small_locations == 0) and (state.big_key_opened or num_bigs == 0 or available_big_locations == 0):
return False return False
else: else:
@@ -821,14 +821,14 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa
return True return True
def cnt_avail_small_locations(key_layout, ttl_locations, state, world): def cnt_avail_small_locations(key_layout, ttl_locations, state, world, player):
if not world.keysanity and world.mode != 'retro': if not world.keyshuffle[player] and not world.retro[player]:
return min(ttl_locations - state.used_locations, state.key_locations - state.used_smalls) return min(ttl_locations - state.used_locations, state.key_locations - state.used_smalls)
return state.key_locations - state.used_smalls return state.key_locations - state.used_smalls
def cnt_avail_big_locations(ttl_locations, state, world): def cnt_avail_big_locations(ttl_locations, state, world, player):
if not world.keysanity: if not world.bigkeyshuffle[player]:
return ttl_locations - state.used_locations if not state.big_key_special else 0 return ttl_locations - state.used_locations if not state.big_key_special else 0
return 1 if not state.big_key_special else 0 return 1 if not state.big_key_special else 0
@@ -868,7 +868,7 @@ def create_key_counter(state, key_layout, world, player):
key_counter = KeyCounter(key_layout.max_chests) key_counter = KeyCounter(key_layout.max_chests)
key_counter.child_doors.update(dict.fromkeys(unique_doors(state.small_doors+state.big_doors))) key_counter.child_doors.update(dict.fromkeys(unique_doors(state.small_doors+state.big_doors)))
for loc in state.found_locations: for loc in state.found_locations:
if important_location(loc, world): if important_location(loc, world, player):
key_counter.important_location = True key_counter.important_location = True
key_counter.other_locations[loc] = None key_counter.other_locations[loc] = None
elif loc.event and 'Small Key' in loc.item.name: elif loc.event and 'Small Key' in loc.item.name:
@@ -891,14 +891,14 @@ def create_key_counter(state, key_layout, world, player):
return key_counter return key_counter
def important_location(loc, world): def important_location(loc, world, player):
important_locations = ['Agahnim 1', 'Agahnim 2', 'Attic Cracked Floor', 'Suspicious Maiden'] important_locations = ['Agahnim 1', 'Agahnim 2', 'Attic Cracked Floor', 'Suspicious Maiden']
if world.mode == 'standard' or world.doorShuffle == 'crossed': if world.mode[player] == 'standard' or world.doorShuffle[player] == 'crossed':
important_locations.append('Hyrule Dungeon Cellblock') important_locations.append('Hyrule Dungeon Cellblock')
return '- Prize' in loc.name or loc.name in important_locations return '- Prize' in loc.name or loc.name in important_locations
def create_odd_key_counter(door, parent_counter, key_layout, world): def create_odd_key_counter(door, parent_counter, key_layout, world, player):
odd_counter = KeyCounter(key_layout.max_chests) odd_counter = KeyCounter(key_layout.max_chests)
next_counter = find_next_counter(door, parent_counter, key_layout) next_counter = find_next_counter(door, parent_counter, key_layout)
odd_counter.free_locations = dict_difference(next_counter.free_locations, parent_counter.free_locations) odd_counter.free_locations = dict_difference(next_counter.free_locations, parent_counter.free_locations)
@@ -906,7 +906,7 @@ def create_odd_key_counter(door, parent_counter, key_layout, world):
odd_counter.child_doors = dict_difference(next_counter.child_doors, parent_counter.child_doors) odd_counter.child_doors = dict_difference(next_counter.child_doors, parent_counter.child_doors)
odd_counter.other_locations = dict_difference(next_counter.other_locations, parent_counter.other_locations) odd_counter.other_locations = dict_difference(next_counter.other_locations, parent_counter.other_locations)
for loc in odd_counter.other_locations: for loc in odd_counter.other_locations:
if important_location(loc, world): if important_location(loc, world, player):
odd_counter.important_location = True odd_counter.important_location = True
return odd_counter return odd_counter
+151 -104
View File
@@ -6,28 +6,34 @@ import logging
import os import os
import random import random
import time import time
import zlib
from BaseClasses import World, CollectionState, Item, Region, Location, Shop from BaseClasses import World, CollectionState, Item, Region, Location, Shop
from Regions import create_regions, mark_light_world_regions from Items import ItemFactory
from Regions import create_regions, create_shops, mark_light_world_regions
from InvertedRegions import create_inverted_regions, mark_dark_world_regions from InvertedRegions import create_inverted_regions, mark_dark_world_regions
from EntranceShuffle import link_entrances, link_inverted_entrances from EntranceShuffle import link_entrances, link_inverted_entrances
from Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, JsonRom
from Doors import create_doors from Doors import create_doors
from DoorShuffle import link_doors from DoorShuffle import link_doors
from Rom import patch_rom, get_enemizer_patch, apply_rom_settings, Sprite, LocalRom, JsonRom
from RoomData import create_rooms from RoomData import create_rooms
from Rules import set_rules from Rules import set_rules
from Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive from Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive
from Fill import distribute_items_cutoff, distribute_items_staleness, distribute_items_restrictive, flood_items, balance_multiworld_progression from Fill import distribute_items_cutoff, distribute_items_staleness, distribute_items_restrictive, flood_items, balance_multiworld_progression
from ItemList import generate_itempool, difficulties, fill_prizes from ItemList import generate_itempool, difficulties, fill_prizes
from Utils import output_path from Utils import output_path, parse_names_string
__version__ = '0.0.1-pre' __version__ = '0.0.1-pre'
def main(args, seed=None): def main(args, seed=None):
start = time.process_time() if args.outputpath:
os.makedirs(args.outputpath, exist_ok=True)
output_path.cached_path = args.outputpath
start = time.perf_counter()
# initialize the world # initialize the world
world = World(args.multi, args.shuffle, args.door_shuffle, args.logic, args.mode, args.swords, args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm, not args.nodungeonitems, args.accessibility, args.shuffleganon, args.quickswap, args.fastmenu, args.disablemusic, args.keysanity, args.retro, args.custom, args.customitemarray, args.shufflebosses, args.hints) world = World(args.multi, args.shuffle, args.door_shuffle, args.logic, args.mode, args.swords, args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm, args.accessibility, args.shuffleganon, args.retro, args.custom, args.customitemarray, args.hints)
logger = logging.getLogger('') logger = logging.getLogger('')
if seed is None: if seed is None:
random.seed(None) random.seed(None)
@@ -36,47 +42,59 @@ def main(args, seed=None):
world.seed = int(seed) world.seed = int(seed)
random.seed(world.seed) random.seed(world.seed)
world.crystals_needed_for_ganon = random.randint(0, 7) if args.crystals_ganon == 'random' else int(args.crystals_ganon) world.mapshuffle = args.mapshuffle.copy()
world.crystals_needed_for_gt = random.randint(0, 7) if args.crystals_gt == 'random' else int(args.crystals_gt) world.compassshuffle = args.compassshuffle.copy()
world.keyshuffle = args.keyshuffle.copy()
world.bigkeyshuffle = args.bigkeyshuffle.copy()
world.crystals_needed_for_ganon = {player: random.randint(0, 7) if args.crystals_ganon[player] == 'random' else int(args.crystals_ganon[player]) for player in range(1, world.players + 1)}
world.crystals_needed_for_gt = {player: random.randint(0, 7) if args.crystals_gt[player] == 'random' else int(args.crystals_gt[player]) for player in range(1, world.players + 1)}
world.open_pyramid = args.openpyramid.copy()
world.boss_shuffle = args.shufflebosses.copy()
world.enemy_shuffle = args.shuffleenemies.copy()
world.enemy_health = args.enemy_health.copy()
world.enemy_damage = args.enemy_damage.copy()
world.beemizer = args.beemizer.copy()
world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)} world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)}
logger.info('ALttP Door Randomizer Version %s - Seed: %s\n\n', __version__, world.seed) logger.info('ALttP Door Randomizer Version %s - Seed: %s\n\n', __version__, world.seed)
world.difficulty_requirements = difficulties[world.difficulty] for player in range(1, world.players + 1):
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
if world.mode != 'inverted': if world.mode[player] == 'standard' and world.enemy_shuffle[player] != 'none':
for player in range(1, world.players + 1): world.escape_assist[player].append('bombs') # enemized escape assumes infinite bombs available and will likely be unbeatable without it
for tok in filter(None, args.startinventory[player].split(',')):
item = ItemFactory(tok.strip(), player)
if item:
world.push_precollected(item)
if world.mode[player] != 'inverted':
create_regions(world, player) create_regions(world, player)
create_doors(world, player) else:
create_rooms(world, player) create_inverted_regions(world, player)
create_dungeons(world, player) create_shops(world, player)
else: create_doors(world, player)
for player in range(1, world.players + 1): create_rooms(world, player)
create_inverted_regions(world, player) # todo: port all the dungeon region work create_dungeons(world, player)
create_doors(world, player)
create_rooms(world, player)
create_dungeons(world, player)
logger.info('Shuffling the World about.') logger.info('Shuffling the World about.')
if world.mode != 'inverted': for player in range(1, world.players + 1):
for player in range(1, world.players + 1): if world.mode[player] != 'inverted':
link_entrances(world, player) link_entrances(world, player)
else: else:
for player in range(1, world.players + 1):
link_inverted_entrances(world, player) link_inverted_entrances(world, player)
logger.info('Shuffling dungeons') logger.info('Shuffling dungeons')
for player in range(1, world.players + 1): for player in range(1, world.players + 1):
link_doors(world, player) link_doors(world, player)
if world.mode[player] != 'inverted':
if world.mode != 'inverted': mark_light_world_regions(world, player)
mark_light_world_regions(world) else:
else: mark_dark_world_regions(world, player)
mark_dark_world_regions(world)
logger.info('Generating Item Pool.') logger.info('Generating Item Pool.')
for player in range(1, world.players + 1): for player in range(1, world.players + 1):
@@ -94,7 +112,8 @@ def main(args, seed=None):
logger.info('Placing Dungeon Items.') logger.info('Placing Dungeon Items.')
shuffled_locations = None shuffled_locations = None
if args.algorithm in ['balanced', 'vt26'] or args.keysanity: if args.algorithm in ['balanced', 'vt26'] or any(list(args.mapshuffle.values()) + list(args.compassshuffle.values()) +
list(args.keyshuffle.values()) + list(args.bigkeyshuffle.values())):
shuffled_locations = world.get_unfilled_locations() shuffled_locations = world.get_unfilled_locations()
random.shuffle(shuffled_locations) random.shuffle(shuffled_locations)
fill_dungeons_restrictive(world, shuffled_locations) fill_dungeons_restrictive(world, shuffled_locations)
@@ -112,12 +131,12 @@ def main(args, seed=None):
elif args.algorithm == 'freshness': elif args.algorithm == 'freshness':
distribute_items_staleness(world) distribute_items_staleness(world)
elif args.algorithm == 'vt25': elif args.algorithm == 'vt25':
distribute_items_restrictive(world, 0) distribute_items_restrictive(world, False)
elif args.algorithm == 'vt26': elif args.algorithm == 'vt26':
distribute_items_restrictive(world, gt_filler(world), shuffled_locations) distribute_items_restrictive(world, True, shuffled_locations)
elif args.algorithm == 'balanced': elif args.algorithm == 'balanced':
distribute_items_restrictive(world, gt_filler(world)) distribute_items_restrictive(world, True)
if world.players > 1: if world.players > 1:
logger.info('Balancing multiworld progression.') logger.info('Balancing multiworld progression.')
@@ -125,52 +144,67 @@ def main(args, seed=None):
logger.info('Patching ROM.') logger.info('Patching ROM.')
if args.sprite is not None: player_names = parse_names_string(args.names)
if isinstance(args.sprite, Sprite): outfilebase = 'DR_%s' % (args.outputname if args.outputname else world.seed)
sprite = args.sprite
else:
sprite = Sprite(args.sprite)
else:
sprite = None
outfilebase = 'DR_%s_%s-%s-%s-%s%s_%s-%s_%s%s%s%s%s_%s' % (world.logic, world.difficulty, world.difficulty_adjustments, world.mode, world.goal, "" if world.timer in ['none', 'display'] else "-" + world.timer, world.shuffle, world.algorithm, world.doorShuffle, "-keysanity" if world.keysanity else "", "-retro" if world.retro else "", "-prog_" + world.progressive if world.progressive in ['off', 'random'] else "", "-nohints" if not world.hints else "", world.seed)
use_enemizer = args.enemizercli and (args.shufflebosses != 'none' or args.shuffleenemies or args.enemy_health != 'default' or args.enemy_health != 'default' or args.enemy_damage or args.shufflepalette or args.shufflepots)
rom_names = []
jsonout = {} jsonout = {}
if not args.suppress_rom: if not args.suppress_rom:
if world.players > 1: for player in range(1, world.players + 1):
raise NotImplementedError("Multiworld rom writes have not been implemented") sprite_random_on_hit = type(args.sprite[player]) is str and args.sprite[player].lower() == 'randomonhit'
else: use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player] != 'none'
player = 1 or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
or args.shufflepots[player] or sprite_random_on_hit)
local_rom = None rom = JsonRom() if args.jsonout or use_enemizer else LocalRom(args.rom)
if args.jsonout:
rom = JsonRom() patch_rom(world, player, rom, use_enemizer)
else: rom_names.append((player, list(rom.name)))
if use_enemizer:
local_rom = LocalRom(args.rom) if use_enemizer and (args.enemizercli or not args.jsonout):
rom = JsonRom() patch_enemizer(world, player, rom, args.rom, args.enemizercli, args.shufflepots[player], sprite_random_on_hit)
else: if not args.jsonout:
patches = rom.patches
rom = LocalRom(args.rom) rom = LocalRom(args.rom)
rom.merge_enemizer_patches(patches)
patch_rom(world, player, rom) if args.race:
patch_race_rom(rom)
enemizer_patch = [] apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player], args.fastmenu[player], args.disablemusic[player], args.sprite[player], args.ow_palettes[player], args.uw_palettes[player], player_names)
if use_enemizer:
enemizer_patch = get_enemizer_patch(world, player, rom, args.rom, args.enemizercli, args.shuffleenemies, args.enemy_health, args.enemy_damage, args.shufflepalette, args.shufflepots)
if args.jsonout: if args.jsonout:
jsonout['patch'] = rom.patches jsonout[f'patch{player}'] = rom.patches
if use_enemizer:
jsonout['enemizer' % player] = enemizer_patch
else: else:
if use_enemizer: mcsb_name = ''
local_rom.patch_enemizer(rom.patches, os.path.join(os.path.dirname(args.enemizercli), "enemizerBasePatch.json"), enemizer_patch) if all([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player], world.bigkeyshuffle[player]]):
rom = local_rom mcsb_name = '-keysanity'
elif [world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player], world.bigkeyshuffle[player]].count(True) == 1:
mcsb_name = '-mapshuffle' if world.mapshuffle[player] else '-compassshuffle' if world.compassshuffle[player] else '-keyshuffle' if world.keyshuffle[player] else '-bigkeyshuffle'
elif any([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player], world.bigkeyshuffle[player]]):
mcsb_name = '-%s%s%s%sshuffle' % (
'M' if world.mapshuffle[player] else '', 'C' if world.compassshuffle[player] else '',
'S' if world.keyshuffle[player] else '', 'B' if world.bigkeyshuffle[player] else '')
apply_rom_settings(rom, args.heartbeep, args.heartcolor, world.quickswap, world.fastmenu, world.disable_music, sprite) playername = f"{f'_P{player}' if world.players > 1 else ''}{f'_{player_names[player]}' if player in player_names else ''}"
rom.write_to_file(output_path('%s.sfc' % outfilebase)) outfilesuffix = ('_%s_%s-%s-%s-%s%s_%s-%s-%s%s%s%s%s' % (world.logic[player], world.difficulty[player], world.difficulty_adjustments[player],
world.mode[player], world.goal[player],
"" if world.timer in ['none', 'display'] else "-" + world.timer,
world.shuffle[player], world.doorShuffle[player], world.algorithm, mcsb_name,
"-retro" if world.retro[player] else "",
"-prog_" + world.progressive if world.progressive in ['off', 'random'] else "",
"-nohints" if not world.hints[player] else "")) if not args.outputname else ''
rom.write_to_file(output_path(f'{outfilebase}{playername}{outfilesuffix}.sfc'))
multidata = zlib.compress(json.dumps((world.players,
rom_names,
[((location.address, location.player), (location.item.code, location.item.player)) for location in world.get_filled_locations() if type(location.address) is int])
).encode("utf-8"))
if args.jsonout:
jsonout["multidata"] = list(multidata)
else:
with open(output_path('%s_multidata' % outfilebase), 'wb') as f:
f.write(multidata)
if args.create_spoiler and not args.jsonout: if args.create_spoiler and not args.jsonout:
world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase)) world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase))
@@ -185,48 +219,54 @@ def main(args, seed=None):
world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase)) world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase))
logger.info('Done. Enjoy.') logger.info('Done. Enjoy.')
logger.debug('Total Time: %s', time.process_time() - start) logger.debug('Total Time: %s', time.perf_counter() - start)
return world return world
def gt_filler(world):
if world.goal == 'triforcehunt':
return random.randint(15, 50)
return random.randint(0, 15)
def copy_world(world): def copy_world(world):
# ToDo: Not good yet # ToDo: Not good yet
ret = World(world.players, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, world.place_dungeon_items, world.accessibility, world.shuffle_ganon, world.quickswap, world.fastmenu, world.disable_music, world.keysanity, world.retro, world.custom, world.customitemarray, world.boss_shuffle, world.hints) ret = World(world.players, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints)
ret.required_medallions = world.required_medallions.copy() ret.required_medallions = world.required_medallions.copy()
ret.swamp_patch_required = world.swamp_patch_required.copy() ret.swamp_patch_required = world.swamp_patch_required.copy()
ret.ganon_at_pyramid = world.ganon_at_pyramid.copy() ret.ganon_at_pyramid = world.ganon_at_pyramid.copy()
ret.powder_patch_required = world.powder_patch_required.copy() ret.powder_patch_required = world.powder_patch_required.copy()
ret.ganonstower_vanilla = world.ganonstower_vanilla.copy() ret.ganonstower_vanilla = world.ganonstower_vanilla.copy()
ret.treasure_hunt_count = world.treasure_hunt_count ret.treasure_hunt_count = world.treasure_hunt_count.copy()
ret.treasure_hunt_icon = world.treasure_hunt_icon ret.treasure_hunt_icon = world.treasure_hunt_icon.copy()
ret.sewer_light_cone = world.sewer_light_cone ret.sewer_light_cone = world.sewer_light_cone.copy()
ret.light_world_light_cone = world.light_world_light_cone ret.light_world_light_cone = world.light_world_light_cone
ret.dark_world_light_cone = world.dark_world_light_cone ret.dark_world_light_cone = world.dark_world_light_cone
ret.seed = world.seed ret.seed = world.seed
ret.can_access_trock_eyebridge = world.can_access_trock_eyebridge ret.can_access_trock_eyebridge = world.can_access_trock_eyebridge.copy()
ret.can_access_trock_front = world.can_access_trock_front ret.can_access_trock_front = world.can_access_trock_front.copy()
ret.can_access_trock_big_chest = world.can_access_trock_big_chest ret.can_access_trock_big_chest = world.can_access_trock_big_chest.copy()
ret.can_access_trock_middle = world.can_access_trock_middle ret.can_access_trock_middle = world.can_access_trock_middle.copy()
ret.can_take_damage = world.can_take_damage ret.can_take_damage = world.can_take_damage
ret.difficulty_requirements = world.difficulty_requirements ret.difficulty_requirements = world.difficulty_requirements.copy()
ret.fix_fake_world = world.fix_fake_world ret.fix_fake_world = world.fix_fake_world.copy()
ret.lamps_needed_for_dark_rooms = world.lamps_needed_for_dark_rooms ret.lamps_needed_for_dark_rooms = world.lamps_needed_for_dark_rooms
ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon ret.mapshuffle = world.mapshuffle.copy()
ret.crystals_needed_for_gt = world.crystals_needed_for_gt ret.compassshuffle = world.compassshuffle.copy()
ret.keyshuffle = world.keyshuffle.copy()
ret.bigkeyshuffle = world.bigkeyshuffle.copy()
ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon.copy()
ret.crystals_needed_for_gt = world.crystals_needed_for_gt.copy()
ret.open_pyramid = world.open_pyramid.copy()
ret.boss_shuffle = world.boss_shuffle.copy()
ret.enemy_shuffle = world.enemy_shuffle.copy()
ret.enemy_health = world.enemy_health.copy()
ret.enemy_damage = world.enemy_damage.copy()
ret.beemizer = world.beemizer.copy()
if world.mode != 'inverted': for player in range(1, world.players + 1):
for player in range(1, world.players + 1): if world.mode[player] != 'inverted':
create_regions(ret, player) create_regions(ret, player)
create_dungeons(ret, player) else:
else:
for player in range(1, world.players + 1):
create_inverted_regions(ret, player) create_inverted_regions(ret, player)
create_dungeons(ret, player) create_shops(ret, player)
create_doors(ret, player)
create_rooms(ret, player)
create_dungeons(ret, player)
copy_dynamic_regions_and_locations(world, ret) copy_dynamic_regions_and_locations(world, ret)
@@ -237,7 +277,6 @@ def copy_world(world):
for shop in world.shops: for shop in world.shops:
copied_shop = ret.get_region(shop.region.name, shop.region.player).shop copied_shop = ret.get_region(shop.region.name, shop.region.player).shop
copied_shop.active = shop.active
copied_shop.inventory = copy.copy(shop.inventory) copied_shop.inventory = copy.copy(shop.inventory)
# connect copied world # connect copied world
@@ -283,14 +322,14 @@ def copy_world(world):
def copy_dynamic_regions_and_locations(world, ret): def copy_dynamic_regions_and_locations(world, ret):
for region in world.dynamic_regions: for region in world.dynamic_regions:
new_reg = Region(region.name, region.type, region.hint_text, region.player) new_reg = Region(region.name, region.type, region.hint_text, region.player)
new_reg.world = ret
ret.regions.append(new_reg) ret.regions.append(new_reg)
ret.initialize_regions([new_reg])
ret.dynamic_regions.append(new_reg) ret.dynamic_regions.append(new_reg)
# Note: ideally exits should be copied here, but the current use case (Take anys) do not require this # Note: ideally exits should be copied here, but the current use case (Take anys) do not require this
if region.shop: if region.shop:
new_reg.shop = Shop(new_reg, region.shop.room_id, region.shop.type, region.shop.shopkeeper_config, region.shop.replaceable) new_reg.shop = Shop(new_reg, region.shop.room_id, region.shop.type, region.shop.shopkeeper_config, region.shop.custom, region.shop.locked)
ret.shops.append(new_reg.shop) ret.shops.append(new_reg.shop)
for location in world.dynamic_locations: for location in world.dynamic_locations:
@@ -312,7 +351,7 @@ def create_playthrough(world):
world = copy_world(world) world = copy_world(world)
# if we only check for beatable, we can do this sanity check first before writing down spheres # if we only check for beatable, we can do this sanity check first before writing down spheres
if world.accessibility == 'none' and not world.can_beat_game(): if not world.can_beat_game():
raise RuntimeError('Cannot beat game. Something went terribly wrong here!') raise RuntimeError('Cannot beat game. Something went terribly wrong here!')
# get locations containing progress items # get locations containing progress items
@@ -323,8 +362,7 @@ def create_playthrough(world):
sphere_candidates = list(prog_locations) sphere_candidates = list(prog_locations)
logging.getLogger('').debug('Building up collection spheres.') logging.getLogger('').debug('Building up collection spheres.')
while sphere_candidates: while sphere_candidates:
if not world.keysanity: state.sweep_for_events(key_only=True)
state.sweep_for_events(key_only=True)
state.sweep_for_crystal_access() state.sweep_for_crystal_access()
sphere = [] sphere = []
@@ -344,9 +382,10 @@ def create_playthrough(world):
logging.getLogger('').debug('Calculated sphere %i, containing %i of %i progress items.', len(collection_spheres), len(sphere), len(prog_locations)) logging.getLogger('').debug('Calculated sphere %i, containing %i of %i progress items.', len(collection_spheres), len(sphere), len(prog_locations))
if not sphere: if not sphere:
logging.getLogger('').debug('The following items could not be reached: %s', ['%s (Player %d) at %s (Player %d)' % (location.item.name, location.item.player, location.name, location.player) for location in sphere_candidates]) logging.getLogger('').debug('The following items could not be reached: %s', ['%s (Player %d) at %s (Player %d)' % (location.item.name, location.item.player, location.name, location.player) for location in sphere_candidates])
if not world.accessibility == 'none': if any([world.accessibility[location.item.player] != 'none' for location in sphere_candidates]):
raise RuntimeError('Not all progression items reachable. Something went terribly wrong here.') raise RuntimeError('Not all progression items reachable. Something went terribly wrong here.')
else: else:
old_world.spoiler.unreachables = sphere_candidates.copy()
break break
# in the second phase, we cull each sphere such that the game is still beatable, reducing each range of influence to the bare minimum required inside it # in the second phase, we cull each sphere such that the game is still beatable, reducing each range of influence to the bare minimum required inside it
@@ -357,7 +396,6 @@ def create_playthrough(world):
logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player) logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player)
old_item = location.item old_item = location.item
location.item = None location.item = None
state.remove(old_item)
if world.can_beat_game(state_cache[num]): if world.can_beat_game(state_cache[num]):
to_delete.append(location) to_delete.append(location)
else: else:
@@ -368,6 +406,14 @@ def create_playthrough(world):
for location in to_delete: for location in to_delete:
sphere.remove(location) sphere.remove(location)
# second phase, sphere 0
for item in [i for i in world.precollected_items if i.advancement]:
logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
world.precollected_items.remove(item)
world.state.remove(item)
if not world.can_beat_game():
world.push_precollected(item)
# we are now down to just the required progress items in collection_spheres. Unfortunately # we are now down to just the required progress items in collection_spheres. Unfortunately
# the previous pruning stage could potentially have made certain items dependant on others # the previous pruning stage could potentially have made certain items dependant on others
# in the same or later sphere (because the location had 2 ways to access but the item originally # in the same or later sphere (because the location had 2 ways to access but the item originally
@@ -378,8 +424,7 @@ def create_playthrough(world):
state = CollectionState(world) state = CollectionState(world)
collection_spheres = [] collection_spheres = []
while required_locations: while required_locations:
if not world.keysanity: state.sweep_for_events(key_only=True)
state.sweep_for_events(key_only=True)
state.sweep_for_crystal_access() state.sweep_for_crystal_access()
sphere = list(filter(lambda loc: state.can_reach(loc) and state.not_flooding_a_key(world, loc), required_locations)) sphere = list(filter(lambda loc: state.can_reach(loc) and state.not_flooding_a_key(world, loc), required_locations))
@@ -415,10 +460,12 @@ def create_playthrough(world):
old_world.spoiler.paths.update({ str(location) : get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere if location.player == player}) old_world.spoiler.paths.update({ str(location) : get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere if location.player == player})
for _, path in dict(old_world.spoiler.paths).items(): for _, path in dict(old_world.spoiler.paths).items():
if any(exit == 'Pyramid Fairy' for (_, exit) in path): if any(exit == 'Pyramid Fairy' for (_, exit) in path):
if world.mode != 'inverted': if world.mode[player] != 'inverted':
old_world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = get_path(state, world.get_region('Big Bomb Shop', player)) old_world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = get_path(state, world.get_region('Big Bomb Shop', player))
else: else:
old_world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = get_path(state, world.get_region('Inverted Big Bomb Shop', player)) old_world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = get_path(state, world.get_region('Inverted Big Bomb Shop', player))
# we can finally output our playthrough # we can finally output our playthrough
old_world.spoiler.playthrough = OrderedDict([(str(i + 1), {str(location): str(location.item) for location in sphere}) for i, sphere in enumerate(collection_spheres)]) old_world.spoiler.playthrough = OrderedDict([("0", [str(item) for item in world.precollected_items if item.advancement])])
for i, sphere in enumerate(collection_spheres):
old_world.spoiler.playthrough[str(i + 1)] = {str(location): str(location.item) for location in sphere}
+921
View File
@@ -0,0 +1,921 @@
import argparse
import asyncio
import json
import logging
import re
import subprocess
import sys
import Items
import Regions
while True:
try:
import aioconsole
break
except ImportError:
aioconsole = None
print('Required python module "aioconsole" not found, press enter to install it')
input()
subprocess.call([sys.executable, '-m', 'pip', 'install', '--upgrade', 'aioconsole'])
while True:
try:
import websockets
break
except ImportError:
websockets = None
print('Required python module "websockets" not found, press enter to install it')
input()
subprocess.call([sys.executable, '-m', 'pip', 'install', '--upgrade', 'websockets'])
try:
import colorama
except ImportError:
colorama = None
class ReceivedItem:
def __init__(self, item, location, player_id, player_name):
self.item = item
self.location = location
self.player_id = player_id
self.player_name = player_name
class Context:
def __init__(self, snes_address, server_address, password, name, team, slot):
self.snes_address = snes_address
self.server_address = server_address
self.exit_event = asyncio.Event()
self.input_queue = asyncio.Queue()
self.input_requests = 0
self.snes_socket = None
self.snes_state = SNES_DISCONNECTED
self.snes_recv_queue = asyncio.Queue()
self.snes_request_lock = asyncio.Lock()
self.is_sd2snes = False
self.snes_write_buffer = []
self.server_task = None
self.socket = None
self.password = password
self.name = name
self.team = team
self.slot = slot
self.locations_checked = set()
self.items_received = []
self.last_rom = None
self.expected_rom = None
self.rom_confirmed = False
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,
'blue_bg': 44, 'purple_bg': 45, 'cyan_bg': 46, 'white_bg': 47}
return '\033[' + ';'.join([str(codes[arg]) for arg in args]) + 'm'
def color(text, *args):
return color_code(*args) + text + color_code('reset')
ROM_START = 0x000000
WRAM_START = 0xF50000
WRAM_SIZE = 0x20000
SRAM_START = 0xE00000
ROMNAME_START = SRAM_START + 0x2000
ROMNAME_SIZE = 0x15
INGAME_MODES = {0x07, 0x09, 0x0b}
SAVEDATA_START = WRAM_START + 0xF000
SAVEDATA_SIZE = 0x500
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
ROOMID_ADDR = SAVEDATA_START + 0x4D4 # 2 bytes
ROOMDATA_ADDR = SAVEDATA_START + 0x4D6 # 1 byte
location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
"Blind's Hideout - Left": (0x11d, 0x20),
"Blind's Hideout - Right": (0x11d, 0x40),
"Blind's Hideout - Far Left": (0x11d, 0x80),
"Blind's Hideout - Far Right": (0x11d, 0x100),
'Secret Passage': (0x55, 0x10),
'Waterfall Fairy - Left': (0x114, 0x10),
'Waterfall Fairy - Right': (0x114, 0x20),
"King's Tomb": (0x113, 0x10),
'Floodgate Chest': (0x10b, 0x10),
"Link's House": (0x104, 0x10),
'Kakariko Tavern': (0x103, 0x10),
'Chicken House': (0x108, 0x10),
"Aginah's Cave": (0x10a, 0x10),
"Sahasrahla's Hut - Left": (0x105, 0x10),
"Sahasrahla's Hut - Middle": (0x105, 0x20),
"Sahasrahla's Hut - Right": (0x105, 0x40),
'Kakariko Well - Top': (0x2f, 0x10),
'Kakariko Well - Left': (0x2f, 0x20),
'Kakariko Well - Middle': (0x2f, 0x40),
'Kakariko Well - Right': (0x2f, 0x80),
'Kakariko Well - Bottom': (0x2f, 0x100),
'Lost Woods Hideout': (0xe1, 0x200),
'Lumberjack Tree': (0xe2, 0x200),
'Cave 45': (0x11b, 0x400),
'Graveyard Cave': (0x11b, 0x200),
'Checkerboard Cave': (0x126, 0x200),
'Mini Moldorm Cave - Far Left': (0x123, 0x10),
'Mini Moldorm Cave - Left': (0x123, 0x20),
'Mini Moldorm Cave - Right': (0x123, 0x40),
'Mini Moldorm Cave - Far Right': (0x123, 0x80),
'Mini Moldorm Cave - Generous Guy': (0x123, 0x400),
'Ice Rod Cave': (0x120, 0x10),
'Bonk Rock Cave': (0x124, 0x10),
'Desert Palace - Big Chest': (0x73, 0x10),
'Desert Palace - Torch': (0x73, 0x400),
'Desert Palace - Map Chest': (0x74, 0x10),
'Desert Palace - Compass Chest': (0x85, 0x10),
'Desert Palace - Big Key Chest': (0x75, 0x10),
'Desert Palace - Boss': (0x33, 0x800),
'Eastern Palace - Compass Chest': (0xa8, 0x10),
'Eastern Palace - Big Chest': (0xa9, 0x10),
'Eastern Palace - Cannonball Chest': (0xb9, 0x10),
'Eastern Palace - Big Key Chest': (0xb8, 0x10),
'Eastern Palace - Map Chest': (0xaa, 0x10),
'Eastern Palace - Boss': (0xc8, 0x800),
'Hyrule Castle - Boomerang Chest': (0x71, 0x10),
'Hyrule Castle - Map Chest': (0x72, 0x10),
"Hyrule Castle - Zelda's Chest": (0x80, 0x10),
'Sewers - Dark Cross': (0x32, 0x10),
'Sewers - Secret Room - Left': (0x11, 0x10),
'Sewers - Secret Room - Middle': (0x11, 0x20),
'Sewers - Secret Room - Right': (0x11, 0x40),
'Sanctuary': (0x12, 0x10),
'Castle Tower - Room 03': (0xe0, 0x10),
'Castle Tower - Dark Maze': (0xd0, 0x10),
'Spectacle Rock Cave': (0xea, 0x400),
'Paradox Cave Lower - Far Left': (0xef, 0x10),
'Paradox Cave Lower - Left': (0xef, 0x20),
'Paradox Cave Lower - Right': (0xef, 0x40),
'Paradox Cave Lower - Far Right': (0xef, 0x80),
'Paradox Cave Lower - Middle': (0xef, 0x100),
'Paradox Cave Upper - Left': (0xff, 0x10),
'Paradox Cave Upper - Right': (0xff, 0x20),
'Spiral Cave': (0xfe, 0x10),
'Tower of Hera - Basement Cage': (0x87, 0x400),
'Tower of Hera - Map Chest': (0x77, 0x10),
'Tower of Hera - Big Key Chest': (0x87, 0x10),
'Tower of Hera - Compass Chest': (0x27, 0x20),
'Tower of Hera - Big Chest': (0x27, 0x10),
'Tower of Hera - Boss': (0x7, 0x800),
'Hype Cave - Top': (0x11e, 0x10),
'Hype Cave - Middle Right': (0x11e, 0x20),
'Hype Cave - Middle Left': (0x11e, 0x40),
'Hype Cave - Bottom': (0x11e, 0x80),
'Hype Cave - Generous Guy': (0x11e, 0x400),
'Peg Cave': (0x127, 0x400),
'Pyramid Fairy - Left': (0x116, 0x10),
'Pyramid Fairy - Right': (0x116, 0x20),
'Brewery': (0x106, 0x10),
'C-Shaped House': (0x11c, 0x10),
'Chest Game': (0x106, 0x400),
'Mire Shed - Left': (0x10d, 0x10),
'Mire Shed - Right': (0x10d, 0x20),
'Superbunny Cave - Top': (0xf8, 0x10),
'Superbunny Cave - Bottom': (0xf8, 0x20),
'Spike Cave': (0x117, 0x10),
'Hookshot Cave - Top Right': (0x3c, 0x10),
'Hookshot Cave - Top Left': (0x3c, 0x20),
'Hookshot Cave - Bottom Right': (0x3c, 0x80),
'Hookshot Cave - Bottom Left': (0x3c, 0x40),
'Mimic Cave': (0x10c, 0x10),
'Swamp Palace - Entrance': (0x28, 0x10),
'Swamp Palace - Map Chest': (0x37, 0x10),
'Swamp Palace - Big Chest': (0x36, 0x10),
'Swamp Palace - Compass Chest': (0x46, 0x10),
'Swamp Palace - Big Key Chest': (0x35, 0x10),
'Swamp Palace - West Chest': (0x34, 0x10),
'Swamp Palace - Flooded Room - Left': (0x76, 0x10),
'Swamp Palace - Flooded Room - Right': (0x76, 0x20),
'Swamp Palace - Waterfall Room': (0x66, 0x10),
'Swamp Palace - Boss': (0x6, 0x800),
"Thieves' Town - Big Key Chest": (0xdb, 0x20),
"Thieves' Town - Map Chest": (0xdb, 0x10),
"Thieves' Town - Compass Chest": (0xdc, 0x10),
"Thieves' Town - Ambush Chest": (0xcb, 0x10),
"Thieves' Town - Attic": (0x65, 0x10),
"Thieves' Town - Big Chest": (0x44, 0x10),
"Thieves' Town - Blind's Cell": (0x45, 0x10),
"Thieves' Town - Boss": (0xac, 0x800),
'Skull Woods - Compass Chest': (0x67, 0x10),
'Skull Woods - Map Chest': (0x58, 0x20),
'Skull Woods - Big Chest': (0x58, 0x10),
'Skull Woods - Pot Prison': (0x57, 0x20),
'Skull Woods - Pinball Room': (0x68, 0x10),
'Skull Woods - Big Key Chest': (0x57, 0x10),
'Skull Woods - Bridge Room': (0x59, 0x10),
'Skull Woods - Boss': (0x29, 0x800),
'Ice Palace - Compass Chest': (0x2e, 0x10),
'Ice Palace - Freezor Chest': (0x7e, 0x10),
'Ice Palace - Big Chest': (0x9e, 0x10),
'Ice Palace - Iced T Room': (0xae, 0x10),
'Ice Palace - Spike Room': (0x5f, 0x10),
'Ice Palace - Big Key Chest': (0x1f, 0x10),
'Ice Palace - Map Chest': (0x3f, 0x10),
'Ice Palace - Boss': (0xde, 0x800),
'Misery Mire - Big Chest': (0xc3, 0x10),
'Misery Mire - Map Chest': (0xc3, 0x20),
'Misery Mire - Main Lobby': (0xc2, 0x10),
'Misery Mire - Bridge Chest': (0xa2, 0x10),
'Misery Mire - Spike Chest': (0xb3, 0x10),
'Misery Mire - Compass Chest': (0xc1, 0x10),
'Misery Mire - Big Key Chest': (0xd1, 0x10),
'Misery Mire - Boss': (0x90, 0x800),
'Turtle Rock - Compass Chest': (0xd6, 0x10),
'Turtle Rock - Roller Room - Left': (0xb7, 0x10),
'Turtle Rock - Roller Room - Right': (0xb7, 0x20),
'Turtle Rock - Chain Chomps': (0xb6, 0x10),
'Turtle Rock - Big Key Chest': (0x14, 0x10),
'Turtle Rock - Big Chest': (0x24, 0x10),
'Turtle Rock - Crystaroller Room': (0x4, 0x10),
'Turtle Rock - Eye Bridge - Bottom Left': (0xd5, 0x80),
'Turtle Rock - Eye Bridge - Bottom Right': (0xd5, 0x40),
'Turtle Rock - Eye Bridge - Top Left': (0xd5, 0x20),
'Turtle Rock - Eye Bridge - Top Right': (0xd5, 0x10),
'Turtle Rock - Boss': (0xa4, 0x800),
'Palace of Darkness - Shooter Room': (0x9, 0x10),
'Palace of Darkness - The Arena - Bridge': (0x2a, 0x20),
'Palace of Darkness - Stalfos Basement': (0xa, 0x10),
'Palace of Darkness - Big Key Chest': (0x3a, 0x10),
'Palace of Darkness - The Arena - Ledge': (0x2a, 0x10),
'Palace of Darkness - Map Chest': (0x2b, 0x10),
'Palace of Darkness - Compass Chest': (0x1a, 0x20),
'Palace of Darkness - Dark Basement - Left': (0x6a, 0x10),
'Palace of Darkness - Dark Basement - Right': (0x6a, 0x20),
'Palace of Darkness - Dark Maze - Top': (0x19, 0x10),
'Palace of Darkness - Dark Maze - Bottom': (0x19, 0x20),
'Palace of Darkness - Big Chest': (0x1a, 0x10),
'Palace of Darkness - Harmless Hellway': (0x1a, 0x40),
'Palace of Darkness - Boss': (0x5a, 0x800),
"Ganons Tower - Bob's Torch": (0x8c, 0x400),
'Ganons Tower - Hope Room - Left': (0x8c, 0x20),
'Ganons Tower - Hope Room - Right': (0x8c, 0x40),
'Ganons Tower - Tile Room': (0x8d, 0x10),
'Ganons Tower - Compass Room - Top Left': (0x9d, 0x10),
'Ganons Tower - Compass Room - Top Right': (0x9d, 0x20),
'Ganons Tower - Compass Room - Bottom Left': (0x9d, 0x40),
'Ganons Tower - Compass Room - Bottom Right': (0x9d, 0x80),
'Ganons Tower - DMs Room - Top Left': (0x7b, 0x10),
'Ganons Tower - DMs Room - Top Right': (0x7b, 0x20),
'Ganons Tower - DMs Room - Bottom Left': (0x7b, 0x40),
'Ganons Tower - DMs Room - Bottom Right': (0x7b, 0x80),
'Ganons Tower - Map Chest': (0x8b, 0x10),
'Ganons Tower - Firesnake Room': (0x7d, 0x10),
'Ganons Tower - Randomizer Room - Top Left': (0x7c, 0x10),
'Ganons Tower - Randomizer Room - Top Right': (0x7c, 0x20),
'Ganons Tower - Randomizer Room - Bottom Left': (0x7c, 0x40),
'Ganons Tower - Randomizer Room - Bottom Right': (0x7c, 0x80),
"Ganons Tower - Bob's Chest": (0x8c, 0x80),
'Ganons Tower - Big Chest': (0x8c, 0x10),
'Ganons Tower - Big Key Room - Left': (0x1c, 0x20),
'Ganons Tower - Big Key Room - Right': (0x1c, 0x40),
'Ganons Tower - Big Key Chest': (0x1c, 0x10),
'Ganons Tower - Mini Helmasaur Room - Left': (0x3d, 0x10),
'Ganons Tower - Mini Helmasaur Room - Right': (0x3d, 0x20),
'Ganons Tower - Pre-Moldorm Chest': (0x3d, 0x40),
'Ganons Tower - Validation Chest': (0x4d, 0x10)}
location_table_npc = {'Mushroom': 0x1000,
'King Zora': 0x2,
'Sahasrahla': 0x10,
'Blacksmith': 0x400,
'Magic Bat': 0x8000,
'Sick Kid': 0x4,
'Library': 0x80,
'Potion Shop': 0x2000,
'Old Man': 0x1,
'Ether Tablet': 0x100,
'Catfish': 0x20,
'Stumpy': 0x8,
'Bombos Tablet': 0x200}
location_table_ow = {'Flute Spot': 0x2a,
'Sunken Treasure': 0x3b,
"Zora's Ledge": 0x81,
'Lake Hylia Island': 0x35,
'Maze Race': 0x28,
'Desert Ledge': 0x30,
'Master Sword Pedestal': 0x80,
'Spectacle Rock': 0x3,
'Pyramid': 0x5b,
'Digging Game': 0x68,
'Bumper Cave Ledge': 0x4a,
'Floating Island': 0x5}
location_table_misc = {'Bottle Merchant': (0x3c9, 0x2),
'Purple Chest': (0x3c9, 0x10),
"Link's Uncle": (0x3c6, 0x1),
'Hobo': (0x3c9, 0x1)}
SNES_DISCONNECTED = 0
SNES_CONNECTING = 1
SNES_CONNECTED = 2
SNES_ATTACHED = 3
async def snes_connect(ctx : Context, address = None):
if ctx.snes_socket is not None:
print('Already connected to snes')
return
ctx.snes_state = SNES_CONNECTING
recv_task = None
if address is None:
address = 'ws://' + ctx.snes_address
print("Connecting to QUsb2snes at %s ..." % address)
try:
ctx.snes_socket = await websockets.connect(address, ping_timeout=None, ping_interval=None)
ctx.snes_state = SNES_CONNECTED
DeviceList_Request = {
"Opcode" : "DeviceList",
"Space" : "SNES"
}
await ctx.snes_socket.send(json.dumps(DeviceList_Request))
reply = json.loads(await ctx.snes_socket.recv())
devices = reply['Results'] if 'Results' in reply and len(reply['Results']) > 0 else None
if not devices:
raise Exception('No device found')
print("Available devices:")
for id, device in enumerate(devices):
print("[%d] %s" % (id + 1, device))
device = None
while True:
print("Enter a number:")
choice = await console_input(ctx)
if choice is None:
raise Exception('Abort input')
if not choice.isdigit() or int(choice) < 1 or int(choice) > len(devices):
print("Invalid choice (%s)" % choice)
continue
device = devices[int(choice) - 1]
break
print("Attaching to " + device)
Attach_Request = {
"Opcode" : "Attach",
"Space" : "SNES",
"Operands" : [device]
}
await ctx.snes_socket.send(json.dumps(Attach_Request))
ctx.snes_state = SNES_ATTACHED
if 'SD2SNES'.lower() in device.lower() or (len(device) == 4 and device[:3] == 'COM'):
print("SD2SNES Detected")
ctx.is_sd2snes = True
await ctx.snes_socket.send(json.dumps({"Opcode" : "Info", "Space" : "SNES"}))
reply = json.loads(await ctx.snes_socket.recv())
if reply and 'Results' in reply:
print(reply['Results'])
else:
ctx.is_sd2snes = False
recv_task = asyncio.create_task(snes_recv_loop(ctx))
except Exception as e:
print("Error connecting to snes (%s)" % e)
if recv_task is not None:
if not ctx.snes_socket.closed:
await ctx.snes_socket.close()
else:
if ctx.snes_socket is not None:
if not ctx.snes_socket.closed:
await ctx.snes_socket.close()
ctx.snes_socket = None
ctx.snes_state = SNES_DISCONNECTED
async def snes_recv_loop(ctx : Context):
try:
async for msg in ctx.snes_socket:
ctx.snes_recv_queue.put_nowait(msg)
print("Snes disconnected, type /snes to reconnect")
except Exception as e:
print("Lost connection to the snes, type /snes to reconnect")
if not isinstance(e, websockets.WebSocketException):
logging.exception(e)
finally:
socket, ctx.snes_socket = ctx.snes_socket, None
if socket is not None and not socket.closed:
await socket.close()
ctx.snes_state = SNES_DISCONNECTED
ctx.snes_recv_queue = asyncio.Queue()
ctx.hud_message_queue = []
ctx.rom_confirmed = False
ctx.last_rom = None
async def snes_read(ctx : Context, address, size):
try:
await ctx.snes_request_lock.acquire()
if ctx.snes_state != SNES_ATTACHED or ctx.snes_socket is None or not ctx.snes_socket.open or ctx.snes_socket.closed:
return None
GetAddress_Request = {
"Opcode" : "GetAddress",
"Space" : "SNES",
"Operands" : [hex(address)[2:], hex(size)[2:]]
}
try:
await ctx.snes_socket.send(json.dumps(GetAddress_Request))
except websockets.ConnectionClosed:
return None
data = bytes()
while len(data) < size:
try:
data += await asyncio.wait_for(ctx.snes_recv_queue.get(), 5)
except asyncio.TimeoutError:
break
if len(data) != size:
print('Error reading %s, requested %d bytes, received %d' % (hex(address), size, len(data)))
if len(data):
print(str(data))
if ctx.snes_socket is not None and not ctx.snes_socket.closed:
await ctx.snes_socket.close()
return None
return data
finally:
ctx.snes_request_lock.release()
async def snes_write(ctx : Context, write_list):
try:
await ctx.snes_request_lock.acquire()
if ctx.snes_state != SNES_ATTACHED or ctx.snes_socket is None or not ctx.snes_socket.open or ctx.snes_socket.closed:
return False
PutAddress_Request = {
"Opcode" : "PutAddress",
"Operands" : []
}
if ctx.is_sd2snes:
cmd = b'\x00\xE2\x20\x48\xEB\x48'
for address, data in write_list:
if (address < WRAM_START) or ((address + len(data)) > (WRAM_START + WRAM_SIZE)):
print("SD2SNES: Write out of range %s (%d)" % (hex(address), len(data)))
return False
for ptr, byte in enumerate(data, address + 0x7E0000 - WRAM_START):
cmd += b'\xA9' # LDA
cmd += bytes([byte])
cmd += b'\x8F' # STA.l
cmd += bytes([ptr & 0xFF, (ptr >> 8) & 0xFF, (ptr >> 16) & 0xFF])
cmd += b'\xA9\x00\x8F\x00\x2C\x00\x68\xEB\x68\x28\x6C\xEA\xFF\x08'
PutAddress_Request['Space'] = 'CMD'
PutAddress_Request['Operands'] = ["2C00", hex(len(cmd)-1)[2:], "2C00", "1"]
try:
if ctx.snes_socket is not None:
await ctx.snes_socket.send(json.dumps(PutAddress_Request))
if ctx.snes_socket is not None:
await ctx.snes_socket.send(cmd)
except websockets.ConnectionClosed:
return False
else:
PutAddress_Request['Space'] = 'SNES'
try:
#will pack those requests as soon as qusb2snes actually supports that for real
for address, data in write_list:
PutAddress_Request['Operands'] = [hex(address)[2:], hex(len(data))[2:]]
if ctx.snes_socket is not None:
await ctx.snes_socket.send(json.dumps(PutAddress_Request))
if ctx.snes_socket is not None:
await ctx.snes_socket.send(data)
except websockets.ConnectionClosed:
return False
return True
finally:
ctx.snes_request_lock.release()
def snes_buffered_write(ctx : Context, address, data):
if len(ctx.snes_write_buffer) > 0 and (ctx.snes_write_buffer[-1][0] + len(ctx.snes_write_buffer[-1][1])) == address:
ctx.snes_write_buffer[-1] = (ctx.snes_write_buffer[-1][0], ctx.snes_write_buffer[-1][1] + data)
else:
ctx.snes_write_buffer.append((address, data))
async def snes_flush_writes(ctx : Context):
if not ctx.snes_write_buffer:
return
await snes_write(ctx, ctx.snes_write_buffer)
ctx.snes_write_buffer = []
async def send_msgs(websocket, msgs):
if not websocket or not websocket.open or websocket.closed:
return
try:
await websocket.send(json.dumps(msgs))
except websockets.ConnectionClosed:
pass
async def server_loop(ctx : Context):
if ctx.socket is not None:
print('Already connected')
return
while not ctx.server_address:
print('Enter multiworld server address')
ctx.server_address = await console_input(ctx)
address = f"ws://{ctx.server_address}" if "://" not in ctx.server_address else ctx.server_address
print('Connecting to multiworld server at %s' % address)
try:
ctx.socket = await websockets.connect(address, ping_timeout=None, ping_interval=None)
print('Connected')
async for data in ctx.socket:
for msg in json.loads(data):
cmd, args = (msg[0], msg[1]) if len(msg) > 1 else (msg, None)
await process_server_cmd(ctx, cmd, args)
print('Disconnected from multiworld server, type /connect to reconnect')
except ConnectionRefusedError:
print('Connection refused by the multiworld server')
except (OSError, websockets.InvalidURI):
print('Failed to connect to the multiworld server')
except Exception as e:
print('Lost connection to the multiworld server, type /connect to reconnect')
if not isinstance(e, websockets.WebSocketException):
logging.exception(e)
finally:
ctx.name = None
ctx.team = None
ctx.slot = None
ctx.expected_rom = None
ctx.rom_confirmed = False
socket, ctx.socket = ctx.socket, None
if socket is not None and not socket.closed:
await socket.close()
ctx.server_task = None
async def process_server_cmd(ctx : Context, cmd, args):
if cmd == 'RoomInfo':
print('--------------------------------')
print('Room Information:')
print('--------------------------------')
if args['password']:
print('Password required')
print('%d players seed' % args['slots'])
if len(args['players']) < 1:
print('No player connected')
else:
args['players'].sort(key=lambda player: ('' if not player[1] else player[1].lower(), player[2]))
current_team = 0
print('Connected players:')
for name, team, slot in args['players']:
if team != current_team:
print(' Default team' if not team else ' Team: %s' % team)
current_team = team
print(' %s (Player %d)' % (name, slot))
await server_auth(ctx, args['password'])
if cmd == 'ConnectionRefused':
password_requested = False
if 'InvalidPassword' in args:
print('Invalid password')
ctx.password = None
password_requested = True
if 'InvalidName' in args:
print('Invalid name')
ctx.name = None
if 'NameAlreadyTaken' in args:
print('Name already taken')
ctx.name = None
if 'InvalidTeam' in args:
print('Invalid team name')
ctx.team = None
if 'InvalidSlot' in args:
print('Invalid player slot')
ctx.slot = None
if 'SlotAlreadyTaken' in args:
print('Player slot already in use for that team')
ctx.team = None
ctx.slot = None
await server_auth(ctx, password_requested)
if cmd == 'Connected':
ctx.expected_rom = args
if ctx.last_rom is not None:
if ctx.last_rom[:len(args)] == ctx.expected_rom:
rom_confirmed(ctx)
if ctx.locations_checked:
await send_msgs(ctx.socket, [['LocationChecks', [Regions.location_table[loc][0] for loc in ctx.locations_checked]]])
else:
raise Exception('Different ROM expected from server')
if cmd == 'ReceivedItems':
start_index, items = args
if start_index == 0:
ctx.items_received = []
elif start_index != len(ctx.items_received):
sync_msg = [['Sync']]
if ctx.locations_checked:
sync_msg.append(['LocationChecks', [Regions.location_table[loc][0] for loc in ctx.locations_checked]])
await send_msgs(ctx.socket, sync_msg)
if start_index == len(ctx.items_received):
for item in items:
ctx.items_received.append(ReceivedItem(item[0], item[1], item[2], item[3]))
if cmd == 'ItemSent':
player_sent, player_recvd, item, location = args
item = color(get_item_name_from_id(item), 'cyan' if player_sent != ctx.name else 'green')
player_sent = color(player_sent, 'yellow' if player_sent != ctx.name else 'magenta')
player_recvd = color(player_recvd, 'yellow' if player_recvd != ctx.name else 'magenta')
print('(%s) %s sent %s to %s (%s)' % (ctx.team if ctx.team else 'Team', player_sent, item, player_recvd, get_location_name_from_address(location)))
if cmd == 'Print':
print(args)
async def server_auth(ctx : Context, password_requested):
if password_requested and not ctx.password:
print('Enter the password required to join this game:')
ctx.password = await console_input(ctx)
while not ctx.name or not re.match(r'\w{1,10}', ctx.name):
print('Enter your name (10 characters):')
ctx.name = await console_input(ctx)
if not ctx.team:
print('Enter your team name (optional):')
ctx.team = await console_input(ctx)
if ctx.team == '': ctx.team = None
if not ctx.slot:
print('Choose your player slot (optional):')
slot = await console_input(ctx)
ctx.slot = int(slot) if slot.isdigit() else None
await send_msgs(ctx.socket, [['Connect', {'password': ctx.password, 'name': ctx.name, 'team': ctx.team, 'slot': ctx.slot}]])
async def console_input(ctx : Context):
ctx.input_requests += 1
return await ctx.input_queue.get()
async def console_loop(ctx : Context):
while not ctx.exit_event.is_set():
input = await aioconsole.ainput()
if ctx.input_requests > 0:
ctx.input_requests -= 1
ctx.input_queue.put_nowait(input)
continue
command = input.split()
if not command:
continue
if command[0] == '/exit':
ctx.exit_event.set()
if command[0] == '/installcolors' and 'colorama' not in sys.modules:
subprocess.call([sys.executable, '-m', 'pip', 'install', '--upgrade', 'colorama'])
global colorama
import colorama
colorama.init()
if command[0] == '/snes':
asyncio.create_task(snes_connect(ctx, command[1] if len(command) > 1 else None))
if command[0] in ['/snes_close', '/snes_quit']:
if ctx.snes_socket is not None and not ctx.snes_socket.closed:
await ctx.snes_socket.close()
async def disconnect():
if ctx.socket is not None and not ctx.socket.closed:
await ctx.socket.close()
if ctx.server_task is not None:
await ctx.server_task
async def connect():
await disconnect()
ctx.server_task = asyncio.create_task(server_loop(ctx))
if command[0] in ['/connect', '/reconnect']:
if len(command) > 1:
ctx.server_address = command[1]
asyncio.create_task(connect())
if command[0] == '/disconnect':
asyncio.create_task(disconnect())
if command[0][:1] != '/':
asyncio.create_task(send_msgs(ctx.socket, [['Say', input]]))
if command[0] == '/received':
print('Received items:')
for index, item in enumerate(ctx.items_received, 1):
print('%s from %s (%s) (%d/%d in list)' % (
color(get_item_name_from_id(item.item), 'red', 'bold'), color(item.player_name, 'yellow'),
get_location_name_from_address(item.location), index, len(ctx.items_received)))
if command[0] == '/missing':
for location in [k for k, v in Regions.location_table.items() if type(v[0]) is int]:
if location not in ctx.locations_checked:
print('Missing: ' + location)
if command[0] == '/getitem' and len(command) > 1:
item = input[9:]
item_id = Items.item_table[item][3] if item in Items.item_table else None
if type(item_id) is int and item_id in range(0x100):
print('Sending item: ' + item)
snes_buffered_write(ctx, RECV_ITEM_ADDR, bytes([item_id]))
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR, bytes([0]))
else:
print('Invalid item: ' + item)
await snes_flush_writes(ctx)
def rom_confirmed(ctx : Context):
ctx.rom_confirmed = True
print('ROM hash Confirmed')
def get_item_name_from_id(code):
items = [k for k, i in Items.item_table.items() if type(i[3]) is int and i[3] == code]
return items[0] if items else 'Unknown item'
def get_location_name_from_address(address):
if type(address) is str:
return address
locs = [k for k, l in Regions.location_table.items() if type(l[0]) is int and l[0] == address]
return locs[0] if locs else 'Unknown location'
async def track_locations(ctx : Context, roomid, roomdata):
new_locations = []
def new_check(location):
ctx.locations_checked.add(location)
print("New check: %s (%d/216)" % (location, len(ctx.locations_checked)))
new_locations.append(Regions.location_table[location][0])
for location, (loc_roomid, loc_mask) in location_table_uw.items():
if location not in ctx.locations_checked and loc_roomid == roomid and (roomdata << 4) & loc_mask != 0:
new_check(location)
uw_begin = 0x129
uw_end = 0
uw_unchecked = {}
for location, (roomid, mask) in location_table_uw.items():
if location not in ctx.locations_checked:
uw_unchecked[location] = (roomid, mask)
uw_begin = min(uw_begin, roomid)
uw_end = max(uw_end, roomid + 1)
if uw_begin < uw_end:
uw_data = await snes_read(ctx, SAVEDATA_START + (uw_begin * 2), (uw_end - uw_begin) * 2)
if uw_data is not None:
for location, (roomid, mask) in uw_unchecked.items():
offset = (roomid - uw_begin) * 2
roomdata = uw_data[offset] | (uw_data[offset + 1] << 8)
if roomdata & mask != 0:
new_check(location)
ow_begin = 0x82
ow_end = 0
ow_unchecked = {}
for location, screenid in location_table_ow.items():
if location not in ctx.locations_checked:
ow_unchecked[location] = screenid
ow_begin = min(ow_begin, screenid)
ow_end = max(ow_end, screenid + 1)
if ow_begin < ow_end:
ow_data = await snes_read(ctx, SAVEDATA_START + 0x280 + ow_begin, ow_end - ow_begin)
if ow_data is not None:
for location, screenid in ow_unchecked.items():
if ow_data[screenid - ow_begin] & 0x40 != 0:
new_check(location)
if not all([location in ctx.locations_checked for location in location_table_npc.keys()]):
npc_data = await snes_read(ctx, SAVEDATA_START + 0x410, 2)
if npc_data is not None:
npc_value = npc_data[0] | (npc_data[1] << 8)
for location, mask in location_table_npc.items():
if npc_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_misc.keys()]):
misc_data = await snes_read(ctx, SAVEDATA_START + 0x3c6, 4)
if misc_data is not None:
for location, (offset, mask) in location_table_misc.items():
assert(0x3c6 <= offset <= 0x3c9)
if misc_data[offset - 0x3c6] & 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):
while not ctx.exit_event.is_set():
await asyncio.sleep(2)
if not ctx.rom_confirmed:
rom = await snes_read(ctx, ROMNAME_START, ROMNAME_SIZE)
if rom is None or rom == bytes([0] * ROMNAME_SIZE):
continue
if list(rom) != ctx.last_rom:
ctx.last_rom = list(rom)
ctx.locations_checked = set()
if ctx.expected_rom is not None:
if ctx.last_rom[:len(ctx.expected_rom)] != ctx.expected_rom:
print("Wrong ROM detected")
await ctx.snes_socket.close()
continue
else:
rom_confirmed(ctx)
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
if gamemode is None or gamemode[0] not in INGAME_MODES:
continue
data = await snes_read(ctx, RECV_PROGRESS_ADDR, 7)
if data is None:
continue
recv_index = data[0] | (data[1] << 8)
assert(RECV_ITEM_ADDR == RECV_PROGRESS_ADDR + 2)
recv_item = data[2]
assert(ROOMID_ADDR == RECV_PROGRESS_ADDR + 4)
roomid = data[4] | (data[5] << 8)
assert(ROOMDATA_ADDR == RECV_PROGRESS_ADDR + 6)
roomdata = data[6]
await track_locations(ctx, roomid, roomdata)
if recv_index < len(ctx.items_received) and recv_item == 0:
item = ctx.items_received[recv_index]
print('Received %s from %s (%s) (%d/%d in list)' % (
color(get_item_name_from_id(item.item), 'red', 'bold'), color(item.player_name, 'yellow'),
get_location_name_from_address(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]))
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR, bytes([item.player_id]))
await snes_flush_writes(ctx)
async def main():
parser = argparse.ArgumentParser()
parser.add_argument('--snes', default='localhost:8080', help='Address of the QUsb2snes server.')
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
parser.add_argument('--password', default=None, help='Password of the multiworld host.')
parser.add_argument('--name', default=None)
parser.add_argument('--team', default=None)
parser.add_argument('--slot', default=None, type=int)
args = parser.parse_args()
ctx = Context(args.snes, args.connect, args.password, args.name, args.team, args.slot)
input_task = asyncio.create_task(console_loop(ctx))
await snes_connect(ctx)
if ctx.server_task is None:
ctx.server_task = asyncio.create_task(server_loop(ctx))
watcher_task = asyncio.create_task(game_watcher(ctx))
await ctx.exit_event.wait()
await watcher_task
if ctx.socket is not None and not ctx.socket.closed:
await ctx.socket.close()
if ctx.server_task is not None:
await ctx.server_task
if ctx.snes_socket is not None and not ctx.snes_socket.closed:
await ctx.snes_socket.close()
while ctx.input_requests > 0:
ctx.input_queue.put_nowait(None)
ctx.input_requests -= 1
await input_task
if __name__ == '__main__':
if 'colorama' in sys.modules:
colorama.init()
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.run_until_complete(asyncio.gather(*asyncio.Task.all_tasks()))
loop.close()
if 'colorama' in sys.modules:
colorama.deinit()
+398
View File
@@ -0,0 +1,398 @@
import aioconsole
import argparse
import asyncio
import functools
import json
import logging
import re
import urllib.request
import websockets
import zlib
import Items
import Regions
from MultiClient import ReceivedItem, get_item_name_from_id, get_location_name_from_address
class Client:
def __init__(self, socket):
self.socket = socket
self.auth = False
self.name = None
self.team = None
self.slot = None
self.send_index = 0
class Context:
def __init__(self, host, port, password):
self.data_filename = None
self.save_filename = None
self.disable_save = False
self.players = 0
self.rom_names = {}
self.locations = {}
self.host = host
self.port = port
self.password = password
self.server = None
self.clients = []
self.received_items = {}
def get_room_info(ctx : Context):
return {
'password': ctx.password is not None,
'slots': ctx.players,
'players': [(client.name, client.team, client.slot) for client in ctx.clients if client.auth]
}
def same_name(lhs, rhs):
return lhs.lower() == rhs.lower()
def same_team(lhs, rhs):
return (type(lhs) is type(rhs)) and ((not lhs and not rhs) or (lhs.lower() == rhs.lower()))
async def send_msgs(websocket, msgs):
if not websocket or not websocket.open or websocket.closed:
return
try:
await websocket.send(json.dumps(msgs))
except websockets.ConnectionClosed:
pass
def broadcast_all(ctx : Context, msgs):
for client in ctx.clients:
if client.auth:
asyncio.create_task(send_msgs(client.socket, msgs))
def broadcast_team(ctx : Context, team, msgs):
for client in ctx.clients:
if client.auth and same_team(client.team, team):
asyncio.create_task(send_msgs(client.socket, msgs))
def notify_all(ctx : Context, text):
print("Notice (all): %s" % text)
broadcast_all(ctx, [['Print', text]])
def notify_team(ctx : Context, team : str, text : str):
print("Team notice (%s): %s" % ("Default" if not team else team, text))
broadcast_team(ctx, team, [['Print', text]])
def notify_client(client : Client, text : str):
if not client.auth:
return
print("Player notice (%s): %s" % (client.name, text))
asyncio.create_task(send_msgs(client.socket, [['Print', text]]))
async def server(websocket, path, ctx : Context):
client = Client(websocket)
ctx.clients.append(client)
try:
await on_client_connected(ctx, client)
async for data in websocket:
for msg in json.loads(data):
if len(msg) == 1:
cmd = msg
args = None
else:
cmd = msg[0]
args = msg[1]
await process_client_cmd(ctx, client, cmd, args)
except Exception as e:
if not isinstance(e, websockets.WebSocketException):
logging.exception(e)
finally:
await on_client_disconnected(ctx, client)
ctx.clients.remove(client)
async def on_client_connected(ctx : Context, client : Client):
await send_msgs(client.socket, [['RoomInfo', get_room_info(ctx)]])
async def on_client_disconnected(ctx : Context, client : Client):
if client.auth:
await on_client_left(ctx, client)
async def on_client_joined(ctx : Context, client : Client):
notify_all(ctx, "%s has joined the game as player %d for %s" % (client.name, client.slot, "the default team" if not client.team else "team %s" % client.team))
async def on_client_left(ctx : Context, client : Client):
notify_all(ctx, "%s (Player %d, %s) has left the game" % (client.name, client.slot, "Default team" if not client.team else "Team %s" % client.team))
def get_connected_players_string(ctx : Context):
auth_clients = [c for c in ctx.clients if c.auth]
if not auth_clients:
return 'No player connected'
auth_clients.sort(key=lambda c: ('' if not c.team else c.team.lower(), c.slot))
current_team = 0
text = ''
for c in auth_clients:
if c.team != current_team:
text += '::' + ('default team' if not c.team else c.team) + ':: '
current_team = c.team
text += '%d:%s ' % (c.slot, c.name)
return 'Connected players: ' + text[:-1]
def get_player_name_in_team(ctx : Context, team, slot):
for client in ctx.clients:
if client.auth and same_team(team, client.team) and client.slot == slot:
return client.name
return "Player %d" % slot
def get_client_from_name(ctx : Context, name):
for client in ctx.clients:
if client.auth and same_name(name, client.name):
return client
return None
def get_received_items(ctx : Context, team, player):
for (c_team, c_id), items in ctx.received_items.items():
if c_id == player and same_team(c_team, team):
return items
ctx.received_items[(team, player)] = []
return ctx.received_items[(team, player)]
def tuplize_received_items(items):
return [(item.item, item.location, item.player_id, item.player_name) for item in items]
def send_new_items(ctx : Context):
for client in ctx.clients:
if not client.auth:
continue
items = get_received_items(ctx, client.team, client.slot)
if len(items) > client.send_index:
asyncio.create_task(send_msgs(client.socket, [['ReceivedItems', (client.send_index, tuplize_received_items(items)[client.send_index:])]]))
client.send_index = len(items)
def forfeit_player(ctx : Context, team, slot, name):
all_locations = [values[0] for values in Regions.location_table.values() if type(values[0]) is int]
notify_all(ctx, "%s (Player %d) in team %s has forfeited" % (name, slot, team if team else 'default'))
register_location_checks(ctx, name, team, slot, all_locations)
def register_location_checks(ctx : Context, name, team, slot, locations):
found_items = False
for location in locations:
if (location, slot) in ctx.locations:
target_item, target_player = ctx.locations[(location, slot)]
if target_player != slot:
found = False
recvd_items = get_received_items(ctx, team, target_player)
for recvd_item in recvd_items:
if recvd_item.location == location and recvd_item.player_id == slot:
found = True
break
if not found:
new_item = ReceivedItem(target_item, location, slot, name)
recvd_items.append(new_item)
target_player_name = get_player_name_in_team(ctx, team, target_player)
broadcast_team(ctx, team, [['ItemSent', (name, target_player_name, target_item, location)]])
print('(%s) %s sent %s to %s (%s)' % (team if team else 'Team', name, get_item_name_from_id(target_item), target_player_name, get_location_name_from_address(location)))
found_items = True
send_new_items(ctx)
if found_items and not ctx.disable_save:
try:
with open(ctx.save_filename, "wb") as f:
jsonstr = json.dumps((ctx.players,
[(k, v) for k, v in ctx.rom_names.items()],
[(k, [i.__dict__ for i in v]) for k, v in ctx.received_items.items()]))
f.write(zlib.compress(jsonstr.encode("utf-8")))
except Exception as e:
logging.exception(e)
async def process_client_cmd(ctx : Context, client : Client, cmd, args):
if type(cmd) is not str:
await send_msgs(client.socket, [['InvalidCmd']])
return
if cmd == 'Connect':
if not args or type(args) is not dict or \
'password' not in args or type(args['password']) not in [str, type(None)] or \
'name' not in args or type(args['name']) is not str or \
'team' not in args or type(args['team']) not in [str, type(None)] or \
'slot' not in args or type(args['slot']) not in [int, type(None)]:
await send_msgs(client.socket, [['InvalidArguments', 'Connect']])
return
errors = set()
if ctx.password is not None and ('password' not in args or args['password'] != ctx.password):
errors.add('InvalidPassword')
if 'name' not in args or not args['name'] or not re.match(r'\w{1,10}', args['name']):
errors.add('InvalidName')
elif any([same_name(c.name, args['name']) for c in ctx.clients if c.auth]):
errors.add('NameAlreadyTaken')
else:
client.name = args['name']
if 'team' in args and args['team'] is not None and not re.match(r'\w{1,15}', args['team']):
errors.add('InvalidTeam')
else:
client.team = args['team'] if 'team' in args else None
if 'slot' in args and any([c.slot == args['slot'] for c in ctx.clients if c.auth and same_team(c.team, client.team)]):
errors.add('SlotAlreadyTaken')
elif 'slot' not in args or not args['slot']:
for slot in range(1, ctx.players + 1):
if slot not in [c.slot for c in ctx.clients if c.auth and same_team(c.team, client.team)]:
client.slot = slot
break
elif slot == ctx.players:
errors.add('SlotAlreadyTaken')
elif args['slot'] not in range(1, ctx.players + 1):
errors.add('InvalidSlot')
else:
client.slot = args['slot']
if errors:
client.name = None
client.team = None
client.slot = None
await send_msgs(client.socket, [['ConnectionRefused', list(errors)]])
else:
client.auth = True
reply = [['Connected', ctx.rom_names[client.slot]]]
items = get_received_items(ctx, client.team, client.slot)
if items:
reply.append(['ReceivedItems', (0, tuplize_received_items(items))])
client.send_index = len(items)
await send_msgs(client.socket, reply)
await on_client_joined(ctx, client)
if not client.auth:
return
if cmd == 'Sync':
items = get_received_items(ctx, client.team, client.slot)
if items:
client.send_index = len(items)
await send_msgs(client.socket, ['ReceivedItems', (0, tuplize_received_items(items))])
if cmd == 'LocationChecks':
if type(args) is not list:
await send_msgs(client.socket, [['InvalidArguments', 'LocationChecks']])
return
register_location_checks(ctx, client.name, client.team, client.slot, args)
if cmd == 'Say':
if type(args) is not str or not args.isprintable():
await send_msgs(client.socket, [['InvalidArguments', 'Say']])
return
notify_all(ctx, client.name + ': ' + args)
if args[:8] == '!players':
notify_all(ctx, get_connected_players_string(ctx))
if args[:8] == '!forfeit':
forfeit_player(ctx, client.team, client.slot, client.name)
def set_password(ctx : Context, password):
ctx.password = password
print('Password set to ' + password if password is not None else 'Password disabled')
async def console(ctx : Context):
while True:
input = await aioconsole.ainput()
command = input.split()
if not command:
continue
if command[0] == '/exit':
ctx.server.ws_server.close()
break
if command[0] == '/players':
print(get_connected_players_string(ctx))
if command[0] == '/password':
set_password(ctx, command[1] if len(command) > 1 else None)
if command[0] == '/kick' and len(command) > 1:
client = get_client_from_name(ctx, command[1])
if client and client.socket and not client.socket.closed:
await client.socket.close()
if command[0] == '/forfeitslot' and len(command) == 3 and command[2].isdigit():
team = command[1] if command[1] != 'default' else None
slot = int(command[2])
name = get_player_name_in_team(ctx, team, slot)
forfeit_player(ctx, team, slot, name)
if command[0] == '/forfeitplayer' and len(command) > 1:
client = get_client_from_name(ctx, command[1])
if client:
forfeit_player(ctx, client.team, client.slot, client.name)
if command[0] == '/senditem' and len(command) > 2:
[(player, item)] = re.findall(r'\S* (\S*) (.*)', input)
if item in Items.item_table:
client = get_client_from_name(ctx, player)
if client:
new_item = ReceivedItem(Items.item_table[item][3], "cheat console", 0, "server")
get_received_items(ctx, client.team, client.slot).append(new_item)
notify_all(ctx, 'Cheat console: sending "' + item + '" to ' + client.name)
send_new_items(ctx)
else:
print("Unknown item: " + item)
if command[0][0] != '/':
notify_all(ctx, '[Server]: ' + input)
async def main():
parser = argparse.ArgumentParser()
parser.add_argument('--host', default=None)
parser.add_argument('--port', default=38281, type=int)
parser.add_argument('--password', default=None)
parser.add_argument('--multidata', default=None)
parser.add_argument('--savefile', default=None)
parser.add_argument('--disable_save', default=False, action='store_true')
args = parser.parse_args()
ctx = Context(args.host, args.port, args.password)
ctx.data_filename = args.multidata
try:
if not ctx.data_filename:
import tkinter
import tkinter.filedialog
root = tkinter.Tk()
root.withdraw()
ctx.data_filename = tkinter.filedialog.askopenfilename(filetypes=(("Multiworld data","*multidata"),))
with open(ctx.data_filename, 'rb') as f:
jsonobj = json.loads(zlib.decompress(f.read()).decode("utf-8"))
ctx.players = jsonobj[0]
ctx.rom_names = {k: v for k, v in jsonobj[1]}
ctx.locations = {tuple(k): tuple(v) for k, v in jsonobj[2]}
except Exception as e:
print('Failed to read multiworld data (%s)' % e)
return
ip = urllib.request.urlopen('https://v4.ident.me').read().decode('utf8') if not ctx.host else ctx.host
print('Hosting game of %d players (%s) at %s:%d' % (ctx.players, 'No password' if not ctx.password else 'Password: %s' % ctx.password, ip, ctx.port))
ctx.disable_save = args.disable_save
if not ctx.disable_save:
if not ctx.save_filename:
ctx.save_filename = (ctx.data_filename[:-9] if ctx.data_filename[-9:] == 'multidata' else (ctx.data_filename + '_')) + 'multisave'
try:
with open(ctx.save_filename, 'rb') as f:
jsonobj = json.loads(zlib.decompress(f.read()).decode("utf-8"))
players = jsonobj[0]
rom_names = {k: v for k, v in jsonobj[1]}
received_items = {tuple(k): [ReceivedItem(**i) for i in v] for k, v in jsonobj[2]}
if players != ctx.players or rom_names != ctx.rom_names:
raise Exception('Save file mismatch, will start a new game')
ctx.received_items = received_items
print('Loaded save file with %d received items for %d players' % (sum([len(p) for p in received_items.values()]), len(received_items)))
except FileNotFoundError:
print('No save data found, starting a new game')
except Exception as e:
print(e)
ctx.server = websockets.serve(functools.partial(server,ctx=ctx), ctx.host, ctx.port, ping_timeout=None, ping_interval=None)
await ctx.server
await console(ctx)
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.run_until_complete(asyncio.gather(*asyncio.Task.all_tasks()))
loop.close()
+221
View File
@@ -0,0 +1,221 @@
import argparse
import logging
import random
import urllib.request
import urllib.parse
from DungeonRandomizer import parse_arguments
from Main import main as DRMain
def parse_yaml(txt):
def strip(s):
s = s.strip()
return '' if not s else s.strip('"') if s[0] == '"' else s.strip("'") if s[0] == "'" else s
ret = {}
indents = {len(txt) - len(txt.lstrip(' ')): ret}
for line in txt.splitlines():
if not line:
continue
name, val = line.split(':', 1)
val = strip(val)
spaces = len(name) - len(name.lstrip(' '))
name = strip(name)
if val:
indents[spaces][name] = val
else:
newdict = {}
indents[spaces][name] = newdict
indents[spaces+2] = newdict
return ret
def main():
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('--multi', default=1, type=lambda value: min(max(int(value), 1), 255))
multiargs, _ = parser.parse_known_args()
parser = argparse.ArgumentParser()
parser.add_argument('--weights', help='Path to the weights file to use for rolling game settings, urls are also valid')
parser.add_argument('--samesettings', help='Rolls settings per weights file rather than per player', action='store_true')
parser.add_argument('--seed', help='Define seed number to generate.', type=int)
parser.add_argument('--multi', default=1, type=lambda value: min(max(int(value), 1), 255))
parser.add_argument('--names', default='')
parser.add_argument('--create_spoiler', action='store_true')
parser.add_argument('--rom')
parser.add_argument('--enemizercli')
parser.add_argument('--outputpath')
for player in range(1, multiargs.multi + 1):
parser.add_argument(f'--p{player}', help=argparse.SUPPRESS)
args = parser.parse_args()
if args.seed is None:
random.seed(None)
seed = random.randint(0, 999999999)
else:
seed = args.seed
random.seed(seed)
seedname = f'M{random.randint(0, 999999999)}'
print(f"Generating mystery for {args.multi} player{'s' if args.multi > 1 else ''}, {seedname} Seed {seed}")
weights_cache = {}
if args.weights:
weights_cache[args.weights] = get_weights(args.weights)
print(f"Weights: {args.weights} >> {weights_cache[args.weights]['description']}")
for player in range(1, args.multi + 1):
path = getattr(args, f'p{player}')
if path:
if path not in weights_cache:
weights_cache[path] = get_weights(path)
print(f"P{player} Weights: {path} >> {weights_cache[path]['description']}")
erargs = parse_arguments(['--multi', str(args.multi)])
erargs.seed = seed
erargs.names = args.names
erargs.create_spoiler = args.create_spoiler
erargs.race = True
erargs.outputname = seedname
erargs.outputpath = args.outputpath
if args.rom:
erargs.rom = args.rom
if args.enemizercli:
erargs.enemizercli = args.enemizercli
settings_cache = {k: (roll_settings(v) if args.samesettings else None) for k, v in weights_cache.items()}
for player in range(1, args.multi + 1):
path = getattr(args, f'p{player}') if getattr(args, f'p{player}') else args.weights
if path:
settings = settings_cache[path] if settings_cache[path] else roll_settings(weights_cache[path])
for k, v in vars(settings).items():
if v is not None:
getattr(erargs, k)[player] = v
else:
raise RuntimeError(f'No weights specified for player {player}')
# set up logger
loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[erargs.loglevel]
logging.basicConfig(format='%(message)s', level=loglevel)
DRMain(erargs, seed)
def get_weights(path):
try:
if urllib.parse.urlparse(path).scheme:
yaml = str(urllib.request.urlopen(path).read(), "utf-8")
else:
with open(path, 'rb') as f:
yaml = str(f.read(), "utf-8")
except Exception as e:
print('Failed to read weights (%s)' % e)
return
return parse_yaml(yaml)
def roll_settings(weights):
def get_choice(option, root=weights):
if option not in root:
return None
if type(root[option]) is not dict:
return root[option]
if not root[option]:
return None
return random.choices(list(root[option].keys()), weights=list(map(int,root[option].values())))[0]
ret = argparse.Namespace()
glitches_required = get_choice('glitches_required')
if glitches_required not in ['none', 'no_logic']:
print("Only NMG and No Logic supported")
glitches_required = 'none'
ret.logic = {'none': 'noglitches', 'no_logic': 'nologic'}[glitches_required]
item_placement = get_choice('item_placement')
# not supported in ER
dungeon_items = get_choice('dungeon_items')
ret.mapshuffle = get_choice('map_shuffle') == 'on' if 'map_shuffle' in weights else dungeon_items in ['mc', 'mcs', 'full']
ret.compassshuffle = get_choice('compass_shuffle') == 'on' if 'compass_shuffle' in weights else dungeon_items in ['mc', 'mcs', 'full']
ret.keyshuffle = get_choice('smallkey_shuffle') == 'on' if 'smallkey_shuffle' in weights else dungeon_items in ['mcs', 'full']
ret.bigkeyshuffle = get_choice('bigkey_shuffle') == 'on' if 'bigkey_shuffle' in weights else dungeon_items in ['full']
ret.accessibility = get_choice('accessibility')
entrance_shuffle = get_choice('entrance_shuffle')
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
door_shuffle = get_choice('door_shuffle')
ret.door_shuffle = door_shuffle if door_shuffle != 'none' else 'vanilla'
ret.goal = {'ganon': 'ganon',
'fast_ganon': 'crystals',
'dungeons': 'dungeons',
'pedestal': 'pedestal',
'triforce-hunt': 'triforcehunt'
}[get_choice('goals')]
ret.openpyramid = ret.goal == 'fast_ganon'
ret.crystals_gt = get_choice('tower_open')
ret.crystals_ganon = get_choice('ganon_open')
ret.mode = get_choice('world_state')
if ret.mode == 'retro':
ret.mode = 'open'
ret.retro = True
ret.hints = get_choice('hints') == 'on'
ret.swords = {'randomized': 'random',
'assured': 'assured',
'vanilla': 'vanilla',
'swordless': 'swordless'
}[get_choice('weapons')]
ret.difficulty = get_choice('item_pool')
ret.item_functionality = get_choice('item_functionality')
ret.shufflebosses = {'none': 'none',
'simple': 'basic',
'full': 'normal',
'random': 'chaos'
}[get_choice('boss_shuffle')]
ret.shuffleenemies = {'none': 'none',
'shuffled': 'shuffled',
'random': 'chaos'
}[get_choice('enemy_shuffle')]
ret.enemy_damage = {'default': 'default',
'shuffled': 'shuffled',
'random': 'chaos'
}[get_choice('enemy_damage')]
ret.enemy_health = get_choice('enemy_health')
ret.shufflepots = get_choice('pot_shuffle') == 'on'
ret.beemizer = int(get_choice('beemizer')) if 'beemizer' in weights else 0
inventoryweights = weights.get('startinventory', {})
startitems = []
for item in inventoryweights.keys():
if get_choice(item, inventoryweights) == 'on':
startitems.append(item)
ret.startinventory = ','.join(startitems)
if 'rom' in weights:
romweights = weights['rom']
ret.sprite = get_choice('sprite', romweights)
ret.disablemusic = get_choice('disablemusic', romweights) == 'on'
ret.quickswap = get_choice('quickswap', romweights) == 'on'
ret.fastmenu = get_choice('menuspeed', romweights)
ret.heartcolor = get_choice('heartcolor', romweights)
ret.heartbeep = get_choice('heartbeep', romweights)
ret.ow_palettes = get_choice('ow_palettes', romweights)
ret.uw_palettes = get_choice('uw_palettes', romweights)
return ret
if __name__ == '__main__':
main()
+19 -20
View File
@@ -10,7 +10,7 @@ import sys
from BaseClasses import World from BaseClasses import World
from Regions import create_regions from Regions import create_regions
from EntranceShuffle import link_entrances, connect_entrance, connect_two_way, connect_exit from EntranceShuffle import link_entrances, connect_entrance, connect_two_way, connect_exit
from Rom import patch_rom, LocalRom, Sprite, write_string_to_rom from Rom import patch_rom, LocalRom, write_string_to_rom, apply_rom_settings, get_sprite_from_name
from Rules import set_rules from Rules import set_rules
from Dungeons import create_dungeons from Dungeons import create_dungeons
from Items import ItemFactory from Items import ItemFactory
@@ -23,7 +23,7 @@ def main(args):
start_time = time.process_time() start_time = time.process_time()
# initialize the world # initialize the world
world = World(1, 'vanilla', 'noglitches', 'standard', 'normal', 'none', 'on', 'ganon', 'freshness', False, False, False, args.quickswap, args.fastmenu, args.disablemusic, False, False, False, None, 'none', False) world = World(1, 'vanilla', 'noglitches', 'standard', 'normal', 'none', 'on', 'ganon', 'freshness', False, False, False, False, False, False, None, False)
logger = logging.getLogger('') logger = logging.getLogger('')
hasher = hashlib.md5() hasher = hashlib.md5()
@@ -36,7 +36,7 @@ def main(args):
logger.info('ALttP Plandomizer Version %s - Seed: %s\n\n', __version__, args.plando) logger.info('ALttP Plandomizer Version %s - Seed: %s\n\n', __version__, args.plando)
world.difficulty_requirements = difficulties[world.difficulty] world.difficulty_requirements[1] = difficulties[world.difficulty[1]]
create_regions(world, 1) create_regions(world, 1)
create_dungeons(world, 1) create_dungeons(world, 1)
@@ -68,13 +68,10 @@ def main(args):
logger.info('Patching ROM.') logger.info('Patching ROM.')
if args.sprite is not None:
sprite = Sprite(args.sprite)
else:
sprite = None
rom = LocalRom(args.rom) rom = LocalRom(args.rom)
patch_rom(world, 1, rom, args.heartbeep, args.heartcolor, sprite) patch_rom(world, 1, rom, False)
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, args.sprite, args.ow_palettes, args.uw_palettes)
for textname, texttype, text in text_patches: for textname, texttype, text in text_patches:
if texttype == 'text': if texttype == 'text':
@@ -114,16 +111,16 @@ def fill_world(world, plando, text_patches):
tr_medallion = medallionstr.strip() tr_medallion = medallionstr.strip()
elif line.startswith('!mode'): elif line.startswith('!mode'):
_, modestr = line.split(':', 1) _, modestr = line.split(':', 1)
world.mode = modestr.strip() world.mode = {1: modestr.strip()}
elif line.startswith('!logic'): elif line.startswith('!logic'):
_, logicstr = line.split(':', 1) _, logicstr = line.split(':', 1)
world.logic = logicstr.strip() world.logic = {1: logicstr.strip()}
elif line.startswith('!goal'): elif line.startswith('!goal'):
_, goalstr = line.split(':', 1) _, goalstr = line.split(':', 1)
world.goal = goalstr.strip() world.goal = {1: goalstr.strip()}
elif line.startswith('!light_cone_sewers'): elif line.startswith('!light_cone_sewers'):
_, sewerstr = line.split(':', 1) _, sewerstr = line.split(':', 1)
world.sewer_light_cone = sewerstr.strip().lower() == 'true' world.sewer_light_cone = {1: sewerstr.strip().lower() == 'true'}
elif line.startswith('!light_cone_lw'): elif line.startswith('!light_cone_lw'):
_, lwconestr = line.split(':', 1) _, lwconestr = line.split(':', 1)
world.light_world_light_cone = lwconestr.strip().lower() == 'true' world.light_world_light_cone = lwconestr.strip().lower() == 'true'
@@ -132,19 +129,19 @@ def fill_world(world, plando, text_patches):
world.dark_world_light_cone = dwconestr.strip().lower() == 'true' world.dark_world_light_cone = dwconestr.strip().lower() == 'true'
elif line.startswith('!fix_trock_doors'): elif line.startswith('!fix_trock_doors'):
_, trdstr = line.split(':', 1) _, trdstr = line.split(':', 1)
world.fix_trock_doors = trdstr.strip().lower() == 'true' world.fix_trock_doors = {1: trdstr.strip().lower() == 'true'}
elif line.startswith('!fix_trock_exit'): elif line.startswith('!fix_trock_exit'):
_, trfstr = line.split(':', 1) _, trfstr = line.split(':', 1)
world.fix_trock_exit = trfstr.strip().lower() == 'true' world.fix_trock_exit = {1: trfstr.strip().lower() == 'true'}
elif line.startswith('!fix_gtower_exit'): elif line.startswith('!fix_gtower_exit'):
_, gtfstr = line.split(':', 1) _, gtfstr = line.split(':', 1)
world.fix_gtower_exit = gtfstr.strip().lower() == 'true' world.fix_gtower_exit = gtfstr.strip().lower() == 'true'
elif line.startswith('!fix_pod_exit'): elif line.startswith('!fix_pod_exit'):
_, podestr = line.split(':', 1) _, podestr = line.split(':', 1)
world.fix_palaceofdarkness_exit = podestr.strip().lower() == 'true' world.fix_palaceofdarkness_exit = {1: podestr.strip().lower() == 'true'}
elif line.startswith('!fix_skullwoods_exit'): elif line.startswith('!fix_skullwoods_exit'):
_, swestr = line.split(':', 1) _, swestr = line.split(':', 1)
world.fix_skullwoods_exit = swestr.strip().lower() == 'true' world.fix_skullwoods_exit = {1: swestr.strip().lower() == 'true'}
elif line.startswith('!check_beatable_only'): elif line.startswith('!check_beatable_only'):
_, chkbtstr = line.split(':', 1) _, chkbtstr = line.split(':', 1)
world.check_beatable_only = chkbtstr.strip().lower() == 'true' world.check_beatable_only = chkbtstr.strip().lower() == 'true'
@@ -172,7 +169,7 @@ def fill_world(world, plando, text_patches):
item = ItemFactory(itemstr.strip(), 1) item = ItemFactory(itemstr.strip(), 1)
if item is not None: if item is not None:
world.push_item(location, item) world.push_item(location, item)
if item.key: if item.smallkey or item.bigkey:
location.event = True location.event = True
elif '<=>' in line: elif '<=>' in line:
entrance, exit = line.split('<=>', 1) entrance, exit = line.split('<=>', 1)
@@ -211,6 +208,8 @@ def start():
help='Select the rate at which the heart beep sound is played at low health.') help='Select the rate at which the heart beep sound is played at low health.')
parser.add_argument('--heartcolor', default='red', const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow'], parser.add_argument('--heartcolor', default='red', const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow'],
help='Select the color of Link\'s heart meter. (default: %(default)s)') help='Select the color of Link\'s heart meter. (default: %(default)s)')
parser.add_argument('--ow_palettes', default='default', choices=['default', 'random', 'blackout'])
parser.add_argument('--uw_palettes', default='default', choices=['default', 'random', 'blackout'])
parser.add_argument('--sprite', help='Path to a sprite sheet to use for Link. Needs to be in binary format and have a length of 0x7000 (28672) bytes.') parser.add_argument('--sprite', help='Path to a sprite sheet to use for Link. Needs to be in binary format and have a length of 0x7000 (28672) bytes.')
parser.add_argument('--plando', help='Filled out template to use for setting up the rom.') parser.add_argument('--plando', help='Filled out template to use for setting up the rom.')
args = parser.parse_args() args = parser.parse_args()
@@ -222,8 +221,8 @@ def start():
if not os.path.isfile(args.plando): if not os.path.isfile(args.plando):
input('Could not find Plandomizer distribution at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.plando) input('Could not find Plandomizer distribution at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.plando)
sys.exit(1) sys.exit(1)
if args.sprite is not None and not os.path.isfile(args.rom): if args.sprite is not None and not os.path.isfile(args.sprite) and not get_sprite_from_name(args.sprite):
input('Could not find link sprite sheet at given location. \nPress Enter to exit.' % args.sprite) input('Could not find link sprite sheet at given location. \nPress Enter to exit.')
sys.exit(1) sys.exit(1)
# set up logger # set up logger
+26
View File
@@ -40,6 +40,32 @@ Doors are not shuffled.
Used for development testing. This will be removed in a future version. Use at your own risk. Might play like a plando. Used for development testing. This will be removed in a future version. Use at your own risk. Might play like a plando.
## Map/Compass/Small Key/Big Key shuffle (aka Keysanity)
These settings allow dungeon specific items to be distributed anywhere in the world and not just in their native dungeon.
Small Keys dropped by enemies or found in pots are not affected. The chest in southeast Skull Woods that is traditionally
a guaranteed Small Key still is. These items will be distributed according to the v26/balanced algorithm, but the rest
of the itempool will respect the algorithm setting. Music for dungeons is randomized so it cannot be used as a tell
for which dungeons contain pendants and crystals; finding a Map for a dungeon will allow the overworld map to display its prize.
## Retro
This setting turns all Small Keys into universal Small Keys that can be used in any dungeon and are distributed across the world.
The Bow now consumed rupees to shoot; the cost is 10 rupees per Wood Arrow and 50 per Silver Arrow. Shooting Wood Arrows requires
the purchase of an arrow item from shops, and to account for this and the dynamic use of keys, both Wood Arrows and Small Keys will
be added to several shops around the world. Four "take any" caves are added that allow the player to choose between an extra Heart
Container and a Bottle being filled with Blue Potion, and one of the four swords from the item pool is placed into a special cave as
well. The five caves that are removed for these will be randomly selected single entrance caves that did not contain any items or any shops.
In further concert with the Bow changes, all arrows under pots, in chests, and elsewhere in the seed will be replaced with rupees.
## Seed
Can be used to set a seed number to generate. Using the same seed with same settings on the same version of the entrance randomizer will always yield an identical output.
## Count
Use to batch generate multiple seeds with same settings. If a seed number is provided, it will be used for the first seed, then used to derive the next seed (i.e. generating 10 seeds with the same seed number given will produce the same 10 (different) roms each time).
# Command Line Options # Command Line Options
``` ```
+272 -288
View File
@@ -705,21 +705,8 @@ def create_regions(world, player):
create_dungeon_region(player, 'GT Agahnim 2', 'Ganon\'s Tower', ['Agahnim 2'], ['GT Agahnim 2 SW']) create_dungeon_region(player, 'GT Agahnim 2', 'Ganon\'s Tower', ['Agahnim 2'], ['GT Agahnim 2 SW'])
] ]
for region_name, (room_id, shopkeeper, replaceable) in shop_table.items(): world.initialize_regions()
region = world.get_region(region_name, player)
shop = Shop(region, room_id, ShopType.Shop, shopkeeper, replaceable)
region.shop = shop
world.shops.append(shop)
for index, (item, price) in enumerate(default_shop_contents[region_name]):
shop.add_inventory(index, item, price)
region = world.get_region('Capacity Upgrade', player)
shop = Shop(region, 0x0115, ShopType.UpgradeShop, 0x04, True)
region.shop = shop
world.shops.append(shop)
shop.add_inventory(0, 'Bomb Upgrade (+5)', 100, 7)
shop.add_inventory(1, 'Arrow Upgrade (+5)', 100, 7)
world.intialize_regions()
def create_lw_region(player, name, locations=None, exits=None): def create_lw_region(player, name, locations=None, exits=None):
return _create_region(player, name, RegionType.LightWorld, 'Light World', locations, exits) return _create_region(player, name, RegionType.LightWorld, 'Light World', locations, exits)
@@ -746,14 +733,14 @@ def _create_region(player, name, type, hint='Hyrule', locations=None, exits=None
if location in key_only_locations: if location in key_only_locations:
ret.locations.append(Location(player, location, None, False, None, ret, key_only_locations[location])) ret.locations.append(Location(player, location, None, False, None, ret, key_only_locations[location]))
else: else:
address, crystal, hint_text = location_table[location] address, player_address, crystal, hint_text = location_table[location]
ret.locations.append(Location(player, location, address, crystal, hint_text, ret)) ret.locations.append(Location(player, location, address, crystal, hint_text, ret, None, player_address))
return ret return ret
def mark_light_world_regions(world): def mark_light_world_regions(world, player):
# cross world caves may have some sections marked as both in_light_world, and in_dark_work. # cross world caves may have some sections marked as both in_light_world, and in_dark_work.
# That is ok. the bunny logic will check for this case and incorporate special rules. # That is ok. the bunny logic will check for this case and incorporate special rules.
queue = collections.deque(region for region in world.regions if region.type == RegionType.LightWorld) queue = collections.deque(region for region in world.get_regions(player) if region.type == RegionType.LightWorld)
seen = set(queue) seen = set(queue)
while queue: while queue:
current = queue.popleft() current = queue.popleft()
@@ -766,7 +753,7 @@ def mark_light_world_regions(world):
seen.add(exit.connected_region) seen.add(exit.connected_region)
queue.append(exit.connected_region) queue.append(exit.connected_region)
queue = collections.deque(region for region in world.regions if region.type == RegionType.DarkWorld) queue = collections.deque(region for region in world.get_regions(player) if region.type == RegionType.DarkWorld)
seen = set(queue) seen = set(queue)
while queue: while queue:
current = queue.popleft() current = queue.popleft()
@@ -780,37 +767,35 @@ def mark_light_world_regions(world):
seen.add(exit.connected_region) seen.add(exit.connected_region)
queue.append(exit.connected_region) queue.append(exit.connected_region)
# (room_id, shopkeeper, replaceable)
shop_table = {
'Cave Shop (Dark Death Mountain)': (0x0112, 0xC1, True),
'Red Shield Shop': (0x0110, 0xC1, True),
'Dark Lake Hylia Shop': (0x010F, 0xC1, True),
'Dark World Lumberjack Shop': (0x010F, 0xC1, True),
'Village of Outcasts Shop': (0x010F, 0xC1, True),
'Dark World Potion Shop': (0x010F, 0xC1, True),
'Light World Death Mountain Shop': (0x00FF, 0xA0, True),
'Kakariko Shop': (0x011F, 0xA0, True),
'Cave Shop (Lake Hylia)': (0x0112, 0xA0, True),
'Potion Shop': (0x0109, 0xFF, False),
# Bomb Shop not currently modeled as a shop, due to special nature of items
}
# region, [item]
# slot, item, price, max=0, replacement=None, replacement_price=0
# item = (item, price)
def create_shops(world, player):
for region_name, (room_id, type, shopkeeper, custom, locked, inventory) in shop_table.items():
if world.mode[player] == 'inverted' and region_name == 'Dark Lake Hylia Shop':
locked = True
inventory = [('Blue Potion', 160), ('Blue Shield', 50), ('Bombs (10)', 50)]
region = world.get_region(region_name, player)
shop = Shop(region, room_id, type, shopkeeper, custom, locked)
region.shop = shop
world.shops.append(shop)
for index, item in enumerate(inventory):
shop.add_inventory(index, *item)
# (type, room_id, shopkeeper, custom, locked, [items])
# item = (item, price, max=0, replacement=None, replacement_price=0)
_basic_shop_defaults = [('Red Potion', 150), ('Small Heart', 10), ('Bombs (10)', 50)] _basic_shop_defaults = [('Red Potion', 150), ('Small Heart', 10), ('Bombs (10)', 50)]
_dark_world_shop_defaults = [('Red Potion', 150), ('Blue Shield', 50), ('Bombs (10)', 50)] _dark_world_shop_defaults = [('Red Potion', 150), ('Blue Shield', 50), ('Bombs (10)', 50)]
default_shop_contents = { shop_table = {
'Cave Shop (Dark Death Mountain)': _basic_shop_defaults, 'Cave Shop (Dark Death Mountain)': (0x0112, ShopType.Shop, 0xC1, True, False, _basic_shop_defaults),
'Red Shield Shop': [('Red Shield', 500), ('Bee', 10), ('Arrows (10)', 30)], 'Red Shield Shop': (0x0110, ShopType.Shop, 0xC1, True, False, [('Red Shield', 500), ('Bee', 10), ('Arrows (10)', 30)]),
'Dark Lake Hylia Shop': _dark_world_shop_defaults, 'Dark Lake Hylia Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults),
'Dark World Lumberjack Shop': _dark_world_shop_defaults, 'Dark World Lumberjack Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults),
'Village of Outcasts Shop': _dark_world_shop_defaults, 'Village of Outcasts Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults),
'Dark World Potion Shop': _dark_world_shop_defaults, 'Dark World Potion Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults),
'Light World Death Mountain Shop': _basic_shop_defaults, 'Light World Death Mountain Shop': (0x00FF, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults),
'Kakariko Shop': _basic_shop_defaults, 'Kakariko Shop': (0x011F, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults),
'Cave Shop (Lake Hylia)': _basic_shop_defaults, 'Cave Shop (Lake Hylia)': (0x0112, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults),
'Potion Shop': [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)], 'Potion Shop': (0x0109, ShopType.Shop, 0xFF, False, True, [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)]),
'Capacity Upgrade': (0x0115, ShopType.UpgradeShop, 0x04, True, True, [('Bomb Upgrade (+5)', 100, 7), ('Arrow Upgrade (+5)', 100, 7)])
} }
key_only_locations = { key_only_locations = {
@@ -863,244 +848,243 @@ flooded_keys_reverse = {
'Swamp Palace - Trench 1 Pot Key': 'Trench 1 Switch', 'Swamp Palace - Trench 1 Pot Key': 'Trench 1 Switch',
'Swamp Palace - Trench 2 Pot Key': 'Trench 2 Switch' 'Swamp Palace - Trench 2 Pot Key': 'Trench 2 Switch'
} }
location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'),
location_table = {'Mushroom': (0x180013, False, 'in the woods'), 'Bottle Merchant': (0x2eb18, 0x186339, False, 'with a merchant'),
'Bottle Merchant': (0x2eb18, False, 'with a merchant'), 'Flute Spot': (0x18014a, 0x18633d, False, 'underground'),
'Flute Spot': (0x18014a, False, 'underground'), 'Sunken Treasure': (0x180145, 0x186354, False, 'underwater'),
'Sunken Treasure': (0x180145, False, 'underwater'), 'Purple Chest': (0x33d68, 0x186359, False, 'from a box'),
'Purple Chest': (0x33d68, False, 'from a box'), "Blind's Hideout - Top": (0xeb0f, 0x1862e3, False, 'in a basement'),
"Blind's Hideout - Top": (0xeb0f, False, 'in a basement'), "Blind's Hideout - Left": (0xeb12, 0x1862e6, False, 'in a basement'),
"Blind's Hideout - Left": (0xeb12, False, 'in a basement'), "Blind's Hideout - Right": (0xeb15, 0x1862e9, False, 'in a basement'),
"Blind's Hideout - Right": (0xeb15, False, 'in a basement'), "Blind's Hideout - Far Left": (0xeb18, 0x1862ec, False, 'in a basement'),
"Blind's Hideout - Far Left": (0xeb18, False, 'in a basement'), "Blind's Hideout - Far Right": (0xeb1b, 0x1862ef, False, 'in a basement'),
"Blind's Hideout - Far Right": (0xeb1b, False, 'in a basement'), "Link's Uncle": (0x2df45, 0x18635f, False, 'with your uncle'),
"Link's Uncle": (0x2df45, False, 'with your uncle'), 'Secret Passage': (0xe971, 0x186145, False, 'near your uncle'),
'Secret Passage': (0xe971, False, 'near your uncle'), 'King Zora': (0xee1c3, 0x186360, False, 'at a high price'),
'King Zora': (0xee1c3, False, 'at a high price'), "Zora's Ledge": (0x180149, 0x186358, False, 'near Zora'),
"Zora's Ledge": (0x180149, False, 'near Zora'), 'Waterfall Fairy - Left': (0xe9b0, 0x186184, False, 'near a fairy'),
'Waterfall Fairy - Left': (0xe9b0, False, 'near a fairy'), 'Waterfall Fairy - Right': (0xe9d1, 0x1861a5, False, 'near a fairy'),
'Waterfall Fairy - Right': (0xe9d1, False, 'near a fairy'), "King's Tomb": (0xe97a, 0x18614e, False, 'alone in a cave'),
"King's Tomb": (0xe97a, False, 'alone in a cave'), 'Floodgate Chest': (0xe98c, 0x186160, False, 'in the dam'),
'Floodgate Chest': (0xe98c, False, 'in the dam'), "Link's House": (0xe9bc, 0x186190, False, 'in your home'),
"Link's House": (0xe9bc, False, 'in your home'), 'Kakariko Tavern': (0xe9ce, 0x1861a2, False, 'in the bar'),
'Kakariko Tavern': (0xe9ce, False, 'in the bar'), 'Chicken House': (0xe9e9, 0x1861bd, False, 'near poultry'),
'Chicken House': (0xe9e9, False, 'near poultry'), "Aginah's Cave": (0xe9f2, 0x1861c6, False, 'with Aginah'),
"Aginah's Cave": (0xe9f2, False, 'with Aginah'), "Sahasrahla's Hut - Left": (0xea82, 0x186256, False, 'near the elder'),
"Sahasrahla's Hut - Left": (0xea82, False, 'near the elder'), "Sahasrahla's Hut - Middle": (0xea85, 0x186259, False, 'near the elder'),
"Sahasrahla's Hut - Middle": (0xea85, False, 'near the elder'), "Sahasrahla's Hut - Right": (0xea88, 0x18625c, False, 'near the elder'),
"Sahasrahla's Hut - Right": (0xea88, False, 'near the elder'), 'Sahasrahla': (0x2f1fc, 0x186365, False, 'with the elder'),
'Sahasrahla': (0x2f1fc, False, 'with the elder'), 'Kakariko Well - Top': (0xea8e, 0x186262, False, 'in a well'),
'Kakariko Well - Top': (0xea8e, False, 'in a well'), 'Kakariko Well - Left': (0xea91, 0x186265, False, 'in a well'),
'Kakariko Well - Left': (0xea91, False, 'in a well'), 'Kakariko Well - Middle': (0xea94, 0x186268, False, 'in a well'),
'Kakariko Well - Middle': (0xea94, False, 'in a well'), 'Kakariko Well - Right': (0xea97, 0x18626b, False, 'in a well'),
'Kakariko Well - Right': (0xea97, False, 'in a well'), 'Kakariko Well - Bottom': (0xea9a, 0x18626e, False, 'in a well'),
'Kakariko Well - Bottom': (0xea9a, False, 'in a well'), 'Blacksmith': (0x18002a, 0x186366, False, 'with the smith'),
'Blacksmith': (0x18002a, False, 'with the smith'), 'Magic Bat': (0x180015, 0x18635e, False, 'with the bat'),
'Magic Bat': (0x180015, False, 'with the bat'), 'Sick Kid': (0x339cf, 0x186367, False, 'with the sick'),
'Sick Kid': (0x339cf, False, 'with the sick'), 'Hobo': (0x33e7d, 0x186368, False, 'with the hobo'),
'Hobo': (0x33e7d, False, 'with the hobo'), 'Lost Woods Hideout': (0x180000, 0x186348, False, 'near a thief'),
'Lost Woods Hideout': (0x180000, False, 'near a thief'), 'Lumberjack Tree': (0x180001, 0x186349, False, 'in a hole'),
'Lumberjack Tree': (0x180001, False, 'in a hole'), 'Cave 45': (0x180003, 0x18634b, False, 'alone in a cave'),
'Cave 45': (0x180003, False, 'alone in a cave'), 'Graveyard Cave': (0x180004, 0x18634c, False, 'alone in a cave'),
'Graveyard Cave': (0x180004, False, 'alone in a cave'), 'Checkerboard Cave': (0x180005, 0x18634d, False, 'alone in a cave'),
'Checkerboard Cave': (0x180005, False, 'alone in a cave'), 'Mini Moldorm Cave - Far Left': (0xeb42, 0x186316, False, 'near Moldorms'),
'Mini Moldorm Cave - Far Left': (0xeb42, False, 'near Moldorms'), 'Mini Moldorm Cave - Left': (0xeb45, 0x186319, False, 'near Moldorms'),
'Mini Moldorm Cave - Left': (0xeb45, False, 'near Moldorms'), 'Mini Moldorm Cave - Right': (0xeb48, 0x18631c, False, 'near Moldorms'),
'Mini Moldorm Cave - Right': (0xeb48, False, 'near Moldorms'), 'Mini Moldorm Cave - Far Right': (0xeb4b, 0x18631f, False, 'near Moldorms'),
'Mini Moldorm Cave - Far Right': (0xeb4b, False, 'near Moldorms'), 'Mini Moldorm Cave - Generous Guy': (0x180010, 0x18635a, False, 'near Moldorms'),
'Mini Moldorm Cave - Generous Guy': (0x180010, False, 'near Moldorms'), 'Ice Rod Cave': (0xeb4e, 0x186322, False, 'in a frozen cave'),
'Ice Rod Cave': (0xeb4e, False, 'in a frozen cave'), 'Bonk Rock Cave': (0xeb3f, 0x186313, False, 'alone in a cave'),
'Bonk Rock Cave': (0xeb3f, False, 'alone in a cave'), 'Library': (0x180012, 0x18635c, False, 'near books'),
'Library': (0x180012, False, 'near books'), 'Potion Shop': (0x180014, 0x18635d, False, 'near potions'),
'Potion Shop': (0x180014, False, 'near potions'), 'Lake Hylia Island': (0x180144, 0x186353, False, 'on an island'),
'Lake Hylia Island': (0x180144, False, 'on an island'), 'Maze Race': (0x180142, 0x186351, False, 'at the race'),
'Maze Race': (0x180142, False, 'at the race'), 'Desert Ledge': (0x180143, 0x186352, False, 'in the desert'),
'Desert Ledge': (0x180143, False, 'in the desert'), 'Desert Palace - Big Chest': (0xe98f, 0x186163, False, 'in Desert Palace'),
'Desert Palace - Big Chest': (0xe98f, False, 'in Desert Palace'), 'Desert Palace - Torch': (0x180160, 0x186362, False, 'in Desert Palace'),
'Desert Palace - Torch': (0x180160, False, 'in Desert Palace'), 'Desert Palace - Map Chest': (0xe9b6, 0x18618a, False, 'in Desert Palace'),
'Desert Palace - Map Chest': (0xe9b6, False, 'in Desert Palace'), 'Desert Palace - Compass Chest': (0xe9cb, 0x18619f, False, 'in Desert Palace'),
'Desert Palace - Compass Chest': (0xe9cb, False, 'in Desert Palace'), 'Desert Palace - Big Key Chest': (0xe9c2, 0x186196, False, 'in Desert Palace'),
'Desert Palace - Big Key Chest': (0xe9c2, False, 'in Desert Palace'), 'Desert Palace - Boss': (0x180151, 0x18633f, False, 'with Lanmolas'),
'Desert Palace - Boss': (0x180151, False, 'with Lanmolas'), 'Eastern Palace - Compass Chest': (0xe977, 0x18614b, False, 'in Eastern Palace'),
'Eastern Palace - Compass Chest': (0xe977, False, 'in Eastern Palace'), 'Eastern Palace - Big Chest': (0xe97d, 0x186151, False, 'in Eastern Palace'),
'Eastern Palace - Big Chest': (0xe97d, False, 'in Eastern Palace'), 'Eastern Palace - Cannonball Chest': (0xe9b3, 0x186187, False, 'in Eastern Palace'),
'Eastern Palace - Cannonball Chest': (0xe9b3, False, 'in Eastern Palace'), 'Eastern Palace - Big Key Chest': (0xe9b9, 0x18618d, False, 'in Eastern Palace'),
'Eastern Palace - Big Key Chest': (0xe9b9, False, 'in Eastern Palace'), 'Eastern Palace - Map Chest': (0xe9f5, 0x1861c9, False, 'in Eastern Palace'),
'Eastern Palace - Map Chest': (0xe9f5, False, 'in Eastern Palace'), 'Eastern Palace - Boss': (0x180150, 0x18633e, False, 'with the Armos'),
'Eastern Palace - Boss': (0x180150, False, 'with the Armos'), 'Master Sword Pedestal': (0x289b0, 0x186369, False, 'at the pedestal'),
'Master Sword Pedestal': (0x289b0, False, 'at the pedestal'), 'Hyrule Castle - Boomerang Chest': (0xe974, 0x186148, False, 'in Hyrule Castle'),
'Hyrule Castle - Boomerang Chest': (0xe974, False, 'in Hyrule Castle'), 'Hyrule Castle - Map Chest': (0xeb0c, 0x1862e0, False, 'in Hyrule Castle'),
'Hyrule Castle - Map Chest': (0xeb0c, False, 'in Hyrule Castle'), "Hyrule Castle - Zelda's Chest": (0xeb09, 0x1862dd, False, 'in Hyrule Castle'),
"Hyrule Castle - Zelda's Chest": (0xeb09, False, 'in Hyrule Castle'), 'Sewers - Dark Cross': (0xe96e, 0x186142, False, 'in the sewers'),
'Sewers - Dark Cross': (0xe96e, False, 'in the sewers'), 'Sewers - Secret Room - Left': (0xeb5d, 0x186331, False, 'in the sewers'),
'Sewers - Secret Room - Left': (0xeb5d, False, 'in the sewers'), 'Sewers - Secret Room - Middle': (0xeb60, 0x186334, False, 'in the sewers'),
'Sewers - Secret Room - Middle': (0xeb60, False, 'in the sewers'), 'Sewers - Secret Room - Right': (0xeb63, 0x186337, False, 'in the sewers'),
'Sewers - Secret Room - Right': (0xeb63, False, 'in the sewers'), 'Sanctuary': (0xea79, 0x18624d, False, 'in Sanctuary'),
'Sanctuary': (0xea79, False, 'in Sanctuary'), 'Castle Tower - Room 03': (0xeab5, 0x186289, False, 'in Castle Tower'),
'Castle Tower - Room 03': (0xeab5, False, 'in Castle Tower'), 'Castle Tower - Dark Maze': (0xeab2, 0x186286, False, 'in Castle Tower'),
'Castle Tower - Dark Maze': (0xeab2, False, 'in Castle Tower'), 'Old Man': (0xf69fa, 0x186364, False, 'with the old man'),
'Old Man': (0xf69fa, False, 'with the old man'), 'Spectacle Rock Cave': (0x180002, 0x18634a, False, 'alone in a cave'),
'Spectacle Rock Cave': (0x180002, False, 'alone in a cave'), 'Paradox Cave Lower - Far Left': (0xeb2a, 0x1862fe, False, 'in a cave with seven chests'),
'Paradox Cave Lower - Far Left': (0xeb2a, False, 'in a cave with seven chests'), 'Paradox Cave Lower - Left': (0xeb2d, 0x186301, False, 'in a cave with seven chests'),
'Paradox Cave Lower - Left': (0xeb2d, False, 'in a cave with seven chests'), 'Paradox Cave Lower - Right': (0xeb30, 0x186304, False, 'in a cave with seven chests'),
'Paradox Cave Lower - Right': (0xeb30, False, 'in a cave with seven chests'), 'Paradox Cave Lower - Far Right': (0xeb33, 0x186307, False, 'in a cave with seven chests'),
'Paradox Cave Lower - Far Right': (0xeb33, False, 'in a cave with seven chests'), 'Paradox Cave Lower - Middle': (0xeb36, 0x18630a, False, 'in a cave with seven chests'),
'Paradox Cave Lower - Middle': (0xeb36, False, 'in a cave with seven chests'), 'Paradox Cave Upper - Left': (0xeb39, 0x18630d, False, 'in a cave with seven chests'),
'Paradox Cave Upper - Left': (0xeb39, False, 'in a cave with seven chests'), 'Paradox Cave Upper - Right': (0xeb3c, 0x186310, False, 'in a cave with seven chests'),
'Paradox Cave Upper - Right': (0xeb3c, False, 'in a cave with seven chests'), 'Spiral Cave': (0xe9bf, 0x186193, False, 'in spiral cave'),
'Spiral Cave': (0xe9bf, False, 'in spiral cave'), 'Ether Tablet': (0x180016, 0x18633b, False, 'at a monolith'),
'Ether Tablet': (0x180016, False, 'at a monolith'), 'Spectacle Rock': (0x180140, 0x18634f, False, 'atop a rock'),
'Spectacle Rock': (0x180140, False, 'atop a rock'), 'Tower of Hera - Basement Cage': (0x180162, 0x18633a, False, 'in Tower of Hera'),
'Tower of Hera - Basement Cage': (0x180162, False, 'in Tower of Hera'), 'Tower of Hera - Map Chest': (0xe9ad, 0x186181, False, 'in Tower of Hera'),
'Tower of Hera - Map Chest': (0xe9ad, False, 'in Tower of Hera'), 'Tower of Hera - Big Key Chest': (0xe9e6, 0x1861ba, False, 'in Tower of Hera'),
'Tower of Hera - Big Key Chest': (0xe9e6, False, 'in Tower of Hera'), 'Tower of Hera - Compass Chest': (0xe9fb, 0x1861cf, False, 'in Tower of Hera'),
'Tower of Hera - Compass Chest': (0xe9fb, False, 'in Tower of Hera'), 'Tower of Hera - Big Chest': (0xe9f8, 0x1861cc, False, 'in Tower of Hera'),
'Tower of Hera - Big Chest': (0xe9f8, False, 'in Tower of Hera'), 'Tower of Hera - Boss': (0x180152, 0x186340, False, 'with Moldorm'),
'Tower of Hera - Boss': (0x180152, False, 'with Moldorm'), 'Pyramid': (0x180147, 0x186356, False, 'on the pyramid'),
'Pyramid': (0x180147, False, 'on the pyramid'), 'Catfish': (0xee185, 0x186361, False, 'with a catfish'),
'Catfish': (0xee185, False, 'with a catfish'), 'Stumpy': (0x330c7, 0x18636a, False, 'with tree boy'),
'Stumpy': (0x330c7, False, 'with tree boy'), 'Digging Game': (0x180148, 0x186357, False, 'underground'),
'Digging Game': (0x180148, False, 'underground'), 'Bombos Tablet': (0x180017, 0x18633c, False, 'at a monolith'),
'Bombos Tablet': (0x180017, False, 'at a monolith'), 'Hype Cave - Top': (0xeb1e, 0x1862f2, False, 'near a bat-like man'),
'Hype Cave - Top': (0xeb1e, False, 'near a bat-like man'), 'Hype Cave - Middle Right': (0xeb21, 0x1862f5, False, 'near a bat-like man'),
'Hype Cave - Middle Right': (0xeb21, False, 'near a bat-like man'), 'Hype Cave - Middle Left': (0xeb24, 0x1862f8, False, 'near a bat-like man'),
'Hype Cave - Middle Left': (0xeb24, False, 'near a bat-like man'), 'Hype Cave - Bottom': (0xeb27, 0x1862fb, False, 'near a bat-like man'),
'Hype Cave - Bottom': (0xeb27, False, 'near a bat-like man'), 'Hype Cave - Generous Guy': (0x180011, 0x18635b, False, 'with a bat-like man'),
'Hype Cave - Generous Guy': (0x180011, False, 'with a bat-like man'), 'Peg Cave': (0x180006, 0x18634e, False, 'alone in a cave'),
'Peg Cave': (0x180006, False, 'alone in a cave'), 'Pyramid Fairy - Left': (0xe980, 0x186154, False, 'near a fairy'),
'Pyramid Fairy - Left': (0xe980, False, 'near a fairy'), 'Pyramid Fairy - Right': (0xe983, 0x186157, False, 'near a fairy'),
'Pyramid Fairy - Right': (0xe983, False, 'near a fairy'), 'Brewery': (0xe9ec, 0x1861c0, False, 'alone in a home'),
'Brewery': (0xe9ec, False, 'alone in a home'), 'C-Shaped House': (0xe9ef, 0x1861c3, False, 'alone in a home'),
'C-Shaped House': (0xe9ef, False, 'alone in a home'), 'Chest Game': (0xeda8, 0x18636b, False, 'as a prize'),
'Chest Game': (0xeda8, False, 'as a prize'), 'Bumper Cave Ledge': (0x180146, 0x186355, False, 'on a ledge'),
'Bumper Cave Ledge': (0x180146, False, 'on a ledge'), 'Mire Shed - Left': (0xea73, 0x186247, False, 'near sparks'),
'Mire Shed - Left': (0xea73, False, 'near sparks'), 'Mire Shed - Right': (0xea76, 0x18624a, False, 'near sparks'),
'Mire Shed - Right': (0xea76, False, 'near sparks'), 'Superbunny Cave - Top': (0xea7c, 0x186250, False, 'in a connection'),
'Superbunny Cave - Top': (0xea7c, False, 'in a connection'), 'Superbunny Cave - Bottom': (0xea7f, 0x186253, False, 'in a connection'),
'Superbunny Cave - Bottom': (0xea7f, False, 'in a connection'), 'Spike Cave': (0xea8b, 0x18625f, False, 'beyond spikes'),
'Spike Cave': (0xea8b, False, 'beyond spikes'), 'Hookshot Cave - Top Right': (0xeb51, 0x186325, False, 'across pits'),
'Hookshot Cave - Top Right': (0xeb51, False, 'across pits'), 'Hookshot Cave - Top Left': (0xeb54, 0x186328, False, 'across pits'),
'Hookshot Cave - Top Left': (0xeb54, False, 'across pits'), 'Hookshot Cave - Bottom Right': (0xeb5a, 0x18632e, False, 'across pits'),
'Hookshot Cave - Bottom Right': (0xeb5a, False, 'across pits'), 'Hookshot Cave - Bottom Left': (0xeb57, 0x18632b, False, 'across pits'),
'Hookshot Cave - Bottom Left': (0xeb57, False, 'across pits'), 'Floating Island': (0x180141, 0x186350, False, 'on an island'),
'Floating Island': (0x180141, False, 'on an island'), 'Mimic Cave': (0xe9c5, 0x186199, False, 'in a cave of mimicry'),
'Mimic Cave': (0xe9c5, False, 'in a cave of mimicry'), 'Swamp Palace - Entrance': (0xea9d, 0x186271, False, 'in Swamp Palace'),
'Swamp Palace - Entrance': (0xea9d, False, 'in Swamp Palace'), 'Swamp Palace - Map Chest': (0xe986, 0x18615a, False, 'in Swamp Palace'),
'Swamp Palace - Map Chest': (0xe986, False, 'in Swamp Palace'), 'Swamp Palace - Big Chest': (0xe989, 0x18615d, False, 'in Swamp Palace'),
'Swamp Palace - Big Chest': (0xe989, False, 'in Swamp Palace'), 'Swamp Palace - Compass Chest': (0xeaa0, 0x186274, False, 'in Swamp Palace'),
'Swamp Palace - Compass Chest': (0xeaa0, False, 'in Swamp Palace'), 'Swamp Palace - Big Key Chest': (0xeaa6, 0x18627a, False, 'in Swamp Palace'),
'Swamp Palace - Big Key Chest': (0xeaa6, False, 'in Swamp Palace'), 'Swamp Palace - West Chest': (0xeaa3, 0x186277, False, 'in Swamp Palace'),
'Swamp Palace - West Chest': (0xeaa3, False, 'in Swamp Palace'), 'Swamp Palace - Flooded Room - Left': (0xeaa9, 0x18627d, False, 'in Swamp Palace'),
'Swamp Palace - Flooded Room - Left': (0xeaa9, False, 'in Swamp Palace'), 'Swamp Palace - Flooded Room - Right': (0xeaac, 0x186280, False, 'in Swamp Palace'),
'Swamp Palace - Flooded Room - Right': (0xeaac, False, 'in Swamp Palace'), 'Swamp Palace - Waterfall Room': (0xeaaf, 0x186283, False, 'in Swamp Palace'),
'Swamp Palace - Waterfall Room': (0xeaaf, False, 'in Swamp Palace'), 'Swamp Palace - Boss': (0x180154, 0x186342, False, 'with Arrghus'),
'Swamp Palace - Boss': (0x180154, False, 'with Arrghus'), "Thieves' Town - Big Key Chest": (0xea04, 0x1861d8, False, "in Thieves' Town"),
"Thieves' Town - Big Key Chest": (0xea04, False, "in Thieves' Town"), "Thieves' Town - Map Chest": (0xea01, 0x1861d5, False, "in Thieves' Town"),
"Thieves' Town - Map Chest": (0xea01, False, "in Thieves' Town"), "Thieves' Town - Compass Chest": (0xea07, 0x1861db, False, "in Thieves' Town"),
"Thieves' Town - Compass Chest": (0xea07, False, "in Thieves' Town"), "Thieves' Town - Ambush Chest": (0xea0a, 0x1861de, False, "in Thieves' Town"),
"Thieves' Town - Ambush Chest": (0xea0a, False, "in Thieves' Town"), "Thieves' Town - Attic": (0xea0d, 0x1861e1, False, "in Thieves' Town"),
"Thieves' Town - Attic": (0xea0d, False, "in Thieves' Town"), "Thieves' Town - Big Chest": (0xea10, 0x1861e4, False, "in Thieves' Town"),
"Thieves' Town - Big Chest": (0xea10, False, "in Thieves' Town"), "Thieves' Town - Blind's Cell": (0xea13, 0x1861e7, False, "in Thieves' Town"),
"Thieves' Town - Blind's Cell": (0xea13, False, "in Thieves' Town"), "Thieves' Town - Boss": (0x180156, 0x186344, False, 'with Blind'),
"Thieves' Town - Boss": (0x180156, False, 'with Blind'), 'Skull Woods - Compass Chest': (0xe992, 0x186166, False, 'in Skull Woods'),
'Skull Woods - Compass Chest': (0xe992, False, 'in Skull Woods'), 'Skull Woods - Map Chest': (0xe99b, 0x18616f, False, 'in Skull Woods'),
'Skull Woods - Map Chest': (0xe99b, False, 'in Skull Woods'), 'Skull Woods - Big Chest': (0xe998, 0x18616c, False, 'in Skull Woods'),
'Skull Woods - Big Chest': (0xe998, False, 'in Skull Woods'), 'Skull Woods - Pot Prison': (0xe9a1, 0x186175, False, 'in Skull Woods'),
'Skull Woods - Pot Prison': (0xe9a1, False, 'in Skull Woods'), 'Skull Woods - Pinball Room': (0xe9c8, 0x18619c, False, 'in Skull Woods'),
'Skull Woods - Pinball Room': (0xe9c8, False, 'in Skull Woods'), 'Skull Woods - Big Key Chest': (0xe99e, 0x186172, False, 'in Skull Woods'),
'Skull Woods - Big Key Chest': (0xe99e, False, 'in Skull Woods'), 'Skull Woods - Bridge Room': (0xe9fe, 0x1861d2, False, 'near Mothula'),
'Skull Woods - Bridge Room': (0xe9fe, False, 'near Mothula'), 'Skull Woods - Boss': (0x180155, 0x186343, False, 'with Mothula'),
'Skull Woods - Boss': (0x180155, False, 'with Mothula'), 'Ice Palace - Compass Chest': (0xe9d4, 0x1861a8, False, 'in Ice Palace'),
'Ice Palace - Compass Chest': (0xe9d4, False, 'in Ice Palace'), 'Ice Palace - Freezor Chest': (0xe995, 0x186169, False, 'in Ice Palace'),
'Ice Palace - Freezor Chest': (0xe995, False, 'in Ice Palace'), 'Ice Palace - Big Chest': (0xe9aa, 0x18617e, False, 'in Ice Palace'),
'Ice Palace - Big Chest': (0xe9aa, False, 'in Ice Palace'), 'Ice Palace - Iced T Room': (0xe9e3, 0x1861b7, False, 'in Ice Palace'),
'Ice Palace - Iced T Room': (0xe9e3, False, 'in Ice Palace'), 'Ice Palace - Spike Room': (0xe9e0, 0x1861b4, False, 'in Ice Palace'),
'Ice Palace - Spike Room': (0xe9e0, False, 'in Ice Palace'), 'Ice Palace - Big Key Chest': (0xe9a4, 0x186178, False, 'in Ice Palace'),
'Ice Palace - Big Key Chest': (0xe9a4, False, 'in Ice Palace'), 'Ice Palace - Map Chest': (0xe9dd, 0x1861b1, False, 'in Ice Palace'),
'Ice Palace - Map Chest': (0xe9dd, False, 'in Ice Palace'), 'Ice Palace - Boss': (0x180157, 0x186345, False, 'with Kholdstare'),
'Ice Palace - Boss': (0x180157, False, 'with Kholdstare'), 'Misery Mire - Big Chest': (0xea67, 0x18623b, False, 'in Misery Mire'),
'Misery Mire - Big Chest': (0xea67, False, 'in Misery Mire'), 'Misery Mire - Map Chest': (0xea6a, 0x18623e, False, 'in Misery Mire'),
'Misery Mire - Map Chest': (0xea6a, False, 'in Misery Mire'), 'Misery Mire - Main Lobby': (0xea5e, 0x186232, False, 'in Misery Mire'),
'Misery Mire - Main Lobby': (0xea5e, False, 'in Misery Mire'), 'Misery Mire - Bridge Chest': (0xea61, 0x186235, False, 'in Misery Mire'),
'Misery Mire - Bridge Chest': (0xea61, False, 'in Misery Mire'), 'Misery Mire - Spike Chest': (0xe9da, 0x1861ae, False, 'in Misery Mire'),
'Misery Mire - Spike Chest': (0xe9da, False, 'in Misery Mire'), 'Misery Mire - Compass Chest': (0xea64, 0x186238, False, 'in Misery Mire'),
'Misery Mire - Compass Chest': (0xea64, False, 'in Misery Mire'), 'Misery Mire - Big Key Chest': (0xea6d, 0x186241, False, 'in Misery Mire'),
'Misery Mire - Big Key Chest': (0xea6d, False, 'in Misery Mire'), 'Misery Mire - Boss': (0x180158, 0x186346, False, 'with Vitreous'),
'Misery Mire - Boss': (0x180158, False, 'with Vitreous'), 'Turtle Rock - Compass Chest': (0xea22, 0x1861f6, False, 'in Turtle Rock'),
'Turtle Rock - Compass Chest': (0xea22, False, 'in Turtle Rock'), 'Turtle Rock - Roller Room - Left': (0xea1c, 0x1861f0, False, 'in Turtle Rock'),
'Turtle Rock - Roller Room - Left': (0xea1c, False, 'in Turtle Rock'), 'Turtle Rock - Roller Room - Right': (0xea1f, 0x1861f3, False, 'in Turtle Rock'),
'Turtle Rock - Roller Room - Right': (0xea1f, False, 'in Turtle Rock'), 'Turtle Rock - Chain Chomps': (0xea16, 0x1861ea, False, 'in Turtle Rock'),
'Turtle Rock - Chain Chomps': (0xea16, False, 'in Turtle Rock'), 'Turtle Rock - Big Key Chest': (0xea25, 0x1861f9, False, 'in Turtle Rock'),
'Turtle Rock - Big Key Chest': (0xea25, False, 'in Turtle Rock'), 'Turtle Rock - Big Chest': (0xea19, 0x1861ed, False, 'in Turtle Rock'),
'Turtle Rock - Big Chest': (0xea19, False, 'in Turtle Rock'), 'Turtle Rock - Crystaroller Room': (0xea34, 0x186208, False, 'in Turtle Rock'),
'Turtle Rock - Crystaroller Room': (0xea34, False, 'in Turtle Rock'), 'Turtle Rock - Eye Bridge - Bottom Left': (0xea31, 0x186205, False, 'in Turtle Rock'),
'Turtle Rock - Eye Bridge - Bottom Left': (0xea31, False, 'in Turtle Rock'), 'Turtle Rock - Eye Bridge - Bottom Right': (0xea2e, 0x186202, False, 'in Turtle Rock'),
'Turtle Rock - Eye Bridge - Bottom Right': (0xea2e, False, 'in Turtle Rock'), 'Turtle Rock - Eye Bridge - Top Left': (0xea2b, 0x1861ff, False, 'in Turtle Rock'),
'Turtle Rock - Eye Bridge - Top Left': (0xea2b, False, 'in Turtle Rock'), 'Turtle Rock - Eye Bridge - Top Right': (0xea28, 0x1861fc, False, 'in Turtle Rock'),
'Turtle Rock - Eye Bridge - Top Right': (0xea28, False, 'in Turtle Rock'), 'Turtle Rock - Boss': (0x180159, 0x186347, False, 'with Trinexx'),
'Turtle Rock - Boss': (0x180159, False, 'with Trinexx'), 'Palace of Darkness - Shooter Room': (0xea5b, 0x18622f, False, 'in Palace of Darkness'),
'Palace of Darkness - Shooter Room': (0xea5b, False, 'in Palace of Darkness'), 'Palace of Darkness - The Arena - Bridge': (0xea3d, 0x186211, False, 'in Palace of Darkness'),
'Palace of Darkness - The Arena - Bridge': (0xea3d, False, 'in Palace of Darkness'), 'Palace of Darkness - Stalfos Basement': (0xea49, 0x18621d, False, 'in Palace of Darkness'),
'Palace of Darkness - Stalfos Basement': (0xea49, False, 'in Palace of Darkness'), 'Palace of Darkness - Big Key Chest': (0xea37, 0x18620b, False, 'in Palace of Darkness'),
'Palace of Darkness - Big Key Chest': (0xea37, False, 'in Palace of Darkness'), 'Palace of Darkness - The Arena - Ledge': (0xea3a, 0x18620e, False, 'in Palace of Darkness'),
'Palace of Darkness - The Arena - Ledge': (0xea3a, False, 'in Palace of Darkness'), 'Palace of Darkness - Map Chest': (0xea52, 0x186226, False, 'in Palace of Darkness'),
'Palace of Darkness - Map Chest': (0xea52, False, 'in Palace of Darkness'), 'Palace of Darkness - Compass Chest': (0xea43, 0x186217, False, 'in Palace of Darkness'),
'Palace of Darkness - Compass Chest': (0xea43, False, 'in Palace of Darkness'), 'Palace of Darkness - Dark Basement - Left': (0xea4c, 0x186220, False, 'in Palace of Darkness'),
'Palace of Darkness - Dark Basement - Left': (0xea4c, False, 'in Palace of Darkness'), 'Palace of Darkness - Dark Basement - Right': (0xea4f, 0x186223, False, 'in Palace of Darkness'),
'Palace of Darkness - Dark Basement - Right': (0xea4f, False, 'in Palace of Darkness'), 'Palace of Darkness - Dark Maze - Top': (0xea55, 0x186229, False, 'in Palace of Darkness'),
'Palace of Darkness - Dark Maze - Top': (0xea55, False, 'in Palace of Darkness'), 'Palace of Darkness - Dark Maze - Bottom': (0xea58, 0x18622c, False, 'in Palace of Darkness'),
'Palace of Darkness - Dark Maze - Bottom': (0xea58, False, 'in Palace of Darkness'), 'Palace of Darkness - Big Chest': (0xea40, 0x186214, False, 'in Palace of Darkness'),
'Palace of Darkness - Big Chest': (0xea40, False, 'in Palace of Darkness'), 'Palace of Darkness - Harmless Hellway': (0xea46, 0x18621a, False, 'in Palace of Darkness'),
'Palace of Darkness - Harmless Hellway': (0xea46, False, 'in Palace of Darkness'), 'Palace of Darkness - Boss': (0x180153, 0x186341, False, 'with Helmasaur King'),
'Palace of Darkness - Boss': (0x180153, False, 'with Helmasaur King'), "Ganons Tower - Bob's Torch": (0x180161, 0x186363, False, "in Ganon's Tower"),
"Ganons Tower - Bob's Torch": (0x180161, False, "in Ganon's Tower"), 'Ganons Tower - Hope Room - Left': (0xead9, 0x1862ad, False, "in Ganon's Tower"),
'Ganons Tower - Hope Room - Left': (0xead9, False, "in Ganon's Tower"), 'Ganons Tower - Hope Room - Right': (0xeadc, 0x1862b0, False, "in Ganon's Tower"),
'Ganons Tower - Hope Room - Right': (0xeadc, False, "in Ganon's Tower"), 'Ganons Tower - Tile Room': (0xeae2, 0x1862b6, False, "in Ganon's Tower"),
'Ganons Tower - Tile Room': (0xeae2, False, "in Ganon's Tower"), 'Ganons Tower - Compass Room - Top Left': (0xeae5, 0x1862b9, False, "in Ganon's Tower"),
'Ganons Tower - Compass Room - Top Left': (0xeae5, False, "in Ganon's Tower"), 'Ganons Tower - Compass Room - Top Right': (0xeae8, 0x1862bc, False, "in Ganon's Tower"),
'Ganons Tower - Compass Room - Top Right': (0xeae8, False, "in Ganon's Tower"), 'Ganons Tower - Compass Room - Bottom Left': (0xeaeb, 0x1862bf, False, "in Ganon's Tower"),
'Ganons Tower - Compass Room - Bottom Left': (0xeaeb, False, "in Ganon's Tower"), 'Ganons Tower - Compass Room - Bottom Right': (0xeaee, 0x1862c2, False, "in Ganon's Tower"),
'Ganons Tower - Compass Room - Bottom Right': (0xeaee, False, "in Ganon's Tower"), 'Ganons Tower - DMs Room - Top Left': (0xeab8, 0x18628c, False, "in Ganon's Tower"),
'Ganons Tower - DMs Room - Top Left': (0xeab8, False, "in Ganon's Tower"), 'Ganons Tower - DMs Room - Top Right': (0xeabb, 0x18628f, False, "in Ganon's Tower"),
'Ganons Tower - DMs Room - Top Right': (0xeabb, False, "in Ganon's Tower"), 'Ganons Tower - DMs Room - Bottom Left': (0xeabe, 0x186292, False, "in Ganon's Tower"),
'Ganons Tower - DMs Room - Bottom Left': (0xeabe, False, "in Ganon's Tower"), 'Ganons Tower - DMs Room - Bottom Right': (0xeac1, 0x186295, False, "in Ganon's Tower"),
'Ganons Tower - DMs Room - Bottom Right': (0xeac1, False, "in Ganon's Tower"), 'Ganons Tower - Map Chest': (0xead3, 0x1862a7, False, "in Ganon's Tower"),
'Ganons Tower - Map Chest': (0xead3, False, "in Ganon's Tower"), 'Ganons Tower - Firesnake Room': (0xead0, 0x1862a4, False, "in Ganon's Tower"),
'Ganons Tower - Firesnake Room': (0xead0, False, "in Ganon's Tower"), 'Ganons Tower - Randomizer Room - Top Left': (0xeac4, 0x186298, False, "in Ganon's Tower"),
'Ganons Tower - Randomizer Room - Top Left': (0xeac4, False, "in Ganon's Tower"), 'Ganons Tower - Randomizer Room - Top Right': (0xeac7, 0x18629b, False, "in Ganon's Tower"),
'Ganons Tower - Randomizer Room - Top Right': (0xeac7, False, "in Ganon's Tower"), 'Ganons Tower - Randomizer Room - Bottom Left': (0xeaca, 0x18629e, False, "in Ganon's Tower"),
'Ganons Tower - Randomizer Room - Bottom Left': (0xeaca, False, "in Ganon's Tower"), 'Ganons Tower - Randomizer Room - Bottom Right': (0xeacd, 0x1862a1, False, "in Ganon's Tower"),
'Ganons Tower - Randomizer Room - Bottom Right': (0xeacd, False, "in Ganon's Tower"), "Ganons Tower - Bob's Chest": (0xeadf, 0x1862b3, False, "in Ganon's Tower"),
"Ganons Tower - Bob's Chest": (0xeadf, False, "in Ganon's Tower"), 'Ganons Tower - Big Chest': (0xead6, 0x1862aa, False, "in Ganon's Tower"),
'Ganons Tower - Big Chest': (0xead6, False, "in Ganon's Tower"), 'Ganons Tower - Big Key Room - Left': (0xeaf4, 0x1862c8, False, "in Ganon's Tower"),
'Ganons Tower - Big Key Room - Left': (0xeaf4, False, "in Ganon's Tower"), 'Ganons Tower - Big Key Room - Right': (0xeaf7, 0x1862cb, False, "in Ganon's Tower"),
'Ganons Tower - Big Key Room - Right': (0xeaf7, False, "in Ganon's Tower"), 'Ganons Tower - Big Key Chest': (0xeaf1, 0x1862c5, False, "in Ganon's Tower"),
'Ganons Tower - Big Key Chest': (0xeaf1, False, "in Ganon's Tower"), 'Ganons Tower - Mini Helmasaur Room - Left': (0xeafd, 0x1862d1, False, "atop Ganon's Tower"),
'Ganons Tower - Mini Helmasaur Room - Left': (0xeafd, False, "atop Ganon's Tower"), 'Ganons Tower - Mini Helmasaur Room - Right': (0xeb00, 0x1862d4, False, "atop Ganon's Tower"),
'Ganons Tower - Mini Helmasaur Room - Right': (0xeb00, False, "atop Ganon's Tower"), 'Ganons Tower - Pre-Moldorm Chest': (0xeb03, 0x1862d7, False, "atop Ganon's Tower"),
'Ganons Tower - Pre-Moldorm Chest': (0xeb03, False, "atop Ganon's Tower"), 'Ganons Tower - Validation Chest': (0xeb06, 0x1862da, False, "atop Ganon's Tower"),
'Ganons Tower - Validation Chest': (0xeb06, False, "atop Ganon's Tower"), 'Ganon': (None, None, False, 'from me'),
'Ganon': (None, False, 'from me'), 'Agahnim 1': (None, None, False, 'from Ganon\'s wizardry form'),
'Agahnim 1': (None, False, 'from Ganon\'s wizardry form'), 'Agahnim 2': (None, None, False, 'from Ganon\'s wizardry form'),
'Agahnim 2': (None, False, 'from Ganon\'s wizardry form'), 'Floodgate': (None, None, False, None),
'Floodgate': (None, False, None), 'Frog': (None, None, False, None),
'Frog': (None, False, None), 'Missing Smith': (None, None, False, None),
'Missing Smith': (None, False, None), 'Dark Blacksmith Ruins': (None, None, False, None),
'Dark Blacksmith Ruins': (None, False, None), 'Trench 1 Switch': (None, None, False, None),
'Trench 1 Switch': (None, False, None), 'Trench 2 Switch': (None, None, False, None),
'Trench 2 Switch': (None, False, None), 'Swamp Drain': (None, None, False, None),
'Swamp Drain': (None, False, None), 'Attic Cracked Floor': (None, None, False, None),
'Attic Cracked Floor': (None, False, None), 'Suspicious Maiden': (None, None, False, None),
'Suspicious Maiden': (None, False, None), 'Revealing Light': (None, None, False, None),
'Revealing Light': (None, False, None), 'Ice Block Drop': (None, None, False, None),
'Ice Block Drop': (None, False, None), 'Eastern Palace - Prize': ([0x1209D, 0x53EF8, 0x53EF9, 0x180052, 0x18007C, 0xC6FE], None, True, 'Eastern Palace'),
'Eastern Palace - Prize': ([0x1209D, 0x53EF8, 0x53EF9, 0x180052, 0x18007C, 0xC6FE], True, 'Eastern Palace'), 'Desert Palace - Prize': ([0x1209E, 0x53F1C, 0x53F1D, 0x180053, 0x180078, 0xC6FF], None, True, 'Desert Palace'),
'Desert Palace - Prize': ([0x1209E, 0x53F1C, 0x53F1D, 0x180053, 0x180078, 0xC6FF], True, 'Desert Palace'), 'Tower of Hera - Prize': ([0x120A5, 0x53F0A, 0x53F0B, 0x18005A, 0x18007A, 0xC706], None, True, 'Tower of Hera'),
'Tower of Hera - Prize': ([0x120A5, 0x53F0A, 0x53F0B, 0x18005A, 0x18007A, 0xC706], True, 'Tower of Hera'), 'Palace of Darkness - Prize': ([0x120A1, 0x53F00, 0x53F01, 0x180056, 0x18007D, 0xC702], None, True, 'Palace of Darkness'),
'Palace of Darkness - Prize': ([0x120A1, 0x53F00, 0x53F01, 0x180056, 0x18007D, 0xC702], True, 'Palace of Darkness'), 'Swamp Palace - Prize': ([0x120A0, 0x53F6C, 0x53F6D, 0x180055, 0x180071, 0xC701], None, True, 'Swamp Palace'),
'Swamp Palace - Prize': ([0x120A0, 0x53F6C, 0x53F6D, 0x180055, 0x180071, 0xC701], True, 'Swamp Palace'), 'Thieves\' Town - Prize': ([0x120A6, 0x53F36, 0x53F37, 0x18005B, 0x180077, 0xC707], None, True, 'Thieves\' Town'),
'Thieves\' Town - Prize': ([0x120A6, 0x53F36, 0x53F37, 0x18005B, 0x180077, 0xC707], True, 'Thieves\' Town'), 'Skull Woods - Prize': ([0x120A3, 0x53F12, 0x53F13, 0x180058, 0x18007B, 0xC704], None, True, 'Skull Woods'),
'Skull Woods - Prize': ([0x120A3, 0x53F12, 0x53F13, 0x180058, 0x18007B, 0xC704], True, 'Skull Woods'), 'Ice Palace - Prize': ([0x120A4, 0x53F5A, 0x53F5B, 0x180059, 0x180073, 0xC705], None, True, 'Ice Palace'),
'Ice Palace - Prize': ([0x120A4, 0x53F5A, 0x53F5B, 0x180059, 0x180073, 0xC705], True, 'Ice Palace'), 'Misery Mire - Prize': ([0x120A2, 0x53F48, 0x53F49, 0x180057, 0x180075, 0xC703], None, True, 'Misery Mire'),
'Misery Mire - Prize': ([0x120A2, 0x53F48, 0x53F49, 0x180057, 0x180075, 0xC703], True, 'Misery Mire'), 'Turtle Rock - Prize': ([0x120A7, 0x53F24, 0x53F25, 0x18005C, 0x180079, 0xC708], None, True, 'Turtle Rock')}
'Turtle Rock - Prize': ([0x120A7, 0x53F24, 0x53F25, 0x18005C, 0x180079, 0xC708], True, 'Turtle Rock')}
+864 -421
View File
File diff suppressed because it is too large Load Diff
+249 -477
View File
File diff suppressed because it is too large Load Diff
+7 -1
View File
@@ -1,8 +1,12 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os import os
import re
import subprocess import subprocess
import sys import sys
def parse_names_string(names):
return {player: name for player, name in enumerate([n for n in re.split(r'[, ]', names) if n], 1)}
def int16_as_bytes(value): def int16_as_bytes(value):
value = value & 0xFFFF value = value & 0xFFFF
return [value & 0xFF, (value >> 8) & 0xFF] return [value & 0xFF, (value >> 8) & 0xFF]
@@ -224,4 +228,6 @@ def print_wiki_doors(d_regions, world, player):
if __name__ == '__main__': if __name__ == '__main__':
read_entrance_data(old_rom='C:\\Users\\Randall\\Documents\\kwyn\\orig\\z3.sfc') pass
# make_new_base2current()
# read_entrance_data(old_rom='')
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.