Merge remote-tracking branch 'remotes/multi/multiworld_31' into Dev
This commit is contained in:
@@ -13,3 +13,4 @@ README.html
|
||||
*multisave
|
||||
EnemizerCLI/
|
||||
.mypy_cache/
|
||||
RaceRom.py
|
||||
@@ -38,6 +38,7 @@ def main():
|
||||
Alternatively, can be a ALttP Rom patched with a Link
|
||||
sprite that will be extracted.
|
||||
''')
|
||||
parser.add_argument('--names', default='', type=str)
|
||||
args = parser.parse_args()
|
||||
|
||||
# ToDo: Validate files further than mere existance
|
||||
|
||||
+3
-2
@@ -1,8 +1,9 @@
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import logging
|
||||
|
||||
from Utils import output_path
|
||||
from Utils import output_path, parse_names_string
|
||||
from Rom import LocalRom, Sprite, apply_rom_settings
|
||||
|
||||
|
||||
@@ -26,7 +27,7 @@ def adjust(args):
|
||||
else:
|
||||
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, sprite, parse_names_string(args.names))
|
||||
|
||||
rom.write_to_file(output_path('%s.sfc' % outfilebase))
|
||||
|
||||
|
||||
+143
-88
@@ -4,21 +4,22 @@ import logging
|
||||
import json
|
||||
from collections import OrderedDict
|
||||
from _vendor.collections_extended import bag
|
||||
from EntranceShuffle import door_addresses
|
||||
from Utils import int16_as_bytes
|
||||
|
||||
class World(object):
|
||||
|
||||
def __init__(self, players, shuffle, 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, logic, mode, swords, difficulty, difficulty_adjustments, timer, progressive, goal, algorithm, accessibility, shuffle_ganon, quickswap, fastmenu, disable_music, retro, custom, customitemarray, hints):
|
||||
self.players = players
|
||||
self.shuffle = shuffle
|
||||
self.logic = logic
|
||||
self.mode = mode
|
||||
self.swords = swords
|
||||
self.difficulty = difficulty
|
||||
self.difficulty_adjustments = difficulty_adjustments
|
||||
self.shuffle = shuffle.copy()
|
||||
self.logic = logic.copy()
|
||||
self.mode = mode.copy()
|
||||
self.swords = swords.copy()
|
||||
self.difficulty = difficulty.copy()
|
||||
self.difficulty_adjustments = difficulty_adjustments.copy()
|
||||
self.timer = timer
|
||||
self.progressive = progressive
|
||||
self.goal = goal
|
||||
self.goal = goal.copy()
|
||||
self.algorithm = algorithm
|
||||
self.dungeons = []
|
||||
self.regions = []
|
||||
@@ -27,73 +28,88 @@ class World(object):
|
||||
self.seed = None
|
||||
self.precollected_items = []
|
||||
self.state = CollectionState(self)
|
||||
self.required_medallions = dict([(player, ['Ether', 'Quake']) for player in range(1, players + 1)])
|
||||
self._cached_entrances = None
|
||||
self._cached_locations = None
|
||||
self._entrance_cache = {}
|
||||
self._region_cache = {}
|
||||
self._entrance_cache = {}
|
||||
self._location_cache = {}
|
||||
self.required_locations = []
|
||||
self.place_dungeon_items = place_dungeon_items # configurable in future
|
||||
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.dark_world_light_cone = False
|
||||
self.treasure_hunt_count = 0
|
||||
self.treasure_hunt_icon = 'Triforce Piece'
|
||||
self.clock_mode = 'off'
|
||||
self.rupoor_cost = 10
|
||||
self.aga_randomness = True
|
||||
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.accessibility = accessibility
|
||||
self.fix_skullwoods_exit = self.shuffle not in ['vanilla', 'simple', 'restricted', 'dungeonssimple']
|
||||
self.fix_palaceofdarkness_exit = self.shuffle not in ['vanilla', 'simple', 'restricted', 'dungeonssimple']
|
||||
self.fix_trock_exit = self.shuffle not in ['vanilla', 'simple', 'restricted', 'dungeonssimple']
|
||||
self.accessibility = accessibility.copy()
|
||||
self.shuffle_ganon = shuffle_ganon
|
||||
self.fix_gtower_exit = self.shuffle_ganon
|
||||
self.can_access_trock_eyebridge = None
|
||||
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.retro = retro.copy()
|
||||
self.custom = custom
|
||||
self.customitemarray = customitemarray
|
||||
self.can_take_damage = True
|
||||
self.difficulty_requirements = None
|
||||
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.hints = hints.copy()
|
||||
self.dynamic_regions = []
|
||||
self.dynamic_locations = []
|
||||
self.spoiler = Spoiler(self)
|
||||
self.lamps_needed_for_dark_rooms = 1
|
||||
|
||||
def intialize_regions(self):
|
||||
for region in self.regions:
|
||||
for player in range(1, players + 1):
|
||||
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
|
||||
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):
|
||||
if isinstance(regionname, Region):
|
||||
return regionname
|
||||
try:
|
||||
return self._region_cache[(regionname, player)]
|
||||
return self._region_cache[player][regionname]
|
||||
except KeyError:
|
||||
for region in self.regions:
|
||||
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
|
||||
raise RuntimeError('No such region %s for player %d' % (regionname, player))
|
||||
|
||||
@@ -140,13 +156,13 @@ class World(object):
|
||||
if 'Sword' in item.name:
|
||||
if ret.has('Golden Sword', item.player):
|
||||
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))
|
||||
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))
|
||||
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))
|
||||
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))
|
||||
elif 'Glove' in item.name:
|
||||
if ret.has('Titans Mitts', item.player):
|
||||
@@ -158,23 +174,23 @@ class World(object):
|
||||
elif 'Shield' in item.name:
|
||||
if ret.has('Mirror Shield', item.player):
|
||||
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))
|
||||
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))
|
||||
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))
|
||||
elif 'Bow' in item.name:
|
||||
if ret.has('Silver Arrows', item.player):
|
||||
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))
|
||||
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))
|
||||
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))
|
||||
elif item.advancement or item.key:
|
||||
elif item.advancement or item.smallkey or item.bigkey:
|
||||
ret.prog_items.add((item.name, item.player))
|
||||
|
||||
for item in self.itempool:
|
||||
@@ -312,14 +328,14 @@ class CollectionState(object):
|
||||
self.collect(item, True)
|
||||
|
||||
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
|
||||
rrp = self.reachable_regions[player]
|
||||
new_regions = True
|
||||
reachable_regions_count = len(rrp)
|
||||
while new_regions:
|
||||
possible = [region for region in player_regions if region not in rrp]
|
||||
for candidate in possible:
|
||||
player_regions = [region for region in player_regions if region not in rrp]
|
||||
for candidate in player_regions:
|
||||
if candidate.can_reach_private(self):
|
||||
rrp.add(candidate)
|
||||
new_regions = len(rrp) > reachable_regions_count
|
||||
@@ -351,12 +367,14 @@ class CollectionState(object):
|
||||
|
||||
def sweep_for_events(self, key_only=False, locations=None):
|
||||
# this may need improvement
|
||||
if locations is None:
|
||||
locations = self.world.get_filled_locations()
|
||||
new_locations = True
|
||||
checked_locations = 0
|
||||
while new_locations:
|
||||
if locations is None:
|
||||
locations = self.world.get_filled_locations()
|
||||
reachable_events = [location for location in locations if location.event and (not key_only or location.item.key) and location.can_reach(self)]
|
||||
reachable_events = [location for location in locations if location.event and
|
||||
(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))
|
||||
and location.can_reach(self)]
|
||||
for event in reachable_events:
|
||||
if (event.name, event.player) not in self.events:
|
||||
self.events.append((event.name, event.player))
|
||||
@@ -370,7 +388,7 @@ class CollectionState(object):
|
||||
return self.prog_items.count((item, player)) >= count
|
||||
|
||||
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)
|
||||
if count == 1:
|
||||
return (item, player) in self.prog_items
|
||||
@@ -404,7 +422,7 @@ class CollectionState(object):
|
||||
|
||||
def heart_count(self, player):
|
||||
# Warning: This only considers items that are marked as advancement items
|
||||
diff = self.world.difficulty_requirements
|
||||
diff = self.world.difficulty_requirements[player]
|
||||
return (
|
||||
min(self.item_count('Boss Heart Container', player), diff.boss_heart_container_limit)
|
||||
+ self.item_count('Sanctuary Heart Container', player)
|
||||
@@ -422,9 +440,9 @@ class CollectionState(object):
|
||||
elif self.has('Half Magic', player):
|
||||
basemagic = 16
|
||||
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))
|
||||
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))
|
||||
else:
|
||||
basemagic = basemagic + basemagic * self.bottle_count(player)
|
||||
@@ -439,7 +457,7 @@ class CollectionState(object):
|
||||
)
|
||||
|
||||
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)
|
||||
#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))
|
||||
@@ -490,7 +508,7 @@ class CollectionState(object):
|
||||
if self.has_Pearl(player):
|
||||
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):
|
||||
if True in [i.is_light_world for i in self.reachable_regions[player]]:
|
||||
@@ -516,16 +534,16 @@ class CollectionState(object):
|
||||
if 'Sword' in item.name:
|
||||
if self.has('Golden Sword', item.player):
|
||||
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))
|
||||
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))
|
||||
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))
|
||||
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))
|
||||
changed = True
|
||||
elif 'Glove' in item.name:
|
||||
@@ -540,13 +558,13 @@ class CollectionState(object):
|
||||
elif 'Shield' in item.name:
|
||||
if self.has('Mirror Shield', item.player):
|
||||
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))
|
||||
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))
|
||||
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))
|
||||
changed = True
|
||||
elif 'Bow' in item.name:
|
||||
@@ -559,7 +577,7 @@ class CollectionState(object):
|
||||
self.prog_items.add(('Bow', item.player))
|
||||
changed = True
|
||||
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))
|
||||
changed = True
|
||||
elif event or item.advancement:
|
||||
@@ -626,6 +644,8 @@ class CollectionState(object):
|
||||
return self.can_reach(item[10])
|
||||
#elif item.startswith('has_'):
|
||||
# return self.has(item[4])
|
||||
if item == '__len__':
|
||||
return
|
||||
|
||||
raise RuntimeError('Cannot parse %s.' % item)
|
||||
|
||||
@@ -674,9 +694,12 @@ class Region(object):
|
||||
return False
|
||||
|
||||
def can_fill(self, item):
|
||||
is_dungeon_item = item.key or item.map or item.compass
|
||||
sewer_hack = self.world.mode == 'standard' and item.name == 'Small Key (Escape)'
|
||||
if sewer_hack or (is_dungeon_item and not self.world.keysanity):
|
||||
inside_dungeon_item = ((item.smallkey and not self.world.keyshuffle[item.player])
|
||||
or (item.bigkey and not self.world.bigkeyshuffle[item.player])
|
||||
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 True
|
||||
@@ -781,18 +804,19 @@ class Boss(object):
|
||||
return self.defeat_rule(state, self.player)
|
||||
|
||||
class Location(object):
|
||||
def __init__(self, player, name='', address=None, crystal=False, hint_text=None, parent=None):
|
||||
def __init__(self, player, name='', address=None, crystal=False, hint_text=None, parent=None, player_address=None):
|
||||
self.name = name
|
||||
self.parent_region = parent
|
||||
self.item = None
|
||||
self.crystal = crystal
|
||||
self.address = address
|
||||
self.player_address = player_address
|
||||
self.spot_type = 'Location'
|
||||
self.hint_text = hint_text if hint_text is not None else 'Hyrule'
|
||||
self.recursion_count = 0
|
||||
self.staleness_count = 0
|
||||
self.event = False
|
||||
self.locked = True
|
||||
self.locked = False
|
||||
self.always_allow = lambda item, state: False
|
||||
self.access_rule = lambda state: True
|
||||
self.item_rule = lambda item: True
|
||||
@@ -834,14 +858,18 @@ class Item(object):
|
||||
self.location = None
|
||||
self.player = player
|
||||
|
||||
@property
|
||||
def key(self):
|
||||
return self.type == 'SmallKey' or self.type == 'BigKey'
|
||||
|
||||
@property
|
||||
def crystal(self):
|
||||
return self.type == 'Crystal'
|
||||
|
||||
@property
|
||||
def smallkey(self):
|
||||
return self.type == 'SmallKey'
|
||||
|
||||
@property
|
||||
def bigkey(self):
|
||||
return self.type == 'BigKey'
|
||||
|
||||
@property
|
||||
def map(self):
|
||||
return self.type == 'Map'
|
||||
@@ -933,6 +961,7 @@ class Spoiler(object):
|
||||
self.entrances = OrderedDict()
|
||||
self.medallions = {}
|
||||
self.playthrough = {}
|
||||
self.unreachables = []
|
||||
self.locations = {}
|
||||
self.paths = {}
|
||||
self.metadata = {}
|
||||
@@ -1006,7 +1035,7 @@ class Spoiler(object):
|
||||
self.bosses[str(player)]["Ice Palace"] = self.world.get_dungeon("Ice Palace", player).boss.name
|
||||
self.bosses[str(player)]["Misery Mire"] = self.world.get_dungeon("Misery Mire", player).boss.name
|
||||
self.bosses[str(player)]["Turtle Rock"] = self.world.get_dungeon("Turtle Rock", player).boss.name
|
||||
if self.world.mode != 'inverted':
|
||||
if self.world.mode[player] != 'inverted':
|
||||
self.bosses[str(player)]["Ganons Tower Basement"] = self.world.get_dungeon('Ganons Tower', player).bosses['bottom'].name
|
||||
self.bosses[str(player)]["Ganons Tower Middle"] = self.world.get_dungeon('Ganons Tower', player).bosses['middle'].name
|
||||
self.bosses[str(player)]["Ganons Tower Top"] = self.world.get_dungeon('Ganons Tower', player).bosses['top'].name
|
||||
@@ -1025,14 +1054,26 @@ class Spoiler(object):
|
||||
self.metadata = {'version': ERVersion,
|
||||
'logic': self.world.logic,
|
||||
'mode': self.world.mode,
|
||||
'retro': self.world.retro,
|
||||
'weapons': self.world.swords,
|
||||
'goal': self.world.goal,
|
||||
'shuffle': self.world.shuffle,
|
||||
'item_pool': self.world.difficulty,
|
||||
'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,
|
||||
'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):
|
||||
@@ -1045,7 +1086,6 @@ class Spoiler(object):
|
||||
out['Shops'] = self.shops
|
||||
out['playthrough'] = self.playthrough
|
||||
out['paths'] = self.paths
|
||||
if self.world.boss_shuffle != 'none':
|
||||
out['Bosses'] = self.bosses
|
||||
out['meta'] = self.metadata
|
||||
|
||||
@@ -1055,19 +1095,31 @@ class Spoiler(object):
|
||||
self.parse_data()
|
||||
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('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('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('Difficulty: %s\n' % self.metadata['item_pool'])
|
||||
outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'])
|
||||
outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'])
|
||||
outfile.write('Filling Algorithm: %s\n' % self.world.algorithm)
|
||||
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('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('Compass shuffle: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['compassshuffle'].items()})
|
||||
outfile.write('Small Key shuffle: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['keyshuffle'].items()})
|
||||
outfile.write('Big Key shuffle: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['bigkeyshuffle'].items()})
|
||||
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()})
|
||||
outfile.write('L\\R Quickswap enabled: %s\n' % ('Yes' if self.world.quickswap else 'No'))
|
||||
outfile.write('Menu speed: %s\n' % self.world.fastmenu)
|
||||
outfile.write('Keysanity enabled: %s\n' % ('Yes' if self.metadata['keysanity'] else 'No'))
|
||||
outfile.write('Players: %d' % self.world.players)
|
||||
outfile.write('Menu speed: %s' % self.world.fastmenu)
|
||||
if self.entrances:
|
||||
outfile.write('\n\nEntrances:\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.entrances.values()]))
|
||||
@@ -1085,6 +1137,9 @@ class Spoiler(object):
|
||||
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'.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()]))
|
||||
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')
|
||||
|
||||
path_listings = []
|
||||
|
||||
@@ -73,7 +73,7 @@ def KholdstareDefeatRule(state, player):
|
||||
(
|
||||
state.has('Bombos', player) and
|
||||
# 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
|
||||
(
|
||||
@@ -83,7 +83,7 @@ def KholdstareDefeatRule(state, player):
|
||||
(
|
||||
state.has('Fire Rod', player) and
|
||||
state.has('Bombos', player) and
|
||||
state.world.swords == 'swordless' and
|
||||
state.world.swords[player] == 'swordless' and
|
||||
state.can_extend_magic(player, 16)
|
||||
)
|
||||
)
|
||||
@@ -115,8 +115,8 @@ boss_table = {
|
||||
'Agahnim2': ('Agahnim2', AgahnimDefeatRule)
|
||||
}
|
||||
|
||||
def can_place_boss(world, boss, dungeon_name, level=None):
|
||||
if world.swords in ['swordless'] and boss == 'Kholdstare' and dungeon_name != 'Ice Palace':
|
||||
def can_place_boss(world, player, boss, dungeon_name, level=None):
|
||||
if world.swords[player] in ['swordless'] and boss == 'Kholdstare' and dungeon_name != 'Ice Palace':
|
||||
return False
|
||||
|
||||
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
|
||||
|
||||
def place_bosses(world, player):
|
||||
if world.boss_shuffle == 'none':
|
||||
if world.boss_shuffle[player] == 'none':
|
||||
return
|
||||
# Most to least restrictive order
|
||||
if world.mode != 'inverted':
|
||||
if world.mode[player] != 'inverted':
|
||||
boss_locations = [
|
||||
['Ganons Tower', 'top'],
|
||||
['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
|
||||
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:
|
||||
if world.swords == 'swordless':
|
||||
if world.swords[player] == 'swordless':
|
||||
world.get_dungeon('Ice Palace', player).boss = BossFactory('Kholdstare', player)
|
||||
logging.getLogger('').debug('Placing boss Kholdstare at Ice Palace')
|
||||
boss_locations.remove(['Ice Palace', None])
|
||||
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']
|
||||
else: # all bosses present, the three duplicates chosen at random
|
||||
bosses = all_bosses + [random.choice(placeable_bosses) for _ in range(3)]
|
||||
@@ -195,18 +195,18 @@ def place_bosses(world, player):
|
||||
random.shuffle(bosses)
|
||||
for [loc, level] in boss_locations:
|
||||
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:
|
||||
raise FillError('Could not place boss for location %s' % loc_text)
|
||||
bosses.remove(boss)
|
||||
|
||||
logging.getLogger('').debug('Placing boss %s at %s', boss, loc_text)
|
||||
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:
|
||||
loc_text = loc + (' ('+level+')' if level else '')
|
||||
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:
|
||||
raise FillError('Could not place boss for location %s' % loc_text)
|
||||
|
||||
|
||||
+14
-13
@@ -8,7 +8,7 @@ from Items import ItemFactory
|
||||
|
||||
def create_dungeons(world, player):
|
||||
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)
|
||||
for region in dungeon.regions:
|
||||
world.get_region(region, player).dungeon = dungeon
|
||||
@@ -27,7 +27,7 @@ def create_dungeons(world, player):
|
||||
MM = make_dungeon('Misery Mire', 'Vitreous', ['Misery Mire (Entrance)', 'Misery Mire (Main)', 'Misery Mire (West)', 'Misery Mire (Final Area)', 'Misery Mire (Vitreous)'], 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', ['Turtle Rock (Entrance)', 'Turtle Rock (First Section)', 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock (Second Section)', 'Turtle Rock (Big Chest)', 'Turtle Rock (Crystaroller Room)', 'Turtle Rock (Dark Room)', 'Turtle Rock (Eye Bridge)', 'Turtle Rock (Trinexx)'], 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', ['Agahnims Tower', 'Agahnim 1'], None, ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), [])
|
||||
GT = make_dungeon('Ganons Tower', 'Agahnim2', ['Ganons Tower (Entrance)', 'Ganons Tower (Tile Room)', 'Ganons Tower (Compass Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower (Map Room)', 'Ganons Tower (Firesnake Room)', 'Ganons Tower (Teleport Room)', 'Ganons Tower (Bottom)', 'Ganons Tower (Top)', 'Ganons Tower (Before Moldorm)', 'Ganons Tower (Moldorm)', 'Agahnim 2'], ItemFactory('Big Key (Ganons Tower)', player), ItemFactory(['Small Key (Ganons Tower)'] * 4, player), ItemFactory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], player))
|
||||
else:
|
||||
@@ -47,7 +47,7 @@ def fill_dungeons(world):
|
||||
|
||||
for player in range(1, world.players + 1):
|
||||
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)
|
||||
else:
|
||||
world.push_item(pinball_room, ItemFactory('Small Key (Skull Woods)', player), False)
|
||||
@@ -113,21 +113,20 @@ def fill_dungeons(world):
|
||||
continue
|
||||
|
||||
# next place dungeon items
|
||||
if world.place_dungeon_items:
|
||||
for dungeon_item in dungeon_items:
|
||||
di_location = dungeon_locations.pop()
|
||||
world.push_item(di_location, dungeon_item, False)
|
||||
|
||||
|
||||
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):
|
||||
all_state_base = world.get_all_state()
|
||||
|
||||
for player in range(1, world.players + 1):
|
||||
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)
|
||||
else:
|
||||
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
|
||||
shuffled_locations.remove(pinball_room)
|
||||
|
||||
if world.keysanity:
|
||||
#in keysanity dungeon items are distributed as part of the normal item pool
|
||||
# with shuffled dungeon items they are distributed as part of the normal item pool
|
||||
for item in world.get_items():
|
||||
if item.key:
|
||||
if (item.smallkey and world.keyshuffle[item.player]) or (item.bigkey and world.bigkeyshuffle[item.player]):
|
||||
all_state_base.collect(item, 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
|
||||
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_order = {"BigKey": 3, "SmallKey": 2}
|
||||
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)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ In the vanilla, dungeonssimple, and dungeonsfull shuffles, the following two loc
|
||||
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 keysanity is being used, you can additionally get hints for Small Keys or Big Keys but not hints for Maps or Compasses.
|
||||
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:
|
||||
|
||||
|
||||
+80
-47
@@ -1,13 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import copy
|
||||
import os
|
||||
import logging
|
||||
import random
|
||||
import textwrap
|
||||
import shlex
|
||||
import sys
|
||||
|
||||
from Main import main
|
||||
from Utils import is_bundled, close_console, output_path
|
||||
from Utils import is_bundled, close_console
|
||||
|
||||
|
||||
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
|
||||
@@ -15,11 +17,18 @@ class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
|
||||
def _get_help_string(self, action):
|
||||
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.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='''\
|
||||
Select Enforcement of Item Requirements. (default: %(default)s)
|
||||
No Glitches:
|
||||
@@ -28,7 +37,7 @@ def start():
|
||||
No Logic: Distribute items without regard for
|
||||
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='''\
|
||||
Select game mode. (default: %(default)s)
|
||||
Open: World starts with Zelda rescued.
|
||||
@@ -41,7 +50,7 @@ def start():
|
||||
Requires the moon pearl to be Link in the Light World
|
||||
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='''\
|
||||
Select sword placement. (default: %(default)s)
|
||||
Random: All swords placed randomly.
|
||||
@@ -55,7 +64,7 @@ def start():
|
||||
Palace, to allow for an alternative to firerod.
|
||||
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='''\
|
||||
Select completion goal. (default: %(default)s)
|
||||
Ganon: Collect all crystals, beat Agahnim 2 then
|
||||
@@ -67,21 +76,21 @@ def start():
|
||||
Triforce Hunt: Places 30 Triforce Pieces in the world, collect
|
||||
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='''\
|
||||
Select game difficulty. Affects available itempool. (default: %(default)s)
|
||||
Normal: Normal difficulty.
|
||||
Hard: A harder setting with less equipment and reduced 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='''\
|
||||
Select limits on item functionality to increase difficulty. (default: %(default)s)
|
||||
Normal: Normal functionality.
|
||||
Hard: 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='''\
|
||||
Select game timer setting. Affects available itempool. (default: %(default)s)
|
||||
None: No timer.
|
||||
@@ -101,7 +110,7 @@ def start():
|
||||
Timed mode. If time runs out, you lose (but can
|
||||
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='''\
|
||||
Select progressive equipment setting. Affects available itempool. (default: %(default)s)
|
||||
On: Swords, Shields, Armor, and Gloves will
|
||||
@@ -115,7 +124,7 @@ def start():
|
||||
category, be randomly progressive or not.
|
||||
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='''\
|
||||
Select item filling algorithm. (default: %(default)s
|
||||
balanced: vt26 derivitive that aims to strike a balance between
|
||||
@@ -138,7 +147,7 @@ def start():
|
||||
slightly biased to placing progression items with
|
||||
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('full'), const='full', nargs='?', choices=['vanilla', 'simple', 'restricted', 'full', 'crossed', 'insanity', 'restricted_legacy', 'full_legacy', 'madness_legacy', 'insanity_legacy', 'dungeonsfull', 'dungeonssimple'],
|
||||
help='''\
|
||||
Select Entrance Shuffling Algorithm. (default: %(default)s)
|
||||
Full: Mix cave and dungeon entrances freely while limiting
|
||||
@@ -162,7 +171,7 @@ def start():
|
||||
The dungeon variants only mix up dungeons and keep the rest of
|
||||
the overworld vanilla.
|
||||
''')
|
||||
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='''\
|
||||
How many crystals are needed to defeat ganon. Any other
|
||||
requirements for ganon for the selected goal still apply.
|
||||
@@ -171,16 +180,18 @@ def start():
|
||||
Random: Picks a random value between 0 and 7 (inclusive).
|
||||
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='''\
|
||||
How many crystals are needed to open GT. For inverted mode
|
||||
this applies to the castle tower door instead. (default: %(default)s)
|
||||
Random: Picks a random value between 0 and 7 (inclusive).
|
||||
0-7: Number of crystals needed
|
||||
''')
|
||||
|
||||
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.')
|
||||
parser.add_argument('--loglevel', default='info', const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
|
||||
parser.add_argument('--openpyramid', default=defval(False), help='''\
|
||||
Pre-opens the pyramid hole, this removes the Agahnim 2 requirement for it
|
||||
''', 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('--count', help='''\
|
||||
Use to batch generate multiple seeds with same settings.
|
||||
@@ -189,49 +200,45 @@ def start():
|
||||
--seed given will produce the same 10 (different) roms each
|
||||
time).
|
||||
''', 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='''\
|
||||
Select the rate at which the menu opens and closes.
|
||||
(default: %(default)s)
|
||||
''')
|
||||
parser.add_argument('--quickswap', help='Enable quick item swapping with L and R.', action='store_true')
|
||||
parser.add_argument('--disablemusic', help='Disables game music.', action='store_true')
|
||||
parser.add_argument('--keysanity', help='''\
|
||||
Keys (and other dungeon items) are no longer restricted to
|
||||
their dungeons, but can be anywhere
|
||||
''', action='store_true')
|
||||
parser.add_argument('--retro', help='''\
|
||||
parser.add_argument('--mapshuffle', default=defval(False), help='Maps are no longer restricted to their dungeons, but can be anywhere', action='store_true')
|
||||
parser.add_argument('--compassshuffle', default=defval(False), help='Compasses are no longer restricted to their dungeons, but can be anywhere', action='store_true')
|
||||
parser.add_argument('--keyshuffle', default=defval(False), help='Small Keys are no longer restricted to their dungeons, but can be anywhere', action='store_true')
|
||||
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', default=defval(False), help='''\
|
||||
Keys are universal, shooting arrows costs rupees,
|
||||
and a few other little things make this more like Zelda-1.
|
||||
''', action='store_true')
|
||||
parser.add_argument('--custom', default=False, help='Not supported.')
|
||||
parser.add_argument('--customitemarray', default=False, help='Not supported.')
|
||||
parser.add_argument('--nodungeonitems', help='''\
|
||||
Remove Maps and Compasses from Itempool, replacing them by
|
||||
empty slots.
|
||||
''', action='store_true')
|
||||
parser.add_argument('--accessibility', default='items', const='items', nargs='?', choices=['items', 'locations', 'none'], help='''\
|
||||
parser.add_argument('--custom', default=defval(False), help='Not supported.')
|
||||
parser.add_argument('--customitemarray', default=defval(False), help='Not supported.')
|
||||
parser.add_argument('--accessibility', default=defval('items'), const='items', nargs='?', choices=['items', 'locations', 'none'], help='''\
|
||||
Select Item/Location Accessibility. (default: %(default)s)
|
||||
Items: You can reach all unique inventory items. No guarantees about
|
||||
reaching all locations or all keys.
|
||||
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.
|
||||
''')
|
||||
parser.add_argument('--hints', help='''\
|
||||
parser.add_argument('--hints', default=defval(False), help='''\
|
||||
Make telepathic tiles and storytellers give helpful hints.
|
||||
''', action='store_true')
|
||||
# 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='''\
|
||||
If set, the Pyramid Hole and Ganon's Tower are not
|
||||
included entrance shuffle pool.
|
||||
''', 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='''\
|
||||
Select the rate at which the heart beep sound is played at
|
||||
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)')
|
||||
parser.add_argument('--sprite', help='''\
|
||||
Path to a sprite sheet to use for Link. Needs to be in
|
||||
@@ -246,21 +253,47 @@ def start():
|
||||
Output .json patch to stdout instead of a patched rom. Used
|
||||
for VT site integration, do not use otherwise.
|
||||
''')
|
||||
parser.add_argument('--skip_playthrough', action='store_true', default=False)
|
||||
parser.add_argument('--enemizercli', default='')
|
||||
parser.add_argument('--shufflebosses', default='none', choices=['none', 'basic', 'normal', 'chaos'])
|
||||
parser.add_argument('--shuffleenemies', default=False, action='store_true')
|
||||
parser.add_argument('--enemy_health', default='default', choices=['default', 'easy', 'normal', 'hard', 'expert'])
|
||||
parser.add_argument('--enemy_damage', default='default', choices=['default', 'shuffled', 'chaos'])
|
||||
parser.add_argument('--shufflepalette', default=False, action='store_true')
|
||||
parser.add_argument('--shufflepots', default=False, action='store_true')
|
||||
parser.add_argument('--multi', default=1, type=lambda value: min(max(int(value), 1), 255))
|
||||
|
||||
parser.add_argument('--skip_playthrough', action='store_true', default=defval(False))
|
||||
parser.add_argument('--enemizercli', default=defval(''))
|
||||
parser.add_argument('--shufflebosses', default=defval('none'), choices=['none', 'basic', 'normal', 'chaos'])
|
||||
parser.add_argument('--shuffleenemies', default=defval('none'), choices=['none', 'shuffled', 'chaos'])
|
||||
parser.add_argument('--enemy_health', default=defval('default'), choices=['default', 'easy', 'normal', 'hard', 'expert'])
|
||||
parser.add_argument('--enemy_damage', default=defval('default'), choices=['default', 'shuffled', 'chaos'])
|
||||
parser.add_argument('--shufflepalette', default=defval(False), action='store_true')
|
||||
parser.add_argument('--shufflepots', default=defval(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=defval(1), type=lambda value: min(max(int(value), 1), 255))
|
||||
parser.add_argument('--names', default=defval(''))
|
||||
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):
|
||||
output_path.cached_path = args.outputpath
|
||||
if multiargs.multi:
|
||||
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 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', 'crystals_ganon', 'crystals_gt', 'openpyramid',
|
||||
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle',
|
||||
'retro', 'accessibility', 'hints', 'shufflepalette', 'shufflepots', 'beemizer',
|
||||
'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage']:
|
||||
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:
|
||||
# for the bundled builds, if we have no arguments, the user
|
||||
|
||||
+57
-87
@@ -19,17 +19,17 @@ def link_entrances(world, player):
|
||||
connect_simple(world, exitname, regionname, player)
|
||||
|
||||
# if we do not shuffle, set default connections
|
||||
if world.shuffle == 'vanilla':
|
||||
if world.shuffle[player] == 'vanilla':
|
||||
for exitname, regionname in default_connections:
|
||||
connect_simple(world, exitname, regionname, player)
|
||||
for exitname, regionname in default_dungeon_connections:
|
||||
connect_simple(world, exitname, regionname, player)
|
||||
elif world.shuffle == 'dungeonssimple':
|
||||
elif world.shuffle[player] == 'dungeonssimple':
|
||||
for exitname, regionname in default_connections:
|
||||
connect_simple(world, exitname, regionname, player)
|
||||
|
||||
simple_shuffle_dungeons(world, player)
|
||||
elif world.shuffle == 'dungeonsfull':
|
||||
elif world.shuffle[player] == 'dungeonsfull':
|
||||
for exitname, regionname in default_connections:
|
||||
connect_simple(world, exitname, regionname, player)
|
||||
|
||||
@@ -39,7 +39,7 @@ def link_entrances(world, player):
|
||||
lw_entrances = list(LW_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
|
||||
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
|
||||
else:
|
||||
@@ -52,14 +52,14 @@ def link_entrances(world, player):
|
||||
dw_entrances.append('Ganons Tower')
|
||||
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
|
||||
connect_mandatory_exits(world, lw_entrances, [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], list(LW_Dungeon_Entrances_Must_Exit), player)
|
||||
else:
|
||||
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_caves(world, lw_entrances, dw_entrances, dungeon_exits, player)
|
||||
elif world.shuffle == 'simple':
|
||||
elif world.shuffle[player] == 'simple':
|
||||
simple_shuffle_dungeons(world, player)
|
||||
|
||||
old_man_entrances = list(Old_Man_Entrances)
|
||||
@@ -130,7 +130,7 @@ def link_entrances(world, player):
|
||||
|
||||
# place remaining doors
|
||||
connect_doors(world, single_doors, door_targets, player)
|
||||
elif world.shuffle == 'restricted':
|
||||
elif world.shuffle[player] == 'restricted':
|
||||
simple_shuffle_dungeons(world, player)
|
||||
|
||||
lw_entrances = list(LW_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances)
|
||||
@@ -201,7 +201,7 @@ def link_entrances(world, player):
|
||||
|
||||
# place remaining doors
|
||||
connect_doors(world, doors, door_targets, player)
|
||||
elif world.shuffle == 'restricted_legacy':
|
||||
elif world.shuffle[player] == 'restricted_legacy':
|
||||
simple_shuffle_dungeons(world, player)
|
||||
|
||||
lw_entrances = list(LW_Entrances)
|
||||
@@ -256,7 +256,7 @@ def link_entrances(world, player):
|
||||
|
||||
# place remaining doors
|
||||
connect_doors(world, single_doors, door_targets, player)
|
||||
elif world.shuffle == 'full':
|
||||
elif world.shuffle[player] == 'full':
|
||||
skull_woods_shuffle(world, player)
|
||||
|
||||
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
|
||||
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
|
||||
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
|
||||
else:
|
||||
@@ -309,7 +309,7 @@ def link_entrances(world, player):
|
||||
pass
|
||||
else: #if the cave wasn't placed we get here
|
||||
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
|
||||
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
|
||||
connect_doors(world, doors, door_targets, player)
|
||||
elif world.shuffle == 'crossed':
|
||||
elif world.shuffle[player] == 'crossed':
|
||||
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)
|
||||
@@ -376,7 +376,7 @@ def link_entrances(world, player):
|
||||
# tavern back door cannot be shuffled yet
|
||||
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
|
||||
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
|
||||
else:
|
||||
@@ -392,7 +392,7 @@ def link_entrances(world, player):
|
||||
#place must-exit caves
|
||||
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
|
||||
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
|
||||
connect_doors(world, entrances, door_targets, player)
|
||||
elif world.shuffle == 'full_legacy':
|
||||
elif world.shuffle[player] == 'full_legacy':
|
||||
skull_woods_shuffle(world, player)
|
||||
|
||||
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)
|
||||
door_targets = list(Single_Cave_Targets)
|
||||
|
||||
if world.mode == 'standard':
|
||||
if world.mode[player] == 'standard':
|
||||
# must connect front of hyrule castle to do escape
|
||||
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
|
||||
else:
|
||||
@@ -471,7 +471,7 @@ def link_entrances(world, player):
|
||||
else:
|
||||
connect_mandatory_exits(world, dw_entrances, caves, dw_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
|
||||
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
|
||||
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
|
||||
lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + Old_Man_Entrances)
|
||||
dw_entrances = list(DW_Entrances + DW_Dungeon_Entrances)
|
||||
@@ -552,7 +552,7 @@ def link_entrances(world, player):
|
||||
('Lumberjack Tree Exit', 'Lumberjack Tree (top)'),
|
||||
(('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
|
||||
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)
|
||||
@@ -606,7 +606,7 @@ def link_entrances(world, player):
|
||||
connect_entrance(world, hole, target, player)
|
||||
|
||||
# hyrule castle handling
|
||||
if world.mode == 'standard':
|
||||
if world.mode[player] == 'standard':
|
||||
# must connect front of hyrule castle to do escape
|
||||
connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (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
|
||||
connect_doors(world, single_doors, door_targets, player)
|
||||
elif world.shuffle == 'insanity':
|
||||
elif world.shuffle[player] == 'insanity':
|
||||
# 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']
|
||||
@@ -792,7 +792,7 @@ def link_entrances(world, player):
|
||||
# tavern back door cannot be shuffled yet
|
||||
connect_doors(world, ['Tavern North'], ['Tavern'], player)
|
||||
|
||||
if world.mode == 'standard':
|
||||
if world.mode[player] == 'standard':
|
||||
# cannot move uncle cave
|
||||
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)
|
||||
@@ -825,7 +825,7 @@ def link_entrances(world, player):
|
||||
connect_entrance(world, hole, hole_targets.pop(), player)
|
||||
|
||||
# hyrule castle handling
|
||||
if world.mode == 'standard':
|
||||
if world.mode[player] == 'standard':
|
||||
# must connect front of hyrule castle to do escape
|
||||
connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (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
|
||||
connect_doors(world, doors, door_targets, player)
|
||||
elif world.shuffle == 'insanity_legacy':
|
||||
world.fix_fake_world = False
|
||||
elif world.shuffle[player] == 'insanity_legacy':
|
||||
world.fix_fake_world[player] = False
|
||||
# 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']
|
||||
@@ -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)',
|
||||
'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
|
||||
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)
|
||||
@@ -960,7 +960,7 @@ def link_entrances(world, player):
|
||||
connect_entrance(world, hole, hole_targets.pop(), player)
|
||||
|
||||
# hyrule castle handling
|
||||
if world.mode == 'standard':
|
||||
if world.mode[player] == 'standard':
|
||||
# must connect front of hyrule castle to do escape
|
||||
connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (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)
|
||||
|
||||
# if we do not shuffle, set default connections
|
||||
if world.shuffle == 'vanilla':
|
||||
if world.shuffle[player] == 'vanilla':
|
||||
for exitname, regionname in inverted_default_connections:
|
||||
connect_simple(world, exitname, regionname, player)
|
||||
for exitname, regionname in inverted_default_dungeon_connections:
|
||||
connect_simple(world, exitname, regionname, player)
|
||||
elif world.shuffle == 'dungeonssimple':
|
||||
elif world.shuffle[player] == 'dungeonssimple':
|
||||
for exitname, regionname in inverted_default_connections:
|
||||
connect_simple(world, exitname, regionname, player)
|
||||
|
||||
simple_shuffle_dungeons(world, player)
|
||||
elif world.shuffle == 'dungeonsfull':
|
||||
elif world.shuffle[player] == 'dungeonsfull':
|
||||
for exitname, regionname in inverted_default_connections:
|
||||
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]
|
||||
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)
|
||||
|
||||
old_man_entrances = list(Inverted_Old_Man_Entrances)
|
||||
@@ -1160,7 +1160,7 @@ def link_inverted_entrances(world, player):
|
||||
|
||||
single_doors = list(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)
|
||||
|
||||
# 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)
|
||||
if links_house in blacksmith_doors:
|
||||
blacksmith_doors.remove(links_house)
|
||||
if links_house in old_man_entrances:
|
||||
old_man_entrances.remove(links_house)
|
||||
|
||||
# place dark sanc
|
||||
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
|
||||
connect_doors(world, single_doors, door_targets, player)
|
||||
|
||||
elif world.shuffle == 'restricted':
|
||||
elif world.shuffle[player] == 'restricted':
|
||||
simple_shuffle_dungeons(world, player)
|
||||
|
||||
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)
|
||||
single_doors = list(Single_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)
|
||||
|
||||
# place links house
|
||||
@@ -1326,7 +1328,7 @@ def link_inverted_entrances(world, player):
|
||||
doors = lw_entrances + dw_entrances
|
||||
# place remaining doors
|
||||
connect_doors(world, doors, door_targets, player)
|
||||
elif world.shuffle == 'full':
|
||||
elif world.shuffle[player] == 'full':
|
||||
skull_woods_shuffle(world, player)
|
||||
|
||||
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'])
|
||||
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)
|
||||
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)
|
||||
old_man_house = list(Old_Man_House)
|
||||
|
||||
@@ -1477,7 +1479,7 @@ def link_inverted_entrances(world, player):
|
||||
|
||||
# place remaining doors
|
||||
connect_doors(world, doors, door_targets, player)
|
||||
elif world.shuffle == 'crossed':
|
||||
elif world.shuffle[player] == 'crossed':
|
||||
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)
|
||||
@@ -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'])
|
||||
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)
|
||||
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)
|
||||
|
||||
# randomize which desert ledge door is a must-exit
|
||||
@@ -1587,7 +1589,7 @@ def link_inverted_entrances(world, player):
|
||||
|
||||
# place remaining doors
|
||||
connect_doors(world, entrances, door_targets, player)
|
||||
elif world.shuffle == 'insanity':
|
||||
elif world.shuffle[player] == 'insanity':
|
||||
# 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)']
|
||||
@@ -1610,7 +1612,7 @@ def link_inverted_entrances(world, player):
|
||||
# bomb shop logic for.
|
||||
# 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 + ['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)
|
||||
|
||||
random.shuffle(doors)
|
||||
@@ -1831,7 +1833,7 @@ def scramble_holes(world, player):
|
||||
else:
|
||||
hole_targets.append(('Pyramid Exit', 'Pyramid'))
|
||||
|
||||
if world.mode == 'standard':
|
||||
if world.mode[player] == 'standard':
|
||||
# cannot move uncle cave
|
||||
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)
|
||||
@@ -1840,14 +1842,14 @@ def scramble_holes(world, player):
|
||||
hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance'))
|
||||
|
||||
# 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'))
|
||||
if world.shuffle_ganon:
|
||||
random.shuffle(hole_targets)
|
||||
exit, target = hole_targets.pop()
|
||||
connect_two_way(world, 'Pyramid Entrance', exit, player)
|
||||
connect_entrance(world, 'Pyramid Hole', target, player)
|
||||
if world.shuffle != 'crossed':
|
||||
if world.shuffle[player] != 'crossed':
|
||||
hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
|
||||
|
||||
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'))
|
||||
|
||||
# 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'))
|
||||
if world.shuffle_ganon:
|
||||
random.shuffle(hole_targets)
|
||||
exit, target = hole_targets.pop()
|
||||
connect_two_way(world, 'Inverted Pyramid Entrance', exit, 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'))
|
||||
|
||||
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:
|
||||
entrance = entrances.pop()
|
||||
# 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()
|
||||
entrances.append(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()
|
||||
entrances.append(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_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:
|
||||
connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player)
|
||||
else:
|
||||
@@ -2021,13 +2023,13 @@ def simple_shuffle_dungeons(world, player):
|
||||
|
||||
# mix up 4 door dungeons
|
||||
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')
|
||||
random.shuffle(multi_dungeons)
|
||||
|
||||
dp_target = multi_dungeons[0]
|
||||
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
|
||||
hc_target = 'Hyrule Castle'
|
||||
else:
|
||||
@@ -2035,7 +2037,7 @@ def simple_shuffle_dungeons(world, player):
|
||||
|
||||
# ToDo improve this?
|
||||
|
||||
if world.mode != 'inverted':
|
||||
if world.mode[player] != 'inverted':
|
||||
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 (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 (North)']
|
||||
|
||||
Inverted_Blacksmith_Multi_Cave_Doors = [] # same as non-inverted
|
||||
|
||||
Inverted_LW_Single_Cave_Doors = LW_Single_Cave_Doors + ['Inverted Big Bomb Shop']
|
||||
|
||||
Inverted_DW_Single_Cave_Doors = ['Bonk Fairy (Dark)',
|
||||
@@ -2717,38 +2717,7 @@ Inverted_Bomb_Shop_Single_Cave_Doors = ['Waterfall of Wishing',
|
||||
'Bumper Cave (Top)',
|
||||
'Mimic Cave',
|
||||
'Dark Lake Hylia Shop',
|
||||
'Inverted Links House']
|
||||
|
||||
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 Links House',
|
||||
'Inverted Big Bomb Shop']
|
||||
|
||||
|
||||
@@ -3019,6 +2988,7 @@ mandatory_connections = [('Lake Hylia Central Island Pier', 'Lake Hylia Central
|
||||
]
|
||||
|
||||
inverted_mandatory_connections = [('Lake Hylia Central Island Pier', 'Lake Hylia Central Island'),
|
||||
('Lake Hylia Island', 'Lake Hylia Island'),
|
||||
('Zoras River', 'Zoras River'),
|
||||
('Kings Grave Outer Rocks', 'Kings Grave Area'),
|
||||
('Kings Grave Inner Rocks', 'Light World'),
|
||||
@@ -3237,7 +3207,7 @@ default_connections = [('Waterfall of Wishing', 'Waterfall of Wishing'),
|
||||
('Lumberjack House', 'Lumberjack House'),
|
||||
("Hyrule Castle Secret Entrance Drop", "Hyrule Castle Secret Entrance"),
|
||||
("Hyrule Castle Secret Entrance Stairs", "Hyrule Castle Secret Entrance"),
|
||||
("Hyrule Castle Secret Entrance Exit", "Light World"),
|
||||
("Hyrule Castle Secret Entrance Exit", "Hyrule Castle Courtyard"),
|
||||
('Bonk Fairy (Light)', 'Bonk Fairy (Light)'),
|
||||
('Lake Hylia Fairy', 'Lake Hylia Healer Fairy'),
|
||||
('Lake Hylia Fortune Teller', 'Lake Hylia Fortune Teller'),
|
||||
@@ -3548,7 +3518,7 @@ default_dungeon_connections = [('Desert Palace Entrance (South)', 'Desert Palace
|
||||
('Hyrule Castle Entrance (South)', 'Hyrule Castle'),
|
||||
('Hyrule Castle Entrance (West)', 'Hyrule Castle'),
|
||||
('Hyrule Castle Entrance (East)', 'Hyrule Castle'),
|
||||
('Hyrule Castle Exit (South)', 'Light World'),
|
||||
('Hyrule Castle Exit (South)', 'Hyrule Castle Courtyard'),
|
||||
('Hyrule Castle Exit (West)', 'Hyrule Castle Ledge'),
|
||||
('Hyrule Castle Exit (East)', 'Hyrule Castle Ledge'),
|
||||
('Agahnims Tower', 'Agahnims Tower'),
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
def fill_restrictive(world, base_state, locations, itempool):
|
||||
def fill_restrictive(world, base_state, locations, itempool, single_player_placement = False):
|
||||
def sweep_from_pool():
|
||||
new_state = base_state.copy()
|
||||
for item in itempool:
|
||||
@@ -169,35 +169,40 @@ def fill_restrictive(world, base_state, locations, itempool):
|
||||
new_state.sweep_for_events()
|
||||
return new_state
|
||||
|
||||
while itempool and locations:
|
||||
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)
|
||||
unplaced_items = []
|
||||
|
||||
no_access_checks = {}
|
||||
reachable_items = {}
|
||||
for item in itempool:
|
||||
if world.accessibility[item.player] == 'none':
|
||||
no_access_checks.setdefault(item.player, []).append(item)
|
||||
else:
|
||||
nextpool.insert(0, item)
|
||||
itempool = nextpool
|
||||
reachable_items.setdefault(item.player, []).append(item)
|
||||
|
||||
for player_items in [no_access_checks, reachable_items]:
|
||||
while any(player_items.values()) and locations:
|
||||
items_to_place = [[itempool.remove(items[-1]), items.pop()][-1] for items in player_items.values() if items]
|
||||
|
||||
maximum_exploration_state = sweep_from_pool()
|
||||
|
||||
perform_access_check = True
|
||||
if world.accessibility == 'none':
|
||||
perform_access_check = not world.has_beaten_game(maximum_exploration_state)
|
||||
has_beaten_game = world.has_beaten_game(maximum_exploration_state)
|
||||
|
||||
for item_to_place in items_to_place:
|
||||
perform_access_check = True
|
||||
if world.accessibility[item_to_place.player] == '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
|
||||
|
||||
spot_to_fill = None
|
||||
for location in locations:
|
||||
if location.can_fill(maximum_exploration_state, item_to_place, perform_access_check):
|
||||
if (not single_player_placement or location.player == item_to_place.player)\
|
||||
and location.can_fill(maximum_exploration_state, item_to_place, perform_access_check):
|
||||
spot_to_fill = location
|
||||
break
|
||||
|
||||
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 != 'none':
|
||||
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)
|
||||
@@ -206,8 +211,9 @@ def fill_restrictive(world, base_state, locations, itempool):
|
||||
locations.remove(spot_to_fill)
|
||||
spot_to_fill.event = True
|
||||
|
||||
itempool.extend(unplaced_items)
|
||||
|
||||
def distribute_items_restrictive(world, gftower_trash_count=0, fill_locations=None):
|
||||
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 fill_locations:
|
||||
fill_locations = world.get_unfilled_locations()
|
||||
@@ -221,7 +227,11 @@ def distribute_items_restrictive(world, gftower_trash_count=0, fill_locations=No
|
||||
|
||||
# fill in gtower locations with trash first
|
||||
for player in range(1, world.players + 1):
|
||||
if world.ganonstower_vanilla[player]:
|
||||
if not gftower_trash or not world.ganonstower_vanilla[player]:
|
||||
continue
|
||||
|
||||
gftower_trash_count = (random.randint(15, 50) if world.goal[player] == 'triforcehunt' else random.randint(0, 15))
|
||||
|
||||
gtower_locations = [location for location in fill_locations if 'Ganons Tower' in location.name and location.player == player]
|
||||
random.shuffle(gtower_locations)
|
||||
trashcnt = 0
|
||||
@@ -235,9 +245,8 @@ def distribute_items_restrictive(world, gftower_trash_count=0, fill_locations=No
|
||||
random.shuffle(fill_locations)
|
||||
fill_locations.reverse()
|
||||
|
||||
# Make sure the escape small key is placed first in standard keysanity 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)' else 0)
|
||||
# Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots
|
||||
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)
|
||||
|
||||
fill_restrictive(world, world.state, fill_locations, progitempool)
|
||||
|
||||
@@ -307,7 +316,7 @@ def flood_items(world):
|
||||
location_list = world.get_reachable_locations()
|
||||
random.shuffle(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
|
||||
replace_item = location.item
|
||||
replace_item.location = None
|
||||
@@ -327,7 +336,6 @@ def balance_multiworld_progression(world):
|
||||
reachable_locations_count[player] = 0
|
||||
|
||||
def get_sphere_locations(sphere_state, locations):
|
||||
if not world.keysanity:
|
||||
sphere_state.sweep_for_events(key_only=True, locations=locations)
|
||||
return [loc for loc in locations if sphere_state.can_reach(loc)]
|
||||
|
||||
@@ -338,8 +346,7 @@ def balance_multiworld_progression(world):
|
||||
reachable_locations_count[location.player] += 1
|
||||
|
||||
if checked_locations:
|
||||
average_reachable_locations = sum(reachable_locations_count.values()) / world.players
|
||||
threshold = ((average_reachable_locations + max(reachable_locations_count.values())) / 2) * 0.8 #todo: probably needs some tweaking
|
||||
threshold = max(reachable_locations_count.values()) - 20
|
||||
|
||||
balancing_players = [player for player, reachables in reachable_locations_count.items() if reachables < threshold]
|
||||
if balancing_players:
|
||||
@@ -350,9 +357,9 @@ def balance_multiworld_progression(world):
|
||||
candidate_items = []
|
||||
while True:
|
||||
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)
|
||||
if location.item.player in balancing_players:
|
||||
if location.item.player in balancing_players and not location.locked:
|
||||
candidate_items.append(location)
|
||||
balancing_sphere = get_sphere_locations(balancing_state, balancing_unchecked_locations)
|
||||
for location in balancing_sphere:
|
||||
@@ -360,11 +367,14 @@ def balance_multiworld_progression(world):
|
||||
balancing_reachables[location.player] += 1
|
||||
if world.has_beaten_game(balancing_state) or all([reachables >= threshold for reachables in balancing_reachables.values()]):
|
||||
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]
|
||||
items_to_replace = []
|
||||
for player in balancing_players:
|
||||
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]
|
||||
while items_to_test:
|
||||
testing = items_to_test.pop()
|
||||
@@ -374,9 +384,6 @@ def balance_multiworld_progression(world):
|
||||
|
||||
reducing_state.sweep_for_events(locations=locations_to_test)
|
||||
|
||||
if testing.locked:
|
||||
continue
|
||||
|
||||
if world.has_beaten_game(balancing_state):
|
||||
if not world.has_beaten_game(reducing_state):
|
||||
items_to_replace.append(testing)
|
||||
@@ -386,13 +393,17 @@ def balance_multiworld_progression(world):
|
||||
items_to_replace.append(testing)
|
||||
|
||||
replaced_items = False
|
||||
locations_for_replacing = [l for l in checked_locations if not l.event and not l.locked]
|
||||
while locations_for_replacing and items_to_replace:
|
||||
new_location = locations_for_replacing.pop()
|
||||
replacement_locations = [l for l in checked_locations if not l.event and not l.locked]
|
||||
while replacement_locations and items_to_replace:
|
||||
new_location = replacement_locations.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.event = True
|
||||
old_location.event = False
|
||||
new_location.event, old_location.event = True, False
|
||||
state.collect(new_location.item, True, new_location)
|
||||
replaced_items = True
|
||||
if replaced_items:
|
||||
@@ -402,9 +413,11 @@ def balance_multiworld_progression(world):
|
||||
sphere_locations.append(location)
|
||||
|
||||
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)
|
||||
checked_locations.extend(sphere_locations)
|
||||
|
||||
if world.has_beaten_game(state):
|
||||
break
|
||||
elif not sphere_locations:
|
||||
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.')
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from argparse import Namespace
|
||||
from glob import glob
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import os
|
||||
import shutil
|
||||
@@ -10,10 +11,11 @@ from urllib.parse import urlparse
|
||||
from urllib.request import urlopen
|
||||
|
||||
from AdjusterMain import adjust
|
||||
from EntranceRandomizer import parse_arguments
|
||||
from GuiUtils import ToolTips, set_icon, BackgroundTaskProgress
|
||||
from Main import main, __version__ as ESVersion
|
||||
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):
|
||||
@@ -60,12 +62,20 @@ def guiMain(args=None):
|
||||
suppressRomCheckbutton = Checkbutton(checkBoxFrame, text="Do not create patched Rom", variable=suppressRomVar)
|
||||
quickSwapVar = IntVar()
|
||||
quickSwapCheckbutton = Checkbutton(checkBoxFrame, text="Enabled L/R Item quickswapping", variable=quickSwapVar)
|
||||
keysanityVar = IntVar()
|
||||
keysanityCheckbutton = Checkbutton(checkBoxFrame, text="Keysanity (keys anywhere)", variable=keysanityVar)
|
||||
openpyramidVar = IntVar()
|
||||
openpyramidCheckbutton = Checkbutton(checkBoxFrame, text="Pre-open Pyramid Hole", variable=openpyramidVar)
|
||||
mcsbshuffleFrame = Frame(checkBoxFrame)
|
||||
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()
|
||||
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()
|
||||
@@ -80,9 +90,14 @@ def guiMain(args=None):
|
||||
createSpoilerCheckbutton.pack(expand=True, anchor=W)
|
||||
suppressRomCheckbutton.pack(expand=True, anchor=W)
|
||||
quickSwapCheckbutton.pack(expand=True, anchor=W)
|
||||
keysanityCheckbutton.pack(expand=True, anchor=W)
|
||||
openpyramidCheckbutton.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)
|
||||
dungeonItemsCheckbutton.pack(expand=True, anchor=W)
|
||||
disableMusicCheckbutton.pack(expand=True, anchor=W)
|
||||
shuffleGanonCheckbutton.pack(expand=True, anchor=W)
|
||||
hintsCheckbutton.pack(expand=True, anchor=W)
|
||||
@@ -120,7 +135,7 @@ def guiMain(args=None):
|
||||
|
||||
romDialogFrame = Frame(fileDialogFrame)
|
||||
baseRomLabel = Label(romDialogFrame, text='Base Rom')
|
||||
romVar = StringVar()
|
||||
romVar = StringVar(value="Zelda no Densetsu - Kamigami no Triforce (Japan).sfc")
|
||||
romEntry = Entry(romDialogFrame, textvariable=romVar)
|
||||
|
||||
def RomSelect():
|
||||
@@ -346,6 +361,9 @@ def guiMain(args=None):
|
||||
worldLabel = Label(bottomFrame, text='Worlds')
|
||||
worldVar = StringVar()
|
||||
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 #')
|
||||
seedVar = StringVar()
|
||||
seedEntry = Entry(bottomFrame, width=15, textvariable=seedVar)
|
||||
@@ -354,8 +372,9 @@ def guiMain(args=None):
|
||||
countSpinbox = Spinbox(bottomFrame, from_=1, to=100, width=5, textvariable=countVar)
|
||||
|
||||
def generateRom():
|
||||
guiargs = Namespace
|
||||
guiargs = Namespace()
|
||||
guiargs.multi = int(worldVar.get())
|
||||
guiargs.names = namesVar.get()
|
||||
guiargs.seed = int(seedVar.get()) if seedVar.get() else None
|
||||
guiargs.count = int(countVar.get()) if countVar.get() != '1' else None
|
||||
guiargs.mode = modeVar.get()
|
||||
@@ -376,16 +395,19 @@ def guiMain(args=None):
|
||||
guiargs.fastmenu = fastMenuVar.get()
|
||||
guiargs.create_spoiler = bool(createSpoilerVar.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.nodungeonitems = bool(dungeonItemsVar.get())
|
||||
guiargs.quickswap = bool(quickSwapVar.get())
|
||||
guiargs.disablemusic = bool(disableMusicVar.get())
|
||||
guiargs.shuffleganon = bool(shuffleGanonVar.get())
|
||||
guiargs.hints = bool(hintsVar.get())
|
||||
guiargs.enemizercli = enemizerCLIpathVar.get()
|
||||
guiargs.shufflebosses = enemizerBossVar.get()
|
||||
guiargs.shuffleenemies = bool(enemyShuffleVar.get())
|
||||
guiargs.shuffleenemies = 'chaos' if bool(enemyShuffleVar.get()) else 'none'
|
||||
guiargs.enemy_health = enemizerHealthVar.get()
|
||||
guiargs.enemy_damage = enemizerDamageVar.get()
|
||||
guiargs.shufflepalette = bool(paletteShuffleVar.get())
|
||||
@@ -401,10 +423,13 @@ def guiMain(args=None):
|
||||
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(triforcecountVar.get()), int(triforceVar.get()), int(rupoorcostVar.get()), int(universalkeyVar.get())]
|
||||
guiargs.rom = romVar.get()
|
||||
guiargs.jsonout = None
|
||||
guiargs.sprite = sprite
|
||||
guiargs.skip_playthrough = False
|
||||
guiargs.outputpath = None
|
||||
# get default values for missing parameters
|
||||
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:
|
||||
if guiargs.count is not None:
|
||||
seed = guiargs.seed
|
||||
@@ -414,14 +439,21 @@ def guiMain(args=None):
|
||||
else:
|
||||
main(seed=guiargs.seed, args=guiargs)
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
messagebox.showerror(title="Error while creating seed", message=str(e))
|
||||
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)
|
||||
|
||||
worldLabel.pack(side=LEFT)
|
||||
worldSpinbox.pack(side=LEFT)
|
||||
namesLabel.pack(side=LEFT)
|
||||
namesEntry.pack(side=LEFT)
|
||||
seedLabel.pack(side=LEFT, padx=(5, 0))
|
||||
seedEntry.pack(side=LEFT)
|
||||
countLabel.pack(side=LEFT, padx=(5, 0))
|
||||
@@ -502,15 +534,23 @@ def guiMain(args=None):
|
||||
fastMenuLabel2 = Label(fastMenuFrame2, text='Menu speed')
|
||||
fastMenuLabel2.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)
|
||||
heartcolorFrame2.pack(expand=True, anchor=E)
|
||||
fastMenuFrame2.pack(expand=True, anchor=E)
|
||||
namesFrame2.pack(expand=True, anchor=E)
|
||||
|
||||
bottomFrame2 = Frame(topFrame2)
|
||||
|
||||
def adjustRom():
|
||||
guiargs = Namespace
|
||||
guiargs = Namespace()
|
||||
guiargs.heartbeep = heartbeepVar.get()
|
||||
guiargs.heartcolor = heartcolorVar.get()
|
||||
guiargs.fastmenu = fastMenuVar.get()
|
||||
@@ -518,12 +558,18 @@ def guiMain(args=None):
|
||||
guiargs.disablemusic = bool(disableMusicVar.get())
|
||||
guiargs.rom = romVar2.get()
|
||||
guiargs.sprite = sprite
|
||||
guiargs.names = namesEntry2.get()
|
||||
try:
|
||||
adjust(args=guiargs)
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
messagebox.showerror(title="Error while creating seed", message=str(e))
|
||||
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)
|
||||
|
||||
@@ -1127,13 +1173,17 @@ def guiMain(args=None):
|
||||
topFrame3.pack(side=TOP, pady=(17,0))
|
||||
|
||||
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
|
||||
createSpoilerVar.set(int(args.create_spoiler))
|
||||
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)
|
||||
if args.nodungeonitems:
|
||||
dungeonItemsVar.set(int(not args.nodungeonitems))
|
||||
quickSwapVar.set(int(args.quickswap))
|
||||
disableMusicVar.set(int(args.disablemusic))
|
||||
if args.count:
|
||||
|
||||
+242
-242
@@ -9,7 +9,7 @@ def create_inverted_regions(world, player):
|
||||
["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',
|
||||
'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',
|
||||
'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)',
|
||||
@@ -316,7 +316,7 @@ def create_inverted_regions(world, player):
|
||||
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()
|
||||
world.initialize_regions()
|
||||
|
||||
def create_lw_region(player, name, locations=None, exits=None):
|
||||
return _create_region(player, name, RegionType.LightWorld, 'Light World', locations, exits)
|
||||
@@ -340,14 +340,14 @@ def _create_region(player, name, type, hint='Hyrule', locations=None, exits=None
|
||||
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))
|
||||
address, player_address, crystal, hint_text = location_table[location]
|
||||
ret.locations.append(Location(player, location, address, crystal, hint_text, ret, player_address))
|
||||
return ret
|
||||
|
||||
def mark_dark_world_regions(world):
|
||||
def mark_dark_world_regions(world, player):
|
||||
# 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.
|
||||
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)
|
||||
while queue:
|
||||
current = queue.popleft()
|
||||
@@ -360,7 +360,7 @@ def mark_dark_world_regions(world):
|
||||
seen.add(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)
|
||||
while queue:
|
||||
current = queue.popleft()
|
||||
@@ -377,7 +377,7 @@ def mark_dark_world_regions(world):
|
||||
shop_table = {
|
||||
'Cave Shop (Dark Death Mountain)': (0x0112, 0xC1, True),
|
||||
'Red Shield Shop': (0x0110, 0xC1, True),
|
||||
'Dark Lake Hylia Shop': (0x010F, 0xC1, True),
|
||||
'Dark Lake Hylia Shop': (0x010F, 0xC1, False),
|
||||
'Dark World Lumberjack Shop': (0x010F, 0xC1, True),
|
||||
'Village of Outcasts Shop': (0x010F, 0xC1, True),
|
||||
'Dark World Potion Shop': (0x010F, 0xC1, True),
|
||||
@@ -396,7 +396,7 @@ _dark_world_shop_defaults = [('Red Potion', 150), ('Blue Shield', 50), ('Bombs (
|
||||
default_shop_contents = {
|
||||
'Cave Shop (Dark Death Mountain)': _basic_shop_defaults,
|
||||
'Red Shield Shop': [('Red Shield', 500), ('Bee', 10), ('Arrows (10)', 30)],
|
||||
'Dark Lake Hylia Shop': _dark_world_shop_defaults,
|
||||
'Dark Lake Hylia Shop': [('Blue Potion', 160), ('Blue Shield', 50), ('Bombs (10)', 50)],
|
||||
'Dark World Lumberjack Shop': _dark_world_shop_defaults,
|
||||
'Village of Outcasts Shop': _dark_world_shop_defaults,
|
||||
'Dark World Potion Shop': _dark_world_shop_defaults,
|
||||
@@ -406,236 +406,236 @@ default_shop_contents = {
|
||||
'Potion Shop': [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)],
|
||||
}
|
||||
|
||||
location_table = {'Mushroom': (0x180013, False, 'in the woods'),
|
||||
'Bottle Merchant': (0x2EB18, False, 'with a merchant'),
|
||||
'Flute Spot': (0x18014A, False, 'underground'),
|
||||
'Sunken Treasure': (0x180145, False, 'underwater'),
|
||||
'Purple Chest': (0x33D68, False, 'from a box'),
|
||||
'Blind\'s Hideout - Top': (0xEB0F, False, 'in a basement'),
|
||||
'Blind\'s Hideout - Left': (0xEB12, False, 'in a basement'),
|
||||
'Blind\'s Hideout - Right': (0xEB15, False, 'in a basement'),
|
||||
'Blind\'s Hideout - Far Left': (0xEB18, False, 'in a basement'),
|
||||
'Blind\'s Hideout - Far Right': (0xEB1B, False, 'in a basement'),
|
||||
'Link\'s Uncle': (0x2DF45, False, 'with your uncle'),
|
||||
'Secret Passage': (0xE971, False, 'near your uncle'),
|
||||
'King Zora': (0xEE1C3, False, 'at a high price'),
|
||||
'Zora\'s Ledge': (0x180149, False, 'near Zora'),
|
||||
'Waterfall Fairy - Left': (0xE9B0, False, 'near a fairy'),
|
||||
'Waterfall Fairy - Right': (0xE9D1, False, 'near a fairy'),
|
||||
'King\'s Tomb': (0xE97A, False, 'alone in a cave'),
|
||||
'Floodgate Chest': (0xE98C, False, 'in the dam'),
|
||||
'Link\'s House': (0xE9BC, False, 'in your home'),
|
||||
'Kakariko Tavern': (0xE9CE, False, 'in the bar'),
|
||||
'Chicken House': (0xE9E9, False, 'near poultry'),
|
||||
'Aginah\'s Cave': (0xE9F2, False, 'with Aginah'),
|
||||
'Sahasrahla\'s Hut - Left': (0xEA82, False, 'near the elder'),
|
||||
'Sahasrahla\'s Hut - Middle': (0xEA85, False, 'near the elder'),
|
||||
'Sahasrahla\'s Hut - Right': (0xEA88, False, 'near the elder'),
|
||||
'Sahasrahla': (0x2F1FC, False, 'with the elder'),
|
||||
'Kakariko Well - Top': (0xEA8E, False, 'in a well'),
|
||||
'Kakariko Well - Left': (0xEA91, False, 'in a well'),
|
||||
'Kakariko Well - Middle': (0xEA94, False, 'in a well'),
|
||||
'Kakariko Well - Right': (0xEA97, False, 'in a well'),
|
||||
'Kakariko Well - Bottom': (0xEA9A, False, 'in a well'),
|
||||
'Blacksmith': (0x18002A, False, 'with the smith'),
|
||||
'Magic Bat': (0x180015, False, 'with the bat'),
|
||||
'Sick Kid': (0x339CF, False, 'with the sick'),
|
||||
'Hobo': (0x33E7D, False, 'with the hobo'),
|
||||
'Lost Woods Hideout': (0x180000, False, 'near a thief'),
|
||||
'Lumberjack Tree': (0x180001, False, 'in a hole'),
|
||||
'Cave 45': (0x180003, False, 'alone in a cave'),
|
||||
'Graveyard Cave': (0x180004, False, 'alone in a cave'),
|
||||
'Checkerboard Cave': (0x180005, False, 'alone in a cave'),
|
||||
'Mini Moldorm Cave - Far Left': (0xEB42, False, 'near Moldorms'),
|
||||
'Mini Moldorm Cave - Left': (0xEB45, False, 'near Moldorms'),
|
||||
'Mini Moldorm Cave - Right': (0xEB48, False, 'near Moldorms'),
|
||||
'Mini Moldorm Cave - Far Right': (0xEB4B, False, 'near Moldorms'),
|
||||
'Mini Moldorm Cave - Generous Guy': (0x180010, False, 'near Moldorms'),
|
||||
'Ice Rod Cave': (0xEB4E, False, 'in a frozen cave'),
|
||||
'Bonk Rock Cave': (0xEB3F, False, 'alone in a cave'),
|
||||
'Library': (0x180012, False, 'near books'),
|
||||
'Potion Shop': (0x180014, False, 'near potions'),
|
||||
'Lake Hylia Island': (0x180144, False, 'on an island'),
|
||||
'Maze Race': (0x180142, False, 'at the race'),
|
||||
'Desert Ledge': (0x180143, False, 'in the desert'),
|
||||
'Desert Palace - Big Chest': (0xE98F, False, 'in Desert Palace'),
|
||||
'Desert Palace - Torch': (0x180160, False, 'in Desert Palace'),
|
||||
'Desert Palace - Map Chest': (0xE9B6, False, 'in Desert Palace'),
|
||||
'Desert Palace - Compass Chest': (0xE9CB, False, 'in Desert Palace'),
|
||||
'Desert Palace - Big Key Chest': (0xE9C2, False, 'in Desert Palace'),
|
||||
'Desert Palace - Boss': (0x180151, False, 'with Lanmolas'),
|
||||
'Eastern Palace - Compass Chest': (0xE977, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Big Chest': (0xE97D, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Cannonball Chest': (0xE9B3, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Big Key Chest': (0xE9B9, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Map Chest': (0xE9F5, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Boss': (0x180150, False, 'with the Armos'),
|
||||
'Master Sword Pedestal': (0x289B0, False, 'at the pedestal'),
|
||||
'Hyrule Castle - Boomerang Chest': (0xE974, False, 'in Hyrule Castle'),
|
||||
'Hyrule Castle - Map Chest': (0xEB0C, False, 'in Hyrule Castle'),
|
||||
'Hyrule Castle - Zelda\'s Chest': (0xEB09, False, 'in Hyrule Castle'),
|
||||
'Sewers - Dark Cross': (0xE96E, False, 'in the sewers'),
|
||||
'Sewers - Secret Room - Left': (0xEB5D, False, 'in the sewers'),
|
||||
'Sewers - Secret Room - Middle': (0xEB60, False, 'in the sewers'),
|
||||
'Sewers - Secret Room - Right': (0xEB63, False, 'in the sewers'),
|
||||
'Sanctuary': (0xEA79, False, 'in Sanctuary'),
|
||||
'Castle Tower - Room 03': (0xEAB5, False, 'in Castle Tower'),
|
||||
'Castle Tower - Dark Maze': (0xEAB2, False, 'in Castle Tower'),
|
||||
'Old Man': (0xF69FA, False, 'with the old man'),
|
||||
'Spectacle Rock Cave': (0x180002, False, 'alone in a cave'),
|
||||
'Paradox Cave Lower - Far Left': (0xEB2A, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Lower - Left': (0xEB2D, 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, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Lower - Middle': (0xEB36, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Upper - Left': (0xEB39, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Upper - Right': (0xEB3C, False, 'in a cave with seven chests'),
|
||||
'Spiral Cave': (0xE9BF, False, 'in spiral cave'),
|
||||
'Ether Tablet': (0x180016, False, 'at a monolith'),
|
||||
'Spectacle Rock': (0x180140, False, 'atop a rock'),
|
||||
'Tower of Hera - Basement Cage': (0x180162, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Map Chest': (0xE9AD, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Big Key Chest': (0xE9E6, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Compass Chest': (0xE9FB, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Big Chest': (0xE9F8, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Boss': (0x180152, False, 'with Moldorm'),
|
||||
'Pyramid': (0x180147, False, 'on the pyramid'),
|
||||
'Catfish': (0xEE185, False, 'with a catfish'),
|
||||
'Stumpy': (0x330C7, False, 'with tree boy'),
|
||||
'Digging Game': (0x180148, False, 'underground'),
|
||||
'Bombos Tablet': (0x180017, False, 'at a monolith'),
|
||||
'Hype Cave - Top': (0xEB1E, False, 'near a bat-like man'),
|
||||
'Hype Cave - Middle Right': (0xEB21, False, 'near a bat-like man'),
|
||||
'Hype Cave - Middle Left': (0xEB24, False, 'near a bat-like man'),
|
||||
'Hype Cave - Bottom': (0xEB27, False, 'near a bat-like man'),
|
||||
'Hype Cave - Generous Guy': (0x180011, False, 'with a bat-like man'),
|
||||
'Peg Cave': (0x180006, False, 'alone in a cave'),
|
||||
'Pyramid Fairy - Left': (0xE980, False, 'near a fairy'),
|
||||
'Pyramid Fairy - Right': (0xE983, False, 'near a fairy'),
|
||||
'Brewery': (0xE9EC, False, 'alone in a home'),
|
||||
'C-Shaped House': (0xE9EF, False, 'alone in a home'),
|
||||
'Chest Game': (0xEDA8, False, 'as a prize'),
|
||||
'Bumper Cave Ledge': (0x180146, False, 'on a ledge'),
|
||||
'Mire Shed - Left': (0xEA73, False, 'near sparks'),
|
||||
'Mire Shed - Right': (0xEA76, False, 'near sparks'),
|
||||
'Superbunny Cave - Top': (0xEA7C, False, 'in a connection'),
|
||||
'Superbunny Cave - Bottom': (0xEA7F, False, 'in a connection'),
|
||||
'Spike Cave': (0xEA8B, False, 'beyond spikes'),
|
||||
'Hookshot Cave - Top Right': (0xEB51, False, 'across pits'),
|
||||
'Hookshot Cave - Top Left': (0xEB54, False, 'across pits'),
|
||||
'Hookshot Cave - Bottom Right': (0xEB5A, False, 'across pits'),
|
||||
'Hookshot Cave - Bottom Left': (0xEB57, False, 'across pits'),
|
||||
'Floating Island': (0x180141, False, 'on an island'),
|
||||
'Mimic Cave': (0xE9C5, False, 'in a cave of mimicry'),
|
||||
'Swamp Palace - Entrance': (0xEA9D, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Map Chest': (0xE986, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Big Chest': (0xE989, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Compass Chest': (0xEAA0, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Big Key Chest': (0xEAA6, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - West Chest': (0xEAA3, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Flooded Room - Left': (0xEAA9, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Flooded Room - Right': (0xEAAC, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Waterfall Room': (0xEAAF, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Boss': (0x180154, False, 'with Arrghus'),
|
||||
'Thieves\' Town - Big Key Chest': (0xEA04, False, 'in Thieves\' Town'),
|
||||
'Thieves\' Town - Map Chest': (0xEA01, False, 'in Thieves\' Town'),
|
||||
'Thieves\' Town - Compass Chest': (0xEA07, False, 'in Thieves\' Town'),
|
||||
'Thieves\' Town - Ambush Chest': (0xEA0A, False, 'in Thieves\' Town'),
|
||||
'Thieves\' Town - Attic': (0xEA0D, False, 'in Thieves\' Town'),
|
||||
'Thieves\' Town - Big Chest': (0xEA10, False, 'in Thieves\' Town'),
|
||||
'Thieves\' Town - Blind\'s Cell': (0xEA13, False, 'in Thieves\' Town'),
|
||||
'Thieves\' Town - Boss': (0x180156, False, 'with Blind'),
|
||||
'Skull Woods - Compass Chest': (0xE992, False, 'in Skull Woods'),
|
||||
'Skull Woods - Map Chest': (0xE99B, False, 'in Skull Woods'),
|
||||
'Skull Woods - Big Chest': (0xE998, False, 'in Skull Woods'),
|
||||
'Skull Woods - Pot Prison': (0xE9A1, False, 'in Skull Woods'),
|
||||
'Skull Woods - Pinball Room': (0xE9C8, False, 'in Skull Woods'),
|
||||
'Skull Woods - Big Key Chest': (0xE99E, False, 'in Skull Woods'),
|
||||
'Skull Woods - Bridge Room': (0xE9FE, False, 'near Mothula'),
|
||||
'Skull Woods - Boss': (0x180155, False, 'with Mothula'),
|
||||
'Ice Palace - Compass Chest': (0xE9D4, False, 'in Ice Palace'),
|
||||
'Ice Palace - Freezor Chest': (0xE995, False, 'in Ice Palace'),
|
||||
'Ice Palace - Big Chest': (0xE9AA, False, 'in Ice Palace'),
|
||||
'Ice Palace - Iced T Room': (0xE9E3, False, 'in Ice Palace'),
|
||||
'Ice Palace - Spike Room': (0xE9E0, False, 'in Ice Palace'),
|
||||
'Ice Palace - Big Key Chest': (0xE9A4, False, 'in Ice Palace'),
|
||||
'Ice Palace - Map Chest': (0xE9DD, False, 'in Ice Palace'),
|
||||
'Ice Palace - Boss': (0x180157, False, 'with Kholdstare'),
|
||||
'Misery Mire - Big Chest': (0xEA67, False, 'in Misery Mire'),
|
||||
'Misery Mire - Map Chest': (0xEA6A, False, 'in Misery Mire'),
|
||||
'Misery Mire - Main Lobby': (0xEA5E, False, 'in Misery Mire'),
|
||||
'Misery Mire - Bridge Chest': (0xEA61, False, 'in Misery Mire'),
|
||||
'Misery Mire - Spike Chest': (0xE9DA, False, 'in Misery Mire'),
|
||||
'Misery Mire - Compass Chest': (0xEA64, False, 'in Misery Mire'),
|
||||
'Misery Mire - Big Key Chest': (0xEA6D, False, 'in Misery Mire'),
|
||||
'Misery Mire - Boss': (0x180158, False, 'with Vitreous'),
|
||||
'Turtle Rock - Compass Chest': (0xEA22, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Roller Room - Left': (0xEA1C, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Roller Room - Right': (0xEA1F, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Chain Chomps': (0xEA16, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Big Key Chest': (0xEA25, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Big Chest': (0xEA19, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Crystaroller Room': (0xEA34, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Eye Bridge - Bottom Left': (0xEA31, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Eye Bridge - Bottom Right': (0xEA2E, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Eye Bridge - Top Left': (0xEA2B, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Eye Bridge - Top Right': (0xEA28, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Boss': (0x180159, False, 'with Trinexx'),
|
||||
'Palace of Darkness - Shooter Room': (0xEA5B, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - The Arena - Bridge': (0xEA3D, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Stalfos Basement': (0xEA49, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Big Key Chest': (0xEA37, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - The Arena - Ledge': (0xEA3A, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Map Chest': (0xEA52, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Compass Chest': (0xEA43, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Dark Basement - Left': (0xEA4C, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Dark Basement - Right': (0xEA4F, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Dark Maze - Top': (0xEA55, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Dark Maze - Bottom': (0xEA58, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Big Chest': (0xEA40, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Harmless Hellway': (0xEA46, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Boss': (0x180153, False, 'with Helmasaur King'),
|
||||
'Ganons Tower - Bob\'s Torch': (0x180161, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Hope Room - Left': (0xEAD9, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Hope Room - Right': (0xEADC, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Tile Room': (0xEAE2, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Compass Room - Top Left': (0xEAE5, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Compass Room - Top Right': (0xEAE8, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Compass Room - Bottom Left': (0xEAEB, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Compass Room - Bottom Right': (0xEAEE, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - DMs Room - Top Left': (0xEAB8, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - DMs Room - Top Right': (0xEABB, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - DMs Room - Bottom Left': (0xEABE, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - DMs Room - Bottom Right': (0xEAC1, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Map Chest': (0xEAD3, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Firesnake Room': (0xEAD0, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Randomizer Room - Top Left': (0xEAC4, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Randomizer Room - Top Right': (0xEAC7, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Randomizer Room - Bottom Left': (0xEACA, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Randomizer Room - Bottom Right': (0xEACD, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Bob\'s Chest': (0xEADF, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Big Chest': (0xEAD6, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Big Key Room - Left': (0xEAF4, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Big Key Room - Right': (0xEAF7, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Big Key Chest': (0xEAF1, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Mini Helmasaur Room - Left': (0xEAFD, False, 'atop Ganon\'s Tower'),
|
||||
'Ganons Tower - Mini Helmasaur Room - Right': (0xEB00, False, 'atop Ganon\'s Tower'),
|
||||
'Ganons Tower - Pre-Moldorm Chest': (0xEB03, False, 'atop Ganon\'s Tower'),
|
||||
'Ganons Tower - Validation Chest': (0xEB06, False, 'atop Ganon\'s Tower'),
|
||||
'Ganon': (None, False, 'from me'),
|
||||
'Agahnim 1': (None, False, 'from Ganon\'s wizardry form'),
|
||||
'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')}
|
||||
location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'),
|
||||
'Bottle Merchant': (0x2eb18, 0x186339, False, 'with a merchant'),
|
||||
'Flute Spot': (0x18014a, 0x18633d, False, 'underground'),
|
||||
'Sunken Treasure': (0x180145, 0x186354, False, 'underwater'),
|
||||
'Purple Chest': (0x33d68, 0x186359, False, 'from a box'),
|
||||
"Blind's Hideout - Top": (0xeb0f, 0x1862e3, False, 'in a basement'),
|
||||
"Blind's Hideout - Left": (0xeb12, 0x1862e6, False, 'in a basement'),
|
||||
"Blind's Hideout - Right": (0xeb15, 0x1862e9, False, 'in a basement'),
|
||||
"Blind's Hideout - Far Left": (0xeb18, 0x1862ec, False, 'in a basement'),
|
||||
"Blind's Hideout - Far Right": (0xeb1b, 0x1862ef, False, 'in a basement'),
|
||||
"Link's Uncle": (0x2df45, 0x18635f, False, 'with your uncle'),
|
||||
'Secret Passage': (0xe971, 0x186145, False, 'near your uncle'),
|
||||
'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'),
|
||||
'Waterfall Fairy - Right': (0xe9d1, 0x1861a5, False, 'near a fairy'),
|
||||
"King's Tomb": (0xe97a, 0x18614e, False, 'alone in a cave'),
|
||||
'Floodgate Chest': (0xe98c, 0x186160, False, 'in the dam'),
|
||||
"Link's House": (0xe9bc, 0x186190, False, 'in your home'),
|
||||
'Kakariko Tavern': (0xe9ce, 0x1861a2, False, 'in the bar'),
|
||||
'Chicken House': (0xe9e9, 0x1861bd, False, 'near poultry'),
|
||||
"Aginah's Cave": (0xe9f2, 0x1861c6, False, 'with Aginah'),
|
||||
"Sahasrahla's Hut - Left": (0xea82, 0x186256, False, 'near the elder'),
|
||||
"Sahasrahla's Hut - Middle": (0xea85, 0x186259, False, 'near the elder'),
|
||||
"Sahasrahla's Hut - Right": (0xea88, 0x18625c, False, 'near the elder'),
|
||||
'Sahasrahla': (0x2f1fc, 0x186365, False, 'with the elder'),
|
||||
'Kakariko Well - Top': (0xea8e, 0x186262, False, 'in a well'),
|
||||
'Kakariko Well - Left': (0xea91, 0x186265, False, 'in a well'),
|
||||
'Kakariko Well - Middle': (0xea94, 0x186268, False, 'in a well'),
|
||||
'Kakariko Well - Right': (0xea97, 0x18626b, False, 'in a well'),
|
||||
'Kakariko Well - Bottom': (0xea9a, 0x18626e, False, 'in a well'),
|
||||
'Blacksmith': (0x18002a, 0x186366, False, 'with the smith'),
|
||||
'Magic Bat': (0x180015, 0x18635e, False, 'with the bat'),
|
||||
'Sick Kid': (0x339cf, 0x186367, False, 'with the sick'),
|
||||
'Hobo': (0x33e7d, 0x186368, False, 'with the hobo'),
|
||||
'Lost Woods Hideout': (0x180000, 0x186348, False, 'near a thief'),
|
||||
'Lumberjack Tree': (0x180001, 0x186349, False, 'in a hole'),
|
||||
'Cave 45': (0x180003, 0x18634b, False, 'alone in a cave'),
|
||||
'Graveyard Cave': (0x180004, 0x18634c, False, 'alone in a cave'),
|
||||
'Checkerboard Cave': (0x180005, 0x18634d, False, 'alone in a cave'),
|
||||
'Mini Moldorm Cave - Far Left': (0xeb42, 0x186316, False, 'near Moldorms'),
|
||||
'Mini Moldorm Cave - Left': (0xeb45, 0x186319, False, 'near Moldorms'),
|
||||
'Mini Moldorm Cave - Right': (0xeb48, 0x18631c, False, 'near Moldorms'),
|
||||
'Mini Moldorm Cave - Far Right': (0xeb4b, 0x18631f, False, 'near Moldorms'),
|
||||
'Mini Moldorm Cave - Generous Guy': (0x180010, 0x18635a, False, 'near Moldorms'),
|
||||
'Ice Rod Cave': (0xeb4e, 0x186322, False, 'in a frozen cave'),
|
||||
'Bonk Rock Cave': (0xeb3f, 0x186313, False, 'alone in a cave'),
|
||||
'Library': (0x180012, 0x18635c, False, 'near books'),
|
||||
'Potion Shop': (0x180014, 0x18635d, False, 'near potions'),
|
||||
'Lake Hylia Island': (0x180144, 0x186353, False, 'on an island'),
|
||||
'Maze Race': (0x180142, 0x186351, False, 'at the race'),
|
||||
'Desert Ledge': (0x180143, 0x186352, False, 'in the desert'),
|
||||
'Desert Palace - Big Chest': (0xe98f, 0x186163, False, 'in Desert Palace'),
|
||||
'Desert Palace - Torch': (0x180160, 0x186362, False, 'in Desert Palace'),
|
||||
'Desert Palace - Map Chest': (0xe9b6, 0x18618a, False, 'in Desert Palace'),
|
||||
'Desert Palace - Compass Chest': (0xe9cb, 0x18619f, False, 'in Desert Palace'),
|
||||
'Desert Palace - Big Key Chest': (0xe9c2, 0x186196, False, 'in Desert Palace'),
|
||||
'Desert Palace - Boss': (0x180151, 0x18633f, False, 'with Lanmolas'),
|
||||
'Eastern Palace - Compass Chest': (0xe977, 0x18614b, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Big Chest': (0xe97d, 0x186151, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Cannonball Chest': (0xe9b3, 0x186187, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Big Key Chest': (0xe9b9, 0x18618d, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Map Chest': (0xe9f5, 0x1861c9, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Boss': (0x180150, 0x18633e, False, 'with the Armos'),
|
||||
'Master Sword Pedestal': (0x289b0, 0x186369, False, 'at the pedestal'),
|
||||
'Hyrule Castle - Boomerang Chest': (0xe974, 0x186148, False, 'in Hyrule Castle'),
|
||||
'Hyrule Castle - Map Chest': (0xeb0c, 0x1862e0, False, 'in Hyrule Castle'),
|
||||
"Hyrule Castle - Zelda's Chest": (0xeb09, 0x1862dd, False, 'in Hyrule Castle'),
|
||||
'Sewers - Dark Cross': (0xe96e, 0x186142, False, 'in the sewers'),
|
||||
'Sewers - Secret Room - Left': (0xeb5d, 0x186331, False, 'in the sewers'),
|
||||
'Sewers - Secret Room - Middle': (0xeb60, 0x186334, False, 'in the sewers'),
|
||||
'Sewers - Secret Room - Right': (0xeb63, 0x186337, False, 'in the sewers'),
|
||||
'Sanctuary': (0xea79, 0x18624d, False, 'in Sanctuary'),
|
||||
'Castle Tower - Room 03': (0xeab5, 0x186289, False, 'in Castle Tower'),
|
||||
'Castle Tower - Dark Maze': (0xeab2, 0x186286, False, 'in Castle Tower'),
|
||||
'Old Man': (0xf69fa, 0x186364, False, 'with the old man'),
|
||||
'Spectacle Rock Cave': (0x180002, 0x18634a, False, 'alone in a cave'),
|
||||
'Paradox Cave Lower - Far Left': (0xeb2a, 0x1862fe, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Lower - Left': (0xeb2d, 0x186301, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Lower - Right': (0xeb30, 0x186304, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Lower - Far Right': (0xeb33, 0x186307, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Lower - Middle': (0xeb36, 0x18630a, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Upper - Left': (0xeb39, 0x18630d, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Upper - Right': (0xeb3c, 0x186310, False, 'in a cave with seven chests'),
|
||||
'Spiral Cave': (0xe9bf, 0x186193, False, 'in spiral cave'),
|
||||
'Ether Tablet': (0x180016, 0x18633b, False, 'at a monolith'),
|
||||
'Spectacle Rock': (0x180140, 0x18634f, False, 'atop a rock'),
|
||||
'Tower of Hera - Basement Cage': (0x180162, 0x18633a, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Map Chest': (0xe9ad, 0x186181, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Big Key Chest': (0xe9e6, 0x1861ba, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Compass Chest': (0xe9fb, 0x1861cf, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Big Chest': (0xe9f8, 0x1861cc, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Boss': (0x180152, 0x186340, False, 'with Moldorm'),
|
||||
'Pyramid': (0x180147, 0x186356, False, 'on the pyramid'),
|
||||
'Catfish': (0xee185, 0x186361, False, 'with a catfish'),
|
||||
'Stumpy': (0x330c7, 0x18636a, False, 'with tree boy'),
|
||||
'Digging Game': (0x180148, 0x186357, False, 'underground'),
|
||||
'Bombos Tablet': (0x180017, 0x18633c, False, 'at a monolith'),
|
||||
'Hype Cave - Top': (0xeb1e, 0x1862f2, False, 'near a bat-like man'),
|
||||
'Hype Cave - Middle Right': (0xeb21, 0x1862f5, False, 'near a bat-like man'),
|
||||
'Hype Cave - Middle Left': (0xeb24, 0x1862f8, False, 'near a bat-like man'),
|
||||
'Hype Cave - Bottom': (0xeb27, 0x1862fb, False, 'near a bat-like man'),
|
||||
'Hype Cave - Generous Guy': (0x180011, 0x18635b, False, 'with a bat-like man'),
|
||||
'Peg Cave': (0x180006, 0x18634e, False, 'alone in a cave'),
|
||||
'Pyramid Fairy - Left': (0xe980, 0x186154, False, 'near a fairy'),
|
||||
'Pyramid Fairy - Right': (0xe983, 0x186157, False, 'near a fairy'),
|
||||
'Brewery': (0xe9ec, 0x1861c0, False, 'alone in a home'),
|
||||
'C-Shaped House': (0xe9ef, 0x1861c3, False, 'alone in a home'),
|
||||
'Chest Game': (0xeda8, 0x18636b, False, 'as a prize'),
|
||||
'Bumper Cave Ledge': (0x180146, 0x186355, False, 'on a ledge'),
|
||||
'Mire Shed - Left': (0xea73, 0x186247, False, 'near sparks'),
|
||||
'Mire Shed - Right': (0xea76, 0x18624a, False, 'near sparks'),
|
||||
'Superbunny Cave - Top': (0xea7c, 0x186250, False, 'in a connection'),
|
||||
'Superbunny Cave - Bottom': (0xea7f, 0x186253, False, 'in a connection'),
|
||||
'Spike Cave': (0xea8b, 0x18625f, False, 'beyond spikes'),
|
||||
'Hookshot Cave - Top Right': (0xeb51, 0x186325, False, 'across pits'),
|
||||
'Hookshot Cave - Top Left': (0xeb54, 0x186328, False, 'across pits'),
|
||||
'Hookshot Cave - Bottom Right': (0xeb5a, 0x18632e, False, 'across pits'),
|
||||
'Hookshot Cave - Bottom Left': (0xeb57, 0x18632b, False, 'across pits'),
|
||||
'Floating Island': (0x180141, 0x186350, False, 'on an island'),
|
||||
'Mimic Cave': (0xe9c5, 0x186199, False, 'in a cave of mimicry'),
|
||||
'Swamp Palace - Entrance': (0xea9d, 0x186271, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Map Chest': (0xe986, 0x18615a, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Big Chest': (0xe989, 0x18615d, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Compass Chest': (0xeaa0, 0x186274, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Big Key Chest': (0xeaa6, 0x18627a, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - West Chest': (0xeaa3, 0x186277, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Flooded Room - Left': (0xeaa9, 0x18627d, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Flooded Room - Right': (0xeaac, 0x186280, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Waterfall Room': (0xeaaf, 0x186283, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Boss': (0x180154, 0x186342, False, 'with Arrghus'),
|
||||
"Thieves' Town - Big Key Chest": (0xea04, 0x1861d8, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Map Chest": (0xea01, 0x1861d5, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Compass Chest": (0xea07, 0x1861db, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Ambush Chest": (0xea0a, 0x1861de, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Attic": (0xea0d, 0x1861e1, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Big Chest": (0xea10, 0x1861e4, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Blind's Cell": (0xea13, 0x1861e7, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Boss": (0x180156, 0x186344, False, 'with Blind'),
|
||||
'Skull Woods - Compass Chest': (0xe992, 0x186166, False, 'in Skull Woods'),
|
||||
'Skull Woods - Map Chest': (0xe99b, 0x18616f, False, 'in Skull Woods'),
|
||||
'Skull Woods - Big Chest': (0xe998, 0x18616c, False, 'in Skull Woods'),
|
||||
'Skull Woods - Pot Prison': (0xe9a1, 0x186175, False, 'in Skull Woods'),
|
||||
'Skull Woods - Pinball Room': (0xe9c8, 0x18619c, False, 'in Skull Woods'),
|
||||
'Skull Woods - Big Key Chest': (0xe99e, 0x186172, False, 'in Skull Woods'),
|
||||
'Skull Woods - Bridge Room': (0xe9fe, 0x1861d2, False, 'near Mothula'),
|
||||
'Skull Woods - Boss': (0x180155, 0x186343, False, 'with Mothula'),
|
||||
'Ice Palace - Compass Chest': (0xe9d4, 0x1861a8, False, 'in Ice Palace'),
|
||||
'Ice Palace - Freezor Chest': (0xe995, 0x186169, False, 'in Ice Palace'),
|
||||
'Ice Palace - Big Chest': (0xe9aa, 0x18617e, False, 'in Ice Palace'),
|
||||
'Ice Palace - Iced T Room': (0xe9e3, 0x1861b7, False, 'in Ice Palace'),
|
||||
'Ice Palace - Spike Room': (0xe9e0, 0x1861b4, False, 'in Ice Palace'),
|
||||
'Ice Palace - Big Key Chest': (0xe9a4, 0x186178, False, 'in Ice Palace'),
|
||||
'Ice Palace - Map Chest': (0xe9dd, 0x1861b1, False, 'in Ice Palace'),
|
||||
'Ice Palace - Boss': (0x180157, 0x186345, False, 'with Kholdstare'),
|
||||
'Misery Mire - Big Chest': (0xea67, 0x18623b, False, 'in Misery Mire'),
|
||||
'Misery Mire - Map Chest': (0xea6a, 0x18623e, False, 'in Misery Mire'),
|
||||
'Misery Mire - Main Lobby': (0xea5e, 0x186232, False, 'in Misery Mire'),
|
||||
'Misery Mire - Bridge Chest': (0xea61, 0x186235, False, 'in Misery Mire'),
|
||||
'Misery Mire - Spike Chest': (0xe9da, 0x1861ae, False, 'in Misery Mire'),
|
||||
'Misery Mire - Compass Chest': (0xea64, 0x186238, False, 'in Misery Mire'),
|
||||
'Misery Mire - Big Key Chest': (0xea6d, 0x186241, False, 'in Misery Mire'),
|
||||
'Misery Mire - Boss': (0x180158, 0x186346, False, 'with Vitreous'),
|
||||
'Turtle Rock - Compass Chest': (0xea22, 0x1861f6, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Roller Room - Left': (0xea1c, 0x1861f0, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Roller Room - Right': (0xea1f, 0x1861f3, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Chain Chomps': (0xea16, 0x1861ea, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Big Key Chest': (0xea25, 0x1861f9, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Big Chest': (0xea19, 0x1861ed, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Crystaroller Room': (0xea34, 0x186208, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Eye Bridge - Bottom Left': (0xea31, 0x186205, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Eye Bridge - Bottom Right': (0xea2e, 0x186202, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Eye Bridge - Top Left': (0xea2b, 0x1861ff, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Eye Bridge - Top Right': (0xea28, 0x1861fc, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Boss': (0x180159, 0x186347, False, 'with Trinexx'),
|
||||
'Palace of Darkness - Shooter Room': (0xea5b, 0x18622f, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - The Arena - Bridge': (0xea3d, 0x186211, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Stalfos Basement': (0xea49, 0x18621d, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Big Key Chest': (0xea37, 0x18620b, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - The Arena - Ledge': (0xea3a, 0x18620e, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Map Chest': (0xea52, 0x186226, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Compass Chest': (0xea43, 0x186217, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Dark Basement - Left': (0xea4c, 0x186220, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Dark Basement - Right': (0xea4f, 0x186223, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Dark Maze - Top': (0xea55, 0x186229, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Dark Maze - Bottom': (0xea58, 0x18622c, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Big Chest': (0xea40, 0x186214, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Harmless Hellway': (0xea46, 0x18621a, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Boss': (0x180153, 0x186341, False, 'with Helmasaur King'),
|
||||
"Ganons Tower - Bob's Torch": (0x180161, 0x186363, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Hope Room - Left': (0xead9, 0x1862ad, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Hope Room - Right': (0xeadc, 0x1862b0, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Tile Room': (0xeae2, 0x1862b6, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Compass Room - Top Left': (0xeae5, 0x1862b9, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Compass Room - Top Right': (0xeae8, 0x1862bc, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Compass Room - Bottom Left': (0xeaeb, 0x1862bf, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Compass Room - Bottom Right': (0xeaee, 0x1862c2, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - DMs Room - Top Left': (0xeab8, 0x18628c, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - DMs Room - Top Right': (0xeabb, 0x18628f, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - DMs Room - Bottom Left': (0xeabe, 0x186292, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - DMs Room - Bottom Right': (0xeac1, 0x186295, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Map Chest': (0xead3, 0x1862a7, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Firesnake Room': (0xead0, 0x1862a4, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Randomizer Room - Top Left': (0xeac4, 0x186298, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Randomizer Room - Top Right': (0xeac7, 0x18629b, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Randomizer Room - Bottom Left': (0xeaca, 0x18629e, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Randomizer Room - Bottom Right': (0xeacd, 0x1862a1, False, "in Ganon's Tower"),
|
||||
"Ganons Tower - Bob's Chest": (0xeadf, 0x1862b3, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Big Chest': (0xead6, 0x1862aa, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Big Key Room - Left': (0xeaf4, 0x1862c8, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Big Key Room - Right': (0xeaf7, 0x1862cb, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Big Key Chest': (0xeaf1, 0x1862c5, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Mini Helmasaur Room - Left': (0xeafd, 0x1862d1, False, "atop Ganon's Tower"),
|
||||
'Ganons Tower - Mini Helmasaur Room - Right': (0xeb00, 0x1862d4, False, "atop Ganon's Tower"),
|
||||
'Ganons Tower - Pre-Moldorm Chest': (0xeb03, 0x1862d7, False, "atop Ganon's Tower"),
|
||||
'Ganons Tower - Validation Chest': (0xeb06, 0x1862da, False, "atop Ganon's Tower"),
|
||||
'Ganon': (None, None, False, 'from me'),
|
||||
'Agahnim 1': (None, None, False, 'from Ganon\'s wizardry form'),
|
||||
'Agahnim 2': (None, None, False, 'from Ganon\'s wizardry form'),
|
||||
'Floodgate': (None, None, False, None),
|
||||
'Frog': (None, None, False, None),
|
||||
'Missing Smith': (None, None, False, None),
|
||||
'Dark Blacksmith Ruins': (None, None, False, None),
|
||||
'Eastern Palace - Prize': ([0x1209D, 0x53EF8, 0x53EF9, 0x180052, 0x18007C, 0xC6FE], None, True, 'Eastern Palace'),
|
||||
'Desert Palace - Prize': ([0x1209E, 0x53F1C, 0x53F1D, 0x180053, 0x180078, 0xC6FF], None, True, 'Desert Palace'),
|
||||
'Tower of Hera - Prize': ([0x120A5, 0x53F0A, 0x53F0B, 0x18005A, 0x18007A, 0xC706], None, True, 'Tower of Hera'),
|
||||
'Palace of Darkness - Prize': ([0x120A1, 0x53F00, 0x53F01, 0x180056, 0x18007D, 0xC702], None, True, 'Palace of Darkness'),
|
||||
'Swamp Palace - Prize': ([0x120A0, 0x53F6C, 0x53F6D, 0x180055, 0x180071, 0xC701], None, True, 'Swamp Palace'),
|
||||
'Thieves\' Town - Prize': ([0x120A6, 0x53F36, 0x53F37, 0x18005B, 0x180077, 0xC707], None, True, 'Thieves\' Town'),
|
||||
'Skull Woods - Prize': ([0x120A3, 0x53F12, 0x53F13, 0x180058, 0x18007B, 0xC704], None, True, 'Skull Woods'),
|
||||
'Ice Palace - Prize': ([0x120A4, 0x53F5A, 0x53F5B, 0x180059, 0x180073, 0xC705], None, True, 'Ice Palace'),
|
||||
'Misery Mire - Prize': ([0x120A2, 0x53F48, 0x53F49, 0x180057, 0x180075, 0xC703], None, True, 'Misery Mire'),
|
||||
'Turtle Rock - Prize': ([0x120A7, 0x53F24, 0x53F25, 0x18005C, 0x180079, 0xC708], None, True, 'Turtle Rock')}
|
||||
|
||||
+93
-44
@@ -125,26 +125,23 @@ difficulties = {
|
||||
}
|
||||
|
||||
def generate_itempool(world, player):
|
||||
if (world.difficulty not in ['normal', 'hard', 'expert'] or world.goal 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']):
|
||||
if (world.difficulty[player] not in ['normal', 'hard', 'expert'] or world.goal[player] not in ['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'crystals']
|
||||
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')
|
||||
|
||||
if world.timer in ['ohko', 'timed-ohko']:
|
||||
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)
|
||||
else:
|
||||
world.push_item(world.get_location('Ganon', player), ItemFactory('Triforce', player), False)
|
||||
|
||||
if world.goal in ['triforcehunt']:
|
||||
if world.mode == 'inverted':
|
||||
if world.goal[player] in ['triforcehunt']:
|
||||
region = world.get_region('Light World',player)
|
||||
else:
|
||||
region = world.get_region('Hyrule Castle Courtyard', player)
|
||||
|
||||
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)
|
||||
world.dynamic_locations.append(loc)
|
||||
|
||||
@@ -177,38 +174,82 @@ def generate_itempool(world, player):
|
||||
|
||||
# set up item pool
|
||||
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[69], 9999)
|
||||
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)
|
||||
world.itempool += ItemFactory(pool, player)
|
||||
(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])
|
||||
|
||||
for item in precollected_items:
|
||||
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.get_location(location, player).event = True
|
||||
world.get_location(location, player).locked = True
|
||||
|
||||
items = ItemFactory(pool, player)
|
||||
|
||||
world.lamps_needed_for_dark_rooms = lamps_needed_for_dark_rooms
|
||||
|
||||
if clock_mode is not None:
|
||||
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:
|
||||
world.itempool.extend([item for item in get_dungeon_item_pool(world) if item.player == player])
|
||||
if treasure_hunt_count is not None:
|
||||
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)
|
||||
# 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)
|
||||
if world.difficulty 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
|
||||
elif world.difficulty 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]
|
||||
if world.difficulty[player] in ['normal', 'hard'] and not (world.custom and world.customitemarray[30] == 0):
|
||||
[item for item in items if item.name == 'Boss Heart Container'][0].advancement = True
|
||||
elif world.difficulty[player] in ['expert'] and not (world.custom and world.customitemarray[29] < 4):
|
||||
adv_heart_pieces = [item for item in items if item.name == 'Piece of Heart'][0:4]
|
||||
for hp in adv_heart_pieces:
|
||||
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
|
||||
mm_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)]
|
||||
tr_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)]
|
||||
@@ -217,7 +258,7 @@ def generate_itempool(world, player):
|
||||
place_bosses(world, player)
|
||||
set_up_shops(world, player)
|
||||
|
||||
if world.retro:
|
||||
if world.retro[player]:
|
||||
set_up_take_anys(world, player)
|
||||
|
||||
create_dynamic_shop_locations(world, player)
|
||||
@@ -233,7 +274,7 @@ take_any_locations = [
|
||||
'Dark Lake Hylia Ledge Spike Cave', 'Fortune Teller (Dark)', 'Dark Sanctuary Hint', 'Dark Desert Hint']
|
||||
|
||||
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')
|
||||
|
||||
regions = random.sample(take_any_locations, 5)
|
||||
@@ -275,7 +316,7 @@ def set_up_take_anys(world, player):
|
||||
take_any.shop.add_inventory(0, 'Blue Potion', 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):
|
||||
for shop in world.shops:
|
||||
@@ -312,9 +353,9 @@ def fill_prizes(world, attempts=15):
|
||||
prize_locs = list(empty_crystal_locations)
|
||||
random.shuffle(prizepool)
|
||||
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:
|
||||
logging.getLogger('').info("Failed to place dungeon prizes (%s). Will retry %s more times", e, attempts)
|
||||
logging.getLogger('').info("Failed to place dungeon prizes (%s). Will retry %s more times", e, attempts - attempt - 1)
|
||||
for location in empty_crystal_locations:
|
||||
location.item = None
|
||||
continue
|
||||
@@ -330,13 +371,13 @@ def set_up_shops(world, player):
|
||||
for shop in world.shops:
|
||||
shop.active = True
|
||||
|
||||
if world.retro:
|
||||
if world.retro[player]:
|
||||
rss = world.get_region('Red Shield Shop', player).shop
|
||||
rss.active = True
|
||||
rss.add_inventory(2, 'Single Arrow', 80)
|
||||
|
||||
# Randomized changes to Shops
|
||||
if world.retro:
|
||||
if world.retro[player]:
|
||||
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)
|
||||
@@ -347,7 +388,7 @@ def set_up_shops(world, player):
|
||||
|
||||
def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, retro):
|
||||
pool = []
|
||||
placed_items = []
|
||||
placed_items = {}
|
||||
precollected_items = []
|
||||
clock_mode = None
|
||||
treasure_hunt_count = None
|
||||
@@ -355,6 +396,10 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, r
|
||||
|
||||
pool.extend(alwaysitems)
|
||||
|
||||
def place_item(loc, item):
|
||||
assert loc not in placed_items
|
||||
placed_items[loc] = item
|
||||
|
||||
def want_progressives():
|
||||
return random.choice([True, False]) if progressive == 'random' else progressive == 'on'
|
||||
|
||||
@@ -367,8 +412,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
|
||||
if shuffle == 'insanity_legacy':
|
||||
placed_items.append(('Link\'s House', 'Magic Mirror'))
|
||||
placed_items.append(('Sanctuary', 'Moon Pearl'))
|
||||
place_item('Link\'s House', 'Magic Mirror')
|
||||
place_item('Sanctuary', 'Moon Pearl')
|
||||
else:
|
||||
pool.extend(['Magic Mirror', 'Moon Pearl'])
|
||||
|
||||
@@ -429,13 +474,13 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, r
|
||||
swords_to_use.extend(['Fighter Sword'])
|
||||
random.shuffle(swords_to_use)
|
||||
|
||||
placed_items.append(('Link\'s Uncle', swords_to_use.pop()))
|
||||
placed_items.append(('Blacksmith', swords_to_use.pop()))
|
||||
placed_items.append(('Pyramid Fairy - Left', swords_to_use.pop()))
|
||||
place_item('Link\'s Uncle', swords_to_use.pop())
|
||||
place_item('Blacksmith', swords_to_use.pop())
|
||||
place_item('Pyramid Fairy - Left', swords_to_use.pop())
|
||||
if goal != 'pedestal':
|
||||
placed_items.append(('Master Sword Pedestal', swords_to_use.pop()))
|
||||
place_item('Master Sword Pedestal', swords_to_use.pop())
|
||||
else:
|
||||
placed_items.append(('Master Sword Pedestal', 'Triforce'))
|
||||
place_item('Master Sword Pedestal', 'Triforce')
|
||||
else:
|
||||
if want_progressives():
|
||||
pool.extend(diff.progressivesword)
|
||||
@@ -466,7 +511,7 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, r
|
||||
extraitems -= len(extra)
|
||||
|
||||
if goal == 'pedestal' and swords != 'vanilla':
|
||||
placed_items.append(('Master Sword Pedestal', 'Triforce'))
|
||||
place_item('Master Sword Pedestal', 'Triforce')
|
||||
if retro:
|
||||
pool = [item.replace('Single Arrow','Rupees (5)') for item in pool]
|
||||
pool = [item.replace('Arrows (10)','Rupees (5)') for item in pool]
|
||||
@@ -475,19 +520,23 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, r
|
||||
pool.extend(diff.retro)
|
||||
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'])
|
||||
placed_items.append((key_location, 'Small Key (Universal)'))
|
||||
place_item(key_location, 'Small Key (Universal)')
|
||||
else:
|
||||
pool.extend(['Small Key (Universal)'])
|
||||
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):
|
||||
pool = []
|
||||
placed_items = []
|
||||
placed_items = {}
|
||||
precollected_items = []
|
||||
clock_mode = None
|
||||
treasure_hunt_count = 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.
|
||||
for x in range(0, 66):
|
||||
if customitemarray[x] > total_items_to_place:
|
||||
@@ -594,13 +643,13 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s
|
||||
clock_mode = 'ohko'
|
||||
|
||||
if goal == 'pedestal':
|
||||
placed_items.append(('Master Sword Pedestal', 'Triforce'))
|
||||
place_item('Master Sword Pedestal', 'Triforce')
|
||||
itemtotal = itemtotal + 1
|
||||
|
||||
if mode == 'standard':
|
||||
if retro:
|
||||
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[70] - 1), 0))
|
||||
else:
|
||||
pool.extend(['Small Key (Universal)'] * customitemarray[70])
|
||||
@@ -611,8 +660,8 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s
|
||||
pool.extend(['Progressive Sword'] * customitemarray[36])
|
||||
|
||||
if shuffle == 'insanity_legacy':
|
||||
placed_items.append(('Link\'s House', 'Magic Mirror'))
|
||||
placed_items.append(('Sanctuary', 'Moon Pearl'))
|
||||
place_item('Link\'s House', 'Magic Mirror')
|
||||
place_item('Sanctuary', 'Moon Pearl')
|
||||
pool.extend(['Magic Mirror'] * max((customitemarray[22] -1 ), 0))
|
||||
pool.extend(['Moon Pearl'] * max((customitemarray[28] - 1), 0))
|
||||
else:
|
||||
|
||||
@@ -161,11 +161,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'),
|
||||
'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'),
|
||||
'Red Potion': (False, False, None, 0x2E, None, None, None, None, None, None, None),
|
||||
'Green Potion': (False, False, None, 0x2F, None, None, None, None, None, None, None),
|
||||
'Blue Potion': (False, False, None, 0x30, None, None, None, None, None, None, None),
|
||||
'Bee': (False, False, None, 0x0E, None, None, None, None, None, None, None),
|
||||
'Small Heart': (False, False, None, 0x42, 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'),
|
||||
'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'),
|
||||
'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'),
|
||||
'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'),
|
||||
'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 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),
|
||||
|
||||
@@ -4,6 +4,7 @@ from itertools import zip_longest
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pickle
|
||||
import random
|
||||
import time
|
||||
|
||||
@@ -11,20 +12,27 @@ from BaseClasses import World, CollectionState, Item, Region, Location, Shop
|
||||
from Regions import create_regions, mark_light_world_regions
|
||||
from InvertedRegions import create_inverted_regions, mark_dark_world_regions
|
||||
from EntranceShuffle import link_entrances, link_inverted_entrances
|
||||
from Rom import patch_rom, get_enemizer_patch, apply_rom_settings, Sprite, LocalRom, JsonRom
|
||||
from Rom import patch_rom, get_race_rom_patches, get_enemizer_patch, apply_rom_settings, Sprite, LocalRom, JsonRom
|
||||
from Rules import set_rules
|
||||
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 ItemList import generate_itempool, difficulties, fill_prizes
|
||||
from Utils import output_path
|
||||
from Utils import output_path, parse_names_string
|
||||
|
||||
__version__ = '0.6.3-pre'
|
||||
|
||||
def main(args, seed=None):
|
||||
if args.outputpath:
|
||||
try:
|
||||
os.mkdir(args.outputpath)
|
||||
except OSError:
|
||||
pass
|
||||
output_path.cached_path = args.outputpath
|
||||
|
||||
start = time.perf_counter()
|
||||
|
||||
# initialize the world
|
||||
world = World(args.multi, args.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.logic, args.mode, args.swords, args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm, args.accessibility, args.shuffleganon, args.quickswap, args.fastmenu, args.disablemusic, args.retro, args.custom, args.customitemarray, args.hints)
|
||||
logger = logging.getLogger('')
|
||||
if seed is None:
|
||||
random.seed(None)
|
||||
@@ -33,36 +41,44 @@ def main(args, seed=None):
|
||||
world.seed = int(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.crystals_needed_for_gt = random.randint(0, 7) if args.crystals_gt == 'random' else int(args.crystals_gt)
|
||||
world.mapshuffle = args.mapshuffle.copy()
|
||||
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)}
|
||||
|
||||
logger.info('ALttP Entrance 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':
|
||||
for player in range(1, world.players + 1):
|
||||
if world.mode[player] == 'standard' and world.enemy_shuffle[player] != 'none':
|
||||
world.escape_assist[player].append('bombs') # enemized escape assumes infinite bombs available and will likely be unbeatable without it
|
||||
|
||||
if world.mode[player] != 'inverted':
|
||||
create_regions(world, player)
|
||||
create_dungeons(world, player)
|
||||
else:
|
||||
for player in range(1, world.players + 1):
|
||||
create_inverted_regions(world, player)
|
||||
create_dungeons(world, player)
|
||||
|
||||
logger.info('Shuffling the World about.')
|
||||
|
||||
if world.mode != 'inverted':
|
||||
for player in range(1, world.players + 1):
|
||||
if world.mode[player] != 'inverted':
|
||||
link_entrances(world, player)
|
||||
|
||||
mark_light_world_regions(world)
|
||||
mark_light_world_regions(world, player)
|
||||
else:
|
||||
for player in range(1, world.players + 1):
|
||||
link_inverted_entrances(world, player)
|
||||
|
||||
mark_dark_world_regions(world)
|
||||
mark_dark_world_regions(world, player)
|
||||
|
||||
logger.info('Generating Item Pool.')
|
||||
|
||||
@@ -81,7 +97,8 @@ def main(args, seed=None):
|
||||
logger.info('Placing Dungeon Items.')
|
||||
|
||||
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()
|
||||
random.shuffle(shuffled_locations)
|
||||
fill_dungeons_restrictive(world, shuffled_locations)
|
||||
@@ -99,12 +116,12 @@ def main(args, seed=None):
|
||||
elif args.algorithm == 'freshness':
|
||||
distribute_items_staleness(world)
|
||||
elif args.algorithm == 'vt25':
|
||||
distribute_items_restrictive(world, 0)
|
||||
distribute_items_restrictive(world, False)
|
||||
elif args.algorithm == 'vt26':
|
||||
|
||||
distribute_items_restrictive(world, gt_filler(world), shuffled_locations)
|
||||
distribute_items_restrictive(world, True, shuffled_locations)
|
||||
elif args.algorithm == 'balanced':
|
||||
distribute_items_restrictive(world, gt_filler(world))
|
||||
distribute_items_restrictive(world, True)
|
||||
|
||||
if world.players > 1:
|
||||
logger.info('Balancing multiworld progression.')
|
||||
@@ -120,16 +137,19 @@ def main(args, seed=None):
|
||||
else:
|
||||
sprite = None
|
||||
|
||||
outfilebase = 'ER_%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, "-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)
|
||||
player_names = parse_names_string(args.names)
|
||||
outfilebase = 'ER_%s' % (args.outputname if args.outputname else world.seed)
|
||||
|
||||
jsonout = {}
|
||||
if not args.suppress_rom:
|
||||
if world.players > 1:
|
||||
raise NotImplementedError("Multiworld rom writes have not been implemented")
|
||||
else:
|
||||
player = 1
|
||||
from MultiServer import MultiWorld
|
||||
multidata = MultiWorld()
|
||||
multidata.players = world.players
|
||||
|
||||
for player in range(1, world.players + 1):
|
||||
use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player] != 'none'
|
||||
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
|
||||
or args.shufflepalette[player] or args.shufflepots[player])
|
||||
|
||||
local_rom = None
|
||||
if args.jsonout:
|
||||
@@ -140,24 +160,56 @@ def main(args, seed=None):
|
||||
rom = JsonRom()
|
||||
else:
|
||||
rom = LocalRom(args.rom)
|
||||
|
||||
patch_rom(world, player, rom)
|
||||
patch_rom(world, player, rom, use_enemizer)
|
||||
|
||||
enemizer_patch = []
|
||||
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 use_enemizer and (args.enemizercli or not args.jsonout):
|
||||
enemizer_patch = get_enemizer_patch(world, player, rom, args.rom, args.enemizercli, args.shufflepalette[player], args.shufflepots[player])
|
||||
|
||||
multidata.rom_names[player] = list(rom.name)
|
||||
for location in world.get_filled_locations(player):
|
||||
if type(location.address) is int:
|
||||
multidata.locations[(location.address, player)] = (location.item.code, location.item.player)
|
||||
|
||||
if args.jsonout:
|
||||
jsonout['patch'] = rom.patches
|
||||
jsonout[f'patch{player}'] = rom.patches
|
||||
if use_enemizer:
|
||||
jsonout['enemizer' % player] = enemizer_patch
|
||||
jsonout[f'enemizer{player}'] = enemizer_patch
|
||||
if args.race:
|
||||
jsonout[f'race{player}'] = get_race_rom_patches(rom)
|
||||
else:
|
||||
if use_enemizer:
|
||||
local_rom.patch_enemizer(rom.patches, os.path.join(os.path.dirname(args.enemizercli), "enemizerBasePatch.json"), enemizer_patch)
|
||||
rom = local_rom
|
||||
|
||||
apply_rom_settings(rom, args.heartbeep, args.heartcolor, world.quickswap, world.fastmenu, world.disable_music, sprite)
|
||||
rom.write_to_file(output_path('%s.sfc' % outfilebase))
|
||||
if args.race:
|
||||
for addr, values in get_race_rom_patches(rom).items():
|
||||
rom.write_bytes(int(addr), values)
|
||||
|
||||
apply_rom_settings(rom, args.heartbeep, args.heartcolor, world.quickswap, world.fastmenu, world.disable_music, sprite, player_names)
|
||||
|
||||
mcsb_name = ''
|
||||
if all([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player], world.bigkeyshuffle[player]]):
|
||||
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 '')
|
||||
|
||||
playername = f"{f'_P{player}' if world.players > 1 else ''}{f'_{player_names[player]}' if player in player_names else ''}"
|
||||
outfilesuffix = ('_%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.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'))
|
||||
|
||||
with open(output_path('%s_multidata' % outfilebase), 'wb') as f:
|
||||
pickle.dump(multidata, f, pickle.HIGHEST_PROTOCOL)
|
||||
|
||||
if args.create_spoiler and not args.jsonout:
|
||||
world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase))
|
||||
@@ -176,42 +228,45 @@ def main(args, seed=None):
|
||||
|
||||
return world
|
||||
|
||||
def gt_filler(world):
|
||||
if world.goal == 'triforcehunt':
|
||||
return random.randint(15, 50)
|
||||
return random.randint(0, 15)
|
||||
|
||||
def copy_world(world):
|
||||
# ToDo: Not good yet
|
||||
ret = World(world.players, world.shuffle, 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.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, world.accessibility, world.shuffle_ganon, world.quickswap, world.fastmenu, world.disable_music, world.retro, world.custom, world.customitemarray, world.hints)
|
||||
ret.required_medallions = world.required_medallions.copy()
|
||||
ret.swamp_patch_required = world.swamp_patch_required.copy()
|
||||
ret.ganon_at_pyramid = world.ganon_at_pyramid.copy()
|
||||
ret.powder_patch_required = world.powder_patch_required.copy()
|
||||
ret.ganonstower_vanilla = world.ganonstower_vanilla.copy()
|
||||
ret.treasure_hunt_count = world.treasure_hunt_count
|
||||
ret.treasure_hunt_icon = world.treasure_hunt_icon
|
||||
ret.sewer_light_cone = world.sewer_light_cone
|
||||
ret.treasure_hunt_count = world.treasure_hunt_count.copy()
|
||||
ret.treasure_hunt_icon = world.treasure_hunt_icon.copy()
|
||||
ret.sewer_light_cone = world.sewer_light_cone.copy()
|
||||
ret.light_world_light_cone = world.light_world_light_cone
|
||||
ret.dark_world_light_cone = world.dark_world_light_cone
|
||||
ret.seed = world.seed
|
||||
ret.can_access_trock_eyebridge = world.can_access_trock_eyebridge
|
||||
ret.can_access_trock_front = world.can_access_trock_front
|
||||
ret.can_access_trock_big_chest = world.can_access_trock_big_chest
|
||||
ret.can_access_trock_middle = world.can_access_trock_middle
|
||||
ret.can_access_trock_eyebridge = world.can_access_trock_eyebridge.copy()
|
||||
ret.can_access_trock_front = world.can_access_trock_front.copy()
|
||||
ret.can_access_trock_big_chest = world.can_access_trock_big_chest.copy()
|
||||
ret.can_access_trock_middle = world.can_access_trock_middle.copy()
|
||||
ret.can_take_damage = world.can_take_damage
|
||||
ret.difficulty_requirements = world.difficulty_requirements
|
||||
ret.fix_fake_world = world.fix_fake_world
|
||||
ret.difficulty_requirements = world.difficulty_requirements.copy()
|
||||
ret.fix_fake_world = world.fix_fake_world.copy()
|
||||
ret.lamps_needed_for_dark_rooms = world.lamps_needed_for_dark_rooms
|
||||
ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon
|
||||
ret.crystals_needed_for_gt = world.crystals_needed_for_gt
|
||||
ret.mapshuffle = world.mapshuffle.copy()
|
||||
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):
|
||||
if world.mode[player] != 'inverted':
|
||||
create_regions(ret, player)
|
||||
create_dungeons(ret, player)
|
||||
else:
|
||||
for player in range(1, world.players + 1):
|
||||
create_inverted_regions(ret, player)
|
||||
create_dungeons(ret, player)
|
||||
|
||||
@@ -263,8 +318,8 @@ def copy_world(world):
|
||||
def copy_dynamic_regions_and_locations(world, ret):
|
||||
for region in world.dynamic_regions:
|
||||
new_reg = Region(region.name, region.type, region.hint_text, region.player)
|
||||
new_reg.world = ret
|
||||
ret.regions.append(new_reg)
|
||||
ret.initialize_regions([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
|
||||
@@ -292,7 +347,7 @@ def create_playthrough(world):
|
||||
world = copy_world(world)
|
||||
|
||||
# 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!')
|
||||
|
||||
# get locations containing progress items
|
||||
@@ -303,7 +358,6 @@ def create_playthrough(world):
|
||||
sphere_candidates = list(prog_locations)
|
||||
logging.getLogger('').debug('Building up collection spheres.')
|
||||
while sphere_candidates:
|
||||
if not world.keysanity:
|
||||
state.sweep_for_events(key_only=True)
|
||||
|
||||
sphere = []
|
||||
@@ -323,9 +377,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))
|
||||
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])
|
||||
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.')
|
||||
else:
|
||||
old_world.spoiler.unreachables = sphere_candidates.copy()
|
||||
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
|
||||
@@ -357,7 +412,6 @@ def create_playthrough(world):
|
||||
state = CollectionState(world)
|
||||
collection_spheres = []
|
||||
while required_locations:
|
||||
if not world.keysanity:
|
||||
state.sweep_for_events(key_only=True)
|
||||
|
||||
sphere = list(filter(state.can_reach, required_locations))
|
||||
@@ -393,7 +447,7 @@ 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})
|
||||
for _, path in dict(old_world.spoiler.paths).items():
|
||||
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))
|
||||
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))
|
||||
|
||||
+921
@@ -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 = 'ws://' + 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()
|
||||
+393
@@ -0,0 +1,393 @@
|
||||
import aioconsole
|
||||
import argparse
|
||||
import asyncio
|
||||
import functools
|
||||
import json
|
||||
import logging
|
||||
import pickle
|
||||
import re
|
||||
import urllib.request
|
||||
import websockets
|
||||
|
||||
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 MultiWorld:
|
||||
def __init__(self):
|
||||
self.players = None
|
||||
self.rom_names = {}
|
||||
self.locations = {}
|
||||
|
||||
class Context:
|
||||
def __init__(self, host, port, password):
|
||||
self.data_filename = None
|
||||
self.save_filename = None
|
||||
self.disable_save = False
|
||||
self.world = MultiWorld()
|
||||
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.world.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.world.locations:
|
||||
target_item, target_player = ctx.world.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:
|
||||
pickle.dump((ctx.world.players, ctx.world.rom_names, ctx.received_items), f, pickle.HIGHEST_PROTOCOL)
|
||||
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.world.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.world.players:
|
||||
errors.add('SlotAlreadyTaken')
|
||||
elif args['slot'] not in range(1, ctx.world.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.world.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:
|
||||
ctx.world = pickle.load(f)
|
||||
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.world.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:
|
||||
players, rom_names, received_items = pickle.load(f)
|
||||
if players != ctx.world.players or rom_names != ctx.world.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()
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
import argparse
|
||||
import logging
|
||||
import random
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
|
||||
from EntranceRandomizer import parse_arguments
|
||||
from Main import main as ERmain
|
||||
|
||||
def parse_yaml(txt):
|
||||
ret = {}
|
||||
indents = {len(txt) - len(txt.lstrip(' ')): ret}
|
||||
for line in txt.splitlines():
|
||||
if not line:
|
||||
continue
|
||||
name, val = line.split(':', 1)
|
||||
val = val.strip()
|
||||
spaces = len(name) - len(name.lstrip(' '))
|
||||
name = name.strip()
|
||||
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
|
||||
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():
|
||||
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)
|
||||
|
||||
ERmain(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):
|
||||
return random.choices(list(weights[option].keys()), weights=list(map(int,weights[option].values())))[0].replace('"','').replace("'",'')
|
||||
|
||||
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
|
||||
|
||||
if {'map_shuffle', 'compass_shuffle', 'smallkey_shuffle', 'bigkey_shuffle'}.issubset(weights.keys()):
|
||||
ret.mapshuffle = get_choice('map_shuffle') == 'on'
|
||||
ret.compassshuffle = get_choice('compass_shuffle') == 'on'
|
||||
ret.keyshuffle = get_choice('smallkey_shuffle') == 'on'
|
||||
ret.bigkeyshuffle = get_choice('bigkey_shuffle') == 'on'
|
||||
else:
|
||||
dungeon_items = get_choice('dungeon_items')
|
||||
ret.mapshuffle = dungeon_items in ['mc', 'mcs', 'full']
|
||||
ret.compassshuffle = dungeon_items in ['mc', 'mcs', 'full']
|
||||
ret.keyshuffle = dungeon_items in ['mcs', 'full']
|
||||
ret.bigkeyshuffle = dungeon_items in ['full']
|
||||
|
||||
accessibility = get_choice('accessibility')
|
||||
ret.accessibility = accessibility
|
||||
|
||||
entrance_shuffle = get_choice('entrance_shuffle')
|
||||
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
|
||||
|
||||
goals = get_choice('goals')
|
||||
ret.goal = {'ganon': 'ganon',
|
||||
'fast_ganon': 'crystals',
|
||||
'dungeons': 'dungeons',
|
||||
'pedestal': 'pedestal',
|
||||
'triforce-hunt': 'triforcehunt'
|
||||
}[goals]
|
||||
ret.openpyramid = goals == 'fast_ganon'
|
||||
|
||||
tower_open = get_choice('tower_open')
|
||||
ret.crystals_gt = tower_open
|
||||
|
||||
ganon_open = get_choice('ganon_open')
|
||||
ret.crystals_ganon = ganon_open
|
||||
|
||||
world_state = get_choice('world_state')
|
||||
ret.mode = world_state
|
||||
if world_state == 'retro':
|
||||
ret.mode = 'open'
|
||||
ret.retro = True
|
||||
|
||||
hints = get_choice('hints')
|
||||
ret.hints = hints == 'on'
|
||||
|
||||
weapons = get_choice('weapons')
|
||||
ret.swords = {'randomized': 'random',
|
||||
'assured': 'assured',
|
||||
'vanilla': 'vanilla',
|
||||
'swordless': 'swordless'
|
||||
}[weapons]
|
||||
|
||||
item_pool = get_choice('item_pool')
|
||||
ret.difficulty = item_pool
|
||||
|
||||
item_functionality = get_choice('item_functionality')
|
||||
ret.item_functionality = item_functionality
|
||||
|
||||
boss_shuffle = get_choice('boss_shuffle')
|
||||
ret.shufflebosses = {'none': 'none',
|
||||
'simple': 'basic',
|
||||
'full': 'normal',
|
||||
'random': 'chaos'
|
||||
}[boss_shuffle]
|
||||
|
||||
enemy_shuffle = get_choice('enemy_shuffle')
|
||||
ret.shuffleenemies = {'none': 'none',
|
||||
'shuffled': 'shuffled',
|
||||
'random': 'chaos'
|
||||
}[enemy_shuffle]
|
||||
|
||||
enemy_damage = get_choice('enemy_damage')
|
||||
ret.enemy_damage = {'default': 'default',
|
||||
'shuffled': 'shuffled',
|
||||
'random': 'chaos'
|
||||
}[enemy_damage]
|
||||
|
||||
enemy_health = get_choice('enemy_health')
|
||||
ret.enemy_health = enemy_health
|
||||
|
||||
ret.beemizer = int(get_choice('beemizer')) if 'beemizer' in weights.keys() else 1 # suck it :)
|
||||
|
||||
return ret
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -10,7 +10,7 @@ import sys
|
||||
from BaseClasses import World
|
||||
from Regions import create_regions
|
||||
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, Sprite, write_string_to_rom, apply_rom_settings
|
||||
from Rules import set_rules
|
||||
from Dungeons import create_dungeons
|
||||
from Items import ItemFactory
|
||||
@@ -23,7 +23,7 @@ def main(args):
|
||||
start_time = time.perf_counter()
|
||||
|
||||
# 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, args.quickswap, args.fastmenu, args.disablemusic, False, False, False, None, False)
|
||||
logger = logging.getLogger('')
|
||||
|
||||
hasher = hashlib.md5()
|
||||
@@ -36,7 +36,7 @@ def main(args):
|
||||
|
||||
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_dungeons(world, 1)
|
||||
@@ -74,7 +74,9 @@ def main(args):
|
||||
sprite = None
|
||||
|
||||
rom = LocalRom(args.rom)
|
||||
patch_rom(world, 1, rom, args.heartbeep, args.heartcolor, sprite)
|
||||
patch_rom(world, 1, rom)
|
||||
|
||||
apply_rom_settings(rom, args.heartbeep, args.heartcolor, world.quickswap, world.fastmenu, world.disable_music, sprite)
|
||||
|
||||
for textname, texttype, text in text_patches:
|
||||
if texttype == 'text':
|
||||
@@ -114,16 +116,16 @@ def fill_world(world, plando, text_patches):
|
||||
tr_medallion = medallionstr.strip()
|
||||
elif line.startswith('!mode'):
|
||||
_, modestr = line.split(':', 1)
|
||||
world.mode = modestr.strip()
|
||||
world.mode = {1: modestr.strip()}
|
||||
elif line.startswith('!logic'):
|
||||
_, logicstr = line.split(':', 1)
|
||||
world.logic = logicstr.strip()
|
||||
world.logic = {1: logicstr.strip()}
|
||||
elif line.startswith('!goal'):
|
||||
_, goalstr = line.split(':', 1)
|
||||
world.goal = goalstr.strip()
|
||||
world.goal = {1: goalstr.strip()}
|
||||
elif line.startswith('!light_cone_sewers'):
|
||||
_, 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'):
|
||||
_, lwconestr = line.split(':', 1)
|
||||
world.light_world_light_cone = lwconestr.strip().lower() == 'true'
|
||||
@@ -132,19 +134,19 @@ def fill_world(world, plando, text_patches):
|
||||
world.dark_world_light_cone = dwconestr.strip().lower() == 'true'
|
||||
elif line.startswith('!fix_trock_doors'):
|
||||
_, 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'):
|
||||
_, 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'):
|
||||
_, gtfstr = line.split(':', 1)
|
||||
world.fix_gtower_exit = gtfstr.strip().lower() == 'true'
|
||||
elif line.startswith('!fix_pod_exit'):
|
||||
_, 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'):
|
||||
_, 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'):
|
||||
_, chkbtstr = line.split(':', 1)
|
||||
world.check_beatable_only = chkbtstr.strip().lower() == 'true'
|
||||
@@ -172,7 +174,7 @@ def fill_world(world, plando, text_patches):
|
||||
item = ItemFactory(itemstr.strip(), 1)
|
||||
if item is not None:
|
||||
world.push_item(location, item)
|
||||
if item.key:
|
||||
if item.smallkey or item.bigkey:
|
||||
location.event = True
|
||||
elif '<=>' in line:
|
||||
entrance, exit = line.split('<=>', 1)
|
||||
|
||||
@@ -121,7 +121,7 @@ Does not invoke a timer.
|
||||
### Display
|
||||
|
||||
Displays a timer on-screen but does not alter the item pool.
|
||||
This will prevent the dungeon item count feature in Easy and Keysanity from working.
|
||||
This will prevent the dungeon item count feature in Easy and Compass shuffle from working.
|
||||
|
||||
### Timed
|
||||
|
||||
@@ -264,12 +264,12 @@ generate spoilers for statistical analysis.
|
||||
|
||||
Use to enable quick item swap with L/R buttons. Press L and R together to switch the state of items like the Mushroom/Powder pair.
|
||||
|
||||
## Keysanity
|
||||
## Map/Compass/Small Key/Big Key shuffle (aka Keysanity)
|
||||
|
||||
This setting allows dungeon specific items (Small Key, Big Key, Map, Compass) 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
|
||||
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
|
||||
@@ -422,10 +422,10 @@ Alters the rate at which the menu opens and closes. (default: normal)
|
||||
Disables game music, resulting in the game sound being just the SFX. (default: False)
|
||||
|
||||
```
|
||||
--keysanity
|
||||
--mapshuffle --compassshuffle --keyshuffle --bigkeyshuffle
|
||||
```
|
||||
|
||||
Enable Keysanity (default: False)
|
||||
Respectively enable Map/Compass/SmallKey/BigKey shuffle (default: False)
|
||||
|
||||
```
|
||||
--retro
|
||||
@@ -433,13 +433,6 @@ Enable Keysanity (default: False)
|
||||
|
||||
Enable Retro mode (default: False)
|
||||
|
||||
```
|
||||
--nodungeonitems
|
||||
```
|
||||
|
||||
If set, Compasses and Maps are removed from the dungeon item pools and replaced by empty chests that may end up anywhere in the world.
|
||||
This may lead to different amount of itempool items being placed in a dungeon than you are used to. (default: False)
|
||||
|
||||
```
|
||||
--heartbeep [{normal,half,quarter,off}]
|
||||
```
|
||||
|
||||
+239
-239
@@ -307,7 +307,7 @@ def create_regions(world, player):
|
||||
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()
|
||||
world.initialize_regions()
|
||||
|
||||
def create_lw_region(player, name, locations=None, exits=None):
|
||||
return _create_region(player, name, RegionType.LightWorld, 'Light World', locations, exits)
|
||||
@@ -331,14 +331,14 @@ def _create_region(player, name, type, hint='Hyrule', locations=None, exits=None
|
||||
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))
|
||||
address, player_address, crystal, hint_text = location_table[location]
|
||||
ret.locations.append(Location(player, location, address, crystal, hint_text, ret, player_address))
|
||||
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.
|
||||
# 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)
|
||||
while queue:
|
||||
current = queue.popleft()
|
||||
@@ -351,7 +351,7 @@ def mark_light_world_regions(world):
|
||||
seen.add(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)
|
||||
while queue:
|
||||
current = queue.popleft()
|
||||
@@ -397,236 +397,236 @@ default_shop_contents = {
|
||||
'Potion Shop': [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)],
|
||||
}
|
||||
|
||||
location_table = {'Mushroom': (0x180013, False, 'in the woods'),
|
||||
'Bottle Merchant': (0x2eb18, False, 'with a merchant'),
|
||||
'Flute Spot': (0x18014a, False, 'underground'),
|
||||
'Sunken Treasure': (0x180145, False, 'underwater'),
|
||||
'Purple Chest': (0x33d68, False, 'from a box'),
|
||||
"Blind's Hideout - Top": (0xeb0f, False, 'in a basement'),
|
||||
"Blind's Hideout - Left": (0xeb12, False, 'in a basement'),
|
||||
"Blind's Hideout - Right": (0xeb15, False, 'in a basement'),
|
||||
"Blind's Hideout - Far Left": (0xeb18, False, 'in a basement'),
|
||||
"Blind's Hideout - Far Right": (0xeb1b, False, 'in a basement'),
|
||||
"Link's Uncle": (0x2df45, False, 'with your uncle'),
|
||||
'Secret Passage': (0xe971, False, 'near your uncle'),
|
||||
'King Zora': (0xee1c3, False, 'at a high price'),
|
||||
"Zora's Ledge": (0x180149, False, 'near Zora'),
|
||||
'Waterfall Fairy - Left': (0xe9b0, False, 'near a fairy'),
|
||||
'Waterfall Fairy - Right': (0xe9d1, False, 'near a fairy'),
|
||||
"King's Tomb": (0xe97a, False, 'alone in a cave'),
|
||||
'Floodgate Chest': (0xe98c, False, 'in the dam'),
|
||||
"Link's House": (0xe9bc, False, 'in your home'),
|
||||
'Kakariko Tavern': (0xe9ce, False, 'in the bar'),
|
||||
'Chicken House': (0xe9e9, False, 'near poultry'),
|
||||
"Aginah's Cave": (0xe9f2, False, 'with Aginah'),
|
||||
"Sahasrahla's Hut - Left": (0xea82, False, 'near the elder'),
|
||||
"Sahasrahla's Hut - Middle": (0xea85, False, 'near the elder'),
|
||||
"Sahasrahla's Hut - Right": (0xea88, False, 'near the elder'),
|
||||
'Sahasrahla': (0x2f1fc, False, 'with the elder'),
|
||||
'Kakariko Well - Top': (0xea8e, False, 'in a well'),
|
||||
'Kakariko Well - Left': (0xea91, False, 'in a well'),
|
||||
'Kakariko Well - Middle': (0xea94, False, 'in a well'),
|
||||
'Kakariko Well - Right': (0xea97, False, 'in a well'),
|
||||
'Kakariko Well - Bottom': (0xea9a, False, 'in a well'),
|
||||
'Blacksmith': (0x18002a, False, 'with the smith'),
|
||||
'Magic Bat': (0x180015, False, 'with the bat'),
|
||||
'Sick Kid': (0x339cf, False, 'with the sick'),
|
||||
'Hobo': (0x33e7d, False, 'with the hobo'),
|
||||
'Lost Woods Hideout': (0x180000, False, 'near a thief'),
|
||||
'Lumberjack Tree': (0x180001, False, 'in a hole'),
|
||||
'Cave 45': (0x180003, False, 'alone in a cave'),
|
||||
'Graveyard Cave': (0x180004, False, 'alone in a cave'),
|
||||
'Checkerboard Cave': (0x180005, False, 'alone in a cave'),
|
||||
'Mini Moldorm Cave - Far Left': (0xeb42, False, 'near Moldorms'),
|
||||
'Mini Moldorm Cave - Left': (0xeb45, False, 'near Moldorms'),
|
||||
'Mini Moldorm Cave - Right': (0xeb48, False, 'near Moldorms'),
|
||||
'Mini Moldorm Cave - Far Right': (0xeb4b, False, 'near Moldorms'),
|
||||
'Mini Moldorm Cave - Generous Guy': (0x180010, False, 'near Moldorms'),
|
||||
'Ice Rod Cave': (0xeb4e, False, 'in a frozen cave'),
|
||||
'Bonk Rock Cave': (0xeb3f, False, 'alone in a cave'),
|
||||
'Library': (0x180012, False, 'near books'),
|
||||
'Potion Shop': (0x180014, False, 'near potions'),
|
||||
'Lake Hylia Island': (0x180144, False, 'on an island'),
|
||||
'Maze Race': (0x180142, False, 'at the race'),
|
||||
'Desert Ledge': (0x180143, False, 'in the desert'),
|
||||
'Desert Palace - Big Chest': (0xe98f, False, 'in Desert Palace'),
|
||||
'Desert Palace - Torch': (0x180160, False, 'in Desert Palace'),
|
||||
'Desert Palace - Map Chest': (0xe9b6, False, 'in Desert Palace'),
|
||||
'Desert Palace - Compass Chest': (0xe9cb, False, 'in Desert Palace'),
|
||||
'Desert Palace - Big Key Chest': (0xe9c2, False, 'in Desert Palace'),
|
||||
'Desert Palace - Boss': (0x180151, False, 'with Lanmolas'),
|
||||
'Eastern Palace - Compass Chest': (0xe977, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Big Chest': (0xe97d, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Cannonball Chest': (0xe9b3, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Big Key Chest': (0xe9b9, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Map Chest': (0xe9f5, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Boss': (0x180150, False, 'with the Armos'),
|
||||
'Master Sword Pedestal': (0x289b0, False, 'at the pedestal'),
|
||||
'Hyrule Castle - Boomerang Chest': (0xe974, False, 'in Hyrule Castle'),
|
||||
'Hyrule Castle - Map Chest': (0xeb0c, False, 'in Hyrule Castle'),
|
||||
"Hyrule Castle - Zelda's Chest": (0xeb09, False, 'in Hyrule Castle'),
|
||||
'Sewers - Dark Cross': (0xe96e, False, 'in the sewers'),
|
||||
'Sewers - Secret Room - Left': (0xeb5d, False, 'in the sewers'),
|
||||
'Sewers - Secret Room - Middle': (0xeb60, False, 'in the sewers'),
|
||||
'Sewers - Secret Room - Right': (0xeb63, False, 'in the sewers'),
|
||||
'Sanctuary': (0xea79, False, 'in Sanctuary'),
|
||||
'Castle Tower - Room 03': (0xeab5, False, 'in Castle Tower'),
|
||||
'Castle Tower - Dark Maze': (0xeab2, False, 'in Castle Tower'),
|
||||
'Old Man': (0xf69fa, False, 'with the old man'),
|
||||
'Spectacle Rock Cave': (0x180002, False, 'alone in a cave'),
|
||||
'Paradox Cave Lower - Far Left': (0xeb2a, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Lower - Left': (0xeb2d, 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, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Lower - Middle': (0xeb36, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Upper - Left': (0xeb39, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Upper - Right': (0xeb3c, False, 'in a cave with seven chests'),
|
||||
'Spiral Cave': (0xe9bf, False, 'in spiral cave'),
|
||||
'Ether Tablet': (0x180016, False, 'at a monolith'),
|
||||
'Spectacle Rock': (0x180140, False, 'atop a rock'),
|
||||
'Tower of Hera - Basement Cage': (0x180162, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Map Chest': (0xe9ad, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Big Key Chest': (0xe9e6, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Compass Chest': (0xe9fb, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Big Chest': (0xe9f8, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Boss': (0x180152, False, 'with Moldorm'),
|
||||
'Pyramid': (0x180147, False, 'on the pyramid'),
|
||||
'Catfish': (0xee185, False, 'with a catfish'),
|
||||
'Stumpy': (0x330c7, False, 'with tree boy'),
|
||||
'Digging Game': (0x180148, False, 'underground'),
|
||||
'Bombos Tablet': (0x180017, False, 'at a monolith'),
|
||||
'Hype Cave - Top': (0xeb1e, False, 'near a bat-like man'),
|
||||
'Hype Cave - Middle Right': (0xeb21, False, 'near a bat-like man'),
|
||||
'Hype Cave - Middle Left': (0xeb24, False, 'near a bat-like man'),
|
||||
'Hype Cave - Bottom': (0xeb27, False, 'near a bat-like man'),
|
||||
'Hype Cave - Generous Guy': (0x180011, False, 'with a bat-like man'),
|
||||
'Peg Cave': (0x180006, False, 'alone in a cave'),
|
||||
'Pyramid Fairy - Left': (0xe980, False, 'near a fairy'),
|
||||
'Pyramid Fairy - Right': (0xe983, False, 'near a fairy'),
|
||||
'Brewery': (0xe9ec, False, 'alone in a home'),
|
||||
'C-Shaped House': (0xe9ef, False, 'alone in a home'),
|
||||
'Chest Game': (0xeda8, False, 'as a prize'),
|
||||
'Bumper Cave Ledge': (0x180146, False, 'on a ledge'),
|
||||
'Mire Shed - Left': (0xea73, False, 'near sparks'),
|
||||
'Mire Shed - Right': (0xea76, False, 'near sparks'),
|
||||
'Superbunny Cave - Top': (0xea7c, False, 'in a connection'),
|
||||
'Superbunny Cave - Bottom': (0xea7f, False, 'in a connection'),
|
||||
'Spike Cave': (0xea8b, False, 'beyond spikes'),
|
||||
'Hookshot Cave - Top Right': (0xeb51, False, 'across pits'),
|
||||
'Hookshot Cave - Top Left': (0xeb54, False, 'across pits'),
|
||||
'Hookshot Cave - Bottom Right': (0xeb5a, False, 'across pits'),
|
||||
'Hookshot Cave - Bottom Left': (0xeb57, False, 'across pits'),
|
||||
'Floating Island': (0x180141, False, 'on an island'),
|
||||
'Mimic Cave': (0xe9c5, False, 'in a cave of mimicry'),
|
||||
'Swamp Palace - Entrance': (0xea9d, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Map Chest': (0xe986, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Big Chest': (0xe989, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Compass Chest': (0xeaa0, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Big Key Chest': (0xeaa6, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - West Chest': (0xeaa3, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Flooded Room - Left': (0xeaa9, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Flooded Room - Right': (0xeaac, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Waterfall Room': (0xeaaf, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Boss': (0x180154, False, 'with Arrghus'),
|
||||
"Thieves' Town - Big Key Chest": (0xea04, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Map Chest": (0xea01, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Compass Chest": (0xea07, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Ambush Chest": (0xea0a, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Attic": (0xea0d, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Big Chest": (0xea10, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Blind's Cell": (0xea13, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Boss": (0x180156, False, 'with Blind'),
|
||||
'Skull Woods - Compass Chest': (0xe992, False, 'in Skull Woods'),
|
||||
'Skull Woods - Map Chest': (0xe99b, False, 'in Skull Woods'),
|
||||
'Skull Woods - Big Chest': (0xe998, False, 'in Skull Woods'),
|
||||
'Skull Woods - Pot Prison': (0xe9a1, False, 'in Skull Woods'),
|
||||
'Skull Woods - Pinball Room': (0xe9c8, False, 'in Skull Woods'),
|
||||
'Skull Woods - Big Key Chest': (0xe99e, False, 'in Skull Woods'),
|
||||
'Skull Woods - Bridge Room': (0xe9fe, False, 'near Mothula'),
|
||||
'Skull Woods - Boss': (0x180155, False, 'with Mothula'),
|
||||
'Ice Palace - Compass Chest': (0xe9d4, False, 'in Ice Palace'),
|
||||
'Ice Palace - Freezor Chest': (0xe995, False, 'in Ice Palace'),
|
||||
'Ice Palace - Big Chest': (0xe9aa, False, 'in Ice Palace'),
|
||||
'Ice Palace - Iced T Room': (0xe9e3, False, 'in Ice Palace'),
|
||||
'Ice Palace - Spike Room': (0xe9e0, False, 'in Ice Palace'),
|
||||
'Ice Palace - Big Key Chest': (0xe9a4, False, 'in Ice Palace'),
|
||||
'Ice Palace - Map Chest': (0xe9dd, False, 'in Ice Palace'),
|
||||
'Ice Palace - Boss': (0x180157, False, 'with Kholdstare'),
|
||||
'Misery Mire - Big Chest': (0xea67, False, 'in Misery Mire'),
|
||||
'Misery Mire - Map Chest': (0xea6a, False, 'in Misery Mire'),
|
||||
'Misery Mire - Main Lobby': (0xea5e, False, 'in Misery Mire'),
|
||||
'Misery Mire - Bridge Chest': (0xea61, False, 'in Misery Mire'),
|
||||
'Misery Mire - Spike Chest': (0xe9da, False, 'in Misery Mire'),
|
||||
'Misery Mire - Compass Chest': (0xea64, False, 'in Misery Mire'),
|
||||
'Misery Mire - Big Key Chest': (0xea6d, False, 'in Misery Mire'),
|
||||
'Misery Mire - Boss': (0x180158, False, 'with Vitreous'),
|
||||
'Turtle Rock - Compass Chest': (0xea22, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Roller Room - Left': (0xea1c, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Roller Room - Right': (0xea1f, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Chain Chomps': (0xea16, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Big Key Chest': (0xea25, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Big Chest': (0xea19, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Crystaroller Room': (0xea34, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Eye Bridge - Bottom Left': (0xea31, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Eye Bridge - Bottom Right': (0xea2e, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Eye Bridge - Top Left': (0xea2b, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Eye Bridge - Top Right': (0xea28, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Boss': (0x180159, False, 'with Trinexx'),
|
||||
'Palace of Darkness - Shooter Room': (0xea5b, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - The Arena - Bridge': (0xea3d, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Stalfos Basement': (0xea49, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Big Key Chest': (0xea37, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - The Arena - Ledge': (0xea3a, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Map Chest': (0xea52, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Compass Chest': (0xea43, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Dark Basement - Left': (0xea4c, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Dark Basement - Right': (0xea4f, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Dark Maze - Top': (0xea55, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Dark Maze - Bottom': (0xea58, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Big Chest': (0xea40, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Harmless Hellway': (0xea46, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Boss': (0x180153, False, 'with Helmasaur King'),
|
||||
"Ganons Tower - Bob's Torch": (0x180161, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Hope Room - Left': (0xead9, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Hope Room - Right': (0xeadc, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Tile Room': (0xeae2, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Compass Room - Top Left': (0xeae5, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Compass Room - Top Right': (0xeae8, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Compass Room - Bottom Left': (0xeaeb, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Compass Room - Bottom Right': (0xeaee, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - DMs Room - Top Left': (0xeab8, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - DMs Room - Top Right': (0xeabb, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - DMs Room - Bottom Left': (0xeabe, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - DMs Room - Bottom Right': (0xeac1, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Map Chest': (0xead3, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Firesnake Room': (0xead0, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Randomizer Room - Top Left': (0xeac4, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Randomizer Room - Top Right': (0xeac7, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Randomizer Room - Bottom Left': (0xeaca, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Randomizer Room - Bottom Right': (0xeacd, False, "in Ganon's Tower"),
|
||||
"Ganons Tower - Bob's Chest": (0xeadf, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Big Chest': (0xead6, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Big Key Room - Left': (0xeaf4, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Big Key Room - Right': (0xeaf7, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Big Key Chest': (0xeaf1, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Mini Helmasaur Room - Left': (0xeafd, False, "atop Ganon's Tower"),
|
||||
'Ganons Tower - Mini Helmasaur Room - Right': (0xeb00, False, "atop Ganon's Tower"),
|
||||
'Ganons Tower - Pre-Moldorm Chest': (0xeb03, False, "atop Ganon's Tower"),
|
||||
'Ganons Tower - Validation Chest': (0xeb06, False, "atop Ganon's Tower"),
|
||||
'Ganon': (None, False, 'from me'),
|
||||
'Agahnim 1': (None, False, 'from Ganon\'s wizardry form'),
|
||||
'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')}
|
||||
location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'),
|
||||
'Bottle Merchant': (0x2eb18, 0x186339, False, 'with a merchant'),
|
||||
'Flute Spot': (0x18014a, 0x18633d, False, 'underground'),
|
||||
'Sunken Treasure': (0x180145, 0x186354, False, 'underwater'),
|
||||
'Purple Chest': (0x33d68, 0x186359, False, 'from a box'),
|
||||
"Blind's Hideout - Top": (0xeb0f, 0x1862e3, False, 'in a basement'),
|
||||
"Blind's Hideout - Left": (0xeb12, 0x1862e6, False, 'in a basement'),
|
||||
"Blind's Hideout - Right": (0xeb15, 0x1862e9, False, 'in a basement'),
|
||||
"Blind's Hideout - Far Left": (0xeb18, 0x1862ec, False, 'in a basement'),
|
||||
"Blind's Hideout - Far Right": (0xeb1b, 0x1862ef, False, 'in a basement'),
|
||||
"Link's Uncle": (0x2df45, 0x18635f, False, 'with your uncle'),
|
||||
'Secret Passage': (0xe971, 0x186145, False, 'near your uncle'),
|
||||
'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'),
|
||||
'Waterfall Fairy - Right': (0xe9d1, 0x1861a5, False, 'near a fairy'),
|
||||
"King's Tomb": (0xe97a, 0x18614e, False, 'alone in a cave'),
|
||||
'Floodgate Chest': (0xe98c, 0x186160, False, 'in the dam'),
|
||||
"Link's House": (0xe9bc, 0x186190, False, 'in your home'),
|
||||
'Kakariko Tavern': (0xe9ce, 0x1861a2, False, 'in the bar'),
|
||||
'Chicken House': (0xe9e9, 0x1861bd, False, 'near poultry'),
|
||||
"Aginah's Cave": (0xe9f2, 0x1861c6, False, 'with Aginah'),
|
||||
"Sahasrahla's Hut - Left": (0xea82, 0x186256, False, 'near the elder'),
|
||||
"Sahasrahla's Hut - Middle": (0xea85, 0x186259, False, 'near the elder'),
|
||||
"Sahasrahla's Hut - Right": (0xea88, 0x18625c, False, 'near the elder'),
|
||||
'Sahasrahla': (0x2f1fc, 0x186365, False, 'with the elder'),
|
||||
'Kakariko Well - Top': (0xea8e, 0x186262, False, 'in a well'),
|
||||
'Kakariko Well - Left': (0xea91, 0x186265, False, 'in a well'),
|
||||
'Kakariko Well - Middle': (0xea94, 0x186268, False, 'in a well'),
|
||||
'Kakariko Well - Right': (0xea97, 0x18626b, False, 'in a well'),
|
||||
'Kakariko Well - Bottom': (0xea9a, 0x18626e, False, 'in a well'),
|
||||
'Blacksmith': (0x18002a, 0x186366, False, 'with the smith'),
|
||||
'Magic Bat': (0x180015, 0x18635e, False, 'with the bat'),
|
||||
'Sick Kid': (0x339cf, 0x186367, False, 'with the sick'),
|
||||
'Hobo': (0x33e7d, 0x186368, False, 'with the hobo'),
|
||||
'Lost Woods Hideout': (0x180000, 0x186348, False, 'near a thief'),
|
||||
'Lumberjack Tree': (0x180001, 0x186349, False, 'in a hole'),
|
||||
'Cave 45': (0x180003, 0x18634b, False, 'alone in a cave'),
|
||||
'Graveyard Cave': (0x180004, 0x18634c, False, 'alone in a cave'),
|
||||
'Checkerboard Cave': (0x180005, 0x18634d, False, 'alone in a cave'),
|
||||
'Mini Moldorm Cave - Far Left': (0xeb42, 0x186316, False, 'near Moldorms'),
|
||||
'Mini Moldorm Cave - Left': (0xeb45, 0x186319, False, 'near Moldorms'),
|
||||
'Mini Moldorm Cave - Right': (0xeb48, 0x18631c, False, 'near Moldorms'),
|
||||
'Mini Moldorm Cave - Far Right': (0xeb4b, 0x18631f, False, 'near Moldorms'),
|
||||
'Mini Moldorm Cave - Generous Guy': (0x180010, 0x18635a, False, 'near Moldorms'),
|
||||
'Ice Rod Cave': (0xeb4e, 0x186322, False, 'in a frozen cave'),
|
||||
'Bonk Rock Cave': (0xeb3f, 0x186313, False, 'alone in a cave'),
|
||||
'Library': (0x180012, 0x18635c, False, 'near books'),
|
||||
'Potion Shop': (0x180014, 0x18635d, False, 'near potions'),
|
||||
'Lake Hylia Island': (0x180144, 0x186353, False, 'on an island'),
|
||||
'Maze Race': (0x180142, 0x186351, False, 'at the race'),
|
||||
'Desert Ledge': (0x180143, 0x186352, False, 'in the desert'),
|
||||
'Desert Palace - Big Chest': (0xe98f, 0x186163, False, 'in Desert Palace'),
|
||||
'Desert Palace - Torch': (0x180160, 0x186362, False, 'in Desert Palace'),
|
||||
'Desert Palace - Map Chest': (0xe9b6, 0x18618a, False, 'in Desert Palace'),
|
||||
'Desert Palace - Compass Chest': (0xe9cb, 0x18619f, False, 'in Desert Palace'),
|
||||
'Desert Palace - Big Key Chest': (0xe9c2, 0x186196, False, 'in Desert Palace'),
|
||||
'Desert Palace - Boss': (0x180151, 0x18633f, False, 'with Lanmolas'),
|
||||
'Eastern Palace - Compass Chest': (0xe977, 0x18614b, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Big Chest': (0xe97d, 0x186151, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Cannonball Chest': (0xe9b3, 0x186187, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Big Key Chest': (0xe9b9, 0x18618d, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Map Chest': (0xe9f5, 0x1861c9, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Boss': (0x180150, 0x18633e, False, 'with the Armos'),
|
||||
'Master Sword Pedestal': (0x289b0, 0x186369, False, 'at the pedestal'),
|
||||
'Hyrule Castle - Boomerang Chest': (0xe974, 0x186148, False, 'in Hyrule Castle'),
|
||||
'Hyrule Castle - Map Chest': (0xeb0c, 0x1862e0, False, 'in Hyrule Castle'),
|
||||
"Hyrule Castle - Zelda's Chest": (0xeb09, 0x1862dd, False, 'in Hyrule Castle'),
|
||||
'Sewers - Dark Cross': (0xe96e, 0x186142, False, 'in the sewers'),
|
||||
'Sewers - Secret Room - Left': (0xeb5d, 0x186331, False, 'in the sewers'),
|
||||
'Sewers - Secret Room - Middle': (0xeb60, 0x186334, False, 'in the sewers'),
|
||||
'Sewers - Secret Room - Right': (0xeb63, 0x186337, False, 'in the sewers'),
|
||||
'Sanctuary': (0xea79, 0x18624d, False, 'in Sanctuary'),
|
||||
'Castle Tower - Room 03': (0xeab5, 0x186289, False, 'in Castle Tower'),
|
||||
'Castle Tower - Dark Maze': (0xeab2, 0x186286, False, 'in Castle Tower'),
|
||||
'Old Man': (0xf69fa, 0x186364, False, 'with the old man'),
|
||||
'Spectacle Rock Cave': (0x180002, 0x18634a, False, 'alone in a cave'),
|
||||
'Paradox Cave Lower - Far Left': (0xeb2a, 0x1862fe, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Lower - Left': (0xeb2d, 0x186301, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Lower - Right': (0xeb30, 0x186304, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Lower - Far Right': (0xeb33, 0x186307, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Lower - Middle': (0xeb36, 0x18630a, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Upper - Left': (0xeb39, 0x18630d, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Upper - Right': (0xeb3c, 0x186310, False, 'in a cave with seven chests'),
|
||||
'Spiral Cave': (0xe9bf, 0x186193, False, 'in spiral cave'),
|
||||
'Ether Tablet': (0x180016, 0x18633b, False, 'at a monolith'),
|
||||
'Spectacle Rock': (0x180140, 0x18634f, False, 'atop a rock'),
|
||||
'Tower of Hera - Basement Cage': (0x180162, 0x18633a, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Map Chest': (0xe9ad, 0x186181, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Big Key Chest': (0xe9e6, 0x1861ba, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Compass Chest': (0xe9fb, 0x1861cf, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Big Chest': (0xe9f8, 0x1861cc, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Boss': (0x180152, 0x186340, False, 'with Moldorm'),
|
||||
'Pyramid': (0x180147, 0x186356, False, 'on the pyramid'),
|
||||
'Catfish': (0xee185, 0x186361, False, 'with a catfish'),
|
||||
'Stumpy': (0x330c7, 0x18636a, False, 'with tree boy'),
|
||||
'Digging Game': (0x180148, 0x186357, False, 'underground'),
|
||||
'Bombos Tablet': (0x180017, 0x18633c, False, 'at a monolith'),
|
||||
'Hype Cave - Top': (0xeb1e, 0x1862f2, False, 'near a bat-like man'),
|
||||
'Hype Cave - Middle Right': (0xeb21, 0x1862f5, False, 'near a bat-like man'),
|
||||
'Hype Cave - Middle Left': (0xeb24, 0x1862f8, False, 'near a bat-like man'),
|
||||
'Hype Cave - Bottom': (0xeb27, 0x1862fb, False, 'near a bat-like man'),
|
||||
'Hype Cave - Generous Guy': (0x180011, 0x18635b, False, 'with a bat-like man'),
|
||||
'Peg Cave': (0x180006, 0x18634e, False, 'alone in a cave'),
|
||||
'Pyramid Fairy - Left': (0xe980, 0x186154, False, 'near a fairy'),
|
||||
'Pyramid Fairy - Right': (0xe983, 0x186157, False, 'near a fairy'),
|
||||
'Brewery': (0xe9ec, 0x1861c0, False, 'alone in a home'),
|
||||
'C-Shaped House': (0xe9ef, 0x1861c3, False, 'alone in a home'),
|
||||
'Chest Game': (0xeda8, 0x18636b, False, 'as a prize'),
|
||||
'Bumper Cave Ledge': (0x180146, 0x186355, False, 'on a ledge'),
|
||||
'Mire Shed - Left': (0xea73, 0x186247, False, 'near sparks'),
|
||||
'Mire Shed - Right': (0xea76, 0x18624a, False, 'near sparks'),
|
||||
'Superbunny Cave - Top': (0xea7c, 0x186250, False, 'in a connection'),
|
||||
'Superbunny Cave - Bottom': (0xea7f, 0x186253, False, 'in a connection'),
|
||||
'Spike Cave': (0xea8b, 0x18625f, False, 'beyond spikes'),
|
||||
'Hookshot Cave - Top Right': (0xeb51, 0x186325, False, 'across pits'),
|
||||
'Hookshot Cave - Top Left': (0xeb54, 0x186328, False, 'across pits'),
|
||||
'Hookshot Cave - Bottom Right': (0xeb5a, 0x18632e, False, 'across pits'),
|
||||
'Hookshot Cave - Bottom Left': (0xeb57, 0x18632b, False, 'across pits'),
|
||||
'Floating Island': (0x180141, 0x186350, False, 'on an island'),
|
||||
'Mimic Cave': (0xe9c5, 0x186199, False, 'in a cave of mimicry'),
|
||||
'Swamp Palace - Entrance': (0xea9d, 0x186271, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Map Chest': (0xe986, 0x18615a, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Big Chest': (0xe989, 0x18615d, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Compass Chest': (0xeaa0, 0x186274, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Big Key Chest': (0xeaa6, 0x18627a, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - West Chest': (0xeaa3, 0x186277, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Flooded Room - Left': (0xeaa9, 0x18627d, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Flooded Room - Right': (0xeaac, 0x186280, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Waterfall Room': (0xeaaf, 0x186283, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Boss': (0x180154, 0x186342, False, 'with Arrghus'),
|
||||
"Thieves' Town - Big Key Chest": (0xea04, 0x1861d8, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Map Chest": (0xea01, 0x1861d5, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Compass Chest": (0xea07, 0x1861db, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Ambush Chest": (0xea0a, 0x1861de, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Attic": (0xea0d, 0x1861e1, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Big Chest": (0xea10, 0x1861e4, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Blind's Cell": (0xea13, 0x1861e7, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Boss": (0x180156, 0x186344, False, 'with Blind'),
|
||||
'Skull Woods - Compass Chest': (0xe992, 0x186166, False, 'in Skull Woods'),
|
||||
'Skull Woods - Map Chest': (0xe99b, 0x18616f, False, 'in Skull Woods'),
|
||||
'Skull Woods - Big Chest': (0xe998, 0x18616c, False, 'in Skull Woods'),
|
||||
'Skull Woods - Pot Prison': (0xe9a1, 0x186175, False, 'in Skull Woods'),
|
||||
'Skull Woods - Pinball Room': (0xe9c8, 0x18619c, False, 'in Skull Woods'),
|
||||
'Skull Woods - Big Key Chest': (0xe99e, 0x186172, False, 'in Skull Woods'),
|
||||
'Skull Woods - Bridge Room': (0xe9fe, 0x1861d2, False, 'near Mothula'),
|
||||
'Skull Woods - Boss': (0x180155, 0x186343, False, 'with Mothula'),
|
||||
'Ice Palace - Compass Chest': (0xe9d4, 0x1861a8, False, 'in Ice Palace'),
|
||||
'Ice Palace - Freezor Chest': (0xe995, 0x186169, False, 'in Ice Palace'),
|
||||
'Ice Palace - Big Chest': (0xe9aa, 0x18617e, False, 'in Ice Palace'),
|
||||
'Ice Palace - Iced T Room': (0xe9e3, 0x1861b7, False, 'in Ice Palace'),
|
||||
'Ice Palace - Spike Room': (0xe9e0, 0x1861b4, False, 'in Ice Palace'),
|
||||
'Ice Palace - Big Key Chest': (0xe9a4, 0x186178, False, 'in Ice Palace'),
|
||||
'Ice Palace - Map Chest': (0xe9dd, 0x1861b1, False, 'in Ice Palace'),
|
||||
'Ice Palace - Boss': (0x180157, 0x186345, False, 'with Kholdstare'),
|
||||
'Misery Mire - Big Chest': (0xea67, 0x18623b, False, 'in Misery Mire'),
|
||||
'Misery Mire - Map Chest': (0xea6a, 0x18623e, False, 'in Misery Mire'),
|
||||
'Misery Mire - Main Lobby': (0xea5e, 0x186232, False, 'in Misery Mire'),
|
||||
'Misery Mire - Bridge Chest': (0xea61, 0x186235, False, 'in Misery Mire'),
|
||||
'Misery Mire - Spike Chest': (0xe9da, 0x1861ae, False, 'in Misery Mire'),
|
||||
'Misery Mire - Compass Chest': (0xea64, 0x186238, False, 'in Misery Mire'),
|
||||
'Misery Mire - Big Key Chest': (0xea6d, 0x186241, False, 'in Misery Mire'),
|
||||
'Misery Mire - Boss': (0x180158, 0x186346, False, 'with Vitreous'),
|
||||
'Turtle Rock - Compass Chest': (0xea22, 0x1861f6, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Roller Room - Left': (0xea1c, 0x1861f0, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Roller Room - Right': (0xea1f, 0x1861f3, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Chain Chomps': (0xea16, 0x1861ea, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Big Key Chest': (0xea25, 0x1861f9, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Big Chest': (0xea19, 0x1861ed, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Crystaroller Room': (0xea34, 0x186208, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Eye Bridge - Bottom Left': (0xea31, 0x186205, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Eye Bridge - Bottom Right': (0xea2e, 0x186202, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Eye Bridge - Top Left': (0xea2b, 0x1861ff, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Eye Bridge - Top Right': (0xea28, 0x1861fc, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Boss': (0x180159, 0x186347, False, 'with Trinexx'),
|
||||
'Palace of Darkness - Shooter Room': (0xea5b, 0x18622f, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - The Arena - Bridge': (0xea3d, 0x186211, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Stalfos Basement': (0xea49, 0x18621d, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Big Key Chest': (0xea37, 0x18620b, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - The Arena - Ledge': (0xea3a, 0x18620e, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Map Chest': (0xea52, 0x186226, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Compass Chest': (0xea43, 0x186217, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Dark Basement - Left': (0xea4c, 0x186220, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Dark Basement - Right': (0xea4f, 0x186223, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Dark Maze - Top': (0xea55, 0x186229, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Dark Maze - Bottom': (0xea58, 0x18622c, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Big Chest': (0xea40, 0x186214, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Harmless Hellway': (0xea46, 0x18621a, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Boss': (0x180153, 0x186341, False, 'with Helmasaur King'),
|
||||
"Ganons Tower - Bob's Torch": (0x180161, 0x186363, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Hope Room - Left': (0xead9, 0x1862ad, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Hope Room - Right': (0xeadc, 0x1862b0, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Tile Room': (0xeae2, 0x1862b6, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Compass Room - Top Left': (0xeae5, 0x1862b9, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Compass Room - Top Right': (0xeae8, 0x1862bc, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Compass Room - Bottom Left': (0xeaeb, 0x1862bf, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Compass Room - Bottom Right': (0xeaee, 0x1862c2, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - DMs Room - Top Left': (0xeab8, 0x18628c, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - DMs Room - Top Right': (0xeabb, 0x18628f, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - DMs Room - Bottom Left': (0xeabe, 0x186292, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - DMs Room - Bottom Right': (0xeac1, 0x186295, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Map Chest': (0xead3, 0x1862a7, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Firesnake Room': (0xead0, 0x1862a4, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Randomizer Room - Top Left': (0xeac4, 0x186298, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Randomizer Room - Top Right': (0xeac7, 0x18629b, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Randomizer Room - Bottom Left': (0xeaca, 0x18629e, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Randomizer Room - Bottom Right': (0xeacd, 0x1862a1, False, "in Ganon's Tower"),
|
||||
"Ganons Tower - Bob's Chest": (0xeadf, 0x1862b3, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Big Chest': (0xead6, 0x1862aa, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Big Key Room - Left': (0xeaf4, 0x1862c8, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Big Key Room - Right': (0xeaf7, 0x1862cb, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Big Key Chest': (0xeaf1, 0x1862c5, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Mini Helmasaur Room - Left': (0xeafd, 0x1862d1, False, "atop Ganon's Tower"),
|
||||
'Ganons Tower - Mini Helmasaur Room - Right': (0xeb00, 0x1862d4, False, "atop Ganon's Tower"),
|
||||
'Ganons Tower - Pre-Moldorm Chest': (0xeb03, 0x1862d7, False, "atop Ganon's Tower"),
|
||||
'Ganons Tower - Validation Chest': (0xeb06, 0x1862da, False, "atop Ganon's Tower"),
|
||||
'Ganon': (None, None, False, 'from me'),
|
||||
'Agahnim 1': (None, None, False, 'from Ganon\'s wizardry form'),
|
||||
'Agahnim 2': (None, None, False, 'from Ganon\'s wizardry form'),
|
||||
'Floodgate': (None, None, False, None),
|
||||
'Frog': (None, None, False, None),
|
||||
'Missing Smith': (None, None, False, None),
|
||||
'Dark Blacksmith Ruins': (None, None, False, None),
|
||||
'Eastern Palace - Prize': ([0x1209D, 0x53EF8, 0x53EF9, 0x180052, 0x18007C, 0xC6FE], None, True, 'Eastern Palace'),
|
||||
'Desert Palace - Prize': ([0x1209E, 0x53F1C, 0x53F1D, 0x180053, 0x180078, 0xC6FF], None, True, 'Desert Palace'),
|
||||
'Tower of Hera - Prize': ([0x120A5, 0x53F0A, 0x53F0B, 0x18005A, 0x18007A, 0xC706], None, True, 'Tower of Hera'),
|
||||
'Palace of Darkness - Prize': ([0x120A1, 0x53F00, 0x53F01, 0x180056, 0x18007D, 0xC702], None, True, 'Palace of Darkness'),
|
||||
'Swamp Palace - Prize': ([0x120A0, 0x53F6C, 0x53F6D, 0x180055, 0x180071, 0xC701], None, True, 'Swamp Palace'),
|
||||
'Thieves\' Town - Prize': ([0x120A6, 0x53F36, 0x53F37, 0x18005B, 0x180077, 0xC707], None, True, 'Thieves\' Town'),
|
||||
'Skull Woods - Prize': ([0x120A3, 0x53F12, 0x53F13, 0x180058, 0x18007B, 0xC704], None, True, 'Skull Woods'),
|
||||
'Ice Palace - Prize': ([0x120A4, 0x53F5A, 0x53F5B, 0x180059, 0x180073, 0xC705], None, True, 'Ice Palace'),
|
||||
'Misery Mire - Prize': ([0x120A2, 0x53F48, 0x53F49, 0x180057, 0x180075, 0xC703], None, True, 'Misery Mire'),
|
||||
'Turtle Rock - Prize': ([0x120A7, 0x53F24, 0x53F25, 0x18005C, 0x180079, 0xC708], None, True, 'Turtle Rock')}
|
||||
|
||||
@@ -5,9 +5,9 @@ from BaseClasses import CollectionState
|
||||
|
||||
def set_rules(world, player):
|
||||
|
||||
if world.logic == 'nologic':
|
||||
if world.logic[player] == 'nologic':
|
||||
logging.getLogger('').info('WARNING! Seeds generated under this logic often require major glitches and may be impossible!')
|
||||
if world.mode != 'inverted':
|
||||
if world.mode[player] != 'inverted':
|
||||
world.get_region('Links House', player).can_reach_private = lambda state: True
|
||||
world.get_region('Sanctuary', player).can_reach_private = lambda state: True
|
||||
old_rule = world.get_region('Old Man House', player).can_reach
|
||||
@@ -16,40 +16,41 @@ def set_rules(world, player):
|
||||
else:
|
||||
world.get_region('Inverted Links House', player).can_reach_private = lambda state: True
|
||||
world.get_region('Inverted Dark Sanctuary', player).entrances[0].parent_region.can_reach_private = lambda state: True
|
||||
if world.shuffle != 'vanilla':
|
||||
if world.shuffle[player] != 'vanilla':
|
||||
old_rule = world.get_region('Old Man House', player).can_reach
|
||||
world.get_region('Old Man House', player).can_reach_private = lambda state: state.can_reach('Old Man', 'Location', player) or old_rule(state)
|
||||
world.get_region('Hyrule Castle Ledge', player).can_reach_private = lambda state: True
|
||||
return
|
||||
|
||||
global_rules(world, player)
|
||||
if world.mode != 'inverted':
|
||||
if world.mode[player] != 'inverted':
|
||||
default_rules(world, player)
|
||||
|
||||
if world.mode == 'open':
|
||||
if world.mode[player] == 'open':
|
||||
open_rules(world, player)
|
||||
elif world.mode == 'standard':
|
||||
elif world.mode[player] == 'standard':
|
||||
standard_rules(world, player)
|
||||
elif world.mode == 'inverted':
|
||||
elif world.mode[player] == 'inverted':
|
||||
open_rules(world, player)
|
||||
inverted_rules(world, player)
|
||||
else:
|
||||
raise NotImplementedError('Not implemented yet')
|
||||
|
||||
if world.logic == 'noglitches':
|
||||
if world.logic[player] == 'noglitches':
|
||||
no_glitches_rules(world, player)
|
||||
elif world.logic == 'minorglitches':
|
||||
elif world.logic[player] == 'minorglitches':
|
||||
logging.getLogger('').info('Minor Glitches may be buggy still. No guarantee for proper logic checks.')
|
||||
else:
|
||||
raise NotImplementedError('Not implemented yet')
|
||||
|
||||
if world.goal == 'dungeons':
|
||||
if world.goal[player] == 'dungeons':
|
||||
# require all dungeons to beat ganon
|
||||
add_rule(world.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player) and state.has('Beat Agahnim 1', player) and state.has('Beat Agahnim 2', player) and state.has_crystals(7, player))
|
||||
elif world.goal == 'ganon':
|
||||
elif world.goal[player] == 'ganon':
|
||||
# require aga2 to beat ganon
|
||||
add_rule(world.get_location('Ganon', player), lambda state: state.has('Beat Agahnim 2', player))
|
||||
|
||||
if world.mode != 'inverted':
|
||||
if world.mode[player] != 'inverted':
|
||||
set_big_bomb_rules(world, player)
|
||||
else:
|
||||
set_inverted_big_bomb_rules(world, player)
|
||||
@@ -58,7 +59,7 @@ def set_rules(world, player):
|
||||
if not world.swamp_patch_required[player]:
|
||||
add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has_Mirror(player))
|
||||
|
||||
if world.mode != 'inverted':
|
||||
if world.mode[player] != 'inverted':
|
||||
set_bunny_rules(world, player)
|
||||
else:
|
||||
set_inverted_bunny_rules(world, player)
|
||||
@@ -106,11 +107,6 @@ def item_name(state, location, player):
|
||||
return (location.item.name, location.item.player)
|
||||
|
||||
def global_rules(world, player):
|
||||
if world.goal == 'triforcehunt':
|
||||
for location in world.get_locations():
|
||||
if location.player != player:
|
||||
forbid_item(location, 'Triforce Piece', player)
|
||||
|
||||
# ganon can only carry triforce
|
||||
add_item_rule(world.get_location('Ganon', player), lambda item: item.name == 'Triforce' and item.player == player)
|
||||
|
||||
@@ -146,7 +142,7 @@ def global_rules(world, player):
|
||||
set_rule(world.get_location('Hookshot Cave - Bottom Right', player), lambda state: state.has('Hookshot', player) or state.has('Pegasus Boots', player))
|
||||
set_rule(world.get_location('Hookshot Cave - Bottom Left', player), lambda state: state.has('Hookshot', player))
|
||||
|
||||
set_rule(world.get_entrance('Sewers Door', player), lambda state: state.has_key('Small Key (Escape)', player))
|
||||
set_rule(world.get_entrance('Sewers Door', player), lambda state: state.has_key('Small Key (Escape)', player) or (world.retro[player] and world.mode[player] == 'standard')) # standard retro cannot access the shop
|
||||
set_rule(world.get_entrance('Sewers Back Door', player), lambda state: state.has_key('Small Key (Escape)', player))
|
||||
set_rule(world.get_entrance('Agahnim 1', player), lambda state: state.has_sword(player) and state.has_key('Small Key (Agahnims Tower)', player, 2))
|
||||
set_defeat_dungeon_boss_rule(world.get_location('Agahnim 1', player))
|
||||
@@ -173,7 +169,7 @@ def global_rules(world, player):
|
||||
set_rule(world.get_entrance('Tower of Hera Big Key Door', player), lambda state: state.has('Big Key (Tower of Hera)', player))
|
||||
set_rule(world.get_location('Tower of Hera - Big Chest', player), lambda state: state.has('Big Key (Tower of Hera)', player))
|
||||
set_rule(world.get_location('Tower of Hera - Big Key Chest', player), lambda state: state.has_fire_source(player))
|
||||
if world.accessibility != 'locations':
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Tower of Hera - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Tower of Hera)' and item.player == player)
|
||||
set_defeat_dungeon_boss_rule(world.get_location('Tower of Hera - Boss', player))
|
||||
set_defeat_dungeon_boss_rule(world.get_location('Tower of Hera - Prize', player))
|
||||
@@ -186,7 +182,7 @@ def global_rules(world, player):
|
||||
set_rule(world.get_entrance('Swamp Palace Small Key Door', player), lambda state: state.has_key('Small Key (Swamp Palace)', player))
|
||||
set_rule(world.get_entrance('Swamp Palace (Center)', player), lambda state: state.has('Hammer', player))
|
||||
set_rule(world.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player) or item_name(state, 'Swamp Palace - Big Chest', player) == ('Big Key (Swamp Palace)', player))
|
||||
if world.accessibility != 'locations':
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Swamp Palace - Big Chest', player), lambda state, item: item.name == 'Big Key (Swamp Palace)' and item.player == player)
|
||||
set_rule(world.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player))
|
||||
set_defeat_dungeon_boss_rule(world.get_location('Swamp Palace - Boss', player))
|
||||
@@ -199,7 +195,7 @@ def global_rules(world, player):
|
||||
set_defeat_dungeon_boss_rule(world.get_location('Thieves\' Town - Boss', player))
|
||||
set_defeat_dungeon_boss_rule(world.get_location('Thieves\' Town - Prize', player))
|
||||
set_rule(world.get_location('Thieves\' Town - Big Chest', player), lambda state: (state.has_key('Small Key (Thieves Town)', player) or item_name(state, 'Thieves\' Town - Big Chest', player) == ('Small Key (Thieves Town)', player)) and state.has('Hammer', player))
|
||||
if world.accessibility != 'locations':
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Thieves\' Town - Big Chest', player), lambda state, item: item.name == 'Small Key (Thieves Town)' and item.player == player and state.has('Hammer', player))
|
||||
set_rule(world.get_location('Thieves\' Town - Attic', player), lambda state: state.has_key('Small Key (Thieves Town)', player))
|
||||
for location in ['Thieves\' Town - Attic', 'Thieves\' Town - Big Chest', 'Thieves\' Town - Blind\'s Cell', 'Thieves\' Town - Boss']:
|
||||
@@ -212,7 +208,7 @@ def global_rules(world, player):
|
||||
set_rule(world.get_entrance('Skull Woods First Section West Door', player), lambda state: state.has_key('Small Key (Skull Woods)', player, 2)) # ideally would only be one key, but we may have spent thst key already on escaping the right section
|
||||
set_rule(world.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state.has_key('Small Key (Skull Woods)', player, 2))
|
||||
set_rule(world.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player) or item_name(state, 'Skull Woods - Big Chest', player) == ('Big Key (Skull Woods)', player))
|
||||
if world.accessibility != 'locations':
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Skull Woods - Big Chest', player), lambda state, item: item.name == 'Big Key (Skull Woods)' and item.player == player)
|
||||
set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state.has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player) and state.has_sword(player)) # sword required for curtain
|
||||
set_defeat_dungeon_boss_rule(world.get_location('Skull Woods - Boss', player))
|
||||
@@ -277,13 +273,13 @@ def global_rules(world, player):
|
||||
set_rule(world.get_location('Palace of Darkness - Big Chest', player), lambda state: state.has('Big Key (Palace of Darkness)', player))
|
||||
|
||||
set_rule(world.get_entrance('Palace of Darkness Big Key Chest Staircase', player), lambda state: state.has_key('Small Key (Palace of Darkness)', player, 6) or (item_name(state, 'Palace of Darkness - Big Key Chest', player) in [('Small Key (Palace of Darkness)', player)] and state.has_key('Small Key (Palace of Darkness)', player, 3)))
|
||||
if world.accessibility != 'locations':
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Palace of Darkness - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state.has_key('Small Key (Palace of Darkness)', player, 5))
|
||||
else:
|
||||
forbid_item(world.get_location('Palace of Darkness - Big Key Chest', player), 'Small Key (Palace of Darkness)', player)
|
||||
|
||||
set_rule(world.get_entrance('Palace of Darkness Spike Statue Room Door', player), lambda state: state.has_key('Small Key (Palace of Darkness)', player, 6) or (item_name(state, 'Palace of Darkness - Harmless Hellway', player) in [('Small Key (Palace of Darkness)', player)] and state.has_key('Small Key (Palace of Darkness)', player, 4)))
|
||||
if world.accessibility != 'locations':
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Palace of Darkness - Harmless Hellway', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state.has_key('Small Key (Palace of Darkness)', player, 5))
|
||||
else:
|
||||
forbid_item(world.get_location('Palace of Darkness - Harmless Hellway', player), 'Small Key (Palace of Darkness)', player)
|
||||
@@ -301,7 +297,7 @@ def global_rules(world, player):
|
||||
set_rule(world.get_entrance('Ganons Tower (Hookshot Room)', player), lambda state: state.has('Hammer', player))
|
||||
|
||||
set_rule(world.get_entrance('Ganons Tower (Map Room)', player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 4) or (item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player), ('Small Key (Ganons Tower)', player)] and state.has_key('Small Key (Ganons Tower)', player, 3)))
|
||||
if world.accessibility != 'locations':
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Ganons Tower - Map Chest', player), lambda state, item: item.name == 'Small Key (Ganons Tower)' and item.player == player and state.has_key('Small Key (Ganons Tower)', player, 3))
|
||||
else:
|
||||
forbid_item(world.get_location('Ganons Tower - Map Chest', player), 'Small Key (Ganons Tower)', player)
|
||||
@@ -339,13 +335,13 @@ def global_rules(world, player):
|
||||
'Ganons Tower - Pre-Moldorm Chest', 'Ganons Tower - Validation Chest']:
|
||||
forbid_item(world.get_location(location, player), 'Big Key (Ganons Tower)', player)
|
||||
|
||||
set_rule(world.get_location('Ganon', player), lambda state: state.has_beam_sword(player) and state.has_fire_source(player) and state.has_crystals(world.crystals_needed_for_ganon, player)
|
||||
set_rule(world.get_location('Ganon', player), lambda state: state.has_beam_sword(player) and state.has_fire_source(player) and state.has_crystals(world.crystals_needed_for_ganon[player], player)
|
||||
and (state.has('Tempered Sword', player) or state.has('Golden Sword', player) or (state.has('Silver Arrows', player) and state.can_shoot_arrows(player)) or state.has('Lamp', player) or state.can_extend_magic(player, 12))) # need to light torch a sufficient amount of times
|
||||
set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has_beam_sword(player)) # need to damage ganon to get tiles to drop
|
||||
|
||||
|
||||
def default_rules(world, player):
|
||||
if world.mode == 'standard':
|
||||
if world.mode[player] == 'standard':
|
||||
world.get_region('Hyrule Castle Secret Entrance', player).can_reach_private = lambda state: True
|
||||
old_rule = world.get_region('Links House', player).can_reach_private
|
||||
world.get_region('Links House', player).can_reach_private = lambda state: state.can_reach('Sanctuary', 'Region', player) or old_rule(state)
|
||||
@@ -455,15 +451,15 @@ def default_rules(world, player):
|
||||
set_rule(world.get_entrance('Floating Island Mirror Spot', player), lambda state: state.has_Mirror(player))
|
||||
set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has_Pearl(player) and state.has_sword(player) and state.has_turtle_rock_medallion(player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!)
|
||||
|
||||
set_rule(world.get_entrance('Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player))
|
||||
set_rule(world.get_entrance('Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.open_pyramid[player])
|
||||
set_rule(world.get_entrance('Ganons Tower', player), lambda state: False) # This is a safety for the TR function below to not require GT entrance in its key logic.
|
||||
|
||||
if world.swords == 'swordless':
|
||||
if world.swords[player] == 'swordless':
|
||||
swordless_rules(world, player)
|
||||
|
||||
set_trock_key_rules(world, player)
|
||||
|
||||
set_rule(world.get_entrance('Ganons Tower', player), lambda state: state.has_crystals(world.crystals_needed_for_gt, player))
|
||||
set_rule(world.get_entrance('Ganons Tower', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player))
|
||||
|
||||
def inverted_rules(world, player):
|
||||
# s&q regions. link's house entrance is set to true so the filler knows the chest inside can always be reached
|
||||
@@ -610,18 +606,18 @@ def inverted_rules(world, player):
|
||||
set_rule(world.get_entrance('Dark Grassy Lawn Flute', player), lambda state: state.can_flute(player))
|
||||
set_rule(world.get_entrance('Hammer Peg Area Flute', player), lambda state: state.can_flute(player))
|
||||
|
||||
set_rule(world.get_entrance('Inverted Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player))
|
||||
set_rule(world.get_entrance('Inverted Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.open_pyramid[player])
|
||||
set_rule(world.get_entrance('Inverted Ganons Tower', player), lambda state: False) # This is a safety for the TR function below to not require GT entrance in its key logic.
|
||||
|
||||
if world.swords == 'swordless':
|
||||
if world.swords[player] == 'swordless':
|
||||
swordless_rules(world, player)
|
||||
|
||||
set_trock_key_rules(world, player)
|
||||
|
||||
set_rule(world.get_entrance('Inverted Ganons Tower', player), lambda state: state.has_crystals(world.crystals_needed_for_gt, player))
|
||||
set_rule(world.get_entrance('Inverted Ganons Tower', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player))
|
||||
|
||||
def no_glitches_rules(world, player):
|
||||
if world.mode != 'inverted':
|
||||
if world.mode[player] != 'inverted':
|
||||
set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Flippers', player) or state.can_lift_rocks(player))
|
||||
set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Flippers', player)) # can be fake flippered to
|
||||
set_rule(world.get_entrance('Hobo Bridge', player), lambda state: state.has('Flippers', player))
|
||||
@@ -631,10 +627,12 @@ def no_glitches_rules(world, player):
|
||||
else:
|
||||
set_rule(world.get_entrance('Zoras River', player), lambda state: state.has_Pearl(player) and (state.has('Flippers', player) or state.can_lift_rocks(player)))
|
||||
set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has_Pearl(player) and state.has('Flippers', player)) # can be fake flippered to
|
||||
set_rule(world.get_entrance('Lake Hylia Island', player), lambda state: state.has_Pearl(player) and state.has('Flippers', player))
|
||||
set_rule(world.get_entrance('Hobo Bridge', player), lambda state: state.has_Pearl(player) and state.has('Flippers', player))
|
||||
set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Flippers', player))
|
||||
set_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Flippers', player) and (state.has('Hammer', player) or state.can_lift_rocks(player)))
|
||||
set_rule(world.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: state.has('Flippers', player))
|
||||
set_rule(world.get_entrance('East Dark World Pier', player), lambda state: state.has('Flippers', player))
|
||||
|
||||
add_rule(world.get_entrance('Ganons Tower (Hookshot Room)', player), lambda state: state.has('Hookshot', player) or state.has_Boots(player))
|
||||
add_rule(world.get_entrance('Ganons Tower (Double Switch Room)', player), lambda state: state.has('Hookshot', player))
|
||||
@@ -671,7 +669,7 @@ def no_glitches_rules(world, player):
|
||||
add_conditional_lamp('Palace of Darkness Maze Door', 'Palace of Darkness (Entrance)', 'Entrance')
|
||||
add_conditional_lamp('Palace of Darkness - Dark Basement - Left', 'Palace of Darkness (Entrance)', 'Location')
|
||||
add_conditional_lamp('Palace of Darkness - Dark Basement - Right', 'Palace of Darkness (Entrance)', 'Location')
|
||||
if world.mode != 'inverted':
|
||||
if world.mode[player] != 'inverted':
|
||||
add_conditional_lamp('Agahnim 1', 'Agahnims Tower', 'Entrance')
|
||||
add_conditional_lamp('Castle Tower - Dark Maze', 'Agahnims Tower', 'Location')
|
||||
else:
|
||||
@@ -687,7 +685,7 @@ def no_glitches_rules(world, player):
|
||||
add_conditional_lamp('Eastern Palace - Boss', 'Eastern Palace', 'Location')
|
||||
add_conditional_lamp('Eastern Palace - Prize', 'Eastern Palace', 'Location')
|
||||
|
||||
if not world.sewer_light_cone:
|
||||
if not world.sewer_light_cone[player]:
|
||||
add_lamp_requirement(world.get_location('Sewers - Dark Cross', player), player)
|
||||
add_lamp_requirement(world.get_entrance('Sewers Back Door', player), player)
|
||||
add_lamp_requirement(world.get_entrance('Throne Room', player), player)
|
||||
@@ -695,9 +693,6 @@ def no_glitches_rules(world, player):
|
||||
|
||||
def open_rules(world, player):
|
||||
# softlock protection as you can reach the sewers small key door with a guard drop key
|
||||
forbid_item(world.get_location('Hyrule Castle - Boomerang Chest', player), 'Small Key (Escape)', player)
|
||||
forbid_item(world.get_location('Hyrule Castle - Zelda\'s Chest', player), 'Small Key (Escape)', player)
|
||||
|
||||
set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player), lambda state: state.has_key('Small Key (Escape)', player))
|
||||
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player), lambda state: state.has_key('Small Key (Escape)', player))
|
||||
|
||||
@@ -708,10 +703,10 @@ def swordless_rules(world, player):
|
||||
set_rule(world.get_location('Ether Tablet', player), lambda state: state.has('Book of Mudora', player) and state.has('Hammer', player))
|
||||
set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state.has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player)) # no curtain
|
||||
set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player)) #in swordless mode bombos pads are present in the relevant parts of ice palace
|
||||
set_rule(world.get_location('Ganon', player), lambda state: state.has('Hammer', player) and state.has_fire_source(player) and state.has('Silver Arrows', player) and state.can_shoot_arrows(player) and state.has_crystals(world.crystals_needed_for_ganon, player))
|
||||
set_rule(world.get_location('Ganon', player), lambda state: state.has('Hammer', player) and state.has_fire_source(player) and state.has('Silver Arrows', player) and state.can_shoot_arrows(player) and state.has_crystals(world.crystals_needed_for_ganon[player], player))
|
||||
set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has('Hammer', player)) # need to damage ganon to get tiles to drop
|
||||
|
||||
if world.mode != 'inverted':
|
||||
if world.mode[player] != 'inverted':
|
||||
set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has('Hammer', player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle
|
||||
set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has_Pearl(player) and state.has_turtle_rock_medallion(player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!)
|
||||
set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has_Pearl(player) and state.has_misery_mire_medallion(player)) # sword not required to use medallion for opening in swordless (!)
|
||||
@@ -722,32 +717,11 @@ def swordless_rules(world, player):
|
||||
set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has_misery_mire_medallion(player)) # sword not required to use medallion for opening in swordless (!)
|
||||
set_rule(world.get_location('Bombos Tablet', player), lambda state: state.has('Book of Mudora', player) and state.has('Hammer', player))
|
||||
|
||||
def standard_rules(world, player):
|
||||
add_rule(world.get_entrance('Sewers Door', player), lambda state: state.can_kill_most_things(player))
|
||||
|
||||
def standard_rules(world, player):
|
||||
set_rule(world.get_entrance('Hyrule Castle Exit (East)', player), lambda state: state.can_reach('Sanctuary', 'Region', player))
|
||||
set_rule(world.get_entrance('Hyrule Castle Exit (West)', player), lambda state: state.can_reach('Sanctuary', 'Region', player))
|
||||
|
||||
# ensures the required weapon for escape lands on uncle (unless player has it pre-equipped)
|
||||
add_rule(world.get_location('Secret Passage', player), lambda state: state.can_kill_most_things(player))
|
||||
add_rule(world.get_location('Hyrule Castle - Map Chest', player), lambda state: state.can_kill_most_things(player))
|
||||
|
||||
def uncle_item_rule(item):
|
||||
copy_state = CollectionState(world)
|
||||
copy_state.collect(item)
|
||||
copy_state.sweep_for_events()
|
||||
return copy_state.can_reach('Sanctuary', 'Region', player)
|
||||
|
||||
add_item_rule(world.get_location('Link\'s Uncle', player), uncle_item_rule)
|
||||
|
||||
# easiest way to enforce key placement not relevant for open
|
||||
set_rule(world.get_location('Sewers - Dark Cross', player), lambda state: state.can_kill_most_things(player))
|
||||
|
||||
set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player), lambda state: state.can_kill_most_things(player))
|
||||
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player), lambda state: state.can_kill_most_things(player))
|
||||
|
||||
|
||||
|
||||
|
||||
def set_trock_key_rules(world, player):
|
||||
|
||||
@@ -759,14 +733,14 @@ def set_trock_key_rules(world, player):
|
||||
all_state = world.get_all_state(True)
|
||||
|
||||
# Check if each of the four main regions of the dungoen can be reached. The previous code section prevents key-costing moves within the dungeon.
|
||||
can_reach_back = all_state.can_reach(world.get_region('Turtle Rock (Eye Bridge)', player)) if world.can_access_trock_eyebridge is None else world.can_access_trock_eyebridge
|
||||
world.can_access_trock_eyebridge = can_reach_back
|
||||
can_reach_front = all_state.can_reach(world.get_region('Turtle Rock (Entrance)', player)) if world.can_access_trock_front is None else world.can_access_trock_front
|
||||
world.can_access_trock_front = can_reach_front
|
||||
can_reach_big_chest = all_state.can_reach(world.get_region('Turtle Rock (Big Chest)', player)) if world.can_access_trock_big_chest is None else world.can_access_trock_big_chest
|
||||
world.can_access_trock_big_chest = can_reach_big_chest
|
||||
can_reach_middle = all_state.can_reach(world.get_region('Turtle Rock (Second Section)', player)) if world.can_access_trock_middle is None else world.can_access_trock_middle
|
||||
world.can_access_trock_middle = can_reach_middle
|
||||
can_reach_back = all_state.can_reach(world.get_region('Turtle Rock (Eye Bridge)', player)) if world.can_access_trock_eyebridge[player] is None else world.can_access_trock_eyebridge[player]
|
||||
world.can_access_trock_eyebridge[player] = can_reach_back
|
||||
can_reach_front = all_state.can_reach(world.get_region('Turtle Rock (Entrance)', player)) if world.can_access_trock_front[player] is None else world.can_access_trock_front[player]
|
||||
world.can_access_trock_front[player] = can_reach_front
|
||||
can_reach_big_chest = all_state.can_reach(world.get_region('Turtle Rock (Big Chest)', player)) if world.can_access_trock_big_chest[player] is None else world.can_access_trock_big_chest[player]
|
||||
world.can_access_trock_big_chest[player] = can_reach_big_chest
|
||||
can_reach_middle = all_state.can_reach(world.get_region('Turtle Rock (Second Section)', player)) if world.can_access_trock_middle[player] is None else world.can_access_trock_middle[player]
|
||||
world.can_access_trock_middle[player] = can_reach_middle
|
||||
|
||||
# No matter what, the key requirement for going from the middle to the bottom should be three keys.
|
||||
set_rule(world.get_entrance('Turtle Rock Dark Room Staircase', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 3))
|
||||
@@ -793,14 +767,16 @@ def set_trock_key_rules(world, player):
|
||||
# might open all the locked doors in any order so we need maximally restrictive rules.
|
||||
if can_reach_back:
|
||||
set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: (state.has_key('Small Key (Turtle Rock)', player, 4) or item_name(state, 'Turtle Rock - Big Key Chest', player) == ('Small Key (Turtle Rock)', player)))
|
||||
if world.accessibility != 'locations':
|
||||
set_always_allow(world.get_location('Turtle Rock - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Turtle Rock)' and item.player == player)
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Turtle Rock - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Turtle Rock)' and item.player == player
|
||||
and state.can_reach(world.get_region('Turtle Rock (Eye Bridge)', player)))
|
||||
else:
|
||||
forbid_item(world.get_location('Turtle Rock - Big Key Chest', player), 'Small Key (Turtle Rock)', player)
|
||||
elif can_reach_front and can_reach_middle:
|
||||
set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, tr_big_key_chest_keys_needed(state)))
|
||||
if world.accessibility != 'locations':
|
||||
set_always_allow(world.get_location('Turtle Rock - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Turtle Rock)' and item.player == player)
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Turtle Rock - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Turtle Rock)' and item.player == player
|
||||
and state.can_reach(world.get_region('Turtle Rock (Entrance)', player)) and state.can_reach(world.get_region('Turtle Rock (Second Section)', player)))
|
||||
else:
|
||||
forbid_item(world.get_location('Turtle Rock - Big Key Chest', player), 'Small Key (Turtle Rock)', player)
|
||||
non_big_key_locations += ['Turtle Rock - Crystaroller Room', 'Turtle Rock - Eye Bridge - Bottom Left',
|
||||
@@ -810,8 +786,9 @@ def set_trock_key_rules(world, player):
|
||||
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 2))
|
||||
set_rule(world.get_entrance('Turtle Rock Pokey Room', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 1))
|
||||
set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, tr_big_key_chest_keys_needed(state)))
|
||||
if world.accessibility != 'locations':
|
||||
set_always_allow(world.get_location('Turtle Rock - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Turtle Rock)' and item.player == player and state.has_key('Small Key (Turtle Rock)', player, 2))
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Turtle Rock - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Turtle Rock)' and item.player == player and state.has_key('Small Key (Turtle Rock)', player, 2)
|
||||
and state.can_reach(world.get_region('Turtle Rock (Entrance)', player)))
|
||||
else:
|
||||
forbid_item(world.get_location('Turtle Rock - Big Key Chest', player), 'Small Key (Turtle Rock)', player)
|
||||
non_big_key_locations += ['Turtle Rock - Crystaroller Room', 'Turtle Rock - Eye Bridge - Bottom Left',
|
||||
@@ -820,24 +797,25 @@ def set_trock_key_rules(world, player):
|
||||
elif can_reach_big_chest:
|
||||
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 2) if item_in_locations(state, 'Big Key (Turtle Rock)', player, [('Turtle Rock - Compass Chest', player), ('Turtle Rock - Roller Room - Left', player), ('Turtle Rock - Roller Room - Right', player)]) else state.has_key('Small Key (Turtle Rock)', player, 4))
|
||||
set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: (state.has_key('Small Key (Turtle Rock)', player, 4) or item_name(state, 'Turtle Rock - Big Key Chest', player) == ('Small Key (Turtle Rock)', player)))
|
||||
if world.accessibility != 'locations':
|
||||
set_always_allow(world.get_location('Turtle Rock - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Turtle Rock)' and item.player == player)
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Turtle Rock - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Turtle Rock)' and item.player == player
|
||||
and state.can_reach(world.get_region('Turtle Rock (Big Chest)', player)))
|
||||
else:
|
||||
forbid_item(world.get_location('Turtle Rock - Big Key Chest', player), 'Small Key (Turtle Rock)', player)
|
||||
non_big_key_locations += ['Turtle Rock - Crystaroller Room', 'Turtle Rock - Eye Bridge - Bottom Left',
|
||||
'Turtle Rock - Eye Bridge - Bottom Right', 'Turtle Rock - Eye Bridge - Top Left',
|
||||
'Turtle Rock - Eye Bridge - Top Right']
|
||||
if not world.keysanity:
|
||||
if not world.keyshuffle[player]:
|
||||
non_big_key_locations += ['Turtle Rock - Big Key Chest']
|
||||
else:
|
||||
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 2) if item_in_locations(state, 'Big Key (Turtle Rock)', player, [('Turtle Rock - Compass Chest', player), ('Turtle Rock - Roller Room - Left', player), ('Turtle Rock - Roller Room - Right', player)]) else state.has_key('Small Key (Turtle Rock)', player, 4))
|
||||
set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: (state.has_key('Small Key (Turtle Rock)', player, 4) or item_name(state, 'Turtle Rock - Big Key Chest', player) == ('Small Key (Turtle Rock)', player)))
|
||||
if world.accessibility != 'locations':
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Turtle Rock - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Turtle Rock)' and item.player == player)
|
||||
non_big_key_locations += ['Turtle Rock - Crystaroller Room', 'Turtle Rock - Eye Bridge - Bottom Left',
|
||||
'Turtle Rock - Eye Bridge - Bottom Right', 'Turtle Rock - Eye Bridge - Top Left',
|
||||
'Turtle Rock - Eye Bridge - Top Right']
|
||||
if not world.keysanity:
|
||||
if not world.keyshuffle[player]:
|
||||
non_big_key_locations += ['Turtle Rock - Big Key Chest', 'Turtle Rock - Chain Chomps']
|
||||
|
||||
# set big key restrictions
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
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):
|
||||
value = value & 0xFFFF
|
||||
return [value & 0xFF, (value >> 8) & 0xFF]
|
||||
|
||||
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user