Compare commits

..

No commits in common. '44640c942757bb2fc6d68b76fa8ade8d7bf141b2' and 'cb28f4fcf954c7cc856439c1b77c966f615bdc14' have entirely different histories.

@ -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,9 +236,9 @@ 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()
@ -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,8 +274,7 @@ 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)
@ -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}\" ...")

@ -4,5 +4,5 @@ from ModManager import ModManager
# Press the green button in the gutter to run the script. # Press the green button in the gutter to run the script.
if __name__ == '__main__': if __name__ == '__main__':
m = ModManager(log_level=logging.DEBUG) m = ModManager(log_level=logging.INFO)
m.run() m.run()

@ -1,7 +1,7 @@
import logging import logging
import os.path import os.path
import sys import sys
from typing import Dict, List, Tuple from typing import Dict, List
from PyQt6 import QtWidgets from PyQt6 import QtWidgets
from PyQt6.QtCore import Qt from PyQt6.QtCore import Qt
@ -42,25 +42,21 @@ class MainWindow(QtWidgets.QMainWindow):
self.ui.actionCheck_for_Updates.setDisabled(True) self.ui.actionCheck_for_Updates.setDisabled(True)
def set_available_mods(self, available_mods: Dict[Tuple[str, str], Dict]): def set_available_mods(self, available_mods: Dict[str, str]):
""" """
Sets the given mods as the list of available mods Sets the given mods as the list of available mods
:param available_mods: :param available_mods:
:return: :return:
""" """
keys = [(name + "|" + version, (name, version)) for name, version in available_mods]
keys.sort(key=lambda x: x[0])
item_model = QStandardItemModel(self.ui.AvailableModsList) item_model = QStandardItemModel(self.ui.AvailableModsList)
for key in keys: for mod_name in available_mods.keys():
mod_name, mod_version = key[1][0], key[1][1] item = QStandardItem(mod_name + f" ({available_mods[mod_name]['version']})")
item = QStandardItem(mod_name + f" ({mod_version})")
item.setFlags(Qt.ItemFlag.ItemIsUserCheckable | Qt.ItemFlag.ItemIsEnabled) item.setFlags(Qt.ItemFlag.ItemIsUserCheckable | Qt.ItemFlag.ItemIsEnabled)
item.setData(Qt.CheckState.Unchecked, Qt.ItemDataRole.CheckStateRole) item.setData(Qt.CheckState.Unchecked, Qt.ItemDataRole.CheckStateRole)
item.setData((mod_name, mod_version), Qt.ItemDataRole.UserRole) item.setData(mod_name, Qt.ItemDataRole.UserRole)
item_model.appendRow(item) item_model.appendRow(item)
if self.parent.is_mod_installed(mod_name, mod_version): if self.parent.is_mod_installed(mod_name):
item.setCheckState(Qt.CheckState.Checked) item.setCheckState(Qt.CheckState.Checked)
item_model.itemChanged.connect(self.on_available_mod_item_changed) item_model.itemChanged.connect(self.on_available_mod_item_changed)
@ -72,20 +68,16 @@ class MainWindow(QtWidgets.QMainWindow):
:param installed_mods: string list of all the mods as to be written to the list :param installed_mods: string list of all the mods as to be written to the list
:return: :return:
""" """
keys = [(name + "|" + version, (name, version)) for name, version in installed_mods]
keys.sort(key=lambda x: x[0])
item_model = QStandardItemModel(self.ui.InstalledModsListView) item_model = QStandardItemModel(self.ui.InstalledModsListView)
for key in keys: for mod_name in installed_mods.keys():
mod_name, mod_version = key[1][0], key[1][1] if mod_name in self.parent.available_mods.keys():
if (mod_name, mod_version) in self.parent.available_mods.keys(): mod_version = self.parent.available_mods[mod_name]['version']
mod_version = self.parent.available_mods[(mod_name, mod_version)]['version']
else: else:
mod_version = "Not Tracked" mod_version = "Not Tracked"
item = QStandardItem(mod_name + f" ({mod_version})") item = QStandardItem(mod_name + f" ({mod_version})")
item.setFlags(Qt.ItemFlag.ItemIsUserCheckable | Qt.ItemFlag.ItemIsEnabled) item.setFlags(Qt.ItemFlag.ItemIsUserCheckable | Qt.ItemFlag.ItemIsEnabled)
item.setData(Qt.CheckState.Unchecked, Qt.ItemDataRole.CheckStateRole) item.setData(Qt.CheckState.Unchecked, Qt.ItemDataRole.CheckStateRole)
item.setData((mod_name, mod_version), Qt.ItemDataRole.UserRole) item.setData(mod_name, Qt.ItemDataRole.UserRole)
item_model.appendRow(item) item_model.appendRow(item)
item.setCheckState(Qt.CheckState.Checked) item.setCheckState(Qt.CheckState.Checked)
@ -102,16 +94,12 @@ class MainWindow(QtWidgets.QMainWindow):
result = dialog.getOpenFileName(filter='ZIP (*.zip)') result = dialog.getOpenFileName(filter='ZIP (*.zip)')
self.__logger.debug(f"user selected \"{result[0]}\"") self.__logger.debug(f"user selected \"{result[0]}\"")
if result == ('', ''):
self.__logger.debug("Action: \"add new mod\" was cancelled!")
return
if not os.path.isfile(result[0]): if not os.path.isfile(result[0]):
dialog = QtWidgets.QMessageBox() dialog = QtWidgets.QMessageBox()
dialog.setWindowTitle("Not a file") dialog.setWindowTitle("Not a file")
dialog.setInformativeText(f"The given file \"{result}\" did not look like a file!") dialog.setInformativeText(f"The given file \"{result}\" did not look like a file!")
dialog.setIcon(QtWidgets.QMessageBox.Icon.Warning) dialog.setIcon(QtWidgets.QMessageBox.Icon.Warning)
dialog.exec() dialog.exec()
return
if not self.parent.is_valid_mod_file(result[0]): if not self.parent.is_valid_mod_file(result[0]):
dialog = QtWidgets.QMessageBox() dialog = QtWidgets.QMessageBox()
dialog.setWindowTitle("Not a valid mod file") dialog.setWindowTitle("Not a valid mod file")
@ -119,7 +107,6 @@ class MainWindow(QtWidgets.QMainWindow):
f"The given file \"{result}\" did not look like a mod file. Is the manifest.json present?") f"The given file \"{result}\" did not look like a mod file. Is the manifest.json present?")
dialog.setIcon(QtWidgets.QMessageBox.Icon.Warning) dialog.setIcon(QtWidgets.QMessageBox.Icon.Warning)
dialog.exec() dialog.exec()
return
self.parent.add_mod_file(result[0]) self.parent.add_mod_file(result[0])
@ -179,20 +166,20 @@ class MainWindow(QtWidgets.QMainWindow):
def on_available_mod_item_changed(self, item: QStandardItem): def on_available_mod_item_changed(self, item: QStandardItem):
self.__logger.debug(f"Available Mod list item \"{item.text()}\" changed to {item.checkState()}") self.__logger.debug(f"Available Mod list item \"{item.text()}\" changed to {item.checkState()}")
mod_name, mod_version = item.data(Qt.ItemDataRole.UserRole) mod_name = item.data(Qt.ItemDataRole.UserRole)
if item.checkState() == Qt.CheckState.Checked: if item.checkState() == Qt.CheckState.Checked:
self.parent.install_mod(mod_name, mod_version) self.parent.install_mod(mod_name)
elif item.checkState() == Qt.CheckState.Unchecked: elif item.checkState() == Qt.CheckState.Unchecked:
self.parent.uninstall_mod(mod_name, mod_version) self.parent.uninstall_mod(mod_name)
def on_installed_mod_item_changed(self, item: QStandardItem): def on_installed_mod_item_changed(self, item: QStandardItem):
self.__logger.debug(f"Installed Mod list item \"{item.text()}\" changed to {item.checkState()}") self.__logger.debug(f"Installed Mod list item \"{item.text()}\" changed to {item.checkState()}")
mod_name, mod_version = item.data(Qt.ItemDataRole.UserRole) mod_name = item.data(Qt.ItemDataRole.UserRole)
if item.checkState() == Qt.CheckState.Unchecked: # mod should be uninstalled if item.checkState() == Qt.CheckState.Unchecked: # mod should be uninstalled
accepted = QtWidgets.QMessageBox.question(self, "Really uninstall mod?", accepted = QtWidgets.QMessageBox.question(self, "Really uninstall mod?",
f"Do you really want to uninstall the mod \"{mod_name}\"?\n" f"Do you really want to uninstall the mod \"{mod_name}\"?\n"
"This could lead to permanent data loss if it wasn't tracked!") "This could lead to permanent data loss if it wasn't tracked!")
if accepted: if accepted:
self.parent.uninstall_mod(mod_name, mod_version) self.parent.uninstall_mod(mod_name)
else: else:
return return

Loading…
Cancel
Save