From d55c23fc19c2884e66770c19516c7b0673ad3922 Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Sat, 28 Feb 2026 23:33:51 +0100 Subject: [PATCH 1/7] 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 2/7] 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 3/7] 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 4/7] 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 5/7] 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 6/7] 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 7/7] 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)