@ -17,8 +17,7 @@ from window.main_window import MainWindow
class ModManager :
VERSION = " 0.1 "
VERSION = " 0.2 "
def __init__ ( self , log_level : int = logging . INFO ) :
self . __logger = logging . getLogger ( " ModManager " )
@ -63,17 +62,36 @@ class ModManager:
status = self . __app . exec ( )
sys . exit ( status )
def __adjust_folder_paths ( self , path : str ) - > str :
@staticmethod
def get_mod_key ( mod_name : str , version : str ) - > Tuple [ str , str ] :
"""
Creates the mod_key used for referencing this mod in the dictionaries
: param mod_name :
: param version :
: return :
"""
return mod_name , version
@staticmethod
def __adjust_folder_paths ( path : str , includes_bepinx = True , includes_folders = True ) - > str :
"""
Adjusts a given path containing common issues to one compatible with BepInEx
: param input :
: param path :
: param includes_bepinx : bool toggle if the mod archive included paths with " BepInEx " , if not it will be added
: param includes_folders : bool toggle if the mod archive included paths with folders , if not within plugins will be assumed
: return :
"""
if not includes_bepinx :
if path . startswith ( " config " ) or path . startswith ( " plugins " ) :
path = os . path . join ( " BepInEx " , path )
else :
path = os . path . join ( " BepInEx " , " plugins " , path )
if path . endswith ( " .dll " ) and " BepInEx " not in path :
path = os . path . join ( " BepInEx " , " plugins " , path )
if path . startswith ( " config " + os . path . sep ) or path . startswith ( " plugins " + os . path . sep ) :
path = os . path . join ( " BepInEx " , path )
# if path.startswith("config" + os.path.sep) or path.startswith("plugins" + os.path.sep) :
# path = os.path.join("BepInEx", path)
if path . lower ( ) . startswith ( " bepinex " ) and not path . startswith ( " BepInEx " ) :
path = " BepInEx " + path [ len ( " BepinEx " ) : ]
@ -131,26 +149,31 @@ class ModManager:
self . index_installed_mods ( )
return is_valid
def is_mod_installed ( self , mod_name : str ) - > bool :
def is_mod_installed ( self , mod_name : str , mod_version : str ) - > bool :
"""
Checks if a given mod has been installed to the game already
: param mod_name :
: param mod_version :
: return :
"""
r = self . available_mods [ mod_name ]
r = self . available_mods [ ( mod_name , mod_version ) ]
mod_files = r [ " mod_files " ]
for i in range ( len ( mod_files ) ) :
file = mod_files [ i ]
orig_file = r [ " orig_mod_files " ] [ i ]
if not os . path . exists ( os . path . join ( self . __settings . get_game_folder ( ) , file ) ) :
self . __logger . debug ( f " Checking if mod \" { mod_name } - { mod_version } \" is installed ... FALSE " )
return False
else :
if os . path . isfile ( os . path . join ( self . __settings . get_game_folder ( ) , file ) ) :
hash_installed = self . get_file_hash ( open ( os . path . join ( self . __settings . get_game_folder ( ) , file ) , ' rb ' ) )
modzip = os . path . join ( self . __settings . get_mod_folder ( ) , self . available_mods [ mod_name ] [ " path " ] )
hash_installed = self . get_file_hash (
open ( os . path . join ( self . __settings . get_game_folder ( ) , file ) , ' rb ' ) )
modzip = os . path . join ( self . __settings . get_mod_folder ( ) , self . available_mods [ ( mod_name , mod_version ) ] [ " path " ] )
hash_in_storage = self . get_file_hash ( ZipFile ( modzip ) . open ( orig_file , ' r ' ) )
if hash_installed != hash_in_storage :
self . __logger . debug ( f " Checking if mod \" { mod_name } - { mod_version } \" is installed ... FALSE " )
return False
self . __logger . debug ( f " Checking if mod \" { mod_name } - { mod_version } \" is installed ... TRUE " )
return True
def is_valid_mod_file ( self , file_path : str ) :
@ -187,12 +210,13 @@ class ModManager:
md5_obj . update ( buffer )
return md5_obj . hexdigest ( )
def get_mod_info ( self , file_path : str ) - > Tuple [ str , str , L ist[ str ] ] :
def get_mod_info ( self , file_path : str ) - > tuple [ str , str , list [ str ] , l ist[ str ] ] :
"""
Returns the name and version string of a given mod file
: param file_path :
: return :
"""
self . __logger . debug ( f " Trying to get mod info of file \" { file_path } \" ... " )
if not self . is_valid_mod_file ( file_path ) :
self . __logger . error ( f " Tried to get mod info of an invalid file: { file_path } " )
raise ValueError ( f " Tried to get mod info of an invalid file: { file_path } " )
@ -201,13 +225,21 @@ class ModManager:
f = zip . open ( ' manifest.json ' )
contents = zip . namelist ( )
contains_bepinex = False
contains_folders = False
for i in range ( len ( contents ) ) :
if " / " in contents [ i ] :
contains_folders = True
if contents [ i ] . lower ( ) . startswith ( " bepinex " ) :
contains_bepinex = True
orig_files = [ ]
files = [ ]
for file in contents :
if " icon.png " in file or " manifest.json " in file or file . endswith ( " .md " ) :
continue
orig_files . append ( file )
file = self . __adjust_folder_paths ( file )
file = self . __adjust_folder_paths ( file , includes_bepinx = contains_bepinex , includes_folders = contains_folders )
files . append ( file )
@ -221,6 +253,7 @@ class ModManager:
For unknown mods a placeholder is created
: return :
"""
self . __logger . debug ( " Indexing all installed mods ... " )
self . installed_mods = dict ( )
if self . __settings . get_game_folder ( ) is None :
return
@ -236,9 +269,9 @@ class ModManager:
elif os . path . isfile ( curr ) and curr . endswith ( ' .dll ' ) :
files . append ( curr )
for mod_name in self . available_mods . keys ( ) :
if self . is_mod_installed ( mod_name ):
self . installed_mods [ mod_name ] = self . available_mods [ mod_name ]
for mod_name , mod_version in self . available_mods . keys ( ) :
if self . is_mod_installed ( mod_name , mod_version ):
self . installed_mods [ ( mod_name , mod_version ) ] = self . available_mods [ ( mod_name , mod_version ) ]
unknown_mod = dict ( )
unresolved_files = files . copy ( )
@ -262,6 +295,7 @@ class ModManager:
Ignores mods that are already on the list
: return :
"""
self . __logger . debug ( " Indexing all stored mods ... " )
self . available_mods = dict ( )
if self . __settings . get_game_folder ( ) is None :
return
@ -274,7 +308,8 @@ class ModManager:
self . __logger . warning ( f " Mod \" { full_path } \" did not have the expected path. Ignoring it ... " )
continue
self . available_mods [ mod_name ] = { " path " : file , " version " : mod_version , " mod_files " : mod_files ,
self . available_mods [ ModManager . get_mod_key ( mod_name , mod_version ) ] = { " path " : file , " version " : mod_version ,
" mod_files " : mod_files ,
" orig_mod_files " : orig_mod_files }
self . __window . set_available_mods ( self . available_mods )
@ -284,6 +319,7 @@ class ModManager:
: param file_path :
: return :
"""
self . __logger . debug ( f " Trying to add mod file \" { file_path } \" to manager ... " )
if self . __settings . get_settings ( ) [ " game_path " ] is None :
self . __logger . error ( " Can ' t add a mod without a valid game path! " )
@ -309,55 +345,70 @@ class ModManager:
self . index_stored_mods ( )
self . index_installed_mods ( )
def install_mod ( self , mod_name : str ):
def install_mod ( self , mod_name : str , mod_version : str ):
"""
Installs the given mod by extracting all files into the game directory .
This assumes that the . zip is structured like the game folder as all mods should be
: param mod_name :
: param mod_version :
: return :
"""
if mod_name not in self . available_mods . keys ( ) :
self . __logger . critical ( f " Tried to install a mod that doesn ' t exist: { mod_name } " )
self . __logger . debug ( f " Trying to install mod \" { mod_name } - { mod_version } \" " )
if ( mod_name , mod_version ) not in self . available_mods . keys ( ) :
self . __logger . critical ( f " Tried to install a mod that doesn ' t exist: { mod_name } - { mod_version } " )
return
self . __logger . info ( f " Installing mod \" { mod_name } \" ... " )
mod_zip = self . available_mods [ mod_name ] [ " path " ]
self . __logger . info ( f " Installing mod \" { mod_name } - { mod_version } \" ... " )
mod_zip = self . available_mods [ ( mod_name , mod_version ) ] [ " path " ]
with ZipFile ( os . path . join ( self . __settings . get_mod_folder ( ) , mod_zip ) , ' r ' ) as zip_ref :
#zip_ref.extractall(self.__settings.get_game_folder())
contents = zip_ref . namelist ( )
contains_bepinex = False
contains_folders = False
for i in range ( len ( contents ) ) :
if " / " in contents [ i ] :
contains_folders = True
if contents [ i ] . lower ( ) . startswith ( " bepinex " ) :
contains_bepinex = True
with tempfile . TemporaryDirectory ( ) as tmp_dir :
for file in contents :
content = file
if " icon.png " in file or " manifest.json " in file or file . endswith ( " .md " ) :
continue
file = self . __adjust_folder_paths ( file )
file = self . __adjust_folder_paths ( file , includes_bepinx = contains_bepinex ,
includes_folders = contains_folders )
#print("Extracting", content, "to", tmp_dir)
zip_ref . extract ( content , tmp_dir )
if content . endswith ( os . path . sep ) :
#print("Skipped moving", os.path.join(tmp_dir, content) )
self . __logger . debug ( f " Skipped moving of { os . path . join ( tmp_dir , content ) } " )
continue
#print("Moving", os.path.join(tmp_dir, content), "to",
# os.path.join(self.__settings.get_game_folder(), file))
parent_dir = os . path . join ( self . __settings . get_game_folder ( ) . replace ( " / " , os . path . sep ) ,
file . replace ( " / " , os . path . sep ) ) . split ( file . replace ( " / " , os . path . sep ) ) [ 0 ]
if not os . path . exists ( parent_dir ) :
os . mkdir ( parent_dir )
if not os . path . exists ( os . path . join ( self . __settings . get_game_folder ( ) . replace ( " / " , os . path . sep ) , file . replace ( " / " , os . path . sep ) ) ) :
shutil . move ( os . path . join ( tmp_dir , content . replace ( " / " , os . path . sep ) ) , os . path . join ( self . __settings . get_game_folder ( ) . replace ( " / " , os . path . sep ) , file . replace ( " / " , os . path . sep ) ) )
if not os . path . exists ( os . path . join ( self . __settings . get_game_folder ( ) . replace ( " / " , os . path . sep ) ,
file . replace ( " / " , os . path . sep ) ) ) :
self . __logger . debug (
f " Extracting file \" { os . path . join ( tmp_dir , content . replace ( ' / ' , os . path . sep ) ) } \" to " +
f " \" { os . path . join ( self . __settings . get_game_folder ( ) . replace ( ' / ' , os . path . sep ) , file . replace ( ' / ' , os . path . sep ) ) } \" " )
shutil . move ( os . path . join ( tmp_dir , content . replace ( " / " , os . path . sep ) ) ,
os . path . join ( self . __settings . get_game_folder ( ) . replace ( " / " , os . path . sep ) ,
file . replace ( " / " , os . path . sep ) ) )
self . index_installed_mods ( )
def uninstall_mod ( self , mod_name : str ) :
def uninstall_mod ( self , mod_name : str , mod_version : str ):
"""
Uninstalls the given mod by removing all files ( not folders ) that the manager is aware of
Note : For untracked mods this will only be the . dll in the plugins folder
: param mod_name :
: param mod_version :
: return :
"""
self . __logger . info ( f " Uninstalling mod \" { mod_name } \" ... " )
for file in self . installed_mods [ mod_name ] [ " mod_files " ] :
self . __logger . info ( f " Uninstalling mod \" { mod_name } - { mod_version } \" ... " )
for file in self . installed_mods [ ( mod_name , mod_version ) ] [ " mod_files " ] :
full_path = os . path . join ( self . __settings . get_game_folder ( ) , file )
if os . path . isfile ( full_path ) :
self . __logger . debug ( f " Deleting file \" { full_path } \" ... " )