2024-12-03 18:31:18 +00:00
import os
import time
2024-12-03 22:50:34 +00:00
import sys
2024-12-03 18:31:18 +00:00
from datetime import datetime
2024-12-10 13:33:28 +00:00
2024-12-10 14:38:00 +01:00
version = " 0.5.3 " # actully 0.5.2 because 0.5.1 was just pushed
2024-12-03 22:50:34 +00:00
gitlab_url = " https://gitlab.com/python_projects3802849/ftl-save-manager/-/raw/main "
settings_location = " settings.txt "
2024-12-06 17:12:31 +00:00
dependencies = ( " simple_term_menu " , " requests " )
if sys . platform != " linux " :
print ( f " { sys . platform } is not supported, only linux. " )
exit ( )
def check_package_installed ( package_name ) :
""" Check if a package is installed. """
try :
__import__ ( package_name )
return True
except ImportError as e :
print ( f " { package_name } not found. Error: { e } " )
return False
for item in dependencies :
if check_package_installed ( item ) == False :
print ( f " One or more dependencies are missing, please ensure follwing packaged are installed: \n requests \n simple-term-menu " )
exit ( )
2024-12-10 13:33:28 +00:00
import requests
2024-12-06 17:12:31 +00:00
from simple_term_menu import TerminalMenu
2024-12-03 18:31:18 +00:00
class BackupApp :
""" " Backup class, copy FTL continue file to backup location and restores it. """
2024-12-10 13:33:28 +00:00
def __init__ ( self ) :
2024-12-03 18:31:18 +00:00
self . backup_files = { " 1 " : " initiating " } # initiating variable
2024-12-10 13:33:28 +00:00
self . game_path = configuration . settings [ " GAME_PATH " ]
self . save_path = configuration . settings [ " SAVE_PATH " ]
2024-12-03 18:31:18 +00:00
def backup ( self ) :
""" Copies the game file to the backup folder and adds date-time stamp. """
now = datetime . now ( )
2024-12-10 13:33:28 +00:00
formatted_now = now . strftime ( " % Y- % m- %d _ % H- % M- % S " )
u . copy_file ( f " { self . game_path } /continue.sav " , f " { self . save_path } / { formatted_now } .bkup " )
2024-12-03 22:50:34 +00:00
time . sleep ( 0.5 )
2024-12-03 18:31:18 +00:00
2024-12-06 17:12:31 +00:00
def restore_backup ( self , file ) :
2024-12-10 13:33:28 +00:00
u . copy_file ( f " { self . save_path } / { file } " , f " { self . game_path } /continue.sav " )
2024-12-03 18:31:18 +00:00
2024-12-10 13:33:28 +00:00
def delete_backup_file ( self ) : # improvment: here also, more generic, delete_files with file type .something
2024-12-03 18:31:18 +00:00
""" Deltes all .bkup files in the backup folder. """
2024-12-10 13:33:28 +00:00
if u . yes_no ( " Are you sure you want to delete all save files? " ) :
for file in os . listdir ( self . save_path ) :
2024-12-03 18:31:18 +00:00
if file [ - 5 : ] == " .bkup " :
try :
print ( f " Deleting { file } . " )
2024-12-10 13:33:28 +00:00
os . remove ( f " { self . save_path } / { file } " )
2024-12-03 22:50:34 +00:00
time . sleep ( 0.3 )
2024-12-03 18:31:18 +00:00
except Exception as e :
print ( f " Failed to delete { file } . Reason: { e } . " )
2024-12-10 13:33:28 +00:00
time . sleep ( 1 )
else :
print ( " Deleting save files canceled " )
time . sleep ( 0.5 )
2024-12-03 18:31:18 +00:00
2024-12-10 13:33:28 +00:00
class Updater :
2024-12-03 22:50:34 +00:00
""" Update local version from gitlab repo. """
2024-12-10 13:33:28 +00:00
def __init__ ( self , version , local_file ) :
2024-12-03 22:50:34 +00:00
self . current_version = version
2024-12-10 13:33:28 +00:00
self . update_url = configuration . settings [ " REPO_URL " ]
2024-12-03 22:50:34 +00:00
self . local_file = local_file
self . git = os . path . exists ( " .gitignore " ) # Checking if the program inside git env
2024-12-10 13:33:28 +00:00
self . auto_check ( )
def auto_check ( self ) :
print ( " Auto update check " )
check_interval = configuration . settings [ " AUTO_UPDATE_CHECK_INTERVAL_H " ]
if check_interval == 0 :
print ( " Auto update disabeld " )
return # Exiting
if " LAST_UPDATE_CHECK " in configuration . settings :
last_time = configuration . settings [ " LAST_UPDATE_CHECK " ]
t_object = datetime . strptime ( last_time , " % Y- % m- %d % H- % M " )
try :
h_since_update_check = u . get_difference_in_hours ( datetime . now ( ) , t_object )
if h_since_update_check > int ( configuration . settings [ " AUTO_UPDATE_CHECK_INTERVAL_H " ] ) :
print ( f " More then { configuration . settings [ " AUTO_UPDATE_CHECK_INTERVAL_H " ] } hours since last update check, checking now... " )
self . check_for_update ( 0 )
except ValueError :
print ( " Invalid timestamp format for LAST_UPDATE_CHECK. " )
input ( " Error checking for updates: \n Enter to continue... " )
else :
self . check_for_update ( 0 )
2024-12-03 22:50:34 +00:00
2024-12-10 13:33:28 +00:00
def check_for_update ( self , verbose = 1 ) :
2024-12-03 22:50:34 +00:00
""" Check if a new version is available. """
try :
2024-12-10 13:33:28 +00:00
if verbose : print ( " Checking for updates... " )
2024-12-03 22:50:34 +00:00
response = requests . get ( f " { self . update_url } /latest_version.txt " )
response . raise_for_status ( )
2024-12-10 13:33:28 +00:00
self . update_info = self . read_version_file ( response )
configuration . settings [ " LAST_ONLINE_VERSION " ] = self . update_info [ " LATEST_VERSION " ] # updating the version number from update class to global var
now = datetime . now ( )
formatted_now = now . strftime ( " % Y- % m- %d % H- % M " )
configuration . settings [ " LAST_UPDATE_CHECK " ] = formatted_now
if self . compare_str_SemVer ( self . current_version , self . update_info [ " LATEST_VERSION " ] ) :
if verbose :
print ( f " New version { self . update_info [ " LATEST_VERSION " ] } available online. \n Current version: { self . current_version } " )
print ( f " \n Update comment: \n { self . update_info [ " COMMENT " ] } \n " )
2024-12-03 22:50:34 +00:00
return True
else :
2024-12-10 13:33:28 +00:00
if verbose :
print ( f " You are up-to-date ( { self . current_version } ). \n Latest version is: { self . update_info [ " LATEST_VERSION " ] } . " )
input ( " Enter to continue... " )
2024-12-03 22:50:34 +00:00
return False
except Exception as e :
2024-12-10 13:33:28 +00:00
if verbose : print ( f " Error checking for updates: { e } " )
input ( " UPDATE ERROR Press Enter to continue... " )
2024-12-03 22:50:34 +00:00
return False
2024-12-10 13:33:28 +00:00
def read_version_file ( self , response ) : # does basicly the same as the read settings function in settingmanger class, optimizing would combine them.
settings = { }
lines = response . text . splitlines ( )
for line in lines :
if line . startswith ( " # " ) or " = " not in line :
continue
key , value = map ( str . strip , line . split ( " = " , 1 ) )
if value . startswith ( ' " ' ) and value . endswith ( ' " ' ) :
settings [ key ] = value [ 1 : - 1 ]
return settings
def compare_str_SemVer ( self , first , second ) :
2024-12-03 22:50:34 +00:00
""" Compare two Semantic versioning strings. """
2024-12-10 13:33:28 +00:00
local_version = self . convert_str_list_to_int ( first . split ( " . " ) )
online_version = self . convert_str_list_to_int ( second . split ( " . " ) )
2024-12-03 22:50:34 +00:00
return local_version < online_version
def convert_str_list_to_int ( self , list_str ) :
""" Converts a list with strings to a list with intengers. """
return [ int ( i ) for i in list_str ]
2024-12-10 13:33:28 +00:00
def download_update ( self ) : # improvment: silly it takes the latest_version as variable
2024-12-03 22:50:34 +00:00
""" Download the new version and overwrite the current file. """
try :
2024-12-10 13:33:28 +00:00
print ( f " Downloading version { self . update_info [ " LATEST_VERSION " ] } ... " )
time . sleep ( 2 )
2024-12-03 22:50:34 +00:00
response = requests . get ( f " { self . update_url } / { self . local_file } " )
response . raise_for_status ( )
with open ( self . local_file , " wb " ) as file :
file . write ( response . content )
print ( " Update downloaded. " )
return True
except Exception as e :
print ( f " Error downloading update: { e } " )
return False
def restart_program ( self ) :
""" Restart the current program. """
print ( " Restarting the program... " )
os . execv ( sys . executable , [ " python " ] + sys . argv )
def run_update ( self ) :
""" Main method to check, update, and restart. """
if self . git :
print ( " Updating only works outside git env. " )
2024-12-06 17:12:31 +00:00
time . sleep ( 2 )
2024-12-03 22:50:34 +00:00
else :
2024-12-10 13:33:28 +00:00
if self . check_for_update ( ) :
if u . yes_no ( " Do you want to update the program? " ) :
success = self . download_update ( )
2024-12-03 22:50:34 +00:00
if success :
self . restart_program ( )
else :
print ( " Update cancelled " )
return
2024-12-06 17:12:31 +00:00
class TerminalUI :
def __init__ ( self , title , options ) :
self . main_menu_title = title
self . main_menu_options = options
self . main_menu_keys = list ( self . main_menu_options . keys ( ) ) # terminal ui takes a list for the menu
self . main_menu = TerminalMenu (
menu_entries = self . main_menu_keys ,
title = self . main_menu_title ,
menu_cursor = " > " ,
menu_cursor_style = ( " fg_gray " , " bold " ) ,
menu_highlight_style = ( " bg_gray " , " fg_black " ) ,
cycle_cursor = True ,
2024-12-10 13:33:28 +00:00
clear_screen = True
2024-12-06 17:12:31 +00:00
)
def show_main_menu ( self ) :
2024-12-10 13:33:28 +00:00
# Adding try makes it possiable to add code after the True loop
try :
while True :
selected_index = self . main_menu . show ( )
selected_key = None
if isinstance ( selected_index , int ) :
selected_key = self . main_menu_keys [ selected_index ]
if selected_index == None :
break
elif selected_key == " [l] Load " :
self . restore_file_menu ( app . save_path )
elif selected_key == " [c] Change settings " :
self . change_settings_menu ( )
elif selected_key == " [p] Profile managment " :
self . profile_menu ( )
elif selected_key in self . main_menu_options :
self . main_menu_options [ selected_key ] ( )
finally :
configuration . write_settings_file ( ) # writing settings when exiting.
2024-12-06 17:12:31 +00:00
def restore_file_menu ( self , folder ) :
menu_options = list ( self . list_files ( folder ) )
folder_menu = TerminalMenu ( menu_entries = menu_options ,
2024-12-10 13:33:28 +00:00
title = f " Content of { folder } (Q or Esc to go back). \n " ,
2024-12-06 17:12:31 +00:00
menu_cursor = " > " ,
menu_cursor_style = ( " fg_red " , " bold " ) ,
menu_highlight_style = ( " bg_gray " , " fg_black " ) ,
cycle_cursor = True ,
2024-12-10 13:33:28 +00:00
clear_screen = False ,
)
2024-12-06 17:12:31 +00:00
menu_entry_index = folder_menu . show ( )
if menu_entry_index == None :
return
else :
app . restore_backup ( menu_options [ menu_entry_index ] )
def list_files ( self , directory ) :
return ( file for file in sorted ( os . listdir ( directory ) , reverse = True ) if os . path . isfile ( os . path . join ( directory , file ) ) and file . endswith ( ( " .bkup " ) ) )
def yes_no_menu ( self , message ) : # oh
menu_options = [ " [y] yes " , " [n] no " ]
menu = TerminalMenu ( menu_entries = menu_options ,
title = f " { message } " ,
menu_cursor = " > " ,
menu_cursor_style = ( " fg_red " , " bold " ) ,
menu_highlight_style = ( " bg_gray " , " fg_black " ) ,
cycle_cursor = True ,
2024-12-10 13:33:28 +00:00
clear_screen = False )
2024-12-06 17:12:31 +00:00
menu_entry_index = menu . show ( )
if menu_entry_index == 0 :
return True
elif menu_entry_index == 1 :
return False
2024-12-10 13:33:28 +00:00
def change_settings_menu ( self ) :
menu_options = [ f " [g] Game path ( { configuration . settings [ " GAME_PATH " ] } ) " ,
f " [s] Save path ( { configuration . settings [ " SAVE_PATH " ] } ) " ,
f " [a] Auto update interval ( { configuration . settings [ " AUTO_UPDATE_CHECK_INTERVAL_H " ] } ) " ,
" [q] Quite " ]
settings_option = [ " GAME_PATH " , " SAVE_PATH " , " AUTO_UPDATE_CHECK_INTERVAL_H " ]
settings_menu = TerminalMenu ( menu_entries = menu_options ,
title = " Change settings (Q or Esc to go back). \n " ,
menu_cursor = " > " ,
menu_cursor_style = ( " fg_red " , " bold " ) ,
menu_highlight_style = ( " bg_gray " , " fg_black " ) ,
cycle_cursor = True ,
clear_screen = False
)
try :
while True :
selected_index = settings_menu . show ( )
selected_key = None
if selected_index == 3 :
break
if isinstance ( selected_index , int ) :
selected_key = settings_option [ selected_index ]
if selected_index == None :
break
elif selected_key in settings_option :
if self . yes_no_menu ( f " Do you want to change { selected_key } from { configuration . settings [ selected_key ] } ? " ) :
new_value = input ( f " Please enter new value for { selected_key } : " )
configuration . settings [ selected_key ] = new_value
configuration . validate_paths_in_settings ( )
configuration . convert_str_to_int_settings ( )
else :
print ( " No changed were made. " )
finally :
configuration . write_settings_file ( ) # writing settings when exiting.
def profile_menu ( self ) :
menu_options = [ " [b] Backup profile " , " [r] Restore profile " , " [q] Quite " ]
menu = TerminalMenu ( menu_entries = menu_options ,
title = f " Backup or restore the profile file, which includes complete progress (Q or Esc to go back). \n " ,
menu_cursor = " > " ,
menu_cursor_style = ( " fg_red " , " bold " ) ,
menu_highlight_style = ( " bg_gray " , " fg_black " ) ,
cycle_cursor = True ,
clear_screen = False )
menu_entry_index = menu . show ( )
profile_path = f " { configuration . settings [ " GAME_PATH " ] } /ae_prof.sav "
backup_path = f " { configuration . settings [ " SAVE_PATH " ] } /ae_prof.sav "
if menu_entry_index == 0 :
u . copy_file_overwrite_if_newer ( profile_path , backup_path )
elif menu_entry_index == 1 :
u . copy_file_overwrite_if_newer ( backup_path , profile_path )
else :
return
class SettingManager :
""" Handles program settings, reading, validating, and writing them. """
# Refactored this class with suggestions from ChatGPT for better readability (code and language) and efficiency
def __init__ ( self , settings_path , repo_url ) :
self . accepted_settings_keys = [ " GAME_PATH " , " SAVE_PATH " , " LAST_ONLINE_VERSION " , " REPO_URL " , " LAST_UPDATE_CHECK " , " AUTO_UPDATE_CHECK_INTERVAL_H " ]
self . settings_file_path = settings_path
self . settings = self . read_settings_file ( )
# Initialize settings
if self . settings :
self . validate_paths_in_settings ( )
self . set_default_values ( repo_url )
else :
self . initialize_settings ( repo_url )
self . convert_str_to_int_settings ( )
def set_default_values ( self , repo_url ) :
default_values = { " REPO_URL " : repo_url ,
" AUTO_UPDATE_CHECK_INTERVAL_H " : " 72 "
}
for key in default_values :
if key not in self . settings :
self . settings [ key ] = default_values [ key ]
print ( f " Setting default value: { default_values [ key ] } for: { key } . " )
#time.sleep(0.5)
def convert_str_to_int_settings ( self ) :
""" Change str to int for a given keys in the settings, if not convertable then replace with default value """
int_values = { " AUTO_UPDATE_CHECK_INTERVAL_H " : 72 } # Dict for all variables which sould be intinger, and their default values. Scable for future
for key in int_values :
try :
self . settings [ key ] = int ( self . settings [ key ] )
except ValueError :
print ( f " Value: { self . settings [ key ] } could not be changed to an int. " )
self . settings [ key ] = int_values [ key ]
print ( f " Changing { key } to default value value: { int_values [ key ] } \n Acknowledged... " )
time . sleep ( 1 )
def initialize_settings ( self , repo_url ) : # URL in settings file has higher priority, is good for feature branches.
""" Initialize settings when the file doesn ' t exist or is empty. """
print ( " Initializing settings... " )
self . set_default_values ( repo_url )
self . get_paths ( )
self . write_settings_file ( )
def validate_paths_in_settings ( self ) :
""" Validate and set paths from the settings file. """
required_paths = [ " GAME_PATH " , " SAVE_PATH " ]
if all ( self . settings . get ( path ) for path in required_paths ) :
valid_game_path = u . check_path_existence ( self . settings [ " GAME_PATH " ] , " ae_prof.sav " )
valid_save_path = u . check_path_existence ( self . settings [ " SAVE_PATH " ] )
if valid_game_path and valid_save_path :
print ( " Paths validated successfully. " )
return
print ( " Invalid paths detected. Requesting user input. " )
self . get_paths ( )
self . write_settings_file ( )
def read_settings_file ( self ) :
""" Read settings from the file. """
settings = { }
if os . path . exists ( self . settings_file_path ) :
try :
with open ( self . settings_file_path , " r " ) as file :
for line in file :
if line . startswith ( " # " ) or " = " not in line :
continue
key , value = map ( str . strip , line . split ( " = " , 1 ) )
if value . startswith ( ' " ' ) and value . endswith ( ' " ' ) :
settings [ key ] = value [ 1 : - 1 ]
return settings
except Exception as e :
print ( f " Error reading settings file: { e } " )
print ( " Settings file does not exist or is invalid. " )
return settings
def write_settings_file ( self ) :
""" Write the settings dictionary to the file. """
print ( " Writing settings " )
try :
with open ( self . settings_file_path , " w " ) as file :
file . write ( " # This file is rewritten before program closes. Comments are ignored. \n # Setting AUTO_UPDATE_CHECK_INTERVAL_H = ' 0 ' disables automatic update checks. \n # DO NOT modify REPO_URL if you don ' t know what value to set, no function to fix incorrect input. \n " )
for key in self . accepted_settings_keys :
if key in self . settings :
value = self . settings [ key ]
file . write ( f ' { key } = " { value } " \n ' )
except Exception as e :
input ( f " Error writing settings file: { e } \n Enter to continue " )
def get_paths ( self ) :
""" Request and validate paths from the user. """
self . settings [ " GAME_PATH " ] = self . ask_for_path ( " Game path " , " ae_prof.sav " )
self . settings [ " SAVE_PATH " ] = self . ask_for_path ( " Save path " )
def ask_for_path ( self , description , required_file = None ) :
""" Prompt the user for a valid path. """
while True :
path = input ( f " Please enter the { description } : " )
if u . check_path_existence ( path , required_file ) :
return path
print ( " Invalid path. Please try again. " )
class Utility ( ) :
@staticmethod
def check_path_existence ( path , required_file = None ) :
""" Check if a path exists and optionally if it contains a specific file. """
if required_file :
full_path = os . path . join ( path , required_file )
return os . path . isfile ( full_path )
return os . path . isdir ( path )
@staticmethod
def get_difference_in_hours ( dt1 , dt2 ) :
diff = abs ( dt1 - dt2 )
hours_difference = int ( diff . total_seconds ( ) / 3600 )
return hours_difference
@staticmethod
def yes_no ( str ) : # improvment: have two yes_no function, should merge them
""" Ask user y/n question """
while True :
choice = input ( f " { str } (y/n): " )
if choice == " y " :
return True
elif choice == " n " :
return False
else :
print ( f " Not a valid option, try again. " )
@staticmethod
def copy_file ( src , dst ) :
""" " Simply copies a file from a to b. """
if not os . path . isfile ( src ) :
print ( f " File to copy { src } does not exist. " )
return False
os . system ( f " cp { src } { dst } " )
print ( f " Wrote: { dst } " )
@staticmethod
def copy_file_overwrite_if_newer ( source , destination ) :
# Main part arrived from opencoder ollama, modified to fit my needs
# check if source file exists
if not os . path . exists ( source ) :
print ( f " Source file { source } does not exist. " )
return False
# get modification times of source and destination files
src_mtime = os . path . getmtime ( source )
dest_mtime = 0
if os . path . exists ( destination ) :
dest_mtime = os . path . getmtime ( destination )
if src_mtime < dest_mtime :
print ( f " Destination file is newer than the source. \n ( { destination } vs { source } ) " )
# ask user permission to overwrite
if u . yes_no ( " Do you want to replace? " ) :
os . system ( f " cp { source } { destination } " )
print ( f " File { source } has been copied to { destination } " )
else :
print ( f " User chose not to replace { destination } . " )
else :
print ( f " Source file is older than or equal to the destination. \n ( { source } vs { destination } ) " )
os . system ( f " cp { source } { destination } " )
print ( f " File { source } has been copied to { destination } " )
time . sleep ( 1 )
return True
2024-12-03 18:31:18 +00:00
2024-12-10 13:33:28 +00:00
u = Utility ( )
configuration = SettingManager ( settings_location , gitlab_url )
up = Updater ( version , " ftl-savemanager.py " )
app = BackupApp ( )
update_key = " [u] Update "
if " LAST_ONLINE_VERSION " in configuration . settings :
if up . compare_str_SemVer ( version , configuration . settings [ " LAST_ONLINE_VERSION " ] ) :
update_key + = f " (v { configuration . settings [ " LAST_ONLINE_VERSION " ] } ) "
2024-12-06 17:12:31 +00:00
main_menu_options = {
2024-12-10 13:33:28 +00:00
" [s] Save " : app . backup ,
" [l] Load " : " restore_file_menu " ,
" [d] Delete save files " : app . delete_backup_file ,
f " { update_key } " : up . run_update ,
" [p] Profile managment " : " backup and restore profile " ,
" [c] Change settings " : " change:_settings_menu " ,
2024-12-06 17:12:31 +00:00
" [q] Quit " : exit
2024-12-03 18:31:18 +00:00
}
2024-12-06 17:12:31 +00:00
title = f " ftl-savemanager v { version } (Press Q or Esc to quit). \n "
tui = TerminalUI ( title , main_menu_options )
tui . show_main_menu ( )