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:
VERSION = "0.3"
VERSION = "0.1"
def __init__(self, log_level: int = logging.INFO):
self.__logger = logging.getLogger("ModManager")
@ -62,36 +63,17 @@ class ModManager:
status = self.__app.exec()
sys.exit(status)
@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:
def __adjust_folder_paths(self, path: str) -> str:
"""
Adjusts a given path containing common issues to one compatible with BepInEx
: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
:param input:
: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"):]
@ -149,31 +131,26 @@ class ModManager:
self.index_installed_mods()
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
:param mod_name:
:param mod_version:
:return:
"""
r = self.available_mods[(mod_name, mod_version)]
r = self.available_mods[mod_name]
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, mod_version)]["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]["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):
@ -210,13 +187,12 @@ class ModManager:
md5_obj.update(buffer)
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
: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}")
@ -225,21 +201,13 @@ 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, includes_bepinx=contains_bepinex, includes_folders=contains_folders)
file = self.__adjust_folder_paths(file)
files.append(file)
@ -253,7 +221,6 @@ 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
@ -269,15 +236,15 @@ class ModManager:
elif os.path.isfile(curr) and curr.endswith('.dll'):
files.append(curr)
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)]
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]
unknown_mod = dict()
unresolved_files = files.copy()
for file in files:
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"]:
unresolved_files.remove(file)
if file in unresolved_files:
@ -295,7 +262,6 @@ 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
@ -308,8 +274,7 @@ class ModManager:
self.__logger.warning(f"Mod \"{full_path}\" did not have the expected path. Ignoring it ...")
continue
self.available_mods[ModManager.get_mod_key(mod_name, mod_version)] = {"path": file, "version": mod_version,
"mod_files": mod_files,
self.available_mods[mod_name] = {"path": file, "version": mod_version, "mod_files": mod_files,
"orig_mod_files": orig_mod_files}
self.__window.set_available_mods(self.available_mods)
@ -319,7 +284,6 @@ 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!")
@ -345,70 +309,55 @@ class ModManager:
self.index_stored_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.
This assumes that the .zip is structured like the game folder as all mods should be
:param mod_name:
:param mod_version:
:return:
"""
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}")
if mod_name not in self.available_mods.keys():
self.__logger.critical(f"Tried to install a mod that doesn't exist: {mod_name}")
return
self.__logger.info(f"Installing mod \"{mod_name} - {mod_version}\" ...")
mod_zip = self.available_mods[(mod_name, mod_version)]["path"]
self.__logger.info(f"Installing mod \"{mod_name}\" ...")
mod_zip = self.available_mods[mod_name]["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, includes_bepinx=contains_bepinex,
includes_folders=contains_folders)
file = self.__adjust_folder_paths(file)
#print("Extracting", content, "to", tmp_dir)
zip_ref.extract(content, tmp_dir)
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
parent_dir = os.path.join(self.__settings.get_game_folder(), file).split(os.path.basename(file))[0]\
.replace("/", os.path.sep)
#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))):
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)))
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)))
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
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} - {mod_version}\" ...")
for file in self.installed_mods[(mod_name, mod_version)]["mod_files"]:
self.__logger.info(f"Uninstalling mod \"{mod_name}\" ...")
for file in self.installed_mods[mod_name]["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}\" ...")

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

@ -1,7 +1,7 @@
import logging
import os.path
import sys
from typing import Dict, List, Tuple
from typing import Dict, List
from PyQt6 import QtWidgets
from PyQt6.QtCore import Qt
@ -42,25 +42,21 @@ class MainWindow(QtWidgets.QMainWindow):
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
:param available_mods:
: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)
for key in keys:
mod_name, mod_version = key[1][0], key[1][1]
item = QStandardItem(mod_name + f" ({mod_version})")
for mod_name in available_mods.keys():
item = QStandardItem(mod_name + f" ({available_mods[mod_name]['version']})")
item.setFlags(Qt.ItemFlag.ItemIsUserCheckable | Qt.ItemFlag.ItemIsEnabled)
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)
if self.parent.is_mod_installed(mod_name, mod_version):
if self.parent.is_mod_installed(mod_name):
item.setCheckState(Qt.CheckState.Checked)
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
: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)
for key in keys:
mod_name, mod_version = key[1][0], key[1][1]
if (mod_name, mod_version) in self.parent.available_mods.keys():
mod_version = self.parent.available_mods[(mod_name, mod_version)]['version']
for mod_name in installed_mods.keys():
if mod_name in self.parent.available_mods.keys():
mod_version = self.parent.available_mods[mod_name]['version']
else:
mod_version = "Not Tracked"
item = QStandardItem(mod_name + f" ({mod_version})")
item.setFlags(Qt.ItemFlag.ItemIsUserCheckable | Qt.ItemFlag.ItemIsEnabled)
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.setCheckState(Qt.CheckState.Checked)
@ -102,16 +94,12 @@ class MainWindow(QtWidgets.QMainWindow):
result = dialog.getOpenFileName(filter='ZIP (*.zip)')
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]):
dialog = QtWidgets.QMessageBox()
dialog.setWindowTitle("Not a file")
dialog.setInformativeText(f"The given file \"{result}\" did not look like a file!")
dialog.setIcon(QtWidgets.QMessageBox.Icon.Warning)
dialog.exec()
return
if not self.parent.is_valid_mod_file(result[0]):
dialog = QtWidgets.QMessageBox()
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?")
dialog.setIcon(QtWidgets.QMessageBox.Icon.Warning)
dialog.exec()
return
self.parent.add_mod_file(result[0])
@ -179,20 +166,20 @@ class MainWindow(QtWidgets.QMainWindow):
def on_available_mod_item_changed(self, item: QStandardItem):
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:
self.parent.install_mod(mod_name, mod_version)
self.parent.install_mod(mod_name)
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):
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
accepted = QtWidgets.QMessageBox.question(self, "Really uninstall mod?",
f"Do you really want to uninstall the mod \"{mod_name}\"?\n"
"This could lead to permanent data loss if it wasn't tracked!")
if accepted:
self.parent.uninstall_mod(mod_name, mod_version)
self.parent.uninstall_mod(mod_name)
else:
return

Loading…
Cancel
Save