#!/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()