From d55c23fc19c2884e66770c19516c7b0673ad3922 Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Sat, 28 Feb 2026 23:33:51 +0100 Subject: [PATCH 01/21] Support for several games and generalized Unreal Tab support. --- games/game_cassettebeasts.py | 118 +++++++ games/game_crimeboss.py | 306 +++++++++++++++++ games/game_emuvr.py | 115 +++++++ games/game_hitman3.py | 226 +++++++++++++ games/game_noita.py | 154 +++++++++ games/game_ovkwalkingdead.py | 284 ++++++++++++++++ games/game_pacificdrive.py | 285 ++++++++++++++++ games/game_payday1.py | 277 ++++++++++++++++ games/game_payday2.py | 295 +++++++++++++++++ games/game_payday3.py | 284 ++++++++++++++++ games/game_raid2.py | 211 ++++++++++++ games/game_roadtovostok.py | 89 +++++ games/game_silenthill2remake.py | 366 +++++++++++++++------ games/game_titanfall2.py | 289 ++++++++++++++++ games/game_zuma_deluxe.py | 329 ++++++++++++++++++ games/unreal_tabs/__init__.py | 0 games/unreal_tabs/constants.py | 11 + games/unreal_tabs/manage_paks/__init__.py | 0 games/unreal_tabs/manage_paks/model.py | 245 ++++++++++++++ games/unreal_tabs/manage_paks/view.py | 37 +++ games/unreal_tabs/manage_paks/widget.py | 207 ++++++++++++ games/unreal_tabs/manage_ue4ss/__init__.py | 0 games/unreal_tabs/manage_ue4ss/model.py | 121 +++++++ games/unreal_tabs/manage_ue4ss/view.py | 36 ++ games/unreal_tabs/manage_ue4ss/widget.py | 168 ++++++++++ 25 files changed, 4356 insertions(+), 97 deletions(-) create mode 100644 games/game_cassettebeasts.py create mode 100644 games/game_crimeboss.py create mode 100644 games/game_emuvr.py create mode 100644 games/game_hitman3.py create mode 100644 games/game_noita.py create mode 100644 games/game_ovkwalkingdead.py create mode 100644 games/game_pacificdrive.py create mode 100644 games/game_payday1.py create mode 100644 games/game_payday2.py create mode 100644 games/game_payday3.py create mode 100644 games/game_raid2.py create mode 100644 games/game_roadtovostok.py create mode 100644 games/game_titanfall2.py create mode 100644 games/game_zuma_deluxe.py create mode 100644 games/unreal_tabs/__init__.py create mode 100644 games/unreal_tabs/constants.py create mode 100644 games/unreal_tabs/manage_paks/__init__.py create mode 100644 games/unreal_tabs/manage_paks/model.py create mode 100644 games/unreal_tabs/manage_paks/view.py create mode 100644 games/unreal_tabs/manage_paks/widget.py create mode 100644 games/unreal_tabs/manage_ue4ss/__init__.py create mode 100644 games/unreal_tabs/manage_ue4ss/model.py create mode 100644 games/unreal_tabs/manage_ue4ss/view.py create mode 100644 games/unreal_tabs/manage_ue4ss/widget.py diff --git a/games/game_cassettebeasts.py b/games/game_cassettebeasts.py new file mode 100644 index 00000000..99b5bd52 --- /dev/null +++ b/games/game_cassettebeasts.py @@ -0,0 +1,118 @@ +import os +import shutil +import mobase + +from enum import IntEnum, auto +from pathlib import Path +from typing import Any, List, Set, cast +from functools import cached_property + +from ..basic_game import BasicGame + +try: + from PyQt6.QtCore import QDir, QFileInfo +except: + from PyQt5.QtCore import QDir, QFileInfo + + +class CassetteBeastsModDataChecker(mobase.ModDataChecker): + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self.organizer: mobase.IOrganizer = organizer + + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + for e in filetree: + if e is not None and e.isFile() and e.suffix().casefold() == "pck": + return mobase.ModDataChecker.VALID + return mobase.ModDataChecker.FIXABLE + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + GameDataPath = self.organizer.managedGame().GameDataPath + "/" + treefixed = 0 + for branch in filetree: + mod_name = filetree.name() + if mod_name == "": + mod_name = branch.name() + mod_path = os.path.join(self.organizer.modsPath(), mod_name) + if filetree.createOrphanTree("OrphanTree") is None and os.path.exists(mod_path) and branch.suffix().casefold() == "pck": + os.makedirs(os.path.join(mod_path, GameDataPath), exist_ok=True) + shutil.move(os.path.join(mod_path, branch.name()), os.path.join(mod_path, GameDataPath, branch.name())) + treefixed = 1 + else: + if branch is not None: + if branch.isDir(): + for e in branch: + if e is not None and e.isFile() and e.suffix().casefold() == "pck": + filetree.move(e, GameDataPath, mobase.IFileTree.MERGE) + treefixed = 1 + elif branch.suffix().casefold() == "pck": + filetree.move(branch, GameDataPath, mobase.IFileTree.MERGE) + treefixed = 1 + if treefixed == 0: + return None + return filetree + + +class CassetteBeastsGame(BasicGame): + appdataenv = os.getenv('APPDATA') + + Name = "Cassette Beasts Support Plugin" + Author = "modworkshop" + Version = "1" + GameName = "Cassette Beasts" + GameShortName = "cassette-beasts" + GameSteamId = 1321440 + GameBinary = "CassetteBeasts.exe" + GameDataPath = appdataenv + '/CassetteBeasts/mods' + GameDocumentsDirectory = appdataenv + '/CassetteBeasts' + GameSaveExtension = "gcpf" + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self.dataChecker = CassetteBeastsModDataChecker(organizer) + self._register_feature(self.dataChecker) + return True + + def executables(self): + return [ + mobase.ExecutableInfo( + "Cassette Beasts (Mods)", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ).withArgument("-load-mods"), + mobase.ExecutableInfo( + "Cassette Beasts (No Mods)", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ), + ] + + @cached_property + def _base_dlls(self) -> set[str]: + base_dir = Path(self.gameDirectory().absolutePath()) + return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + try: + efls = super().executableForcedLoads() + except AttributeError: + efls = [] + libs: set[str] = set() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + if type(tree) is not mobase.IFileTree: + return efls + for e in tree: + relpath = e.pathFrom(tree) + if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + libs.add(relpath) + exes = self.executables() + efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + return efls + + def iniFiles(self): + return ["settings.cfg"] + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + modsPath = self.dataDirectory().absolutePath() + if not os.path.exists(modsPath): + os.mkdir(modsPath) + super().initializeProfile(directory, settings) + \ No newline at end of file diff --git a/games/game_crimeboss.py b/games/game_crimeboss.py new file mode 100644 index 00000000..648267ed --- /dev/null +++ b/games/game_crimeboss.py @@ -0,0 +1,306 @@ +import json +import os +import shutil +import mobase + +from enum import IntEnum, auto +from pathlib import Path +from typing import Any, List, Set, cast +from functools import cached_property + +from .unreal_tabs.constants import DEFAULT_UE4SS_MODS, UE4SSModInfo +from .unreal_tabs.manage_paks.widget import PaksTabWidget +from .unreal_tabs.manage_ue4ss.widget import UE4SSTabWidget + +from ..basic_game import BasicGame + +try: + from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget + from PyQt6.QtCore import QDir, QFileInfo +except: + from PyQt5.QtWidgets import QMainWindow, QTabWidget, QWidget + from PyQt5.QtCore import QDir, QFileInfo + + +class Content(IntEnum): + UCAS = auto() + UTOC = auto() + PAK = auto() + UE4SS = auto() + DLL = auto() + BK2 = auto() + + +class CrimeBossModDataContent(mobase.ModDataContent): + contents: list[int] = [] + GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ + (Content.UCAS, "UCAS", ":/MO/gui/content/geometries"), + (Content.UTOC, "UTOC", ":/MO/gui/content/inifile"), + (Content.PAK, "PAK", ":/MO/gui/content/geometries"), + (Content.UE4SS, "UE4SS", ":/MO/gui/content/script"), + (Content.DLL, "DLL", ":/MO/gui/content/skse"), + (Content.BK2, "Video", ":/MO/gui/content/skse"), + ] + + def getAllContents(self) -> list[mobase.ModDataContent.Content]: + return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + + def walkContent(self, path: str, entry: mobase.FileTreeEntry): + if entry.isFile(): + match entry.suffix().casefold(): + case "utoc": + self.contents.add(Content.UTOC) + case "ucas": + self.contents.add(Content.UCAS) + case "pak": + self.contents.add(Content.PAK) + case "lua": + self.contents.add(Content.UE4SS) + case "dll": + self.contents.add(Content.DLL) + case "bk2": + self.contents.add(Content.BK2) + case _: + pass + return mobase.IFileTree.WalkReturn.CONTINUE + + def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: + self.contents: set[int] = set() + filetree.walk(self.walkContent, "/") + return list(self.contents) + + +class CrimeBossModDataChecker(mobase.ModDataChecker): + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self.organizer: mobase.IOrganizer = organizer + self.organizer.modList().onModInstalled(self._Fix_Installed_Mod) + self.needsNameFix = False + + def move_overwrite_merge(self, source, destination): + if not os.path.exists(destination): + shutil.move(source, destination) + return + if os.path.isfile(source): + os.replace(source, destination) + return + for item in os.listdir(source): + s_item = os.path.join(source, item) + d_item = os.path.join(destination, item) + self.move_overwrite_merge(s_item, d_item) + os.rmdir(source) + + def _Fix_Installed_Mod(self, mod: mobase.IModInterface): + if not self.needsNameFix: + return + GameDataNativeMods = self.organizer.managedGame().GameDataNativeMods + filetree: mobase.IFileTree = mod.fileTree() + fixed = False + modname = mod.name() + if filetree is not None and filetree.exists(GameDataNativeMods + "/FOLDERNAME", mobase.IFileTree.DIRECTORY): + path = mod.absolutePath() + old_path = os.path.join(path, GameDataNativeMods + "/FOLDERNAME") + new_path = os.path.join(path, GameDataNativeMods + f"/{modname}") + self.move_overwrite_merge(old_path, new_path) + fixed = True + if not fixed: + return + self.needsNameFix = False + + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods + GameDataPakMods = self.organizer.managedGame().GameDataPakMods + GameDataNativeMods = self.organizer.managedGame().GameDataNativeMods + if filetree.exists(GameDataPakMods, mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + if filetree.exists(os.path.dirname(GameDataUE4SSMods), mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + if filetree.exists(GameDataNativeMods, mobase.IFileTree.DIRECTORY) and not filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): + return mobase.ModDataChecker.VALID + return mobase.ModDataChecker.FIXABLE + + def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): + for branch in filetree: + if branch is not None and branch.isDir(): + for e in branch: + if e is not None and e.name() == name: + return True + return False + + def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): + entriesToMove: list[mobase.FileTreeEntry] = [] + retVal = 0 + for e in filetree: + if e is not None: + entriesToMove.append(e) + for e in entriesToMove: + filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) + retVal = 1 + return retVal + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods + "/" + GameDataPakMods = self.organizer.managedGame().GameDataPakMods + "/" + GameDataNativeMods = self.organizer.managedGame().GameDataNativeMods + "/" + treefixed = 0 + if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): + treefixed = self.allMoveTo(filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/") + if treefixed == 1: + return filetree + if filetree.exists("Content", mobase.IFileTree.DIRECTORY): + treefixed = self.allMoveTo(filetree, GameDataNativeMods + "FOLDERNAME/") + if treefixed == 1: + self.needsNameFix = True + if self.fileExistsInNextSubDir(filetree, "Content"): + filetree.move(filetree[0], GameDataNativeMods + "/", mobase.IFileTree.MERGE) + treefixed = 1 + if treefixed == 0: + allowedUnzippedExt = ["pak", "utoc", "ucas", "bk2", "dll"] + entriesToMove: list[mobase.FileTreeEntry] = [] + for e in filetree: + if e is not None: + if e.isFile(): + fileext = e.suffix().casefold() + if fileext in allowedUnzippedExt: + mod_name = filetree.name() + if mod_name == "": + mod_name = e.name() + mod_path = os.path.join(self.organizer.modsPath(), mod_name) + if filetree.createOrphanTree("OrphanTree") is None and os.path.exists(mod_path): + match e.suffix().casefold(): + case "pak" | "utoc" | "ucas": + os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) + case "bk2": + os.makedirs(os.path.join(mod_path, GameDataMovies), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovies, e.name())) + case _: + pass + treefixed = 1 + else: + entriesToMove.append(e) + if entriesToMove is not None: + for e in entriesToMove: + match e.suffix().casefold(): + case "pak" | "utoc" | "ucas": + filetree.move(e, GameDataPakMods, mobase.IFileTree.MERGE) + case "dll": + filetree.move(e, os.path.dirname(GameDataUE4SSMods) + "/", mobase.IFileTree.MERGE) + case "bk2": + filetree.move(e, GameDataMovies, mobase.IFileTree.MERGE) + case _: + pass + treefixed = 1 + if treefixed == 0: + return None + return filetree + + +class CrimeBossGame(BasicGame): + Name = "Crime Boss Support Plugin" + Author = "modworkshop, MaskPlague and Silarn" + Version = "1" + GameName = "Crime Boss Rockay City" + GameShortName = "crimeboss" + GameSteamId = 2933080 + GameBinary = "CrimeBoss/Binaries/Win64/CrimeBoss-Win64-Shipping.exe" + GameDataPath = "CrimeBoss" + GameDataUE4SSMods = "Binaries/Win64/Mods" + GameDataNativeMods = "Mods" + GameDataPakMods = "Content/Paks/~Mods" + GameDocumentsDirectory = "%USERPROFILE%/Saved Games/CrimeBoss/Steam/Saved/Config/WindowsNoEditor" + GameSaveExtension = "sav" + _main_window: QMainWindow + _ue4ss_tab: UE4SSTabWidget + _paks_tab: PaksTabWidget + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self.dataChecker = CrimeBossModDataChecker(organizer) + self._register_feature(self.dataChecker) + self._register_feature(CrimeBossModDataContent()) + organizer.onUserInterfaceInitialized(self.init_tab) + return True + + def init_tab(self, main_window: QMainWindow): + if self._organizer.managedGame() != self: + return + self._main_window = main_window + tab_widget: QTabWidget = main_window.findChild(QTabWidget, "tabWidget") + if not tab_widget or not tab_widget.findChild(QWidget, "espTab"): + return + self._ue4ss_tab = UE4SSTabWidget(main_window, self._organizer) + plugin_tab = tab_widget.findChild(QWidget, "espTab") + tab_index = tab_widget.indexOf(plugin_tab) + 1 + if not tab_widget.isTabVisible(tab_widget.indexOf(plugin_tab)): + tab_index += 1 + tab_widget.insertTab(tab_index, self._ue4ss_tab, "UE4SS") + self._paks_tab = PaksTabWidget(main_window, self._organizer) + tab_index += 1 + tab_widget.insertTab(tab_index, self._paks_tab, "Paks") + + def executables(self): + return [ + mobase.ExecutableInfo( + "Crime Boss: Rockay City", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ) + ] + + @cached_property + def _base_dlls(self) -> set[str]: + base_dir = Path(self.gameDirectory().absolutePath()) + return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + try: + efls = super().executableForcedLoads() + except AttributeError: + efls = [] + libs: set[str] = set() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + if type(tree) is not mobase.IFileTree: + return efls + for e in tree: + relpath = e.pathFrom(tree) + if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + libs.add(relpath) + exes = self.executables() + efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + return efls + + def paksDirectory(self) -> QDir: + return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataPakMods) + + def ue4ssDirectory(self) -> QDir: + return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataUE4SSMods) + + def nativeDirectory(self) -> QDir: + return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataNativeMods) + + def write_default_mods(self, profile: QDir): + ue4ss_mods_txt = QFileInfo(profile.absoluteFilePath("mods.txt")) + ue4ss_mods_json = QFileInfo(profile.absoluteFilePath("mods.json")) + if not ue4ss_mods_txt.exists(): + with open(ue4ss_mods_txt.absoluteFilePath(), "w") as mods_txt: + for mod in DEFAULT_UE4SS_MODS: + mods_txt.write(f"{mod['mod_name']} : 1\n") + if not ue4ss_mods_json.exists(): + mods_data: list[UE4SSModInfo] = [] + for mod in DEFAULT_UE4SS_MODS: + mods_data.append({"mod_name": mod["mod_name"], "mod_enabled": True}) + with open(ue4ss_mods_json.absoluteFilePath(), "w") as mods_json: + mods_json.write(json.dumps(mods_data, indent=4)) + + def iniFiles(self): + return ["GameUserSettings.ini", "Input.ini"] + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + self.write_default_mods(directory) + if not self.paksDirectory().exists(): + os.makedirs(self.paksDirectory().absolutePath()) + if not self.ue4ssDirectory().exists(): + os.makedirs(self.ue4ssDirectory().absolutePath()) + if not self.nativeDirectory().exists(): + os.makedirs(self.nativeDirectory().absolutePath()) + super().initializeProfile(directory, settings) \ No newline at end of file diff --git a/games/game_emuvr.py b/games/game_emuvr.py new file mode 100644 index 00000000..e1418ba4 --- /dev/null +++ b/games/game_emuvr.py @@ -0,0 +1,115 @@ +import os +import shutil +import mobase + +from enum import IntEnum, auto +from pathlib import Path +from typing import Any, List, Set, cast +from functools import cached_property + +from ..basic_game import BasicGame + +try: + from PyQt6.QtCore import QDir, QFileInfo +except: + from PyQt5.QtCore import QDir, QFileInfo + + +class EmuVRModDataChecker(mobase.ModDataChecker): + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self.organizer: mobase.IOrganizer = organizer + + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + GameDataUGCMods = self.organizer.managedGame().GameDataUGCMods + if filetree.exists(GameDataUGCMods, mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + return mobase.ModDataChecker.FIXABLE + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + GameDataUGCMods = self.organizer.managedGame().GameDataUGCMods + "/" + treefixed = 0 + for branch in filetree: + mod_name = filetree.name() + if mod_name == "": + mod_name = branch.name() + mod_path = os.path.join(self.organizer.modsPath(), mod_name) + if filetree.createOrphanTree("OrphanTree") is None and os.path.exists(mod_path) and branch.suffix().casefold() == "ugc": + os.makedirs(os.path.join(mod_path, GameDataUGCMods), exist_ok=True) + shutil.move(os.path.join(mod_path, branch.name()), os.path.join(mod_path, GameDataUGCMods, branch.name())) + treefixed = 1 + else: + if branch is not None: + if branch.isDir(): + for e in branch: + if e is not None and e.isFile() and e.suffix().casefold() == "ugc": + filetree.move(e, GameDataUGCMods, mobase.IFileTree.MERGE) + treefixed = 1 + elif branch.suffix().casefold() == "ugc": + filetree.move(branch, GameDataUGCMods, mobase.IFileTree.MERGE) + treefixed = 1 + if treefixed == 0: + return None + return filetree + + +class EmuVRGame(BasicGame): + Name = "Emu VR Support Plugin" + Author = "modworkshop" + Version = "1" + GameName = "Emu VR" + GameShortName = "emuvr" + GameBinary = "EmuVR.exe" + GameDataPath = "%GAME_PATH%" + GameDataUGCMods = "Custom/UGC" + GameDocumentsDirectory = "%GAME_PATH%/Saved Data" + GameSavesDirectory = "%GAME_PATH%/Saved Data" + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self.dataChecker = EmuVRModDataChecker(organizer) + self._register_feature(self.dataChecker) + return True + + def executables(self): + return [ + mobase.ExecutableInfo( + "Emu VR", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ), + mobase.ExecutableInfo("Force SteamVR", QFileInfo(self.gameDirectory(), "Force SteamVR.exe")), + mobase.ExecutableInfo("Force Oculus", QFileInfo(self.gameDirectory(), "Force Oculus.exe")), + mobase.ExecutableInfo("Force Virtual Desktop Streamer", QFileInfo(self.gameDirectory(), "Force Virtual Desktop Streamer.exe")), + mobase.ExecutableInfo("Force Desktop", QFileInfo(self.gameDirectory(), "Force Desktop.exe")), + ] + + def iniFiles(self): + return ["settings.ini"] + + @cached_property + def _base_dlls(self) -> set[str]: + base_dir = Path(self.gameDirectory().absolutePath()) + return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + try: + efls = super().executableForcedLoads() + except AttributeError: + efls = [] + libs: set[str] = set() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + if type(tree) is not mobase.IFileTree: + return efls + for e in tree: + relpath = e.pathFrom(tree) + if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + libs.add(relpath) + exes = self.executables() + efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + return efls + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + modsPath = self.dataDirectory().absolutePath() + if not os.path.exists(modsPath): + os.mkdir(modsPath) + super().initializeProfile(directory, settings) \ No newline at end of file diff --git a/games/game_hitman3.py b/games/game_hitman3.py new file mode 100644 index 00000000..3c31b210 --- /dev/null +++ b/games/game_hitman3.py @@ -0,0 +1,226 @@ +import os +import shutil +import json +import mobase + +from json import JSONDecodeError +from enum import IntEnum, auto +from pathlib import Path +from typing import Any, List, Set, cast +from functools import cached_property + +from ..basic_game import BasicGame + +try: + from PyQt6.QtCore import QDir, QFileInfo +except: + from PyQt5.QtCore import QDir, QFileInfo + + +class Hitman3ModDataChecker(mobase.ModDataChecker): + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self.organizer: mobase.IOrganizer = organizer + self.organizer.modList().onModInstalled(self._Fix_Installed_Mod) + self.needsNameFix = False + + def move_overwrite_merge(self, source, destination): + if not os.path.exists(destination): + shutil.move(source, destination) + return + if os.path.isfile(source): + os.replace(source, destination) + return + for item in os.listdir(source): + s_item = os.path.join(source, item) + d_item = os.path.join(destination, item) + self.move_overwrite_merge(s_item, d_item) + os.rmdir(source) + + def _Fix_Installed_Mod(self, mod: mobase.IModInterface): + if not self.needsNameFix: + return + GameSMMPath = self.organizer.managedGame().GameSMMPath + filetree: mobase.IFileTree = mod.fileTree() + fixed = False + if filetree is not None and filetree.exists(GameSMMPath + "/Mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): + path = mod.absolutePath() + json_path = os.path.join(path, GameSMMPath + "/Mods/FOLDERNAME/manifest.json") + mod_data = json.load(open(json_path, encoding="utf-8")) + modname = mod_data["id"] + old_path = os.path.join(path, GameSMMPath + "/Mods/FOLDERNAME") + new_path = os.path.join(path, GameSMMPath + f"/Mods/{modname}") + self.move_overwrite_merge(old_path, new_path) + fixed = True + if not fixed: + return + self.needsNameFix = False + + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + if filetree.exists("Simple Mod Framework", mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + return mobase.ModDataChecker.FIXABLE + + def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): + for branch in filetree: + if branch is not None and branch.isDir(): + for e in branch: + if e is not None and e.name() == name: + return True + return False + + def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): + entriesToMove: list[mobase.FileTreeEntry] = [] + retVal = 0 + for e in filetree: + if e is not None: + entriesToMove.append(e) + for e in entriesToMove: + filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) + retVal = 1 + return retVal + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + GameSMMPath = self.organizer.managedGame().GameSMMPath + treefixed = 0 + if filetree.exists("manifest.json", mobase.IFileTree.FILE): + treefixed = self.allMoveTo(filetree, GameSMMPath + "/Mods/FOLDERNAME/") + if treefixed == 1: + self.needsNameFix = True + if treefixed == 0: + if len(filetree) == 1: + filetree = filetree.find(filetree[0].path("/")) + treefixed = self.allMoveTo(filetree, GameSMMPath + "/Mods/FOLDERNAME/") + if treefixed == 1: + self.needsNameFix = True + if treefixed == 0: + return None + return filetree + + +class Hitman3Game(BasicGame): + Name = "Hitman 3 Support Plugin" + Author = "modworkshop" + Version = "1" + GameName = "Hitman: World of Assassination" + GameShortName = "hitman3" + GameSteamId = 1659040 + GameBinary = "Retail/HITMAN3.exe" + GameDataPath = "%GAME_PATH%" + GameSMMPath = "Simple Mod Framework" + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self.dataChecker = Hitman3ModDataChecker(organizer) + self._register_feature(self.dataChecker) + organizer.modList().onModStateChanged(self.update_smm_meta) + return True + + def update_smm_meta(self, mods: dict[str, mobase.ModState]): + GameSMMPath = self._organizer.managedGame().GameSMMPath + SMM_Path = os.path.join(self.dataDirectory().absolutePath(), self.GameSMMPath) + SMM_Config_Json = SMM_Path + "/config.json" + for key, value in mods.items(): + key = self._organizer.modList().getMod(key) + tree = key.fileTree() + subtree = tree.find(self.GameSMMPath + "/Mods", mobase.IFileTree.DIRECTORY) + if subtree is not None and subtree.isDir(): + for e in subtree: + if e is not None and e.isDir(): + if e.exists("manifest.json", mobase.IFileTree.FILE): + json_path = key.absolutePath() + "/" + e.path() + "/manifest.json" + mod_data = json.load(open(json_path, encoding="utf-8")) + modname = mod_data["id"] + if value == 35: + with open(SMM_Config_Json, "r") as config_json: + config_json_content = config_json.read() + config_json.close() + good_code = '"knownMods": []' + if good_code in config_json_content: + bad_code = "{runtimePath:'..\\Runtime',retailPath:'..\\Retail',skipIntro:false,outputToSeparateDirectory:false,loadOrder:[''],modOptions:{},outputConfigToAppDataOnDeploy:true,knownMods:[''],developerMode:false,reportErrors:false}" + config_json_content = bad_code + if modname not in config_json_content: + substr = "knownMods:[" + config_json_content = config_json_content.replace(substr, substr + "'" + modname + "',") + substr = "loadOrder:[" + config_json_content = config_json_content.replace(substr, substr + "'" + modname + "',") + substr = ",],modOptions" + config_json_content = config_json_content.replace(substr, "],modOptions") + substr = ",],developer" + config_json_content = config_json_content.replace(substr, "],developer") + with open(SMM_Config_Json, "w") as config_json: + config_json.write(config_json_content) + config_json.close() + return None + if value == 33: + with open(SMM_Config_Json, "r") as config_json: + config_json_content = config_json.read() + config_json.close() + if modname in config_json_content: + config_json_content = config_json_content.replace("'" + modname + "',", "") + config_json_content = config_json_content.replace(",,", ",") + substr = ",],modOptions" + config_json_content = config_json_content.replace(substr, "],modOptions") + substr = ",],developer" + config_json_content = config_json_content.replace(substr, "],developer") + with open(SMM_Config_Json, "w") as config_json: + config_json.write(config_json_content) + config_json.close() + return None + + def executables(self): + return [ + mobase.ExecutableInfo( + "Hitman: World of Assassination", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ), + mobase.ExecutableInfo( + "Launcher", + QFileInfo( + self.gameDirectory(), + "Launcher.exe", + ), + ), + mobase.ExecutableInfo( + "Configure via Simple Mod Framework", + QFileInfo( + self.gameDirectory(), + "Simple Mod Framework/Mod Manager/Mod Manager.exe", + ), + ), + mobase.ExecutableInfo( + "Deploy via Simple Mod Framework", + QFileInfo( + self.gameDirectory(), + "Simple Mod Framework/Deploy.exe", + ), + ), + ] + + @cached_property + def _base_dlls(self) -> set[str]: + base_dir = Path(self.gameDirectory().absolutePath()) + return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + try: + efls = super().executableForcedLoads() + except AttributeError: + efls = [] + libs: set[str] = set() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + if type(tree) is not mobase.IFileTree: + return efls + for e in tree: + relpath = e.pathFrom(tree) + if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + libs.add(relpath) + exes = self.executables() + efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + return efls + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + modsPath = self.dataDirectory().absolutePath() + if not os.path.exists(modsPath): + os.mkdir(modsPath) + super().initializeProfile(directory, settings) \ No newline at end of file diff --git a/games/game_noita.py b/games/game_noita.py new file mode 100644 index 00000000..729d5b39 --- /dev/null +++ b/games/game_noita.py @@ -0,0 +1,154 @@ +import os +import shutil +import json +import mobase + +from json import JSONDecodeError +from enum import IntEnum, auto +from pathlib import Path +from typing import Any, List, Set, cast +from functools import cached_property + +from ..basic_game import BasicGame + +try: + from PyQt6.QtCore import QDir, QFileInfo +except: + from PyQt5.QtCore import QDir, QFileInfo + + +class NoitaModDataChecker(mobase.ModDataChecker): + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self.organizer: mobase.IOrganizer = organizer + self.organizer.modList().onModInstalled(self._Fix_Installed_Mod) + self.needsNameFix = False + + def move_overwrite_merge(self, source, destination): + if not os.path.exists(destination): + shutil.move(source, destination) + return + if os.path.isfile(source): + os.replace(source, destination) + return + for item in os.listdir(source): + s_item = os.path.join(source, item) + d_item = os.path.join(destination, item) + self.move_overwrite_merge(s_item, d_item) + os.rmdir(source) + + def _Fix_Installed_Mod(self, mod: mobase.IModInterface): + if not self.needsNameFix: + return + GameModsPath = self.organizer.managedGame().GameModsPath + filetree: mobase.IFileTree = mod.fileTree() + fixed = False + modname = mod.name() + if filetree is not None and filetree.exists(GameModsPath + "/FOLDERNAME", mobase.IFileTree.DIRECTORY): + path = mod.absolutePath() + old_path = os.path.join(path, GameModsPath + "/FOLDERNAME") + new_path = os.path.join(path, GameModsPath + f"/{modname}") + self.move_overwrite_merge(old_path, new_path) + fixed = True + if not fixed: + return + self.needsNameFix = False + + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + if filetree.exists("mods", mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + return mobase.ModDataChecker.FIXABLE + + def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): + for branch in filetree: + if branch is not None and branch.isDir(): + for e in branch: + if e is not None and e.name() == name: + return True + return False + + def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): + entriesToMove: list[mobase.FileTreeEntry] = [] + retVal = 0 + for e in filetree: + if e is not None: + entriesToMove.append(e) + for e in entriesToMove: + filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) + retVal = 1 + return retVal + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + GameModsPath = self.organizer.managedGame().GameModsPath + treefixed = 0 + if filetree.exists("mod.xml", mobase.IFileTree.FILE): + treefixed = self.allMoveTo(filetree, GameModsPath + "/FOLDERNAME/") + if treefixed == 1: + self.needsNameFix = True + if self.fileExistsInNextSubDir(filetree, "mod.xml"): + filetree.move(filetree[0], GameModsPath + "/", mobase.IFileTree.MERGE) + treefixed = 1 + if treefixed == 0: + return None + return filetree + + +class NoitaGame(BasicGame): + Name = "Noita Support Plugin" + Author = "modworkshop" + Version = "1" + GameName = "Noita" + GameShortName = "noita" + GameSteamId = 881100 + GameBinary = "noita.exe" + GameDataPath = "%GAME_PATH%" + GameModsPath = "mods" + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self.dataChecker = NoitaModDataChecker(organizer) + self._register_feature(self.dataChecker) + return True + + def executables(self): + return [ + mobase.ExecutableInfo( + "Noita", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ), + mobase.ExecutableInfo( + "Noita Dev", + QFileInfo( + self.gameDirectory(), + "noita_dev.exe", + ), + ), + ] + + @cached_property + def _base_dlls(self) -> set[str]: + base_dir = Path(self.gameDirectory().absolutePath()) + return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + try: + efls = super().executableForcedLoads() + except AttributeError: + efls = [] + libs: set[str] = set() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + if type(tree) is not mobase.IFileTree: + return efls + for e in tree: + relpath = e.pathFrom(tree) + if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + libs.add(relpath) + exes = self.executables() + efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + return efls + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + modsPath = self.dataDirectory().absolutePath() + if not os.path.exists(modsPath): + os.mkdir(modsPath) + super().initializeProfile(directory, settings) \ No newline at end of file diff --git a/games/game_ovkwalkingdead.py b/games/game_ovkwalkingdead.py new file mode 100644 index 00000000..e3916d89 --- /dev/null +++ b/games/game_ovkwalkingdead.py @@ -0,0 +1,284 @@ +import json +import os +import shutil +import mobase + +from enum import IntEnum, auto +from pathlib import Path +from typing import Any, List, Set, cast +from functools import cached_property + +from .unreal_tabs.constants import DEFAULT_UE4SS_MODS, UE4SSModInfo +from .unreal_tabs.manage_paks.widget import PaksTabWidget +from .unreal_tabs.manage_ue4ss.widget import UE4SSTabWidget + +from ..basic_game import BasicGame + +try: + from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget + from PyQt6.QtCore import QDir, QFileInfo +except: + from PyQt5.QtWidgets import QMainWindow, QTabWidget, QWidget + from PyQt5.QtCore import QDir, QFileInfo + + +class Content(IntEnum): + UCAS = auto() + UTOC = auto() + PAK = auto() + UE4SS = auto() + DLL = auto() + BK2 = auto() + + +class OTWDModDataContent(mobase.ModDataContent): + contents: list[int] = [] + GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ + (Content.UCAS, "UCAS", ":/MO/gui/content/geometries"), + (Content.UTOC, "UTOC", ":/MO/gui/content/inifile"), + (Content.PAK, "PAK", ":/MO/gui/content/geometries"), + (Content.UE4SS, "UE4SS", ":/MO/gui/content/script"), + (Content.DLL, "DLL", ":/MO/gui/content/skse"), + (Content.BK2, "Video", ":/MO/gui/content/skse"), + ] + + def getAllContents(self) -> list[mobase.ModDataContent.Content]: + return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + + def walkContent(self, path: str, entry: mobase.FileTreeEntry): + if entry.isFile(): + match entry.suffix().casefold(): + case "utoc": + self.contents.add(Content.UTOC) + case "ucas": + self.contents.add(Content.UCAS) + case "pak": + self.contents.add(Content.PAK) + case "lua": + self.contents.add(Content.UE4SS) + case "dll": + self.contents.add(Content.DLL) + case "bk2": + self.contents.add(Content.BK2) + case _: + pass + return mobase.IFileTree.WalkReturn.CONTINUE + + def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: + self.contents: set[int] = set() + filetree.walk(self.walkContent, "/") + return list(self.contents) + + +class OTWDModDataChecker(mobase.ModDataChecker): + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self.organizer: mobase.IOrganizer = organizer + + def move_overwrite_merge(self, source, destination): + if not os.path.exists(destination): + shutil.move(source, destination) + return + if os.path.isfile(source): + os.replace(source, destination) + return + for item in os.listdir(source): + s_item = os.path.join(source, item) + d_item = os.path.join(destination, item) + self.move_overwrite_merge(s_item, d_item) + os.rmdir(source) + + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods + GameDataPakMods = self.organizer.managedGame().GameDataPakMods + GameDataMovies = self.organizer.managedGame().GameDataMovieMods + if filetree.exists(GameDataPakMods, mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + if filetree.exists(GameDataMovies, mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + if filetree.exists(GameDataUE4SSMods, mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + return mobase.ModDataChecker.FIXABLE + + def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): + for branch in filetree: + if branch is not None and branch.isDir(): + for e in branch: + if e is not None and e.name() == name: + return True + return False + + def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): + entriesToMove: list[mobase.FileTreeEntry] = [] + retVal = 0 + for e in filetree: + if e is not None: + entriesToMove.append(e) + for e in entriesToMove: + filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) + retVal = 1 + return retVal + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods + "/" + GameDataPakMods = self.organizer.managedGame().GameDataPakMods + "/" + GameDataMovies = self.organizer.managedGame().GameDataMovieMods + "/" + treefixed = 0 + if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): + treefixed = self.allMoveTo(filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/") + if treefixed == 1: + return filetree + if filetree.exists("Scripts", mobase.IFileTree.DIRECTORY) or filetree.exists("dlls", mobase.IFileTree.DIRECTORY): + treefixed = self.allMoveTo(filetree, GameDataUE4SSMods) + if treefixed == 1: + return filetree + if treefixed == 0: + allowedUnzippedExt = ["pak", "utoc", "ucas", "bk2", "dll"] + entriesToMove: list[mobase.FileTreeEntry] = [] + for e in filetree: + if e is not None: + if e.isFile(): + fileext = e.suffix().casefold() + if fileext in allowedUnzippedExt: + mod_name = filetree.name() + if mod_name == "": + mod_name = e.name() + mod_path = os.path.join(self.organizer.modsPath(), mod_name) + if filetree.createOrphanTree("OrphanTree") is None and os.path.exists(mod_path): + match e.suffix().casefold(): + case "pak" | "utoc" | "ucas": + os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) + case "bk2": + os.makedirs(os.path.join(mod_path, GameDataMovies), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovies, e.name())) + case _: + pass + treefixed = 1 + else: + entriesToMove.append(e) + if entriesToMove is not None: + for e in entriesToMove: + match e.suffix().casefold(): + case "pak" | "utoc" | "ucas": + filetree.move(e, GameDataPakMods, mobase.IFileTree.MERGE) + case "dll": + filetree.move(e, os.path.dirname(GameDataUE4SSMods) + "/", mobase.IFileTree.MERGE) + case "bk2": + filetree.move(e, GameDataMovies, mobase.IFileTree.MERGE) + case _: + pass + treefixed = 1 + if treefixed == 0: + return None + return filetree + + +class OTWDGame(BasicGame): + Name = "OVERKILL's The Walking Dead Support Plugin" + Author = "modworkshop, MaskPlague and Silarn" + Version = "1" + GameName = "OVERKILL's The Walking Dead" + GameShortName = "otwd" + GameSteamId = 717690 + GameBinary = "OTWD/Binaries/Win64/OTWD-Win64-Shipping.exe" + GameDataPath = "OTWD" + GameDataUE4SSMods = "Binaries/Win64/Mods" + GameDataPakMods = "Content/Paks/~Mods" + GameDataMovieMods = "Content/Movies" + GameDocumentsDirectory = "%LOCALAPPDATA%/OTWD/Saved/Config/WindowsClient" + GameSaveExtension = "sav" + _main_window: QMainWindow + _ue4ss_tab: UE4SSTabWidget + _paks_tab: PaksTabWidget + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self.dataChecker = OTWDModDataChecker(organizer) + self._register_feature(self.dataChecker) + self._register_feature(OTWDModDataContent()) + organizer.onUserInterfaceInitialized(self.init_tab) + return True + + def init_tab(self, main_window: QMainWindow): + if self._organizer.managedGame() != self: + return + self._main_window = main_window + tab_widget: QTabWidget = main_window.findChild(QTabWidget, "tabWidget") + if not tab_widget or not tab_widget.findChild(QWidget, "espTab"): + return + self._ue4ss_tab = UE4SSTabWidget(main_window, self._organizer) + plugin_tab = tab_widget.findChild(QWidget, "espTab") + tab_index = tab_widget.indexOf(plugin_tab) + 1 + if not tab_widget.isTabVisible(tab_widget.indexOf(plugin_tab)): + tab_index += 1 + tab_widget.insertTab(tab_index, self._ue4ss_tab, "UE4SS") + self._paks_tab = PaksTabWidget(main_window, self._organizer) + tab_index += 1 + tab_widget.insertTab(tab_index, self._paks_tab, "Paks") + + def executables(self): + return [ + mobase.ExecutableInfo( + "OVERKILL's The Walking Dead", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ) + ] + + @cached_property + def _base_dlls(self) -> set[str]: + base_dir = Path(self.gameDirectory().absolutePath()) + return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + try: + efls = super().executableForcedLoads() + except AttributeError: + efls = [] + libs: set[str] = set() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + if type(tree) is not mobase.IFileTree: + return efls + for e in tree: + relpath = e.pathFrom(tree) + if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + libs.add(relpath) + exes = self.executables() + efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + return efls + + def paksDirectory(self) -> QDir: + return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataPakMods) + + def ue4ssDirectory(self) -> QDir: + return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataUE4SSMods) + + def movieDirectory(self) -> QDir: + return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataMovieMods) + + def write_default_mods(self, profile: QDir): + ue4ss_mods_txt = QFileInfo(profile.absoluteFilePath("mods.txt")) + ue4ss_mods_json = QFileInfo(profile.absoluteFilePath("mods.json")) + if not ue4ss_mods_txt.exists(): + with open(ue4ss_mods_txt.absoluteFilePath(), "w") as mods_txt: + for mod in DEFAULT_UE4SS_MODS: + mods_txt.write(f"{mod['mod_name']} : 1\n") + if not ue4ss_mods_json.exists(): + mods_data: list[UE4SSModInfo] = [] + for mod in DEFAULT_UE4SS_MODS: + mods_data.append({"mod_name": mod["mod_name"], "mod_enabled": True}) + with open(ue4ss_mods_json.absoluteFilePath(), "w") as mods_json: + mods_json.write(json.dumps(mods_data, indent=4)) + + def iniFiles(self): + return ["GameUserSettings.ini", "Input.ini"] + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + self.write_default_mods(directory) + if not self.paksDirectory().exists(): + os.makedirs(self.paksDirectory().absolutePath()) + if not self.ue4ssDirectory().exists(): + os.makedirs(self.ue4ssDirectory().absolutePath()) + if not self.movieDirectory().exists(): + os.makedirs(self.movieDirectory().absolutePath()) + super().initializeProfile(directory, settings) \ No newline at end of file diff --git a/games/game_pacificdrive.py b/games/game_pacificdrive.py new file mode 100644 index 00000000..0c2f99e8 --- /dev/null +++ b/games/game_pacificdrive.py @@ -0,0 +1,285 @@ +import json +import os +import shutil +import mobase + +from enum import IntEnum, auto +from pathlib import Path +from typing import Any, List, Set, cast +from functools import cached_property + +from .unreal_tabs.constants import DEFAULT_UE4SS_MODS, UE4SSModInfo +from .unreal_tabs.manage_paks.widget import PaksTabWidget +from .unreal_tabs.manage_ue4ss.widget import UE4SSTabWidget + +from ..basic_game import BasicGame + +try: + from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget + from PyQt6.QtCore import QDir, QFileInfo +except: + from PyQt5.QtWidgets import QMainWindow, QTabWidget, QWidget + from PyQt5.QtCore import QDir, QFileInfo + + +class Content(IntEnum): + UCAS = auto() + UTOC = auto() + PAK = auto() + UE4SS = auto() + DLL = auto() + BK2 = auto() + + +class PacificDriveModDataContent(mobase.ModDataContent): + contents: list[int] = [] + GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ + (Content.UCAS, "UCAS", ":/MO/gui/content/geometries"), + (Content.UTOC, "UTOC", ":/MO/gui/content/inifile"), + (Content.PAK, "PAK", ":/MO/gui/content/geometries"), + (Content.UE4SS, "UE4SS", ":/MO/gui/content/script"), + (Content.DLL, "DLL", ":/MO/gui/content/skse"), + (Content.BK2, "Video", ":/MO/gui/content/skse"), + ] + + def getAllContents(self) -> list[mobase.ModDataContent.Content]: + return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + + def walkContent(self, path: str, entry: mobase.FileTreeEntry): + if entry.isFile(): + match entry.suffix().casefold(): + case "utoc": + self.contents.add(Content.UTOC) + case "ucas": + self.contents.add(Content.UCAS) + case "pak": + self.contents.add(Content.PAK) + case "lua": + self.contents.add(Content.UE4SS) + case "dll": + self.contents.add(Content.DLL) + case "bk2": + self.contents.add(Content.BK2) + case _: + pass + return mobase.IFileTree.WalkReturn.CONTINUE + + def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: + self.contents: set[int] = set() + filetree.walk(self.walkContent, "/") + return list(self.contents) + + +class PacificDriveModDataChecker(mobase.ModDataChecker): + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self.organizer: mobase.IOrganizer = organizer + + def move_overwrite_merge(self, source, destination): + if not os.path.exists(destination): + shutil.move(source, destination) + return + if os.path.isfile(source): + os.replace(source, destination) + return + for item in os.listdir(source): + s_item = os.path.join(source, item) + d_item = os.path.join(destination, item) + self.move_overwrite_merge(s_item, d_item) + os.rmdir(source) + + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods + GameDataPakMods = self.organizer.managedGame().GameDataPakMods + GameDataMovies = self.organizer.managedGame().GameDataMovieMods + if filetree.exists(GameDataPakMods, mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + if filetree.exists(GameDataMovies, mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + if filetree.exists(GameDataUE4SSMods, mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + return mobase.ModDataChecker.FIXABLE + + def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): + for branch in filetree: + if branch is not None and branch.isDir(): + for e in branch: + if e is not None and e.name() == name: + return True + return False + + def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): + entriesToMove: list[mobase.FileTreeEntry] = [] + retVal = 0 + for e in filetree: + if e is not None: + entriesToMove.append(e) + for e in entriesToMove: + filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) + retVal = 1 + return retVal + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods + "/" + GameDataPakMods = self.organizer.managedGame().GameDataPakMods + "/" + GameDataMovies = self.organizer.managedGame().GameDataMovieMods + "/" + treefixed = 0 + if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): + treefixed = self.allMoveTo(filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/") + if treefixed == 1: + return filetree + if filetree.exists("Scripts", mobase.IFileTree.DIRECTORY) or filetree.exists("dlls", mobase.IFileTree.DIRECTORY): + treefixed = self.allMoveTo(filetree, GameDataUE4SSMods) + if treefixed == 1: + return filetree + if treefixed == 0: + allowedUnzippedExt = ["pak", "utoc", "ucas", "bk2", "dll"] + entriesToMove: list[mobase.FileTreeEntry] = [] + for e in filetree: + if e is not None: + if e.isFile(): + fileext = e.suffix().casefold() + if fileext in allowedUnzippedExt: + mod_name = filetree.name() + if mod_name == "": + mod_name = e.name() + mod_path = os.path.join(self.organizer.modsPath(), mod_name) + if filetree.createOrphanTree("OrphanTree") is None and os.path.exists(mod_path): + match e.suffix().casefold(): + case "pak" | "utoc" | "ucas": + os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) + case "bk2": + os.makedirs(os.path.join(mod_path, GameDataMovies), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovies, e.name())) + case _: + pass + treefixed = 1 + else: + entriesToMove.append(e) + if entriesToMove is not None: + for e in entriesToMove: + match e.suffix().casefold(): + case "pak" | "utoc" | "ucas": + filetree.move(e, GameDataPakMods, mobase.IFileTree.MERGE) + case "dll": + filetree.move(e, os.path.dirname(GameDataUE4SSMods) + "/", mobase.IFileTree.MERGE) + case "bk2": + filetree.move(e, GameDataMovies, mobase.IFileTree.MERGE) + case _: + pass + treefixed = 1 + if treefixed == 0: + return None + return filetree + + +class PacificDriveGame(BasicGame): + Name = "Pacific Drive Support Plugin" + Author = "modworkshop" + Version = "1" + GameName = "Pacific Drive" + GameLauncher = "PenDriverPro.exe" + GameShortName = "pacificdrive" + GameSteamId = 1458140 + GameBinary = "PenDriverPro/Binaries/Win64/PenDriverPro-Win64-Shipping.exe" + GameDataPath = "PenDriverPro" + GameDataUE4SSMods = "Binaries/Win64/Mods" + GameDataPakMods = "Content/Paks/~Mods" + GameDataMovieMods = "Content/Movies" + GameDocumentsDirectory = "%LOCALAPPDATA%/PenDriverPro/Saved/Config/WindowsNoEditor" + GameSaveExtension = "sav" + _main_window: QMainWindow + _ue4ss_tab: UE4SSTabWidget + _paks_tab: PaksTabWidget + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self.dataChecker = PacificDriveModDataChecker(organizer) + self._register_feature(self.dataChecker) + self._register_feature(PacificDriveModDataContent()) + organizer.onUserInterfaceInitialized(self.init_tab) + return True + + def init_tab(self, main_window: QMainWindow): + if self._organizer.managedGame() != self: + return + self._main_window = main_window + tab_widget: QTabWidget = main_window.findChild(QTabWidget, "tabWidget") + if not tab_widget or not tab_widget.findChild(QWidget, "espTab"): + return + self._ue4ss_tab = UE4SSTabWidget(main_window, self._organizer) + plugin_tab = tab_widget.findChild(QWidget, "espTab") + tab_index = tab_widget.indexOf(plugin_tab) + 1 + if not tab_widget.isTabVisible(tab_widget.indexOf(plugin_tab)): + tab_index += 1 + tab_widget.insertTab(tab_index, self._ue4ss_tab, "UE4SS") + self._paks_tab = PaksTabWidget(main_window, self._organizer) + tab_index += 1 + tab_widget.insertTab(tab_index, self._paks_tab, "Paks") + + def executables(self): + return [ + mobase.ExecutableInfo( + "Pacific Drive", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ) + ] + + @cached_property + def _base_dlls(self) -> set[str]: + base_dir = Path(self.gameDirectory().absolutePath()) + return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + try: + efls = super().executableForcedLoads() + except AttributeError: + efls = [] + libs: set[str] = set() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + if type(tree) is not mobase.IFileTree: + return efls + for e in tree: + relpath = e.pathFrom(tree) + if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + libs.add(relpath) + exes = self.executables() + efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + return efls + + def paksDirectory(self) -> QDir: + return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataPakMods) + + def ue4ssDirectory(self) -> QDir: + return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataUE4SSMods) + + def movieDirectory(self) -> QDir: + return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataMovieMods) + + def write_default_mods(self, profile: QDir): + ue4ss_mods_txt = QFileInfo(profile.absoluteFilePath("mods.txt")) + ue4ss_mods_json = QFileInfo(profile.absoluteFilePath("mods.json")) + if not ue4ss_mods_txt.exists(): + with open(ue4ss_mods_txt.absoluteFilePath(), "w") as mods_txt: + for mod in DEFAULT_UE4SS_MODS: + mods_txt.write(f"{mod['mod_name']} : 1\n") + if not ue4ss_mods_json.exists(): + mods_data: list[UE4SSModInfo] = [] + for mod in DEFAULT_UE4SS_MODS: + mods_data.append({"mod_name": mod["mod_name"], "mod_enabled": True}) + with open(ue4ss_mods_json.absoluteFilePath(), "w") as mods_json: + mods_json.write(json.dumps(mods_data, indent=4)) + + def iniFiles(self): + return ["GameUserSettings.ini", "Input.ini"] + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + self.write_default_mods(directory) + if not self.paksDirectory().exists(): + os.makedirs(self.paksDirectory().absolutePath()) + if not self.ue4ssDirectory().exists(): + os.makedirs(self.ue4ssDirectory().absolutePath()) + if not self.movieDirectory().exists(): + os.makedirs(self.movieDirectory().absolutePath()) + super().initializeProfile(directory, settings) \ No newline at end of file diff --git a/games/game_payday1.py b/games/game_payday1.py new file mode 100644 index 00000000..a7766f85 --- /dev/null +++ b/games/game_payday1.py @@ -0,0 +1,277 @@ +import os +import shutil +import mobase + +from enum import IntEnum, auto +from pathlib import Path +from typing import Any, List, Set, cast +from functools import cached_property + +from ..basic_game import BasicGame + +try: + from PyQt6.QtCore import QDir, QFileInfo +except: + from PyQt5.QtCore import QDir, QFileInfo + + +class Content(IntEnum): + TEXTURE = auto() + MESH = auto() + SCRIPT = auto() + SOUND = auto() + STRING = auto() + CONFIG = auto() + + +class Payday1ModDataContent(mobase.ModDataContent): + GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ + (Content.TEXTURE, "Textures", ":/MO/gui/content/texture"), + (Content.MESH, "Meshes", ":/MO/gui/content/mesh"), + (Content.SCRIPT, "Scripts", ":/MO/gui/content/script"), + (Content.SOUND, "Sounds", ":/MO/gui/content/sound"), + (Content.STRING, "Strings", ":/MO/gui/content/string"), + (Content.CONFIG, "Configs", ":/MO/gui/content/inifile"), + ] + + def getAllContents(self) -> list[mobase.ModDataContent.Content]: + return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + + contents = set() + + def walkContent(self, path: str, entry: mobase.FileTreeEntry): + if entry.isFile(): + match entry.suffix().casefold(): + case "texture": + self.contents.add(Content.TEXTURE) + case "model": + self.contents.add(Content.MESH) + case "lua": + self.contents.add(Content.SCRIPT) + case "stream": + self.contents.add(Content.SOUND) + case "txt": + self.contents.add(Content.STRING) + case "json": + self.contents.add(Content.CONFIG) + case _: + pass + return mobase.IFileTree.WalkReturn.CONTINUE + + def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: + filetree.walk(self.walkContent, "/") + return list(self.contents) + + +class Payday1ModDataChecker(mobase.ModDataChecker): + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self.organizer: mobase.IOrganizer = organizer + self.organizer.modList().onModInstalled(self._Fix_Installed_Mod) + self.needsNameFix = False + + def move_overwrite_merge(self, source, destination): + if not os.path.exists(destination): + shutil.move(source, destination) + return + if os.path.isfile(source): + os.replace(source, destination) + return + for item in os.listdir(source): + s_item = os.path.join(source, item) + d_item = os.path.join(destination, item) + self.move_overwrite_merge(s_item, d_item) + os.rmdir(source) + + def _Fix_Installed_Mod(self, mod: mobase.IModInterface): + if not self.needsNameFix: + return + filetree: mobase.IFileTree = mod.fileTree() + fixed = False + modname = mod.name() + if filetree is not None and filetree.exists("mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): + path = mod.absolutePath() + old_path = os.path.join(path, "mods/FOLDERNAME") + new_path = os.path.join(path, f"mods/{modname}") + self.move_overwrite_merge(old_path, new_path) + fixed = True + elif filetree is not None and filetree.exists("assets/mod_overrides/FOLDERNAME/", mobase.IFileTree.DIRECTORY): + path = mod.absolutePath() + old_path = os.path.join(path, "assets/mod_overrides/FOLDERNAME") + new_path = os.path.join(path, f"assets/mod_overrides/{modname}") + self.move_overwrite_merge(old_path, new_path) + fixed = True + elif filetree is not None and filetree.exists("maps/FOLDERNAME", mobase.IFileTree.DIRECTORY): + path = mod.absolutePath() + old_path = os.path.join(path, "maps/FOLDERNAME") + new_path = os.path.join(path, f"maps/{modname}") + self.move_overwrite_merge(old_path, new_path) + fixed = True + if not fixed: + return + self.needsNameFix = False + + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + if filetree.exists("assets/mod_overrides", mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + if filetree.exists("mods", mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + if filetree.exists("maps", mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + for e in filetree: + if e is not None and e.suffix().casefold() == "dll": + return mobase.ModDataChecker.VALID + return mobase.ModDataChecker.FIXABLE + + def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): + for branch in filetree: + if branch is not None and branch.isDir(): + for e in branch: + if e is not None and e.name() == name: + return True + return False + + def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): + entriesToMove: list[mobase.FileTreeEntry] = [] + retVal = 0 + for e in filetree: + if e is not None: + entriesToMove.append(e) + for e in entriesToMove: + filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) + retVal = 1 + return retVal + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + treefixed = 0 + if filetree.exists("mod.txt", mobase.IFileTree.FILE): + treefixed = self.allMoveTo(filetree, "mods/FOLDERNAME/") + if treefixed == 1: + self.needsNameFix = True + elif self.fileExistsInNextSubDir(filetree, "mod.txt"): + filetree.move(filetree[0], "mods/", mobase.IFileTree.MERGE) + treefixed = 1 + elif self.fileExistsInNextSubDir(filetree, "main.xml"): + if self.fileExistsInNextSubDir(filetree, "levels"): + filetree.move(filetree[0], "maps/", mobase.IFileTree.MERGE) + treefixed = 1 + else: + filetree.move(filetree[0], "assets/mod_overrides/", mobase.IFileTree.MERGE) + treefixed = 1 + elif filetree.exists("main.xml", mobase.IFileTree.FILE): + if filetree.exists("levels", mobase.IFileTree.DIRECTORY): + treefixed = self.move_overwrite_merge(filetree, "maps/FOLDERNAME") + if treefixed == 1: + self.needsNameFix = True + else: + treefixed = self.allMoveTo(filetree, "assets/mod_overrides/FOLDERNAME/") + if treefixed == 1: + self.needsNameFix = True + else: + if filetree[0][0].exists("mod.txt", mobase.IFileTree.FILE): + filetree.move(filetree[0][0], filetree[0].path("/"), mobase.IFileTree.REPLACE) + filetree.move(filetree[0], "mods/", mobase.IFileTree.MERGE) + treefixed = 1 + elif filetree[0][0].exists("main.xml", mobase.IFileTree.FILE): + if filetree.exists("levels", mobase.IFileTree.DIRECTORY): + filetree.move(filetree[0][0], filetree[0].path("/"), mobase.IFileTree.REPLACE) + filetree.move(filetree[0], "maps/", mobase.IFileTree.MERGE) + treefixed = 1 + else: + filetree.move(filetree[0][0], filetree[0].path("/"), mobase.IFileTree.REPLACE) + filetree.move(filetree[0], "assets/mod_overrides/", mobase.IFileTree.MERGE) + treefixed = 1 + if treefixed == 0: + if len(filetree) == 1: + filetree.move(filetree[0], "assets/mod_overrides/", mobase.IFileTree.MERGE) + treefixed = 1 + else: + for e in filetree: + if e is not None and e.path("/").count("/") == 0: + filetree.move(e, "assets/mod_overrides/FOLDERNAME/", mobase.IFileTree.MERGE) + treefixed = 1 + self.needsNameFix = True + if treefixed == 0: + return None + return filetree + + +class Payday1Game(BasicGame): + Name = "Payday 1 Support Plugin" + Author = "modworkshop" + Version = "1" + GameName = "Payday: The Heist" + GameShortName = "pdth" + GameSteamId = 24240 + GameBinary = "payday_win32_release.exe" + GameDataPath = "%GAME_PATH%" + GameDocumentsDirectory = "%LOCALAPPDATA%/PAYDAY" + _forced_libraries = ["IPHLPAPI.dll", "WSOCK32.dll"] + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self.dataChecker = Payday1ModDataChecker(organizer) + self._register_feature(self.dataChecker) + self._register_feature(Payday1ModDataContent()) + organizer.modList().onModStateChanged(self.dll_copy) + return True + + def dll_copy( + self, mods: dict[str, mobase.ModState] + ): + + game_path = self.dataDirectory().absolutePath() + "/" + + for key, value in mods.items(): + key = self._organizer.modList().getMod(key) + tree = key.fileTree() + for e in tree: + if e is not None and e.name() in self._forced_libraries: + #add file + file_path_source = key.absolutePath() + "/" + e.path() + file_path_target = game_path + e.name() + if value == 35: + shutil.copyfile(file_path_source, file_path_target) + #remove file + if value == 33: + if os.path.exists(file_path_target): + os.remove(file_path_target) + + def executables(self): + return [ + mobase.ExecutableInfo( + "Payday: The Heist", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ), + ] + + @cached_property + def _base_dlls(self) -> set[str]: + base_dir = Path(self.gameDirectory().absolutePath()) + return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + try: + efls = super().executableForcedLoads() + except AttributeError: + efls = [] + libs: set[str] = set() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + if type(tree) is not mobase.IFileTree: + return efls + for e in tree: + relpath = e.pathFrom(tree) + if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + libs.add(relpath) + exes = self.executables() + efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + return efls + + def iniFiles(self): + return ["renderer_settings.xml"] + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + modsPath = self.dataDirectory().absolutePath() + if not os.path.exists(modsPath): + os.mkdir(modsPath) + super().initializeProfile(directory, settings) \ No newline at end of file diff --git a/games/game_payday2.py b/games/game_payday2.py new file mode 100644 index 00000000..24b29b5f --- /dev/null +++ b/games/game_payday2.py @@ -0,0 +1,295 @@ +import os +import shutil +import mobase + +from enum import IntEnum, auto +from pathlib import Path +from typing import Any, List, Set, cast +from functools import cached_property + +from ..basic_game import BasicGame + +try: + from PyQt6.QtCore import QDir, QFileInfo +except: + from PyQt5.QtCore import QDir, QFileInfo + + +class Content(IntEnum): + TEXTURE = auto() + MESH = auto() + SCRIPT = auto() + SOUND = auto() + STRING = auto() + CONFIG = auto() + + +class Payday2ModDataContent(mobase.ModDataContent): + GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ + (Content.TEXTURE, "Textures", ":/MO/gui/content/texture"), + (Content.MESH, "Meshes", ":/MO/gui/content/mesh"), + (Content.SCRIPT, "Scripts", ":/MO/gui/content/script"), + (Content.SOUND, "Sounds", ":/MO/gui/content/sound"), + (Content.STRING, "Strings", ":/MO/gui/content/string"), + (Content.CONFIG, "Configs", ":/MO/gui/content/inifile"), + ] + + def getAllContents(self) -> list[mobase.ModDataContent.Content]: + return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + + contents = set() + + def walkContent(self, path: str, entry: mobase.FileTreeEntry): + if entry.isFile(): + match entry.suffix().casefold(): + case "texture": + self.contents.add(Content.TEXTURE) + case "model": + self.contents.add(Content.MESH) + case "lua": + self.contents.add(Content.SCRIPT) + case "stream": + self.contents.add(Content.SOUND) + case "txt": + self.contents.add(Content.STRING) + case "json": + self.contents.add(Content.CONFIG) + case _: + pass + return mobase.IFileTree.WalkReturn.CONTINUE + + def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: + filetree.walk(self.walkContent, "/") + return list(self.contents) + + +class Payday2ModDataChecker(mobase.ModDataChecker): + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self.organizer: mobase.IOrganizer = organizer + self.organizer.modList().onModInstalled(self._Fix_Installed_Mod) + self.needsNameFix = False + + def move_overwrite_merge(self, source, destination): + if not os.path.exists(destination): + shutil.move(source, destination) + return + if os.path.isfile(source): + os.replace(source, destination) + return + for item in os.listdir(source): + s_item = os.path.join(source, item) + d_item = os.path.join(destination, item) + self.move_overwrite_merge(s_item, d_item) + os.rmdir(source) + + def _Fix_Installed_Mod(self, mod: mobase.IModInterface): + if not self.needsNameFix: + return + filetree: mobase.IFileTree = mod.fileTree() + fixed = False + modname = mod.name() + if filetree is not None and filetree.exists("mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): + path = mod.absolutePath() + old_path = os.path.join(path, "mods/FOLDERNAME") + new_path = os.path.join(path, f"mods/{modname}") + self.move_overwrite_merge(old_path, new_path) + fixed = True + elif filetree is not None and filetree.exists("assets/mod_overrides/FOLDERNAME", mobase.IFileTree.DIRECTORY): + path = mod.absolutePath() + old_path = os.path.join(path, "assets/mod_overrides/FOLDERNAME") + new_path = os.path.join(path, f"assets/mod_overrides/{modname}") + self.move_overwrite_merge(old_path, new_path) + fixed = True + elif filetree is not None and filetree.exists("maps/FOLDERNAME", mobase.IFileTree.DIRECTORY): + path = mod.absolutePath() + old_path = os.path.join(path, "maps/FOLDERNAME") + new_path = os.path.join(path, f"maps/{modname}") + self.move_overwrite_merge(old_path, new_path) + fixed = True + if not fixed: + return + self.needsNameFix = False + + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + if filetree.exists("assets/mod_overrides", mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + if filetree.exists("mods", mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + if filetree.exists("maps", mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + if filetree.exists("IPHLPAPI.dll", mobase.IFileTree.FILE): + return mobase.ModDataChecker.VALID + if filetree.exists("WSOCK32.dll", mobase.IFileTree.FILE): + return mobase.ModDataChecker.VALID + return mobase.ModDataChecker.FIXABLE + + def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): + for branch in filetree: + if branch is not None and branch.isDir(): + for e in branch: + if e is not None and e.name() == name: + return True + return False + + def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): + entriesToMove: list[mobase.FileTreeEntry] = [] + retVal = 0 + for e in filetree: + if e is not None: + entriesToMove.append(e) + for e in entriesToMove: + filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) + retVal = 1 + return retVal + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + treefixed = 0 + + if filetree.exists("mod.txt", mobase.IFileTree.FILE): + treefixed = self.allMoveTo(filetree, "mods/FOLDERNAME/") + if treefixed == 1: + self.needsNameFix = True + elif self.fileExistsInNextSubDir(filetree, "mod.txt"): + filetree.move(filetree[0], "mods/", mobase.IFileTree.MERGE) + treefixed = 1 + elif self.fileExistsInNextSubDir(filetree, "main.xml"): + if self.fileExistsInNextSubDir(filetree, "levels"): + filetree.move(filetree[0], "maps/", mobase.IFileTree.MERGE) + treefixed = 1 + else: + filetree.move(filetree[0], "assets/mod_overrides/", mobase.IFileTree.MERGE) + treefixed = 1 + elif filetree.exists("main.xml", mobase.IFileTree.FILE): + if filetree.exists("levels", mobase.IFileTree.DIRECTORY): + treefixed = self.move_overwrite_merge(filetree, "maps/FOLDERNAME") + if treefixed == 1: + self.needsNameFix = True + else: + treefixed = self.allMoveTo(filetree, "assets/mod_overrides/FOLDERNAME/") + if treefixed == 1: + self.needsNameFix = True + else: + try: + if filetree[0][0].exists("mod.txt", mobase.IFileTree.FILE): + filetree.move(filetree[0][0], filetree[0].path("/"), mobase.IFileTree.REPLACE) + filetree.move(filetree[0], "mods/", mobase.IFileTree.MERGE) + treefixed = 1 + elif filetree[0][0].exists("main.xml", mobase.IFileTree.FILE): + if filetree.exists("levels", mobase.IFileTree.DIRECTORY): + filetree.move(filetree[0][0], filetree[0].path("/"), mobase.IFileTree.REPLACE) + filetree.move(filetree[0], "maps/", mobase.IFileTree.MERGE) + treefixed = 1 + else: + filetree.move(filetree[0][0], filetree[0].path("/"), mobase.IFileTree.REPLACE) + filetree.move(filetree[0], "assets/mod_overrides/", mobase.IFileTree.MERGE) + except TypeError: + pass + if treefixed == 0: + if len(filetree) == 1 and filetree[0].isDir: + filetree.move(filetree[0], "assets/mod_overrides/", mobase.IFileTree.MERGE) + treefixed = 1 + else: + for e in filetree: + if e is not None and e.path("/").count("/") == 0: + filetree.move(e, "assets/mod_overrides/FOLDERNAME/", mobase.IFileTree.MERGE) + treefixed = 1 + self.needsNameFix = True + if treefixed == 0: + return None + return filetree + + +class Payday2Game(BasicGame): + Name = "Payday 2 Support Plugin" + Author = "modworkshop" + Version = "1" + GameName = "Payday 2" + GameShortName = "payday-2" + GameSteamId = 218620 + GameBinary = "payday2_win32_release.exe" + GameDataPath = "%GAME_PATH%" + GameDocumentsDirectory = "%USERPROFILE%/AppData/Local/PAYDAY 2" + GameSavesDirectory = "%USERPROFILE%/AppData/Local/PAYDAY 2/saves" + _forced_libraries = ["IPHLPAPI.dll", "WSOCK32.dll"] + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self.dataChecker = Payday2ModDataChecker(organizer) + self._register_feature(self.dataChecker) + self._register_feature(Payday2ModDataContent()) + organizer.modList().onModStateChanged(self.dll_copy) + return True + + def executables(self): + return [ + mobase.ExecutableInfo( + "Payday 2", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ), + mobase.ExecutableInfo("Payday 2 VR", QFileInfo(self.gameDirectory(), "payday2_win32_release_vr.exe")), + ] + + def dll_copy( + self, mods: dict[str, mobase.ModState] + ): + + game_path = self.dataDirectory().absolutePath() + "/" + + for key, value in mods.items(): + key = self._organizer.modList().getMod(key) + tree = key.fileTree() + for e in tree: + if e is not None and e.name() in self._forced_libraries: + #add file + file_path_source = key.absolutePath() + "/" + e.path() + file_path_target = game_path + e.name() + if value == 35: + shutil.copyfile(file_path_source, file_path_target) + #remove file + if value == 33: + if os.path.exists(file_path_target): + os.remove(file_path_target) + + @cached_property + def _base_dlls(self) -> set[str]: + base_dir = Path(self.gameDirectory().absolutePath()) + return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + try: + efls = super().executableForcedLoads() + except AttributeError: + efls = [] + libs: set[str] = set() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + if type(tree) is not mobase.IFileTree: + return efls + for e in tree: + relpath = e.pathFrom(tree) + if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + libs.add(relpath) + exes = self.executables() + efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + return efls + + def mapsDirectory(self) -> QDir: + return QDir(self.gameDirectory().absolutePath() + "/maps") + + def modsDirectory(self) -> QDir: + return QDir(self.gameDirectory().absolutePath() + "/mods") + + def overridesDirectory(self) -> QDir: + return QDir(self.gameDirectory().absolutePath() + "/assets/mod_overrides") + + def iniFiles(self): + return ["renderer_settings.xml"] + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + if not self.mapsDirectory().exists(): + os.makedirs(self.mapsDirectory().absolutePath()) + if not self.modsDirectory().exists(): + os.makedirs(self.modsDirectory().absolutePath()) + if not self.overridesDirectory().exists(): + os.makedirs(self.overridesDirectory().absolutePath()) + super().initializeProfile(directory, settings) \ No newline at end of file diff --git a/games/game_payday3.py b/games/game_payday3.py new file mode 100644 index 00000000..c0060bd6 --- /dev/null +++ b/games/game_payday3.py @@ -0,0 +1,284 @@ +import json +import os +import shutil +import mobase + +from enum import IntEnum, auto +from pathlib import Path +from typing import Any, List, Set, cast +from functools import cached_property + +from .unreal_tabs.constants import DEFAULT_UE4SS_MODS, UE4SSModInfo +from .unreal_tabs.manage_paks.widget import PaksTabWidget +from .unreal_tabs.manage_ue4ss.widget import UE4SSTabWidget + +from ..basic_game import BasicGame + +try: + from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget + from PyQt6.QtCore import QDir, QFileInfo +except: + from PyQt5.QtWidgets import QMainWindow, QTabWidget, QWidget + from PyQt5.QtCore import QDir, QFileInfo + + +class Content(IntEnum): + UCAS = auto() + UTOC = auto() + PAK = auto() + UE4SS = auto() + DLL = auto() + BK2 = auto() + + +class Payday3ModDataContent(mobase.ModDataContent): + contents: list[int] = [] + GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ + (Content.UCAS, "UCAS", ":/MO/gui/content/geometries"), + (Content.UTOC, "UTOC", ":/MO/gui/content/inifile"), + (Content.PAK, "PAK", ":/MO/gui/content/geometries"), + (Content.UE4SS, "UE4SS", ":/MO/gui/content/script"), + (Content.DLL, "DLL", ":/MO/gui/content/skse"), + (Content.BK2, "Video", ":/MO/gui/content/skse"), + ] + + def getAllContents(self) -> list[mobase.ModDataContent.Content]: + return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + + def walkContent(self, path: str, entry: mobase.FileTreeEntry): + if entry.isFile(): + match entry.suffix().casefold(): + case "utoc": + self.contents.add(Content.UTOC) + case "ucas": + self.contents.add(Content.UCAS) + case "pak": + self.contents.add(Content.PAK) + case "lua": + self.contents.add(Content.UE4SS) + case "dll": + self.contents.add(Content.DLL) + case "bk2": + self.contents.add(Content.BK2) + case _: + pass + return mobase.IFileTree.WalkReturn.CONTINUE + + def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: + self.contents: set[int] = set() + filetree.walk(self.walkContent, "/") + return list(self.contents) + + +class Payday3ModDataChecker(mobase.ModDataChecker): + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self.organizer: mobase.IOrganizer = organizer + + def move_overwrite_merge(self, source, destination): + if not os.path.exists(destination): + shutil.move(source, destination) + return + if os.path.isfile(source): + os.replace(source, destination) + return + for item in os.listdir(source): + s_item = os.path.join(source, item) + d_item = os.path.join(destination, item) + self.move_overwrite_merge(s_item, d_item) + os.rmdir(source) + + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods + GameDataPakMods = self.organizer.managedGame().GameDataPakMods + GameDataMovies = self.organizer.managedGame().GameDataMovieMods + if filetree.exists(GameDataPakMods, mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + if filetree.exists(GameDataMovies, mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + if filetree.exists(GameDataUE4SSMods, mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + return mobase.ModDataChecker.FIXABLE + + def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): + for branch in filetree: + if branch is not None and branch.isDir(): + for e in branch: + if e is not None and e.name() == name: + return True + return False + + def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): + entriesToMove: list[mobase.FileTreeEntry] = [] + retVal = 0 + for e in filetree: + if e is not None: + entriesToMove.append(e) + for e in entriesToMove: + filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) + retVal = 1 + return retVal + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods + "/" + GameDataPakMods = self.organizer.managedGame().GameDataPakMods + "/" + GameDataMovies = self.organizer.managedGame().GameDataMovieMods + "/" + treefixed = 0 + if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): + treefixed = self.allMoveTo(filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/") + if treefixed == 1: + return filetree + if filetree.exists("Scripts", mobase.IFileTree.DIRECTORY) or filetree.exists("dlls", mobase.IFileTree.DIRECTORY): + treefixed = self.allMoveTo(filetree, GameDataUE4SSMods) + if treefixed == 1: + return filetree + if treefixed == 0: + allowedUnzippedExt = ["pak", "utoc", "ucas", "bk2", "dll"] + entriesToMove: list[mobase.FileTreeEntry] = [] + for e in filetree: + if e is not None: + if e.isFile(): + fileext = e.suffix().casefold() + if fileext in allowedUnzippedExt: + mod_name = filetree.name() + if mod_name == "": + mod_name = e.name() + mod_path = os.path.join(self.organizer.modsPath(), mod_name) + if filetree.createOrphanTree("OrphanTree") is None and os.path.exists(mod_path): + match e.suffix().casefold(): + case "pak" | "utoc" | "ucas": + os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) + case "bk2": + os.makedirs(os.path.join(mod_path, GameDataMovies), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovies, e.name())) + case _: + pass + treefixed = 1 + else: + entriesToMove.append(e) + if entriesToMove is not None: + for e in entriesToMove: + match e.suffix().casefold(): + case "pak" | "utoc" | "ucas": + filetree.move(e, GameDataPakMods, mobase.IFileTree.MERGE) + case "dll": + filetree.move(e, os.path.dirname(GameDataUE4SSMods) + "/", mobase.IFileTree.MERGE) + case "bk2": + filetree.move(e, GameDataMovies, mobase.IFileTree.MERGE) + case _: + pass + treefixed = 1 + if treefixed == 0: + return None + return filetree + + +class Payday3Game(BasicGame): + Name = "Payday 3 Support Plugin" + Author = "modworkshop, MaskPlague and Silarn" + Version = "1" + GameName = "Payday 3" + GameShortName = "payday-3" + GameSteamId = 1272080 + GameBinary = "PAYDAY3/Binaries/Win64/PAYDAY3-Win64-Shipping.exe" + GameDataPath = "PAYDAY3" + GameDataUE4SSMods = "Binaries/Win64/Mods" + GameDataPakMods = "Content/Paks/~Mods" + GameDataMovieMods = "Content/Movies" + GameDocumentsDirectory = "%LOCALAPPDATA%/PAYDAY3/Saved/Config/WindowsClient" + GameSaveExtension = "sav" + _main_window: QMainWindow + _ue4ss_tab: UE4SSTabWidget + _paks_tab: PaksTabWidget + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self.dataChecker = Payday3ModDataChecker(organizer) + self._register_feature(self.dataChecker) + self._register_feature(Payday3ModDataContent()) + organizer.onUserInterfaceInitialized(self.init_tab) + return True + + def init_tab(self, main_window: QMainWindow): + if self._organizer.managedGame() != self: + return + self._main_window = main_window + tab_widget: QTabWidget = main_window.findChild(QTabWidget, "tabWidget") + if not tab_widget or not tab_widget.findChild(QWidget, "espTab"): + return + self._ue4ss_tab = UE4SSTabWidget(main_window, self._organizer) + plugin_tab = tab_widget.findChild(QWidget, "espTab") + tab_index = tab_widget.indexOf(plugin_tab) + 1 + if not tab_widget.isTabVisible(tab_widget.indexOf(plugin_tab)): + tab_index += 1 + tab_widget.insertTab(tab_index, self._ue4ss_tab, "UE4SS") + self._paks_tab = PaksTabWidget(main_window, self._organizer) + tab_index += 1 + tab_widget.insertTab(tab_index, self._paks_tab, "Paks") + + def executables(self): + return [ + mobase.ExecutableInfo( + "Payday 3", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ).withArgument("-fileopenlog") + ] + + @cached_property + def _base_dlls(self) -> set[str]: + base_dir = Path(self.gameDirectory().absolutePath()) + return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + try: + efls = super().executableForcedLoads() + except AttributeError: + efls = [] + libs: set[str] = set() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + if type(tree) is not mobase.IFileTree: + return efls + for e in tree: + relpath = e.pathFrom(tree) + if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + libs.add(relpath) + exes = self.executables() + efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + return efls + + def paksDirectory(self) -> QDir: + return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataPakMods) + + def ue4ssDirectory(self) -> QDir: + return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataUE4SSMods) + + def movieDirectory(self) -> QDir: + return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataMovieMods) + + def write_default_mods(self, profile: QDir): + ue4ss_mods_txt = QFileInfo(profile.absoluteFilePath("mods.txt")) + ue4ss_mods_json = QFileInfo(profile.absoluteFilePath("mods.json")) + if not ue4ss_mods_txt.exists(): + with open(ue4ss_mods_txt.absoluteFilePath(), "w") as mods_txt: + for mod in DEFAULT_UE4SS_MODS: + mods_txt.write(f"{mod['mod_name']} : 1\n") + if not ue4ss_mods_json.exists(): + mods_data: list[UE4SSModInfo] = [] + for mod in DEFAULT_UE4SS_MODS: + mods_data.append({"mod_name": mod["mod_name"], "mod_enabled": True}) + with open(ue4ss_mods_json.absoluteFilePath(), "w") as mods_json: + mods_json.write(json.dumps(mods_data, indent=4)) + + def iniFiles(self): + return ["GameUserSettings.ini", "Input.ini"] + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + self.write_default_mods(directory) + if not self.paksDirectory().exists(): + os.makedirs(self.paksDirectory().absolutePath()) + if not self.ue4ssDirectory().exists(): + os.makedirs(self.ue4ssDirectory().absolutePath()) + if not self.movieDirectory().exists(): + os.makedirs(self.movieDirectory().absolutePath()) + super().initializeProfile(directory, settings) \ No newline at end of file diff --git a/games/game_raid2.py b/games/game_raid2.py new file mode 100644 index 00000000..0d089d74 --- /dev/null +++ b/games/game_raid2.py @@ -0,0 +1,211 @@ +import os +import shutil +import mobase + +from enum import IntEnum, auto +from pathlib import Path +from typing import Any, List, Set, cast +from functools import cached_property + +from ..basic_game import BasicGame + +try: + from PyQt6.QtCore import QDir, QFileInfo +except: + from PyQt5.QtCore import QDir, QFileInfo + + +class Content(IntEnum): + TEXTURE = auto() + MESH = auto() + SCRIPT = auto() + SOUND = auto() + STRING = auto() + CONFIG = auto() + + +class RaidWW2ModDataContent(mobase.ModDataContent): + GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ + (Content.TEXTURE, "Textures", ":/MO/gui/content/texture"), + (Content.MESH, "Meshes", ":/MO/gui/content/mesh"), + (Content.SCRIPT, "Scripts", ":/MO/gui/content/script"), + (Content.SOUND, "Sounds", ":/MO/gui/content/sound"), + (Content.STRING, "Strings", ":/MO/gui/content/string"), + (Content.CONFIG, "Configs", ":/MO/gui/content/inifile"), + ] + + def getAllContents(self) -> list[mobase.ModDataContent.Content]: + return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + + contents = set() + + def walkContent(self, path: str, entry: mobase.FileTreeEntry): + if entry.isFile(): + match entry.suffix().casefold(): + case "texture": + self.contents.add(Content.TEXTURE) + case "model": + self.contents.add(Content.MESH) + case "lua": + self.contents.add(Content.SCRIPT) + case "stream": + self.contents.add(Content.SOUND) + case "txt": + self.contents.add(Content.STRING) + case "json": + self.contents.add(Content.CONFIG) + case _: + pass + return mobase.IFileTree.WalkReturn.CONTINUE + + def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: + filetree.walk(self.walkContent, "/") + return list(self.contents) + + +class RaidWW2ModDataChecker(mobase.ModDataChecker): + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self.organizer: mobase.IOrganizer = organizer + self.organizer.modList().onModInstalled(self._Fix_Installed_Mod) + self.needsNameFix = False + + def move_overwrite_merge(self, source, destination): + if not os.path.exists(destination): + shutil.move(source, destination) + return + if os.path.isfile(source): + os.replace(source, destination) + return + for item in os.listdir(source): + s_item = os.path.join(source, item) + d_item = os.path.join(destination, item) + self.move_overwrite_merge(s_item, d_item) + os.rmdir(source) + + def _Fix_Installed_Mod(self, mod: mobase.IModInterface): + if not self.needsNameFix: + return + filetree: mobase.IFileTree = mod.fileTree() + fixed = False + modname = mod.name() + if filetree is not None and filetree.exists("FOLDERNAME", mobase.IFileTree.DIRECTORY): + path = mod.absolutePath() + old_path = os.path.join(path, "FOLDERNAME") + new_path = os.path.join(path, f"{modname}") + self.move_overwrite_merge(old_path, new_path) + fixed = True + if not fixed: + return + self.needsNameFix = False + + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + if len(filetree) == 1: + return mobase.ModDataChecker.VALID + return mobase.ModDataChecker.FIXABLE + + def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): + for branch in filetree: + if branch is not None and branch.isDir(): + for e in branch: + if e is not None and e.name() == name: + return True + return False + + def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): + entriesToMove: list[mobase.FileTreeEntry] = [] + retVal = 0 + for e in filetree: + if e is not None: + entriesToMove.append(e) + for e in entriesToMove: + filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) + retVal = 1 + return retVal + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + treefixed = self.allMoveTo(filetree, "FOLDERNAME/") + if treefixed == 1: + self.needsNameFix = True + return filetree + + +class RaidWW2Game(BasicGame): + Name = "RAID World War II Support Plugin" + Author = "modworkshop" + Version = "1" + GameName = "RAID World War II" + GameShortName = "raidww2" + GameSteamId = 414740 + GameBinary = "raid_win64_release.exe" + GameDataPath = "mods" + GameDocumentsDirectory = "%LOCALAPPDATA%/RAID WW2" + _forced_libraries = ["IPHLPAPI.dll", "WSOCK32.dll"] + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self.dataChecker = RaidWW2ModDataChecker(organizer) + self._register_feature(self.dataChecker) + self._register_feature(RaidWW2ModDataContent()) + organizer.modList().onModStateChanged(self.dll_copy) + return True + + def dll_copy( + self, mods: dict[str, mobase.ModState] + ): + + game_path = self.dataDirectory().absolutePath() + "/" + + for key, value in mods.items(): + key = self._organizer.modList().getMod(key) + tree = key.fileTree() + for e in tree: + if e is not None and e.name() in self._forced_libraries: + #add file + file_path_source = key.absolutePath() + "/" + e.path() + file_path_target = game_path + e.name() + if value == 35: + shutil.copyfile(file_path_source, file_path_target) + #remove file + if value == 33: + if os.path.exists(file_path_target): + os.remove(file_path_target) + + def executables(self): + return [ + mobase.ExecutableInfo( + "Raid: World War II", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ), + ] + + @cached_property + def _base_dlls(self) -> set[str]: + base_dir = Path(self.gameDirectory().absolutePath()) + return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + try: + efls = super().executableForcedLoads() + except AttributeError: + efls = [] + libs: set[str] = set() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + if type(tree) is not mobase.IFileTree: + return efls + for e in tree: + relpath = e.pathFrom(tree) + if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + libs.add(relpath) + exes = self.executables() + efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + return efls + + def iniFiles(self): + return ["renderer_settings.xml"] + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + modsPath = self.dataDirectory().absolutePath() + if not os.path.exists(modsPath): + os.mkdir(modsPath) + super().initializeProfile(directory, settings) \ No newline at end of file diff --git a/games/game_roadtovostok.py b/games/game_roadtovostok.py new file mode 100644 index 00000000..81e5b23e --- /dev/null +++ b/games/game_roadtovostok.py @@ -0,0 +1,89 @@ +import os +import shutil +import mobase + +from enum import IntEnum, auto +from pathlib import Path +from typing import Any, List, Set, cast + +from ..basic_game import BasicGame + +try: + from PyQt6.QtCore import QDir, QFileInfo +except: + from PyQt5.QtCore import QDir, QFileInfo + + +class RoadToVostokModDataChecker(mobase.ModDataChecker): + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self.organizer: mobase.IOrganizer = organizer + + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + + if filetree.exists("mods", mobase.IFileTree.DIRECTORY) and not filetree.exists("mod.txt", mobase.IFileTree.FILE): + return mobase.ModDataChecker.VALID + for e in filetree: + if e is not None and e.isFile() and e.suffix().casefold() == "pck": + return mobase.ModDataChecker.VALID + return mobase.ModDataChecker.FIXABLE + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + GameModsPath = self.organizer.managedGame().GameModsPath + "/" + treefixed = 0 + + for branch in filetree: + mod_name = filetree.name() + if mod_name == "": + mod_name = branch.name() + mod_path = os.path.join(self.organizer.modsPath(), mod_name) + if filetree.createOrphanTree("OrphanTree") is None and os.path.exists(mod_path) and branch.suffix().casefold() == "zip": + os.makedirs(os.path.join(mod_path, GameModsPath), exist_ok=True) + shutil.move(os.path.join(mod_path, branch.name()), os.path.join(mod_path, GameModsPath, branch.name())) + treefixed = 1 + + if treefixed == 0: + return None + return filetree + + +class RoadToVostokGame(BasicGame): + + Name = "Road to Vostok Support Plugin" + Author = "modworkshop" + Version = "1" + GameName = "Road to Vostok" + GameShortName = "road-to-vostok" + GameSteamId = 1963610 + GameBinary = "Road_to_Vostok_Demo.exe" + GameDataPath = "%GAME_PATH%" + GameModsPath = "mods" + GameDocumentsDirectory = '%APPDATA%/Godot/app_userdata/Road to Vostok' + GameSaveExtension = "tres" + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self.dataChecker = RoadToVostokModDataChecker(organizer) + self._register_feature(self.dataChecker) + return True + + def executables(self): + return [ + mobase.ExecutableInfo( + "Road to Vostok (Use Injector)", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ).withArgument("--main-pack Injector.pck"), + mobase.ExecutableInfo( + "Road to Vostok (No Mods)", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ), + ] + + def iniFiles(self): + return ["settings.cfg"] + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + modsPath = self.dataDirectory().absolutePath() + if not os.path.exists(modsPath): + os.mkdir(modsPath) + super().initializeProfile(directory, settings) \ No newline at end of file diff --git a/games/game_silenthill2remake.py b/games/game_silenthill2remake.py index 4683a5d1..022c0e8c 100644 --- a/games/game_silenthill2remake.py +++ b/games/game_silenthill2remake.py @@ -1,113 +1,285 @@ -from typing import Tuple - +import json +import os +import shutil import mobase -from ..basic_features import BasicModDataChecker, GlobPatterns +from enum import IntEnum, auto +from pathlib import Path +from typing import Any, List, Set, cast +from functools import cached_property + +from .unreal_tabs.constants import DEFAULT_UE4SS_MODS, UE4SSModInfo +from .unreal_tabs.manage_paks.widget import PaksTabWidget +from .unreal_tabs.manage_ue4ss.widget import UE4SSTabWidget + from ..basic_game import BasicGame +try: + from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget + from PyQt6.QtCore import QDir, QFileInfo +except: + from PyQt5.QtWidgets import QMainWindow, QTabWidget, QWidget + from PyQt5.QtCore import QDir, QFileInfo -class SilentHill2RemakeModDataChecker(BasicModDataChecker): - def __init__(self): - super().__init__( - GlobPatterns( - delete=[ - "*.txt", - "*.md", - "manifest.json", - "icon.png", - ], - ) - ) - self.mod_path = ["SHProto", "Content", "Paks", "~mod"] - self.mod_path_lower = [name.lower() for name in self.mod_path] - - def _find_tree( - self, filetree: mobase.IFileTree - ) -> Tuple[str | None, mobase.FileTreeEntry | None]: - """ - Search the given filetree for a directory name that matches any component - of self.mod_path (case-insensitive). - - Returns: - (prefix, entry) - prefix: The missing part before the match (e.g. 'SHProto/Content/') - entry: The IFileTree entry that matched (e.g. the 'Paks' directory) - (None, None) if nothing matches. - """ - for entry in filetree: - if not entry.isDir(): - continue - - name_lower = entry.name().lower() - for i, component in enumerate(self.mod_path_lower): - if name_lower == component: - # Build the prefix string for everything *before* this match - prefix_parts = self.mod_path[:i] - prefix = "/".join(prefix_parts) + ("/" if prefix_parts else "") - return (prefix, entry) - # No matches found - return (None, None) - - def dataLooksValid( - self, filetree: mobase.IFileTree - ) -> mobase.ModDataChecker.CheckReturn: - # Check for fully valid layout - has_entry, _ = self._find_tree(filetree) - if has_entry is None: - # in this case we check to make sure there's a .pak file - for entry in filetree: - if entry.name().lower().endswith(".pak") and entry.isFile(): - return mobase.ModDataChecker.FIXABLE - elif has_entry == "": + +class Content(IntEnum): + UCAS = auto() + UTOC = auto() + PAK = auto() + UE4SS = auto() + DLL = auto() + BK2 = auto() + + +class SilentHill2ModDataContent(mobase.ModDataContent): + contents: list[int] = [] + GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ + (Content.UCAS, "UCAS", ":/MO/gui/content/geometries"), + (Content.UTOC, "UTOC", ":/MO/gui/content/inifile"), + (Content.PAK, "PAK", ":/MO/gui/content/geometries"), + (Content.UE4SS, "UE4SS", ":/MO/gui/content/script"), + (Content.DLL, "DLL", ":/MO/gui/content/skse"), + (Content.BK2, "Video", ":/MO/gui/content/skse"), + ] + + def getAllContents(self) -> list[mobase.ModDataContent.Content]: + return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + + def walkContent(self, path: str, entry: mobase.FileTreeEntry): + if entry.isFile(): + match entry.suffix().casefold(): + case "utoc": + self.contents.add(Content.UTOC) + case "ucas": + self.contents.add(Content.UCAS) + case "pak": + self.contents.add(Content.PAK) + case "lua": + self.contents.add(Content.UE4SS) + case "dll": + self.contents.add(Content.DLL) + case "bk2": + self.contents.add(Content.BK2) + case _: + pass + return mobase.IFileTree.WalkReturn.CONTINUE + + def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: + self.contents: set[int] = set() + filetree.walk(self.walkContent, "/") + return list(self.contents) + + +class SilentHill2ModDataChecker(mobase.ModDataChecker): + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self.organizer: mobase.IOrganizer = organizer + + def move_overwrite_merge(self, source, destination): + if not os.path.exists(destination): + shutil.move(source, destination) + return + if os.path.isfile(source): + os.replace(source, destination) + return + for item in os.listdir(source): + s_item = os.path.join(source, item) + d_item = os.path.join(destination, item) + self.move_overwrite_merge(s_item, d_item) + os.rmdir(source) + + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods + GameDataPakMods = self.organizer.managedGame().GameDataPakMods + GameDataMovies = self.organizer.managedGame().GameDataMovieMods + if filetree.exists(GameDataPakMods, mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + if filetree.exists(GameDataMovies, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID - else: - return mobase.ModDataChecker.FIXABLE + if filetree.exists(GameDataUE4SSMods, mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + return mobase.ModDataChecker.FIXABLE + + def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): + for branch in filetree: + if branch is not None and branch.isDir(): + for e in branch: + if e is not None and e.name() == name: + return True + return False - # Otherwise, not recognizable - return mobase.ModDataChecker.INVALID + def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): + entriesToMove: list[mobase.FileTreeEntry] = [] + retVal = 0 + for e in filetree: + if e is not None: + entriesToMove.append(e) + for e in entriesToMove: + filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) + retVal = 1 + return retVal def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: - filetree = super().fix(filetree) - prefix, item = self._find_tree(filetree) - if prefix is None: - foundAPak = False - # Move all top-level items to BepInEx/plugins/ - items_to_move = list(filetree) - for cur_item in items_to_move: - if cur_item.name().lower().endswith(".pak"): - foundAPak = True - filetree.move(cur_item, f"SHProto/Content/Paks/~mod/{cur_item.name()}") - # foundAPack MUST be true because if 'prefix' returned None then - # there must be a .pak file or dataLooksValid wouldn't have returned - # a FIXABLE. This is therefore just a sanity check - assert foundAPak - return filetree - elif prefix == "": - return filetree - else: - # if prefix is not None then item cannot be None - assert item is not None - filetree.move(item, f"{prefix}{item.name()}") - return filetree - - -class SilentHill2RemakeGame(BasicGame): + GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods + "/" + GameDataPakMods = self.organizer.managedGame().GameDataPakMods + "/" + GameDataMovies = self.organizer.managedGame().GameDataMovieMods + "/" + treefixed = 0 + if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): + treefixed = self.allMoveTo(filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/") + if treefixed == 1: + return filetree + if filetree.exists("Scripts", mobase.IFileTree.DIRECTORY) or filetree.exists("dlls", mobase.IFileTree.DIRECTORY): + treefixed = self.allMoveTo(filetree, GameDataUE4SSMods) + if treefixed == 1: + return filetree + if treefixed == 0: + allowedUnzippedExt = ["pak", "utoc", "ucas", "bk2", "dll"] + entriesToMove: list[mobase.FileTreeEntry] = [] + for e in filetree: + if e is not None: + if e.isFile(): + fileext = e.suffix().casefold() + if fileext in allowedUnzippedExt: + mod_name = filetree.name() + if mod_name == "": + mod_name = e.name() + mod_path = os.path.join(self.organizer.modsPath(), mod_name) + if filetree.createOrphanTree("OrphanTree") is None and os.path.exists(mod_path): + match e.suffix().casefold(): + case "pak" | "utoc" | "ucas": + os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) + case "bk2": + os.makedirs(os.path.join(mod_path, GameDataMovies), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovies, e.name())) + case _: + pass + treefixed = 1 + else: + entriesToMove.append(e) + if entriesToMove is not None: + for e in entriesToMove: + match e.suffix().casefold(): + case "pak" | "utoc" | "ucas": + filetree.move(e, GameDataPakMods, mobase.IFileTree.MERGE) + case "dll": + filetree.move(e, os.path.dirname(GameDataUE4SSMods) + "/", mobase.IFileTree.MERGE) + case "bk2": + filetree.move(e, GameDataMovies, mobase.IFileTree.MERGE) + case _: + pass + treefixed = 1 + if treefixed == 0: + return None + return filetree + + +class SilentHill2Game(BasicGame): + Name = "Silent Hill 2 Support Plugin" + Author = "modworkshop" + Version = "1" + GameName = "Silent Hill 2 Remake" + GameLauncher = "SHProto.exe" + GameShortName = "silenthill-2" + GameSteamId = 2124490 + GameBinary = "SHProto/Binaries/Win64/SHProto-Win64-Shipping.exe" + GameDataPath = "SHProto" + GameDataUE4SSMods = "Binaries/Win64/Mods" + GameDataPakMods = "Content/Paks/~Mods" + GameDataMovieMods = "Content/Movies" + GameDocumentsDirectory = "%LOCALAPPDATA%/SilentHill2/Saved/Config/Windows" + GameSaveExtension = "sav" + _main_window: QMainWindow + _ue4ss_tab: UE4SSTabWidget + _paks_tab: PaksTabWidget + def init(self, organizer: mobase.IOrganizer) -> bool: super().init(organizer) - self._register_feature(SilentHill2RemakeModDataChecker()) + self.dataChecker = SilentHill2ModDataChecker(organizer) + self._register_feature(self.dataChecker) + self._register_feature(SilentHill2ModDataContent()) + organizer.onUserInterfaceInitialized(self.init_tab) return True - Name = "Silent Hill 2 Remake Support Plugin" - Author = "HomerSimpleton Returns" - Version = "1.0" + def init_tab(self, main_window: QMainWindow): + if self._organizer.managedGame() != self: + return + self._main_window = main_window + tab_widget: QTabWidget = main_window.findChild(QTabWidget, "tabWidget") + if not tab_widget or not tab_widget.findChild(QWidget, "espTab"): + return + self._ue4ss_tab = UE4SSTabWidget(main_window, self._organizer) + plugin_tab = tab_widget.findChild(QWidget, "espTab") + tab_index = tab_widget.indexOf(plugin_tab) + 1 + if not tab_widget.isTabVisible(tab_widget.indexOf(plugin_tab)): + tab_index += 1 + tab_widget.insertTab(tab_index, self._ue4ss_tab, "UE4SS") + self._paks_tab = PaksTabWidget(main_window, self._organizer) + tab_index += 1 + tab_widget.insertTab(tab_index, self._paks_tab, "Paks") - GameName = "Silent Hill 2 Remake" - GameShortName = "silenthill2" - GameNexusName = "silenthill2" + def executables(self): + return [ + mobase.ExecutableInfo( + "Silent Hill 2", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ) + ] - GameBinary = "SHProto/Binaries/Win64/SHProto-Win64-Shipping.exe" - GameLauncher = "SHProto.exe" - GameDataPath = "%GAME_PATH%" - GameSupportURL = "https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/Game:-Silent-Hill-2-Remake" + @cached_property + def _base_dlls(self) -> set[str]: + base_dir = Path(self.gameDirectory().absolutePath()) + return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + try: + efls = super().executableForcedLoads() + except AttributeError: + efls = [] + libs: set[str] = set() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + if type(tree) is not mobase.IFileTree: + return efls + for e in tree: + relpath = e.pathFrom(tree) + if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + libs.add(relpath) + exes = self.executables() + efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + return efls + + def paksDirectory(self) -> QDir: + return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataPakMods) + + def ue4ssDirectory(self) -> QDir: + return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataUE4SSMods) + + def movieDirectory(self) -> QDir: + return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataMovieMods) + + def write_default_mods(self, profile: QDir): + ue4ss_mods_txt = QFileInfo(profile.absoluteFilePath("mods.txt")) + ue4ss_mods_json = QFileInfo(profile.absoluteFilePath("mods.json")) + if not ue4ss_mods_txt.exists(): + with open(ue4ss_mods_txt.absoluteFilePath(), "w") as mods_txt: + for mod in DEFAULT_UE4SS_MODS: + mods_txt.write(f"{mod['mod_name']} : 1\n") + if not ue4ss_mods_json.exists(): + mods_data: list[UE4SSModInfo] = [] + for mod in DEFAULT_UE4SS_MODS: + mods_data.append({"mod_name": mod["mod_name"], "mod_enabled": True}) + with open(ue4ss_mods_json.absoluteFilePath(), "w") as mods_json: + mods_json.write(json.dumps(mods_data, indent=4)) + + def iniFiles(self): + return ["GameUserSettings.ini", "Input.ini"] - GameGogId = [1225972913, 2051029707] + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + self.write_default_mods(directory) + if not self.paksDirectory().exists(): + os.makedirs(self.paksDirectory().absolutePath()) + if not self.ue4ssDirectory().exists(): + os.makedirs(self.ue4ssDirectory().absolutePath()) + if not self.movieDirectory().exists(): + os.makedirs(self.movieDirectory().absolutePath()) + super().initializeProfile(directory, settings) \ No newline at end of file diff --git a/games/game_titanfall2.py b/games/game_titanfall2.py new file mode 100644 index 00000000..a5708d24 --- /dev/null +++ b/games/game_titanfall2.py @@ -0,0 +1,289 @@ +import os +import shutil +import json +import mobase + +from json import JSONDecodeError +from enum import IntEnum, auto +from pathlib import Path +from typing import Any, List, Set, cast +from functools import cached_property + +from ..basic_game import BasicGame + +try: + from PyQt6.QtCore import QDir, QFileInfo +except: + from PyQt5.QtCore import QDir, QFileInfo + + +class Content(IntEnum): + MATERIAL = auto() + TEXTURE = auto() + MODELS = auto() + SCRIPT = auto() + CONFIG = auto() + VIDEO = auto() + AUDIO = auto() + STARPAK = auto() + + +class Titanfall2ModDataContent(mobase.ModDataContent): + GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ + (Content.MATERIAL, "Materials", ":/MO/gui/content/interface"), + (Content.TEXTURE, "Textures", ":/MO/gui/content/texture"), + (Content.MODELS, "Models", ":/MO/gui/content/mesh"), + (Content.SCRIPT, "Scripts", ":/MO/gui/content/script"), + (Content.CONFIG, "Configs", ":/MO/gui/content/inifile"), + (Content.VIDEO, "Video", ":/MO/gui/content/modgroup"), + (Content.AUDIO, "Audio", ":/MO/gui/content/sound"), + (Content.STARPAK, "Starpak", ":/MO/gui/content/bsa"), + ] + + def getAllContents(self) -> list[mobase.ModDataContent.Content]: + return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + + contents = set() + + def walkContent(self, path: str, entry: mobase.FileTreeEntry): + if entry.isFile(): + match entry.suffix().casefold(): + case "vmt": + self.contents.add(Content.MATERIAL) + case "vtf": + self.contents.add(Content.TEXTURE) + case "mdl": + self.contents.add(Content.MODELS) + case "nut": + self.contents.add(Content.SCRIPT) + case "txt": + self.contents.add(Content.CONFIG) + case "bik": + self.contents.add(Content.VIDEO) + case "wav": + self.contents.add(Content.AUDIO) + case "rpak" | "starmap" | "starpak": + self.contents.add(Content.STARPAK) + case _: + pass + return mobase.IFileTree.WalkReturn.CONTINUE + + def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: + filetree.walk(self.walkContent, "/") + return list(self.contents) + + +class Titanfall2ModDataChecker(mobase.ModDataChecker): + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self.organizer: mobase.IOrganizer = organizer + self.organizer.modList().onModInstalled(self._Fix_Installed_Mod) + self.needsNameFix = False + + def move_overwrite_merge(self, source, destination): + if not os.path.exists(destination): + shutil.move(source, destination) + return + if os.path.isfile(source): + os.replace(source, destination) + return + for item in os.listdir(source): + s_item = os.path.join(source, item) + d_item = os.path.join(destination, item) + self.move_overwrite_merge(s_item, d_item) + os.rmdir(source) + + def _Fix_Installed_Mod(self, mod: mobase.IModInterface): + if not self.needsNameFix: + return + northstarModPath = self.organizer.managedGame().GameNorthstarPath + "/" + filetree: mobase.IFileTree = mod.fileTree() + fixed = False + modname = mod.name() + if filetree is not None and filetree.exists(northstarModPath + "FOLDERNAME", mobase.IFileTree.DIRECTORY): + path = mod.absolutePath() + json_path = os.path.join(path, northstarModPath + "FOLDERNAME/mod.json") + mod_data = json.load(open(json_path, encoding="utf-8")) + modname = mod_data["name"] + old_path = os.path.join(path, northstarModPath + "FOLDERNAME") + new_path = os.path.join(path, northstarModPath + f"{modname}") + self.move_overwrite_merge(old_path, new_path) + fixed = True + elif filetree is not None and filetree.exists(northstarModPath + "FOLDERNAME_NAME", mobase.IFileTree.DIRECTORY): + path = mod.absolutePath() + old_path = os.path.join(path, northstarModPath + "FOLDERNAME_NAME") + new_path = os.path.join(path, northstarModPath + f"{modname}") + self.move_overwrite_merge(old_path, new_path) + fixed = True + if not fixed: + return + self.needsNameFix = False + + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + if filetree.exists("R2Northstar", mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + return mobase.ModDataChecker.FIXABLE + + def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): + for branch in filetree: + if branch is not None and branch.isDir(): + for e in branch: + if e is not None and e.name() == name: + return True + return False + + def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): + entriesToMove: list[mobase.FileTreeEntry] = [] + retVal = 0 + for e in filetree: + if e is not None: + entriesToMove.append(e) + for e in entriesToMove: + filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) + retVal = 1 + return retVal + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + northstarModPath = self.organizer.managedGame().GameNorthstarPath + "/" + treefixed = 0 + if filetree.exists("mod.json", mobase.IFileTree.FILE): + treefixed = self.allMoveTo(filetree, northstarModPath + "FOLDERNAME/") + if treefixed == 1: + self.needsNameFix = True + elif self.fileExistsInNextSubDir(filetree, "mod.json"): + filetree.move(filetree[0], northstarModPath, mobase.IFileTree.MERGE) + treefixed = 1 + else: + try: + if filetree[0][0].exists("mod.json", mobase.IFileTree.FILE): + filetree.move(filetree[0][0], filetree[0].path("/"), mobase.IFileTree.REPLACE) + filetree.move(filetree[0], northstarModPath, mobase.IFileTree.MERGE) + treefixed = 1 + except TypeError: + pass + if treefixed == 0: + if len(filetree) == 1 and filetree[0].isDir: + filetree.move(filetree[0], northstarModPath, mobase.IFileTree.MERGE) + treefixed = 1 + else: + for e in filetree: + if e is not None and e.path("/").count("/") == 0: + filetree.move(e, northstarModPath + "FOLDERNAME_NAME/", mobase.IFileTree.MERGE) + treefixed = 1 + self.needsNameFix = True + if treefixed == 0: + return None + return filetree + + +class Titanfall2Game(BasicGame): + Name = "Titanfall 2 Support Plugin" + Author = "modworkshop" + Version = "1" + GameName = "Titanfall 2" + GameShortName = "titanfall-2" + GameSteamId = 1237970 + GameBinary = "Titanfall2.exe" + GameDataPath = "%GAME_PATH%" + GameNorthstarPath = "R2Northstar/mods" + NorthstarModJson = "enabledmods.json" + GameDocumentsDirectory = "%USERPROFILE%/Documents/Respawn/Titanfall2/profile" + GameSavesDirectory = "%USERPROFILE%/Documents/Respawn/Titanfall2/profile/savegames/" + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self.dataChecker = Titanfall2ModDataChecker(organizer) + self._register_feature(self.dataChecker) + self._register_feature(Titanfall2ModDataContent()) + organizer.modList().onModStateChanged(self.update_enable_mods_json) + return True + + def update_enable_mods_json(self, mods: dict[str, mobase.ModState]): + Northstar_Config_Json = self._organizer.profilePath() + "/" + self.NorthstarModJson + with open(Northstar_Config_Json, "r", encoding="utf-8") as f: + Northstar = json.load(f) + for key, value in mods.items(): + key = self._organizer.modList().getMod(key) + tree = key.fileTree() + subtree = tree.find("R2Northstar/mods", mobase.IFileTree.DIRECTORY) + if subtree is not None and subtree.isDir(): + for e in subtree: + if e is not None and e.isDir(): + if e.exists("mod.json", mobase.IFileTree.FILE): + json_path = key.absolutePath() + "/" + e.path() + "/mod.json" + with open(json_path, "r", encoding="utf-8") as f: + mod_data = json.load(f) + modname = mod_data["Name"] + if "Version" not in mod_data: + modversion = "0.0.0" + else: + modversion = mod_data["Version"] + if value == 35 and modname not in Northstar: + Northstar[modname] = {modversion: True} + if value == 33 and modname in Northstar: + removed_value = Northstar.pop(modname) + with open(Northstar_Config_Json, "w", encoding="utf-8") as f: + json.dump(Northstar, f, ensure_ascii=False, indent=4) + + def executables(self): + return [ + mobase.ExecutableInfo( + "Titanfall 2", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ), + mobase.ExecutableInfo("Northstar", QFileInfo(self.gameDirectory(), "NorthstarLauncher.exe")), + ] + + @cached_property + def _base_dlls(self) -> set[str]: + base_dir = Path(self.gameDirectory().absolutePath()) + return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + try: + efls = super().executableForcedLoads() + except AttributeError: + efls = [] + libs: set[str] = set() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + if type(tree) is not mobase.IFileTree: + return efls + for e in tree: + relpath = e.pathFrom(tree) + if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + libs.add(relpath) + exes = self.executables() + efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + return efls + + def northstarDirectory(self) -> QDir: + return QDir(self.gameDirectory().absolutePath() + self.GameNorthstarPath) + + def iniFiles(self): + return ["profile.cfg"] + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + northstar_json_path = directory.absolutePath() + "/" + self.NorthstarModJson + northstar_json_game_path = self.gameDirectory().absolutePath() + "/R2Northstar/" + self.NorthstarModJson + blank_mod_json = '{"Version": 1,"Northstar.Client": {"1.31.6": true},"Northstar.CustomServers": {"1.31.6": true},"Northstar.Custom": {"1.31.6": true}}' + if not os.path.exists(northstar_json_path) or os.path.getsize(northstar_json_path) == 0: + if os.path.exists(northstar_json_game_path): + with open(northstar_json_game_path, "r") as game_json: + game_json_content = game_json.read() + game_json.close() + with open(northstar_json_path, "w") as northstar_json: + northstar_json.write(game_json_content) + northstar_json.close() + else: + with open(northstar_json_path, "w") as northstar_json: + northstar_json.write(blank_mod_json) + northstar_json.close() + modsPath = os.path.join(self.dataDirectory().absolutePath(), self.GameNorthstarPath) + if not os.path.exists(modsPath): + os.mkdir(modsPath) + super().initializeProfile(directory, settings) + + def mappings(self) -> list[mobase.Mapping]: + return [ + mobase.Mapping(self._organizer.profilePath() + "/" + self.NorthstarModJson, self.gameDirectory().absolutePath() + "/R2Northstar/" + self.NorthstarModJson, False, False), + ] \ No newline at end of file diff --git a/games/game_zuma_deluxe.py b/games/game_zuma_deluxe.py new file mode 100644 index 00000000..6511fc4d --- /dev/null +++ b/games/game_zuma_deluxe.py @@ -0,0 +1,329 @@ +import os +import shutil +import re +import mobase + +from enum import IntEnum, auto +from pathlib import Path +from typing import Any, List, Set, cast +from functools import cached_property + +from ..basic_game import BasicGame +from ..basic_features import BasicGameSaveGameInfo + +try: + from PyQt6.QtCore import QDir, QFileInfo +except: + from PyQt5.QtCore import QDir, QFileInfo + + +class Content(IntEnum): + TEXTURE = auto() + MESH = auto() + SCRIPT = auto() + SOUND = auto() + STRING = auto() + CONFIG = auto() + + +class ZumaModDataContent(mobase.ModDataContent): + GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ + (Content.TEXTURE, "Textures", ":/MO/gui/content/texture"), + (Content.MESH, "Meshes", ":/MO/gui/content/mesh"), + (Content.SCRIPT, "Scripts", ":/MO/gui/content/script"), + (Content.SOUND, "Sounds", ":/MO/gui/content/sound"), + (Content.STRING, "Strings", ":/MO/gui/content/string"), + (Content.CONFIG, "Configs", ":/MO/gui/content/inifile"), + ] + + def getAllContents(self) -> list[mobase.ModDataContent.Content]: + return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + + contents = set() + + def walkContent(self, path: str, entry: mobase.FileTreeEntry): + if entry.isFile(): + match entry.suffix().lower(): + case "texture": + self.contents.add(Content.TEXTURE) + case "model": + self.contents.add(Content.MESH) + case "lua": + self.contents.add(Content.SCRIPT) + case "stream": + self.contents.add(Content.SOUND) + case "txt": + self.contents.add(Content.STRING) + case "json": + self.contents.add(Content.CONFIG) + case _: + pass + return mobase.IFileTree.WalkReturn.CONTINUE + + def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: + filetree.walk(self.walkContent, "/") + return list(self.contents) + + +class ZumaModDataChecker(mobase.ModDataChecker): + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self.organizer: mobase.IOrganizer = organizer + self.organizer.modList().onModInstalled(self._Fix_Installed_Mod) + self.needsNameFix = False + + def move_overwrite_merge(self, source, destination): + if not os.path.exists(destination): + shutil.move(source, destination) + return + if os.path.isfile(source): + os.replace(source, destination) + return + for item in os.listdir(source): + s_item = os.path.join(source, item) + d_item = os.path.join(destination, item) + self.move_overwrite_merge(s_item, d_item) + os.rmdir(source) + + def _Fix_Installed_Mod(self, mod: mobase.IModInterface): + if not self.needsNameFix: + return + filetree: mobase.IFileTree = mod.fileTree() + fixed = False + modname = mod.name() + if filetree is not None and filetree.exists("mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): + path = mod.absolutePath() + old_path = os.path.join(path, "mods/FOLDERNAME") + new_path = os.path.join(path, f"mods/{modname}") + self.move_overwrite_merge(old_path, new_path) + fixed = True + if not fixed: + return + self.needsNameFix = False + + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + validFolders = ["images", "levels", "music", "sounds", "fonts", "properties", "userdata"] + validFiles = ["exe"] + for e in filetree: + if e.isDir(): + for folder in validFolders: + if filetree.exists(folder, mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + elif e.isFile(): + for ext in validFiles: + if e.suffix().lower() == ext: + return mobase.ModDataChecker.VALID + else: + pass + return mobase.ModDataChecker.FIXABLE + + def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): + for branch in filetree: + if branch is not None and branch.isDir(): + for e in branch: + if e is not None and e.name() == name: + return True + return False + + def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): + entriesToMove: list[mobase.FileTreeEntry] = [] + retVal = 0 + for e in filetree: + if e is not None: + entriesToMove.append(e) + for e in entriesToMove: + filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) + retVal = 1 + return retVal + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + GameLevelsPath = self.organizer.managedGame().GameLevelsPath + validFolders = ["images", "levels", "music", "sounds", "fonts", "properties", "userdata"] + entriesToMove: list[mobase.FileTreeEntry] = [] + treefixed = 0 + if filetree.exists("map.txt", mobase.IFileTree.FILE): + treefixed = self.allMoveTo(filetree, GameLevelsPath + "/FOLDERNAME/") + if treefixed == 1: + self.needsNameFix = True + elif self.fileExistsInNextSubDir(filetree, "map.txt"): + filetree.move(filetree[0], GameLevelsPath, mobase.IFileTree.MERGE) + treefixed = 1 + else: + moveonce = 0 + for branch in filetree: + if branch is not None and branch.isDir(): + for entry in branch: + for folder in validFolders: + if entry is not None and entry.name() == folder: + moveonce = 1 + if moveonce == 1: + for branch in filetree: + if branch is not None and branch.isDir(): + for entry in branch: + entriesToMove.append(entry) + if entriesToMove is not None: + for e in entriesToMove: + filetree.move(e, "", mobase.IFileTree.MERGE) + treefixed = 1 + for branch in filetree: + if branch is not None and branch.isDir(): + if len(branch) == 0: + filetree.remove(branch) + if treefixed == 0: + return None + return filetree + + +PROGRAM_DATA = str(os.getenv("ProgramData")) + + +class ZumaGame(BasicGame, mobase.IPluginFileMapper): + Name = "Zuma Deluxe Support Plugin" + Author = "modworkshop" + Version = "1" + GameName = "Zuma Deluxe" + GameShortName = "zuma" + GameSteamId = 3330 + GameBinary = "Zuma.exe" + GameDataPath = "%GAME_PATH%" + GameLevelsPath = "levels" + GameLevelsXml = "levels/levels.xml" + ProfileLevelsXml = "levels.xml" + GameDocumentsDirectory = PROGRAM_DATA + "/Steam/Zuma/userdata" + GameSaveExtension = "sav" + + def __init__(self): + BasicGame.__init__(self) + mobase.IPluginFileMapper.__init__(self) + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self.dataChecker = ZumaModDataChecker(organizer) + self._register_feature(self.dataChecker) + self._register_feature(ZumaModDataContent()) + self._register_feature(BasicGameSaveGameInfo()) + organizer.modList().onModStateChanged(self.update_levels) + return True + + def update_levels(self, mods: dict[str, mobase.ModState]): + profile_levels_path = self._organizer.profilePath() + "/" + self.ProfileLevelsXml + game_levels_path = os.path.join(self.dataDirectory().absolutePath(), self.GameLevelsXml) + for key, value in mods.items(): + key = self._organizer.modList().getMod(key) + tree = key.fileTree() + if tree.exists("levels/levels.xml", mobase.IFileTree.FILE): + levels_txt_path = os.path.join(key.absolutePath(), "levels/levels.xml") + profile_levels_path = self._organizer.profilePath() + "/" + self.ProfileLevelsXml + if value == 35: + with open(levels_txt_path, "r") as levels_txt: + levels_txt_content = levels_txt.read() + levels_txt.close() + with open(profile_levels_path, "w") as profile_levels: + profile_levels.write(levels_txt_content) + profile_levels.close() + if value == 33: + with open(game_levels_path, "r") as game_levels: + game_levels_content = game_levels.read() + game_levels.close() + with open(profile_levels_path, "w") as profile_levels: + profile_levels.write(game_levels_content) + profile_levels.close() + for key, value in mods.items(): + key = self._organizer.modList().getMod(key) + map_txt_path = os.path.join(key.absolutePath(), "levels/map.txt") + tree = key.fileTree() + if tree.exists("levels/map.txt", mobase.IFileTree.FILE): + with open(map_txt_path, "r") as map_txt: + map_txt_content = map_txt.read() + map_txt.close() + graphics_pattern = r"(?=)" + levels_pattern = r"(?=)" + id_pattern = r'(?<=id=")(.*?)(?=")' + graphics_tag = re.findall(graphics_pattern, map_txt_content, re.DOTALL) + levels_tag = re.findall(levels_pattern, map_txt_content, re.DOTALL) + id_name = re.findall(id_pattern, map_txt_content, re.DOTALL) + with open(profile_levels_path, "r+") as profile_levels: + profile_levels_content = profile_levels.read() + profile_levels.seek(0) + if value == 35: + insert_graphics_string = "" + for graphic in graphics_tag: + insert_graphics_string += "\n\n" + graphic + insert_graphics_string += "\n\n set[str]: + base_dir = Path(self.gameDirectory().absolutePath()) + return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + try: + efls = super().executableForcedLoads() + except AttributeError: + efls = [] + libs: set[str] = set() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + if type(tree) is not mobase.IFileTree: + return efls + for e in tree: + relpath = e.pathFrom(tree) + if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + libs.add(relpath) + exes = self.executables() + efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + return efls + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + modsPath = self.dataDirectory().absolutePath() + profile_levels_path = directory.absolutePath() + "/" + self.ProfileLevelsXml + game_levels_path = os.path.join(self.dataDirectory().absolutePath(), self.GameLevelsXml) + if not os.path.exists(profile_levels_path) or os.path.getsize(profile_levels_path) == 0: + with open(game_levels_path, "r") as game_levels: + profile_levels_content = game_levels.read() + game_levels.close() + with open(profile_levels_path, "w") as profile_levels: + profile_levels.write(profile_levels_content) + profile_levels.close() + if not os.path.exists(modsPath): + os.mkdir(modsPath) + super().initializeProfile(directory, settings) + + def mappings(self) -> list[mobase.Mapping]: + return [ + mobase.Mapping(self._organizer.profilePath() + "/" + self.ProfileLevelsXml, self.gameDirectory().absolutePath() + "/" + self.GameLevelsXml, False, False), + ] \ No newline at end of file diff --git a/games/unreal_tabs/__init__.py b/games/unreal_tabs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/games/unreal_tabs/constants.py b/games/unreal_tabs/constants.py new file mode 100644 index 00000000..0b10c821 --- /dev/null +++ b/games/unreal_tabs/constants.py @@ -0,0 +1,11 @@ +from typing import TypedDict + + +class UE4SSModInfo(TypedDict): + mod_name: str + mod_enabled: bool + +DEFAULT_UE4SS_MODS: list[UE4SSModInfo] = [ + {"mod_name": "BPML_GenericFunctions", "mod_enabled": True}, + {"mod_name": "BPModLoaderMod", "mod_enabled": True}, +] diff --git a/games/unreal_tabs/manage_paks/__init__.py b/games/unreal_tabs/manage_paks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/games/unreal_tabs/manage_paks/model.py b/games/unreal_tabs/manage_paks/model.py new file mode 100644 index 00000000..9e7216f7 --- /dev/null +++ b/games/unreal_tabs/manage_paks/model.py @@ -0,0 +1,245 @@ +import itertools +import typing +from enum import IntEnum, auto +from typing import Any, TypeAlias, overload + +import mobase + +try: + from PyQt6.QtCore import (QAbstractItemModel, QByteArray, QDataStream, QDir, QFileInfo, QMimeData, QModelIndex, QObject, Qt, QVariant) + from PyQt6.QtWidgets import QWidget +except: + from PyQt5.QtCore import (QAbstractItemModel, QByteArray, QDataStream, QDir, QFileInfo, QMimeData, QModelIndex, QObject, Qt, QVariant) + from PyQt5.QtWidgets import QWidget + +_PakInfo: TypeAlias = tuple[str, str, str, str] + +class PaksColumns(IntEnum): + PRIORITY = auto() + PAK_NAME = auto() + SOURCE = auto() + + +class PaksModel(QAbstractItemModel): + def __init__(self, parent: QWidget | None, organizer: mobase.IOrganizer): + super().__init__(parent) + self.paks: dict[int, _PakInfo] = {} + self._organizer = organizer + self._init_mod_states() + + def _init_mod_states(self): + profile = QDir(self._organizer.profilePath()) + paks_txt = QFileInfo(profile.absoluteFilePath("paks.txt")) + if paks_txt.exists(): + with open(paks_txt.absoluteFilePath(), "r") as paks_file: + index = 0 + for line in paks_file: + self.paks[index] = (line, "", "", "") + index += 1 + + def set_paks(self, paks: dict[int, _PakInfo]): + self.layoutAboutToBeChanged.emit() + self.paks = paks + self.layoutChanged.emit() + self.dataChanged.emit( + self.index(0, 0), + self.index(self.rowCount(), self.columnCount()), + [Qt.ItemDataRole.DisplayRole], + ) + + def flags(self, index: QModelIndex) -> Qt.ItemFlag: + if not index.isValid(): + return ( + Qt.ItemFlag.ItemIsSelectable + | Qt.ItemFlag.ItemIsDragEnabled + | Qt.ItemFlag.ItemIsDropEnabled + | Qt.ItemFlag.ItemIsEnabled + ) + return ( + super().flags(index) + | Qt.ItemFlag.ItemIsDragEnabled + | Qt.ItemFlag.ItemIsDropEnabled & Qt.ItemFlag.ItemIsEditable + ) + + def columnCount(self, parent: QModelIndex = QModelIndex()) -> int: + return len(PaksColumns) + + def index( + self, row: int, column: int, parent: QModelIndex = QModelIndex() + ) -> QModelIndex: + if ( + row < 0 + or row >= self.rowCount() + or column < 0 + or column >= self.columnCount() + ): + return QModelIndex() + return self.createIndex(row, column, row) + + @overload + def parent(self, child: QModelIndex) -> QModelIndex: ... + @overload + def parent(self) -> QObject | None: ... + + def parent(self, child: QModelIndex | None = None) -> QModelIndex | QObject | None: + if child is None: + return super().parent() + return QModelIndex() + + def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: + return len(self.paks) + + def setData( + self, index: QModelIndex, value: Any, role: int = Qt.ItemDataRole.EditRole + ) -> bool: + return False + + def headerData( + self, + section: int, + orientation: Qt.Orientation, + role: int = Qt.ItemDataRole.DisplayRole, + ) -> typing.Any: + if ( + orientation != Qt.Orientation.Horizontal + or role != Qt.ItemDataRole.DisplayRole + ): + return QVariant() + + column = PaksColumns(section + 1) + match column: + case PaksColumns.PAK_NAME: + return "Pak Group" + case PaksColumns.PRIORITY: + return "Priority" + case PaksColumns.SOURCE: + return "Source" + + return QVariant() + + def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any: + if not index.isValid(): + return None + if index.column() + 1 == PaksColumns.PAK_NAME: + if role == Qt.ItemDataRole.DisplayRole: + return self.paks[index.row()][0] + elif index.column() + 1 == PaksColumns.PRIORITY: + if role == Qt.ItemDataRole.DisplayRole: + return index.row() + elif index.column() + 1 == PaksColumns.SOURCE: + if role == Qt.ItemDataRole.DisplayRole: + return self.paks[index.row()][1] + return QVariant() + + def canDropMimeData( + self, + data: QMimeData | None, + action: Qt.DropAction, + row: int, + column: int, + parent: QModelIndex, + ) -> bool: + if action == Qt.DropAction.MoveAction and (row != -1 or column != -1): + return True + return False + + def supportedDropActions(self) -> Qt.DropAction: + return Qt.DropAction.MoveAction + + def dropMimeData( + self, + data: QMimeData | None, + action: Qt.DropAction, + row: int, + column: int, + parent: QModelIndex, + ) -> bool: + if action == Qt.DropAction.IgnoreAction: + return True + + if data is None: + return False + + encoded: QByteArray = data.data("application/x-qabstractitemmodeldatalist") + stream: QDataStream = QDataStream(encoded, QDataStream.OpenModeFlag.ReadOnly) + source_rows: list[int] = [] + + while not stream.atEnd(): + source_row = stream.readInt() + col = stream.readInt() + size = stream.readInt() + item_data = {} + for _ in range(size): + role = stream.readInt() + value = stream.readQVariant() + item_data[role] = value + if col == 0: + source_rows.append(source_row) + + if row == -1: + row = parent.row() + + if row < 0 or row >= len(self.paks): + new_priority = len(self.paks) + else: + new_priority = row + + before_paks: list[_PakInfo] = [] + moved_paks: list[_PakInfo] = [] + after_paks: list[_PakInfo] = [] + before_paks_p: list[_PakInfo] = [] + moved_paks_p: list[_PakInfo] = [] + after_paks_p: list[_PakInfo] = [] + for row, paks in sorted(self.paks.items()): + if row < new_priority: + if row in source_rows: + if paks[0].casefold()[-2:] == "_p": + moved_paks_p.append(paks) + else: + moved_paks.append(paks) + else: + if paks[0].casefold()[-2:] == "_p": + before_paks_p.append(paks) + else: + before_paks.append(paks) + if row >= new_priority: + if row in source_rows: + if paks[0].casefold()[-2:] == "_p": + moved_paks_p.append(paks) + else: + moved_paks.append(paks) + else: + if paks[0].casefold()[-2:] == "_p": + after_paks_p.append(paks) + else: + after_paks.append(paks) + + new_paks = dict( + enumerate( + itertools.chain( + before_paks, + moved_paks, + after_paks, + before_paks_p, + moved_paks_p, + after_paks_p, + ) + ) + ) + + index = 8999 + for row, pak in new_paks.items(): + current_dir = QDir(pak[2]) + parent_dir = QDir(pak[2]) + parent_dir.cdUp() + if current_dir.exists() and parent_dir.dirName().casefold() == "~mods": + new_paks[row] = ( + pak[0], + pak[1], + pak[2], + parent_dir.absoluteFilePath(str(index).zfill(4)), + ) + index -= 1 + + self.set_paks(new_paks) + return False diff --git a/games/unreal_tabs/manage_paks/view.py b/games/unreal_tabs/manage_paks/view.py new file mode 100644 index 00000000..0e5d6385 --- /dev/null +++ b/games/unreal_tabs/manage_paks/view.py @@ -0,0 +1,37 @@ +from typing import Iterable + +try: + from PyQt6.QtCore import QModelIndex, Qt, pyqtSignal + from PyQt6.QtGui import QDropEvent + from PyQt6.QtWidgets import QAbstractItemView, QTreeView, QWidget +except: + from PyQt5.QtCore import QModelIndex, Qt, pyqtSignal + from PyQt5.QtGui import QDropEvent + from PyQt5.QtWidgets import QAbstractItemView, QTreeView, QWidget + +class PaksView(QTreeView): + data_dropped = pyqtSignal() + + def __init__(self, parent: QWidget | None): + super().__init__(parent) + self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) + self.setDragEnabled(True) + self.setAcceptDrops(True) + self.setDropIndicatorShown(True) + self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) + self.setDefaultDropAction(Qt.DropAction.MoveAction) + if (viewport := self.viewport()) is not None: + viewport.setAcceptDrops(True) + self.setItemsExpandable(False) + self.setRootIsDecorated(False) + + def dropEvent(self, e: QDropEvent | None): + super().dropEvent(e) + self.clearSelection() + self.data_dropped.emit() + + def dataChanged( + self, topLeft: QModelIndex, bottomRight: QModelIndex, roles: Iterable[int] = () + ): + super().dataChanged(topLeft, bottomRight, roles) + self.repaint() diff --git a/games/unreal_tabs/manage_paks/widget.py b/games/unreal_tabs/manage_paks/widget.py new file mode 100644 index 00000000..6c7a5eb3 --- /dev/null +++ b/games/unreal_tabs/manage_paks/widget.py @@ -0,0 +1,207 @@ +from functools import cmp_to_key +from pathlib import Path +from typing import cast + +import mobase + +from ....basic_features.utils import is_directory +from .model import PaksModel +from .view import PaksView + +try: + from PyQt6.QtWidgets import QGridLayout, QWidget + from PyQt6.QtCore import QDir, QFileInfo, Qt +except: + from PyQt5.QtWidgets import QGridLayout, QWidget + from PyQt5.QtCore import QDir, QFileInfo, Qt + +def pak_sort(a: tuple[str, str], b: tuple[str, str]) -> int: + a_pak, a_str = a[0], a[1] or a[0] + b_pak, b_str = b[0], b[1] or b[0] + + a_pak_ends_p = a_pak.casefold().endswith("_p") + b_pak_ends_p = b_pak.casefold().endswith("_p") + + if a_pak_ends_p == b_pak_ends_p: + if a_str.casefold() <= b_str.casefold(): + return 1 + return -1 + elif a_pak_ends_p: + return 1 + elif b_pak_ends_p: + return -1 + return 0 + + +class PaksTabWidget(QWidget): + def __init__(self, parent: QWidget | None, organizer: mobase.IOrganizer): + super().__init__(parent) + self._organizer = organizer + self._view = PaksView(self) + self._layout = QGridLayout(self) + self._layout.addWidget(self._view) + self._model = PaksModel(self._view, organizer) + self._view.setModel(self._model) + self._model.dataChanged.connect(self.write_paks_list) # type: ignore + self._view.data_dropped.connect(self.write_paks_list) # type: ignore + organizer.onProfileChanged(lambda profile_a, profile_b: self._parse_pak_files()) + organizer.modList().onModInstalled(lambda mod: self._parse_pak_files()) + organizer.modList().onModRemoved(lambda mod: self._parse_pak_files()) + organizer.modList().onModStateChanged(lambda mods: self._parse_pak_files()) + self._parse_pak_files() + + def load_paks_list(self) -> list[str]: + profile = QDir(self._organizer.profilePath()) + paks_txt = QFileInfo(profile.absoluteFilePath("paks.txt")) + paks_list: list[str] = [] + if paks_txt.exists(): + with open(paks_txt.absoluteFilePath(), "r") as paks_file: + for line in paks_file: + paks_list.append(line.strip()) + return paks_list + + def write_paks_list(self): + profile = QDir(self._organizer.profilePath()) + paks_txt = QFileInfo(profile.absoluteFilePath("paks.txt")) + with open(paks_txt.absoluteFilePath(), "w") as paks_file: + for _, pak in sorted(self._model.paks.items()): + name, _, _, _ = pak + paks_file.write(f"{name}\n") + self.write_pak_files() + + def write_pak_files(self): + for index, pak in sorted(self._model.paks.items()): + _, _, current_path, target_path = pak + if current_path and current_path != target_path: + path_dir = Path(current_path) + target_dir = Path(target_path) + if not target_dir.exists(): + target_dir.mkdir(parents=True, exist_ok=True) + if path_dir.exists(): + for pak_file in path_dir.glob("*.pak"): + ucas_file = pak_file.with_suffix(".ucas") + utoc_file = pak_file.with_suffix(".utoc") + for file in (pak_file, ucas_file, utoc_file): + if not file.exists(): + continue + try: + file.rename(target_dir.joinpath(file.name)) + except FileExistsError: + pass + data = self._model.paks[index] + self._model.paks[index] = ( + data[0], + data[1], + data[3], + data[3], + ) + break + if not list(path_dir.iterdir()): + path_dir.rmdir() + + def _shake_paks(self, sorted_paks: dict[str, str]) -> list[str]: + shaken_paks: list[str] = [] + shaken_paks_p: list[str] = [] + paks_list = self.load_paks_list() + for pak in paks_list: + if pak in sorted_paks.keys(): + if pak.casefold().endswith("_p"): + shaken_paks_p.append(pak) + else: + shaken_paks.append(pak) + sorted_paks.pop(pak) + for pak in sorted_paks.keys(): + if pak.casefold().endswith("_p"): + shaken_paks_p.append(pak) + else: + shaken_paks.append(pak) + return shaken_paks + shaken_paks_p + + def _parse_pak_files(self): + game = self._organizer.managedGame() + mods = self._organizer.modList().allMods() + paks: dict[str, str] = {} + pak_paths: dict[str, tuple[str, str]] = {} + pak_source: dict[str, str] = {} + for mod in mods: + mod_item = self._organizer.modList().getMod(mod) + if not self._organizer.modList().state(mod) & mobase.ModState.ACTIVE: + continue + filetree = mod_item.fileTree() + pak_mods = filetree.find(game.GameDataPakMods) + if isinstance(pak_mods, mobase.IFileTree): + for entry in pak_mods: + if is_directory(entry): + for sub_entry in entry: + if ( + sub_entry.isFile() + and sub_entry.suffix().casefold() == "pak" + ): + pak_name = sub_entry.name()[ + : -1 - len(sub_entry.suffix()) + ] + paks[pak_name] = entry.name() + pak_paths[pak_name] = ( + mod_item.absolutePath() + + "/" + + cast(mobase.IFileTree, sub_entry.parent()).path( + "/" + ), + mod_item.absolutePath() + "/" + pak_mods.path("/"), + ) + pak_source[pak_name] = mod_item.name() + else: + if entry.suffix().casefold() == "pak": + pak_name = entry.name()[: -1 - len(entry.suffix())] + paks[pak_name] = "" + pak_paths[pak_name] = ( + mod_item.absolutePath() + + "/" + + cast(mobase.IFileTree, entry.parent()).path("/"), + mod_item.absolutePath() + "/" + pak_mods.path("/"), + ) + pak_source[pak_name] = mod_item.name() + pak_mods = QFileInfo(game.paksDirectory().absolutePath()) + if pak_mods.exists() and pak_mods.isDir(): + for entry in QDir(pak_mods.absoluteFilePath()).entryInfoList( + QDir.Filter.Dirs | QDir.Filter.Files | QDir.Filter.NoDotAndDotDot + ): + if entry.isDir(): + for sub_entry in QDir(entry.absoluteFilePath()).entryInfoList( + QDir.Filter.Files + ): + if ( + sub_entry.isFile() + and sub_entry.suffix().casefold() == "pak" + ): + pak_name = sub_entry.completeBaseName() + paks[pak_name] = entry.completeBaseName() + pak_paths[pak_name] = ( + sub_entry.absolutePath(), + pak_mods.absolutePath(), + ) + pak_source[pak_name] = "Game Directory" + else: + if entry.suffix().casefold() == "pak": + pak_name = entry.completeBaseName() + paks[pak_name] = "" + pak_paths[pak_name] = ( + entry.absolutePath(), + pak_mods.absolutePath(), + ) + pak_source[pak_name] = "Game Directory" + sorted_paks = dict(sorted(paks.items(), key=cmp_to_key(pak_sort))) + shaken_paks: list[str] = self._shake_paks(sorted_paks) + final_paks: dict[str, tuple[str, str, str]] = {} + pak_index = 8999 + for pak in shaken_paks: + target_dir = pak_paths[pak][1] + "/" + str(pak_index).zfill(4) + final_paks[pak] = (pak_source[pak], pak_paths[pak][0], target_dir) + pak_index -= 1 + new_data_paks: dict[int, tuple[str, str, str, str]] = {} + i = 0 + for pak, data in final_paks.items(): + source, current_path, target_path = data + new_data_paks[i] = (pak, source, current_path, target_path) + i += 1 + self._model.set_paks(new_data_paks) diff --git a/games/unreal_tabs/manage_ue4ss/__init__.py b/games/unreal_tabs/manage_ue4ss/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/games/unreal_tabs/manage_ue4ss/model.py b/games/unreal_tabs/manage_ue4ss/model.py new file mode 100644 index 00000000..8289b019 --- /dev/null +++ b/games/unreal_tabs/manage_ue4ss/model.py @@ -0,0 +1,121 @@ +import json +from json import JSONDecodeError +from typing import Any, Iterable + +try: + from PyQt6.QtCore import (QDir, QFileInfo, QMimeData, QModelIndex, QStringListModel, Qt) + from PyQt6.QtWidgets import QWidget +except: + from PyQt5.QtCore import (QDir, QFileInfo, QMimeData, QModelIndex, QStringListModel, Qt) + from PyQt5.QtWidgets import QWidget + +import mobase + +from ..constants import DEFAULT_UE4SS_MODS + +class UE4SSListModel(QStringListModel): + def __init__(self, parent: QWidget | None, organizer: mobase.IOrganizer): + super().__init__(parent) + self._checked_items: set[str] = set() + self._organizer = organizer + self._init_mod_states() + + def _init_mod_states(self): + profile = QDir(self._organizer.profilePath()) + mods_json = QFileInfo(profile.absoluteFilePath("mods.json")) + if mods_json.exists(): + with open(mods_json.absoluteFilePath(), "r") as json_file: + try: + mod_data = json.load(json_file) + except JSONDecodeError: + mod_data = DEFAULT_UE4SS_MODS + for mod in mod_data: + if mod["mod_enabled"]: + self._checked_items.add(mod["mod_name"]) + + def _set_mod_states(self): + profile = QDir(self._organizer.profilePath()) + mods_json = QFileInfo(profile.absoluteFilePath("mods.json")) + mod_list: dict[str, bool] = {} + if mods_json.exists(): + with open(mods_json.absoluteFilePath(), "r") as json_file: + try: + mod_data = json.load(json_file) + except JSONDecodeError: + mod_data = DEFAULT_UE4SS_MODS + for mod in mod_data: + mod_list[mod["mod_name"]] = mod["mod_enabled"] + for i in range(self.rowCount()): + item = self.index(i, 0) + name = self.data(item, Qt.ItemDataRole.DisplayRole) + if name in mod_list: + self.setData( + item, + True if mod_list[name] else False, + Qt.ItemDataRole.CheckStateRole, + ) + else: + self.setData(item, True, Qt.ItemDataRole.CheckStateRole) + + def flags(self, index: QModelIndex) -> Qt.ItemFlag: + flags = super().flags(index) + if not index.isValid(): + return ( + Qt.ItemFlag.ItemIsSelectable + | Qt.ItemFlag.ItemIsDragEnabled + | Qt.ItemFlag.ItemIsDropEnabled + | Qt.ItemFlag.ItemIsEnabled + ) + return ( + flags + | Qt.ItemFlag.ItemIsUserCheckable + | Qt.ItemFlag.ItemIsDragEnabled & Qt.ItemFlag.ItemIsEditable + ) + + def setData( + self, index: QModelIndex, value: Any, role: int = Qt.ItemDataRole.EditRole + ) -> bool: + if not index.isValid() or role != Qt.ItemDataRole.CheckStateRole: + return False + + if ( + bool(value) + and self.data(index, Qt.ItemDataRole.DisplayRole) not in self._checked_items + ): + self._checked_items.add(self.data(index, Qt.ItemDataRole.DisplayRole)) + elif ( + not bool(value) + and self.data(index, Qt.ItemDataRole.DisplayRole) in self._checked_items + ): + self._checked_items.remove(self.data(index, Qt.ItemDataRole.DisplayRole)) + self.dataChanged.emit(index, index, [role]) + return True + + def setStringList(self, strings: Iterable[str | None]): + super().setStringList(strings) + self._set_mod_states() + + def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any: + if not index.isValid(): + return None + + if role == Qt.ItemDataRole.CheckStateRole: + return ( + Qt.CheckState.Checked + if self.data(index, Qt.ItemDataRole.DisplayRole) in self._checked_items + else Qt.CheckState.Unchecked + ) + + return super().data(index, role) + + def canDropMimeData( + self, + data: QMimeData | None, + action: Qt.DropAction, + row: int, + column: int, + parent: QModelIndex, + ) -> bool: + if action == Qt.DropAction.MoveAction and (row != -1 or column != -1): + return True + return False diff --git a/games/unreal_tabs/manage_ue4ss/view.py b/games/unreal_tabs/manage_ue4ss/view.py new file mode 100644 index 00000000..218468b0 --- /dev/null +++ b/games/unreal_tabs/manage_ue4ss/view.py @@ -0,0 +1,36 @@ +from typing import Iterable + +try: + from PyQt6.QtCore import QModelIndex, Qt, pyqtSignal + from PyQt6.QtGui import QDropEvent + from PyQt6.QtWidgets import QAbstractItemView, QListView, QWidget +except: + from PyQt5.QtCore import QModelIndex, Qt, pyqtSignal + from PyQt5.QtGui import QDropEvent + from PyQt5.QtWidgets import QAbstractItemView, QListView, QWidget + + +class UE4SSView(QListView): + data_dropped = pyqtSignal() + + def __init__(self, parent: QWidget | None): + super().__init__(parent) + self.setAcceptDrops(True) + self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) + self.setDragEnabled(True) + self.setAcceptDrops(True) + self.setDropIndicatorShown(True) + self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) + self.setDefaultDropAction(Qt.DropAction.MoveAction) + if (viewport := self.viewport()) is not None: + viewport.setAcceptDrops(True) + + def dropEvent(self, e: QDropEvent | None): + super().dropEvent(e) + self.data_dropped.emit() + + def dataChanged( + self, topLeft: QModelIndex, bottomRight: QModelIndex, roles: Iterable[int] = () + ): + super().dataChanged(topLeft, bottomRight, roles) + self.repaint() diff --git a/games/unreal_tabs/manage_ue4ss/widget.py b/games/unreal_tabs/manage_ue4ss/widget.py new file mode 100644 index 00000000..6d20ead6 --- /dev/null +++ b/games/unreal_tabs/manage_ue4ss/widget.py @@ -0,0 +1,168 @@ +import json +from functools import cmp_to_key +from json import JSONDecodeError +from pathlib import Path + +import mobase + +from ..constants import DEFAULT_UE4SS_MODS, UE4SSModInfo +from .model import UE4SSListModel +from .view import UE4SSView + +try: + from PyQt6.QtWidgets import QGridLayout, QWidget + from PyQt6.QtCore import QDir, QFileInfo, Qt +except: + from PyQt5.QtWidgets import QGridLayout, QWidget + from PyQt5.QtCore import QDir, QFileInfo, Qt + +class UE4SSTabWidget(QWidget): + def __init__(self, parent: QWidget | None, organizer: mobase.IOrganizer): + super().__init__(parent) + self._organizer = organizer + self._view = UE4SSView(self) + self._layout = QGridLayout(self) + self._layout.addWidget(self._view) + self._model = UE4SSListModel(self._view, organizer) + self._view.setModel(self._model) + self._model.dataChanged.connect(self.write_mod_list) # type: ignore + self._view.data_dropped.connect(self.write_mod_list) # type: ignore + organizer.onProfileChanged(lambda profile_a, profile_b: self._parse_mod_files()) + organizer.modList().onModInstalled(self.update_mod_files) + organizer.modList().onModRemoved(lambda mod: self._parse_mod_files()) + organizer.modList().onModStateChanged(self.update_mod_files) + self._parse_mod_files() + + def get_mod_list(self) -> list[str]: + mod_list: list[str] = [] + for index in range(self._model.rowCount()): + mod_list.append( + self._model.data( + self._model.index(index, 0), Qt.ItemDataRole.DisplayRole + ) + ) + return mod_list + + def update_mod_files( + self, mods: dict[str, mobase.ModState] | mobase.IModInterface | str + ): + game = self._organizer.managedGame() + mod_list: list[mobase.IModInterface] = [] + if isinstance(mods, dict): + for mod in mods.keys(): + mod_list.append(self._organizer.modList().getMod(mod)) + elif isinstance(mods, mobase.IModInterface): + mod_list.append(mods) + else: + mod_list.append(self._organizer.modList().getMod(mods)) + + for mod in mod_list: + tree = mod.fileTree() + ue4ss_files = tree.find(game.GameDataUE4SSMods) + if isinstance(ue4ss_files, mobase.IFileTree): + for entry in ue4ss_files: + if isinstance(entry, mobase.IFileTree): + if enabled_txt := entry.find("enabled.txt"): + try: + Path(mod.absolutePath(), enabled_txt.path("/")).unlink() + self._organizer.modDataChanged(mod) + except FileNotFoundError: + pass + + self._parse_mod_files() + + def _parse_mod_files(self): + game = self._organizer.managedGame() + mod_list: set[str] = set() + for mod in self._organizer.modList().allMods(): + if ( + mobase.ModState(self._organizer.modList().state(mod)) + & mobase.ModState.ACTIVE + ): + tree = self._organizer.modList().getMod(mod).fileTree() + ue4ss_files = tree.find(game.GameDataUE4SSMods) + if isinstance(ue4ss_files, mobase.IFileTree): + for entry in ue4ss_files: + if isinstance(entry, mobase.IFileTree): + if entry.find("scripts/main.lua") or entry.find("dlls/main.dll"): + mod_list.add(entry.name()) + if enabled_txt := entry.find("enabled.txt"): + try: + Path( + self._organizer.modList() + .getMod(mod) + .absolutePath(), + enabled_txt.path("/"), + ).unlink() + self._organizer.modDataChanged( + self._organizer.modList().getMod(mod) + ) + except FileNotFoundError: + pass + + if game.ue4ssDirectory().exists(): + for dir_info in game.ue4ssDirectory().entryInfoList( + QDir.Filter.Dirs | QDir.Filter.NoDotAndDotDot + ): + if QFileInfo( + QDir(dir_info.absoluteFilePath()).absoluteFilePath( + "scripts/main.lua" + ) + ).exists(): + mod_list.add(dir_info.fileName()) + if QFileInfo( + QDir(dir_info.absoluteFilePath()).absoluteFilePath( + "enabled.txt" + ) + ).exists(): + Path(dir_info.absoluteFilePath(), "enabled.txt").unlink() + + final_list = sorted(mod_list, key=cmp_to_key(self.sort_mods)) + self._model.setStringList(final_list) + + def write_mod_list(self): + mod_list: list[UE4SSModInfo] = [] + profile = QDir(self._organizer.profilePath()) + mods_txt = QFileInfo(profile.absoluteFilePath("mods.txt")) + with open(mods_txt.absoluteFilePath(), "w") as txt_file: + for i in range(self._model.rowCount()): + item = self._model.index(i, 0) + name = self._model.data(item, Qt.ItemDataRole.DisplayRole) + active = ( + self._model.data(item, Qt.ItemDataRole.CheckStateRole) + == Qt.CheckState.Checked + ) + mod_list.append({"mod_name": name, "mod_enabled": active}) + txt_file.write(f"{name} : {1 if active else 0}\n") + mods_json = QFileInfo(profile.absoluteFilePath("mods.json")) + with open(mods_json.absoluteFilePath(), "w") as json_file: + json_file.write(json.dumps(mod_list, indent=4)) + + def sort_mods(self, mod_a: str, mod_b: str) -> int: + profile = QDir(self._organizer.profilePath()) + mods_json = QFileInfo(profile.absoluteFilePath("mods.json")) + mods_list: list[str] = [] + if mods_json.exists() and mods_json.isFile(): + with open(mods_json.absoluteFilePath(), "r") as json_file: + try: + mods = json.load(json_file) + except JSONDecodeError: + mods = DEFAULT_UE4SS_MODS + for mod in mods: + if mod["mod_enabled"]: + mods_list.append(mod["mod_name"]) + index_a = -1 + if mod_a in mods_list: + index_a = mods_list.index(mod_a) + index_b = -1 + if mod_b in mods_list: + index_b = mods_list.index(mod_b) + if index_a != -1 and index_b != -1: + return index_a - index_b + if index_a != -1: + return -1 + if index_b != -1: + return 1 + if mod_a < mod_b: + return -1 + return 1 From 54fbab645bbf8f7b115a091c02b3efc8fd830de6 Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Sat, 28 Feb 2026 23:54:34 +0100 Subject: [PATCH 02/21] Removed unused variables and bare excepts. --- games/game_cassettebeasts.py | 8 ++------ games/game_crimeboss.py | 11 ++++------- games/game_hitman3.py | 8 ++------ games/game_noita.py | 7 ++----- games/game_payday1.py | 11 ++++------- games/game_payday2.py | 11 ++++------- games/game_raid2.py | 11 ++++------- games/game_roadtovostok.py | 16 ++++++---------- games/game_silenthill2remake.py | 10 +++------- 9 files changed, 31 insertions(+), 62 deletions(-) diff --git a/games/game_cassettebeasts.py b/games/game_cassettebeasts.py index 99b5bd52..8b342a3a 100644 --- a/games/game_cassettebeasts.py +++ b/games/game_cassettebeasts.py @@ -9,10 +9,7 @@ from ..basic_game import BasicGame -try: - from PyQt6.QtCore import QDir, QFileInfo -except: - from PyQt5.QtCore import QDir, QFileInfo +from PyQt6.QtCore import QDir, QFileInfo class CassetteBeastsModDataChecker(mobase.ModDataChecker): @@ -55,7 +52,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: class CassetteBeastsGame(BasicGame): appdataenv = os.getenv('APPDATA') - + Name = "Cassette Beasts Support Plugin" Author = "modworkshop" Version = "1" @@ -115,4 +112,3 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): if not os.path.exists(modsPath): os.mkdir(modsPath) super().initializeProfile(directory, settings) - \ No newline at end of file diff --git a/games/game_crimeboss.py b/games/game_crimeboss.py index 648267ed..200119a0 100644 --- a/games/game_crimeboss.py +++ b/games/game_crimeboss.py @@ -14,12 +14,8 @@ from ..basic_game import BasicGame -try: - from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget - from PyQt6.QtCore import QDir, QFileInfo -except: - from PyQt5.QtWidgets import QMainWindow, QTabWidget, QWidget - from PyQt5.QtCore import QDir, QFileInfo +from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget +from PyQt6.QtCore import QDir, QFileInfo class Content(IntEnum): @@ -142,6 +138,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods + "/" GameDataPakMods = self.organizer.managedGame().GameDataPakMods + "/" GameDataNativeMods = self.organizer.managedGame().GameDataNativeMods + "/" + GameDataMovies = self.organizer.managedGame().GameDataMovies + "/" treefixed = 0 if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): treefixed = self.allMoveTo(filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/") @@ -303,4 +300,4 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): os.makedirs(self.ue4ssDirectory().absolutePath()) if not self.nativeDirectory().exists(): os.makedirs(self.nativeDirectory().absolutePath()) - super().initializeProfile(directory, settings) \ No newline at end of file + super().initializeProfile(directory, settings) diff --git a/games/game_hitman3.py b/games/game_hitman3.py index 3c31b210..7068bbb2 100644 --- a/games/game_hitman3.py +++ b/games/game_hitman3.py @@ -11,10 +11,7 @@ from ..basic_game import BasicGame -try: - from PyQt6.QtCore import QDir, QFileInfo -except: - from PyQt5.QtCore import QDir, QFileInfo +from PyQt6.QtCore import QDir, QFileInfo class Hitman3ModDataChecker(mobase.ModDataChecker): @@ -117,7 +114,6 @@ def init(self, organizer: mobase.IOrganizer) -> bool: return True def update_smm_meta(self, mods: dict[str, mobase.ModState]): - GameSMMPath = self._organizer.managedGame().GameSMMPath SMM_Path = os.path.join(self.dataDirectory().absolutePath(), self.GameSMMPath) SMM_Config_Json = SMM_Path + "/config.json" for key, value in mods.items(): @@ -223,4 +219,4 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): modsPath = self.dataDirectory().absolutePath() if not os.path.exists(modsPath): os.mkdir(modsPath) - super().initializeProfile(directory, settings) \ No newline at end of file + super().initializeProfile(directory, settings) diff --git a/games/game_noita.py b/games/game_noita.py index 729d5b39..d58effc5 100644 --- a/games/game_noita.py +++ b/games/game_noita.py @@ -11,10 +11,7 @@ from ..basic_game import BasicGame -try: - from PyQt6.QtCore import QDir, QFileInfo -except: - from PyQt5.QtCore import QDir, QFileInfo +from PyQt6.QtCore import QDir, QFileInfo class NoitaModDataChecker(mobase.ModDataChecker): @@ -151,4 +148,4 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): modsPath = self.dataDirectory().absolutePath() if not os.path.exists(modsPath): os.mkdir(modsPath) - super().initializeProfile(directory, settings) \ No newline at end of file + super().initializeProfile(directory, settings) diff --git a/games/game_payday1.py b/games/game_payday1.py index a7766f85..a3bae8cb 100644 --- a/games/game_payday1.py +++ b/games/game_payday1.py @@ -9,10 +9,7 @@ from ..basic_game import BasicGame -try: - from PyQt6.QtCore import QDir, QFileInfo -except: - from PyQt5.QtCore import QDir, QFileInfo +from PyQt6.QtCore import QDir, QFileInfo class Content(IntEnum): @@ -219,9 +216,9 @@ def init(self, organizer: mobase.IOrganizer) -> bool: def dll_copy( self, mods: dict[str, mobase.ModState] ): - + game_path = self.dataDirectory().absolutePath() + "/" - + for key, value in mods.items(): key = self._organizer.modList().getMod(key) tree = key.fileTree() @@ -274,4 +271,4 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): modsPath = self.dataDirectory().absolutePath() if not os.path.exists(modsPath): os.mkdir(modsPath) - super().initializeProfile(directory, settings) \ No newline at end of file + super().initializeProfile(directory, settings) diff --git a/games/game_payday2.py b/games/game_payday2.py index 24b29b5f..ee532302 100644 --- a/games/game_payday2.py +++ b/games/game_payday2.py @@ -9,10 +9,7 @@ from ..basic_game import BasicGame -try: - from PyQt6.QtCore import QDir, QFileInfo -except: - from PyQt5.QtCore import QDir, QFileInfo +from PyQt6.QtCore import QDir, QFileInfo class Content(IntEnum): @@ -145,7 +142,7 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: treefixed = 0 - + if filetree.exists("mod.txt", mobase.IFileTree.FILE): treefixed = self.allMoveTo(filetree, "mods/FOLDERNAME/") if treefixed == 1: @@ -233,9 +230,9 @@ def executables(self): def dll_copy( self, mods: dict[str, mobase.ModState] ): - + game_path = self.dataDirectory().absolutePath() + "/" - + for key, value in mods.items(): key = self._organizer.modList().getMod(key) tree = key.fileTree() diff --git a/games/game_raid2.py b/games/game_raid2.py index 0d089d74..673aedb8 100644 --- a/games/game_raid2.py +++ b/games/game_raid2.py @@ -9,10 +9,7 @@ from ..basic_game import BasicGame -try: - from PyQt6.QtCore import QDir, QFileInfo -except: - from PyQt5.QtCore import QDir, QFileInfo +from PyQt6.QtCore import QDir, QFileInfo class Content(IntEnum): @@ -153,9 +150,9 @@ def init(self, organizer: mobase.IOrganizer) -> bool: def dll_copy( self, mods: dict[str, mobase.ModState] ): - + game_path = self.dataDirectory().absolutePath() + "/" - + for key, value in mods.items(): key = self._organizer.modList().getMod(key) tree = key.fileTree() @@ -208,4 +205,4 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): modsPath = self.dataDirectory().absolutePath() if not os.path.exists(modsPath): os.mkdir(modsPath) - super().initializeProfile(directory, settings) \ No newline at end of file + super().initializeProfile(directory, settings) diff --git a/games/game_roadtovostok.py b/games/game_roadtovostok.py index 81e5b23e..ccbffb53 100644 --- a/games/game_roadtovostok.py +++ b/games/game_roadtovostok.py @@ -8,11 +8,7 @@ from ..basic_game import BasicGame -try: - from PyQt6.QtCore import QDir, QFileInfo -except: - from PyQt5.QtCore import QDir, QFileInfo - +from PyQt6.QtCore import QDir, QFileInfo class RoadToVostokModDataChecker(mobase.ModDataChecker): def __init__(self, organizer: mobase.IOrganizer): @@ -20,7 +16,7 @@ def __init__(self, organizer: mobase.IOrganizer): self.organizer: mobase.IOrganizer = organizer def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: - + if filetree.exists("mods", mobase.IFileTree.DIRECTORY) and not filetree.exists("mod.txt", mobase.IFileTree.FILE): return mobase.ModDataChecker.VALID for e in filetree: @@ -31,7 +27,7 @@ def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.Ch def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: GameModsPath = self.organizer.managedGame().GameModsPath + "/" treefixed = 0 - + for branch in filetree: mod_name = filetree.name() if mod_name == "": @@ -41,14 +37,14 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: os.makedirs(os.path.join(mod_path, GameModsPath), exist_ok=True) shutil.move(os.path.join(mod_path, branch.name()), os.path.join(mod_path, GameModsPath, branch.name())) treefixed = 1 - + if treefixed == 0: return None return filetree class RoadToVostokGame(BasicGame): - + Name = "Road to Vostok Support Plugin" Author = "modworkshop" Version = "1" @@ -86,4 +82,4 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): modsPath = self.dataDirectory().absolutePath() if not os.path.exists(modsPath): os.mkdir(modsPath) - super().initializeProfile(directory, settings) \ No newline at end of file + super().initializeProfile(directory, settings) diff --git a/games/game_silenthill2remake.py b/games/game_silenthill2remake.py index 022c0e8c..3ed4c6b9 100644 --- a/games/game_silenthill2remake.py +++ b/games/game_silenthill2remake.py @@ -14,12 +14,8 @@ from ..basic_game import BasicGame -try: - from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget - from PyQt6.QtCore import QDir, QFileInfo -except: - from PyQt5.QtWidgets import QMainWindow, QTabWidget, QWidget - from PyQt5.QtCore import QDir, QFileInfo +from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget +from PyQt6.QtCore import QDir, QFileInfo class Content(IntEnum): @@ -282,4 +278,4 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): os.makedirs(self.ue4ssDirectory().absolutePath()) if not self.movieDirectory().exists(): os.makedirs(self.movieDirectory().absolutePath()) - super().initializeProfile(directory, settings) \ No newline at end of file + super().initializeProfile(directory, settings) From 8f09d133c58d39c407099089fc744316e7b182f7 Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Sun, 1 Mar 2026 00:17:47 +0100 Subject: [PATCH 03/21] Cleanup --- games/game_emuvr.py | 7 ++----- games/game_ovkwalkingdead.py | 10 +++------- games/game_pacificdrive.py | 9 +++------ games/game_payday2.py | 2 +- games/game_payday3.py | 10 +++------- games/game_titanfall2.py | 7 ++----- games/game_zuma_deluxe.py | 7 ++----- games/unreal_tabs/manage_paks/model.py | 20 +++++++++++--------- games/unreal_tabs/manage_paks/view.py | 12 ++++-------- games/unreal_tabs/manage_paks/widget.py | 9 +++------ games/unreal_tabs/manage_ue4ss/model.py | 9 +++------ games/unreal_tabs/manage_ue4ss/view.py | 11 +++-------- games/unreal_tabs/manage_ue4ss/widget.py | 8 ++------ 13 files changed, 42 insertions(+), 79 deletions(-) diff --git a/games/game_emuvr.py b/games/game_emuvr.py index e1418ba4..2a0a85d1 100644 --- a/games/game_emuvr.py +++ b/games/game_emuvr.py @@ -9,10 +9,7 @@ from ..basic_game import BasicGame -try: - from PyQt6.QtCore import QDir, QFileInfo -except: - from PyQt5.QtCore import QDir, QFileInfo +from PyQt6.QtCore import QDir, QFileInfo class EmuVRModDataChecker(mobase.ModDataChecker): @@ -112,4 +109,4 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): modsPath = self.dataDirectory().absolutePath() if not os.path.exists(modsPath): os.mkdir(modsPath) - super().initializeProfile(directory, settings) \ No newline at end of file + super().initializeProfile(directory, settings) diff --git a/games/game_ovkwalkingdead.py b/games/game_ovkwalkingdead.py index e3916d89..127f172d 100644 --- a/games/game_ovkwalkingdead.py +++ b/games/game_ovkwalkingdead.py @@ -14,12 +14,8 @@ from ..basic_game import BasicGame -try: - from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget - from PyQt6.QtCore import QDir, QFileInfo -except: - from PyQt5.QtWidgets import QMainWindow, QTabWidget, QWidget - from PyQt5.QtCore import QDir, QFileInfo +from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget +from PyQt6.QtCore import QDir, QFileInfo class Content(IntEnum): @@ -281,4 +277,4 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): os.makedirs(self.ue4ssDirectory().absolutePath()) if not self.movieDirectory().exists(): os.makedirs(self.movieDirectory().absolutePath()) - super().initializeProfile(directory, settings) \ No newline at end of file + super().initializeProfile(directory, settings) diff --git a/games/game_pacificdrive.py b/games/game_pacificdrive.py index 0c2f99e8..45b2f1bf 100644 --- a/games/game_pacificdrive.py +++ b/games/game_pacificdrive.py @@ -14,12 +14,9 @@ from ..basic_game import BasicGame -try: - from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget - from PyQt6.QtCore import QDir, QFileInfo -except: - from PyQt5.QtWidgets import QMainWindow, QTabWidget, QWidget - from PyQt5.QtCore import QDir, QFileInfo +from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget +from PyQt6.QtCore import QDir, QFileInfo + class Content(IntEnum): diff --git a/games/game_payday2.py b/games/game_payday2.py index ee532302..3da17b04 100644 --- a/games/game_payday2.py +++ b/games/game_payday2.py @@ -289,4 +289,4 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): os.makedirs(self.modsDirectory().absolutePath()) if not self.overridesDirectory().exists(): os.makedirs(self.overridesDirectory().absolutePath()) - super().initializeProfile(directory, settings) \ No newline at end of file + super().initializeProfile(directory, settings) diff --git a/games/game_payday3.py b/games/game_payday3.py index c0060bd6..9e762400 100644 --- a/games/game_payday3.py +++ b/games/game_payday3.py @@ -14,12 +14,8 @@ from ..basic_game import BasicGame -try: - from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget - from PyQt6.QtCore import QDir, QFileInfo -except: - from PyQt5.QtWidgets import QMainWindow, QTabWidget, QWidget - from PyQt5.QtCore import QDir, QFileInfo +from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget +from PyQt6.QtCore import QDir, QFileInfo class Content(IntEnum): @@ -281,4 +277,4 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): os.makedirs(self.ue4ssDirectory().absolutePath()) if not self.movieDirectory().exists(): os.makedirs(self.movieDirectory().absolutePath()) - super().initializeProfile(directory, settings) \ No newline at end of file + super().initializeProfile(directory, settings) diff --git a/games/game_titanfall2.py b/games/game_titanfall2.py index a5708d24..8ef69875 100644 --- a/games/game_titanfall2.py +++ b/games/game_titanfall2.py @@ -11,10 +11,7 @@ from ..basic_game import BasicGame -try: - from PyQt6.QtCore import QDir, QFileInfo -except: - from PyQt5.QtCore import QDir, QFileInfo +from PyQt6.QtCore import QDir, QFileInfo class Content(IntEnum): @@ -286,4 +283,4 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): def mappings(self) -> list[mobase.Mapping]: return [ mobase.Mapping(self._organizer.profilePath() + "/" + self.NorthstarModJson, self.gameDirectory().absolutePath() + "/R2Northstar/" + self.NorthstarModJson, False, False), - ] \ No newline at end of file + ] diff --git a/games/game_zuma_deluxe.py b/games/game_zuma_deluxe.py index 6511fc4d..b8c2d1c4 100644 --- a/games/game_zuma_deluxe.py +++ b/games/game_zuma_deluxe.py @@ -11,10 +11,7 @@ from ..basic_game import BasicGame from ..basic_features import BasicGameSaveGameInfo -try: - from PyQt6.QtCore import QDir, QFileInfo -except: - from PyQt5.QtCore import QDir, QFileInfo +from PyQt6.QtCore import QDir, QFileInfo class Content(IntEnum): @@ -326,4 +323,4 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): def mappings(self) -> list[mobase.Mapping]: return [ mobase.Mapping(self._organizer.profilePath() + "/" + self.ProfileLevelsXml, self.gameDirectory().absolutePath() + "/" + self.GameLevelsXml, False, False), - ] \ No newline at end of file + ] diff --git a/games/unreal_tabs/manage_paks/model.py b/games/unreal_tabs/manage_paks/model.py index 9e7216f7..a21ec9e1 100644 --- a/games/unreal_tabs/manage_paks/model.py +++ b/games/unreal_tabs/manage_paks/model.py @@ -5,12 +5,8 @@ import mobase -try: - from PyQt6.QtCore import (QAbstractItemModel, QByteArray, QDataStream, QDir, QFileInfo, QMimeData, QModelIndex, QObject, Qt, QVariant) - from PyQt6.QtWidgets import QWidget -except: - from PyQt5.QtCore import (QAbstractItemModel, QByteArray, QDataStream, QDir, QFileInfo, QMimeData, QModelIndex, QObject, Qt, QVariant) - from PyQt5.QtWidgets import QWidget +from PyQt6.QtCore import (QAbstractItemModel, QByteArray, QDataStream, QDir, QFileInfo, QMimeData, QModelIndex, QObject, Qt, QVariant) +from PyQt6.QtWidgets import QWidget _PakInfo: TypeAlias = tuple[str, str, str, str] @@ -61,12 +57,16 @@ def flags(self, index: QModelIndex) -> Qt.ItemFlag: | Qt.ItemFlag.ItemIsDropEnabled & Qt.ItemFlag.ItemIsEditable ) - def columnCount(self, parent: QModelIndex = QModelIndex()) -> int: + def columnCount(self, parent: QModelIndex) -> int: + if parent is None: + parent = QModelIndex() return len(PaksColumns) def index( - self, row: int, column: int, parent: QModelIndex = QModelIndex() + self, row: int, column: int, parent: QModelIndex ) -> QModelIndex: + if parent is None: + parent = QModelIndex() if ( row < 0 or row >= self.rowCount() @@ -86,7 +86,9 @@ def parent(self, child: QModelIndex | None = None) -> QModelIndex | QObject | No return super().parent() return QModelIndex() - def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: + def rowCount(self, parent: QModelIndex) -> int: + if parent is None: + parent = QModelIndex() return len(self.paks) def setData( diff --git a/games/unreal_tabs/manage_paks/view.py b/games/unreal_tabs/manage_paks/view.py index 0e5d6385..a56b0cdd 100644 --- a/games/unreal_tabs/manage_paks/view.py +++ b/games/unreal_tabs/manage_paks/view.py @@ -1,13 +1,9 @@ from typing import Iterable -try: - from PyQt6.QtCore import QModelIndex, Qt, pyqtSignal - from PyQt6.QtGui import QDropEvent - from PyQt6.QtWidgets import QAbstractItemView, QTreeView, QWidget -except: - from PyQt5.QtCore import QModelIndex, Qt, pyqtSignal - from PyQt5.QtGui import QDropEvent - from PyQt5.QtWidgets import QAbstractItemView, QTreeView, QWidget +from PyQt6.QtCore import QModelIndex, Qt, pyqtSignal +from PyQt6.QtGui import QDropEvent +from PyQt6.QtWidgets import QAbstractItemView, QTreeView, QWidget + class PaksView(QTreeView): data_dropped = pyqtSignal() diff --git a/games/unreal_tabs/manage_paks/widget.py b/games/unreal_tabs/manage_paks/widget.py index 6c7a5eb3..b84f99bb 100644 --- a/games/unreal_tabs/manage_paks/widget.py +++ b/games/unreal_tabs/manage_paks/widget.py @@ -8,12 +8,9 @@ from .model import PaksModel from .view import PaksView -try: - from PyQt6.QtWidgets import QGridLayout, QWidget - from PyQt6.QtCore import QDir, QFileInfo, Qt -except: - from PyQt5.QtWidgets import QGridLayout, QWidget - from PyQt5.QtCore import QDir, QFileInfo, Qt + +from PyQt6.QtWidgets import QGridLayout, QWidget +from PyQt6.QtCore import QDir, QFileInfo, Qt def pak_sort(a: tuple[str, str], b: tuple[str, str]) -> int: a_pak, a_str = a[0], a[1] or a[0] diff --git a/games/unreal_tabs/manage_ue4ss/model.py b/games/unreal_tabs/manage_ue4ss/model.py index 8289b019..82e5f2e4 100644 --- a/games/unreal_tabs/manage_ue4ss/model.py +++ b/games/unreal_tabs/manage_ue4ss/model.py @@ -2,12 +2,9 @@ from json import JSONDecodeError from typing import Any, Iterable -try: - from PyQt6.QtCore import (QDir, QFileInfo, QMimeData, QModelIndex, QStringListModel, Qt) - from PyQt6.QtWidgets import QWidget -except: - from PyQt5.QtCore import (QDir, QFileInfo, QMimeData, QModelIndex, QStringListModel, Qt) - from PyQt5.QtWidgets import QWidget +from PyQt6.QtCore import (QDir, QFileInfo, QMimeData, QModelIndex, QStringListModel, Qt) +from PyQt6.QtWidgets import QWidget + import mobase diff --git a/games/unreal_tabs/manage_ue4ss/view.py b/games/unreal_tabs/manage_ue4ss/view.py index 218468b0..bb994ca6 100644 --- a/games/unreal_tabs/manage_ue4ss/view.py +++ b/games/unreal_tabs/manage_ue4ss/view.py @@ -1,13 +1,8 @@ from typing import Iterable -try: - from PyQt6.QtCore import QModelIndex, Qt, pyqtSignal - from PyQt6.QtGui import QDropEvent - from PyQt6.QtWidgets import QAbstractItemView, QListView, QWidget -except: - from PyQt5.QtCore import QModelIndex, Qt, pyqtSignal - from PyQt5.QtGui import QDropEvent - from PyQt5.QtWidgets import QAbstractItemView, QListView, QWidget +from PyQt6.QtCore import QModelIndex, Qt, pyqtSignal +from PyQt6.QtGui import QDropEvent +from PyQt6.QtWidgets import QAbstractItemView, QListView, QWidget class UE4SSView(QListView): diff --git a/games/unreal_tabs/manage_ue4ss/widget.py b/games/unreal_tabs/manage_ue4ss/widget.py index 6d20ead6..d3732161 100644 --- a/games/unreal_tabs/manage_ue4ss/widget.py +++ b/games/unreal_tabs/manage_ue4ss/widget.py @@ -9,12 +9,8 @@ from .model import UE4SSListModel from .view import UE4SSView -try: - from PyQt6.QtWidgets import QGridLayout, QWidget - from PyQt6.QtCore import QDir, QFileInfo, Qt -except: - from PyQt5.QtWidgets import QGridLayout, QWidget - from PyQt5.QtCore import QDir, QFileInfo, Qt +from PyQt6.QtWidgets import QGridLayout, QWidget +from PyQt6.QtCore import QDir, QFileInfo, Qt class UE4SSTabWidget(QWidget): def __init__(self, parent: QWidget | None, organizer: mobase.IOrganizer): From 870fbc6533259d24175bf1d0ba4d297414ce686b Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Sun, 1 Mar 2026 00:37:14 +0100 Subject: [PATCH 04/21] More Cleanup --- games/game_cassettebeasts.py | 8 +++----- games/game_crimeboss.py | 1 - games/game_emuvr.py | 2 -- games/game_hitman3.py | 3 --- games/game_noita.py | 2 -- games/game_pacificdrive.py | 2 +- games/game_payday1.py | 1 - games/game_payday3.py | 1 - games/game_raid2.py | 1 - games/game_roadtovostok.py | 4 +--- games/game_silenthill2remake.py | 1 - games/game_titanfall2.py | 4 +--- games/game_zuma_deluxe.py | 1 - 13 files changed, 6 insertions(+), 25 deletions(-) diff --git a/games/game_cassettebeasts.py b/games/game_cassettebeasts.py index 8b342a3a..41401f56 100644 --- a/games/game_cassettebeasts.py +++ b/games/game_cassettebeasts.py @@ -2,9 +2,7 @@ import shutil import mobase -from enum import IntEnum, auto from pathlib import Path -from typing import Any, List, Set, cast from functools import cached_property from ..basic_game import BasicGame @@ -51,7 +49,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: class CassetteBeastsGame(BasicGame): - appdataenv = os.getenv('APPDATA') + appdataenv = os.getenv("APPDATA") Name = "Cassette Beasts Support Plugin" Author = "modworkshop" @@ -60,8 +58,8 @@ class CassetteBeastsGame(BasicGame): GameShortName = "cassette-beasts" GameSteamId = 1321440 GameBinary = "CassetteBeasts.exe" - GameDataPath = appdataenv + '/CassetteBeasts/mods' - GameDocumentsDirectory = appdataenv + '/CassetteBeasts' + GameDataPath = appdataenv + "/CassetteBeasts/mods" + GameDocumentsDirectory = appdataenv + "/CassetteBeasts" GameSaveExtension = "gcpf" def init(self, organizer: mobase.IOrganizer) -> bool: diff --git a/games/game_crimeboss.py b/games/game_crimeboss.py index 200119a0..bf88013a 100644 --- a/games/game_crimeboss.py +++ b/games/game_crimeboss.py @@ -5,7 +5,6 @@ from enum import IntEnum, auto from pathlib import Path -from typing import Any, List, Set, cast from functools import cached_property from .unreal_tabs.constants import DEFAULT_UE4SS_MODS, UE4SSModInfo diff --git a/games/game_emuvr.py b/games/game_emuvr.py index 2a0a85d1..1b4737b2 100644 --- a/games/game_emuvr.py +++ b/games/game_emuvr.py @@ -2,9 +2,7 @@ import shutil import mobase -from enum import IntEnum, auto from pathlib import Path -from typing import Any, List, Set, cast from functools import cached_property from ..basic_game import BasicGame diff --git a/games/game_hitman3.py b/games/game_hitman3.py index 7068bbb2..3b3ad92c 100644 --- a/games/game_hitman3.py +++ b/games/game_hitman3.py @@ -3,10 +3,7 @@ import json import mobase -from json import JSONDecodeError -from enum import IntEnum, auto from pathlib import Path -from typing import Any, List, Set, cast from functools import cached_property from ..basic_game import BasicGame diff --git a/games/game_noita.py b/games/game_noita.py index d58effc5..5a3dfeef 100644 --- a/games/game_noita.py +++ b/games/game_noita.py @@ -4,9 +4,7 @@ import mobase from json import JSONDecodeError -from enum import IntEnum, auto from pathlib import Path -from typing import Any, List, Set, cast from functools import cached_property from ..basic_game import BasicGame diff --git a/games/game_pacificdrive.py b/games/game_pacificdrive.py index 45b2f1bf..38204cb1 100644 --- a/games/game_pacificdrive.py +++ b/games/game_pacificdrive.py @@ -279,4 +279,4 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): os.makedirs(self.ue4ssDirectory().absolutePath()) if not self.movieDirectory().exists(): os.makedirs(self.movieDirectory().absolutePath()) - super().initializeProfile(directory, settings) \ No newline at end of file + super().initializeProfile(directory, settings) diff --git a/games/game_payday1.py b/games/game_payday1.py index a3bae8cb..67928066 100644 --- a/games/game_payday1.py +++ b/games/game_payday1.py @@ -4,7 +4,6 @@ from enum import IntEnum, auto from pathlib import Path -from typing import Any, List, Set, cast from functools import cached_property from ..basic_game import BasicGame diff --git a/games/game_payday3.py b/games/game_payday3.py index 9e762400..9135403c 100644 --- a/games/game_payday3.py +++ b/games/game_payday3.py @@ -5,7 +5,6 @@ from enum import IntEnum, auto from pathlib import Path -from typing import Any, List, Set, cast from functools import cached_property from .unreal_tabs.constants import DEFAULT_UE4SS_MODS, UE4SSModInfo diff --git a/games/game_raid2.py b/games/game_raid2.py index 673aedb8..105a0bdf 100644 --- a/games/game_raid2.py +++ b/games/game_raid2.py @@ -4,7 +4,6 @@ from enum import IntEnum, auto from pathlib import Path -from typing import Any, List, Set, cast from functools import cached_property from ..basic_game import BasicGame diff --git a/games/game_roadtovostok.py b/games/game_roadtovostok.py index ccbffb53..063128d8 100644 --- a/games/game_roadtovostok.py +++ b/games/game_roadtovostok.py @@ -2,9 +2,7 @@ import shutil import mobase -from enum import IntEnum, auto from pathlib import Path -from typing import Any, List, Set, cast from ..basic_game import BasicGame @@ -54,7 +52,7 @@ class RoadToVostokGame(BasicGame): GameBinary = "Road_to_Vostok_Demo.exe" GameDataPath = "%GAME_PATH%" GameModsPath = "mods" - GameDocumentsDirectory = '%APPDATA%/Godot/app_userdata/Road to Vostok' + GameDocumentsDirectory = "%APPDATA%/Godot/app_userdata/Road to Vostok" GameSaveExtension = "tres" def init(self, organizer: mobase.IOrganizer) -> bool: diff --git a/games/game_silenthill2remake.py b/games/game_silenthill2remake.py index 3ed4c6b9..9d7964b1 100644 --- a/games/game_silenthill2remake.py +++ b/games/game_silenthill2remake.py @@ -5,7 +5,6 @@ from enum import IntEnum, auto from pathlib import Path -from typing import Any, List, Set, cast from functools import cached_property from .unreal_tabs.constants import DEFAULT_UE4SS_MODS, UE4SSModInfo diff --git a/games/game_titanfall2.py b/games/game_titanfall2.py index 8ef69875..abf8afc8 100644 --- a/games/game_titanfall2.py +++ b/games/game_titanfall2.py @@ -3,10 +3,8 @@ import json import mobase -from json import JSONDecodeError from enum import IntEnum, auto from pathlib import Path -from typing import Any, List, Set, cast from functools import cached_property from ..basic_game import BasicGame @@ -218,7 +216,7 @@ def update_enable_mods_json(self, mods: dict[str, mobase.ModState]): if value == 35 and modname not in Northstar: Northstar[modname] = {modversion: True} if value == 33 and modname in Northstar: - removed_value = Northstar.pop(modname) + Northstar = Northstar.pop(modname) with open(Northstar_Config_Json, "w", encoding="utf-8") as f: json.dump(Northstar, f, ensure_ascii=False, indent=4) diff --git a/games/game_zuma_deluxe.py b/games/game_zuma_deluxe.py index b8c2d1c4..9d7da777 100644 --- a/games/game_zuma_deluxe.py +++ b/games/game_zuma_deluxe.py @@ -5,7 +5,6 @@ from enum import IntEnum, auto from pathlib import Path -from typing import Any, List, Set, cast from functools import cached_property from ..basic_game import BasicGame From ea390e467196d706d9b42e0b0b3b8fbea4061305 Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Sun, 1 Mar 2026 01:08:20 +0100 Subject: [PATCH 05/21] Additional Cleanup --- games/game_noita.py | 31 ++++-- games/game_ovkwalkingdead.py | 17 ++-- games/game_pacificdrive.py | 18 ++-- games/game_payday1.py | 12 +-- games/game_payday2.py | 13 +-- games/game_payday3.py | 63 +++++++++--- games/game_roadtovostok.py | 6 +- games/game_silenthill2remake.py | 17 ++-- games/game_titanfall2.py | 91 ++++++++++++----- games/game_zuma_deluxe.py | 124 +++++++++++++++++------ games/unreal_tabs/manage_paks/model.py | 2 +- games/unreal_tabs/manage_paks/widget.py | 6 +- games/unreal_tabs/manage_ue4ss/model.py | 4 +- games/unreal_tabs/manage_ue4ss/widget.py | 14 +-- 14 files changed, 277 insertions(+), 141 deletions(-) diff --git a/games/game_noita.py b/games/game_noita.py index 5a3dfeef..38db9331 100644 --- a/games/game_noita.py +++ b/games/game_noita.py @@ -1,16 +1,13 @@ +from functools import cached_property +from pathlib import Path import os import shutil -import json -import mobase -from json import JSONDecodeError -from pathlib import Path -from functools import cached_property +import mobase +from PyQt6.QtCore import QDir, QFileInfo from ..basic_game import BasicGame -from PyQt6.QtCore import QDir, QFileInfo - class NoitaModDataChecker(mobase.ModDataChecker): def __init__(self, organizer: mobase.IOrganizer): @@ -39,7 +36,9 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): filetree: mobase.IFileTree = mod.fileTree() fixed = False modname = mod.name() - if filetree is not None and filetree.exists(GameModsPath + "/FOLDERNAME", mobase.IFileTree.DIRECTORY): + if filetree is not None and filetree.exists( + GameModsPath + "/FOLDERNAME", mobase.IFileTree.DIRECTORY + ): path = mod.absolutePath() old_path = os.path.join(path, GameModsPath + "/FOLDERNAME") new_path = os.path.join(path, GameModsPath + f"/{modname}") @@ -49,7 +48,9 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): return self.needsNameFix = False - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: if filetree.exists("mods", mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID return mobase.ModDataChecker.FIXABLE @@ -131,7 +132,9 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: except AttributeError: efls = [] libs: set[str] = set() - tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = ( + self._organizer.virtualFileTree() + ) if type(tree) is not mobase.IFileTree: return efls for e in tree: @@ -139,7 +142,13 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: libs.add(relpath) exes = self.executables() - efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + efls = efls + [ + mobase.ExecutableForcedLoadSetting( + exe.binary().fileName(), lib + ).withEnabled(True) + for lib in libs + for exe in exes + ] return efls def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): diff --git a/games/game_ovkwalkingdead.py b/games/game_ovkwalkingdead.py index 127f172d..8437e4c5 100644 --- a/games/game_ovkwalkingdead.py +++ b/games/game_ovkwalkingdead.py @@ -1,22 +1,19 @@ +from enum import IntEnum, auto +from functools import cached_property +from pathlib import Path import json import os import shutil -import mobase -from enum import IntEnum, auto -from pathlib import Path -from typing import Any, List, Set, cast -from functools import cached_property +import mobase +from PyQt6.QtCore import QDir, QFileInfo +from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget +from ..basic_game import BasicGame from .unreal_tabs.constants import DEFAULT_UE4SS_MODS, UE4SSModInfo from .unreal_tabs.manage_paks.widget import PaksTabWidget from .unreal_tabs.manage_ue4ss.widget import UE4SSTabWidget -from ..basic_game import BasicGame - -from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget -from PyQt6.QtCore import QDir, QFileInfo - class Content(IntEnum): UCAS = auto() diff --git a/games/game_pacificdrive.py b/games/game_pacificdrive.py index 38204cb1..410fc17d 100644 --- a/games/game_pacificdrive.py +++ b/games/game_pacificdrive.py @@ -1,23 +1,19 @@ +from enum import IntEnum, auto +from functools import cached_property +from pathlib import Path import json import os import shutil -import mobase -from enum import IntEnum, auto -from pathlib import Path -from typing import Any, List, Set, cast -from functools import cached_property +import mobase +from PyQt6.QtCore import QDir, QFileInfo +from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget +from ..basic_game import BasicGame from .unreal_tabs.constants import DEFAULT_UE4SS_MODS, UE4SSModInfo from .unreal_tabs.manage_paks.widget import PaksTabWidget from .unreal_tabs.manage_ue4ss.widget import UE4SSTabWidget -from ..basic_game import BasicGame - -from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget -from PyQt6.QtCore import QDir, QFileInfo - - class Content(IntEnum): UCAS = auto() diff --git a/games/game_payday1.py b/games/game_payday1.py index 67928066..7fee5286 100644 --- a/games/game_payday1.py +++ b/games/game_payday1.py @@ -1,15 +1,13 @@ -import os -import shutil -import mobase - from enum import IntEnum, auto -from pathlib import Path from functools import cached_property +from pathlib import Path +import os +import shutil -from ..basic_game import BasicGame - +import mobase from PyQt6.QtCore import QDir, QFileInfo +from ..basic_game import BasicGame class Content(IntEnum): TEXTURE = auto() diff --git a/games/game_payday2.py b/games/game_payday2.py index 3da17b04..5998bb69 100644 --- a/games/game_payday2.py +++ b/games/game_payday2.py @@ -1,16 +1,13 @@ -import os -import shutil -import mobase - from enum import IntEnum, auto -from pathlib import Path -from typing import Any, List, Set, cast from functools import cached_property +from pathlib import Path +import os +import shutil -from ..basic_game import BasicGame - +import mobase from PyQt6.QtCore import QDir, QFileInfo +from ..basic_game import BasicGame class Content(IntEnum): TEXTURE = auto() diff --git a/games/game_payday3.py b/games/game_payday3.py index 9135403c..7410d5c9 100644 --- a/games/game_payday3.py +++ b/games/game_payday3.py @@ -38,7 +38,10 @@ class Payday3ModDataContent(mobase.ModDataContent): ] def getAllContents(self) -> list[mobase.ModDataContent.Content]: - return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + return [ + mobase.ModDataContent.Content(id, name, icon, *filter_only) + for id, name, icon, *filter_only in self.GAMECONTENTS + ] def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): @@ -83,7 +86,9 @@ def move_overwrite_merge(self, source, destination): self.move_overwrite_merge(s_item, d_item) os.rmdir(source) - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods GameDataPakMods = self.organizer.managedGame().GameDataPakMods GameDataMovies = self.organizer.managedGame().GameDataMovieMods @@ -120,10 +125,14 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: GameDataMovies = self.organizer.managedGame().GameDataMovieMods + "/" treefixed = 0 if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): - treefixed = self.allMoveTo(filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/") + treefixed = self.allMoveTo( + filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/" + ) if treefixed == 1: return filetree - if filetree.exists("Scripts", mobase.IFileTree.DIRECTORY) or filetree.exists("dlls", mobase.IFileTree.DIRECTORY): + if filetree.exists("Scripts", mobase.IFileTree.DIRECTORY) or filetree.exists( + "dlls", mobase.IFileTree.DIRECTORY + ): treefixed = self.allMoveTo(filetree, GameDataUE4SSMods) if treefixed == 1: return filetree @@ -139,14 +148,32 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: if mod_name == "": mod_name = e.name() mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if filetree.createOrphanTree("OrphanTree") is None and os.path.exists(mod_path): + if filetree.createOrphanTree( + "OrphanTree" + ) is None and os.path.exists(mod_path): match e.suffix().casefold(): case "pak" | "utoc" | "ucas": - os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) + os.makedirs( + os.path.join(mod_path, GameDataPakMods), + exist_ok=True, + ) + shutil.move( + os.path.join(mod_path, e.name()), + os.path.join( + mod_path, GameDataPakMods, e.name() + ), + ) case "bk2": - os.makedirs(os.path.join(mod_path, GameDataMovies), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovies, e.name())) + os.makedirs( + os.path.join(mod_path, GameDataMovies), + exist_ok=True, + ) + shutil.move( + os.path.join(mod_path, e.name()), + os.path.join( + mod_path, GameDataMovies, e.name() + ), + ) case _: pass treefixed = 1 @@ -158,7 +185,11 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: case "pak" | "utoc" | "ucas": filetree.move(e, GameDataPakMods, mobase.IFileTree.MERGE) case "dll": - filetree.move(e, os.path.dirname(GameDataUE4SSMods) + "/", mobase.IFileTree.MERGE) + filetree.move( + e, + os.path.dirname(GameDataUE4SSMods) + "/", + mobase.IFileTree.MERGE, + ) case "bk2": filetree.move(e, GameDataMovies, mobase.IFileTree.MERGE) case _: @@ -231,7 +262,9 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: except AttributeError: efls = [] libs: set[str] = set() - tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = ( + self._organizer.virtualFileTree() + ) if type(tree) is not mobase.IFileTree: return efls for e in tree: @@ -239,7 +272,13 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: libs.add(relpath) exes = self.executables() - efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + efls = efls + [ + mobase.ExecutableForcedLoadSetting( + exe.binary().fileName(), lib + ).withEnabled(True) + for lib in libs + for exe in exes + ] return efls def paksDirectory(self) -> QDir: diff --git a/games/game_roadtovostok.py b/games/game_roadtovostok.py index 063128d8..871e84a7 100644 --- a/games/game_roadtovostok.py +++ b/games/game_roadtovostok.py @@ -1,13 +1,11 @@ import os import shutil -import mobase -from pathlib import Path +import mobase +from PyQt6.QtCore import QDir, QFileInfo from ..basic_game import BasicGame -from PyQt6.QtCore import QDir, QFileInfo - class RoadToVostokModDataChecker(mobase.ModDataChecker): def __init__(self, organizer: mobase.IOrganizer): super().__init__() diff --git a/games/game_silenthill2remake.py b/games/game_silenthill2remake.py index 9d7964b1..fda5ee34 100644 --- a/games/game_silenthill2remake.py +++ b/games/game_silenthill2remake.py @@ -1,22 +1,19 @@ +from enum import IntEnum, auto +from functools import cached_property +from pathlib import Path import json import os import shutil -import mobase -from enum import IntEnum, auto -from pathlib import Path -from functools import cached_property +import mobase +from PyQt6.QtCore import QDir, QFileInfo +from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget +from ..basic_game import BasicGame from .unreal_tabs.constants import DEFAULT_UE4SS_MODS, UE4SSModInfo from .unreal_tabs.manage_paks.widget import PaksTabWidget from .unreal_tabs.manage_ue4ss.widget import UE4SSTabWidget -from ..basic_game import BasicGame - -from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget -from PyQt6.QtCore import QDir, QFileInfo - - class Content(IntEnum): UCAS = auto() UTOC = auto() diff --git a/games/game_titanfall2.py b/games/game_titanfall2.py index abf8afc8..f7cc5f87 100644 --- a/games/game_titanfall2.py +++ b/games/game_titanfall2.py @@ -1,16 +1,14 @@ +from enum import IntEnum, auto +from functools import cached_property +from pathlib import Path import os import shutil -import json -import mobase -from enum import IntEnum, auto -from pathlib import Path -from functools import cached_property +import mobase +from PyQt6.QtCore import QDir, QFileInfo from ..basic_game import BasicGame -from PyQt6.QtCore import QDir, QFileInfo - class Content(IntEnum): MATERIAL = auto() @@ -36,7 +34,10 @@ class Titanfall2ModDataContent(mobase.ModDataContent): ] def getAllContents(self) -> list[mobase.ModDataContent.Content]: - return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + return [ + mobase.ModDataContent.Content(id, name, icon, *filter_only) + for id, name, icon, *filter_only in self.GAMECONTENTS + ] contents = set() @@ -95,7 +96,9 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): filetree: mobase.IFileTree = mod.fileTree() fixed = False modname = mod.name() - if filetree is not None and filetree.exists(northstarModPath + "FOLDERNAME", mobase.IFileTree.DIRECTORY): + if filetree is not None and filetree.exists( + northstarModPath + "FOLDERNAME", mobase.IFileTree.DIRECTORY + ): path = mod.absolutePath() json_path = os.path.join(path, northstarModPath + "FOLDERNAME/mod.json") mod_data = json.load(open(json_path, encoding="utf-8")) @@ -104,7 +107,9 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): new_path = os.path.join(path, northstarModPath + f"{modname}") self.move_overwrite_merge(old_path, new_path) fixed = True - elif filetree is not None and filetree.exists(northstarModPath + "FOLDERNAME_NAME", mobase.IFileTree.DIRECTORY): + elif filetree is not None and filetree.exists( + northstarModPath + "FOLDERNAME_NAME", mobase.IFileTree.DIRECTORY + ): path = mod.absolutePath() old_path = os.path.join(path, northstarModPath + "FOLDERNAME_NAME") new_path = os.path.join(path, northstarModPath + f"{modname}") @@ -114,7 +119,9 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): return self.needsNameFix = False - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: if filetree.exists("R2Northstar", mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID return mobase.ModDataChecker.FIXABLE @@ -151,7 +158,9 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: else: try: if filetree[0][0].exists("mod.json", mobase.IFileTree.FILE): - filetree.move(filetree[0][0], filetree[0].path("/"), mobase.IFileTree.REPLACE) + filetree.move( + filetree[0][0], filetree[0].path("/"), mobase.IFileTree.REPLACE + ) filetree.move(filetree[0], northstarModPath, mobase.IFileTree.MERGE) treefixed = 1 except TypeError: @@ -163,7 +172,11 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: else: for e in filetree: if e is not None and e.path("/").count("/") == 0: - filetree.move(e, northstarModPath + "FOLDERNAME_NAME/", mobase.IFileTree.MERGE) + filetree.move( + e, + northstarModPath + "FOLDERNAME_NAME/", + mobase.IFileTree.MERGE, + ) treefixed = 1 self.needsNameFix = True if treefixed == 0: @@ -194,7 +207,9 @@ def init(self, organizer: mobase.IOrganizer) -> bool: return True def update_enable_mods_json(self, mods: dict[str, mobase.ModState]): - Northstar_Config_Json = self._organizer.profilePath() + "/" + self.NorthstarModJson + Northstar_Config_Json = ( + self._organizer.profilePath() + "/" + self.NorthstarModJson + ) with open(Northstar_Config_Json, "r", encoding="utf-8") as f: Northstar = json.load(f) for key, value in mods.items(): @@ -205,7 +220,9 @@ def update_enable_mods_json(self, mods: dict[str, mobase.ModState]): for e in subtree: if e is not None and e.isDir(): if e.exists("mod.json", mobase.IFileTree.FILE): - json_path = key.absolutePath() + "/" + e.path() + "/mod.json" + json_path = ( + key.absolutePath() + "/" + e.path() + "/mod.json" + ) with open(json_path, "r", encoding="utf-8") as f: mod_data = json.load(f) modname = mod_data["Name"] @@ -217,7 +234,9 @@ def update_enable_mods_json(self, mods: dict[str, mobase.ModState]): Northstar[modname] = {modversion: True} if value == 33 and modname in Northstar: Northstar = Northstar.pop(modname) - with open(Northstar_Config_Json, "w", encoding="utf-8") as f: + with open( + Northstar_Config_Json, "w", encoding="utf-8" + ) as f: json.dump(Northstar, f, ensure_ascii=False, indent=4) def executables(self): @@ -226,7 +245,9 @@ def executables(self): "Titanfall 2", QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), ), - mobase.ExecutableInfo("Northstar", QFileInfo(self.gameDirectory(), "NorthstarLauncher.exe")), + mobase.ExecutableInfo( + "Northstar", QFileInfo(self.gameDirectory(), "NorthstarLauncher.exe") + ), ] @cached_property @@ -240,7 +261,9 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: except AttributeError: efls = [] libs: set[str] = set() - tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = ( + self._organizer.virtualFileTree() + ) if type(tree) is not mobase.IFileTree: return efls for e in tree: @@ -248,7 +271,13 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: libs.add(relpath) exes = self.executables() - efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + efls = efls + [ + mobase.ExecutableForcedLoadSetting( + exe.binary().fileName(), lib + ).withEnabled(True) + for lib in libs + for exe in exes + ] return efls def northstarDirectory(self) -> QDir: @@ -259,9 +288,16 @@ def iniFiles(self): def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): northstar_json_path = directory.absolutePath() + "/" + self.NorthstarModJson - northstar_json_game_path = self.gameDirectory().absolutePath() + "/R2Northstar/" + self.NorthstarModJson + northstar_json_game_path = ( + self.gameDirectory().absolutePath() + + "/R2Northstar/" + + self.NorthstarModJson + ) blank_mod_json = '{"Version": 1,"Northstar.Client": {"1.31.6": true},"Northstar.CustomServers": {"1.31.6": true},"Northstar.Custom": {"1.31.6": true}}' - if not os.path.exists(northstar_json_path) or os.path.getsize(northstar_json_path) == 0: + if ( + not os.path.exists(northstar_json_path) + or os.path.getsize(northstar_json_path) == 0 + ): if os.path.exists(northstar_json_game_path): with open(northstar_json_game_path, "r") as game_json: game_json_content = game_json.read() @@ -273,12 +309,21 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): with open(northstar_json_path, "w") as northstar_json: northstar_json.write(blank_mod_json) northstar_json.close() - modsPath = os.path.join(self.dataDirectory().absolutePath(), self.GameNorthstarPath) + modsPath = os.path.join( + self.dataDirectory().absolutePath(), self.GameNorthstarPath + ) if not os.path.exists(modsPath): os.mkdir(modsPath) super().initializeProfile(directory, settings) def mappings(self) -> list[mobase.Mapping]: return [ - mobase.Mapping(self._organizer.profilePath() + "/" + self.NorthstarModJson, self.gameDirectory().absolutePath() + "/R2Northstar/" + self.NorthstarModJson, False, False), + mobase.Mapping( + self._organizer.profilePath() + "/" + self.NorthstarModJson, + self.gameDirectory().absolutePath() + + "/R2Northstar/" + + self.NorthstarModJson, + False, + False, + ), ] diff --git a/games/game_zuma_deluxe.py b/games/game_zuma_deluxe.py index 9d7da777..a6c3bc73 100644 --- a/games/game_zuma_deluxe.py +++ b/games/game_zuma_deluxe.py @@ -1,16 +1,15 @@ +from enum import IntEnum, auto +from functools import cached_property +from pathlib import Path import os -import shutil import re -import mobase +import shutil -from enum import IntEnum, auto -from pathlib import Path -from functools import cached_property +import mobase +from PyQt6.QtCore import QDir, QFileInfo -from ..basic_game import BasicGame from ..basic_features import BasicGameSaveGameInfo - -from PyQt6.QtCore import QDir, QFileInfo +from ..basic_game import BasicGame class Content(IntEnum): @@ -33,7 +32,10 @@ class ZumaModDataContent(mobase.ModDataContent): ] def getAllContents(self) -> list[mobase.ModDataContent.Content]: - return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + return [ + mobase.ModDataContent.Content(id, name, icon, *filter_only) + for id, name, icon, *filter_only in self.GAMECONTENTS + ] contents = set() @@ -87,7 +89,9 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): filetree: mobase.IFileTree = mod.fileTree() fixed = False modname = mod.name() - if filetree is not None and filetree.exists("mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): + if filetree is not None and filetree.exists( + "mods/FOLDERNAME", mobase.IFileTree.DIRECTORY + ): path = mod.absolutePath() old_path = os.path.join(path, "mods/FOLDERNAME") new_path = os.path.join(path, f"mods/{modname}") @@ -97,8 +101,18 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): return self.needsNameFix = False - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: - validFolders = ["images", "levels", "music", "sounds", "fonts", "properties", "userdata"] + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + validFolders = [ + "images", + "levels", + "music", + "sounds", + "fonts", + "properties", + "userdata", + ] validFiles = ["exe"] for e in filetree: if e.isDir(): @@ -134,7 +148,15 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: GameLevelsPath = self.organizer.managedGame().GameLevelsPath - validFolders = ["images", "levels", "music", "sounds", "fonts", "properties", "userdata"] + validFolders = [ + "images", + "levels", + "music", + "sounds", + "fonts", + "properties", + "userdata", + ] entriesToMove: list[mobase.FileTreeEntry] = [] treefixed = 0 if filetree.exists("map.txt", mobase.IFileTree.FILE): @@ -202,14 +224,20 @@ def init(self, organizer: mobase.IOrganizer) -> bool: return True def update_levels(self, mods: dict[str, mobase.ModState]): - profile_levels_path = self._organizer.profilePath() + "/" + self.ProfileLevelsXml - game_levels_path = os.path.join(self.dataDirectory().absolutePath(), self.GameLevelsXml) + profile_levels_path = ( + self._organizer.profilePath() + "/" + self.ProfileLevelsXml + ) + game_levels_path = os.path.join( + self.dataDirectory().absolutePath(), self.GameLevelsXml + ) for key, value in mods.items(): key = self._organizer.modList().getMod(key) tree = key.fileTree() if tree.exists("levels/levels.xml", mobase.IFileTree.FILE): levels_txt_path = os.path.join(key.absolutePath(), "levels/levels.xml") - profile_levels_path = self._organizer.profilePath() + "/" + self.ProfileLevelsXml + profile_levels_path = ( + self._organizer.profilePath() + "/" + self.ProfileLevelsXml + ) if value == 35: with open(levels_txt_path, "r") as levels_txt: levels_txt_content = levels_txt.read() @@ -246,27 +274,43 @@ def update_levels(self, mods: dict[str, mobase.ModState]): for graphic in graphics_tag: insert_graphics_string += "\n\n" + graphic insert_graphics_string += "\n\n list[mobase.ExecutableForcedLoadSetting]: except AttributeError: efls = [] libs: set[str] = set() - tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = ( + self._organizer.virtualFileTree() + ) if type(tree) is not mobase.IFileTree: return efls for e in tree: @@ -301,14 +349,25 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: libs.add(relpath) exes = self.executables() - efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + efls = efls + [ + mobase.ExecutableForcedLoadSetting( + exe.binary().fileName(), lib + ).withEnabled(True) + for lib in libs + for exe in exes + ] return efls def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): modsPath = self.dataDirectory().absolutePath() profile_levels_path = directory.absolutePath() + "/" + self.ProfileLevelsXml - game_levels_path = os.path.join(self.dataDirectory().absolutePath(), self.GameLevelsXml) - if not os.path.exists(profile_levels_path) or os.path.getsize(profile_levels_path) == 0: + game_levels_path = os.path.join( + self.dataDirectory().absolutePath(), self.GameLevelsXml + ) + if ( + not os.path.exists(profile_levels_path) + or os.path.getsize(profile_levels_path) == 0 + ): with open(game_levels_path, "r") as game_levels: profile_levels_content = game_levels.read() game_levels.close() @@ -321,5 +380,10 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): def mappings(self) -> list[mobase.Mapping]: return [ - mobase.Mapping(self._organizer.profilePath() + "/" + self.ProfileLevelsXml, self.gameDirectory().absolutePath() + "/" + self.GameLevelsXml, False, False), + mobase.Mapping( + self._organizer.profilePath() + "/" + self.ProfileLevelsXml, + self.gameDirectory().absolutePath() + "/" + self.GameLevelsXml, + False, + False, + ), ] diff --git a/games/unreal_tabs/manage_paks/model.py b/games/unreal_tabs/manage_paks/model.py index a21ec9e1..eb4802b5 100644 --- a/games/unreal_tabs/manage_paks/model.py +++ b/games/unreal_tabs/manage_paks/model.py @@ -1,6 +1,6 @@ +from enum import IntEnum, auto import itertools import typing -from enum import IntEnum, auto from typing import Any, TypeAlias, overload import mobase diff --git a/games/unreal_tabs/manage_paks/widget.py b/games/unreal_tabs/manage_paks/widget.py index b84f99bb..c0788274 100644 --- a/games/unreal_tabs/manage_paks/widget.py +++ b/games/unreal_tabs/manage_paks/widget.py @@ -3,15 +3,13 @@ from typing import cast import mobase +from PyQt6.QtWidgets import QGridLayout, QWidget +from PyQt6.QtCore import QDir, QFileInfo from ....basic_features.utils import is_directory from .model import PaksModel from .view import PaksView - -from PyQt6.QtWidgets import QGridLayout, QWidget -from PyQt6.QtCore import QDir, QFileInfo, Qt - def pak_sort(a: tuple[str, str], b: tuple[str, str]) -> int: a_pak, a_str = a[0], a[1] or a[0] b_pak, b_str = b[0], b[1] or b[0] diff --git a/games/unreal_tabs/manage_ue4ss/model.py b/games/unreal_tabs/manage_ue4ss/model.py index 82e5f2e4..b3431353 100644 --- a/games/unreal_tabs/manage_ue4ss/model.py +++ b/games/unreal_tabs/manage_ue4ss/model.py @@ -2,12 +2,10 @@ from json import JSONDecodeError from typing import Any, Iterable +import mobase from PyQt6.QtCore import (QDir, QFileInfo, QMimeData, QModelIndex, QStringListModel, Qt) from PyQt6.QtWidgets import QWidget - -import mobase - from ..constants import DEFAULT_UE4SS_MODS class UE4SSListModel(QStringListModel): diff --git a/games/unreal_tabs/manage_ue4ss/widget.py b/games/unreal_tabs/manage_ue4ss/widget.py index d3732161..f4cbcce1 100644 --- a/games/unreal_tabs/manage_ue4ss/widget.py +++ b/games/unreal_tabs/manage_ue4ss/widget.py @@ -1,16 +1,16 @@ -import json from functools import cmp_to_key +import json from json import JSONDecodeError from pathlib import Path import mobase +from PyQt6.QtCore import QDir, QFileInfo, Qt +from PyQt6.QtWidgets import QGridLayout, QWidget from ..constants import DEFAULT_UE4SS_MODS, UE4SSModInfo from .model import UE4SSListModel from .view import UE4SSView -from PyQt6.QtWidgets import QGridLayout, QWidget -from PyQt6.QtCore import QDir, QFileInfo, Qt class UE4SSTabWidget(QWidget): def __init__(self, parent: QWidget | None, organizer: mobase.IOrganizer): @@ -80,7 +80,9 @@ def _parse_mod_files(self): if isinstance(ue4ss_files, mobase.IFileTree): for entry in ue4ss_files: if isinstance(entry, mobase.IFileTree): - if entry.find("scripts/main.lua") or entry.find("dlls/main.dll"): + if entry.find("scripts/main.lua") or entry.find( + "dlls/main.dll" + ): mod_list.add(entry.name()) if enabled_txt := entry.find("enabled.txt"): try: @@ -107,9 +109,7 @@ def _parse_mod_files(self): ).exists(): mod_list.add(dir_info.fileName()) if QFileInfo( - QDir(dir_info.absoluteFilePath()).absoluteFilePath( - "enabled.txt" - ) + QDir(dir_info.absoluteFilePath()).absoluteFilePath("enabled.txt") ).exists(): Path(dir_info.absoluteFilePath(), "enabled.txt").unlink() From f138917249d12468ae17d8da8bcd6de9fda3ed6d Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Sun, 1 Mar 2026 01:11:29 +0100 Subject: [PATCH 06/21] Fixed Missing Import --- games/game_titanfall2.py | 1 + 1 file changed, 1 insertion(+) diff --git a/games/game_titanfall2.py b/games/game_titanfall2.py index f7cc5f87..51ac39a0 100644 --- a/games/game_titanfall2.py +++ b/games/game_titanfall2.py @@ -1,5 +1,6 @@ from enum import IntEnum, auto from functools import cached_property +import json from pathlib import Path import os import shutil From 8c0e785481c2de6bfb267e20f6f1b3a0e70de3c7 Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Sun, 1 Mar 2026 01:31:52 +0100 Subject: [PATCH 07/21] Bugfix --- games/unreal_tabs/manage_paks/model.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/games/unreal_tabs/manage_paks/model.py b/games/unreal_tabs/manage_paks/model.py index eb4802b5..b8f72d2c 100644 --- a/games/unreal_tabs/manage_paks/model.py +++ b/games/unreal_tabs/manage_paks/model.py @@ -57,13 +57,13 @@ def flags(self, index: QModelIndex) -> Qt.ItemFlag: | Qt.ItemFlag.ItemIsDropEnabled & Qt.ItemFlag.ItemIsEditable ) - def columnCount(self, parent: QModelIndex) -> int: + def columnCount(self, parent: QModelIndex = None) -> int: if parent is None: parent = QModelIndex() return len(PaksColumns) def index( - self, row: int, column: int, parent: QModelIndex + self, row: int, column: int, parent: QModelIndex = None ) -> QModelIndex: if parent is None: parent = QModelIndex() @@ -86,7 +86,7 @@ def parent(self, child: QModelIndex | None = None) -> QModelIndex | QObject | No return super().parent() return QModelIndex() - def rowCount(self, parent: QModelIndex) -> int: + def rowCount(self, parent: QModelIndex = None) -> int: if parent is None: parent = QModelIndex() return len(self.paks) From 5b4aa6eb59a6f452a589279923dbae00de52db47 Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Thu, 5 Mar 2026 10:31:34 +0100 Subject: [PATCH 08/21] Added saves and mod config to casette beasts --- games/game_cassettebeasts.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/games/game_cassettebeasts.py b/games/game_cassettebeasts.py index 41401f56..50d04f49 100644 --- a/games/game_cassettebeasts.py +++ b/games/game_cassettebeasts.py @@ -60,6 +60,7 @@ class CassetteBeastsGame(BasicGame): GameBinary = "CassetteBeasts.exe" GameDataPath = appdataenv + "/CassetteBeasts/mods" GameDocumentsDirectory = appdataenv + "/CassetteBeasts" + GameSavesDirectory = '%GAME_DOCUMENTS%' GameSaveExtension = "gcpf" def init(self, organizer: mobase.IOrganizer) -> bool: @@ -103,7 +104,7 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: return efls def iniFiles(self): - return ["settings.cfg"] + return ["settings.cfg", "mod_settings.cfg"] def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): modsPath = self.dataDirectory().absolutePath() From 224c6a03c80b1bcb8d6656a450da879bccbf9a4f Mon Sep 17 00:00:00 2001 From: Tsuna Date: Fri, 6 Mar 2026 00:50:11 +0100 Subject: [PATCH 09/21] Added Savegame Data for Cassette Beasts Thanks PandoraFox for the code for unpacking gcpf that this is based on. --- games/game_cassettebeasts.py | 79 ++++++++++++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 3 deletions(-) diff --git a/games/game_cassettebeasts.py b/games/game_cassettebeasts.py index 50d04f49..eb1fe576 100644 --- a/games/game_cassettebeasts.py +++ b/games/game_cassettebeasts.py @@ -1,13 +1,22 @@ +from collections.abc import Mapping +from io import BytesIO +import json +import mobase +import math import os import shutil -import mobase +import struct +import sys +import zlib from pathlib import Path from functools import cached_property +from ..basic_features import BasicLocalSavegames +from ..basic_features.basic_save_game_info import (BasicGameSaveGame,BasicGameSaveGameInfo) from ..basic_game import BasicGame -from PyQt6.QtCore import QDir, QFileInfo +from PyQt6.QtCore import QDateTime, QDir, QFile, QFileInfo class CassetteBeastsModDataChecker(mobase.ModDataChecker): @@ -47,6 +56,60 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: return None return filetree +class CassetteBlock: + def __init__(self): + compressed_size = None + data = None + +class CassetteBeastsSaveGame(BasicGameSaveGame): + def __init__(self, filepath: Path): + super().__init__(filepath) + self.name: str = "" + self.cheated: str = "" + self.lastsave: int = 0 + self.elapsed: int = 0 + info = bytearray() + data = bytes() + + with open(filepath, 'rb') as infile: + magic_string = infile.read(4) + + compression_mode, blocksize, raw_size = struct.unpack("III", infile.read(12)) + + num_blocks = math.ceil(raw_size / blocksize) + + blocks = [] + + for bnum in range(num_blocks): + block = CassetteBlock() + block.compressed_size = struct.unpack("I", infile.read(4))[0] + blocks.append(block) + + for block in blocks: + block.data = infile.read(block.compressed_size) + + magic_string = infile.read(4) + infile.close() + + for block in blocks: + data = zlib.decompress(block.data, wbits=40, bufsize=blocksize) + info = info + data + + save_data = json.load(BytesIO(info)) + self.name = save_data["party"]["player"]["custom"]["name"] + self.cheated = save_data["has_cheated"] + + def getName(self) -> str: + return self.name + + def getCheated(self) -> str: + return self.cheated + +def getMetadata(p: Path, save: mobase.ISaveGame) -> Mapping[str, str]: + return { + "Character": save.getName(), + "Cheated": save.getCheated() + } class CassetteBeastsGame(BasicGame): appdataenv = os.getenv("APPDATA") @@ -60,13 +123,16 @@ class CassetteBeastsGame(BasicGame): GameBinary = "CassetteBeasts.exe" GameDataPath = appdataenv + "/CassetteBeasts/mods" GameDocumentsDirectory = appdataenv + "/CassetteBeasts" - GameSavesDirectory = '%GAME_DOCUMENTS%' GameSaveExtension = "gcpf" def init(self, organizer: mobase.IOrganizer) -> bool: super().init(organizer) self.dataChecker = CassetteBeastsModDataChecker(organizer) self._register_feature(self.dataChecker) + self._register_feature(BasicLocalSavegames(self)) + self._register_feature( + BasicGameSaveGameInfo(None, getMetadata) + ) return True def executables(self): @@ -81,6 +147,13 @@ def executables(self): ), ] + def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]: + ext = self._mappings.savegameExtension.get() + return [ + CassetteBeastsSaveGame(path) + for path in Path(folder.absolutePath()).glob(f"*.{ext}") + ] + @cached_property def _base_dlls(self) -> set[str]: base_dir = Path(self.gameDirectory().absolutePath()) From dd51459af82c8f23ebb6f9232775f36d113f2983 Mon Sep 17 00:00:00 2001 From: Tsuna Date: Fri, 6 Mar 2026 20:19:15 +0100 Subject: [PATCH 10/21] Added Save Error Handling, Additional Save Meta and Local Save Fix Authored by Cuteness with small edits by Floofytsuna --- games/game_cassettebeasts.py | 127 ++++++++++++++++++++++++----------- 1 file changed, 88 insertions(+), 39 deletions(-) diff --git a/games/game_cassettebeasts.py b/games/game_cassettebeasts.py index eb1fe576..9c4abfdf 100644 --- a/games/game_cassettebeasts.py +++ b/games/game_cassettebeasts.py @@ -1,16 +1,17 @@ -from collections.abc import Mapping -from io import BytesIO import json -import mobase import math import os import shutil import struct -import sys import zlib +import mobase -from pathlib import Path +from collections.abc import Mapping, Sequence +from datetime import datetime from functools import cached_property +from io import BytesIO +from typing import Any, Optional +from pathlib import Path from ..basic_features import BasicLocalSavegames from ..basic_features.basic_save_game_info import (BasicGameSaveGame,BasicGameSaveGameInfo) @@ -19,6 +20,13 @@ from PyQt6.QtCore import QDateTime, QDir, QFile, QFileInfo +def json_get_me(value: Any, path: Sequence[str | int], /, default: Any) -> Any: + for part in path: + if type(part) not in (str, int) or type(value) not in (dict, list): + return default + value = value[part] + return value + class CassetteBeastsModDataChecker(mobase.ModDataChecker): def __init__(self, organizer: mobase.IOrganizer): super().__init__() @@ -64,40 +72,71 @@ def __init__(self): class CassetteBeastsSaveGame(BasicGameSaveGame): def __init__(self, filepath: Path): super().__init__(filepath) - self.name: str = "" - self.cheated: str = "" - self.lastsave: int = 0 - self.elapsed: int = 0 - info = bytearray() - data = bytes() + self.name: str = "(unknown)" + self.cheated: str = "(unknown)" + self.lastsave: str = "(unknown)" + self.elapsed: str = "(unknown)" + # This doesn't state wether the game would load it, + # only if the data was properly parsed. + self.errorMessage: str = "" + + save_data = None + try: + info = bytearray() + data = bytes() + with open(filepath, 'rb') as infile: + magic_string = infile.read(4) - with open(filepath, 'rb') as infile: - magic_string = infile.read(4) + compression_mode, blocksize, raw_size = struct.unpack("III", infile.read(12)) - compression_mode, blocksize, raw_size = struct.unpack("III", infile.read(12)) + num_blocks = math.ceil(raw_size / blocksize) - num_blocks = math.ceil(raw_size / blocksize) + blocks = [] - blocks = [] + for bnum in range(num_blocks): + block = CassetteBlock() + block.compressed_size = struct.unpack("I", infile.read(4))[0] + blocks.append(block) - for bnum in range(num_blocks): - block = CassetteBlock() - block.compressed_size = struct.unpack("I", infile.read(4))[0] - blocks.append(block) + for block in blocks: + block.data = infile.read(block.compressed_size) + magic_string = infile.read(4) + infile.close() for block in blocks: - block.data = infile.read(block.compressed_size) - - magic_string = infile.read(4) - infile.close() - - for block in blocks: - data = zlib.decompress(block.data, wbits=40, bufsize=blocksize) - info = info + data - - save_data = json.load(BytesIO(info)) - self.name = save_data["party"]["player"]["custom"]["name"] - self.cheated = save_data["has_cheated"] + data = zlib.decompress(block.data, wbits=40, bufsize=blocksize) + info = info + data + save_data = json.load(BytesIO(info)) + except (OSError, struct.error, ValueError) as err: + s = str(err) + self.errorMessage = ('{0}: {1}' if s else '{0}').format( + err.__class__.__name__, s + ) + return + x = json_get_me(save_data, ["party", "player", "custom", "name"], None) + if type(x) is str: + self.name = x + x = json_get_me(save_data, ["saved_datetime"], None) + if type(x) in (int, float): + try: + dt = datetime.fromtimestamp(float(x)) + except OSError: + pass + else: + self.lastsave = "{0:d}-{1:02d}-{2:02d} at {3:02d}:{4:02d}:{5:02d}".format( + dt.year, dt.month, dt.day, + dt.hour, dt.minute, dt.second + ) + x = json_get_me(save_data, ["play_time"], None) + if type(x) in (int, float): + a = [ 0, 0, 0, int(x * 10) ] + a[2:4] = divmod(a[3], 10) + a[1:3] = divmod(a[2], 60) + a[0:2] = divmod(a[1], 60) + self.elapsed = "{0:02d}:{1:02d}:{2:02d}.{3:01d}".format(*a) + x = json_get_me(save_data, ["has_cheated"], None) + if type(x) is bool: + self.cheated = "Yes" if x else "No" def getName(self) -> str: return self.name @@ -105,15 +144,25 @@ def getName(self) -> str: def getCheated(self) -> str: return self.cheated + def getLastSaved(self) -> str: + return self.lastsave + + def getPlayTime(self) -> str: + return self.elapsed + def getMetadata(p: Path, save: mobase.ISaveGame) -> Mapping[str, str]: + if not save.errorMessage: + return { + "Character": save.getName(), + "Last Saved": save.getLastSaved(), + "Play Time": save.getPlayTime(), + "Cheated": save.getCheated() + } return { - "Character": save.getName(), - "Cheated": save.getCheated() + "Error loading file:": save.errorMessage } class CassetteBeastsGame(BasicGame): - appdataenv = os.getenv("APPDATA") - Name = "Cassette Beasts Support Plugin" Author = "modworkshop" Version = "1" @@ -121,15 +170,15 @@ class CassetteBeastsGame(BasicGame): GameShortName = "cassette-beasts" GameSteamId = 1321440 GameBinary = "CassetteBeasts.exe" - GameDataPath = appdataenv + "/CassetteBeasts/mods" - GameDocumentsDirectory = appdataenv + "/CassetteBeasts" + GameDataPath = os.getenv("APPDATA") + "/CassetteBeasts/mods" + GameDocumentsDirectory = os.getenv("APPDATA") + "/CassetteBeasts" GameSaveExtension = "gcpf" def init(self, organizer: mobase.IOrganizer) -> bool: super().init(organizer) self.dataChecker = CassetteBeastsModDataChecker(organizer) self._register_feature(self.dataChecker) - self._register_feature(BasicLocalSavegames(self)) + self._register_feature(BasicLocalSavegames(QDir(self.GameDocumentsDirectory))) self._register_feature( BasicGameSaveGameInfo(None, getMetadata) ) From 2af7ce10274f3d6d5ade10828da16ec7371e4cca Mon Sep 17 00:00:00 2001 From: Tsuna Date: Fri, 6 Mar 2026 20:34:05 +0100 Subject: [PATCH 11/21] Refactor game_cassettebeasts.py for clarity Refactor imports and variable initializations, and improve readability by renaming loop variables. --- games/game_cassettebeasts.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/games/game_cassettebeasts.py b/games/game_cassettebeasts.py index 9c4abfdf..752aa2d0 100644 --- a/games/game_cassettebeasts.py +++ b/games/game_cassettebeasts.py @@ -1,24 +1,23 @@ +from collections.abc import Mapping, Sequence +from datetime import datetime +from functools import cached_property +from io import BytesIO +from typing import Any, Optional +from pathlib import Path import json import math import os import shutil import struct import zlib -import mobase -from collections.abc import Mapping, Sequence -from datetime import datetime -from functools import cached_property -from io import BytesIO -from typing import Any, Optional -from pathlib import Path +import mobase +from PyQt6.QtCore import QDateTime, QDir, QFile, QFileInfo from ..basic_features import BasicLocalSavegames from ..basic_features.basic_save_game_info import (BasicGameSaveGame,BasicGameSaveGameInfo) from ..basic_game import BasicGame -from PyQt6.QtCore import QDateTime, QDir, QFile, QFileInfo - def json_get_me(value: Any, path: Sequence[str | int], /, default: Any) -> Any: for part in path: @@ -66,8 +65,8 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: class CassetteBlock: def __init__(self): - compressed_size = None - data = None + compressed_size: str = "(unknown)" + data: str = "(unknown)" class CassetteBeastsSaveGame(BasicGameSaveGame): def __init__(self, filepath: Path): @@ -85,7 +84,7 @@ def __init__(self, filepath: Path): info = bytearray() data = bytes() with open(filepath, 'rb') as infile: - magic_string = infile.read(4) + infile.read(4) compression_mode, blocksize, raw_size = struct.unpack("III", infile.read(12)) @@ -93,7 +92,7 @@ def __init__(self, filepath: Path): blocks = [] - for bnum in range(num_blocks): + for _bnum in range(num_blocks): block = CassetteBlock() block.compressed_size = struct.unpack("I", infile.read(4))[0] blocks.append(block) @@ -101,7 +100,7 @@ def __init__(self, filepath: Path): for block in blocks: block.data = infile.read(block.compressed_size) - magic_string = infile.read(4) + infile.read(4) infile.close() for block in blocks: data = zlib.decompress(block.data, wbits=40, bufsize=blocksize) From 9012030002b4f6f78d9d28477b063974bd220bce Mon Sep 17 00:00:00 2001 From: Tsuna Date: Fri, 6 Mar 2026 20:37:34 +0100 Subject: [PATCH 12/21] Update CassetteBlock attributes to correct types Change attributes in CassetteBlock class to use appropriate types. --- games/game_cassettebeasts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/games/game_cassettebeasts.py b/games/game_cassettebeasts.py index 752aa2d0..de218205 100644 --- a/games/game_cassettebeasts.py +++ b/games/game_cassettebeasts.py @@ -65,8 +65,8 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: class CassetteBlock: def __init__(self): - compressed_size: str = "(unknown)" - data: str = "(unknown)" + self.compressed_size: int = 0 + self.data: bytes = b'' class CassetteBeastsSaveGame(BasicGameSaveGame): def __init__(self, filepath: Path): From db6fdcd1a90176cc93693a901d5641a556e35e7a Mon Sep 17 00:00:00 2001 From: Tsuna Date: Sat, 7 Mar 2026 20:11:46 +0100 Subject: [PATCH 13/21] Fix registration of BasicLocalSavegames feature its changed to be in line with other basic games, while there likely is a bug involving that line its better addressed in basic games itself. --- games/game_cassettebeasts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/games/game_cassettebeasts.py b/games/game_cassettebeasts.py index de218205..bd2326b7 100644 --- a/games/game_cassettebeasts.py +++ b/games/game_cassettebeasts.py @@ -177,7 +177,7 @@ def init(self, organizer: mobase.IOrganizer) -> bool: super().init(organizer) self.dataChecker = CassetteBeastsModDataChecker(organizer) self._register_feature(self.dataChecker) - self._register_feature(BasicLocalSavegames(QDir(self.GameDocumentsDirectory))) + self._register_feature(BasicLocalSavegames(self)) self._register_feature( BasicGameSaveGameInfo(None, getMetadata) ) From 81219cfb8d2e05c7015e03a4ecc4de7bd7bec575 Mon Sep 17 00:00:00 2001 From: Tsuna Date: Sat, 14 Mar 2026 19:30:45 +0100 Subject: [PATCH 14/21] Add GameSavesDirectory path for CassetteBeasts --- games/game_cassettebeasts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/games/game_cassettebeasts.py b/games/game_cassettebeasts.py index bd2326b7..90fc1847 100644 --- a/games/game_cassettebeasts.py +++ b/games/game_cassettebeasts.py @@ -171,6 +171,7 @@ class CassetteBeastsGame(BasicGame): GameBinary = "CassetteBeasts.exe" GameDataPath = os.getenv("APPDATA") + "/CassetteBeasts/mods" GameDocumentsDirectory = os.getenv("APPDATA") + "/CassetteBeasts" + GameSavesDirectory = os.getenv("APPDATA") + "/CassetteBeasts" GameSaveExtension = "gcpf" def init(self, organizer: mobase.IOrganizer) -> bool: From 18923257f8d9b8449f8fba5e866a768d597d72ba Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Mon, 16 Mar 2026 20:27:31 +0100 Subject: [PATCH 15/21] updated env variables and fix cassette beasts again --- games/game_cassettebeasts.py | 140 ++++++++++++++++++++++++++++++++--- games/game_ovkwalkingdead.py | 2 +- games/game_pacificdrive.py | 2 +- games/game_payday1.py | 2 +- games/game_payday3.py | 2 +- games/game_roadtovostok.py | 4 +- games/game_titanfall2.py | 4 +- 7 files changed, 140 insertions(+), 16 deletions(-) diff --git a/games/game_cassettebeasts.py b/games/game_cassettebeasts.py index 50d04f49..8f1d0790 100644 --- a/games/game_cassettebeasts.py +++ b/games/game_cassettebeasts.py @@ -1,14 +1,30 @@ +from collections.abc import Mapping, Sequence +from datetime import datetime +from functools import cached_property +from io import BytesIO +from typing import Any, Optional +from pathlib import Path +import json +import math import os import shutil -import mobase +import struct +import zlib -from pathlib import Path -from functools import cached_property +import mobase +from PyQt6.QtCore import QDateTime, QDir, QFile, QFileInfo +from ..basic_features import BasicLocalSavegames +from ..basic_features.basic_save_game_info import (BasicGameSaveGame,BasicGameSaveGameInfo) from ..basic_game import BasicGame -from PyQt6.QtCore import QDir, QFileInfo +def json_get_me(value: Any, path: Sequence[str | int], /, default: Any) -> Any: + for part in path: + if type(part) not in (str, int) or type(value) not in (dict, list): + return default + value = value[part] + return value class CassetteBeastsModDataChecker(mobase.ModDataChecker): def __init__(self, organizer: mobase.IOrganizer): @@ -47,10 +63,105 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: return None return filetree +class CassetteBlock: + def __init__(self): + compressed_size: str = "(unknown)" + data: str = "(unknown)" + +class CassetteBeastsSaveGame(BasicGameSaveGame): + def __init__(self, filepath: Path): + super().__init__(filepath) + self.name: str = "(unknown)" + self.cheated: str = "(unknown)" + self.lastsave: str = "(unknown)" + self.elapsed: str = "(unknown)" + # This doesn't state wether the game would load it, + # only if the data was properly parsed. + self.errorMessage: str = "" + + save_data = None + try: + info = bytearray() + data = bytes() + with open(filepath, 'rb') as infile: + infile.read(4) + + compression_mode, blocksize, raw_size = struct.unpack("III", infile.read(12)) + + num_blocks = math.ceil(raw_size / blocksize) + + blocks = [] + + for _bnum in range(num_blocks): + block = CassetteBlock() + block.compressed_size = struct.unpack("I", infile.read(4))[0] + blocks.append(block) + + for block in blocks: + block.data = infile.read(block.compressed_size) + + infile.read(4) + infile.close() + for block in blocks: + data = zlib.decompress(block.data, wbits=40, bufsize=blocksize) + info = info + data + save_data = json.load(BytesIO(info)) + except (OSError, struct.error, ValueError) as err: + s = str(err) + self.errorMessage = ('{0}: {1}' if s else '{0}').format( + err.__class__.__name__, s + ) + return + x = json_get_me(save_data, ["party", "player", "custom", "name"], None) + if type(x) is str: + self.name = x + x = json_get_me(save_data, ["saved_datetime"], None) + if type(x) in (int, float): + try: + dt = datetime.fromtimestamp(float(x)) + except OSError: + pass + else: + self.lastsave = "{0:d}-{1:02d}-{2:02d} at {3:02d}:{4:02d}:{5:02d}".format( + dt.year, dt.month, dt.day, + dt.hour, dt.minute, dt.second + ) + x = json_get_me(save_data, ["play_time"], None) + if type(x) in (int, float): + a = [ 0, 0, 0, int(x * 10) ] + a[2:4] = divmod(a[3], 10) + a[1:3] = divmod(a[2], 60) + a[0:2] = divmod(a[1], 60) + self.elapsed = "{0:02d}:{1:02d}:{2:02d}.{3:01d}".format(*a) + x = json_get_me(save_data, ["has_cheated"], None) + if type(x) is bool: + self.cheated = "Yes" if x else "No" + + def getName(self) -> str: + return self.name + + def getCheated(self) -> str: + return self.cheated + + def getLastSaved(self) -> str: + return self.lastsave + + def getPlayTime(self) -> str: + return self.elapsed + +def getMetadata(p: Path, save: mobase.ISaveGame) -> Mapping[str, str]: + if not save.errorMessage: + return { + "Character": save.getName(), + "Last Saved": save.getLastSaved(), + "Play Time": save.getPlayTime(), + "Cheated": save.getCheated() + } + return { + "Error loading file:": save.errorMessage + } class CassetteBeastsGame(BasicGame): - appdataenv = os.getenv("APPDATA") - Name = "Cassette Beasts Support Plugin" Author = "modworkshop" Version = "1" @@ -58,15 +169,19 @@ class CassetteBeastsGame(BasicGame): GameShortName = "cassette-beasts" GameSteamId = 1321440 GameBinary = "CassetteBeasts.exe" - GameDataPath = appdataenv + "/CassetteBeasts/mods" - GameDocumentsDirectory = appdataenv + "/CassetteBeasts" - GameSavesDirectory = '%GAME_DOCUMENTS%' + GameDataPath = "%USERPROFILE%/AppData/Roaming/CassetteBeasts/mods" + GameDocumentsDirectory = "%USERPROFILE%/AppData/Roaming/CassetteBeasts" + GameSavesDirectory = "%GAME_DOCUMENTS%" GameSaveExtension = "gcpf" def init(self, organizer: mobase.IOrganizer) -> bool: super().init(organizer) self.dataChecker = CassetteBeastsModDataChecker(organizer) self._register_feature(self.dataChecker) + self._register_feature(BasicLocalSavegames(self)) + self._register_feature( + BasicGameSaveGameInfo(None, getMetadata) + ) return True def executables(self): @@ -81,6 +196,13 @@ def executables(self): ), ] + def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]: + ext = self._mappings.savegameExtension.get() + return [ + CassetteBeastsSaveGame(path) + for path in Path(folder.absolutePath()).glob(f"*.{ext}") + ] + @cached_property def _base_dlls(self) -> set[str]: base_dir = Path(self.gameDirectory().absolutePath()) diff --git a/games/game_ovkwalkingdead.py b/games/game_ovkwalkingdead.py index 8437e4c5..7b2314eb 100644 --- a/games/game_ovkwalkingdead.py +++ b/games/game_ovkwalkingdead.py @@ -179,7 +179,7 @@ class OTWDGame(BasicGame): GameDataUE4SSMods = "Binaries/Win64/Mods" GameDataPakMods = "Content/Paks/~Mods" GameDataMovieMods = "Content/Movies" - GameDocumentsDirectory = "%LOCALAPPDATA%/OTWD/Saved/Config/WindowsClient" + GameDocumentsDirectory = "%USERPROFILE%/AppData/Local/OTWD/Saved/Config/WindowsClient" GameSaveExtension = "sav" _main_window: QMainWindow _ue4ss_tab: UE4SSTabWidget diff --git a/games/game_pacificdrive.py b/games/game_pacificdrive.py index 410fc17d..72ea8fff 100644 --- a/games/game_pacificdrive.py +++ b/games/game_pacificdrive.py @@ -180,7 +180,7 @@ class PacificDriveGame(BasicGame): GameDataUE4SSMods = "Binaries/Win64/Mods" GameDataPakMods = "Content/Paks/~Mods" GameDataMovieMods = "Content/Movies" - GameDocumentsDirectory = "%LOCALAPPDATA%/PenDriverPro/Saved/Config/WindowsNoEditor" + GameDocumentsDirectory = "%USERPROFILE%/AppData/Local/PenDriverPro/Saved/Config/WindowsNoEditor" GameSaveExtension = "sav" _main_window: QMainWindow _ue4ss_tab: UE4SSTabWidget diff --git a/games/game_payday1.py b/games/game_payday1.py index 7fee5286..8b20dd05 100644 --- a/games/game_payday1.py +++ b/games/game_payday1.py @@ -199,7 +199,7 @@ class Payday1Game(BasicGame): GameSteamId = 24240 GameBinary = "payday_win32_release.exe" GameDataPath = "%GAME_PATH%" - GameDocumentsDirectory = "%LOCALAPPDATA%/PAYDAY" + GameDocumentsDirectory = "%USERPROFILE%/AppData/Local/PAYDAY" _forced_libraries = ["IPHLPAPI.dll", "WSOCK32.dll"] def init(self, organizer: mobase.IOrganizer) -> bool: diff --git a/games/game_payday3.py b/games/game_payday3.py index 7410d5c9..170fb7c5 100644 --- a/games/game_payday3.py +++ b/games/game_payday3.py @@ -212,7 +212,7 @@ class Payday3Game(BasicGame): GameDataUE4SSMods = "Binaries/Win64/Mods" GameDataPakMods = "Content/Paks/~Mods" GameDataMovieMods = "Content/Movies" - GameDocumentsDirectory = "%LOCALAPPDATA%/PAYDAY3/Saved/Config/WindowsClient" + GameDocumentsDirectory = "%USERPROFILE%/AppData/Local/PAYDAY3/Saved/Config/WindowsClient" GameSaveExtension = "sav" _main_window: QMainWindow _ue4ss_tab: UE4SSTabWidget diff --git a/games/game_roadtovostok.py b/games/game_roadtovostok.py index 871e84a7..e5692fff 100644 --- a/games/game_roadtovostok.py +++ b/games/game_roadtovostok.py @@ -42,7 +42,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: class RoadToVostokGame(BasicGame): Name = "Road to Vostok Support Plugin" - Author = "modworkshop" + Author = "modworkshop" Version = "1" GameName = "Road to Vostok" GameShortName = "road-to-vostok" @@ -50,7 +50,7 @@ class RoadToVostokGame(BasicGame): GameBinary = "Road_to_Vostok_Demo.exe" GameDataPath = "%GAME_PATH%" GameModsPath = "mods" - GameDocumentsDirectory = "%APPDATA%/Godot/app_userdata/Road to Vostok" + GameDocumentsDirectory = "%USERPROFILE%/AppData/Local/Godot/app_userdata/Road to Vostok" GameSaveExtension = "tres" def init(self, organizer: mobase.IOrganizer) -> bool: diff --git a/games/game_titanfall2.py b/games/game_titanfall2.py index 51ac39a0..759cd55a 100644 --- a/games/game_titanfall2.py +++ b/games/game_titanfall2.py @@ -102,7 +102,9 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): ): path = mod.absolutePath() json_path = os.path.join(path, northstarModPath + "FOLDERNAME/mod.json") - mod_data = json.load(open(json_path, encoding="utf-8")) + with open(json_path, "r") as json_data: + mod_data = json.load(json_data) + json_data.close() modname = mod_data["name"] old_path = os.path.join(path, northstarModPath + "FOLDERNAME") new_path = os.path.join(path, northstarModPath + f"{modname}") From d84b34cdfdfbeccdf094b0cb7e7e0269b5ea8274 Mon Sep 17 00:00:00 2001 From: Tsuna Date: Thu, 19 Mar 2026 13:25:22 +0100 Subject: [PATCH 16/21] Fix GameSavesDirectory usage in feature registration --- games/game_cassettebeasts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/games/game_cassettebeasts.py b/games/game_cassettebeasts.py index 8f1d0790..fc1d29ea 100644 --- a/games/game_cassettebeasts.py +++ b/games/game_cassettebeasts.py @@ -178,7 +178,7 @@ def init(self, organizer: mobase.IOrganizer) -> bool: super().init(organizer) self.dataChecker = CassetteBeastsModDataChecker(organizer) self._register_feature(self.dataChecker) - self._register_feature(BasicLocalSavegames(self)) + self._register_feature(BasicLocalSavegames(QDir(self.GameSavesDirectory))) self._register_feature( BasicGameSaveGameInfo(None, getMetadata) ) From 0c3a8990e7ffcb3e055edaa702144a03f8e5f701 Mon Sep 17 00:00:00 2001 From: Tsuna Date: Wed, 8 Apr 2026 23:49:21 +0200 Subject: [PATCH 17/21] Updated EXE Name for Game EA Release --- games/game_roadtovostok.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/games/game_roadtovostok.py b/games/game_roadtovostok.py index e5692fff..f8b84ccd 100644 --- a/games/game_roadtovostok.py +++ b/games/game_roadtovostok.py @@ -47,7 +47,7 @@ class RoadToVostokGame(BasicGame): GameName = "Road to Vostok" GameShortName = "road-to-vostok" GameSteamId = 1963610 - GameBinary = "Road_to_Vostok_Demo.exe" + GameBinary = "RTV.exe" GameDataPath = "%GAME_PATH%" GameModsPath = "mods" GameDocumentsDirectory = "%USERPROFILE%/AppData/Local/Godot/app_userdata/Road to Vostok" From f6c4bf50a0042bbd516df66143da5cc0112154c5 Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Tue, 14 Apr 2026 00:11:17 +0200 Subject: [PATCH 18/21] Clean Code Update --- games/game_cassettebeasts.py | 54 ++++---- games/game_crimeboss.py | 87 ++++++------ games/game_emuvr.py | 25 ++-- games/game_hitman3.py | 55 +++++--- games/game_noita.py | 23 ++-- games/game_oblivion_remaster.py | 2 +- games/game_ovkwalkingdead.py | 112 ++++++++-------- games/game_pacificdrive.py | 112 ++++++++-------- games/game_payday1.py | 160 +++++++++++++---------- games/game_payday2.py | 154 +++++++++++----------- games/game_payday3.py | 141 ++++++++------------ games/game_raid2.py | 57 ++++---- games/game_roadtovostok.py | 8 +- games/game_silenthill2remake.py | 112 ++++++++-------- games/game_titanfall2.py | 114 ++++++++-------- games/game_zuma_deluxe.py | 29 ++-- games/oblivion_remaster/paks/view.py | 2 +- games/oblivion_remaster/ue4ss/view.py | 2 +- games/oblivion_remaster/ue4ss/widget.py | 4 +- games/unreal_tabs/manage_paks/model.py | 8 +- games/unreal_tabs/manage_paks/view.py | 2 +- games/unreal_tabs/manage_paks/widget.py | 86 ++++++------ games/unreal_tabs/manage_ue4ss/model.py | 6 +- games/unreal_tabs/manage_ue4ss/view.py | 2 +- games/unreal_tabs/manage_ue4ss/widget.py | 33 +++-- 25 files changed, 687 insertions(+), 703 deletions(-) diff --git a/games/game_cassettebeasts.py b/games/game_cassettebeasts.py index fc1d29ea..5f965c54 100644 --- a/games/game_cassettebeasts.py +++ b/games/game_cassettebeasts.py @@ -2,7 +2,7 @@ from datetime import datetime from functools import cached_property from io import BytesIO -from typing import Any, Optional +from typing import Any from pathlib import Path import json import math @@ -12,7 +12,7 @@ import zlib import mobase -from PyQt6.QtCore import QDateTime, QDir, QFile, QFileInfo +from PyQt6.QtCore import QDir, QFileInfo from ..basic_features import BasicLocalSavegames from ..basic_features.basic_save_game_info import (BasicGameSaveGame,BasicGameSaveGameInfo) @@ -33,30 +33,29 @@ def __init__(self, organizer: mobase.IOrganizer): def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: for e in filetree: - if e is not None and e.isFile() and e.suffix().casefold() == "pck": + if e.suffix().casefold() == "pck": return mobase.ModDataChecker.VALID return mobase.ModDataChecker.FIXABLE - def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: - GameDataPath = self.organizer.managedGame().GameDataPath + "/" + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + GameDataPath = getattr(self.organizer.managedGame(), "GameDataPath", "") + "/" treefixed = 0 for branch in filetree: mod_name = filetree.name() if mod_name == "": mod_name = branch.name() mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if filetree.createOrphanTree("OrphanTree") is None and os.path.exists(mod_path) and branch.suffix().casefold() == "pck": + if not filetree.createOrphanTree("OrphanTree") and os.path.exists(mod_path) and branch.suffix().casefold() == "pck": os.makedirs(os.path.join(mod_path, GameDataPath), exist_ok=True) shutil.move(os.path.join(mod_path, branch.name()), os.path.join(mod_path, GameDataPath, branch.name())) treefixed = 1 else: - if branch is not None: - if branch.isDir(): - for e in branch: - if e is not None and e.isFile() and e.suffix().casefold() == "pck": - filetree.move(e, GameDataPath, mobase.IFileTree.MERGE) - treefixed = 1 - elif branch.suffix().casefold() == "pck": + if isinstance(branch, mobase.IFileTree): + for e in branch: + if e.suffix().casefold() == "pck": + filetree.move(e, GameDataPath, mobase.IFileTree.MERGE) + treefixed = 1 + elif branch.suffix().casefold() == "pck": filetree.move(branch, GameDataPath, mobase.IFileTree.MERGE) treefixed = 1 if treefixed == 0: @@ -64,9 +63,8 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: return filetree class CassetteBlock: - def __init__(self): - compressed_size: str = "(unknown)" - data: str = "(unknown)" + compressed_size: int = 0 + data: bytes = b'' class CassetteBeastsSaveGame(BasicGameSaveGame): def __init__(self, filepath: Path): @@ -86,11 +84,11 @@ def __init__(self, filepath: Path): with open(filepath, 'rb') as infile: infile.read(4) - compression_mode, blocksize, raw_size = struct.unpack("III", infile.read(12)) + blocksize, raw_size = struct.unpack("III", infile.read(12)) num_blocks = math.ceil(raw_size / blocksize) - blocks = [] + blocks: list[CassetteBlock] = [] for _bnum in range(num_blocks): block = CassetteBlock() @@ -149,17 +147,23 @@ def getLastSaved(self) -> str: def getPlayTime(self) -> str: return self.elapsed -def getMetadata(p: Path, save: mobase.ISaveGame) -> Mapping[str, str]: - if not save.errorMessage: - return { +def getMetadata(p: Path, save: mobase.ISaveGame) -> Mapping[str, str] | None: + err = getattr(save, "errorMessage", "") + if err: + return {"Error loading file:": err} + + # If this is our concrete save-game class, the type checker knows the methods. + if isinstance(save, BasicGameSaveGame): + return + { "Character": save.getName(), "Last Saved": save.getLastSaved(), "Play Time": save.getPlayTime(), "Cheated": save.getCheated() } - return { - "Error loading file:": save.errorMessage - } + else: + return None + class CassetteBeastsGame(BasicGame): Name = "Cassette Beasts Support Plugin" @@ -178,7 +182,7 @@ def init(self, organizer: mobase.IOrganizer) -> bool: super().init(organizer) self.dataChecker = CassetteBeastsModDataChecker(organizer) self._register_feature(self.dataChecker) - self._register_feature(BasicLocalSavegames(QDir(self.GameSavesDirectory))) + self._register_feature(BasicLocalSavegames(QDir(self.GameSavesDirectory))) # type: ignore self._register_feature( BasicGameSaveGameInfo(None, getMetadata) ) diff --git a/games/game_crimeboss.py b/games/game_crimeboss.py index bf88013a..d030bb22 100644 --- a/games/game_crimeboss.py +++ b/games/game_crimeboss.py @@ -27,7 +27,7 @@ class Content(IntEnum): class CrimeBossModDataContent(mobase.ModDataContent): - contents: list[int] = [] + content: list[int] = [] GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ (Content.UCAS, "UCAS", ":/MO/gui/content/geometries"), (Content.UTOC, "UTOC", ":/MO/gui/content/inifile"), @@ -44,15 +44,15 @@ def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): match entry.suffix().casefold(): case "utoc": - self.contents.add(Content.UTOC) + self.content.append(Content.UTOC) case "ucas": - self.contents.add(Content.UCAS) + self.content.append(Content.UCAS) case "pak": - self.contents.add(Content.PAK) + self.content.append(Content.PAK) case "lua": - self.contents.add(Content.UE4SS) + self.content.append(Content.UE4SS) case "dll": - self.contents.add(Content.DLL) + self.content.append(Content.DLL) case "bk2": self.contents.add(Content.BK2) case _: @@ -72,7 +72,7 @@ def __init__(self, organizer: mobase.IOrganizer): self.organizer.modList().onModInstalled(self._Fix_Installed_Mod) self.needsNameFix = False - def move_overwrite_merge(self, source, destination): + def move_overwrite_merge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -88,11 +88,11 @@ def move_overwrite_merge(self, source, destination): def _Fix_Installed_Mod(self, mod: mobase.IModInterface): if not self.needsNameFix: return - GameDataNativeMods = self.organizer.managedGame().GameDataNativeMods + GameDataNativeMods = getattr(self.organizer.managedGame(), "GameDataNativeMods", "") filetree: mobase.IFileTree = mod.fileTree() fixed = False modname = mod.name() - if filetree is not None and filetree.exists(GameDataNativeMods + "/FOLDERNAME", mobase.IFileTree.DIRECTORY): + if filetree.exists(GameDataNativeMods + "/FOLDERNAME", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() old_path = os.path.join(path, GameDataNativeMods + "/FOLDERNAME") new_path = os.path.join(path, GameDataNativeMods + f"/{modname}") @@ -103,22 +103,25 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): self.needsNameFix = False def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: - GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods - GameDataPakMods = self.organizer.managedGame().GameDataPakMods - GameDataNativeMods = self.organizer.managedGame().GameDataNativeMods + GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") + GameDataNativeMods = getattr(self.organizer.managedGame(), "GameDataNativeMods", "") + GameDataMovies = getattr(self.organizer.managedGame(), "GameDataMovies", "") if filetree.exists(GameDataPakMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID if filetree.exists(os.path.dirname(GameDataUE4SSMods), mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID if filetree.exists(GameDataNativeMods, mobase.IFileTree.DIRECTORY) and not filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): return mobase.ModDataChecker.VALID + if filetree.exists(GameDataMovies, mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID return mobase.ModDataChecker.FIXABLE def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): for branch in filetree: - if branch is not None and branch.isDir(): + if isinstance(branch, mobase.IFileTree): for e in branch: - if e is not None and e.name() == name: + if e.name() == name: return True return False @@ -126,18 +129,17 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): entriesToMove: list[mobase.FileTreeEntry] = [] retVal = 0 for e in filetree: - if e is not None: - entriesToMove.append(e) + entriesToMove.append(e) for e in entriesToMove: filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) retVal = 1 return retVal - def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: - GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods + "/" - GameDataPakMods = self.organizer.managedGame().GameDataPakMods + "/" - GameDataNativeMods = self.organizer.managedGame().GameDataNativeMods + "/" - GameDataMovies = self.organizer.managedGame().GameDataMovies + "/" + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + "/" + GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") + "/" + GameDataNativeMods = getattr(self.organizer.managedGame(), "GameDataNativeMods", "") + "/" + GameDataMovies = getattr(self.organizer.managedGame(), "GameDataMovies", "") + "/" treefixed = 0 if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): treefixed = self.allMoveTo(filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/") @@ -154,28 +156,27 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: allowedUnzippedExt = ["pak", "utoc", "ucas", "bk2", "dll"] entriesToMove: list[mobase.FileTreeEntry] = [] for e in filetree: - if e is not None: - if e.isFile(): - fileext = e.suffix().casefold() - if fileext in allowedUnzippedExt: - mod_name = filetree.name() - if mod_name == "": - mod_name = e.name() - mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if filetree.createOrphanTree("OrphanTree") is None and os.path.exists(mod_path): - match e.suffix().casefold(): - case "pak" | "utoc" | "ucas": - os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) - case "bk2": - os.makedirs(os.path.join(mod_path, GameDataMovies), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovies, e.name())) - case _: - pass - treefixed = 1 - else: - entriesToMove.append(e) - if entriesToMove is not None: + if e.isFile(): + fileext = e.suffix().casefold() + if fileext in allowedUnzippedExt: + mod_name = filetree.name() + if mod_name == "": + mod_name = e.name() + mod_path = os.path.join(self.organizer.modsPath(), mod_name) + if not filetree.createOrphanTree("OrphanTree") and os.path.exists(mod_path): + match e.suffix().casefold(): + case "pak" | "utoc" | "ucas": + os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) + case "bk2": + os.makedirs(os.path.join(mod_path, GameDataMovies), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovies, e.name())) + case _: + pass + treefixed = 1 + else: + entriesToMove.append(e) + if entriesToMove: for e in entriesToMove: match e.suffix().casefold(): case "pak" | "utoc" | "ucas": diff --git a/games/game_emuvr.py b/games/game_emuvr.py index 1b4737b2..744c4b78 100644 --- a/games/game_emuvr.py +++ b/games/game_emuvr.py @@ -16,33 +16,32 @@ def __init__(self, organizer: mobase.IOrganizer): self.organizer: mobase.IOrganizer = organizer def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: - GameDataUGCMods = self.organizer.managedGame().GameDataUGCMods + GameDataUGCMods = getattr(self.organizer.managedGame(), "GameDataUGCMods", "") if filetree.exists(GameDataUGCMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID return mobase.ModDataChecker.FIXABLE - def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: - GameDataUGCMods = self.organizer.managedGame().GameDataUGCMods + "/" + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + GameDataUGCMods = getattr(self.organizer.managedGame(), "GameDataUGCMods", "") + "/" treefixed = 0 for branch in filetree: mod_name = filetree.name() if mod_name == "": mod_name = branch.name() mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if filetree.createOrphanTree("OrphanTree") is None and os.path.exists(mod_path) and branch.suffix().casefold() == "ugc": + if not filetree.createOrphanTree("OrphanTree") and os.path.exists(mod_path) and branch.suffix().casefold() == "ugc": os.makedirs(os.path.join(mod_path, GameDataUGCMods), exist_ok=True) shutil.move(os.path.join(mod_path, branch.name()), os.path.join(mod_path, GameDataUGCMods, branch.name())) treefixed = 1 else: - if branch is not None: - if branch.isDir(): - for e in branch: - if e is not None and e.isFile() and e.suffix().casefold() == "ugc": - filetree.move(e, GameDataUGCMods, mobase.IFileTree.MERGE) - treefixed = 1 - elif branch.suffix().casefold() == "ugc": - filetree.move(branch, GameDataUGCMods, mobase.IFileTree.MERGE) - treefixed = 1 + if isinstance(branch, mobase.IFileTree): + for e in branch: + if e.isFile() and e.suffix().casefold() == "ugc": + filetree.move(e, GameDataUGCMods, mobase.IFileTree.MERGE) + treefixed = 1 + elif branch.suffix().casefold() == "ugc": + filetree.move(branch, GameDataUGCMods, mobase.IFileTree.MERGE) + treefixed = 1 if treefixed == 0: return None return filetree diff --git a/games/game_hitman3.py b/games/game_hitman3.py index 3b3ad92c..996ab66a 100644 --- a/games/game_hitman3.py +++ b/games/game_hitman3.py @@ -18,7 +18,7 @@ def __init__(self, organizer: mobase.IOrganizer): self.organizer.modList().onModInstalled(self._Fix_Installed_Mod) self.needsNameFix = False - def move_overwrite_merge(self, source, destination): + def move_overwrite_merge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -34,12 +34,15 @@ def move_overwrite_merge(self, source, destination): def _Fix_Installed_Mod(self, mod: mobase.IModInterface): if not self.needsNameFix: return - GameSMMPath = self.organizer.managedGame().GameSMMPath + GameSMMPath = getattr(self.organizer.managedGame(), "GameSMMPath", "") filetree: mobase.IFileTree = mod.fileTree() fixed = False - if filetree is not None and filetree.exists(GameSMMPath + "/Mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): + if filetree.exists(GameSMMPath + "/Mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): + print("Found folder") path = mod.absolutePath() + print(path) json_path = os.path.join(path, GameSMMPath + "/Mods/FOLDERNAME/manifest.json") + print(json_path) mod_data = json.load(open(json_path, encoding="utf-8")) modname = mod_data["id"] old_path = os.path.join(path, GameSMMPath + "/Mods/FOLDERNAME") @@ -57,36 +60,46 @@ def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.Ch def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): for branch in filetree: - if branch is not None and branch.isDir(): + if isinstance(branch, mobase.IFileTree): for e in branch: - if e is not None and e.name() == name: + if e.name() == name: return True return False - def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): + def allMoveTo(self, sourcetree: mobase.IFileTree, targettree: mobase.IFileTree, toMoveTo: str): entriesToMove: list[mobase.FileTreeEntry] = [] retVal = 0 - for e in filetree: - if e is not None: - entriesToMove.append(e) + for e in sourcetree: + entriesToMove.append(e) for e in entriesToMove: - filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) + targettree.move(e, toMoveTo, mobase.IFileTree.MERGE) retVal = 1 + targettree.remove(sourcetree) return retVal - def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: - GameSMMPath = self.organizer.managedGame().GameSMMPath + def first_tree(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + for e in filetree: + if isinstance(e, mobase.IFileTree) and e.isDir(): + return e + return None + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + GameSMMPath = getattr(self.organizer.managedGame(), "GameSMMPath", "") treefixed = 0 if filetree.exists("manifest.json", mobase.IFileTree.FILE): - treefixed = self.allMoveTo(filetree, GameSMMPath + "/Mods/FOLDERNAME/") + print("Found manifest in root, moving to SMM folder") + print(GameSMMPath + "/Mods/FOLDERNAME/") + treefixed = self.allMoveTo(filetree, filetree, GameSMMPath + "/Mods/FOLDERNAME/") if treefixed == 1: self.needsNameFix = True - if treefixed == 0: - if len(filetree) == 1: - filetree = filetree.find(filetree[0].path("/")) - treefixed = self.allMoveTo(filetree, GameSMMPath + "/Mods/FOLDERNAME/") - if treefixed == 1: - self.needsNameFix = True + elif len(filetree) == 1: + firsttreelayer: mobase.IFileTree | None = self.first_tree(filetree) + if firsttreelayer is not None: + if firsttreelayer.exists("manifest.json", mobase.IFileTree.FILE): + print(GameSMMPath + "/Mods/FOLDERNAME/") + treefixed = self.allMoveTo(firsttreelayer, filetree, GameSMMPath + "/Mods/FOLDERNAME/") + if treefixed == 1: + self.needsNameFix = True if treefixed == 0: return None return filetree @@ -117,9 +130,9 @@ def update_smm_meta(self, mods: dict[str, mobase.ModState]): key = self._organizer.modList().getMod(key) tree = key.fileTree() subtree = tree.find(self.GameSMMPath + "/Mods", mobase.IFileTree.DIRECTORY) - if subtree is not None and subtree.isDir(): + if isinstance(subtree, mobase.IFileTree): for e in subtree: - if e is not None and e.isDir(): + if isinstance(e, mobase.IFileTree): if e.exists("manifest.json", mobase.IFileTree.FILE): json_path = key.absolutePath() + "/" + e.path() + "/manifest.json" mod_data = json.load(open(json_path, encoding="utf-8")) diff --git a/games/game_noita.py b/games/game_noita.py index 38db9331..ca2cac04 100644 --- a/games/game_noita.py +++ b/games/game_noita.py @@ -16,7 +16,7 @@ def __init__(self, organizer: mobase.IOrganizer): self.organizer.modList().onModInstalled(self._Fix_Installed_Mod) self.needsNameFix = False - def move_overwrite_merge(self, source, destination): + def move_overwrite_merge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -32,13 +32,11 @@ def move_overwrite_merge(self, source, destination): def _Fix_Installed_Mod(self, mod: mobase.IModInterface): if not self.needsNameFix: return - GameModsPath = self.organizer.managedGame().GameModsPath + GameModsPath = getattr(self.organizer.managedGame(), "GameModsPath", "") filetree: mobase.IFileTree = mod.fileTree() fixed = False modname = mod.name() - if filetree is not None and filetree.exists( - GameModsPath + "/FOLDERNAME", mobase.IFileTree.DIRECTORY - ): + if filetree.exists(GameModsPath + "/FOLDERNAME", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() old_path = os.path.join(path, GameModsPath + "/FOLDERNAME") new_path = os.path.join(path, GameModsPath + f"/{modname}") @@ -48,18 +46,16 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): return self.needsNameFix = False - def dataLooksValid( - self, filetree: mobase.IFileTree - ) -> mobase.ModDataChecker.CheckReturn: + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: if filetree.exists("mods", mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID return mobase.ModDataChecker.FIXABLE def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): for branch in filetree: - if branch is not None and branch.isDir(): + if isinstance(branch, mobase.IFileTree): for e in branch: - if e is not None and e.name() == name: + if e.name() == name: return True return False @@ -67,15 +63,14 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): entriesToMove: list[mobase.FileTreeEntry] = [] retVal = 0 for e in filetree: - if e is not None: - entriesToMove.append(e) + entriesToMove.append(e) for e in entriesToMove: filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) retVal = 1 return retVal - def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: - GameModsPath = self.organizer.managedGame().GameModsPath + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + GameModsPath = getattr(self.organizer.managedGame(), "GameModsPath", "") treefixed = 0 if filetree.exists("mod.xml", mobase.IFileTree.FILE): treefixed = self.allMoveTo(filetree, GameModsPath + "/FOLDERNAME/") diff --git a/games/game_oblivion_remaster.py b/games/game_oblivion_remaster.py index 218bb048..218fded0 100644 --- a/games/game_oblivion_remaster.py +++ b/games/game_oblivion_remaster.py @@ -344,7 +344,7 @@ def fullDescription(self, key: int) -> str: match key: case Problems.UE4SS_LOADER: return ( - "The UE4SS loader DLL is present (dwmapi.dll). This will not function out-of-the box with MO2's virtual filesystem.\n\n" + "The UE4SS loader DLL is present (dwmapi.dll). This will not function out-of-the-box with MO2's virtual filesystem.\n\n" + "In order to resolve this, either delete the DLL and use the OBSE UE4SS Loader plugin, or rename " + "the DLL (ex. 'ue4ss_loader.dll') and set it to force load with the game exe.\n\n" + "Do this for any executable which runs the game, such as the OBSE64 loader." diff --git a/games/game_ovkwalkingdead.py b/games/game_ovkwalkingdead.py index 7b2314eb..b702d51e 100644 --- a/games/game_ovkwalkingdead.py +++ b/games/game_ovkwalkingdead.py @@ -25,7 +25,7 @@ class Content(IntEnum): class OTWDModDataContent(mobase.ModDataContent): - contents: list[int] = [] + content: list[int] = [] GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ (Content.UCAS, "UCAS", ":/MO/gui/content/geometries"), (Content.UTOC, "UTOC", ":/MO/gui/content/inifile"), @@ -42,17 +42,17 @@ def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): match entry.suffix().casefold(): case "utoc": - self.contents.add(Content.UTOC) + self.content.append(Content.UTOC) case "ucas": - self.contents.add(Content.UCAS) + self.content.append(Content.UCAS) case "pak": - self.contents.add(Content.PAK) + self.content.append(Content.PAK) case "lua": - self.contents.add(Content.UE4SS) + self.content.append(Content.UE4SS) case "dll": - self.contents.add(Content.DLL) + self.content.append(Content.DLL) case "bk2": - self.contents.add(Content.BK2) + self.content.append(Content.BK2) case _: pass return mobase.IFileTree.WalkReturn.CONTINUE @@ -68,7 +68,7 @@ def __init__(self, organizer: mobase.IOrganizer): super().__init__() self.organizer: mobase.IOrganizer = organizer - def move_overwrite_merge(self, source, destination): + def move_overwrite_merge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -82,12 +82,12 @@ def move_overwrite_merge(self, source, destination): os.rmdir(source) def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: - GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods - GameDataPakMods = self.organizer.managedGame().GameDataPakMods - GameDataMovies = self.organizer.managedGame().GameDataMovieMods + GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") + GameDataMovieMods = getattr(self.organizer.managedGame(), "GameDataMovieMods", "") if filetree.exists(GameDataPakMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID - if filetree.exists(GameDataMovies, mobase.IFileTree.DIRECTORY): + if filetree.exists(GameDataMovieMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID if filetree.exists(GameDataUE4SSMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID @@ -95,9 +95,9 @@ def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.Ch def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): for branch in filetree: - if branch is not None and branch.isDir(): + if isinstance(branch, mobase.IFileTree): for e in branch: - if e is not None and e.name() == name: + if e.name() == name: return True return False @@ -105,17 +105,16 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): entriesToMove: list[mobase.FileTreeEntry] = [] retVal = 0 for e in filetree: - if e is not None: - entriesToMove.append(e) + entriesToMove.append(e) for e in entriesToMove: filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) retVal = 1 return retVal - def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: - GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods + "/" - GameDataPakMods = self.organizer.managedGame().GameDataPakMods + "/" - GameDataMovies = self.organizer.managedGame().GameDataMovieMods + "/" + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + "/" + GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") + "/" + GameDataMovieMods = getattr(self.organizer.managedGame(), "GameDataMovieMods", "") + "/" treefixed = 0 if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): treefixed = self.allMoveTo(filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/") @@ -129,28 +128,27 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: allowedUnzippedExt = ["pak", "utoc", "ucas", "bk2", "dll"] entriesToMove: list[mobase.FileTreeEntry] = [] for e in filetree: - if e is not None: - if e.isFile(): - fileext = e.suffix().casefold() - if fileext in allowedUnzippedExt: - mod_name = filetree.name() - if mod_name == "": - mod_name = e.name() - mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if filetree.createOrphanTree("OrphanTree") is None and os.path.exists(mod_path): - match e.suffix().casefold(): - case "pak" | "utoc" | "ucas": - os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) - case "bk2": - os.makedirs(os.path.join(mod_path, GameDataMovies), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovies, e.name())) - case _: - pass - treefixed = 1 - else: - entriesToMove.append(e) - if entriesToMove is not None: + if e.isFile(): + fileext = e.suffix().casefold() + if fileext in allowedUnzippedExt: + mod_name = filetree.name() + if mod_name == "": + mod_name = e.name() + mod_path = os.path.join(self.organizer.modsPath(), mod_name) + if not filetree.createOrphanTree("OrphanTree") and os.path.exists(mod_path): + match e.suffix().casefold(): + case "pak" | "utoc" | "ucas": + os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) + case "bk2": + os.makedirs(os.path.join(mod_path, GameDataMovieMods), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovieMods, e.name())) + case _: + pass + treefixed = 1 + else: + entriesToMove.append(e) + if entriesToMove: for e in entriesToMove: match e.suffix().casefold(): case "pak" | "utoc" | "ucas": @@ -158,7 +156,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: case "dll": filetree.move(e, os.path.dirname(GameDataUE4SSMods) + "/", mobase.IFileTree.MERGE) case "bk2": - filetree.move(e, GameDataMovies, mobase.IFileTree.MERGE) + filetree.move(e, GameDataMovieMods, mobase.IFileTree.MERGE) case _: pass treefixed = 1 @@ -240,15 +238,6 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] return efls - def paksDirectory(self) -> QDir: - return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataPakMods) - - def ue4ssDirectory(self) -> QDir: - return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataUE4SSMods) - - def movieDirectory(self) -> QDir: - return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataMovieMods) - def write_default_mods(self, profile: QDir): ue4ss_mods_txt = QFileInfo(profile.absoluteFilePath("mods.txt")) ue4ss_mods_json = QFileInfo(profile.absoluteFilePath("mods.json")) @@ -268,10 +257,17 @@ def iniFiles(self): def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): self.write_default_mods(directory) - if not self.paksDirectory().exists(): - os.makedirs(self.paksDirectory().absolutePath()) - if not self.ue4ssDirectory().exists(): - os.makedirs(self.ue4ssDirectory().absolutePath()) - if not self.movieDirectory().exists(): - os.makedirs(self.movieDirectory().absolutePath()) + + base_data_dir = self.dataDirectory().absolutePath() + + paksDirectory = QDir(base_data_dir + "/" + self.GameDataPakMods) + ue4ssDirectory = QDir(base_data_dir + "/" + self.GameDataUE4SSMods) + movieDirectory = QDir(base_data_dir + "/" + self.GameDataMovieMods) + + if not paksDirectory.exists(): + os.makedirs(paksDirectory.absolutePath()) + if not ue4ssDirectory.exists(): + os.makedirs(ue4ssDirectory.absolutePath()) + if not movieDirectory.exists(): + os.makedirs(movieDirectory.absolutePath()) super().initializeProfile(directory, settings) diff --git a/games/game_pacificdrive.py b/games/game_pacificdrive.py index 72ea8fff..90fc1873 100644 --- a/games/game_pacificdrive.py +++ b/games/game_pacificdrive.py @@ -25,7 +25,7 @@ class Content(IntEnum): class PacificDriveModDataContent(mobase.ModDataContent): - contents: list[int] = [] + content: list[int] = [] GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ (Content.UCAS, "UCAS", ":/MO/gui/content/geometries"), (Content.UTOC, "UTOC", ":/MO/gui/content/inifile"), @@ -42,17 +42,17 @@ def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): match entry.suffix().casefold(): case "utoc": - self.contents.add(Content.UTOC) + self.content.append(Content.UTOC) case "ucas": - self.contents.add(Content.UCAS) + self.content.append(Content.UCAS) case "pak": - self.contents.add(Content.PAK) + self.content.append(Content.PAK) case "lua": - self.contents.add(Content.UE4SS) + self.content.append(Content.UE4SS) case "dll": - self.contents.add(Content.DLL) + self.content.append(Content.DLL) case "bk2": - self.contents.add(Content.BK2) + self.content.append(Content.BK2) case _: pass return mobase.IFileTree.WalkReturn.CONTINUE @@ -68,7 +68,7 @@ def __init__(self, organizer: mobase.IOrganizer): super().__init__() self.organizer: mobase.IOrganizer = organizer - def move_overwrite_merge(self, source, destination): + def move_overwrite_merge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -82,12 +82,12 @@ def move_overwrite_merge(self, source, destination): os.rmdir(source) def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: - GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods - GameDataPakMods = self.organizer.managedGame().GameDataPakMods - GameDataMovies = self.organizer.managedGame().GameDataMovieMods + GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") + GameDataMovieMods = getattr(self.organizer.managedGame(), "GameDataMovieMods", "") if filetree.exists(GameDataPakMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID - if filetree.exists(GameDataMovies, mobase.IFileTree.DIRECTORY): + if filetree.exists(GameDataMovieMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID if filetree.exists(GameDataUE4SSMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID @@ -95,9 +95,9 @@ def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.Ch def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): for branch in filetree: - if branch is not None and branch.isDir(): + if isinstance(branch, mobase.IFileTree): for e in branch: - if e is not None and e.name() == name: + if e.name() == name: return True return False @@ -105,17 +105,16 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): entriesToMove: list[mobase.FileTreeEntry] = [] retVal = 0 for e in filetree: - if e is not None: - entriesToMove.append(e) + entriesToMove.append(e) for e in entriesToMove: filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) retVal = 1 return retVal - def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: - GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods + "/" - GameDataPakMods = self.organizer.managedGame().GameDataPakMods + "/" - GameDataMovies = self.organizer.managedGame().GameDataMovieMods + "/" + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + "/" + GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") + "/" + GameDataMovieMods = getattr(self.organizer.managedGame(), "GameDataMovieMods", "") + "/" treefixed = 0 if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): treefixed = self.allMoveTo(filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/") @@ -129,28 +128,27 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: allowedUnzippedExt = ["pak", "utoc", "ucas", "bk2", "dll"] entriesToMove: list[mobase.FileTreeEntry] = [] for e in filetree: - if e is not None: - if e.isFile(): - fileext = e.suffix().casefold() - if fileext in allowedUnzippedExt: - mod_name = filetree.name() - if mod_name == "": - mod_name = e.name() - mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if filetree.createOrphanTree("OrphanTree") is None and os.path.exists(mod_path): - match e.suffix().casefold(): - case "pak" | "utoc" | "ucas": - os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) - case "bk2": - os.makedirs(os.path.join(mod_path, GameDataMovies), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovies, e.name())) - case _: - pass - treefixed = 1 - else: - entriesToMove.append(e) - if entriesToMove is not None: + if e.isFile(): + fileext = e.suffix().casefold() + if fileext in allowedUnzippedExt: + mod_name = filetree.name() + if mod_name == "": + mod_name = e.name() + mod_path = os.path.join(self.organizer.modsPath(), mod_name) + if not filetree.createOrphanTree("OrphanTree") and os.path.exists(mod_path): + match e.suffix().casefold(): + case "pak" | "utoc" | "ucas": + os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) + case "bk2": + os.makedirs(os.path.join(mod_path, GameDataMovieMods), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovieMods, e.name())) + case _: + pass + treefixed = 1 + else: + entriesToMove.append(e) + if entriesToMove: for e in entriesToMove: match e.suffix().casefold(): case "pak" | "utoc" | "ucas": @@ -158,7 +156,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: case "dll": filetree.move(e, os.path.dirname(GameDataUE4SSMods) + "/", mobase.IFileTree.MERGE) case "bk2": - filetree.move(e, GameDataMovies, mobase.IFileTree.MERGE) + filetree.move(e, GameDataMovieMods, mobase.IFileTree.MERGE) case _: pass treefixed = 1 @@ -241,15 +239,6 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] return efls - def paksDirectory(self) -> QDir: - return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataPakMods) - - def ue4ssDirectory(self) -> QDir: - return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataUE4SSMods) - - def movieDirectory(self) -> QDir: - return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataMovieMods) - def write_default_mods(self, profile: QDir): ue4ss_mods_txt = QFileInfo(profile.absoluteFilePath("mods.txt")) ue4ss_mods_json = QFileInfo(profile.absoluteFilePath("mods.json")) @@ -269,10 +258,17 @@ def iniFiles(self): def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): self.write_default_mods(directory) - if not self.paksDirectory().exists(): - os.makedirs(self.paksDirectory().absolutePath()) - if not self.ue4ssDirectory().exists(): - os.makedirs(self.ue4ssDirectory().absolutePath()) - if not self.movieDirectory().exists(): - os.makedirs(self.movieDirectory().absolutePath()) + + base_data_dir = self.dataDirectory().absolutePath() + + paksDirectory = QDir(base_data_dir + "/" + self.GameDataPakMods) + ue4ssDirectory = QDir(base_data_dir + "/" + self.GameDataUE4SSMods) + movieDirectory = QDir(base_data_dir + "/" + self.GameDataMovieMods) + + if not paksDirectory.exists(): + os.makedirs(paksDirectory.absolutePath()) + if not ue4ssDirectory.exists(): + os.makedirs(ue4ssDirectory.absolutePath()) + if not movieDirectory.exists(): + os.makedirs(movieDirectory.absolutePath()) super().initializeProfile(directory, settings) diff --git a/games/game_payday1.py b/games/game_payday1.py index 8b20dd05..f4b59e19 100644 --- a/games/game_payday1.py +++ b/games/game_payday1.py @@ -19,6 +19,7 @@ class Content(IntEnum): class Payday1ModDataContent(mobase.ModDataContent): + content: list[int] = [] GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ (Content.TEXTURE, "Textures", ":/MO/gui/content/texture"), (Content.MESH, "Meshes", ":/MO/gui/content/mesh"), @@ -31,30 +32,28 @@ class Payday1ModDataContent(mobase.ModDataContent): def getAllContents(self) -> list[mobase.ModDataContent.Content]: return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] - contents = set() - def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): match entry.suffix().casefold(): case "texture": - self.contents.add(Content.TEXTURE) + self.content.append(Content.TEXTURE) case "model": - self.contents.add(Content.MESH) + self.content.append(Content.MESH) case "lua": - self.contents.add(Content.SCRIPT) + self.content.append(Content.SCRIPT) case "stream": - self.contents.add(Content.SOUND) + self.content.append(Content.SOUND) case "txt": - self.contents.add(Content.STRING) + self.content.append(Content.STRING) case "json": - self.contents.add(Content.CONFIG) + self.content.append(Content.CONFIG) case _: pass return mobase.IFileTree.WalkReturn.CONTINUE def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: filetree.walk(self.walkContent, "/") - return list(self.contents) + return list(self.content) class Payday1ModDataChecker(mobase.ModDataChecker): @@ -64,7 +63,7 @@ def __init__(self, organizer: mobase.IOrganizer): self.organizer.modList().onModInstalled(self._Fix_Installed_Mod) self.needsNameFix = False - def move_overwrite_merge(self, source, destination): + def move_overwrite_merge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -83,19 +82,19 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): filetree: mobase.IFileTree = mod.fileTree() fixed = False modname = mod.name() - if filetree is not None and filetree.exists("mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): + if filetree.exists("mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() old_path = os.path.join(path, "mods/FOLDERNAME") new_path = os.path.join(path, f"mods/{modname}") self.move_overwrite_merge(old_path, new_path) fixed = True - elif filetree is not None and filetree.exists("assets/mod_overrides/FOLDERNAME/", mobase.IFileTree.DIRECTORY): + elif filetree.exists("assets/mod_overrides/FOLDERNAME/", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() old_path = os.path.join(path, "assets/mod_overrides/FOLDERNAME") new_path = os.path.join(path, f"assets/mod_overrides/{modname}") self.move_overwrite_merge(old_path, new_path) fixed = True - elif filetree is not None and filetree.exists("maps/FOLDERNAME", mobase.IFileTree.DIRECTORY): + elif filetree.exists("maps/FOLDERNAME", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() old_path = os.path.join(path, "maps/FOLDERNAME") new_path = os.path.join(path, f"maps/{modname}") @@ -113,78 +112,86 @@ def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.Ch if filetree.exists("maps", mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID for e in filetree: - if e is not None and e.suffix().casefold() == "dll": + if e.suffix().casefold() == "dll": return mobase.ModDataChecker.VALID return mobase.ModDataChecker.FIXABLE def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): for branch in filetree: - if branch is not None and branch.isDir(): + if isinstance(branch, mobase.IFileTree): for e in branch: - if e is not None and e.name() == name: + if e.name() == name: return True return False + def first_tree(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + for e in filetree: + if isinstance(e, mobase.IFileTree) and e.isDir(): + return e + return None + def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): entriesToMove: list[mobase.FileTreeEntry] = [] retVal = 0 for e in filetree: - if e is not None: - entriesToMove.append(e) + entriesToMove.append(e) for e in entriesToMove: filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) retVal = 1 return retVal - def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: treefixed = 0 - if filetree.exists("mod.txt", mobase.IFileTree.FILE): - treefixed = self.allMoveTo(filetree, "mods/FOLDERNAME/") - if treefixed == 1: - self.needsNameFix = True - elif self.fileExistsInNextSubDir(filetree, "mod.txt"): - filetree.move(filetree[0], "mods/", mobase.IFileTree.MERGE) - treefixed = 1 - elif self.fileExistsInNextSubDir(filetree, "main.xml"): - if self.fileExistsInNextSubDir(filetree, "levels"): - filetree.move(filetree[0], "maps/", mobase.IFileTree.MERGE) - treefixed = 1 - else: - filetree.move(filetree[0], "assets/mod_overrides/", mobase.IFileTree.MERGE) - treefixed = 1 - elif filetree.exists("main.xml", mobase.IFileTree.FILE): - if filetree.exists("levels", mobase.IFileTree.DIRECTORY): - treefixed = self.move_overwrite_merge(filetree, "maps/FOLDERNAME") + firsttreelayer: mobase.IFileTree | None = self.first_tree(filetree) + if firsttreelayer is not None: + secondtreelayer: mobase.IFileTree | None = self.first_tree(firsttreelayer) + if filetree.exists("mod.txt", mobase.IFileTree.FILE): + treefixed = self.allMoveTo(filetree, "mods/FOLDERNAME/") if treefixed == 1: self.needsNameFix = True - else: - treefixed = self.allMoveTo(filetree, "assets/mod_overrides/FOLDERNAME/") - if treefixed == 1: - self.needsNameFix = True - else: - if filetree[0][0].exists("mod.txt", mobase.IFileTree.FILE): - filetree.move(filetree[0][0], filetree[0].path("/"), mobase.IFileTree.REPLACE) - filetree.move(filetree[0], "mods/", mobase.IFileTree.MERGE) + elif self.fileExistsInNextSubDir(filetree, "mod.txt"): + filetree.move(firsttreelayer, "mods/", mobase.IFileTree.MERGE) treefixed = 1 - elif filetree[0][0].exists("main.xml", mobase.IFileTree.FILE): - if filetree.exists("levels", mobase.IFileTree.DIRECTORY): - filetree.move(filetree[0][0], filetree[0].path("/"), mobase.IFileTree.REPLACE) - filetree.move(filetree[0], "maps/", mobase.IFileTree.MERGE) + elif self.fileExistsInNextSubDir(filetree, "main.xml"): + if self.fileExistsInNextSubDir(filetree, "levels"): + filetree.move(firsttreelayer, "maps/", mobase.IFileTree.MERGE) treefixed = 1 else: - filetree.move(filetree[0][0], filetree[0].path("/"), mobase.IFileTree.REPLACE) - filetree.move(filetree[0], "assets/mod_overrides/", mobase.IFileTree.MERGE) + filetree.move(firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE) treefixed = 1 - if treefixed == 0: - if len(filetree) == 1: - filetree.move(filetree[0], "assets/mod_overrides/", mobase.IFileTree.MERGE) - treefixed = 1 - else: - for e in filetree: - if e is not None and e.path("/").count("/") == 0: - filetree.move(e, "assets/mod_overrides/FOLDERNAME/", mobase.IFileTree.MERGE) - treefixed = 1 + elif filetree.exists("main.xml", mobase.IFileTree.FILE): + if filetree.exists("levels", mobase.IFileTree.DIRECTORY): + treefixed = self.allMoveTo(filetree, "maps/FOLDERNAME/") + if treefixed == 1: + self.needsNameFix = True + else: + treefixed = self.allMoveTo(filetree, "assets/mod_overrides/FOLDERNAME/") + if treefixed == 1: self.needsNameFix = True + elif secondtreelayer is not None: + if secondtreelayer.exists("mod.txt", mobase.IFileTree.FILE): + filetree.move(secondtreelayer, firsttreelayer.path("/"), mobase.IFileTree.REPLACE) + filetree.move(firsttreelayer, "mods/", mobase.IFileTree.MERGE) + treefixed = 1 + elif secondtreelayer.exists("main.xml", mobase.IFileTree.FILE): + if filetree.exists("levels", mobase.IFileTree.DIRECTORY): + filetree.move(secondtreelayer, firsttreelayer.path("/"), mobase.IFileTree.REPLACE) + filetree.move(firsttreelayer, "maps/", mobase.IFileTree.MERGE) + treefixed = 1 + else: + filetree.move(secondtreelayer, firsttreelayer.path("/"), mobase.IFileTree.REPLACE) + filetree.move(firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE) + treefixed = 1 + if treefixed == 0: + if len(filetree) == 1: + filetree.move(firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE) + treefixed = 1 + else: + for e in filetree: + if e.path("/").count("/") == 0: + filetree.move(e, "assets/mod_overrides/FOLDERNAME/", mobase.IFileTree.MERGE) + treefixed = 1 + self.needsNameFix = True if treefixed == 0: return None return filetree @@ -200,7 +207,7 @@ class Payday1Game(BasicGame): GameBinary = "payday_win32_release.exe" GameDataPath = "%GAME_PATH%" GameDocumentsDirectory = "%USERPROFILE%/AppData/Local/PAYDAY" - _forced_libraries = ["IPHLPAPI.dll", "WSOCK32.dll"] + _forced_libraries = ["IPHLPAPI.dll", "WSOCK32.dll" , "DINPUT8.dll", "PDTHModOverrides.dll"] def init(self, organizer: mobase.IOrganizer) -> bool: super().init(organizer) @@ -210,6 +217,14 @@ def init(self, organizer: mobase.IOrganizer) -> bool: organizer.modList().onModStateChanged(self.dll_copy) return True + def executables(self): + return [ + mobase.ExecutableInfo( + "Payday the Heist", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ) + ] + def dll_copy( self, mods: dict[str, mobase.ModState] ): @@ -220,7 +235,7 @@ def dll_copy( key = self._organizer.modList().getMod(key) tree = key.fileTree() for e in tree: - if e is not None and e.name() in self._forced_libraries: + if e.name() in self._forced_libraries: #add file file_path_source = key.absolutePath() + "/" + e.path() file_path_target = game_path + e.name() @@ -231,14 +246,6 @@ def dll_copy( if os.path.exists(file_path_target): os.remove(file_path_target) - def executables(self): - return [ - mobase.ExecutableInfo( - "Payday: The Heist", - QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), - ), - ] - @cached_property def _base_dlls(self) -> set[str]: base_dir = Path(self.gameDirectory().absolutePath()) @@ -265,7 +272,16 @@ def iniFiles(self): return ["renderer_settings.xml"] def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): - modsPath = self.dataDirectory().absolutePath() - if not os.path.exists(modsPath): - os.mkdir(modsPath) + base_data_dir = self.dataDirectory().absolutePath() + + mapsDirectory = QDir(base_data_dir + "/maps") + modsDirectory = QDir(base_data_dir + "/mods") + overridesDirectory = QDir(base_data_dir + "/assets/mod_overrides") + + if not mapsDirectory.exists(): + os.makedirs(mapsDirectory.absolutePath()) + if not modsDirectory.exists(): + os.makedirs(modsDirectory.absolutePath()) + if not overridesDirectory.exists(): + os.makedirs(overridesDirectory.absolutePath()) super().initializeProfile(directory, settings) diff --git a/games/game_payday2.py b/games/game_payday2.py index 5998bb69..e500d0c8 100644 --- a/games/game_payday2.py +++ b/games/game_payday2.py @@ -19,6 +19,7 @@ class Content(IntEnum): class Payday2ModDataContent(mobase.ModDataContent): + content: list[int] = [] GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ (Content.TEXTURE, "Textures", ":/MO/gui/content/texture"), (Content.MESH, "Meshes", ":/MO/gui/content/mesh"), @@ -31,30 +32,29 @@ class Payday2ModDataContent(mobase.ModDataContent): def getAllContents(self) -> list[mobase.ModDataContent.Content]: return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] - contents = set() def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): match entry.suffix().casefold(): case "texture": - self.contents.add(Content.TEXTURE) + self.content.append(Content.TEXTURE) case "model": - self.contents.add(Content.MESH) + self.content.append(Content.MESH) case "lua": - self.contents.add(Content.SCRIPT) + self.content.append(Content.SCRIPT) case "stream": - self.contents.add(Content.SOUND) + self.content.append(Content.SOUND) case "txt": - self.contents.add(Content.STRING) + self.content.append(Content.STRING) case "json": - self.contents.add(Content.CONFIG) + self.content.append(Content.CONFIG) case _: pass return mobase.IFileTree.WalkReturn.CONTINUE def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: filetree.walk(self.walkContent, "/") - return list(self.contents) + return list(self.content) class Payday2ModDataChecker(mobase.ModDataChecker): @@ -64,7 +64,7 @@ def __init__(self, organizer: mobase.IOrganizer): self.organizer.modList().onModInstalled(self._Fix_Installed_Mod) self.needsNameFix = False - def move_overwrite_merge(self, source, destination): + def move_overwrite_merge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -83,19 +83,19 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): filetree: mobase.IFileTree = mod.fileTree() fixed = False modname = mod.name() - if filetree is not None and filetree.exists("mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): + if filetree.exists("mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() old_path = os.path.join(path, "mods/FOLDERNAME") new_path = os.path.join(path, f"mods/{modname}") self.move_overwrite_merge(old_path, new_path) fixed = True - elif filetree is not None and filetree.exists("assets/mod_overrides/FOLDERNAME", mobase.IFileTree.DIRECTORY): + elif filetree.exists("assets/mod_overrides/FOLDERNAME", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() old_path = os.path.join(path, "assets/mod_overrides/FOLDERNAME") new_path = os.path.join(path, f"assets/mod_overrides/{modname}") self.move_overwrite_merge(old_path, new_path) fixed = True - elif filetree is not None and filetree.exists("maps/FOLDERNAME", mobase.IFileTree.DIRECTORY): + elif filetree.exists("maps/FOLDERNAME", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() old_path = os.path.join(path, "maps/FOLDERNAME") new_path = os.path.join(path, f"maps/{modname}") @@ -120,75 +120,80 @@ def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.Ch def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): for branch in filetree: - if branch is not None and branch.isDir(): + if isinstance(branch, mobase.IFileTree): for e in branch: - if e is not None and e.name() == name: + if e.name() == name: return True return False + def first_tree(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + for e in filetree: + if isinstance(e, mobase.IFileTree) and e.isDir(): + return e + return None + def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): entriesToMove: list[mobase.FileTreeEntry] = [] retVal = 0 for e in filetree: - if e is not None: - entriesToMove.append(e) + entriesToMove.append(e) for e in entriesToMove: filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) retVal = 1 return retVal - def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: treefixed = 0 - - if filetree.exists("mod.txt", mobase.IFileTree.FILE): - treefixed = self.allMoveTo(filetree, "mods/FOLDERNAME/") - if treefixed == 1: - self.needsNameFix = True - elif self.fileExistsInNextSubDir(filetree, "mod.txt"): - filetree.move(filetree[0], "mods/", mobase.IFileTree.MERGE) - treefixed = 1 - elif self.fileExistsInNextSubDir(filetree, "main.xml"): - if self.fileExistsInNextSubDir(filetree, "levels"): - filetree.move(filetree[0], "maps/", mobase.IFileTree.MERGE) - treefixed = 1 - else: - filetree.move(filetree[0], "assets/mod_overrides/", mobase.IFileTree.MERGE) - treefixed = 1 - elif filetree.exists("main.xml", mobase.IFileTree.FILE): - if filetree.exists("levels", mobase.IFileTree.DIRECTORY): - treefixed = self.move_overwrite_merge(filetree, "maps/FOLDERNAME") + firsttreelayer: mobase.IFileTree | None = self.first_tree(filetree) + if firsttreelayer is not None: + secondtreelayer: mobase.IFileTree | None = self.first_tree(firsttreelayer) + if filetree.exists("mod.txt", mobase.IFileTree.FILE): + treefixed = self.allMoveTo(filetree, "mods/FOLDERNAME/") if treefixed == 1: self.needsNameFix = True - else: - treefixed = self.allMoveTo(filetree, "assets/mod_overrides/FOLDERNAME/") - if treefixed == 1: - self.needsNameFix = True - else: - try: - if filetree[0][0].exists("mod.txt", mobase.IFileTree.FILE): - filetree.move(filetree[0][0], filetree[0].path("/"), mobase.IFileTree.REPLACE) - filetree.move(filetree[0], "mods/", mobase.IFileTree.MERGE) + elif self.fileExistsInNextSubDir(filetree, "mod.txt"): + filetree.move(firsttreelayer, "mods/", mobase.IFileTree.MERGE) + treefixed = 1 + elif self.fileExistsInNextSubDir(filetree, "main.xml"): + if self.fileExistsInNextSubDir(filetree, "levels"): + filetree.move(firsttreelayer, "maps/", mobase.IFileTree.MERGE) treefixed = 1 - elif filetree[0][0].exists("main.xml", mobase.IFileTree.FILE): + else: + filetree.move(firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE) + treefixed = 1 + elif filetree.exists("main.xml", mobase.IFileTree.FILE): + if filetree.exists("levels", mobase.IFileTree.DIRECTORY): + treefixed = self.allMoveTo(filetree, "maps/FOLDERNAME/") + if treefixed == 1: + self.needsNameFix = True + else: + treefixed = self.allMoveTo(filetree, "assets/mod_overrides/FOLDERNAME/") + if treefixed == 1: + self.needsNameFix = True + elif secondtreelayer is not None: + if secondtreelayer.exists("mod.txt", mobase.IFileTree.FILE): + filetree.move(secondtreelayer, firsttreelayer.path("/"), mobase.IFileTree.REPLACE) + filetree.move(firsttreelayer, "mods/", mobase.IFileTree.MERGE) + treefixed = 1 + elif secondtreelayer.exists("main.xml", mobase.IFileTree.FILE): if filetree.exists("levels", mobase.IFileTree.DIRECTORY): - filetree.move(filetree[0][0], filetree[0].path("/"), mobase.IFileTree.REPLACE) - filetree.move(filetree[0], "maps/", mobase.IFileTree.MERGE) + filetree.move(secondtreelayer, firsttreelayer.path("/"), mobase.IFileTree.REPLACE) + filetree.move(firsttreelayer, "maps/", mobase.IFileTree.MERGE) treefixed = 1 else: - filetree.move(filetree[0][0], filetree[0].path("/"), mobase.IFileTree.REPLACE) - filetree.move(filetree[0], "assets/mod_overrides/", mobase.IFileTree.MERGE) - except TypeError: - pass - if treefixed == 0: - if len(filetree) == 1 and filetree[0].isDir: - filetree.move(filetree[0], "assets/mod_overrides/", mobase.IFileTree.MERGE) - treefixed = 1 - else: - for e in filetree: - if e is not None and e.path("/").count("/") == 0: - filetree.move(e, "assets/mod_overrides/FOLDERNAME/", mobase.IFileTree.MERGE) + filetree.move(secondtreelayer, firsttreelayer.path("/"), mobase.IFileTree.REPLACE) + filetree.move(firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE) treefixed = 1 - self.needsNameFix = True + if treefixed == 0: + if len(filetree) == 1: + filetree.move(firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE) + treefixed = 1 + else: + for e in filetree: + if e.path("/").count("/") == 0: + filetree.move(e, "assets/mod_overrides/FOLDERNAME/", mobase.IFileTree.MERGE) + treefixed = 1 + self.needsNameFix = True if treefixed == 0: return None return filetree @@ -234,7 +239,7 @@ def dll_copy( key = self._organizer.modList().getMod(key) tree = key.fileTree() for e in tree: - if e is not None and e.name() in self._forced_libraries: + if e.name() in self._forced_libraries: #add file file_path_source = key.absolutePath() + "/" + e.path() file_path_target = game_path + e.name() @@ -267,23 +272,20 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] return efls - def mapsDirectory(self) -> QDir: - return QDir(self.gameDirectory().absolutePath() + "/maps") - - def modsDirectory(self) -> QDir: - return QDir(self.gameDirectory().absolutePath() + "/mods") - - def overridesDirectory(self) -> QDir: - return QDir(self.gameDirectory().absolutePath() + "/assets/mod_overrides") - def iniFiles(self): return ["renderer_settings.xml"] def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): - if not self.mapsDirectory().exists(): - os.makedirs(self.mapsDirectory().absolutePath()) - if not self.modsDirectory().exists(): - os.makedirs(self.modsDirectory().absolutePath()) - if not self.overridesDirectory().exists(): - os.makedirs(self.overridesDirectory().absolutePath()) + base_data_dir = self.dataDirectory().absolutePath() + + mapsDirectory = QDir(base_data_dir + "/maps") + modsDirectory = QDir(base_data_dir + "/mods") + overridesDirectory = QDir(base_data_dir + "/assets/mod_overrides") + + if not mapsDirectory.exists(): + os.makedirs(mapsDirectory.absolutePath()) + if not modsDirectory.exists(): + os.makedirs(modsDirectory.absolutePath()) + if not overridesDirectory.exists(): + os.makedirs(overridesDirectory.absolutePath()) super().initializeProfile(directory, settings) diff --git a/games/game_payday3.py b/games/game_payday3.py index 170fb7c5..91a6a496 100644 --- a/games/game_payday3.py +++ b/games/game_payday3.py @@ -27,7 +27,8 @@ class Content(IntEnum): class Payday3ModDataContent(mobase.ModDataContent): - contents: list[int] = [] + content: list[int] = [] + GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ (Content.UCAS, "UCAS", ":/MO/gui/content/geometries"), (Content.UTOC, "UTOC", ":/MO/gui/content/inifile"), @@ -47,17 +48,17 @@ def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): match entry.suffix().casefold(): case "utoc": - self.contents.add(Content.UTOC) + self.content.append(Content.UTOC) case "ucas": - self.contents.add(Content.UCAS) + self.content.append(Content.UCAS) case "pak": - self.contents.add(Content.PAK) + self.content.append(Content.PAK) case "lua": - self.contents.add(Content.UE4SS) + self.content.append(Content.UE4SS) case "dll": - self.contents.add(Content.DLL) + self.content.append(Content.DLL) case "bk2": - self.contents.add(Content.BK2) + self.content.append(Content.BK2) case _: pass return mobase.IFileTree.WalkReturn.CONTINUE @@ -73,7 +74,7 @@ def __init__(self, organizer: mobase.IOrganizer): super().__init__() self.organizer: mobase.IOrganizer = organizer - def move_overwrite_merge(self, source, destination): + def move_overwrite_merge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -86,15 +87,13 @@ def move_overwrite_merge(self, source, destination): self.move_overwrite_merge(s_item, d_item) os.rmdir(source) - def dataLooksValid( - self, filetree: mobase.IFileTree - ) -> mobase.ModDataChecker.CheckReturn: - GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods - GameDataPakMods = self.organizer.managedGame().GameDataPakMods - GameDataMovies = self.organizer.managedGame().GameDataMovieMods + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") + GameDataMovieMods = getattr(self.organizer.managedGame(), "GameDataMovieMods", "") if filetree.exists(GameDataPakMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID - if filetree.exists(GameDataMovies, mobase.IFileTree.DIRECTORY): + if filetree.exists(GameDataMovieMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID if filetree.exists(GameDataUE4SSMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID @@ -102,9 +101,9 @@ def dataLooksValid( def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): for branch in filetree: - if branch is not None and branch.isDir(): + if isinstance(branch, mobase.IFileTree): for e in branch: - if e is not None and e.name() == name: + if e.name() == name: return True return False @@ -112,17 +111,16 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): entriesToMove: list[mobase.FileTreeEntry] = [] retVal = 0 for e in filetree: - if e is not None: - entriesToMove.append(e) + entriesToMove.append(e) for e in entriesToMove: filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) retVal = 1 return retVal - def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: - GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods + "/" - GameDataPakMods = self.organizer.managedGame().GameDataPakMods + "/" - GameDataMovies = self.organizer.managedGame().GameDataMovieMods + "/" + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + "/" + GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") + "/" + GameDataMovieMods = getattr(self.organizer.managedGame(), "GameDataMovieMods", "") + "/" treefixed = 0 if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): treefixed = self.allMoveTo( @@ -140,58 +138,35 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: allowedUnzippedExt = ["pak", "utoc", "ucas", "bk2", "dll"] entriesToMove: list[mobase.FileTreeEntry] = [] for e in filetree: - if e is not None: - if e.isFile(): - fileext = e.suffix().casefold() - if fileext in allowedUnzippedExt: - mod_name = filetree.name() - if mod_name == "": - mod_name = e.name() - mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if filetree.createOrphanTree( - "OrphanTree" - ) is None and os.path.exists(mod_path): - match e.suffix().casefold(): - case "pak" | "utoc" | "ucas": - os.makedirs( - os.path.join(mod_path, GameDataPakMods), - exist_ok=True, - ) - shutil.move( - os.path.join(mod_path, e.name()), - os.path.join( - mod_path, GameDataPakMods, e.name() - ), - ) - case "bk2": - os.makedirs( - os.path.join(mod_path, GameDataMovies), - exist_ok=True, - ) - shutil.move( - os.path.join(mod_path, e.name()), - os.path.join( - mod_path, GameDataMovies, e.name() - ), - ) - case _: - pass - treefixed = 1 - else: - entriesToMove.append(e) - if entriesToMove is not None: + if e.isFile(): + fileext = e.suffix().casefold() + if fileext in allowedUnzippedExt: + mod_name = filetree.name() + if mod_name == "": + mod_name = e.name() + mod_path = os.path.join(self.organizer.modsPath(), mod_name) + if not filetree.createOrphanTree("OrphanTree") and os.path.exists(mod_path): + match e.suffix().casefold(): + case "pak" | "utoc" | "ucas": + os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) + case "bk2": + os.makedirs(os.path.join(mod_path, GameDataMovieMods), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovieMods, e.name())) + case _: + pass + treefixed = 1 + else: + entriesToMove.append(e) + if entriesToMove: for e in entriesToMove: match e.suffix().casefold(): case "pak" | "utoc" | "ucas": filetree.move(e, GameDataPakMods, mobase.IFileTree.MERGE) case "dll": - filetree.move( - e, - os.path.dirname(GameDataUE4SSMods) + "/", - mobase.IFileTree.MERGE, - ) + filetree.move(e,os.path.dirname(GameDataUE4SSMods) + "/",mobase.IFileTree.MERGE) case "bk2": - filetree.move(e, GameDataMovies, mobase.IFileTree.MERGE) + filetree.move(e, GameDataMovieMods, mobase.IFileTree.MERGE) case _: pass treefixed = 1 @@ -281,15 +256,6 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: ] return efls - def paksDirectory(self) -> QDir: - return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataPakMods) - - def ue4ssDirectory(self) -> QDir: - return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataUE4SSMods) - - def movieDirectory(self) -> QDir: - return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataMovieMods) - def write_default_mods(self, profile: QDir): ue4ss_mods_txt = QFileInfo(profile.absoluteFilePath("mods.txt")) ue4ss_mods_json = QFileInfo(profile.absoluteFilePath("mods.json")) @@ -309,10 +275,17 @@ def iniFiles(self): def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): self.write_default_mods(directory) - if not self.paksDirectory().exists(): - os.makedirs(self.paksDirectory().absolutePath()) - if not self.ue4ssDirectory().exists(): - os.makedirs(self.ue4ssDirectory().absolutePath()) - if not self.movieDirectory().exists(): - os.makedirs(self.movieDirectory().absolutePath()) + + base_data_dir = self.dataDirectory().absolutePath() + + paksDirectory = QDir(base_data_dir + "/" + self.GameDataPakMods) + ue4ssDirectory = QDir(base_data_dir + "/" + self.GameDataUE4SSMods) + movieDirectory = QDir(base_data_dir + "/" + self.GameDataMovieMods) + + if not paksDirectory.exists(): + os.makedirs(paksDirectory.absolutePath()) + if not ue4ssDirectory.exists(): + os.makedirs(ue4ssDirectory.absolutePath()) + if not movieDirectory.exists(): + os.makedirs(movieDirectory.absolutePath()) super().initializeProfile(directory, settings) diff --git a/games/game_raid2.py b/games/game_raid2.py index 105a0bdf..d52ae603 100644 --- a/games/game_raid2.py +++ b/games/game_raid2.py @@ -21,6 +21,7 @@ class Content(IntEnum): class RaidWW2ModDataContent(mobase.ModDataContent): + content: list[int] = [] GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ (Content.TEXTURE, "Textures", ":/MO/gui/content/texture"), (Content.MESH, "Meshes", ":/MO/gui/content/mesh"), @@ -33,30 +34,28 @@ class RaidWW2ModDataContent(mobase.ModDataContent): def getAllContents(self) -> list[mobase.ModDataContent.Content]: return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] - contents = set() - def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): match entry.suffix().casefold(): case "texture": - self.contents.add(Content.TEXTURE) + self.content.append(Content.TEXTURE) case "model": - self.contents.add(Content.MESH) + self.content.append(Content.MESH) case "lua": - self.contents.add(Content.SCRIPT) + self.content.append(Content.SCRIPT) case "stream": - self.contents.add(Content.SOUND) + self.content.append(Content.SOUND) case "txt": - self.contents.add(Content.STRING) + self.content.append(Content.STRING) case "json": - self.contents.add(Content.CONFIG) + self.content.append(Content.CONFIG) case _: pass return mobase.IFileTree.WalkReturn.CONTINUE def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: filetree.walk(self.walkContent, "/") - return list(self.contents) + return list(self.content) class RaidWW2ModDataChecker(mobase.ModDataChecker): @@ -66,7 +65,7 @@ def __init__(self, organizer: mobase.IOrganizer): self.organizer.modList().onModInstalled(self._Fix_Installed_Mod) self.needsNameFix = False - def move_overwrite_merge(self, source, destination): + def move_overwrite_merge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -85,7 +84,7 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): filetree: mobase.IFileTree = mod.fileTree() fixed = False modname = mod.name() - if filetree is not None and filetree.exists("FOLDERNAME", mobase.IFileTree.DIRECTORY): + if filetree.exists("FOLDERNAME", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() old_path = os.path.join(path, "FOLDERNAME") new_path = os.path.join(path, f"{modname}") @@ -102,9 +101,9 @@ def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.Ch def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): for branch in filetree: - if branch is not None and branch.isDir(): + if isinstance(branch, mobase.IFileTree): for e in branch: - if e is not None and e.name() == name: + if e.name() == name: return True return False @@ -112,8 +111,7 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): entriesToMove: list[mobase.FileTreeEntry] = [] retVal = 0 for e in filetree: - if e is not None: - entriesToMove.append(e) + entriesToMove.append(e) for e in entriesToMove: filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) retVal = 1 @@ -146,6 +144,14 @@ def init(self, organizer: mobase.IOrganizer) -> bool: organizer.modList().onModStateChanged(self.dll_copy) return True + def executables(self): + return [ + mobase.ExecutableInfo( + "Raid: World War II", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ) + ] + def dll_copy( self, mods: dict[str, mobase.ModState] ): @@ -156,7 +162,7 @@ def dll_copy( key = self._organizer.modList().getMod(key) tree = key.fileTree() for e in tree: - if e is not None and e.name() in self._forced_libraries: + if e.name() in self._forced_libraries: #add file file_path_source = key.absolutePath() + "/" + e.path() file_path_target = game_path + e.name() @@ -167,14 +173,6 @@ def dll_copy( if os.path.exists(file_path_target): os.remove(file_path_target) - def executables(self): - return [ - mobase.ExecutableInfo( - "Raid: World War II", - QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), - ), - ] - @cached_property def _base_dlls(self) -> set[str]: base_dir = Path(self.gameDirectory().absolutePath()) @@ -201,7 +199,10 @@ def iniFiles(self): return ["renderer_settings.xml"] def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): - modsPath = self.dataDirectory().absolutePath() - if not os.path.exists(modsPath): - os.mkdir(modsPath) - super().initializeProfile(directory, settings) + base_data_dir = self.dataDirectory().absolutePath() + + modsDirectory = QDir(base_data_dir + "/" + self.GameDataPath) + + if not modsDirectory.exists(): + os.makedirs(modsDirectory.absolutePath()) + super().initializeProfile(directory, settings) \ No newline at end of file diff --git a/games/game_roadtovostok.py b/games/game_roadtovostok.py index f8b84ccd..9e4780fd 100644 --- a/games/game_roadtovostok.py +++ b/games/game_roadtovostok.py @@ -16,12 +16,12 @@ def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.Ch if filetree.exists("mods", mobase.IFileTree.DIRECTORY) and not filetree.exists("mod.txt", mobase.IFileTree.FILE): return mobase.ModDataChecker.VALID for e in filetree: - if e is not None and e.isFile() and e.suffix().casefold() == "pck": + if e.isFile() and e.suffix().casefold() == "pck": return mobase.ModDataChecker.VALID return mobase.ModDataChecker.FIXABLE - def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: - GameModsPath = self.organizer.managedGame().GameModsPath + "/" + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + GameModsPath = getattr(self.organizer.managedGame(), "GameModsPath", "") + "/" treefixed = 0 for branch in filetree: @@ -29,7 +29,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: if mod_name == "": mod_name = branch.name() mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if filetree.createOrphanTree("OrphanTree") is None and os.path.exists(mod_path) and branch.suffix().casefold() == "zip": + if not filetree.createOrphanTree("OrphanTree") and os.path.exists(mod_path) and branch.suffix().casefold() == "zip": os.makedirs(os.path.join(mod_path, GameModsPath), exist_ok=True) shutil.move(os.path.join(mod_path, branch.name()), os.path.join(mod_path, GameModsPath, branch.name())) treefixed = 1 diff --git a/games/game_silenthill2remake.py b/games/game_silenthill2remake.py index fda5ee34..b3dcf8d8 100644 --- a/games/game_silenthill2remake.py +++ b/games/game_silenthill2remake.py @@ -24,7 +24,7 @@ class Content(IntEnum): class SilentHill2ModDataContent(mobase.ModDataContent): - contents: list[int] = [] + content: list[int] = [] GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ (Content.UCAS, "UCAS", ":/MO/gui/content/geometries"), (Content.UTOC, "UTOC", ":/MO/gui/content/inifile"), @@ -41,17 +41,17 @@ def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): match entry.suffix().casefold(): case "utoc": - self.contents.add(Content.UTOC) + self.content.append(Content.UTOC) case "ucas": - self.contents.add(Content.UCAS) + self.content.append(Content.UCAS) case "pak": - self.contents.add(Content.PAK) + self.content.append(Content.PAK) case "lua": - self.contents.add(Content.UE4SS) + self.content.append(Content.UE4SS) case "dll": - self.contents.add(Content.DLL) + self.content.append(Content.DLL) case "bk2": - self.contents.add(Content.BK2) + self.content.append(Content.BK2) case _: pass return mobase.IFileTree.WalkReturn.CONTINUE @@ -67,7 +67,7 @@ def __init__(self, organizer: mobase.IOrganizer): super().__init__() self.organizer: mobase.IOrganizer = organizer - def move_overwrite_merge(self, source, destination): + def move_overwrite_merge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -81,12 +81,12 @@ def move_overwrite_merge(self, source, destination): os.rmdir(source) def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: - GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods - GameDataPakMods = self.organizer.managedGame().GameDataPakMods - GameDataMovies = self.organizer.managedGame().GameDataMovieMods + GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") + GameDataMovieMods = getattr(self.organizer.managedGame(), "GameDataMovieMods", "") if filetree.exists(GameDataPakMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID - if filetree.exists(GameDataMovies, mobase.IFileTree.DIRECTORY): + if filetree.exists(GameDataMovieMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID if filetree.exists(GameDataUE4SSMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID @@ -94,9 +94,9 @@ def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.Ch def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): for branch in filetree: - if branch is not None and branch.isDir(): + if isinstance(branch, mobase.IFileTree): for e in branch: - if e is not None and e.name() == name: + if e.name() == name: return True return False @@ -104,17 +104,16 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): entriesToMove: list[mobase.FileTreeEntry] = [] retVal = 0 for e in filetree: - if e is not None: - entriesToMove.append(e) + entriesToMove.append(e) for e in entriesToMove: filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) retVal = 1 return retVal - def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: - GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods + "/" - GameDataPakMods = self.organizer.managedGame().GameDataPakMods + "/" - GameDataMovies = self.organizer.managedGame().GameDataMovieMods + "/" + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + "/" + GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") + "/" + GameDataMovieMods = getattr(self.organizer.managedGame(), "GameDataMovieMods", "") + "/" treefixed = 0 if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): treefixed = self.allMoveTo(filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/") @@ -128,28 +127,27 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: allowedUnzippedExt = ["pak", "utoc", "ucas", "bk2", "dll"] entriesToMove: list[mobase.FileTreeEntry] = [] for e in filetree: - if e is not None: - if e.isFile(): - fileext = e.suffix().casefold() - if fileext in allowedUnzippedExt: - mod_name = filetree.name() - if mod_name == "": - mod_name = e.name() - mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if filetree.createOrphanTree("OrphanTree") is None and os.path.exists(mod_path): - match e.suffix().casefold(): - case "pak" | "utoc" | "ucas": - os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) - case "bk2": - os.makedirs(os.path.join(mod_path, GameDataMovies), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovies, e.name())) - case _: - pass - treefixed = 1 - else: - entriesToMove.append(e) - if entriesToMove is not None: + if e.isFile(): + fileext = e.suffix().casefold() + if fileext in allowedUnzippedExt: + mod_name = filetree.name() + if mod_name == "": + mod_name = e.name() + mod_path = os.path.join(self.organizer.modsPath(), mod_name) + if not filetree.createOrphanTree("OrphanTree") and os.path.exists(mod_path): + match e.suffix().casefold(): + case "pak" | "utoc" | "ucas": + os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) + case "bk2": + os.makedirs(os.path.join(mod_path, GameDataMovieMods), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovieMods, e.name())) + case _: + pass + treefixed = 1 + else: + entriesToMove.append(e) + if entriesToMove: for e in entriesToMove: match e.suffix().casefold(): case "pak" | "utoc" | "ucas": @@ -157,7 +155,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: case "dll": filetree.move(e, os.path.dirname(GameDataUE4SSMods) + "/", mobase.IFileTree.MERGE) case "bk2": - filetree.move(e, GameDataMovies, mobase.IFileTree.MERGE) + filetree.move(e, GameDataMovieMods, mobase.IFileTree.MERGE) case _: pass treefixed = 1 @@ -240,15 +238,6 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] return efls - def paksDirectory(self) -> QDir: - return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataPakMods) - - def ue4ssDirectory(self) -> QDir: - return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataUE4SSMods) - - def movieDirectory(self) -> QDir: - return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataMovieMods) - def write_default_mods(self, profile: QDir): ue4ss_mods_txt = QFileInfo(profile.absoluteFilePath("mods.txt")) ue4ss_mods_json = QFileInfo(profile.absoluteFilePath("mods.json")) @@ -268,10 +257,17 @@ def iniFiles(self): def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): self.write_default_mods(directory) - if not self.paksDirectory().exists(): - os.makedirs(self.paksDirectory().absolutePath()) - if not self.ue4ssDirectory().exists(): - os.makedirs(self.ue4ssDirectory().absolutePath()) - if not self.movieDirectory().exists(): - os.makedirs(self.movieDirectory().absolutePath()) + + base_data_dir = self.dataDirectory().absolutePath() + + paksDirectory = QDir(base_data_dir + "/" + self.GameDataPakMods) + ue4ssDirectory = QDir(base_data_dir + "/" + self.GameDataUE4SSMods) + movieDirectory = QDir(base_data_dir + "/" + self.GameDataMovieMods) + + if not paksDirectory.exists(): + os.makedirs(paksDirectory.absolutePath()) + if not ue4ssDirectory.exists(): + os.makedirs(ue4ssDirectory.absolutePath()) + if not movieDirectory.exists(): + os.makedirs(movieDirectory.absolutePath()) super().initializeProfile(directory, settings) diff --git a/games/game_titanfall2.py b/games/game_titanfall2.py index 759cd55a..dd67358e 100644 --- a/games/game_titanfall2.py +++ b/games/game_titanfall2.py @@ -23,6 +23,7 @@ class Content(IntEnum): class Titanfall2ModDataContent(mobase.ModDataContent): + content: list[int] = [] GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ (Content.MATERIAL, "Materials", ":/MO/gui/content/interface"), (Content.TEXTURE, "Textures", ":/MO/gui/content/texture"), @@ -40,34 +41,33 @@ def getAllContents(self) -> list[mobase.ModDataContent.Content]: for id, name, icon, *filter_only in self.GAMECONTENTS ] - contents = set() def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): match entry.suffix().casefold(): case "vmt": - self.contents.add(Content.MATERIAL) + self.content.append(Content.MATERIAL) case "vtf": - self.contents.add(Content.TEXTURE) + self.content.append(Content.TEXTURE) case "mdl": - self.contents.add(Content.MODELS) + self.content.append(Content.MODELS) case "nut": - self.contents.add(Content.SCRIPT) + self.content.append(Content.SCRIPT) case "txt": - self.contents.add(Content.CONFIG) + self.content.append(Content.CONFIG) case "bik": - self.contents.add(Content.VIDEO) + self.content.append(Content.VIDEO) case "wav": - self.contents.add(Content.AUDIO) + self.content.append(Content.AUDIO) case "rpak" | "starmap" | "starpak": - self.contents.add(Content.STARPAK) + self.content.append(Content.STARPAK) case _: pass return mobase.IFileTree.WalkReturn.CONTINUE def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: filetree.walk(self.walkContent, "/") - return list(self.contents) + return list(self.content) class Titanfall2ModDataChecker(mobase.ModDataChecker): @@ -77,7 +77,7 @@ def __init__(self, organizer: mobase.IOrganizer): self.organizer.modList().onModInstalled(self._Fix_Installed_Mod) self.needsNameFix = False - def move_overwrite_merge(self, source, destination): + def move_overwrite_merge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -93,29 +93,25 @@ def move_overwrite_merge(self, source, destination): def _Fix_Installed_Mod(self, mod: mobase.IModInterface): if not self.needsNameFix: return - northstarModPath = self.organizer.managedGame().GameNorthstarPath + "/" + GameNorthstarPath = getattr(self.organizer.managedGame(), "GameNorthstarPath", "") + "/" filetree: mobase.IFileTree = mod.fileTree() fixed = False modname = mod.name() - if filetree is not None and filetree.exists( - northstarModPath + "FOLDERNAME", mobase.IFileTree.DIRECTORY - ): + if filetree.exists(GameNorthstarPath + "FOLDERNAME", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() - json_path = os.path.join(path, northstarModPath + "FOLDERNAME/mod.json") + json_path = os.path.join(path, GameNorthstarPath + "FOLDERNAME/mod.json") with open(json_path, "r") as json_data: mod_data = json.load(json_data) json_data.close() modname = mod_data["name"] - old_path = os.path.join(path, northstarModPath + "FOLDERNAME") - new_path = os.path.join(path, northstarModPath + f"{modname}") + old_path = os.path.join(path, GameNorthstarPath + "FOLDERNAME") + new_path = os.path.join(path, GameNorthstarPath + f"{modname}") self.move_overwrite_merge(old_path, new_path) fixed = True - elif filetree is not None and filetree.exists( - northstarModPath + "FOLDERNAME_NAME", mobase.IFileTree.DIRECTORY - ): + elif filetree.exists(GameNorthstarPath + "FOLDERNAME_NAME", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() - old_path = os.path.join(path, northstarModPath + "FOLDERNAME_NAME") - new_path = os.path.join(path, northstarModPath + f"{modname}") + old_path = os.path.join(path, GameNorthstarPath + "FOLDERNAME_NAME") + new_path = os.path.join(path, GameNorthstarPath + f"{modname}") self.move_overwrite_merge(old_path, new_path) fixed = True if not fixed: @@ -131,57 +127,55 @@ def dataLooksValid( def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): for branch in filetree: - if branch is not None and branch.isDir(): + if isinstance(branch, mobase.IFileTree): for e in branch: - if e is not None and e.name() == name: + if e.name() == name: return True return False + def first_tree(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + for e in filetree: + if isinstance(e, mobase.IFileTree) and e.isDir(): + return e + return None + def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): entriesToMove: list[mobase.FileTreeEntry] = [] retVal = 0 for e in filetree: - if e is not None: - entriesToMove.append(e) + entriesToMove.append(e) for e in entriesToMove: filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) retVal = 1 return retVal - def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: - northstarModPath = self.organizer.managedGame().GameNorthstarPath + "/" + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + GameNorthstarPath = getattr(self.organizer.managedGame(), "GameNorthstarPath", "") + "/" treefixed = 0 - if filetree.exists("mod.json", mobase.IFileTree.FILE): - treefixed = self.allMoveTo(filetree, northstarModPath + "FOLDERNAME/") - if treefixed == 1: - self.needsNameFix = True - elif self.fileExistsInNextSubDir(filetree, "mod.json"): - filetree.move(filetree[0], northstarModPath, mobase.IFileTree.MERGE) - treefixed = 1 - else: - try: - if filetree[0][0].exists("mod.json", mobase.IFileTree.FILE): - filetree.move( - filetree[0][0], filetree[0].path("/"), mobase.IFileTree.REPLACE - ) - filetree.move(filetree[0], northstarModPath, mobase.IFileTree.MERGE) - treefixed = 1 - except TypeError: - pass - if treefixed == 0: - if len(filetree) == 1 and filetree[0].isDir: - filetree.move(filetree[0], northstarModPath, mobase.IFileTree.MERGE) + firsttreelayer: mobase.IFileTree | None = self.first_tree(filetree) + if firsttreelayer is not None: + secondtreelayer: mobase.IFileTree | None = self.first_tree(firsttreelayer) + if filetree.exists("mod.json", mobase.IFileTree.FILE): + treefixed = self.allMoveTo(filetree, GameNorthstarPath + "FOLDERNAME/") + if treefixed == 1: + self.needsNameFix = True + elif self.fileExistsInNextSubDir(filetree, "mod.json"): + filetree.move(firsttreelayer, GameNorthstarPath, mobase.IFileTree.MERGE) treefixed = 1 - else: - for e in filetree: - if e is not None and e.path("/").count("/") == 0: - filetree.move( - e, - northstarModPath + "FOLDERNAME_NAME/", - mobase.IFileTree.MERGE, - ) + if secondtreelayer is not None: + if secondtreelayer.exists("mod.json", mobase.IFileTree.FILE): + filetree.move(secondtreelayer, firsttreelayer.path("/"), mobase.IFileTree.REPLACE) + filetree.move(firsttreelayer, GameNorthstarPath, mobase.IFileTree.MERGE) + treefixed = 1 + elif len(filetree) == 1: + filetree.move(firsttreelayer, GameNorthstarPath, mobase.IFileTree.MERGE) treefixed = 1 - self.needsNameFix = True + else: + for e in filetree: + if e.path("/").count("/") == 0: + filetree.move(e,GameNorthstarPath + "FOLDERNAME_NAME/",mobase.IFileTree.MERGE) + treefixed = 1 + self.needsNameFix = True if treefixed == 0: return None return filetree @@ -219,9 +213,9 @@ def update_enable_mods_json(self, mods: dict[str, mobase.ModState]): key = self._organizer.modList().getMod(key) tree = key.fileTree() subtree = tree.find("R2Northstar/mods", mobase.IFileTree.DIRECTORY) - if subtree is not None and subtree.isDir(): + if isinstance(subtree, mobase.IFileTree): for e in subtree: - if e is not None and e.isDir(): + if isinstance(e, mobase.IFileTree): if e.exists("mod.json", mobase.IFileTree.FILE): json_path = ( key.absolutePath() + "/" + e.path() + "/mod.json" diff --git a/games/game_zuma_deluxe.py b/games/game_zuma_deluxe.py index a6c3bc73..39cd3377 100644 --- a/games/game_zuma_deluxe.py +++ b/games/game_zuma_deluxe.py @@ -37,7 +37,7 @@ def getAllContents(self) -> list[mobase.ModDataContent.Content]: for id, name, icon, *filter_only in self.GAMECONTENTS ] - contents = set() + contents: set[int] = set() def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): @@ -70,7 +70,7 @@ def __init__(self, organizer: mobase.IOrganizer): self.organizer.modList().onModInstalled(self._Fix_Installed_Mod) self.needsNameFix = False - def move_overwrite_merge(self, source, destination): + def move_overwrite_merge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -89,7 +89,7 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): filetree: mobase.IFileTree = mod.fileTree() fixed = False modname = mod.name() - if filetree is not None and filetree.exists( + if filetree.exists( "mods/FOLDERNAME", mobase.IFileTree.DIRECTORY ): path = mod.absolutePath() @@ -129,9 +129,9 @@ def dataLooksValid( def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): for branch in filetree: - if branch is not None and branch.isDir(): + if isinstance(branch, mobase.IFileTree): for e in branch: - if e is not None and e.name() == name: + if e.name() == name: return True return False @@ -139,15 +139,16 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): entriesToMove: list[mobase.FileTreeEntry] = [] retVal = 0 for e in filetree: - if e is not None: - entriesToMove.append(e) + entriesToMove.append(e) for e in entriesToMove: filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) retVal = 1 return retVal - def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: - GameLevelsPath = self.organizer.managedGame().GameLevelsPath + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + GameLevelsPath: str = str( + getattr(self.organizer.managedGame(), "GameLevelsPath", "levels") +) validFolders = [ "images", "levels", @@ -169,22 +170,22 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: else: moveonce = 0 for branch in filetree: - if branch is not None and branch.isDir(): + if isinstance(branch, mobase.IFileTree): for entry in branch: for folder in validFolders: - if entry is not None and entry.name() == folder: + if entry.name() == folder: moveonce = 1 if moveonce == 1: for branch in filetree: - if branch is not None and branch.isDir(): + if isinstance(branch, mobase.IFileTree): for entry in branch: entriesToMove.append(entry) - if entriesToMove is not None: + if entriesToMove: for e in entriesToMove: filetree.move(e, "", mobase.IFileTree.MERGE) treefixed = 1 for branch in filetree: - if branch is not None and branch.isDir(): + if isinstance(branch, mobase.IFileTree): if len(branch) == 0: filetree.remove(branch) if treefixed == 0: diff --git a/games/oblivion_remaster/paks/view.py b/games/oblivion_remaster/paks/view.py index a56b0cdd..f432d42d 100644 --- a/games/oblivion_remaster/paks/view.py +++ b/games/oblivion_remaster/paks/view.py @@ -29,5 +29,5 @@ def dropEvent(self, e: QDropEvent | None): def dataChanged( self, topLeft: QModelIndex, bottomRight: QModelIndex, roles: Iterable[int] = () ): - super().dataChanged(topLeft, bottomRight, roles) + super().dataChanged(topLeft, bottomRight, roles) # type: ignore self.repaint() diff --git a/games/oblivion_remaster/ue4ss/view.py b/games/oblivion_remaster/ue4ss/view.py index bb994ca6..61822841 100644 --- a/games/oblivion_remaster/ue4ss/view.py +++ b/games/oblivion_remaster/ue4ss/view.py @@ -27,5 +27,5 @@ def dropEvent(self, e: QDropEvent | None): def dataChanged( self, topLeft: QModelIndex, bottomRight: QModelIndex, roles: Iterable[int] = () ): - super().dataChanged(topLeft, bottomRight, roles) + super().dataChanged(topLeft, bottomRight, roles) # type: ignore self.repaint() diff --git a/games/oblivion_remaster/ue4ss/widget.py b/games/oblivion_remaster/ue4ss/widget.py index 246b7fa0..071071ef 100644 --- a/games/oblivion_remaster/ue4ss/widget.py +++ b/games/oblivion_remaster/ue4ss/widget.py @@ -108,9 +108,7 @@ def _parse_mod_files(self): game = self._organizer.managedGame() if isinstance(game, OblivionRemasteredGame): if game.ue4ssDirectory().exists(): - for dir_info in game.ue4ssDirectory().entryInfoList( - QDir.Filter.Dirs | QDir.Filter.NoDotAndDotDot - ): + for dir_info in game.ue4ssDirectory().entryInfoList(QDir.Filter.Dirs | QDir.Filter.NoDotAndDotDot): # type: ignore if QFileInfo( QDir(dir_info.absoluteFilePath()).absoluteFilePath( "scripts/main.lua" diff --git a/games/unreal_tabs/manage_paks/model.py b/games/unreal_tabs/manage_paks/model.py index b8f72d2c..2edeb7b4 100644 --- a/games/unreal_tabs/manage_paks/model.py +++ b/games/unreal_tabs/manage_paks/model.py @@ -57,14 +57,12 @@ def flags(self, index: QModelIndex) -> Qt.ItemFlag: | Qt.ItemFlag.ItemIsDropEnabled & Qt.ItemFlag.ItemIsEditable ) - def columnCount(self, parent: QModelIndex = None) -> int: + def columnCount(self, parent: QModelIndex | None = None) -> int: if parent is None: parent = QModelIndex() return len(PaksColumns) - def index( - self, row: int, column: int, parent: QModelIndex = None - ) -> QModelIndex: + def index(self, row: int, column: int, parent: QModelIndex | None = None) -> QModelIndex: if parent is None: parent = QModelIndex() if ( @@ -86,7 +84,7 @@ def parent(self, child: QModelIndex | None = None) -> QModelIndex | QObject | No return super().parent() return QModelIndex() - def rowCount(self, parent: QModelIndex = None) -> int: + def rowCount(self, parent: QModelIndex | None = None) -> int: if parent is None: parent = QModelIndex() return len(self.paks) diff --git a/games/unreal_tabs/manage_paks/view.py b/games/unreal_tabs/manage_paks/view.py index a56b0cdd..05681ff4 100644 --- a/games/unreal_tabs/manage_paks/view.py +++ b/games/unreal_tabs/manage_paks/view.py @@ -29,5 +29,5 @@ def dropEvent(self, e: QDropEvent | None): def dataChanged( self, topLeft: QModelIndex, bottomRight: QModelIndex, roles: Iterable[int] = () ): - super().dataChanged(topLeft, bottomRight, roles) + super().dataChanged(topLeft, bottomRight, roles) # type: ignore self.repaint() diff --git a/games/unreal_tabs/manage_paks/widget.py b/games/unreal_tabs/manage_paks/widget.py index c0788274..5c6fcf2c 100644 --- a/games/unreal_tabs/manage_paks/widget.py +++ b/games/unreal_tabs/manage_paks/widget.py @@ -115,6 +115,10 @@ def _shake_paks(self, sorted_paks: dict[str, str]) -> list[str]: def _parse_pak_files(self): game = self._organizer.managedGame() mods = self._organizer.modList().allMods() + pak_mods = None + data_pak_mods = getattr(game, "GameDataPakMods", None) + data_path = game.dataDirectory() + pak_dir = None paks: dict[str, str] = {} pak_paths: dict[str, tuple[str, str]] = {} pak_source: dict[str, str] = {} @@ -123,7 +127,8 @@ def _parse_pak_files(self): if not self._organizer.modList().state(mod) & mobase.ModState.ACTIVE: continue filetree = mod_item.fileTree() - pak_mods = filetree.find(game.GameDataPakMods) + if data_pak_mods: + pak_mods = filetree.find(data_pak_mods) if isinstance(pak_mods, mobase.IFileTree): for entry in pak_mods: if is_directory(entry): @@ -156,47 +161,44 @@ def _parse_pak_files(self): mod_item.absolutePath() + "/" + pak_mods.path("/"), ) pak_source[pak_name] = mod_item.name() - pak_mods = QFileInfo(game.paksDirectory().absolutePath()) - if pak_mods.exists() and pak_mods.isDir(): - for entry in QDir(pak_mods.absoluteFilePath()).entryInfoList( - QDir.Filter.Dirs | QDir.Filter.Files | QDir.Filter.NoDotAndDotDot - ): - if entry.isDir(): - for sub_entry in QDir(entry.absoluteFilePath()).entryInfoList( - QDir.Filter.Files - ): - if ( - sub_entry.isFile() - and sub_entry.suffix().casefold() == "pak" - ): - pak_name = sub_entry.completeBaseName() - paks[pak_name] = entry.completeBaseName() + if data_path and data_pak_mods: + pak_dir = QFileInfo(data_path.absolutePath() + "/" + data_pak_mods) + if pak_dir.exists() and pak_dir.isDir(): + for entry in QDir(pak_dir.absoluteFilePath()).entryInfoList(QDir.Filter.Dirs | QDir.Filter.Files | QDir.Filter.NoDotAndDotDot): # type: ignore + if entry.isDir(): + for sub_entry in QDir(entry.absoluteFilePath()).entryInfoList(QDir.Filter.Files): # type: ignore + if ( + sub_entry.isFile() + and sub_entry.suffix().casefold() == "pak" + ): + pak_name = sub_entry.completeBaseName() + paks[pak_name] = entry.completeBaseName() + pak_paths[pak_name] = ( + sub_entry.absolutePath(), + pak_dir.absolutePath(), + ) + pak_source[pak_name] = "Game Directory" + else: + if entry.suffix().casefold() == "pak": + pak_name = entry.completeBaseName() + paks[pak_name] = "" pak_paths[pak_name] = ( - sub_entry.absolutePath(), - pak_mods.absolutePath(), + entry.absolutePath(), + pak_dir.absolutePath(), ) pak_source[pak_name] = "Game Directory" - else: - if entry.suffix().casefold() == "pak": - pak_name = entry.completeBaseName() - paks[pak_name] = "" - pak_paths[pak_name] = ( - entry.absolutePath(), - pak_mods.absolutePath(), - ) - pak_source[pak_name] = "Game Directory" - sorted_paks = dict(sorted(paks.items(), key=cmp_to_key(pak_sort))) - shaken_paks: list[str] = self._shake_paks(sorted_paks) - final_paks: dict[str, tuple[str, str, str]] = {} - pak_index = 8999 - for pak in shaken_paks: - target_dir = pak_paths[pak][1] + "/" + str(pak_index).zfill(4) - final_paks[pak] = (pak_source[pak], pak_paths[pak][0], target_dir) - pak_index -= 1 - new_data_paks: dict[int, tuple[str, str, str, str]] = {} - i = 0 - for pak, data in final_paks.items(): - source, current_path, target_path = data - new_data_paks[i] = (pak, source, current_path, target_path) - i += 1 - self._model.set_paks(new_data_paks) + sorted_paks = dict(sorted(paks.items(), key=cmp_to_key(pak_sort))) + shaken_paks: list[str] = self._shake_paks(sorted_paks) + final_paks: dict[str, tuple[str, str, str]] = {} + pak_index = 8999 + for pak in shaken_paks: + target_dir = pak_paths[pak][1] + "/" + str(pak_index).zfill(4) + final_paks[pak] = (pak_source[pak], pak_paths[pak][0], target_dir) + pak_index -= 1 + new_data_paks: dict[int, tuple[str, str, str, str]] = {} + i = 0 + for pak, data in final_paks.items(): + source, current_path, target_path = data + new_data_paks[i] = (pak, source, current_path, target_path) + i += 1 + self._model.set_paks(new_data_paks) diff --git a/games/unreal_tabs/manage_ue4ss/model.py b/games/unreal_tabs/manage_ue4ss/model.py index b3431353..8909d3aa 100644 --- a/games/unreal_tabs/manage_ue4ss/model.py +++ b/games/unreal_tabs/manage_ue4ss/model.py @@ -10,7 +10,7 @@ class UE4SSListModel(QStringListModel): def __init__(self, parent: QWidget | None, organizer: mobase.IOrganizer): - super().__init__(parent) + super().__init__(parent) # type: ignore self._checked_items: set[str] = set() self._organizer = organizer self._init_mod_states() @@ -86,8 +86,8 @@ def setData( self.dataChanged.emit(index, index, [role]) return True - def setStringList(self, strings: Iterable[str | None]): - super().setStringList(strings) + def setStringList(self, strings: Iterable[str | None]) -> None: + super().setStringList(strings) # type: ignore self._set_mod_states() def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any: diff --git a/games/unreal_tabs/manage_ue4ss/view.py b/games/unreal_tabs/manage_ue4ss/view.py index bb994ca6..61822841 100644 --- a/games/unreal_tabs/manage_ue4ss/view.py +++ b/games/unreal_tabs/manage_ue4ss/view.py @@ -27,5 +27,5 @@ def dropEvent(self, e: QDropEvent | None): def dataChanged( self, topLeft: QModelIndex, bottomRight: QModelIndex, roles: Iterable[int] = () ): - super().dataChanged(topLeft, bottomRight, roles) + super().dataChanged(topLeft, bottomRight, roles) # type: ignore self.repaint() diff --git a/games/unreal_tabs/manage_ue4ss/widget.py b/games/unreal_tabs/manage_ue4ss/widget.py index f4cbcce1..47b874e0 100644 --- a/games/unreal_tabs/manage_ue4ss/widget.py +++ b/games/unreal_tabs/manage_ue4ss/widget.py @@ -43,6 +43,7 @@ def update_mod_files( self, mods: dict[str, mobase.ModState] | mobase.IModInterface | str ): game = self._organizer.managedGame() + game_data_ue4ss_mods = getattr(game, "GameDataUE4SSMods", None) mod_list: list[mobase.IModInterface] = [] if isinstance(mods, dict): for mod in mods.keys(): @@ -54,7 +55,7 @@ def update_mod_files( for mod in mod_list: tree = mod.fileTree() - ue4ss_files = tree.find(game.GameDataUE4SSMods) + ue4ss_files = tree.find(game_data_ue4ss_mods) if isinstance(game_data_ue4ss_mods, str) else None if isinstance(ue4ss_files, mobase.IFileTree): for entry in ue4ss_files: if isinstance(entry, mobase.IFileTree): @@ -69,6 +70,9 @@ def update_mod_files( def _parse_mod_files(self): game = self._organizer.managedGame() + data_ue4ss_mods = getattr(game, "GameDataUE4SSMods", None) + data_path = game.dataDirectory() + ue4ss_dir = None mod_list: set[str] = set() for mod in self._organizer.modList().allMods(): if ( @@ -76,7 +80,7 @@ def _parse_mod_files(self): & mobase.ModState.ACTIVE ): tree = self._organizer.modList().getMod(mod).fileTree() - ue4ss_files = tree.find(game.GameDataUE4SSMods) + ue4ss_files = tree.find(data_ue4ss_mods) if isinstance(data_ue4ss_mods, str) else None if isinstance(ue4ss_files, mobase.IFileTree): for entry in ue4ss_files: if isinstance(entry, mobase.IFileTree): @@ -97,21 +101,16 @@ def _parse_mod_files(self): ) except FileNotFoundError: pass - - if game.ue4ssDirectory().exists(): - for dir_info in game.ue4ssDirectory().entryInfoList( - QDir.Filter.Dirs | QDir.Filter.NoDotAndDotDot - ): - if QFileInfo( - QDir(dir_info.absoluteFilePath()).absoluteFilePath( - "scripts/main.lua" - ) - ).exists(): - mod_list.add(dir_info.fileName()) - if QFileInfo( - QDir(dir_info.absoluteFilePath()).absoluteFilePath("enabled.txt") - ).exists(): - Path(dir_info.absoluteFilePath(), "enabled.txt").unlink() + if data_path and data_ue4ss_mods: + ue4ss_dir = QDir(data_path.absolutePath() + "/" + data_ue4ss_mods) + if ue4ss_dir.exists(): + for dir_info in ue4ss_dir.entryInfoList(QDir.Filter.Dirs | QDir.Filter.NoDotAndDotDot):# type: ignore + if QFileInfo(QDir(dir_info.absoluteFilePath()).absoluteFilePath("scripts/main.lua")).exists() or QFileInfo(QDir(dir_info.absoluteFilePath()).absoluteFilePath("dlls/main.dll")).exists(): + mod_list.add(dir_info.fileName()) + if QFileInfo( + QDir(dir_info.absoluteFilePath()).absoluteFilePath("enabled.txt") + ).exists(): + Path(dir_info.absoluteFilePath(), "enabled.txt").unlink() final_list = sorted(mod_list, key=cmp_to_key(self.sort_mods)) self._model.setStringList(final_list) From 3f4719df7ccf62f98c10ba3ea6379f9100cfb578 Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Tue, 14 Apr 2026 00:40:44 +0200 Subject: [PATCH 19/21] Ruff Formatting --- games/game_cassettebeasts.py | 82 ++++++++++------ games/game_crimeboss.py | 118 +++++++++++++++++------ games/game_emuvr.py | 58 ++++++++--- games/game_hitman3.py | 84 +++++++++++----- games/game_noita.py | 11 ++- games/game_ovkwalkingdead.py | 96 +++++++++++++----- games/game_pacificdrive.py | 98 ++++++++++++++----- games/game_payday1.py | 90 ++++++++++++----- games/game_payday2.py | 89 ++++++++++++----- games/game_payday3.py | 77 +++++++++++---- games/game_raid2.py | 41 +++++--- games/game_roadtovostok.py | 30 ++++-- games/game_silenthill2remake.py | 93 ++++++++++++++---- games/game_titanfall2.py | 48 ++++++--- games/game_zuma_deluxe.py | 15 ++- games/unreal_tabs/manage_paks/model.py | 24 ++++- games/unreal_tabs/manage_paks/widget.py | 14 ++- games/unreal_tabs/manage_ue4ss/model.py | 10 +- games/unreal_tabs/manage_ue4ss/widget.py | 38 ++++++-- 19 files changed, 818 insertions(+), 298 deletions(-) diff --git a/games/game_cassettebeasts.py b/games/game_cassettebeasts.py index 5f965c54..5257a2be 100644 --- a/games/game_cassettebeasts.py +++ b/games/game_cassettebeasts.py @@ -1,21 +1,25 @@ -from collections.abc import Mapping, Sequence -from datetime import datetime -from functools import cached_property -from io import BytesIO -from typing import Any -from pathlib import Path import json import math import os import shutil import struct import zlib +from collections.abc import Mapping, Sequence +from datetime import datetime +from functools import cached_property +from io import BytesIO +from pathlib import Path +from typing import Any -import mobase from PyQt6.QtCore import QDir, QFileInfo +import mobase + from ..basic_features import BasicLocalSavegames -from ..basic_features.basic_save_game_info import (BasicGameSaveGame,BasicGameSaveGameInfo) +from ..basic_features.basic_save_game_info import ( + BasicGameSaveGame, + BasicGameSaveGameInfo, +) from ..basic_game import BasicGame @@ -26,14 +30,17 @@ def json_get_me(value: Any, path: Sequence[str | int], /, default: Any) -> Any: value = value[part] return value + class CassetteBeastsModDataChecker(mobase.ModDataChecker): def __init__(self, organizer: mobase.IOrganizer): super().__init__() self.organizer: mobase.IOrganizer = organizer - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: for e in filetree: - if e.suffix().casefold() == "pck": + if e.suffix().casefold() == "pck": return mobase.ModDataChecker.VALID return mobase.ModDataChecker.FIXABLE @@ -45,9 +52,16 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: if mod_name == "": mod_name = branch.name() mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if not filetree.createOrphanTree("OrphanTree") and os.path.exists(mod_path) and branch.suffix().casefold() == "pck": + if ( + not filetree.createOrphanTree("OrphanTree") + and os.path.exists(mod_path) + and branch.suffix().casefold() == "pck" + ): os.makedirs(os.path.join(mod_path, GameDataPath), exist_ok=True) - shutil.move(os.path.join(mod_path, branch.name()), os.path.join(mod_path, GameDataPath, branch.name())) + shutil.move( + os.path.join(mod_path, branch.name()), + os.path.join(mod_path, GameDataPath, branch.name()), + ) treefixed = 1 else: if isinstance(branch, mobase.IFileTree): @@ -56,15 +70,17 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: filetree.move(e, GameDataPath, mobase.IFileTree.MERGE) treefixed = 1 elif branch.suffix().casefold() == "pck": - filetree.move(branch, GameDataPath, mobase.IFileTree.MERGE) - treefixed = 1 + filetree.move(branch, GameDataPath, mobase.IFileTree.MERGE) + treefixed = 1 if treefixed == 0: return None return filetree + class CassetteBlock: compressed_size: int = 0 - data: bytes = b'' + data: bytes = b"" + class CassetteBeastsSaveGame(BasicGameSaveGame): def __init__(self, filepath: Path): @@ -81,7 +97,7 @@ def __init__(self, filepath: Path): try: info = bytearray() data = bytes() - with open(filepath, 'rb') as infile: + with open(filepath, "rb") as infile: infile.read(4) blocksize, raw_size = struct.unpack("III", infile.read(12)) @@ -106,7 +122,7 @@ def __init__(self, filepath: Path): save_data = json.load(BytesIO(info)) except (OSError, struct.error, ValueError) as err: s = str(err) - self.errorMessage = ('{0}: {1}' if s else '{0}').format( + self.errorMessage = ("{0}: {1}" if s else "{0}").format( err.__class__.__name__, s ) return @@ -120,13 +136,14 @@ def __init__(self, filepath: Path): except OSError: pass else: - self.lastsave = "{0:d}-{1:02d}-{2:02d} at {3:02d}:{4:02d}:{5:02d}".format( - dt.year, dt.month, dt.day, - dt.hour, dt.minute, dt.second + self.lastsave = ( + "{0:d}-{1:02d}-{2:02d} at {3:02d}:{4:02d}:{5:02d}".format( + dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second + ) ) x = json_get_me(save_data, ["play_time"], None) if type(x) in (int, float): - a = [ 0, 0, 0, int(x * 10) ] + a = [0, 0, 0, int(x * 10)] a[2:4] = divmod(a[3], 10) a[1:3] = divmod(a[2], 60) a[0:2] = divmod(a[1], 60) @@ -147,6 +164,7 @@ def getLastSaved(self) -> str: def getPlayTime(self) -> str: return self.elapsed + def getMetadata(p: Path, save: mobase.ISaveGame) -> Mapping[str, str] | None: err = getattr(save, "errorMessage", "") if err: @@ -154,12 +172,12 @@ def getMetadata(p: Path, save: mobase.ISaveGame) -> Mapping[str, str] | None: # If this is our concrete save-game class, the type checker knows the methods. if isinstance(save, BasicGameSaveGame): - return + return { "Character": save.getName(), "Last Saved": save.getLastSaved(), "Play Time": save.getPlayTime(), - "Cheated": save.getCheated() + "Cheated": save.getCheated(), } else: return None @@ -182,10 +200,8 @@ def init(self, organizer: mobase.IOrganizer) -> bool: super().init(organizer) self.dataChecker = CassetteBeastsModDataChecker(organizer) self._register_feature(self.dataChecker) - self._register_feature(BasicLocalSavegames(QDir(self.GameSavesDirectory))) # type: ignore - self._register_feature( - BasicGameSaveGameInfo(None, getMetadata) - ) + self._register_feature(BasicLocalSavegames(QDir(self.GameSavesDirectory))) # type: ignore + self._register_feature(BasicGameSaveGameInfo(None, getMetadata)) return True def executables(self): @@ -218,7 +234,9 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: except AttributeError: efls = [] libs: set[str] = set() - tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = ( + self._organizer.virtualFileTree() + ) if type(tree) is not mobase.IFileTree: return efls for e in tree: @@ -226,7 +244,13 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: libs.add(relpath) exes = self.executables() - efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + efls = efls + [ + mobase.ExecutableForcedLoadSetting( + exe.binary().fileName(), lib + ).withEnabled(True) + for lib in libs + for exe in exes + ] return efls def iniFiles(self): diff --git a/games/game_crimeboss.py b/games/game_crimeboss.py index d030bb22..bf8f6106 100644 --- a/games/game_crimeboss.py +++ b/games/game_crimeboss.py @@ -1,21 +1,20 @@ import json import os import shutil -import mobase - from enum import IntEnum, auto -from pathlib import Path from functools import cached_property +from pathlib import Path + +from PyQt6.QtCore import QDir, QFileInfo +from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget +import mobase + +from ..basic_game import BasicGame from .unreal_tabs.constants import DEFAULT_UE4SS_MODS, UE4SSModInfo from .unreal_tabs.manage_paks.widget import PaksTabWidget from .unreal_tabs.manage_ue4ss.widget import UE4SSTabWidget -from ..basic_game import BasicGame - -from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget -from PyQt6.QtCore import QDir, QFileInfo - class Content(IntEnum): UCAS = auto() @@ -38,7 +37,10 @@ class CrimeBossModDataContent(mobase.ModDataContent): ] def getAllContents(self) -> list[mobase.ModDataContent.Content]: - return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + return [ + mobase.ModDataContent.Content(id, name, icon, *filter_only) + for id, name, icon, *filter_only in self.GAMECONTENTS + ] def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): @@ -88,11 +90,15 @@ def move_overwrite_merge(self, source: str, destination: str): def _Fix_Installed_Mod(self, mod: mobase.IModInterface): if not self.needsNameFix: return - GameDataNativeMods = getattr(self.organizer.managedGame(), "GameDataNativeMods", "") + GameDataNativeMods = getattr( + self.organizer.managedGame(), "GameDataNativeMods", "" + ) filetree: mobase.IFileTree = mod.fileTree() fixed = False modname = mod.name() - if filetree.exists(GameDataNativeMods + "/FOLDERNAME", mobase.IFileTree.DIRECTORY): + if filetree.exists( + GameDataNativeMods + "/FOLDERNAME", mobase.IFileTree.DIRECTORY + ): path = mod.absolutePath() old_path = os.path.join(path, GameDataNativeMods + "/FOLDERNAME") new_path = os.path.join(path, GameDataNativeMods + f"/{modname}") @@ -102,16 +108,26 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): return self.needsNameFix = False - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: - GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + GameDataUE4SSMods = getattr( + self.organizer.managedGame(), "GameDataUE4SSMods", "" + ) GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") - GameDataNativeMods = getattr(self.organizer.managedGame(), "GameDataNativeMods", "") + GameDataNativeMods = getattr( + self.organizer.managedGame(), "GameDataNativeMods", "" + ) GameDataMovies = getattr(self.organizer.managedGame(), "GameDataMovies", "") if filetree.exists(GameDataPakMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID - if filetree.exists(os.path.dirname(GameDataUE4SSMods), mobase.IFileTree.DIRECTORY): + if filetree.exists( + os.path.dirname(GameDataUE4SSMods), mobase.IFileTree.DIRECTORY + ): return mobase.ModDataChecker.VALID - if filetree.exists(GameDataNativeMods, mobase.IFileTree.DIRECTORY) and not filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): + if filetree.exists( + GameDataNativeMods, mobase.IFileTree.DIRECTORY + ) and not filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): return mobase.ModDataChecker.VALID if filetree.exists(GameDataMovies, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID @@ -136,13 +152,23 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): return retVal def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: - GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + "/" - GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") + "/" - GameDataNativeMods = getattr(self.organizer.managedGame(), "GameDataNativeMods", "") + "/" - GameDataMovies = getattr(self.organizer.managedGame(), "GameDataMovies", "") + "/" + GameDataUE4SSMods = ( + getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + "/" + ) + GameDataPakMods = ( + getattr(self.organizer.managedGame(), "GameDataPakMods", "") + "/" + ) + GameDataNativeMods = ( + getattr(self.organizer.managedGame(), "GameDataNativeMods", "") + "/" + ) + GameDataMovies = ( + getattr(self.organizer.managedGame(), "GameDataMovies", "") + "/" + ) treefixed = 0 if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): - treefixed = self.allMoveTo(filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/") + treefixed = self.allMoveTo( + filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/" + ) if treefixed == 1: return filetree if filetree.exists("Content", mobase.IFileTree.DIRECTORY): @@ -163,14 +189,32 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: if mod_name == "": mod_name = e.name() mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if not filetree.createOrphanTree("OrphanTree") and os.path.exists(mod_path): + if not filetree.createOrphanTree( + "OrphanTree" + ) and os.path.exists(mod_path): match e.suffix().casefold(): case "pak" | "utoc" | "ucas": - os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) + os.makedirs( + os.path.join(mod_path, GameDataPakMods), + exist_ok=True, + ) + shutil.move( + os.path.join(mod_path, e.name()), + os.path.join( + mod_path, GameDataPakMods, e.name() + ), + ) case "bk2": - os.makedirs(os.path.join(mod_path, GameDataMovies), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovies, e.name())) + os.makedirs( + os.path.join(mod_path, GameDataMovies), + exist_ok=True, + ) + shutil.move( + os.path.join(mod_path, e.name()), + os.path.join( + mod_path, GameDataMovies, e.name() + ), + ) case _: pass treefixed = 1 @@ -182,7 +226,11 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: case "pak" | "utoc" | "ucas": filetree.move(e, GameDataPakMods, mobase.IFileTree.MERGE) case "dll": - filetree.move(e, os.path.dirname(GameDataUE4SSMods) + "/", mobase.IFileTree.MERGE) + filetree.move( + e, + os.path.dirname(GameDataUE4SSMods) + "/", + mobase.IFileTree.MERGE, + ) case "bk2": filetree.move(e, GameDataMovies, mobase.IFileTree.MERGE) case _: @@ -205,7 +253,9 @@ class CrimeBossGame(BasicGame): GameDataUE4SSMods = "Binaries/Win64/Mods" GameDataNativeMods = "Mods" GameDataPakMods = "Content/Paks/~Mods" - GameDocumentsDirectory = "%USERPROFILE%/Saved Games/CrimeBoss/Steam/Saved/Config/WindowsNoEditor" + GameDocumentsDirectory = ( + "%USERPROFILE%/Saved Games/CrimeBoss/Steam/Saved/Config/WindowsNoEditor" + ) GameSaveExtension = "sav" _main_window: QMainWindow _ue4ss_tab: UE4SSTabWidget @@ -255,7 +305,9 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: except AttributeError: efls = [] libs: set[str] = set() - tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = ( + self._organizer.virtualFileTree() + ) if type(tree) is not mobase.IFileTree: return efls for e in tree: @@ -263,7 +315,13 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: libs.add(relpath) exes = self.executables() - efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + efls = efls + [ + mobase.ExecutableForcedLoadSetting( + exe.binary().fileName(), lib + ).withEnabled(True) + for lib in libs + for exe in exes + ] return efls def paksDirectory(self) -> QDir: diff --git a/games/game_emuvr.py b/games/game_emuvr.py index 744c4b78..f5f26c90 100644 --- a/games/game_emuvr.py +++ b/games/game_emuvr.py @@ -1,37 +1,48 @@ import os import shutil -import mobase - -from pathlib import Path from functools import cached_property - -from ..basic_game import BasicGame +from pathlib import Path from PyQt6.QtCore import QDir, QFileInfo +import mobase + +from ..basic_game import BasicGame + class EmuVRModDataChecker(mobase.ModDataChecker): def __init__(self, organizer: mobase.IOrganizer): super().__init__() self.organizer: mobase.IOrganizer = organizer - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: GameDataUGCMods = getattr(self.organizer.managedGame(), "GameDataUGCMods", "") if filetree.exists(GameDataUGCMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID return mobase.ModDataChecker.FIXABLE def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: - GameDataUGCMods = getattr(self.organizer.managedGame(), "GameDataUGCMods", "") + "/" + GameDataUGCMods = ( + getattr(self.organizer.managedGame(), "GameDataUGCMods", "") + "/" + ) treefixed = 0 for branch in filetree: mod_name = filetree.name() if mod_name == "": mod_name = branch.name() mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if not filetree.createOrphanTree("OrphanTree") and os.path.exists(mod_path) and branch.suffix().casefold() == "ugc": + if ( + not filetree.createOrphanTree("OrphanTree") + and os.path.exists(mod_path) + and branch.suffix().casefold() == "ugc" + ): os.makedirs(os.path.join(mod_path, GameDataUGCMods), exist_ok=True) - shutil.move(os.path.join(mod_path, branch.name()), os.path.join(mod_path, GameDataUGCMods, branch.name())) + shutil.move( + os.path.join(mod_path, branch.name()), + os.path.join(mod_path, GameDataUGCMods, branch.name()), + ) treefixed = 1 else: if isinstance(branch, mobase.IFileTree): @@ -71,10 +82,19 @@ def executables(self): "Emu VR", QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), ), - mobase.ExecutableInfo("Force SteamVR", QFileInfo(self.gameDirectory(), "Force SteamVR.exe")), - mobase.ExecutableInfo("Force Oculus", QFileInfo(self.gameDirectory(), "Force Oculus.exe")), - mobase.ExecutableInfo("Force Virtual Desktop Streamer", QFileInfo(self.gameDirectory(), "Force Virtual Desktop Streamer.exe")), - mobase.ExecutableInfo("Force Desktop", QFileInfo(self.gameDirectory(), "Force Desktop.exe")), + mobase.ExecutableInfo( + "Force SteamVR", QFileInfo(self.gameDirectory(), "Force SteamVR.exe") + ), + mobase.ExecutableInfo( + "Force Oculus", QFileInfo(self.gameDirectory(), "Force Oculus.exe") + ), + mobase.ExecutableInfo( + "Force Virtual Desktop Streamer", + QFileInfo(self.gameDirectory(), "Force Virtual Desktop Streamer.exe"), + ), + mobase.ExecutableInfo( + "Force Desktop", QFileInfo(self.gameDirectory(), "Force Desktop.exe") + ), ] def iniFiles(self): @@ -91,7 +111,9 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: except AttributeError: efls = [] libs: set[str] = set() - tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = ( + self._organizer.virtualFileTree() + ) if type(tree) is not mobase.IFileTree: return efls for e in tree: @@ -99,7 +121,13 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: libs.add(relpath) exes = self.executables() - efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + efls = efls + [ + mobase.ExecutableForcedLoadSetting( + exe.binary().fileName(), lib + ).withEnabled(True) + for lib in libs + for exe in exes + ] return efls def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): diff --git a/games/game_hitman3.py b/games/game_hitman3.py index 996ab66a..7003538d 100644 --- a/games/game_hitman3.py +++ b/games/game_hitman3.py @@ -1,15 +1,15 @@ +import json import os import shutil -import json -import mobase - -from pathlib import Path from functools import cached_property - -from ..basic_game import BasicGame +from pathlib import Path from PyQt6.QtCore import QDir, QFileInfo +import mobase + +from ..basic_game import BasicGame + class Hitman3ModDataChecker(mobase.ModDataChecker): def __init__(self, organizer: mobase.IOrganizer): @@ -37,11 +37,15 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): GameSMMPath = getattr(self.organizer.managedGame(), "GameSMMPath", "") filetree: mobase.IFileTree = mod.fileTree() fixed = False - if filetree.exists(GameSMMPath + "/Mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): + if filetree.exists( + GameSMMPath + "/Mods/FOLDERNAME", mobase.IFileTree.DIRECTORY + ): print("Found folder") path = mod.absolutePath() print(path) - json_path = os.path.join(path, GameSMMPath + "/Mods/FOLDERNAME/manifest.json") + json_path = os.path.join( + path, GameSMMPath + "/Mods/FOLDERNAME/manifest.json" + ) print(json_path) mod_data = json.load(open(json_path, encoding="utf-8")) modname = mod_data["id"] @@ -53,7 +57,9 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): return self.needsNameFix = False - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: if filetree.exists("Simple Mod Framework", mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID return mobase.ModDataChecker.FIXABLE @@ -66,7 +72,9 @@ def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): return True return False - def allMoveTo(self, sourcetree: mobase.IFileTree, targettree: mobase.IFileTree, toMoveTo: str): + def allMoveTo( + self, sourcetree: mobase.IFileTree, targettree: mobase.IFileTree, toMoveTo: str + ): entriesToMove: list[mobase.FileTreeEntry] = [] retVal = 0 for e in sourcetree: @@ -89,7 +97,9 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: if filetree.exists("manifest.json", mobase.IFileTree.FILE): print("Found manifest in root, moving to SMM folder") print(GameSMMPath + "/Mods/FOLDERNAME/") - treefixed = self.allMoveTo(filetree, filetree, GameSMMPath + "/Mods/FOLDERNAME/") + treefixed = self.allMoveTo( + filetree, filetree, GameSMMPath + "/Mods/FOLDERNAME/" + ) if treefixed == 1: self.needsNameFix = True elif len(filetree) == 1: @@ -97,7 +107,9 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: if firsttreelayer is not None: if firsttreelayer.exists("manifest.json", mobase.IFileTree.FILE): print(GameSMMPath + "/Mods/FOLDERNAME/") - treefixed = self.allMoveTo(firsttreelayer, filetree, GameSMMPath + "/Mods/FOLDERNAME/") + treefixed = self.allMoveTo( + firsttreelayer, filetree, GameSMMPath + "/Mods/FOLDERNAME/" + ) if treefixed == 1: self.needsNameFix = True if treefixed == 0: @@ -134,7 +146,9 @@ def update_smm_meta(self, mods: dict[str, mobase.ModState]): for e in subtree: if isinstance(e, mobase.IFileTree): if e.exists("manifest.json", mobase.IFileTree.FILE): - json_path = key.absolutePath() + "/" + e.path() + "/manifest.json" + json_path = ( + key.absolutePath() + "/" + e.path() + "/manifest.json" + ) mod_data = json.load(open(json_path, encoding="utf-8")) modname = mod_data["id"] if value == 35: @@ -147,13 +161,21 @@ def update_smm_meta(self, mods: dict[str, mobase.ModState]): config_json_content = bad_code if modname not in config_json_content: substr = "knownMods:[" - config_json_content = config_json_content.replace(substr, substr + "'" + modname + "',") + config_json_content = config_json_content.replace( + substr, substr + "'" + modname + "'," + ) substr = "loadOrder:[" - config_json_content = config_json_content.replace(substr, substr + "'" + modname + "',") + config_json_content = config_json_content.replace( + substr, substr + "'" + modname + "'," + ) substr = ",],modOptions" - config_json_content = config_json_content.replace(substr, "],modOptions") + config_json_content = config_json_content.replace( + substr, "],modOptions" + ) substr = ",],developer" - config_json_content = config_json_content.replace(substr, "],developer") + config_json_content = config_json_content.replace( + substr, "],developer" + ) with open(SMM_Config_Json, "w") as config_json: config_json.write(config_json_content) config_json.close() @@ -163,12 +185,20 @@ def update_smm_meta(self, mods: dict[str, mobase.ModState]): config_json_content = config_json.read() config_json.close() if modname in config_json_content: - config_json_content = config_json_content.replace("'" + modname + "',", "") - config_json_content = config_json_content.replace(",,", ",") + config_json_content = config_json_content.replace( + "'" + modname + "',", "" + ) + config_json_content = config_json_content.replace( + ",,", "," + ) substr = ",],modOptions" - config_json_content = config_json_content.replace(substr, "],modOptions") + config_json_content = config_json_content.replace( + substr, "],modOptions" + ) substr = ",],developer" - config_json_content = config_json_content.replace(substr, "],developer") + config_json_content = config_json_content.replace( + substr, "],developer" + ) with open(SMM_Config_Json, "w") as config_json: config_json.write(config_json_content) config_json.close() @@ -214,7 +244,9 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: except AttributeError: efls = [] libs: set[str] = set() - tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = ( + self._organizer.virtualFileTree() + ) if type(tree) is not mobase.IFileTree: return efls for e in tree: @@ -222,7 +254,13 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: libs.add(relpath) exes = self.executables() - efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + efls = efls + [ + mobase.ExecutableForcedLoadSetting( + exe.binary().fileName(), lib + ).withEnabled(True) + for lib in libs + for exe in exes + ] return efls def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): diff --git a/games/game_noita.py b/games/game_noita.py index ca2cac04..78b9a993 100644 --- a/games/game_noita.py +++ b/games/game_noita.py @@ -1,11 +1,12 @@ -from functools import cached_property -from pathlib import Path import os import shutil +from functools import cached_property +from pathlib import Path -import mobase from PyQt6.QtCore import QDir, QFileInfo +import mobase + from ..basic_game import BasicGame @@ -46,7 +47,9 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): return self.needsNameFix = False - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: if filetree.exists("mods", mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID return mobase.ModDataChecker.FIXABLE diff --git a/games/game_ovkwalkingdead.py b/games/game_ovkwalkingdead.py index b702d51e..050e2b26 100644 --- a/games/game_ovkwalkingdead.py +++ b/games/game_ovkwalkingdead.py @@ -1,14 +1,15 @@ -from enum import IntEnum, auto -from functools import cached_property -from pathlib import Path import json import os import shutil +from enum import IntEnum, auto +from functools import cached_property +from pathlib import Path -import mobase from PyQt6.QtCore import QDir, QFileInfo from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget +import mobase + from ..basic_game import BasicGame from .unreal_tabs.constants import DEFAULT_UE4SS_MODS, UE4SSModInfo from .unreal_tabs.manage_paks.widget import PaksTabWidget @@ -36,7 +37,10 @@ class OTWDModDataContent(mobase.ModDataContent): ] def getAllContents(self) -> list[mobase.ModDataContent.Content]: - return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + return [ + mobase.ModDataContent.Content(id, name, icon, *filter_only) + for id, name, icon, *filter_only in self.GAMECONTENTS + ] def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): @@ -81,10 +85,16 @@ def move_overwrite_merge(self, source: str, destination: str): self.move_overwrite_merge(s_item, d_item) os.rmdir(source) - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: - GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + GameDataUE4SSMods = getattr( + self.organizer.managedGame(), "GameDataUE4SSMods", "" + ) GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") - GameDataMovieMods = getattr(self.organizer.managedGame(), "GameDataMovieMods", "") + GameDataMovieMods = getattr( + self.organizer.managedGame(), "GameDataMovieMods", "" + ) if filetree.exists(GameDataPakMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID if filetree.exists(GameDataMovieMods, mobase.IFileTree.DIRECTORY): @@ -112,15 +122,25 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): return retVal def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: - GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + "/" - GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") + "/" - GameDataMovieMods = getattr(self.organizer.managedGame(), "GameDataMovieMods", "") + "/" + GameDataUE4SSMods = ( + getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + "/" + ) + GameDataPakMods = ( + getattr(self.organizer.managedGame(), "GameDataPakMods", "") + "/" + ) + GameDataMovieMods = ( + getattr(self.organizer.managedGame(), "GameDataMovieMods", "") + "/" + ) treefixed = 0 if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): - treefixed = self.allMoveTo(filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/") + treefixed = self.allMoveTo( + filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/" + ) if treefixed == 1: return filetree - if filetree.exists("Scripts", mobase.IFileTree.DIRECTORY) or filetree.exists("dlls", mobase.IFileTree.DIRECTORY): + if filetree.exists("Scripts", mobase.IFileTree.DIRECTORY) or filetree.exists( + "dlls", mobase.IFileTree.DIRECTORY + ): treefixed = self.allMoveTo(filetree, GameDataUE4SSMods) if treefixed == 1: return filetree @@ -135,14 +155,32 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: if mod_name == "": mod_name = e.name() mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if not filetree.createOrphanTree("OrphanTree") and os.path.exists(mod_path): + if not filetree.createOrphanTree( + "OrphanTree" + ) and os.path.exists(mod_path): match e.suffix().casefold(): case "pak" | "utoc" | "ucas": - os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) + os.makedirs( + os.path.join(mod_path, GameDataPakMods), + exist_ok=True, + ) + shutil.move( + os.path.join(mod_path, e.name()), + os.path.join( + mod_path, GameDataPakMods, e.name() + ), + ) case "bk2": - os.makedirs(os.path.join(mod_path, GameDataMovieMods), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovieMods, e.name())) + os.makedirs( + os.path.join(mod_path, GameDataMovieMods), + exist_ok=True, + ) + shutil.move( + os.path.join(mod_path, e.name()), + os.path.join( + mod_path, GameDataMovieMods, e.name() + ), + ) case _: pass treefixed = 1 @@ -154,7 +192,11 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: case "pak" | "utoc" | "ucas": filetree.move(e, GameDataPakMods, mobase.IFileTree.MERGE) case "dll": - filetree.move(e, os.path.dirname(GameDataUE4SSMods) + "/", mobase.IFileTree.MERGE) + filetree.move( + e, + os.path.dirname(GameDataUE4SSMods) + "/", + mobase.IFileTree.MERGE, + ) case "bk2": filetree.move(e, GameDataMovieMods, mobase.IFileTree.MERGE) case _: @@ -177,7 +219,9 @@ class OTWDGame(BasicGame): GameDataUE4SSMods = "Binaries/Win64/Mods" GameDataPakMods = "Content/Paks/~Mods" GameDataMovieMods = "Content/Movies" - GameDocumentsDirectory = "%USERPROFILE%/AppData/Local/OTWD/Saved/Config/WindowsClient" + GameDocumentsDirectory = ( + "%USERPROFILE%/AppData/Local/OTWD/Saved/Config/WindowsClient" + ) GameSaveExtension = "sav" _main_window: QMainWindow _ue4ss_tab: UE4SSTabWidget @@ -227,7 +271,9 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: except AttributeError: efls = [] libs: set[str] = set() - tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = ( + self._organizer.virtualFileTree() + ) if type(tree) is not mobase.IFileTree: return efls for e in tree: @@ -235,7 +281,13 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: libs.add(relpath) exes = self.executables() - efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + efls = efls + [ + mobase.ExecutableForcedLoadSetting( + exe.binary().fileName(), lib + ).withEnabled(True) + for lib in libs + for exe in exes + ] return efls def write_default_mods(self, profile: QDir): diff --git a/games/game_pacificdrive.py b/games/game_pacificdrive.py index 90fc1873..218ec86b 100644 --- a/games/game_pacificdrive.py +++ b/games/game_pacificdrive.py @@ -1,14 +1,15 @@ -from enum import IntEnum, auto -from functools import cached_property -from pathlib import Path import json import os import shutil +from enum import IntEnum, auto +from functools import cached_property +from pathlib import Path -import mobase from PyQt6.QtCore import QDir, QFileInfo from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget +import mobase + from ..basic_game import BasicGame from .unreal_tabs.constants import DEFAULT_UE4SS_MODS, UE4SSModInfo from .unreal_tabs.manage_paks.widget import PaksTabWidget @@ -36,7 +37,10 @@ class PacificDriveModDataContent(mobase.ModDataContent): ] def getAllContents(self) -> list[mobase.ModDataContent.Content]: - return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + return [ + mobase.ModDataContent.Content(id, name, icon, *filter_only) + for id, name, icon, *filter_only in self.GAMECONTENTS + ] def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): @@ -68,7 +72,7 @@ def __init__(self, organizer: mobase.IOrganizer): super().__init__() self.organizer: mobase.IOrganizer = organizer - def move_overwrite_merge(self, source: str, destination: str): + def move_overwrite_merge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -81,10 +85,16 @@ def move_overwrite_merge(self, source: str, destination: str): self.move_overwrite_merge(s_item, d_item) os.rmdir(source) - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: - GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + GameDataUE4SSMods = getattr( + self.organizer.managedGame(), "GameDataUE4SSMods", "" + ) GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") - GameDataMovieMods = getattr(self.organizer.managedGame(), "GameDataMovieMods", "") + GameDataMovieMods = getattr( + self.organizer.managedGame(), "GameDataMovieMods", "" + ) if filetree.exists(GameDataPakMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID if filetree.exists(GameDataMovieMods, mobase.IFileTree.DIRECTORY): @@ -112,15 +122,25 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): return retVal def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: - GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + "/" - GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") + "/" - GameDataMovieMods = getattr(self.organizer.managedGame(), "GameDataMovieMods", "") + "/" + GameDataUE4SSMods = ( + getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + "/" + ) + GameDataPakMods = ( + getattr(self.organizer.managedGame(), "GameDataPakMods", "") + "/" + ) + GameDataMovieMods = ( + getattr(self.organizer.managedGame(), "GameDataMovieMods", "") + "/" + ) treefixed = 0 if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): - treefixed = self.allMoveTo(filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/") + treefixed = self.allMoveTo( + filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/" + ) if treefixed == 1: return filetree - if filetree.exists("Scripts", mobase.IFileTree.DIRECTORY) or filetree.exists("dlls", mobase.IFileTree.DIRECTORY): + if filetree.exists("Scripts", mobase.IFileTree.DIRECTORY) or filetree.exists( + "dlls", mobase.IFileTree.DIRECTORY + ): treefixed = self.allMoveTo(filetree, GameDataUE4SSMods) if treefixed == 1: return filetree @@ -135,14 +155,32 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: if mod_name == "": mod_name = e.name() mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if not filetree.createOrphanTree("OrphanTree") and os.path.exists(mod_path): + if not filetree.createOrphanTree( + "OrphanTree" + ) and os.path.exists(mod_path): match e.suffix().casefold(): case "pak" | "utoc" | "ucas": - os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) + os.makedirs( + os.path.join(mod_path, GameDataPakMods), + exist_ok=True, + ) + shutil.move( + os.path.join(mod_path, e.name()), + os.path.join( + mod_path, GameDataPakMods, e.name() + ), + ) case "bk2": - os.makedirs(os.path.join(mod_path, GameDataMovieMods), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovieMods, e.name())) + os.makedirs( + os.path.join(mod_path, GameDataMovieMods), + exist_ok=True, + ) + shutil.move( + os.path.join(mod_path, e.name()), + os.path.join( + mod_path, GameDataMovieMods, e.name() + ), + ) case _: pass treefixed = 1 @@ -154,7 +192,11 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: case "pak" | "utoc" | "ucas": filetree.move(e, GameDataPakMods, mobase.IFileTree.MERGE) case "dll": - filetree.move(e, os.path.dirname(GameDataUE4SSMods) + "/", mobase.IFileTree.MERGE) + filetree.move( + e, + os.path.dirname(GameDataUE4SSMods) + "/", + mobase.IFileTree.MERGE, + ) case "bk2": filetree.move(e, GameDataMovieMods, mobase.IFileTree.MERGE) case _: @@ -178,7 +220,9 @@ class PacificDriveGame(BasicGame): GameDataUE4SSMods = "Binaries/Win64/Mods" GameDataPakMods = "Content/Paks/~Mods" GameDataMovieMods = "Content/Movies" - GameDocumentsDirectory = "%USERPROFILE%/AppData/Local/PenDriverPro/Saved/Config/WindowsNoEditor" + GameDocumentsDirectory = ( + "%USERPROFILE%/AppData/Local/PenDriverPro/Saved/Config/WindowsNoEditor" + ) GameSaveExtension = "sav" _main_window: QMainWindow _ue4ss_tab: UE4SSTabWidget @@ -228,7 +272,9 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: except AttributeError: efls = [] libs: set[str] = set() - tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = ( + self._organizer.virtualFileTree() + ) if type(tree) is not mobase.IFileTree: return efls for e in tree: @@ -236,7 +282,13 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: libs.add(relpath) exes = self.executables() - efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + efls = efls + [ + mobase.ExecutableForcedLoadSetting( + exe.binary().fileName(), lib + ).withEnabled(True) + for lib in libs + for exe in exes + ] return efls def write_default_mods(self, profile: QDir): diff --git a/games/game_payday1.py b/games/game_payday1.py index f4b59e19..b4efb18d 100644 --- a/games/game_payday1.py +++ b/games/game_payday1.py @@ -1,14 +1,16 @@ +import os +import shutil from enum import IntEnum, auto from functools import cached_property from pathlib import Path -import os -import shutil -import mobase from PyQt6.QtCore import QDir, QFileInfo +import mobase + from ..basic_game import BasicGame + class Content(IntEnum): TEXTURE = auto() MESH = auto() @@ -30,7 +32,10 @@ class Payday1ModDataContent(mobase.ModDataContent): ] def getAllContents(self) -> list[mobase.ModDataContent.Content]: - return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + return [ + mobase.ModDataContent.Content(id, name, icon, *filter_only) + for id, name, icon, *filter_only in self.GAMECONTENTS + ] def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): @@ -88,7 +93,9 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): new_path = os.path.join(path, f"mods/{modname}") self.move_overwrite_merge(old_path, new_path) fixed = True - elif filetree.exists("assets/mod_overrides/FOLDERNAME/", mobase.IFileTree.DIRECTORY): + elif filetree.exists( + "assets/mod_overrides/FOLDERNAME/", mobase.IFileTree.DIRECTORY + ): path = mod.absolutePath() old_path = os.path.join(path, "assets/mod_overrides/FOLDERNAME") new_path = os.path.join(path, f"assets/mod_overrides/{modname}") @@ -104,7 +111,9 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): return self.needsNameFix = False - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: if filetree.exists("assets/mod_overrides", mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID if filetree.exists("mods", mobase.IFileTree.DIRECTORY): @@ -157,7 +166,9 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: filetree.move(firsttreelayer, "maps/", mobase.IFileTree.MERGE) treefixed = 1 else: - filetree.move(firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE) + filetree.move( + firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE + ) treefixed = 1 elif filetree.exists("main.xml", mobase.IFileTree.FILE): if filetree.exists("levels", mobase.IFileTree.DIRECTORY): @@ -165,31 +176,55 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: if treefixed == 1: self.needsNameFix = True else: - treefixed = self.allMoveTo(filetree, "assets/mod_overrides/FOLDERNAME/") + treefixed = self.allMoveTo( + filetree, "assets/mod_overrides/FOLDERNAME/" + ) if treefixed == 1: self.needsNameFix = True elif secondtreelayer is not None: if secondtreelayer.exists("mod.txt", mobase.IFileTree.FILE): - filetree.move(secondtreelayer, firsttreelayer.path("/"), mobase.IFileTree.REPLACE) + filetree.move( + secondtreelayer, + firsttreelayer.path("/"), + mobase.IFileTree.REPLACE, + ) filetree.move(firsttreelayer, "mods/", mobase.IFileTree.MERGE) treefixed = 1 elif secondtreelayer.exists("main.xml", mobase.IFileTree.FILE): if filetree.exists("levels", mobase.IFileTree.DIRECTORY): - filetree.move(secondtreelayer, firsttreelayer.path("/"), mobase.IFileTree.REPLACE) + filetree.move( + secondtreelayer, + firsttreelayer.path("/"), + mobase.IFileTree.REPLACE, + ) filetree.move(firsttreelayer, "maps/", mobase.IFileTree.MERGE) treefixed = 1 else: - filetree.move(secondtreelayer, firsttreelayer.path("/"), mobase.IFileTree.REPLACE) - filetree.move(firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE) + filetree.move( + secondtreelayer, + firsttreelayer.path("/"), + mobase.IFileTree.REPLACE, + ) + filetree.move( + firsttreelayer, + "assets/mod_overrides/", + mobase.IFileTree.MERGE, + ) treefixed = 1 if treefixed == 0: if len(filetree) == 1: - filetree.move(firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE) + filetree.move( + firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE + ) treefixed = 1 else: for e in filetree: if e.path("/").count("/") == 0: - filetree.move(e, "assets/mod_overrides/FOLDERNAME/", mobase.IFileTree.MERGE) + filetree.move( + e, + "assets/mod_overrides/FOLDERNAME/", + mobase.IFileTree.MERGE, + ) treefixed = 1 self.needsNameFix = True if treefixed == 0: @@ -207,7 +242,12 @@ class Payday1Game(BasicGame): GameBinary = "payday_win32_release.exe" GameDataPath = "%GAME_PATH%" GameDocumentsDirectory = "%USERPROFILE%/AppData/Local/PAYDAY" - _forced_libraries = ["IPHLPAPI.dll", "WSOCK32.dll" , "DINPUT8.dll", "PDTHModOverrides.dll"] + _forced_libraries = [ + "IPHLPAPI.dll", + "WSOCK32.dll", + "DINPUT8.dll", + "PDTHModOverrides.dll", + ] def init(self, organizer: mobase.IOrganizer) -> bool: super().init(organizer) @@ -225,9 +265,7 @@ def executables(self): ) ] - def dll_copy( - self, mods: dict[str, mobase.ModState] - ): + def dll_copy(self, mods: dict[str, mobase.ModState]): game_path = self.dataDirectory().absolutePath() + "/" @@ -236,12 +274,12 @@ def dll_copy( tree = key.fileTree() for e in tree: if e.name() in self._forced_libraries: - #add file + # add file file_path_source = key.absolutePath() + "/" + e.path() file_path_target = game_path + e.name() if value == 35: shutil.copyfile(file_path_source, file_path_target) - #remove file + # remove file if value == 33: if os.path.exists(file_path_target): os.remove(file_path_target) @@ -257,7 +295,9 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: except AttributeError: efls = [] libs: set[str] = set() - tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = ( + self._organizer.virtualFileTree() + ) if type(tree) is not mobase.IFileTree: return efls for e in tree: @@ -265,7 +305,13 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: libs.add(relpath) exes = self.executables() - efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + efls = efls + [ + mobase.ExecutableForcedLoadSetting( + exe.binary().fileName(), lib + ).withEnabled(True) + for lib in libs + for exe in exes + ] return efls def iniFiles(self): diff --git a/games/game_payday2.py b/games/game_payday2.py index e500d0c8..5efe083e 100644 --- a/games/game_payday2.py +++ b/games/game_payday2.py @@ -1,14 +1,16 @@ +import os +import shutil from enum import IntEnum, auto from functools import cached_property from pathlib import Path -import os -import shutil -import mobase from PyQt6.QtCore import QDir, QFileInfo +import mobase + from ..basic_game import BasicGame + class Content(IntEnum): TEXTURE = auto() MESH = auto() @@ -30,8 +32,10 @@ class Payday2ModDataContent(mobase.ModDataContent): ] def getAllContents(self) -> list[mobase.ModDataContent.Content]: - return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] - + return [ + mobase.ModDataContent.Content(id, name, icon, *filter_only) + for id, name, icon, *filter_only in self.GAMECONTENTS + ] def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): @@ -89,7 +93,9 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): new_path = os.path.join(path, f"mods/{modname}") self.move_overwrite_merge(old_path, new_path) fixed = True - elif filetree.exists("assets/mod_overrides/FOLDERNAME", mobase.IFileTree.DIRECTORY): + elif filetree.exists( + "assets/mod_overrides/FOLDERNAME", mobase.IFileTree.DIRECTORY + ): path = mod.absolutePath() old_path = os.path.join(path, "assets/mod_overrides/FOLDERNAME") new_path = os.path.join(path, f"assets/mod_overrides/{modname}") @@ -105,7 +111,9 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): return self.needsNameFix = False - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: if filetree.exists("assets/mod_overrides", mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID if filetree.exists("mods", mobase.IFileTree.DIRECTORY): @@ -159,7 +167,9 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: filetree.move(firsttreelayer, "maps/", mobase.IFileTree.MERGE) treefixed = 1 else: - filetree.move(firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE) + filetree.move( + firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE + ) treefixed = 1 elif filetree.exists("main.xml", mobase.IFileTree.FILE): if filetree.exists("levels", mobase.IFileTree.DIRECTORY): @@ -167,31 +177,55 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: if treefixed == 1: self.needsNameFix = True else: - treefixed = self.allMoveTo(filetree, "assets/mod_overrides/FOLDERNAME/") + treefixed = self.allMoveTo( + filetree, "assets/mod_overrides/FOLDERNAME/" + ) if treefixed == 1: self.needsNameFix = True elif secondtreelayer is not None: if secondtreelayer.exists("mod.txt", mobase.IFileTree.FILE): - filetree.move(secondtreelayer, firsttreelayer.path("/"), mobase.IFileTree.REPLACE) + filetree.move( + secondtreelayer, + firsttreelayer.path("/"), + mobase.IFileTree.REPLACE, + ) filetree.move(firsttreelayer, "mods/", mobase.IFileTree.MERGE) treefixed = 1 elif secondtreelayer.exists("main.xml", mobase.IFileTree.FILE): if filetree.exists("levels", mobase.IFileTree.DIRECTORY): - filetree.move(secondtreelayer, firsttreelayer.path("/"), mobase.IFileTree.REPLACE) + filetree.move( + secondtreelayer, + firsttreelayer.path("/"), + mobase.IFileTree.REPLACE, + ) filetree.move(firsttreelayer, "maps/", mobase.IFileTree.MERGE) treefixed = 1 else: - filetree.move(secondtreelayer, firsttreelayer.path("/"), mobase.IFileTree.REPLACE) - filetree.move(firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE) + filetree.move( + secondtreelayer, + firsttreelayer.path("/"), + mobase.IFileTree.REPLACE, + ) + filetree.move( + firsttreelayer, + "assets/mod_overrides/", + mobase.IFileTree.MERGE, + ) treefixed = 1 if treefixed == 0: if len(filetree) == 1: - filetree.move(firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE) + filetree.move( + firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE + ) treefixed = 1 else: for e in filetree: if e.path("/").count("/") == 0: - filetree.move(e, "assets/mod_overrides/FOLDERNAME/", mobase.IFileTree.MERGE) + filetree.move( + e, + "assets/mod_overrides/FOLDERNAME/", + mobase.IFileTree.MERGE, + ) treefixed = 1 self.needsNameFix = True if treefixed == 0: @@ -226,12 +260,13 @@ def executables(self): "Payday 2", QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), ), - mobase.ExecutableInfo("Payday 2 VR", QFileInfo(self.gameDirectory(), "payday2_win32_release_vr.exe")), + mobase.ExecutableInfo( + "Payday 2 VR", + QFileInfo(self.gameDirectory(), "payday2_win32_release_vr.exe"), + ), ] - def dll_copy( - self, mods: dict[str, mobase.ModState] - ): + def dll_copy(self, mods: dict[str, mobase.ModState]): game_path = self.dataDirectory().absolutePath() + "/" @@ -240,12 +275,12 @@ def dll_copy( tree = key.fileTree() for e in tree: if e.name() in self._forced_libraries: - #add file + # add file file_path_source = key.absolutePath() + "/" + e.path() file_path_target = game_path + e.name() if value == 35: shutil.copyfile(file_path_source, file_path_target) - #remove file + # remove file if value == 33: if os.path.exists(file_path_target): os.remove(file_path_target) @@ -261,7 +296,9 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: except AttributeError: efls = [] libs: set[str] = set() - tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = ( + self._organizer.virtualFileTree() + ) if type(tree) is not mobase.IFileTree: return efls for e in tree: @@ -269,7 +306,13 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: libs.add(relpath) exes = self.executables() - efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + efls = efls + [ + mobase.ExecutableForcedLoadSetting( + exe.binary().fileName(), lib + ).withEnabled(True) + for lib in libs + for exe in exes + ] return efls def iniFiles(self): diff --git a/games/game_payday3.py b/games/game_payday3.py index 91a6a496..a94afebf 100644 --- a/games/game_payday3.py +++ b/games/game_payday3.py @@ -1,21 +1,20 @@ import json import os import shutil -import mobase - from enum import IntEnum, auto -from pathlib import Path from functools import cached_property +from pathlib import Path + +from PyQt6.QtCore import QDir, QFileInfo +from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget +import mobase + +from ..basic_game import BasicGame from .unreal_tabs.constants import DEFAULT_UE4SS_MODS, UE4SSModInfo from .unreal_tabs.manage_paks.widget import PaksTabWidget from .unreal_tabs.manage_ue4ss.widget import UE4SSTabWidget -from ..basic_game import BasicGame - -from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget -from PyQt6.QtCore import QDir, QFileInfo - class Content(IntEnum): UCAS = auto() @@ -87,10 +86,16 @@ def move_overwrite_merge(self, source: str, destination: str): self.move_overwrite_merge(s_item, d_item) os.rmdir(source) - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: - GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + GameDataUE4SSMods = getattr( + self.organizer.managedGame(), "GameDataUE4SSMods", "" + ) GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") - GameDataMovieMods = getattr(self.organizer.managedGame(), "GameDataMovieMods", "") + GameDataMovieMods = getattr( + self.organizer.managedGame(), "GameDataMovieMods", "" + ) if filetree.exists(GameDataPakMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID if filetree.exists(GameDataMovieMods, mobase.IFileTree.DIRECTORY): @@ -118,9 +123,15 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): return retVal def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: - GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + "/" - GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") + "/" - GameDataMovieMods = getattr(self.organizer.managedGame(), "GameDataMovieMods", "") + "/" + GameDataUE4SSMods = ( + getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + "/" + ) + GameDataPakMods = ( + getattr(self.organizer.managedGame(), "GameDataPakMods", "") + "/" + ) + GameDataMovieMods = ( + getattr(self.organizer.managedGame(), "GameDataMovieMods", "") + "/" + ) treefixed = 0 if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): treefixed = self.allMoveTo( @@ -145,14 +156,32 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: if mod_name == "": mod_name = e.name() mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if not filetree.createOrphanTree("OrphanTree") and os.path.exists(mod_path): + if not filetree.createOrphanTree( + "OrphanTree" + ) and os.path.exists(mod_path): match e.suffix().casefold(): case "pak" | "utoc" | "ucas": - os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) + os.makedirs( + os.path.join(mod_path, GameDataPakMods), + exist_ok=True, + ) + shutil.move( + os.path.join(mod_path, e.name()), + os.path.join( + mod_path, GameDataPakMods, e.name() + ), + ) case "bk2": - os.makedirs(os.path.join(mod_path, GameDataMovieMods), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovieMods, e.name())) + os.makedirs( + os.path.join(mod_path, GameDataMovieMods), + exist_ok=True, + ) + shutil.move( + os.path.join(mod_path, e.name()), + os.path.join( + mod_path, GameDataMovieMods, e.name() + ), + ) case _: pass treefixed = 1 @@ -164,7 +193,11 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: case "pak" | "utoc" | "ucas": filetree.move(e, GameDataPakMods, mobase.IFileTree.MERGE) case "dll": - filetree.move(e,os.path.dirname(GameDataUE4SSMods) + "/",mobase.IFileTree.MERGE) + filetree.move( + e, + os.path.dirname(GameDataUE4SSMods) + "/", + mobase.IFileTree.MERGE, + ) case "bk2": filetree.move(e, GameDataMovieMods, mobase.IFileTree.MERGE) case _: @@ -187,7 +220,9 @@ class Payday3Game(BasicGame): GameDataUE4SSMods = "Binaries/Win64/Mods" GameDataPakMods = "Content/Paks/~Mods" GameDataMovieMods = "Content/Movies" - GameDocumentsDirectory = "%USERPROFILE%/AppData/Local/PAYDAY3/Saved/Config/WindowsClient" + GameDocumentsDirectory = ( + "%USERPROFILE%/AppData/Local/PAYDAY3/Saved/Config/WindowsClient" + ) GameSaveExtension = "sav" _main_window: QMainWindow _ue4ss_tab: UE4SSTabWidget diff --git a/games/game_raid2.py b/games/game_raid2.py index d52ae603..1869911e 100644 --- a/games/game_raid2.py +++ b/games/game_raid2.py @@ -1,15 +1,15 @@ import os import shutil -import mobase - from enum import IntEnum, auto -from pathlib import Path from functools import cached_property - -from ..basic_game import BasicGame +from pathlib import Path from PyQt6.QtCore import QDir, QFileInfo +import mobase + +from ..basic_game import BasicGame + class Content(IntEnum): TEXTURE = auto() @@ -32,7 +32,10 @@ class RaidWW2ModDataContent(mobase.ModDataContent): ] def getAllContents(self) -> list[mobase.ModDataContent.Content]: - return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + return [ + mobase.ModDataContent.Content(id, name, icon, *filter_only) + for id, name, icon, *filter_only in self.GAMECONTENTS + ] def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): @@ -94,7 +97,9 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): return self.needsNameFix = False - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: if len(filetree) == 1: return mobase.ModDataChecker.VALID return mobase.ModDataChecker.FIXABLE @@ -152,9 +157,7 @@ def executables(self): ) ] - def dll_copy( - self, mods: dict[str, mobase.ModState] - ): + def dll_copy(self, mods: dict[str, mobase.ModState]): game_path = self.dataDirectory().absolutePath() + "/" @@ -163,12 +166,12 @@ def dll_copy( tree = key.fileTree() for e in tree: if e.name() in self._forced_libraries: - #add file + # add file file_path_source = key.absolutePath() + "/" + e.path() file_path_target = game_path + e.name() if value == 35: shutil.copyfile(file_path_source, file_path_target) - #remove file + # remove file if value == 33: if os.path.exists(file_path_target): os.remove(file_path_target) @@ -184,7 +187,9 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: except AttributeError: efls = [] libs: set[str] = set() - tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = ( + self._organizer.virtualFileTree() + ) if type(tree) is not mobase.IFileTree: return efls for e in tree: @@ -192,7 +197,13 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: libs.add(relpath) exes = self.executables() - efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + efls = efls + [ + mobase.ExecutableForcedLoadSetting( + exe.binary().fileName(), lib + ).withEnabled(True) + for lib in libs + for exe in exes + ] return efls def iniFiles(self): @@ -205,4 +216,4 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): if not modsDirectory.exists(): os.makedirs(modsDirectory.absolutePath()) - super().initializeProfile(directory, settings) \ No newline at end of file + super().initializeProfile(directory, settings) diff --git a/games/game_roadtovostok.py b/games/game_roadtovostok.py index 9e4780fd..1df7db7f 100644 --- a/games/game_roadtovostok.py +++ b/games/game_roadtovostok.py @@ -1,19 +1,25 @@ import os import shutil -import mobase from PyQt6.QtCore import QDir, QFileInfo +import mobase + from ..basic_game import BasicGame + class RoadToVostokModDataChecker(mobase.ModDataChecker): def __init__(self, organizer: mobase.IOrganizer): super().__init__() self.organizer: mobase.IOrganizer = organizer - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: - if filetree.exists("mods", mobase.IFileTree.DIRECTORY) and not filetree.exists("mod.txt", mobase.IFileTree.FILE): + if filetree.exists("mods", mobase.IFileTree.DIRECTORY) and not filetree.exists( + "mod.txt", mobase.IFileTree.FILE + ): return mobase.ModDataChecker.VALID for e in filetree: if e.isFile() and e.suffix().casefold() == "pck": @@ -29,9 +35,16 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: if mod_name == "": mod_name = branch.name() mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if not filetree.createOrphanTree("OrphanTree") and os.path.exists(mod_path) and branch.suffix().casefold() == "zip": + if ( + not filetree.createOrphanTree("OrphanTree") + and os.path.exists(mod_path) + and branch.suffix().casefold() == "zip" + ): os.makedirs(os.path.join(mod_path, GameModsPath), exist_ok=True) - shutil.move(os.path.join(mod_path, branch.name()), os.path.join(mod_path, GameModsPath, branch.name())) + shutil.move( + os.path.join(mod_path, branch.name()), + os.path.join(mod_path, GameModsPath, branch.name()), + ) treefixed = 1 if treefixed == 0: @@ -40,9 +53,8 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: class RoadToVostokGame(BasicGame): - Name = "Road to Vostok Support Plugin" - Author = "modworkshop" + Author = "modworkshop" Version = "1" GameName = "Road to Vostok" GameShortName = "road-to-vostok" @@ -50,7 +62,9 @@ class RoadToVostokGame(BasicGame): GameBinary = "RTV.exe" GameDataPath = "%GAME_PATH%" GameModsPath = "mods" - GameDocumentsDirectory = "%USERPROFILE%/AppData/Local/Godot/app_userdata/Road to Vostok" + GameDocumentsDirectory = ( + "%USERPROFILE%/AppData/Local/Godot/app_userdata/Road to Vostok" + ) GameSaveExtension = "tres" def init(self, organizer: mobase.IOrganizer) -> bool: diff --git a/games/game_silenthill2remake.py b/games/game_silenthill2remake.py index b3dcf8d8..83fe8d4f 100644 --- a/games/game_silenthill2remake.py +++ b/games/game_silenthill2remake.py @@ -1,19 +1,21 @@ -from enum import IntEnum, auto -from functools import cached_property -from pathlib import Path import json import os import shutil +from enum import IntEnum, auto +from functools import cached_property +from pathlib import Path -import mobase from PyQt6.QtCore import QDir, QFileInfo from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget +import mobase + from ..basic_game import BasicGame from .unreal_tabs.constants import DEFAULT_UE4SS_MODS, UE4SSModInfo from .unreal_tabs.manage_paks.widget import PaksTabWidget from .unreal_tabs.manage_ue4ss.widget import UE4SSTabWidget + class Content(IntEnum): UCAS = auto() UTOC = auto() @@ -35,7 +37,10 @@ class SilentHill2ModDataContent(mobase.ModDataContent): ] def getAllContents(self) -> list[mobase.ModDataContent.Content]: - return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + return [ + mobase.ModDataContent.Content(id, name, icon, *filter_only) + for id, name, icon, *filter_only in self.GAMECONTENTS + ] def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): @@ -80,10 +85,16 @@ def move_overwrite_merge(self, source: str, destination: str): self.move_overwrite_merge(s_item, d_item) os.rmdir(source) - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: - GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + GameDataUE4SSMods = getattr( + self.organizer.managedGame(), "GameDataUE4SSMods", "" + ) GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") - GameDataMovieMods = getattr(self.organizer.managedGame(), "GameDataMovieMods", "") + GameDataMovieMods = getattr( + self.organizer.managedGame(), "GameDataMovieMods", "" + ) if filetree.exists(GameDataPakMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID if filetree.exists(GameDataMovieMods, mobase.IFileTree.DIRECTORY): @@ -111,15 +122,25 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): return retVal def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: - GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + "/" - GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") + "/" - GameDataMovieMods = getattr(self.organizer.managedGame(), "GameDataMovieMods", "") + "/" + GameDataUE4SSMods = ( + getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + "/" + ) + GameDataPakMods = ( + getattr(self.organizer.managedGame(), "GameDataPakMods", "") + "/" + ) + GameDataMovieMods = ( + getattr(self.organizer.managedGame(), "GameDataMovieMods", "") + "/" + ) treefixed = 0 if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): - treefixed = self.allMoveTo(filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/") + treefixed = self.allMoveTo( + filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/" + ) if treefixed == 1: return filetree - if filetree.exists("Scripts", mobase.IFileTree.DIRECTORY) or filetree.exists("dlls", mobase.IFileTree.DIRECTORY): + if filetree.exists("Scripts", mobase.IFileTree.DIRECTORY) or filetree.exists( + "dlls", mobase.IFileTree.DIRECTORY + ): treefixed = self.allMoveTo(filetree, GameDataUE4SSMods) if treefixed == 1: return filetree @@ -134,14 +155,32 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: if mod_name == "": mod_name = e.name() mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if not filetree.createOrphanTree("OrphanTree") and os.path.exists(mod_path): + if not filetree.createOrphanTree( + "OrphanTree" + ) and os.path.exists(mod_path): match e.suffix().casefold(): case "pak" | "utoc" | "ucas": - os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) + os.makedirs( + os.path.join(mod_path, GameDataPakMods), + exist_ok=True, + ) + shutil.move( + os.path.join(mod_path, e.name()), + os.path.join( + mod_path, GameDataPakMods, e.name() + ), + ) case "bk2": - os.makedirs(os.path.join(mod_path, GameDataMovieMods), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovieMods, e.name())) + os.makedirs( + os.path.join(mod_path, GameDataMovieMods), + exist_ok=True, + ) + shutil.move( + os.path.join(mod_path, e.name()), + os.path.join( + mod_path, GameDataMovieMods, e.name() + ), + ) case _: pass treefixed = 1 @@ -153,7 +192,11 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: case "pak" | "utoc" | "ucas": filetree.move(e, GameDataPakMods, mobase.IFileTree.MERGE) case "dll": - filetree.move(e, os.path.dirname(GameDataUE4SSMods) + "/", mobase.IFileTree.MERGE) + filetree.move( + e, + os.path.dirname(GameDataUE4SSMods) + "/", + mobase.IFileTree.MERGE, + ) case "bk2": filetree.move(e, GameDataMovieMods, mobase.IFileTree.MERGE) case _: @@ -227,7 +270,9 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: except AttributeError: efls = [] libs: set[str] = set() - tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = ( + self._organizer.virtualFileTree() + ) if type(tree) is not mobase.IFileTree: return efls for e in tree: @@ -235,7 +280,13 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: libs.add(relpath) exes = self.executables() - efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + efls = efls + [ + mobase.ExecutableForcedLoadSetting( + exe.binary().fileName(), lib + ).withEnabled(True) + for lib in libs + for exe in exes + ] return efls def write_default_mods(self, profile: QDir): diff --git a/games/game_titanfall2.py b/games/game_titanfall2.py index dd67358e..da931ddf 100644 --- a/games/game_titanfall2.py +++ b/games/game_titanfall2.py @@ -1,13 +1,14 @@ -from enum import IntEnum, auto -from functools import cached_property import json -from pathlib import Path import os import shutil +from enum import IntEnum, auto +from functools import cached_property +from pathlib import Path -import mobase from PyQt6.QtCore import QDir, QFileInfo +import mobase + from ..basic_game import BasicGame @@ -41,7 +42,6 @@ def getAllContents(self) -> list[mobase.ModDataContent.Content]: for id, name, icon, *filter_only in self.GAMECONTENTS ] - def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): match entry.suffix().casefold(): @@ -93,11 +93,15 @@ def move_overwrite_merge(self, source: str, destination: str): def _Fix_Installed_Mod(self, mod: mobase.IModInterface): if not self.needsNameFix: return - GameNorthstarPath = getattr(self.organizer.managedGame(), "GameNorthstarPath", "") + "/" + GameNorthstarPath = ( + getattr(self.organizer.managedGame(), "GameNorthstarPath", "") + "/" + ) filetree: mobase.IFileTree = mod.fileTree() fixed = False modname = mod.name() - if filetree.exists(GameNorthstarPath + "FOLDERNAME", mobase.IFileTree.DIRECTORY): + if filetree.exists( + GameNorthstarPath + "FOLDERNAME", mobase.IFileTree.DIRECTORY + ): path = mod.absolutePath() json_path = os.path.join(path, GameNorthstarPath + "FOLDERNAME/mod.json") with open(json_path, "r") as json_data: @@ -108,7 +112,9 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): new_path = os.path.join(path, GameNorthstarPath + f"{modname}") self.move_overwrite_merge(old_path, new_path) fixed = True - elif filetree.exists(GameNorthstarPath + "FOLDERNAME_NAME", mobase.IFileTree.DIRECTORY): + elif filetree.exists( + GameNorthstarPath + "FOLDERNAME_NAME", mobase.IFileTree.DIRECTORY + ): path = mod.absolutePath() old_path = os.path.join(path, GameNorthstarPath + "FOLDERNAME_NAME") new_path = os.path.join(path, GameNorthstarPath + f"{modname}") @@ -150,7 +156,9 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): return retVal def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: - GameNorthstarPath = getattr(self.organizer.managedGame(), "GameNorthstarPath", "") + "/" + GameNorthstarPath = ( + getattr(self.organizer.managedGame(), "GameNorthstarPath", "") + "/" + ) treefixed = 0 firsttreelayer: mobase.IFileTree | None = self.first_tree(filetree) if firsttreelayer is not None: @@ -164,16 +172,28 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: treefixed = 1 if secondtreelayer is not None: if secondtreelayer.exists("mod.json", mobase.IFileTree.FILE): - filetree.move(secondtreelayer, firsttreelayer.path("/"), mobase.IFileTree.REPLACE) - filetree.move(firsttreelayer, GameNorthstarPath, mobase.IFileTree.MERGE) - treefixed = 1 + filetree.move( + secondtreelayer, + firsttreelayer.path("/"), + mobase.IFileTree.REPLACE, + ) + filetree.move( + firsttreelayer, GameNorthstarPath, mobase.IFileTree.MERGE + ) + treefixed = 1 elif len(filetree) == 1: - filetree.move(firsttreelayer, GameNorthstarPath, mobase.IFileTree.MERGE) + filetree.move( + firsttreelayer, GameNorthstarPath, mobase.IFileTree.MERGE + ) treefixed = 1 else: for e in filetree: if e.path("/").count("/") == 0: - filetree.move(e,GameNorthstarPath + "FOLDERNAME_NAME/",mobase.IFileTree.MERGE) + filetree.move( + e, + GameNorthstarPath + "FOLDERNAME_NAME/", + mobase.IFileTree.MERGE, + ) treefixed = 1 self.needsNameFix = True if treefixed == 0: diff --git a/games/game_zuma_deluxe.py b/games/game_zuma_deluxe.py index 39cd3377..ef05ee4a 100644 --- a/games/game_zuma_deluxe.py +++ b/games/game_zuma_deluxe.py @@ -1,13 +1,14 @@ -from enum import IntEnum, auto -from functools import cached_property -from pathlib import Path import os import re import shutil +from enum import IntEnum, auto +from functools import cached_property +from pathlib import Path -import mobase from PyQt6.QtCore import QDir, QFileInfo +import mobase + from ..basic_features import BasicGameSaveGameInfo from ..basic_game import BasicGame @@ -89,9 +90,7 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): filetree: mobase.IFileTree = mod.fileTree() fixed = False modname = mod.name() - if filetree.exists( - "mods/FOLDERNAME", mobase.IFileTree.DIRECTORY - ): + if filetree.exists("mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() old_path = os.path.join(path, "mods/FOLDERNAME") new_path = os.path.join(path, f"mods/{modname}") @@ -148,7 +147,7 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: GameLevelsPath: str = str( getattr(self.organizer.managedGame(), "GameLevelsPath", "levels") -) + ) validFolders = [ "images", "levels", diff --git a/games/unreal_tabs/manage_paks/model.py b/games/unreal_tabs/manage_paks/model.py index 2edeb7b4..9fe09e94 100644 --- a/games/unreal_tabs/manage_paks/model.py +++ b/games/unreal_tabs/manage_paks/model.py @@ -1,15 +1,27 @@ -from enum import IntEnum, auto import itertools import typing +from enum import IntEnum, auto from typing import Any, TypeAlias, overload -import mobase - -from PyQt6.QtCore import (QAbstractItemModel, QByteArray, QDataStream, QDir, QFileInfo, QMimeData, QModelIndex, QObject, Qt, QVariant) +from PyQt6.QtCore import ( + QAbstractItemModel, + QByteArray, + QDataStream, + QDir, + QFileInfo, + QMimeData, + QModelIndex, + QObject, + Qt, + QVariant, +) from PyQt6.QtWidgets import QWidget +import mobase + _PakInfo: TypeAlias = tuple[str, str, str, str] + class PaksColumns(IntEnum): PRIORITY = auto() PAK_NAME = auto() @@ -62,7 +74,9 @@ def columnCount(self, parent: QModelIndex | None = None) -> int: parent = QModelIndex() return len(PaksColumns) - def index(self, row: int, column: int, parent: QModelIndex | None = None) -> QModelIndex: + def index( + self, row: int, column: int, parent: QModelIndex | None = None + ) -> QModelIndex: if parent is None: parent = QModelIndex() if ( diff --git a/games/unreal_tabs/manage_paks/widget.py b/games/unreal_tabs/manage_paks/widget.py index 5c6fcf2c..2789a6e4 100644 --- a/games/unreal_tabs/manage_paks/widget.py +++ b/games/unreal_tabs/manage_paks/widget.py @@ -2,14 +2,16 @@ from pathlib import Path from typing import cast -import mobase -from PyQt6.QtWidgets import QGridLayout, QWidget from PyQt6.QtCore import QDir, QFileInfo +from PyQt6.QtWidgets import QGridLayout, QWidget + +import mobase from ....basic_features.utils import is_directory from .model import PaksModel from .view import PaksView + def pak_sort(a: tuple[str, str], b: tuple[str, str]) -> int: a_pak, a_str = a[0], a[1] or a[0] b_pak, b_str = b[0], b[1] or b[0] @@ -164,9 +166,13 @@ def _parse_pak_files(self): if data_path and data_pak_mods: pak_dir = QFileInfo(data_path.absolutePath() + "/" + data_pak_mods) if pak_dir.exists() and pak_dir.isDir(): - for entry in QDir(pak_dir.absoluteFilePath()).entryInfoList(QDir.Filter.Dirs | QDir.Filter.Files | QDir.Filter.NoDotAndDotDot): # type: ignore + for entry in QDir(pak_dir.absoluteFilePath()).entryInfoList( + QDir.Filter.Dirs | QDir.Filter.Files | QDir.Filter.NoDotAndDotDot + ): # type: ignore if entry.isDir(): - for sub_entry in QDir(entry.absoluteFilePath()).entryInfoList(QDir.Filter.Files): # type: ignore + for sub_entry in QDir(entry.absoluteFilePath()).entryInfoList( + QDir.Filter.Files + ): # type: ignore if ( sub_entry.isFile() and sub_entry.suffix().casefold() == "pak" diff --git a/games/unreal_tabs/manage_ue4ss/model.py b/games/unreal_tabs/manage_ue4ss/model.py index 8909d3aa..7cad001e 100644 --- a/games/unreal_tabs/manage_ue4ss/model.py +++ b/games/unreal_tabs/manage_ue4ss/model.py @@ -2,15 +2,17 @@ from json import JSONDecodeError from typing import Any, Iterable -import mobase -from PyQt6.QtCore import (QDir, QFileInfo, QMimeData, QModelIndex, QStringListModel, Qt) +from PyQt6.QtCore import QDir, QFileInfo, QMimeData, QModelIndex, QStringListModel, Qt from PyQt6.QtWidgets import QWidget +import mobase + from ..constants import DEFAULT_UE4SS_MODS + class UE4SSListModel(QStringListModel): def __init__(self, parent: QWidget | None, organizer: mobase.IOrganizer): - super().__init__(parent) # type: ignore + super().__init__(parent) # type: ignore self._checked_items: set[str] = set() self._organizer = organizer self._init_mod_states() @@ -87,7 +89,7 @@ def setData( return True def setStringList(self, strings: Iterable[str | None]) -> None: - super().setStringList(strings) # type: ignore + super().setStringList(strings) # type: ignore self._set_mod_states() def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any: diff --git a/games/unreal_tabs/manage_ue4ss/widget.py b/games/unreal_tabs/manage_ue4ss/widget.py index 47b874e0..69759393 100644 --- a/games/unreal_tabs/manage_ue4ss/widget.py +++ b/games/unreal_tabs/manage_ue4ss/widget.py @@ -1,12 +1,13 @@ -from functools import cmp_to_key import json +from functools import cmp_to_key from json import JSONDecodeError from pathlib import Path -import mobase from PyQt6.QtCore import QDir, QFileInfo, Qt from PyQt6.QtWidgets import QGridLayout, QWidget +import mobase + from ..constants import DEFAULT_UE4SS_MODS, UE4SSModInfo from .model import UE4SSListModel from .view import UE4SSView @@ -55,7 +56,11 @@ def update_mod_files( for mod in mod_list: tree = mod.fileTree() - ue4ss_files = tree.find(game_data_ue4ss_mods) if isinstance(game_data_ue4ss_mods, str) else None + ue4ss_files = ( + tree.find(game_data_ue4ss_mods) + if isinstance(game_data_ue4ss_mods, str) + else None + ) if isinstance(ue4ss_files, mobase.IFileTree): for entry in ue4ss_files: if isinstance(entry, mobase.IFileTree): @@ -80,7 +85,11 @@ def _parse_mod_files(self): & mobase.ModState.ACTIVE ): tree = self._organizer.modList().getMod(mod).fileTree() - ue4ss_files = tree.find(data_ue4ss_mods) if isinstance(data_ue4ss_mods, str) else None + ue4ss_files = ( + tree.find(data_ue4ss_mods) + if isinstance(data_ue4ss_mods, str) + else None + ) if isinstance(ue4ss_files, mobase.IFileTree): for entry in ue4ss_files: if isinstance(entry, mobase.IFileTree): @@ -104,11 +113,26 @@ def _parse_mod_files(self): if data_path and data_ue4ss_mods: ue4ss_dir = QDir(data_path.absolutePath() + "/" + data_ue4ss_mods) if ue4ss_dir.exists(): - for dir_info in ue4ss_dir.entryInfoList(QDir.Filter.Dirs | QDir.Filter.NoDotAndDotDot):# type: ignore - if QFileInfo(QDir(dir_info.absoluteFilePath()).absoluteFilePath("scripts/main.lua")).exists() or QFileInfo(QDir(dir_info.absoluteFilePath()).absoluteFilePath("dlls/main.dll")).exists(): + for dir_info in ue4ss_dir.entryInfoList( + QDir.Filter.Dirs | QDir.Filter.NoDotAndDotDot + ): # type: ignore + if ( + QFileInfo( + QDir(dir_info.absoluteFilePath()).absoluteFilePath( + "scripts/main.lua" + ) + ).exists() + or QFileInfo( + QDir(dir_info.absoluteFilePath()).absoluteFilePath( + "dlls/main.dll" + ) + ).exists() + ): mod_list.add(dir_info.fileName()) if QFileInfo( - QDir(dir_info.absoluteFilePath()).absoluteFilePath("enabled.txt") + QDir(dir_info.absoluteFilePath()).absoluteFilePath( + "enabled.txt" + ) ).exists(): Path(dir_info.absoluteFilePath(), "enabled.txt").unlink() From 3963c3b035c5455dd3c7996008630a52c1653c83 Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Tue, 14 Apr 2026 01:13:38 +0200 Subject: [PATCH 20/21] Ruff Run --- games/game_cyberpunk2077.py | 5 +++-- games/oblivion_remaster/ue4ss/widget.py | 4 +++- games/unreal_tabs/constants.py | 1 + games/unreal_tabs/manage_paks/view.py | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/games/game_cyberpunk2077.py b/games/game_cyberpunk2077.py index ab44b8b9..83587969 100644 --- a/games/game_cyberpunk2077.py +++ b/games/game_cyberpunk2077.py @@ -551,8 +551,9 @@ def _onFinishedRun(self, path: str, exit_code: int) -> None: hide_cb.setToolTip(f"Settings/Plugins/{self.name()}/crash_message") crash_message.setCheckBox(hide_cb) crash_message.open( # type: ignore - lambda: hide_cb.isChecked() - and self._set_setting("crash_message", False) + lambda: ( + hide_cb.isChecked() and self._set_setting("crash_message", False) + ) ) def _check_redmod_result(self, result: tuple[bool, int]) -> bool: diff --git a/games/oblivion_remaster/ue4ss/widget.py b/games/oblivion_remaster/ue4ss/widget.py index 071071ef..4ece7981 100644 --- a/games/oblivion_remaster/ue4ss/widget.py +++ b/games/oblivion_remaster/ue4ss/widget.py @@ -108,7 +108,9 @@ def _parse_mod_files(self): game = self._organizer.managedGame() if isinstance(game, OblivionRemasteredGame): if game.ue4ssDirectory().exists(): - for dir_info in game.ue4ssDirectory().entryInfoList(QDir.Filter.Dirs | QDir.Filter.NoDotAndDotDot): # type: ignore + for dir_info in game.ue4ssDirectory().entryInfoList( + QDir.Filter.Dirs | QDir.Filter.NoDotAndDotDot + ): # type: ignore if QFileInfo( QDir(dir_info.absoluteFilePath()).absoluteFilePath( "scripts/main.lua" diff --git a/games/unreal_tabs/constants.py b/games/unreal_tabs/constants.py index 0b10c821..d29fded3 100644 --- a/games/unreal_tabs/constants.py +++ b/games/unreal_tabs/constants.py @@ -5,6 +5,7 @@ class UE4SSModInfo(TypedDict): mod_name: str mod_enabled: bool + DEFAULT_UE4SS_MODS: list[UE4SSModInfo] = [ {"mod_name": "BPML_GenericFunctions", "mod_enabled": True}, {"mod_name": "BPModLoaderMod", "mod_enabled": True}, diff --git a/games/unreal_tabs/manage_paks/view.py b/games/unreal_tabs/manage_paks/view.py index 05681ff4..f432d42d 100644 --- a/games/unreal_tabs/manage_paks/view.py +++ b/games/unreal_tabs/manage_paks/view.py @@ -29,5 +29,5 @@ def dropEvent(self, e: QDropEvent | None): def dataChanged( self, topLeft: QModelIndex, bottomRight: QModelIndex, roles: Iterable[int] = () ): - super().dataChanged(topLeft, bottomRight, roles) # type: ignore + super().dataChanged(topLeft, bottomRight, roles) # type: ignore self.repaint() From b6793be706d746e902688141fc8312bba203b57a Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Tue, 14 Apr 2026 01:40:42 +0200 Subject: [PATCH 21/21] Ruff 0.12.2 Reformat --- games/game_payday1.py | 1 - games/game_payday2.py | 1 - games/game_raid2.py | 1 - games/game_roadtovostok.py | 1 - 4 files changed, 4 deletions(-) diff --git a/games/game_payday1.py b/games/game_payday1.py index b4efb18d..0d6d24cf 100644 --- a/games/game_payday1.py +++ b/games/game_payday1.py @@ -266,7 +266,6 @@ def executables(self): ] def dll_copy(self, mods: dict[str, mobase.ModState]): - game_path = self.dataDirectory().absolutePath() + "/" for key, value in mods.items(): diff --git a/games/game_payday2.py b/games/game_payday2.py index 5efe083e..43ef3f0d 100644 --- a/games/game_payday2.py +++ b/games/game_payday2.py @@ -267,7 +267,6 @@ def executables(self): ] def dll_copy(self, mods: dict[str, mobase.ModState]): - game_path = self.dataDirectory().absolutePath() + "/" for key, value in mods.items(): diff --git a/games/game_raid2.py b/games/game_raid2.py index 1869911e..c9749a06 100644 --- a/games/game_raid2.py +++ b/games/game_raid2.py @@ -158,7 +158,6 @@ def executables(self): ] def dll_copy(self, mods: dict[str, mobase.ModState]): - game_path = self.dataDirectory().absolutePath() + "/" for key, value in mods.items(): diff --git a/games/game_roadtovostok.py b/games/game_roadtovostok.py index 1df7db7f..88a8da1f 100644 --- a/games/game_roadtovostok.py +++ b/games/game_roadtovostok.py @@ -16,7 +16,6 @@ def __init__(self, organizer: mobase.IOrganizer): def dataLooksValid( self, filetree: mobase.IFileTree ) -> mobase.ModDataChecker.CheckReturn: - if filetree.exists("mods", mobase.IFileTree.DIRECTORY) and not filetree.exists( "mod.txt", mobase.IFileTree.FILE ):