#!/usr/bin/env python3 # File: srb_tk.py # Location: https://bgstack15.ddns.net/cgit/srb_lib # Author: bgstack15 # SPDX-License-Identifier: GPL-3.0-only # Startdate: 2024-03-28-5 13:43 # Title: Graphical frontend for savegame editor for Snoopy vs. the Red Baron # Project: srb_lib # Purpose: Savegame editor graphical frontend for srb_lib # History: # Usage: # ./srb_tk.py # References: # logout-manager-tcl, lmlib.py # https://likegeeks.com/python-gui-examples-tkinter-tutorial/#Add_a_Menu_bar # https://www.tcl.tk/man/tcl8.6/TkCmd/bind.htm from https://www.tcl.tk/man/tcl8.6/TkCmd/contents.htm # https://www.reddit.com/r/learnpython/comments/mavinn/tkinter_remove_optionmenu_icon/ # https://stackoverflow.com/questions/69806038/navigate-to-tkinter-optionmenu-with-tab-key/69806039#69806039 # icon courtesy of https://icons8.com/icon/9336/aircraft # possible way to have a widget store its variable: https://stackoverflow.com/questions/63871376/tkinter-widget-cgetvariable # count number of 1s in a binary value https://stackoverflow.com/questions/8871204/count-number-of-1s-in-binary-representation/71307775#71307775 # https://stackoverflow.com/questions/67334913/can-i-possibly-put-an-image-label-into-an-option-box-in-tkinter # haybale Hay Bale icon by Icons8 # barrel Wooden Beer Keg icon by Icons8 # geyser Geyser icon by Icons8 # pumpkin Pumpkin icon by Icons8 # battery Battery icon by Icons8 # transparent image https://stackoverflow.com/questions/8376359/how-to-create-a-transparent-gif-or-png-with-pil-python-imaging # combine images https://stackoverflow.com/questions/30227466/combine-several-images-horizontally-with-python/30228308#30228308 # Improve: # Dependencies: # dep-devuan: python3-tkinter, python3-pil.imagetk # rec-devuan: python3-cairosvg # Documentation: # spinbox appears to way too wide by default, even without a .grid(sticky="EW"). Try defining on the spinbox object at instantiation width=3 import tkinter as tk from pathlib import Path import tkinter.filedialog, srb_lib, os, time, sys import stackrpms_tk_lib as stk from PIL import Image, ImageTk srb_tk_dir = os.path.split(os.path.abspath(__file__))[0] image_dir = os.path.join(srb_tk_dir,"images") # make this configurable? IMAGES = True IMAGE_FAILURES = 0 ABOUT_TEXT = """ srb_tk \"Savegame Editor for Snoopy vs. the Red Baron\" (C) 2024 bgstack15 SPDX-License-Identifier: GPL-3.0-only Icon courtesy of https://icons8.com/icon/9336/aircraft """ def get_srb_image(filename, sizex, sizey, resample=Image.Resampling.LANCZOS, photoImage = True): """ helper function that returns an ImageTk from the named file from the configured images directory, resized for you. """ global IMAGE_FAILURES if IMAGES and IMAGE_FAILURES < 5: try: img = Image.open(os.path.join(image_dir,filename)).resize((sizex,sizey),resample=resample) if photoImage: return ImageTk.PhotoImage(img) else: return img except: IMAGE_FAILURES += 1 print(f"Failed to load image {filename}, only {5-IMAGE_FAILURES} failures left before giving up on images entirely.",file=sys.stderr) return stk.empty_photoimage(sizex) else: return stk.empty_photoimage(sizex) class App(tk.Frame): def __init__(self, master): super().__init__(master) self.master.title("Savegame editor for SRB") self.bdata = None menu = tk.Menu(self.master) self.master.config(menu=menu) menu_file = tk.Menu(menu,tearoff=0) menu_file.add_command(label="Open...", command=self.func_open, underline=0) menu_file.add_command(label="Reload current file", command=self.func_reload, underline=0) menu_file.add_command(label="Save", command=self.func_save, underline=0) menu_file.add_command(label="Save as...", command=self.func_save_as, underline=5) menu_file.add_separator() menu_file.add_command(label="Exit", command=self.func_exit, underline=1) menu.add_cascade(label="File",menu=menu_file, underline=0) menu_help = tk.Menu(menu,tearoff=0) menu_help.add_command(label="About", command=self.func_about, underline=0) menu.add_cascade(label="Help",menu=menu_help,underline=0) self.grid() # use this geometry manager instead of pack() # app icon #imgicon = stk.get_scaled_icon("hexedit",24,"Numix-Circle", "","apps") #imgicon = stk.get_scaled_icon(os.path.join(os.getenv("HOME"),"Downloads/snoopy.png"),24,"Numix-Circle", "","apps") imgicon = stk.get_scaled_icon(os.path.join(image_dir,"srb.png"),24,"default", "","apps") self.master.tk.call("wm","iconphoto",self.master._w,imgicon) # app variables self.silent = tk.BooleanVar(value=True) self.frame_backgrounds = tk.BooleanVar(value=False) self.frame_backgrounds.trace("w",self.set_frame_backgrounds) # data variables # Do not use traces for most things, because it will catch itself in a loop. Instead, use the bind on the form fields. self.current_file = tk.StringVar(value = os.path.join(os.getenv("HOME"),"Documents","Snoopy vs. the Red Baron","Profile 1","Profile 1.sav")) #print(f"beginning: {self.current_file.get()}") self.checksum = tk.IntVar() self.selected_profile = tk.IntVar(value=1) self.prof1_used = tk.BooleanVar() self.prof1name = tk.StringVar() self.prof1tut = tk.BooleanVar() self.prof2_used = tk.BooleanVar() self.prof2name = tk.StringVar() self.prof2tut = tk.BooleanVar() self.prof3_used = tk.BooleanVar() self.prof3name = tk.StringVar() self.prof3tut = tk.BooleanVar() # current-profile form: self.money = tk.IntVar() self.equipped_weapon = tk.StringVar() self.health = tk.IntVar() self.stunt = tk.IntVar() self.gun = tk.IntVar() self.pur_weap_potato = tk.BooleanVar() self.pur_weap_stinger = tk.BooleanVar() self.pur_weap_watball = tk.BooleanVar() self.pur_weap_snow = tk.BooleanVar() self.pur_weap_fire = tk.BooleanVar() self.pur_weap_lightn = tk.BooleanVar() self.pur_weap_romanc = tk.BooleanVar() self.pur_weap_pumpkin = tk.BooleanVar() # these strings will hold the uppercase-when-collected letters, and be used by the checkboxes for the purchasable planes. self.pur_plane_str_marcie = tk.StringVar(value="marcie") self.pur_plane_bool_marcie = tk.BooleanVar() self.pur_plane_str_sally = tk.StringVar(value="sally") self.pur_plane_bool_sally = tk.BooleanVar() self.pur_plane_str_rerun = tk.StringVar(value="rerun") self.pur_plane_bool_rerun = tk.BooleanVar() self.pur_plane_str_pigpen = tk.StringVar(value="pigpen") self.pur_plane_bool_pigpen = tk.BooleanVar() self.pur_plane_str_woodstock = tk.StringVar(value="woodstock") self.pur_plane_bool_woodstock = tk.BooleanVar() self.pur_plane_str_baron = tk.StringVar(value="baron") self.pur_plane_bool_baron = tk.BooleanVar() # levelsets and levels self.levelset_status_ints = [] self.levelset_balloons_ints = [] for i in range(0,len(srb_lib.LEVELSETS)): self.levelset_status_ints.append(tk.StringVar()) # will hold the status, as in completed mission mask self.levelset_balloons_ints.append(tk.IntVar()) # will hold the number of balloons for the levelset self.level_strs = [] self.level_statuses_hex = [] self.level_statuses = [] self.level_balloons = [] self.level_letters = [] statuses = [i["name"] for i in srb_lib.LEVEL_STATUSES] statuses_hex = [i["b"] for i in srb_lib.LEVEL_STATUSES] for i in range(0,len(srb_lib.LEVELS)): self.level_strs.append(tk.StringVar(value=srb_lib.get_level_info(i)[0]["name"])) self.level_statuses_hex.append(tk.StringVar(value=statuses_hex[0])) self.level_statuses.append(tk.StringVar(value=statuses[0])) self.level_balloons.append(tk.IntVar()) self.level_letters.append(tk.IntVar()) # END DATA VARIABLES # statusbar variable self.statustext = tk.StringVar() # we use this variable later to set "transparent" color on certain widgets. self.background_color = self.master.cget("bg") # app label, for vanity self.lbl_app = tk.Label(self.master,text="Savegame editor for Snoopy vs. the Red Baron") self.lbl_app.grid(row=0,column=0,columnspan=2) stk.Tooltip(self.lbl_app,text="Also checkout the cli with ./srb.py") tk.Label(self.master,text="Checksum",underline=0).grid(row=1,column=1) tk.Entry(state="readonly",textvariable=self.checksum).grid(row=1,column=0) tk.Checkbutton(self.master,text="silent",variable=self.silent).grid(row=2,column=0) tk.Checkbutton(self.master,text="frame colors",variable=self.frame_backgrounds).grid(row=3,column=0) self.frm_profiles = tk.Frame(self.master) self.frm_profiles.grid(row=4,column=0,columnspan=2) # investigate radio button key-bind, up-down, to increment/decrement choice? tk.Label(self.frm_profiles,text="Used").grid(row=0,column=2) tk.Label(self.frm_profiles,text="Tutorial").grid(row=0,column=3) stk.Radiobutton(self.frm_profiles,value=1,text="Profile 1",variable=self.selected_profile,underline=8).grid(row=1,column=0) tk.Checkbutton(self.frm_profiles,variable=self.prof1_used).grid(row=1,column=2) stk.Checkbutton(self.frm_profiles,variable=self.prof1tut,func=self.load_form_into_data).grid(row=1,column=3) self.ent_prof1name = stk.Entry(self.frm_profiles,textvariable=self.prof1name,func=self.load_form_into_data,width=11) self.ent_prof1name.grid(row=1,column=4) stk.Radiobutton(self.frm_profiles,value=2,text="Profile 2",variable=self.selected_profile,underline=8).grid(row=2,column=0) tk.Checkbutton(self.frm_profiles,variable=self.prof2_used).grid(row=2,column=2) stk.Checkbutton(self.frm_profiles,variable=self.prof2tut,func=self.load_form_into_data).grid(row=2,column=3) self.ent_prof2name = stk.Entry(self.frm_profiles,textvariable=self.prof2name,func=self.load_form_into_data,width=11) self.ent_prof2name.grid(row=2,column=4) stk.Radiobutton(self.frm_profiles,value=3,text="Profile 3",variable=self.selected_profile,underline=8).grid(row=3,column=0) tk.Checkbutton(self.frm_profiles,variable=self.prof3_used).grid(row=3,column=2) stk.Checkbutton(self.frm_profiles,variable=self.prof3tut,func=self.load_form_into_data).grid(row=3,column=3) self.ent_prof3name = stk.Entry(self.frm_profiles,textvariable=self.prof3name,func=self.load_form_into_data,width=11) self.ent_prof3name.grid(row=3,column=4) #self.selected_profile.set(1) # purposefully do not set this value. # one profile, so we will have to use the value of self.selected_profile self.frm_curprof = tk.Frame(self.master) self.frm_curprof.grid(row=5,column=0,rowspan=22,columnspan=2) # most form fields are inside the current profile self.img_money = get_srb_image("money.png",20,16) tk.Label(self.frm_curprof,text="Money",image=self.img_money,compound="right").grid(row=0,column=0) stk.Entry(self.frm_curprof,textvariable=self.money,func=self.load_form_into_data).grid(row=0,column=1) tk.Label(self.frm_curprof,text="Equipped weapon",compound="right").grid(row=1,column=0) # list of names equippable_weapons = [i["name"] for i in srb_lib.WEAPONS if i["e"]] self.img_equ_weapon = get_srb_image("balloon-none.png",16,16) self.opt_equ_weapon = tk.OptionMenu(self.frm_curprof,self.equipped_weapon,*equippable_weapons) # the indicatoron,padx,pady and grid sticky are incredibly useful and hard to find in the docs self.opt_equ_weapon.config(indicatoron=False,padx=1,pady=1,takefocus=1,compound="left") self.opt_equ_weapon.grid(row=1,column=1,sticky="ew") ow = self.opt_equ_weapon.nametowidget(self.opt_equ_weapon.menuname) self.img_weapons = [] for i in equippable_weapons: self.img_weapons.append(get_srb_image(i+".png",16,16)) ow.entryconfigure(i,image=self.img_weapons[-1],compound="left") self.img_health = get_srb_image("health.png",16,16) tk.Label(self.frm_curprof,text="Health",image=self.img_health,compound="right").grid(row=2,column=0) stk.Spinbox(self.frm_curprof,from_=1,to=4,textvariable=self.health,width=2).grid(row=2,column=1) self.img_stunt = get_srb_image("stunt.png",16,16) tk.Label(self.frm_curprof,text="Stunt",image=self.img_stunt,compound="right").grid(row=3,column=0) stk.Spinbox(self.frm_curprof,from_=1,to=4,textvariable=self.stunt,width=2).grid(row=3,column=1) self.img_gun = get_srb_image("gun.png",19,16) tk.Label(self.frm_curprof,text="Gun",image=self.img_gun,compound="right").grid(row=4,column=0) stk.Spinbox(self.frm_curprof,from_=1,to=5,textvariable=self.gun,width=2).grid(row=4,column=1) tk.Label(self.frm_curprof,text="Purchased: Weapons").grid(row=5,column=0) tk.Label(self.frm_curprof,text="Planes").grid(row=5,column=1) self.img_potato = get_srb_image("Potato Gun.png",16,16) stk.Checkbutton(self.frm_curprof,text="Potato Gun",variable=self.pur_weap_potato,func=self.load_form_into_data,image=self.img_potato,compound="left").grid(row=6,column=0,sticky="W") self.img_stinger = get_srb_image("Stinger.png",16,16) stk.Checkbutton(self.frm_curprof,text="Stinger",variable=self.pur_weap_stinger,func=self.load_form_into_data,image=self.img_stinger,compound="left").grid(row=7,column=0,sticky="W") self.img_watball = get_srb_image("Water Balloon Cannon.png",12,16) stk.Checkbutton(self.frm_curprof,text="Water Balloon Cannon",variable=self.pur_weap_watball,func=self.load_form_into_data,image=self.img_watball,compound="left").grid(row=8,column=0,sticky="W") self.img_snow = get_srb_image("Snow Blower.png",16,16) stk.Checkbutton(self.frm_curprof,text="Snow Blower",variable=self.pur_weap_snow,func=self.load_form_into_data,image=self.img_snow,compound="left").grid(row=9,column=0,sticky="W") self.img_fire = get_srb_image("Fire Boomerang.png",12,16) stk.Checkbutton(self.frm_curprof,text="Fire Boomerang",variable=self.pur_weap_fire,func=self.load_form_into_data,image=self.img_fire,compound="left").grid(row=10,column=0,sticky="W") self.img_lightn = get_srb_image("Lightning Rod.png",16,16) stk.Checkbutton(self.frm_curprof,text="Lightning Rod",variable=self.pur_weap_lightn,func=self.load_form_into_data,image=self.img_lightn,compound="left").grid(row=11,column=0,sticky="W") self.img_romanc = get_srb_image("Roman Candles.png",16,16) stk.Checkbutton(self.frm_curprof,text="Roman Candles",variable=self.pur_weap_romanc,func=self.load_form_into_data,image=self.img_romanc,compound="left").grid(row=12,column=0,sticky="W") self.img_pumpkin = get_srb_image("10 Gauge Pumpkin.png",16,16) stk.Checkbutton(self.frm_curprof,text="10 Gauge Pumpkin",variable=self.pur_weap_pumpkin,func=self.load_form_into_data,image=self.img_pumpkin,compound="left").grid(row=13,column=0,sticky="W") stk.Checkbutton(self.frm_curprof,textvariable=self.pur_plane_str_marcie,variable=self.pur_plane_bool_marcie,func=self.load_form_into_data).grid(row=6,column=1,sticky="W") stk.Checkbutton(self.frm_curprof,textvariable=self.pur_plane_str_sally,variable=self.pur_plane_bool_sally,func=self.load_form_into_data).grid(row=7,column=1,sticky="W") stk.Checkbutton(self.frm_curprof,textvariable=self.pur_plane_str_rerun,variable=self.pur_plane_bool_rerun,func=self.load_form_into_data).grid(row=8,column=1,sticky="W") stk.Checkbutton(self.frm_curprof,textvariable=self.pur_plane_str_pigpen,variable=self.pur_plane_bool_pigpen,func=self.load_form_into_data).grid(row=9,column=1,sticky="W") stk.Checkbutton(self.frm_curprof,textvariable=self.pur_plane_str_woodstock,variable=self.pur_plane_bool_woodstock,func=self.load_form_into_data).grid(row=10,column=1,sticky="W") stk.Checkbutton(self.frm_curprof,textvariable=self.pur_plane_str_baron,variable=self.pur_plane_bool_baron,func=self.load_form_into_data).grid(row=11,column=1,sticky="W") # level and levelsets self.frm_levels = tk.Frame(self.master) self.frm_levels.grid(row=0,column=3,rowspan=len(srb_lib.LEVELS)+4) self.lbl_levelsets = [] self.spn_levelsets = [] self.ent_levelsets_balloons = [] tk.Label(self.frm_levels,text="Billboard/Completed").grid(row=0,column=0,sticky="EW",columnspan=4) tk.Label(self.frm_levels,text="Level").grid(row=0,column=5,sticky="EW") tk.Label(self.frm_levels,text="Rank").grid(row=0,column=6,sticky="EW",columnspan=2) tk.Label(self.frm_levels,text="Balloons").grid(row=0,column=8,columnspan=4,sticky="EW") tk.Label(self.frm_levels,text="Letters").grid(row=0,column=12,columnspan=4,sticky="EW") tk.Label(self.frm_levels,text="Breakables").grid(row=0,column=16,columnspan=1,sticky="EW") self.lbl_levels = [] self.opt_levels_hex = [] self.opt_levels = [] self.ent_levels_balloons = [] self.ent_levels_letters = [] self.btn_balloons_none = [] self.btn_balloons_all = [] self.btn_letters_none = [] self.btn_letters_all = [] self.btn_levels_breakables = [] self.bim_levels_breakables = [] self.ttp_levels_breakables = [] self.img_balloons = [] self.img_breakables = [] self.BREAKABLES = sorted(srb_lib.BREAKABLES,key=lambda i: i["w"]) self.BREAKABLES = [b for b in self.BREAKABLES if b["w"] != 0] self.BREAKABLES_NAMES = [b["name"] for b in self.BREAKABLES if b["w"] != 0] for i in self.BREAKABLES: self.img_breakables.append(get_srb_image(os.path.join(image_dir,"breakables",i["name"]+".png"),16,16,photoImage=False)) self.img_breakables.append(get_srb_image(os.path.join(image_dir,"breakables","empty.png"),16,16,photoImage=False)) x = 1 for i in range(0,len(srb_lib.LEVELSETS)): tl = srb_lib.get_levelset_info(i)[0] if "c" not in tl or tl["c"] == "none": # this one will fail, but that is ok. there is no balloon color for the last levelset. self.img_balloons.append(get_srb_image("balloon-none.png",16,20)) elif tl["c"] and tl["c"] != "none": self.img_balloons.append(get_srb_image("balloon-" + tl["c"] + ".png",16,20)) #self.lbl_levelsets.append(tk.Label(self.frm_levels,text=tl["name"],image=self.img_balloons[-1],compound="right")) self.lbl_levelsets.append(tk.Label(self.frm_levels,text=tl["name"])) self.lbl_levelsets[i].grid(row=x,column=0,sticky="EW",columnspan=2) if "c" in tl and tl["c"] != "none": tk.Label(self.frm_levels,image=self.img_balloons[-1]).grid(row=x,column=2,columnspan=2) self.spn_levelsets.append(stk.Spinbox(self.frm_levels,textvariable=self.levelset_status_ints[i],from_=0,to=tl["l"],width=2)) self.spn_levelsets[i].grid(row=x+1,column=0) tk.Label(self.frm_levels,text="/ " + str(tl["l"])).grid(row=x+1,column=1,sticky="EW") # balloons per levelset if "c" in tl and tl["c"] != "none": self.ent_levelsets_balloons.append(tk.Entry(self.frm_levels,textvariable=self.levelset_balloons_ints[i],state="readonly",width=3)) self.ent_levelsets_balloons[-1].grid(row=x+1,column=2) tk.Label(self.frm_levels,text="/" + str(tl["b"])).grid(row=x+1,column=3,sticky="E") #print(f"For levelset {tl},") these_levels = [k for k in srb_lib.LEVELS if k["setid"] == tl["id"]] sorted(these_levels,key=lambda i: i["id"]) y = 0 for j in these_levels: #print(f"Process level {j}") self.lbl_levels.append(tk.Label(self.frm_levels,text=j["name"])) self.lbl_levels[-1].grid(row=x,column=5) self.opt_levels_hex.append(tk.Entry(self.frm_levels,state="readonly",textvariable=self.level_statuses_hex[j["id"]],width=2)) self.opt_levels_hex[-1].grid(row=x,column=6) self.opt_levels.append(tk.OptionMenu(self.frm_levels,self.level_statuses[j["id"]],*statuses)) self.opt_levels[-1].config(indicatoron=False,padx=0,pady=0,takefocus=1,width=8) self.opt_levels[-1].grid(row=x,column=7,sticky="EW") # only some levels have balloons if j["b"]: self.ent_levels_balloons.append(tk.Entry(self.frm_levels,state="readonly",textvariable=self.level_balloons[j["id"]],width=2)) self.ent_levels_balloons[-1].grid(row=x,column=8) tk.Label(self.frm_levels,text="/ " + str(j["b"])).grid(row=x,column=9) self.btn_balloons_none.append(tk.Button(self.frm_levels,text="none",padx=0,pady=0,command=lambda tl=j,action="none",ty="balloons": self.button_action_level(tl,action,ty))) self.btn_balloons_none[-1].grid(row=x,column=10) self.btn_balloons_all.append(tk.Button(self.frm_levels,text="all",padx=0,pady=0,command=lambda tl=j,action="all",ty="balloons": self.button_action_level(tl,action,ty))) self.btn_balloons_all[-1].grid(row=x,column=11) # every level has letters though self.ent_levels_letters.append(tk.Entry(self.frm_levels,state="readonly",textvariable=self.level_letters[j["id"]],width=2)) self.ent_levels_letters[-1].grid(row=x,column=12) tk.Label(self.frm_levels,text="/ " + str(len(str(j["l"])))).grid(row=x,column=13) self.btn_letters_none.append(tk.Button(self.frm_levels,text="none",padx=0,pady=0,command=lambda tl=j,action="none",ty="letters": self.button_action_level(tl,action,ty))) self.btn_letters_none[-1].grid(row=x,column=14) self.btn_letters_all.append(tk.Button(self.frm_levels,text="all",padx=0,pady=0,command=lambda tl=j,action="all",ty="letters": self.button_action_level(tl,action,ty))) self.btn_letters_all[-1].grid(row=x,column=15) # breakables if "r" in j and j["r"]: self.bim_levels_breakables.append(get_srb_image(os.path.join("breakables","empty.png"),16,16)) # until you apply an image, width of a widget measures approximate width of text characters self.btn_levels_breakables.append(tk.Button(self.frm_levels,padx=0,pady=0,width=2,compound="left",command=lambda tl=j: self.button_reset_level_breakables(tl))) if IMAGES: # and now all of a sudden it does pixels. self.btn_levels_breakables[-1].config(width=88,image=self.bim_levels_breakables[-1]) self.btn_levels_breakables[-1].grid(row=x,column=16) self.ttp_levels_breakables.append(stk.Tooltip(self.btn_levels_breakables[-1])) x += 1 # end for-j-in-levels x += 1 # end for-i-in-levelsets # status bar stk.StatusBar(self.master,var=self.statustext) # and now open the first file self.func_open(self.current_file.get()) def get_single_image_from_breakables(self,b_list): """ Given a list of breakable names, return a single image that has those items displayed. Fixed size of 80px x 16px. """ #print(f"debug: got list of {b_list}") if IMAGES: new_im = Image.new("RGBA",(16*5,16),(255,127,127,0)) x_offset = 0 for i in self.BREAKABLES_NAMES: using = i in b_list #print(f"For {i}, using {using}, index is {self.BREAKABLES_NAMES.index(i)}") this_img = self.img_breakables[self.BREAKABLES_NAMES.index(i)] if i in b_list else self.img_breakables[-1] new_im.paste(this_img, (x_offset,0)) x_offset += this_img.size[0] return ImageTk.PhotoImage(new_im) else: return -1 def set_frame_backgrounds(self, arg1 = None, arg2 = None, arg3 = None): """ Reacts to the frame colors checkbox and sets frames which make development and layout easier. """ if self.frame_backgrounds.get(): self.frm_profiles.config(background="orange",borderwidth=1) self.frm_curprof.config(background="red",borderwidth=1) self.frm_levels.config(background="green",borderwidth=1) else: bg = self.background_color # I manually set this when instantiating App with master.cget("bg") self.frm_profiles.config(background=bg,borderwidth=0) self.frm_curprof.config(background=bg,borderwidth=0) self.frm_levels.config(background=bg,borderwidth=0) def button_reset_level_breakables(self, level_obj): """ Handle button press to reset breakables for a level. """ profile_id = self.selected_profile.get() _, b_mask = srb_lib.get_collected_breakables(self.bdata,profile_id,level_obj,silent=True) amount = "all" if 0 == b_mask else "none" bdata, message = srb_lib.set_collected_breakables(self.bdata,profile_id,level_obj,amount) if -1 == bdata or message != "": raise Exception("Unable to reset breakables for level {level_obj} because {message}") else: self.bdata = bdata self.load_form_into_data() def button_action_level(self, level_obj, action, type_): """ Handle button press from "none" and "all" for balloons and letters for a level """ #print(f"debug (button_action_level): {level_obj}, {action}, {type_}") if type_ not in ["balloons","letters"]: raise Exception("Button for level action got invalid type {type_}") action = action.lower() if action not in ["none","all"]: raise Exception("Button for level action got invalid action {action}.") if level_obj not in srb_lib.LEVELS: raise Exception("Button for level action got invalid level {level_obj}.") profile_id = self.selected_profile.get() bdata = self.bdata func = srb_lib.set_level_balloons if type_ == "balloons" else srb_lib.set_level_letters bdata, message = func(bdata,profile_id,level_obj,action, silent=self.silent.get()) if bdata == -1 or message != "": raise Exception("Failed to set {type_} to {action} for level {level_obj}, because {message}") else: self.bdata = bdata self.load_form_into_data() def load_form_into_data(self, arg1 = None, arg2 = None, arg3 = None): """ Update self.bdata with the values currently displayed/edited on the form. This also updates the checksum. """ #print(f"debug (load_form_into_data): {arg1}, {arg2}, {arg3}") if self.bdata: bdata = self.bdata else: raise Exception("No file loaded.") try: bdata, _ = srb_lib.set_profile_in_use(bdata,1,self.prof1_used.get()) bdata, _ = srb_lib.set_profile_in_use(bdata,2,self.prof2_used.get()) bdata, _ = srb_lib.set_profile_in_use(bdata,3,self.prof3_used.get()) bdata, message = srb_lib.set_name(bdata,1,self.prof1name.get()) if message != "": stk.flash_entry(self.ent_prof1name,["red","white","red","white","red","white","red","white"],333) raise Exception("Profile 1 name") bdata, message = srb_lib.set_name(bdata,2,self.prof2name.get()) if message != "": stk.flash_entry(self.ent_prof2name,["red","white","red","white","red","white","red","white"],333) raise Exception("Profile 2 name") bdata, message = srb_lib.set_name(bdata,3,self.prof3name.get()) if message != "": stk.flash_entry(self.ent_prof3name,["red","white","red","white","red","white","red","white"],333) raise Exception("Profile 3 name") #print(f"debug: loading data with value prof1 tutorial: {self.prof1tut.get()}") bdata, _ = srb_lib.set_tutorial_completed(bdata,1,self.prof1tut.get()) bdata, _ = srb_lib.set_tutorial_completed(bdata,2,self.prof2tut.get()) bdata, _ = srb_lib.set_tutorial_completed(bdata,3,self.prof3tut.get()) # and now the current profile stuff profile_id = self.selected_profile.get() if profile_id >= 1 and profile_id <= 3: bdata = srb_lib.set_money(bdata,profile_id,self.money.get(),silent=self.silent.get()) bdata, _ = srb_lib.set_plane_stat(bdata,profile_id,"health",self.health.get()) bdata, _ = srb_lib.set_plane_stat(bdata,profile_id,"stunt",self.stunt.get()) bdata, _ = srb_lib.set_plane_stat(bdata,profile_id,"gun",self.gun.get()) bdata2 = srb_lib.set_weapon(bdata,profile_id,self.equipped_weapon.get(),silent=self.silent.get()) if bdata2 == -1: raise Exception("Failed when setting equipped weapon.") else: bdata = bdata2 # purchased weapons pur_weap_list = [] if self.pur_weap_potato.get(): pur_weap_list.append("Potato Gun") if self.pur_weap_stinger.get(): pur_weap_list.append("Stinger") if self.pur_weap_watball.get(): pur_weap_list.append("Water Balloon Cannon") if self.pur_weap_snow.get(): pur_weap_list.append("Snow Blower") if self.pur_weap_fire.get(): pur_weap_list.append("Fire Boomerang") if self.pur_weap_lightn.get(): pur_weap_list.append("Lightning Rod") if self.pur_weap_romanc.get(): pur_weap_list.append("Roman Candles") if self.pur_weap_pumpkin.get(): pur_weap_list.append("10 Gauge Pumpkin") bdata, message = srb_lib.set_purchased_weapons(bdata, profile_id, "remove",["all"], silent=self.silent.get()) if bdata == -1 or message != "": raise Exception("Failed when clearing purchased weapons.") #print(f"debug: will set weapon list {pur_weap_list}") bdata, message = srb_lib.set_purchased_weapons(bdata, profile_id, "add",pur_weap_list, silent=self.silent.get()) if bdata == -1 or message != "": raise Exception("Failed when setting purchased weapons.") # purchased planes pur_planes_list = [] if self.pur_plane_bool_marcie.get(): pur_planes_list.append("marcie") if self.pur_plane_bool_sally.get(): pur_planes_list.append("sally") if self.pur_plane_bool_rerun.get(): pur_planes_list.append("rerun") if self.pur_plane_bool_pigpen.get(): pur_planes_list.append("pigpen") if self.pur_plane_bool_woodstock.get(): pur_planes_list.append("woodstock") if self.pur_plane_bool_baron.get(): pur_planes_list.append("baron") bdata, message = srb_lib.set_purchased_planes(bdata, profile_id, "remove",["all"], silent=self.silent.get()) if bdata == -1 or message != "": raise Exception("Failed when clearing purchased planes.") #print(f"debug: will set plane list {pur_planes_list}") bdata, message = srb_lib.set_purchased_planes(bdata, profile_id, "add", pur_planes_list, silent=self.silent.get()) if bdata == -1 or message != "": raise Exception(f"Failed when setting purchased planes, message {message}.") # levelset completed levels for i in range(0,len(srb_lib.LEVELSETS)): bdata, message = srb_lib.set_levelset_completed_levels(bdata, profile_id, i, self.levelset_status_ints[i].get()) if bdata == -1 or message != "": raise Exception(f"Failed to set levelset completed levels for levelset {i}, message {message}.") for i in range(0,len(srb_lib.LEVELS)): ts = self.level_statuses[i].get() if ts != "unknown": bdata, current_status, message = srb_lib.set_level_status(bdata, profile_id, i, ts, fix_levelset_completed_levels=False) if bdata == -1 or current_status == -1 or message != "": raise Exception(f"Failed to set level status for {i} to {ts}, because {message}") except Exception as e: bdata = -1 print(f"ERROR! Invalid {e}") # If everything saved correctly, reload the checksum and then reload the form with the new data. This means that if you type in lower-case letters in a profile name, it will reload the valid upper-case name because the game only stores uppercase letters. if bdata != -1: self.bdata = bdata csum = 0 try: csum = srb_lib.calculate_checksum(self.bdata) except: pass if csum != 0: #print(f"need to do something with new checksum 0x{csum:08x}") self.bchecksum = csum self.load_data_into_form() # functions def func_about(self): """ Display about dialog. """ tkinter.messagebox.Message(title="About",message=ABOUT_TEXT,icon="info").show() def func_exit(self): # in case we need to manually do stuff # otherwise command=self.client_exit would have sufficed. self.master.quit() def func_open(self, filen = None): if filen: filename = filen else: filename = tkinter.filedialog.askopenfilename() # filename might be a None or something if the dialog was cancelled if filename: self.current_file.set(filename) self.statustext.set(f"File: {self.current_file.get()}") self.bchecksum, self.bdata = srb_lib.read_file(filename) if not self.silent.get(): print(f"File {filename} has checksum 0x{self.bchecksum:08x}, len(data)={len(self.bdata)}") self.load_data_into_form() def func_reload(self): """ Convenience function to just run func_open against the current file. The command lambda func_open failed to work because it seems to hardcode to just the first-defined current_file. """ self.func_open(self.current_file.get()) def mark_traces(self,enable = False): """ Enable or disable traces for variables. This protects the form from always calling load-form-into-data while we are populating the form initially! """ if enable: for j in [ self.health, self.stunt, self.gun, self.equipped_weapon, self.prof1_used, self.prof2_used, self.prof3_used, #self.prof1name, # do not trace the profile names, because it prevents spaces ending on the variables #self.prof2name, #self.prof3name, ] + self.levelset_status_ints + self.level_statuses: #print(f"adding w trace for {j}") j.trace("w",self.load_form_into_data) # different one for j in [ self.selected_profile ]: j.trace("w",self.load_data_into_form_for_current_profile) else: # disable for j in [ self.health, self.selected_profile, self.stunt, self.gun, self.equipped_weapon, self.prof1_used, self.prof2_used, self.prof3_used, ] + self.levelset_status_ints + self.level_statuses: for i in j.trace_info(): if i[0][0] == "write": #print(f"removing w trace for {j}") j.trace_remove(i[0][0],i[1]) def load_data_into_form(self, arg1 = None, arg2 = None, arg3 = None): """ Update all fields in dialog with contents of bdata. """ #print(f"debug (load_data_into_form): {arg1}, {arg2}, {arg3}") self.mark_traces(False) # set all variables that are tied to entry boxes self.checksum.set(hex(self.bchecksum)[2:]) self.prof1_used.set(srb_lib.get_profile_in_use(self.bdata,1)) self.prof1tut.set(srb_lib.get_tutorial_completed(self.bdata,1)) prof1name = srb_lib.get_name(self.bdata,1).rstrip() self.prof1name.set(prof1name) self.prof2_used.set(srb_lib.get_profile_in_use(self.bdata,2)) self.prof2tut.set(srb_lib.get_tutorial_completed(self.bdata,2)) prof2name = srb_lib.get_name(self.bdata,2).rstrip() self.prof2name.set(prof2name) self.prof3_used.set(srb_lib.get_profile_in_use(self.bdata,3)) self.prof3tut.set(srb_lib.get_tutorial_completed(self.bdata,3)) prof3name = srb_lib.get_name(self.bdata,3).rstrip() self.prof3name.set(prof3name) self.load_data_into_form_for_current_profile() # not necessary because for_current_profile runs the enable. #self.mark_traces(True) def load_data_into_form_for_current_profile(self, arg1 = None, arg2 = None, arg3 = None): """ Determine the currently-selected radio button for profile and load those values into the form. """ if not self.bdata: return "No file loaded." profile_id = self.selected_profile.get() y = 0 if profile_id >= 1 and profile_id <= 3: self.mark_traces(False) # money self.money.set(srb_lib.get_money(self.bdata,profile_id)) # equipped weapon w, _ = srb_lib.get_weapon_info(srb_lib.get_weapon(self.bdata,profile_id)) self.equipped_weapon.set(w["name"]) self.img_equ_weapon = get_srb_image(w["name"]+".png",16,16) # sets it on the label #self.lbl_equ_weapon.config(image=self.img_equ_weapon) # sets it on the item in the drop-down self.opt_equ_weapon.config(image=self.img_equ_weapon) # plane stats stat, _ = srb_lib.get_plane_stat(self.bdata,profile_id,"health") self.health.set(stat) stat, _ = srb_lib.get_plane_stat(self.bdata,profile_id,"stunt") self.stunt.set(stat) stat, _ = srb_lib.get_plane_stat(self.bdata,profile_id,"gun") self.gun.set(stat) # purchased weapons weapons_list, weapons_mask = srb_lib.get_purchased_weapons(self.bdata,profile_id,silent=self.silent.get()) if weapons_list == "all": weapons_list = [i["name"] for i in srb_lib.WEAPONS if i["e"]] elif weapons_list == "none": weapons_list = [] # and now set each weapon self.pur_weap_potato.set(True if "Potato Gun" in weapons_list else False) self.pur_weap_stinger.set(True if "Stinger" in weapons_list else False) self.pur_weap_watball.set(True if "Water Balloon Cannon" in weapons_list else False) self.pur_weap_snow.set(True if "Snow Blower" in weapons_list else False) self.pur_weap_fire.set(True if "Fire Boomerang" in weapons_list else False) self.pur_weap_lightn.set(True if "Lightning Rod" in weapons_list else False) self.pur_weap_romanc.set(True if "Roman Candles" in weapons_list else False) self.pur_weap_pumpkin.set(True if "10 Gauge Pumpkin" in weapons_list else False) # purchased planes planes_list, planes_mask = srb_lib.get_purchased_planes(self.bdata,profile_id,silent=self.silent.get()) if planes_list == "all": planes_list = [i["name"] for i in srb_lib.PLANES] elif planes_list == "none": planes_list = [] self.pur_plane_bool_marcie.set(True if "marcie" in planes_list else False) self.pur_plane_bool_sally.set(True if "sally" in planes_list else False) self.pur_plane_bool_rerun.set(True if "rerun" in planes_list else False) self.pur_plane_bool_pigpen.set(True if "pigpen" in planes_list else False) self.pur_plane_bool_woodstock.set(True if "woodstock" in planes_list else False) self.pur_plane_bool_baron.set(True if "baron" in planes_list else False) x = 0 for i in srb_lib.LEVELSETS: mission_mask, letters, balloon_count = srb_lib.get_levelset_status(self.bdata,profile_id,x,silent=self.silent.get()) if 0 == x: self.pur_plane_str_marcie.set(letters) elif 1 == x: self.pur_plane_str_sally.set(letters) elif 2 == x: self.pur_plane_str_rerun.set(letters) elif 3 == x: self.pur_plane_str_pigpen.set(letters) elif 4 == x: self.pur_plane_str_woodstock.set(letters) elif 5 == x: self.pur_plane_str_baron.set(letters) # levelset completed levels mission_mask = bin(int(mission_mask[1:],2)).count('1') #print(f"for levelset {i}, need to set max {i['l']}, current {mission_mask}") self.levelset_status_ints[x].set(mission_mask) self.levelset_balloons_ints[x].set(balloon_count) x += 1 # end for-in-in-levelsets # levels for i in range(0,len(srb_lib.LEVELS)): this_level = [j for j in srb_lib.LEVELS if j["id"] == i][0] tl_status, tl_balloons, tl_letters = srb_lib.get_level_status(self.bdata,profile_id,i,silent=True) # level status tl_status = int(tl_status,16) # look up name of status if possible else put "unknown". Thankfully tk handles it gracefully and shows it, but does not let a user pick it if he selects the drop-down menu. tl_status_str = "" #print(f"got tl_status {tl_status}, type {type(tl_status)}") try: tl_status_str = [i["name"] for i in srb_lib.LEVEL_STATUSES if i["b"] == tl_status][0] except: tl_status_str = "unknown" self.level_statuses_hex[i].set(hex(tl_status)[2:]) self.level_statuses[i].set(tl_status_str) # level balloons self.level_balloons[i].set(tl_balloons) self.level_letters[i].set(tl_letters) # level available because of completed levels # we are not in the loop of levelsets, so we need to re-get mission_mask. mission_mask, _, _ = srb_lib.get_levelset_status(self.bdata,profile_id,this_level["setid"],silent=True) mission_mask = bin(int(mission_mask[1:],2)).count('1') prev_mission_mask = 0 this_levelset = [l for l in srb_lib.LEVELSETS if l["id"] == this_level["setid"]][0] if this_level["setid"] > 0: prev_mission_mask, _, _ = srb_lib.get_levelset_status(self.bdata,profile_id,this_level["setid"]-1,silent=True) prev_mission_mask = bin(int(prev_mission_mask[1:],2)).count('1') # work up from here state = "disabled" if this_level["set_pos"] == 0: if this_level["setid"] == 0: if srb_lib.get_tutorial_completed(self.bdata,profile_id): state = "normal" elif prev_mission_mask >= this_levelset["a"]: state = "normal" elif this_level["set_pos"] <= mission_mask: state = "normal" self.lbl_levels[this_level['id']].config(state=state) # if level set_pos 0, then update the label for the whole levelset if this_level["set_pos"] == 0: self.lbl_levelsets[this_level["setid"]].config(state=state) if this_level["r"]: b_str, b_mask = srb_lib.get_collected_breakables(self.bdata,profile_id,this_level,silent=True) if "all" == b_str: b_str = ','.join([b["name"] for b in self.BREAKABLES]) b_list = b_str.split(",") if "none" in b_list: b_list.pop(b_list.index("none")) #print(f"debug: for {this_level['name']}, i {i}, y {y}, got b {b_list}, mask {b_mask}") self.bim_levels_breakables[y] = self.get_single_image_from_breakables(b_list) if self.bim_levels_breakables[y] != -1: # which really means, if IMAGES self.btn_levels_breakables[y].config(image=self.bim_levels_breakables[y]) self.btn_levels_breakables[y].config(text=str(len(b_list))) self.ttp_levels_breakables[y].text = b_str # y counts number of levels with breakables y = y + 1 # end of if-level-has-breakables # end for-i-in-levels # end if-profile_id is 1,2 or 3. # mark-traces-enable must happen at the end! self.mark_traces(True) else: print(f"Error! Cannot load that profile_id into the form.") def refresh_to_data(self): """ Update bdata with values from dialog? """ #srb_lib. def func_save(self): filename = None try: filename = self.current_file.get() except: msg = "Cannot save because we have not opened anything yet." print(f"WARNING: {msg}") tk.messagebox.showerror(title="Error",message=msg) if filename: checksum = self.bchecksum #print(f"how about save to file {filename}, checksum {checksum}") #print(f"checksum hex 0x{checksum:08x}, int {checksum}") srb_lib.write_file(filename,checksum, self.bdata) def func_save_as(self): filename = None try: filename = tkinter.filedialog.asksaveasfilename(initialfile=self.current_file.get()) except: msg = "Cannot save as because did not get a valid file from the dialog." tk.messagebox.showerror(title="Error",message=msg) if filename: checksum = self.bchecksum srb_lib.write_file(filename,checksum,self.bdata) # main root = tk.Tk() srb_tk = App(root) srb_tk.mainloop()