import json import os import random import shutil import ssl import webbrowser from glob import glob from tkinter import ( ALL, BOTTOM, EW, LEFT, NS, RIGHT, TOP, Button, Canvas, Frame, Label, LabelFrame, PhotoImage, Scrollbar, Toplevel, X, filedialog, messagebox, ) from urllib.parse import urlparse from urllib.request import urlopen from GuiUtils import BackgroundTaskProgress, ToolTips, set_icon from Rom import Sprite from Utils import is_bundled, local_path, open_file, output_path class SpriteSelector(object): def __init__(self, parent, callback, adjuster=False): if is_bundled(): self.deploy_icons() self.parent = parent self.window = Toplevel(parent) self.window.geometry("800x650") self.sections = [] self.callback = callback self.adjuster = adjuster self.window.wm_title("TAKE ANY ONE YOU WANT") self.window['padx'] = 5 self.window['pady'] = 5 self.all_sprites = [] def open_official_sprite_listing(_evt): webbrowser.open("http://alttpr.com/sprite_preview") def open_official_sprite_dir(_evt): if not os.path.isdir(self.official_sprite_dir): os.makedirs(self.official_sprite_dir) open_file(self.official_sprite_dir) def open_unofficial_sprite_dir(_evt): if not os.path.isdir(self.unofficial_sprite_dir): os.makedirs(self.unofficial_sprite_dir) open_file(self.unofficial_sprite_dir) # Open SpriteSomething directory for Link sprites def open_spritesomething_listing(_evt): webbrowser.open("https://miketrethewey.github.io/SpriteSomething-collections/snes/zelda3/link/") official_frametitle = Frame(self.window) official_title_text = Label(official_frametitle, text="Official Sprites") official_title_text.pack(side=LEFT) official_local_title_link = Label(official_frametitle, text="(open local)", fg="blue", cursor="hand2") official_local_title_link.pack(side=LEFT) official_local_title_link.bind("", open_official_sprite_dir) official_title_link = Label(official_frametitle, text="(ALttPR)", fg="blue", cursor="hand2") official_title_link.pack(side=LEFT) official_title_link.bind("", open_official_sprite_listing) unofficial_frametitle = Frame(self.window) unofficial_title_text = Label(unofficial_frametitle, text="Unofficial Sprites") unofficial_title_link = Label(unofficial_frametitle, text="(open local)", fg="blue", cursor="hand2") unofficial_title_text.pack(side=LEFT) unofficial_title_link.pack(side=LEFT) unofficial_title_link.bind("", open_unofficial_sprite_dir) # Include hyperlink to SpriteSomething directory for Link sprites spritesomething_title_link = Label(unofficial_frametitle, text="(SpriteSomething)", fg="blue", cursor="hand2") spritesomething_title_link.pack(side=LEFT) spritesomething_title_link.bind("", open_spritesomething_listing) self.icon_section(official_frametitle, os.path.join(self.official_sprite_dir,"*"), 'Official sprites not found. Click "Update official sprites" to download them.') self.icon_section(unofficial_frametitle, os.path.join(self.unofficial_sprite_dir,"*"), 'Put sprites in the unofficial sprites folder (see open link above) to have them appear here.') frame = Frame(self.window) frame.pack(side=BOTTOM, fill=X, pady=5) button = Button(frame, text="Browse for file...", command=self.browse_for_sprite) button.pack(side=RIGHT, padx=(5, 0)) button = Button(frame, text="Update official sprites", command=self.update_official_sprites) button.pack(side=RIGHT, padx=(5, 0)) button = Button(frame, text="Default Link sprite", command=self.use_default_link_sprite) button.pack(side=LEFT, padx=(0, 5)) button = Button(frame, text="Random sprite", command=self.use_random_sprite) button.pack(side=LEFT, padx=(0, 5)) if adjuster: button = Button(frame, text="Current sprite from rom", command=self.use_default_sprite) button.pack(side=LEFT, padx=(0, 5)) set_icon(self.window) self.window.focus() def icon_section(self, frame_label, path, no_results_label): frame = LabelFrame(self.window, labelwidget=frame_label, padx=5, pady=5) canvas = Canvas(frame, borderwidth=0) y_scrollbar = Scrollbar(frame, orient="vertical", command=canvas.yview) y_scrollbar.pack(side="right", fill="y") content_frame = Frame(canvas) canvas.pack(side="left", fill="both", expand=True) canvas.create_window((4, 4), window=content_frame, anchor="nw") canvas.configure(yscrollcommand=y_scrollbar.set) def onFrameConfigure(canvas): """Reset the scroll region to encompass the inner frame""" canvas.configure(scrollregion=canvas.bbox("all")) content_frame.bind("", lambda event, canvas=canvas: onFrameConfigure(canvas)) frame.pack(side=TOP, fill=X) sprites = [] for file in glob(path): sprites.append(Sprite(file)) sprites.sort(key=lambda s: str.lower(s.name or "").strip()) i = 0 for sprite in sprites: image = get_image_for_sprite(sprite) if image is None: continue self.all_sprites.append(sprite) button = Button(content_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 "")) button.image = image button.grid(row=i // 16, column=i % 16) i += 1 if i == 0: label = Label(content_frame, text=no_results_label) label.pack() def update_official_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 work(task): resultmessage = "" successful = True def finished(): task.close_window() if successful: messagebox.showinfo("Sprite Updater", resultmessage) else: messagebox.showerror("Sprite Updater", resultmessage) SpriteSelector(self.parent, self.callback, self.adjuster) try: task.update_status("Downloading official sprites list") with urlopen('https://alttpr.com/sprites', context=ssl._create_unverified_context()) as response: sprites_arr = json.loads(response.read().decode("utf-8")) except Exception as e: resultmessage = "Error getting list of official 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(os.path.join(self.official_sprite_dir,"*"))] official_sprites = [(sprite['file'], os.path.basename(urlparse(sprite['file']).path)) for sprite in sprites_arr] needed_sprites = [(sprite_url, filename) for (sprite_url, filename) in official_sprites if filename not in current_sprites and filename != "001.link.1.zspr"] bundled_sprites = [os.path.basename(file) for file in glob(os.path.join(self.unofficial_sprite_dir,"*"))] # todo: eventually use the above list to avoid downloading any sprites that we already have cached in the bundle. official_filenames = [filename for (_, filename) in official_sprites] obsolete_sprites = [sprite for sprite in current_sprites if sprite not in official_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 updated = 0 for (sprite_url, filename) in needed_sprites: try: task.update_status("Downloading needed sprite %g/%g" % (updated + 1, len(needed_sprites))) target = os.path.join(self.official_sprite_dir, filename) with urlopen(sprite_url) as response, open(target, 'wb') as out: shutil.copyfileobj(response, out) except Exception as e: resultmessage = "Error downloading sprite. Not all sprites updated.\n\n%s: %s" % (type(e).__name__, e) successful = False updated += 1 deleted = 0 for sprite in obsolete_sprites: try: task.update_status("Removing obsolete sprite %g/%g" % (deleted + 1, len(obsolete_sprites))) os.remove(os.path.join(self.official_sprite_dir, sprite)) except Exception as e: resultmessage = "Error removing obsolete sprite. Not all sprites updated.\n\n%s: %s" % (type(e).__name__, e) successful = False deleted += 1 if successful: resultmessage = "official sprites updated successfully" task.queue_event(finished) BackgroundTaskProgress(self.parent, work, "Updating Sprites") 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, False) self.window.destroy() def use_default_link_sprite(self): self.callback(Sprite.default_link_sprite(), False) self.window.destroy() def use_random_sprite(self): self.callback(random.choice(self.all_sprites) if self.all_sprites else None, True) self.window.destroy() def select_sprite(self, spritename): self.callback(spritename, False) self.window.destroy() def deploy_icons(self): if not os.path.exists(self.unofficial_sprite_dir): os.makedirs(self.unofficial_sprite_dir) if not os.path.exists(self.official_sprite_dir): shutil.copytree(self.local_official_sprite_dir, self.official_sprite_dir) @property def official_sprite_dir(self): return self.local_official_sprite_dir @property def local_official_sprite_dir(self): return local_path(os.path.join("data","sprites","official")) @property def unofficial_sprite_dir(self): return self.local_unofficial_sprite_dir @property def local_unofficial_sprite_dir(self): return local_path(os.path.join("data","sprites","unofficial")) def get_image_for_sprite(sprite): 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) image = PhotoImage(data=gif_data) return image.zoom(2)