1282 lines
		
	
	
		
			52 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			1282 lines
		
	
	
		
			52 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, Radiobutton, LEFT, X, TOP, LabelFrame, \
 | |
|     IntVar, Checkbutton, E, W, OptionMenu, Toplevel, BOTTOM, RIGHT, font as font, PhotoImage
 | |
| from tkinter.constants import DISABLED, NORMAL
 | |
| from urllib.parse import urlparse
 | |
| from urllib.request import urlopen
 | |
| 
 | |
| import ModuleUpdate
 | |
| ModuleUpdate.update()
 | |
| 
 | |
| from worlds.alttp.Rom import Sprite, LocalRom, apply_rom_settings, get_base_rom_bytes
 | |
| from Utils import output_path, local_path, user_path, open_file, get_cert_none_ssl_context, persistent_store, \
 | |
|     get_adjuster_settings, tkinter_center_window, init_logging
 | |
| from Patch import GAME_ALTTP
 | |
| 
 | |
| 
 | |
| 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', nargs="?", default='AP_LttP.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 Japan(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('--allowcollect', help='Allow collection of other player items', 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
 | |
|     # 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 args.update_sprites:
 | |
|         run_sprite_update()
 | |
|         sys.exit()
 | |
| 
 | |
|     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)
 | |
|         if isinstance(args.sprite, Sprite):
 | |
|             args.sprite = args.sprite.name
 | |
|         persistent_store("adjuster", GAME_ALTTP, args)
 | |
| 
 | |
| 
 | |
| def adjust(args):
 | |
|     start = time.perf_counter()
 | |
|     init_logging("LttP Adjuster")
 | |
|     logger = logging.getLogger('Adjuster')
 | |
|     logger.info('Patching ROM.')
 | |
|     vanillaRom = args.baserom
 | |
|     if not os.path.exists(vanillaRom) and not os.path.isabs(vanillaRom):
 | |
|         vanillaRom = local_path(vanillaRom)
 | |
|     if os.path.splitext(args.rom)[-1].lower() == '.aplttp':
 | |
|         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, allowcollect=args.allowcollect)
 | |
|     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", ".aplttp")), ("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.auto_apply = rom_vars.auto_apply.get()
 | |
|         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.allowcollect = bool(rom_vars.AllowCollectVar.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}")
 | |
|             if isinstance(guiargs.sprite, Sprite):
 | |
|                 guiargs.sprite = guiargs.sprite.name
 | |
|             delattr(guiargs, "rom")
 | |
|             persistent_store("adjuster", GAME_ALTTP, guiargs)
 | |
| 
 | |
|     def saveGUISettings():
 | |
|         guiargs = Namespace()
 | |
|         guiargs.auto_apply = rom_vars.auto_apply.get()
 | |
|         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.allowcollect = bool(rom_vars.AllowCollectVar.get())
 | |
|         guiargs.baserom = romVar.get()
 | |
|         if isinstance(rom_vars.sprite, Sprite):
 | |
|             guiargs.sprite = rom_vars.sprite.name
 | |
|         else:
 | |
|             guiargs.sprite = rom_vars.sprite
 | |
|         guiargs.sprite_pool = rom_vars.sprite_pool
 | |
|         persistent_store("adjuster", GAME_ALTTP, guiargs)
 | |
|         messagebox.showinfo(title="Success", message="Settings saved to persistent storage")
 | |
| 
 | |
|     adjustButton = Button(bottomFrame2, text='Adjust Rom', command=adjustRom)
 | |
|     rom_options_frame.pack(side=TOP)
 | |
|     adjustButton.pack(side=LEFT, padx=(5,5))
 | |
| 
 | |
|     saveButton = Button(bottomFrame2, text='Save Settings', command=saveGUISettings)
 | |
|     saveButton.pack(side=LEFT, padx=(5,5))
 | |
| 
 | |
|     bottomFrame2.pack(side=TOP, pady=(5,5))
 | |
| 
 | |
|     tkinter_center_window(adjustWindow)
 | |
|     adjustWindow.mainloop()
 | |
| 
 | |
| 
 | |
| def run_sprite_update():
 | |
|     import threading
 | |
|     done = threading.Event()
 | |
|     try:
 | |
|         top = Tk()
 | |
|     except:
 | |
|         task = BackgroundTaskProgressNullWindow(update_sprites, lambda successful, resultmessage: done.set())
 | |
|     else:
 | |
|         top.withdraw()
 | |
|         task = BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
 | |
|     while not done.is_set():
 | |
|         task.do_events()
 | |
|     logging.info("Done updating sprites")
 | |
| 
 | |
| 
 | |
| def update_sprites(task, on_finish=None):
 | |
|     resultmessage = ""
 | |
|     successful = True
 | |
|     sprite_dir = user_path("data", "sprites", "alttpr")
 | |
|     os.makedirs(sprite_dir, exist_ok=True)
 | |
|     ctx = get_cert_none_ssl_context()
 | |
| 
 | |
|     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', context=ctx) 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, context=ctx) 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))
 | |
| 
 | |
|     def do_events(self):
 | |
|         self.parent.update()
 | |
| 
 | |
|     # only call this in an event callback
 | |
|     def close_window(self):
 | |
|         self.stop()
 | |
|         self.window.destroy()
 | |
| 
 | |
| 
 | |
| class BackgroundTaskProgressNullWindow(BackgroundTask):
 | |
|     def __init__(self, code_to_run, *args):
 | |
|         super().__init__(None, code_to_run, *args)
 | |
| 
 | |
|     def process_queue(self):
 | |
|         try:
 | |
|             while True:
 | |
|                 if not self.running:
 | |
|                     return
 | |
|                 event = self.queue.get_nowait()
 | |
|                 event()
 | |
|         except queue.Empty:
 | |
|             pass
 | |
| 
 | |
|     def do_events(self):
 | |
|         self.process_queue()
 | |
| 
 | |
|     def update_status(self, text):
 | |
|         self.queue_event(lambda: logging.info(text))
 | |
| 
 | |
|     def close_window(self):
 | |
|         self.stop()
 | |
| 
 | |
| 
 | |
| def get_rom_frame(parent=None):
 | |
|     adjuster_settings = get_adjuster_settings(GAME_ALTTP)
 | |
|     if not adjuster_settings:
 | |
|         adjuster_settings = Namespace()
 | |
|         adjuster_settings.baserom = "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"
 | |
| 
 | |
|     romFrame = Frame(parent)
 | |
|     baseRomLabel = Label(romFrame, text='LttP Base Rom: ')
 | |
|     romVar = StringVar(value=adjuster_settings.baserom)
 | |
|     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):
 | |
|     adjuster_settings = get_adjuster_settings(GAME_ALTTP)
 | |
|     defaults = {
 | |
|         "auto_apply": 'ask',
 | |
|         "music": True,
 | |
|         "reduceflashing": True,
 | |
|         "deathlink": False,
 | |
|         "sprite": None,
 | |
|         "quickswap": True,
 | |
|         "menuspeed": 'normal',
 | |
|         "heartcolor": 'red',
 | |
|         "heartbeep": 'normal',
 | |
|         "ow_palettes": 'default',
 | |
|         "uw_palettes": 'default',
 | |
|         "hud_palettes": 'default',
 | |
|         "sword_palettes": 'default',
 | |
|         "shield_palettes": 'default',
 | |
|         "sprite_pool": [],
 | |
|         "allowcollect": False,
 | |
|     }
 | |
|     if not adjuster_settings:
 | |
|         adjuster_settings = Namespace()
 | |
|     for key, defaultvalue in defaults.items():
 | |
|         if not hasattr(adjuster_settings, key):
 | |
|             setattr(adjuster_settings, key, defaultvalue)
 | |
| 
 | |
|     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(adjuster_settings.music)
 | |
|     MusicCheckbutton = Checkbutton(romOptionsFrame, text="Music", variable=vars.MusicVar)
 | |
|     MusicCheckbutton.grid(row=0, column=0, sticky=E)
 | |
| 
 | |
|     vars.disableFlashingVar = IntVar(value=adjuster_settings.reduceflashing)
 | |
|     disableFlashingCheckbutton = Checkbutton(romOptionsFrame, text="Disable flashing (anti-epilepsy)",
 | |
|                                              variable=vars.disableFlashingVar)
 | |
|     disableFlashingCheckbutton.grid(row=6, column=0, sticky=W)
 | |
| 
 | |
|     vars.DeathLinkVar = IntVar(value=adjuster_settings.deathlink)
 | |
|     DeathLinkCheckbutton = Checkbutton(romOptionsFrame, text="DeathLink (Team Deaths)", variable=vars.DeathLinkVar)
 | |
|     DeathLinkCheckbutton.grid(row=7, column=0, sticky=W)
 | |
| 
 | |
|     vars.AllowCollectVar = IntVar(value=adjuster_settings.allowcollect)
 | |
|     AllowCollectCheckbutton = Checkbutton(romOptionsFrame, text="Allow Collect", variable=vars.AllowCollectVar)
 | |
|     AllowCollectCheckbutton.grid(row=8, column=0, sticky=W)
 | |
| 
 | |
|     spriteDialogFrame = Frame(romOptionsFrame)
 | |
|     spriteDialogFrame.grid(row=0, column=1)
 | |
|     baseSpriteLabel = Label(spriteDialogFrame, text='Sprite:')
 | |
| 
 | |
|     vars.spriteNameVar = StringVar()
 | |
|     vars.sprite = adjuster_settings.sprite
 | |
| 
 | |
|     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(adjuster_settings.sprite)
 | |
|     #vars.spriteNameVar.set(adjuster_settings.sprite)
 | |
|     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=adjuster_settings.quickswap)
 | |
|     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(adjuster_settings.menuspeed)
 | |
|     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(adjuster_settings.heartcolor)
 | |
|     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(adjuster_settings.heartbeep)
 | |
|     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(adjuster_settings.ow_palettes)
 | |
|     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(adjuster_settings.uw_palettes)
 | |
|     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(adjuster_settings.hud_palettes)
 | |
|     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(adjuster_settings.sword_palettes)
 | |
|     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(adjuster_settings.shield_palettes)
 | |
|     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 = adjuster_settings.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(len(adjuster_settings.sprite_pool))
 | |
|     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)
 | |
| 
 | |
|     vars.auto_apply = StringVar(value=adjuster_settings.auto_apply)
 | |
|     autoApplyFrame = Frame(romOptionsFrame)
 | |
|     autoApplyFrame.grid(row=9, column=0, columnspan=2, sticky=W)
 | |
|     filler = Label(autoApplyFrame, text="Automatically apply last used settings on opening .aplttp files")
 | |
|     filler.pack(side=TOP, expand=True, fill=X)
 | |
|     askRadio = Radiobutton(autoApplyFrame, text='Ask', variable=vars.auto_apply, value='ask')
 | |
|     askRadio.pack(side=LEFT, padx=5, pady=5)
 | |
|     alwaysRadio = Radiobutton(autoApplyFrame, text='Always', variable=vars.auto_apply, value='always')
 | |
|     alwaysRadio.pack(side=LEFT, padx=5, pady=5)
 | |
|     neverRadio = Radiobutton(autoApplyFrame, text='Never', variable=vars.auto_apply, value='never')
 | |
|     neverRadio.pack(side=LEFT, padx=5, pady=5)
 | |
| 
 | |
|     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.invalid_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="Do not adjust sprite",command=self.use_default_sprite)
 | |
|         button.pack(side=LEFT,padx=(0,5))
 | |
| 
 | |
|         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()
 | |
|         self.randomOnAllVar = IntVar()
 | |
| 
 | |
|         if self.randomOnEvent:
 | |
|             self.buttonHit = Checkbutton(frame, text="Hit", command=self.update_random_button, variable=self.randomOnHitVar)
 | |
|             self.buttonHit.pack(side=LEFT, padx=(0, 5))
 | |
| 
 | |
|             self.buttonEnter = Checkbutton(frame, text="Enter", command=self.update_random_button, variable=self.randomOnEnterVar)
 | |
|             self.buttonEnter.pack(side=LEFT, padx=(0, 5))
 | |
| 
 | |
|             self.buttonExit = Checkbutton(frame, text="Exit", command=self.update_random_button, variable=self.randomOnExitVar)
 | |
|             self.buttonExit.pack(side=LEFT, padx=(0, 5))
 | |
| 
 | |
|             self.buttonSlash = Checkbutton(frame, text="Slash", command=self.update_random_button, variable=self.randomOnSlashVar)
 | |
|             self.buttonSlash.pack(side=LEFT, padx=(0, 5))
 | |
| 
 | |
|             self.buttonItem = Checkbutton(frame, text="Item", command=self.update_random_button, variable=self.randomOnItemVar)
 | |
|             self.buttonItem.pack(side=LEFT, padx=(0, 5))
 | |
| 
 | |
|             self.buttonBonk = Checkbutton(frame, text="Bonk", command=self.update_random_button, variable=self.randomOnBonkVar)
 | |
|             self.buttonBonk.pack(side=LEFT, padx=(0, 5))
 | |
| 
 | |
|             self.buttonRandom = Checkbutton(frame, text="Random", command=self.update_random_button, variable=self.randomOnRandomVar)
 | |
|             self.buttonRandom.pack(side=LEFT, padx=(0, 5))
 | |
| 
 | |
|             self.buttonAll = Checkbutton(frame, text="All", command=self.update_random_button, variable=self.randomOnAllVar)
 | |
|             self.buttonAll.pack(side=LEFT, padx=(0, 5))
 | |
| 
 | |
|         set_icon(self.window)
 | |
|         self.window.focus()
 | |
|         tkinter_center_window(self.window)
 | |
| 
 | |
|         if self.invalid_sprites:
 | |
|             invalid = sorted(self.invalid_sprites)
 | |
|             logging.warning(f"The following sprites are invalid: {', '.join(invalid)}")
 | |
|             msg = f"{invalid[0]} "
 | |
|             msg += f"and {len(invalid)-1} more are invalid" if len(invalid) > 1 else "is invalid"
 | |
|             messagebox.showerror("Invalid sprites detected", msg, parent=self.window)
 | |
| 
 | |
|     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):
 | |
|             if file == '.gitignore':
 | |
|                 continue
 | |
|             sprite = Sprite(os.path.join(path, file))
 | |
|             if sprite.valid:
 | |
|                 sprites.append((file, sprite))
 | |
|             else:
 | |
|                 self.invalid_sprites.append(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.randomOnAllVar.get():
 | |
|             randomon = "all"
 | |
|             self.buttonHit.config(state=DISABLED)
 | |
|             self.buttonEnter.config(state=DISABLED)
 | |
|             self.buttonExit.config(state=DISABLED)
 | |
|             self.buttonSlash.config(state=DISABLED)
 | |
|             self.buttonItem.config(state=DISABLED)
 | |
|             self.buttonBonk.config(state=DISABLED)
 | |
|             self.buttonRandom.config(state=DISABLED)
 | |
|         elif self.randomOnRandomVar.get():
 | |
|             randomon = "random"
 | |
|             self.buttonHit.config(state=DISABLED)
 | |
|             self.buttonEnter.config(state=DISABLED)
 | |
|             self.buttonExit.config(state=DISABLED)
 | |
|             self.buttonSlash.config(state=DISABLED)
 | |
|             self.buttonItem.config(state=DISABLED)
 | |
|             self.buttonBonk.config(state=DISABLED)
 | |
|         else:
 | |
|             self.buttonHit.config(state=NORMAL)
 | |
|             self.buttonEnter.config(state=NORMAL)
 | |
|             self.buttonExit.config(state=NORMAL)
 | |
|             self.buttonSlash.config(state=NORMAL)
 | |
|             self.buttonItem.config(state=NORMAL)
 | |
|             self.buttonBonk.config(state=NORMAL)
 | |
|             self.buttonRandom.config(state=NORMAL)
 | |
|             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 user_path("data", "sprites", "alttpr")
 | |
| 
 | |
|     @property
 | |
|     def custom_sprite_dir(self):
 | |
|         return user_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()
 | 
