Merge remote-tracking branch 'remotes/multi/multiworld_31' into Dev

This commit is contained in:
compiling
2020-01-11 08:58:09 +11:00
16 changed files with 727 additions and 507 deletions

2
.gitignore vendored
View File

@@ -16,5 +16,7 @@ README.html
EnemizerCLI/ EnemizerCLI/
.mypy_cache/ .mypy_cache/
RaceRom.py RaceRom.py
weights/
venv venv
test test

View File

@@ -6,6 +6,7 @@ import textwrap
import sys import sys
from AdjusterMain import adjust from AdjusterMain import adjust
from Rom import get_sprite_from_name
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter): class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
@@ -15,7 +16,8 @@ class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
def main(): def main():
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument('--rom', default='ER_base.sfc', help='Path to an ALttP JAP(1.0) rom to use as a base.') parser.add_argument('--rom', default='ER_base.sfc', help='Path to an ALttPR rom to adjust.')
parser.add_argument('--baserom', default='Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', help='Path to an ALttP JAP(1.0) rom to use as a base.')
parser.add_argument('--loglevel', default='info', const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.') parser.add_argument('--loglevel', default='info', const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
parser.add_argument('--fastmenu', default='normal', const='normal', nargs='?', choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'], parser.add_argument('--fastmenu', default='normal', const='normal', nargs='?', choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
help='''\ help='''\
@@ -31,6 +33,8 @@ def main():
''') ''')
parser.add_argument('--heartcolor', default='red', const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow', 'random'], parser.add_argument('--heartcolor', default='red', const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow', 'random'],
help='Select the color of Link\'s heart meter. (default: %(default)s)') help='Select the color of Link\'s heart meter. (default: %(default)s)')
parser.add_argument('--ow_palettes', default='default', choices=['default', 'random', 'blackout'])
parser.add_argument('--uw_palettes', default='default', choices=['default', 'random', 'blackout'])
parser.add_argument('--sprite', help='''\ parser.add_argument('--sprite', help='''\
Path to a sprite sheet to use for Link. Needs to be in Path to a sprite sheet to use for Link. Needs to be in
binary format and have a length of 0x7000 (28672) bytes, binary format and have a length of 0x7000 (28672) bytes,
@@ -43,10 +47,10 @@ def main():
# ToDo: Validate files further than mere existance # ToDo: Validate files further than mere existance
if not os.path.isfile(args.rom): if not os.path.isfile(args.rom):
input('Could not find valid base rom for patching at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.rom) input('Could not find valid rom for patching at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.rom)
sys.exit(1) sys.exit(1)
if args.sprite is not None and not os.path.isfile(args.sprite): if args.sprite is not None and not os.path.isfile(args.sprite) and not get_sprite_from_name(args.sprite):
input('Could not find link sprite sheet at given location. \nPress Enter to exit.' % args.sprite) input('Could not find link sprite sheet at given location. \nPress Enter to exit.')
sys.exit(1) sys.exit(1)
# set up logger # set up logger

View File

@@ -1,10 +1,9 @@
import os import os
import re
import time import time
import logging import logging
from Utils import output_path, parse_names_string from Utils import output_path, parse_names_string
from Rom import LocalRom, Sprite, apply_rom_settings from Rom import LocalRom, apply_rom_settings
def adjust(args): def adjust(args):
@@ -12,22 +11,17 @@ def adjust(args):
logger = logging.getLogger('') logger = logging.getLogger('')
logger.info('Patching ROM.') logger.info('Patching ROM.')
if args.sprite is not None:
if isinstance(args.sprite, Sprite):
sprite = args.sprite
else:
sprite = Sprite(args.sprite)
else:
sprite = None
outfilebase = os.path.basename(args.rom)[:-4] + '_adjusted' outfilebase = os.path.basename(args.rom)[:-4] + '_adjusted'
if os.stat(args.rom).st_size in (0x200000, 0x400000) and os.path.splitext(args.rom)[-1].lower() == '.sfc': if os.stat(args.rom).st_size in (0x200000, 0x400000) and os.path.splitext(args.rom)[-1].lower() == '.sfc':
rom = LocalRom(args.rom, False) rom = LocalRom(args.rom, False)
if os.path.isfile(args.baserom):
baserom = LocalRom(args.baserom, True)
rom.orig_buffer = baserom.orig_buffer
else: else:
raise RuntimeError('Provided Rom is not a valid Link to the Past Randomizer Rom. Please provide one for adjusting.') raise RuntimeError('Provided Rom is not a valid Link to the Past Randomizer Rom. Please provide one for adjusting.')
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, sprite, parse_names_string(args.names)) apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, args.sprite, args.ow_palettes, args.uw_palettes, parse_names_string(args.names))
rom.write_to_file(output_path('%s.sfc' % outfilebase)) rom.write_to_file(output_path('%s.sfc' % outfilebase))

View File

@@ -11,7 +11,7 @@ from RoomData import Room
class World(object): class World(object):
def __init__(self, players, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments, timer, progressive, goal, algorithm, accessibility, shuffle_ganon, quickswap, fastmenu, disable_music, retro, custom, customitemarray, hints): def __init__(self, players, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments, timer, progressive, goal, algorithm, accessibility, shuffle_ganon, retro, custom, customitemarray, hints):
self.players = players self.players = players
self.shuffle = shuffle.copy() self.shuffle = shuffle.copy()
self.doorShuffle = doorShuffle self.doorShuffle = doorShuffle
@@ -50,9 +50,6 @@ class World(object):
self.fix_trock_exit = {} self.fix_trock_exit = {}
self.shuffle_ganon = shuffle_ganon self.shuffle_ganon = shuffle_ganon
self.fix_gtower_exit = self.shuffle_ganon self.fix_gtower_exit = self.shuffle_ganon
self.quickswap = quickswap
self.fastmenu = fastmenu
self.disable_music = disable_music
self.retro = retro.copy() self.retro = retro.copy()
self.custom = custom self.custom = custom
self.customitemarray = customitemarray self.customitemarray = customitemarray
@@ -268,6 +265,8 @@ class World(object):
return [location for location in self.get_locations() if location.item is not None and location.item.name == item and location.item.player == player] return [location for location in self.get_locations() if location.item is not None and location.item.name == item and location.item.player == player]
def push_precollected(self, item): def push_precollected(self, item):
if (item.smallkey and self.keyshuffle[item.player]) or (item.bigkey and self.bigkeyshuffle[item.player]):
item.advancement = True
self.precollected_items.append(item) self.precollected_items.append(item)
self.state.collect(item, True) self.state.collect(item, True)
@@ -1275,15 +1274,15 @@ class ShopType(Enum):
UpgradeShop = 2 UpgradeShop = 2
class Shop(object): class Shop(object):
def __init__(self, region, room_id, default_door_id, type, shopkeeper_config, replaceable): def __init__(self, region, room_id, type, shopkeeper_config, custom, locked):
self.region = region self.region = region
self.room_id = room_id self.room_id = room_id
self.default_door_id = default_door_id self.default_door_id = default_door_id
self.type = type self.type = type
self.inventory = [None, None, None] self.inventory = [None, None, None]
self.shopkeeper_config = shopkeeper_config self.shopkeeper_config = shopkeeper_config
self.replaceable = replaceable self.custom = custom
self.active = False self.locked = locked
@property @property
def item_count(self): def item_count(self):
@@ -1343,6 +1342,7 @@ class Spoiler(object):
self.medallions = {} self.medallions = {}
self.playthrough = {} self.playthrough = {}
self.unreachables = [] self.unreachables = []
self.startinventory = []
self.locations = {} self.locations = {}
self.paths = {} self.paths = {}
self.metadata = {} self.metadata = {}
@@ -1377,6 +1377,8 @@ class Spoiler(object):
self.medallions['Misery Mire (Player %d)' % player] = self.world.required_medallions[player][0] self.medallions['Misery Mire (Player %d)' % player] = self.world.required_medallions[player][0]
self.medallions['Turtle Rock (Player %d)' % player] = self.world.required_medallions[player][1] self.medallions['Turtle Rock (Player %d)' % player] = self.world.required_medallions[player][1]
self.startinventory = list(map(str, self.world.precollected_items))
self.locations = OrderedDict() self.locations = OrderedDict()
listed_locations = set() listed_locations = set()
@@ -1404,7 +1406,7 @@ class Spoiler(object):
self.shops = [] self.shops = []
for shop in self.world.shops: for shop in self.world.shops:
if not shop.active: if not shop.custom:
continue continue
shopdata = {'location': str(shop.region), shopdata = {'location': str(shop.region),
'type': 'Take Any' if shop.type == ShopType.TakeAny else 'Shop' 'type': 'Take Any' if shop.type == ShopType.TakeAny else 'Shop'
@@ -1471,6 +1473,7 @@ class Spoiler(object):
out['Doors'] = list(self.doors.values()) out['Doors'] = list(self.doors.values())
out['DoorTypes'] = list(self.doorTypes.values()) out['DoorTypes'] = list(self.doorTypes.values())
out.update(self.locations) out.update(self.locations)
out['Starting Inventory'] = self.startinventory
out['Special'] = self.medallions out['Special'] = self.medallions
if self.shops: if self.shops:
out['Shops'] = self.shops out['Shops'] = self.shops
@@ -1508,8 +1511,6 @@ class Spoiler(object):
outfile.write('Enemy health: %s\n' % self.metadata['enemy_health']) outfile.write('Enemy health: %s\n' % self.metadata['enemy_health'])
outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage']) outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'])
outfile.write('Hints: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['hints'].items()}) outfile.write('Hints: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['hints'].items()})
outfile.write('L\\R Quickswap enabled: %s\n' % ('Yes' if self.world.quickswap else 'No'))
outfile.write('Menu speed: %s' % self.world.fastmenu)
if self.doors: if self.doors:
outfile.write('\n\nDoors:\n\n') outfile.write('\n\nDoors:\n\n')
outfile.write('\n'.join(['%s%s %s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '', entry['entrance'], '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', entry['exit']) for entry in self.doors.values()])) outfile.write('\n'.join(['%s%s %s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '', entry['entrance'], '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', entry['exit']) for entry in self.doors.values()]))
@@ -1527,12 +1528,14 @@ class Spoiler(object):
for player in range(1, self.world.players + 1): for player in range(1, self.world.players + 1):
outfile.write('\nMisery Mire Medallion (Player %d): %s' % (player, self.medallions['Misery Mire (Player %d)' % player])) outfile.write('\nMisery Mire Medallion (Player %d): %s' % (player, self.medallions['Misery Mire (Player %d)' % player]))
outfile.write('\nTurtle Rock Medallion (Player %d): %s' % (player, self.medallions['Turtle Rock (Player %d)' % player])) outfile.write('\nTurtle Rock Medallion (Player %d): %s' % (player, self.medallions['Turtle Rock (Player %d)' % player]))
outfile.write('\n\nStarting Inventory:\n\n')
outfile.write('\n'.join(self.startinventory))
outfile.write('\n\nLocations:\n\n') outfile.write('\n\nLocations:\n\n')
outfile.write('\n'.join(['%s: %s' % (location, item) for grouping in self.locations.values() for (location, item) in grouping.items()])) outfile.write('\n'.join(['%s: %s' % (location, item) for grouping in self.locations.values() for (location, item) in grouping.items()]))
outfile.write('\n\nShops:\n\n') outfile.write('\n\nShops:\n\n')
outfile.write('\n'.join("{} [{}]\n {}".format(shop['location'], shop['type'], "\n ".join(item for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if item)) for shop in self.shops)) outfile.write('\n'.join("{} [{}]\n {}".format(shop['location'], shop['type'], "\n ".join(item for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if item)) for shop in self.shops))
outfile.write('\n\nPlaythrough:\n\n') outfile.write('\n\nPlaythrough:\n\n')
outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join([' %s: %s' % (location, item) for (location, item) in sphere.items()])) for (sphere_nr, sphere) in self.playthrough.items()])) outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join([' %s: %s' % (location, item) for (location, item) in sphere.items()] if sphere_nr != '0' else [f' {item}' for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()]))
if self.unreachables: if self.unreachables:
outfile.write('\n\nUnreachable Items:\n\n') outfile.write('\n\nUnreachable Items:\n\n')
outfile.write('\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables])) outfile.write('\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables]))

View File

@@ -9,6 +9,7 @@ import shlex
import sys import sys
from Main import main from Main import main
from Rom import get_sprite_from_name
from Utils import is_bundled, close_console from Utils import is_bundled, close_console
from Fill import FillError from Fill import FillError
@@ -223,10 +224,12 @@ def parse_arguments(argv, no_defaults=False):
parser.add_argument('--compassshuffle', default=defval(False), help='Compasses are no longer restricted to their dungeons, but can be anywhere', action='store_true') parser.add_argument('--compassshuffle', default=defval(False), help='Compasses are no longer restricted to their dungeons, but can be anywhere', action='store_true')
parser.add_argument('--keyshuffle', default=defval(False), help='Small Keys are no longer restricted to their dungeons, but can be anywhere', action='store_true') parser.add_argument('--keyshuffle', default=defval(False), help='Small Keys are no longer restricted to their dungeons, but can be anywhere', action='store_true')
parser.add_argument('--bigkeyshuffle', default=defval(False), help='Big Keys are no longer restricted to their dungeons, but can be anywhere', action='store_true') parser.add_argument('--bigkeyshuffle', default=defval(False), help='Big Keys are no longer restricted to their dungeons, but can be anywhere', action='store_true')
parser.add_argument('--keysanity', default=defval(False), help=argparse.SUPPRESS, action='store_true')
parser.add_argument('--retro', default=defval(False), help='''\ parser.add_argument('--retro', default=defval(False), help='''\
Keys are universal, shooting arrows costs rupees, Keys are universal, shooting arrows costs rupees,
and a few other little things make this more like Zelda-1. and a few other little things make this more like Zelda-1.
''', action='store_true') ''', action='store_true')
parser.add_argument('--startinventory', default=defval(''), help='Specifies a list of items that will be in your starting inventory (separated by commas)')
parser.add_argument('--custom', default=defval(False), help='Not supported.') parser.add_argument('--custom', default=defval(False), help='Not supported.')
parser.add_argument('--customitemarray', default=defval(False), help='Not supported.') parser.add_argument('--customitemarray', default=defval(False), help='Not supported.')
parser.add_argument('--accessibility', default=defval('items'), const='items', nargs='?', choices=['items', 'locations', 'none'], help='''\ parser.add_argument('--accessibility', default=defval('items'), const='items', nargs='?', choices=['items', 'locations', 'none'], help='''\
@@ -252,6 +255,8 @@ def parse_arguments(argv, no_defaults=False):
''') ''')
parser.add_argument('--heartcolor', default=defval('red'), const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow', 'random'], parser.add_argument('--heartcolor', default=defval('red'), const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow', 'random'],
help='Select the color of Link\'s heart meter. (default: %(default)s)') help='Select the color of Link\'s heart meter. (default: %(default)s)')
parser.add_argument('--ow_palettes', default=defval('default'), choices=['default', 'random', 'blackout'])
parser.add_argument('--uw_palettes', default=defval('default'), choices=['default', 'random', 'blackout'])
parser.add_argument('--sprite', help='''\ parser.add_argument('--sprite', help='''\
Path to a sprite sheet to use for Link. Needs to be in Path to a sprite sheet to use for Link. Needs to be in
binary format and have a length of 0x7000 (28672) bytes, binary format and have a length of 0x7000 (28672) bytes,
@@ -266,12 +271,11 @@ def parse_arguments(argv, no_defaults=False):
for VT site integration, do not use otherwise. for VT site integration, do not use otherwise.
''') ''')
parser.add_argument('--skip_playthrough', action='store_true', default=defval(False)) parser.add_argument('--skip_playthrough', action='store_true', default=defval(False))
parser.add_argument('--enemizercli', default=defval('')) parser.add_argument('--enemizercli', default=defval('EnemizerCLI/EnemizerCLI.Core'))
parser.add_argument('--shufflebosses', default=defval('none'), choices=['none', 'basic', 'normal', 'chaos']) parser.add_argument('--shufflebosses', default=defval('none'), choices=['none', 'basic', 'normal', 'chaos'])
parser.add_argument('--shuffleenemies', default=defval('none'), choices=['none', 'shuffled', 'chaos']) parser.add_argument('--shuffleenemies', default=defval('none'), choices=['none', 'shuffled', 'chaos'])
parser.add_argument('--enemy_health', default=defval('default'), choices=['default', 'easy', 'normal', 'hard', 'expert']) parser.add_argument('--enemy_health', default=defval('default'), choices=['default', 'easy', 'normal', 'hard', 'expert'])
parser.add_argument('--enemy_damage', default=defval('default'), choices=['default', 'shuffled', 'chaos']) parser.add_argument('--enemy_damage', default=defval('default'), choices=['default', 'shuffled', 'chaos'])
parser.add_argument('--shufflepalette', default=defval(False), action='store_true')
parser.add_argument('--shufflepots', default=defval(False), action='store_true') parser.add_argument('--shufflepots', default=defval(False), action='store_true')
parser.add_argument('--beemizer', default=defval(0), type=lambda value: min(max(int(value), 0), 4)) parser.add_argument('--beemizer', default=defval(0), type=lambda value: min(max(int(value), 0), 4))
parser.add_argument('--multi', default=defval(1), type=lambda value: min(max(int(value), 1), 255)) parser.add_argument('--multi', default=defval(1), type=lambda value: min(max(int(value), 1), 255))
@@ -285,6 +289,8 @@ def parse_arguments(argv, no_defaults=False):
parser.add_argument(f'--p{player}', default=defval(''), help=argparse.SUPPRESS) parser.add_argument(f'--p{player}', default=defval(''), help=argparse.SUPPRESS)
ret = parser.parse_args(argv) ret = parser.parse_args(argv)
if ret.keysanity:
ret.mapshuffle, ret.compassshuffle, ret.keyshuffle, ret.bigkeyshuffle = [True] * 4
if multiargs.multi: if multiargs.multi:
defaults = copy.deepcopy(ret) defaults = copy.deepcopy(ret)
@@ -293,9 +299,10 @@ def parse_arguments(argv, no_defaults=False):
for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality',
'shuffle', 'crystals_ganon', 'crystals_gt', 'openpyramid', 'shuffle', 'crystals_ganon', 'crystals_gt', 'openpyramid',
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
'retro', 'accessibility', 'hints', 'shufflepalette', 'shufflepots', 'beemizer', 'retro', 'accessibility', 'hints', 'beemizer',
'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage']: 'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots',
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep']:
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
if player == 1: if player == 1:
setattr(ret, name, {1: value}) setattr(ret, name, {1: value})
@@ -321,9 +328,9 @@ def start():
if not args.jsonout and not os.path.isfile(args.rom): if not args.jsonout and not os.path.isfile(args.rom):
input('Could not find valid base rom for patching at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.rom) input('Could not find valid base rom for patching at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.rom)
sys.exit(1) sys.exit(1)
if args.sprite is not None and not os.path.isfile(args.sprite): if any([sprite is not None and not os.path.isfile(sprite) and not get_sprite_from_name(sprite) for sprite in args.sprite.values()]):
if not args.jsonout: if not args.jsonout:
input('Could not find link sprite sheet at given location. \nPress Enter to exit.' % args.sprite) input('Could not find link sprite sheet at given location. \nPress Enter to exit.')
sys.exit(1) sys.exit(1)
else: else:
raise IOError('Cannot find sprite file at %s' % args.sprite) raise IOError('Cannot find sprite file at %s' % args.sprite)

189
Gui.py
View File

@@ -60,8 +60,6 @@ def guiMain(args=None):
createSpoilerCheckbutton = Checkbutton(checkBoxFrame, text="Create Spoiler Log", variable=createSpoilerVar) createSpoilerCheckbutton = Checkbutton(checkBoxFrame, text="Create Spoiler Log", variable=createSpoilerVar)
suppressRomVar = IntVar() suppressRomVar = IntVar()
suppressRomCheckbutton = Checkbutton(checkBoxFrame, text="Do not create patched Rom", variable=suppressRomVar) suppressRomCheckbutton = Checkbutton(checkBoxFrame, text="Do not create patched Rom", variable=suppressRomVar)
quickSwapVar = IntVar()
quickSwapCheckbutton = Checkbutton(checkBoxFrame, text="Enabled L/R Item quickswapping", variable=quickSwapVar)
openpyramidVar = IntVar() openpyramidVar = IntVar()
openpyramidCheckbutton = Checkbutton(checkBoxFrame, text="Pre-open Pyramid Hole", variable=openpyramidVar) openpyramidCheckbutton = Checkbutton(checkBoxFrame, text="Pre-open Pyramid Hole", variable=openpyramidVar)
mcsbshuffleFrame = Frame(checkBoxFrame) mcsbshuffleFrame = Frame(checkBoxFrame)
@@ -76,8 +74,6 @@ def guiMain(args=None):
bigkeyshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="BigKeys", variable=bigkeyshuffleVar) bigkeyshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="BigKeys", variable=bigkeyshuffleVar)
retroVar = IntVar() retroVar = IntVar()
retroCheckbutton = Checkbutton(checkBoxFrame, text="Retro mode (universal keys)", variable=retroVar) retroCheckbutton = Checkbutton(checkBoxFrame, text="Retro mode (universal keys)", variable=retroVar)
disableMusicVar = IntVar()
disableMusicCheckbutton = Checkbutton(checkBoxFrame, text="Disable game music", variable=disableMusicVar)
shuffleGanonVar = IntVar() shuffleGanonVar = IntVar()
shuffleGanonVar.set(1) #set default shuffleGanonVar.set(1) #set default
shuffleGanonCheckbutton = Checkbutton(checkBoxFrame, text="Include Ganon's Tower and Pyramid Hole in shuffle pool", variable=shuffleGanonVar) shuffleGanonCheckbutton = Checkbutton(checkBoxFrame, text="Include Ganon's Tower and Pyramid Hole in shuffle pool", variable=shuffleGanonVar)
@@ -89,7 +85,6 @@ def guiMain(args=None):
createSpoilerCheckbutton.pack(expand=True, anchor=W) createSpoilerCheckbutton.pack(expand=True, anchor=W)
suppressRomCheckbutton.pack(expand=True, anchor=W) suppressRomCheckbutton.pack(expand=True, anchor=W)
quickSwapCheckbutton.pack(expand=True, anchor=W)
openpyramidCheckbutton.pack(expand=True, anchor=W) openpyramidCheckbutton.pack(expand=True, anchor=W)
mcsbshuffleFrame.pack(expand=True, anchor=W) mcsbshuffleFrame.pack(expand=True, anchor=W)
mcsbLabel.grid(row=0, column=0) mcsbLabel.grid(row=0, column=0)
@@ -98,57 +93,23 @@ def guiMain(args=None):
keyshuffleCheckbutton.grid(row=0, column=3) keyshuffleCheckbutton.grid(row=0, column=3)
bigkeyshuffleCheckbutton.grid(row=0, column=4) bigkeyshuffleCheckbutton.grid(row=0, column=4)
retroCheckbutton.pack(expand=True, anchor=W) retroCheckbutton.pack(expand=True, anchor=W)
disableMusicCheckbutton.pack(expand=True, anchor=W)
shuffleGanonCheckbutton.pack(expand=True, anchor=W) shuffleGanonCheckbutton.pack(expand=True, anchor=W)
hintsCheckbutton.pack(expand=True, anchor=W) hintsCheckbutton.pack(expand=True, anchor=W)
customCheckbutton.pack(expand=True, anchor=W) customCheckbutton.pack(expand=True, anchor=W)
fileDialogFrame = Frame(rightHalfFrame) romOptionsFrame = LabelFrame(rightHalfFrame, text="Rom options")
romOptionsFrame.columnconfigure(0, weight=1)
romOptionsFrame.columnconfigure(1, weight=1)
for i in range(5):
romOptionsFrame.rowconfigure(i, weight=1)
heartbeepFrame = Frame(fileDialogFrame) disableMusicVar = IntVar()
heartbeepVar = StringVar() disableMusicCheckbutton = Checkbutton(romOptionsFrame, text="Disable music", variable=disableMusicVar)
heartbeepVar.set('normal') disableMusicCheckbutton.grid(row=0, column=0, sticky=E)
heartbeepOptionMenu = OptionMenu(heartbeepFrame, heartbeepVar, 'double', 'normal', 'half', 'quarter', 'off')
heartbeepOptionMenu.pack(side=RIGHT)
heartbeepLabel = Label(heartbeepFrame, text='Heartbeep sound rate')
heartbeepLabel.pack(side=LEFT, padx=(0,52))
heartcolorFrame = Frame(fileDialogFrame) spriteDialogFrame = Frame(romOptionsFrame)
heartcolorVar = StringVar() spriteDialogFrame.grid(row=0, column=1)
heartcolorVar.set('red') baseSpriteLabel = Label(spriteDialogFrame, text='Sprite:')
heartcolorOptionMenu = OptionMenu(heartcolorFrame, heartcolorVar, 'red', 'blue', 'green', 'yellow', 'random')
heartcolorOptionMenu.pack(side=RIGHT)
heartcolorLabel = Label(heartcolorFrame, text='Heart color')
heartcolorLabel.pack(side=LEFT, padx=(0,127))
fastMenuFrame = Frame(fileDialogFrame)
fastMenuVar = StringVar()
fastMenuVar.set('normal')
fastMenuOptionMenu = OptionMenu(fastMenuFrame, fastMenuVar, 'normal', 'instant', 'double', 'triple', 'quadruple', 'half')
fastMenuOptionMenu.pack(side=RIGHT)
fastMenuLabel = Label(fastMenuFrame, text='Menu speed')
fastMenuLabel.pack(side=LEFT, padx=(0,100))
heartbeepFrame.pack(expand=True, anchor=E)
heartcolorFrame.pack(expand=True, anchor=E)
fastMenuFrame.pack(expand=True, anchor=E)
romDialogFrame = Frame(fileDialogFrame)
baseRomLabel = Label(romDialogFrame, text='Base Rom')
romVar = StringVar(value="Zelda no Densetsu - Kamigami no Triforce (Japan).sfc")
romEntry = Entry(romDialogFrame, textvariable=romVar)
def RomSelect():
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")])
romVar.set(rom)
romSelectButton = Button(romDialogFrame, text='Select Rom', command=RomSelect)
baseRomLabel.pack(side=LEFT)
romEntry.pack(side=LEFT)
romSelectButton.pack(side=LEFT)
spriteDialogFrame = Frame(fileDialogFrame)
baseSpriteLabel = Label(spriteDialogFrame, text='Link Sprite:')
spriteNameVar = StringVar() spriteNameVar = StringVar()
sprite = None sprite = None
@@ -168,17 +129,79 @@ def guiMain(args=None):
def SpriteSelect(): def SpriteSelect():
SpriteSelector(mainWindow, set_sprite) SpriteSelector(mainWindow, set_sprite)
spriteSelectButton = Button(spriteDialogFrame, text='Open Sprite Picker', command=SpriteSelect) spriteSelectButton = Button(spriteDialogFrame, text='...', command=SpriteSelect)
baseSpriteLabel.pack(side=LEFT) baseSpriteLabel.pack(side=LEFT)
spriteEntry.pack(side=LEFT) spriteEntry.pack(side=LEFT)
spriteSelectButton.pack(side=LEFT) spriteSelectButton.pack(side=LEFT)
romDialogFrame.pack() quickSwapVar = IntVar()
spriteDialogFrame.pack() quickSwapCheckbutton = Checkbutton(romOptionsFrame, text="L/R Quickswapping", variable=quickSwapVar)
quickSwapCheckbutton.grid(row=1, column=0, sticky=E)
checkBoxFrame.pack() fastMenuFrame = Frame(romOptionsFrame)
fileDialogFrame.pack() fastMenuFrame.grid(row=1, column=1, sticky=E)
fastMenuLabel = Label(fastMenuFrame, text='Menu speed')
fastMenuLabel.pack(side=LEFT)
fastMenuVar = StringVar()
fastMenuVar.set('normal')
fastMenuOptionMenu = OptionMenu(fastMenuFrame, fastMenuVar, 'normal', 'instant', 'double', 'triple', 'quadruple', 'half')
fastMenuOptionMenu.pack(side=LEFT)
heartcolorFrame = Frame(romOptionsFrame)
heartcolorFrame.grid(row=2, column=0, sticky=E)
heartcolorLabel = Label(heartcolorFrame, text='Heart color')
heartcolorLabel.pack(side=LEFT)
heartcolorVar = StringVar()
heartcolorVar.set('red')
heartcolorOptionMenu = OptionMenu(heartcolorFrame, heartcolorVar, 'red', 'blue', 'green', 'yellow', 'random')
heartcolorOptionMenu.pack(side=LEFT)
heartbeepFrame = Frame(romOptionsFrame)
heartbeepFrame.grid(row=2, column=1, sticky=E)
heartbeepLabel = Label(heartbeepFrame, text='Heartbeep')
heartbeepLabel.pack(side=LEFT)
heartbeepVar = StringVar()
heartbeepVar.set('normal')
heartbeepOptionMenu = OptionMenu(heartbeepFrame, heartbeepVar, 'double', 'normal', 'half', 'quarter', 'off')
heartbeepOptionMenu.pack(side=LEFT)
owPalettesFrame = Frame(romOptionsFrame)
owPalettesFrame.grid(row=3, column=0, sticky=E)
owPalettesLabel = Label(owPalettesFrame, text='Overworld palettes')
owPalettesLabel.pack(side=LEFT)
owPalettesVar = StringVar()
owPalettesVar.set('default')
owPalettesOptionMenu = OptionMenu(owPalettesFrame, owPalettesVar, 'default', 'random', 'blackout')
owPalettesOptionMenu.pack(side=LEFT)
uwPalettesFrame = Frame(romOptionsFrame)
uwPalettesFrame.grid(row=3, column=1, sticky=E)
uwPalettesLabel = Label(uwPalettesFrame, text='Dungeon palettes')
uwPalettesLabel.pack(side=LEFT)
uwPalettesVar = StringVar()
uwPalettesVar.set('default')
uwPalettesOptionMenu = OptionMenu(uwPalettesFrame, uwPalettesVar, 'default', 'random', 'blackout')
uwPalettesOptionMenu.pack(side=LEFT)
romDialogFrame = Frame(romOptionsFrame)
romDialogFrame.grid(row=4, column=0, columnspan=2, sticky=W+E)
baseRomLabel = Label(romDialogFrame, text='Base Rom: ')
romVar = StringVar(value="Zelda no Densetsu - Kamigami no Triforce (Japan).sfc")
romEntry = Entry(romDialogFrame, textvariable=romVar)
def RomSelect():
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")])
romVar.set(rom)
romSelectButton = Button(romDialogFrame, text='Select Rom', command=RomSelect)
baseRomLabel.pack(side=LEFT)
romEntry.pack(side=LEFT, expand=True, fill=X)
romSelectButton.pack(side=LEFT)
checkBoxFrame.pack(side=TOP, anchor=W, padx=5, pady=10)
romOptionsFrame.pack(expand=True, fill=BOTH, padx=3)
drowDownFrame = Frame(topFrame) drowDownFrame = Frame(topFrame)
@@ -336,18 +359,19 @@ def guiMain(args=None):
accessibilityFrame.pack(expand=True, anchor=E) accessibilityFrame.pack(expand=True, anchor=E)
algorithmFrame.pack(expand=True, anchor=E) algorithmFrame.pack(expand=True, anchor=E)
enemizerFrame = LabelFrame(randomizerWindow, text="Enemizer", padx=5, pady=5) enemizerFrame = LabelFrame(randomizerWindow, text="Enemizer", padx=5, pady=2)
enemizerFrame.columnconfigure(0, weight=1) enemizerFrame.columnconfigure(0, weight=1)
enemizerFrame.columnconfigure(1, weight=1) enemizerFrame.columnconfigure(1, weight=1)
enemizerFrame.columnconfigure(2, weight=1) enemizerFrame.columnconfigure(2, weight=1)
enemizerFrame.columnconfigure(3, weight=1)
enemizerPathFrame = Frame(enemizerFrame) enemizerPathFrame = Frame(enemizerFrame)
enemizerPathFrame.grid(row=0, column=0, columnspan=3, sticky=W) enemizerPathFrame.grid(row=0, column=0, columnspan=3, sticky=W+E, padx=3)
enemizerCLIlabel = Label(enemizerPathFrame, text="EnemizerCLI path: ") enemizerCLIlabel = Label(enemizerPathFrame, text="EnemizerCLI path: ")
enemizerCLIlabel.pack(side=LEFT) enemizerCLIlabel.pack(side=LEFT)
enemizerCLIpathVar = StringVar() enemizerCLIpathVar = StringVar(value="EnemizerCLI/EnemizerCLI.Core")
enemizerCLIpathEntry = Entry(enemizerPathFrame, textvariable=enemizerCLIpathVar, width=80) enemizerCLIpathEntry = Entry(enemizerPathFrame, textvariable=enemizerCLIpathVar)
enemizerCLIpathEntry.pack(side=LEFT) enemizerCLIpathEntry.pack(side=LEFT, expand=True, fill=X)
def EnemizerSelectPath(): def EnemizerSelectPath():
path = filedialog.askopenfilename(filetypes=[("EnemizerCLI executable", "*EnemizerCLI*")]) path = filedialog.askopenfilename(filetypes=[("EnemizerCLI executable", "*EnemizerCLI*")])
if path: if path:
@@ -355,18 +379,21 @@ def guiMain(args=None):
enemizerCLIbrowseButton = Button(enemizerPathFrame, text='...', command=EnemizerSelectPath) enemizerCLIbrowseButton = Button(enemizerPathFrame, text='...', command=EnemizerSelectPath)
enemizerCLIbrowseButton.pack(side=LEFT) enemizerCLIbrowseButton.pack(side=LEFT)
enemyShuffleVar = IntVar()
enemyShuffleButton = Checkbutton(enemizerFrame, text="Enemy shuffle", variable=enemyShuffleVar)
enemyShuffleButton.grid(row=1, column=0)
paletteShuffleVar = IntVar()
paletteShuffleButton = Checkbutton(enemizerFrame, text="Palette shuffle", variable=paletteShuffleVar)
paletteShuffleButton.grid(row=1, column=1)
potShuffleVar = IntVar() potShuffleVar = IntVar()
potShuffleButton = Checkbutton(enemizerFrame, text="Pot shuffle", variable=potShuffleVar) potShuffleButton = Checkbutton(enemizerFrame, text="Pot shuffle", variable=potShuffleVar)
potShuffleButton.grid(row=1, column=2) potShuffleButton.grid(row=0, column=3)
enemizerEnemyFrame = Frame(enemizerFrame)
enemizerEnemyFrame.grid(row=1, column=0, pady=5)
enemizerEnemyLabel = Label(enemizerEnemyFrame, text='Enemy shuffle')
enemizerEnemyLabel.pack(side=LEFT)
enemyShuffleVar = StringVar()
enemyShuffleVar.set('none')
enemizerEnemyOption = OptionMenu(enemizerEnemyFrame, enemyShuffleVar, 'none', 'shuffled', 'chaos')
enemizerEnemyOption.pack(side=LEFT)
enemizerBossFrame = Frame(enemizerFrame) enemizerBossFrame = Frame(enemizerFrame)
enemizerBossFrame.grid(row=2, column=0) enemizerBossFrame.grid(row=1, column=1)
enemizerBossLabel = Label(enemizerBossFrame, text='Boss shuffle') enemizerBossLabel = Label(enemizerBossFrame, text='Boss shuffle')
enemizerBossLabel.pack(side=LEFT) enemizerBossLabel.pack(side=LEFT)
enemizerBossVar = StringVar() enemizerBossVar = StringVar()
@@ -375,7 +402,7 @@ def guiMain(args=None):
enemizerBossOption.pack(side=LEFT) enemizerBossOption.pack(side=LEFT)
enemizerDamageFrame = Frame(enemizerFrame) enemizerDamageFrame = Frame(enemizerFrame)
enemizerDamageFrame.grid(row=2, column=1) enemizerDamageFrame.grid(row=1, column=2)
enemizerDamageLabel = Label(enemizerDamageFrame, text='Enemy damage') enemizerDamageLabel = Label(enemizerDamageFrame, text='Enemy damage')
enemizerDamageLabel.pack(side=LEFT) enemizerDamageLabel.pack(side=LEFT)
enemizerDamageVar = StringVar() enemizerDamageVar = StringVar()
@@ -384,7 +411,7 @@ def guiMain(args=None):
enemizerDamageOption.pack(side=LEFT) enemizerDamageOption.pack(side=LEFT)
enemizerHealthFrame = Frame(enemizerFrame) enemizerHealthFrame = Frame(enemizerFrame)
enemizerHealthFrame.grid(row=2, column=2) enemizerHealthFrame.grid(row=1, column=3)
enemizerHealthLabel = Label(enemizerHealthFrame, text='Enemy health') enemizerHealthLabel = Label(enemizerHealthFrame, text='Enemy health')
enemizerHealthLabel.pack(side=LEFT) enemizerHealthLabel.pack(side=LEFT)
enemizerHealthVar = StringVar() enemizerHealthVar = StringVar()
@@ -441,14 +468,15 @@ def guiMain(args=None):
guiargs.retro = bool(retroVar.get()) guiargs.retro = bool(retroVar.get())
guiargs.quickswap = bool(quickSwapVar.get()) guiargs.quickswap = bool(quickSwapVar.get())
guiargs.disablemusic = bool(disableMusicVar.get()) guiargs.disablemusic = bool(disableMusicVar.get())
guiargs.ow_palettes = owPalettesVar.get()
guiargs.uw_palettes = uwPalettesVar.get()
guiargs.shuffleganon = bool(shuffleGanonVar.get()) guiargs.shuffleganon = bool(shuffleGanonVar.get())
guiargs.hints = bool(hintsVar.get()) guiargs.hints = bool(hintsVar.get())
guiargs.enemizercli = enemizerCLIpathVar.get() guiargs.enemizercli = enemizerCLIpathVar.get()
guiargs.shufflebosses = enemizerBossVar.get() guiargs.shufflebosses = enemizerBossVar.get()
guiargs.shuffleenemies = 'chaos' if bool(enemyShuffleVar.get()) else 'none' guiargs.shuffleenemies = enemyShuffleVar.get()
guiargs.enemy_health = enemizerHealthVar.get() guiargs.enemy_health = enemizerHealthVar.get()
guiargs.enemy_damage = enemizerDamageVar.get() guiargs.enemy_damage = enemizerDamageVar.get()
guiargs.shufflepalette = bool(paletteShuffleVar.get())
guiargs.shufflepots = bool(potShuffleVar.get()) guiargs.shufflepots = bool(potShuffleVar.get())
guiargs.custom = bool(customVar.get()) guiargs.custom = bool(customVar.get())
guiargs.customitemarray = [int(bowVar.get()), int(silverarrowVar.get()), int(boomerangVar.get()), int(magicboomerangVar.get()), int(hookshotVar.get()), int(mushroomVar.get()), int(magicpowderVar.get()), int(firerodVar.get()), guiargs.customitemarray = [int(bowVar.get()), int(silverarrowVar.get()), int(boomerangVar.get()), int(magicboomerangVar.get()), int(hookshotVar.get()), int(mushroomVar.get()), int(magicpowderVar.get()), int(firerodVar.get()),
@@ -572,6 +600,18 @@ def guiMain(args=None):
fastMenuLabel2 = Label(fastMenuFrame2, text='Menu speed') fastMenuLabel2 = Label(fastMenuFrame2, text='Menu speed')
fastMenuLabel2.pack(side=LEFT) fastMenuLabel2.pack(side=LEFT)
owPalettesFrame2 = Frame(drowDownFrame2)
owPalettesOptionMenu2 = OptionMenu(owPalettesFrame2, owPalettesVar, 'default', 'random', 'blackout')
owPalettesOptionMenu2.pack(side=RIGHT)
owPalettesLabel2 = Label(owPalettesFrame2, text='Overworld palettes')
owPalettesLabel2.pack(side=LEFT)
uwPalettesFrame2 = Frame(drowDownFrame2)
uwPalettesOptionMenu2 = OptionMenu(uwPalettesFrame2, uwPalettesVar, 'default', 'random', 'blackout')
uwPalettesOptionMenu2.pack(side=RIGHT)
uwPalettesLabel2 = Label(uwPalettesFrame2, text='Dungeon palettes')
uwPalettesLabel2.pack(side=LEFT)
namesFrame2 = Frame(drowDownFrame2) namesFrame2 = Frame(drowDownFrame2)
namesLabel2 = Label(namesFrame2, text='Player names') namesLabel2 = Label(namesFrame2, text='Player names')
namesVar2 = StringVar() namesVar2 = StringVar()
@@ -583,6 +623,8 @@ def guiMain(args=None):
heartbeepFrame2.pack(expand=True, anchor=E) heartbeepFrame2.pack(expand=True, anchor=E)
heartcolorFrame2.pack(expand=True, anchor=E) heartcolorFrame2.pack(expand=True, anchor=E)
fastMenuFrame2.pack(expand=True, anchor=E) fastMenuFrame2.pack(expand=True, anchor=E)
owPalettesFrame2.pack(expand=True, anchor=E)
uwPalettesFrame2.pack(expand=True, anchor=E)
namesFrame2.pack(expand=True, anchor=E) namesFrame2.pack(expand=True, anchor=E)
bottomFrame2 = Frame(topFrame2) bottomFrame2 = Frame(topFrame2)
@@ -592,9 +634,12 @@ def guiMain(args=None):
guiargs.heartbeep = heartbeepVar.get() guiargs.heartbeep = heartbeepVar.get()
guiargs.heartcolor = heartcolorVar.get() guiargs.heartcolor = heartcolorVar.get()
guiargs.fastmenu = fastMenuVar.get() guiargs.fastmenu = fastMenuVar.get()
guiargs.ow_palettes = owPalettesVar.get()
guiargs.uw_palettes = uwPalettesVar.get()
guiargs.quickswap = bool(quickSwapVar.get()) guiargs.quickswap = bool(quickSwapVar.get())
guiargs.disablemusic = bool(disableMusicVar.get()) guiargs.disablemusic = bool(disableMusicVar.get())
guiargs.rom = romVar2.get() guiargs.rom = romVar2.get()
guiargs.baserom = romVar.get()
guiargs.sprite = sprite guiargs.sprite = sprite
guiargs.names = namesEntry2.get() guiargs.names = namesEntry2.get()
try: try:

View File

@@ -1,5 +1,6 @@
import collections import collections
from BaseClasses import Region, Location, Entrance, RegionType, Shop, ShopType from BaseClasses import RegionType
from Regions import create_lw_region, create_dw_region, create_cave_region, create_dungeon_region
def create_inverted_regions(world, player): def create_inverted_regions(world, player):
@@ -318,31 +319,6 @@ def create_inverted_regions(world, player):
shop.add_inventory(1, 'Arrow Upgrade (+5)', 100, 7) shop.add_inventory(1, 'Arrow Upgrade (+5)', 100, 7)
world.initialize_regions() world.initialize_regions()
def create_lw_region(player, name, locations=None, exits=None):
return _create_region(player, name, RegionType.LightWorld, 'Light World', locations, exits)
def create_dw_region(player, name, locations=None, exits=None):
return _create_region(player, name, RegionType.DarkWorld, 'Dark World', locations, exits)
def create_cave_region(player, name, hint='Hyrule', locations=None, exits=None):
return _create_region(player, name, RegionType.Cave, hint, locations, exits)
def create_dungeon_region(player, name, hint='Hyrule', locations=None, exits=None):
return _create_region(player, name, RegionType.Dungeon, hint, locations, exits)
def _create_region(player, name, type, hint='Hyrule', locations=None, exits=None):
ret = Region(name, type, hint, player)
if locations is None:
locations = []
if exits is None:
exits = []
for exit in exits:
ret.exits.append(Entrance(player, exit, ret))
for location in locations:
address, player_address, crystal, hint_text = location_table[location]
ret.locations.append(Location(player, location, address, crystal, hint_text, ret, player_address))
return ret
def mark_dark_world_regions(world, player): def mark_dark_world_regions(world, player):
# cross world caves may have some sections marked as both in_light_world, and in_dark_work. # cross world caves may have some sections marked as both in_light_world, and in_dark_work.

View File

@@ -51,8 +51,8 @@ difficulties = {
progressivearmor = ['Progressive Armor'] * 2, progressivearmor = ['Progressive Armor'] * 2,
basicarmor = ['Blue Mail', 'Red Mail'], basicarmor = ['Blue Mail', 'Red Mail'],
swordless = ['Rupees (20)'] * 4, swordless = ['Rupees (20)'] * 4,
progressivesword = ['Progressive Sword'] * 3, progressivesword = ['Progressive Sword'] * 4,
basicsword = ['Master Sword', 'Tempered Sword', 'Golden Sword'], basicsword = ['Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'],
basicbow = ['Bow', 'Silver Arrows'], basicbow = ['Bow', 'Silver Arrows'],
timedohko = ['Green Clock'] * 25, timedohko = ['Green Clock'] * 25,
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10, timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
@@ -78,8 +78,8 @@ difficulties = {
progressivearmor = ['Progressive Armor'] * 2, progressivearmor = ['Progressive Armor'] * 2,
basicarmor = ['Progressive Armor'] * 2, # neither will count basicarmor = ['Progressive Armor'] * 2, # neither will count
swordless = ['Rupees (20)'] * 4, swordless = ['Rupees (20)'] * 4,
progressivesword = ['Progressive Sword'] * 3, progressivesword = ['Progressive Sword'] * 4,
basicsword = ['Master Sword', 'Master Sword', 'Tempered Sword'], basicsword = ['Fighter Sword', 'Master Sword', 'Master Sword', 'Tempered Sword'],
basicbow = ['Bow'] * 2, basicbow = ['Bow'] * 2,
timedohko = ['Green Clock'] * 25, timedohko = ['Green Clock'] * 25,
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10, timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
@@ -105,8 +105,8 @@ difficulties = {
progressivearmor = ['Progressive Armor'] * 2, # neither will count progressivearmor = ['Progressive Armor'] * 2, # neither will count
basicarmor = ['Progressive Armor'] * 2, # neither will count basicarmor = ['Progressive Armor'] * 2, # neither will count
swordless = ['Rupees (20)'] * 4, swordless = ['Rupees (20)'] * 4,
progressivesword = ['Progressive Sword'] * 3, progressivesword = ['Progressive Sword'] * 4,
basicsword = ['Fighter Sword', 'Master Sword', 'Master Sword'], basicsword = ['Fighter Sword', 'Fighter Sword', 'Master Sword', 'Master Sword'],
basicbow = ['Bow'] * 2, basicbow = ['Bow'] * 2,
timedohko = ['Green Clock'] * 20 + ['Red Clock'] * 5, timedohko = ['Green Clock'] * 20 + ['Red Clock'] * 5,
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10, timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
@@ -308,9 +308,8 @@ def set_up_take_anys(world, player):
entrance = world.get_region(reg, player).entrances[0] entrance = world.get_region(reg, player).entrances[0]
connect_entrance(world, entrance, old_man_take_any, player) connect_entrance(world, entrance, old_man_take_any, player)
entrance.target = 0x58 entrance.target = 0x58
old_man_take_any.shop = Shop(old_man_take_any, 0x0112, None, ShopType.TakeAny, 0xE2, True) old_man_take_any.shop = Shop(old_man_take_any, 0x0112, ShopType.TakeAny, 0xE2, True, True)
world.shops.append(old_man_take_any.shop) world.shops.append(old_man_take_any.shop)
old_man_take_any.shop.active = True
swords = [item for item in world.itempool if item.type == 'Sword' and item.player == player] swords = [item for item in world.itempool if item.type == 'Sword' and item.player == player]
if swords: if swords:
@@ -331,9 +330,8 @@ def set_up_take_anys(world, player):
entrance = world.get_region(reg, player).entrances[0] entrance = world.get_region(reg, player).entrances[0]
connect_entrance(world, entrance, take_any, player) connect_entrance(world, entrance, take_any, player)
entrance.target = target entrance.target = target
take_any.shop = Shop(take_any, room_id, None, ShopType.TakeAny, 0xE3, True) take_any.shop = Shop(take_any, room_id, ShopType.TakeAny, 0xE3, True, True)
world.shops.append(take_any.shop) world.shops.append(take_any.shop)
take_any.shop.active = True
take_any.shop.add_inventory(0, 'Blue Potion', 0, 0) take_any.shop.add_inventory(0, 'Blue Potion', 0, 0)
take_any.shop.add_inventory(1, 'Boss Heart Container', 0, 0) take_any.shop.add_inventory(1, 'Boss Heart Container', 0, 0)
@@ -386,26 +384,19 @@ def fill_prizes(world, attempts=15):
def set_up_shops(world, player): def set_up_shops(world, player):
# Changes to basic Shops
# TODO: move hard+ mode changes for sheilds here, utilizing the new shops # TODO: move hard+ mode changes for sheilds here, utilizing the new shops
for shop in world.shops:
shop.active = True
if world.retro[player]: if world.retro[player]:
rss = world.get_region('Red Shield Shop', player).shop rss = world.get_region('Red Shield Shop', player).shop
rss.active = True if not rss.locked:
rss.add_inventory(2, 'Single Arrow', 80) rss.add_inventory(2, 'Single Arrow', 80)
for shop in random.sample([s for s in world.shops if s.custom and not s.locked and s.region.player == player], 5):
# Randomized changes to Shops shop.locked = True
if world.retro[player]:
for shop in random.sample([s for s in world.shops if s.replaceable and s.type == ShopType.Shop and s.region.player == player], 5):
shop.active = True
shop.add_inventory(0, 'Single Arrow', 80) shop.add_inventory(0, 'Single Arrow', 80)
shop.add_inventory(1, 'Small Key (Universal)', 100) shop.add_inventory(1, 'Small Key (Universal)', 100)
shop.add_inventory(2, 'Bombs (10)', 50) shop.add_inventory(2, 'Bombs (10)', 50)
rss.locked = True
#special shop types
def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, retro): def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, retro):
pool = [] pool = []
@@ -465,34 +456,17 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, r
else: else:
pool.extend(diff.basicarmor) pool.extend(diff.basicarmor)
if swords != 'swordless':
if want_progressives(): if want_progressives():
pool.extend(['Progressive Bow'] * 2) pool.extend(['Progressive Bow'] * 2)
else: elif swords != 'swordless':
pool.extend(diff.basicbow) pool.extend(diff.basicbow)
else:
pool.extend(['Bow', 'Silver Arrows'])
if swords == 'swordless': if swords == 'swordless':
pool.extend(diff.swordless) pool.extend(diff.swordless)
if want_progressives():
pool.extend(['Progressive Bow'] * 2)
else:
pool.extend(['Bow', 'Silver Arrows'])
elif swords == 'assured':
precollected_items.append('Fighter Sword')
if want_progressives():
pool.extend(diff.progressivesword)
pool.extend(['Rupees (100)'])
else:
pool.extend(diff.basicsword)
pool.extend(['Rupees (100)'])
elif swords == 'vanilla': elif swords == 'vanilla':
swords_to_use = [] swords_to_use = diff.progressivesword.copy() if want_progressives() else diff.basicsword.copy()
if want_progressives():
swords_to_use.extend(diff.progressivesword)
swords_to_use.extend(['Progressive Sword'])
else:
swords_to_use.extend(diff.basicsword)
swords_to_use.extend(['Fighter Sword'])
random.shuffle(swords_to_use) random.shuffle(swords_to_use)
place_item('Link\'s Uncle', swords_to_use.pop()) place_item('Link\'s Uncle', swords_to_use.pop())
@@ -503,12 +477,15 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, r
else: else:
place_item('Master Sword Pedestal', 'Triforce') place_item('Master Sword Pedestal', 'Triforce')
else: else:
pool.extend(diff.progressivesword if want_progressives() else diff.basicsword)
if swords == 'assured':
if want_progressives(): if want_progressives():
pool.extend(diff.progressivesword) precollected_items.append('Progressive Sword')
pool.extend(['Progressive Sword']) pool.remove('Progressive Sword')
else: else:
pool.extend(diff.basicsword) precollected_items.append('Fighter Sword')
pool.extend(['Fighter Sword']) pool.remove('Fighter Sword')
pool.extend(['Rupees (50)'])
extraitems = total_items_to_place - len(pool) - len(placed_items) extraitems = total_items_to_place - len(pool) - len(placed_items)

View File

@@ -25,6 +25,7 @@ def ItemFactory(items, player):
# Format: Name: (Advancement, Priority, Type, ItemCode, Pedestal Hint Text, Pedestal Credit Text, Sick Kid Credit Text, Zora Credit Text, Witch Credit Text, Flute Boy Credit Text, Hint Text) # Format: Name: (Advancement, Priority, Type, ItemCode, Pedestal Hint Text, Pedestal Credit Text, Sick Kid Credit Text, Zora Credit Text, Witch Credit Text, Flute Boy Credit Text, Hint Text)
item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'the Bow'), item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'the Bow'),
'Progressive Bow': (True, False, None, 0x64, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'), 'Progressive Bow': (True, False, None, 0x64, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'),
'Progressive Bow (Alt)': (True, False, None, 0x65, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'),
'Book of Mudora': (True, False, None, 0x1D, 'This is a\nparadox?!', 'and the story book', 'the scholarly kid', 'moon runes for sale', 'drugs for literacy', 'book-worm boy can read again', 'the Book'), 'Book of Mudora': (True, False, None, 0x1D, 'This is a\nparadox?!', 'and the story book', 'the scholarly kid', 'moon runes for sale', 'drugs for literacy', 'book-worm boy can read again', 'the Book'),
'Hammer': (True, False, None, 0x09, 'stop\nhammer time!', 'and m c hammer', 'hammer-smashing kid', 'm c hammer for sale', 'stop... hammer time', 'stop, hammer time', 'the hammer'), 'Hammer': (True, False, None, 0x09, 'stop\nhammer time!', 'and m c hammer', 'hammer-smashing kid', 'm c hammer for sale', 'stop... hammer time', 'stop, hammer time', 'the hammer'),
'Hookshot': (True, False, None, 0x0A, 'BOING!!!\nBOING!!!\nBOING!!!', 'and the tickle beam', 'tickle-monster kid', 'tickle beam for sale', 'witch and tickle boy', 'beam boy tickles again', 'the Hookshot'), 'Hookshot': (True, False, None, 0x0A, 'BOING!!!\nBOING!!!\nBOING!!!', 'and the tickle beam', 'tickle-monster kid', 'tickle beam for sale', 'witch and tickle boy', 'beam boy tickles again', 'the Hookshot'),
@@ -43,8 +44,8 @@ item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher cla
'Flippers': (True, False, None, 0x1E, 'fancy a swim?', 'and the toewebs', 'the swimming kid', 'finger webs for sale', 'shrooms let you swim', 'swimming boy swims again', 'the flippers'), 'Flippers': (True, False, None, 0x1E, 'fancy a swim?', 'and the toewebs', 'the swimming kid', 'finger webs for sale', 'shrooms let you swim', 'swimming boy swims again', 'the flippers'),
'Ice Rod': (True, False, None, 0x08, 'I\'m the cold\nrod. I make\nthings freeze!', 'and the freeze ray', 'the ice-bending kid', 'freeze ray for sale', 'fungus for ice-rod', 'ice-cube boy freezes again', 'the ice rod'), 'Ice Rod': (True, False, None, 0x08, 'I\'m the cold\nrod. I make\nthings freeze!', 'and the freeze ray', 'the ice-bending kid', 'freeze ray for sale', 'fungus for ice-rod', 'ice-cube boy freezes again', 'the ice rod'),
'Titans Mitts': (True, False, None, 0x1C, 'Now you can\nlift heavy\nstuff!', 'and the golden glove', 'body-building kid', 'carry glove for sale', 'fungus for bling-gloves', 'body-building boy has gold again', 'the mitts'), 'Titans Mitts': (True, False, None, 0x1C, 'Now you can\nlift heavy\nstuff!', 'and the golden glove', 'body-building kid', 'carry glove for sale', 'fungus for bling-gloves', 'body-building boy has gold again', 'the mitts'),
'Ether': (True, False, None, 0x10, 'This magic\ncoin freezes\neverything!', 'and the bolt coin', 'coin-collecting kid', 'bolt coin for sale', 'shrooms for bolt-coin', 'medallion boy sees floor again', 'Ether'),
'Bombos': (True, False, None, 0x0F, 'Burn, baby,\nburn! Fear my\nring of fire!', 'and the swirly coin', 'coin-collecting kid', 'swirly coin for sale', 'shrooms for swirly-coin', 'medallion boy melts room again', 'Bombos'), 'Bombos': (True, False, None, 0x0F, 'Burn, baby,\nburn! Fear my\nring of fire!', 'and the swirly coin', 'coin-collecting kid', 'swirly coin for sale', 'shrooms for swirly-coin', 'medallion boy melts room again', 'Bombos'),
'Ether': (True, False, None, 0x10, 'This magic\ncoin freezes\neverything!', 'and the bolt coin', 'coin-collecting kid', 'bolt coin for sale', 'shrooms for bolt-coin', 'medallion boy sees floor again', 'Ether'),
'Quake': (True, False, None, 0x11, 'Maxing out the\nRichter scale\nis what I do!', 'and the wavy coin', 'coin-collecting kid', 'wavy coin for sale', 'shrooms for wavy-coin', 'medallion boy shakes dirt again', 'Quake'), 'Quake': (True, False, None, 0x11, 'Maxing out the\nRichter scale\nis what I do!', 'and the wavy coin', 'coin-collecting kid', 'wavy coin for sale', 'shrooms for wavy-coin', 'medallion boy shakes dirt again', 'Quake'),
'Bottle': (True, False, None, 0x16, 'Now you can\nstore potions\nand stuff!', 'and the terrarium', 'the terrarium kid', 'terrarium for sale', 'special promotion', 'bottle boy has terrarium again', 'a Bottle'), 'Bottle': (True, False, None, 0x16, 'Now you can\nstore potions\nand stuff!', 'and the terrarium', 'the terrarium kid', 'terrarium for sale', 'special promotion', 'bottle boy has terrarium again', 'a Bottle'),
'Bottle (Red Potion)': (True, False, None, 0x2B, 'Hearty red goop!', 'and the red goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has red goo again', 'a Bottle'), 'Bottle (Red Potion)': (True, False, None, 0x2B, 'Hearty red goop!', 'and the red goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has red goo again', 'a Bottle'),

110
Main.py
View File

@@ -4,15 +4,16 @@ from itertools import zip_longest
import json import json
import logging import logging
import os import os
import pickle
import random import random
import time import time
import zlib
from BaseClasses import World, CollectionState, Item, Region, Location, Shop from BaseClasses import World, CollectionState, Item, Region, Location, Shop
from Regions import create_regions, mark_light_world_regions from Items import ItemFactory
from Regions import create_regions, create_shops, mark_light_world_regions
from InvertedRegions import create_inverted_regions, mark_dark_world_regions from InvertedRegions import create_inverted_regions, mark_dark_world_regions
from EntranceShuffle import link_entrances, link_inverted_entrances from EntranceShuffle import link_entrances, link_inverted_entrances
from Rom import patch_rom, get_race_rom_patches, get_enemizer_patch, apply_rom_settings, Sprite, LocalRom, JsonRom from Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, JsonRom
from Doors import create_doors from Doors import create_doors
from DoorShuffle import link_doors from DoorShuffle import link_doors
from RoomData import create_rooms from RoomData import create_rooms
@@ -26,16 +27,13 @@ __version__ = '0.0.1-pre'
def main(args, seed=None): def main(args, seed=None):
if args.outputpath: if args.outputpath:
try: os.makedirs(args.outputpath, exist_ok=True)
os.mkdir(args.outputpath)
except OSError:
pass
output_path.cached_path = args.outputpath output_path.cached_path = args.outputpath
start = time.perf_counter() start = time.perf_counter()
# initialize the world # initialize the world
world = World(args.multi, args.shuffle, args.door_shuffle, args.logic, args.mode, args.swords, args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm, args.accessibility, args.shuffleganon, args.quickswap, args.fastmenu, args.disablemusic, args.retro, args.custom, args.customitemarray, args.hints) world = World(args.multi, args.shuffle, args.door_shuffle, args.logic, args.mode, args.swords, args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm, args.accessibility, args.shuffleganon, args.retro, args.custom, args.customitemarray, args.hints)
logger = logging.getLogger('') logger = logging.getLogger('')
if seed is None: if seed is None:
random.seed(None) random.seed(None)
@@ -67,13 +65,16 @@ def main(args, seed=None):
if world.mode[player] == 'standard' and world.enemy_shuffle[player] != 'none': if world.mode[player] == 'standard' and world.enemy_shuffle[player] != 'none':
world.escape_assist[player].append('bombs') # enemized escape assumes infinite bombs available and will likely be unbeatable without it world.escape_assist[player].append('bombs') # enemized escape assumes infinite bombs available and will likely be unbeatable without it
for tok in filter(None, args.startinventory[player].split(',')):
item = ItemFactory(tok.strip(), player)
if item:
world.push_precollected(item)
if world.mode[player] != 'inverted': if world.mode[player] != 'inverted':
create_regions(world, player) create_regions(world, player)
create_doors(world, player)
create_rooms(world, player)
create_dungeons(world, player)
else: else:
create_inverted_regions(world, player) # todo: port all the dungeon region work create_inverted_regions(world, player)
create_shops(world, player)
create_doors(world, player) create_doors(world, player)
create_rooms(world, player) create_rooms(world, player)
create_dungeons(world, player) create_dungeons(world, player)
@@ -143,65 +144,38 @@ def main(args, seed=None):
logger.info('Patching ROM.') logger.info('Patching ROM.')
if args.sprite is not None:
if isinstance(args.sprite, Sprite):
sprite = args.sprite
else:
sprite = Sprite(args.sprite)
else:
sprite = None
player_names = parse_names_string(args.names) player_names = parse_names_string(args.names)
outfilebase = 'ER_%s' % (args.outputname if args.outputname else world.seed) outfilebase = 'ER_%s' % (args.outputname if args.outputname else world.seed)
rom_names = []
jsonout = {} jsonout = {}
if not args.suppress_rom: if not args.suppress_rom:
from MultiServer import MultiWorld
multidata = MultiWorld()
multidata.players = world.players
for player in range(1, world.players + 1): for player in range(1, world.players + 1):
sprite_random_on_hit = type(args.sprite[player]) is str and args.sprite[player].lower() == 'randomonhit'
use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player] != 'none' use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player] != 'none'
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default' or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
or args.shufflepalette[player] or args.shufflepots[player]) or args.shufflepots[player] or sprite_random_on_hit)
rom = JsonRom() if args.jsonout or use_enemizer else LocalRom(args.rom)
local_rom = None
if args.jsonout:
rom = JsonRom()
else:
if use_enemizer:
local_rom = LocalRom(args.rom)
rom = JsonRom()
else:
rom = LocalRom(args.rom)
patch_rom(world, player, rom, use_enemizer) patch_rom(world, player, rom, use_enemizer)
rom_names.append((player, list(rom.name)))
enemizer_patch = []
if use_enemizer and (args.enemizercli or not args.jsonout): if use_enemizer and (args.enemizercli or not args.jsonout):
enemizer_patch = get_enemizer_patch(world, player, rom, args.rom, args.enemizercli, args.shufflepalette[player], args.shufflepots[player]) patch_enemizer(world, player, rom, args.rom, args.enemizercli, args.shufflepots[player], sprite_random_on_hit)
if not args.jsonout:
patches = rom.patches
rom = LocalRom(args.rom)
rom.merge_enemizer_patches(patches)
multidata.rom_names[player] = list(rom.name) if args.race:
for location in world.get_filled_locations(player): patch_race_rom(rom)
if type(location.address) is int:
multidata.locations[(location.address, player)] = (location.item.code, location.item.player) apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player], args.fastmenu[player], args.disablemusic[player], args.sprite[player], args.ow_palettes[player], args.uw_palettes[player], player_names)
if args.jsonout: if args.jsonout:
jsonout[f'patch{player}'] = rom.patches jsonout[f'patch{player}'] = rom.patches
if use_enemizer:
jsonout[f'enemizer{player}'] = enemizer_patch
if args.race:
jsonout[f'race{player}'] = get_race_rom_patches(rom)
else: else:
if use_enemizer:
local_rom.patch_enemizer(rom.patches, os.path.join(os.path.dirname(args.enemizercli), "enemizerBasePatch.json"), enemizer_patch)
rom = local_rom
if args.race:
for addr, values in get_race_rom_patches(rom).items():
rom.write_bytes(int(addr), values)
apply_rom_settings(rom, args.heartbeep, args.heartcolor, world.quickswap, world.fastmenu, world.disable_music, sprite, player_names)
mcsb_name = '' mcsb_name = ''
if all([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player], world.bigkeyshuffle[player]]): if all([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player], world.bigkeyshuffle[player]]):
mcsb_name = '-keysanity' mcsb_name = '-keysanity'
@@ -222,8 +196,15 @@ def main(args, seed=None):
"-nohints" if not world.hints[player] else "")) if not args.outputname else '' "-nohints" if not world.hints[player] else "")) if not args.outputname else ''
rom.write_to_file(output_path(f'{outfilebase}{playername}{outfilesuffix}.sfc')) rom.write_to_file(output_path(f'{outfilebase}{playername}{outfilesuffix}.sfc'))
multidata = zlib.compress(json.dumps((world.players,
rom_names,
[((location.address, location.player), (location.item.code, location.item.player)) for location in world.get_filled_locations() if type(location.address) is int])
).encode("utf-8"))
if args.jsonout:
jsonout["multidata"] = list(multidata)
else:
with open(output_path('%s_multidata' % outfilebase), 'wb') as f: with open(output_path('%s_multidata' % outfilebase), 'wb') as f:
pickle.dump(multidata, f, pickle.HIGHEST_PROTOCOL) f.write(multidata)
if args.create_spoiler and not args.jsonout: if args.create_spoiler and not args.jsonout:
world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase)) world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase))
@@ -244,7 +225,7 @@ def main(args, seed=None):
def copy_world(world): def copy_world(world):
# ToDo: Not good yet # ToDo: Not good yet
ret = World(world.players, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, world.accessibility, world.shuffle_ganon, world.quickswap, world.fastmenu, world.disable_music, world.retro, world.custom, world.customitemarray, world.hints) ret = World(world.players, world.shuffle, world.door_shuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints)
ret.required_medallions = world.required_medallions.copy() ret.required_medallions = world.required_medallions.copy()
ret.swamp_patch_required = world.swamp_patch_required.copy() ret.swamp_patch_required = world.swamp_patch_required.copy()
ret.ganon_at_pyramid = world.ganon_at_pyramid.copy() ret.ganon_at_pyramid = world.ganon_at_pyramid.copy()
@@ -282,6 +263,7 @@ def copy_world(world):
create_regions(ret, player) create_regions(ret, player)
else: else:
create_inverted_regions(ret, player) create_inverted_regions(ret, player)
create_shops(ret, player)
create_dungeons(ret, player) create_dungeons(ret, player)
copy_dynamic_regions_and_locations(world, ret) copy_dynamic_regions_and_locations(world, ret)
@@ -293,7 +275,6 @@ def copy_world(world):
for shop in world.shops: for shop in world.shops:
copied_shop = ret.get_region(shop.region.name, shop.region.player).shop copied_shop = ret.get_region(shop.region.name, shop.region.player).shop
copied_shop.active = shop.active
copied_shop.inventory = copy.copy(shop.inventory) copied_shop.inventory = copy.copy(shop.inventory)
# connect copied world # connect copied world
@@ -346,7 +327,7 @@ def copy_dynamic_regions_and_locations(world, ret):
# Note: ideally exits should be copied here, but the current use case (Take anys) do not require this # Note: ideally exits should be copied here, but the current use case (Take anys) do not require this
if region.shop: if region.shop:
new_reg.shop = Shop(new_reg, region.shop.room_id, region.shop.default_door_id, region.shop.type, region.shop.shopkeeper_config, region.shop.replaceable) new_reg.shop = Shop(new_reg, region.shop.room_id, region.shop.type, region.shop.shopkeeper_config, region.shop.custom, region.shop.locked)
ret.shops.append(new_reg.shop) ret.shops.append(new_reg.shop)
for location in world.dynamic_locations: for location in world.dynamic_locations:
@@ -412,7 +393,6 @@ def create_playthrough(world):
logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player) logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player)
old_item = location.item old_item = location.item
location.item = None location.item = None
state.remove(old_item)
if world.can_beat_game(state_cache[num]): if world.can_beat_game(state_cache[num]):
to_delete.append(location) to_delete.append(location)
else: else:
@@ -423,6 +403,14 @@ def create_playthrough(world):
for location in to_delete: for location in to_delete:
sphere.remove(location) sphere.remove(location)
# second phase, sphere 0
for item in [i for i in world.precollected_items if i.advancement]:
logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
world.precollected_items.remove(item)
world.state.remove(item)
if not world.can_beat_game():
world.push_precollected(item)
# we are now down to just the required progress items in collection_spheres. Unfortunately # we are now down to just the required progress items in collection_spheres. Unfortunately
# the previous pruning stage could potentially have made certain items dependant on others # the previous pruning stage could potentially have made certain items dependant on others
# in the same or later sphere (because the location had 2 ways to access but the item originally # in the same or later sphere (because the location had 2 ways to access but the item originally
@@ -474,4 +462,6 @@ def create_playthrough(world):
old_world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = get_path(state, world.get_region('Inverted Big Bomb Shop', player)) old_world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = get_path(state, world.get_region('Inverted Big Bomb Shop', player))
# we can finally output our playthrough # we can finally output our playthrough
old_world.spoiler.playthrough = OrderedDict([(str(i + 1), {str(location): str(location.item) for location in sphere}) for i, sphere in enumerate(collection_spheres)]) old_world.spoiler.playthrough = OrderedDict([("0", [str(item) for item in world.precollected_items if item.advancement])])
for i, sphere in enumerate(collection_spheres):
old_world.spoiler.playthrough[str(i + 1)] = {str(location): str(location.item) for location in sphere}

View File

@@ -543,7 +543,7 @@ async def server_loop(ctx : Context):
print('Enter multiworld server address') print('Enter multiworld server address')
ctx.server_address = await console_input(ctx) ctx.server_address = await console_input(ctx)
address = 'ws://' + ctx.server_address address = f"ws://{ctx.server_address}" if "://" not in ctx.server_address else ctx.server_address
print('Connecting to multiworld server at %s' % address) print('Connecting to multiworld server at %s' % address)
try: try:

View File

@@ -4,10 +4,10 @@ import asyncio
import functools import functools
import json import json
import logging import logging
import pickle
import re import re
import urllib.request import urllib.request
import websockets import websockets
import zlib
import Items import Items
import Regions import Regions
@@ -22,18 +22,14 @@ class Client:
self.slot = None self.slot = None
self.send_index = 0 self.send_index = 0
class MultiWorld:
def __init__(self):
self.players = None
self.rom_names = {}
self.locations = {}
class Context: class Context:
def __init__(self, host, port, password): def __init__(self, host, port, password):
self.data_filename = None self.data_filename = None
self.save_filename = None self.save_filename = None
self.disable_save = False self.disable_save = False
self.world = MultiWorld() self.players = 0
self.rom_names = {}
self.locations = {}
self.host = host self.host = host
self.port = port self.port = port
self.password = password self.password = password
@@ -44,7 +40,7 @@ class Context:
def get_room_info(ctx : Context): def get_room_info(ctx : Context):
return { return {
'password': ctx.password is not None, 'password': ctx.password is not None,
'slots': ctx.world.players, 'slots': ctx.players,
'players': [(client.name, client.team, client.slot) for client in ctx.clients if client.auth] 'players': [(client.name, client.team, client.slot) for client in ctx.clients if client.auth]
} }
@@ -175,8 +171,8 @@ def forfeit_player(ctx : Context, team, slot, name):
def register_location_checks(ctx : Context, name, team, slot, locations): def register_location_checks(ctx : Context, name, team, slot, locations):
found_items = False found_items = False
for location in locations: for location in locations:
if (location, slot) in ctx.world.locations: if (location, slot) in ctx.locations:
target_item, target_player = ctx.world.locations[(location, slot)] target_item, target_player = ctx.locations[(location, slot)]
if target_player != slot: if target_player != slot:
found = False found = False
recvd_items = get_received_items(ctx, team, target_player) recvd_items = get_received_items(ctx, team, target_player)
@@ -196,7 +192,10 @@ def register_location_checks(ctx : Context, name, team, slot, locations):
if found_items and not ctx.disable_save: if found_items and not ctx.disable_save:
try: try:
with open(ctx.save_filename, "wb") as f: with open(ctx.save_filename, "wb") as f:
pickle.dump((ctx.world.players, ctx.world.rom_names, ctx.received_items), f, pickle.HIGHEST_PROTOCOL) jsonstr = json.dumps((ctx.players,
[(k, v) for k, v in ctx.rom_names.items()],
[(k, [i.__dict__ for i in v]) for k, v in ctx.received_items.items()]))
f.write(zlib.compress(jsonstr.encode("utf-8")))
except Exception as e: except Exception as e:
logging.exception(e) logging.exception(e)
@@ -233,13 +232,13 @@ async def process_client_cmd(ctx : Context, client : Client, cmd, args):
if 'slot' in args and any([c.slot == args['slot'] for c in ctx.clients if c.auth and same_team(c.team, client.team)]): if 'slot' in args and any([c.slot == args['slot'] for c in ctx.clients if c.auth and same_team(c.team, client.team)]):
errors.add('SlotAlreadyTaken') errors.add('SlotAlreadyTaken')
elif 'slot' not in args or not args['slot']: elif 'slot' not in args or not args['slot']:
for slot in range(1, ctx.world.players + 1): for slot in range(1, ctx.players + 1):
if slot not in [c.slot for c in ctx.clients if c.auth and same_team(c.team, client.team)]: if slot not in [c.slot for c in ctx.clients if c.auth and same_team(c.team, client.team)]:
client.slot = slot client.slot = slot
break break
elif slot == ctx.world.players: elif slot == ctx.players:
errors.add('SlotAlreadyTaken') errors.add('SlotAlreadyTaken')
elif args['slot'] not in range(1, ctx.world.players + 1): elif args['slot'] not in range(1, ctx.players + 1):
errors.add('InvalidSlot') errors.add('InvalidSlot')
else: else:
client.slot = args['slot'] client.slot = args['slot']
@@ -251,7 +250,7 @@ async def process_client_cmd(ctx : Context, client : Client, cmd, args):
await send_msgs(client.socket, [['ConnectionRefused', list(errors)]]) await send_msgs(client.socket, [['ConnectionRefused', list(errors)]])
else: else:
client.auth = True client.auth = True
reply = [['Connected', ctx.world.rom_names[client.slot]]] reply = [['Connected', ctx.rom_names[client.slot]]]
items = get_received_items(ctx, client.team, client.slot) items = get_received_items(ctx, client.team, client.slot)
if items: if items:
reply.append(['ReceivedItems', (0, tuplize_received_items(items))]) reply.append(['ReceivedItems', (0, tuplize_received_items(items))])
@@ -358,13 +357,16 @@ async def main():
ctx.data_filename = tkinter.filedialog.askopenfilename(filetypes=(("Multiworld data","*multidata"),)) ctx.data_filename = tkinter.filedialog.askopenfilename(filetypes=(("Multiworld data","*multidata"),))
with open(ctx.data_filename, 'rb') as f: with open(ctx.data_filename, 'rb') as f:
ctx.world = pickle.load(f) jsonobj = json.loads(zlib.decompress(f.read()).decode("utf-8"))
ctx.players = jsonobj[0]
ctx.rom_names = {k: v for k, v in jsonobj[1]}
ctx.locations = {tuple(k): tuple(v) for k, v in jsonobj[2]}
except Exception as e: except Exception as e:
print('Failed to read multiworld data (%s)' % e) print('Failed to read multiworld data (%s)' % e)
return return
ip = urllib.request.urlopen('https://v4.ident.me').read().decode('utf8') if not ctx.host else ctx.host ip = urllib.request.urlopen('https://v4.ident.me').read().decode('utf8') if not ctx.host else ctx.host
print('Hosting game of %d players (%s) at %s:%d' % (ctx.world.players, 'No password' if not ctx.password else 'Password: %s' % ctx.password, ip, ctx.port)) print('Hosting game of %d players (%s) at %s:%d' % (ctx.players, 'No password' if not ctx.password else 'Password: %s' % ctx.password, ip, ctx.port))
ctx.disable_save = args.disable_save ctx.disable_save = args.disable_save
if not ctx.disable_save: if not ctx.disable_save:
@@ -372,8 +374,11 @@ async def main():
ctx.save_filename = (ctx.data_filename[:-9] if ctx.data_filename[-9:] == 'multidata' else (ctx.data_filename + '_')) + 'multisave' ctx.save_filename = (ctx.data_filename[:-9] if ctx.data_filename[-9:] == 'multidata' else (ctx.data_filename + '_')) + 'multisave'
try: try:
with open(ctx.save_filename, 'rb') as f: with open(ctx.save_filename, 'rb') as f:
players, rom_names, received_items = pickle.load(f) jsonobj = json.loads(zlib.decompress(f.read()).decode("utf-8"))
if players != ctx.world.players or rom_names != ctx.world.rom_names: players = jsonobj[0]
rom_names = {k: v for k, v in jsonobj[1]}
received_items = {tuple(k): [ReceivedItem(**i) for i in v] for k, v in jsonobj[2]}
if players != ctx.players or rom_names != ctx.rom_names:
raise Exception('Save file mismatch, will start a new game') raise Exception('Save file mismatch, will start a new game')
ctx.received_items = received_items ctx.received_items = received_items
print('Loaded save file with %d received items for %d players' % (sum([len(p) for p in received_items.values()]), len(received_items))) print('Loaded save file with %d received items for %d players' % (sum([len(p) for p in received_items.values()]), len(received_items)))

View File

@@ -8,15 +8,18 @@ from EntranceRandomizer import parse_arguments
from Main import main as ERmain from Main import main as ERmain
def parse_yaml(txt): def parse_yaml(txt):
def strip(s):
s = s.strip()
return '' if not s else s.strip('"') if s[0] == '"' else s.strip("'") if s[0] == "'" else s
ret = {} ret = {}
indents = {len(txt) - len(txt.lstrip(' ')): ret} indents = {len(txt) - len(txt.lstrip(' ')): ret}
for line in txt.splitlines(): for line in txt.splitlines():
if not line: if not line:
continue continue
name, val = line.split(':', 1) name, val = line.split(':', 1)
val = val.strip() val = strip(val)
spaces = len(name) - len(name.lstrip(' ')) spaces = len(name) - len(name.lstrip(' '))
name = name.strip() name = strip(name)
if val: if val:
indents[spaces][name] = val indents[spaces][name] = val
else: else:
@@ -75,6 +78,7 @@ def main():
if args.rom: if args.rom:
erargs.rom = args.rom erargs.rom = args.rom
if args.enemizercli:
erargs.enemizercli = args.enemizercli erargs.enemizercli = args.enemizercli
settings_cache = {k: (roll_settings(v) if args.samesettings else None) for k, v in weights_cache.items()} settings_cache = {k: (roll_settings(v) if args.samesettings else None) for k, v in weights_cache.items()}
@@ -84,6 +88,7 @@ def main():
if path: if path:
settings = settings_cache[path] if settings_cache[path] else roll_settings(weights_cache[path]) settings = settings_cache[path] if settings_cache[path] else roll_settings(weights_cache[path])
for k, v in vars(settings).items(): for k, v in vars(settings).items():
if v is not None:
getattr(erargs, k)[player] = v getattr(erargs, k)[player] = v
else: else:
raise RuntimeError(f'No weights specified for player {player}') raise RuntimeError(f'No weights specified for player {player}')
@@ -108,8 +113,14 @@ def get_weights(path):
return parse_yaml(yaml) return parse_yaml(yaml)
def roll_settings(weights): def roll_settings(weights):
def get_choice(option): def get_choice(option, root=weights):
return random.choices(list(weights[option].keys()), weights=list(map(int,weights[option].values())))[0].replace('"','').replace("'",'') if option not in root:
return None
if type(root[option]) is not dict:
return root[option]
if not root[option]:
return None
return random.choices(list(root[option].keys()), weights=list(map(int,root[option].values())))[0]
ret = argparse.Namespace() ret = argparse.Namespace()
@@ -122,84 +133,85 @@ def roll_settings(weights):
item_placement = get_choice('item_placement') item_placement = get_choice('item_placement')
# not supported in ER # not supported in ER
if {'map_shuffle', 'compass_shuffle', 'smallkey_shuffle', 'bigkey_shuffle'}.issubset(weights.keys()):
ret.mapshuffle = get_choice('map_shuffle') == 'on'
ret.compassshuffle = get_choice('compass_shuffle') == 'on'
ret.keyshuffle = get_choice('smallkey_shuffle') == 'on'
ret.bigkeyshuffle = get_choice('bigkey_shuffle') == 'on'
else:
dungeon_items = get_choice('dungeon_items') dungeon_items = get_choice('dungeon_items')
ret.mapshuffle = dungeon_items in ['mc', 'mcs', 'full'] ret.mapshuffle = get_choice('map_shuffle') == 'on' if 'map_shuffle' in weights else dungeon_items in ['mc', 'mcs', 'full']
ret.compassshuffle = dungeon_items in ['mc', 'mcs', 'full'] ret.compassshuffle = get_choice('compass_shuffle') == 'on' if 'compass_shuffle' in weights else dungeon_items in ['mc', 'mcs', 'full']
ret.keyshuffle = dungeon_items in ['mcs', 'full'] ret.keyshuffle = get_choice('smallkey_shuffle') == 'on' if 'smallkey_shuffle' in weights else dungeon_items in ['mcs', 'full']
ret.bigkeyshuffle = dungeon_items in ['full'] ret.bigkeyshuffle = get_choice('bigkey_shuffle') == 'on' if 'bigkey_shuffle' in weights else dungeon_items in ['full']
accessibility = get_choice('accessibility') ret.accessibility = get_choice('accessibility')
ret.accessibility = accessibility
entrance_shuffle = get_choice('entrance_shuffle') entrance_shuffle = get_choice('entrance_shuffle')
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla' ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
goals = get_choice('goals')
ret.goal = {'ganon': 'ganon', ret.goal = {'ganon': 'ganon',
'fast_ganon': 'crystals', 'fast_ganon': 'crystals',
'dungeons': 'dungeons', 'dungeons': 'dungeons',
'pedestal': 'pedestal', 'pedestal': 'pedestal',
'triforce-hunt': 'triforcehunt' 'triforce-hunt': 'triforcehunt'
}[goals] }[get_choice('goals')]
ret.openpyramid = goals == 'fast_ganon' ret.openpyramid = ret.goal == 'fast_ganon'
tower_open = get_choice('tower_open') ret.crystals_gt = get_choice('tower_open')
ret.crystals_gt = tower_open
ganon_open = get_choice('ganon_open') ret.crystals_ganon = get_choice('ganon_open')
ret.crystals_ganon = ganon_open
world_state = get_choice('world_state') ret.mode = get_choice('world_state')
ret.mode = world_state if ret.mode == 'retro':
if world_state == 'retro':
ret.mode = 'open' ret.mode = 'open'
ret.retro = True ret.retro = True
hints = get_choice('hints') ret.hints = get_choice('hints') == 'on'
ret.hints = hints == 'on'
weapons = get_choice('weapons')
ret.swords = {'randomized': 'random', ret.swords = {'randomized': 'random',
'assured': 'assured', 'assured': 'assured',
'vanilla': 'vanilla', 'vanilla': 'vanilla',
'swordless': 'swordless' 'swordless': 'swordless'
}[weapons] }[get_choice('weapons')]
item_pool = get_choice('item_pool') ret.difficulty = get_choice('item_pool')
ret.difficulty = item_pool
item_functionality = get_choice('item_functionality') ret.item_functionality = get_choice('item_functionality')
ret.item_functionality = item_functionality
boss_shuffle = get_choice('boss_shuffle')
ret.shufflebosses = {'none': 'none', ret.shufflebosses = {'none': 'none',
'simple': 'basic', 'simple': 'basic',
'full': 'normal', 'full': 'normal',
'random': 'chaos' 'random': 'chaos'
}[boss_shuffle] }[get_choice('boss_shuffle')]
enemy_shuffle = get_choice('enemy_shuffle')
ret.shuffleenemies = {'none': 'none', ret.shuffleenemies = {'none': 'none',
'shuffled': 'shuffled', 'shuffled': 'shuffled',
'random': 'chaos' 'random': 'chaos'
}[enemy_shuffle] }[get_choice('enemy_shuffle')]
enemy_damage = get_choice('enemy_damage')
ret.enemy_damage = {'default': 'default', ret.enemy_damage = {'default': 'default',
'shuffled': 'shuffled', 'shuffled': 'shuffled',
'random': 'chaos' 'random': 'chaos'
}[enemy_damage] }[get_choice('enemy_damage')]
enemy_health = get_choice('enemy_health') ret.enemy_health = get_choice('enemy_health')
ret.enemy_health = enemy_health
ret.beemizer = int(get_choice('beemizer')) if 'beemizer' in weights.keys() else 1 # suck it :) ret.shufflepots = get_choice('pot_shuffle') == 'on'
ret.beemizer = int(get_choice('beemizer')) if 'beemizer' in weights else 0
inventoryweights = weights.get('startinventory', {})
startitems = []
for item in inventoryweights.keys():
if get_choice(item, inventoryweights) == 'on':
startitems.append(item)
ret.startinventory = ','.join(startitems)
if 'rom' in weights:
romweights = weights['rom']
ret.sprite = get_choice('sprite', romweights)
ret.disablemusic = get_choice('disablemusic', romweights) == 'on'
ret.quickswap = get_choice('quickswap', romweights) == 'on'
ret.fastmenu = get_choice('menuspeed', romweights)
ret.heartcolor = get_choice('heartcolor', romweights)
ret.heartbeep = get_choice('heartbeep', romweights)
ret.ow_palettes = get_choice('ow_palettes', romweights)
ret.uw_palettes = get_choice('uw_palettes', romweights)
return ret return ret

View File

@@ -10,7 +10,7 @@ import sys
from BaseClasses import World from BaseClasses import World
from Regions import create_regions from Regions import create_regions
from EntranceShuffle import link_entrances, connect_entrance, connect_two_way, connect_exit from EntranceShuffle import link_entrances, connect_entrance, connect_two_way, connect_exit
from Rom import patch_rom, LocalRom, Sprite, write_string_to_rom, apply_rom_settings from Rom import patch_rom, LocalRom, write_string_to_rom, apply_rom_settings, get_sprite_from_name
from Rules import set_rules from Rules import set_rules
from Dungeons import create_dungeons from Dungeons import create_dungeons
from Items import ItemFactory from Items import ItemFactory
@@ -23,7 +23,7 @@ def main(args):
start_time = time.perf_counter() start_time = time.perf_counter()
# initialize the world # initialize the world
world = World(1, 'vanilla', 'noglitches', 'standard', 'normal', 'none', 'on', 'ganon', 'freshness', False, False, False, args.quickswap, args.fastmenu, args.disablemusic, False, False, False, None, False) world = World(1, 'vanilla', 'noglitches', 'standard', 'normal', 'none', 'on', 'ganon', 'freshness', False, False, False, False, False, False, None, False)
logger = logging.getLogger('') logger = logging.getLogger('')
hasher = hashlib.md5() hasher = hashlib.md5()
@@ -68,15 +68,10 @@ def main(args):
logger.info('Patching ROM.') logger.info('Patching ROM.')
if args.sprite is not None:
sprite = Sprite(args.sprite)
else:
sprite = None
rom = LocalRom(args.rom) rom = LocalRom(args.rom)
patch_rom(world, 1, rom) patch_rom(world, 1, rom, False)
apply_rom_settings(rom, args.heartbeep, args.heartcolor, world.quickswap, world.fastmenu, world.disable_music, sprite) apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, args.sprite, args.ow_palettes, args.uw_palettes)
for textname, texttype, text in text_patches: for textname, texttype, text in text_patches:
if texttype == 'text': if texttype == 'text':
@@ -213,6 +208,8 @@ def start():
help='Select the rate at which the heart beep sound is played at low health.') help='Select the rate at which the heart beep sound is played at low health.')
parser.add_argument('--heartcolor', default='red', const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow'], parser.add_argument('--heartcolor', default='red', const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow'],
help='Select the color of Link\'s heart meter. (default: %(default)s)') help='Select the color of Link\'s heart meter. (default: %(default)s)')
parser.add_argument('--ow_palettes', default='default', choices=['default', 'random', 'blackout'])
parser.add_argument('--uw_palettes', default='default', choices=['default', 'random', 'blackout'])
parser.add_argument('--sprite', help='Path to a sprite sheet to use for Link. Needs to be in binary format and have a length of 0x7000 (28672) bytes.') parser.add_argument('--sprite', help='Path to a sprite sheet to use for Link. Needs to be in binary format and have a length of 0x7000 (28672) bytes.')
parser.add_argument('--plando', help='Filled out template to use for setting up the rom.') parser.add_argument('--plando', help='Filled out template to use for setting up the rom.')
args = parser.parse_args() args = parser.parse_args()
@@ -224,8 +221,8 @@ def start():
if not os.path.isfile(args.plando): if not os.path.isfile(args.plando):
input('Could not find Plandomizer distribution at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.plando) input('Could not find Plandomizer distribution at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.plando)
sys.exit(1) sys.exit(1)
if args.sprite is not None and not os.path.isfile(args.rom): if args.sprite is not None and not os.path.isfile(args.sprite) and not get_sprite_from_name(args.sprite):
input('Could not find link sprite sheet at given location. \nPress Enter to exit.' % args.sprite) input('Could not find link sprite sheet at given location. \nPress Enter to exit.')
sys.exit(1) sys.exit(1)
# set up logger # set up logger

View File

@@ -721,6 +721,7 @@ def create_regions(world, player):
shop.add_inventory(1, 'Arrow Upgrade (+5)', 100, 7) shop.add_inventory(1, 'Arrow Upgrade (+5)', 100, 7)
world.initialize_regions() world.initialize_regions()
def create_lw_region(player, name, locations=None, exits=None): def create_lw_region(player, name, locations=None, exits=None):
return _create_region(player, name, RegionType.LightWorld, 'Light World', locations, exits) return _create_region(player, name, RegionType.LightWorld, 'Light World', locations, exits)
@@ -798,19 +799,34 @@ shop_table = {
# slot, item, price, max=0, replacement=None, replacement_price=0 # slot, item, price, max=0, replacement=None, replacement_price=0
# item = (item, price) # item = (item, price)
def create_shops(world, player):
for region_name, (room_id, type, shopkeeper, custom, locked, inventory) in shop_table.items():
if world.mode[player] == 'inverted' and region_name == 'Dark Lake Hylia Shop':
locked = True
inventory = [('Blue Potion', 160), ('Blue Shield', 50), ('Bombs (10)', 50)]
region = world.get_region(region_name, player)
shop = Shop(region, room_id, type, shopkeeper, custom, locked)
region.shop = shop
world.shops.append(shop)
for index, item in enumerate(inventory):
shop.add_inventory(index, *item)
# (type, room_id, shopkeeper, custom, locked, [items])
# item = (item, price, max=0, replacement=None, replacement_price=0)
_basic_shop_defaults = [('Red Potion', 150), ('Small Heart', 10), ('Bombs (10)', 50)] _basic_shop_defaults = [('Red Potion', 150), ('Small Heart', 10), ('Bombs (10)', 50)]
_dark_world_shop_defaults = [('Red Potion', 150), ('Blue Shield', 50), ('Bombs (10)', 50)] _dark_world_shop_defaults = [('Red Potion', 150), ('Blue Shield', 50), ('Bombs (10)', 50)]
default_shop_contents = { shop_table = {
'Cave Shop (Dark Death Mountain)': _basic_shop_defaults, 'Cave Shop (Dark Death Mountain)': (0x0112, ShopType.Shop, 0xC1, True, False, _basic_shop_defaults),
'Red Shield Shop': [('Red Shield', 500), ('Bee', 10), ('Arrows (10)', 30)], 'Red Shield Shop': (0x0110, ShopType.Shop, 0xC1, True, False, [('Red Shield', 500), ('Bee', 10), ('Arrows (10)', 30)]),
'Dark Lake Hylia Shop': _dark_world_shop_defaults, 'Dark Lake Hylia Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults),
'Dark World Lumberjack Shop': _dark_world_shop_defaults, 'Dark World Lumberjack Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults),
'Village of Outcasts Shop': _dark_world_shop_defaults, 'Village of Outcasts Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults),
'Dark World Potion Shop': _dark_world_shop_defaults, 'Dark World Potion Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults),
'Light World Death Mountain Shop': _basic_shop_defaults, 'Light World Death Mountain Shop': (0x00FF, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults),
'Kakariko Shop': _basic_shop_defaults, 'Kakariko Shop': (0x011F, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults),
'Cave Shop (Lake Hylia)': _basic_shop_defaults, 'Cave Shop (Lake Hylia)': (0x0112, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults),
'Potion Shop': [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)], 'Potion Shop': (0x0109, ShopType.Shop, 0xFF, False, True, [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)]),
'Capacity Upgrade': (0x0115, ShopType.UpgradeShop, 0x04, True, True, [('Bomb Upgrade (+5)', 100, 7), ('Arrow Upgrade (+5)', 100, 7)])
} }
key_only_locations = { key_only_locations = {

527
Rom.py
View File

@@ -9,13 +9,13 @@ import struct
import sys import sys
import subprocess import subprocess
from BaseClasses import ShopType, Region, Location, Item, DoorType from BaseClasses import CollectionState, ShopType, Region, Location, Item, DoorType
from Dungeons import dungeon_music_addresses from Dungeons import dungeon_music_addresses
from Text import MultiByteTextMapper, CompressedTextMapper, text_addresses, Credits, TextTable from Text import MultiByteTextMapper, CompressedTextMapper, text_addresses, Credits, TextTable
from Text import Uncle_texts, Ganon1_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, Blind_texts, BombShop2_texts, junk_texts from Text import Uncle_texts, Ganon1_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, Blind_texts, BombShop2_texts, junk_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 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 output_path, local_path, int16_as_bytes, int32_as_bytes, snes_to_pc from Utils import output_path, local_path, int16_as_bytes, int32_as_bytes, snes_to_pc
from Items import ItemFactory, item_table from Items import ItemFactory
from EntranceShuffle import door_addresses, exit_ids from EntranceShuffle import door_addresses, exit_ids
@@ -27,6 +27,7 @@ class JsonRom(object):
def __init__(self): def __init__(self):
self.name = None self.name = None
self.orig_buffer = None
self.patches = {} self.patches = {}
self.addresses = [] self.addresses = []
@@ -36,7 +37,6 @@ class JsonRom(object):
def write_bytes(self, startaddress, values): def write_bytes(self, startaddress, values):
if not values: if not values:
return return
if type(values) is not list:
values = list(values) values = list(values)
pos = bisect.bisect_right(self.addresses, startaddress) pos = bisect.bisect_right(self.addresses, startaddress)
@@ -72,10 +72,12 @@ class LocalRom(object):
def __init__(self, file, patch=True): def __init__(self, file, patch=True):
self.name = None self.name = None
self.orig_buffer = None
with open(file, 'rb') as stream: with open(file, 'rb') as stream:
self.buffer = read_rom(stream) self.buffer = read_rom(stream)
if patch: if patch:
self.patch_base_rom() self.patch_base_rom()
self.orig_buffer = self.buffer.copy()
def write_byte(self, address, value): def write_byte(self, address, value):
self.buffer[address] = value self.buffer[address] = value
@@ -112,24 +114,11 @@ class LocalRom(object):
# if 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.') # 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 patch_enemizer(self, rando_patch, base_enemizer_patch_path, enemizer_patch): def merge_enemizer_patches(self, patches):
# extend to 4MB
self.buffer.extend(bytearray([0x00] * (0x400000 - len(self.buffer)))) self.buffer.extend(bytearray([0x00] * (0x400000 - len(self.buffer))))
for address, values in patches.items():
# apply randomizer patches
for address, values in rando_patch.items():
self.write_bytes(int(address), values) self.write_bytes(int(address), values)
# load base enemizer patches
with open(base_enemizer_patch_path, 'r') as f:
base_enemizer_patch = json.load(f)
for patch in base_enemizer_patch:
self.write_bytes(patch["address"], patch["patchData"])
# apply enemizer patches
for patch in enemizer_patch:
self.write_bytes(patch["address"], patch["patchData"])
def write_crc(self): def write_crc(self):
crc = (sum(self.buffer[:0x7FDC] + self.buffer[0x7FE0:]) + 0x01FE) & 0xFFFF crc = (sum(self.buffer[:0x7FDC] + self.buffer[0x7FE0:]) + 0x01FE) & 0xFFFF
inv = crc ^ 0xFFFF inv = crc ^ 0xFFFF
@@ -161,9 +150,10 @@ def read_rom(stream):
buffer = buffer[0x200:] buffer = buffer[0x200:]
return buffer return buffer
def get_enemizer_patch(world, player, rom, baserom_path, enemizercli, shufflepalette, shufflepots): def patch_enemizer(world, player, rom, baserom_path, enemizercli, shufflepots, random_sprite_on_hit):
baserom_path = os.path.abspath(baserom_path) baserom_path = os.path.abspath(baserom_path)
basepatch_path = os.path.abspath(local_path('data/base2current.json')) basepatch_path = os.path.abspath(local_path('data/base2current.json'))
enemizer_basepatch_path = os.path.join(os.path.dirname(enemizercli), "enemizerBasePatch.json")
randopatch_path = os.path.abspath(output_path('enemizer_randopatch.json')) randopatch_path = os.path.abspath(output_path('enemizer_randopatch.json'))
options_path = os.path.abspath(output_path('enemizer_options.json')) options_path = os.path.abspath(output_path('enemizer_options.json'))
enemizer_output_path = os.path.abspath(output_path('enemizer_output.json')) enemizer_output_path = os.path.abspath(output_path('enemizer_output.json'))
@@ -197,10 +187,10 @@ def get_enemizer_patch(world, player, rom, baserom_path, enemizercli, shufflepal
'RandomizeBossDamageMinAmount': 0, 'RandomizeBossDamageMinAmount': 0,
'RandomizeBossDamageMaxAmount': 200, 'RandomizeBossDamageMaxAmount': 200,
'RandomizeBossBehavior': False, 'RandomizeBossBehavior': False,
'RandomizeDungeonPalettes': shufflepalette, 'RandomizeDungeonPalettes': False,
'SetBlackoutMode': False, 'SetBlackoutMode': False,
'RandomizeOverworldPalettes': shufflepalette, 'RandomizeOverworldPalettes': False,
'RandomizeSpritePalettes': shufflepalette, 'RandomizeSpritePalettes': False,
'SetAdvancedSpritePalettes': False, 'SetAdvancedSpritePalettes': False,
'PukeMode': False, 'PukeMode': False,
'NegativeMode': False, 'NegativeMode': False,
@@ -221,7 +211,7 @@ def get_enemizer_patch(world, player, rom, baserom_path, enemizercli, shufflepal
'RandomizeTileTrapPattern': world.enemy_shuffle[player] == 'chaos', 'RandomizeTileTrapPattern': world.enemy_shuffle[player] == 'chaos',
'RandomizeTileTrapFloorTile': False, 'RandomizeTileTrapFloorTile': False,
'AllowKillableThief': bool(random.randint(0,1)) if world.enemy_shuffle[player] == 'chaos' else world.enemy_shuffle[player] != 'none', 'AllowKillableThief': bool(random.randint(0,1)) if world.enemy_shuffle[player] == 'chaos' else world.enemy_shuffle[player] != 'none',
'RandomizeSpriteOnHit': False, 'RandomizeSpriteOnHit': random_sprite_on_hit,
'DebugMode': False, 'DebugMode': False,
'DebugForceEnemy': False, 'DebugForceEnemy': False,
'DebugForceEnemyId': 0, 'DebugForceEnemyId': 0,
@@ -243,21 +233,14 @@ def get_enemizer_patch(world, player, rom, baserom_path, enemizercli, shufflepal
'IcePalace': world.get_dungeon("Ice Palace", player).boss.enemizer_name, 'IcePalace': world.get_dungeon("Ice Palace", player).boss.enemizer_name,
'MiseryMire': world.get_dungeon("Misery Mire", player).boss.enemizer_name, 'MiseryMire': world.get_dungeon("Misery Mire", player).boss.enemizer_name,
'TurtleRock': world.get_dungeon("Turtle Rock", player).boss.enemizer_name, 'TurtleRock': world.get_dungeon("Turtle Rock", player).boss.enemizer_name,
'GanonsTower1': world.get_dungeon('Ganons Tower' if world.mode[player] != 'inverted' else 'Inverted Ganons Tower', player).bosses['bottom'].enemizer_name,
'GanonsTower2': world.get_dungeon('Ganons Tower' if world.mode[player] != 'inverted' else 'Inverted Ganons Tower', player).bosses['middle'].enemizer_name,
'GanonsTower3': world.get_dungeon('Ganons Tower' if world.mode[player] != 'inverted' else 'Inverted Ganons Tower', player).bosses['top'].enemizer_name,
'GanonsTower4': 'Agahnim2', 'GanonsTower4': 'Agahnim2',
'Ganon': 'Ganon', 'Ganon': 'Ganon',
} }
} }
if world.mode[player] != 'inverted':
options['ManualBosses']['GanonsTower1'] = world.get_dungeon('Ganons Tower', player).bosses['bottom'].enemizer_name
options['ManualBosses']['GanonsTower2'] = world.get_dungeon('Ganons Tower', player).bosses['middle'].enemizer_name
options['ManualBosses']['GanonsTower3'] = world.get_dungeon('Ganons Tower', player).bosses['top'].enemizer_name
else:
options['ManualBosses']['GanonsTower1'] = world.get_dungeon('Inverted Ganons Tower', player).bosses['bottom'].enemizer_name
options['ManualBosses']['GanonsTower2'] = world.get_dungeon('Inverted Ganons Tower', player).bosses['middle'].enemizer_name
options['ManualBosses']['GanonsTower3'] = world.get_dungeon('Inverted Ganons Tower', player).bosses['top'].enemizer_name
rom.write_to_file(randopatch_path) rom.write_to_file(randopatch_path)
with open(options_path, 'w') as f: with open(options_path, 'w') as f:
@@ -272,19 +255,61 @@ def get_enemizer_patch(world, player, rom, baserom_path, enemizercli, shufflepal
'--output', enemizer_output_path], '--output', enemizer_output_path],
cwd=os.path.dirname(enemizercli), stdout=subprocess.DEVNULL) cwd=os.path.dirname(enemizercli), stdout=subprocess.DEVNULL)
with open(enemizer_basepatch_path, 'r') as f:
for patch in json.load(f):
rom.write_bytes(patch["address"], patch["patchData"])
with open(enemizer_output_path, 'r') as f: with open(enemizer_output_path, 'r') as f:
ret = json.load(f) for patch in json.load(f):
rom.write_bytes(patch["address"], patch["patchData"])
if os.path.exists(randopatch_path): if random_sprite_on_hit:
_populate_sprite_table()
sprites = list(_sprite_table.values())
if sprites:
while len(sprites) < 32:
sprites.extend(sprites)
random.shuffle(sprites)
for i, path in enumerate(sprites[:32]):
sprite = Sprite(path)
rom.write_bytes(0x300000 + (i * 0x8000), sprite.sprite)
rom.write_bytes(0x307000 + (i * 0x8000), sprite.palette)
rom.write_bytes(0x307078 + (i * 0x8000), sprite.glove_palette)
try:
os.remove(randopatch_path) os.remove(randopatch_path)
except OSError:
pass
if os.path.exists(options_path): try:
os.remove(options_path) os.remove(options_path)
except OSError:
pass
if os.path.exists(enemizer_output_path): try:
os.remove(enemizer_output_path) os.remove(enemizer_output_path)
except OSError:
pass
return ret _sprite_table = {}
def _populate_sprite_table():
if not _sprite_table:
for dir in [local_path('data/sprites/official'), local_path('data/sprites/unofficial')]:
for file in os.listdir(dir):
filepath = os.path.join(dir, file)
if not os.path.isfile(filepath):
continue
sprite = Sprite(filepath)
if sprite.valid:
_sprite_table[sprite.name.lower()] = filepath
def get_sprite_from_name(name):
_populate_sprite_table()
name = name.lower()
if name in ['random', 'randomonhit']:
return Sprite(random.choice(list(_sprite_table.values())))
return Sprite(_sprite_table[name]) if name in _sprite_table else None
class Sprite(object): class Sprite(object):
default_palette = [255, 127, 126, 35, 183, 17, 158, 54, 165, 20, 255, 1, 120, 16, 157, default_palette = [255, 127, 126, 35, 183, 17, 158, 54, 165, 20, 255, 1, 120, 16, 157,
@@ -922,24 +947,158 @@ def patch_rom(world, player, rom, enemized):
rom.write_byte(0x1800A1, 0x01) # enable overworld screen transition draining for water level inside swamp rom.write_byte(0x1800A1, 0x01) # enable overworld screen transition draining for water level inside swamp
rom.write_byte(0x180174, 0x01 if world.fix_fake_world[player] else 0x00) rom.write_byte(0x180174, 0x01 if world.fix_fake_world[player] else 0x00)
rom.write_byte(0x18017E, 0x01) # Fairy fountains only trade in bottles rom.write_byte(0x18017E, 0x01) # Fairy fountains only trade in bottles
rom.write_byte(0x180034, 0x0A) # starting max bombs
rom.write_byte(0x180035, 30) # starting max arrows # Starting equipment
for x in range(0x183000, 0x18304F): equip = [0] * (0x340 + 0x4F)
rom.write_byte(x, 0) # Zero the initial equipment array equip[0x36C] = 0x18
rom.write_byte(0x18302C, 0x18) # starting max health equip[0x36D] = 0x18
rom.write_byte(0x18302D, 0x18) # starting current health equip[0x379] = 0x68
rom.write_byte(0x183039, 0x68) # starting abilities, bit array starting_max_bombs = 10
starting_max_arrows = 30
startingstate = CollectionState(world)
if startingstate.has('Bow', player):
equip[0x340] = 1
equip[0x38E] |= 0x20 # progressive flag to get the correct hint in all cases
if not world.retro[player]:
equip[0x38E] |= 0x80
if startingstate.has('Silver Arrows', player):
equip[0x38E] |= 0x40
if startingstate.has('Titans Mitts', player):
equip[0x354] = 2
elif startingstate.has('Power Glove', player):
equip[0x354] = 1
if startingstate.has('Golden Sword', player):
equip[0x359] = 4
elif startingstate.has('Tempered Sword', player):
equip[0x359] = 3
elif startingstate.has('Master Sword', player):
equip[0x359] = 2
elif startingstate.has('Fighter Sword', player):
equip[0x359] = 1
if startingstate.has('Mirror Shield', player):
equip[0x35A] = 3
elif startingstate.has('Red Shield', player):
equip[0x35A] = 2
elif startingstate.has('Blue Shield', player):
equip[0x35A] = 1
if startingstate.has('Red Mail', player):
equip[0x35B] = 2
elif startingstate.has('Blue Mail', player):
equip[0x35B] = 1
if startingstate.has('Magic Upgrade (1/4)', player):
equip[0x37B] = 2
equip[0x36E] = 0x80
elif startingstate.has('Magic Upgrade (1/2)', player):
equip[0x37B] = 1
equip[0x36E] = 0x80
for item in world.precollected_items: for item in world.precollected_items:
if item.player != player: if item.player != player:
continue continue
if item.name == 'Fighter Sword': if item.name in ['Bow', 'Silver Arrows', 'Progressive Bow', 'Progressive Bow (Alt)',
rom.write_byte(0x183000+0x19, 0x01) 'Titans Mitts', 'Power Glove', 'Progressive Glove',
rom.write_byte(0x0271A6+0x19, 0x01) 'Golden Sword', 'Tempered Sword', 'Master Sword', 'Fighter Sword', 'Progressive Sword',
rom.write_byte(0x180043, 0x01) # special starting sword byte 'Mirror Shield', 'Red Shield', 'Blue Shield', 'Progressive Shield',
'Red Mail', 'Blue Mail', 'Progressive Armor',
'Magic Upgrade (1/4)', 'Magic Upgrade (1/2)']:
continue
set_table = {'Book of Mudora': (0x34E, 1), 'Hammer': (0x34B, 1), 'Bug Catching Net': (0x34D, 1), 'Hookshot': (0x342, 1), 'Magic Mirror': (0x353, 2),
'Cape': (0x352, 1), 'Lamp': (0x34A, 1), 'Moon Pearl': (0x357, 1), 'Cane of Somaria': (0x350, 1), 'Cane of Byrna': (0x351, 1),
'Fire Rod': (0x345, 1), 'Ice Rod': (0x346, 1), 'Bombos': (0x347, 1), 'Ether': (0x348, 1), 'Quake': (0x349, 1)}
or_table = {'Green Pendant': (0x374, 0x04), 'Red Pendant': (0x374, 0x01), 'Blue Pendant': (0x374, 0x02),
'Crystal 1': (0x37A, 0x02), 'Crystal 2': (0x37A, 0x10), 'Crystal 3': (0x37A, 0x40), 'Crystal 4': (0x37A, 0x20),
'Crystal 5': (0x37A, 0x04), 'Crystal 6': (0x37A, 0x01), 'Crystal 7': (0x37A, 0x08),
'Big Key (Eastern Palace)': (0x367, 0x20), 'Compass (Eastern Palace)': (0x365, 0x20), 'Map (Eastern Palace)': (0x369, 0x20),
'Big Key (Desert Palace)': (0x367, 0x10), 'Compass (Desert Palace)': (0x365, 0x10), 'Map (Desert Palace)': (0x369, 0x10),
'Big Key (Tower of Hera)': (0x366, 0x20), 'Compass (Tower of Hera)': (0x364, 0x20), 'Map (Tower of Hera)': (0x368, 0x20),
'Big Key (Escape)': (0x367, 0xC0), 'Compass (Escape)': (0x365, 0xC0), 'Map (Escape)': (0x369, 0xC0),
'Big Key (Palace of Darkness)': (0x367, 0x02), 'Compass (Palace of Darkness)': (0x365, 0x02), 'Map (Palace of Darkness)': (0x369, 0x02),
'Big Key (Thieves Town)': (0x366, 0x10), 'Compass (Thieves Town)': (0x364, 0x10), 'Map (Thieves Town)': (0x368, 0x10),
'Big Key (Skull Woods)': (0x366, 0x80), 'Compass (Skull Woods)': (0x364, 0x80), 'Map (Skull Woods)': (0x368, 0x80),
'Big Key (Swamp Palace)': (0x367, 0x04), 'Compass (Swamp Palace)': (0x365, 0x04), 'Map (Swamp Palace)': (0x369, 0x04),
'Big Key (Ice Palace)': (0x366, 0x40), 'Compass (Ice Palace)': (0x364, 0x40), 'Map (Ice Palace)': (0x368, 0x40),
'Big Key (Misery Mire)': (0x367, 0x01), 'Compass (Misery Mire)': (0x365, 0x01), 'Map (Misery Mire)': (0x369, 0x01),
'Big Key (Turtle Rock)': (0x366, 0x08), 'Compass (Turtle Rock)': (0x364, 0x08), 'Map (Turtle Rock)': (0x368, 0x08),
'Big Key (Ganons Tower)': (0x366, 0x04), 'Compass (Ganons Tower)': (0x364, 0x04), 'Map (Ganons Tower)': (0x368, 0x04)}
set_or_table = {'Flippers': (0x356, 1, 0x379, 0x02),'Pegasus Boots': (0x355, 1, 0x379, 0x04),
'Shovel': (0x34C, 1, 0x38C, 0x04), 'Ocarina': (0x34C, 3, 0x38C, 0x01),
'Mushroom': (0x344, 1, 0x38C, 0x20 | 0x08), 'Magic Powder': (0x344, 2, 0x38C, 0x10),
'Blue Boomerang': (0x341, 1, 0x38C, 0x80), 'Red Boomerang': (0x341, 2, 0x38C, 0x40)}
keys = {'Small Key (Eastern Palace)': [0x37E], 'Small Key (Desert Palace)': [0x37F],
'Small Key (Tower of Hera)': [0x386],
'Small Key (Agahnims Tower)': [0x380], 'Small Key (Palace of Darkness)': [0x382],
'Small Key (Thieves Town)': [0x387],
'Small Key (Skull Woods)': [0x384], 'Small Key (Swamp Palace)': [0x381],
'Small Key (Ice Palace)': [0x385],
'Small Key (Misery Mire)': [0x383], 'Small Key (Turtle Rock)': [0x388],
'Small Key (Ganons Tower)': [0x389],
'Small Key (Universal)': [0x38B], 'Small Key (Escape)': [0x37C, 0x37D]}
bottles = {'Bottle': 2, 'Bottle (Red Potion)': 3, 'Bottle (Green Potion)': 4, 'Bottle (Blue Potion)': 5,
'Bottle (Fairy)': 6, 'Bottle (Bee)': 7, 'Bottle (Good Bee)': 8}
rupees = {'Rupee (1)': 1, 'Rupees (5)': 5, 'Rupees (20)': 20, 'Rupees (50)': 50, 'Rupees (100)': 100, 'Rupees (300)': 300}
bomb_caps = {'Bomb Upgrade (+5)': 5, 'Bomb Upgrade (+10)': 10}
arrow_caps = {'Arrow Upgrade (+5)': 5, 'Arrow Upgrade (+10)': 10}
bombs = {'Single Bomb': 1, 'Bombs (3)': 3, 'Bombs (10)': 10}
arrows = {'Single Arrow': 1, 'Arrows (10)': 10}
if item.name in set_table:
equip[set_table[item.name][0]] = set_table[item.name][1]
elif item.name in or_table:
equip[or_table[item.name][0]] |= or_table[item.name][1]
elif item.name in set_or_table:
equip[set_or_table[item.name][0]] = set_or_table[item.name][1]
equip[set_or_table[item.name][2]] |= set_or_table[item.name][3]
elif item.name in keys:
for address in keys[item.name]:
equip[address] = min(equip[address] + 1, 99)
elif item.name in bottles:
if equip[0x34F] < world.difficulty_requirements[player].progressive_bottle_limit:
equip[0x35C + equip[0x34F]] = bottles[item.name]
equip[0x34F] += 1
elif item.name in rupees:
equip[0x360:0x362] = list(min(equip[0x360] + (equip[0x361] << 8) + rupees[item.name], 9999).to_bytes(2, byteorder='little', signed=False))
equip[0x362:0x364] = list(min(equip[0x362] + (equip[0x363] << 8) + rupees[item.name], 9999).to_bytes(2, byteorder='little', signed=False))
elif item.name in bomb_caps:
starting_max_bombs = min(starting_max_bombs + bomb_caps[item.name], 50)
elif item.name in arrow_caps:
starting_max_arrows = min(starting_max_arrows + arrow_caps[item.name], 70)
elif item.name in bombs:
equip[0x343] += bombs[item.name]
elif item.name in arrows:
if world.retro[player]:
equip[0x38E] |= 0x80
equip[0x377] = 1
else: else:
raise RuntimeError("Unsupported pre-collected item: {}".format(item)) equip[0x377] += arrows[item.name]
elif item.name in ['Piece of Heart', 'Boss Heart Container', 'Sanctuary Heart Container']:
if item.name == 'Piece of Heart':
equip[0x36B] = (equip[0x36B] + 1) % 4
if item.name != 'Piece of Heart' or equip[0x36B] == 0:
equip[0x36C] = min(equip[0x36C] + 0x08, 0xA0)
equip[0x36D] = min(equip[0x36D] + 0x08, 0xA0)
else:
raise RuntimeError(f'Unsupported item in starting equipment: {item.name}')
equip[0x343] = min(equip[0x343], starting_max_bombs)
rom.write_byte(0x180034, starting_max_bombs)
equip[0x377] = min(equip[0x377], starting_max_arrows)
rom.write_byte(0x180035, starting_max_arrows)
rom.write_bytes(0x180046, equip[0x360:0x362])
if equip[0x359]:
rom.write_byte(0x180043, equip[0x359])
assert equip[:0x340] == [0] * 0x340
rom.write_bytes(0x183000, equip[0x340:])
rom.write_bytes(0x271A6, equip[0x340:0x340+60])
rom.write_byte(0x18004A, 0x00 if world.mode[player] != 'inverted' else 0x01) # Inverted mode rom.write_byte(0x18004A, 0x00 if world.mode[player] != 'inverted' else 0x01) # Inverted mode
rom.write_byte(0x18005D, 0x00) # Hammer always breaks barrier rom.write_byte(0x18005D, 0x00) # Hammer always breaks barrier
@@ -965,7 +1124,9 @@ def patch_rom(world, player, rom, enemized):
rom.write_byte(0x18005E, world.crystals_needed_for_gt[player]) rom.write_byte(0x18005E, world.crystals_needed_for_gt[player])
rom.write_byte(0x18005F, world.crystals_needed_for_ganon[player]) rom.write_byte(0x18005F, world.crystals_needed_for_ganon[player])
rom.write_byte(0x18008A, 0x01 if world.mode[player] == "standard" else 0x00) # block HC upstairs doors in rain state in standard mode
# block HC upstairs doors in rain state in standard mode
rom.write_byte(0x18008A, 0x01 if world.mode[player] == "standard" and world.shuffle[player] != 'vanilla' else 0x00)
rom.write_byte(0x18016A, 0x10 | ((0x01 if world.keyshuffle[player] else 0x00) rom.write_byte(0x18016A, 0x10 | ((0x01 if world.keyshuffle[player] else 0x00)
| (0x02 if world.compassshuffle[player] else 0x00) | (0x02 if world.compassshuffle[player] else 0x00)
@@ -1114,16 +1275,14 @@ try:
except ImportError: except ImportError:
RaceRom = None RaceRom = None
def get_race_rom_patches(rom): def patch_race_rom(rom):
patches = {str(0x180213): [0x01, 0x00]} # Tournament Seed rom.write_bytes(0x180213, [0x01, 0x00]) # Tournament Seed
if 'RaceRom' in sys.modules: if 'RaceRom' in sys.modules:
RaceRom.encrypt(rom, patches) RaceRom.encrypt(rom)
return patches
def write_custom_shops(rom, world, player): def write_custom_shops(rom, world, player):
shops = [shop for shop in world.shops if shop.replaceable and shop.active and shop.region.player == player] shops = [shop for shop in world.shops if shop.custom and shop.region.player == player]
shop_data = bytearray() shop_data = bytearray()
items_data = bytearray() items_data = bytearray()
@@ -1171,7 +1330,9 @@ def hud_format_text(text):
return output[:32] return output[:32]
def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite, names = None): def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite, ow_palettes, uw_palettes, names = None):
if sprite and not isinstance(sprite, Sprite):
sprite = Sprite(sprite) if os.path.isfile(sprite) else get_sprite_from_name(sprite)
# enable instant item menu # enable instant item menu
if fastmenu == 'instant': if fastmenu == 'instant':
@@ -1197,119 +1358,13 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr
rom.write_byte(0x18004B, 0x01 if quickswap else 0x00) rom.write_byte(0x18004B, 0x01 if quickswap else 0x00)
music_volumes = [ rom.write_byte(0x0CFE18, 0x00 if disable_music else rom.orig_buffer[0x0CFE18] if rom.orig_buffer else 0x70)
(0x00, [0xD373B, 0xD375B, 0xD90F8]), rom.write_byte(0x0CFEC1, 0x00 if disable_music else rom.orig_buffer[0x0CFEC1] if rom.orig_buffer else 0xC0)
(0x14, [0xDA710, 0xDA7A4, 0xDA7BB, 0xDA7D2]), rom.write_bytes(0x0D0000, [0x00, 0x00] if disable_music else rom.orig_buffer[0x0D0000:0x0D0002] if rom.orig_buffer else [0xDA, 0x58])
(0x3C, [0xD5954, 0xD653B, 0xDA736, 0xDA752, 0xDA772, 0xDA792]), rom.write_bytes(0x0D00E7, [0xC4, 0x58] if disable_music else rom.orig_buffer[0x0D00E7:0x0D00E9] if rom.orig_buffer else [0xDA, 0x58])
(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, 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, 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)
rom.write_byte(0x18021A, 1 if disable_music else 0x00) rom.write_byte(0x18021A, 1 if disable_music else 0x00)
# restore Mirror sound effect volumes (for existing seeds that lack it)
rom.write_byte(0xD3E04, 0xC8)
rom.write_byte(0xD3DC6, 0xC8)
rom.write_byte(0xD3D6E, 0xC8)
rom.write_byte(0xD3D34, 0xC8)
rom.write_byte(0xD3D55, 0xC8)
rom.write_byte(0xD3E38, 0xC8)
rom.write_byte(0xD3DAA, 0xFA)
# set heart beep rate # set heart beep rate
rom.write_byte(0x180033, {'off': 0x00, 'half': 0x40, 'quarter': 0x80, 'normal': 0x20, 'double': 0x10}[beep]) rom.write_byte(0x180033, {'off': 0x00, 'half': 0x40, 'quarter': 0x80, 'normal': 0x20, 'double': 0x10}[beep])
@@ -1332,6 +1387,18 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr
if sprite is not None: if sprite is not None:
write_sprite(rom, sprite) write_sprite(rom, sprite)
default_ow_palettes(rom)
if ow_palettes == 'random':
randomize_ow_palettes(rom)
elif ow_palettes == 'blackout':
blackout_ow_palettes(rom)
default_uw_palettes(rom)
if uw_palettes == 'random':
randomize_uw_palettes(rom)
elif uw_palettes == 'blackout':
blackout_uw_palettes(rom)
# set player names # set player names
for player, name in names.items(): for player, name in names.items():
if 0 < player <= 64: if 0 < player <= 64:
@@ -1348,6 +1415,130 @@ def write_sprite(rom, sprite):
rom.write_bytes(0xDD308, sprite.palette) rom.write_bytes(0xDD308, sprite.palette)
rom.write_bytes(0xDEDF5, sprite.glove_palette) rom.write_bytes(0xDEDF5, sprite.glove_palette)
def set_color(rom, address, color, shade):
r = round(min(color[0], 0xFF) * pow(0.8, shade) * 0x1F / 0xFF)
g = round(min(color[1], 0xFF) * pow(0.8, shade) * 0x1F / 0xFF)
b = round(min(color[2], 0xFF) * pow(0.8, shade) * 0x1F / 0xFF)
rom.write_bytes(address, ((b << 10) | (g << 5) | (r << 0)).to_bytes(2, byteorder='little', signed=False))
def default_ow_palettes(rom):
if not rom.orig_buffer:
return
rom.write_bytes(0xDE604, rom.orig_buffer[0xDE604:0xDEBB4])
for address in [0x067FB4, 0x067F94, 0x067FC6, 0x067FE6, 0x067FE1, 0x05FEA9, 0x05FEB3]:
rom.write_bytes(address, rom.orig_buffer[address:address+2])
def randomize_ow_palettes(rom):
grass, grass2, grass3, dirt, dirt2, water, clouds, dwdirt,\
dwgrass, dwwater, dwdmdirt, dwdmgrass, dwdmclouds1, dwdmclouds2 = [[random.randint(60, 215) for _ in range(3)] for _ in range(14)]
dwtree = [c + random.randint(-20, 10) for c in dwgrass]
treeleaf = [c + random.randint(-20, 10) for c in grass]
patches = {0x067FB4: (grass, 0), 0x067F94: (grass, 0), 0x067FC6: (grass, 0), 0x067FE6: (grass, 0), 0x067FE1: (grass, 3), 0x05FEA9: (grass, 0), 0x05FEB3: (dwgrass, 1),
0x0DD4AC: (grass, 2), 0x0DE6DE: (grass2, 2), 0x0DE6E0: (grass2, 1), 0x0DD4AE: (grass2, 1), 0x0DE9FA: (grass2, 1), 0x0DEA0E: (grass2, 1), 0x0DE9FE: (grass2, 0),
0x0DD3D2: (grass2, 2), 0x0DE88C: (grass2, 2), 0x0DE8A8: (grass2, 2), 0x0DE9F8: (grass2, 2), 0x0DEA4E: (grass2, 2), 0x0DEAF6: (grass2, 2), 0x0DEB2E: (grass2, 2), 0x0DEB4A: (grass2, 2),
0x0DE892: (grass, 1), 0x0DE886: (grass, 0), 0x0DE6D2: (grass, 0), 0x0DE6FA: (grass, 3), 0x0DE6FC: (grass, 0), 0x0DE6FE: (grass, 0), 0x0DE70A: (grass, 0), 0x0DE708: (grass, 2), 0x0DE70C: (grass, 1),
0x0DE6D4: (dirt, 2), 0x0DE6CA: (dirt, 5), 0x0DE6CC: (dirt, 4), 0x0DE6CE: (dirt, 3), 0x0DE6E2: (dirt, 2), 0x0DE6D8: (dirt, 5), 0x0DE6DA: (dirt, 4), 0x0DE6DC: (dirt, 2),
0x0DE6F0: (dirt, 2), 0x0DE6E6: (dirt, 5), 0x0DE6E8: (dirt, 4), 0x0DE6EA: (dirt, 2), 0x0DE6EC: (dirt, 4), 0x0DE6EE: (dirt, 2),
0x0DE91E: (grass, 0),
0x0DE920: (dirt, 2), 0x0DE916: (dirt, 3), 0x0DE934: (dirt, 3),
0x0DE92C: (grass, 0), 0x0DE93A: (grass, 0), 0x0DE91C: (grass, 1), 0x0DE92A: (grass, 1), 0x0DEA1C: (grass, 0), 0x0DEA2A: (grass, 0), 0x0DEA30: (grass, 0),
0x0DEA2E: (dirt, 5),
0x0DE884: (grass, 3), 0x0DE8AE: (grass, 3), 0x0DE8BE: (grass, 3), 0x0DE8E4: (grass, 3), 0x0DE938: (grass, 3), 0x0DE9C4: (grass, 3), 0x0DE6D0: (grass, 4),
0x0DE890: (treeleaf, 1), 0x0DE894: (treeleaf, 0),
0x0DE924: (water, 3), 0x0DE668: (water, 3), 0x0DE66A: (water, 2), 0x0DE670: (water, 1), 0x0DE918: (water, 1), 0x0DE66C: (water, 0), 0x0DE91A: (water, 0), 0x0DE92E: (water, 1), 0x0DEA1A: (water, 1), 0x0DEA16: (water, 3), 0x0DEA10: (water, 4),
0x0DE66E: (dirt, 3), 0x0DE672: (dirt, 2), 0x0DE932: (dirt, 4), 0x0DE936: (dirt, 2), 0x0DE93C: (dirt, 1),
0x0DE756: (dirt2, 4), 0x0DE764: (dirt2, 4), 0x0DE772: (dirt2, 4), 0x0DE994: (dirt2, 4), 0x0DE9A2: (dirt2, 4), 0x0DE758: (dirt2, 3), 0x0DE766: (dirt2, 3), 0x0DE774: (dirt2, 3),
0x0DE996: (dirt2, 3), 0x0DE9A4: (dirt2, 3), 0x0DE75A: (dirt2, 2), 0x0DE768: (dirt2, 2), 0x0DE776: (dirt2, 2), 0x0DE778: (dirt2, 2), 0x0DE998: (dirt2, 2), 0x0DE9A6: (dirt2, 2),
0x0DE9AC: (dirt2, 1), 0x0DE99E: (dirt2, 1), 0x0DE760: (dirt2, 1), 0x0DE77A: (dirt2, 1), 0x0DE77C: (dirt2, 1), 0x0DE798: (dirt2, 1), 0x0DE980: (dirt2, 1),
0x0DE75C: (grass3, 2), 0x0DE786: (grass3, 2), 0x0DE794: (grass3, 2), 0x0DE99A: (grass3, 2), 0x0DE75E: (grass3, 1), 0x0DE788: (grass3, 1), 0x0DE796: (grass3, 1), 0x0DE99C: (grass3, 1),
0x0DE76A: (clouds, 2), 0x0DE9A8: (clouds, 2), 0x0DE76E: (clouds, 0), 0x0DE9AA: (clouds, 0), 0x0DE8DA: (clouds, 0), 0x0DE8D8: (clouds, 0), 0x0DE8D0: (clouds, 0), 0x0DE98C: (clouds, 2), 0x0DE990: (clouds, 0),
0x0DEB34: (dwtree, 4), 0x0DEB30: (dwtree, 3), 0x0DEB32: (dwtree, 1),
0x0DE710: (dwdirt, 5), 0x0DE71E: (dwdirt, 5), 0x0DE72C: (dwdirt, 5), 0x0DEAD6: (dwdirt, 5), 0x0DE712: (dwdirt, 4), 0x0DE720: (dwdirt, 4), 0x0DE72E: (dwdirt, 4), 0x0DE660: (dwdirt, 4),
0x0DEAD8: (dwdirt, 4), 0x0DEADA: (dwdirt, 3), 0x0DE714: (dwdirt, 3), 0x0DE722: (dwdirt, 3), 0x0DE730: (dwdirt, 3), 0x0DE732: (dwdirt, 3), 0x0DE734: (dwdirt, 2), 0x0DE736: (dwdirt, 2),
0x0DE728: (dwdirt, 2), 0x0DE71A: (dwdirt, 2), 0x0DE664: (dwdirt, 2), 0x0DEAE0: (dwdirt, 2),
0x0DE716: (dwgrass, 3), 0x0DE740: (dwgrass, 3), 0x0DE74E: (dwgrass, 3), 0x0DEAC0: (dwgrass, 3), 0x0DEACE: (dwgrass, 3), 0x0DEADC: (dwgrass, 3), 0x0DEB24: (dwgrass, 3), 0x0DE752: (dwgrass, 2),
0x0DE718: (dwgrass, 1), 0x0DE742: (dwgrass, 1), 0x0DE750: (dwgrass, 1), 0x0DEB26: (dwgrass, 1), 0x0DEAC2: (dwgrass, 1), 0x0DEAD0: (dwgrass, 1), 0x0DEADE: (dwgrass, 1),
0x0DE65A: (dwwater, 5), 0x0DE65C: (dwwater, 3), 0x0DEAC8: (dwwater, 3), 0x0DEAD2: (dwwater, 2), 0x0DEABC: (dwwater, 2), 0x0DE662: (dwwater, 2), 0x0DE65E: (dwwater, 1), 0x0DEABE: (dwwater, 1), 0x0DEA98: (dwwater, 2),
0x0DE79A: (dwdmdirt, 6), 0x0DE7A8: (dwdmdirt, 6), 0x0DE7B6: (dwdmdirt, 6), 0x0DEB60: (dwdmdirt, 6), 0x0DEB6E: (dwdmdirt, 6), 0x0DE93E: (dwdmdirt, 6), 0x0DE94C: (dwdmdirt, 6), 0x0DEBA6: (dwdmdirt, 6),
0x0DE79C: (dwdmdirt, 4), 0x0DE7AA: (dwdmdirt, 4), 0x0DE7B8: (dwdmdirt, 4), 0x0DEB70: (dwdmdirt, 4), 0x0DEBA8: (dwdmdirt, 4), 0x0DEB72: (dwdmdirt, 3), 0x0DEB74: (dwdmdirt, 3), 0x0DE79E: (dwdmdirt, 3), 0x0DE7AC: (dwdmdirt, 3), 0x0DEBAA: (dwdmdirt, 3), 0x0DE7A0: (dwdmdirt, 3),
0x0DE7BC: (dwdmgrass, 3),
0x0DEBAC: (dwdmdirt, 2), 0x0DE7AE: (dwdmdirt, 2), 0x0DE7C2: (dwdmdirt, 2), 0x0DE7A6: (dwdmdirt, 2), 0x0DEB7A: (dwdmdirt, 2), 0x0DEB6C: (dwdmdirt, 2), 0x0DE7C0: (dwdmdirt, 2),
0x0DE7A2: (dwdmgrass, 3), 0x0DE7BE: (dwdmgrass, 3), 0x0DE7CC: (dwdmgrass, 3), 0x0DE7DA: (dwdmgrass, 3), 0x0DEB6A: (dwdmgrass, 3), 0x0DE948: (dwdmgrass, 3), 0x0DE956: (dwdmgrass, 3), 0x0DE964: (dwdmgrass, 3), 0x0DE7CE: (dwdmgrass, 1), 0x0DE7A4: (dwdmgrass, 1), 0x0DEBA2: (dwdmgrass, 1), 0x0DEBB0: (dwdmgrass, 1),
0x0DE644: (dwdmclouds1, 2), 0x0DEB84: (dwdmclouds1, 2), 0x0DE648: (dwdmclouds1, 1), 0x0DEB88: (dwdmclouds1, 1),
0x0DEBAE: (dwdmclouds2, 2), 0x0DE7B0: (dwdmclouds2, 2), 0x0DE7B4: (dwdmclouds2, 0), 0x0DEB78: (dwdmclouds2, 0), 0x0DEBB2: (dwdmclouds2, 0)
}
for address, (color, shade) in patches.items():
set_color(rom, address, color, shade)
def blackout_ow_palettes(rom):
rom.write_bytes(0xDE604, [0] * 0xC4)
for i in range(0xDE6C8, 0xDE86C, 70):
rom.write_bytes(i, [0] * 64)
rom.write_bytes(i+66, [0] * 4)
rom.write_bytes(0xDE86C, [0] * 0x348)
for address in [0x067FB4, 0x067F94, 0x067FC6, 0x067FE6, 0x067FE1, 0x05FEA9, 0x05FEB3]:
rom.write_bytes(address, [0,0])
def default_uw_palettes(rom):
if not rom.orig_buffer:
return
rom.write_bytes(0xDD734, rom.orig_buffer[0xDD734:0xDE544])
def randomize_uw_palettes(rom):
for dungeon in range(20):
wall, pot, chest, floor1, floor2, floor3 = [[random.randint(60, 240) for _ in range(3)] for _ in range(6)]
for i in range(5):
shade = 10 - (i * 2)
set_color(rom, 0x0DD734 + (0xB4 * dungeon) + (i * 2), wall, shade)
set_color(rom, 0x0DD770 + (0xB4 * dungeon) + (i * 2), wall, shade)
set_color(rom, 0x0DD744 + (0xB4 * dungeon) + (i * 2), wall, shade)
if dungeon == 0:
set_color(rom, 0x0DD7CA + (0xB4 * dungeon) + (i * 2), wall, shade)
if dungeon == 2:
set_color(rom, 0x0DD74E + (0xB4 * dungeon), wall, 3)
set_color(rom, 0x0DD750 + (0xB4 * dungeon), wall, 5)
set_color(rom, 0x0DD73E + (0xB4 * dungeon), wall, 3)
set_color(rom, 0x0DD740 + (0xB4 * dungeon), wall, 5)
set_color(rom, 0x0DD7E4 + (0xB4 * dungeon), wall, 4)
set_color(rom, 0x0DD7E6 + (0xB4 * dungeon), wall, 2)
set_color(rom, 0xDD7DA + (0xB4 * dungeon), wall, 10)
set_color(rom, 0xDD7DC + (0xB4 * dungeon), wall, 8)
set_color(rom, 0x0DD75A + (0xB4 * dungeon), pot, 7)
set_color(rom, 0x0DD75C + (0xB4 * dungeon), pot, 1)
set_color(rom, 0x0DD75E + (0xB4 * dungeon), pot, 3)
set_color(rom, 0x0DD76A + (0xB4 * dungeon), wall, 7)
set_color(rom, 0x0DD76C + (0xB4 * dungeon), wall, 2)
set_color(rom, 0x0DD76E + (0xB4 * dungeon), wall, 4)
set_color(rom, 0x0DD7AE + (0xB4 * dungeon), chest, 2)
set_color(rom, 0x0DD7B0 + (0xB4 * dungeon), chest, 0)
for i in range(3):
shade = 6 - (i * 2)
set_color(rom, 0x0DD764 + (0xB4 * dungeon) + (i * 2), floor1, shade)
set_color(rom, 0x0DD782 + (0xB4 * dungeon) + (i * 2), floor1, shade + 3)
set_color(rom, 0x0DD7A0 + (0xB4 * dungeon) + (i * 2), floor2, shade)
set_color(rom, 0x0DD7BE + (0xB4 * dungeon) + (i * 2), floor2, shade + 3)
set_color(rom, 0x0DD7E2 + (0xB4 * dungeon), floor3, 3)
set_color(rom, 0x0DD796 + (0xB4 * dungeon), floor3, 4)
def blackout_uw_palettes(rom):
for i in range(0xDD734, 0xDE544, 180):
rom.write_bytes(i, [0] * 38)
rom.write_bytes(i+44, [0] * 76)
rom.write_bytes(i+136, [0] * 44)
def write_string_to_rom(rom, target, string): def write_string_to_rom(rom, target, string):
address, maxbytes = text_addresses[target] address, maxbytes = text_addresses[target]