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