class Login: #--------------------------------------------------------------------------------- # Initialise instance of admin #--------------------------------------------------------------------------------- def __init__(self): self._db = Debug("admin", True) #----------------------------------------------------------------------------------------------- # Read a Boat by Code #----------------------------------------------------------------------------------------------- def AuthLoginByUserCode(self, con, UserCode, password): # retValue contains the success or failure of the read operation. Default to success self._retvalue = False self._error = None self._db.print(self._retvalue) # define SQL query query = "SELECT u.Password FROM users u WHERE u.UserCode = ?" self._db.print(query) try: con.row_factory = sqlite3.Row cur = con.cursor() cur.execute(query, (UserCode, )) self._db.print("Execute") row = cur.fetchone() if row == None: self._error = "No User found for User Code: " + UserCode self._db.print(self._error) self._retvalue = False return (self._retvalue, self._error) self._db.print(row[0]) self._db.print(password) if row[0] == password: self._db.print("If") self._retvalue = True else: self._error = "Incorrect Password" self._db.print("else") self._retvalue = False return (self._retvalue, self._error) # Exception processing logic here. except Exception as err: self._error = "Query Failed: " + str(err) self._db.print(self._error) return (self._retvalue) # Property functions here @property def UserCode(self): return self.UserCode @property def password(self): return self.password # ----- any error codes ----- @property def error(self): return self._error
#--------------------------------------------------------------------------- # Create a debug object here. Either set to True for debugging or False # Set to False by default and we can enable / disable it when we like #--------------------------------------------------------------------------- db = Debug("app2", True) #--------------------------------------------------------------------------- # Make a connection to the database #--------------------------------------------------------------------------- con = sqlite3.connect('database/Fox_II.db') #Makes sure foreign keys are enforce for database integrity con.execute('pragma foreign_keys=ON') #Confirm database connection. db.print("Opened database successfully") #---------------------------------------------------------------------------- # Procedure to read the Country table and populate the list before # rendering in the customer details page #---------------------------------------------------------------------------- def readCountryTable(): #create country object and read the Countries ctry = Country() (dbStatus, countries) = ctry.readCountries(con) if (dbStatus == False): return render_template('error.html', error=ctry.error) else: return (countries)
class Schedule: #--------------------------------------------------------------------------------- # Initialise instance of schedule #--------------------------------------------------------------------------------- def __init__(self): self.__nullSchedule() # Set debugging for this module. self._db = Debug("schedule", False) #--------------------------------------------------------------------------------- # Initialse a blank Schedule structure #--------------------------------------------------------------------------------- def __nullSchedule(self): # Create blank instance variables for the new created object here. We will # prefix these with a single '_' to make clear that they are internal to this class. self._CruiseDate = None self._CruiseNo = 1 self._departure = "" self._BoatID = "" self._RouteID = "" self._return = "" self._available = 0 self._error = "" #--------------------------------------------------------------------------------- # Internal function to set the property values to the current row. The __ at the # start of the dunction indicate that it cannot be access for any calling programs #--------------------------------------------------------------------------------- def __setSchedule(self, row): # Allocate the retrieved columns into the object variables. self._CruiseDate = row['CruiseDate'] self._CruiseNo = row['CruiseNo'] self._departure = row['departure'] self._BoatID = row['BoatID'] self._RouteID = row['RouteID'] self._return = row['return'] self._available = int(row['available']) #---------------------------------------------------------------------------------- # Return a blank schedule row #---------------------------------------------------------------------------------- def blankScheduleRow(self): row = [] self.__nullSchedule() row.append({ 'CruiseDate': self._CruiseDate, 'CruiseNo': self._CruiseNo, 'departure': self._departure, 'BoatID': self._BoatID, 'Route_ID': self._RouteID, 'return': self._return, 'available': self._available }) return (row) #--------------------------------------------------------------------------------- # Internal function to check the date or time format. Usually we expect that the # incoming value has been successfully validated, but this final check will ensure # database integrity in the event that it hasn't been validated correctly #--------------------------------------------------------------------------------- def __validateDT(self, date_text, format): try: # Take the date_text and return a datetime value formatted as supplied to the function. # Reformatting the string back ensure we have the format we require, including leading # 0's. eg 09:04, 2017-03-02 # If there is an error converting the date with strptime, then a Value error execption # is raised. If the dates don't match we raise the ValueError eception as well so # it can be handled in the same way if date_text != datetime.strptime(date_text, format).strftime(format): raise ValueError return True except ValueError: return False #---------------------------------------------------------------------------------- # Fuction to validate the fields of any record being inserted or updated. Any # failure will be return to the user instead of updating the database #---------------------------------------------------------------------------------- def __validateFields(self): # Do any data validation checks here to ensure database integrity. Some fields will be handled by # constraints within the database itself. if not self.__validateDT(self._CruiseDate, "%Y-%m-%d"): self._error = "Invalid Cruise date format" return False # Check the departure time is the time format we require self._db.print("Departure = " + str(self._departure)) if not self._departure: self._error = "Departure Time is required" return False if not self.__validateDT(self._departure, "%H:%M"): self._error = "Invalid departure time format" return False # Check the return time is the time we require self._db.print("Return Time = " + str(self._return)) if not self._return: self._error = "Return Time is required" return False if not self.__validateDT(self._return, "%H:%M"): self._error = "Invalid return time format" return False # Even though the CruiseNo field is defined as Integer, SQLite will allow string values # to be inserted! So doesn't hurt to to a check here. try: self._CruiseNo = int(self._CruiseNo) except: self._error = "CruiseNo is not numeric" return False if self._CruiseNo < 1: self._error = "Cruise Number must be greater than 0" return self._retvalue return True #----------------------------------------------------------------------------------------------- # Read a schedule record from the database. Required is the database handle, the Cruise Date # and Cruise No #----------------------------------------------------------------------------------------------- def readSched(self, con, CruiseDate, CruiseNo): # retValue contains the success or failure of the read operation. Default to success self._retvalue = True self._error = "" row = [] self.__nullSchedule # Check the date is in the correct format. The query would fail to return a record anyway # but it's nicer to return to the calling program a valid reason why it has failed. if not self.__validateDT(CruiseDate, "%Y-%m-%d"): self._error = "Invalid date format" self._retvalue = False return self._retvalue # Same for the Cruise No return a 'nice' error if CruiseNo < 1 try: CruiseNo = int(CruiseNo) except: self._error = "Cruise Number in not an integer" self._retvalue = False if CruiseNo < 1: self._error = "Cruise Number must be greater than 0" self._retvalue = False return self._retvalue self._db.print("readSched") self._db.print("CruiseDate = " + CruiseDate) self._db.print("CruiseNo = " + str(CruiseNo)) # define SQL query read_query = "SELECT s.CruiseDate, s.CruiseNo, s.departure, s.BoatID, b.name, s.RouteID, \ r.description, s.return, s.available \ FROM schedule s \ INNER JOIN boat b \ ON b.BoatID = s.BoatID \ INNER JOIN route r \ ON s.RouteID = r.RouteID \ WHERE s.CruiseDate = ? \ AND s.CruiseNo = ?" try: # define cursone and execute the query, CustID is the primary key so we will only expect # one record to be returned. con.row_factory = sqlite3.Row cur = con.cursor() cur.execute(read_query, (CruiseDate, CruiseNo)) row = cur.fetchall() if not row: self._error = "No schedule record found for date " + str( CruiseDate) + " and number " + str(CruiseNo) self._retvalue = False else: self.__setSchedule(row[0]) # Exception processing logic here. except Exception as err: self._error = "Query Failed: " + str(err) self._retvalue = False return (self._retvalue, row) #----------------------------------------------------------------------------------------------- # Read the schedule record and return those matching the starting and ending dates as passed # from the calling program #----------------------------------------------------------------------------------------------- def readSchedulebyDate(self, con, startDate, endDate): # retValue contains the success or failure of the read operation. Default to success self._retvalue = True self._error = "" rows = [] # Null the schedule properties self.__nullSchedule # Check that the dates are in a valid format and consistant with those that will be stored # within the Schedule table if not self.__validateDT(startDate, "%Y-%m-%d") or not self.__validateDT( endDate, "%Y-%m-%d"): self._error = "Invalid date format" self._retvalue = False return (self._retvalue, rows) if startDate > endDate: self._error = "Start date cannot be greater than end date" self._retvalue = False return (self._retvalue, rows) # Print these values if we are debugging self._db.print("readSchedulebyDate") self._db.print("startDate = " + startDate) self._db.print("endDate = " + endDate) # define SQL query # taking CruiseDate and CruiseNo and join into one field so can be used for the book now button read_query = "SELECT s.CruiseDate, s.CruiseNo, s.departure, s.BoatID, b.name, s.RouteID, \ r.description, s.return, s.available, (s.CruiseDate || '.' || s.CruiseNo) as 'key' \ FROM schedule s \ INNER JOIN boat b \ ON b.BoatID = s.BoatID \ INNER JOIN route r \ ON s.RouteID = r.RouteID \ WHERE s.CruiseDate between ? and ? \ ORDER BY s.CruiseDate ASC" try: # define cursor and execute the query con.row_factory = sqlite3.Row cur = con.cursor() cur.execute(read_query, (startDate, endDate)) rows = cur.fetchall() if not rows: self._error = "No schedule records found between: " + str( startDate) + " " + str(endDate) self._retvalue = False # Exception processing logic here. except Exception as err: self._error = "Query Failed: " + str(err) self._retvalue = False return (self._retvalue, rows) #------------------------------------------------------------------------------------------------- # Delete a schedule record from the database. Required is the database handle and the Cruise Date # and cruise No. #------------------------------------------------------------------------------------------------- def deleteSchedule(self, con, CruiseDate, CruiseNo): # retValue contains the success or failure of the update operation. Default to success self._retvalue = True self._error = "" # Check the date is in the correct format. The query would fail to return a record anyway # but it's nicer to return to the calling program a valid reason why it has failed. if not self.__validateDT(CruiseDate, "%Y-%m-%d"): self._error = "Invalid date format" self._retvalue = False return self._retvalue # Same for the Cruise No return a 'nice' error if CruiseNo < 1 try: CruiseNo = int(CruiseNo) except: self._error = "Cruise Number in not an integer" self._retvalue = False return self._retvalue if CruiseNo < 1: self._error = "Cruise Number must be greater than 0" self._retvalue = False return self._retvalue self._db.print("deleteSchedule") self._db.print("CruiseDate = " + CruiseDate) self._db.print("CruiseNo = " + str(CruiseNo)) # define SQL query delete_query = "delete from schedule where CruiseDate = ? and CruiseNo = ?" # attempt to execute the query try: cur = con.cursor() cur.execute(delete_query, (CruiseDate, CruiseNo)) # Commit the trasaction if successful. con.commit() self._error = "Schedule successfully deleted" # Exception processing logic here. except Exception as err: self._error = "Query Failed: " + str(err) # Rollback transaction if failed. con.rollback() self._retvalue = False return self._retvalue #------------------------------------------------------------------------------------------------- # Update a schedule record from the database. Required is the database handle and the Cruise Date # and cruise No. # All fields will be updated with the object variables. #------------------------------------------------------------------------------------------------- def updateSchedule(self, con, CruiseDate, CruiseNo): # retValue contains the success or failure of the update operation. Default to success self._retvalue = True self._error = "" # Make sure all of the fields are good before we attempt to insert the new Schedule self._CruiseDate = CruiseDate self._CruiseNo = CruiseNo self._retvalue = self.__validateFields() if self._retvalue == False: return self._retvalue # define SQL query update_query = "update schedule set departure = ?, BoatID = ?," \ "RouteID = ?, return = ?, available = ?" \ "where CruiseDate = ? and CruiseNo = ?" # attempt to execute the query try: cur = con.cursor() cur.execute(update_query, (self._departure, self._BoatID, self._RouteID, \ self._return, self._available, \ self._CruiseDate, self._CruiseNo)) # Commit the trasaction if successful. con.commit() self._error = "Schedule successfully updated" # Exception processing logic here. except Exception as err: self._error = "Query Failed: " + str(err) # Rollback transaction if failed. con.rollback() self._retvalue = False return self._retvalue #------------------------------------------------------------------------------------------------- # Insert a new schedule using the property values for schedule which need to have been # set by the calling program. #------------------------------------------------------------------------------------------------- def insertSchedule(self, con): # retValue contains the success or failure of the update operation. Default to success self._retvalue = True self._error = "" # Make sure all of the fields are good before we attempt to insert the new Customer self._retvalue = self.__validateFields() if self._retvalue == False: return self._retvalue # define SQL query insert_query = "insert into schedule (CruiseDate, CruiseNo, departure, BoatID, RouteID, \ return, available) VALUES (?, ?, ?, ?, ?, ?, ?)" # attempt to execute the query try: cur = con.cursor() cur.execute(insert_query, (self._CruiseDate, self._CruiseNo, self._departure, \ self._BoatID, self._RouteID, self._return, self._available)) # Commit the trasaction if successful. con.commit() self._error = "Schedule successfully inserted" # Exception processing logic here. except Exception as err: self._error = "Query Failed: " + str(err) # Rollback transaction if failed. con.rollback() self._retvalue = False return self._retvalue #------------------------------------------------------------------------------------------------ # If a new booking is made then we subtract the number of people booked from the # available seats #------------------------------------------------------------------------------------------------ def newBooking(self, seats): self._available -= seats #------------------------------------------------------------------------------------------------- # Expose the instance variables to calling programs using 'setter' and 'getter' routines instead # of using individual methods. This allows us to control how the properties are set and returned # to the calling program. #------------------------------------------------------------------------------------------------- # ----- Cruise Date ------ @property def CruiseDate(self): return self._CruiseDate @CruiseDate.setter def CruiseDate(self, CruiseDate): self._CruiseDate = CruiseDate # ----- Cruise Number ------ @property def CruiseNo(self): return self._CruiseNo @CruiseNo.setter def CruiseNo(self, CruiseNo): self._CruiseNo = CruiseNo # ---- Boat ID ----- @property def BoatID(self): return self._BoatID @BoatID.setter def BoatID(self, BoatID): self._BoatID = BoatID # ---- Route ID ----- @property def RouteID(self): return self._RouteID @RouteID.setter def RouteID(self, RouteID): self._RouteID = RouteID # ---- departure ----- @property def departure(self): return self._departure @RouteID.setter def departure(self, departure): self._departure = departure # ---- return time ----- @property def returntime(self): return self._return @returntime.setter def returntime(self, returntime): self._return = returntime @property def available(self): return self._available @available.setter def available(self, available): self._available = available # ----- any error codes ----- @property def error(self): return self._error
class VitaResFinder: def __init__(self): self.debug = Debug() self.startLogo() self.startWarning() self.debug.letWait() def startLogo(self): self.debug.print("VitaResFinder, version:{0} by K4CZP3R".format( values.version)) def startWarning(self): self.debug.print( "THIS TOOL IS NOT NOOB-PROOF, It'll only work if you know what you are doing", color=Fore.RED) def askForAction(self): self.debug.print( "Question \/ \nChoose option\n1. Help about decompiling bin to arm\n2. Search for resolution entry in arm code\n3. Get new resolution entry\n4. Show known mods\n0. Exit" ) tmp = self.debug.ask("Selection") return tmp def showDecompileHelp(self): self.debug.print("Decompile help selected!") help_lines = [ "=== basic info ===", "To decompile eboot.bin to desired format you'll need:", "1. Modified prx-tool (included)", "2. (decrypted) eboot.bin [tested on maidumps eboots]", "3. Default resolution of game (ex. 720x408)" ] prxtool_help_lines = [ "=== prx-tool info ===", "To decompile it, follow these steps:", "1. Get eboot.bin of your game and copy it near prx-tool", "2. Run following command", "Where ebootbin, give path of eboot.bin", "Where ebootout, give path of output (file needed for this script)", "./prxtool -r 0x0 -i -b -w -n db.json ebootbin > ebootout", "Then wait a couple of minutes (no progress bar)" ] program_help_lines = [ '=== VitaResFinder ===', "After you have decompiled your eboot with prx-tool and you know default resolution of your game", "You can choose option 2 in this program", "After saving results of option 2, go to option 3 to generate new resolution" ] for line in help_lines: self.debug.print(line) for line in prxtool_help_lines: self.debug.print(line) for line in program_help_lines: self.debug.print(line) def resSearchMain(self): self.debug.print("Resolution search selected!") input_eboot = str(self.debug.ask("Location of decompiled eboot.bin")) input_resx = str( self.debug.ask("Default width resolution (ex *720*x408)")) input_resy = str( self.debug.ask("Default height resolution (ex 720x*408*)")) input_armfunction = str( self.debug.ask("ARM fuction to search ({0})? ".format( values.default_armfunction))) if (len(input_armfunction) != 0): armfunction = input_armfunction else: armfunction = values.default_armfunction summary_lines = [ "=== summary ===", "Eboot: {0}".format(input_eboot), "Resolution: {0}x{1}".format(input_resx, input_resy), "ARM Function: {0}".format(armfunction) ] for line in summary_lines: self.debug.print(line) input_change = self.debug.ask( "Want to change something? (path,resx,resy,armfunc,no)") if str(input_change) == "path": input_eboot = str( self.debug.ask("Location of decompiled eboot.bin")) if str(input_change) == "resx": input_resx = str( self.debug.ask("Default width resolution (ex *720*x408)")) if str(input_change) == "resy": input_resy = str( self.debug.ask("Default height resolution (ex 720x*408*)")) if str(input_change) == "armfunc": input_armfunction = str( self.debug.ask("ARM fuction to search ({0})? ".format( values.default_armfunction))) self.debug.print("Will perform checks on user input...") if not Path(input_eboot).is_file(): self.debug.printError("Can't find {0}".format(input_eboot)) return if len(input_resx) != 3 or len(input_resy) != 3: self.debug.printError("Resolution is too big/small") return action_resx = "{0}{1}".format( "#", str(hex(int(input_resx))).upper().replace('X', 'x')) action_resy = "{0}{1}".format( "#", str(hex(int(input_resy))).upper().replace('X', 'x')) self.debug.print( "Ok, will search for those values: {0} and {2} ({1}x{3})".format( action_resx, input_resx, action_resy, input_resy)) self.resSearchAction(input_eboot, armfunction, action_resx, action_resy) def resSearchAction(self, path, instr, resx, resy): resx = resx.lower() resy = resy.lower() instr = instr.lower() location_a = 0 location_b = 0 value_a = "" value_b = "" closeList = list() maxSpace = 32 count = 0 info = "" line_color = Fore.GREEN self.debug.print( "Opening file to read lines (will take a minute or 2)") f = open(path) f_lines = len(f.readlines()) f.close() self.debug.print("File contains {0} lines".format(str(f_lines))) f = open(path) start_time = time.time() while count < f_lines: line = f.readline().strip('\n').lower() if instr in line: if resx in line or resy in line: location_a = count value_a = line if ((location_a - location_b) < maxSpace): line_color = Fore.RED if resx in value_a and resx in value_b: info = "{0} copy".format(resx) elif resy in value_a and resy in value_b: info = "{0} copy".format(resy) else: closeList.append( "//begin\nvalA: '{0}' [@{1}line]\nvalB: '{2}' [@{3}line]\n//end\n" .format(str(value_a), str(location_a), str(value_b), str(location_b))) else: info = "" line_color = Fore.GREEN print("{0}{1} * [delta:{2}, line:{3}/{5}] {4}*".format( line_color, line, str(location_a - location_b), str(count), str(info), str(f_lines - count))) location_b = location_a value_b = value_a count = count + 1 end_time = time.time() - start_time self.debug.print("Took {0}s".format(str(end_time))) self.debug.print("Showing results, save them in pairs (valA,valB)") print(*closeList, sep='\n') def newResolution(self): self.debug.print("Resolution update selected!") input_eboot = str( self.debug.ask("Location of eboot.bin (NOT prxtooled EBOOT)")) info_lines = [ " === Resolution change ===", "To perform it you'll need:", "valA and valB from option 2" ] info_lines_example = [ "//valA example: ' 0x00196210: 0x72ccf45f '_..r' - movs.w a3, #0x198' [@424324line]", "//valB example: ' 0x0019620c: 0x7134f45f '_.4q' - movs.w a2, #0x2d0' [@424323line]" ] for line in info_lines: self.debug.print(line, color=Fore.YELLOW) for line in info_lines_example: self.debug.print(line, color=Fore.BLUE) resolution_info_lines = [ " === Following resolutions are supported ===", "960x544, 720x408, 640x368, 480x272" ] for line in resolution_info_lines: self.debug.print(line) default_resx = self.debug.ask( "What was the default width resolution? (ex *720*x408)") default_resy = self.debug.ask( "What was the default height resolution? (ex 720x*408*)") default_resolution_lines = [ "=== Default resolutions ===", "Width: {0}, search for value: {1}".format(str(default_resx), hex(int(default_resx))), "Height: {0}, search for value: {1}".format( str(default_resy), hex(int(default_resy))) ] for line in default_resolution_lines: self.debug.print(line) supported_resolutions_lines = [ "=== Supported resolutions ===", "960 | 544", "720 | 408", "640 | 368", "480 | 272" ] for line in supported_resolutions_lines: self.debug.print(line) new_resx = self.debug.ask("What is new width resolution (ex 640)") new_resy = self.debug.ask("What is new height resolution (ex 368)") update_resolution_lines = [ "Change in valA and valB: {0} to {1} and {2} to {3}".format( hex(int(default_resx)), hex(int(new_resx)), hex(int(default_resy)), hex(int(new_resy))), "Then, visit: http://armconverter.com and select x32 - ARM32/AArch32/ARMv7 Converter and then paste function of valA and then of valB", "Get Thumb-2 HEX from this website" ] update_resolution_lines_example = [ "//example: copy 'movs.w a1, #0x198' to the site and get Thumb-2 HEX" ] for line in update_resolution_lines: self.debug.print(line) for line in update_resolution_lines_example: self.debug.print(line, color=Fore.BLUE) offset_lines = [ "Get offset of height and width instruction (0xOFFSET)" ] offset_lines_example = [ "//example: 0xOFFSET: 0xInstrOff '_..p' - movs.w a1, #0x198" ] for line in offset_lines: self.debug.print(line) for line in offset_lines_example: self.debug.print(line, color=Fore.BLUE) offset_width = self.debug.ask("Offset of width instruction") thumb2_width = self.debug.ask("Thumb2 HEX of width instruction") offset_height = self.debug.ask("Offset of height instruction") thumb2_height = self.debug.ask("Thumb2 HEX of height instruction") offset2_lines = [ "You have 2 methods", "1. Patch it yourself using hxd", "2. Let program patch it" ] for line in offset2_lines: self.debug.print(line) input_selection = self.debug.ask("Selected method") if (int(input_selection) is 2): self.newResolutionPatch(input_eboot, offset_width, thumb2_width, offset_height, thumb2_height) else: offset3_lines = [ "Open hex editor (Edit hex, not characters!):", "1. Go to {0} and enter {1}".format(offset_width, thumb2_width), "2. Go to {0} and enter {1}".format(offset_height, thumb2_height) ] for line in offset3_lines: self.debug.print(line) self.debug.letWait() self.debug.print( "All actions performed, copy patched eboot.bin and try to run game!" ) def newResolutionPatch(self, file, offset_w, thumb2_w, offset_h, thumb2_h): summary_lines = [ "Will patch {0} at offsets: {1} and {2} with values {3} and {4}". format(file, offset_w, offset_h, thumb2_w, thumb2_h) ] for line in summary_lines: self.debug.print(line) #binascii.a2b_hex("THUMB2") offset_w = int(offset_w, 16) offset_h = int(offset_h, 16) self.debug.print("Writing thumb2 of width!") f = open(file, "r+b") f.seek(offset_w) f.write(binascii.a2b_hex(thumb2_w)) f.close() self.debug.print("OK!") self.debug.print("Writing thumb2 of height!") f = open(file, "r+b") f.seek(offset_h) f.write(binascii.a2b_hex(thumb2_h)) f.close() self.debug.print("OK!") def knownResGames(self): self.debug.print("Known games!") for x in range(0, len(values.known_games)): info = "Game: '{0}' | mod for '{1}' | change (offset, value): '{2}' and '{3}'".format( values.known_games[x], values.known_mod[x], values.known_offsetandval_w[x], values.known_offsetandval_h[x]) self.debug.print(info)
from k4czp3r_psvitares import VitaResFinder from debug import Debug vrf = VitaResFinder() debug=Debug() main_loop=True while main_loop: debug.clearScreen() input_action=vrf.askForAction() #1. info decompiling, 2. res search, 3. try: int(input_action) except: debug.printError("Selection is not valid!") if int(input_action) is 1: vrf.showDecompileHelp() elif int(input_action) is 2: vrf.resSearchMain() elif int(input_action) is 3: vrf.newResolution() elif int(input_action) is 4: vrf.knownResGames() elif int(input_action) is 0: break else: debug.printError("Selection not valid!") debug.letWait() debug.print("Thanks for using it!") debug.print("K4CZP3R, 2018")