mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 04:01:32 -06:00

* First Pass removal of game-specific code * SMW, DKC3, and SM hooked into AutoClient * All SNES autoclients functional * Fix ALttP Deathlink * Don't default to being ALttP, and properly error check ctx.game * Adjust variable naming * In response to: > we should probably document usage somewhere. I'm open to suggestions of where this should be documented. I think the most valuable documentation for APIs is docstrings and full typing. about websockets change in imports - from websockets documentation: > For convenience, many public APIs can be imported from the websockets package. However, this feature is incompatible with static code analysis. It breaks autocompletion in an IDE or type checking with mypy. If you’re using such tools, use the real import paths. * todo note for python 3.11 typing.NotRequired * missed staging in previous commit * added missing death Game States for DeathLink Co-authored-by: beauxq <beauxq@users.noreply.github.com> Co-authored-by: lordlou <87331798+lordlou@users.noreply.github.com>
1284 lines
52 KiB
Python
1284 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
|
|
|
|
|
|
GAME_ALTTP = "A Link to the Past"
|
|
|
|
|
|
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()
|