diff --git a/.gitignore b/.gitignore index 8c412af3..8c5d8669 100644 --- a/.gitignore +++ b/.gitignore @@ -16,5 +16,7 @@ README.html EnemizerCLI/ .mypy_cache/ RaceRom.py +weights/ + venv test diff --git a/Adjuster.py b/Adjuster.py index 393b7b75..570bcef9 100755 --- a/Adjuster.py +++ b/Adjuster.py @@ -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 diff --git a/AdjusterMain.py b/AdjusterMain.py index b4c1834c..800e307d 100644 --- a/AdjusterMain.py +++ b/AdjusterMain.py @@ -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)) diff --git a/BaseClasses.py b/BaseClasses.py index cf360836..d9306584 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -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])) diff --git a/DungeonRandomizer.py b/DungeonRandomizer.py index 80fe219c..9df11af2 100755 --- a/DungeonRandomizer.py +++ b/DungeonRandomizer.py @@ -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) diff --git a/Gui.py b/Gui.py index 1d68f195..c3a26589 100755 --- a/Gui.py +++ b/Gui.py @@ -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: diff --git a/InvertedRegions.py b/InvertedRegions.py index 5c9360d1..b83c586c 100644 --- a/InvertedRegions.py +++ b/InvertedRegions.py @@ -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. diff --git a/ItemList.py b/ItemList.py index ea89bca3..a59e5c58 100644 --- a/ItemList.py +++ b/ItemList.py @@ -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) diff --git a/Items.py b/Items.py index c9f3fd42..840b6289 100644 --- a/Items.py +++ b/Items.py @@ -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'), diff --git a/Main.py b/Main.py index 025386c4..7c94feac 100644 --- a/Main.py +++ b/Main.py @@ -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} diff --git a/MultiClient.py b/MultiClient.py index fd7cfa9a..7435cc2f 100644 --- a/MultiClient.py +++ b/MultiClient.py @@ -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: diff --git a/MultiServer.py b/MultiServer.py index cb753721..87d1fd41 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -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))) diff --git a/Mystery.py b/Mystery.py index 06f57aca..0c5c848c 100644 --- a/Mystery.py +++ b/Mystery.py @@ -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 diff --git a/Plando.py b/Plando.py index ce208e64..e92a40bf 100755 --- a/Plando.py +++ b/Plando.py @@ -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 diff --git a/Regions.py b/Regions.py index 0cd9cc53..e3ec85c1 100644 --- a/Regions.py +++ b/Regions.py @@ -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 = { diff --git a/Rom.py b/Rom.py index a3c0fa84..c03c392a 100644 --- a/Rom.py +++ b/Rom.py @@ -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]