0.5.2 release

0.5.2 release
This commit is contained in:
AmazingAmpharos 2018-01-08 10:45:25 -06:00 committed by GitHub
commit 8a38128976
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
99 changed files with 2190 additions and 641 deletions

56
Adjuster.py Executable file
View File

@ -0,0 +1,56 @@
#!/usr/bin/env python3
import argparse
import os
import logging
import textwrap
import sys
from AdjusterMain import adjust
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
def _get_help_string(self, action):
return textwrap.dedent(action.help)
def main():
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument('--rom', default='ER_base.sfc', help='Path to an ALttP JAP(1.0) rom to use as a base.')
parser.add_argument('--loglevel', default='info', const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
parser.add_argument('--fastmenu', default='normal', const='normal', nargs='?', choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
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('--heartbeep', default='normal', const='normal', nargs='?', choices=['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('--sprite', help='''\
Path to a sprite sheet to use for Link. Needs to be in
binary format and have a length of 0x7000 (28672) bytes,
or 0x7078 (28792) bytes including palette data.
Alternatively, can be a ALttP Rom patched with a Link
sprite that will be extracted.
''')
args = parser.parse_args()
# ToDo: Validate files further than mere existance
if not os.path.isfile(args.rom):
input('Could not find valid base rom for patching at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.rom)
sys.exit(1)
if args.sprite is not None and not os.path.isfile(args.sprite):
input('Could not find link sprite sheet at given location. \nPress Enter to exit.' % args.sprite)
sys.exit(1)
# set up logger
loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[args.loglevel]
logging.basicConfig(format='%(message)s', level=loglevel)
adjust(args=args)
if __name__ == '__main__':
main()

36
AdjusterMain.py Normal file
View File

@ -0,0 +1,36 @@
import os
import time
import logging
from Utils import output_path
from Rom import LocalRom, Sprite, apply_rom_settings
def adjust(args):
start = time.clock()
logger = logging.getLogger('')
logger.info('Patching ROM.')
if args.sprite is not None:
if isinstance(args.sprite, Sprite):
sprite = args.sprite
else:
sprite = Sprite(args.sprite)
else:
sprite = None
outfilebase = os.path.basename(args.rom)[:-4] + '_adjusted'
if os.stat(args.rom).st_size == 2097152 and os.path.splitext(args.rom)[-1].lower() == '.sfc':
rom = LocalRom(args.rom, False)
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.quickswap, args.fastmenu, args.disablemusic, sprite)
rom.write_to_file(output_path('%s.sfc' % outfilebase))
logger.info('Done. Enjoy.')
logger.debug('Total Time: %s', time.clock() - start)
return args

View File

@ -26,7 +26,6 @@ class World(object):
self._region_cache = {}
self._entrance_cache = {}
self._location_cache = {}
self._item_cache = {}
self.required_locations = []
self.place_dungeon_items = place_dungeon_items # configurable in future
self.shuffle_bonk_prizes = False
@ -53,6 +52,8 @@ class World(object):
self.fastmenu = fastmenu
self.disable_music = disable_music
self.keysanity = keysanity
self.can_take_damage = True
self.difficulty_requirements = None
self.spoiler = Spoiler(self)
def intialize_regions(self):
@ -105,13 +106,13 @@ class World(object):
if 'Sword' in item.name:
if ret.has('Golden Sword'):
pass
elif ret.has('Tempered Sword'):
elif ret.has('Tempered Sword') and self.difficulty_requirements.progressive_sword_limit >= 4:
ret.prog_items.append('Golden Sword')
elif ret.has('Master Sword'):
elif ret.has('Master Sword') and self.difficulty_requirements.progressive_sword_limit >= 3:
ret.prog_items.append('Tempered Sword')
elif ret.has('Fighter Sword'):
elif ret.has('Fighter Sword') and self.difficulty_requirements.progressive_sword_limit >= 2:
ret.prog_items.append('Master Sword')
else:
elif self.difficulty_requirements.progressive_sword_limit >= 1:
ret.prog_items.append('Fighter Sword')
elif 'Glove' in item.name:
if ret.has('Titans Mitts'):
@ -120,7 +121,18 @@ class World(object):
ret.prog_items.append('Titans Mitts')
else:
ret.prog_items.append('Power Glove')
elif 'Shield' in item.name:
if ret.has('Mirror Shield'):
pass
elif ret.has('Red Shield') and self.difficulty_requirements.progressive_shield_limit >= 3:
ret.prog_items.append('Mirror Shield')
elif ret.has('Blue Shield') and self.difficulty_requirements.progressive_shield_limit >= 2:
ret.prog_items.append('Red Shield')
elif self.difficulty_requirements.progressive_shield_limit >= 1:
ret.prog_items.append('Blue Shield')
elif item.name.startswith('Bottle'):
if ret.bottle_count() < self.difficulty_requirements.progressive_bottle_limit:
ret.prog_items.append(item.name)
elif item.advancement or item.key:
ret.prog_items.append(item.name)
@ -133,9 +145,12 @@ class World(object):
'Small Key (Swamp Palace)', 'Big Key (Ice Palace)'] + ['Small Key (Ice Palace)'] * 2 + ['Big Key (Misery Mire)', 'Big Key (Turtle Rock)', 'Big Key (Ganons Tower)'] + ['Small Key (Misery Mire)'] * 3 + ['Small Key (Turtle Rock)'] * 4 + ['Small Key (Ganons Tower)'] * 4):
soft_collect(item)
ret.sweep_for_events()
ret._clear_cache()
ret.clear_cached_unreachable()
return ret
def get_items(self):
return [loc.item for loc in self.get_filled_locations()] + self.itempool
def find_items(self, item):
return [location for location in self.get_locations() if location.item is not None and location.item.name == item]
@ -143,13 +158,13 @@ class World(object):
if not isinstance(location, Location):
location = self.get_location(location)
if location.can_fill(item):
if location.can_fill(self.state, item, False):
location.item = item
item.location = location
if collect:
self.state.collect(item, location.event)
self.state.collect(item, location.event, location)
logging.getLogger('').debug('Placed %s at %s' % (item, location))
logging.getLogger('').debug('Placed %s at %s', item, location)
else:
raise RuntimeError('Cannot assign item %s to location %s.' % (item, location))
@ -178,7 +193,7 @@ class World(object):
def unlocks_new_location(self, item):
temp_state = self.state.copy()
temp_state._clear_cache()
temp_state.clear_cached_unreachable()
temp_state.collect(item, True)
for location in self.get_unfilled_locations():
@ -188,20 +203,25 @@ class World(object):
return False
def has_beaten_game(self, state):
if state.has('Triforce'): return True
if state.has('Triforce'):
return True
if self.goal in ['triforcehunt']:
if state.item_count('Triforce Piece')+state.item_count('Power Star')> self.treasure_hunt_count:
if state.item_count('Triforce Piece') + state.item_count('Power Star') > self.treasure_hunt_count:
return True
return False
def can_beat_game(self, starting_state=None):
prog_locations = [location for location in self.get_locations() if location.item is not None and (location.item.advancement or location.event)]
if starting_state:
state = starting_state.copy()
else:
state = CollectionState(self)
treasure_pieces_collected = 0
if self.has_beaten_game(state):
return True
prog_locations = [location for location in self.get_locations() if location.item is not None and (location.item.advancement or location.event) and location not in state.locations_checked]
treasure_pieces_collected = state.item_count('Triforce Piece') + state.item_count('Power Star')
while prog_locations:
sphere = []
# build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
@ -221,7 +241,7 @@ class World(object):
for location in sphere:
prog_locations.remove(location)
state.collect(location.item, True)
state.collect(location.item, True, location)
return False
@ -233,7 +253,7 @@ class World(object):
goal = ['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'crystals'].index(self.goal)
shuffle = ['vanilla', 'simple', 'restricted', 'full', 'madness', 'insanity', 'dungeonsfull', 'dungeonssimple'].index(self.shuffle)
difficulty = ['easy', 'normal', 'hard', 'expert', 'insane'].index(self.difficulty)
timer = ['none', 'display', 'timed', 'timed-ohko', 'timed-countdown','ohko'].index(self.timer)
timer = ['none', 'display', 'timed', 'timed-ohko', 'timed-countdown', 'ohko'].index(self.timer)
progressive = ['on', 'off', 'random'].index(self.progressive)
algorithm = ['freshness', 'flood', 'vt21', 'vt22', 'vt25', 'vt26', 'balanced'].index(self.algorithm)
beatableonly = 1 if self.check_beatable_only else 0
@ -252,8 +272,11 @@ class CollectionState(object):
self.entrance_cache = {}
self.recursion_count = 0
self.events = []
self.path = {}
self.locations_checked = set()
def _clear_cache(self):
def clear_cached_unreachable(self):
# we only need to invalidate results which were False, places we could reach before we can still reach after adding more items
self.region_cache = {k: v for k, v in self.region_cache.items() if v}
self.location_cache = {k: v for k, v in self.location_cache.items() if v}
@ -266,6 +289,8 @@ class CollectionState(object):
ret.location_cache = copy.copy(self.location_cache)
ret.entrance_cache = copy.copy(self.entrance_cache)
ret.events = copy.copy(self.events)
ret.path = copy.copy(self.path)
ret.locations_checked = copy.copy(self.locations_checked)
return ret
def can_reach(self, spot, resolution_hint=None):
@ -321,15 +346,14 @@ class CollectionState(object):
for event in reachable_events:
if event.name not in self.events:
self.events.append(event.name)
self.collect(event.item, True)
self.collect(event.item, True, event)
new_locations = len(reachable_events) > checked_locations
checked_locations = len(reachable_events)
def has(self, item, count=1):
if count == 1:
return item in self.prog_items
else:
return self.item_count(item) >= count
return self.item_count(item) >= count
def item_count(self, item):
return len([pritem for pritem in self.prog_items if pritem == item])
@ -338,11 +362,51 @@ class CollectionState(object):
return self.has('Power Glove') or self.has('Titans Mitts')
def has_bottle(self):
return self.has('Bottle') or self.has('Bottle (Red Potion)') or self.has('Bottle (Green Potion)') or self.has('Bottle (Blue Potion)') or self.has('Bottle (Fairy)') or self.has('Bottle (Bee)') or self.has('Bottle (Good Bee)')
return self.bottle_count() > 0
def bottle_count(self):
return len([pritem for pritem in self.prog_items if pritem.startswith('Bottle')])
def has_hearts(self, count):
# Warning: This oncly considers items that are marked as advancement items
return self.heart_count() >= count
def heart_count(self):
# Warning: This oncly considers items that are marked as advancement items
return (
self.item_count('Boss Heart Container')
+ self.item_count('Sanctuary Heart Container')
+ self.item_count('Piece of Heart') // 4
+ 3 # starting hearts
)
def can_lift_heavy_rocks(self):
return self.has('Titans Mitts')
def can_extend_magic(self, smallmagic=8): #This reflects the total magic Link has, not the total extra he has.
basemagic = 8
if self.has('Quarter Magic'):
basemagic = 32
elif self.has('Half Magic'):
basemagic = 16
if self.world.difficulty == 'hard':
basemagic = basemagic + int(basemagic * 0.5 * self.bottle_count())
elif self.world.difficulty == 'expert':
basemagic = basemagic + int(basemagic * 0.25 * self.bottle_count())
elif self.world.difficulty == 'insane':
basemagic = basemagic
else:
basemagic = basemagic + basemagic * self.bottle_count()
return basemagic >= smallmagic # FIXME bottle should really also have a requirement that we can reach some shop that sells green or blue potions
def can_kill_most_things(self, enemies=5):
return (self.has_blunt_weapon()
or self.has('Cane of Somaria')
or (self.has('Cane of Byrna') and (enemies < 6 or self.can_extend_Magic()))
or self.has('Bow')
or self.has('Fire Rod')
)
def has_sword(self):
return self.has('Fighter Sword') or self.has('Master Sword') or self.has('Tempered Sword') or self.has('Golden Sword')
@ -370,22 +434,24 @@ class CollectionState(object):
def has_turtle_rock_medallion(self):
return self.has(self.world.required_medallions[1])
def collect(self, item, event=False):
def collect(self, item, event=False, location=None):
if location:
self.locations_checked.add(location)
changed = False
if item.name.startswith('Progressive '):
if 'Sword' in item.name:
if self.has('Golden Sword'):
pass
elif self.has('Tempered Sword'):
elif self.has('Tempered Sword') and self.world.difficulty_requirements.progressive_sword_limit >= 4:
self.prog_items.append('Golden Sword')
changed = True
elif self.has('Master Sword'):
elif self.has('Master Sword') and self.world.difficulty_requirements.progressive_sword_limit >= 3:
self.prog_items.append('Tempered Sword')
changed = True
elif self.has('Fighter Sword'):
elif self.has('Fighter Sword') and self.world.difficulty_requirements.progressive_sword_limit >= 2:
self.prog_items.append('Master Sword')
changed = True
else:
elif self.world.difficulty_requirements.progressive_sword_limit >= 1:
self.prog_items.append('Fighter Sword')
changed = True
elif 'Glove' in item.name:
@ -400,25 +466,28 @@ class CollectionState(object):
elif 'Shield' in item.name:
if self.has('Mirror Shield'):
pass
elif self.has('Red Shield'):
elif self.has('Red Shield') and self.world.difficulty_requirements.progressive_shield_limit >= 3:
self.prog_items.append('Mirror Shield')
changed = True
elif self.has('Blue Shield'):
elif self.has('Blue Shield') and self.world.difficulty_requirements.progressive_shield_limit >= 2:
self.prog_items.append('Red Shield')
changed = True
else:
elif self.world.difficulty_requirements.progressive_shield_limit >= 1:
self.prog_items.append('Blue Shield')
changed = True
elif item.name.startswith('Bottle'):
if self.bottle_count() < self.world.difficulty_requirements.progressive_bottle_limit:
self.prog_items.append(item.name)
changed = True
elif event or item.advancement:
self.prog_items.append(item.name)
changed = True
if changed:
self._clear_cache()
self.clear_cached_unreachable()
if not event:
self.sweep_for_events()
self._clear_cache()
self.clear_cached_unreachable()
def remove(self, item):
if item.advancement:
@ -473,6 +542,7 @@ class Region(object):
self.locations = []
self.dungeon = None
self.world = None
self.is_light_world = False # will be set aftermaking connections.
self.spot_type = 'Region'
self.hint_text = 'Hyrule'
self.recursion_count = 0
@ -480,6 +550,8 @@ class Region(object):
def can_reach(self, state):
for entrance in self.entrances:
if state.can_reach(entrance):
if not self in state.path:
state.path[self] = (self.name, state.path.get(entrance, None))
return True
return False
@ -487,10 +559,7 @@ class Region(object):
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):
if self.dungeon and self.dungeon.is_dungeon_item(item):
return True
else:
return False
return self.dungeon and self.dungeon.is_dungeon_item(item)
return True
@ -512,12 +581,12 @@ class Entrance(object):
self.spot_type = 'Entrance'
self.recursion_count = 0
self.vanilla = None
def access_rule(self, state):
return True
self.access_rule = lambda state: True
def can_reach(self, state):
if self.access_rule(state) and state.can_reach(self.parent_region):
if not self in state.path:
state.path[self] = (self.name, state.path.get(self.parent_region, (self.parent_region.name, None)))
return True
return False
@ -576,15 +645,12 @@ class Location(object):
self.recursion_count = 0
self.staleness_count = 0
self.event = False
self.always_allow = lambda item, state: False
self.access_rule = lambda state: True
self.item_rule = lambda item: True
def access_rule(self, state):
return True
def item_rule(self, item):
return True
def can_fill(self, item):
return self.parent_region.can_fill(item) and self.item_rule(item)
def can_fill(self, state, item, check_access=True):
return self.always_allow(item, self) or (self.parent_region.can_fill(item) and self.item_rule(item) and (not check_access or self.can_reach(state)))
def can_reach(self, state):
if self.access_rule(state) and state.can_reach(self.parent_region):
@ -650,6 +716,7 @@ class Spoiler(object):
self.medallions = {}
self.playthrough = {}
self.locations = {}
self.paths = {}
self.metadata = {}
def set_entrance(self, entrance, exit, direction):
@ -683,6 +750,7 @@ class Spoiler(object):
out.update(self.locations)
out['medallions'] = self.medallions
out['playthrough'] = self.playthrough
out['paths'] = self.paths
out['meta'] = self.metadata
return json.dumps(out)
@ -698,7 +766,7 @@ class Spoiler(object):
outfile.write('All Locations Accessible: %s\n' % ('Yes' if self.metadata['completeable'] else 'No, some locations may be unreachable'))
outfile.write('Maps and Compasses in Dungeons: %s\n' % ('Yes' if self.metadata['dungeonitems'] else 'No'))
outfile.write('L\\R Quickswap enabled: %s\n' % ('Yes' if self.metadata['quickswap'] else 'No'))
outfile.write('Fastmenu enabled: %s\n' % ('Yes' if self.metadata['fastmenu'] else 'No'))
outfile.write('Menu speed: %s\n' % self.metadata['fastmenu'])
outfile.write('Keysanity enabled: %s' % ('Yes' if self.metadata['keysanity'] else 'No'))
if self.entrances:
outfile.write('\n\nEntrances:\n\n')
@ -710,3 +778,16 @@ class Spoiler(object):
outfile.write('\n'.join(['%s: %s' % (location, item) for (location, item) in self.locations['other locations'].items()]))
outfile.write('\n\nPlaythrough:\n\n')
outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join([' %s: %s' % (location, item) for (location, item) in sphere.items()])) for (sphere_nr, sphere) in self.playthrough.items()]))
outfile.write('\n\nPaths:\n\n')
path_listings = []
for location, path in sorted(self.paths.items()):
path_lines = []
for region, exit in path:
if exit is not None:
path_lines.append("{} -> {}".format(region, exit))
else:
path_lines.append(region)
path_listings.append("{}\n {}".format(location, "\n => ".join(path_lines)))
outfile.write('\n'.join(path_listings))

View File

@ -1,7 +1,8 @@
from Items import ItemFactory
import random
from BaseClasses import Dungeon
from Fill import fill_restrictive
import random
from Items import ItemFactory
def create_dungeons(world):
@ -18,7 +19,7 @@ def create_dungeons(world):
AT = make_dungeon('Agahnims Tower', ['Agahnims Tower', 'Agahnim 1'], None, ItemFactory(['Small Key (Agahnims Tower)'] * 2), [])
PoD = make_dungeon('Palace of Darkness', ['Palace of Darkness (Entrance)', 'Palace of Darkness (Center)', 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness (Bonk Section)', 'Palace of Darkness (North)', 'Palace of Darkness (Maze)', 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness (Final Section)'], ItemFactory('Big Key (Palace of Darkness)'), ItemFactory(['Small Key (Palace of Darkness)'] * 6), ItemFactory(['Map (Palace of Darkness)', 'Compass (Palace of Darkness)']))
TT = make_dungeon('Thieves Town', ['Thieves Town (Entrance)', 'Thieves Town (Deep)', 'Blind Fight'], ItemFactory('Big Key (Thieves Town)'), [ItemFactory('Small Key (Thieves Town)')], ItemFactory(['Map (Thieves Town)', 'Compass (Thieves Town)']))
SW = make_dungeon('Skull Woods', ['Skull Woods Final Section (Entrance)', 'Skull Woods First Section', 'Skull Woods Second Section', 'Skull Woods Final Section (Mothula)', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Left)', 'Skull Woods First Section (Top)'], ItemFactory('Big Key (Skull Woods)'), ItemFactory(['Small Key (Skull Woods)'] * 2), ItemFactory(['Map (Skull Woods)', 'Compass (Skull Woods)']))
SW = make_dungeon('Skull Woods', ['Skull Woods Final Section (Entrance)', 'Skull Woods First Section', 'Skull Woods Second Section', 'Skull Woods Second Section (Drop)', 'Skull Woods Final Section (Mothula)', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Left)', 'Skull Woods First Section (Top)'], ItemFactory('Big Key (Skull Woods)'), ItemFactory(['Small Key (Skull Woods)'] * 2), ItemFactory(['Map (Skull Woods)', 'Compass (Skull Woods)']))
SP = make_dungeon('Swamp Palace', ['Swamp Palace (Entrance)', 'Swamp Palace (First Room)', 'Swamp Palace (Starting Area)', 'Swamp Palace (Center)', 'Swamp Palace (North)'], ItemFactory('Big Key (Swamp Palace)'), [ItemFactory('Small Key (Swamp Palace)')], ItemFactory(['Map (Swamp Palace)', 'Compass (Swamp Palace)']))
IP = make_dungeon('Ice Palace', ['Ice Palace (Entrance)', 'Ice Palace (Main)', 'Ice Palace (East)', 'Ice Palace (East Top)', 'Ice Palace (Kholdstare)'], ItemFactory('Big Key (Ice Palace)'), ItemFactory(['Small Key (Ice Palace)'] * 2), ItemFactory(['Map (Ice Palace)', 'Compass (Ice Palace)']))
MM = make_dungeon('Misery Mire', ['Misery Mire (Entrance)', 'Misery Mire (Main)', 'Misery Mire (West)', 'Misery Mire (Final Area)', 'Misery Mire (Vitreous)'], ItemFactory('Big Key (Misery Mire)'), ItemFactory(['Small Key (Misery Mire)'] * 3), ItemFactory(['Map (Misery Mire)', 'Compass (Misery Mire)']))
@ -62,7 +63,7 @@ def fill_dungeons(world):
world.push_item(bk_location, big_key, False)
bk_location.event = True
dungeon_locations.remove(bk_location)
all_state._clear_cache()
all_state.clear_cached_unreachable()
big_key = None
# next place small keys
@ -88,7 +89,7 @@ def fill_dungeons(world):
world.push_item(sk_location, small_key, False)
sk_location.event = True
dungeon_locations.remove(sk_location)
all_state._clear_cache()
all_state.clear_cached_unreachable()
if small_keys:
# key placement not finished, loop again
@ -100,8 +101,10 @@ def fill_dungeons(world):
di_location = dungeon_locations.pop()
world.push_item(di_location, dungeon_item, False)
world.state._clear_cache()
world.state.clear_cached_unreachable()
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]
def fill_dungeons_restrictive(world, shuffled_locations):
all_state_base = world.get_all_state()
@ -111,7 +114,16 @@ def fill_dungeons_restrictive(world, shuffled_locations):
skull_woods_big_chest.event = True
shuffled_locations.remove(skull_woods_big_chest)
dungeon_items = [item for dungeon in world.dungeons for item in dungeon.all_items if item.key or world.place_dungeon_items]
if world.keysanity:
#in keysanity dungeon items are distributed as part of the normal item pool
for item in world.get_items():
if item.key:
item.advancement = True
elif item.map or item.compass:
item.priority = True
return
dungeon_items = get_dungeon_item_pool(world)
# sort in the order Big Key, Small Key, Other before placing dungeon items
sort_order = {"BigKey": 3, "SmallKey": 2}
@ -119,7 +131,7 @@ def fill_dungeons_restrictive(world, shuffled_locations):
fill_restrictive(world, all_state_base, shuffled_locations, dungeon_items)
world.state._clear_cache()
world.state.clear_cached_unreachable()
dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A],

18
EntranceRandomizer.py Normal file → Executable file
View File

@ -1,3 +1,4 @@
#!/usr/bin/env python3
import argparse
import os
import logging
@ -5,8 +6,8 @@ import random
import textwrap
import sys
from Main import main
from Gui import guiMain
from Main import main
from Utils import is_bundled, close_console
@ -16,7 +17,7 @@ class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
return textwrap.dedent(action.help)
if __name__ == '__main__':
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'],
@ -151,7 +152,11 @@ if __name__ == '__main__':
--seed given will produce the same 10 (different) roms each
time).
''', type=int)
parser.add_argument('--fastmenu', help='Enable instant menu', action='store_true')
parser.add_argument('--fastmenu', default='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='''\
@ -191,7 +196,7 @@ if __name__ == '__main__':
''')
args = parser.parse_args()
if is_bundled and len(sys.argv) == 1 :
if is_bundled() and len(sys.argv) == 1:
# for the bundled builds, if we have no arguments, the user
# probably wants the gui. Users of the bundled build who want the command line
# interface shouuld specify at least one option, possibly setting a value to a
@ -219,8 +224,11 @@ if __name__ == '__main__':
guiMain(args)
elif args.count is not None:
seed = args.seed
for i in range(args.count):
for _ in range(args.count):
main(seed=seed, args=args)
seed = random.randint(0, 999999999)
else:
main(seed=args.seed, args=args)
if __name__ == '__main__':
start()

View File

@ -293,7 +293,7 @@ def link_entrances(world):
('North Fairy Cave Exit', 'North Fairy Cave'),
('Lost Woods Hideout Exit', 'Lost Woods Hideout (top)'),
('Lumberjack Tree Exit', 'Lumberjack Tree (top)'),
(('Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)'), 'Skull Woods Second Section')]
(('Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)'), 'Skull Woods Second Section (Drop)')]
if world.mode == 'standard':
# cannot move uncle cave
@ -519,7 +519,7 @@ def link_entrances(world):
hole_entrances = ['Kakariko Well Drop', 'Bat Cave Drop', 'North Fairy Cave Drop', 'Lost Woods Hideout Drop', 'Lumberjack Tree Tree', 'Sanctuary Grave',
'Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole']
hole_targets = ['Kakariko Well (top)', 'Bat Cave (right)', 'North Fairy Cave', 'Lost Woods Hideout (top)', 'Lumberjack Tree (top)', 'Sewer Drop', 'Skull Woods Second Section',
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':
@ -834,7 +834,7 @@ def connect_doors(world, doors, targets):
def skull_woods_shuffle(world):
connect_random(world, ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'],
['Skull Woods First Section (Left)', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Top)', 'Skull Woods Second Section'])
['Skull Woods First Section (Left)', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Top)', 'Skull Woods Second Section (Drop)'])
connect_random(world, ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)'],
['Skull Woods First Section Exit', 'Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)'], True)
@ -1162,6 +1162,7 @@ mandatory_connections = [('Links House', 'Links House'), # unshuffled. For now
('Desert Palace Entrance (North) Rocks', 'Desert Palace Entrance (North) Spot'),
('Desert Ledge Return Rocks', 'Desert Ledge'),
('Hyrule Castle Ledge Courtyard Drop', 'Hyrule Castle Courtyard'),
('Hyrule Castle Main Gate', 'Hyrule Castle Courtyard'),
('Throne Room', 'Sewers (Dark)'),
('Sewers Door', 'Sewers'),
('Sanctuary Push Door', 'Sanctuary'),
@ -1172,6 +1173,8 @@ mandatory_connections = [('Links House', 'Links House'), # unshuffled. For now
('Spectacle Rock Cave Drop', 'Spectacle Rock Cave (Bottom)'),
('Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave (Bottom)'),
('Death Mountain Return Ledge Drop', 'Light World'),
('Old Man House Front to Back', 'Old Man House Back'),
('Old Man House Back to Front', 'Old Man House'),
('Broken Bridge (West)', 'East Death Mountain (Bottom)'),
('Broken Bridge (East)', 'Death Mountain'),
('East Death Mountain Drop', 'East Death Mountain (Bottom)'),
@ -1231,6 +1234,7 @@ mandatory_connections = [('Links House', 'Links House'), # unshuffled. For now
('Turtle Rock Teleporter', 'Turtle Rock (Top)'),
('Turtle Rock Drop', 'Dark Death Mountain (Top)'),
('Floating Island Drop', 'Dark Death Mountain (Top)'),
('Floating Island Mirror Spot', 'Death Mountain Floating Island (Light World)'),
('East Death Mountain Teleporter', 'Dark Death Mountain (East Bottom)'),
('Isolated Ledge Mirror Spot', 'Fairy Ascension Ledge'),
('Spiral Cave Mirror Spot', 'Spiral Cave Ledge'),
@ -1248,6 +1252,7 @@ mandatory_connections = [('Links House', 'Links House'), # unshuffled. For now
('Skull Woods First Section (Left) Door to Right', 'Skull Woods First Section (Right)'),
('Skull Woods First Section (Left) Door to Exit', 'Skull Woods First Section'),
('Skull Woods First Section (Top) One-Way Path', 'Skull Woods First Section'),
('Skull Woods Second Section (Drop)', 'Skull Woods Second Section'),
('Blind Fight', 'Blind Fight'),
('Ice Palace Entrance Room', 'Ice Palace (Main)'),
('Ice Palace (East)', 'Ice Palace (East)'),
@ -1293,7 +1298,7 @@ mandatory_connections = [('Links House', 'Links House'), # unshuffled. For now
('Ganons Tower Moldorm Gap', 'Agahnim 2'),
('Ganon Drop', 'Bottom of Pyramid'),
('Pyramid Drop', 'East Dark World')
]
]
# non-shuffled entrance links
default_connections = [('Waterfall of Wishing', 'Waterfall of Wishing'),
@ -1438,14 +1443,14 @@ default_connections = [('Waterfall of Wishing', 'Waterfall of Wishing'),
('Superbunny Cave Exit (Top)', 'Dark Death Mountain (Top)'),
('Superbunny Cave Exit (Bottom)', 'Dark Death Mountain (East Bottom)'),
('Hookshot Cave Exit (South)', 'Dark Death Mountain (Top)'),
('Hookshot Cave Exit (North)', 'Death Mountain Floating Island'),
('Hookshot Cave Exit (North)', 'Death Mountain Floating Island (Dark World)'),
('Hookshot Cave Back Entrance', 'Hookshot Cave'),
('Mimic Cave Mirror Spot', 'Mimic Cave'),
('Pyramid Hole', 'Pyramid'),
('Pyramid Exit', 'Pyramid Ledge'),
('Pyramid Entrance', 'Bottom of Pyramid')
]
]
# non shuffled dungeons
default_dungeon_connections = [('Desert Palace Entrance (South)', 'Desert Palace Main'),
@ -1478,7 +1483,7 @@ default_dungeon_connections = [('Desert Palace Entrance (South)', 'Desert Palace
('Skull Woods First Section Hole (North)', 'Skull Woods First Section (Top)'),
('Skull Woods First Section Door', 'Skull Woods First Section'),
('Skull Woods First Section Exit', 'Skull Woods Forest'),
('Skull Woods Second Section Hole', 'Skull Woods Second Section'),
('Skull Woods Second Section Hole', 'Skull Woods Second Section (Drop)'),
('Skull Woods Second Section Door (East)', 'Skull Woods Second Section'),
('Skull Woods Second Section Door (West)', 'Skull Woods Second Section'),
('Skull Woods Second Section Exit (East)', 'Skull Woods Forest'),
@ -1505,7 +1510,7 @@ default_dungeon_connections = [('Desert Palace Entrance (South)', 'Desert Palace
('Ganons Tower', 'Ganons Tower (Entrance)'),
('Ganons Tower Exit', 'Dark Death Mountain (Top)')
]
]
# ToDo somehow merge this with creation of the locations
@ -1771,7 +1776,7 @@ exit_ids = {'Desert Palace Exit (South)': (0x09, 0x84),
'Lost Woods Hideout (top)': 0x7A,
'Lumberjack Tree (top)': 0x7F,
'Sewer Drop': 0x81,
'Skull Woods Second Section': 0x79,
'Skull Woods Second Section (Drop)': 0x79,
'Skull Woods First Section (Left)': 0x77,
'Skull Woods First Section (Right)': 0x78,
'Skull Woods First Section (Top)': 0x76,

27
Fill.py
View File

@ -56,8 +56,8 @@ def distribute_items_cutoff(world, cutoffrate=0.33):
raise RuntimeError('No more progress items left to place.')
spot_to_fill = None
for location in (fill_locations if placed_advancement_items / total_advancement_items < cutoffrate else reversed(fill_locations)):
if world.state.can_reach(location) and location.can_fill(item_to_place):
for location in fill_locations if placed_advancement_items / total_advancement_items < cutoffrate else reversed(fill_locations):
if location.can_fill(world.state, item_to_place):
spot_to_fill = location
break
@ -72,7 +72,7 @@ def distribute_items_cutoff(world, cutoffrate=0.33):
itempool.remove(item_to_place)
fill_locations.remove(spot_to_fill)
logging.getLogger('').debug('Unplaced items: %s - Unfilled Locations: %s' % ([item.name for item in itempool], [location.name for location in fill_locations]))
logging.getLogger('').debug('Unplaced items: %s - Unfilled Locations: %s', [item.name for item in itempool], [location.name for location in fill_locations])
def distribute_items_staleness(world):
@ -129,7 +129,7 @@ def distribute_items_staleness(world):
if not progress_done and random.randint(0, location.staleness_count) > 2:
continue
if world.state.can_reach(location) and location.can_fill(item_to_place):
if location.can_fill(world.state, item_to_place):
spot_to_fill = location
break
else:
@ -138,7 +138,7 @@ def distribute_items_staleness(world):
# might have skipped too many locations due to potential staleness. Do not check for staleness now to find a candidate
if spot_to_fill is None:
for location in fill_locations:
if world.state.can_reach(location) and location.can_fill(item_to_place):
if location.can_fill(world.state, item_to_place):
spot_to_fill = location
break
@ -153,7 +153,7 @@ def distribute_items_staleness(world):
itempool.remove(item_to_place)
fill_locations.remove(spot_to_fill)
logging.getLogger('').debug('Unplaced items: %s - Unfilled Locations: %s' % ([item.name for item in itempool], [location.name for location in fill_locations]))
logging.getLogger('').debug('Unplaced items: %s - Unfilled Locations: %s', [item.name for item in itempool], [location.name for location in fill_locations])
def fill_restrictive(world, base_state, locations, itempool):
@ -168,15 +168,16 @@ def fill_restrictive(world, base_state, locations, itempool):
item_to_place = itempool.pop()
maximum_exploration_state = sweep_from_pool()
perform_access_check = True
if world.check_beatable_only:
can_beat_without = world.has_beaten_game(maximum_exploration_state)
perform_access_check = not world.has_beaten_game(maximum_exploration_state)
spot_to_fill = None
for location in locations:
if location.can_fill(item_to_place):
if (world.check_beatable_only and can_beat_without) or maximum_exploration_state.can_reach(location):
spot_to_fill = location
break
if 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?
@ -226,7 +227,7 @@ def distribute_items_restrictive(world, gftower_trash_count=0, fill_locations=No
fast_fill(world, restitempool, fill_locations)
logging.getLogger('').debug('Unplaced items: %s - Unfilled Locations: %s' % ([item.name for item in progitempool + prioitempool + restitempool], [location.name for location in fill_locations]))
logging.getLogger('').debug('Unplaced items: %s - Unfilled Locations: %s', [item.name for item in progitempool + prioitempool + restitempool], [location.name for location in fill_locations])
def fast_fill(world, item_pool, fill_locations):
@ -251,7 +252,7 @@ def flood_items(world):
random.shuffle(location_list)
spot_to_fill = None
for location in location_list:
if world.state.can_reach(location) and location.can_fill(itempool[0]):
if location.can_fill(world.state, itempool[0]):
spot_to_fill = location
break

516
Gui.py Normal file → Executable file
View File

@ -1,11 +1,19 @@
from Main import main, __version__ as ESVersion
from Utils import is_bundled, local_path, output_path, open_file
#!/usr/bin/env python3
from argparse import Namespace
from glob import glob
import json
import random
import subprocess
import os
import sys
from tkinter import Checkbutton, OptionMenu, Tk, LEFT, RIGHT, BOTTOM, TOP, StringVar, IntVar, Frame, Label, W, E, X, Entry, Spinbox, Button, filedialog, messagebox, PhotoImage
import shutil
from tkinter import Checkbutton, OptionMenu, Toplevel, LabelFrame, PhotoImage, Tk, LEFT, RIGHT, BOTTOM, TOP, StringVar, IntVar, Frame, Label, W, E, X, Entry, Spinbox, Button, filedialog, messagebox, ttk
from urllib.parse import urlparse
from urllib.request import urlopen
from AdjusterMain import adjust
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
def guiMain(args=None):
@ -14,7 +22,33 @@ def guiMain(args=None):
set_icon(mainWindow)
topFrame = Frame(mainWindow)
notebook = ttk.Notebook(mainWindow)
randomizerWindow = ttk.Frame(notebook)
adjustWindow = ttk.Frame(notebook)
notebook.add(randomizerWindow, text='Randomize')
notebook.add(adjustWindow, text='Adjust')
notebook.pack()
# Shared Controls
farBottomFrame = Frame(mainWindow)
def open_output():
open_file(output_path(''))
openOutputButton = Button(farBottomFrame, text='Open Output Directory', command=open_output)
if os.path.exists(local_path('README.html')):
def open_readme():
open_file(local_path('README.html'))
openReadmeButton = Button(farBottomFrame, text='Open Documentation', command=open_readme)
openReadmeButton.pack(side=LEFT)
farBottomFrame.pack(side=BOTTOM, fill=X, padx=5, pady=5)
# randomizer controls
topFrame = Frame(randomizerWindow)
rightHalfFrame = Frame(topFrame)
checkBoxFrame = Frame(rightHalfFrame)
@ -24,8 +58,6 @@ 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)
fastMenuVar = IntVar()
fastMenuCheckbutton = Checkbutton(checkBoxFrame, text="Enable instant menu", variable=fastMenuVar)
keysanityVar = IntVar()
keysanityCheckbutton = Checkbutton(checkBoxFrame, text="Keysanity (keys anywhere)", variable=keysanityVar)
dungeonItemsVar = IntVar()
@ -40,7 +72,6 @@ def guiMain(args=None):
createSpoilerCheckbutton.pack(expand=True, anchor=W)
suppressRomCheckbutton.pack(expand=True, anchor=W)
quickSwapCheckbutton.pack(expand=True, anchor=W)
fastMenuCheckbutton.pack(expand=True, anchor=W)
keysanityCheckbutton.pack(expand=True, anchor=W)
dungeonItemsCheckbutton.pack(expand=True, anchor=W)
beatableOnlyCheckbutton.pack(expand=True, anchor=W)
@ -55,7 +86,7 @@ def guiMain(args=None):
romEntry = Entry(romDialogFrame, textvariable=romVar)
def RomSelect():
rom = filedialog.askopenfilename()
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")])
romVar.set(rom)
romSelectButton = Button(romDialogFrame, text='Select Rom', command=RomSelect)
@ -64,15 +95,27 @@ def guiMain(args=None):
romSelectButton.pack(side=LEFT)
spriteDialogFrame = Frame(fileDialogFrame)
baseSpriteLabel = Label(spriteDialogFrame, text='Link Sprite')
spriteVar = StringVar()
spriteEntry = Entry(spriteDialogFrame, textvariable=spriteVar)
baseSpriteLabel = Label(spriteDialogFrame, text='Link Sprite:')
spriteNameVar = StringVar()
sprite = None
def set_sprite(sprite_param):
nonlocal sprite
if sprite_param is None or not sprite_param.valid:
sprite = None
spriteNameVar.set('(unchanged)')
else:
sprite = sprite_param
spriteNameVar.set(sprite.name)
set_sprite(None)
spriteNameVar.set('(unchanged)')
spriteEntry = Label(spriteDialogFrame, textvariable=spriteNameVar)
def SpriteSelect():
sprite = filedialog.askopenfilename()
spriteVar.set(sprite)
SpriteSelector(mainWindow, set_sprite)
spriteSelectButton = Button(spriteDialogFrame, text='Select Sprite', command=SpriteSelect)
spriteSelectButton = Button(spriteDialogFrame, text='Open Sprite Picker', command=SpriteSelect)
baseSpriteLabel.pack(side=LEFT)
spriteEntry.pack(side=LEFT)
@ -158,6 +201,15 @@ def guiMain(args=None):
heartbeepLabel = Label(heartbeepFrame, text='Heartbeep sound rate')
heartbeepLabel.pack(side=LEFT)
fastMenuFrame = Frame(drowDownFrame)
fastMenuVar = StringVar()
fastMenuVar.set('normal')
fastMenuOptionMenu = OptionMenu(fastMenuFrame, fastMenuVar, 'normal', 'instant', 'double', 'triple', 'quadruple', 'half')
fastMenuOptionMenu.pack(side=RIGHT)
fastMenuLabel = Label(fastMenuFrame, text='Menu speed')
fastMenuLabel.pack(side=LEFT)
modeFrame.pack(expand=True, anchor=E)
logicFrame.pack(expand=True, anchor=E)
goalFrame.pack(expand=True, anchor=E)
@ -167,9 +219,9 @@ def guiMain(args=None):
algorithmFrame.pack(expand=True, anchor=E)
shuffleFrame.pack(expand=True, anchor=E)
heartbeepFrame.pack(expand=True, anchor=E)
fastMenuFrame.pack(expand=True, anchor=E)
bottomFrame = Frame(mainWindow)
farBottomFrame = Frame(mainWindow)
bottomFrame = Frame(randomizerWindow)
seedLabel = Label(bottomFrame, text='Seed #')
seedVar = StringVar()
@ -191,22 +243,22 @@ def guiMain(args=None):
guiargs.algorithm = algorithmVar.get()
guiargs.shuffle = shuffleVar.get()
guiargs.heartbeep = heartbeepVar.get()
guiargs.fastmenu = fastMenuVar.get()
guiargs.create_spoiler = bool(createSpoilerVar.get())
guiargs.suppress_rom = bool(suppressRomVar.get())
guiargs.keysanity = bool(keysanityVar.get())
guiargs.nodungeonitems = bool(dungeonItemsVar.get())
guiargs.beatableonly = bool(beatableOnlyVar.get())
guiargs.fastmenu = bool(fastMenuVar.get())
guiargs.quickswap = bool(quickSwapVar.get())
guiargs.disablemusic = bool(disableMusicVar.get())
guiargs.shuffleganon = bool(shuffleGanonVar.get())
guiargs.rom = romVar.get()
guiargs.jsonout = None
guiargs.sprite = spriteVar.get() if spriteVar.get() else None
guiargs.sprite = sprite
try:
if guiargs.count is not None:
seed = guiargs.seed
for i in range(guiargs.count):
for _ in range(guiargs.count):
main(seed=seed, args=guiargs)
seed = random.randint(0, 999999999)
else:
@ -218,31 +270,108 @@ def guiMain(args=None):
generateButton = Button(bottomFrame, text='Generate Patched Rom', command=generateRom)
def open_output():
open_file(output_path(''))
openOutputButton = Button(farBottomFrame, text='Open Output Directory', command=open_output)
if os.path.exists(local_path('README.html')):
def open_readme():
open_file(local_path('README.html'))
openReadmeButton = Button(farBottomFrame, text='Open Documentation', command=open_readme)
openReadmeButton.pack(side=LEFT)
seedLabel.pack(side=LEFT)
seedEntry.pack(side=LEFT)
countLabel.pack(side=LEFT, padx=(5,0))
countLabel.pack(side=LEFT, padx=(5, 0))
countSpinbox.pack(side=LEFT)
generateButton.pack(side=LEFT, padx=(5,0))
generateButton.pack(side=LEFT, padx=(5, 0))
openOutputButton.pack(side=RIGHT)
drowDownFrame.pack(side=LEFT)
rightHalfFrame.pack(side=RIGHT)
topFrame.pack(side=TOP)
farBottomFrame.pack(side=BOTTOM, fill=X, padx=5, pady=5)
bottomFrame.pack(side=BOTTOM)
# Adjuster Controls
topFrame2 = Frame(adjustWindow)
rightHalfFrame2 = Frame(topFrame2)
checkBoxFrame2 = Frame(rightHalfFrame2)
quickSwapCheckbutton2 = Checkbutton(checkBoxFrame2, text="Enabled L/R Item quickswapping", variable=quickSwapVar)
disableMusicCheckbutton2 = Checkbutton(checkBoxFrame2, text="Disable game music", variable=disableMusicVar)
quickSwapCheckbutton2.pack(expand=True, anchor=W)
disableMusicCheckbutton2.pack(expand=True, anchor=W)
fileDialogFrame2 = Frame(rightHalfFrame2)
romDialogFrame2 = Frame(fileDialogFrame2)
baseRomLabel2 = Label(romDialogFrame2, text='Rom to adjust')
romVar2 = StringVar()
romEntry2 = Entry(romDialogFrame2, textvariable=romVar2)
def RomSelect2():
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")])
romVar2.set(rom)
romSelectButton2 = Button(romDialogFrame2, text='Select Rom', command=RomSelect2)
baseRomLabel2.pack(side=LEFT)
romEntry2.pack(side=LEFT)
romSelectButton2.pack(side=LEFT)
spriteDialogFrame2 = Frame(fileDialogFrame2)
baseSpriteLabel2 = Label(spriteDialogFrame2, text='Link Sprite')
spriteEntry2 = Label(spriteDialogFrame2, textvariable=spriteNameVar)
def SpriteSelectAdjuster():
SpriteSelector(mainWindow, set_sprite, adjuster=True)
spriteSelectButton2 = Button(spriteDialogFrame2, text='Select Sprite', command=SpriteSelectAdjuster)
baseSpriteLabel2.pack(side=LEFT)
spriteEntry2.pack(side=LEFT)
spriteSelectButton2.pack(side=LEFT)
romDialogFrame2.pack()
spriteDialogFrame2.pack()
checkBoxFrame2.pack()
fileDialogFrame2.pack()
drowDownFrame2 = Frame(topFrame2)
heartbeepFrame2 = Frame(drowDownFrame2)
heartbeepOptionMenu2 = OptionMenu(heartbeepFrame2, heartbeepVar, 'normal', 'half', 'quarter', 'off')
heartbeepOptionMenu2.pack(side=RIGHT)
heartbeepLabel2 = Label(heartbeepFrame2, text='Heartbeep sound rate')
heartbeepLabel2.pack(side=LEFT)
fastMenuFrame2 = Frame(drowDownFrame2)
fastMenuOptionMenu2 = OptionMenu(fastMenuFrame2, fastMenuVar, 'normal', 'instant', 'double', 'triple', 'quadruple', 'half')
fastMenuOptionMenu2.pack(side=RIGHT)
fastMenuLabel2 = Label(fastMenuFrame2, text='Menu speed')
fastMenuLabel2.pack(side=LEFT)
heartbeepFrame2.pack(expand=True, anchor=E)
fastMenuFrame2.pack(expand=True, anchor=E)
bottomFrame2 = Frame(topFrame2)
def adjustRom():
guiargs = Namespace
guiargs.heartbeep = heartbeepVar.get()
guiargs.fastmenu = bool(fastMenuVar.get())
guiargs.quickswap = bool(quickSwapVar.get())
guiargs.disablemusic = bool(disableMusicVar.get())
guiargs.rom = romVar2.get()
guiargs.sprite = sprite
try:
adjust(args=guiargs)
except Exception as e:
messagebox.showerror(title="Error while creating seed", message=str(e))
else:
messagebox.showinfo(title="Success", message="Rom patched successfully")
adjustButton = Button(bottomFrame2, text='Adjust Rom', command=adjustRom)
adjustButton.pack(side=LEFT, padx=(5, 0))
drowDownFrame2.pack(side=LEFT, pady=(0, 40))
rightHalfFrame2.pack(side=RIGHT)
topFrame2.pack(side=TOP, pady=30)
bottomFrame2.pack(side=BOTTOM, pady=(180, 0))
if args is not None:
# load values from commandline args
createSpoilerVar.set(int(args.create_spoiler))
@ -251,7 +380,6 @@ def guiMain(args=None):
if args.nodungeonitems:
dungeonItemsVar.set(int(not args.nodungeonitems))
beatableOnlyVar.set(int(args.beatableonly))
fastMenuVar.set(int(args.fastmenu))
quickSwapVar.set(int(args.quickswap))
disableMusicVar.set(int(args.disablemusic))
if args.count:
@ -266,19 +394,323 @@ def guiMain(args=None):
algorithmVar.set(args.algorithm)
shuffleVar.set(args.shuffle)
heartbeepVar.set(args.heartbeep)
fastMenuVar.set(args.fastmenu)
logicVar.set(args.logic)
romVar.set(args.rom)
shuffleGanonVar.set(args.shuffleganon)
if args.sprite is not None:
spriteVar.set(args.sprite)
set_sprite(Sprite(args.sprite))
mainWindow.mainloop()
def set_icon(window):
er16 = PhotoImage(file=local_path('data/ER16.gif'))
er32 = PhotoImage(file=local_path('data/ER32.gif'))
er48 = PhotoImage(file=local_path('data/ER32.gif'))
window.tk.call('wm', 'iconphoto', window._w, er16, er32, er48)
class SpriteSelector(object):
def __init__(self, parent, callback, adjuster=False):
if is_bundled():
self.deploy_icons()
self.parent = parent
self.window = Toplevel(parent)
self.callback = callback
self.adjuster = adjuster
self.window.wm_title("TAKE ANY ONE YOU WANT")
self.window['padx'] = 5
self.window['pady'] = 5
self.all_sprites = []
def open_unofficial_sprite_dir(_evt):
open_file(self.unofficial_sprite_dir)
official_frametitle = Label(self.window, text='Official Sprites')
unofficial_frametitle = Frame(self.window)
title_text = Label(unofficial_frametitle, text="Unofficial Sprites")
title_link = Label(unofficial_frametitle, text="(open)", fg="blue", cursor="hand2")
title_text.pack(side=LEFT)
title_link.pack(side=LEFT)
title_link.bind("<Button-1>", open_unofficial_sprite_dir)
self.icon_section(official_frametitle, self.official_sprite_dir+'/*', 'Official sprites not found. Click "Update official sprites" to download them.')
self.icon_section(unofficial_frametitle, self.unofficial_sprite_dir+'/*', 'Put sprites in the unofficial sprites folder (see open link above) to have them appear here.')
frame = Frame(self.window)
frame.pack(side=BOTTOM, fill=X, pady=5)
button = Button(frame, text="Browse for file...", command=self.browse_for_sprite)
button.pack(side=RIGHT, padx=(5, 0))
button = Button(frame, text="Update official sprites", command=self.update_official_sprites)
button.pack(side=RIGHT, padx=(5, 0))
button = Button(frame, text="Default Link sprite", command=self.use_default_link_sprite)
button.pack(side=LEFT, padx=(0, 5))
button = Button(frame, text="Random sprite", command=self.use_random_sprite)
button.pack(side=LEFT, padx=(0, 5))
if adjuster:
button = Button(frame, text="Current sprite from rom", command=self.use_default_sprite)
button.pack(side=LEFT, padx=(0, 5))
set_icon(self.window)
self.window.focus()
def icon_section(self, frame_label, path, no_results_label):
frame = LabelFrame(self.window, labelwidget=frame_label, padx=5, pady=5)
frame.pack(side=TOP, fill=X)
sprites = []
for file in glob(output_path(path)):
sprites.append(Sprite(file))
sprites.sort(key=lambda s: str.lower(s.name or ""))
i = 0
for sprite in sprites:
image = get_image_for_sprite(sprite)
if image is None:
continue
self.all_sprites.append(sprite)
button = Button(frame, image=image, command=lambda spr=sprite: self.select_sprite(spr))
ToolTips.register(button, sprite.name + ("\nBy: %s" % sprite.author_name if sprite.author_name else ""))
button.image = image
button.grid(row=i // 16, column=i % 16)
i += 1
if i == 0:
label = Label(frame, text=no_results_label)
label.pack()
def update_official_sprites(self):
# need to wrap in try catch. We don't want errors getting the json or downloading the files to break us.
self.window.destroy()
self.parent.update()
def work(task):
resultmessage = ""
successful = True
def finished():
task.close_window()
if successful:
messagebox.showinfo("Sprite Updater", resultmessage)
else:
messagebox.showerror("Sprite Updater", resultmessage)
SpriteSelector(self.parent, self.callback, self.adjuster)
try:
task.update_status("Downloading official sprites list")
with urlopen('http://vt.alttp.run/sprites') as response:
sprites_arr = json.loads(response.read().decode("utf-8"))
except Exception as e:
resultmessage = "Error getting list of official sprites. Sprites not updated.\n\n%s: %s" % (type(e).__name__, e)
successful = False
task.queue_event(finished)
return
try:
task.update_status("Determining needed sprites")
current_sprites = [os.path.basename(file) for file in glob(self.official_sprite_dir+'/*')]
official_sprites = [(sprite['file'], os.path.basename(urlparse(sprite['file']).path)) for sprite in sprites_arr]
needed_sprites = [(sprite_url, filename) for (sprite_url, filename) in official_sprites if filename not in current_sprites]
bundled_sprites = [os.path.basename(file) for file in glob(self.local_official_sprite_dir+'/*')]
# todo: eventually use the above list to avoid downloading any sprites that we already have cached in the bundle.
official_filenames = [filename for (_, filename) in official_sprites]
obsolete_sprites = [sprite for sprite in current_sprites if sprite not in official_filenames]
except Exception as e:
resultmessage = "Error Determining which sprites to update. Sprites not updated.\n\n%s: %s" % (type(e).__name__, e)
successful = False
task.queue_event(finished)
return
updated = 0
for (sprite_url, filename) in needed_sprites:
try:
task.update_status("Downloading needed sprite %g/%g" % (updated + 1, len(needed_sprites)))
target = os.path.join(self.official_sprite_dir, filename)
with urlopen(sprite_url) as response, open(target, 'wb') as out:
shutil.copyfileobj(response, out)
except Exception as e:
resultmessage = "Error downloading sprite. Not all sprites updated.\n\n%s: %s" % (type(e).__name__, e)
successful = False
updated += 1
deleted = 0
for sprite in obsolete_sprites:
try:
task.update_status("Removing obsolete sprite %g/%g" % (deleted + 1, len(obsolete_sprites)))
os.remove(os.path.join(self.official_sprite_dir, sprite))
except Exception as e:
resultmessage = "Error removing obsolete sprite. Not all sprites updated.\n\n%s: %s" % (type(e).__name__, e)
successful = False
deleted += 1
if successful:
resultmessage = "official sprites updated sucessfully"
task.queue_event(finished)
BackgroundTaskProgress(self.parent, work, "Updating Sprites")
def browse_for_sprite(self):
sprite = filedialog.askopenfilename(
filetypes=[("All Sprite Sources", (".zspr", ".spr", ".sfc", ".smc")),
("ZSprite files", ".zspr"),
("Sprite files", ".spr"),
("Rom Files", (".sfc", ".smc")),
("All Files", "*")])
try:
self.callback(Sprite(sprite))
except Exception:
self.callback(None)
self.window.destroy()
def use_default_sprite(self):
self.callback(None)
self.window.destroy()
def use_default_link_sprite(self):
self.callback(Sprite.default_link_sprite())
self.window.destroy()
def use_random_sprite(self):
self.callback(random.choice(self.all_sprites) if self.all_sprites else None)
self.window.destroy()
def select_sprite(self, spritename):
self.callback(spritename)
self.window.destroy()
def deploy_icons(self):
if not os.path.exists(self.unofficial_sprite_dir):
os.makedirs(self.unofficial_sprite_dir)
if not os.path.exists(self.official_sprite_dir):
shutil.copytree(self.local_official_sprite_dir, self.official_sprite_dir)
@property
def official_sprite_dir(self):
if is_bundled():
return output_path("sprites/official")
return self.local_official_sprite_dir
@property
def local_official_sprite_dir(self):
return local_path("data/sprites/official")
@property
def unofficial_sprite_dir(self):
if is_bundled():
return output_path("sprites/unofficial")
return self.local_unofficial_sprite_dir
@property
def local_unofficial_sprite_dir(self):
return local_path("data/sprites/unofficial")
def get_image_for_sprite(sprite):
if not sprite.valid:
return None
height = 24
width = 16
def draw_sprite_into_gif(add_palette_color, set_pixel_color_index):
def drawsprite(spr, pal_as_colors, offset):
for y, row in enumerate(spr):
for x, pal_index in enumerate(row):
if pal_index:
color = pal_as_colors[pal_index - 1]
set_pixel_color_index(x + offset[0], y + offset[1], color)
add_palette_color(16, (40, 40, 40))
shadow = [
[0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0],
]
drawsprite(shadow, [16], (2, 17))
palettes = sprite.decode_palette()
for i in range(15):
add_palette_color(i + 1, palettes[0][i])
body = sprite.decode16(0x4C0)
drawsprite(body, list(range(1, 16)), (0, 8))
head = sprite.decode16(0x40)
drawsprite(head, list(range(1, 16)), (0, 0))
def make_gif(callback):
gif_header = b'GIF89a'
gif_lsd = bytearray(7)
gif_lsd[0] = width
gif_lsd[2] = height
gif_lsd[4] = 0xF4 # 32 color palette follows. transparant + 15 for sprite + 1 for shadow=17 which rounds up to 32 as nearest power of 2
gif_lsd[5] = 0 # background color is zero
gif_lsd[6] = 0 # aspect raio not specified
gif_gct = bytearray(3 * 32)
gif_gce = bytearray(8)
gif_gce[0] = 0x21 # start of extention blocked
gif_gce[1] = 0xF9 # identifies this as the Graphics Control extension
gif_gce[2] = 4 # we are suppling only the 4 four bytes
gif_gce[3] = 0x01 # this gif includes transparency
gif_gce[4] = gif_gce[5] = 0 # animation frrame delay (unused)
gif_gce[6] = 0 # transparent color is index 0
gif_gce[7] = 0 # end of gif_gce
gif_id = bytearray(10)
gif_id[0] = 0x2c
# byte 1,2 are image left. 3,4 are image top both are left as zerosuitsamus
gif_id[5] = width
gif_id[7] = height
gif_id[9] = 0 # no local color table
gif_img_minimum_code_size = bytes([7]) # we choose 7 bits, so that each pixel is represented by a byte, for conviennce.
clear = 0x80
stop = 0x81
unchunked_image_data = bytearray(height * (width + 1) + 1)
# we technically need a Clear code once every 125 bytes, but we do it at the start of every row for simplicity
for row in range(height):
unchunked_image_data[row * (width + 1)] = clear
unchunked_image_data[-1] = stop
def add_palette_color(index, color):
gif_gct[3 * index] = color[0]
gif_gct[3 * index + 1] = color[1]
gif_gct[3 * index + 2] = color[2]
def set_pixel_color_index(x, y, color):
unchunked_image_data[y * (width + 1) + x + 1] = color
callback(add_palette_color, set_pixel_color_index)
def chunk_image(img):
for i in range(0, len(img), 255):
chunk = img[i:i + 255]
yield bytes([len(chunk)])
yield chunk
gif_img = b''.join([gif_img_minimum_code_size] + list(chunk_image(unchunked_image_data)) + [b'\x00'])
gif = b''.join([gif_header, gif_lsd, gif_gct, gif_gce, gif_id, gif_img, b'\x3b'])
return gif
gif_data = make_gif(draw_sprite_into_gif)
image = PhotoImage(data=gif_data)
return image.zoom(2)
if __name__ == '__main__':
guiMain()

194
GuiUtils.py Normal file
View File

@ -0,0 +1,194 @@
import queue
import threading
import tkinter as tk
from Utils import local_path
def set_icon(window):
er16 = tk.PhotoImage(file=local_path('data/ER16.gif'))
er32 = tk.PhotoImage(file=local_path('data/ER32.gif'))
er48 = tk.PhotoImage(file=local_path('data/ER32.gif'))
window.tk.call('wm', 'iconphoto', window._w, er16, er32, er48) # pylint: disable=protected-access
# Although tkinter is intended to be thread safe, there are many reports of issues
# some which may be platform specific, or depend on if the TCL library was compiled without
# multithreading support. Therefore I will assume it is not thread safe to avoid any possible problems
class BackgroundTask(object):
def __init__(self, window, code_to_run):
self.window = window
self.queue = queue.Queue()
self.running = True
self.process_queue()
self.task = threading.Thread(target=code_to_run, args=(self,))
self.task.start()
def stop(self):
self.running = False
#safe to call from worker
def queue_event(self, event):
self.queue.put(event)
def process_queue(self):
try:
while True:
if not self.running:
return
event = self.queue.get_nowait()
event()
if self.running:
#if self is no longer running self.window may no longer be valid
self.window.update_idletasks()
except queue.Empty:
pass
if self.running:
self.window.after(100, self.process_queue)
class BackgroundTaskProgress(BackgroundTask):
def __init__(self, parent, code_to_run, title):
self.parent = parent
self.window = tk.Toplevel(parent)
self.window['padx'] = 5
self.window['pady'] = 5
try:
self.window.attributes("-toolwindow", 1)
except tk.TclError:
pass
self.window.wm_title(title)
self.label_var = tk.StringVar()
self.label_var.set("")
self.label = tk.Label(self.window, textvariable=self.label_var, width=50)
self.label.pack()
self.window.resizable(width=False, height=False)
set_icon(self.window)
self.window.focus()
super().__init__(self.window, code_to_run)
#safe to call from worker thread
def update_status(self, text):
self.queue_event(lambda: self.label_var.set(text))
# only call this in an event callback
def close_window(self):
self.stop()
self.window.destroy()
class ToolTips(object):
# This class derived from wckToolTips which is available under the following license:
# Copyright (c) 1998-2007 by Secret Labs AB
# Copyright (c) 1998-2007 by Fredrik Lundh
#
# By obtaining, using, and/or copying this software and/or its
# associated documentation, you agree that you have read, understood,
# and will comply with the following terms and conditions:
#
# Permission to use, copy, modify, and distribute this software and its
# associated documentation for any purpose and without fee is hereby
# granted, provided that the above copyright notice appears in all
# copies, and that both that copyright notice and this permission notice
# appear in supporting documentation, and that the name of Secret Labs
# AB or the author not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior
# permission.
#
# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO
# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
# FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
label = None
window = None
active = 0
tag = None
after_id = None
@classmethod
def getcontroller(cls, widget):
if cls.tag is None:
cls.tag = "ui_tooltip_%d" % id(cls)
widget.bind_class(cls.tag, "<Enter>", cls.enter)
widget.bind_class(cls.tag, "<Leave>", cls.leave)
widget.bind_class(cls.tag, "<Motion>", cls.motion)
widget.bind_class(cls.tag, "<Destroy>", cls.leave)
# pick suitable colors for tooltips
try:
cls.bg = "systeminfobackground"
cls.fg = "systeminfotext"
widget.winfo_rgb(cls.fg) # make sure system colors exist
widget.winfo_rgb(cls.bg)
except Exception:
cls.bg = "#ffffe0"
cls.fg = "black"
return cls.tag
@classmethod
def register(cls, widget, text):
widget.ui_tooltip_text = text
tags = list(widget.bindtags())
tags.append(cls.getcontroller(widget))
widget.bindtags(tuple(tags))
@classmethod
def unregister(cls, widget):
tags = list(widget.bindtags())
tags.remove(cls.getcontroller(widget))
widget.bindtags(tuple(tags))
# event handlers
@classmethod
def enter(cls, event):
widget = event.widget
if not cls.label:
# create and hide balloon help window
cls.popup = tk.Toplevel(bg=cls.fg, bd=1)
cls.popup.overrideredirect(1)
cls.popup.withdraw()
cls.label = tk.Label(
cls.popup, fg=cls.fg, bg=cls.bg, bd=0, padx=2, justify=tk.LEFT
)
cls.label.pack()
cls.active = 0
cls.xy = event.x_root + 16, event.y_root + 10
cls.event_xy = event.x, event.y
cls.after_id = widget.after(200, cls.display, widget)
@classmethod
def motion(cls, event):
cls.xy = event.x_root + 16, event.y_root + 10
cls.event_xy = event.x, event.y
@classmethod
def display(cls, widget):
if not cls.active:
# display balloon help window
text = widget.ui_tooltip_text
if callable(text):
text = text(widget, cls.event_xy)
cls.label.config(text=text)
cls.popup.deiconify()
cls.popup.lift()
cls.popup.geometry("+%d+%d" % cls.xy)
cls.active = 1
cls.after_id = None
@classmethod
def leave(cls, event):
widget = event.widget
if cls.active:
cls.popup.withdraw()
cls.active = 0
if cls.after_id:
widget.after_cancel(cls.after_id)
cls.after_id = None

View File

@ -1,13 +1,15 @@
from Items import ItemFactory
from Fill import fill_restrictive
from collections import namedtuple
import random
from Items import ItemFactory
from Fill import fill_restrictive
from Dungeons import get_dungeon_item_pool
#This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space.
#Some basic items that various modes require are placed here, including pendants and crystals. Medallion requirements for the two relevant entrances are also decided.
alwaysitems = ['Bombos', 'Book of Mudora', 'Bow', 'Cane of Somaria', 'Ether', 'Fire Rod', 'Flippers', 'Ocarina', 'Hammer', 'Hookshot', 'Ice Rod', 'Lamp',
'Cape', 'Magic Powder', 'Mushroom', 'Pegasus Boots', 'Quake', 'Shovel', 'Bug Catching Net', 'Cane of Byrna']
'Cape', 'Magic Powder', 'Mushroom', 'Pegasus Boots', 'Quake', 'Shovel', 'Bug Catching Net', 'Cane of Byrna']
progressivegloves = ['Progressive Glove'] * 2
basicgloves = ['Power Glove', 'Titans Mitts']
@ -15,7 +17,7 @@ normalbottles = ['Bottle', 'Bottle (Red Potion)', 'Bottle (Green Potion)', 'Bott
hardbottles = ['Bottle', 'Bottle (Red Potion)', 'Bottle (Green Potion)', 'Bottle (Blue Potion)', 'Bottle (Bee)', 'Bottle (Good Bee)']
normalbaseitems = (['Blue Boomerang', 'Red Boomerang', 'Silver Arrows', 'Magic Upgrade (1/2)'] + ['Rupees (300)'] * 4 +
['Single Arrow', 'Sanctuary Heart Container', 'Arrow Upgrade (+10)', 'Bomb Upgrade (+10)'] + ['Boss Heart Container'] * 10 + ['Piece of Heart'] * 24)
['Single Arrow', 'Sanctuary Heart Container', 'Arrow Upgrade (+10)', 'Bomb Upgrade (+10)'] + ['Boss Heart Container'] * 10 + ['Piece of Heart'] * 24)
normalfirst15extra = ['Rupees (100)', 'Rupees (300)', 'Rupees (50)'] + ['Arrow Upgrade (+5)'] * 6 + ['Bomb Upgrade (+5)'] * 6
normalsecond15extra = ['Bombs (3)'] * 10 + ['Rupees (50)'] * 2 + ['Arrows (10)'] * 2 + ['Rupee (1)']
normalthird10extra = ['Rupees (50)'] * 4 + ['Rupees (20)'] * 3 + ['Arrows (10)', 'Rupee (1)', 'Rupees (5)']
@ -23,32 +25,32 @@ normalfourth5extra = ['Arrows (10)'] * 2 + ['Rupees (20)'] * 2 + ['Rupees (5)']
normalfinal25extra = ['Rupees (20)'] * 23 + ['Rupees (5)'] * 2
easybaseitems = (['Blue Boomerang', 'Red Boomerang', 'Silver Arrows'] + ['Rupees (300)'] * 4 + ['Magic Upgrade (1/2)'] * 2 +
['Single Arrow', 'Sanctuary Heart Container', 'Arrow Upgrade (+10)', 'Bomb Upgrade (+10)'] + ['Boss Heart Container'] * 10 + ['Piece of Heart'] * 12)
easybaseitems = (['Blue Boomerang', 'Red Boomerang', 'Silver Arrows'] + ['Rupees (300)'] * 4 + ['Magic Upgrade (1/2)'] * 2 + ['Lamp'] * 2 +
['Single Arrow', 'Sanctuary Heart Container'] + ['Boss Heart Container'] * 10 + ['Piece of Heart'] * 12)
easyextra = ['Piece of Heart'] * 12 + ['Rupees (300)']
easylimitedextra = ['Boss Heart Container'] * 3 # collapsing down the 12 pieces of heart
easyfirst15extra = ['Rupees (100)'] + ['Rupees (50)'] + ['Arrow Upgrade (+5)'] * 6 + ['Bomb Upgrade (+5)'] * 6 + ['Bombs (3)']
easysecond10extra = ['Bombs (3)'] * 9 + ['Rupee (1)']
easythird5extra = ['Rupees (50)'] * 2 + ['Arrows (10)'] * 2 + ['Rupees (5)']
easyfinal25extra = ['Rupees (50)'] * 4 + ['Rupees (20)'] * 14 + ['Rupee (1)'] + ['Arrows (10)'] * 3 + ['Rupees (5)'] * 3
easyfirst15extra = ['Rupees (100)', 'Arrow Upgrade (+10)', 'Bomb Upgrade (+10)'] + ['Arrow Upgrade (+5)'] * 6 + ['Bomb Upgrade (+5)'] * 6
easysecond10extra = ['Bombs (3)'] * 8 + ['Rupee (1)', 'Rupees (50)']
easythird5extra = ['Rupees (50)'] * 2 + ['Bombs (3)'] * 2 + ['Arrows (10)']
easyfinal25extra = ['Rupees (50)'] * 4 + ['Rupees (20)'] * 14 + ['Rupee (1)'] + ['Arrows (10)'] * 4 + ['Rupees (5)'] * 2
easytimedotherextra = ['Red Clock'] * 5
hardbaseitems = (['Silver Arrows', 'Single Arrow'] + ['Rupees (300)'] + ['Rupees (100)'] * 2 + ['Rupees (50)'] + ['Bombs (3)'] +
['Boss Heart Container'] * 5 + ['Piece of Heart'] * 24)
['Boss Heart Container'] * 5 + ['Piece of Heart'] * 24)
hardfirst20extra = ['Bombs (3)'] * 4 + ['Single Bomb'] * 4 + ['Rupees (5)'] * 5 + ['Rupee (1)'] * 2 + ['Rupees (100)'] + ['Rupees (50)'] * 4
hardsecond20extra = ['Single Bomb'] * 4 + ['Rupees (5)'] * 10 + ['Rupees (20)'] * 2 + ['Rupee (1)'] * 3 + ['Arrows (10)']
hardthird20extra = ['Arrows (10)'] * 4 + ['Rupees (20)'] * 3 + ['Rupees (5)'] * 3 + ['Single Bomb'] * 5 + ['Single Arrow'] * 5
hardfinal20extra = ['Single Bomb'] * 4 + ['Rupees (5)'] * 2 + ['Single Arrow'] * 14
expertbaseitems = (['Single Arrow', 'Rupees (300)', 'Rupees (100)', 'Bombs (3)', 'Arrows (10)'] + ['Rupees (50)'] * 4 + ['Rupees (5)'] * 5 +
['Rupees (20)'] + ['Single Bomb'] * 2 + ['Piece of Heart'] * 24)
['Rupees (20)'] + ['Single Bomb'] * 2 + ['Piece of Heart'] * 24)
expertfirst15extra = ['Single Bomb'] * 13 + ['Rupees (20)'] * 2
expertsecond25extra = ['Single Bomb'] * 8 + ['Single Arrow'] * 9 + ['Rupees (20)'] * 3 + ['Rupee (1)'] * 5
expertthird15extra = ['Rupees (5)'] * 5 + ['Single Bomb'] * 3 + ['Rupees (20)'] * 2 + ['Single Arrow'] * 5
expertfinal25extra = ['Single Bomb'] * 4 + ['Rupees (20)'] * 3 + ['Single Arrow'] * 18
insanebaseitems = (['Single Arrow', 'Bombs (3)', 'Arrows (10)'] + ['Rupees (50)'] * 3 + ['Rupees (5)'] * 10 + ['Rupees (300)'] * 4 + ['Rupees (100)'] * 3 +
['Rupee (1)'] * 4 + ['Single Bomb'] * 4)
['Rupee (1)'] * 4 + ['Single Bomb'] * 4)
insanefirst15extra = ['Single Bomb'] * 4 + ['Single Arrow'] * 4 + ['Rupee (1)'] * 4 + ['Rupees (300)'] + ['Rupees (100)'] + ['Rupees (50)']
insanesecond25extra = ['Single Bomb'] * 7 + ['Single Arrow'] * 7 + ['Rupee (1)'] * 7 + ['Rupees (20)'] * 4
insanethird10extra = ['Single Bomb'] * 3 + ['Single Arrow'] * 3 + ['Rupee (1)'] * 3 + ['Rupees (20)']
@ -56,29 +58,28 @@ insanefourth15extra = ['Single Bomb'] * 5 + ['Single Arrow'] * 5 + ['Rupee (1)']
insanefinal25extra = ['Single Bomb'] * 2 + ['Single Arrow'] * 10 + ['Rupee (1)'] * 7 + ['Rupees (20)'] * 6
Difficulty = namedtuple('Difficulty',
['baseitems', 'bottles', 'bottle_count','same_bottle', 'progressiveshield',
'basicshield', 'progressivearmor', 'basicarmor', 'swordless',
'progressivesword', 'basicsword', 'timedohko', 'timedother',
'triforcehunt', 'triforce_pieces_required', 'conditional_extras',
'extras'])
['baseitems', 'bottles', 'bottle_count', 'same_bottle', 'progressiveshield',
'basicshield', 'progressivearmor', 'basicarmor', 'swordless',
'progressivesword', 'basicsword', 'timedohko', 'timedother',
'triforcehunt', 'triforce_pieces_required', 'conditional_extras',
'extras', 'progressive_sword_limit', 'progressive_shield_limit',
'progressive_armor_limit', 'progressive_bottle_limit'])
TotalItemsToPlace = 153
total_items_to_place = 153
def easy_conditional_extras(timer, goal, mode, pool, placed_items):
extraitems = TotalItemsToPlace - len(pool) - len(placed_items)
def easy_conditional_extras(timer, _goal, _mode, pool, placed_items):
extraitems = total_items_to_place - len(pool) - len(placed_items)
if extraitems < len(easyextra):
return easylimitedextra
if timer in ['timed', 'timed-countdown']:
return easytimedotherextra
return []
def no_conditonal_extras(*args):
def no_conditonal_extras(*_args):
return []
#def Difficulty(**kwargs):
# protodifficulty._replace(**kwargs)
difficulties= {
difficulties = {
'normal': Difficulty(
baseitems = normalbaseitems,
bottles = normalbottles,
@ -96,7 +97,11 @@ difficulties= {
triforcehunt = ['Triforce Piece'] * 30,
triforce_pieces_required = 20,
conditional_extras = no_conditonal_extras,
extras = [normalfirst15extra,normalsecond15extra,normalthird10extra,normalfourth5extra,normalfinal25extra]
extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
progressive_sword_limit = 4,
progressive_shield_limit = 3,
progressive_armor_limit = 2,
progressive_bottle_limit = 4,
),
'easy': Difficulty(
baseitems = easybaseitems,
@ -109,13 +114,17 @@ difficulties= {
basicarmor = ['Blue Mail', 'Red Mail'] * 2,
swordless = ['Rupees (20)'] * 8,
progressivesword = ['Progressive Sword'] * 7,
basicsword = ['Master Sword', 'Tempered Sword', 'Golden Sword'] *2 + ['Fighter Sword'],
basicsword = ['Master Sword', 'Tempered Sword', 'Golden Sword'] *2 + ['Fighter Sword'],
timedohko = ['Green Clock'] * 25,
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 5, # +5 more Red Clocks if there is room
triforcehunt = ['Triforce Piece'] * 30,
triforce_pieces_required = 10,
conditional_extras = easy_conditional_extras,
extras = [easyextra, easyfirst15extra, easysecond10extra, easythird5extra, easyfinal25extra],
progressive_sword_limit = 4,
progressive_shield_limit = 3,
progressive_armor_limit = 2,
progressive_bottle_limit = 4,
),
'hard': Difficulty(
baseitems = hardbaseitems,
@ -135,6 +144,10 @@ difficulties= {
triforce_pieces_required = 30,
conditional_extras = no_conditonal_extras,
extras = [hardfirst20extra, hardsecond20extra, hardthird20extra, hardfinal20extra],
progressive_sword_limit = 3,
progressive_shield_limit = 2,
progressive_armor_limit = 1,
progressive_bottle_limit = 2,
),
'expert': Difficulty(
baseitems = expertbaseitems,
@ -154,6 +167,10 @@ difficulties= {
triforce_pieces_required = 40,
conditional_extras = no_conditonal_extras,
extras = [expertfirst15extra, expertsecond25extra, expertthird15extra, expertfinal25extra],
progressive_sword_limit = 2,
progressive_shield_limit = 0,
progressive_armor_limit = 0,
progressive_bottle_limit = 1,
),
'insane': Difficulty(
baseitems = insanebaseitems,
@ -173,14 +190,21 @@ difficulties= {
triforce_pieces_required = 50,
conditional_extras = no_conditonal_extras,
extras = [insanefirst15extra, insanesecond25extra, insanethird10extra, insanefourth15extra, insanefinal25extra],
progressive_sword_limit = 2,
progressive_shield_limit = 0,
progressive_armor_limit = 0,
progressive_bottle_limit = 1,
),
}
def generate_itempool(world):
if (world.difficulty not in ['easy', 'normal', 'hard', 'expert', 'insane'] or world.goal not in ['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'crystals']
or world.mode not in ['open', 'standard', 'swordless'] or world.timer not in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'] or world.progressive not in ['on', 'off', 'random']):
or world.mode not in ['open', 'standard', 'swordless'] 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
world.push_item('Ganon', ItemFactory('Triforce'), False)
world.get_location('Ganon').event = True
world.push_item('Agahnim 1', ItemFactory('Beat Agahnim 1'), False)
@ -201,6 +225,19 @@ def generate_itempool(world):
if treasure_hunt_icon is not None:
world.treasure_hunt_icon = treasure_hunt_icon
if world.keysanity:
world.itempool.extend(get_dungeon_item_pool(world))
# 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 peices in expert mode)
if world.difficulty in ['easy', 'normal', 'hard']:
[item for item in world.itempool if item.name == 'Boss Heart Container'][0].advancement = True
elif world.difficulty in ['expert']:
adv_heart_pieces = [item for item in world.itempool if item.name == 'Piece of Heart'][0:4]
for hp in adv_heart_pieces:
hp.advancement = True
# shuffle medallions
mm_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)]
tr_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)]
@ -217,19 +254,19 @@ def generate_itempool(world):
fill_restrictive(world, world.get_all_state(keys=True), crystal_locations, crystals)
def get_pool_core(progressive,shuffle,difficulty,timer, goal, mode):
pool=[]
placed_items=[]
clock_mode=None
treasure_hunt_count=None
treasure_hunt_icon=None
def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode):
pool = []
placed_items = []
clock_mode = None
treasure_hunt_count = None
treasure_hunt_icon = None
pool.extend(alwaysitems)
def wantProgressives():
return random.choice([True, False]) if progressive == 'random' else progressive=='on'
def want_progressives():
return random.choice([True, False]) if progressive == 'random' else progressive == 'on'
if wantProgressives():
if want_progressives():
pool.extend(progressivegloves)
else:
pool.extend(basicgloves)
@ -253,17 +290,17 @@ def get_pool_core(progressive,shuffle,difficulty,timer, goal, mode):
# all bottles, since only one bottle is available
if diff.same_bottle:
thisbottle = random.choice(diff.bottles)
for i in range (diff.bottle_count):
for _ in range(diff.bottle_count):
if not diff.same_bottle:
thisbottle = random.choice(diff.bottles)
pool.append(thisbottle)
if wantProgressives():
if want_progressives():
pool.extend(diff.progressiveshield)
else:
pool.extend(diff.basicshield)
if wantProgressives():
if want_progressives():
pool.extend(diff.progressivearmor)
else:
pool.extend(diff.basicarmor)
@ -271,21 +308,21 @@ def get_pool_core(progressive,shuffle,difficulty,timer, goal, mode):
if mode == 'swordless':
pool.extend(diff.swordless)
elif mode == 'standard':
if wantProgressives():
if want_progressives():
placed_items.append(('Link\'s Uncle', 'Progressive Sword'))
pool.extend(diff.progressivesword)
else:
placed_items.append(('Link\'s Uncle', 'Fighter Sword'))
pool.extend(diff.basicsword)
else:
if wantProgressives():
if want_progressives():
pool.extend(diff.progressivesword)
pool.extend(['Progressive Sword'])
else:
pool.extend(diff.basicsword)
pool.extend(['Fighter Sword'])
extraitems = TotalItemsToPlace - len(pool) - len(placed_items)
extraitems = total_items_to_place - len(pool) - len(placed_items)
if timer in ['timed', 'timed-countdown']:
pool.extend(diff.timedother)
@ -306,8 +343,8 @@ def get_pool_core(progressive,shuffle,difficulty,timer, goal, mode):
extraitems -= len(cond_extras)
for extra in diff.extras:
if(extraitems > 0):
pool.extend(extra )
if extraitems > 0:
pool.extend(extra)
extraitems -= len(extra)
if goal == 'pedestal':
@ -315,19 +352,22 @@ def get_pool_core(progressive,shuffle,difficulty,timer, goal, mode):
return (pool, placed_items, clock_mode, treasure_hunt_count, treasure_hunt_icon)
# A quick test to ensure all combinations generate the correct amount of items.
if __name__ == '__main__':
def test():
for difficulty in ['easy', 'normal', 'hard', 'expert', 'insane']:
for goal in ['ganon', 'triforcehunt', 'pedestal']:
for timer in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown']:
for mode in ['open', 'standard', 'swordless']:
for progressive in ['on','off']:
for shuffle in ['full','insane']:
for progressive in ['on', 'off']:
for shuffle in ['full', 'insane']:
out = get_pool_core(progressive, shuffle, difficulty, timer, goal, mode)
count = len(out[0]) + len(out[1])
correct_count = TotalItemsToPlace
correct_count = total_items_to_place
if goal in ['pedestal']:
# pedestal goals generate one extra item
correct_count += 1
assert count == correct_count, "expected {0} items but found {1} items for {2}".format(correct_count, count, (progressive, shuffle, difficulty, timer, goal, mode))
if __name__ == '__main__':
test()

255
Items.py
View File

@ -1,7 +1,7 @@
from BaseClasses import World, Item
import random
import logging
from BaseClasses import Item
def ItemFactory(items):
ret = []
@ -14,58 +14,57 @@ def ItemFactory(items):
advancement, priority, type, code, pedestal_hint, pedestal_credit, sickkid_credit, zora_credit, witch_credit, fluteboy_credit = item_table[item]
ret.append(Item(item, advancement, priority, type, code, pedestal_hint, pedestal_credit, sickkid_credit, zora_credit, witch_credit, fluteboy_credit))
else:
logging.getLogger('').warning('Unknown Item: %s' % item)
logging.getLogger('').warning('Unknown Item: %s', item)
return None
if singleton:
return ret[0]
else:
return ret
return ret
# Format: Name: (Advancement, Priority, Type, ItemCode, Pedestal Hint Text, Pedestal Credit Text, Sick Kid Credit Text, Zora Credit Text, Witch Credit Text, Flute Boy Credit Text)
item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'boy shoots again'),
'Book of Mudora': (True, False, None, 0x1D, 'This is a\nparadox?!', 'and the story book', 'the scholarly kid', 'moon runes for sale', 'drugs for literacy', 'boy can read again'),
item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again'),
'Book of Mudora': (True, False, None, 0x1D, 'This is a\nparadox?!', 'and the story book', 'the scholarly kid', 'moon runes for sale', 'drugs for literacy', 'book-worm boy can read again'),
'Hammer': (True, False, None, 0x09, 'stop\nhammer time!', 'and m c hammer', 'hammer-smashing kid', 'm c hammer for sale', 'stop... hammer time', 'stop, hammer time'),
'Hookshot': (True, False, None, 0x0A, 'BOING!!!\nBOING!!!\nBOING!!!', 'and the tickle beam', 'tickle-monster kid', 'tickle beam for sale', 'witch and tickle boy', 'boy tickles again'),
'Magic Mirror': (True, False, None, 0x1A, 'Isn\'t your\nreflection so\npretty?', 'the face reflector', 'the narcissistic kid', 'your face for sale', 'trades looking-glass', 'boy sees himself again'),
'Hookshot': (True, False, None, 0x0A, 'BOING!!!\nBOING!!!\nBOING!!!', 'and the tickle beam', 'tickle-monster kid', 'tickle beam for sale', 'witch and tickle boy', 'beam boy tickles again'),
'Magic Mirror': (True, False, None, 0x1A, 'Isn\'t your\nreflection so\npretty?', 'the face reflector', 'the narcissistic kid', 'your face for sale', 'trades looking-glass', 'narcissistic boy is happy again'),
'Ocarina': (True, False, None, 0x14, 'Save the duck\nand fly to\nfreedom!', 'and the duck call', 'the duck-call kid', 'duck call for sale', 'duck-calls for trade', 'ocarina boy plays again'),
'Pegasus Boots': (True, False, None, 0x4B, 'Gotta go fast!', 'and the sprint shoes', 'the running-man kid', 'sprint shoe for sale', 'shrooms for speed', 'boy runs again'),
'Power Glove': (True, False, None, 0x1B, 'Now you can\nlift weak\nstuff!', 'and the grey mittens', 'body-building kid', 'lift glove for sale', 'fungus for gloves', 'boy lifts again'),
'Cape': (True, False, None, 0x19, 'Wear this to\nbecome\ninvisible!', 'the camouflage cape', 'red riding-hood kid', 'red hood for sale', 'hood from a hood', 'boy hides again'),
'Mushroom': (True, False, None, 0x29, 'I\'m a fun guy!\n\nI\'m a funghi!', 'and the legal drugs', 'the drug-dealing kid', 'legal drugs for sale', 'shroom swap', 'boy sells drugs again'),
'Shovel': (True, False, None, 0x13, 'Can\n You\n Dig it?', 'and the fetch quest', 'archaeologist kid', 'dirt spade for sale', 'can you dig it', 'boy digs again'),
'Lamp': (True, False, None, 0x12, 'Baby, baby,\nbaby.\nLight my way!', 'and the flashlight', 'light-shining kid', 'flashlight for sale', 'fungus for illumination', 'boy sees at night again'),
'Magic Powder': (True, False, None, 0x0D, 'you can turn\nanti-faeries\ninto fairies', 'and the magic sack', 'the sack-holding kid', 'magic sack for sale', 'the witch and assistant', 'boy plays marbles again'),
'Moon Pearl': (True, False, None, 0x1F, ' Bunny Link\n be\n gone!', 'and the jaw breaker', 'fortune-telling kid', 'lunar orb for sale', 'shrooms for moon rock', 'boy plays ball again'),
'Cane of Somaria': (True, False, None, 0x15, 'I make blocks\nto hold down\nswitches!', 'and the red blocks', 'the block-making kid', 'block stick for sale', 'block stick for trade', 'boy makes blocks again'),
'Fire Rod': (True, False, None, 0x07, 'I\'m the hot\nrod. I make\nthings burn!', 'and the flamethrower', 'fire-starting kid', 'rage rod for sale', 'fungus for rage-rod', 'boy burns again'),
'Flippers': (True, False, None, 0x1E, 'fancy a swim?', 'and the toewebs', 'the swimming kid', 'finger webs for sale', 'shrooms let you swim', 'boy swims again'),
'Ice Rod': (True, False, None, 0x08, 'I\'m the cold\nrod. I make\nthings freeze!', 'and the freeze ray', 'the ice-bending kid', 'freeze ray for sale', 'fungus for ice-rod', 'boy freezes again'),
'Titans Mitts': (True, False, None, 0x1C, 'Now you can\nlift heavy\nstuff!', 'and the golden glove', 'body-building kid', 'carry glove for sale', 'fungus for bling-gloves', 'boy has bling again'),
'Ether': (True, False, None, 0x10, 'This magic\ncoin freezes\neverything!', 'and the bolt coin', 'coin-collecting kid', 'bolt coin for sale', 'shrooms for bolt-coin', 'boy hides coin again'),
'Bombos': (True, False, None, 0x0F, 'Burn, baby,\nburn! Fear my\nring of fire!', 'and the swirly coin', 'coin-collecting kid', 'swirly coin for sale', 'shrooms for swirly-coin', 'boy hides coin again'),
'Quake': (True, False, None, 0x11, 'Maxing out the\nRichter scale\nis what I do!', 'and the wavy coin', 'coin-collecting kid', 'wavy coin for sale', 'shrooms for wavy-coin', 'boy hides coin again'),
'Bottle': (True, False, None, 0x16, 'Now you can\nstore potions\nand stuff!', 'and the terrarium', 'the terrarium kid', 'terrarium for sale', 'special promotion', 'boy stores things again'),
'Bottle (Red Potion)': (True, False, None, 0x2B, 'Hearty red goop!', 'and the red goo', 'the liquid kid', 'potion for sale', 'free samples', 'boy drinks again'),
'Bottle (Green Potion)': (True, False, None, 0x2C, 'Refreshing green goop!', 'and the green goo', 'the liquid kid', 'potion for sale', 'free samples', 'boy drinks again'),
'Bottle (Blue Potion)': (True, False, None, 0x2D, 'Delicious blue goop!', 'and the blue goo', 'the liquid kid', 'potion for sale', 'free samples', 'boy stores drinks again'),
'Bottle (Fairy)': (True, False, None, 0x3D, 'Save me and I will revive you', 'and the captive', 'the tingle kid', 'hostage for sale', 'fairy dust and shrooms', 'boy revives again'),
'Bottle (Bee)': (True, False, None, 0x3C, 'I will sting your foes a few times', 'and the sting buddy', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'boy is stung again'),
'Bottle (Good Bee)': (True, False, None, 0x48, 'I will sting your foes a whole lot!', 'and the sparkle sting', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'boy is stung again'),
'Master Sword': (True, False, None, 0x50, 'I beat barries and pigs alike', 'and the master sword', 'sword-wielding kid', 'glow sword for sale', 'fungus for blue slasher', 'boy fights again'),
'Tempered Sword': (True, False, None, 0x02, 'I stole the\nblacksmith\'s\njob!', 'the tempered sword', 'sword-wielding kid', 'flame sword for sale', 'fungus for red slasher', 'boy fights again'),
'Fighter Sword': (True, False, None, 0x49, 'A pathetic\nsword rests\nhere!', 'the tiny sword', 'sword-wielding kid', 'tiny sword for sale', 'fungus for tiny slasher', 'boy fights again'),
'Golden Sword': (True, False, None, 0x03, 'The butter\nsword rests\nhere!', 'and the butter sword', 'sword-wielding kid', 'butter for sale', 'cap churned to butter', 'boy fights again'),
'Progressive Sword': (True, False, None, 0x5E, 'a better copy\nof your sword\nfor your time', 'the unknown sword', 'sword-wielding kid', 'sword for sale', 'fungus for some slasher', 'boy fights again'),
'Progressive Glove': (True, False, None, 0x61, 'a way to lift\nheavier things', 'and the lift upgrade', 'body-building kid', 'some glove for sale', 'fungus for gloves', 'boy lifts again'),
'Silver Arrows': (True, False, None, 0x58, 'Do you fancy\nsilver tipped\narrows?', 'and the ganonsbane', 'ganon-killing kid', 'ganon doom for sale', 'fungus for pork', 'boy eats pork chops'),
'Pegasus Boots': (True, False, None, 0x4B, 'Gotta go fast!', 'and the sprint shoes', 'the running-man kid', 'sprint shoe for sale', 'shrooms for speed', 'gotta-go-fast boy runs again'),
'Power Glove': (True, False, None, 0x1B, 'Now you can\nlift weak\nstuff!', 'and the grey mittens', 'body-building kid', 'lift glove for sale', 'fungus for gloves', 'body-building boy lifts again'),
'Cape': (True, False, None, 0x19, 'Wear this to\nbecome\ninvisible!', 'the camouflage cape', 'red riding-hood kid', 'red hood for sale', 'hood from a hood', 'dapper boy hides again'),
'Mushroom': (True, False, None, 0x29, 'I\'m a fun guy!\n\nI\'m a funghi!', 'and the legal drugs', 'the drug-dealing kid', 'legal drugs for sale', 'shroom swap', 'shroom boy sells drugs again'),
'Shovel': (True, False, None, 0x13, 'Can\n You\n Dig it?', 'and the fetch quest', 'archaeologist kid', 'dirt spade for sale', 'can you dig it', 'shovel boy digs again'),
'Lamp': (True, False, None, 0x12, 'Baby, baby,\nbaby.\nLight my way!', 'and the flashlight', 'light-shining kid', 'flashlight for sale', 'fungus for illumination', 'illuminated boy can see again'),
'Magic Powder': (True, False, None, 0x0D, 'you can turn\nanti-faeries\ninto faeries', 'and the magic sack', 'the sack-holding kid', 'magic sack for sale', 'the witch and assistant', 'magic boy plays marbles again'),
'Moon Pearl': (True, False, None, 0x1F, ' Bunny Link\n be\n gone!', 'and the jaw breaker', 'fortune-telling kid', 'lunar orb for sale', 'shrooms for moon rock', 'moon boy plays ball again'),
'Cane of Somaria': (True, False, None, 0x15, 'I make blocks\nto hold down\nswitches!', 'and the red blocks', 'the block-making kid', 'block stick for sale', 'block stick for trade', 'cane boy makes blocks again'),
'Fire Rod': (True, False, None, 0x07, 'I\'m the hot\nrod. I make\nthings burn!', 'and the flamethrower', 'fire-starting kid', 'rage rod for sale', 'fungus for rage-rod', 'firestarter boy burns again'),
'Flippers': (True, False, None, 0x1E, 'fancy a swim?', 'and the toewebs', 'the swimming kid', 'finger webs for sale', 'shrooms let you swim', 'swimming boy swims agai'),
'Ice Rod': (True, False, None, 0x08, 'I\'m the cold\nrod. I make\nthings freeze!', 'and the freeze ray', 'the ice-bending kid', 'freeze ray for sale', 'fungus for ice-rod', 'ice-cube boy freezes again'),
'Titans Mitts': (True, False, None, 0x1C, 'Now you can\nlift heavy\nstuff!', 'and the golden glove', 'body-building kid', 'carry glove for sale', 'fungus for bling-gloves', 'body-building boy has gold again'),
'Ether': (True, False, None, 0x10, 'This magic\ncoin freezes\neverything!', 'and the bolt coin', 'coin-collecting kid', 'bolt coin for sale', 'shrooms for bolt-coin', 'medallion boy sees floor again'),
'Bombos': (True, False, None, 0x0F, 'Burn, baby,\nburn! Fear my\nring of fire!', 'and the swirly coin', 'coin-collecting kid', 'swirly coin for sale', 'shrooms for swirly-coin', 'medallion boy melts room again'),
'Quake': (True, False, None, 0x11, 'Maxing out the\nRichter scale\nis what I do!', 'and the wavy coin', 'coin-collecting kid', 'wavy coin for sale', 'shrooms for wavy-coin', 'medallion boy shakes dirt again'),
'Bottle': (True, False, None, 0x16, 'Now you can\nstore potions\nand stuff!', 'and the terrarium', 'the terrarium kid', 'terrarium for sale', 'special promotion', 'bottle boy has terrarium again'),
'Bottle (Red Potion)': (True, False, None, 0x2B, 'Hearty red goop!', 'and the red goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has red goo again'),
'Bottle (Green Potion)': (True, False, None, 0x2C, 'Refreshing green goop!', 'and the green goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has green goo again'),
'Bottle (Blue Potion)': (True, False, None, 0x2D, 'Delicious blue goop!', 'and the blue goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has blue goo again'),
'Bottle (Fairy)': (True, False, None, 0x3D, 'Save me and I will revive you', 'and the captive', 'the tingle kid', 'hostage for sale', 'fairy dust and shrooms', 'bottle boy has friend again'),
'Bottle (Bee)': (True, False, None, 0x3C, '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'),
'Bottle (Good Bee)': (True, False, None, 0x48, 'I will sting your foes a whole lot!', 'and the sparkle sting', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has beetor again'),
'Master Sword': (True, False, None, 0x50, 'I beat barries and pigs alike', 'and the master sword', 'sword-wielding kid', 'glow sword for sale', 'fungus for blue slasher', 'sword boy fights again'),
'Tempered Sword': (True, False, None, 0x02, 'I stole the\nblacksmith\'s\njob!', 'the tempered sword', 'sword-wielding kid', 'flame sword for sale', 'fungus for red slasher', 'sword boy fights again'),
'Fighter Sword': (True, False, None, 0x49, 'A pathetic\nsword rests\nhere!', 'the tiny sword', 'sword-wielding kid', 'tiny sword for sale', 'fungus for tiny slasher', 'sword boy fights again'),
'Golden Sword': (True, False, None, 0x03, 'The butter\nsword rests\nhere!', 'and the butter sword', 'sword-wielding kid', 'butter for sale', 'cap churned to butter', 'sword boy fights again'),
'Progressive Sword': (True, False, None, 0x5E, 'a better copy\nof your sword\nfor your time', 'the unknown sword', 'sword-wielding kid', 'sword for sale', 'fungus for some slasher', 'sword boy fights again'),
'Progressive Glove': (True, False, None, 0x61, 'a way to lift\nheavier things', 'and the lift upgrade', 'body-building kid', 'some glove for sale', 'fungus for gloves', 'body-building boy lifts again'),
'Silver Arrows': (True, False, None, 0x58, 'Do you fancy\nsilver tipped\narrows?', 'and the ganonsbane', 'ganon-killing kid', 'ganon doom for sale', 'fungus for pork', 'archer boy shines again'),
'Green Pendant': (True, False, 'Crystal', [0x04, 0x38, 0x62, 0x00, 0x69, 0x01], None, None, None, None, None, None),
'Red Pendant': (True, False, 'Crystal', [0x02, 0x34, 0x60, 0x00, 0x69, 0x02], None, None, None, None, None, None),
'Blue Pendant': (True, False, 'Crystal', [0x01, 0x32, 0x60, 0x00, 0x69, 0x03], None, None, None, None, None, None),
'Triforce': (True, False, None, 0x6A, '\n YOU WIN!', 'and the triforce', 'victorious kid', 'victory for sale', 'fungus for the win', 'boy wins again'),
'Power Star': (True, False, None, 0x6B, 'a small victory', 'and the power star', 'star-struck kid', 'star for sale', 'see stars with shroom', 'boy sees stars again'),
'Triforce Piece': (True, False, None, 0x6C, 'a small victory', 'and the thirdforce', 'triangular kid', 'triangle for sale', 'fungus for triangle', 'boy triangulates again'),
'Triforce': (True, False, None, 0x6A, '\n YOU WIN!', 'and the triforce', 'victorious kid', 'victory for sale', 'fungus for the win', 'greedy boy wins game again'),
'Power Star': (True, False, None, 0x6B, 'a small victory', 'and the power star', 'star-struck kid', 'star for sale', 'see stars with shroom', 'mario powers up again'),
'Triforce Piece': (True, False, None, 0x6C, 'a small victory', 'and the thirdforce', 'triangular kid', 'triangle for sale', 'fungus for triangle', 'wise boy has triangle again'),
'Crystal 1': (True, False, 'Crystal', [0x02, 0x34, 0x64, 0x40, 0x7F, 0x06], None, None, None, None, None, None),
'Crystal 2': (True, False, 'Crystal', [0x10, 0x34, 0x64, 0x40, 0x79, 0x06], None, None, None, None, None, None),
'Crystal 3': (True, False, 'Crystal', [0x40, 0x34, 0x64, 0x40, 0x6C, 0x06], None, None, None, None, None, None),
@ -73,91 +72,91 @@ item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher cla
'Crystal 5': (True, False, 'Crystal', [0x04, 0x32, 0x64, 0x40, 0x6E, 0x06], None, None, None, None, None, None),
'Crystal 6': (True, False, 'Crystal', [0x01, 0x32, 0x64, 0x40, 0x6F, 0x06], None, None, None, None, None, None),
'Crystal 7': (True, False, 'Crystal', [0x08, 0x34, 0x64, 0x40, 0x7C, 0x06], None, None, None, None, None, None),
'Single Arrow': (False, False, None, 0x43, 'a lonely arrow\nsits here.', 'and the arrow', 'stick-collecting kid', 'sewing kit for sale', 'fungus for arrow', 'boy sews again'),
'Arrows (10)': (False, False, None, 0x44, 'This will give\nyou ten shots\nwith your bow!', 'and the arrow pack', 'stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'boy sews again'),
'Arrow Upgrade (+10)': (False, False, None, 0x54, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'boy sews more again'),
'Arrow Upgrade (+5)': (False, False, None, 0x53, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'boy sews more again'),
'Single Bomb': (False, False, None, 0x27, 'I make things\ngo BOOM! But\njust once.', 'and the explosion', 'the bomb-holding kid', 'firecracker for sale', 'blend fungus into bomb', 'boy explodes again'),
'Bombs (3)': (False, False, None, 0x28, 'I make things\ngo triple\nBOOM!!!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', 'boy explodes again'),
'Bomb Upgrade (+10)': (False, False, None, 0x52, 'increase bomb\nstorage, low\nlow price', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'boy explodes more again'),
'Bomb Upgrade (+5)': (False, False, None, 0x51, 'increase bomb\nstorage, low\nlow price', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'boy explodes more again'),
'Blue Mail': (False, True, None, 0x22, 'Now you\'re a\nblue elf!', 'and the banana hat', 'the protected kid', 'banana hat for sale', 'the clothing store', 'boy fears little again'),
'Red Mail': (False, True, None, 0x23, 'Now you\'re a\nred elf!', 'and the eggplant hat', 'well-protected kid', 'purple hat for sale', 'the nice clothing store', 'boy fears nothing again'),
'Progressive Armor': (False, True, None, 0x60, 'time for a\nchange of\nclothes?', 'and the unknown hat', 'the protected kid', 'new hat for sale', 'the clothing store', 'boy fears less again'),
'Blue Boomerang': (False, True, None, 0x0C, 'No matter what\nyou do, blue\nreturns to you', 'and the bluemarang', 'the bat-throwing kid', 'bent stick for sale', 'fungus for puma-stick', 'boy plays fetch again'),
'Red Boomerang': (False, True, None, 0x2A, 'No matter what\nyou do, red\nreturns to you', 'and the badmarang', 'the bat-throwing kid', 'air foil for sale', 'fungus for return-stick', 'boy plays fetch again'),
'Blue Shield': (False, True, None, 0x04, 'Now you can\ndefend against\npebbles!', 'and the stone blocker', 'shield-wielding kid', 'shield for sale', 'fungus for shield', 'boy defends again'),
'Red Shield': (False, True, None, 0x05, 'Now you can\ndefend against\nfireballs!', 'and the shot blocker', 'shield-wielding kid', 'fire shield for sale', 'fungus for fire shield', 'boy defends again'),
'Mirror Shield': (True, False, None, 0x06, 'Now you can\ndefend against\nlasers!', 'and the laser blocker', 'shield-wielding kid', 'face shield for sale', 'fungus for face shield', 'boy defends again'),
'Progressive Shield': (True, False, None, 0x5F, 'have a better\nblocker in\nfront of you', 'and the new shield', 'shield-wielding kid', 'shield for sale', 'fungus for shield', 'boy defends again'),
'Bug Catching Net': (True, False, None, 0x21, 'Let\'s catch\nsome bees and\nfaeries!', 'and the bee catcher', 'the bug-catching kid', 'stick web for sale', 'fungus for butterflies', 'boy catches bees again'),
'Cane of Byrna': (True, False, None, 0x18, 'Use this to\nbecome\ninvincible!', 'and the bad cane', 'the spark-making kid', 'spark stick for sale', 'spark-stick for trade', 'boy encircles again'),
'Boss Heart Container': (False, False, None, 0x3E, 'Maximum health\nincreased!\nYeah!', 'and the full heart', 'the life-giving kid', 'love for sale', 'fungus for life', 'boy feels love again'),
'Sanctuary Heart Container': (False, False, None, 0x3F, 'Maximum health\nincreased!\nYeah!', 'and the full heart', 'the life-giving kid', 'love for sale', 'fungus for life', 'boy feels love again'),
'Piece of Heart': (False, False, None, 0x17, 'Just a little\npiece of love!', 'and the broken heart', 'the life-giving kid', 'little love for sale', 'fungus for life', 'boy feels love again'),
'Rupee (1)': (False, False, None, 0x34, 'Just pocket\nchange. Move\nright along.', 'the pocket change', 'poverty-struck kid', 'life lesson for sale', 'buying cheap drugs', 'boy has snack again'),
'Rupees (5)': (False, False, None, 0x35, 'Just pocket\nchange. Move\nright along.', 'the pocket change', 'poverty-struck kid', 'life lesson for sale', 'buying cheap drugs', 'boy has snack again'),
'Rupees (20)': (False, False, None, 0x36, 'Just couch\ncash. Move\nright along.', 'and the couch cash', 'the piggy-bank kid', 'life lesson for sale', 'the witch buying drugs', 'boy has lunch again'),
'Rupees (50)': (False, False, None, 0x41, 'A rupee pile!\nOkay?', 'and the rupee pile', 'the well-off kid', 'life lesson for sale', 'buying okay drugs', 'boy has dinner again'),
'Rupees (100)': (False, False, None, 0x40, 'A rupee stash!\nHell yeah!', 'and the rupee stash', 'the kind-of-rich kid', 'life lesson for sale', 'buying good drugs', 'boy has buffet again'),
'Rupees (300)': (False, False, None, 0x46, 'A rupee hoard!\nHell yeah!', 'and the rupee hoard', 'the really-rich kid', 'life lesson for sale', 'buying the best drugs', 'boy is rich again'),
'Rupoor': (False, False, None, 0x59, 'a debt collector', 'and the toll-booth', 'the toll-booth kid', 'double loss for sale', 'witch stole your rupees', 'boy steals rupees again'),
'Red Clock': (False, True, None, 0x5B, 'a waste of time', 'the ruby clock', 'the ruby-time kid', 'red time for sale', 'for ruby time', 'boy adjusts time again'),
'Blue Clock': (False, True, None, 0x5C, 'a bit of time', 'the sapphire clock', 'sapphire-time kid', 'blue time for sale', 'for sapphire time', 'boy adjusts time again'),
'Green Clock': (False, True, None, 0x5D, 'a lot of time', 'the emerald clock', 'the emerald-time kid', 'green time for sale', 'for emerald time', 'boy adjusts time again'),
'Single RNG': (False, True, None, 0x62, 'something you don\'t yet have', None, None, None, None, None),
'Multi RNG': (False, True, None, 0x63, 'something you may already have', None, None, None, None, None),
'Magic Upgrade (1/2)': (True, False, None, 0x4E, 'Your magic\npower has been\ndoubled!', 'and the spell power', 'the magic-saving kid', 'wizardry for sale', 'mekalekahi mekahiney ho', 'boy saves magic again'), # can be required to beat mothula in an open seed in very very rare circumstance
'Magic Upgrade (1/4)': (True, False, None, 0x4F, 'Your magic\npower has been\nquadrupled!', 'and the spell power', 'the magic-saving kid', 'wizardry for sale', 'mekalekahi mekahiney ho', 'boy saves magic again'), # can be required to beat mothula in an open seed in very very rare circumstance
'Small Key (Eastern Palace)': (False, False, 'SmallKey', 0xA2, 'A small key to Armos Knights', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'boy opens door again'),
'Big Key (Eastern Palace)': (False, False, 'BigKey', 0x9D, 'A big key to Armos Knights', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'boy opens chest again'),
'Compass (Eastern Palace)': (False, True, 'Compass', 0x8D, 'Now you can find the Armos Knights!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'boy points north again'),
'Map (Eastern Palace)': (False, True, 'Map', 0x7D, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'boy draws again'),
'Small Key (Desert Palace)': (False, False, 'SmallKey', 0xA3, 'A small key to the desert', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'boy opens door again'),
'Big Key (Desert Palace)': (False, False, 'BigKey', 0x9C, 'A big key to the desert', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'boy opens chest again'),
'Compass (Desert Palace)': (False, True, 'Compass', 0x8C, 'Now you can find Lanmolas!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'boy points north again'),
'Map (Desert Palace)': (False, True, 'Map', 0x7C, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'boy draws again'),
'Small Key (Tower of Hera)': (False, False, 'SmallKey', 0xAA, 'A small key to Hera', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'boy opens door again'),
'Big Key (Tower of Hera)': (False, False, 'BigKey', 0x95, 'A big key to Hera', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'boy opens chest again'),
'Compass (Tower of Hera)': (False, True, 'Compass', 0x85, 'Now you can find Moldorm!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'boy points north again'),
'Map (Tower of Hera)': (False, True, 'Map', 0x75, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'boy draws again'),
'Small Key (Escape)': (False, False, 'SmallKey', 0xA0, 'A small key to the castle', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'boy opens door again'),
'Big Key (Escape)': (False, False, 'BigKey', 0x9F, 'A big key to the castle', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'boy opens chest again'),
'Compass (Escape)': (False, True, 'Compass', 0x8F, 'Now you can find no boss!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'boy points north again'),
'Map (Escape)': (False, True, 'Map', 0x7F, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'boy draws again'),
'Small Key (Agahnims Tower)': (False, False, 'SmallKey', 0xA4, 'A small key to Agahnim', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'boy opens door again'),
'Small Key (Palace of Darkness)': (False, False, 'SmallKey', 0xA6, 'A small key to darkness', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'boy opens door again'),
'Big Key (Palace of Darkness)': (False, False, 'BigKey', 0x99, 'A big key to darkness', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'boy opens chest again'),
'Compass (Palace of Darkness)': (False, True, 'Compass', 0x89, 'Now you can find Helmasaur King!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'boy points north again'),
'Map (Palace of Darkness)': (False, True, 'Map', 0x79, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'boy draws again'),
'Small Key (Thieves Town)': (False, False, 'SmallKey', 0xAB, 'A small key to thievery', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'boy opens door again'),
'Big Key (Thieves Town)': (False, False, 'BigKey', 0x94, 'A big key to thievery', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'boy opens chest again'),
'Compass (Thieves Town)': (False, True, 'Compass', 0x84, 'Now you can find Blind!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'boy points north again'),
'Map (Thieves Town)': (False, True, 'Map', 0x74, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'boy draws again'),
'Small Key (Skull Woods)': (False, False, 'SmallKey', 0xA8, 'A small key to the woods', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'boy opens door again'),
'Big Key (Skull Woods)': (False, False, 'BigKey', 0x97, 'A big key to the woods', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'boy opens chest again'),
'Compass (Skull Woods)': (False, True, 'Compass', 0x87, 'Now you can find Mothula!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'boy points north again'),
'Map (Skull Woods)': (False, True, 'Map', 0x77, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'boy draws again'),
'Small Key (Swamp Palace)': (False, False, 'SmallKey', 0xA5, 'A small key to the swamp', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'boy opens door again'),
'Big Key (Swamp Palace)': (False, False, 'BigKey', 0x9A, 'A big key to the swamp', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'boy opens chest again'),
'Compass (Swamp Palace)': (False, True, 'Compass', 0x8A, 'Now you can find Arrghus!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'boy points north again'),
'Map (Swamp Palace)': (False, True, 'Map', 0x7A, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'boy draws again'),
'Small Key (Ice Palace)': (False, False, 'SmallKey', 0xA9, 'A small key to the iceberg', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'boy opens door again'),
'Big Key (Ice Palace)': (False, False, 'BigKey', 0x96, 'A big key to the iceberg', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'boy opens chest again'),
'Compass (Ice Palace)': (False, True, 'Compass', 0x86, 'Now you can find Kholdstare!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'boy points north again'),
'Map (Ice Palace)': (False, True, 'Map', 0x76, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'boy draws again'),
'Small Key (Misery Mire)': (False, False, 'SmallKey', 0xA7, 'A small key to the mire', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'boy opens door again'),
'Big Key (Misery Mire)': (False, False, 'BigKey', 0x98, 'A big key to the mire', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'boy opens chest again'),
'Compass (Misery Mire)': (False, True, 'Compass', 0x88, 'Now you can find Vitreous!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'boy points north again'),
'Map (Misery Mire)': (False, True, 'Map', 0x78, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'boy draws again'),
'Small Key (Turtle Rock)': (False, False, 'SmallKey', 0xAC, 'A small key to the pipe maze', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'boy opens door again'),
'Big Key (Turtle Rock)': (False, False, 'BigKey', 0x93, 'A big key to the pipe maze', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'boy opens chest again'),
'Compass (Turtle Rock)': (False, True, 'Compass', 0x83, 'Now you can find Trinexx!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'boy points north again'),
'Map (Turtle Rock)': (False, True, 'Map', 0x73, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'boy draws again'),
'Small Key (Ganons Tower)': (False, False, 'SmallKey', 0xAD, 'A small key to the evil tower', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'boy opens door again'),
'Big Key (Ganons Tower)': (False, False, 'BigKey', 0x92, 'A big key to the evil tower', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'boy opens chest again'),
'Compass (Ganons Tower)': (False, True, 'Compass', 0x82, 'Now you can find Agahnim!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'boy points north again'),
'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', 'boy draws again'),
'Nothing': (False, False, None, 0x5A, 'Some Hot Air', 'and the Nothing', 'the zen kid', 'outright theft', 'shroom theft', 'boy is bored again'),
'Single Arrow': (False, False, None, 0x43, 'a lonely arrow\nsits here.', 'and the arrow', 'stick-collecting kid', 'sewing kit for sale', 'fungus for arrow', 'archer boy sews again'),
'Arrows (10)': (False, False, None, 0x44, 'This will give\nyou ten shots\nwith your bow!', 'and the arrow pack', 'stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'archer boy sews again'),
'Arrow Upgrade (+10)': (False, False, None, 0x54, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again'),
'Arrow Upgrade (+5)': (False, False, None, 0x53, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again'),
'Single Bomb': (False, False, None, 0x27, 'I make things\ngo BOOM! But\njust once.', 'and the explosion', 'the bomb-holding kid', 'firecracker for sale', 'blend fungus into bomb', '\'splosion boy explodes again'),
'Bombs (3)': (False, False, None, 0x28, 'I make things\ngo triple\nBOOM!!!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again'),
'Bomb Upgrade (+10)': (False, False, None, 0x52, 'increase bomb\nstorage, low\nlow price', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again'),
'Bomb Upgrade (+5)': (False, False, None, 0x51, 'increase bomb\nstorage, low\nlow price', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again'),
'Blue Mail': (False, True, None, 0x22, 'Now you\'re a\nblue elf!', 'and the banana hat', 'the protected kid', 'banana hat for sale', 'the clothing store', 'tailor boy banana hatted again'),
'Red Mail': (False, True, None, 0x23, 'Now you\'re a\nred elf!', 'and the eggplant hat', 'well-protected kid', 'purple hat for sale', 'the nice clothing store', 'tailor boy fears nothing again'),
'Progressive Armor': (False, True, None, 0x60, 'time for a\nchange of\nclothes?', 'and the unknown hat', 'the protected kid', 'new hat for sale', 'the clothing store', 'tailor boy has threads again'),
'Blue Boomerang': (False, True, None, 0x0C, 'No matter what\nyou do, blue\nreturns to you', 'and the bluemarang', 'the bat-throwing kid', 'bent stick for sale', 'fungus for puma-stick', 'throwing boy plays fetch again'),
'Red Boomerang': (False, True, None, 0x2A, 'No matter what\nyou do, red\nreturns to you', 'and the badmarang', 'the bat-throwing kid', 'air foil for sale', 'fungus for return-stick', 'magical boy plays fetch again'),
'Blue Shield': (False, True, None, 0x04, 'Now you can\ndefend against\npebbles!', 'and the stone blocker', 'shield-wielding kid', 'shield for sale', 'fungus for shield', 'shield boy defends again'),
'Red Shield': (False, True, None, 0x05, 'Now you can\ndefend against\nfireballs!', 'and the shot blocker', 'shield-wielding kid', 'fire shield for sale', 'fungus for fire shield', 'shield boy defends again'),
'Mirror Shield': (True, False, None, 0x06, 'Now you can\ndefend against\nlasers!', 'and the laser blocker', 'shield-wielding kid', 'face shield for sale', 'fungus for face shield', 'shield boy defends again'),
'Progressive Shield': (True, False, None, 0x5F, 'have a better\nblocker in\nfront of you', 'and the new shield', 'shield-wielding kid', 'shield for sale', 'fungus for shield', 'shield boy defends again'),
'Bug Catching Net': (True, False, None, 0x21, 'Let\'s catch\nsome bees and\nfaeries!', 'and the bee catcher', 'the bug-catching kid', 'stick web for sale', 'fungus for butterflies', 'wrong boy catches bees again'),
'Cane of Byrna': (True, False, None, 0x18, 'Use this to\nbecome\ninvincible!', 'and the bad cane', 'the spark-making kid', 'spark stick for sale', 'spark-stick for trade', 'cane boy encircles again'),
'Boss Heart Container': (False, False, None, 0x3E, 'Maximum health\nincreased!\nYeah!', 'and the full heart', 'the life-giving kid', 'love for sale', 'fungus for life', 'life boy feels love again'),
'Sanctuary Heart Container': (False, False, None, 0x3F, 'Maximum health\nincreased!\nYeah!', 'and the full heart', 'the life-giving kid', 'love for sale', 'fungus for life', 'life boy feels love again'),
'Piece of Heart': (False, False, None, 0x17, 'Just a little\npiece of love!', 'and the broken heart', 'the life-giving kid', 'little love for sale', 'fungus for life', 'life boy feels some love again"'),
'Rupee (1)': (False, False, None, 0x34, 'Just pocket\nchange. Move\nright along.', 'the pocket change', 'poverty-struck kid', 'life lesson for sale', 'buying cheap drugs', 'destitute boy has snack again'),
'Rupees (5)': (False, False, None, 0x35, 'Just pocket\nchange. Move\nright along.', 'the pocket change', 'poverty-struck kid', 'life lesson for sale', 'buying cheap drugs', 'destitute boy has snack again'),
'Rupees (20)': (False, False, None, 0x36, 'Just couch\ncash. Move\nright along.', 'and the couch cash', 'the piggy-bank kid', 'life lesson for sale', 'the witch buying drugs', 'destitute boy has lunch again'),
'Rupees (50)': (False, False, None, 0x41, 'A rupee pile!\nOkay?', 'and the rupee pile', 'the well-off kid', 'life lesson for sale', 'buying okay drugs', 'destitute boy has dinner again'),
'Rupees (100)': (False, False, None, 0x40, 'A rupee stash!\nHell yeah!', 'and the rupee stash', 'the kind-of-rich kid', 'life lesson for sale', 'buying good drugs', 'affluent boy goes drinking again'),
'Rupees (300)': (False, False, None, 0x46, 'A rupee hoard!\nHell yeah!', 'and the rupee hoard', 'the really-rich kid', 'life lesson for sale', 'buying the best drugs', 'fat-cat boy is rich again'),
'Rupoor': (False, False, None, 0x59, 'a debt collector', 'and the toll-booth', 'the toll-booth kid', 'double loss for sale', 'witch stole your rupees', 'affluent boy steals rupees'),
'Red Clock': (False, True, None, 0x5B, 'a waste of time', 'the ruby clock', 'the ruby-time kid', 'red time for sale', 'for ruby time', 'moment boy travels time again'),
'Blue Clock': (False, True, None, 0x5C, 'a bit of time', 'the sapphire clock', 'sapphire-time kid', 'blue time for sale', 'for sapphire time', 'moment boy time travels again'),
'Green Clock': (False, True, None, 0x5D, 'a lot of time', 'the emerald clock', 'the emerald-time kid', 'green time for sale', 'for emerald time', 'moment boy adjusts time again'),
'Single RNG': (False, True, None, 0x62, 'something you don\'t yet have', None, None, None, None, 'unknown boy somethings again'),
'Multi RNG': (False, True, None, 0x63, 'something you may already have', None, None, None, None, 'unknown boy somethings again'),
'Magic Upgrade (1/2)': (True, False, None, 0x4E, 'Your magic\npower has been\ndoubled!', 'and the spell power', 'the magic-saving kid', 'wizardry for sale', 'mekalekahi mekahiney ho', 'magic boy saves magic again'), # can be required to beat mothula in an open seed in very very rare circumstance
'Magic Upgrade (1/4)': (True, False, None, 0x4F, 'Your magic\npower has been\nquadrupled!', 'and the spell power', 'the magic-saving kid', 'wizardry for sale', 'mekalekahi mekahiney ho', 'magic boy saves magic again'), # can be required to beat mothula in an open seed in very very rare circumstance
'Small Key (Eastern Palace)': (False, False, 'SmallKey', 0xA2, 'A small key to Armos Knights', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again'),
'Big Key (Eastern Palace)': (False, False, 'BigKey', 0x9D, 'A big key to Armos Knights', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again'),
'Compass (Eastern Palace)': (False, True, 'Compass', 0x8D, 'Now you can find the Armos Knights!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again'),
'Map (Eastern Palace)': (False, True, 'Map', 0x7D, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again'),
'Small Key (Desert Palace)': (False, False, 'SmallKey', 0xA3, 'A small key to the desert', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again'),
'Big Key (Desert Palace)': (False, False, 'BigKey', 0x9C, 'A big key to the desert', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again'),
'Compass (Desert Palace)': (False, True, 'Compass', 0x8C, 'Now you can find Lanmolas!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again'),
'Map (Desert Palace)': (False, True, 'Map', 0x7C, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again'),
'Small Key (Tower of Hera)': (False, False, 'SmallKey', 0xAA, 'A small key to Hera', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again'),
'Big Key (Tower of Hera)': (False, False, 'BigKey', 0x95, 'A big key to Hera', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again'),
'Compass (Tower of Hera)': (False, True, 'Compass', 0x85, 'Now you can find Moldorm!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again'),
'Map (Tower of Hera)': (False, True, 'Map', 0x75, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again'),
'Small Key (Escape)': (False, False, 'SmallKey', 0xA0, 'A small key to the castle', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again'),
'Big Key (Escape)': (False, False, 'BigKey', 0x9F, 'A big key to the castle', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again'),
'Compass (Escape)': (False, True, 'Compass', 0x8F, 'Now you can find no boss!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again'),
'Map (Escape)': (False, True, 'Map', 0x7F, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again'),
'Small Key (Agahnims Tower)': (False, False, 'SmallKey', 0xA4, 'A small key to Agahnim', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again'),
'Small Key (Palace of Darkness)': (False, False, 'SmallKey', 0xA6, 'A small key to darkness', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again'),
'Big Key (Palace of Darkness)': (False, False, 'BigKey', 0x99, 'A big key to darkness', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again'),
'Compass (Palace of Darkness)': (False, True, 'Compass', 0x89, 'Now you can find Helmasaur King!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again'),
'Map (Palace of Darkness)': (False, True, 'Map', 0x79, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again'),
'Small Key (Thieves Town)': (False, False, 'SmallKey', 0xAB, 'A small key to thievery', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again'),
'Big Key (Thieves Town)': (False, False, 'BigKey', 0x94, 'A big key to thievery', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again'),
'Compass (Thieves Town)': (False, True, 'Compass', 0x84, 'Now you can find Blind!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again'),
'Map (Thieves Town)': (False, True, 'Map', 0x74, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again'),
'Small Key (Skull Woods)': (False, False, 'SmallKey', 0xA8, 'A small key to the woods', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again'),
'Big Key (Skull Woods)': (False, False, 'BigKey', 0x97, 'A big key to the woods', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again'),
'Compass (Skull Woods)': (False, True, 'Compass', 0x87, 'Now you can find Mothula!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again'),
'Map (Skull Woods)': (False, True, 'Map', 0x77, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again'),
'Small Key (Swamp Palace)': (False, False, 'SmallKey', 0xA5, 'A small key to the swamp', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again'),
'Big Key (Swamp Palace)': (False, False, 'BigKey', 0x9A, 'A big key to the swamp', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again'),
'Compass (Swamp Palace)': (False, True, 'Compass', 0x8A, 'Now you can find Arrghus!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again'),
'Map (Swamp Palace)': (False, True, 'Map', 0x7A, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again'),
'Small Key (Ice Palace)': (False, False, 'SmallKey', 0xA9, 'A small key to the iceberg', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again'),
'Big Key (Ice Palace)': (False, False, 'BigKey', 0x96, 'A big key to the iceberg', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again'),
'Compass (Ice Palace)': (False, True, 'Compass', 0x86, 'Now you can find Kholdstare!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again'),
'Map (Ice Palace)': (False, True, 'Map', 0x76, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again'),
'Small Key (Misery Mire)': (False, False, 'SmallKey', 0xA7, 'A small key to the mire', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again'),
'Big Key (Misery Mire)': (False, False, 'BigKey', 0x98, 'A big key to the mire', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again'),
'Compass (Misery Mire)': (False, True, 'Compass', 0x88, 'Now you can find Vitreous!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again'),
'Map (Misery Mire)': (False, True, 'Map', 0x78, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again'),
'Small Key (Turtle Rock)': (False, False, 'SmallKey', 0xAC, 'A small key to the pipe maze', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again'),
'Big Key (Turtle Rock)': (False, False, 'BigKey', 0x93, 'A big key to the pipe maze', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again'),
'Compass (Turtle Rock)': (False, True, 'Compass', 0x83, 'Now you can find Trinexx!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again'),
'Map (Turtle Rock)': (False, True, 'Map', 0x73, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again'),
'Small Key (Ganons Tower)': (False, False, 'SmallKey', 0xAD, 'A small key to the evil tower', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again'),
'Big Key (Ganons Tower)': (False, False, 'BigKey', 0x92, 'A big key to the evil tower', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again'),
'Compass (Ganons Tower)': (False, True, 'Compass', 0x82, 'Now you can find Agahnim!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again'),
'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'),
'Nothing': (False, False, None, 0x5A, 'Some Hot Air', 'and the Nothing', 'the zen kid', 'outright theft', 'shroom theft', 'empty boy is bored again'),
'Beat Agahnim 1': (True, False, 'Event', None, None, None, None, None, None, None),
'Beat Agahnim 2': (True, False, 'Event', None, None, None, None, None, None, None)}

135
Main.py
View File

@ -1,29 +1,38 @@
from BaseClasses import World, CollectionState, Item
from Regions import create_regions
from EntranceShuffle import link_entrances
from Rom import patch_rom, LocalRom, JsonRom
from Rules import set_rules
from Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive
from Items import ItemFactory
from Fill import distribute_items_cutoff, distribute_items_staleness, distribute_items_restrictive, fill_restrictive, flood_items
from collections import OrderedDict
from ItemList import generate_itempool
from Utils import output_path
from itertools import zip_longest
import json
import logging
import random
import time
import logging
import json
__version__ = '0.5.1-dev'
from BaseClasses import World, CollectionState, Item
from Regions import create_regions, mark_light_world_regions
from EntranceShuffle import link_entrances
from Rom import patch_rom, 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
from ItemList import generate_itempool, difficulties
from Utils import output_path
logic_hash = [117, 227, 77, 12, 94, 219, 67, 70, 58, 42, 7, 75, 132, 55, 130, 97, 235, 46, 206, 185, 243, 64, 109, 161, 107, 91, 224, 142, 25, 84, 4, 78,
160, 245, 143, 18, 251, 114, 165, 157, 13, 26, 119, 92, 188, 216, 27, 39, 76, 238, 152, 113, 231, 193, 191, 103, 118, 182, 213, 134, 41, 90, 246, 82,
57, 225, 150, 139, 99, 151, 184, 11, 85, 209, 144, 147, 47, 56, 129, 247, 121, 177, 79, 1, 215, 207, 126, 136, 105, 100, 180, 5, 2, 14, 153, 6,
163, 192, 198, 88, 98, 174, 149, 201, 249, 200, 158, 116, 196, 80, 220, 31, 111, 214, 194, 248, 221, 167, 250, 115, 38, 10, 32, 218, 133, 19, 253, 122,
239, 16, 52, 48, 156, 205, 127, 3, 138, 237, 234, 190, 37, 112, 189, 86, 223, 236, 195, 54, 71, 181, 43, 49, 226, 255, 0, 135, 186, 203, 175, 87,
21, 229, 120, 124, 145, 171, 252, 155, 22, 62, 199, 51, 35, 179, 159, 44, 69, 30, 172, 242, 140, 74, 9, 83, 183, 93, 202, 137, 108, 241, 173, 23,
164, 45, 222, 232, 166, 176, 230, 63, 154, 96, 170, 34, 66, 50, 17, 211, 95, 53, 208, 244, 36, 123, 81, 187, 106, 131, 169, 29, 104, 72, 101, 141,
68, 24, 168, 125, 217, 240, 15, 162, 148, 8, 40, 102, 33, 89, 128, 61, 210, 204, 73, 228, 59, 146, 28, 110, 233, 178, 254, 65, 197, 20, 212, 60]
__version__ = '0.5.2-dev'
logic_hash = [84, 16, 141, 33, 92, 37, 243, 181, 99, 63, 34, 196, 242, 156, 170, 54,
249, 188, 62, 89, 236, 131, 109, 118, 24, 116, 239, 48, 227, 30, 126, 43,
160, 20, 12, 130, 201, 166, 23, 76, 195, 21, 238, 145, 228, 178, 229, 47,
230, 32, 207, 191, 204, 152, 240, 64, 124, 215, 55, 245, 176, 150, 198, 19,
67, 151, 222, 175, 127, 246, 26, 162, 112, 173, 205, 83, 86, 121, 79, 75,
185, 102, 157, 85, 69, 4, 125, 211, 115, 155, 78, 220, 45, 169, 143, 18,
31, 187, 184, 50, 154, 193, 57, 72, 182, 119, 90, 216, 101, 194, 49, 107,
15, 98, 117, 226, 241, 134, 167, 29, 17, 68, 234, 11, 66, 28, 172, 53,
97, 183, 250, 14, 221, 110, 104, 203, 200, 153, 137, 128, 105, 6, 8, 142,
88, 7, 0, 212, 46, 251, 135, 147, 95, 202, 93, 189, 114, 225, 25, 161,
158, 218, 210, 174, 146, 214, 252, 74, 164, 237, 35, 165, 70, 171, 65, 219,
27, 38, 224, 51, 248, 82, 122, 148, 73, 60, 168, 247, 163, 41, 44, 133,
209, 139, 2, 106, 22, 80, 81, 180, 103, 9, 235, 213, 138, 94, 208, 132,
77, 61, 254, 96, 186, 253, 217, 199, 42, 40, 223, 177, 3, 159, 255, 113,
58, 129, 197, 91, 52, 233, 1, 144, 59, 123, 87, 149, 140, 231, 5, 10,
36, 111, 136, 232, 108, 192, 39, 56, 13, 120, 71, 190, 179, 206, 244, 100]
def main(args, seed=None):
@ -39,7 +48,9 @@ def main(args, seed=None):
world.seed = int(seed)
random.seed(world.seed)
logger.info('ALttP Entrance Randomizer Version %s - Seed: %s\n\n' % (__version__, world.seed))
logger.info('ALttP Entrance Randomizer Version %s - Seed: %s\n\n', __version__, world.seed)
world.difficulty_requirements = difficulties[world.difficulty]
create_regions(world)
@ -48,6 +59,7 @@ def main(args, seed=None):
logger.info('Shuffling the World about.')
link_entrances(world)
mark_light_world_regions(world)
logger.info('Calculating Access Rules.')
@ -60,7 +72,7 @@ def main(args, seed=None):
logger.info('Placing Dungeon Items.')
shuffled_locations = None
if args.algorithm == 'vt26' or args.keysanity:
if args.algorithm in ['balanced', 'vt26'] or args.keysanity:
shuffled_locations = world.get_unfilled_locations()
random.shuffle(shuffled_locations)
fill_dungeons_restrictive(world, shuffled_locations)
@ -91,11 +103,14 @@ def main(args, seed=None):
logger.info('Patching ROM.')
if args.sprite is not None:
sprite = bytearray(open(args.sprite, 'rb').read())
if isinstance(args.sprite, Sprite):
sprite = args.sprite
else:
sprite = Sprite(args.sprite)
else:
sprite = None
outfilebase = 'ER_%s_%s-%s-%s%s_%s-%s%s%s%s%s%s_%s' % (world.logic, world.difficulty, world.mode, world.goal, "" if world.timer in ['none', 'display'] else "-" + world.timer, world.shuffle, world.algorithm, "-keysanity" if world.keysanity else "", "-progressives_" + world.progressive if world.progressive in ['off', 'random'] else "", "-fastmenu" if world.fastmenu else "", "-quickswap" if world.quickswap else "", "-shuffleganon" if world.shuffle_ganon else "", world.seed)
outfilebase = 'ER_%s_%s-%s-%s%s_%s-%s%s%s%s_%s' % (world.logic, world.difficulty, world.mode, world.goal, "" if world.timer in ['none', 'display'] else "-" + world.timer, world.shuffle, world.algorithm, "-keysanity" if world.keysanity else "", "-prog_" + world.progressive if world.progressive in ['off', 'random'] else "", "-shuffleganon" if world.shuffle_ganon else "", world.seed)
if not args.suppress_rom:
if args.jsonout:
@ -112,7 +127,7 @@ def main(args, seed=None):
world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase))
logger.info('Done. Enjoy.')
logger.debug('Total Time: %s' % (time.clock() - start))
logger.debug('Total Time: %s', time.clock() - start)
return world
@ -129,13 +144,17 @@ def copy_world(world):
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_take_damage = world.can_take_damage
ret.difficulty_requirements = world.difficulty_requirements
create_regions(ret)
create_dungeons(ret)
# connect copied world
for region in world.regions:
copied_region = ret.get_region(region.name)
copied_region.is_light_world = region.is_light_world
for entrance in region.entrances:
ret.get_entrance(entrance.name).connect(ret.get_region(region.name))
ret.get_entrance(entrance.name).connect(copied_region)
# fill locations
for location in world.get_locations():
@ -171,8 +190,8 @@ def create_playthrough(world):
raise RuntimeError('Cannot beat game. Something went terribly wrong here!')
# get locations containing progress items
prog_locations = [location for location in world.get_locations() if location.item is not None and (location.item.advancement or (location.item.key and world.keysanity))]
prog_locations = [location for location in world.get_filled_locations() if location.item.advancement]
state_cache = [None]
collection_spheres = []
state = CollectionState(world)
sphere_candidates = list(prog_locations)
@ -189,30 +208,30 @@ def create_playthrough(world):
for location in sphere:
sphere_candidates.remove(location)
state.collect(location.item, True)
state.collect(location.item, True, location)
collection_spheres.append(sphere)
logging.getLogger('').debug('Calculated sphere %i, containing %i of %i progress items.' % (len(collection_spheres), len(sphere), len(prog_locations)))
state_cache.append(state.copy())
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 at %s' % (location.item.name, location.name) for location in sphere_candidates])
logging.getLogger('').debug('The following items could not be reached: %s', ['%s at %s' % (location.item.name, location.name) for location in sphere_candidates])
if not world.check_beatable_only:
raise RuntimeError('Not all progression items reachable. Something went terribly wrong here.')
else:
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
for sphere in reversed(collection_spheres):
for num, sphere in reversed(list(enumerate(collection_spheres))):
to_delete = []
for location in sphere:
# we remove the item at location and check if game is still beatable
logging.getLogger('').debug('Checking if %s is required to beat the game.' % location.item.name)
logging.getLogger('').debug('Checking if %s is required to beat the game.', location.item.name)
old_item = location.item
location.item = None
state.remove(old_item)
world._item_cache = {} # need to invalidate
if world.can_beat_game():
if world.can_beat_game(state_cache[num]):
to_delete.append(location)
else:
# still required, got to keep it around
@ -222,11 +241,51 @@ def create_playthrough(world):
for location in to_delete:
sphere.remove(location)
# we are now down to just the required progress items in collection_spheres in a minimum number of spheres. As a cleanup, we right trim empty spheres (can happen if we have multiple triforces)
collection_spheres = [sphere for sphere in collection_spheres if sphere]
# we are now down to just the required progress items in collection_spheres. Unfortunately
# the previous pruning stage could potentially have made certain items dependant on others
# in the same or later sphere (because the location had 2 ways to access but the item originally
# used to access it was deemed not required.) So we need to do one final sphere collection pass
# to build up the correct spheres
required_locations = [item for sphere in collection_spheres for item in sphere]
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))
for location in sphere:
required_locations.remove(location)
state.collect(location.item, True, location)
collection_spheres.append(sphere)
logging.getLogger('').debug('Calculated final sphere %i, containing %i of %i progress items.', len(collection_spheres), len(sphere), len(required_locations))
if not sphere:
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.')
# store the required locations for statistical analysis
old_world.required_locations = [location.name for sphere in collection_spheres for location in sphere]
def flist_to_iter(node):
while node:
value, node = node
yield value
def get_path(state, region):
reversed_path_as_flist = state.path.get(region, (region, None))
string_path_flat = reversed(list(map(str, flist_to_iter(reversed_path_as_flist))))
# Now we combine the flat string list into (region, exit) pairs
pathsiter = iter(string_path_flat)
pathpairs = zip_longest(pathsiter, pathsiter)
return list(pathpairs)
old_world.spoiler.paths = {location.name : get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere}
if any(exit == 'Pyramid Fairy' for path in old_world.spoiler.paths.values() for (_, exit) in path):
old_world.spoiler.paths['Big Bomb Shop'] = get_path(state, world.get_region('Big Bomb Shop'))
print(world.seed)
# we can finally output our playthrough
old_world.spoiler.playthrough = OrderedDict([(str(i + 1), {str(location): str(location.item) for location in sphere}) for i, sphere in enumerate(collection_spheres)])

48
Plando.py Normal file → Executable file
View File

@ -1,18 +1,21 @@
#!/usr/bin/env python3
import argparse
import hashlib
import logging
import os
import random
import time
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, write_string_to_rom
from Rom import patch_rom, LocalRom, Sprite, write_string_to_rom
from Rules import set_rules
from Dungeons import create_dungeons
from Items import ItemFactory
from ItemList import difficulties
from Main import create_playthrough
import random
import time
import logging
import argparse
import os
import hashlib
import sys
__version__ = '0.2-dev'
@ -26,8 +29,8 @@ logic_hash = [182, 244, 144, 92, 149, 200, 93, 183, 124, 169, 226, 46, 111, 163,
153, 217, 252, 158, 25, 205, 22, 133, 254, 138, 203, 118, 210, 204, 82, 97, 52, 164, 68, 139, 120, 109, 54, 3, 41, 179, 212, 42]
def main(args, seed=None):
start = time.clock()
def main(args):
start_time = time.clock()
# initialize the world
world = World('vanilla', 'noglitches', 'standard', 'normal', 'none', 'on', 'ganon', 'freshness', False, False, False, args.quickswap, args.fastmenu, args.disablemusic, False)
@ -41,7 +44,9 @@ def main(args, seed=None):
random.seed(world.seed)
logger.info('ALttP Plandomizer Version %s - Seed: %s\n\n' % (__version__, args.plando))
logger.info('ALttP Plandomizer Version %s - Seed: %s\n\n', __version__, args.plando)
world.difficulty_requirements = difficulties[world.difficulty]
create_regions(world)
create_dungeons(world)
@ -74,7 +79,7 @@ def main(args, seed=None):
logger.info('Patching ROM.')
if args.sprite is not None:
sprite = bytearray(open(args.sprite, 'rb').read())
sprite = Sprite(args.sprite)
else:
sprite = None
@ -84,8 +89,8 @@ def main(args, seed=None):
for textname, texttype, text in text_patches:
if texttype == 'text':
write_string_to_rom(rom, textname, text)
elif texttype == 'credit':
write_credits_string_to_rom(rom, textname, text)
#elif texttype == 'credit':
# write_credits_string_to_rom(rom, textname, text)
outfilebase = 'Plando_%s_%s' % (os.path.splitext(os.path.basename(args.plando))[0], world.seed)
@ -94,7 +99,7 @@ def main(args, seed=None):
world.spoiler.to_file('%s_Spoiler.txt' % outfilebase)
logger.info('Done. Enjoy.')
logger.debug('Total Time: %s' % (time.clock() - start))
logger.debug('Total Time: %s', time.clock() - start_time)
return world
@ -171,7 +176,7 @@ def fill_world(world, plando, text_patches):
locationstr, itemstr = line.split(':', 1)
location = world.get_location(locationstr.strip())
if location is None:
logger.warn('Unknown location: %s' % locationstr)
logger.warning('Unknown location: %s', locationstr)
continue
else:
item = ItemFactory(itemstr.strip())
@ -198,14 +203,18 @@ def fill_world(world, plando, text_patches):
world.get_location('Agahnim 2').item = ItemFactory('Beat Agahnim 2')
if __name__ == '__main__':
def start():
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('--create_spoiler', help='Output a Spoiler File', action='store_true')
parser.add_argument('--ignore_unsolvable', help='Do not abort if seed is deemed unsolvable.', action='store_true')
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('--seed', help='Define seed number to generate.', type=int)
parser.add_argument('--fastmenu', help='Enable instant menu', action='store_true')
parser.add_argument('--fastmenu', default='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('--heartbeep', default='normal', const='normal', nargs='?', choices=['normal', 'half', 'quarter', 'off'],
@ -230,3 +239,6 @@ if __name__ == '__main__':
logging.basicConfig(format='%(message)s', level=loglevel)
main(args=args)
if __name__ == '__main__':
start()

View File

@ -239,6 +239,10 @@ The dungeon variants only mix up dungeons and keep the rest of the overworld van
Select frequency of beeps when on low health. Can completely disable them.
## Menu Speed
A setting that lets the player set the rate at which the menu opens and closes.
## Create Spoiler Log
Output a Spoiler File.
@ -252,10 +256,6 @@ generate spoilers for statistical analysis.
Use to enable quick item swap with L/R buttons
## Instant Menu
As an alternative to quickswap, opens menu instantly.
## Keysanity
This setting allows dungeon specific items (Small Key, Big Key, Map, Compass) to be distributed anywhere in the world and not just
@ -378,10 +378,10 @@ Set the count option (default: None)
Use to enable quick item swap with L/R buttons. (default: False)
```
--fastmenu
--fastmenu [{normal,instant,double,triple,quadruple,half}]
```
As an alternative to quickswap, opens menu instantly. (default: False)
Alters the rate at which the menu opens and closes. (default: normal)
```

View File

@ -1,3 +1,4 @@
import collections
from BaseClasses import Region, Location, Entrance
@ -12,7 +13,7 @@ def create_regions(world):
'Bonk Rock Cave', 'Library', 'Potion Shop', 'Two Brothers House (East)', 'Desert Palace Stairs', 'Eastern Palace', 'Master Sword Meadow',
'Sanctuary', 'Sanctuary Grave', 'Old Man Cave (West)', 'Flute Spot 1', 'Dark Desert Teleporter', 'East Hyrule Teleporter', 'South Hyrule Teleporter', 'Kakariko Teleporter',
'Elder House (East)', 'Elder House (West)', 'North Fairy Cave', 'North Fairy Cave Drop', 'Lost Woods Gamble', 'Snitch Lady (East)', 'Snitch Lady (West)', 'Tavern (Front)',
'Bush Covered House', 'Light World Bomb Hut', 'Kakariko Shop', 'Long Fairy Cave', 'Good Bee Cave', '20 Rupee Cave', 'Cave Shop (Lake Hylia)', 'Waterfall of Wishing',
'Bush Covered House', 'Light World Bomb Hut', 'Kakariko Shop', 'Long Fairy Cave', 'Good Bee Cave', '20 Rupee Cave', 'Cave Shop (Lake Hylia)', 'Waterfall of Wishing', 'Hyrule Castle Main Gate',
'Bonk Fairy (Light)', '50 Rupee Cave', 'Fortune Teller (Light)', 'Lake Hylia Fairy', 'Swamp Fairy', 'Desert Fairy', 'Lumberjack House', 'Lake Hylia Fortune Teller', 'Kakariko Gamble Game', 'Top of Pyramid']),
create_region('Lake Hylia Central Island', None, ['Capacity Upgrade', 'Lake Hylia Central Island Teleporter']),
create_region('Blinds Hideout', ["Blind\'s Hideout - Top",
@ -133,7 +134,7 @@ def create_regions(world):
create_region('Tower of Hera (Top)', ['Tower of Hera - Compass Chest', 'Tower of Hera - Big Chest', 'Tower of Hera - Moldorm', 'Tower of Hera - Prize']),
create_region('East Dark World', ['Pyramid', 'Catfish'], ['Pyramid Fairy', 'South Dark World Bridge', 'West Dark World Gap', 'Palace of Darkness', 'Dark Lake Hylia Drop (East)', 'Dark Lake Hylia Teleporter',
'Hyrule Castle Ledge Mirror Spot', 'Dark Lake Hylia Fairy', 'Palace of Darkness Hint', 'East Dark World Hint', 'Dark World Potion Shop', 'Pyramid Hole']),
'Hyrule Castle Ledge Mirror Spot', 'Dark Lake Hylia Fairy', 'Palace of Darkness Hint', 'East Dark World Hint', 'Dark World Potion Shop', 'Pyramid Hole']),
create_region('Palace of Darkness Hint'),
create_region('East Dark World Hint'),
create_region('South Dark World', ['Stumpy', 'Digging Game', 'Bombos Tablet'], ['Dark Lake Hylia Drop (South)', 'Hype Cave', 'Swamp Palace', 'Village of Outcasts Heavy Rock',
@ -146,7 +147,7 @@ def create_regions(world):
create_region('Dark Lake Hylia Ledge Hint'),
create_region('Dark Lake Hylia Ledge Spike Cave'),
create_region('Hype Cave', ['Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left',
'Hype Cave - Bottom', 'Hype Cave - Generous Guy']),
'Hype Cave - Bottom', 'Hype Cave - Generous Guy']),
create_region('West Dark World', None, ['Village of Outcasts Drop', 'East Dark World River Pier', 'Brewery', 'C-Shaped House', 'Chest Game', 'Thieves Town', 'Graveyard Cave', 'Bumper Cave (Bottom)', 'Skull Woods Forest',
'Bat Cave Drop Ledge Mirror Spot', 'Dark World Hammer Peg Cave', 'Red Shield Shop', 'Dark Sanctuary Hint', 'Fortune Teller (Dark)', 'Dark World Shop', 'Dark World Lumberjack Shop']),
create_region('Fortune Teller (Dark)'),
@ -178,7 +179,8 @@ def create_regions(world):
create_region('Spike Cave', ['Spike Cave']),
create_region('Hookshot Cave', ['Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left'],
['Hookshot Cave Exit (South)', 'Hookshot Cave Exit (North)']),
create_region('Death Mountain Floating Island', ['Floating Island'], ['Floating Island Drop', 'Hookshot Cave Back Entrance']),
create_region('Death Mountain Floating Island (Dark World)', None, ['Floating Island Drop', 'Hookshot Cave Back Entrance', 'Floating Island Mirror Spot']),
create_region('Death Mountain Floating Island (Light World)', ['Floating Island']),
create_region('Turtle Rock (Top)', None, ['Turtle Rock Drop']),
create_region('Mimic Cave', ['Mimic Cave']),
@ -201,6 +203,7 @@ def create_regions(world):
create_region('Skull Woods First Section (Right)', ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']),
create_region('Skull Woods First Section (Left)', ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'], ['Skull Woods First Section (Left) Door to Exit', 'Skull Woods First Section (Left) Door to Right']),
create_region('Skull Woods First Section (Top)', ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']),
create_region('Skull Woods Second Section (Drop)', None, ['Skull Woods Second Section (Drop)']),
create_region('Skull Woods Second Section', ['Skull Woods - Big Key Chest'], ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']),
create_region('Skull Woods Final Section (Entrance)', ['Skull Woods - Bridge Room'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']),
create_region('Skull Woods Final Section (Mothula)', ['Skull Woods - Mothula', 'Skull Woods - Prize']),
@ -282,6 +285,27 @@ def create_region(name, locations=None, exits=None):
return ret
def mark_light_world_regions(world):
# Note that in "inanity" shuffle this code may mark some dark world locations as being in light world. That is fine because this flag
# is only used for bunny logic, and you start with a Moon pearl immediately availible in Insanity shuffle.
# Exclude entrances that represent connections from the light world to the dark world
excluded_entrances = set(['Top of Pyramid', 'Lake Hylia Central Island Teleporter', 'Dark Desert Teleporter', 'East Hyrule Teleporter', 'South Hyrule Teleporter', 'Kakariko Teleporter', 'Death Mountain Teleporter', 'East Death Mountain Teleporter', 'Turtle Rock Teleporter'])
starting_regions = ['Links House', 'Cave 45', 'Graveyard Cave', 'Mimic Cave', 'Death Mountain Floating Island (Light World)', 'Desert Ledge (West)', 'Lake Hylia Island', 'Spectacle Rock']
queue = collections.deque([world.get_region(region) for region in starting_regions])
seen = set(queue)
while queue:
current = queue.popleft()
current.is_light_world = True
for exit in current.exits:
if exit.name in excluded_entrances:
continue
if exit.connected_region not in seen:
seen.add(exit.connected_region)
queue.append(exit.connected_region)
location_table = {'Mushroom': (0x180013, False, 'in the woods'),
'Bottle Merchant': (0x2EB18, False, 'with a merchant'),
'Flute Spot': (0x18014A, False, 'underground'),

518
Rom.py
View File

@ -1,15 +1,21 @@
from Dungeons import dungeon_music_addresses
from Text import string_to_alttp_text, text_addresses, Credits
from Text import Uncle_texts, Ganon1_texts, PyramidFairy_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, Blind_texts, BombShop2_texts
from Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, DeathMountain_texts, LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts
from Utils import local_path
import random
import io
import json
import hashlib
import logging
import os
import struct
import random
from Dungeons import dungeon_music_addresses
from Text import string_to_alttp_text, text_addresses, Credits
from Text import Uncle_texts, Ganon1_texts, PyramidFairy_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, Blind_texts, BombShop2_texts
from Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, DeathMountain_texts, LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names
from Utils import local_path
from Items import ItemFactory
JAP10HASH = '03a63945398191337e896e5771f77173'
RANDOMIZERBASEHASH = '1deebb05eccefd2ab68297c6e9c0d25f'
RANDOMIZERBASEHASH = '214e4b2a50cb65cd13a8194bc88cb030'
class JsonRom(object):
@ -30,14 +36,17 @@ class JsonRom(object):
self.write_bytes(address, int32_as_bytes(value))
def write_to_file(self, file):
json.dump([self.patches], open(file, 'w'))
with open(file, 'w') as stream:
json.dump([self.patches], stream)
class LocalRom(object):
def __init__(self, file):
self.buffer = bytearray(open(file, 'rb').read())
self.patch_base_rom()
def __init__(self, file, patch=True):
with open(file, 'rb') as stream:
self.buffer = bytearray(stream.read())
if patch:
self.patch_base_rom()
def write_byte(self, address, value):
self.buffer[address] = value
@ -60,14 +69,15 @@ class LocalRom(object):
# verify correct checksum of baserom
basemd5 = hashlib.md5()
basemd5.update(self.buffer)
if not JAP10HASH == basemd5.hexdigest():
if JAP10HASH != basemd5.hexdigest():
logging.getLogger('').warning('Supplied Base Rom does not match known MD5 for JAP(1.0) release. Will try to patch anyway.')
# extend to 2MB
self.buffer.extend(bytearray([0x00] * (2097152 - len(self.buffer))))
# load randomizer patches
patches = json.load(open(local_path('data/base2current.json'), 'r'))
with open(local_path('data/base2current.json'), 'r') as stream:
patches = json.load(stream)
for patch in patches:
if isinstance(patch, dict):
for baseaddress, values in patch.items():
@ -76,7 +86,7 @@ class LocalRom(object):
# verify md5
patchedmd5 = hashlib.md5()
patchedmd5.update(self.buffer)
if not RANDOMIZERBASEHASH == patchedmd5.hexdigest():
if RANDOMIZERBASEHASH != patchedmd5.hexdigest():
raise RuntimeError('Provided Base Rom unsuitable for patching. Please provide a JAP(1.0) "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc" rom to use as a base.')
def write_crc(self):
@ -84,6 +94,165 @@ class LocalRom(object):
inv = crc ^ 0xFFFF
self.write_bytes(0x7FDC, [inv & 0xFF, (inv >> 8) & 0xFF, crc & 0xFF, (crc >> 8) & 0xFF])
class Sprite(object):
default_palette = [255, 127, 126, 35, 183, 17, 158, 54, 165, 20, 255, 1, 120, 16, 157,
89, 71, 54, 104, 59, 74, 10, 239, 18, 92, 42, 113, 21, 24, 122,
255, 127, 126, 35, 183, 17, 158, 54, 165, 20, 255, 1, 120, 16, 157,
89, 128, 105, 145, 118, 184, 38, 127, 67, 92, 42, 153, 17, 24, 122,
255, 127, 126, 35, 183, 17, 158, 54, 165, 20, 255, 1, 120, 16, 157,
89, 87, 16, 126, 69, 243, 109, 185, 126, 92, 42, 39, 34, 24, 122,
255, 127, 126, 35, 218, 17, 158, 54, 165, 20, 255, 1, 120, 16, 151,
61, 71, 54, 104, 59, 74, 10, 239, 18, 126, 86, 114, 24, 24, 122]
default_glove_palette = [246, 82, 118, 3]
def __init__(self, filename):
with open(filename, 'rb') as file:
filedata = bytearray(file.read())
self.name = os.path.basename(filename)
self.author_name = None
self.valid = True
if len(filedata) == 0x7000:
# sprite file with graphics and without palette data
self.sprite = filedata[:0x7000]
self.palette = list(self.default_palette)
self.glove_palette = list(self.default_glove_palette)
elif len(filedata) == 0x7078:
# sprite file with graphics and palette data
self.sprite = filedata[:0x7000]
self.palette = filedata[0x7000:]
self.glove_palette = filedata[0x7036:0x7038] + filedata[0x7054:0x7056]
elif len(filedata) == 0x707C:
# sprite file with graphics and palette data including gloves
self.sprite = filedata[:0x7000]
self.palette = filedata[0x7000:0x7078]
self.glove_palette = filedata[0x7078:]
elif len(filedata) in [0x100000, 0x200000]:
# full rom with patched sprite, extract it
self.sprite = filedata[0x80000:0x87000]
self.palette = filedata[0xDD308:0xDD380]
self.glove_palette = filedata[0xDEDF5:0xDEDF9]
elif filedata.startswith(b'ZSPR'):
result = self.parse_zspr(filedata, 1)
if result is None:
self.valid = False
return
(sprite, palette, self.name, self.author_name) = result
if len(sprite) != 0x7000:
self.valid = False
return
self.sprite = sprite
if len(palette) == 0:
self.palette = list(self.default_palette)
self.glove_palette = list(self.default_glove_palette)
elif len(palette) == 0x78:
self.palette = palette
self.glove_palette = list(self.default_glove_palette)
elif len(palette) == 0x7C:
self.palette = palette[:0x78]
self.glove_palette = palette[0x78:]
else:
self.valid = False
else:
self.valid = False
@staticmethod
def default_link_sprite():
return Sprite(local_path('data/default.zspr'))
def decode8(self, pos):
arr = [[0 for _ in range(8)] for _ in range(8)]
for y in range(8):
for x in range(8):
position = 1<<(7-x)
val = 0
if self.sprite[pos+2*y] & position:
val += 1
if self.sprite[pos+2*y+1] & position:
val += 2
if self.sprite[pos+2*y+16] & position:
val += 4
if self.sprite[pos+2*y+17] & position:
val += 8
arr[y][x] = val
return arr
def decode16(self, pos):
arr = [[0 for _ in range(16)] for _ in range(16)]
top_left = self.decode8(pos)
top_right = self.decode8(pos+0x20)
bottom_left = self.decode8(pos+0x200)
bottom_right = self.decode8(pos+0x220)
for x in range(8):
for y in range(8):
arr[y][x] = top_left[y][x]
arr[y][x+8] = top_right[y][x]
arr[y+8][x] = bottom_left[y][x]
arr[y+8][x+8] = bottom_right[y][x]
return arr
def parse_zspr(self, filedata, expected_kind):
logger = logging.getLogger('')
headerstr = "<4xBHHIHIHH6x"
headersize = struct.calcsize(headerstr)
if len(filedata) < headersize:
return None
(version, csum, icsum, sprite_offset, sprite_size, palette_offset, palette_size, kind) = struct.unpack_from(headerstr, filedata)
if version not in [1]:
logger.error('Error parsing ZSPR file: Version %g not supported', version)
return None
if kind != expected_kind:
return None
stream = io.BytesIO(filedata)
stream.seek(headersize)
def read_utf16le(stream):
"Decodes a null-terminated UTF-16_LE string of unknown size from a stream"
raw = bytearray()
while True:
char = stream.read(2)
if char in [b'', b'\x00\x00']:
break
raw += char
return raw.decode('utf-16_le')
sprite_name = read_utf16le(stream)
author_name = read_utf16le(stream)
# Ignoring the Author Rom name for the time being.
real_csum = sum(filedata) % 0x10000
if real_csum != csum or real_csum ^ 0xFFFF != icsum:
logger.warning('ZSPR file has incorrect checksum. It may be corrupted.')
sprite = filedata[sprite_offset:sprite_offset + sprite_size]
palette = filedata[palette_offset:palette_offset + palette_size]
if len(sprite) != sprite_size or len(palette) != palette_size:
logger.error('Error parsing ZSPR file: Unexpected end of file')
return None
return (sprite, palette, sprite_name, author_name)
def decode_palette(self):
"Returns the palettes as an array of arrays of 15 colors"
def array_chunk(arr, size):
return list(zip(*[iter(arr)] * size))
def make_int16(pair):
return pair[1]<<8 | pair[0]
def expand_color(i):
return ((i & 0x1F) * 8, (i>>5 & 0x1F) * 8, (i>>10 & 0x1F) * 8)
raw_palette = self.palette
if raw_palette is None:
raw_palette = Sprite.default_palette
# turn palette data into a list of RGB tuples with 8 bit values
palette_as_colors = [expand_color(make_int16(chnk)) for chnk in array_chunk(raw_palette, 2)]
# split into palettes of 15 colors
return array_chunk(palette_as_colors, 15)
def int16_as_bytes(value):
value = value & 0xFFFF
return [value & 0xFF, (value >> 8) & 0xFF]
@ -181,6 +350,10 @@ def patch_rom(world, rom, hashtable, beep='normal', sprite=None):
rom.write_byte(0x180039, 0x01 if world.light_world_light_cone else 0x00)
rom.write_byte(0x18003A, 0x01 if world.dark_world_light_cone else 0x00)
GREEN_TWENTY_RUPEES = 0x47
TRIFORCE_PIECE = ItemFactory('Triforce Piece').code
GREEN_CLOCK = ItemFactory('Green Clock').code
# handle difficulty
if world.difficulty == 'hard':
# Powdered Fairies Prize
@ -192,11 +365,12 @@ def patch_rom(world, rom, hashtable, beep='normal', sprite=None):
#Cape magic cost
rom.write_bytes(0x3ADA7, [0x02, 0x02, 0x02])
#Byrna residual magic cost
rom.write_bytes(0x3ADA7, [0x08, 0x08, 0x08])
rom.write_bytes(0x45C42, [0x08, 0x08, 0x08])
#Disable catching fairies
rom.write_byte(0x34FD6, 0x80)
#Set overflow items for progressive equipment
rom.write_bytes(0x180090, [0x03, 0x47, 0x02, 0x47, 0x01, 0x47, 0x02, 0x47])
overflow_replacement = GREEN_TWENTY_RUPEES
# Rupoor negative value
rom.write_int16_to_rom(0x180036, 10)
#Make Blue Shield more expensive
rom.write_bytes(0xF73D2, [0xFC, 0xFF])
rom.write_bytes(0xF73DA, [0x04, 0x00])
@ -225,11 +399,12 @@ def patch_rom(world, rom, hashtable, beep='normal', sprite=None):
#Cape magic cost
rom.write_bytes(0x3ADA7, [0x02, 0x02, 0x02])
#Byrna residual magic cost
rom.write_bytes(0x3ADA7, [0x08, 0x08, 0x08])
rom.write_bytes(0x45C42, [0x08, 0x08, 0x08])
#Disable catching fairies
rom.write_byte(0x34FD6, 0x80)
#Set overflow items for progressive equipment
rom.write_bytes(0x180090, [0x02, 0x47, 0x00, 0x47, 0x00, 0x47, 0x01, 0x47])
overflow_replacement = GREEN_TWENTY_RUPEES
# Rupoor negative value
rom.write_int16_to_rom(0x180036, 20)
#Make Blue Shield more expensive
rom.write_bytes(0xF73D2, [0xFC, 0xFF])
rom.write_bytes(0xF73DA, [0x04, 0x00])
@ -258,11 +433,12 @@ def patch_rom(world, rom, hashtable, beep='normal', sprite=None):
#Cape magic cost
rom.write_bytes(0x3ADA7, [0x02, 0x02, 0x02])
#Byrna residual magic cost
rom.write_bytes(0x3ADA7, [0x08, 0x08, 0x08])
rom.write_bytes(0x45C42, [0x08, 0x08, 0x08])
#Disable catching fairies
rom.write_byte(0x34FD6, 0x80)
#Set overflow items for progressive equipment
rom.write_bytes(0x180090, [0x02, 0x47, 0x00, 0x47, 0x00, 0x47, 0x01, 0x47])
overflow_replacement = GREEN_TWENTY_RUPEES
# Rupoor negative value
rom.write_int16_to_rom(0x180036, 9999)
#Make Blue Shield more expensive
rom.write_bytes(0xF73D2, [0xFC, 0xFF])
rom.write_bytes(0xF73DA, [0x04, 0x00])
@ -288,13 +464,27 @@ def patch_rom(world, rom, hashtable, beep='normal', sprite=None):
rom.write_byte(0x180084, 0xA0) # full
# potion magic restore amount
rom.write_byte(0x180085, 0x80) # full
#Cape magic cost
rom.write_bytes(0x3ADA7, [0x04, 0x08, 0x10])
#Byrna residual magic cost
rom.write_bytes(0x45C42, [0x04, 0x02, 0x01])
#Enable catching fairies
rom.write_byte(0x34FD6, 0xF0)
#Set overflow items for progressive equipment
if world.goal == 'triforcehunt':
rom.write_bytes(0x180090, [0x04, 0x6C, 0x03, 0x6C, 0x02, 0x6C, 0x04, 0x6C])
overflow_replacement = TRIFORCE_PIECE
elif world.timer in ['timed', 'timed-countdown', 'timed-ohko']:
rom.write_bytes(0x180090, [0x04, 0x5D, 0x03, 0x5D, 0x02, 0x5D, 0x04, 0x5D])
overflow_replacement = GREEN_CLOCK
else:
rom.write_bytes(0x180090, [0x04, 0x47, 0x03, 0x47, 0x02, 0x47, 0x04, 0x47])
overflow_replacement = GREEN_TWENTY_RUPEES
difficulty = world.difficulty_requirements
#Set overflow items for progressive equipment
rom.write_bytes(0x180090,
[difficulty.progressive_sword_limit, overflow_replacement,
difficulty.progressive_shield_limit, overflow_replacement,
difficulty.progressive_armor_limit, overflow_replacement,
difficulty.progressive_bottle_limit, overflow_replacement])
# set up game internal RNG seed
for i in range(1024):
@ -347,16 +537,17 @@ def patch_rom(world, rom, hashtable, beep='normal', sprite=None):
rom.write_byte(0x6B632 + i, (vanilla_prize_pack_assignment[i] & 0xF0) | random.randint(1, 7))
# set bonk prizes
bonk_prizes = [0x79, 0xE3, 0x79, 0xAC, 0xAC, 0xE0, 0xDC, 0xAC, 0xE3, 0xE3, 0xDA, 0xE3, 0xDA, 0xD8, 0xAC, 0xAC, 0xE3, 0xD8, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xDC, 0xDB, 0xE3, 0xDA, 0x79, 0x79, 0xE3, 0xE3,
0xDA, 0x79, 0xAC, 0xAC, 0x79, 0xE3, 0x79, 0xAC, 0xAC, 0xE0, 0xDC, 0xE3, 0x79, 0xDE, 0xE3, 0xAC, 0xDB, 0x79, 0xE3, 0xD8, 0xAC, 0x79, 0xE3, 0xDB, 0xDB, 0xE3, 0xE3, 0x79, 0xD8, 0xDD]
bonk_addresses = [0x4CF6C, 0x4CFBA, 0x4CFE0, 0x4CFFB, 0x4D018, 0x4D01B, 0x4D028, 0x4D03C, 0x4D059, 0x4D07A, 0x4D09E, 0x4D0A8, 0x4D0AB, 0x4D0AE, 0x4D0BE, 0x4D0DD,
0x4D16A, 0x4D1E5, 0x4D1EE, 0x4D20B, 0x4CBBF, 0x4CBBF, 0x4CC17, 0x4CC1A, 0x4CC4A, 0x4CC4D, 0x4CC53, 0x4CC69, 0x4CC6F, 0x4CC7C, 0x4CCEF, 0x4CD51,
0x4CDC0, 0x4CDC3, 0x4CDC6, 0x4CE37, 0x4D2DE, 0x4D32F, 0x4D355, 0x4D367, 0x4D384, 0x4D387, 0x4D397, 0x4D39E, 0x4D3AB, 0x4D3AE, 0x4D3D1, 0x4D3D7,
0x4D3F8, 0x4D416, 0x4D420, 0x4D423, 0x4D42D, 0x4D449, 0x4D48C, 0x4D4D9, 0x4D4DC, 0x4D4E3, 0x4D504, 0x4D507, 0x4D55E, 0x4D56A]
if world.shuffle_bonk_prizes:
bonk_prizes = [0x79, 0xE3, 0x79, 0xAC, 0xAC, 0xE0, 0xDC, 0xAC, 0xE3, 0xE3, 0xDA, 0xE3, 0xDA, 0xD8, 0xAC, 0xAC, 0xE3, 0xD8, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xDC, 0xDB, 0xE3, 0xDA, 0x79, 0x79, 0xE3, 0xE3,
0xDA, 0x79, 0xAC, 0xAC, 0x79, 0xE3, 0x79, 0xAC, 0xAC, 0xE0, 0xDC, 0xE3, 0x79, 0xDE, 0xE3, 0xAC, 0xDB, 0x79, 0xE3, 0xD8, 0xAC, 0x79, 0xE3, 0xDB, 0xDB, 0xE3, 0xE3, 0x79, 0xD8, 0xDD]
bonk_addresses = [0x4CF6C, 0x4CFBA, 0x4CFE0, 0x4CFFB, 0x4D018, 0x4D01B, 0x4D028, 0x4D03C, 0x4D059, 0x4D07A, 0x4D09E, 0x4D0A8, 0x4D0AB, 0x4D0AE, 0x4D0BE, 0x4D0DD,
0x4D16A, 0x4D1E5, 0x4D1EE, 0x4D20B, 0x4CBBF, 0x4CBBF, 0x4CC17, 0x4CC1A, 0x4CC4A, 0x4CC4D, 0x4CC53, 0x4CC69, 0x4CC6F, 0x4CC7C, 0x4CCEF, 0x4CD51,
0x4CDC0, 0x4CDC3, 0x4CDC6, 0x4CE37, 0x4D2DE, 0x4D32F, 0x4D355, 0x4D367, 0x4D384, 0x4D387, 0x4D397, 0x4D39E, 0x4D3AB, 0x4D3AE, 0x4D3D1, 0x4D3D7,
0x4D3F8, 0x4D416, 0x4D420, 0x4D423, 0x4D42D, 0x4D449, 0x4D48C, 0x4D4D9, 0x4D4DC, 0x4D4E3, 0x4D504, 0x4D507, 0x4D55E, 0x4D56A]
random.shuffle(bonk_prizes)
for prize, address in zip(bonk_prizes, bonk_addresses):
rom.write_byte(address, prize)
for prize, address in zip(bonk_prizes, bonk_addresses):
rom.write_byte(address, prize)
# set Fountain bottle exchange items
if world.difficulty in ['hard', 'expert', 'insane']:
@ -365,6 +556,9 @@ def patch_rom(world, rom, hashtable, beep='normal', sprite=None):
else:
rom.write_byte(0x348FF, [0x16, 0x2B, 0x2C, 0x2D, 0x3C, 0x3D, 0x48][random.randint(0, 6)])
rom.write_byte(0x3493B, [0x16, 0x2B, 0x2C, 0x2D, 0x3C, 0x3D, 0x48][random.randint(0, 6)])
#enable Fat Fairy Chests
rom.write_bytes(0x1FC16, [0xB1, 0xC6, 0xF9, 0xC9, 0xC6, 0xF9])
# set Fat Fairy Bow/Sword prizes to be disappointing
rom.write_byte(0x34914, 0x3A) # Bow and Arrow
rom.write_byte(0x180028, 0x49) # Fighter Sword
@ -382,6 +576,8 @@ def patch_rom(world, rom, hashtable, beep='normal', sprite=None):
rom.write_byte(0x348DB, 0x3A) # Red Boomerang becomes Red Boomerang
rom.write_byte(0x348EB, 0x05) # Blue Shield becomes Blue Shield
rom.write_byte(0x180029, 0x01) # Smithy quick item give
# set swordless mode settings
rom.write_byte(0x18003F, 0x01 if world.mode == 'swordless' else 0x00) # hammer can harm ganon
rom.write_byte(0x180040, 0x01 if world.mode == 'swordless' else 0x00) # open curtains
@ -390,6 +586,14 @@ def patch_rom(world, rom, hashtable, beep='normal', sprite=None):
rom.write_byte(0x180044, 0x01 if world.mode == 'swordless' else 0x00) # hammer activates tablets
# set up clocks for timed modes
if world.shuffle == 'vanilla':
ERtimeincrease = 0
elif world.shuffle in ['dungeonssimple', 'dungeonsfull']:
ERtimeincrease = 10
else:
ERtimeincrease = 20
if world.keysanity:
ERtimeincrease = ERtimeincrease + 15
if world.clock_mode == 'off':
rom.write_bytes(0x180190, [0x00, 0x00, 0x00]) # turn off clock mode
rom.write_int32_to_rom(0x180200, 0) # red clock adjustment time (in frames, sint32)
@ -408,11 +612,11 @@ def patch_rom(world, rom, hashtable, beep='normal', sprite=None):
rom.write_int32_to_rom(0x180204, 2 * 60 * 60) # blue clock adjustment time (in frames, sint32)
rom.write_int32_to_rom(0x180208, 4 * 60 * 60) # green clock adjustment time (in frames, sint32)
if world.difficulty == 'easy':
rom.write_int32_to_rom(0x18020C, 20 * 60 * 60) # starting time (in frames, sint32)
rom.write_int32_to_rom(0x18020C, (20 + ERtimeincrease) * 60 * 60) # starting time (in frames, sint32)
elif world.difficulty == 'normal':
rom.write_int32_to_rom(0x18020C, 10 * 60 * 60) # starting time (in frames, sint32)
rom.write_int32_to_rom(0x18020C, (10 + ERtimeincrease) * 60 * 60) # starting time (in frames, sint32)
else:
rom.write_int32_to_rom(0x18020C, 5 * 60 * 60) # starting time (in frames, sint32)
rom.write_int32_to_rom(0x18020C, int((5 + ERtimeincrease / 2) * 60 * 60)) # starting time (in frames, sint32)
if world.clock_mode == 'stopwatch':
rom.write_bytes(0x180190, [0x02, 0x01, 0x00]) # set stopwatch mode
rom.write_int32_to_rom(0x180200, -2 * 60 * 60) # red clock adjustment time (in frames, sint32)
@ -424,28 +628,43 @@ def patch_rom(world, rom, hashtable, beep='normal', sprite=None):
rom.write_int32_to_rom(0x180200, -2 * 60 * 60) # red clock adjustment time (in frames, sint32)
rom.write_int32_to_rom(0x180204, 2 * 60 * 60) # blue clock adjustment time (in frames, sint32)
rom.write_int32_to_rom(0x180208, 4 * 60 * 60) # green clock adjustment time (in frames, sint32)
rom.write_int32_to_rom(0x18020C, 40 * 60 * 60) # starting time (in frames, sint32)
rom.write_int32_to_rom(0x18020C, (40 + ERtimeincrease) * 60 * 60) # starting time (in frames, sint32)
# set up goals for treasure hunt
rom.write_bytes(0x180165, [0x0E, 0x28] if world.treasure_hunt_icon == 'Triforce Piece' else [0x0D, 0x28])
rom.write_byte(0x180167, world.treasure_hunt_count % 256)
# TODO: a proper race rom mode should be implemented, that changes the following flag, and rummages the table (or uses the future encryption feature, etc)
rom.write_bytes(0x180213, [0x00, 0x01]) # Not a Tournament Seed
# assorted fixes
rom.write_byte(0x180030, 0x00) # Disable SRAM trace
rom.write_byte(0x180036, 0x0A) # Rupoor negative value
rom.write_byte(0x1800A2, 0x01) # remain in real dark world when dying in dark word dungion before killing aga1
rom.write_byte(0x180169, 0x01 if world.lock_aga_door_in_escape else 0x00) # Lock or unlock aga tower door during escape sequence.
rom.write_byte(0x180171, 0x01 if world.ganon_at_pyramid else 0x00) # Enable respawning on pyramid after ganon death
rom.write_byte(0x180168, 0x08) # Spike Cave Damage
rom.write_bytes(0x18016B, [0x04, 0x02, 0x01]) #Set spike cave and MM spike room Cape usage
rom.write_bytes(0x18016E, [0x04, 0x08, 0x10]) #Set spike cave and MM spike room Cape usage
rom.write_bytes(0x50563, [0x3F, 0x14]) # disable below ganon chest
rom.write_byte(0x50599, 0x00) # disable below ganon chest
rom.write_bytes(0xE9A5, [0x7E, 0x00, 0x24]) # disable below ganon chest
rom.write_byte(0xF5D73, 0xF0) # bees are catchable
rom.write_byte(0xF5F10, 0xF0) # bees are catchable
rom.write_byte(0x180086, 0x00 if world.aga_randomness else 0x01) # set blue ball and ganon warp randomness
rom.write_byte(0x1800A0, 0x01) # return to light world on s+q without mirror
rom.write_byte(0x1800A1, 0x01) # enable overworld screen transition draining for water level inside swamp
if world.goal in ['ganon']:
rom.write_byte(0x18003E, 0x03) # make ganon invincible until all crystals and aga 2 are collected
elif world.goal in ['pedestal', 'triforcehunt']:
rom.write_byte(0x180034, 0x0A) # starting max bombs
rom.write_byte(0x180035, 30) # starting max bombs
if world.goal in ['pedestal', 'triforcehunt']:
rom.write_byte(0x18003E, 0x01) # make ganon invincible
elif world.goal in ['dungeons']:
rom.write_byte(0x18003E, 0x02) # make ganon invincible until all dungeons are beat
elif world.goal in ['crystals']:
rom.write_byte(0x18003E, 0x04) # make ganon invincible until all crystals
else:
rom.write_byte(0x18003E, 0x03) # make ganon invincible until all crystals and aga 2 are collected
rom.write_byte(0x18016A, 0x01 if world.keysanity else 0x00) # free roaming item text boxes
rom.write_byte(0x18003B, 0x01 if world.keysanity else 0x00) # maps showing crystals on overworld
@ -475,9 +694,9 @@ def patch_rom(world, rom, hashtable, beep='normal', sprite=None):
rom.write_bytes(0x6D2FB, [0x00, 0x00, 0xf7, 0xff, 0x02, 0x0E])
rom.write_bytes(0x6D313, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E])
if world.swamp_patch_required:
# patch swamp: Need to enable permanent drain of water as dam or swamp were moved
rom.write_byte(0x18003D, 0x01)
# patch swamp: Need to enable permanent drain of water as dam or swamp were moved
rom.write_byte(0x18003D, 0x01 if world.swamp_patch_required else 0x00)
# set correct flag for hera basement item
if world.get_location('Tower of Hera - Basement Cage').item is not None and world.get_location('Tower of Hera - Basement Cage').item.name == 'Small Key (Tower of Hera)':
@ -489,36 +708,65 @@ def patch_rom(world, rom, hashtable, beep='normal', sprite=None):
if world.fix_trock_doors:
rom.write_byte(0xFED31, 0x0E) # preopen bombable exit
rom.write_byte(0xFEE41, 0x0E) # preopen bombable exit
rom.write_byte(0xFE465, 0x1E) # remove small key door on backside of big key door
# included unconditionally in base2current
#rom.write_byte(0xFE465, 0x1E) # remove small key door on backside of big key door
else:
rom.write_byte(0xFED31, 0x2A) # preopen bombable exit
rom.write_byte(0xFEE41, 0x2A) # preopen bombable exit
# Thanks to Zarby89 for finding these values
# fix skull woods exit point
if world.fix_skullwoods_exit:
rom.write_byte(0x15E0D, 0xF8)
rom.write_byte(0x15E0D, 0xF8 if world.fix_skullwoods_exit else 0xB8)
# fix palace of darkness exit point
if world.fix_palaceofdarkness_exit:
rom.write_byte(0x15E03, 0x40)
rom.write_byte(0x15E03, 0x40 if world.fix_palaceofdarkness_exit else 0x28)
# fix turtle rock exit point
if world.fix_trock_exit:
rom.write_byte(0x15E1D, 0x34)
rom.write_byte(0x15E1D, 0x34 if world.fix_trock_exit else 0x28)
# fix ganons tower exit point
if world.fix_gtower_exit:
rom.write_byte(0x15E25, 0xA4)
# todo fix screen scrolling
rom.write_byte(0x15E25, 0xA4 if world.fix_gtower_exit else 0x28)
# todo fix screen scrolling
write_strings(rom, world)
# set rom name
# 21 bytes
rom.write_bytes(0x7FC0, bytearray('ER_052_%09d\0' % world.seed, 'utf8') + world.option_identifier.to_bytes(4, 'big'))
# store hash table for main menu hash
rom.write_bytes(0x187F00, hashtable)
apply_rom_settings(rom, beep, world.quickswap, world.fastmenu, world.disable_music, sprite)
return rom
def apply_rom_settings(rom, beep, quickswap, fastmenu, disable_music, sprite):
# enable instant item menu
if world.fastmenu:
rom.write_byte(0x180048, 0x01)
# Sound twekas for fastmenu:
if fastmenu == 'instant':
rom.write_byte(0x6DD9A, 0x20)
rom.write_byte(0x6DF2A, 0x20)
rom.write_byte(0x6E0E9, 0x20)
else:
rom.write_byte(0x6DD9A, 0x11)
rom.write_byte(0x6DF2A, 0x12)
rom.write_byte(0x6E0E9, 0x12)
if fastmenu == 'instant':
rom.write_byte(0x180048, 0xE8)
elif fastmenu == 'double':
rom.write_byte(0x180048, 0x10)
elif fastmenu == 'triple':
rom.write_byte(0x180048, 0x18)
elif fastmenu == 'quadruple':
rom.write_byte(0x180048, 0x20)
elif fastmenu == 'half':
rom.write_byte(0x180048, 0x04)
else:
rom.write_byte(0x180048, 0x08)
# enable quick item swapping with L and R (ported by Amazing Ampharos)
if world.quickswap:
if quickswap:
rom.write_bytes(0x107fb, [0x22, 0x50, 0xFF, 0x1F])
rom.write_bytes(0x12451, [0x22, 0x50, 0xFF, 0x1F])
rom.write_bytes(0xfff50, [0x20, 0x58, 0xFF, 0xA5, 0xF6, 0x29, 0x40, 0x6B, 0xA5, 0xF6, 0x89, 0x10, 0xF0, 0x03, 0x4C, 0x69,
@ -532,66 +780,84 @@ def patch_rom(world, rom, hashtable, beep='normal', sprite=None):
0x7E, 0xAA, 0xE0, 0x01, 0xF0, 0x10, 0xCA, 0xBF, 0x5B, 0xF3, 0x7E, 0xF0, 0xF5, 0x8A, 0x8F, 0x4F,
0xF3, 0x7E, 0xA2, 0x10, 0x80, 0xE0, 0xA2, 0x0F, 0x80, 0xD6, 0x60, 0xA9, 0x20, 0x8D, 0x2F, 0x01,
0x8E, 0x02, 0x02, 0x22, 0x7F, 0xDB, 0x0D, 0xFA, 0x60, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
else:
rom.write_bytes(0x107fb, [0xa5, 0xf6, 0x29, 0x40])
rom.write_bytes(0x12451, [0xa5, 0xf6, 0x29, 0x40])
rom.write_bytes(0xfff50, [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
write_strings(rom, world)
if world.disable_music:
volumeaddresses = [0xD373B, 0xD375B, 0xD90F8, 0xDA710, 0xDA7A4, 0xDA7BB, 0xDA7D2, 0xD5954, 0xD653B, 0xDA736, 0xDA752, 0xDA772, 0xDA792,
0xD5B47, 0xD5B5E, 0xD4306, 0xD6878, 0xD6883, 0xD6E48, 0xD6E76, 0xD6EFB, 0xD6F2D, 0xDA211, 0xDA35B, 0xDA37B, 0xDA38E,
0xDA39F, 0xDA5C3, 0xDA691, 0xDA6A8, 0xDA6DF, 0xD2349, 0xD3F45, 0xD42EB, 0xD48B9, 0xD48FF, 0xD543F, 0xD5817, 0xD5957,
0xD5ACB, 0xD5AE8, 0xD5B4A, 0xDA5DE, 0xDA608, 0xDA635, 0xDA662, 0xDA71F, 0xDA7AF, 0xDA7C6, 0xDA7DD, 0xD2F00, 0xDA3D5,
0xD249C, 0xD24CD, 0xD2C09, 0xD2C53, 0xD2CAF, 0xD2CEB, 0xD2D91, 0xD2EE6, 0xD38ED, 0xD3C91, 0xD3CD3, 0xD3CE8, 0xD3F0C,
0xD3F82, 0xD405F, 0xD4139, 0xD4198, 0xD41D5, 0xD41F6, 0xD422B, 0xD4270, 0xD42B1, 0xD4334, 0xD4371, 0xD43A6, 0xD43DB,
0xD441E, 0xD4597, 0xD4B3C, 0xD4BAB, 0xD4C03, 0xD4C53, 0xD4C7F, 0xD4D9C, 0xD5424, 0xD65D2, 0xD664F, 0xD6698, 0xD66FF,
0xD6985, 0xD6C5C, 0xD6C6F, 0xD6C8E, 0xD6CB4, 0xD6D7D, 0xD827D, 0xD960C, 0xD9828, 0xDA233, 0xDA3A2, 0xDA49E, 0xDA72B,
0xDA745, 0xDA765, 0xDA785, 0xDABF6, 0xDAC0D, 0xDAEBE, 0xDAFAC, 0xD9A02, 0xD9BD6, 0xD21CD, 0xD2279, 0xD2E66, 0xD2E70,
0xD2EAB, 0xD3B97, 0xD3BAC, 0xD3BE8, 0xD3C0D, 0xD3C39, 0xD3C68, 0xD3C9F, 0xD3CBC, 0xD401E, 0xD4290, 0xD443E, 0xD456F,
0xD47D3, 0xD4D43, 0xD4DCC, 0xD4EBA, 0xD4F0B, 0xD4FE5, 0xD5012, 0xD54BC, 0xD54D5, 0xD54F0, 0xD5509, 0xD57D8, 0xD59B9,
0xD5A2F, 0xD5AEB, 0xD5E5E, 0xD5FE9, 0xD658F, 0xD674A, 0xD6827, 0xD69D6, 0xD69F5, 0xD6A05, 0xD6AE9, 0xD6DCF, 0xD6E20,
0xD6ECB, 0xD71D4, 0xD71E6, 0xD7203, 0xD721E, 0xD8724, 0xD8732, 0xD9652, 0xD9698, 0xD9CBC, 0xD9DC0, 0xD9E49, 0xDAA68,
0xDAA77, 0xDAA88, 0xDAA99, 0xDAF04, 0xD1D28, 0xD1D41, 0xD1D5C, 0xD1D77, 0xD1EEE, 0xD311D, 0xD31D1, 0xD4148, 0xD5543,
0xD5B6F, 0xD65B3, 0xD6760, 0xD6B6B, 0xD6DF6, 0xD6E0D, 0xD73A1, 0xD814C, 0xD825D, 0xD82BE, 0xD8340, 0xD8394, 0xD842C,
0xD8796, 0xD8903, 0xD892A, 0xD91E8, 0xD922B, 0xD92E0, 0xD937E, 0xD93C1, 0xDA958, 0xDA971, 0xDA98C, 0xDA9A7, 0xD1D92,
0xD1DBD, 0xD1DEB, 0xD1F5D, 0xD1F9F, 0xD1FBD, 0xD1FDC, 0xD1FEA, 0xD20CA, 0xD21BB, 0xD22C9, 0xD2754, 0xD284C, 0xD2866,
0xD2887, 0xD28A0, 0xD28BA, 0xD28DB, 0xD28F4, 0xD293E, 0xD2BF3, 0xD2C1F, 0xD2C69, 0xD2CA1, 0xD2CC5, 0xD2D05, 0xD2D73,
0xD2DAF, 0xD2E3D, 0xD2F36, 0xD2F46, 0xD2F6F, 0xD2FCF, 0xD2FDF, 0xD302B, 0xD3086, 0xD3099, 0xD30A5, 0xD30CD, 0xD30F6,
0xD3154, 0xD3184, 0xD333A, 0xD33D9, 0xD349F, 0xD354A, 0xD35E5, 0xD3624, 0xD363C, 0xD3672, 0xD3691, 0xD36B4, 0xD36C6,
0xD3724, 0xD3767, 0xD38CB, 0xD3B1D, 0xD3B2F, 0xD3B55, 0xD3B70, 0xD3B81, 0xD3BBF, 0xD3D34, 0xD3D55, 0xD3D6E, 0xD3DC6,
0xD3E04, 0xD3E38, 0xD3F65, 0xD3FA6, 0xD404F, 0xD4087, 0xD417A, 0xD41A0, 0xD425C, 0xD4319, 0xD433C, 0xD43EF, 0xD440C,
0xD4452, 0xD4494, 0xD44B5, 0xD4512, 0xD45D1, 0xD45EF, 0xD4682, 0xD46C3, 0xD483C, 0xD4848, 0xD4855, 0xD4862, 0xD486F,
0xD487C, 0xD4A1C, 0xD4A3B, 0xD4A60, 0xD4B27, 0xD4C7A, 0xD4D12, 0xD4D81, 0xD4E90, 0xD4ED6, 0xD4EE2, 0xD5005, 0xD502E,
0xD503C, 0xD5081, 0xD51B1, 0xD51C7, 0xD51CF, 0xD51EF, 0xD520C, 0xD5214, 0xD5231, 0xD5257, 0xD526D, 0xD5275, 0xD52AF,
0xD52BD, 0xD52CD, 0xD52DB, 0xD549C, 0xD5801, 0xD58A4, 0xD5A68, 0xD5A7F, 0xD5C12, 0xD5D71, 0xD5E10, 0xD5E9A, 0xD5F8B,
0xD5FA4, 0xD651A, 0xD6542, 0xD65ED, 0xD661D, 0xD66D7, 0xD6776, 0xD68BD, 0xD68E5, 0xD6956, 0xD6973, 0xD69A8, 0xD6A51,
0xD6A86, 0xD6B96, 0xD6C3E, 0xD6D4A, 0xD6E9C, 0xD6F80, 0xD717E, 0xD7190, 0xD71B9, 0xD811D, 0xD8139, 0xD816B, 0xD818A,
0xD819E, 0xD81BE, 0xD829C, 0xD82E1, 0xD8306, 0xD830E, 0xD835E, 0xD83AB, 0xD83CA, 0xD83F0, 0xD83F8, 0xD844B, 0xD8479,
0xD849E, 0xD84CB, 0xD84EB, 0xD84F3, 0xD854A, 0xD8573, 0xD859D, 0xD85B4, 0xD85CE, 0xD862A, 0xD8681, 0xD87E3, 0xD87FF,
0xD887B, 0xD88C6, 0xD88E3, 0xD8944, 0xD897B, 0xD8C97, 0xD8CA4, 0xD8CB3, 0xD8CC2, 0xD8CD1, 0xD8D01, 0xD917B, 0xD918C,
0xD919A, 0xD91B5, 0xD91D0, 0xD91DD, 0xD9220, 0xD9273, 0xD9284, 0xD9292, 0xD92AD, 0xD92C8, 0xD92D5, 0xD9311, 0xD9322,
0xD9330, 0xD934B, 0xD9366, 0xD9373, 0xD93B6, 0xD97A6, 0xD97C2, 0xD97DC, 0xD97FB, 0xD9811, 0xD98FF, 0xD996F, 0xD99A8,
0xD99D5, 0xD9A30, 0xD9A4E, 0xD9A6B, 0xD9A88, 0xD9AF7, 0xD9B1D, 0xD9B43, 0xD9B7C, 0xD9BA9, 0xD9C84, 0xD9C8D, 0xD9CAC,
0xD9CE8, 0xD9CF3, 0xD9CFD, 0xD9D46, 0xDA35E, 0xDA37E, 0xDA391, 0xDA478, 0xDA4C3, 0xDA4D7, 0xDA4F6, 0xDA515, 0xDA6E2,
0xDA9C2, 0xDA9ED, 0xDAA1B, 0xDAA57, 0xDABAF, 0xDABC9, 0xDABE2, 0xDAC28, 0xDAC46, 0xDAC63, 0xDACB8, 0xDACEC, 0xDAD08,
0xDAD25, 0xDAD42, 0xDAD5F, 0xDAE17, 0xDAE34, 0xDAE51, 0xDAF2E, 0xDAF55, 0xDAF6B, 0xDAF81, 0xDB14F, 0xDB16B, 0xDB180,
0xDB195, 0xDB1AA, 0xD2B88, 0xD364A, 0xD369F, 0xD3747, 0xD213F, 0xD2174, 0xD229E, 0xD2426, 0xD4731, 0xD4753, 0xD4774,
0xD4795, 0xD47B6, 0xD4AA5, 0xD4AE4, 0xD4B96, 0xD4CA5, 0xD5477, 0xD5A3D, 0xD6566, 0xD672C, 0xD67C0, 0xD69B8, 0xD6AB1,
0xD6C05, 0xD6DB3, 0xD71AB, 0xD8E2D, 0xD8F0D, 0xD94E0, 0xD9544, 0xD95A8, 0xD9982, 0xD9B56, 0xDA694, 0xDA6AB, 0xDAE88,
0xDAEC8, 0xDAEE6, 0xDB1BF, 0xD210A, 0xD22DC, 0xD2447, 0xD5A4D, 0xD5DDC, 0xDA251, 0xDA26C, 0xD945E, 0xD967D, 0xD96C2,
0xD9C95, 0xD9EE6, 0xDA5C6, 0xD2047, 0xD24C2, 0xD24EC, 0xD25A4, 0xD3DAA, 0xD51A8, 0xD51E6, 0xD524E, 0xD529E, 0xD6045,
0xD81DE, 0xD821E, 0xD94AA, 0xD9A9E, 0xD9AE4, 0xDA289, 0xD2085, 0xD21C5, 0xD5F28]
for address in volumeaddresses:
rom.write_byte(address, 0x00)
# set rom name
# 21 bytes
rom.write_bytes(0x7FC0, bytearray('ER_050_%09d\0' % world.seed, 'utf8') + world.option_identifier.to_bytes(4, 'big'))
music_volumes = [
(0x00, [0xD373B, 0xD375B, 0xD90F8]),
(0x14, [0xDA710, 0xDA7A4, 0xDA7BB, 0xDA7D2]),
(0x3C, [0xD5954, 0xD653B, 0xDA736, 0xDA752, 0xDA772, 0xDA792]),
(0x50, [0xD5B47, 0xD5B5E]),
(0x54, [0xD4306]),
(0x64, [0xD6878, 0xD6883, 0xD6E48, 0xD6E76, 0xD6EFB, 0xD6F2D, 0xDA211, 0xDA35B, 0xDA37B, 0xDA38E, 0xDA39F, 0xDA5C3, 0xDA691, 0xDA6A8, 0xDA6DF]),
(0x78, [0xD2349, 0xD3F45, 0xD42EB, 0xD48B9, 0xD48FF, 0xD543F, 0xD5817, 0xD5957, 0xD5ACB, 0xD5AE8, 0xD5B4A, 0xDA5DE, 0xDA608, 0xDA635,
0xDA662, 0xDA71F, 0xDA7AF, 0xDA7C6, 0xDA7DD]),
(0x82, [0xD2F00, 0xDA3D5]),
(0xA0, [0xD249C, 0xD24CD, 0xD2C09, 0xD2C53, 0xD2CAF, 0xD2CEB, 0xD2D91, 0xD2EE6, 0xD38ED, 0xD3C91, 0xD3CD3, 0xD3CE8, 0xD3F0C,
0xD3F82, 0xD405F, 0xD4139, 0xD4198, 0xD41D5, 0xD41F6, 0xD422B, 0xD4270, 0xD42B1, 0xD4334, 0xD4371, 0xD43A6, 0xD43DB,
0xD441E, 0xD4597, 0xD4B3C, 0xD4BAB, 0xD4C03, 0xD4C53, 0xD4C7F, 0xD4D9C, 0xD5424, 0xD65D2, 0xD664F, 0xD6698, 0xD66FF,
0xD6985, 0xD6C5C, 0xD6C6F, 0xD6C8E, 0xD6CB4, 0xD6D7D, 0xD827D, 0xD960C, 0xD9828, 0xDA233, 0xDA3A2, 0xDA49E, 0xDA72B,
0xDA745, 0xDA765, 0xDA785, 0xDABF6, 0xDAC0D, 0xDAEBE, 0xDAFAC]),
(0xAA, [0xD9A02, 0xD9BD6]),
(0xB4, [0xD21CD, 0xD2279, 0xD2E66, 0xD2E70, 0xD2EAB, 0xD3B97, 0xD3BAC, 0xD3BE8, 0xD3C0D, 0xD3C39, 0xD3C68, 0xD3C9F, 0xD3CBC,
0xD401E, 0xD4290, 0xD443E, 0xD456F, 0xD47D3, 0xD4D43, 0xD4DCC, 0xD4EBA, 0xD4F0B, 0xD4FE5, 0xD5012, 0xD54BC, 0xD54D5,
0xD54F0, 0xD5509, 0xD57D8, 0xD59B9, 0xD5A2F, 0xD5AEB, 0xD5E5E, 0xD5FE9, 0xD658F, 0xD674A, 0xD6827, 0xD69D6, 0xD69F5,
0xD6A05, 0xD6AE9, 0xD6DCF, 0xD6E20, 0xD6ECB, 0xD71D4, 0xD71E6, 0xD7203, 0xD721E, 0xD8724, 0xD8732, 0xD9652, 0xD9698,
0xD9CBC, 0xD9DC0, 0xD9E49, 0xDAA68, 0xDAA77, 0xDAA88, 0xDAA99, 0xDAF04]),
(0x8c, [0xD1D28, 0xD1D41, 0xD1D5C, 0xD1D77, 0xD1EEE, 0xD311D, 0xD31D1, 0xD4148, 0xD5543, 0xD5B6F, 0xD65B3, 0xD6760, 0xD6B6B,
0xD6DF6, 0xD6E0D, 0xD73A1, 0xD814C, 0xD825D, 0xD82BE, 0xD8340, 0xD8394, 0xD842C, 0xD8796, 0xD8903, 0xD892A, 0xD91E8,
0xD922B, 0xD92E0, 0xD937E, 0xD93C1, 0xDA958, 0xDA971, 0xDA98C, 0xDA9A7]),
(0xC8, [0xD1D92, 0xD1DBD, 0xD1DEB, 0xD1F5D, 0xD1F9F, 0xD1FBD, 0xD1FDC, 0xD1FEA, 0xD20CA, 0xD21BB, 0xD22C9, 0xD2754, 0xD284C,
0xD2866, 0xD2887, 0xD28A0, 0xD28BA, 0xD28DB, 0xD28F4, 0xD293E, 0xD2BF3, 0xD2C1F, 0xD2C69, 0xD2CA1, 0xD2CC5, 0xD2D05,
0xD2D73, 0xD2DAF, 0xD2E3D, 0xD2F36, 0xD2F46, 0xD2F6F, 0xD2FCF, 0xD2FDF, 0xD302B, 0xD3086, 0xD3099, 0xD30A5, 0xD30CD,
0xD30F6, 0xD3154, 0xD3184, 0xD333A, 0xD33D9, 0xD349F, 0xD354A, 0xD35E5, 0xD3624, 0xD363C, 0xD3672, 0xD3691, 0xD36B4,
0xD36C6, 0xD3724, 0xD3767, 0xD38CB, 0xD3B1D, 0xD3B2F, 0xD3B55, 0xD3B70, 0xD3B81, 0xD3BBF, 0xD3D34, 0xD3D55, 0xD3D6E,
0xD3DC6, 0xD3E04, 0xD3E38, 0xD3F65, 0xD3FA6, 0xD404F, 0xD4087, 0xD417A, 0xD41A0, 0xD425C, 0xD4319, 0xD433C, 0xD43EF,
0xD440C, 0xD4452, 0xD4494, 0xD44B5, 0xD4512, 0xD45D1, 0xD45EF, 0xD4682, 0xD46C3, 0xD483C, 0xD4848, 0xD4855, 0xD4862,
0xD486F, 0xD487C, 0xD4A1C, 0xD4A3B, 0xD4A60, 0xD4B27, 0xD4C7A, 0xD4D12, 0xD4D81, 0xD4E90, 0xD4ED6, 0xD4EE2, 0xD5005,
0xD502E, 0xD503C, 0xD5081, 0xD51B1, 0xD51C7, 0xD51CF, 0xD51EF, 0xD520C, 0xD5214, 0xD5231, 0xD5257, 0xD526D, 0xD5275,
0xD52AF, 0xD52BD, 0xD52CD, 0xD52DB, 0xD549C, 0xD5801, 0xD58A4, 0xD5A68, 0xD5A7F, 0xD5C12, 0xD5D71, 0xD5E10, 0xD5E9A,
0xD5F8B, 0xD5FA4, 0xD651A, 0xD6542, 0xD65ED, 0xD661D, 0xD66D7, 0xD6776, 0xD68BD, 0xD68E5, 0xD6956, 0xD6973, 0xD69A8,
0xD6A51, 0xD6A86, 0xD6B96, 0xD6C3E, 0xD6D4A, 0xD6E9C, 0xD6F80, 0xD717E, 0xD7190, 0xD71B9, 0xD811D, 0xD8139, 0xD816B,
0xD818A, 0xD819E, 0xD81BE, 0xD829C, 0xD82E1, 0xD8306, 0xD830E, 0xD835E, 0xD83AB, 0xD83CA, 0xD83F0, 0xD83F8, 0xD844B,
0xD8479, 0xD849E, 0xD84CB, 0xD84EB, 0xD84F3, 0xD854A, 0xD8573, 0xD859D, 0xD85B4, 0xD85CE, 0xD862A, 0xD8681, 0xD87E3,
0xD87FF, 0xD887B, 0xD88C6, 0xD88E3, 0xD8944, 0xD897B, 0xD8C97, 0xD8CA4, 0xD8CB3, 0xD8CC2, 0xD8CD1, 0xD8D01, 0xD917B,
0xD918C, 0xD919A, 0xD91B5, 0xD91D0, 0xD91DD, 0xD9220, 0xD9273, 0xD9284, 0xD9292, 0xD92AD, 0xD92C8, 0xD92D5, 0xD9311,
0xD9322, 0xD9330, 0xD934B, 0xD9366, 0xD9373, 0xD93B6, 0xD97A6, 0xD97C2, 0xD97DC, 0xD97FB, 0xD9811, 0xD98FF, 0xD996F,
0xD99A8, 0xD99D5, 0xD9A30, 0xD9A4E, 0xD9A6B, 0xD9A88, 0xD9AF7, 0xD9B1D, 0xD9B43, 0xD9B7C, 0xD9BA9, 0xD9C84, 0xD9C8D,
0xD9CAC, 0xD9CE8, 0xD9CF3, 0xD9CFD, 0xD9D46, 0xDA35E, 0xDA37E, 0xDA391, 0xDA478, 0xDA4C3, 0xDA4D7, 0xDA4F6, 0xDA515,
0xDA6E2, 0xDA9C2, 0xDA9ED, 0xDAA1B, 0xDAA57, 0xDABAF, 0xDABC9, 0xDABE2, 0xDAC28, 0xDAC46, 0xDAC63, 0xDACB8, 0xDACEC,
0xDAD08, 0xDAD25, 0xDAD42, 0xDAD5F, 0xDAE17, 0xDAE34, 0xDAE51, 0xDAF2E, 0xDAF55, 0xDAF6B, 0xDAF81, 0xDB14F, 0xDB16B,
0xDB180, 0xDB195, 0xDB1AA]),
(0xD2, [0xD2B88, 0xD364A, 0xD369F, 0xD3747]),
(0xDC, [0xD213F, 0xD2174, 0xD229E, 0xD2426, 0xD4731, 0xD4753, 0xD4774, 0xD4795, 0xD47B6, 0xD4AA5, 0xD4AE4, 0xD4B96, 0xD4CA5,
0xD5477, 0xD5A3D, 0xD6566, 0xD672C, 0xD67C0, 0xD69B8, 0xD6AB1, 0xD6C05, 0xD6DB3, 0xD71AB, 0xD8E2D, 0xD8F0D, 0xD94E0,
0xD9544, 0xD95A8, 0xD9982, 0xD9B56, 0xDA694, 0xDA6AB, 0xDAE88, 0xDAEC8, 0xDAEE6, 0xDB1BF]),
(0xE6, [0xD210A, 0xD22DC, 0xD2447, 0xD5A4D, 0xD5DDC, 0xDA251, 0xDA26C]),
(0xF0, [0xD945E, 0xD967D, 0xD96C2, 0xD9C95, 0xD9EE6, 0xDA5C6]),
(0xFA, [0xD2047, 0xD24C2, 0xD24EC, 0xD25A4, 0xD3DAA, 0xD51A8, 0xD51E6, 0xD524E, 0xD529E, 0xD6045, 0xD81DE, 0xD821E, 0xD94AA,
0xD9A9E, 0xD9AE4, 0xDA289]),
(0xFF, [0xD2085, 0xD21C5, 0xD5F28])
]
for volume, addresses in music_volumes:
for address in addresses:
rom.write_byte(address, volume if not disable_music else 0x00)
# set heart beep rate
rom.write_byte(0x180033, {'off': 0x00, 'half': 0x40, 'quarter': 0x80, 'normal': 0x20}[beep])
# store hash table for main menu hash
rom.write_bytes(0x187F00, hashtable)
# write link sprite if required
if sprite is not None:
write_sprite(rom, sprite)
@ -599,24 +865,13 @@ def patch_rom(world, rom, hashtable, beep='normal', sprite=None):
if isinstance(rom, LocalRom):
rom.write_crc()
return rom
def write_sprite(rom, sprite):
if len(sprite) == 0x7000:
# sprite file with graphics and without palette data
rom.write_bytes(0x80000, sprite[:0x7000])
elif len(sprite) == 0x7078:
# sprite file with graphics and palette data
rom.write_bytes(0x80000, sprite[:0x7000])
rom.write_bytes(0xDD308, sprite[0x7000:])
rom.write_bytes(0xDEDF5, sprite[0x7036:0x7038])
rom.write_bytes(0xDEDF7, sprite[0x7054:0x7056])
elif len(sprite) in [0x100000, 0x200000]:
# full rom with patched sprite, extract it
rom.write_bytes(0x80000, sprite[0x80000:0x87000])
rom.write_bytes(0xDD308, sprite[0xDD308:0xDD380])
rom.write_bytes(0xDEDF5, sprite[0xDEDF5:0xDEDF9])
if not sprite.valid:
return
rom.write_bytes(0x80000, sprite.sprite)
rom.write_bytes(0xDD308, sprite.palette)
rom.write_bytes(0xDEDF5, sprite.glove_palette)
def write_string_to_rom(rom, target, string):
@ -679,7 +934,8 @@ def write_strings(rom, world):
credits.update_credits_line('castle', 0, random.choice(KingsReturn_texts))
credits.update_credits_line('sancturary', 0, random.choice(Sanctuary_texts))
credits.update_credits_line('kakariko', 0, random.choice(Kakariko_texts))
credits.update_credits_line('kakariko', 0, random.choice(Kakariko_texts).format(random.choice(Sahasrahla_names)))
credits.update_credits_line('desert', 0, random.choice(DesertPalace_texts))
credits.update_credits_line('hera', 0, random.choice(MountainTower_texts))
credits.update_credits_line('house', 0, random.choice(LinksHouse_texts))

297
Rules.py
View File

@ -33,10 +33,15 @@ def set_rules(world):
if not world.swamp_patch_required:
add_rule(world.get_entrance('Swamp Palace Moat'), lambda state: state.has_Mirror())
set_bunny_rules(world)
def set_rule(spot, rule):
spot.access_rule = rule
def set_always_allow(spot, rule):
spot.always_allow = rule
def add_rule(spot, rule, combine='and'):
old_rule = spot.access_rule
@ -57,11 +62,16 @@ def forbid_item(location, item):
def item_in_locations(state, item, locations):
for location in locations:
loc = state.world.get_location(location)
if loc.item is not None and loc.item.name == item:
if item_name(state, location) == item:
return True
return False
def item_name(state, location):
location = state.world.get_location(location)
if location.item is None:
return None
return location.item.name
def global_rules(world):
# ganon can only carry triforce
@ -76,7 +86,7 @@ def global_rules(world):
world.get_region('Old Man House').can_reach = lambda state: state.can_reach('Old Man', 'Location') or old_rule(state)
# overworld requirements
set_rule(world.get_entrance('Kings Grave'), lambda state: state.has_Boots() and (state.can_lift_heavy_rocks() or (state.has_Mirror() and state.can_reach('West Dark World'))))
set_rule(world.get_entrance('Kings Grave'), lambda state: state.has_Boots() and (state.can_lift_heavy_rocks() or (state.has_Pearl() and state.has_Mirror() and state.can_reach('West Dark World'))))
set_rule(world.get_entrance('Bonk Fairy (Light)'), lambda state: state.has_Boots())
set_rule(world.get_location('Sunken Treasure'), lambda state: state.can_reach('Dam'))
set_rule(world.get_entrance('Bat Cave Drop Ledge'), lambda state: state.has('Hammer'))
@ -90,9 +100,9 @@ def global_rules(world):
set_rule(world.get_entrance('Flute Spot 1'), lambda state: state.has('Ocarina'))
set_rule(world.get_entrance('Lake Hylia Central Island Teleporter'), lambda state: state.can_lift_heavy_rocks())
set_rule(world.get_entrance('Dark Desert Teleporter'), lambda state: state.has('Ocarina') and state.can_lift_heavy_rocks())
set_rule(world.get_entrance('East Hyrule Teleporter'), lambda state: state.has('Hammer') and state.can_lift_rocks() and state.has_Pearl())
set_rule(world.get_entrance('South Hyrule Teleporter'), lambda state: state.has('Hammer') and state.can_lift_rocks() and state.has_Pearl())
set_rule(world.get_entrance('Kakariko Teleporter'), lambda state: ((state.has('Hammer') and state.can_lift_rocks()) or state.can_lift_heavy_rocks()) and state.has_Pearl())
set_rule(world.get_entrance('East Hyrule Teleporter'), lambda state: state.has('Hammer') and state.can_lift_rocks() and state.has_Pearl()) # bunny cannot use hammer
set_rule(world.get_entrance('South Hyrule Teleporter'), lambda state: state.has('Hammer') and state.can_lift_rocks() and state.has_Pearl()) # bunny cannot use hammer
set_rule(world.get_entrance('Kakariko Teleporter'), lambda state: ((state.has('Hammer') and state.can_lift_rocks()) or state.can_lift_heavy_rocks()) and state.has_Pearl()) # bunny cannot lift bushes
set_rule(world.get_location('Flute Spot'), lambda state: state.has('Shovel'))
set_rule(world.get_location('Purple Chest'), lambda state: state.can_reach('Blacksmith', 'Location')) # Can S&Q with chest
@ -109,6 +119,7 @@ def global_rules(world):
set_rule(world.get_location('Master Sword Pedestal'), lambda state: state.has('Red Pendant') and state.has('Blue Pendant') and state.has('Green Pendant'))
set_rule(world.get_location('Sahasrahla'), lambda state: state.has('Green Pendant'))
set_rule(world.get_entrance('Agahnims Tower'), lambda state: state.has('Cape') or state.has_beam_sword() or state.has('Beat Agahnim 1')) # barrier gets removed after killing agahnim, relevant for entrance shuffle
# FIXME: VT has a can_kill_most_things(8) call on Aga Tower's entrance. I think this is supposed to reflect that a better weapon than 10 bombs is needed to reach the two chests in this tower
set_rule(world.get_entrance('Agahnim 1'), lambda state: state.has_sword() and state.has('Small Key (Agahnims Tower)', 2))
set_rule(world.get_location('Castle Tower - Dark Maze'), lambda state: state.has('Small Key (Agahnims Tower)'))
set_rule(world.get_entrance('Top of Pyramid'), lambda state: state.has('Beat Agahnim 1'))
@ -123,66 +134,78 @@ def global_rules(world):
set_rule(world.get_location('Ether Tablet'), lambda state: state.has('Book of Mudora') and state.has_beam_sword())
set_rule(world.get_entrance('East Death Mountain (Top)'), lambda state: state.has('Hammer'))
set_rule(world.get_location('Catfish'), lambda state: state.has_Pearl() and state.can_lift_rocks())
set_rule(world.get_entrance('Dark Lake Hylia Fairy'), lambda state: state.has_Pearl())
set_rule(world.get_entrance('Palace of Darkness Hint'), lambda state: state.has_Pearl())
set_rule(world.get_entrance('East Dark World Hint'), lambda state: state.has_Pearl())
set_rule(world.get_location('Catfish'), lambda state: state.can_lift_rocks())
set_rule(world.get_entrance('Dark World Potion Shop'), lambda state: state.has_Pearl() and (state.can_lift_rocks() or state.has('Hammer') or state.has('Flippers')))
set_rule(world.get_entrance('South Dark World Bridge'), lambda state: state.has('Hammer') and state.has_Pearl())
set_rule(world.get_entrance('Bonk Fairy (Dark)'), lambda state: state.has_Boots())
set_rule(world.get_entrance('Bonk Fairy (Dark)'), lambda state: state.has_Pearl() and state.has_Boots())
set_rule(world.get_entrance('West Dark World Gap'), lambda state: state.has_Pearl() and state.has('Hookshot') and (state.has('Flippers') or state.has('Hammer') or state.can_lift_rocks()))
set_rule(world.get_entrance('Palace of Darkness'), lambda state: state.has_Pearl())
set_rule(world.get_entrance('Palace of Darkness'), lambda state: state.has_Pearl()) # kiki needs pearl
set_rule(world.get_entrance('Hyrule Castle Ledge Mirror Spot'), lambda state: state.has_Mirror())
set_rule(world.get_entrance('Hyrule Castle Main Gate'), lambda state: state.has_Mirror())
set_rule(world.get_entrance('Dark Lake Hylia Drop (East)'), lambda state: (state.has_Pearl() and state.has('Flippers') or state.has_Mirror())) # Overworld Bunny Revival
set_rule(world.get_location('Bombos Tablet'), lambda state: state.has('Book of Mudora') and state.has_beam_sword() and state.has_Mirror())
set_rule(world.get_entrance('Dark Lake Hylia Drop (South)'), lambda state: state.has('Flippers')) # ToDo any fake flipper set up?
set_rule(world.get_entrance('Dark Lake Hylia Ledge Spike Cave'), lambda state: state.can_lift_rocks())
set_rule(world.get_entrance('Dark Lake Hylia Ledge'), lambda state: state.has_Pearl()) # To avoid Bunny nonsense for now
set_rule(world.get_entrance('Dark Lake Hylia Drop (South)'), lambda state: state.has_Pearl() and state.has('Flippers')) # ToDo any fake flipper set up?
set_rule(world.get_entrance('Dark Lake Hylia Ledge Fairy'), lambda state: state.has_Pearl()) # bomb required
set_rule(world.get_entrance('Dark Lake Hylia Ledge Spike Cave'), lambda state: state.can_lift_rocks() and state.has_Pearl())
set_rule(world.get_entrance('Dark Lake Hylia Teleporter'), lambda state: state.has_Pearl() and (state.has('Hammer') or state.can_lift_rocks())) # Fake Flippers
set_rule(world.get_entrance('Village of Outcasts Heavy Rock'), lambda state: state.can_lift_heavy_rocks())
set_rule(world.get_entrance('Village of Outcasts Heavy Rock'), lambda state: state.has_Pearl() and state.can_lift_heavy_rocks())
set_rule(world.get_entrance('Hype Cave'), lambda state: state.has_Pearl()) # bomb required
set_rule(world.get_entrance('Brewery'), lambda state: state.has_Pearl()) # bomb required
set_rule(world.get_entrance('Thieves Town'), lambda state: state.has_Pearl()) # bunny cannot pull
set_rule(world.get_entrance('Skull Woods First Section Hole (North)'), lambda state: state.has_Pearl()) # bunny cannot lift bush
set_rule(world.get_entrance('Maze Race Mirror Spot'), lambda state: state.has_Mirror())
set_rule(world.get_entrance('Cave 45'), lambda state: state.has_Mirror())
set_rule(world.get_entrance('East Dark World Bridge'), lambda state: state.has('Hammer'))
set_rule(world.get_entrance('Lake Hylia Island Mirror Spot'), lambda state: state.has_Mirror() and state.has('Flippers'))
set_rule(world.get_entrance('East Dark World Bridge'), lambda state: state.has_Pearl() and state.has('Hammer'))
set_rule(world.get_entrance('Lake Hylia Island Mirror Spot'), lambda state: state.has_Pearl() and state.has_Mirror() and state.has('Flippers'))
set_rule(world.get_entrance('Lake Hylia Central Island Mirror Spot'), lambda state: state.has_Mirror())
set_rule(world.get_entrance('East Dark World River Pier'), lambda state: state.has('Flippers')) # ToDo any fake flipper set up?
set_rule(world.get_entrance('Graveyard Cave'), lambda state: state.has_Mirror())
set_rule(world.get_entrance('Bumper Cave (Bottom)'), lambda state: state.can_lift_rocks())
set_rule(world.get_entrance('East Dark World River Pier'), lambda state: state.has_Pearl() and state.has('Flippers')) # ToDo any fake flipper set up?
set_rule(world.get_entrance('Graveyard Cave'), lambda state: state.has_Pearl() and state.has_Mirror())
set_rule(world.get_entrance('Bumper Cave (Bottom)'), lambda state: state.has_Pearl() and state.can_lift_rocks())
set_rule(world.get_entrance('Bumper Cave Ledge Mirror Spot'), lambda state: state.has_Mirror())
set_rule(world.get_entrance('Bat Cave Drop Ledge Mirror Spot'), lambda state: state.can_lift_heavy_rocks() and state.has_Mirror())
set_rule(world.get_entrance('Dark World Hammer Peg Cave'), lambda state: state.can_lift_heavy_rocks() and state.has('Hammer'))
set_rule(world.get_entrance('Dark World Shop'), lambda state: state.has('Hammer'))
set_rule(world.get_entrance('Bat Cave Drop Ledge Mirror Spot'), lambda state: state.has_Pearl() and state.can_lift_heavy_rocks() and state.has_Mirror())
set_rule(world.get_entrance('Dark World Hammer Peg Cave'), lambda state: state.has_Pearl() and state.can_lift_heavy_rocks() and state.has('Hammer'))
set_rule(world.get_entrance('Dark World Shop'), lambda state: state.has_Pearl() and state.has('Hammer'))
set_rule(world.get_entrance('Bumper Cave Exit (Top)'), lambda state: state.has('Cape'))
set_rule(world.get_entrance('Bumper Cave Exit (Bottom)'), lambda state: state.has('Cape') or state.has('Hookshot'))
set_rule(world.get_entrance('Skull Woods Final Section'), lambda state: state.has('Fire Rod'))
set_rule(world.get_entrance('Skull Woods Final Section'), lambda state: state.has('Fire Rod') and state.has_Pearl()) # bunny cannot use fire rod
set_rule(world.get_entrance('Misery Mire'), lambda state: state.has_Pearl() and state.has_sword() and state.has_misery_mire_medallion()) # sword required to cast magic (!)
set_rule(world.get_entrance('Desert Ledge (West) Mirror Spot'), lambda state: state.has_Mirror())
set_rule(world.get_entrance('Desert Ledge Mirror Spot'), lambda state: state.has_Mirror())
set_rule(world.get_entrance('Desert Palace Stairs Mirror Spot'), lambda state: state.has_Mirror())
set_rule(world.get_entrance('Desert Palace Entrance (North) Mirror Spot'), lambda state: state.has_Mirror())
set_rule(world.get_entrance('Mire Shed'), lambda state: state.has_Pearl()) # ToDo Bunny Revival can give access to this cave in super bunny state. Not sure how to deal with shuffled entrances, as much easier to block of cave entrances than individual shuffled chests
set_rule(world.get_entrance('Dark Desert Hint'), lambda state: state.has_Pearl()) # ToDo Bunny Revival can give access to this cave in super bunny state. Not sure how to deal with shuffled entrances, as much easier to block of cave entrances than individual shuffled chests
set_rule(world.get_entrance('Dark Desert Fairy'), lambda state: state.has_Pearl()) # ToDo Bunny Revival can give access to this cave in super bunny state. Not sure how to deal with shuffled entrances, as much easier to block of cave entrances than individual shuffled chests
set_rule(world.get_entrance('Spike Cave'), lambda state: state.has_Pearl())
set_rule(world.get_entrance('Dark Death Mountain Fairy'), lambda state: state.has_Pearl())
set_rule(world.get_entrance('Spectacle Rock Mirror Spot'), lambda state: state.has_Mirror())
set_rule(world.get_entrance('Ganons Tower'), lambda state: state.has('Crystal 1') and state.has('Crystal 2') and state.has('Crystal 3') and state.has('Crystal 4') and state.has('Crystal 5') and state.has('Crystal 6') and state.has('Crystal 7'))
set_rule(world.get_entrance('Hookshot Cave'), lambda state: state.can_lift_rocks() and state.has_Pearl())
set_rule(world.get_entrance('East Death Mountain (Top) Mirror Spot'), lambda state: state.has_Mirror())
set_rule(world.get_entrance('Mimic Cave Mirror Spot'), lambda state: state.has_Mirror())
set_rule(world.get_entrance('Spiral Cave Mirror Spot'), lambda state: state.has_Mirror())
set_rule(world.get_entrance('Fairy Ascension Mirror Spot'), lambda state: state.has_Mirror() and state.has_Pearl()) # need to lift flowers
set_rule(world.get_entrance('Isolated Ledge Mirror Spot'), lambda state: state.has_Mirror())
set_rule(world.get_entrance('Superbunny Cave (Top)'), lambda state: state.has_Pearl()) # Chests inside could be collected with super bunny, but may be shuffled. rather limit access for now ToDo
set_rule(world.get_entrance('Superbunny Cave (Bottom)'), lambda state: state.has_Pearl())
set_rule(world.get_entrance('Cave Shop (Dark Death Mountain)'), lambda state: state.has_Pearl()) # just for save bunny algo for now
set_rule(world.get_entrance('Superbunny Cave Exit (Bottom)'), lambda state: False) # Cannot get to bottom exit from top. Just exists for shuffling
set_rule(world.get_location('Spike Cave'), lambda state: state.has('Hammer') and state.can_lift_rocks() and (state.has('Cane of Byrna') or state.has('Cape')) and (state.has_bottle() or state.has('Half Magic') or state.has('Quarter Magic')))
set_rule(world.get_location('Spike Cave'), lambda state:
state.has('Hammer') and state.can_lift_rocks() and
(
(
state.has('Cape')
and (state.can_extend_magic(16)
or (state.can_extend_magic(12) and (state.world.can_take_damage or state.has_Boots()))
or (state.can_extend_magic(10) and state.world.can_take_damage and state.has_Boots()))
) or (
state.has('Cane of Byrna')
and (state.can_extend_magic(12)
or (state.can_extend_magic(10) and (state.has_Boots() or state.world.can_take_damage))
or (state.world.can_take_damage and (state.has_Boots() or state.has_hearts(4)))))
)
)
set_rule(world.get_location('Hookshot Cave - Top Right'), lambda state: state.has('Hookshot'))
set_rule(world.get_location('Hookshot Cave - Top Left'), lambda state: state.has('Hookshot'))
set_rule(world.get_location('Hookshot Cave - Bottom Right'), lambda state: state.has('Hookshot') or state.has('Pegasus Boots'))
set_rule(world.get_location('Hookshot Cave - Bottom Left'), lambda state: state.has('Hookshot'))
set_rule(world.get_location('Floating Island'), lambda state: state.has_Mirror())
set_rule(world.get_entrance('Floating Island Mirror Spot'), lambda state: state.has_Mirror())
set_rule(world.get_entrance('Turtle Rock'), lambda state: state.has_Pearl() and state.has_sword() and state.has_turtle_rock_medallion() and state.can_reach('Turtle Rock (Top)', 'Region')) # sword required to cast magic (!)
set_rule(world.get_location('Mimic Cave'), lambda state: state.has('Hammer'))
@ -208,10 +231,11 @@ def global_rules(world):
for location in ['Desert Palace - Lanmolas', 'Desert Palace - Big Key Chest', 'Desert Palace - Compass Chest']:
forbid_item(world.get_location(location), 'Small Key (Desert Palace)')
set_rule(world.get_entrance('Tower of Hera Small Key Door'), lambda state: state.has('Small Key (Tower of Hera)'))
set_rule(world.get_entrance('Tower of Hera Small Key Door'), lambda state: state.has('Small Key (Tower of Hera)') or item_name(state, 'Tower of Hera - Big Key Chest') == 'Small Key (Tower of Hera)')
set_rule(world.get_entrance('Tower of Hera Big Key Door'), lambda state: state.has('Big Key (Tower of Hera)'))
set_rule(world.get_location('Tower of Hera - Big Chest'), lambda state: state.has('Big Key (Tower of Hera)'))
set_rule(world.get_location('Tower of Hera - Big Key Chest'), lambda state: state.has_fire_source())
set_always_allow(world.get_location('Tower of Hera - Big Key Chest'), lambda state, item: item.name == 'Small Key (Tower of Hera)')
set_rule(world.get_location('Tower of Hera - Moldorm'), lambda state: state.has_blunt_weapon())
set_rule(world.get_location('Tower of Hera - Prize'), lambda state: state.has_blunt_weapon())
for location in ['Tower of Hera - Moldorm', 'Tower of Hera - Big Chest', 'Tower of Hera - Compass Chest']:
@ -222,44 +246,45 @@ def global_rules(world):
set_rule(world.get_entrance('Swamp Palace Moat'), lambda state: state.has('Flippers') and state.can_reach('Dam'))
set_rule(world.get_entrance('Swamp Palace Small Key Door'), lambda state: state.has('Small Key (Swamp Palace)'))
set_rule(world.get_entrance('Swamp Palace (Center)'), lambda state: state.has('Hammer'))
set_rule(world.get_location('Swamp Palace - Big Chest'), lambda state: state.has('Big Key (Swamp Palace)'))
set_rule(world.get_location('Swamp Palace - Big Chest'), lambda state: state.has('Big Key (Swamp Palace)') or item_name(state, 'Swamp Palace - Big Chest') == 'Big Key (Swamp Palace)')
set_always_allow(world.get_location('Swamp Palace - Big Chest'), lambda state, item: item.name == 'Big Key (Swamp Palace)')
set_rule(world.get_entrance('Swamp Palace (North)'), lambda state: state.has('Hookshot'))
set_rule(world.get_location('Swamp Palace - Arrghus'), lambda state: state.has_blunt_weapon())
set_rule(world.get_location('Swamp Palace - Prize'), lambda state: state.has_blunt_weapon())
for location in ['Swamp Palace - Big Chest', 'Swamp Palace - Entrance']:
for location in ['Swamp Palace - Entrance']:
forbid_item(world.get_location(location), 'Big Key (Swamp Palace)')
set_rule(world.get_entrance('Thieves Town Big Key Door'), lambda state: state.has('Big Key (Thieves Town)'))
set_rule(world.get_entrance('Blind Fight'), lambda state: state.has('Small Key (Thieves Town)') and (state.has_blunt_weapon() or state.has('Cane of Somaria') or state.has('Cane of Byrna')))
set_rule(world.get_location('Thieves\' Town - Big Chest'), lambda state: state.has('Small Key (Thieves Town)') and state.has('Hammer'))
set_rule(world.get_location('Thieves\' Town - Big Chest'), lambda state: (state.has('Small Key (Thieves Town)') or item_name(state, 'Thieves\' Town - Big Chest') == 'Small Key (Thieves Town)') and state.has('Hammer'))
set_always_allow(world.get_location('Thieves\' Town - Big Chest'), lambda state, item: item.name == 'Small Key (Thieves Town)')
set_rule(world.get_location('Thieves\' Town - Attic'), lambda state: state.has('Small Key (Thieves Town)'))
for location in ['Thieves\' Town - Attic', 'Thieves\' Town - Big Chest', 'Thieves\' Town - Blind\'s Cell', 'Thieves Town - Blind']:
forbid_item(world.get_location(location), 'Big Key (Thieves Town)')
for location in ['Thieves\' Town - Attic', 'Thieves\' Town - Big Chest', 'Thieves Town - Blind']:
for location in ['Thieves\' Town - Attic', 'Thieves Town - Blind']:
forbid_item(world.get_location(location), 'Small Key (Thieves Town)')
set_rule(world.get_entrance('Skull Woods First Section South Door'), lambda state: state.has('Small Key (Skull Woods)'))
set_rule(world.get_entrance('Skull Woods First Section (Right) North Door'), lambda state: state.has('Small Key (Skull Woods)'))
set_rule(world.get_entrance('Skull Woods First Section West Door'), lambda state: state.has('Small Key (Skull Woods)', 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'), lambda state: state.has('Small Key (Skull Woods)', 2))
set_rule(world.get_location('Skull Woods - Big Chest'), lambda state: state.has('Big Key (Skull Woods)'))
set_rule(world.get_location('Skull Woods - Big Chest'), lambda state: state.has('Big Key (Skull Woods)') or item_name(state, 'Skull Woods - Big Chest') == 'Big Key (Skull Woods)')
set_always_allow(world.get_location('Skull Woods - Big Chest'), lambda state, item: item.name == 'Big Key (Skull Woods)')
set_rule(world.get_entrance('Skull Woods Torch Room'), lambda state: state.has('Small Key (Skull Woods)', 3) and state.has('Fire Rod') and state.has_sword()) # sword required for curtain
for location in ['Skull Woods - Big Chest']:
forbid_item(world.get_location(location), 'Big Key (Skull Woods)')
for location in ['Skull Woods - Mothula']:
forbid_item(world.get_location(location), 'Small Key (Skull Woods)')
set_rule(world.get_entrance('Ice Palace Entrance Room'), lambda state: state.has('Fire Rod') or (state.has('Bombos') and state.has_sword()))
set_rule(world.get_location('Ice Palace - Big Chest'), lambda state: state.has('Big Key (Ice Palace)'))
set_rule(world.get_entrance('Ice Palace (Kholdstare)'), lambda state: state.can_lift_rocks() and state.has('Hammer') and state.has('Big Key (Ice Palace)') and (state.has('Small Key (Ice Palace)', 2) or (state.has('Cane of Somaria') and state.has('Small Key (Ice Palace)', 1))))
set_rule(world.get_entrance('Ice Palace (East)'), lambda state: (state.has('Hookshot') or (item_in_locations(state, 'Big Key (Ice Palace)', ['Ice Palace - Spike Room', 'Ice Palace - Big Key Chest', 'Ice Palace - Map Chest']) and state.has('Small Key (Ice Palace)')) or state.has('Small Key (Ice Palace)', 2)) and (state.has('Hookshot') or state.has('Cape') or state.has('Cane of Byrna')))
set_rule(world.get_entrance('Ice Palace (East)'), lambda state: (state.has('Hookshot') or (item_in_locations(state, 'Big Key (Ice Palace)', ['Ice Palace - Spike Room', 'Ice Palace - Big Key Chest', 'Ice Palace - Map Chest']) and state.has('Small Key (Ice Palace)'))) and (state.world.can_take_damage or state.has('Hookshot') or state.has('Cape') or state.has('Cane of Byrna')))
set_rule(world.get_entrance('Ice Palace (East Top)'), lambda state: state.can_lift_rocks() and state.has('Hammer'))
for location in ['Ice Palace - Big Chest', 'Ice Palace - Kholdstare']:
forbid_item(world.get_location(location), 'Big Key (Ice Palace)')
set_rule(world.get_entrance('Misery Mire Entrance Gap'), lambda state: (state.has_Boots() or state.has('Hookshot')) and (state.has_sword() or state.has('Fire Rod') or state.has('Ice Rod') or state.has('Hammer') or state.has('Cane of Somaria') or state.has('Bow'))) # need to defeat wizzrobes, bombs don't work ...
set_rule(world.get_location('Misery Mire - Big Chest'), lambda state: state.has('Big Key (Misery Mire)'))
set_rule(world.get_location('Misery Mire - Spike Chest'), lambda state: state.has('Cane of Byrna') or state.has('Cape'))
set_rule(world.get_location('Misery Mire - Spike Chest'), lambda state: (state.world.can_take_damage and state.has_hearts(4)) or state.has('Cane of Byrna') or state.has('Cape'))
set_rule(world.get_entrance('Misery Mire Big Key Door'), lambda state: state.has('Big Key (Misery Mire)'))
# you can squander the free small key from the pot by opening the south door to the north west switch room, locking you out of accessing a color switch ...
# big key gives backdoor access to that from the teleporter in the north west
@ -267,8 +292,8 @@ def global_rules(world):
# in addition, you can open the door to the map room before getting access to a color switch, so this is locked behing 2 small keys or the big key...
set_rule(world.get_location('Misery Mire - Main Lobby'), lambda state: state.has('Small Key (Misery Mire)', 2) or state.has('Big Key (Misery Mire)'))
# we can place a small key in the West wing iff it also contains/blocks the Big Key, as we cannot reach and softlock with the basement key door yet
set_rule(world.get_entrance('Misery Mire (West)'), lambda state: state.has('Small Key (Misery Mire)', 2) if ((state.world.get_location('Misery Mire - Compass Chest').item is not None and state.world.get_location('Misery Mire - Compass Chest').item.name in ['Big Key (Misery Mire)']) or
(state.world.get_location('Misery Mire - Big Key Chest').item is not None and state.world.get_location('Misery Mire - Big Key Chest').item.name in ['Big Key (Misery Mire)'])) else state.has('Small Key (Misery Mire)', 3))
set_rule(world.get_entrance('Misery Mire (West)'), lambda state: state.has('Small Key (Misery Mire)', 2) if ((item_name(state, 'Misery Mire - Compass Chest') in ['Big Key (Misery Mire)']) or
(item_name(state, 'Misery Mire - Big Key Chest') in ['Big Key (Misery Mire)'])) else state.has('Small Key (Misery Mire)', 3))
set_rule(world.get_location('Misery Mire - Compass Chest'), lambda state: state.has_fire_source())
set_rule(world.get_location('Misery Mire - Big Key Chest'), lambda state: state.has_fire_source())
set_rule(world.get_entrance('Misery Mire (Vitreous)'), lambda state: state.has('Cane of Somaria') and (state.has('Bow') or state.has_blunt_weapon()))
@ -291,31 +316,23 @@ def global_rules(world):
set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Left'), lambda state: state.has('Cane of Byrna') or state.has('Cape') or state.has('Mirror Shield'))
set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Right'), lambda state: state.has('Cane of Byrna') or state.has('Cape') or state.has('Mirror Shield'))
set_rule(world.get_entrance('Turtle Rock (Trinexx)'), lambda state: state.has('Small Key (Turtle Rock)', 4) and state.has('Big Key (Turtle Rock)') and state.has('Cane of Somaria') and state.has('Fire Rod') and state.has('Ice Rod') and
(state.has('Hammer') or state.has_beam_sword() or state.has_bottle() or state.has('Half Magic') or state.has('Quarter Magic')))
(state.has('Hammer') or state.has_beam_sword() or (state.has_sword and state.can_extend_magic(32))))
# TODO: Per VT, possibly allow a regular sword with 4x extended magic (ie. quater magic, or half magic+bottle or 3 bottles)
set_trock_key_rules(world)
set_rule(world.get_entrance('Palace of Darkness Bonk Wall'), lambda state: state.has('Bow'))
set_rule(world.get_entrance('Palace of Darkness Hammer Peg Drop'), lambda state: state.has('Hammer'))
set_rule(world.get_entrance('Palace of Darkness Bridge Room'), lambda state: state.has('Small Key (Palace of Darkness)', 1)) # If we can reach any other small key door, we already have back door access to this area
set_rule(world.get_entrance('Palace of Darkness Big Key Door'), lambda state: state.has('Small Key (Palace of Darkness)', 6) and state.has('Big Key (Palace of Darkness)') and state.has('Bow') and state.has('Hammer'))
set_rule(world.get_entrance('Palace of Darkness Big Key Chest Staircase'), lambda state: state.has('Small Key (Palace of Darkness)', 5) or (state.world.get_location('Palace of Darkness - Big Key Chest').item is not None and (state.world.get_location('Palace of Darkness - Big Key Chest').item.name in ['Small Key (Palace of Darkness)'])))
set_rule(world.get_entrance('Palace of Darkness (North)'), lambda state: state.has('Small Key (Palace of Darkness)', 4))
set_rule(world.get_location('Palace of Darkness - Big Chest'), lambda state: state.has('Big Key (Palace of Darkness)'))
if world.keysanity:
set_rule(world.get_entrance('Palace of Darkness Spike Statue Room Door'), lambda state: state.has('Small Key (Palace of Darkness)', 6) or (state.world.get_location('Palace of Darkness - Harmless Hellway').item is not None and (state.world.get_location('Palace of Darkness - Harmless Hellway').item.name in ['Small Key (Palace of Darkness)'])))
set_rule(world.get_entrance('Palace of Darkness Big Key Chest Staircase'), lambda state: state.has('Small Key (Palace of Darkness)', 6) or (state.world.get_location('Palace of Darkness - Big Key Chest').item is not None and (state.world.get_location('Palace of Darkness - Big Key Chest').item.name in ['Small Key (Palace of Darkness)'])))
set_rule(world.get_entrance('Palace of Darkness Maze Door'), lambda state: state.has('Small Key (Palace of Darkness)', 6))
else:
set_rule(world.get_entrance('Palace of Darkness Spike Statue Room Door'), lambda state: state.has('Small Key (Palace of Darkness)', 5) or (state.world.get_location('Palace of Darkness - Harmless Hellway').item is not None and (state.world.get_location('Palace of Darkness - Harmless Hellway').item.name in ['Small Key (Palace of Darkness)'])))
set_rule(world.get_entrance('Palace of Darkness Big Key Chest Staircase'), lambda state: state.has('Small Key (Palace of Darkness)', 5) or (state.world.get_location('Palace of Darkness - Big Key Chest').item is not None and (state.world.get_location('Palace of Darkness - Big Key Chest').item.name in ['Small Key (Palace of Darkness)'])))
set_rule(world.get_entrance('Palace of Darkness Maze Door'), lambda state: state.has('Small Key (Palace of Darkness)', 5))
set_rule(world.get_entrance('Palace of Darkness Big Key Chest Staircase'), lambda state: state.has('Small Key (Palace of Darkness)', 6) or (item_name(state, 'Palace of Darkness - Big Key Chest') in ['Small Key (Palace of Darkness)'] and state.has('Small Key (Palace of Darkness)', 3)))
set_always_allow(world.get_location('Palace of Darkness - Big Key Chest'), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and state.has('Small Key (Palace of Darkness)', 5))
for location in ['Palace of Darkness - Big Chest', 'Palace of Darkness - Helmasaur']:
forbid_item(world.get_location(location), 'Big Key (Palace of Darkness)')
for location in ['Palace of Darkness - Big Chest', 'Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom']:
forbid_item(world.get_location(location), 'Small Key (Palace of Darkness)')
set_rule(world.get_entrance('Palace of Darkness Spike Statue Room Door'), lambda state: state.has('Small Key (Palace of Darkness)', 6) or (item_name(state, 'Palace of Darkness - Harmless Hellway') in ['Small Key (Palace of Darkness)'] and state.has('Small Key (Palace of Darkness)', 4)))
set_always_allow(world.get_location('Palace of Darkness - Harmless Hellway'), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and state.has('Small Key (Palace of Darkness)', 5))
set_rule(world.get_entrance('Palace of Darkness Maze Door'), lambda state: state.has('Small Key (Palace of Darkness)', 6))
# these key rules are conservative, you might be able to get away with more lenient rules
randomizer_room_chests = ['Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right', 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right']
@ -324,13 +341,12 @@ def global_rules(world):
set_rule(world.get_location('Ganons Tower - Bob\'s Torch'), lambda state: state.has_Boots())
set_rule(world.get_entrance('Ganons Tower (Tile Room)'), lambda state: state.has('Cane of Somaria'))
set_rule(world.get_entrance('Ganons Tower (Hookshot Room)'), lambda state: state.has('Hammer'))
if world.keysanity:
set_rule(world.get_entrance('Ganons Tower (Map Room)'), lambda state: state.has('Small Key (Ganons Tower)', 4) or (state.world.get_location('Ganons Tower - Map Chest').item is not None and state.world.get_location('Ganons Tower - Map Chest').item.name == 'Big Key (Ganons Tower)' and state.has('Small Key (Ganons Tower)', 3)) or (state.world.get_location('Ganons Tower - Map Chest').item is not None and state.world.get_location('Ganons Tower - Map Chest').item.name == 'Small Key (Ganons Tower)'))
else:
set_rule(world.get_entrance('Ganons Tower (Map Room)'), lambda state: state.has('Small Key (Ganons Tower)', 3) or (state.world.get_location('Ganons Tower - Map Chest').item is not None and state.world.get_location('Ganons Tower - Map Chest').item.name == 'Small Key (Ganons Tower)'))
# It is possible to need more than 2 keys to get through this entance if you spend keys elsewhere We reflect this in the chest requirements.
# However we need to leave these at the lower values derive that with 3 keys it is always possible to reach Bob and Ice Armos.
set_rule(world.get_entrance('Ganons Tower (Map Room)'), lambda state: state.has('Small Key (Ganons Tower)', 4) or (item_name(state, 'Ganons Tower - Map Chest') in ['Big Key (Ganons Tower)', 'Small Key (Ganons Tower)'] and state.has('Small Key (Ganons Tower)', 3)))
set_always_allow(world.get_location('Ganons Tower - Map Chest'), lambda state, item: item.name == 'Small Key (Ganons Tower)' and state.has('Small Key (Ganons Tower)', 3))
# It is possible to need more than 2 keys to get through this entance if you spend keys elsewhere. We reflect this in the chest requirements.
# However we need to leave these at the lower values to derive that with 3 keys it is always possible to reach Bob and Ice Armos.
set_rule(world.get_entrance('Ganons Tower (Double Switch Room)'), lambda state: state.has('Small Key (Ganons Tower)', 2))
# It is possible to need more than 3 keys ....
set_rule(world.get_entrance('Ganons Tower (Firesnake Room)'), lambda state: state.has('Small Key (Ganons Tower)', 3))
@ -354,17 +370,16 @@ def global_rules(world):
set_rule(world.get_entrance('Ganons Tower Torch Rooms'), lambda state: state.has_fire_source())
set_rule(world.get_location('Ganons Tower - Pre-Moldorm Chest'), lambda state: state.has('Small Key (Ganons Tower)', 3))
set_rule(world.get_entrance('Ganons Tower Moldorm Door'), lambda state: state.has('Small Key (Ganons Tower)', 4))
set_rule(world.get_entrance('Ganons Tower Moldorm Gap'), lambda state: state.has('Hookshot'))
set_rule(world.get_entrance('Ganons Tower Moldorm Gap'), lambda state: state.has('Hookshot') and state.has_blunt_weapon())
set_rule(world.get_location('Agahnim 2'), lambda state: state.has_sword() or state.has('Hammer') or state.has('Bug Catching Net'))
set_rule(world.get_entrance('Pyramid Hole'), lambda state: state.has('Beat Agahnim 2') and state.has_Pearl())
set_rule(world.get_entrance('Pyramid Entrance'), lambda state: state.has_Pearl())
set_rule(world.get_entrance('Pyramid Hole'), lambda state: state.has('Beat Agahnim 2'))
for location in ['Ganons Tower - Big Chest', 'Ganons Tower - Mini Helmasaur Room - Left', 'Ganons Tower - Mini Helmasaur Room - Right',
'Ganons Tower - Pre-Moldorm Chest', 'Ganons Tower - Validation Chest']:
forbid_item(world.get_location(location), 'Big Key (Ganons Tower)')
set_rule(world.get_location('Ganon'), lambda state: state.has_beam_sword() and state.has_fire_source() and state.has('Crystal 1') and state.has('Crystal 2')
and state.has('Crystal 3') and state.has('Crystal 4') and state.has('Crystal 5') and state.has('Crystal 6') and state.has('Crystal 7')
and (state.has('Tempered Sword') or state.has('Golden Sword') or (state.has('Silver Arrows') and state.has('Bow')) or state.has('Lamp') or state.has_bottle() or state.has('Half Magic') or state.has('Quarter Magic'))) # need to light torch a sufficient amount of times
and (state.has('Tempered Sword') or state.has('Golden Sword') or (state.has('Silver Arrows') and state.has('Bow')) or state.has('Lamp') or state.can_extend_magic(12))) # need to light torch a sufficient amount of times
set_rule(world.get_entrance('Ganon Drop'), lambda state: state.has_beam_sword()) # need to damage ganon to get tiles to drop
@ -372,10 +387,9 @@ def no_glitches_rules(world):
set_rule(world.get_entrance('Zoras River'), lambda state: state.has('Flippers') or state.can_lift_rocks())
set_rule(world.get_entrance('Lake Hylia Central Island Pier'), lambda state: state.has('Flippers')) # can be fake flippered to
set_rule(world.get_entrance('Hobo Bridge'), lambda state: state.has('Flippers'))
add_rule(world.get_entrance('Ice Palace'), lambda state: state.has_Pearl())
set_rule(world.get_entrance('Dark Lake Hylia Drop (East)'), lambda state: state.has_Pearl() and state.has('Flippers'))
set_rule(world.get_entrance('Dark Lake Hylia Teleporter'), lambda state: state.has_Pearl() and state.has('Flippers') and (state.has('Hammer') or state.can_lift_rocks()))
set_rule(world.get_entrance('Dark Lake Hylia Ledge Drop'), lambda state: state.has('Flippers'))
set_rule(world.get_entrance('Dark Lake Hylia Ledge Drop'), lambda state: state.has_Pearl() and state.has('Flippers'))
add_rule(world.get_entrance('Ganons Tower (Hookshot Room)'), lambda state: state.has('Hookshot'))
set_rule(world.get_entrance('Paradox Cave Push Block Reverse'), lambda state: False) # no glitches does not require block override
set_rule(world.get_entrance('Paradox Cave Bomb Jump'), lambda state: False)
@ -430,27 +444,25 @@ def open_rules(world):
forbid_item(world.get_location('Hyrule Castle - Boomerang Chest'), 'Small Key (Escape)')
forbid_item(world.get_location('Hyrule Castle - Zelda\'s Chest'), 'Small Key (Escape)')
# to prevent key-lock in keysanity we need to prevent these chests from having an item that
# blocks the small key
if (world.keysanity):
set_rule(world.get_location('Hyrule Castle - Boomerang Chest'), lambda state: state.has('Small Key (Escape)'))
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest'), lambda state: state.has('Small Key (Escape)'))
set_rule(world.get_location('Hyrule Castle - Boomerang Chest'), lambda state: state.has('Small Key (Escape)'))
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest'), lambda state: state.has('Small Key (Escape)'))
def swordless_rules(world):
# for the time being swordless mode just inhierits all fixes from open mode.
# should there ever be fixes that apply to open mode but not swordless, this
# can be revisited.
open_rules(world)
# for the time being swordless mode just inhierits all fixes from open mode.
# should there ever be fixes that apply to open mode but not swordless, this
# can be revisited.
open_rules(world)
set_rule(world.get_entrance('Agahnims Tower'), lambda state: state.has('Cape') or state.has('Hammer') or state.has('Beat Agahnim 1')) # barrier gets removed after killing agahnim, relevant for entrance shuffle
set_rule(world.get_entrance('Agahnim 1'), lambda state: state.has('Hammer') or state.has('Bug Catching Net') and state.has('Small Key (Agahnims Tower)', 2))
set_rule(world.get_entrance('Agahnim 1'), lambda state: (state.has('Hammer') or (state.has('Bug Catching Net') and (state.has('Fire Rod') or state.has('Bow') or state.has('Cane of Somaria')))) and state.has('Small Key (Agahnims Tower)', 2))
set_rule(world.get_location('Ether Tablet'), lambda state: state.has('Book of Mudora') and state.has('Hammer'))
set_rule(world.get_location('Bombos Tablet'), lambda state: state.has('Book of Mudora') and state.has('Hammer') and state.has_Mirror())
set_rule(world.get_entrance('Misery Mire'), lambda state: state.has_Pearl() and state.has_misery_mire_medallion()) # sword not required to use medallion for opening in swordless (!)
set_rule(world.get_entrance('Turtle Rock'), lambda state: state.has_Pearl() and state.has_turtle_rock_medallion() and state.can_reach('Turtle Rock (Top)', 'Region')) # sword not required to use medallion for opening in swordless (!)
set_rule(world.get_entrance('Skull Woods Torch Room'), lambda state: state.has('Small Key (Skull Woods)', 3) and state.has('Fire Rod')) # no curtain
set_rule(world.get_entrance('Ice Palace Entrance Room'), lambda state: state.has('Fire Rod') or state.has('Bombos')) #in swordless mode bombos pads are present in the relevant parts of ice palace
set_rule(world.get_location('Agahnim 2'), lambda state: state.has('Hammer') or state.has('Bug Catching Net'))
set_rule(world.get_location('Ganon'), lambda state: state.has('Hammer') and state.has_fire_source() and state.has('Silver Arrows') and state.has('Bow') and state.has('Crystal 1') and state.has('Crystal 2')
and state.has('Crystal 3') and state.has('Crystal 4') and state.has('Crystal 5') and state.has('Crystal 6') and state.has('Crystal 7'))
@ -459,10 +471,13 @@ def swordless_rules(world):
def standard_rules(world):
# easiest way to enforce key placement not relevant for open
forbid_item(world.get_location('Sewers - Secret Room - Left'), 'Small Key (Escape)')
forbid_item(world.get_location('Sewers - Secret Room - Middle'), 'Small Key (Escape)')
forbid_item(world.get_location('Sewers - Secret Room - Right'), 'Small Key (Escape)')
forbid_item(world.get_location('Sanctuary'), 'Small Key (Escape)')
set_rule(world.get_location('Sewers - Dark Cross'), lambda state: state.can_kill_most_things())
add_rule(world.get_entrance('Sewers Door'), lambda state: state.can_kill_most_things())
set_rule(world.get_location('Hyrule Castle - Boomerang Chest'), lambda state: state.can_kill_most_things())
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest'), lambda state: state.can_kill_most_things())
def set_trock_key_rules(world):
@ -487,20 +502,27 @@ def set_trock_key_rules(world):
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)'), lambda state: state.has('Small Key (Turtle Rock)', 4))
# this is just the pokey room with one more key
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)'), lambda state: state.has('Small Key (Turtle Rock)', 2)) if not can_reach_back else set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)'), lambda state: state.has('Small Key (Turtle Rock)', 3))
if not can_reach_back:
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)'), lambda state: state.has('Small Key (Turtle Rock)', 2))
else:
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)'), lambda state: state.has('Small Key (Turtle Rock)', 3))
# the most complicated one
# if we have back entrance access, we could waste all keys before touching this
# if we don't, we have access to all chests by the time we can waste a key on trinexx door
# in that case, if it contains the big key, we can also not waste a key on the roller switch door
# however in keysanity being able to reach all other chests while only having three keys does not imply this contains
# a key, so we again need all four keys unless it contains the big key
if can_reach_back:
set_rule(world.get_location('Turtle Rock - Big Key Chest'), lambda state: state.has('Small Key (Turtle Rock)', 4) or (state.world.get_location('Turtle Rock - Big Key Chest').item is not None and (state.world.get_location('Turtle Rock - Big Key Chest').item.name in ['Small Key (Turtle Rock)'])))
elif world.keysanity:
set_rule(world.get_location('Turtle Rock - Big Key Chest'), lambda state: state.has('Small Key (Turtle Rock)', 2) if (state.world.get_location('Turtle Rock - Big Key Chest').item is not None and (state.world.get_location('Turtle Rock - Big Key Chest').item.name in ['Big Key (Turtle Rock)'])) else state.has('Small Key (Turtle Rock)', 4) or (state.world.get_location('Turtle Rock - Big Key Chest').item is not None and (state.world.get_location('Turtle Rock - Big Key Chest').item.name in ['Small Key (Turtle Rock)'])))
else:
set_rule(world.get_location('Turtle Rock - Big Key Chest'), lambda state: state.has('Small Key (Turtle Rock)', 2) if (state.world.get_location('Turtle Rock - Big Key Chest').item is not None and (state.world.get_location('Turtle Rock - Big Key Chest').item.name in ['Big Key (Turtle Rock)'])) else state.has('Small Key (Turtle Rock)', 3) or (state.world.get_location('Turtle Rock - Big Key Chest').item is not None and (state.world.get_location('Turtle Rock - Big Key Chest').item.name in ['Small Key (Turtle Rock)'])))
def tr_big_key_chest_keys_needed(state):
item = item_name(state, 'Turtle Rock - Big Key Chest')
# handle key for a key situation in the usual way (by letting us logically open the door using the key locked inside it)
if item in ['Small Key (Turtle Rock)']:
return 3
# if we lack backdoor access and cannot reach the back before opening this chest because it contains the big key
# then that means there are two doors left that we cannot have spent a key on, (crystalroller and trinexx) so we only need
# two keys
if item in ['Big Key (Turtle Rock)'] and not can_reach_back:
return 2
# otherwise we could potentially have opened every other door already, so we need all 4 keys.
return 4
set_rule(world.get_location('Turtle Rock - Big Key Chest'), lambda state: state.has('Small Key (Turtle Rock)', tr_big_key_chest_keys_needed(state)))
set_always_allow(world.get_location('Turtle Rock - Big Key Chest'), lambda state, item: item.name == 'Small Key (Turtle Rock)' and state.has('Small Key (Turtle Rock)', 3))
# set big key restrictions
non_big_key_locations = ['Turtle Rock - Big Chest', 'Turtle Rock - Trinexx']
@ -582,20 +604,75 @@ def set_big_bomb_rules(world):
'Mimic Cave Mirror Spot']
Isolated_LW_entrances = ['Capacity Upgrade',
'Hookshot Fairy']
set_rule(world.get_entrance('Pyramid Fairy'), lambda state: state.has_Pearl() and state.can_reach('Big Bomb Shop', 'Region') and state.has('Crystal 5') and state.has('Crystal 6'))
set_rule(world.get_entrance('Pyramid Fairy'), lambda state: state.can_reach('East Dark World', 'Region') and state.can_reach('Big Bomb Shop', 'Region') and state.has('Crystal 5') and state.has('Crystal 6'))
if bombshop_entrance.name in Normal_LW_entrances:
add_rule(world.get_entrance('Pyramid Fairy'), lambda state: state.can_reach('Top of Pyramid', 'Entrance') or (state.has('Hammer') and state.can_lift_rocks()) or state.has_Mirror())
#1. Enter via gate: Needs Aga1
#2. south hyrule teleporter and cross peg bridge: Hammer and moon pearl
#3. Can reach Eastern dark world some other way, mirror, get bomb, return to mirror spot, walk to pyramid: Needs mirror
# -> A or (H and P)) or (M)
add_rule(world.get_entrance('Pyramid Fairy'), lambda state: state.can_reach('Top of Pyramid', 'Entrance') or (state.has('Hammer') and state.has_Pearl()) or state.has_Mirror())
elif bombshop_entrance.name in LW_walkable_entrances:
add_rule(world.get_entrance('Pyramid Fairy'), lambda state: state.has_Mirror() and (state.can_reach('Top of Pyramid', 'Entrance') or (state.has('Hammer') and state.can_lift_rocks())))
#1. Mirror then gate: Needs mirror and Aga1
#2. Mirror then go to south hyrule teleporter and cross peg bridge: Needs Mirror and Hammer and moon pearl
# -> M and (A or (H and P))
add_rule(world.get_entrance('Pyramid Fairy'), lambda state: state.has_Mirror() and (state.can_reach('Top of Pyramid', 'Entrance') or (state.has('Hammer') and state.has_Pearl())))
elif bombshop_entrance.name in Northern_DW_entrances:
add_rule(world.get_entrance('Pyramid Fairy'), lambda state: (state.can_lift_heavy_rocks() and state.has('Hammer')) or (state.has_Mirror() and (state.can_reach('Top of Pyramid', 'Entrance') or (state.has('Hammer') and state.can_lift_rocks()))))
#1. Mirror and enter via gate: Need mirror and Aga1
#2. Mirror and enter via south hyrule teleporter: Need mirror and hammer and moon pearl
#3. Go to south DW and then cross peg bridge: Need Mitts and hammer and moon pearl
# -> (Mitts and P and H) or (M and (A or (H and P)))
add_rule(world.get_entrance('Pyramid Fairy'), lambda state: (state.can_lift_heavy_rocks() and state.has_Pearl() and state.has('Hammer')) or (state.has_Mirror() and (state.can_reach('Top of Pyramid', 'Entrance') or (state.has('Hammer') and state.has_Pearl()))))
elif bombshop_entrance.name in Southern_DW_entrances:
add_rule(world.get_entrance('Pyramid Fairy'), lambda state: state.has('Hammer') or (state.has_Mirror() and (state.can_reach('Top of Pyramid', 'Entrance') or (state.has('Hammer') and state.can_lift_rocks()))))
#1. Mirror and enter via gate: Need mirror and Aga1
#2. cross peg bridge: Need hammer and moon pearl
# -> (H and P) or (M and A)
add_rule(world.get_entrance('Pyramid Fairy'), lambda state: (state.has('Hammer') and state.has_Pearl()) or (state.has_Mirror() and state.can_reach('Top of Pyramid', 'Entrance')))
elif bombshop_entrance.name in Isolated_DW_entrances:
add_rule(world.get_entrance('Pyramid Fairy'), lambda state: state.has_Mirror() and state.has('Ocarina') and (state.can_reach('Top of Pyramid', 'Entrance') or (state.has('Hammer') and state.can_lift_rocks())))
# 1. mirror then flute then enter via gate: Needs mirror and flute and Aga 1
# 2. mirror then flute then enter via south hyrule teleporter: Needs mirror and Flute and hammer and moon pearl
# -> M and Flute and (A or (H and P))
add_rule(world.get_entrance('Pyramid Fairy'), lambda state: state.has_Mirror() and state.has('Ocarina') and (state.can_reach('Top of Pyramid', 'Entrance') or (state.has('Hammer') and state.has_Pearl())))
elif bombshop_entrance.name in Isolated_LW_entrances:
add_rule(world.get_entrance('Pyramid Fairy'), lambda state: state.has('Ocarina') and (state.can_reach('Top of Pyramid', 'Entrance') or (state.has('Hammer') and state.can_lift_rocks())))
# 1. flute then enter via gate: Needs flute and Aga 1
# 2. flute then enter via south hyrule teleporter: Needs Flute and hammer and moon pearl
# Prexisting mirror spot is not permitted, because mirror might have been needed to reach these isolated locations.
# -> Flute and (A or (H and P))
add_rule(world.get_entrance('Pyramid Fairy'), lambda state: state.has('Ocarina') and (state.can_reach('Top of Pyramid', 'Entrance') or (state.has('Hammer') and state.has_Pearl())))
elif bombshop_entrance.name == 'Dark World Potion Shop':
add_rule(world.get_entrance('Pyramid Fairy'), lambda state: state.has('Hammer') or state.can_lift_rocks() or (state.has_Mirror() and state.can_reach('Top of Pyramid', 'Entrance')))
# 1. walk down by lifting rock: needs gloves and pearl`
# 2. walk down by hammering peg: needs hammer and pearl
# 3. mirror and eneter via gate: needs Mirror and Aga1
# (south hyrule teleporter would be redundant with #2)
# -> P and (H or H) or (M and A)
add_rule(world.get_entrance('Pyramid Fairy'), lambda state: (state.has_Pearl() and (state.has('Hammer') or state.can_lift_rocks())) or (state.has_Mirror() and state.can_reach('Top of Pyramid', 'Entrance')))
elif bombshop_entrance.name == 'Kings Grave':
add_rule(world.get_entrance('Pyramid Fairy'), lambda state: state.can_reach('Top of Pyramid', 'Entrance') or (state.can_lift_rocks() and state.has('Hammer')) or (state.can_lift_heavy_rocks() and state.has_Mirror()))
# same as the Normal_LW_entrances case except that the pre-existing mirror is only possible if you have mitts
# (because otherwise mirror was used to reach the grave, so would cancel a pre-existing mirror spot)
# -> A or (H and P) or (M and Mitts)
add_rule(world.get_entrance('Pyramid Fairy'), lambda state: state.can_reach('Top of Pyramid', 'Entrance') or (state.has('Hammer') and state.has_Pearl()) or (state.can_lift_heavy_rocks() and state.has_Mirror()))
def set_bunny_rules(world):
# regions for the extis of multi-entrace caves/drops that bunny cannot pass
# Note spiral cave may be technically passible, but it would be too absurd to require since OHKO mode is a thing.
bunny_impassable_caves = ['Bumper Cave', 'Two Brothers House', 'Hookshot Cave', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Left)', 'Skull Woods First Section (Top)', 'Turtle Rock (Entrance)', 'Turtle Rock (Second Section)', 'Turtle Rock (Big Chest)', 'Skull Woods Second Section (Drop)',
'Turtle Rock (Eye Bridge)', 'Sewers', 'Pyramid', 'Spiral Cave (Top)']
bunny_accessible_locations = ['Link\'s Uncle', 'Sahasrahla', 'Sick Kid', 'Lost Woods Hideout', 'Lumberjack Tree', 'Checkerboard Cave', 'Potion Shop', 'Spectacle Rock Cave', 'Pyramid', 'Hype Cave - Generous Guy', 'Peg Cave', 'Bumper Cave Ledge']
# Add pearl requirements for bunny-impassible caves if they occur in the dark world
for region in [world.get_region(name) for name in bunny_impassable_caves]:
if region.is_light_world:
continue
for exit in region.exits:
add_rule(exit, lambda state: state.has_Pearl)
# Add a moon pearl requirement for all locations that are actually in the dark world, except those available to the bunny
for location in world.get_locations():
if not location.parent_region.is_light_world:
if location.name in bunny_accessible_locations:
continue
add_rule(location, lambda state: state.has_Pearl())

270
Text.py
View File

@ -16,31 +16,250 @@ text_addresses = {'Pedestal': (0x180300, 256),
'Ganon2Invincible': (0x181200, 256)}
Uncle_texts = ['Good Luck!\nYou will need it.', 'Forward this message to 10 other people or this seed will be awful.', 'I hope you like your seeds bootless and fluteless.',
'10\n9\n8\n7\n6\n5\n4\n3\n2\n1\nGo!', 'I have crippling depression.', 'I\'m off to visit cousin Fritzl.']
Triforce_texts = ['Product has Hole in center. Bad seller, 0 out of 5.', 'Who stole the fourth triangle?', 'Trifource?\nMore Like Tritrice, am I right?'
'\n Well Done!', 'You just wasted 2 hours of your life.', 'This was meant to be a trapezoid']
Uncle_texts = [
'Good Luck!\nYou will need it.',
'Forward this message to 10 other people or this seed will be awful.',
'I hope you like your seeds bootless and fluteless.',
'10\n9\n8\n7\n6\n5\n4\n3\n2\n1\nGo!',
'I\'m off to visit cousin Fritzl.'
] * 2 + [
"We're out of\nWeetabix. To\nthe store!",
"This seed is\nbootless\nuntil boots.",
"Why do we only\nhave one bed?",
"This is the\nonly textbox.",
"I'm going to\ngo watch the\nMoth tutorial.",
"This seed is\nthe worst.",
"Chasing tail.\nFly ladies.\nDo not follow.",
"I feel like\nI've done this\nbefore...",
"Magic cape can\npass through\nthe barrier!",
"If this is a\nKanzeon seed,\nI'm quitting.",
"I am not your\nreal uncle.",
"You're going\nto have a very\nbad time.",
"Today you\nwill have\nbad luck.",
"I am leaving\nforever.\nGoodbye.",
"Don't worry.\nI got this\ncovered.",
"Race you to\nthe castle!",
"\n hi",
"I'M JUST GOING\nOUT FOR A\nPACK OF SMOKES",
"It's dangerous\nto go alone.\nSee ya!",
"ARE YOU A BAD\nENOUGH DUDE TO\nRESCUE ZELDA?",
"\n\n I AM ERROR",
"This seed is\nsub 2 hours,\nguaranteed.",
"The chest is\na secret to\neverybody.",
"I'm off to\nfind the\nwind fish.",
"The shortcut\nto Ganon\nis this way!",
"THE MOON IS\nCRASHING! RUN\nFOR YOUR LIFE!",
"Time to fight\nhe who must\nnot be named.",
"RED MAIL\nIS FOR\nCOWARDS.",
"HEY!\n\nLISTEN!",
"Well\nexcuuuuuse me,\nprincess!",
"5,000 Rupee\nreward for >\nYou're boned",
"Welcome to\nStoops Lonk's\nHoose",
"Erreur de\ntraduction.\nsvp reessayer",
"I could beat\nit in an hour\nand one life",
"I thought this\nwas open mode?",
]
Triforce_texts = [
'Product has Hole in center. Bad seller, 0 out of 5.',
'Who stole the fourth triangle?',
'Trifource?\nMore Like Tritrice, am I right?'
'\n Well Done!',
'You just wasted 2 hours of your life.',
'This was meant to be a trapezoid'
] * 2 + [
"\n G G",
"All your base\nare belong\nto us.",
"You have ended\nthe domination\nof dr. wily",
" thanks for\n playing!!!",
"\n You Win!",
" Thank you!\n your quest\n is over.",
" A winner\n is\n you!",
"\n WINNER!!",
"\n I'm sorry\n\nbut your\nprincess is in\nanother castle",
"\n success!",
" Whelp…\n that just\n happened",
" Oh hey…\n it's you",
"\n Wheeeeee!!",
" Time for\n another one?",
"and\n\n scene",
"\n GOT EM!!",
"\nTHE VALUUUE!!!",
"Cool seed,\n\nright?",
"\n We did it!",
" Spam those\n emotes in\n wilds chat",
"\n O M G",
" Hello. Will\n you be my\n friend?",
" Beetorp\n was\n here!",
"The Wind Fish\nwill wake\nsoon. Hoot!",
"meow meow meow\nmeow meow meow\n oh my god!",
"Ahhhhhhhhh\nYa ya yaaaah\nYa ya yaaah",
".done\n\n.comment lol",
]
BombShop2_texts = ['Bombs!\nBombs!\nBiggest!\nBestest!\nGreatest!\nBoomest!']
PyramidFairy_texts = ['May I talk to you about our lord and savior, Ganon?']
Sahasrahla2_texts = ['You already got my item, idiot.', 'Why are you still talking to me?', 'This text won\'t change.', 'Have you met my brother, Hasarahshla?']
Blind_texts = ['I bet you expected a vision related pun?\n\nNot Today.\n Didn\'t see that coming, did you?', 'What do you call a blind dinosaur?\n A Doyouthinkhe-saurus',
'A blind man walks into a bar...\n\n\n and a table\n\n\n and a door.',
'Why can\'t blind people eat fish?\n Because it\'s see food']
Ganon1_texts = ['\n\n\n\n\n\n\n\n\nWhy are you reading an empty textbox?', 'Hi', 'Hey, can you turn off the lights?', 'Oink Oink',
'Uncle: How do you like my Ganon cosplay?', 'I\'ll try spinning - that\'s a good trick!', 'Did you ever hear the tragedy of Darth Plagueis the Wise?']
TavernMan_texts = ['Did you know that talking to random NPCs wastes time in a race? I hope this information may be of use to you in the future.']
Blind_texts = [
"I hate insect\npuns, they\nreally bug me.",
"I haven't seen\nthe eye doctor\nin years",
"I don't see\nyou having a\nbright future",
"Are you doing\na blind run\nof this game?",
"pizza joke? no\nI think it's a\nbit too cheesy",
"A novice skier\noften jumps to\ncontusions.",
"the beach?\nI'm not shore\nI can make it.",
"Rental agents\noffer quarters\nfor dollars.",
"I got my tires\nfixed for a\nflat rate.",
"New lightbulb\ninvented?\nEnlighten me.",
"A baker's job\nis a piece of\ncake.",
"My optometrist\nsaid I have\nvision!",
"when you're a\nbaker, don't\nloaf around",
"mire requires\nether quake,\nor bombos",
"Broken pencils\nare pointless.",
"The food they\nserve guards\nlasts sentries",
"being crushed\nby big objects\nis depressing.",
"A tap dancer's\nroutine runs\nhot and cold.",
"A weeknight is\na tiny\nnobleman",
"The chimney\nsweep wore a\nsoot and tye.",
"Gardeners like\nto spring into\naction.",
"bad at nuclear\nphysics. I\nGot no fission",
]
Ganon1_texts = [
"Start your day\nsmiling with a\ndelicious\nwholegrain\nbreakfast\ncreated for\nyour\nincredible\ninsides.",
"You drove\naway my other\nself, Agahnim\ntwo times…\nBut, I won't\ngive you the\nTriforce.\nI'll defeat\nyou!",
"Impa says that\nthe mark on\nyour hand\nmeans that you\nare the hero\nchosen to\nawaken Zelda.\nyour blood can\nresurrect me.",
"Don't stand,\n\ndon't stand so\nDon't stand so\n\nclose to me\nDon't stand so\nclose to me\nback off buddy",
"So ya\nThought ya\nMight like to\ngo to the show\nTo feel the\nwarm thrill of\nconfusion\nThat space\ncadet glow.",
"Like other\npulmonate land\ngastropods,\nthe majority\nof land slugs\nhave two pairs\nof 'feelers'\nor tentacles\non their head.",
"If you were a\nburrito, what\nkind of a\nburrito would\nyou be?\nMe, I fancy I\nwould be a\nspicy barbacoa\nburrito.",
"I am your\nfather's\nbrother's\nnephew's\ncousin's\nformer\nroommate. What\ndoes that make\nus, you ask?",
"I'll be more\neager about\nencouraging\nthinking\noutside the\nbox when there\nis evidence of\nany thinking\ninside it.",
"If we're not\nmeant to have\nmidnight\nsnacks, then\nwhy is there\na light in the\nfridge?\n",
"I feel like we\nkeep ending up\nhere.\n\nDon't you?\n\nIt's like\ndeja vu\nall over again",
"Did you know?\nThe biggest\nand heaviest\ncheese ever\nproduced\nweighed\n57,518 pounds\nand was 32\nfeet long.",
"Now there was\na time, When\nyou loved me\nso. I couldn't\ndo wrong,\nAnd now you\nneed to know.\nSo How you\nlike me now?",
"Did you know?\nNutrition\nexperts\nrecommend that\nat least half\nof our daily\ngrains come\nfrom whole\ngrain products",
]
TavernMan_texts = [
'Did you know that talking to random NPCs wastes time in a race? I hope this information may be of use to you in the future.'
] + [
"What do you\ncall a blind\ndinosaur?\nadoyouthink-\nhesaurus\n",
"A blind man\nwalks into\na bar.\nAnd a table.\nAnd a chair.\n",
"What do ducks\nlike to eat?\n\nQuackers!\n",
"How do you\nset up a party\nin space?\n\nYou planet!\n",
"I'm glad I\nknow sign\nlanguage,\nit's pretty\nhandy.\n",
"What did Zelda\nsay to Link at\na secure door?\n\nTRIFORCE!\n",
"I am on a\nseafood diet.\n\nEvery time\nI see food,\nI eat it.",
"I've decided\nto sell my\nvacuum.\nIt was just\ngathering\ndust.",
"Whats the best\ntime to go to\nthe dentist?\n\nTooth-hurtie!\n",
"Why can't a\nbike stand on\nits own?\n\nIt's two-tired!\n",
"If you haven't\nfound Quake\nyet…\nit's not your\nfault.",
"Why is Peter\nPan always\nflying?\nBecause he\nNeverlands!",
"I once told a\njoke to Armos.\n\nBut he\nremained\nstone-faced!",
"Lanmola was\nlate to our\ndinner party.\nHe just came\nfor the desert",
"Moldorm is\nsuch a\nprankster.\nAnd I fall for\nit every time!",
"Helmasaur is\nthrowing a\nparty.\nI hope it's\na masquerade!",
"I'd like to\nknow Arrghus\nbetter.\nBut he won't\ncome out of\nhis shell!",
"Mothula didn't\nhave much fun\nat the party.\nHe's immune to\nspiked punch!",
"Don't set me\nup with that\nchick from\nSteve's Town.\n\n\nI'm not\ninterested in\na Blind date!",
"Kholdstare is\nafraid to go\nto the circus.\nHungry kids\nthought he was\ncotton candy!",
"I asked who\nVitreous' best\nfriends are.\nHe said,\n'Me, Myself,\nand Eye!'",
"Trinexx can be\na hothead or\nhe can be an\nice guy. In\nthe end, he's\na solid\nindividual!",
"Bari thought I\nhad moved out\nof town.\nHe was shocked\nto see me!",
"I can only get\nWeetabix\naround here.\nI have to go\nto Steve's\nTown for Count\nChocula!",
"Don't argue\nwith a frozen\nDeadrock.\nHe'll never\nchange his\nposition!",
"I offered a\ndrink to a\nself-loathing\nGhini.\nHe said he\ndidn't like\nspirits!",
"I was supposed\nto meet Gibdo\nfor lunch.\nBut he got\nwrapped up in\nsomething!",
"Goriya sure\nhas changed\nin this game.\nI hope he\ncomes back\naround!",
"Hinox actually\nwants to be a\nlawyer.\nToo bad he\nbombed the\nBar exam!",
"I'm surprised\nMoblin's tusks\nare so gross.\nHe always has\nhis Trident\nwith him!",
"Dont tell\nStalfos Im\nhere.\nHe has a bone\nto pick with\nme!",
"I got\nWallmaster to\nhelp me move\nfurniture.\nHe was really\nhandy!",
"Wizzrobe was\njust here.\nHe always\nvanishes right\nbefore we get\nthe check!",
"I shouldn't\nhave picked up\nZora's tab.\nThat guy\ndrinks like\na fish!",
"I was sharing\na drink with\nPoe.\nFor no reason,\nhe left in a\nheartbeat!",
"Dont trust\nhorsemen on\nDeath Mountain\nTheyre Lynel\nthe time!",
"Today's\nspecial is\nbattered bat.\nGot slapped\nfor offering a\nlady a Keese!",
"Dont walk\nunder\npropellered\npineapples.\nYou may end up\nwearing\na pee hat!",
"My girlfriend\nburrowed under\nthe sand.\nSo I decided\nto Leever!",
"Geldman wants\nto be a\nBroadway star.\nHes always\npracticing\nJazz Hands!",
"Octoballoon\nmust be mad\nat me.\nHe blows up\nat the sight\nof me!",
"Toppo is a\ntotal pothead.\n\nHe hates it\nwhen you take\naway his grass",
"I lost my\nshield by\nthat house.\nWhy did they\nput up a\nPikit fence?!",
"Know that fox\nin Steves\nTown?\nHell Pikku\npockets if you\naren't careful",
"Dash through\nDark World\nbushes.\nYoull see\nGanon is tryin\nto Stal you!",
"Eyegore!\n\nYou gore!\nWe all gore\nthose jerks\nwith arrows!",
"I like my\nwhiskey neat.\n\nSome prefer it\nOctoroks!",
"I consoled\nFreezor over a\ncup of coffee.\nHis problems\njust seemed to\nmelt away!",
"Magic droplets\nof water dont\nshut up.\nThey just\nKyameron!",
"I bought hot\nwings for\nSluggula.\nThey gave him\nexplosive\ndiarrhea!",
"Hardhat Beetle\nwont\nLet It Be?\nTell it to Get\nBack or give\nit a Ticket to\nRide down\na hole!",
]
KingsReturn_texts = ['Who is this even', 'The Harem']
Sanctuary_texts = ['A Priest\'s love']
Kakariko_texts = ['Shasschahshahsahahrahsashsa', 'Schaschlik']
Blacksmiths_texts = ['frogs for bread', 'That\'s not a sword', 'The Rupeesmiths']
DeathMountain_texts = ['lost again', 'Alzheimer']
LostWoods_texts = ['thieves\' stump', 'He\'s got wood', 'Dancing pickles']
WishingWell_texts = ['Bottle for Bottle']
DesertPalace_texts = ['literacy moves']
MountainTower_texts = ['up up and away']
LinksHouse_texts = ['Home Sweet Home', 'Only one bed']
Lumberjacks_texts = ['Chop Chop', 'logfellas']
KingsReturn_texts = [
'Who is this even',
'The Harem'
] * 2 + [
"the return of the king",
"fellowship of the ring",
"the two towers",
]
Sanctuary_texts = [
'A Priest\'s love'
] * 2 + [
"the loyal priest",
"read a book",
"sits in own pew",
]
Sahasrahla_names = [
"sahasralah", "sabotaging", "sacahuista", "sacahuiste", "saccharase", "saccharide", "saccharify",
"saccharine", "saccharins", "sacerdotal", "sackcloths", "salmonella", "saltarelli", "saltarello",
"saltations", "saltbushes", "saltcellar", "saltshaker", "salubrious", "sandgrouse", "sandlotter",
"sandstorms", "sandwiched", "sauerkraut", "schipperke", "schismatic", "schizocarp", "schmalzier",
"schmeering", "schmoosing", "shibboleth", "shovelnose", "sahananana", "sarararara", "salamander",
"sharshalah", "shahabadoo", "sassafrass",
]
Kakariko_texts = ["{}'s homecoming"]
Blacksmiths_texts = [
'frogs for bread',
'That\'s not a sword',
'The Rupeesmiths'
] * 1 + [
"the dwarven breadsmiths"
]
DeathMountain_texts = [
"the lost old man",
"gary the old man",
"Your ad here"
]
LostWoods_texts = [
'thieves\' stump',
'He\'s got wood',
] * 2 + [
"the forest thief",
"dancing pickles",
"flying vultures",
]
WishingWell_texts = [
"venus. queen of faeries",
"Venus was her name",
"I'm your Venus",
"Yeah, baby, shes got it",
"Venus, I'm your fire",
"Venus, At your desire",
]
DesertPalace_texts = ['vultures rule the desert', 'literacy moves']
MountainTower_texts = ['the bully makes a friend', 'up up and away']
LinksHouse_texts = ['your uncle recovers', 'Home Sweet Home', 'Only one bed']
Lumberjacks_texts = [
'Chop Chop'
] * 2 + [
"twin lumberjacks",
"fresh flapjacks",
"two woodchoppers",
"double lumberman",
"lumberclones",
"woodfellas",
]
SickKid_texts = ['Next Time Stay Down']
Zora_texts = ['Splashes For Sale', 'Slippery when wet']
MagicShop_texts = ['Drug deal', 'Shrooms for days']
@ -120,7 +339,7 @@ class Credits(object):
self.scene_order = ['castle', 'sancturary', 'kakariko', 'desert', 'hera', 'house', 'zora', 'witch',
'lumberjacks', 'grove', 'well', 'smithy', 'kakariko2', 'bridge', 'woods', 'pedestal']
def update_credits_line(self, scene, line, text, align='center'):
def update_credits_line(self, scene, line, text):
scenes = self.credit_scenes
text = text[:32]
@ -162,7 +381,7 @@ class SceneCreditLine(CreditLine):
"""Base class for credit lines for the scene portion of the credits"""
def __init__(self, y, text, align='center'):
self.y = y
super().__init__(text,align)
super().__init__(text, align)
def header(self, x=None, y=None, length=None):
if x is None:
@ -456,9 +675,10 @@ def char_to_alttp_char(char):
return char_map.get(char, 0xFF)
class TextMapper(object):
number_offset = None
alpha_offset = 0
char_map = {}
@classmethod
def map_char(cls, char):
if cls.number_offset is not None:
@ -492,7 +712,7 @@ class GreenCreditMapper(TextMapper):
class RedCreditMapper(TextMapper):
char_map = {' ': 0x9F} #fixme
alpha_offset= -0x61
alpha_offset = -0x61
class LargeCreditTopMapper(TextMapper):
char_map = {' ': 0x9F,

View File

@ -1,4 +1,5 @@
import os
import subprocess
import sys
def is_bundled():
@ -10,7 +11,7 @@ def local_path(path):
if is_bundled():
# we are running in a bundle
local_path.cached_path = sys._MEIPASS
local_path.cached_path = sys._MEIPASS # pylint: disable=protected-access,no-member
else:
# we are running in a normal Python environment
local_path.cached_path = os.path.dirname(os.path.abspath(__file__))
@ -40,7 +41,7 @@ def output_path(path):
documents = buf.value
elif sys.platform == 'darwin':
from AppKit import NSSearchPathForDirectoriesInDomains
from AppKit import NSSearchPathForDirectoriesInDomains # pylint: disable=import-error
# http://developer.apple.com/DOCUMENTATION/Cocoa/Reference/Foundation/Miscellaneous/Foundation_Functions/Reference/reference.html#//apple_ref/c/func/NSSearchPathForDirectoriesInDomains
NSDocumentDirectory = 9
NSUserDomainMask = 1
@ -60,7 +61,7 @@ def open_file(filename):
if sys.platform == 'win32':
os.startfile(filename)
else:
open_Command = 'open' if sys.platform == 'darwin' else 'xdg-open'
open_command = 'open' if sys.platform == 'darwin' else 'xdg-open'
subprocess.call([open_command, filename])
def close_console():
@ -69,5 +70,39 @@ def close_console():
import ctypes.wintypes
try:
ctypes.windll.kernel32.FreeConsole()
except:
except Exception:
pass
def new_logic_array():
import random
l = list(range(256))
random.SystemRandom().shuffle(l)
chunks = [l[i:i + 16] for i in range(0, len(l), 16)]
lines = [", ".join([str(j) for j in i]) for i in chunks]
print("logic_hash = ["+",\n ".join(lines)+"]")
def make_new_base2current(old_rom='Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', new_rom='working.sfc'):
from collections import OrderedDict
import json
import hashlib
with open(old_rom, 'rb') as stream:
old_rom_data = bytearray(stream.read())
with open(new_rom, 'rb') as stream:
new_rom_data = bytearray(stream.read())
# extend to 2 mb
old_rom_data.extend(bytearray([0x00] * (2097152 - len(old_rom_data))))
out_data = OrderedDict()
for idx, old in enumerate(old_rom_data):
new = new_rom_data[idx]
if old != new:
out_data[idx] = [int(new)]
for offset in reversed(list(out_data.keys())):
if offset - 1 in out_data:
out_data[offset-1].extend(out_data.pop(offset))
with open('data/base2current.json', 'wt') as outfile:
json.dump([{key:value} for key, value in out_data.items()], outfile, separators=(",", ":"))
basemd5 = hashlib.md5()
basemd5.update(new_rom_data)
return "New Rom Hash: " + basemd5.hexdigest()

File diff suppressed because one or more lines are too long

BIN
data/default.zspr Normal file

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.

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.

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.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Binary file not shown.

2
data/sprites/unofficial/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore