1124 lines
		
	
	
		
			44 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			1124 lines
		
	
	
		
			44 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
#!/usr/bin/env python3
 | 
						|
import argparse
 | 
						|
import json
 | 
						|
import os
 | 
						|
import logging
 | 
						|
import queue
 | 
						|
import random
 | 
						|
import shutil
 | 
						|
import textwrap
 | 
						|
import sys
 | 
						|
import threading
 | 
						|
import time
 | 
						|
import tkinter as tk
 | 
						|
from argparse import Namespace
 | 
						|
from concurrent.futures import as_completed, ThreadPoolExecutor
 | 
						|
from glob import glob
 | 
						|
from tkinter import Tk, Frame, Label, StringVar, Entry, filedialog, messagebox, Button, LEFT, X, TOP, LabelFrame, \
 | 
						|
    IntVar, Checkbutton, E, W, OptionMenu, Toplevel, BOTTOM, RIGHT, font as font, PhotoImage
 | 
						|
from urllib.parse import urlparse
 | 
						|
from urllib.request import urlopen
 | 
						|
 | 
						|
from worlds.alttp.Rom import Sprite, LocalRom, apply_rom_settings, get_base_rom_bytes
 | 
						|
from Utils import output_path, local_path, open_file
 | 
						|
 | 
						|
 | 
						|
class AdjusterWorld(object):
 | 
						|
    def __init__(self, sprite_pool):
 | 
						|
        import random
 | 
						|
        self.sprite_pool = {1: sprite_pool}
 | 
						|
        self.slot_seeds = {1: random}
 | 
						|
 | 
						|
 | 
						|
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
 | 
						|
 | 
						|
    def _get_help_string(self, action):
 | 
						|
        return textwrap.dedent(action.help)
 | 
						|
 | 
						|
 | 
						|
def main():
 | 
						|
    parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
 | 
						|
 | 
						|
    parser.add_argument('--rom', default='ER_base.sfc', help='Path to an ALttP 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('--menuspeed', default='normal', const='normal', nargs='?',
 | 
						|
                        choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
 | 
						|
                        help='''\
 | 
						|
                             Select the rate at which the menu opens and closes.
 | 
						|
                             (default: %(default)s)
 | 
						|
                             ''')
 | 
						|
    parser.add_argument('--quickswap', help='Enable quick item swapping with L and R.', action='store_true')
 | 
						|
    parser.add_argument('--deathlink', help='Enable DeathLink system.', action='store_true')
 | 
						|
    parser.add_argument('--disablemusic', help='Disables game music.', action='store_true')
 | 
						|
    parser.add_argument('--triforcehud', default='hide_goal', const='hide_goal', nargs='?',
 | 
						|
                        choices=['normal', 'hide_goal', 'hide_required', 'hide_both'],
 | 
						|
                        help='''\
 | 
						|
                            Hide the triforce hud in certain circumstances.
 | 
						|
                            hide_goal will hide the hud until finding a triforce piece, hide_required will hide the total amount needed to win
 | 
						|
                            (Both can be revealed when speaking to Murahalda)
 | 
						|
                            (default: %(default)s)
 | 
						|
                            ''')
 | 
						|
    parser.add_argument('--enableflashing',
 | 
						|
                        help='Reenable flashing animations (unfriendly to epilepsy, always disabled in race roms)',
 | 
						|
                        action='store_false', dest="reduceflashing")
 | 
						|
    parser.add_argument('--heartbeep', default='normal', const='normal', nargs='?',
 | 
						|
                        choices=['double', 'normal', 'half', 'quarter', 'off'],
 | 
						|
                        help='''\
 | 
						|
                             Select the rate at which the heart beep sound is played at
 | 
						|
                             low health. (default: %(default)s)
 | 
						|
                             ''')
 | 
						|
    parser.add_argument('--heartcolor', default='red', const='red', nargs='?',
 | 
						|
                        choices=['red', 'blue', 'green', 'yellow', 'random'],
 | 
						|
                        help='Select the color of Link\'s heart meter. (default: %(default)s)')
 | 
						|
    parser.add_argument('--ow_palettes', default='default',
 | 
						|
                        choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
 | 
						|
                                 'sick'])
 | 
						|
    parser.add_argument('--link_palettes', default='default',
 | 
						|
                        choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
 | 
						|
                                 'sick'])
 | 
						|
    parser.add_argument('--shield_palettes', default='default',
 | 
						|
                        choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
 | 
						|
                                 'sick'])
 | 
						|
    parser.add_argument('--sword_palettes', default='default',
 | 
						|
                        choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
 | 
						|
                                 'sick'])
 | 
						|
    parser.add_argument('--hud_palettes', default='default',
 | 
						|
                        choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
 | 
						|
                                 'sick'])
 | 
						|
    parser.add_argument('--uw_palettes', default='default',
 | 
						|
                        choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
 | 
						|
                                 'sick'])
 | 
						|
    parser.add_argument('--sprite', help='''\
 | 
						|
                             Path to a sprite sheet to use for Link. Needs to be in
 | 
						|
                             binary format and have a length of 0x7000 (28672) bytes,
 | 
						|
                             or 0x7078 (28792) bytes including palette data.
 | 
						|
                             Alternatively, can be a ALttP Rom patched with a Link
 | 
						|
                             sprite that will be extracted.
 | 
						|
                             ''')
 | 
						|
    parser.add_argument('--names', default='', type=str)
 | 
						|
    parser.add_argument('--update_sprites', action='store_true', help='Update Sprite Database, then exit.')
 | 
						|
    args = parser.parse_args()
 | 
						|
    args.music = not args.disablemusic
 | 
						|
    if args.update_sprites:
 | 
						|
        run_sprite_update()
 | 
						|
        sys.exit()
 | 
						|
    # set up logger
 | 
						|
    loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[
 | 
						|
        args.loglevel]
 | 
						|
    logging.basicConfig(format='%(message)s', level=loglevel)
 | 
						|
 | 
						|
    if not os.path.isfile(args.rom):
 | 
						|
        adjustGUI()
 | 
						|
    else:
 | 
						|
        if args.sprite is not None and not os.path.isfile(args.sprite) and not Sprite.get_sprite_from_name(args.sprite):
 | 
						|
            input('Could not find link sprite sheet at given location. \nPress Enter to exit.')
 | 
						|
            sys.exit(1)
 | 
						|
 | 
						|
        args, path = adjust(args=args)
 | 
						|
        from Utils import persistent_store
 | 
						|
        if isinstance(args.sprite, Sprite):
 | 
						|
            args.sprite = args.sprite.name
 | 
						|
        persistent_store("adjuster", "last_settings_3", args)
 | 
						|
 | 
						|
 | 
						|
def adjust(args):
 | 
						|
    start = time.perf_counter()
 | 
						|
    logger = logging.getLogger('Adjuster')
 | 
						|
    logger.info('Patching ROM.')
 | 
						|
    vanillaRom = args.baserom
 | 
						|
    if os.path.splitext(args.rom)[-1].lower() == '.apbp':
 | 
						|
        import Patch
 | 
						|
        meta, args.rom = Patch.create_rom_file(args.rom)
 | 
						|
 | 
						|
    if os.stat(args.rom).st_size in (0x200000, 0x400000) and os.path.splitext(args.rom)[-1].lower() == '.sfc':
 | 
						|
        rom = LocalRom(args.rom, patch=False, vanillaRom=vanillaRom)
 | 
						|
    else:
 | 
						|
        raise RuntimeError(
 | 
						|
            'Provided Rom is not a valid Link to the Past Randomizer Rom. Please provide one for adjusting.')
 | 
						|
    palettes_options = {}
 | 
						|
    palettes_options['dungeon'] = args.uw_palettes
 | 
						|
 | 
						|
    palettes_options['overworld'] = args.ow_palettes
 | 
						|
    palettes_options['hud'] = args.hud_palettes
 | 
						|
    palettes_options['sword'] = args.sword_palettes
 | 
						|
    palettes_options['shield'] = args.shield_palettes
 | 
						|
    # palettes_options['link']=args.link_palettesvera
 | 
						|
 | 
						|
    racerom = rom.read_byte(0x180213) > 0
 | 
						|
    world = None
 | 
						|
    if hasattr(args, "world"):
 | 
						|
        world = getattr(args, "world")
 | 
						|
 | 
						|
    apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.menuspeed, args.music,
 | 
						|
                       args.sprite, palettes_options, reduceflashing=args.reduceflashing or racerom, world=world,
 | 
						|
                       deathlink=args.deathlink)
 | 
						|
    path = output_path(f'{os.path.basename(args.rom)[:-4]}_adjusted.sfc')
 | 
						|
    rom.write_to_file(path)
 | 
						|
 | 
						|
    logger.info('Done. Enjoy.')
 | 
						|
    logger.debug('Total Time: %s', time.perf_counter() - start)
 | 
						|
 | 
						|
    return args, path
 | 
						|
 | 
						|
 | 
						|
def adjustGUI():
 | 
						|
    from tkinter import Tk, LEFT, BOTTOM, TOP, \
 | 
						|
        StringVar, Frame, Label, X, Entry, Button, filedialog, messagebox, ttk
 | 
						|
    from argparse import Namespace
 | 
						|
    from Main import __version__ as MWVersion
 | 
						|
    adjustWindow = Tk()
 | 
						|
    adjustWindow.wm_title("Archipelago %s LttP Adjuster" % MWVersion)
 | 
						|
    set_icon(adjustWindow)
 | 
						|
 | 
						|
    rom_options_frame, rom_vars, set_sprite = get_rom_options_frame(adjustWindow)
 | 
						|
 | 
						|
    bottomFrame2 = Frame(adjustWindow)
 | 
						|
 | 
						|
    romFrame, romVar = get_rom_frame(adjustWindow)
 | 
						|
 | 
						|
    romDialogFrame = Frame(adjustWindow)
 | 
						|
    baseRomLabel2 = Label(romDialogFrame, text='Rom to adjust')
 | 
						|
    romVar2 = StringVar()
 | 
						|
    romEntry2 = Entry(romDialogFrame, textvariable=romVar2)
 | 
						|
 | 
						|
    def RomSelect2():
 | 
						|
        rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc", ".apbp")), ("All Files", "*")])
 | 
						|
        romVar2.set(rom)
 | 
						|
 | 
						|
    romSelectButton2 = Button(romDialogFrame, text='Select Rom', command=RomSelect2)
 | 
						|
    romDialogFrame.pack(side=TOP, expand=True, fill=X)
 | 
						|
    baseRomLabel2.pack(side=LEFT)
 | 
						|
    romEntry2.pack(side=LEFT, expand=True, fill=X)
 | 
						|
    romSelectButton2.pack(side=LEFT)
 | 
						|
 | 
						|
    def adjustRom():
 | 
						|
        guiargs = Namespace()
 | 
						|
        guiargs.heartbeep = rom_vars.heartbeepVar.get()
 | 
						|
        guiargs.heartcolor = rom_vars.heartcolorVar.get()
 | 
						|
        guiargs.menuspeed = rom_vars.menuspeedVar.get()
 | 
						|
        guiargs.ow_palettes = rom_vars.owPalettesVar.get()
 | 
						|
        guiargs.uw_palettes = rom_vars.uwPalettesVar.get()
 | 
						|
        guiargs.hud_palettes = rom_vars.hudPalettesVar.get()
 | 
						|
        guiargs.sword_palettes = rom_vars.swordPalettesVar.get()
 | 
						|
        guiargs.shield_palettes = rom_vars.shieldPalettesVar.get()
 | 
						|
        guiargs.quickswap = bool(rom_vars.quickSwapVar.get())
 | 
						|
        guiargs.music = bool(rom_vars.MusicVar.get())
 | 
						|
        guiargs.reduceflashing = bool(rom_vars.disableFlashingVar.get())
 | 
						|
        guiargs.deathlink = bool(rom_vars.DeathLinkVar.get())
 | 
						|
        guiargs.rom = romVar2.get()
 | 
						|
        guiargs.baserom = romVar.get()
 | 
						|
        guiargs.sprite = rom_vars.sprite
 | 
						|
        if rom_vars.sprite_pool:
 | 
						|
            guiargs.world = AdjusterWorld(rom_vars.sprite_pool)
 | 
						|
 | 
						|
        try:
 | 
						|
            guiargs, path = adjust(args=guiargs)
 | 
						|
            if rom_vars.sprite_pool:
 | 
						|
                guiargs.sprite_pool = rom_vars.sprite_pool
 | 
						|
                delattr(guiargs, "world")
 | 
						|
        except Exception as e:
 | 
						|
            logging.exception(e)
 | 
						|
            messagebox.showerror(title="Error while adjusting Rom", message=str(e))
 | 
						|
        else:
 | 
						|
            messagebox.showinfo(title="Success", message=f"Rom patched successfully to {path}")
 | 
						|
            from Utils import persistent_store
 | 
						|
            if isinstance(guiargs.sprite, Sprite):
 | 
						|
                guiargs.sprite = guiargs.sprite.name
 | 
						|
            persistent_store("adjuster", "last_settings_3", guiargs)
 | 
						|
 | 
						|
    adjustButton = Button(bottomFrame2, text='Adjust Rom', command=adjustRom)
 | 
						|
    rom_options_frame.pack(side=TOP)
 | 
						|
    adjustButton.pack(side=BOTTOM, padx=(5, 5))
 | 
						|
 | 
						|
    bottomFrame2.pack(side=BOTTOM, pady=(5, 5))
 | 
						|
 | 
						|
    adjustWindow.mainloop()
 | 
						|
 | 
						|
 | 
						|
def run_sprite_update():
 | 
						|
    import threading
 | 
						|
    done = threading.Event()
 | 
						|
    top = Tk()
 | 
						|
    top.withdraw()
 | 
						|
    BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
 | 
						|
    while not done.isSet():
 | 
						|
        top.update()
 | 
						|
    print("Done updating sprites")
 | 
						|
 | 
						|
 | 
						|
def update_sprites(task, on_finish=None):
 | 
						|
    resultmessage = ""
 | 
						|
    successful = True
 | 
						|
    sprite_dir = local_path("data", "sprites", "alttpr")
 | 
						|
    os.makedirs(sprite_dir, exist_ok=True)
 | 
						|
 | 
						|
    def finished():
 | 
						|
        task.close_window()
 | 
						|
        if on_finish:
 | 
						|
            on_finish(successful, resultmessage)
 | 
						|
 | 
						|
    try:
 | 
						|
        task.update_status("Downloading alttpr sprites list")
 | 
						|
        with urlopen('https://alttpr.com/sprites') as response:
 | 
						|
            sprites_arr = json.loads(response.read().decode("utf-8"))
 | 
						|
    except Exception as e:
 | 
						|
        resultmessage = "Error getting list of alttpr sprites. Sprites not updated.\n\n%s: %s" % (type(e).__name__, e)
 | 
						|
        successful = False
 | 
						|
        task.queue_event(finished)
 | 
						|
        return
 | 
						|
 | 
						|
    try:
 | 
						|
        task.update_status("Determining needed sprites")
 | 
						|
        current_sprites = [os.path.basename(file) for file in glob(sprite_dir + '/*')]
 | 
						|
        alttpr_sprites = [(sprite['file'], os.path.basename(urlparse(sprite['file']).path))
 | 
						|
                          for sprite in sprites_arr if sprite["author"] != "Nintendo"]
 | 
						|
        needed_sprites = [(sprite_url, filename) for (sprite_url, filename) in alttpr_sprites if
 | 
						|
                          filename not in current_sprites]
 | 
						|
 | 
						|
        alttpr_filenames = [filename for (_, filename) in alttpr_sprites]
 | 
						|
        obsolete_sprites = [sprite for sprite in current_sprites if sprite not in alttpr_filenames]
 | 
						|
    except Exception as e:
 | 
						|
        resultmessage = "Error Determining which sprites to update. Sprites not updated.\n\n%s: %s" % (
 | 
						|
        type(e).__name__, e)
 | 
						|
        successful = False
 | 
						|
        task.queue_event(finished)
 | 
						|
        return
 | 
						|
 | 
						|
    def dl(sprite_url, filename):
 | 
						|
        target = os.path.join(sprite_dir, filename)
 | 
						|
        with urlopen(sprite_url) as response, open(target, 'wb') as out:
 | 
						|
            shutil.copyfileobj(response, out)
 | 
						|
 | 
						|
    def rem(sprite):
 | 
						|
        os.remove(os.path.join(sprite_dir, sprite))
 | 
						|
 | 
						|
    with ThreadPoolExecutor() as pool:
 | 
						|
        dl_tasks = []
 | 
						|
        rem_tasks = []
 | 
						|
 | 
						|
        for (sprite_url, filename) in needed_sprites:
 | 
						|
            dl_tasks.append(pool.submit(dl, sprite_url, filename))
 | 
						|
 | 
						|
        for sprite in obsolete_sprites:
 | 
						|
            rem_tasks.append(pool.submit(rem, sprite))
 | 
						|
 | 
						|
        deleted = 0
 | 
						|
        updated = 0
 | 
						|
 | 
						|
        for dl_task in as_completed(dl_tasks):
 | 
						|
            updated += 1
 | 
						|
            task.update_status("Downloading needed sprite %g/%g" % (updated, len(needed_sprites)))
 | 
						|
            try:
 | 
						|
                dl_task.result()
 | 
						|
            except Exception as e:
 | 
						|
                logging.exception(e)
 | 
						|
                resultmessage = "Error downloading sprite. Not all sprites updated.\n\n%s: %s" % (
 | 
						|
                    type(e).__name__, e)
 | 
						|
                successful = False
 | 
						|
 | 
						|
        for rem_task in as_completed(rem_tasks):
 | 
						|
            deleted += 1
 | 
						|
            task.update_status("Removing obsolete sprite %g/%g" % (deleted, len(obsolete_sprites)))
 | 
						|
            try:
 | 
						|
                rem_task.result()
 | 
						|
            except Exception as e:
 | 
						|
                logging.exception(e)
 | 
						|
                resultmessage = "Error removing obsolete sprite. Not all sprites updated.\n\n%s: %s" % (
 | 
						|
                    type(e).__name__, e)
 | 
						|
                successful = False
 | 
						|
 | 
						|
    if successful:
 | 
						|
        resultmessage = "alttpr sprites updated successfully"
 | 
						|
 | 
						|
    task.queue_event(finished)
 | 
						|
 | 
						|
 | 
						|
def set_icon(window):
 | 
						|
    logo = tk.PhotoImage(file=local_path('data', 'icon.png'))
 | 
						|
    window.tk.call('wm', 'iconphoto', window._w, logo)
 | 
						|
 | 
						|
 | 
						|
class BackgroundTask(object):
 | 
						|
    def __init__(self, window, code_to_run, *args):
 | 
						|
        self.window = window
 | 
						|
        self.queue = queue.Queue()
 | 
						|
        self.running = True
 | 
						|
        self.process_queue()
 | 
						|
        self.task = threading.Thread(target=code_to_run, args=(self, *args))
 | 
						|
        self.task.start()
 | 
						|
 | 
						|
    def stop(self):
 | 
						|
        self.running = False
 | 
						|
 | 
						|
    # safe to call from worker
 | 
						|
    def queue_event(self, event):
 | 
						|
        self.queue.put(event)
 | 
						|
 | 
						|
    def process_queue(self):
 | 
						|
        try:
 | 
						|
            while True:
 | 
						|
                if not self.running:
 | 
						|
                    return
 | 
						|
                event = self.queue.get_nowait()
 | 
						|
                event()
 | 
						|
                if self.running:
 | 
						|
                    # if self is no longer running self.window may no longer be valid
 | 
						|
                    self.window.update_idletasks()
 | 
						|
        except queue.Empty:
 | 
						|
            pass
 | 
						|
        if self.running:
 | 
						|
            self.window.after(100, self.process_queue)
 | 
						|
 | 
						|
 | 
						|
class BackgroundTaskProgress(BackgroundTask):
 | 
						|
    def __init__(self, parent, code_to_run, title, *args):
 | 
						|
        self.parent = parent
 | 
						|
        self.window = tk.Toplevel(parent)
 | 
						|
        self.window['padx'] = 5
 | 
						|
        self.window['pady'] = 5
 | 
						|
 | 
						|
        try:
 | 
						|
            self.window.attributes("-toolwindow", 1)
 | 
						|
        except tk.TclError:
 | 
						|
            pass
 | 
						|
 | 
						|
        self.window.wm_title(title)
 | 
						|
        self.label_var = tk.StringVar()
 | 
						|
        self.label_var.set("")
 | 
						|
        self.label = tk.Label(self.window, textvariable=self.label_var, width=50)
 | 
						|
        self.label.pack()
 | 
						|
        self.window.resizable(width=False, height=False)
 | 
						|
 | 
						|
        set_icon(self.window)
 | 
						|
        self.window.focus()
 | 
						|
        super().__init__(self.window, code_to_run, *args)
 | 
						|
 | 
						|
    # safe to call from worker thread
 | 
						|
    def update_status(self, text):
 | 
						|
        self.queue_event(lambda: self.label_var.set(text))
 | 
						|
 | 
						|
    # only call this in an event callback
 | 
						|
    def close_window(self):
 | 
						|
        self.stop()
 | 
						|
        self.window.destroy()
 | 
						|
 | 
						|
 | 
						|
def get_rom_frame(parent=None):
 | 
						|
    romFrame = Frame(parent)
 | 
						|
    baseRomLabel = Label(romFrame, text='LttP Base Rom: ')
 | 
						|
    romVar = StringVar(value="Zelda no Densetsu - Kamigami no Triforce (Japan).sfc")
 | 
						|
    romEntry = Entry(romFrame, textvariable=romVar)
 | 
						|
 | 
						|
    def RomSelect():
 | 
						|
        rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")])
 | 
						|
        try:
 | 
						|
            get_base_rom_bytes(rom)  # throws error on checksum fail
 | 
						|
        except Exception as e:
 | 
						|
            logging.exception(e)
 | 
						|
            messagebox.showerror(title="Error while reading ROM", message=str(e))
 | 
						|
        else:
 | 
						|
            romVar.set(rom)
 | 
						|
            romSelectButton['state'] = "disabled"
 | 
						|
            romSelectButton["text"] = "ROM verified"
 | 
						|
 | 
						|
    romSelectButton = Button(romFrame, text='Select Rom', command=RomSelect)
 | 
						|
 | 
						|
    baseRomLabel.pack(side=LEFT)
 | 
						|
    romEntry.pack(side=LEFT, expand=True, fill=X)
 | 
						|
    romSelectButton.pack(side=LEFT)
 | 
						|
    romFrame.pack(side=TOP, expand=True, fill=X)
 | 
						|
 | 
						|
    return romFrame, romVar
 | 
						|
 | 
						|
 | 
						|
def get_rom_options_frame(parent=None):
 | 
						|
    romOptionsFrame = LabelFrame(parent, text="Rom options")
 | 
						|
    romOptionsFrame.columnconfigure(0, weight=1)
 | 
						|
    romOptionsFrame.columnconfigure(1, weight=1)
 | 
						|
    for i in range(5):
 | 
						|
        romOptionsFrame.rowconfigure(i, weight=1)
 | 
						|
    vars = Namespace()
 | 
						|
 | 
						|
    vars.MusicVar = IntVar()
 | 
						|
    vars.MusicVar.set(1)
 | 
						|
    MusicCheckbutton = Checkbutton(romOptionsFrame, text="Music", variable=vars.MusicVar)
 | 
						|
    MusicCheckbutton.grid(row=0, column=0, sticky=E)
 | 
						|
 | 
						|
    vars.disableFlashingVar = IntVar(value=1)
 | 
						|
    disableFlashingCheckbutton = Checkbutton(romOptionsFrame, text="Disable flashing (anti-epilepsy)",
 | 
						|
                                             variable=vars.disableFlashingVar)
 | 
						|
    disableFlashingCheckbutton.grid(row=6, column=0, sticky=W)
 | 
						|
 | 
						|
    vars.DeathLinkVar = IntVar(value=0)
 | 
						|
    DeathLinkCheckbutton = Checkbutton(romOptionsFrame, text="DeathLink (Team Deaths)", variable=vars.DeathLinkVar)
 | 
						|
    DeathLinkCheckbutton.grid(row=7, column=0, sticky=W)
 | 
						|
 | 
						|
    spriteDialogFrame = Frame(romOptionsFrame)
 | 
						|
    spriteDialogFrame.grid(row=0, column=1)
 | 
						|
    baseSpriteLabel = Label(spriteDialogFrame, text='Sprite:')
 | 
						|
 | 
						|
    vars.spriteNameVar = StringVar()
 | 
						|
    vars.sprite = None
 | 
						|
 | 
						|
    def set_sprite(sprite_param):
 | 
						|
        nonlocal vars
 | 
						|
        if isinstance(sprite_param, str):
 | 
						|
            vars.sprite = sprite_param
 | 
						|
            vars.spriteNameVar.set(sprite_param)
 | 
						|
        elif sprite_param is None or not sprite_param.valid:
 | 
						|
            vars.sprite = None
 | 
						|
            vars.spriteNameVar.set('(unchanged)')
 | 
						|
        else:
 | 
						|
            vars.sprite = sprite_param
 | 
						|
            vars.spriteNameVar.set(vars.sprite.name)
 | 
						|
 | 
						|
    set_sprite(None)
 | 
						|
    vars.spriteNameVar.set('(unchanged)')
 | 
						|
    spriteEntry = Label(spriteDialogFrame, textvariable=vars.spriteNameVar)
 | 
						|
 | 
						|
    def SpriteSelect():
 | 
						|
        nonlocal vars
 | 
						|
        SpriteSelector(parent, set_sprite, spritePool=vars.sprite_pool)
 | 
						|
 | 
						|
    spriteSelectButton = Button(spriteDialogFrame, text='...', command=SpriteSelect)
 | 
						|
 | 
						|
    baseSpriteLabel.pack(side=LEFT)
 | 
						|
    spriteEntry.pack(side=LEFT)
 | 
						|
    spriteSelectButton.pack(side=LEFT)
 | 
						|
 | 
						|
    vars.quickSwapVar = IntVar(value=1)
 | 
						|
    quickSwapCheckbutton = Checkbutton(romOptionsFrame, text="L/R Quickswapping", variable=vars.quickSwapVar)
 | 
						|
    quickSwapCheckbutton.grid(row=1, column=0, sticky=E)
 | 
						|
 | 
						|
    menuspeedFrame = Frame(romOptionsFrame)
 | 
						|
    menuspeedFrame.grid(row=1, column=1, sticky=E)
 | 
						|
    menuspeedLabel = Label(menuspeedFrame, text='Menu speed')
 | 
						|
    menuspeedLabel.pack(side=LEFT)
 | 
						|
    vars.menuspeedVar = StringVar()
 | 
						|
    vars.menuspeedVar.set('normal')
 | 
						|
    menuspeedOptionMenu = OptionMenu(menuspeedFrame, vars.menuspeedVar, 'normal', 'instant', 'double', 'triple',
 | 
						|
                                     'quadruple', 'half')
 | 
						|
    menuspeedOptionMenu.pack(side=LEFT)
 | 
						|
 | 
						|
    heartcolorFrame = Frame(romOptionsFrame)
 | 
						|
    heartcolorFrame.grid(row=2, column=0, sticky=E)
 | 
						|
    heartcolorLabel = Label(heartcolorFrame, text='Heart color')
 | 
						|
    heartcolorLabel.pack(side=LEFT)
 | 
						|
    vars.heartcolorVar = StringVar()
 | 
						|
    vars.heartcolorVar.set('red')
 | 
						|
    heartcolorOptionMenu = OptionMenu(heartcolorFrame, vars.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)
 | 
						|
    vars.heartbeepVar = StringVar()
 | 
						|
    vars.heartbeepVar.set('normal')
 | 
						|
    heartbeepOptionMenu = OptionMenu(heartbeepFrame, vars.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)
 | 
						|
    vars.owPalettesVar = StringVar()
 | 
						|
    vars.owPalettesVar.set('default')
 | 
						|
    owPalettesOptionMenu = OptionMenu(owPalettesFrame, vars.owPalettesVar, 'default', 'good', 'blackout', 'grayscale',
 | 
						|
                                      'negative', 'classic', 'dizzy', 'sick', 'puke')
 | 
						|
    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)
 | 
						|
    vars.uwPalettesVar = StringVar()
 | 
						|
    vars.uwPalettesVar.set('default')
 | 
						|
    uwPalettesOptionMenu = OptionMenu(uwPalettesFrame, vars.uwPalettesVar, 'default', 'good', 'blackout', 'grayscale',
 | 
						|
                                      'negative', 'classic', 'dizzy', 'sick', 'puke')
 | 
						|
    uwPalettesOptionMenu.pack(side=LEFT)
 | 
						|
 | 
						|
    hudPalettesFrame = Frame(romOptionsFrame)
 | 
						|
    hudPalettesFrame.grid(row=4, column=0, sticky=E)
 | 
						|
    hudPalettesLabel = Label(hudPalettesFrame, text='HUD palettes')
 | 
						|
    hudPalettesLabel.pack(side=LEFT)
 | 
						|
    vars.hudPalettesVar = StringVar()
 | 
						|
    vars.hudPalettesVar.set('default')
 | 
						|
    hudPalettesOptionMenu = OptionMenu(hudPalettesFrame, vars.hudPalettesVar, 'default', 'good', 'blackout',
 | 
						|
                                       'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
 | 
						|
    hudPalettesOptionMenu.pack(side=LEFT)
 | 
						|
 | 
						|
    swordPalettesFrame = Frame(romOptionsFrame)
 | 
						|
    swordPalettesFrame.grid(row=4, column=1, sticky=E)
 | 
						|
    swordPalettesLabel = Label(swordPalettesFrame, text='Sword palettes')
 | 
						|
    swordPalettesLabel.pack(side=LEFT)
 | 
						|
    vars.swordPalettesVar = StringVar()
 | 
						|
    vars.swordPalettesVar.set('default')
 | 
						|
    swordPalettesOptionMenu = OptionMenu(swordPalettesFrame, vars.swordPalettesVar, 'default', 'good', 'blackout',
 | 
						|
                                         'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
 | 
						|
    swordPalettesOptionMenu.pack(side=LEFT)
 | 
						|
 | 
						|
    shieldPalettesFrame = Frame(romOptionsFrame)
 | 
						|
    shieldPalettesFrame.grid(row=5, column=0, sticky=E)
 | 
						|
    shieldPalettesLabel = Label(shieldPalettesFrame, text='Shield palettes')
 | 
						|
    shieldPalettesLabel.pack(side=LEFT)
 | 
						|
    vars.shieldPalettesVar = StringVar()
 | 
						|
    vars.shieldPalettesVar.set('default')
 | 
						|
    shieldPalettesOptionMenu = OptionMenu(shieldPalettesFrame, vars.shieldPalettesVar, 'default', 'good', 'blackout',
 | 
						|
                                          'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
 | 
						|
    shieldPalettesOptionMenu.pack(side=LEFT)
 | 
						|
 | 
						|
    spritePoolFrame = Frame(romOptionsFrame)
 | 
						|
    spritePoolFrame.grid(row=5, column=1)
 | 
						|
    baseSpritePoolLabel = Label(spritePoolFrame, text='Sprite Pool:')
 | 
						|
 | 
						|
    vars.spritePoolCountVar = StringVar()
 | 
						|
    vars.sprite_pool = []
 | 
						|
 | 
						|
    def set_sprite_pool(sprite_param):
 | 
						|
        nonlocal vars
 | 
						|
        operation = "add"
 | 
						|
        if isinstance(sprite_param, tuple):
 | 
						|
            operation, sprite_param = sprite_param
 | 
						|
        if isinstance(sprite_param, Sprite) and sprite_param.valid:
 | 
						|
            sprite_param = sprite_param.name
 | 
						|
        if isinstance(sprite_param, str):
 | 
						|
            if operation == "add":
 | 
						|
                vars.sprite_pool.append(sprite_param)
 | 
						|
            elif operation == "remove":
 | 
						|
                vars.sprite_pool.remove(sprite_param)
 | 
						|
            elif operation == "clear":
 | 
						|
                vars.sprite_pool.clear()
 | 
						|
        vars.spritePoolCountVar.set(str(len(vars.sprite_pool)))
 | 
						|
 | 
						|
    set_sprite_pool(None)
 | 
						|
    vars.spritePoolCountVar.set('0')
 | 
						|
    spritePoolEntry = Label(spritePoolFrame, textvariable=vars.spritePoolCountVar)
 | 
						|
 | 
						|
    def SpritePoolSelect():
 | 
						|
        nonlocal vars
 | 
						|
        SpriteSelector(parent, set_sprite_pool, randomOnEvent=False, spritePool=vars.sprite_pool)
 | 
						|
 | 
						|
    def SpritePoolClear():
 | 
						|
        nonlocal vars
 | 
						|
        vars.sprite_pool.clear()
 | 
						|
        vars.spritePoolCountVar.set('0')
 | 
						|
 | 
						|
    spritePoolSelectButton = Button(spritePoolFrame, text='...', command=SpritePoolSelect)
 | 
						|
    spritePoolClearButton = Button(spritePoolFrame, text='Clear', command=SpritePoolClear)
 | 
						|
 | 
						|
    baseSpritePoolLabel.pack(side=LEFT)
 | 
						|
    spritePoolEntry.pack(side=LEFT)
 | 
						|
    spritePoolSelectButton.pack(side=LEFT)
 | 
						|
    spritePoolClearButton.pack(side=LEFT)
 | 
						|
 | 
						|
    return romOptionsFrame, vars, set_sprite
 | 
						|
 | 
						|
 | 
						|
class SpriteSelector():
 | 
						|
    def __init__(self, parent, callback, adjuster=False, randomOnEvent=True, spritePool=None):
 | 
						|
        self.deploy_icons()
 | 
						|
        self.parent = parent
 | 
						|
        self.window = Toplevel(parent)
 | 
						|
        self.callback = callback
 | 
						|
        self.adjuster = adjuster
 | 
						|
        self.randomOnEvent = randomOnEvent
 | 
						|
        self.spritePoolButtons = None
 | 
						|
 | 
						|
        self.window.wm_title("TAKE ANY ONE YOU WANT")
 | 
						|
        self.window['padx'] = 5
 | 
						|
        self.window['pady'] = 5
 | 
						|
        self.spritesPerRow = 32
 | 
						|
        self.all_sprites = []
 | 
						|
        self.sprite_pool = spritePool
 | 
						|
 | 
						|
        def open_custom_sprite_dir(_evt):
 | 
						|
            open_file(self.custom_sprite_dir)
 | 
						|
 | 
						|
        alttpr_frametitle = Label(self.window, text='ALTTPR Sprites')
 | 
						|
 | 
						|
        custom_frametitle = Frame(self.window)
 | 
						|
        title_text = Label(custom_frametitle, text="Custom Sprites")
 | 
						|
        title_link = Label(custom_frametitle, text="(open)", fg="blue", cursor="hand2")
 | 
						|
        title_text.pack(side=LEFT)
 | 
						|
        title_link.pack(side=LEFT)
 | 
						|
        title_link.bind("<Button-1>", open_custom_sprite_dir)
 | 
						|
 | 
						|
        self.icon_section(alttpr_frametitle, self.alttpr_sprite_dir,
 | 
						|
                          'ALTTPR sprites not found. Click "Update alttpr sprites" to download them.')
 | 
						|
        self.icon_section(custom_frametitle, self.custom_sprite_dir,
 | 
						|
                          'Put sprites in the custom sprites folder (see open link above) to have them appear here.')
 | 
						|
        if not randomOnEvent:
 | 
						|
            self.sprite_pool_section(spritePool)
 | 
						|
 | 
						|
        frame = Frame(self.window)
 | 
						|
        frame.pack(side=BOTTOM, fill=X, pady=5)
 | 
						|
 | 
						|
        if self.randomOnEvent:
 | 
						|
            button = Button(frame, text="Browse for file...", command=self.browse_for_sprite)
 | 
						|
            button.pack(side=RIGHT, padx=(5, 0))
 | 
						|
 | 
						|
        button = Button(frame, text="Update alttpr sprites", command=self.update_alttpr_sprites)
 | 
						|
        button.pack(side=RIGHT, padx=(5, 0))
 | 
						|
 | 
						|
        button = Button(frame, text="Default Link sprite", command=self.use_default_link_sprite)
 | 
						|
        button.pack(side=LEFT, padx=(0, 5))
 | 
						|
 | 
						|
        self.randomButtonText = StringVar()
 | 
						|
        button = Button(frame, textvariable=self.randomButtonText, command=self.use_random_sprite)
 | 
						|
        button.pack(side=LEFT, padx=(0, 5))
 | 
						|
        self.randomButtonText.set("Random")
 | 
						|
 | 
						|
        self.randomOnEventText = StringVar()
 | 
						|
        self.randomOnHitVar = IntVar()
 | 
						|
        self.randomOnEnterVar = IntVar()
 | 
						|
        self.randomOnExitVar = IntVar()
 | 
						|
        self.randomOnSlashVar = IntVar()
 | 
						|
        self.randomOnItemVar = IntVar()
 | 
						|
        self.randomOnBonkVar = IntVar()
 | 
						|
        self.randomOnRandomVar = IntVar()
 | 
						|
 | 
						|
        if self.randomOnEvent:
 | 
						|
            button = Checkbutton(frame, text="Hit", command=self.update_random_button, variable=self.randomOnHitVar)
 | 
						|
            button.pack(side=LEFT, padx=(0, 5))
 | 
						|
 | 
						|
            button = Checkbutton(frame, text="Enter", command=self.update_random_button, variable=self.randomOnEnterVar)
 | 
						|
            button.pack(side=LEFT, padx=(0, 5))
 | 
						|
 | 
						|
            button = Checkbutton(frame, text="Exit", command=self.update_random_button, variable=self.randomOnExitVar)
 | 
						|
            button.pack(side=LEFT, padx=(0, 5))
 | 
						|
 | 
						|
            button = Checkbutton(frame, text="Slash", command=self.update_random_button, variable=self.randomOnSlashVar)
 | 
						|
            button.pack(side=LEFT, padx=(0, 5))
 | 
						|
 | 
						|
            button = Checkbutton(frame, text="Item", command=self.update_random_button, variable=self.randomOnItemVar)
 | 
						|
            button.pack(side=LEFT, padx=(0, 5))
 | 
						|
 | 
						|
            button = Checkbutton(frame, text="Bonk", command=self.update_random_button, variable=self.randomOnBonkVar)
 | 
						|
            button.pack(side=LEFT, padx=(0, 5))
 | 
						|
 | 
						|
            button = Checkbutton(frame, text="Random", command=self.update_random_button,
 | 
						|
                                 variable=self.randomOnRandomVar)
 | 
						|
            button.pack(side=LEFT, padx=(0, 5))
 | 
						|
 | 
						|
        if adjuster:
 | 
						|
            button = Button(frame, text="Current sprite from rom", command=self.use_default_sprite)
 | 
						|
            button.pack(side=LEFT, padx=(0, 5))
 | 
						|
 | 
						|
        set_icon(self.window)
 | 
						|
        self.window.focus()
 | 
						|
 | 
						|
    def remove_from_sprite_pool(self, button, spritename):
 | 
						|
        self.callback(("remove", spritename))
 | 
						|
        self.spritePoolButtons.buttons.remove(button)
 | 
						|
        button.destroy()
 | 
						|
 | 
						|
    def add_to_sprite_pool(self, spritename):
 | 
						|
        if isinstance(spritename, str):
 | 
						|
            if spritename == "random":
 | 
						|
                button = Button(self.spritePoolButtons, text="?")
 | 
						|
                button['font'] = font.Font(size=19)
 | 
						|
                button.configure(command=lambda spr="random": self.remove_from_sprite_pool(button, spr))
 | 
						|
                ToolTips.register(button, "Random")
 | 
						|
                self.spritePoolButtons.buttons.append(button)
 | 
						|
            else:
 | 
						|
                spritename = Sprite.get_sprite_from_name(spritename)
 | 
						|
        if isinstance(spritename, Sprite) and spritename.valid:
 | 
						|
            image = get_image_for_sprite(spritename)
 | 
						|
            if image is None:
 | 
						|
                return
 | 
						|
            button = Button(self.spritePoolButtons, image=image)
 | 
						|
            button.configure(command=lambda spr=spritename: self.remove_from_sprite_pool(button, spr.name))
 | 
						|
            ToolTips.register(button, spritename.name +
 | 
						|
                              f"\nBy: {spritename.author_name if spritename.author_name else ''}")
 | 
						|
            button.image = image
 | 
						|
 | 
						|
            self.spritePoolButtons.buttons.append(button)
 | 
						|
        self.grid_fill_sprites(self.spritePoolButtons)
 | 
						|
 | 
						|
    def sprite_pool_section(self, spritePool):
 | 
						|
        def clear_sprite_pool(_evt):
 | 
						|
            self.callback(("clear", "Clear"))
 | 
						|
            for button in self.spritePoolButtons.buttons:
 | 
						|
                button.destroy()
 | 
						|
            self.spritePoolButtons.buttons.clear()
 | 
						|
 | 
						|
        frametitle = Frame(self.window)
 | 
						|
        title_text = Label(frametitle, text="Sprite Pool")
 | 
						|
        title_link = Label(frametitle, text="(clear)", fg="blue", cursor="hand2")
 | 
						|
        title_text.pack(side=LEFT)
 | 
						|
        title_link.pack(side=LEFT)
 | 
						|
        title_link.bind("<Button-1>", clear_sprite_pool)
 | 
						|
 | 
						|
        self.spritePoolButtons = LabelFrame(self.window, labelwidget=frametitle, padx=5, pady=5)
 | 
						|
        self.spritePoolButtons.pack(side=TOP, fill=X)
 | 
						|
        self.spritePoolButtons.buttons = []
 | 
						|
 | 
						|
        def update_sprites(event):
 | 
						|
            self.spritesPerRow = (event.width - 10) // 38
 | 
						|
            self.grid_fill_sprites(self.spritePoolButtons)
 | 
						|
 | 
						|
        self.grid_fill_sprites(self.spritePoolButtons)
 | 
						|
        self.spritePoolButtons.bind("<Configure>", update_sprites)
 | 
						|
 | 
						|
        if spritePool:
 | 
						|
            for sprite in spritePool:
 | 
						|
                self.add_to_sprite_pool(sprite)
 | 
						|
 | 
						|
    def icon_section(self, frame_label, path, no_results_label):
 | 
						|
        frame = LabelFrame(self.window, labelwidget=frame_label, padx=5, pady=5)
 | 
						|
        frame.pack(side=TOP, fill=X)
 | 
						|
 | 
						|
        sprites = []
 | 
						|
 | 
						|
        for file in os.listdir(path):
 | 
						|
            sprites.append((file, Sprite(os.path.join(path, file))))
 | 
						|
 | 
						|
        sprites.sort(key=lambda s: str.lower(s[1].name or "").strip())
 | 
						|
 | 
						|
        frame.buttons = []
 | 
						|
        for file, sprite in sprites:
 | 
						|
            image = get_image_for_sprite(sprite)
 | 
						|
            if image is None:
 | 
						|
                continue
 | 
						|
            self.all_sprites.append(sprite)
 | 
						|
            button = Button(frame, image=image, command=lambda spr=sprite: self.select_sprite(spr))
 | 
						|
            ToolTips.register(button, sprite.name +
 | 
						|
                              ("\nBy: %s" % sprite.author_name if sprite.author_name else "") +
 | 
						|
                              f"\nFrom: {file}")
 | 
						|
            button.image = image
 | 
						|
            frame.buttons.append(button)
 | 
						|
 | 
						|
        if not frame.buttons:
 | 
						|
            label = Label(frame, text=no_results_label)
 | 
						|
            label.pack()
 | 
						|
 | 
						|
        def update_sprites(event):
 | 
						|
            self.spritesPerRow = (event.width - 10) // 38
 | 
						|
            self.grid_fill_sprites(frame)
 | 
						|
 | 
						|
        self.grid_fill_sprites(frame)
 | 
						|
 | 
						|
        frame.bind("<Configure>", update_sprites)
 | 
						|
 | 
						|
    def grid_fill_sprites(self, frame):
 | 
						|
        for i, button in enumerate(frame.buttons):
 | 
						|
            button.grid(row=i // self.spritesPerRow, column=i % self.spritesPerRow)
 | 
						|
 | 
						|
    def update_alttpr_sprites(self):
 | 
						|
        # need to wrap in try catch. We don't want errors getting the json or downloading the files to break us.
 | 
						|
        self.window.destroy()
 | 
						|
        self.parent.update()
 | 
						|
 | 
						|
        def on_finish(successful, resultmessage):
 | 
						|
            if successful:
 | 
						|
                messagebox.showinfo("Sprite Updater", resultmessage)
 | 
						|
            else:
 | 
						|
                logging.error(resultmessage)
 | 
						|
                messagebox.showerror("Sprite Updater", resultmessage)
 | 
						|
            SpriteSelector(self.parent, self.callback, self.adjuster)
 | 
						|
 | 
						|
        BackgroundTaskProgress(self.parent, update_sprites, "Updating Sprites", on_finish)
 | 
						|
 | 
						|
    def browse_for_sprite(self):
 | 
						|
        sprite = filedialog.askopenfilename(
 | 
						|
            filetypes=[("All Sprite Sources", (".zspr", ".spr", ".sfc", ".smc")),
 | 
						|
                       ("ZSprite files", ".zspr"),
 | 
						|
                       ("Sprite files", ".spr"),
 | 
						|
                       ("Rom Files", (".sfc", ".smc")),
 | 
						|
                       ("All Files", "*")])
 | 
						|
        try:
 | 
						|
            self.callback(Sprite(sprite))
 | 
						|
        except Exception:
 | 
						|
            self.callback(None)
 | 
						|
        self.window.destroy()
 | 
						|
 | 
						|
    def use_default_sprite(self):
 | 
						|
        self.callback(None)
 | 
						|
        self.window.destroy()
 | 
						|
 | 
						|
    def use_default_link_sprite(self):
 | 
						|
        if self.randomOnEvent:
 | 
						|
            self.callback(Sprite.default_link_sprite())
 | 
						|
            self.window.destroy()
 | 
						|
        else:
 | 
						|
            self.callback("link")
 | 
						|
            self.add_to_sprite_pool("link")
 | 
						|
 | 
						|
    def update_random_button(self):
 | 
						|
        if self.randomOnRandomVar.get():
 | 
						|
            randomon = "random"
 | 
						|
        else:
 | 
						|
            randomon = "-hit" if self.randomOnHitVar.get() else ""
 | 
						|
            randomon += "-enter" if self.randomOnEnterVar.get() else ""
 | 
						|
            randomon += "-exit" if self.randomOnExitVar.get() else ""
 | 
						|
            randomon += "-slash" if self.randomOnSlashVar.get() else ""
 | 
						|
            randomon += "-item" if self.randomOnItemVar.get() else ""
 | 
						|
            randomon += "-bonk" if self.randomOnBonkVar.get() else ""
 | 
						|
 | 
						|
        self.randomOnEventText.set(f"randomon{randomon}" if randomon else None)
 | 
						|
        self.randomButtonText.set("Random On Event" if randomon else "Random")
 | 
						|
 | 
						|
    def use_random_sprite(self):
 | 
						|
        if not self.randomOnEvent:
 | 
						|
            self.callback("random")
 | 
						|
            self.add_to_sprite_pool("random")
 | 
						|
            return
 | 
						|
        elif self.randomOnEventText.get():
 | 
						|
            self.callback(self.randomOnEventText.get())
 | 
						|
        elif self.sprite_pool:
 | 
						|
            self.callback(random.choice(self.sprite_pool))
 | 
						|
        elif self.all_sprites:
 | 
						|
            self.callback(random.choice(self.all_sprites))
 | 
						|
        else:
 | 
						|
            self.callback(None)
 | 
						|
        self.window.destroy()
 | 
						|
 | 
						|
    def select_sprite(self, spritename):
 | 
						|
        self.callback(spritename)
 | 
						|
        if self.randomOnEvent:
 | 
						|
            self.window.destroy()
 | 
						|
        else:
 | 
						|
            self.add_to_sprite_pool(spritename)
 | 
						|
 | 
						|
    def deploy_icons(self):
 | 
						|
        if not os.path.exists(self.custom_sprite_dir):
 | 
						|
            os.makedirs(self.custom_sprite_dir)
 | 
						|
 | 
						|
    @property
 | 
						|
    def alttpr_sprite_dir(self):
 | 
						|
        return local_path("data", "sprites", "alttpr")
 | 
						|
 | 
						|
    @property
 | 
						|
    def custom_sprite_dir(self):
 | 
						|
        return local_path("data", "sprites", "custom")
 | 
						|
 | 
						|
 | 
						|
def get_image_for_sprite(sprite, gif_only: bool = False):
 | 
						|
    if not sprite.valid:
 | 
						|
        return None
 | 
						|
    height = 24
 | 
						|
    width = 16
 | 
						|
 | 
						|
    def draw_sprite_into_gif(add_palette_color, set_pixel_color_index):
 | 
						|
 | 
						|
        def drawsprite(spr, pal_as_colors, offset):
 | 
						|
            for y, row in enumerate(spr):
 | 
						|
                for x, pal_index in enumerate(row):
 | 
						|
                    if pal_index:
 | 
						|
                        color = pal_as_colors[pal_index - 1]
 | 
						|
                        set_pixel_color_index(x + offset[0], y + offset[1], color)
 | 
						|
 | 
						|
        add_palette_color(16, (40, 40, 40))
 | 
						|
        shadow = [
 | 
						|
            [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0],
 | 
						|
            [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
 | 
						|
            [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
 | 
						|
            [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
 | 
						|
            [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
 | 
						|
            [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0],
 | 
						|
        ]
 | 
						|
 | 
						|
        drawsprite(shadow, [16], (2, 17))
 | 
						|
 | 
						|
        palettes = sprite.decode_palette()
 | 
						|
        for i in range(15):
 | 
						|
            add_palette_color(i + 1, palettes[0][i])
 | 
						|
 | 
						|
        body = sprite.decode16(0x4C0)
 | 
						|
        drawsprite(body, list(range(1, 16)), (0, 8))
 | 
						|
        head = sprite.decode16(0x40)
 | 
						|
        drawsprite(head, list(range(1, 16)), (0, 0))
 | 
						|
 | 
						|
    def make_gif(callback):
 | 
						|
        gif_header = b'GIF89a'
 | 
						|
 | 
						|
        gif_lsd = bytearray(7)
 | 
						|
        gif_lsd[0] = width
 | 
						|
        gif_lsd[2] = height
 | 
						|
        gif_lsd[
 | 
						|
            4] = 0xF4  # 32 color palette follows.  transparant + 15 for sprite + 1 for shadow=17 which rounds up to 32 as nearest power of 2
 | 
						|
        gif_lsd[5] = 0  # background color is zero
 | 
						|
        gif_lsd[6] = 0  # aspect raio not specified
 | 
						|
        gif_gct = bytearray(3 * 32)
 | 
						|
 | 
						|
        gif_gce = bytearray(8)
 | 
						|
        gif_gce[0] = 0x21  # start of extention blocked
 | 
						|
        gif_gce[1] = 0xF9  # identifies this as the Graphics Control extension
 | 
						|
        gif_gce[2] = 4  # we are suppling only the 4 four bytes
 | 
						|
        gif_gce[3] = 0x01  # this gif includes transparency
 | 
						|
        gif_gce[4] = gif_gce[5] = 0  # animation frrame delay (unused)
 | 
						|
        gif_gce[6] = 0  # transparent color is index 0
 | 
						|
        gif_gce[7] = 0  # end of gif_gce
 | 
						|
        gif_id = bytearray(10)
 | 
						|
        gif_id[0] = 0x2c
 | 
						|
        # byte 1,2 are image left. 3,4 are image top both are left as zerosuitsamus
 | 
						|
        gif_id[5] = width
 | 
						|
        gif_id[7] = height
 | 
						|
        gif_id[9] = 0  # no local color table
 | 
						|
 | 
						|
        gif_img_minimum_code_size = bytes(
 | 
						|
            [7])  # we choose 7 bits, so that each pixel is represented by a byte, for conviennce.
 | 
						|
 | 
						|
        clear = 0x80
 | 
						|
        stop = 0x81
 | 
						|
 | 
						|
        unchunked_image_data = bytearray(height * (width + 1) + 1)
 | 
						|
        # we technically need a Clear code once every 125 bytes, but we do it at the start of every row for simplicity
 | 
						|
        for row in range(height):
 | 
						|
            unchunked_image_data[row * (width + 1)] = clear
 | 
						|
        unchunked_image_data[-1] = stop
 | 
						|
 | 
						|
        def add_palette_color(index, color):
 | 
						|
            gif_gct[3 * index] = color[0]
 | 
						|
            gif_gct[3 * index + 1] = color[1]
 | 
						|
            gif_gct[3 * index + 2] = color[2]
 | 
						|
 | 
						|
        def set_pixel_color_index(x, y, color):
 | 
						|
            unchunked_image_data[y * (width + 1) + x + 1] = color
 | 
						|
 | 
						|
        callback(add_palette_color, set_pixel_color_index)
 | 
						|
 | 
						|
        def chunk_image(img):
 | 
						|
            for i in range(0, len(img), 255):
 | 
						|
                chunk = img[i:i + 255]
 | 
						|
                yield bytes([len(chunk)])
 | 
						|
                yield chunk
 | 
						|
 | 
						|
        gif_img = b''.join([gif_img_minimum_code_size] + list(chunk_image(unchunked_image_data)) + [b'\x00'])
 | 
						|
 | 
						|
        gif = b''.join([gif_header, gif_lsd, gif_gct, gif_gce, gif_id, gif_img, b'\x3b'])
 | 
						|
 | 
						|
        return gif
 | 
						|
 | 
						|
    gif_data = make_gif(draw_sprite_into_gif)
 | 
						|
    if gif_only:
 | 
						|
        return gif_data
 | 
						|
 | 
						|
    image = PhotoImage(data=gif_data)
 | 
						|
 | 
						|
    return image.zoom(2)
 | 
						|
 | 
						|
 | 
						|
class ToolTips(object):
 | 
						|
    # This class derived from wckToolTips which is available under the following license:
 | 
						|
 | 
						|
    # Copyright (c) 1998-2007 by Secret Labs AB
 | 
						|
    # Copyright (c) 1998-2007 by Fredrik Lundh
 | 
						|
    #
 | 
						|
    # By obtaining, using, and/or copying this software and/or its
 | 
						|
    # associated documentation, you agree that you have read, understood,
 | 
						|
    # and will comply with the following terms and conditions:
 | 
						|
    #
 | 
						|
    # Permission to use, copy, modify, and distribute this software and its
 | 
						|
    # associated documentation for any purpose and without fee is hereby
 | 
						|
    # granted, provided that the above copyright notice appears in all
 | 
						|
    # copies, and that both that copyright notice and this permission notice
 | 
						|
    # appear in supporting documentation, and that the name of Secret Labs
 | 
						|
    # AB or the author not be used in advertising or publicity pertaining to
 | 
						|
    # distribution of the software without specific, written prior
 | 
						|
    # permission.
 | 
						|
    #
 | 
						|
    # SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO
 | 
						|
    # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
 | 
						|
    # FITNESS.  IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR
 | 
						|
    # ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
						|
    # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 | 
						|
    # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
 | 
						|
    # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
						|
 | 
						|
    label = None
 | 
						|
    window = None
 | 
						|
    active = 0
 | 
						|
    tag = None
 | 
						|
    after_id = None
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def getcontroller(cls, widget):
 | 
						|
        if cls.tag is None:
 | 
						|
 | 
						|
            cls.tag = "ui_tooltip_%d" % id(cls)
 | 
						|
            widget.bind_class(cls.tag, "<Enter>", cls.enter)
 | 
						|
            widget.bind_class(cls.tag, "<Leave>", cls.leave)
 | 
						|
            widget.bind_class(cls.tag, "<Motion>", cls.motion)
 | 
						|
            widget.bind_class(cls.tag, "<Destroy>", cls.leave)
 | 
						|
 | 
						|
            # pick suitable colors for tooltips
 | 
						|
            try:
 | 
						|
                cls.bg = "systeminfobackground"
 | 
						|
                cls.fg = "systeminfotext"
 | 
						|
                widget.winfo_rgb(cls.fg)  # make sure system colors exist
 | 
						|
                widget.winfo_rgb(cls.bg)
 | 
						|
            except Exception:
 | 
						|
                cls.bg = "#ffffe0"
 | 
						|
                cls.fg = "black"
 | 
						|
 | 
						|
        return cls.tag
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def register(cls, widget, text):
 | 
						|
        widget.ui_tooltip_text = text
 | 
						|
        tags = list(widget.bindtags())
 | 
						|
        tags.append(cls.getcontroller(widget))
 | 
						|
        widget.bindtags(tuple(tags))
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def unregister(cls, widget):
 | 
						|
        tags = list(widget.bindtags())
 | 
						|
        tags.remove(cls.getcontroller(widget))
 | 
						|
        widget.bindtags(tuple(tags))
 | 
						|
 | 
						|
    # event handlers
 | 
						|
    @classmethod
 | 
						|
    def enter(cls, event):
 | 
						|
        widget = event.widget
 | 
						|
        if not cls.label:
 | 
						|
            # create and hide balloon help window
 | 
						|
            cls.popup = tk.Toplevel(bg=cls.fg, bd=1)
 | 
						|
            cls.popup.overrideredirect(1)
 | 
						|
            cls.popup.withdraw()
 | 
						|
            cls.label = tk.Label(
 | 
						|
                cls.popup, fg=cls.fg, bg=cls.bg, bd=0, padx=2, justify=tk.LEFT
 | 
						|
            )
 | 
						|
            cls.label.pack()
 | 
						|
            cls.active = 0
 | 
						|
        cls.xy = event.x_root + 16, event.y_root + 10
 | 
						|
        cls.event_xy = event.x, event.y
 | 
						|
        cls.after_id = widget.after(200, cls.display, widget)
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def motion(cls, event):
 | 
						|
        cls.xy = event.x_root + 16, event.y_root + 10
 | 
						|
        cls.event_xy = event.x, event.y
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def display(cls, widget):
 | 
						|
        if not cls.active:
 | 
						|
            # display balloon help window
 | 
						|
            text = widget.ui_tooltip_text
 | 
						|
            if callable(text):
 | 
						|
                text = text(widget, cls.event_xy)
 | 
						|
            cls.label.config(text=text)
 | 
						|
            cls.popup.deiconify()
 | 
						|
            cls.popup.lift()
 | 
						|
            cls.popup.geometry("+%d+%d" % cls.xy)
 | 
						|
            cls.active = 1
 | 
						|
            cls.after_id = None
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def leave(cls, event):
 | 
						|
        widget = event.widget
 | 
						|
        if cls.active:
 | 
						|
            cls.popup.withdraw()
 | 
						|
            cls.active = 0
 | 
						|
        if cls.after_id:
 | 
						|
            widget.after_cancel(cls.after_id)
 | 
						|
            cls.after_id = None
 | 
						|
 | 
						|
 | 
						|
if __name__ == '__main__':
 | 
						|
    main()
 |