#!/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 # icon from https://www.iconarchive.com/show/peanuts-icons-by-ed-mundy/Snoopy-icon.html # 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 # Improve: # Enable the checkbox for profile-in-use # Add reset-level-breakables # disable/gray out levels labels not reachable because of levelset_available_levels # setting that sets the background colors of the regions # add images to option menus if possible: https://stackoverflow.com/questions/67334913/can-i-possibly-put-an-image-label-into-an-option-box-in-tkinter # 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): """ 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) return ImageTk.PhotoImage(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() self.silent.set(True) # data variables 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() # Do not use traces for most things though, because it will catch itself in a loop. Instead, use the bind on the form fields. 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() # 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") self.lbl_checksum = tk.Label(self.master,text="Checksum",underline=0) self.lbl_checksum.grid(row=1,column=0) self.ent_checksum = tk.Entry(state="readonly",textvariable=self.checksum) #self.ent_checksum.bind("",self.load_form_into_data) self.ent_checksum.grid(row=2,column=0) self.chk_silent = tk.Checkbutton(self.master,text="silent",variable=self.silent) self.chk_silent.grid(row=2,column=1) self.frm_profiles = tk.Frame(self.master) #,background="orange",borderwidth=1) self.frm_profiles.grid(row=3,column=0,columnspan=2) # investigate radio button key-bind, up-down, to increment/decrement choice? self.rad_prof1 = stk.Radiobutton(self.frm_profiles,value=1,text="Profile 1",variable=self.selected_profile,underline=8) self.rad_prof1.grid(row=0,column=0) self.chk_prof1_used = tk.Checkbutton(self.frm_profiles,text="used",state="disabled",variable=self.prof1_used) self.chk_prof1_used.grid(row=0,column=2) self.chk_prof1_tut = stk.Checkbutton(self.frm_profiles,text="tutorial",variable=self.prof1tut,func=self.load_form_into_data) #self.hook_checkbox_enter_keys(self.chk_prof1_tut,self.load_form_into_data) self.chk_prof1_tut.grid(row=0,column=3) self.ent_prof1name = stk.Entry(self.frm_profiles,textvariable=self.prof1name,func=self.load_form_into_data,width=11) self.ent_prof1name.bind("",self.load_form_into_data) self.ent_prof1name.bind("",self.load_form_into_data) self.ent_prof1name.grid(row=0,column=4) self.rad_prof2 = stk.Radiobutton(self.frm_profiles,value=2,text="Profile 2",variable=self.selected_profile,underline=8) self.rad_prof2.grid(row=1,column=0) self.prof2_used.set(False) self.chk_prof2_used = tk.Checkbutton(self.frm_profiles,text="used",state="disabled",variable=self.prof2_used) self.chk_prof2_used.grid(row=1,column=2) self.chk_prof2_tut = stk.Checkbutton(self.frm_profiles,text="tutorial",variable=self.prof2tut,func=self.load_form_into_data) #self.hook_checkbox_enter_keys(self.chk_prof2_tut,self.load_form_into_data) self.chk_prof2_tut.grid(row=1,column=3) self.ent_prof2name = stk.Entry(self.frm_profiles,textvariable=self.prof2name,func=self.load_form_into_data,width=11) self.ent_prof2name.bind("",self.load_form_into_data) self.ent_prof2name.grid(row=1,column=4) self.rad_prof3 = stk.Radiobutton(self.frm_profiles,value=3,text="Profile 3",variable=self.selected_profile,underline=8) self.rad_prof3.grid(row=2,column=0) self.prof3_used.set(False) self.chk_prof3_used = tk.Checkbutton(self.frm_profiles,text="used",state="disabled",variable=self.prof3_used) self.chk_prof3_used.grid(row=2,column=2) self.chk_prof3_tut = stk.Checkbutton(self.frm_profiles,text="tutorial",variable=self.prof3tut,func=self.load_form_into_data) #self.hook_checkbox_enter_keys(self.chk_prof3_tut,self.load_form_into_data) self.chk_prof3_tut.grid(row=2,column=3) self.ent_prof3name = stk.Entry(self.frm_profiles,textvariable=self.prof3name,func=self.load_form_into_data,width=11) self.ent_prof3name.bind("",self.load_form_into_data) self.ent_prof3name.grid(row=2,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) #,background="red") self.frm_curprof.grid(row=4,column=0,rowspan=22,columnspan=2) # most form fields are inside the current profile self.img_money = get_srb_image("money.png",20,16) self.lbl_money = tk.Label(self.frm_curprof,text="Money",image=self.img_money,compound="left") self.lbl_money.grid(row=0,column=0) self.ent_money = stk.Entry(self.frm_curprof,textvariable=self.money,func=self.load_form_into_data) #self.ent_money.bind("",self.load_form_into_data) self.ent_money.grid(row=0,column=1) self.lbl_equ_weapon = tk.Label(self.frm_curprof,text="Equipped weapon") self.lbl_equ_weapon.grid(row=1,column=0) # list of names equippable_weapons = [i["name"] for i in srb_lib.WEAPONS if i["e"]] 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) self.opt_equ_weapon.grid(row=1,column=1,sticky="ew") self.img_health = get_srb_image("health.png",16,16) self.lbl_health = tk.Label(self.frm_curprof,text="Health",image=self.img_health,compound="left") self.lbl_health.grid(row=2,column=0) self.spn_health = stk.Spinbox(self.frm_curprof,from_=1,to=4,textvariable=self.health,width=2) self.spn_health.grid(row=2,column=1) self.img_stunt = get_srb_image("stunt.png",16,16) self.lbl_stunt = tk.Label(self.frm_curprof,text="Stunt",image=self.img_stunt,compound="left") self.lbl_stunt.grid(row=3,column=0) self.spn_stunt = stk.Spinbox(self.frm_curprof,from_=1,to=4,textvariable=self.stunt,width=2) self.spn_stunt.grid(row=3,column=1) self.img_gun = get_srb_image("gun.png",19,16) self.lbl_gun = tk.Label(self.frm_curprof,text="Gun",image=self.img_gun,compound="left") self.lbl_gun.grid(row=4,column=0) self.spn_gun = stk.Spinbox(self.frm_curprof,from_=1,to=5,textvariable=self.gun,width=2) self.spn_gun.grid(row=4,column=1) self.lbl_pur = tk.Label(self.frm_curprof,text="Purchased: Weapons") self.lbl_pur.grid(row=5,column=0) #self.lbl_pur_weap = tk.Label(self.frm_curprof,text="Weapons") #self.lbl_pur_weap.grid(row=5,column=0) self.lbl_pur_weap = tk.Label(self.frm_curprof,text="Planes") self.lbl_pur_weap.grid(row=5,column=1) self.img_potato = get_srb_image("Potato Gun.png",16,16) self.chk_pur_weap_potato = 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") self.chk_pur_weap_potato.grid(row=6,column=0,sticky="W") self.img_stinger = get_srb_image("Stinger.png",16,16) self.chk_pur_weap_stinger = stk.Checkbutton(self.frm_curprof,text="Stinger",variable=self.pur_weap_stinger,func=self.load_form_into_data,image=self.img_stinger,compound="left") self.chk_pur_weap_stinger.grid(row=7,column=0,sticky="W") self.img_watball = get_srb_image("Water Balloon Cannon.png",12,16) self.chk_pur_weap_watball = 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") self.chk_pur_weap_watball.grid(row=8,column=0,sticky="W") self.img_snow = get_srb_image("Snow Blower.png",16,16) self.chk_pur_weap_snow = 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") self.chk_pur_weap_snow.grid(row=9,column=0,sticky="W") self.img_fire = get_srb_image("Fire Boomerang.png",12,16) self.chk_pur_weap_fire = 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") self.chk_pur_weap_fire.grid(row=10,column=0,sticky="W") self.img_lightn = get_srb_image("Lightning Rod.png",16,16) self.chk_pur_weap_lightn = 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") self.chk_pur_weap_lightn.grid(row=11,column=0,sticky="W") self.img_romanc = get_srb_image("Roman Candles.png",16,16) self.chk_pur_weap_romanc = 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") self.chk_pur_weap_romanc.grid(row=12,column=0,sticky="W") self.img_pumpkin = get_srb_image("10 Gauge Pumpkin.png",16,16) self.chk_pur_weap_pumpkin = 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") self.chk_pur_weap_pumpkin.grid(row=13,column=0,sticky="W") self.chk_pur_plane_marcie = stk.Checkbutton(self.frm_curprof,textvariable=self.pur_plane_str_marcie,variable=self.pur_plane_bool_marcie,func=self.load_form_into_data) self.chk_pur_plane_marcie.grid(row=6,column=1,sticky="W") self.chk_pur_plane_sally = stk.Checkbutton(self.frm_curprof,textvariable=self.pur_plane_str_sally,variable=self.pur_plane_bool_sally,func=self.load_form_into_data) self.chk_pur_plane_sally.grid(row=7,column=1,sticky="W") self.chk_pur_plane_rerun = stk.Checkbutton(self.frm_curprof,textvariable=self.pur_plane_str_rerun,variable=self.pur_plane_bool_rerun,func=self.load_form_into_data) self.chk_pur_plane_rerun.grid(row=8,column=1,sticky="W") self.chk_pur_plane_pigpen = stk.Checkbutton(self.frm_curprof,textvariable=self.pur_plane_str_pigpen,variable=self.pur_plane_bool_pigpen,func=self.load_form_into_data) self.chk_pur_plane_pigpen.grid(row=9,column=1,sticky="W") self.chk_pur_plane_woodstock = stk.Checkbutton(self.frm_curprof,textvariable=self.pur_plane_str_woodstock,variable=self.pur_plane_bool_woodstock,func=self.load_form_into_data) self.chk_pur_plane_woodstock.grid(row=10,column=1,sticky="W") self.chk_pur_plane_baron = stk.Checkbutton(self.frm_curprof,textvariable=self.pur_plane_str_baron,variable=self.pur_plane_bool_baron,func=self.load_form_into_data) self.chk_pur_plane_baron.grid(row=11,column=1,sticky="W") # level and levelsets self.frm_levels = tk.Frame(self.master) #,borderwidth=1,background="green") self.frm_levels.grid(row=0,column=3,rowspan=len(srb_lib.LEVELS)+4) self.lbl_levelsets = [] self.spn_levelsets = [] self.ent_levelsets_balloons = [] self.lbl_levelsets_name = tk.Label(self.frm_levels,text="Billboard/Available") self.lbl_levelsets_name.grid(row=0,column=0,sticky="EW",columnspan=4) #self.lbl_levelsets_available_levels = tk.Label(self.frm_levels,text="Available") #self.lbl_levelsets_available_levels.grid(row=0,column=1,columnspan=2) self.lbl_levels_name = tk.Label(self.frm_levels,text="Level") self.lbl_levels_name.grid(row=0,column=5,sticky="EW") self.lbl_levels_status = tk.Label(self.frm_levels,text="Rank") self.lbl_levels_status.grid(row=0,column=6,sticky="EW",columnspan=2) self.lbl_levels_balloons = tk.Label(self.frm_levels,text="Balloons") self.lbl_levels_balloons.grid(row=0,column=8,columnspan=4,sticky="EW") self.lbl_levels_letters = tk.Label(self.frm_levels,text="Letters") self.lbl_levels_letters.grid(row=0,column=12,columnspan=4,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 = [] x = 1 self.img_balloons = [] 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) x += 1 x += 1 # end for-i-in-levelsets # status bar self.status = stk.StatusBar(self.master,var=self.statustext) #self.grid(expand=True,fill=tk.BOTH) # and now open the first file self.func_open(self.current_file.get()) 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, message = srb_lib.set_name(bdata,1,self.prof1name.get()) if message != "": self.flash_entry(self.ent_prof1name) raise Exception("Profile 1 name") bdata, message = srb_lib.set_name(bdata,2,self.prof2name.get()) if message != "": self.flash_entry(self.ent_prof2name) raise Exception("Profile 2 name") bdata, message = srb_lib.set_name(bdata,3,self.prof3name.get()) if message != "": self.flash_entry(self.ent_prof3name) 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 available levels for i in range(0,len(srb_lib.LEVELSETS)): bdata, message = srb_lib.set_levelset_available_levels(bdata, profile_id, i, self.levelset_status_ints[i].get()) if bdata == -1 or message != "": raise Exception(f"Failed to set levelset available 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_available_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() def flash_entry(self, this_entry): """ Flash the offending entry box red for one second. """ # Improve: move this to stackrpms_tk_lib? current_color = this_entry.cget("background") #print(f"DEBUG (flash_entry): this_entry={this_entry}") #print(f"DEBUG: current_color {current_color}") if current_color in ["#ffffff","white"]: # name is supposed to work but it does not next_color = "red" #if current_color == "#ffffff" else "white" this_entry.config(background=next_color) self.master.after(1000, lambda te=this_entry: self.flash_entry(te)) else: next_color = "white" this_entry.config(background=next_color) # 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.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.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() #print(f"debug: Using profile_id {profile_id}") #print(f"debug: arg1 {arg1}") #print(f"debug: arg2 {arg2}") #print(f"debug: arg3 {arg3}") 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 self.equipped_weapon.set(srb_lib.get_weapon(self.bdata,profile_id)) # 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 available 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 # levels for i in range(0,len(srb_lib.LEVELS)): 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) # 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()