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/
.mypy_cache/
RaceRom.py
weights/
venv
test

View File

@@ -6,6 +6,7 @@ import textwrap
import sys
from AdjusterMain import adjust
from Rom import get_sprite_from_name
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
@@ -15,7 +16,8 @@ class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
def main():
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument('--rom', default='ER_base.sfc', help='Path to an ALttP JAP(1.0) rom to use as a base.')
parser.add_argument('--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('--fastmenu', default='normal', const='normal', nargs='?', choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
help='''\
@@ -31,6 +33,8 @@ def main():
''')
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)')
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,
@@ -43,10 +47,10 @@ def main():
# ToDo: Validate files further than mere existance
if not os.path.isfile(args.rom):
input('Could not find valid base rom for patching at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.rom)
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)
if args.sprite is not None and not os.path.isfile(args.sprite):
input('Could not find link sprite sheet at given location. \nPress Enter to exit.' % args.sprite)
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.')
sys.exit(1)
# set up logger

View File

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

View File

@@ -11,7 +11,7 @@ from RoomData import Room
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.shuffle = shuffle.copy()
self.doorShuffle = doorShuffle
@@ -50,9 +50,6 @@ class World(object):
self.fix_trock_exit = {}
self.shuffle_ganon = 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.custom = custom
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]
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.state.collect(item, True)
@@ -1275,15 +1274,15 @@ class ShopType(Enum):
UpgradeShop = 2
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.room_id = room_id
self.default_door_id = default_door_id
self.type = type
self.inventory = [None, None, None]
self.shopkeeper_config = shopkeeper_config
self.replaceable = replaceable
self.active = False
self.custom = custom
self.locked = locked
@property
def item_count(self):
@@ -1343,6 +1342,7 @@ class Spoiler(object):
self.medallions = {}
self.playthrough = {}
self.unreachables = []
self.startinventory = []
self.locations = {}
self.paths = {}
self.metadata = {}
@@ -1377,6 +1377,8 @@ class Spoiler(object):
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.startinventory = list(map(str, self.world.precollected_items))
self.locations = OrderedDict()
listed_locations = set()
@@ -1404,7 +1406,7 @@ class Spoiler(object):
self.shops = []
for shop in self.world.shops:
if not shop.active:
if not shop.custom:
continue
shopdata = {'location': str(shop.region),
'type': 'Take Any' if shop.type == ShopType.TakeAny else 'Shop'
@@ -1471,6 +1473,7 @@ class Spoiler(object):
out['Doors'] = list(self.doors.values())
out['DoorTypes'] = list(self.doorTypes.values())
out.update(self.locations)
out['Starting Inventory'] = self.startinventory
out['Special'] = self.medallions
if 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 damage: %s\n' % self.metadata['enemy_damage'])
outfile.write('Hints: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['hints'].items()})
outfile.write('L\\R Quickswap enabled: %s\n' % ('Yes' if self.world.quickswap else 'No'))
outfile.write('Menu speed: %s' % self.world.fastmenu)
if self.doors:
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()]))
@@ -1527,12 +1528,14 @@ class Spoiler(object):
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('\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'.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'.join("{} [{}]\n {}".format(shop['location'], shop['type'], "\n ".join(item for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if item)) for shop in self.shops))
outfile.write('\n\nPlaythrough:\n\n')
outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join([' %s: %s' % (location, item) for (location, item) in sphere.items()])) for (sphere_nr, sphere) in self.playthrough.items()]))
outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join([' %s: %s' % (location, item) for (location, item) in sphere.items()] if sphere_nr != '0' else [f' {item}' for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()]))
if self.unreachables:
outfile.write('\n\nUnreachable Items:\n\n')
outfile.write('\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables]))

View File

@@ -9,6 +9,7 @@ import shlex
import sys
from Main import main
from Rom import get_sprite_from_name
from Utils import is_bundled, close_console
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('--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('--keysanity', default=defval(False), help=argparse.SUPPRESS, action='store_true')
parser.add_argument('--retro', default=defval(False), help='''\
Keys are universal, shooting arrows costs rupees,
and a few other little things make this more like Zelda-1.
''', 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('--customitemarray', default=defval(False), help='Not supported.')
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'],
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='''\
Path to a sprite sheet to use for Link. Needs to be in
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.
''')
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('--shuffleenemies', default=defval('none'), choices=['none', 'shuffled', 'chaos'])
parser.add_argument('--enemy_health', default=defval('default'), choices=['default', 'easy', 'normal', 'hard', 'expert'])
parser.add_argument('--enemy_damage', default=defval('default'), choices=['default', 'shuffled', 'chaos'])
parser.add_argument('--shufflepalette', default=defval(False), action='store_true')
parser.add_argument('--shufflepots', default=defval(False), action='store_true')
parser.add_argument('--beemizer', default=defval(0), type=lambda value: min(max(int(value), 0), 4))
parser.add_argument('--multi', default=defval(1), type=lambda value: min(max(int(value), 1), 255))
@@ -285,6 +289,8 @@ def parse_arguments(argv, no_defaults=False):
parser.add_argument(f'--p{player}', default=defval(''), help=argparse.SUPPRESS)
ret = parser.parse_args(argv)
if ret.keysanity:
ret.mapshuffle, ret.compassshuffle, ret.keyshuffle, ret.bigkeyshuffle = [True] * 4
if multiargs.multi:
defaults = copy.deepcopy(ret)
@@ -293,9 +299,10 @@ def parse_arguments(argv, no_defaults=False):
for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality',
'shuffle', 'crystals_ganon', 'crystals_gt', 'openpyramid',
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle',
'retro', 'accessibility', 'hints', 'shufflepalette', 'shufflepots', 'beemizer',
'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage']:
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
'retro', 'accessibility', 'hints', 'beemizer',
'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots',
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep']:
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
if player == 1:
setattr(ret, name, {1: value})
@@ -321,9 +328,9 @@ def start():
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)
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:
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)
else:
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)
suppressRomVar = IntVar()
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()
openpyramidCheckbutton = Checkbutton(checkBoxFrame, text="Pre-open Pyramid Hole", variable=openpyramidVar)
mcsbshuffleFrame = Frame(checkBoxFrame)
@@ -76,8 +74,6 @@ def guiMain(args=None):
bigkeyshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="BigKeys", variable=bigkeyshuffleVar)
retroVar = IntVar()
retroCheckbutton = Checkbutton(checkBoxFrame, text="Retro mode (universal keys)", variable=retroVar)
disableMusicVar = IntVar()
disableMusicCheckbutton = Checkbutton(checkBoxFrame, text="Disable game music", variable=disableMusicVar)
shuffleGanonVar = IntVar()
shuffleGanonVar.set(1) #set default
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)
suppressRomCheckbutton.pack(expand=True, anchor=W)
quickSwapCheckbutton.pack(expand=True, anchor=W)
openpyramidCheckbutton.pack(expand=True, anchor=W)
mcsbshuffleFrame.pack(expand=True, anchor=W)
mcsbLabel.grid(row=0, column=0)
@@ -98,57 +93,23 @@ def guiMain(args=None):
keyshuffleCheckbutton.grid(row=0, column=3)
bigkeyshuffleCheckbutton.grid(row=0, column=4)
retroCheckbutton.pack(expand=True, anchor=W)
disableMusicCheckbutton.pack(expand=True, anchor=W)
shuffleGanonCheckbutton.pack(expand=True, anchor=W)
hintsCheckbutton.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)
heartbeepVar = StringVar()
heartbeepVar.set('normal')
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))
disableMusicVar = IntVar()
disableMusicCheckbutton = Checkbutton(romOptionsFrame, text="Disable music", variable=disableMusicVar)
disableMusicCheckbutton.grid(row=0, column=0, sticky=E)
heartcolorFrame = Frame(fileDialogFrame)
heartcolorVar = StringVar()
heartcolorVar.set('red')
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:')
spriteDialogFrame = Frame(romOptionsFrame)
spriteDialogFrame.grid(row=0, column=1)
baseSpriteLabel = Label(spriteDialogFrame, text='Sprite:')
spriteNameVar = StringVar()
sprite = None
@@ -168,17 +129,79 @@ def guiMain(args=None):
def SpriteSelect():
SpriteSelector(mainWindow, set_sprite)
spriteSelectButton = Button(spriteDialogFrame, text='Open Sprite Picker', command=SpriteSelect)
spriteSelectButton = Button(spriteDialogFrame, text='...', command=SpriteSelect)
baseSpriteLabel.pack(side=LEFT)
spriteEntry.pack(side=LEFT)
spriteSelectButton.pack(side=LEFT)
romDialogFrame.pack()
spriteDialogFrame.pack()
quickSwapVar = IntVar()
quickSwapCheckbutton = Checkbutton(romOptionsFrame, text="L/R Quickswapping", variable=quickSwapVar)
quickSwapCheckbutton.grid(row=1, column=0, sticky=E)
checkBoxFrame.pack()
fileDialogFrame.pack()
fastMenuFrame = Frame(romOptionsFrame)
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)
@@ -336,18 +359,19 @@ def guiMain(args=None):
accessibilityFrame.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(1, weight=1)
enemizerFrame.columnconfigure(2, weight=1)
enemizerFrame.columnconfigure(3, weight=1)
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.pack(side=LEFT)
enemizerCLIpathVar = StringVar()
enemizerCLIpathEntry = Entry(enemizerPathFrame, textvariable=enemizerCLIpathVar, width=80)
enemizerCLIpathEntry.pack(side=LEFT)
enemizerCLIpathVar = StringVar(value="EnemizerCLI/EnemizerCLI.Core")
enemizerCLIpathEntry = Entry(enemizerPathFrame, textvariable=enemizerCLIpathVar)
enemizerCLIpathEntry.pack(side=LEFT, expand=True, fill=X)
def EnemizerSelectPath():
path = filedialog.askopenfilename(filetypes=[("EnemizerCLI executable", "*EnemizerCLI*")])
if path:
@@ -355,18 +379,21 @@ def guiMain(args=None):
enemizerCLIbrowseButton = Button(enemizerPathFrame, text='...', command=EnemizerSelectPath)
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()
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.grid(row=2, column=0)
enemizerBossFrame.grid(row=1, column=1)
enemizerBossLabel = Label(enemizerBossFrame, text='Boss shuffle')
enemizerBossLabel.pack(side=LEFT)
enemizerBossVar = StringVar()
@@ -375,7 +402,7 @@ def guiMain(args=None):
enemizerBossOption.pack(side=LEFT)
enemizerDamageFrame = Frame(enemizerFrame)
enemizerDamageFrame.grid(row=2, column=1)
enemizerDamageFrame.grid(row=1, column=2)
enemizerDamageLabel = Label(enemizerDamageFrame, text='Enemy damage')
enemizerDamageLabel.pack(side=LEFT)
enemizerDamageVar = StringVar()
@@ -384,7 +411,7 @@ def guiMain(args=None):
enemizerDamageOption.pack(side=LEFT)
enemizerHealthFrame = Frame(enemizerFrame)
enemizerHealthFrame.grid(row=2, column=2)
enemizerHealthFrame.grid(row=1, column=3)
enemizerHealthLabel = Label(enemizerHealthFrame, text='Enemy health')
enemizerHealthLabel.pack(side=LEFT)
enemizerHealthVar = StringVar()
@@ -441,14 +468,15 @@ def guiMain(args=None):
guiargs.retro = bool(retroVar.get())
guiargs.quickswap = bool(quickSwapVar.get())
guiargs.disablemusic = bool(disableMusicVar.get())
guiargs.ow_palettes = owPalettesVar.get()
guiargs.uw_palettes = uwPalettesVar.get()
guiargs.shuffleganon = bool(shuffleGanonVar.get())
guiargs.hints = bool(hintsVar.get())
guiargs.enemizercli = enemizerCLIpathVar.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_damage = enemizerDamageVar.get()
guiargs.shufflepalette = bool(paletteShuffleVar.get())
guiargs.shufflepots = bool(potShuffleVar.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()),
@@ -572,6 +600,18 @@ def guiMain(args=None):
fastMenuLabel2 = Label(fastMenuFrame2, text='Menu speed')
fastMenuLabel2.pack(side=LEFT)
owPalettesFrame2 = Frame(drowDownFrame2)
owPalettesOptionMenu2 = OptionMenu(owPalettesFrame2, owPalettesVar, 'default', 'random', 'blackout')
owPalettesOptionMenu2.pack(side=RIGHT)
owPalettesLabel2 = Label(owPalettesFrame2, text='Overworld palettes')
owPalettesLabel2.pack(side=LEFT)
uwPalettesFrame2 = Frame(drowDownFrame2)
uwPalettesOptionMenu2 = OptionMenu(uwPalettesFrame2, uwPalettesVar, 'default', 'random', 'blackout')
uwPalettesOptionMenu2.pack(side=RIGHT)
uwPalettesLabel2 = Label(uwPalettesFrame2, text='Dungeon palettes')
uwPalettesLabel2.pack(side=LEFT)
namesFrame2 = Frame(drowDownFrame2)
namesLabel2 = Label(namesFrame2, text='Player names')
namesVar2 = StringVar()
@@ -583,6 +623,8 @@ def guiMain(args=None):
heartbeepFrame2.pack(expand=True, anchor=E)
heartcolorFrame2.pack(expand=True, anchor=E)
fastMenuFrame2.pack(expand=True, anchor=E)
owPalettesFrame2.pack(expand=True, anchor=E)
uwPalettesFrame2.pack(expand=True, anchor=E)
namesFrame2.pack(expand=True, anchor=E)
bottomFrame2 = Frame(topFrame2)
@@ -592,9 +634,12 @@ def guiMain(args=None):
guiargs.heartbeep = heartbeepVar.get()
guiargs.heartcolor = heartcolorVar.get()
guiargs.fastmenu = fastMenuVar.get()
guiargs.ow_palettes = owPalettesVar.get()
guiargs.uw_palettes = uwPalettesVar.get()
guiargs.quickswap = bool(quickSwapVar.get())
guiargs.disablemusic = bool(disableMusicVar.get())
guiargs.rom = romVar2.get()
guiargs.baserom = romVar.get()
guiargs.sprite = sprite
guiargs.names = namesEntry2.get()
try:

View File

@@ -1,5 +1,6 @@
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):
@@ -318,31 +319,6 @@ def create_inverted_regions(world, player):
shop.add_inventory(1, 'Arrow Upgrade (+5)', 100, 7)
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):
# 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,
basicarmor = ['Blue Mail', 'Red Mail'],
swordless = ['Rupees (20)'] * 4,
progressivesword = ['Progressive Sword'] * 3,
basicsword = ['Master Sword', 'Tempered Sword', 'Golden Sword'],
progressivesword = ['Progressive Sword'] * 4,
basicsword = ['Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'],
basicbow = ['Bow', 'Silver Arrows'],
timedohko = ['Green Clock'] * 25,
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
@@ -78,8 +78,8 @@ difficulties = {
progressivearmor = ['Progressive Armor'] * 2,
basicarmor = ['Progressive Armor'] * 2, # neither will count
swordless = ['Rupees (20)'] * 4,
progressivesword = ['Progressive Sword'] * 3,
basicsword = ['Master Sword', 'Master Sword', 'Tempered Sword'],
progressivesword = ['Progressive Sword'] * 4,
basicsword = ['Fighter Sword', 'Master Sword', 'Master Sword', 'Tempered Sword'],
basicbow = ['Bow'] * 2,
timedohko = ['Green Clock'] * 25,
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
@@ -105,8 +105,8 @@ difficulties = {
progressivearmor = ['Progressive Armor'] * 2, # neither will count
basicarmor = ['Progressive Armor'] * 2, # neither will count
swordless = ['Rupees (20)'] * 4,
progressivesword = ['Progressive Sword'] * 3,
basicsword = ['Fighter Sword', 'Master Sword', 'Master Sword'],
progressivesword = ['Progressive Sword'] * 4,
basicsword = ['Fighter Sword', 'Fighter Sword', 'Master Sword', 'Master Sword'],
basicbow = ['Bow'] * 2,
timedohko = ['Green Clock'] * 20 + ['Red Clock'] * 5,
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]
connect_entrance(world, entrance, old_man_take_any, player)
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)
old_man_take_any.shop.active = True
swords = [item for item in world.itempool if item.type == 'Sword' and item.player == player]
if swords:
@@ -331,9 +330,8 @@ def set_up_take_anys(world, player):
entrance = world.get_region(reg, player).entrances[0]
connect_entrance(world, entrance, take_any, player)
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)
take_any.shop.active = True
take_any.shop.add_inventory(0, 'Blue Potion', 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):
# Changes to basic 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]:
rss = world.get_region('Red Shield Shop', player).shop
rss.active = True
rss.add_inventory(2, 'Single Arrow', 80)
# Randomized changes to Shops
if world.retro[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
if not rss.locked:
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):
shop.locked = True
shop.add_inventory(0, 'Single Arrow', 80)
shop.add_inventory(1, 'Small Key (Universal)', 100)
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):
pool = []
@@ -465,34 +456,17 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, r
else:
pool.extend(diff.basicarmor)
if swords != 'swordless':
if want_progressives():
pool.extend(['Progressive Bow'] * 2)
else:
pool.extend(diff.basicbow)
if want_progressives():
pool.extend(['Progressive Bow'] * 2)
elif swords != 'swordless':
pool.extend(diff.basicbow)
else:
pool.extend(['Bow', 'Silver Arrows'])
if swords == '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':
swords_to_use = []
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'])
swords_to_use = diff.progressivesword.copy() if want_progressives() else diff.basicsword.copy()
random.shuffle(swords_to_use)
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:
place_item('Master Sword Pedestal', 'Triforce')
else:
if want_progressives():
pool.extend(diff.progressivesword)
pool.extend(['Progressive Sword'])
else:
pool.extend(diff.basicsword)
pool.extend(['Fighter Sword'])
pool.extend(diff.progressivesword if want_progressives() else diff.basicsword)
if swords == 'assured':
if want_progressives():
precollected_items.append('Progressive Sword')
pool.remove('Progressive Sword')
else:
precollected_items.append('Fighter Sword')
pool.remove('Fighter Sword')
pool.extend(['Rupees (50)'])
extraitems = total_items_to_place - len(pool) - len(placed_items)

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

118
Main.py
View File

@@ -4,15 +4,16 @@ from itertools import zip_longest
import json
import logging
import os
import pickle
import random
import time
import zlib
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 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 DoorShuffle import link_doors
from RoomData import create_rooms
@@ -26,16 +27,13 @@ __version__ = '0.0.1-pre'
def main(args, seed=None):
if args.outputpath:
try:
os.mkdir(args.outputpath)
except OSError:
pass
os.makedirs(args.outputpath, exist_ok=True)
output_path.cached_path = args.outputpath
start = time.perf_counter()
# 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('')
if seed is None:
random.seed(None)
@@ -67,16 +65,19 @@ def main(args, seed=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
for tok in filter(None, args.startinventory[player].split(',')):
item = ItemFactory(tok.strip(), player)
if item:
world.push_precollected(item)
if world.mode[player] != 'inverted':
create_regions(world, player)
create_doors(world, player)
create_rooms(world, player)
create_dungeons(world, player)
else:
create_inverted_regions(world, player) # todo: port all the dungeon region work
create_doors(world, player)
create_rooms(world, player)
create_dungeons(world, player)
create_inverted_regions(world, player)
create_shops(world, player)
create_doors(world, player)
create_rooms(world, player)
create_dungeons(world, player)
logger.info('Shuffling the World about.')
@@ -143,65 +144,38 @@ def main(args, seed=None):
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)
outfilebase = 'ER_%s' % (args.outputname if args.outputname else world.seed)
rom_names = []
jsonout = {}
if not args.suppress_rom:
from MultiServer import MultiWorld
multidata = MultiWorld()
multidata.players = world.players
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'
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)
rom_names.append((player, list(rom.name)))
enemizer_patch = []
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)
for location in world.get_filled_locations(player):
if type(location.address) is int:
multidata.locations[(location.address, player)] = (location.item.code, location.item.player)
if args.race:
patch_race_rom(rom)
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:
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:
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 = ''
if all([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player], world.bigkeyshuffle[player]]):
mcsb_name = '-keysanity'
@@ -222,8 +196,15 @@ def main(args, seed=None):
"-nohints" if not world.hints[player] else "")) if not args.outputname else ''
rom.write_to_file(output_path(f'{outfilebase}{playername}{outfilesuffix}.sfc'))
with open(output_path('%s_multidata' % outfilebase), 'wb') as f:
pickle.dump(multidata, f, pickle.HIGHEST_PROTOCOL)
multidata = zlib.compress(json.dumps((world.players,
rom_names,
[((location.address, location.player), (location.item.code, location.item.player)) for location in world.get_filled_locations() if type(location.address) is int])
).encode("utf-8"))
if args.jsonout:
jsonout["multidata"] = list(multidata)
else:
with open(output_path('%s_multidata' % outfilebase), 'wb') as f:
f.write(multidata)
if args.create_spoiler and not args.jsonout:
world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase))
@@ -244,7 +225,7 @@ def main(args, seed=None):
def copy_world(world):
# 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.swamp_patch_required = world.swamp_patch_required.copy()
ret.ganon_at_pyramid = world.ganon_at_pyramid.copy()
@@ -282,6 +263,7 @@ def copy_world(world):
create_regions(ret, player)
else:
create_inverted_regions(ret, player)
create_shops(ret, player)
create_dungeons(ret, player)
copy_dynamic_regions_and_locations(world, ret)
@@ -293,7 +275,6 @@ def copy_world(world):
for shop in world.shops:
copied_shop = ret.get_region(shop.region.name, shop.region.player).shop
copied_shop.active = shop.active
copied_shop.inventory = copy.copy(shop.inventory)
# 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
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)
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)
old_item = location.item
location.item = None
state.remove(old_item)
if world.can_beat_game(state_cache[num]):
to_delete.append(location)
else:
@@ -423,6 +403,14 @@ def create_playthrough(world):
for location in to_delete:
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
# 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
@@ -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))
# 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')
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)
try:

View File

@@ -4,10 +4,10 @@ import asyncio
import functools
import json
import logging
import pickle
import re
import urllib.request
import websockets
import zlib
import Items
import Regions
@@ -22,18 +22,14 @@ class Client:
self.slot = None
self.send_index = 0
class MultiWorld:
def __init__(self):
self.players = None
self.rom_names = {}
self.locations = {}
class Context:
def __init__(self, host, port, password):
self.data_filename = None
self.save_filename = None
self.disable_save = False
self.world = MultiWorld()
self.players = 0
self.rom_names = {}
self.locations = {}
self.host = host
self.port = port
self.password = password
@@ -44,7 +40,7 @@ class Context:
def get_room_info(ctx : Context):
return {
'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]
}
@@ -175,8 +171,8 @@ def forfeit_player(ctx : Context, team, slot, name):
def register_location_checks(ctx : Context, name, team, slot, locations):
found_items = False
for location in locations:
if (location, slot) in ctx.world.locations:
target_item, target_player = ctx.world.locations[(location, slot)]
if (location, slot) in ctx.locations:
target_item, target_player = ctx.locations[(location, slot)]
if target_player != slot:
found = False
recvd_items = get_received_items(ctx, team, target_player)
@@ -196,7 +192,10 @@ def register_location_checks(ctx : Context, name, team, slot, locations):
if found_items and not ctx.disable_save:
try:
with open(ctx.save_filename, "wb") as f:
pickle.dump((ctx.world.players, ctx.world.rom_names, ctx.received_items), f, pickle.HIGHEST_PROTOCOL)
jsonstr = json.dumps((ctx.players,
[(k, v) for k, v in ctx.rom_names.items()],
[(k, [i.__dict__ for i in v]) for k, v in ctx.received_items.items()]))
f.write(zlib.compress(jsonstr.encode("utf-8")))
except Exception as e:
logging.exception(e)
@@ -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)]):
errors.add('SlotAlreadyTaken')
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)]:
client.slot = slot
break
elif slot == ctx.world.players:
elif slot == ctx.players:
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')
else:
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)]])
else:
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)
if 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"),))
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:
print('Failed to read multiworld data (%s)' % e)
return
ip = urllib.request.urlopen('https://v4.ident.me').read().decode('utf8') if not ctx.host else ctx.host
print('Hosting game of %d players (%s) at %s:%d' % (ctx.world.players, 'No password' if not ctx.password else 'Password: %s' % ctx.password, ip, ctx.port))
print('Hosting game of %d players (%s) at %s:%d' % (ctx.players, 'No password' if not ctx.password else 'Password: %s' % ctx.password, ip, ctx.port))
ctx.disable_save = args.disable_save
if not ctx.disable_save:
@@ -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'
try:
with open(ctx.save_filename, 'rb') as f:
players, rom_names, received_items = pickle.load(f)
if players != ctx.world.players or rom_names != ctx.world.rom_names:
jsonobj = json.loads(zlib.decompress(f.read()).decode("utf-8"))
players = jsonobj[0]
rom_names = {k: v for k, v in jsonobj[1]}
received_items = {tuple(k): [ReceivedItem(**i) for i in v] for k, v in jsonobj[2]}
if players != ctx.players or rom_names != ctx.rom_names:
raise Exception('Save file mismatch, will start a new game')
ctx.received_items = received_items
print('Loaded save file with %d received items for %d players' % (sum([len(p) for p in received_items.values()]), len(received_items)))

View File

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

View File

@@ -10,7 +10,7 @@ import sys
from BaseClasses import World
from Regions import create_regions
from EntranceShuffle import link_entrances, connect_entrance, connect_two_way, connect_exit
from Rom import patch_rom, LocalRom, Sprite, write_string_to_rom, 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 Dungeons import create_dungeons
from Items import ItemFactory
@@ -23,7 +23,7 @@ def main(args):
start_time = time.perf_counter()
# initialize the world
world = World(1, 'vanilla', 'noglitches', 'standard', 'normal', 'none', 'on', 'ganon', 'freshness', False, False, False, args.quickswap, args.fastmenu, args.disablemusic, False, False, False, None, False)
world = World(1, 'vanilla', 'noglitches', 'standard', 'normal', 'none', 'on', 'ganon', 'freshness', False, False, False, False, False, False, None, False)
logger = logging.getLogger('')
hasher = hashlib.md5()
@@ -68,15 +68,10 @@ def main(args):
logger.info('Patching ROM.')
if args.sprite is not None:
sprite = Sprite(args.sprite)
else:
sprite = None
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:
if texttype == 'text':
@@ -213,6 +208,8 @@ def start():
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'],
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('--plando', help='Filled out template to use for setting up the rom.')
args = parser.parse_args()
@@ -224,8 +221,8 @@ def start():
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)
sys.exit(1)
if args.sprite is not None and not os.path.isfile(args.rom):
input('Could not find link sprite sheet at given location. \nPress Enter to exit.' % 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.')
sys.exit(1)
# set up logger

View File

@@ -721,6 +721,7 @@ def create_regions(world, player):
shop.add_inventory(1, 'Arrow Upgrade (+5)', 100, 7)
world.initialize_regions()
def create_lw_region(player, name, locations=None, exits=None):
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
# 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)]
_dark_world_shop_defaults = [('Red Potion', 150), ('Blue Shield', 50), ('Bombs (10)', 50)]
default_shop_contents = {
'Cave Shop (Dark Death Mountain)': _basic_shop_defaults,
'Red Shield Shop': [('Red Shield', 500), ('Bee', 10), ('Arrows (10)', 30)],
'Dark Lake Hylia Shop': _dark_world_shop_defaults,
'Dark World Lumberjack Shop': _dark_world_shop_defaults,
'Village of Outcasts Shop': _dark_world_shop_defaults,
'Dark World Potion Shop': _dark_world_shop_defaults,
'Light World Death Mountain Shop': _basic_shop_defaults,
'Kakariko Shop': _basic_shop_defaults,
'Cave Shop (Lake Hylia)': _basic_shop_defaults,
'Potion Shop': [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)],
shop_table = {
'Cave Shop (Dark Death Mountain)': (0x0112, ShopType.Shop, 0xC1, True, False, _basic_shop_defaults),
'Red Shield Shop': (0x0110, ShopType.Shop, 0xC1, True, False, [('Red Shield', 500), ('Bee', 10), ('Arrows (10)', 30)]),
'Dark Lake Hylia Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults),
'Dark World Lumberjack Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults),
'Village of Outcasts Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults),
'Dark World Potion Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults),
'Light World Death Mountain Shop': (0x00FF, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults),
'Kakariko Shop': (0x011F, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults),
'Cave Shop (Lake Hylia)': (0x0112, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults),
'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 = {

529
Rom.py
View File

@@ -9,13 +9,13 @@ import struct
import sys
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 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 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 Items import ItemFactory, item_table
from Items import ItemFactory
from EntranceShuffle import door_addresses, exit_ids
@@ -27,6 +27,7 @@ class JsonRom(object):
def __init__(self):
self.name = None
self.orig_buffer = None
self.patches = {}
self.addresses = []
@@ -36,8 +37,7 @@ class JsonRom(object):
def write_bytes(self, startaddress, values):
if not values:
return
if type(values) is not list:
values = list(values)
values = list(values)
pos = bisect.bisect_right(self.addresses, startaddress)
intervalstart = self.addresses[pos-1] if pos else None
@@ -72,10 +72,12 @@ class LocalRom(object):
def __init__(self, file, patch=True):
self.name = None
self.orig_buffer = None
with open(file, 'rb') as stream:
self.buffer = read_rom(stream)
if patch:
self.patch_base_rom()
self.orig_buffer = self.buffer.copy()
def write_byte(self, address, value):
self.buffer[address] = value
@@ -112,24 +114,11 @@ class LocalRom(object):
# if RANDOMIZERBASEHASH != patchedmd5.hexdigest():
# raise RuntimeError('Provided Base Rom unsuitable for patching. Please provide a JAP(1.0) "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc" rom to use as a base.')
def patch_enemizer(self, rando_patch, base_enemizer_patch_path, enemizer_patch):
# extend to 4MB
def merge_enemizer_patches(self, patches):
self.buffer.extend(bytearray([0x00] * (0x400000 - len(self.buffer))))
# apply randomizer patches
for address, values in rando_patch.items():
for address, values in patches.items():
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):
crc = (sum(self.buffer[:0x7FDC] + self.buffer[0x7FE0:]) + 0x01FE) & 0xFFFF
inv = crc ^ 0xFFFF
@@ -161,9 +150,10 @@ def read_rom(stream):
buffer = buffer[0x200:]
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)
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'))
options_path = os.path.abspath(output_path('enemizer_options.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,
'RandomizeBossDamageMaxAmount': 200,
'RandomizeBossBehavior': False,
'RandomizeDungeonPalettes': shufflepalette,
'RandomizeDungeonPalettes': False,
'SetBlackoutMode': False,
'RandomizeOverworldPalettes': shufflepalette,
'RandomizeSpritePalettes': shufflepalette,
'RandomizeOverworldPalettes': False,
'RandomizeSpritePalettes': False,
'SetAdvancedSpritePalettes': False,
'PukeMode': False,
'NegativeMode': False,
@@ -221,7 +211,7 @@ def get_enemizer_patch(world, player, rom, baserom_path, enemizercli, shufflepal
'RandomizeTileTrapPattern': world.enemy_shuffle[player] == 'chaos',
'RandomizeTileTrapFloorTile': False,
'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,
'DebugForceEnemy': False,
'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,
'MiseryMire': world.get_dungeon("Misery Mire", 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',
'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)
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],
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:
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)
except OSError:
pass
if os.path.exists(options_path):
try:
os.remove(options_path)
except OSError:
pass
if os.path.exists(enemizer_output_path):
try:
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):
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(0x180174, 0x01 if world.fix_fake_world[player] else 0x00)
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
for x in range(0x183000, 0x18304F):
rom.write_byte(x, 0) # Zero the initial equipment array
rom.write_byte(0x18302C, 0x18) # starting max health
rom.write_byte(0x18302D, 0x18) # starting current health
rom.write_byte(0x183039, 0x68) # starting abilities, bit array
# Starting equipment
equip = [0] * (0x340 + 0x4F)
equip[0x36C] = 0x18
equip[0x36D] = 0x18
equip[0x379] = 0x68
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:
if item.player != player:
continue
if item.name == 'Fighter Sword':
rom.write_byte(0x183000+0x19, 0x01)
rom.write_byte(0x0271A6+0x19, 0x01)
rom.write_byte(0x180043, 0x01) # special starting sword byte
if item.name in ['Bow', 'Silver Arrows', 'Progressive Bow', 'Progressive Bow (Alt)',
'Titans Mitts', 'Power Glove', 'Progressive Glove',
'Golden Sword', 'Tempered Sword', 'Master Sword', 'Fighter Sword', 'Progressive Sword',
'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:
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("Unsupported pre-collected item: {}".format(item))
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(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(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)
| (0x02 if world.compassshuffle[player] else 0x00)
@@ -1114,16 +1275,14 @@ try:
except ImportError:
RaceRom = None
def get_race_rom_patches(rom):
patches = {str(0x180213): [0x01, 0x00]} # Tournament Seed
def patch_race_rom(rom):
rom.write_bytes(0x180213, [0x01, 0x00]) # Tournament Seed
if 'RaceRom' in sys.modules:
RaceRom.encrypt(rom, patches)
return patches
RaceRom.encrypt(rom)
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()
items_data = bytearray()
@@ -1171,7 +1330,9 @@ def hud_format_text(text):
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
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)
music_volumes = [
(0x00, [0xD373B, 0xD375B, 0xD90F8]),
(0x14, [0xDA710, 0xDA7A4, 0xDA7BB, 0xDA7D2]),
(0x3C, [0xD5954, 0xD653B, 0xDA736, 0xDA752, 0xDA772, 0xDA792]),
(0x50, [0xD5B47, 0xD5B5E]),
(0x54, [0xD4306]),
(0x64,
[0xD6878, 0xD6883, 0xD6E48, 0xD6E76, 0xD6EFB, 0xD6F2D, 0xDA211, 0xDA35B, 0xDA37B, 0xDA38E, 0xDA39F, 0xDA5C3,
0xDA691, 0xDA6A8, 0xDA6DF]),
(0x78,
[0xD2349, 0xD3F45, 0xD42EB, 0xD48B9, 0xD48FF, 0xD543F, 0xD5817, 0xD5957, 0xD5ACB, 0xD5AE8, 0xD5B4A, 0xDA5DE,
0xDA608, 0xDA635,
0xDA662, 0xDA71F, 0xDA7AF, 0xDA7C6, 0xDA7DD]),
(0x82, [0xD2F00, 0xDA3D5]),
(0xA0,
[0xD249C, 0xD24CD, 0xD2C09, 0xD2C53, 0xD2CAF, 0xD2CEB, 0xD2D91, 0xD2EE6, 0xD38ED, 0xD3C91, 0xD3CD3, 0xD3CE8,
0xD3F0C,
0xD3F82, 0xD405F, 0xD4139, 0xD4198, 0xD41D5, 0xD41F6, 0xD422B, 0xD4270, 0xD42B1, 0xD4334, 0xD4371, 0xD43A6,
0xD43DB,
0xD441E, 0xD4597, 0xD4B3C, 0xD4BAB, 0xD4C03, 0xD4C53, 0xD4C7F, 0xD4D9C, 0xD5424, 0xD65D2, 0xD664F, 0xD6698,
0xD66FF,
0xD6985, 0xD6C5C, 0xD6C6F, 0xD6C8E, 0xD6CB4, 0xD6D7D, 0xD827D, 0xD960C, 0xD9828, 0xDA233, 0xDA3A2, 0xDA49E,
0xDA72B,
0xDA745, 0xDA765, 0xDA785, 0xDABF6, 0xDAC0D, 0xDAEBE, 0xDAFAC]),
(0xAA, [0xD9A02, 0xD9BD6]),
(0xB4,
[0xD21CD, 0xD2279, 0xD2E66, 0xD2E70, 0xD2EAB, 0xD3B97, 0xD3BAC, 0xD3BE8, 0xD3C0D, 0xD3C39, 0xD3C68, 0xD3C9F,
0xD3CBC,
0xD401E, 0xD4290, 0xD443E, 0xD456F, 0xD47D3, 0xD4D43, 0xD4DCC, 0xD4EBA, 0xD4F0B, 0xD4FE5, 0xD5012, 0xD54BC,
0xD54D5,
0xD54F0, 0xD5509, 0xD57D8, 0xD59B9, 0xD5A2F, 0xD5AEB, 0xD5E5E, 0xD5FE9, 0xD658F, 0xD674A, 0xD6827, 0xD69D6,
0xD69F5,
0xD6A05, 0xD6AE9, 0xD6DCF, 0xD6E20, 0xD6ECB, 0xD71D4, 0xD71E6, 0xD7203, 0xD721E, 0xD8724, 0xD8732, 0xD9652,
0xD9698,
0xD9CBC, 0xD9DC0, 0xD9E49, 0xDAA68, 0xDAA77, 0xDAA88, 0xDAA99, 0xDAF04]),
(0x8c,
[0xD1D28, 0xD1D41, 0xD1D5C, 0xD1D77, 0xD1EEE, 0xD311D, 0xD31D1, 0xD4148, 0xD5543, 0xD5B6F, 0xD65B3, 0xD6760,
0xD6B6B,
0xD6DF6, 0xD6E0D, 0xD73A1, 0xD814C, 0xD825D, 0xD82BE, 0xD8340, 0xD8394, 0xD842C, 0xD8796, 0xD8903, 0xD892A,
0xD91E8,
0xD922B, 0xD92E0, 0xD937E, 0xD93C1, 0xDA958, 0xDA971, 0xDA98C, 0xDA9A7]),
(0xC8,
[0xD1D92, 0xD1DBD, 0xD1DEB, 0xD1F5D, 0xD1F9F, 0xD1FBD, 0xD1FDC, 0xD1FEA, 0xD20CA, 0xD21BB, 0xD22C9, 0xD2754,
0xD284C,
0xD2866, 0xD2887, 0xD28A0, 0xD28BA, 0xD28DB, 0xD28F4, 0xD293E, 0xD2BF3, 0xD2C1F, 0xD2C69, 0xD2CA1, 0xD2CC5,
0xD2D05,
0xD2D73, 0xD2DAF, 0xD2E3D, 0xD2F36, 0xD2F46, 0xD2F6F, 0xD2FCF, 0xD2FDF, 0xD302B, 0xD3086, 0xD3099, 0xD30A5,
0xD30CD,
0xD30F6, 0xD3154, 0xD3184, 0xD333A, 0xD33D9, 0xD349F, 0xD354A, 0xD35E5, 0xD3624, 0xD363C, 0xD3672, 0xD3691,
0xD36B4,
0xD36C6, 0xD3724, 0xD3767, 0xD38CB, 0xD3B1D, 0xD3B2F, 0xD3B55, 0xD3B70, 0xD3B81, 0xD3BBF, 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(0x0CFE18, 0x00 if disable_music else rom.orig_buffer[0x0CFE18] if rom.orig_buffer else 0x70)
rom.write_byte(0x0CFEC1, 0x00 if disable_music else rom.orig_buffer[0x0CFEC1] if rom.orig_buffer else 0xC0)
rom.write_bytes(0x0D0000, [0x00, 0x00] if disable_music else rom.orig_buffer[0x0D0000:0x0D0002] if rom.orig_buffer else [0xDA, 0x58])
rom.write_bytes(0x0D00E7, [0xC4, 0x58] if disable_music else rom.orig_buffer[0x0D00E7:0x0D00E9] if rom.orig_buffer else [0xDA, 0x58])
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
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:
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
for player, name in names.items():
if 0 < player <= 64:
@@ -1348,6 +1415,130 @@ def write_sprite(rom, sprite):
rom.write_bytes(0xDD308, sprite.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):
address, maxbytes = text_addresses[target]