From d55c23fc19c2884e66770c19516c7b0673ad3922 Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Sat, 28 Feb 2026 23:33:51 +0100 Subject: [PATCH 01/31] Support for several games and generalized Unreal Tab support. --- games/game_cassettebeasts.py | 118 +++++++ games/game_crimeboss.py | 306 +++++++++++++++++ games/game_emuvr.py | 115 +++++++ games/game_hitman3.py | 226 +++++++++++++ games/game_noita.py | 154 +++++++++ games/game_ovkwalkingdead.py | 284 ++++++++++++++++ games/game_pacificdrive.py | 285 ++++++++++++++++ games/game_payday1.py | 277 ++++++++++++++++ games/game_payday2.py | 295 +++++++++++++++++ games/game_payday3.py | 284 ++++++++++++++++ games/game_raid2.py | 211 ++++++++++++ games/game_roadtovostok.py | 89 +++++ games/game_silenthill2remake.py | 366 +++++++++++++++------ games/game_titanfall2.py | 289 ++++++++++++++++ games/game_zuma_deluxe.py | 329 ++++++++++++++++++ games/unreal_tabs/__init__.py | 0 games/unreal_tabs/constants.py | 11 + games/unreal_tabs/manage_paks/__init__.py | 0 games/unreal_tabs/manage_paks/model.py | 245 ++++++++++++++ games/unreal_tabs/manage_paks/view.py | 37 +++ games/unreal_tabs/manage_paks/widget.py | 207 ++++++++++++ games/unreal_tabs/manage_ue4ss/__init__.py | 0 games/unreal_tabs/manage_ue4ss/model.py | 121 +++++++ games/unreal_tabs/manage_ue4ss/view.py | 36 ++ games/unreal_tabs/manage_ue4ss/widget.py | 168 ++++++++++ 25 files changed, 4356 insertions(+), 97 deletions(-) create mode 100644 games/game_cassettebeasts.py create mode 100644 games/game_crimeboss.py create mode 100644 games/game_emuvr.py create mode 100644 games/game_hitman3.py create mode 100644 games/game_noita.py create mode 100644 games/game_ovkwalkingdead.py create mode 100644 games/game_pacificdrive.py create mode 100644 games/game_payday1.py create mode 100644 games/game_payday2.py create mode 100644 games/game_payday3.py create mode 100644 games/game_raid2.py create mode 100644 games/game_roadtovostok.py create mode 100644 games/game_titanfall2.py create mode 100644 games/game_zuma_deluxe.py create mode 100644 games/unreal_tabs/__init__.py create mode 100644 games/unreal_tabs/constants.py create mode 100644 games/unreal_tabs/manage_paks/__init__.py create mode 100644 games/unreal_tabs/manage_paks/model.py create mode 100644 games/unreal_tabs/manage_paks/view.py create mode 100644 games/unreal_tabs/manage_paks/widget.py create mode 100644 games/unreal_tabs/manage_ue4ss/__init__.py create mode 100644 games/unreal_tabs/manage_ue4ss/model.py create mode 100644 games/unreal_tabs/manage_ue4ss/view.py create mode 100644 games/unreal_tabs/manage_ue4ss/widget.py diff --git a/games/game_cassettebeasts.py b/games/game_cassettebeasts.py new file mode 100644 index 00000000..99b5bd52 --- /dev/null +++ b/games/game_cassettebeasts.py @@ -0,0 +1,118 @@ +import os +import shutil +import mobase + +from enum import IntEnum, auto +from pathlib import Path +from typing import Any, List, Set, cast +from functools import cached_property + +from ..basic_game import BasicGame + +try: + from PyQt6.QtCore import QDir, QFileInfo +except: + from PyQt5.QtCore import QDir, QFileInfo + + +class CassetteBeastsModDataChecker(mobase.ModDataChecker): + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self.organizer: mobase.IOrganizer = organizer + + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + for e in filetree: + if e is not None and e.isFile() and e.suffix().casefold() == "pck": + return mobase.ModDataChecker.VALID + return mobase.ModDataChecker.FIXABLE + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + GameDataPath = self.organizer.managedGame().GameDataPath + "/" + treefixed = 0 + for branch in filetree: + mod_name = filetree.name() + if mod_name == "": + mod_name = branch.name() + mod_path = os.path.join(self.organizer.modsPath(), mod_name) + if filetree.createOrphanTree("OrphanTree") is None and os.path.exists(mod_path) and branch.suffix().casefold() == "pck": + os.makedirs(os.path.join(mod_path, GameDataPath), exist_ok=True) + shutil.move(os.path.join(mod_path, branch.name()), os.path.join(mod_path, GameDataPath, branch.name())) + treefixed = 1 + else: + if branch is not None: + if branch.isDir(): + for e in branch: + if e is not None and e.isFile() and e.suffix().casefold() == "pck": + filetree.move(e, GameDataPath, mobase.IFileTree.MERGE) + treefixed = 1 + elif branch.suffix().casefold() == "pck": + filetree.move(branch, GameDataPath, mobase.IFileTree.MERGE) + treefixed = 1 + if treefixed == 0: + return None + return filetree + + +class CassetteBeastsGame(BasicGame): + appdataenv = os.getenv('APPDATA') + + Name = "Cassette Beasts Support Plugin" + Author = "modworkshop" + Version = "1" + GameName = "Cassette Beasts" + GameShortName = "cassette-beasts" + GameSteamId = 1321440 + GameBinary = "CassetteBeasts.exe" + GameDataPath = appdataenv + '/CassetteBeasts/mods' + GameDocumentsDirectory = appdataenv + '/CassetteBeasts' + GameSaveExtension = "gcpf" + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self.dataChecker = CassetteBeastsModDataChecker(organizer) + self._register_feature(self.dataChecker) + return True + + def executables(self): + return [ + mobase.ExecutableInfo( + "Cassette Beasts (Mods)", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ).withArgument("-load-mods"), + mobase.ExecutableInfo( + "Cassette Beasts (No Mods)", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ), + ] + + @cached_property + def _base_dlls(self) -> set[str]: + base_dir = Path(self.gameDirectory().absolutePath()) + return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + try: + efls = super().executableForcedLoads() + except AttributeError: + efls = [] + libs: set[str] = set() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + if type(tree) is not mobase.IFileTree: + return efls + for e in tree: + relpath = e.pathFrom(tree) + if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + libs.add(relpath) + exes = self.executables() + efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + return efls + + def iniFiles(self): + return ["settings.cfg"] + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + modsPath = self.dataDirectory().absolutePath() + if not os.path.exists(modsPath): + os.mkdir(modsPath) + super().initializeProfile(directory, settings) + \ No newline at end of file diff --git a/games/game_crimeboss.py b/games/game_crimeboss.py new file mode 100644 index 00000000..648267ed --- /dev/null +++ b/games/game_crimeboss.py @@ -0,0 +1,306 @@ +import json +import os +import shutil +import mobase + +from enum import IntEnum, auto +from pathlib import Path +from typing import Any, List, Set, cast +from functools import cached_property + +from .unreal_tabs.constants import DEFAULT_UE4SS_MODS, UE4SSModInfo +from .unreal_tabs.manage_paks.widget import PaksTabWidget +from .unreal_tabs.manage_ue4ss.widget import UE4SSTabWidget + +from ..basic_game import BasicGame + +try: + from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget + from PyQt6.QtCore import QDir, QFileInfo +except: + from PyQt5.QtWidgets import QMainWindow, QTabWidget, QWidget + from PyQt5.QtCore import QDir, QFileInfo + + +class Content(IntEnum): + UCAS = auto() + UTOC = auto() + PAK = auto() + UE4SS = auto() + DLL = auto() + BK2 = auto() + + +class CrimeBossModDataContent(mobase.ModDataContent): + contents: list[int] = [] + GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ + (Content.UCAS, "UCAS", ":/MO/gui/content/geometries"), + (Content.UTOC, "UTOC", ":/MO/gui/content/inifile"), + (Content.PAK, "PAK", ":/MO/gui/content/geometries"), + (Content.UE4SS, "UE4SS", ":/MO/gui/content/script"), + (Content.DLL, "DLL", ":/MO/gui/content/skse"), + (Content.BK2, "Video", ":/MO/gui/content/skse"), + ] + + def getAllContents(self) -> list[mobase.ModDataContent.Content]: + return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + + def walkContent(self, path: str, entry: mobase.FileTreeEntry): + if entry.isFile(): + match entry.suffix().casefold(): + case "utoc": + self.contents.add(Content.UTOC) + case "ucas": + self.contents.add(Content.UCAS) + case "pak": + self.contents.add(Content.PAK) + case "lua": + self.contents.add(Content.UE4SS) + case "dll": + self.contents.add(Content.DLL) + case "bk2": + self.contents.add(Content.BK2) + case _: + pass + return mobase.IFileTree.WalkReturn.CONTINUE + + def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: + self.contents: set[int] = set() + filetree.walk(self.walkContent, "/") + return list(self.contents) + + +class CrimeBossModDataChecker(mobase.ModDataChecker): + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self.organizer: mobase.IOrganizer = organizer + self.organizer.modList().onModInstalled(self._Fix_Installed_Mod) + self.needsNameFix = False + + def move_overwrite_merge(self, source, destination): + if not os.path.exists(destination): + shutil.move(source, destination) + return + if os.path.isfile(source): + os.replace(source, destination) + return + for item in os.listdir(source): + s_item = os.path.join(source, item) + d_item = os.path.join(destination, item) + self.move_overwrite_merge(s_item, d_item) + os.rmdir(source) + + def _Fix_Installed_Mod(self, mod: mobase.IModInterface): + if not self.needsNameFix: + return + GameDataNativeMods = self.organizer.managedGame().GameDataNativeMods + filetree: mobase.IFileTree = mod.fileTree() + fixed = False + modname = mod.name() + if filetree is not None and filetree.exists(GameDataNativeMods + "/FOLDERNAME", mobase.IFileTree.DIRECTORY): + path = mod.absolutePath() + old_path = os.path.join(path, GameDataNativeMods + "/FOLDERNAME") + new_path = os.path.join(path, GameDataNativeMods + f"/{modname}") + self.move_overwrite_merge(old_path, new_path) + fixed = True + if not fixed: + return + self.needsNameFix = False + + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods + GameDataPakMods = self.organizer.managedGame().GameDataPakMods + GameDataNativeMods = self.organizer.managedGame().GameDataNativeMods + if filetree.exists(GameDataPakMods, mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + if filetree.exists(os.path.dirname(GameDataUE4SSMods), mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + if filetree.exists(GameDataNativeMods, mobase.IFileTree.DIRECTORY) and not filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): + return mobase.ModDataChecker.VALID + return mobase.ModDataChecker.FIXABLE + + def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): + for branch in filetree: + if branch is not None and branch.isDir(): + for e in branch: + if e is not None and e.name() == name: + return True + return False + + def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): + entriesToMove: list[mobase.FileTreeEntry] = [] + retVal = 0 + for e in filetree: + if e is not None: + entriesToMove.append(e) + for e in entriesToMove: + filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) + retVal = 1 + return retVal + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods + "/" + GameDataPakMods = self.organizer.managedGame().GameDataPakMods + "/" + GameDataNativeMods = self.organizer.managedGame().GameDataNativeMods + "/" + treefixed = 0 + if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): + treefixed = self.allMoveTo(filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/") + if treefixed == 1: + return filetree + if filetree.exists("Content", mobase.IFileTree.DIRECTORY): + treefixed = self.allMoveTo(filetree, GameDataNativeMods + "FOLDERNAME/") + if treefixed == 1: + self.needsNameFix = True + if self.fileExistsInNextSubDir(filetree, "Content"): + filetree.move(filetree[0], GameDataNativeMods + "/", mobase.IFileTree.MERGE) + treefixed = 1 + if treefixed == 0: + allowedUnzippedExt = ["pak", "utoc", "ucas", "bk2", "dll"] + entriesToMove: list[mobase.FileTreeEntry] = [] + for e in filetree: + if e is not None: + if e.isFile(): + fileext = e.suffix().casefold() + if fileext in allowedUnzippedExt: + mod_name = filetree.name() + if mod_name == "": + mod_name = e.name() + mod_path = os.path.join(self.organizer.modsPath(), mod_name) + if filetree.createOrphanTree("OrphanTree") is None and os.path.exists(mod_path): + match e.suffix().casefold(): + case "pak" | "utoc" | "ucas": + os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) + case "bk2": + os.makedirs(os.path.join(mod_path, GameDataMovies), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovies, e.name())) + case _: + pass + treefixed = 1 + else: + entriesToMove.append(e) + if entriesToMove is not None: + for e in entriesToMove: + match e.suffix().casefold(): + case "pak" | "utoc" | "ucas": + filetree.move(e, GameDataPakMods, mobase.IFileTree.MERGE) + case "dll": + filetree.move(e, os.path.dirname(GameDataUE4SSMods) + "/", mobase.IFileTree.MERGE) + case "bk2": + filetree.move(e, GameDataMovies, mobase.IFileTree.MERGE) + case _: + pass + treefixed = 1 + if treefixed == 0: + return None + return filetree + + +class CrimeBossGame(BasicGame): + Name = "Crime Boss Support Plugin" + Author = "modworkshop, MaskPlague and Silarn" + Version = "1" + GameName = "Crime Boss Rockay City" + GameShortName = "crimeboss" + GameSteamId = 2933080 + GameBinary = "CrimeBoss/Binaries/Win64/CrimeBoss-Win64-Shipping.exe" + GameDataPath = "CrimeBoss" + GameDataUE4SSMods = "Binaries/Win64/Mods" + GameDataNativeMods = "Mods" + GameDataPakMods = "Content/Paks/~Mods" + GameDocumentsDirectory = "%USERPROFILE%/Saved Games/CrimeBoss/Steam/Saved/Config/WindowsNoEditor" + GameSaveExtension = "sav" + _main_window: QMainWindow + _ue4ss_tab: UE4SSTabWidget + _paks_tab: PaksTabWidget + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self.dataChecker = CrimeBossModDataChecker(organizer) + self._register_feature(self.dataChecker) + self._register_feature(CrimeBossModDataContent()) + organizer.onUserInterfaceInitialized(self.init_tab) + return True + + def init_tab(self, main_window: QMainWindow): + if self._organizer.managedGame() != self: + return + self._main_window = main_window + tab_widget: QTabWidget = main_window.findChild(QTabWidget, "tabWidget") + if not tab_widget or not tab_widget.findChild(QWidget, "espTab"): + return + self._ue4ss_tab = UE4SSTabWidget(main_window, self._organizer) + plugin_tab = tab_widget.findChild(QWidget, "espTab") + tab_index = tab_widget.indexOf(plugin_tab) + 1 + if not tab_widget.isTabVisible(tab_widget.indexOf(plugin_tab)): + tab_index += 1 + tab_widget.insertTab(tab_index, self._ue4ss_tab, "UE4SS") + self._paks_tab = PaksTabWidget(main_window, self._organizer) + tab_index += 1 + tab_widget.insertTab(tab_index, self._paks_tab, "Paks") + + def executables(self): + return [ + mobase.ExecutableInfo( + "Crime Boss: Rockay City", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ) + ] + + @cached_property + def _base_dlls(self) -> set[str]: + base_dir = Path(self.gameDirectory().absolutePath()) + return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + try: + efls = super().executableForcedLoads() + except AttributeError: + efls = [] + libs: set[str] = set() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + if type(tree) is not mobase.IFileTree: + return efls + for e in tree: + relpath = e.pathFrom(tree) + if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + libs.add(relpath) + exes = self.executables() + efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + return efls + + def paksDirectory(self) -> QDir: + return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataPakMods) + + def ue4ssDirectory(self) -> QDir: + return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataUE4SSMods) + + def nativeDirectory(self) -> QDir: + return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataNativeMods) + + def write_default_mods(self, profile: QDir): + ue4ss_mods_txt = QFileInfo(profile.absoluteFilePath("mods.txt")) + ue4ss_mods_json = QFileInfo(profile.absoluteFilePath("mods.json")) + if not ue4ss_mods_txt.exists(): + with open(ue4ss_mods_txt.absoluteFilePath(), "w") as mods_txt: + for mod in DEFAULT_UE4SS_MODS: + mods_txt.write(f"{mod['mod_name']} : 1\n") + if not ue4ss_mods_json.exists(): + mods_data: list[UE4SSModInfo] = [] + for mod in DEFAULT_UE4SS_MODS: + mods_data.append({"mod_name": mod["mod_name"], "mod_enabled": True}) + with open(ue4ss_mods_json.absoluteFilePath(), "w") as mods_json: + mods_json.write(json.dumps(mods_data, indent=4)) + + def iniFiles(self): + return ["GameUserSettings.ini", "Input.ini"] + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + self.write_default_mods(directory) + if not self.paksDirectory().exists(): + os.makedirs(self.paksDirectory().absolutePath()) + if not self.ue4ssDirectory().exists(): + os.makedirs(self.ue4ssDirectory().absolutePath()) + if not self.nativeDirectory().exists(): + os.makedirs(self.nativeDirectory().absolutePath()) + super().initializeProfile(directory, settings) \ No newline at end of file diff --git a/games/game_emuvr.py b/games/game_emuvr.py new file mode 100644 index 00000000..e1418ba4 --- /dev/null +++ b/games/game_emuvr.py @@ -0,0 +1,115 @@ +import os +import shutil +import mobase + +from enum import IntEnum, auto +from pathlib import Path +from typing import Any, List, Set, cast +from functools import cached_property + +from ..basic_game import BasicGame + +try: + from PyQt6.QtCore import QDir, QFileInfo +except: + from PyQt5.QtCore import QDir, QFileInfo + + +class EmuVRModDataChecker(mobase.ModDataChecker): + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self.organizer: mobase.IOrganizer = organizer + + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + GameDataUGCMods = self.organizer.managedGame().GameDataUGCMods + if filetree.exists(GameDataUGCMods, mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + return mobase.ModDataChecker.FIXABLE + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + GameDataUGCMods = self.organizer.managedGame().GameDataUGCMods + "/" + treefixed = 0 + for branch in filetree: + mod_name = filetree.name() + if mod_name == "": + mod_name = branch.name() + mod_path = os.path.join(self.organizer.modsPath(), mod_name) + if filetree.createOrphanTree("OrphanTree") is None and os.path.exists(mod_path) and branch.suffix().casefold() == "ugc": + os.makedirs(os.path.join(mod_path, GameDataUGCMods), exist_ok=True) + shutil.move(os.path.join(mod_path, branch.name()), os.path.join(mod_path, GameDataUGCMods, branch.name())) + treefixed = 1 + else: + if branch is not None: + if branch.isDir(): + for e in branch: + if e is not None and e.isFile() and e.suffix().casefold() == "ugc": + filetree.move(e, GameDataUGCMods, mobase.IFileTree.MERGE) + treefixed = 1 + elif branch.suffix().casefold() == "ugc": + filetree.move(branch, GameDataUGCMods, mobase.IFileTree.MERGE) + treefixed = 1 + if treefixed == 0: + return None + return filetree + + +class EmuVRGame(BasicGame): + Name = "Emu VR Support Plugin" + Author = "modworkshop" + Version = "1" + GameName = "Emu VR" + GameShortName = "emuvr" + GameBinary = "EmuVR.exe" + GameDataPath = "%GAME_PATH%" + GameDataUGCMods = "Custom/UGC" + GameDocumentsDirectory = "%GAME_PATH%/Saved Data" + GameSavesDirectory = "%GAME_PATH%/Saved Data" + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self.dataChecker = EmuVRModDataChecker(organizer) + self._register_feature(self.dataChecker) + return True + + def executables(self): + return [ + mobase.ExecutableInfo( + "Emu VR", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ), + mobase.ExecutableInfo("Force SteamVR", QFileInfo(self.gameDirectory(), "Force SteamVR.exe")), + mobase.ExecutableInfo("Force Oculus", QFileInfo(self.gameDirectory(), "Force Oculus.exe")), + mobase.ExecutableInfo("Force Virtual Desktop Streamer", QFileInfo(self.gameDirectory(), "Force Virtual Desktop Streamer.exe")), + mobase.ExecutableInfo("Force Desktop", QFileInfo(self.gameDirectory(), "Force Desktop.exe")), + ] + + def iniFiles(self): + return ["settings.ini"] + + @cached_property + def _base_dlls(self) -> set[str]: + base_dir = Path(self.gameDirectory().absolutePath()) + return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + try: + efls = super().executableForcedLoads() + except AttributeError: + efls = [] + libs: set[str] = set() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + if type(tree) is not mobase.IFileTree: + return efls + for e in tree: + relpath = e.pathFrom(tree) + if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + libs.add(relpath) + exes = self.executables() + efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + return efls + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + modsPath = self.dataDirectory().absolutePath() + if not os.path.exists(modsPath): + os.mkdir(modsPath) + super().initializeProfile(directory, settings) \ No newline at end of file diff --git a/games/game_hitman3.py b/games/game_hitman3.py new file mode 100644 index 00000000..3c31b210 --- /dev/null +++ b/games/game_hitman3.py @@ -0,0 +1,226 @@ +import os +import shutil +import json +import mobase + +from json import JSONDecodeError +from enum import IntEnum, auto +from pathlib import Path +from typing import Any, List, Set, cast +from functools import cached_property + +from ..basic_game import BasicGame + +try: + from PyQt6.QtCore import QDir, QFileInfo +except: + from PyQt5.QtCore import QDir, QFileInfo + + +class Hitman3ModDataChecker(mobase.ModDataChecker): + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self.organizer: mobase.IOrganizer = organizer + self.organizer.modList().onModInstalled(self._Fix_Installed_Mod) + self.needsNameFix = False + + def move_overwrite_merge(self, source, destination): + if not os.path.exists(destination): + shutil.move(source, destination) + return + if os.path.isfile(source): + os.replace(source, destination) + return + for item in os.listdir(source): + s_item = os.path.join(source, item) + d_item = os.path.join(destination, item) + self.move_overwrite_merge(s_item, d_item) + os.rmdir(source) + + def _Fix_Installed_Mod(self, mod: mobase.IModInterface): + if not self.needsNameFix: + return + GameSMMPath = self.organizer.managedGame().GameSMMPath + filetree: mobase.IFileTree = mod.fileTree() + fixed = False + if filetree is not None and filetree.exists(GameSMMPath + "/Mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): + path = mod.absolutePath() + json_path = os.path.join(path, GameSMMPath + "/Mods/FOLDERNAME/manifest.json") + mod_data = json.load(open(json_path, encoding="utf-8")) + modname = mod_data["id"] + old_path = os.path.join(path, GameSMMPath + "/Mods/FOLDERNAME") + new_path = os.path.join(path, GameSMMPath + f"/Mods/{modname}") + self.move_overwrite_merge(old_path, new_path) + fixed = True + if not fixed: + return + self.needsNameFix = False + + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + if filetree.exists("Simple Mod Framework", mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + return mobase.ModDataChecker.FIXABLE + + def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): + for branch in filetree: + if branch is not None and branch.isDir(): + for e in branch: + if e is not None and e.name() == name: + return True + return False + + def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): + entriesToMove: list[mobase.FileTreeEntry] = [] + retVal = 0 + for e in filetree: + if e is not None: + entriesToMove.append(e) + for e in entriesToMove: + filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) + retVal = 1 + return retVal + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + GameSMMPath = self.organizer.managedGame().GameSMMPath + treefixed = 0 + if filetree.exists("manifest.json", mobase.IFileTree.FILE): + treefixed = self.allMoveTo(filetree, GameSMMPath + "/Mods/FOLDERNAME/") + if treefixed == 1: + self.needsNameFix = True + if treefixed == 0: + if len(filetree) == 1: + filetree = filetree.find(filetree[0].path("/")) + treefixed = self.allMoveTo(filetree, GameSMMPath + "/Mods/FOLDERNAME/") + if treefixed == 1: + self.needsNameFix = True + if treefixed == 0: + return None + return filetree + + +class Hitman3Game(BasicGame): + Name = "Hitman 3 Support Plugin" + Author = "modworkshop" + Version = "1" + GameName = "Hitman: World of Assassination" + GameShortName = "hitman3" + GameSteamId = 1659040 + GameBinary = "Retail/HITMAN3.exe" + GameDataPath = "%GAME_PATH%" + GameSMMPath = "Simple Mod Framework" + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self.dataChecker = Hitman3ModDataChecker(organizer) + self._register_feature(self.dataChecker) + organizer.modList().onModStateChanged(self.update_smm_meta) + return True + + def update_smm_meta(self, mods: dict[str, mobase.ModState]): + GameSMMPath = self._organizer.managedGame().GameSMMPath + SMM_Path = os.path.join(self.dataDirectory().absolutePath(), self.GameSMMPath) + SMM_Config_Json = SMM_Path + "/config.json" + for key, value in mods.items(): + key = self._organizer.modList().getMod(key) + tree = key.fileTree() + subtree = tree.find(self.GameSMMPath + "/Mods", mobase.IFileTree.DIRECTORY) + if subtree is not None and subtree.isDir(): + for e in subtree: + if e is not None and e.isDir(): + if e.exists("manifest.json", mobase.IFileTree.FILE): + json_path = key.absolutePath() + "/" + e.path() + "/manifest.json" + mod_data = json.load(open(json_path, encoding="utf-8")) + modname = mod_data["id"] + if value == 35: + with open(SMM_Config_Json, "r") as config_json: + config_json_content = config_json.read() + config_json.close() + good_code = '"knownMods": []' + if good_code in config_json_content: + bad_code = "{runtimePath:'..\\Runtime',retailPath:'..\\Retail',skipIntro:false,outputToSeparateDirectory:false,loadOrder:[''],modOptions:{},outputConfigToAppDataOnDeploy:true,knownMods:[''],developerMode:false,reportErrors:false}" + config_json_content = bad_code + if modname not in config_json_content: + substr = "knownMods:[" + config_json_content = config_json_content.replace(substr, substr + "'" + modname + "',") + substr = "loadOrder:[" + config_json_content = config_json_content.replace(substr, substr + "'" + modname + "',") + substr = ",],modOptions" + config_json_content = config_json_content.replace(substr, "],modOptions") + substr = ",],developer" + config_json_content = config_json_content.replace(substr, "],developer") + with open(SMM_Config_Json, "w") as config_json: + config_json.write(config_json_content) + config_json.close() + return None + if value == 33: + with open(SMM_Config_Json, "r") as config_json: + config_json_content = config_json.read() + config_json.close() + if modname in config_json_content: + config_json_content = config_json_content.replace("'" + modname + "',", "") + config_json_content = config_json_content.replace(",,", ",") + substr = ",],modOptions" + config_json_content = config_json_content.replace(substr, "],modOptions") + substr = ",],developer" + config_json_content = config_json_content.replace(substr, "],developer") + with open(SMM_Config_Json, "w") as config_json: + config_json.write(config_json_content) + config_json.close() + return None + + def executables(self): + return [ + mobase.ExecutableInfo( + "Hitman: World of Assassination", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ), + mobase.ExecutableInfo( + "Launcher", + QFileInfo( + self.gameDirectory(), + "Launcher.exe", + ), + ), + mobase.ExecutableInfo( + "Configure via Simple Mod Framework", + QFileInfo( + self.gameDirectory(), + "Simple Mod Framework/Mod Manager/Mod Manager.exe", + ), + ), + mobase.ExecutableInfo( + "Deploy via Simple Mod Framework", + QFileInfo( + self.gameDirectory(), + "Simple Mod Framework/Deploy.exe", + ), + ), + ] + + @cached_property + def _base_dlls(self) -> set[str]: + base_dir = Path(self.gameDirectory().absolutePath()) + return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + try: + efls = super().executableForcedLoads() + except AttributeError: + efls = [] + libs: set[str] = set() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + if type(tree) is not mobase.IFileTree: + return efls + for e in tree: + relpath = e.pathFrom(tree) + if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + libs.add(relpath) + exes = self.executables() + efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + return efls + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + modsPath = self.dataDirectory().absolutePath() + if not os.path.exists(modsPath): + os.mkdir(modsPath) + super().initializeProfile(directory, settings) \ No newline at end of file diff --git a/games/game_noita.py b/games/game_noita.py new file mode 100644 index 00000000..729d5b39 --- /dev/null +++ b/games/game_noita.py @@ -0,0 +1,154 @@ +import os +import shutil +import json +import mobase + +from json import JSONDecodeError +from enum import IntEnum, auto +from pathlib import Path +from typing import Any, List, Set, cast +from functools import cached_property + +from ..basic_game import BasicGame + +try: + from PyQt6.QtCore import QDir, QFileInfo +except: + from PyQt5.QtCore import QDir, QFileInfo + + +class NoitaModDataChecker(mobase.ModDataChecker): + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self.organizer: mobase.IOrganizer = organizer + self.organizer.modList().onModInstalled(self._Fix_Installed_Mod) + self.needsNameFix = False + + def move_overwrite_merge(self, source, destination): + if not os.path.exists(destination): + shutil.move(source, destination) + return + if os.path.isfile(source): + os.replace(source, destination) + return + for item in os.listdir(source): + s_item = os.path.join(source, item) + d_item = os.path.join(destination, item) + self.move_overwrite_merge(s_item, d_item) + os.rmdir(source) + + def _Fix_Installed_Mod(self, mod: mobase.IModInterface): + if not self.needsNameFix: + return + GameModsPath = self.organizer.managedGame().GameModsPath + filetree: mobase.IFileTree = mod.fileTree() + fixed = False + modname = mod.name() + if filetree is not None and filetree.exists(GameModsPath + "/FOLDERNAME", mobase.IFileTree.DIRECTORY): + path = mod.absolutePath() + old_path = os.path.join(path, GameModsPath + "/FOLDERNAME") + new_path = os.path.join(path, GameModsPath + f"/{modname}") + self.move_overwrite_merge(old_path, new_path) + fixed = True + if not fixed: + return + self.needsNameFix = False + + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + if filetree.exists("mods", mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + return mobase.ModDataChecker.FIXABLE + + def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): + for branch in filetree: + if branch is not None and branch.isDir(): + for e in branch: + if e is not None and e.name() == name: + return True + return False + + def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): + entriesToMove: list[mobase.FileTreeEntry] = [] + retVal = 0 + for e in filetree: + if e is not None: + entriesToMove.append(e) + for e in entriesToMove: + filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) + retVal = 1 + return retVal + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + GameModsPath = self.organizer.managedGame().GameModsPath + treefixed = 0 + if filetree.exists("mod.xml", mobase.IFileTree.FILE): + treefixed = self.allMoveTo(filetree, GameModsPath + "/FOLDERNAME/") + if treefixed == 1: + self.needsNameFix = True + if self.fileExistsInNextSubDir(filetree, "mod.xml"): + filetree.move(filetree[0], GameModsPath + "/", mobase.IFileTree.MERGE) + treefixed = 1 + if treefixed == 0: + return None + return filetree + + +class NoitaGame(BasicGame): + Name = "Noita Support Plugin" + Author = "modworkshop" + Version = "1" + GameName = "Noita" + GameShortName = "noita" + GameSteamId = 881100 + GameBinary = "noita.exe" + GameDataPath = "%GAME_PATH%" + GameModsPath = "mods" + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self.dataChecker = NoitaModDataChecker(organizer) + self._register_feature(self.dataChecker) + return True + + def executables(self): + return [ + mobase.ExecutableInfo( + "Noita", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ), + mobase.ExecutableInfo( + "Noita Dev", + QFileInfo( + self.gameDirectory(), + "noita_dev.exe", + ), + ), + ] + + @cached_property + def _base_dlls(self) -> set[str]: + base_dir = Path(self.gameDirectory().absolutePath()) + return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + try: + efls = super().executableForcedLoads() + except AttributeError: + efls = [] + libs: set[str] = set() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + if type(tree) is not mobase.IFileTree: + return efls + for e in tree: + relpath = e.pathFrom(tree) + if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + libs.add(relpath) + exes = self.executables() + efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + return efls + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + modsPath = self.dataDirectory().absolutePath() + if not os.path.exists(modsPath): + os.mkdir(modsPath) + super().initializeProfile(directory, settings) \ No newline at end of file diff --git a/games/game_ovkwalkingdead.py b/games/game_ovkwalkingdead.py new file mode 100644 index 00000000..e3916d89 --- /dev/null +++ b/games/game_ovkwalkingdead.py @@ -0,0 +1,284 @@ +import json +import os +import shutil +import mobase + +from enum import IntEnum, auto +from pathlib import Path +from typing import Any, List, Set, cast +from functools import cached_property + +from .unreal_tabs.constants import DEFAULT_UE4SS_MODS, UE4SSModInfo +from .unreal_tabs.manage_paks.widget import PaksTabWidget +from .unreal_tabs.manage_ue4ss.widget import UE4SSTabWidget + +from ..basic_game import BasicGame + +try: + from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget + from PyQt6.QtCore import QDir, QFileInfo +except: + from PyQt5.QtWidgets import QMainWindow, QTabWidget, QWidget + from PyQt5.QtCore import QDir, QFileInfo + + +class Content(IntEnum): + UCAS = auto() + UTOC = auto() + PAK = auto() + UE4SS = auto() + DLL = auto() + BK2 = auto() + + +class OTWDModDataContent(mobase.ModDataContent): + contents: list[int] = [] + GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ + (Content.UCAS, "UCAS", ":/MO/gui/content/geometries"), + (Content.UTOC, "UTOC", ":/MO/gui/content/inifile"), + (Content.PAK, "PAK", ":/MO/gui/content/geometries"), + (Content.UE4SS, "UE4SS", ":/MO/gui/content/script"), + (Content.DLL, "DLL", ":/MO/gui/content/skse"), + (Content.BK2, "Video", ":/MO/gui/content/skse"), + ] + + def getAllContents(self) -> list[mobase.ModDataContent.Content]: + return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + + def walkContent(self, path: str, entry: mobase.FileTreeEntry): + if entry.isFile(): + match entry.suffix().casefold(): + case "utoc": + self.contents.add(Content.UTOC) + case "ucas": + self.contents.add(Content.UCAS) + case "pak": + self.contents.add(Content.PAK) + case "lua": + self.contents.add(Content.UE4SS) + case "dll": + self.contents.add(Content.DLL) + case "bk2": + self.contents.add(Content.BK2) + case _: + pass + return mobase.IFileTree.WalkReturn.CONTINUE + + def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: + self.contents: set[int] = set() + filetree.walk(self.walkContent, "/") + return list(self.contents) + + +class OTWDModDataChecker(mobase.ModDataChecker): + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self.organizer: mobase.IOrganizer = organizer + + def move_overwrite_merge(self, source, destination): + if not os.path.exists(destination): + shutil.move(source, destination) + return + if os.path.isfile(source): + os.replace(source, destination) + return + for item in os.listdir(source): + s_item = os.path.join(source, item) + d_item = os.path.join(destination, item) + self.move_overwrite_merge(s_item, d_item) + os.rmdir(source) + + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods + GameDataPakMods = self.organizer.managedGame().GameDataPakMods + GameDataMovies = self.organizer.managedGame().GameDataMovieMods + if filetree.exists(GameDataPakMods, mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + if filetree.exists(GameDataMovies, mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + if filetree.exists(GameDataUE4SSMods, mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + return mobase.ModDataChecker.FIXABLE + + def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): + for branch in filetree: + if branch is not None and branch.isDir(): + for e in branch: + if e is not None and e.name() == name: + return True + return False + + def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): + entriesToMove: list[mobase.FileTreeEntry] = [] + retVal = 0 + for e in filetree: + if e is not None: + entriesToMove.append(e) + for e in entriesToMove: + filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) + retVal = 1 + return retVal + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods + "/" + GameDataPakMods = self.organizer.managedGame().GameDataPakMods + "/" + GameDataMovies = self.organizer.managedGame().GameDataMovieMods + "/" + treefixed = 0 + if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): + treefixed = self.allMoveTo(filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/") + if treefixed == 1: + return filetree + if filetree.exists("Scripts", mobase.IFileTree.DIRECTORY) or filetree.exists("dlls", mobase.IFileTree.DIRECTORY): + treefixed = self.allMoveTo(filetree, GameDataUE4SSMods) + if treefixed == 1: + return filetree + if treefixed == 0: + allowedUnzippedExt = ["pak", "utoc", "ucas", "bk2", "dll"] + entriesToMove: list[mobase.FileTreeEntry] = [] + for e in filetree: + if e is not None: + if e.isFile(): + fileext = e.suffix().casefold() + if fileext in allowedUnzippedExt: + mod_name = filetree.name() + if mod_name == "": + mod_name = e.name() + mod_path = os.path.join(self.organizer.modsPath(), mod_name) + if filetree.createOrphanTree("OrphanTree") is None and os.path.exists(mod_path): + match e.suffix().casefold(): + case "pak" | "utoc" | "ucas": + os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) + case "bk2": + os.makedirs(os.path.join(mod_path, GameDataMovies), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovies, e.name())) + case _: + pass + treefixed = 1 + else: + entriesToMove.append(e) + if entriesToMove is not None: + for e in entriesToMove: + match e.suffix().casefold(): + case "pak" | "utoc" | "ucas": + filetree.move(e, GameDataPakMods, mobase.IFileTree.MERGE) + case "dll": + filetree.move(e, os.path.dirname(GameDataUE4SSMods) + "/", mobase.IFileTree.MERGE) + case "bk2": + filetree.move(e, GameDataMovies, mobase.IFileTree.MERGE) + case _: + pass + treefixed = 1 + if treefixed == 0: + return None + return filetree + + +class OTWDGame(BasicGame): + Name = "OVERKILL's The Walking Dead Support Plugin" + Author = "modworkshop, MaskPlague and Silarn" + Version = "1" + GameName = "OVERKILL's The Walking Dead" + GameShortName = "otwd" + GameSteamId = 717690 + GameBinary = "OTWD/Binaries/Win64/OTWD-Win64-Shipping.exe" + GameDataPath = "OTWD" + GameDataUE4SSMods = "Binaries/Win64/Mods" + GameDataPakMods = "Content/Paks/~Mods" + GameDataMovieMods = "Content/Movies" + GameDocumentsDirectory = "%LOCALAPPDATA%/OTWD/Saved/Config/WindowsClient" + GameSaveExtension = "sav" + _main_window: QMainWindow + _ue4ss_tab: UE4SSTabWidget + _paks_tab: PaksTabWidget + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self.dataChecker = OTWDModDataChecker(organizer) + self._register_feature(self.dataChecker) + self._register_feature(OTWDModDataContent()) + organizer.onUserInterfaceInitialized(self.init_tab) + return True + + def init_tab(self, main_window: QMainWindow): + if self._organizer.managedGame() != self: + return + self._main_window = main_window + tab_widget: QTabWidget = main_window.findChild(QTabWidget, "tabWidget") + if not tab_widget or not tab_widget.findChild(QWidget, "espTab"): + return + self._ue4ss_tab = UE4SSTabWidget(main_window, self._organizer) + plugin_tab = tab_widget.findChild(QWidget, "espTab") + tab_index = tab_widget.indexOf(plugin_tab) + 1 + if not tab_widget.isTabVisible(tab_widget.indexOf(plugin_tab)): + tab_index += 1 + tab_widget.insertTab(tab_index, self._ue4ss_tab, "UE4SS") + self._paks_tab = PaksTabWidget(main_window, self._organizer) + tab_index += 1 + tab_widget.insertTab(tab_index, self._paks_tab, "Paks") + + def executables(self): + return [ + mobase.ExecutableInfo( + "OVERKILL's The Walking Dead", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ) + ] + + @cached_property + def _base_dlls(self) -> set[str]: + base_dir = Path(self.gameDirectory().absolutePath()) + return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + try: + efls = super().executableForcedLoads() + except AttributeError: + efls = [] + libs: set[str] = set() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + if type(tree) is not mobase.IFileTree: + return efls + for e in tree: + relpath = e.pathFrom(tree) + if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + libs.add(relpath) + exes = self.executables() + efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + return efls + + def paksDirectory(self) -> QDir: + return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataPakMods) + + def ue4ssDirectory(self) -> QDir: + return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataUE4SSMods) + + def movieDirectory(self) -> QDir: + return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataMovieMods) + + def write_default_mods(self, profile: QDir): + ue4ss_mods_txt = QFileInfo(profile.absoluteFilePath("mods.txt")) + ue4ss_mods_json = QFileInfo(profile.absoluteFilePath("mods.json")) + if not ue4ss_mods_txt.exists(): + with open(ue4ss_mods_txt.absoluteFilePath(), "w") as mods_txt: + for mod in DEFAULT_UE4SS_MODS: + mods_txt.write(f"{mod['mod_name']} : 1\n") + if not ue4ss_mods_json.exists(): + mods_data: list[UE4SSModInfo] = [] + for mod in DEFAULT_UE4SS_MODS: + mods_data.append({"mod_name": mod["mod_name"], "mod_enabled": True}) + with open(ue4ss_mods_json.absoluteFilePath(), "w") as mods_json: + mods_json.write(json.dumps(mods_data, indent=4)) + + def iniFiles(self): + return ["GameUserSettings.ini", "Input.ini"] + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + self.write_default_mods(directory) + if not self.paksDirectory().exists(): + os.makedirs(self.paksDirectory().absolutePath()) + if not self.ue4ssDirectory().exists(): + os.makedirs(self.ue4ssDirectory().absolutePath()) + if not self.movieDirectory().exists(): + os.makedirs(self.movieDirectory().absolutePath()) + super().initializeProfile(directory, settings) \ No newline at end of file diff --git a/games/game_pacificdrive.py b/games/game_pacificdrive.py new file mode 100644 index 00000000..0c2f99e8 --- /dev/null +++ b/games/game_pacificdrive.py @@ -0,0 +1,285 @@ +import json +import os +import shutil +import mobase + +from enum import IntEnum, auto +from pathlib import Path +from typing import Any, List, Set, cast +from functools import cached_property + +from .unreal_tabs.constants import DEFAULT_UE4SS_MODS, UE4SSModInfo +from .unreal_tabs.manage_paks.widget import PaksTabWidget +from .unreal_tabs.manage_ue4ss.widget import UE4SSTabWidget + +from ..basic_game import BasicGame + +try: + from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget + from PyQt6.QtCore import QDir, QFileInfo +except: + from PyQt5.QtWidgets import QMainWindow, QTabWidget, QWidget + from PyQt5.QtCore import QDir, QFileInfo + + +class Content(IntEnum): + UCAS = auto() + UTOC = auto() + PAK = auto() + UE4SS = auto() + DLL = auto() + BK2 = auto() + + +class PacificDriveModDataContent(mobase.ModDataContent): + contents: list[int] = [] + GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ + (Content.UCAS, "UCAS", ":/MO/gui/content/geometries"), + (Content.UTOC, "UTOC", ":/MO/gui/content/inifile"), + (Content.PAK, "PAK", ":/MO/gui/content/geometries"), + (Content.UE4SS, "UE4SS", ":/MO/gui/content/script"), + (Content.DLL, "DLL", ":/MO/gui/content/skse"), + (Content.BK2, "Video", ":/MO/gui/content/skse"), + ] + + def getAllContents(self) -> list[mobase.ModDataContent.Content]: + return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + + def walkContent(self, path: str, entry: mobase.FileTreeEntry): + if entry.isFile(): + match entry.suffix().casefold(): + case "utoc": + self.contents.add(Content.UTOC) + case "ucas": + self.contents.add(Content.UCAS) + case "pak": + self.contents.add(Content.PAK) + case "lua": + self.contents.add(Content.UE4SS) + case "dll": + self.contents.add(Content.DLL) + case "bk2": + self.contents.add(Content.BK2) + case _: + pass + return mobase.IFileTree.WalkReturn.CONTINUE + + def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: + self.contents: set[int] = set() + filetree.walk(self.walkContent, "/") + return list(self.contents) + + +class PacificDriveModDataChecker(mobase.ModDataChecker): + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self.organizer: mobase.IOrganizer = organizer + + def move_overwrite_merge(self, source, destination): + if not os.path.exists(destination): + shutil.move(source, destination) + return + if os.path.isfile(source): + os.replace(source, destination) + return + for item in os.listdir(source): + s_item = os.path.join(source, item) + d_item = os.path.join(destination, item) + self.move_overwrite_merge(s_item, d_item) + os.rmdir(source) + + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods + GameDataPakMods = self.organizer.managedGame().GameDataPakMods + GameDataMovies = self.organizer.managedGame().GameDataMovieMods + if filetree.exists(GameDataPakMods, mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + if filetree.exists(GameDataMovies, mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + if filetree.exists(GameDataUE4SSMods, mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + return mobase.ModDataChecker.FIXABLE + + def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): + for branch in filetree: + if branch is not None and branch.isDir(): + for e in branch: + if e is not None and e.name() == name: + return True + return False + + def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): + entriesToMove: list[mobase.FileTreeEntry] = [] + retVal = 0 + for e in filetree: + if e is not None: + entriesToMove.append(e) + for e in entriesToMove: + filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) + retVal = 1 + return retVal + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods + "/" + GameDataPakMods = self.organizer.managedGame().GameDataPakMods + "/" + GameDataMovies = self.organizer.managedGame().GameDataMovieMods + "/" + treefixed = 0 + if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): + treefixed = self.allMoveTo(filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/") + if treefixed == 1: + return filetree + if filetree.exists("Scripts", mobase.IFileTree.DIRECTORY) or filetree.exists("dlls", mobase.IFileTree.DIRECTORY): + treefixed = self.allMoveTo(filetree, GameDataUE4SSMods) + if treefixed == 1: + return filetree + if treefixed == 0: + allowedUnzippedExt = ["pak", "utoc", "ucas", "bk2", "dll"] + entriesToMove: list[mobase.FileTreeEntry] = [] + for e in filetree: + if e is not None: + if e.isFile(): + fileext = e.suffix().casefold() + if fileext in allowedUnzippedExt: + mod_name = filetree.name() + if mod_name == "": + mod_name = e.name() + mod_path = os.path.join(self.organizer.modsPath(), mod_name) + if filetree.createOrphanTree("OrphanTree") is None and os.path.exists(mod_path): + match e.suffix().casefold(): + case "pak" | "utoc" | "ucas": + os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) + case "bk2": + os.makedirs(os.path.join(mod_path, GameDataMovies), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovies, e.name())) + case _: + pass + treefixed = 1 + else: + entriesToMove.append(e) + if entriesToMove is not None: + for e in entriesToMove: + match e.suffix().casefold(): + case "pak" | "utoc" | "ucas": + filetree.move(e, GameDataPakMods, mobase.IFileTree.MERGE) + case "dll": + filetree.move(e, os.path.dirname(GameDataUE4SSMods) + "/", mobase.IFileTree.MERGE) + case "bk2": + filetree.move(e, GameDataMovies, mobase.IFileTree.MERGE) + case _: + pass + treefixed = 1 + if treefixed == 0: + return None + return filetree + + +class PacificDriveGame(BasicGame): + Name = "Pacific Drive Support Plugin" + Author = "modworkshop" + Version = "1" + GameName = "Pacific Drive" + GameLauncher = "PenDriverPro.exe" + GameShortName = "pacificdrive" + GameSteamId = 1458140 + GameBinary = "PenDriverPro/Binaries/Win64/PenDriverPro-Win64-Shipping.exe" + GameDataPath = "PenDriverPro" + GameDataUE4SSMods = "Binaries/Win64/Mods" + GameDataPakMods = "Content/Paks/~Mods" + GameDataMovieMods = "Content/Movies" + GameDocumentsDirectory = "%LOCALAPPDATA%/PenDriverPro/Saved/Config/WindowsNoEditor" + GameSaveExtension = "sav" + _main_window: QMainWindow + _ue4ss_tab: UE4SSTabWidget + _paks_tab: PaksTabWidget + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self.dataChecker = PacificDriveModDataChecker(organizer) + self._register_feature(self.dataChecker) + self._register_feature(PacificDriveModDataContent()) + organizer.onUserInterfaceInitialized(self.init_tab) + return True + + def init_tab(self, main_window: QMainWindow): + if self._organizer.managedGame() != self: + return + self._main_window = main_window + tab_widget: QTabWidget = main_window.findChild(QTabWidget, "tabWidget") + if not tab_widget or not tab_widget.findChild(QWidget, "espTab"): + return + self._ue4ss_tab = UE4SSTabWidget(main_window, self._organizer) + plugin_tab = tab_widget.findChild(QWidget, "espTab") + tab_index = tab_widget.indexOf(plugin_tab) + 1 + if not tab_widget.isTabVisible(tab_widget.indexOf(plugin_tab)): + tab_index += 1 + tab_widget.insertTab(tab_index, self._ue4ss_tab, "UE4SS") + self._paks_tab = PaksTabWidget(main_window, self._organizer) + tab_index += 1 + tab_widget.insertTab(tab_index, self._paks_tab, "Paks") + + def executables(self): + return [ + mobase.ExecutableInfo( + "Pacific Drive", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ) + ] + + @cached_property + def _base_dlls(self) -> set[str]: + base_dir = Path(self.gameDirectory().absolutePath()) + return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + try: + efls = super().executableForcedLoads() + except AttributeError: + efls = [] + libs: set[str] = set() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + if type(tree) is not mobase.IFileTree: + return efls + for e in tree: + relpath = e.pathFrom(tree) + if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + libs.add(relpath) + exes = self.executables() + efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + return efls + + def paksDirectory(self) -> QDir: + return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataPakMods) + + def ue4ssDirectory(self) -> QDir: + return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataUE4SSMods) + + def movieDirectory(self) -> QDir: + return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataMovieMods) + + def write_default_mods(self, profile: QDir): + ue4ss_mods_txt = QFileInfo(profile.absoluteFilePath("mods.txt")) + ue4ss_mods_json = QFileInfo(profile.absoluteFilePath("mods.json")) + if not ue4ss_mods_txt.exists(): + with open(ue4ss_mods_txt.absoluteFilePath(), "w") as mods_txt: + for mod in DEFAULT_UE4SS_MODS: + mods_txt.write(f"{mod['mod_name']} : 1\n") + if not ue4ss_mods_json.exists(): + mods_data: list[UE4SSModInfo] = [] + for mod in DEFAULT_UE4SS_MODS: + mods_data.append({"mod_name": mod["mod_name"], "mod_enabled": True}) + with open(ue4ss_mods_json.absoluteFilePath(), "w") as mods_json: + mods_json.write(json.dumps(mods_data, indent=4)) + + def iniFiles(self): + return ["GameUserSettings.ini", "Input.ini"] + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + self.write_default_mods(directory) + if not self.paksDirectory().exists(): + os.makedirs(self.paksDirectory().absolutePath()) + if not self.ue4ssDirectory().exists(): + os.makedirs(self.ue4ssDirectory().absolutePath()) + if not self.movieDirectory().exists(): + os.makedirs(self.movieDirectory().absolutePath()) + super().initializeProfile(directory, settings) \ No newline at end of file diff --git a/games/game_payday1.py b/games/game_payday1.py new file mode 100644 index 00000000..a7766f85 --- /dev/null +++ b/games/game_payday1.py @@ -0,0 +1,277 @@ +import os +import shutil +import mobase + +from enum import IntEnum, auto +from pathlib import Path +from typing import Any, List, Set, cast +from functools import cached_property + +from ..basic_game import BasicGame + +try: + from PyQt6.QtCore import QDir, QFileInfo +except: + from PyQt5.QtCore import QDir, QFileInfo + + +class Content(IntEnum): + TEXTURE = auto() + MESH = auto() + SCRIPT = auto() + SOUND = auto() + STRING = auto() + CONFIG = auto() + + +class Payday1ModDataContent(mobase.ModDataContent): + GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ + (Content.TEXTURE, "Textures", ":/MO/gui/content/texture"), + (Content.MESH, "Meshes", ":/MO/gui/content/mesh"), + (Content.SCRIPT, "Scripts", ":/MO/gui/content/script"), + (Content.SOUND, "Sounds", ":/MO/gui/content/sound"), + (Content.STRING, "Strings", ":/MO/gui/content/string"), + (Content.CONFIG, "Configs", ":/MO/gui/content/inifile"), + ] + + def getAllContents(self) -> list[mobase.ModDataContent.Content]: + return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + + contents = set() + + def walkContent(self, path: str, entry: mobase.FileTreeEntry): + if entry.isFile(): + match entry.suffix().casefold(): + case "texture": + self.contents.add(Content.TEXTURE) + case "model": + self.contents.add(Content.MESH) + case "lua": + self.contents.add(Content.SCRIPT) + case "stream": + self.contents.add(Content.SOUND) + case "txt": + self.contents.add(Content.STRING) + case "json": + self.contents.add(Content.CONFIG) + case _: + pass + return mobase.IFileTree.WalkReturn.CONTINUE + + def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: + filetree.walk(self.walkContent, "/") + return list(self.contents) + + +class Payday1ModDataChecker(mobase.ModDataChecker): + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self.organizer: mobase.IOrganizer = organizer + self.organizer.modList().onModInstalled(self._Fix_Installed_Mod) + self.needsNameFix = False + + def move_overwrite_merge(self, source, destination): + if not os.path.exists(destination): + shutil.move(source, destination) + return + if os.path.isfile(source): + os.replace(source, destination) + return + for item in os.listdir(source): + s_item = os.path.join(source, item) + d_item = os.path.join(destination, item) + self.move_overwrite_merge(s_item, d_item) + os.rmdir(source) + + def _Fix_Installed_Mod(self, mod: mobase.IModInterface): + if not self.needsNameFix: + return + filetree: mobase.IFileTree = mod.fileTree() + fixed = False + modname = mod.name() + if filetree is not None and filetree.exists("mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): + path = mod.absolutePath() + old_path = os.path.join(path, "mods/FOLDERNAME") + new_path = os.path.join(path, f"mods/{modname}") + self.move_overwrite_merge(old_path, new_path) + fixed = True + elif filetree is not None and filetree.exists("assets/mod_overrides/FOLDERNAME/", mobase.IFileTree.DIRECTORY): + path = mod.absolutePath() + old_path = os.path.join(path, "assets/mod_overrides/FOLDERNAME") + new_path = os.path.join(path, f"assets/mod_overrides/{modname}") + self.move_overwrite_merge(old_path, new_path) + fixed = True + elif filetree is not None and filetree.exists("maps/FOLDERNAME", mobase.IFileTree.DIRECTORY): + path = mod.absolutePath() + old_path = os.path.join(path, "maps/FOLDERNAME") + new_path = os.path.join(path, f"maps/{modname}") + self.move_overwrite_merge(old_path, new_path) + fixed = True + if not fixed: + return + self.needsNameFix = False + + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + if filetree.exists("assets/mod_overrides", mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + if filetree.exists("mods", mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + if filetree.exists("maps", mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + for e in filetree: + if e is not None and e.suffix().casefold() == "dll": + return mobase.ModDataChecker.VALID + return mobase.ModDataChecker.FIXABLE + + def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): + for branch in filetree: + if branch is not None and branch.isDir(): + for e in branch: + if e is not None and e.name() == name: + return True + return False + + def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): + entriesToMove: list[mobase.FileTreeEntry] = [] + retVal = 0 + for e in filetree: + if e is not None: + entriesToMove.append(e) + for e in entriesToMove: + filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) + retVal = 1 + return retVal + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + treefixed = 0 + if filetree.exists("mod.txt", mobase.IFileTree.FILE): + treefixed = self.allMoveTo(filetree, "mods/FOLDERNAME/") + if treefixed == 1: + self.needsNameFix = True + elif self.fileExistsInNextSubDir(filetree, "mod.txt"): + filetree.move(filetree[0], "mods/", mobase.IFileTree.MERGE) + treefixed = 1 + elif self.fileExistsInNextSubDir(filetree, "main.xml"): + if self.fileExistsInNextSubDir(filetree, "levels"): + filetree.move(filetree[0], "maps/", mobase.IFileTree.MERGE) + treefixed = 1 + else: + filetree.move(filetree[0], "assets/mod_overrides/", mobase.IFileTree.MERGE) + treefixed = 1 + elif filetree.exists("main.xml", mobase.IFileTree.FILE): + if filetree.exists("levels", mobase.IFileTree.DIRECTORY): + treefixed = self.move_overwrite_merge(filetree, "maps/FOLDERNAME") + if treefixed == 1: + self.needsNameFix = True + else: + treefixed = self.allMoveTo(filetree, "assets/mod_overrides/FOLDERNAME/") + if treefixed == 1: + self.needsNameFix = True + else: + if filetree[0][0].exists("mod.txt", mobase.IFileTree.FILE): + filetree.move(filetree[0][0], filetree[0].path("/"), mobase.IFileTree.REPLACE) + filetree.move(filetree[0], "mods/", mobase.IFileTree.MERGE) + treefixed = 1 + elif filetree[0][0].exists("main.xml", mobase.IFileTree.FILE): + if filetree.exists("levels", mobase.IFileTree.DIRECTORY): + filetree.move(filetree[0][0], filetree[0].path("/"), mobase.IFileTree.REPLACE) + filetree.move(filetree[0], "maps/", mobase.IFileTree.MERGE) + treefixed = 1 + else: + filetree.move(filetree[0][0], filetree[0].path("/"), mobase.IFileTree.REPLACE) + filetree.move(filetree[0], "assets/mod_overrides/", mobase.IFileTree.MERGE) + treefixed = 1 + if treefixed == 0: + if len(filetree) == 1: + filetree.move(filetree[0], "assets/mod_overrides/", mobase.IFileTree.MERGE) + treefixed = 1 + else: + for e in filetree: + if e is not None and e.path("/").count("/") == 0: + filetree.move(e, "assets/mod_overrides/FOLDERNAME/", mobase.IFileTree.MERGE) + treefixed = 1 + self.needsNameFix = True + if treefixed == 0: + return None + return filetree + + +class Payday1Game(BasicGame): + Name = "Payday 1 Support Plugin" + Author = "modworkshop" + Version = "1" + GameName = "Payday: The Heist" + GameShortName = "pdth" + GameSteamId = 24240 + GameBinary = "payday_win32_release.exe" + GameDataPath = "%GAME_PATH%" + GameDocumentsDirectory = "%LOCALAPPDATA%/PAYDAY" + _forced_libraries = ["IPHLPAPI.dll", "WSOCK32.dll"] + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self.dataChecker = Payday1ModDataChecker(organizer) + self._register_feature(self.dataChecker) + self._register_feature(Payday1ModDataContent()) + organizer.modList().onModStateChanged(self.dll_copy) + return True + + def dll_copy( + self, mods: dict[str, mobase.ModState] + ): + + game_path = self.dataDirectory().absolutePath() + "/" + + for key, value in mods.items(): + key = self._organizer.modList().getMod(key) + tree = key.fileTree() + for e in tree: + if e is not None and e.name() in self._forced_libraries: + #add file + file_path_source = key.absolutePath() + "/" + e.path() + file_path_target = game_path + e.name() + if value == 35: + shutil.copyfile(file_path_source, file_path_target) + #remove file + if value == 33: + if os.path.exists(file_path_target): + os.remove(file_path_target) + + def executables(self): + return [ + mobase.ExecutableInfo( + "Payday: The Heist", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ), + ] + + @cached_property + def _base_dlls(self) -> set[str]: + base_dir = Path(self.gameDirectory().absolutePath()) + return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + try: + efls = super().executableForcedLoads() + except AttributeError: + efls = [] + libs: set[str] = set() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + if type(tree) is not mobase.IFileTree: + return efls + for e in tree: + relpath = e.pathFrom(tree) + if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + libs.add(relpath) + exes = self.executables() + efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + return efls + + def iniFiles(self): + return ["renderer_settings.xml"] + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + modsPath = self.dataDirectory().absolutePath() + if not os.path.exists(modsPath): + os.mkdir(modsPath) + super().initializeProfile(directory, settings) \ No newline at end of file diff --git a/games/game_payday2.py b/games/game_payday2.py new file mode 100644 index 00000000..24b29b5f --- /dev/null +++ b/games/game_payday2.py @@ -0,0 +1,295 @@ +import os +import shutil +import mobase + +from enum import IntEnum, auto +from pathlib import Path +from typing import Any, List, Set, cast +from functools import cached_property + +from ..basic_game import BasicGame + +try: + from PyQt6.QtCore import QDir, QFileInfo +except: + from PyQt5.QtCore import QDir, QFileInfo + + +class Content(IntEnum): + TEXTURE = auto() + MESH = auto() + SCRIPT = auto() + SOUND = auto() + STRING = auto() + CONFIG = auto() + + +class Payday2ModDataContent(mobase.ModDataContent): + GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ + (Content.TEXTURE, "Textures", ":/MO/gui/content/texture"), + (Content.MESH, "Meshes", ":/MO/gui/content/mesh"), + (Content.SCRIPT, "Scripts", ":/MO/gui/content/script"), + (Content.SOUND, "Sounds", ":/MO/gui/content/sound"), + (Content.STRING, "Strings", ":/MO/gui/content/string"), + (Content.CONFIG, "Configs", ":/MO/gui/content/inifile"), + ] + + def getAllContents(self) -> list[mobase.ModDataContent.Content]: + return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + + contents = set() + + def walkContent(self, path: str, entry: mobase.FileTreeEntry): + if entry.isFile(): + match entry.suffix().casefold(): + case "texture": + self.contents.add(Content.TEXTURE) + case "model": + self.contents.add(Content.MESH) + case "lua": + self.contents.add(Content.SCRIPT) + case "stream": + self.contents.add(Content.SOUND) + case "txt": + self.contents.add(Content.STRING) + case "json": + self.contents.add(Content.CONFIG) + case _: + pass + return mobase.IFileTree.WalkReturn.CONTINUE + + def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: + filetree.walk(self.walkContent, "/") + return list(self.contents) + + +class Payday2ModDataChecker(mobase.ModDataChecker): + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self.organizer: mobase.IOrganizer = organizer + self.organizer.modList().onModInstalled(self._Fix_Installed_Mod) + self.needsNameFix = False + + def move_overwrite_merge(self, source, destination): + if not os.path.exists(destination): + shutil.move(source, destination) + return + if os.path.isfile(source): + os.replace(source, destination) + return + for item in os.listdir(source): + s_item = os.path.join(source, item) + d_item = os.path.join(destination, item) + self.move_overwrite_merge(s_item, d_item) + os.rmdir(source) + + def _Fix_Installed_Mod(self, mod: mobase.IModInterface): + if not self.needsNameFix: + return + filetree: mobase.IFileTree = mod.fileTree() + fixed = False + modname = mod.name() + if filetree is not None and filetree.exists("mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): + path = mod.absolutePath() + old_path = os.path.join(path, "mods/FOLDERNAME") + new_path = os.path.join(path, f"mods/{modname}") + self.move_overwrite_merge(old_path, new_path) + fixed = True + elif filetree is not None and filetree.exists("assets/mod_overrides/FOLDERNAME", mobase.IFileTree.DIRECTORY): + path = mod.absolutePath() + old_path = os.path.join(path, "assets/mod_overrides/FOLDERNAME") + new_path = os.path.join(path, f"assets/mod_overrides/{modname}") + self.move_overwrite_merge(old_path, new_path) + fixed = True + elif filetree is not None and filetree.exists("maps/FOLDERNAME", mobase.IFileTree.DIRECTORY): + path = mod.absolutePath() + old_path = os.path.join(path, "maps/FOLDERNAME") + new_path = os.path.join(path, f"maps/{modname}") + self.move_overwrite_merge(old_path, new_path) + fixed = True + if not fixed: + return + self.needsNameFix = False + + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + if filetree.exists("assets/mod_overrides", mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + if filetree.exists("mods", mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + if filetree.exists("maps", mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + if filetree.exists("IPHLPAPI.dll", mobase.IFileTree.FILE): + return mobase.ModDataChecker.VALID + if filetree.exists("WSOCK32.dll", mobase.IFileTree.FILE): + return mobase.ModDataChecker.VALID + return mobase.ModDataChecker.FIXABLE + + def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): + for branch in filetree: + if branch is not None and branch.isDir(): + for e in branch: + if e is not None and e.name() == name: + return True + return False + + def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): + entriesToMove: list[mobase.FileTreeEntry] = [] + retVal = 0 + for e in filetree: + if e is not None: + entriesToMove.append(e) + for e in entriesToMove: + filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) + retVal = 1 + return retVal + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + treefixed = 0 + + if filetree.exists("mod.txt", mobase.IFileTree.FILE): + treefixed = self.allMoveTo(filetree, "mods/FOLDERNAME/") + if treefixed == 1: + self.needsNameFix = True + elif self.fileExistsInNextSubDir(filetree, "mod.txt"): + filetree.move(filetree[0], "mods/", mobase.IFileTree.MERGE) + treefixed = 1 + elif self.fileExistsInNextSubDir(filetree, "main.xml"): + if self.fileExistsInNextSubDir(filetree, "levels"): + filetree.move(filetree[0], "maps/", mobase.IFileTree.MERGE) + treefixed = 1 + else: + filetree.move(filetree[0], "assets/mod_overrides/", mobase.IFileTree.MERGE) + treefixed = 1 + elif filetree.exists("main.xml", mobase.IFileTree.FILE): + if filetree.exists("levels", mobase.IFileTree.DIRECTORY): + treefixed = self.move_overwrite_merge(filetree, "maps/FOLDERNAME") + if treefixed == 1: + self.needsNameFix = True + else: + treefixed = self.allMoveTo(filetree, "assets/mod_overrides/FOLDERNAME/") + if treefixed == 1: + self.needsNameFix = True + else: + try: + if filetree[0][0].exists("mod.txt", mobase.IFileTree.FILE): + filetree.move(filetree[0][0], filetree[0].path("/"), mobase.IFileTree.REPLACE) + filetree.move(filetree[0], "mods/", mobase.IFileTree.MERGE) + treefixed = 1 + elif filetree[0][0].exists("main.xml", mobase.IFileTree.FILE): + if filetree.exists("levels", mobase.IFileTree.DIRECTORY): + filetree.move(filetree[0][0], filetree[0].path("/"), mobase.IFileTree.REPLACE) + filetree.move(filetree[0], "maps/", mobase.IFileTree.MERGE) + treefixed = 1 + else: + filetree.move(filetree[0][0], filetree[0].path("/"), mobase.IFileTree.REPLACE) + filetree.move(filetree[0], "assets/mod_overrides/", mobase.IFileTree.MERGE) + except TypeError: + pass + if treefixed == 0: + if len(filetree) == 1 and filetree[0].isDir: + filetree.move(filetree[0], "assets/mod_overrides/", mobase.IFileTree.MERGE) + treefixed = 1 + else: + for e in filetree: + if e is not None and e.path("/").count("/") == 0: + filetree.move(e, "assets/mod_overrides/FOLDERNAME/", mobase.IFileTree.MERGE) + treefixed = 1 + self.needsNameFix = True + if treefixed == 0: + return None + return filetree + + +class Payday2Game(BasicGame): + Name = "Payday 2 Support Plugin" + Author = "modworkshop" + Version = "1" + GameName = "Payday 2" + GameShortName = "payday-2" + GameSteamId = 218620 + GameBinary = "payday2_win32_release.exe" + GameDataPath = "%GAME_PATH%" + GameDocumentsDirectory = "%USERPROFILE%/AppData/Local/PAYDAY 2" + GameSavesDirectory = "%USERPROFILE%/AppData/Local/PAYDAY 2/saves" + _forced_libraries = ["IPHLPAPI.dll", "WSOCK32.dll"] + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self.dataChecker = Payday2ModDataChecker(organizer) + self._register_feature(self.dataChecker) + self._register_feature(Payday2ModDataContent()) + organizer.modList().onModStateChanged(self.dll_copy) + return True + + def executables(self): + return [ + mobase.ExecutableInfo( + "Payday 2", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ), + mobase.ExecutableInfo("Payday 2 VR", QFileInfo(self.gameDirectory(), "payday2_win32_release_vr.exe")), + ] + + def dll_copy( + self, mods: dict[str, mobase.ModState] + ): + + game_path = self.dataDirectory().absolutePath() + "/" + + for key, value in mods.items(): + key = self._organizer.modList().getMod(key) + tree = key.fileTree() + for e in tree: + if e is not None and e.name() in self._forced_libraries: + #add file + file_path_source = key.absolutePath() + "/" + e.path() + file_path_target = game_path + e.name() + if value == 35: + shutil.copyfile(file_path_source, file_path_target) + #remove file + if value == 33: + if os.path.exists(file_path_target): + os.remove(file_path_target) + + @cached_property + def _base_dlls(self) -> set[str]: + base_dir = Path(self.gameDirectory().absolutePath()) + return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + try: + efls = super().executableForcedLoads() + except AttributeError: + efls = [] + libs: set[str] = set() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + if type(tree) is not mobase.IFileTree: + return efls + for e in tree: + relpath = e.pathFrom(tree) + if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + libs.add(relpath) + exes = self.executables() + efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + return efls + + def mapsDirectory(self) -> QDir: + return QDir(self.gameDirectory().absolutePath() + "/maps") + + def modsDirectory(self) -> QDir: + return QDir(self.gameDirectory().absolutePath() + "/mods") + + def overridesDirectory(self) -> QDir: + return QDir(self.gameDirectory().absolutePath() + "/assets/mod_overrides") + + def iniFiles(self): + return ["renderer_settings.xml"] + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + if not self.mapsDirectory().exists(): + os.makedirs(self.mapsDirectory().absolutePath()) + if not self.modsDirectory().exists(): + os.makedirs(self.modsDirectory().absolutePath()) + if not self.overridesDirectory().exists(): + os.makedirs(self.overridesDirectory().absolutePath()) + super().initializeProfile(directory, settings) \ No newline at end of file diff --git a/games/game_payday3.py b/games/game_payday3.py new file mode 100644 index 00000000..c0060bd6 --- /dev/null +++ b/games/game_payday3.py @@ -0,0 +1,284 @@ +import json +import os +import shutil +import mobase + +from enum import IntEnum, auto +from pathlib import Path +from typing import Any, List, Set, cast +from functools import cached_property + +from .unreal_tabs.constants import DEFAULT_UE4SS_MODS, UE4SSModInfo +from .unreal_tabs.manage_paks.widget import PaksTabWidget +from .unreal_tabs.manage_ue4ss.widget import UE4SSTabWidget + +from ..basic_game import BasicGame + +try: + from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget + from PyQt6.QtCore import QDir, QFileInfo +except: + from PyQt5.QtWidgets import QMainWindow, QTabWidget, QWidget + from PyQt5.QtCore import QDir, QFileInfo + + +class Content(IntEnum): + UCAS = auto() + UTOC = auto() + PAK = auto() + UE4SS = auto() + DLL = auto() + BK2 = auto() + + +class Payday3ModDataContent(mobase.ModDataContent): + contents: list[int] = [] + GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ + (Content.UCAS, "UCAS", ":/MO/gui/content/geometries"), + (Content.UTOC, "UTOC", ":/MO/gui/content/inifile"), + (Content.PAK, "PAK", ":/MO/gui/content/geometries"), + (Content.UE4SS, "UE4SS", ":/MO/gui/content/script"), + (Content.DLL, "DLL", ":/MO/gui/content/skse"), + (Content.BK2, "Video", ":/MO/gui/content/skse"), + ] + + def getAllContents(self) -> list[mobase.ModDataContent.Content]: + return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + + def walkContent(self, path: str, entry: mobase.FileTreeEntry): + if entry.isFile(): + match entry.suffix().casefold(): + case "utoc": + self.contents.add(Content.UTOC) + case "ucas": + self.contents.add(Content.UCAS) + case "pak": + self.contents.add(Content.PAK) + case "lua": + self.contents.add(Content.UE4SS) + case "dll": + self.contents.add(Content.DLL) + case "bk2": + self.contents.add(Content.BK2) + case _: + pass + return mobase.IFileTree.WalkReturn.CONTINUE + + def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: + self.contents: set[int] = set() + filetree.walk(self.walkContent, "/") + return list(self.contents) + + +class Payday3ModDataChecker(mobase.ModDataChecker): + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self.organizer: mobase.IOrganizer = organizer + + def move_overwrite_merge(self, source, destination): + if not os.path.exists(destination): + shutil.move(source, destination) + return + if os.path.isfile(source): + os.replace(source, destination) + return + for item in os.listdir(source): + s_item = os.path.join(source, item) + d_item = os.path.join(destination, item) + self.move_overwrite_merge(s_item, d_item) + os.rmdir(source) + + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods + GameDataPakMods = self.organizer.managedGame().GameDataPakMods + GameDataMovies = self.organizer.managedGame().GameDataMovieMods + if filetree.exists(GameDataPakMods, mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + if filetree.exists(GameDataMovies, mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + if filetree.exists(GameDataUE4SSMods, mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + return mobase.ModDataChecker.FIXABLE + + def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): + for branch in filetree: + if branch is not None and branch.isDir(): + for e in branch: + if e is not None and e.name() == name: + return True + return False + + def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): + entriesToMove: list[mobase.FileTreeEntry] = [] + retVal = 0 + for e in filetree: + if e is not None: + entriesToMove.append(e) + for e in entriesToMove: + filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) + retVal = 1 + return retVal + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods + "/" + GameDataPakMods = self.organizer.managedGame().GameDataPakMods + "/" + GameDataMovies = self.organizer.managedGame().GameDataMovieMods + "/" + treefixed = 0 + if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): + treefixed = self.allMoveTo(filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/") + if treefixed == 1: + return filetree + if filetree.exists("Scripts", mobase.IFileTree.DIRECTORY) or filetree.exists("dlls", mobase.IFileTree.DIRECTORY): + treefixed = self.allMoveTo(filetree, GameDataUE4SSMods) + if treefixed == 1: + return filetree + if treefixed == 0: + allowedUnzippedExt = ["pak", "utoc", "ucas", "bk2", "dll"] + entriesToMove: list[mobase.FileTreeEntry] = [] + for e in filetree: + if e is not None: + if e.isFile(): + fileext = e.suffix().casefold() + if fileext in allowedUnzippedExt: + mod_name = filetree.name() + if mod_name == "": + mod_name = e.name() + mod_path = os.path.join(self.organizer.modsPath(), mod_name) + if filetree.createOrphanTree("OrphanTree") is None and os.path.exists(mod_path): + match e.suffix().casefold(): + case "pak" | "utoc" | "ucas": + os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) + case "bk2": + os.makedirs(os.path.join(mod_path, GameDataMovies), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovies, e.name())) + case _: + pass + treefixed = 1 + else: + entriesToMove.append(e) + if entriesToMove is not None: + for e in entriesToMove: + match e.suffix().casefold(): + case "pak" | "utoc" | "ucas": + filetree.move(e, GameDataPakMods, mobase.IFileTree.MERGE) + case "dll": + filetree.move(e, os.path.dirname(GameDataUE4SSMods) + "/", mobase.IFileTree.MERGE) + case "bk2": + filetree.move(e, GameDataMovies, mobase.IFileTree.MERGE) + case _: + pass + treefixed = 1 + if treefixed == 0: + return None + return filetree + + +class Payday3Game(BasicGame): + Name = "Payday 3 Support Plugin" + Author = "modworkshop, MaskPlague and Silarn" + Version = "1" + GameName = "Payday 3" + GameShortName = "payday-3" + GameSteamId = 1272080 + GameBinary = "PAYDAY3/Binaries/Win64/PAYDAY3-Win64-Shipping.exe" + GameDataPath = "PAYDAY3" + GameDataUE4SSMods = "Binaries/Win64/Mods" + GameDataPakMods = "Content/Paks/~Mods" + GameDataMovieMods = "Content/Movies" + GameDocumentsDirectory = "%LOCALAPPDATA%/PAYDAY3/Saved/Config/WindowsClient" + GameSaveExtension = "sav" + _main_window: QMainWindow + _ue4ss_tab: UE4SSTabWidget + _paks_tab: PaksTabWidget + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self.dataChecker = Payday3ModDataChecker(organizer) + self._register_feature(self.dataChecker) + self._register_feature(Payday3ModDataContent()) + organizer.onUserInterfaceInitialized(self.init_tab) + return True + + def init_tab(self, main_window: QMainWindow): + if self._organizer.managedGame() != self: + return + self._main_window = main_window + tab_widget: QTabWidget = main_window.findChild(QTabWidget, "tabWidget") + if not tab_widget or not tab_widget.findChild(QWidget, "espTab"): + return + self._ue4ss_tab = UE4SSTabWidget(main_window, self._organizer) + plugin_tab = tab_widget.findChild(QWidget, "espTab") + tab_index = tab_widget.indexOf(plugin_tab) + 1 + if not tab_widget.isTabVisible(tab_widget.indexOf(plugin_tab)): + tab_index += 1 + tab_widget.insertTab(tab_index, self._ue4ss_tab, "UE4SS") + self._paks_tab = PaksTabWidget(main_window, self._organizer) + tab_index += 1 + tab_widget.insertTab(tab_index, self._paks_tab, "Paks") + + def executables(self): + return [ + mobase.ExecutableInfo( + "Payday 3", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ).withArgument("-fileopenlog") + ] + + @cached_property + def _base_dlls(self) -> set[str]: + base_dir = Path(self.gameDirectory().absolutePath()) + return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + try: + efls = super().executableForcedLoads() + except AttributeError: + efls = [] + libs: set[str] = set() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + if type(tree) is not mobase.IFileTree: + return efls + for e in tree: + relpath = e.pathFrom(tree) + if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + libs.add(relpath) + exes = self.executables() + efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + return efls + + def paksDirectory(self) -> QDir: + return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataPakMods) + + def ue4ssDirectory(self) -> QDir: + return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataUE4SSMods) + + def movieDirectory(self) -> QDir: + return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataMovieMods) + + def write_default_mods(self, profile: QDir): + ue4ss_mods_txt = QFileInfo(profile.absoluteFilePath("mods.txt")) + ue4ss_mods_json = QFileInfo(profile.absoluteFilePath("mods.json")) + if not ue4ss_mods_txt.exists(): + with open(ue4ss_mods_txt.absoluteFilePath(), "w") as mods_txt: + for mod in DEFAULT_UE4SS_MODS: + mods_txt.write(f"{mod['mod_name']} : 1\n") + if not ue4ss_mods_json.exists(): + mods_data: list[UE4SSModInfo] = [] + for mod in DEFAULT_UE4SS_MODS: + mods_data.append({"mod_name": mod["mod_name"], "mod_enabled": True}) + with open(ue4ss_mods_json.absoluteFilePath(), "w") as mods_json: + mods_json.write(json.dumps(mods_data, indent=4)) + + def iniFiles(self): + return ["GameUserSettings.ini", "Input.ini"] + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + self.write_default_mods(directory) + if not self.paksDirectory().exists(): + os.makedirs(self.paksDirectory().absolutePath()) + if not self.ue4ssDirectory().exists(): + os.makedirs(self.ue4ssDirectory().absolutePath()) + if not self.movieDirectory().exists(): + os.makedirs(self.movieDirectory().absolutePath()) + super().initializeProfile(directory, settings) \ No newline at end of file diff --git a/games/game_raid2.py b/games/game_raid2.py new file mode 100644 index 00000000..0d089d74 --- /dev/null +++ b/games/game_raid2.py @@ -0,0 +1,211 @@ +import os +import shutil +import mobase + +from enum import IntEnum, auto +from pathlib import Path +from typing import Any, List, Set, cast +from functools import cached_property + +from ..basic_game import BasicGame + +try: + from PyQt6.QtCore import QDir, QFileInfo +except: + from PyQt5.QtCore import QDir, QFileInfo + + +class Content(IntEnum): + TEXTURE = auto() + MESH = auto() + SCRIPT = auto() + SOUND = auto() + STRING = auto() + CONFIG = auto() + + +class RaidWW2ModDataContent(mobase.ModDataContent): + GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ + (Content.TEXTURE, "Textures", ":/MO/gui/content/texture"), + (Content.MESH, "Meshes", ":/MO/gui/content/mesh"), + (Content.SCRIPT, "Scripts", ":/MO/gui/content/script"), + (Content.SOUND, "Sounds", ":/MO/gui/content/sound"), + (Content.STRING, "Strings", ":/MO/gui/content/string"), + (Content.CONFIG, "Configs", ":/MO/gui/content/inifile"), + ] + + def getAllContents(self) -> list[mobase.ModDataContent.Content]: + return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + + contents = set() + + def walkContent(self, path: str, entry: mobase.FileTreeEntry): + if entry.isFile(): + match entry.suffix().casefold(): + case "texture": + self.contents.add(Content.TEXTURE) + case "model": + self.contents.add(Content.MESH) + case "lua": + self.contents.add(Content.SCRIPT) + case "stream": + self.contents.add(Content.SOUND) + case "txt": + self.contents.add(Content.STRING) + case "json": + self.contents.add(Content.CONFIG) + case _: + pass + return mobase.IFileTree.WalkReturn.CONTINUE + + def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: + filetree.walk(self.walkContent, "/") + return list(self.contents) + + +class RaidWW2ModDataChecker(mobase.ModDataChecker): + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self.organizer: mobase.IOrganizer = organizer + self.organizer.modList().onModInstalled(self._Fix_Installed_Mod) + self.needsNameFix = False + + def move_overwrite_merge(self, source, destination): + if not os.path.exists(destination): + shutil.move(source, destination) + return + if os.path.isfile(source): + os.replace(source, destination) + return + for item in os.listdir(source): + s_item = os.path.join(source, item) + d_item = os.path.join(destination, item) + self.move_overwrite_merge(s_item, d_item) + os.rmdir(source) + + def _Fix_Installed_Mod(self, mod: mobase.IModInterface): + if not self.needsNameFix: + return + filetree: mobase.IFileTree = mod.fileTree() + fixed = False + modname = mod.name() + if filetree is not None and filetree.exists("FOLDERNAME", mobase.IFileTree.DIRECTORY): + path = mod.absolutePath() + old_path = os.path.join(path, "FOLDERNAME") + new_path = os.path.join(path, f"{modname}") + self.move_overwrite_merge(old_path, new_path) + fixed = True + if not fixed: + return + self.needsNameFix = False + + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + if len(filetree) == 1: + return mobase.ModDataChecker.VALID + return mobase.ModDataChecker.FIXABLE + + def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): + for branch in filetree: + if branch is not None and branch.isDir(): + for e in branch: + if e is not None and e.name() == name: + return True + return False + + def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): + entriesToMove: list[mobase.FileTreeEntry] = [] + retVal = 0 + for e in filetree: + if e is not None: + entriesToMove.append(e) + for e in entriesToMove: + filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) + retVal = 1 + return retVal + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + treefixed = self.allMoveTo(filetree, "FOLDERNAME/") + if treefixed == 1: + self.needsNameFix = True + return filetree + + +class RaidWW2Game(BasicGame): + Name = "RAID World War II Support Plugin" + Author = "modworkshop" + Version = "1" + GameName = "RAID World War II" + GameShortName = "raidww2" + GameSteamId = 414740 + GameBinary = "raid_win64_release.exe" + GameDataPath = "mods" + GameDocumentsDirectory = "%LOCALAPPDATA%/RAID WW2" + _forced_libraries = ["IPHLPAPI.dll", "WSOCK32.dll"] + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self.dataChecker = RaidWW2ModDataChecker(organizer) + self._register_feature(self.dataChecker) + self._register_feature(RaidWW2ModDataContent()) + organizer.modList().onModStateChanged(self.dll_copy) + return True + + def dll_copy( + self, mods: dict[str, mobase.ModState] + ): + + game_path = self.dataDirectory().absolutePath() + "/" + + for key, value in mods.items(): + key = self._organizer.modList().getMod(key) + tree = key.fileTree() + for e in tree: + if e is not None and e.name() in self._forced_libraries: + #add file + file_path_source = key.absolutePath() + "/" + e.path() + file_path_target = game_path + e.name() + if value == 35: + shutil.copyfile(file_path_source, file_path_target) + #remove file + if value == 33: + if os.path.exists(file_path_target): + os.remove(file_path_target) + + def executables(self): + return [ + mobase.ExecutableInfo( + "Raid: World War II", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ), + ] + + @cached_property + def _base_dlls(self) -> set[str]: + base_dir = Path(self.gameDirectory().absolutePath()) + return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + try: + efls = super().executableForcedLoads() + except AttributeError: + efls = [] + libs: set[str] = set() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + if type(tree) is not mobase.IFileTree: + return efls + for e in tree: + relpath = e.pathFrom(tree) + if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + libs.add(relpath) + exes = self.executables() + efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + return efls + + def iniFiles(self): + return ["renderer_settings.xml"] + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + modsPath = self.dataDirectory().absolutePath() + if not os.path.exists(modsPath): + os.mkdir(modsPath) + super().initializeProfile(directory, settings) \ No newline at end of file diff --git a/games/game_roadtovostok.py b/games/game_roadtovostok.py new file mode 100644 index 00000000..81e5b23e --- /dev/null +++ b/games/game_roadtovostok.py @@ -0,0 +1,89 @@ +import os +import shutil +import mobase + +from enum import IntEnum, auto +from pathlib import Path +from typing import Any, List, Set, cast + +from ..basic_game import BasicGame + +try: + from PyQt6.QtCore import QDir, QFileInfo +except: + from PyQt5.QtCore import QDir, QFileInfo + + +class RoadToVostokModDataChecker(mobase.ModDataChecker): + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self.organizer: mobase.IOrganizer = organizer + + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + + if filetree.exists("mods", mobase.IFileTree.DIRECTORY) and not filetree.exists("mod.txt", mobase.IFileTree.FILE): + return mobase.ModDataChecker.VALID + for e in filetree: + if e is not None and e.isFile() and e.suffix().casefold() == "pck": + return mobase.ModDataChecker.VALID + return mobase.ModDataChecker.FIXABLE + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + GameModsPath = self.organizer.managedGame().GameModsPath + "/" + treefixed = 0 + + for branch in filetree: + mod_name = filetree.name() + if mod_name == "": + mod_name = branch.name() + mod_path = os.path.join(self.organizer.modsPath(), mod_name) + if filetree.createOrphanTree("OrphanTree") is None and os.path.exists(mod_path) and branch.suffix().casefold() == "zip": + os.makedirs(os.path.join(mod_path, GameModsPath), exist_ok=True) + shutil.move(os.path.join(mod_path, branch.name()), os.path.join(mod_path, GameModsPath, branch.name())) + treefixed = 1 + + if treefixed == 0: + return None + return filetree + + +class RoadToVostokGame(BasicGame): + + Name = "Road to Vostok Support Plugin" + Author = "modworkshop" + Version = "1" + GameName = "Road to Vostok" + GameShortName = "road-to-vostok" + GameSteamId = 1963610 + GameBinary = "Road_to_Vostok_Demo.exe" + GameDataPath = "%GAME_PATH%" + GameModsPath = "mods" + GameDocumentsDirectory = '%APPDATA%/Godot/app_userdata/Road to Vostok' + GameSaveExtension = "tres" + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self.dataChecker = RoadToVostokModDataChecker(organizer) + self._register_feature(self.dataChecker) + return True + + def executables(self): + return [ + mobase.ExecutableInfo( + "Road to Vostok (Use Injector)", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ).withArgument("--main-pack Injector.pck"), + mobase.ExecutableInfo( + "Road to Vostok (No Mods)", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ), + ] + + def iniFiles(self): + return ["settings.cfg"] + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + modsPath = self.dataDirectory().absolutePath() + if not os.path.exists(modsPath): + os.mkdir(modsPath) + super().initializeProfile(directory, settings) \ No newline at end of file diff --git a/games/game_silenthill2remake.py b/games/game_silenthill2remake.py index 4683a5d1..022c0e8c 100644 --- a/games/game_silenthill2remake.py +++ b/games/game_silenthill2remake.py @@ -1,113 +1,285 @@ -from typing import Tuple - +import json +import os +import shutil import mobase -from ..basic_features import BasicModDataChecker, GlobPatterns +from enum import IntEnum, auto +from pathlib import Path +from typing import Any, List, Set, cast +from functools import cached_property + +from .unreal_tabs.constants import DEFAULT_UE4SS_MODS, UE4SSModInfo +from .unreal_tabs.manage_paks.widget import PaksTabWidget +from .unreal_tabs.manage_ue4ss.widget import UE4SSTabWidget + from ..basic_game import BasicGame +try: + from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget + from PyQt6.QtCore import QDir, QFileInfo +except: + from PyQt5.QtWidgets import QMainWindow, QTabWidget, QWidget + from PyQt5.QtCore import QDir, QFileInfo -class SilentHill2RemakeModDataChecker(BasicModDataChecker): - def __init__(self): - super().__init__( - GlobPatterns( - delete=[ - "*.txt", - "*.md", - "manifest.json", - "icon.png", - ], - ) - ) - self.mod_path = ["SHProto", "Content", "Paks", "~mod"] - self.mod_path_lower = [name.lower() for name in self.mod_path] - - def _find_tree( - self, filetree: mobase.IFileTree - ) -> Tuple[str | None, mobase.FileTreeEntry | None]: - """ - Search the given filetree for a directory name that matches any component - of self.mod_path (case-insensitive). - - Returns: - (prefix, entry) - prefix: The missing part before the match (e.g. 'SHProto/Content/') - entry: The IFileTree entry that matched (e.g. the 'Paks' directory) - (None, None) if nothing matches. - """ - for entry in filetree: - if not entry.isDir(): - continue - - name_lower = entry.name().lower() - for i, component in enumerate(self.mod_path_lower): - if name_lower == component: - # Build the prefix string for everything *before* this match - prefix_parts = self.mod_path[:i] - prefix = "/".join(prefix_parts) + ("/" if prefix_parts else "") - return (prefix, entry) - # No matches found - return (None, None) - - def dataLooksValid( - self, filetree: mobase.IFileTree - ) -> mobase.ModDataChecker.CheckReturn: - # Check for fully valid layout - has_entry, _ = self._find_tree(filetree) - if has_entry is None: - # in this case we check to make sure there's a .pak file - for entry in filetree: - if entry.name().lower().endswith(".pak") and entry.isFile(): - return mobase.ModDataChecker.FIXABLE - elif has_entry == "": + +class Content(IntEnum): + UCAS = auto() + UTOC = auto() + PAK = auto() + UE4SS = auto() + DLL = auto() + BK2 = auto() + + +class SilentHill2ModDataContent(mobase.ModDataContent): + contents: list[int] = [] + GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ + (Content.UCAS, "UCAS", ":/MO/gui/content/geometries"), + (Content.UTOC, "UTOC", ":/MO/gui/content/inifile"), + (Content.PAK, "PAK", ":/MO/gui/content/geometries"), + (Content.UE4SS, "UE4SS", ":/MO/gui/content/script"), + (Content.DLL, "DLL", ":/MO/gui/content/skse"), + (Content.BK2, "Video", ":/MO/gui/content/skse"), + ] + + def getAllContents(self) -> list[mobase.ModDataContent.Content]: + return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + + def walkContent(self, path: str, entry: mobase.FileTreeEntry): + if entry.isFile(): + match entry.suffix().casefold(): + case "utoc": + self.contents.add(Content.UTOC) + case "ucas": + self.contents.add(Content.UCAS) + case "pak": + self.contents.add(Content.PAK) + case "lua": + self.contents.add(Content.UE4SS) + case "dll": + self.contents.add(Content.DLL) + case "bk2": + self.contents.add(Content.BK2) + case _: + pass + return mobase.IFileTree.WalkReturn.CONTINUE + + def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: + self.contents: set[int] = set() + filetree.walk(self.walkContent, "/") + return list(self.contents) + + +class SilentHill2ModDataChecker(mobase.ModDataChecker): + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self.organizer: mobase.IOrganizer = organizer + + def move_overwrite_merge(self, source, destination): + if not os.path.exists(destination): + shutil.move(source, destination) + return + if os.path.isfile(source): + os.replace(source, destination) + return + for item in os.listdir(source): + s_item = os.path.join(source, item) + d_item = os.path.join(destination, item) + self.move_overwrite_merge(s_item, d_item) + os.rmdir(source) + + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods + GameDataPakMods = self.organizer.managedGame().GameDataPakMods + GameDataMovies = self.organizer.managedGame().GameDataMovieMods + if filetree.exists(GameDataPakMods, mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + if filetree.exists(GameDataMovies, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID - else: - return mobase.ModDataChecker.FIXABLE + if filetree.exists(GameDataUE4SSMods, mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + return mobase.ModDataChecker.FIXABLE + + def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): + for branch in filetree: + if branch is not None and branch.isDir(): + for e in branch: + if e is not None and e.name() == name: + return True + return False - # Otherwise, not recognizable - return mobase.ModDataChecker.INVALID + def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): + entriesToMove: list[mobase.FileTreeEntry] = [] + retVal = 0 + for e in filetree: + if e is not None: + entriesToMove.append(e) + for e in entriesToMove: + filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) + retVal = 1 + return retVal def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: - filetree = super().fix(filetree) - prefix, item = self._find_tree(filetree) - if prefix is None: - foundAPak = False - # Move all top-level items to BepInEx/plugins/ - items_to_move = list(filetree) - for cur_item in items_to_move: - if cur_item.name().lower().endswith(".pak"): - foundAPak = True - filetree.move(cur_item, f"SHProto/Content/Paks/~mod/{cur_item.name()}") - # foundAPack MUST be true because if 'prefix' returned None then - # there must be a .pak file or dataLooksValid wouldn't have returned - # a FIXABLE. This is therefore just a sanity check - assert foundAPak - return filetree - elif prefix == "": - return filetree - else: - # if prefix is not None then item cannot be None - assert item is not None - filetree.move(item, f"{prefix}{item.name()}") - return filetree - - -class SilentHill2RemakeGame(BasicGame): + GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods + "/" + GameDataPakMods = self.organizer.managedGame().GameDataPakMods + "/" + GameDataMovies = self.organizer.managedGame().GameDataMovieMods + "/" + treefixed = 0 + if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): + treefixed = self.allMoveTo(filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/") + if treefixed == 1: + return filetree + if filetree.exists("Scripts", mobase.IFileTree.DIRECTORY) or filetree.exists("dlls", mobase.IFileTree.DIRECTORY): + treefixed = self.allMoveTo(filetree, GameDataUE4SSMods) + if treefixed == 1: + return filetree + if treefixed == 0: + allowedUnzippedExt = ["pak", "utoc", "ucas", "bk2", "dll"] + entriesToMove: list[mobase.FileTreeEntry] = [] + for e in filetree: + if e is not None: + if e.isFile(): + fileext = e.suffix().casefold() + if fileext in allowedUnzippedExt: + mod_name = filetree.name() + if mod_name == "": + mod_name = e.name() + mod_path = os.path.join(self.organizer.modsPath(), mod_name) + if filetree.createOrphanTree("OrphanTree") is None and os.path.exists(mod_path): + match e.suffix().casefold(): + case "pak" | "utoc" | "ucas": + os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) + case "bk2": + os.makedirs(os.path.join(mod_path, GameDataMovies), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovies, e.name())) + case _: + pass + treefixed = 1 + else: + entriesToMove.append(e) + if entriesToMove is not None: + for e in entriesToMove: + match e.suffix().casefold(): + case "pak" | "utoc" | "ucas": + filetree.move(e, GameDataPakMods, mobase.IFileTree.MERGE) + case "dll": + filetree.move(e, os.path.dirname(GameDataUE4SSMods) + "/", mobase.IFileTree.MERGE) + case "bk2": + filetree.move(e, GameDataMovies, mobase.IFileTree.MERGE) + case _: + pass + treefixed = 1 + if treefixed == 0: + return None + return filetree + + +class SilentHill2Game(BasicGame): + Name = "Silent Hill 2 Support Plugin" + Author = "modworkshop" + Version = "1" + GameName = "Silent Hill 2 Remake" + GameLauncher = "SHProto.exe" + GameShortName = "silenthill-2" + GameSteamId = 2124490 + GameBinary = "SHProto/Binaries/Win64/SHProto-Win64-Shipping.exe" + GameDataPath = "SHProto" + GameDataUE4SSMods = "Binaries/Win64/Mods" + GameDataPakMods = "Content/Paks/~Mods" + GameDataMovieMods = "Content/Movies" + GameDocumentsDirectory = "%LOCALAPPDATA%/SilentHill2/Saved/Config/Windows" + GameSaveExtension = "sav" + _main_window: QMainWindow + _ue4ss_tab: UE4SSTabWidget + _paks_tab: PaksTabWidget + def init(self, organizer: mobase.IOrganizer) -> bool: super().init(organizer) - self._register_feature(SilentHill2RemakeModDataChecker()) + self.dataChecker = SilentHill2ModDataChecker(organizer) + self._register_feature(self.dataChecker) + self._register_feature(SilentHill2ModDataContent()) + organizer.onUserInterfaceInitialized(self.init_tab) return True - Name = "Silent Hill 2 Remake Support Plugin" - Author = "HomerSimpleton Returns" - Version = "1.0" + def init_tab(self, main_window: QMainWindow): + if self._organizer.managedGame() != self: + return + self._main_window = main_window + tab_widget: QTabWidget = main_window.findChild(QTabWidget, "tabWidget") + if not tab_widget or not tab_widget.findChild(QWidget, "espTab"): + return + self._ue4ss_tab = UE4SSTabWidget(main_window, self._organizer) + plugin_tab = tab_widget.findChild(QWidget, "espTab") + tab_index = tab_widget.indexOf(plugin_tab) + 1 + if not tab_widget.isTabVisible(tab_widget.indexOf(plugin_tab)): + tab_index += 1 + tab_widget.insertTab(tab_index, self._ue4ss_tab, "UE4SS") + self._paks_tab = PaksTabWidget(main_window, self._organizer) + tab_index += 1 + tab_widget.insertTab(tab_index, self._paks_tab, "Paks") - GameName = "Silent Hill 2 Remake" - GameShortName = "silenthill2" - GameNexusName = "silenthill2" + def executables(self): + return [ + mobase.ExecutableInfo( + "Silent Hill 2", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ) + ] - GameBinary = "SHProto/Binaries/Win64/SHProto-Win64-Shipping.exe" - GameLauncher = "SHProto.exe" - GameDataPath = "%GAME_PATH%" - GameSupportURL = "https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/Game:-Silent-Hill-2-Remake" + @cached_property + def _base_dlls(self) -> set[str]: + base_dir = Path(self.gameDirectory().absolutePath()) + return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + try: + efls = super().executableForcedLoads() + except AttributeError: + efls = [] + libs: set[str] = set() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + if type(tree) is not mobase.IFileTree: + return efls + for e in tree: + relpath = e.pathFrom(tree) + if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + libs.add(relpath) + exes = self.executables() + efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + return efls + + def paksDirectory(self) -> QDir: + return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataPakMods) + + def ue4ssDirectory(self) -> QDir: + return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataUE4SSMods) + + def movieDirectory(self) -> QDir: + return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataMovieMods) + + def write_default_mods(self, profile: QDir): + ue4ss_mods_txt = QFileInfo(profile.absoluteFilePath("mods.txt")) + ue4ss_mods_json = QFileInfo(profile.absoluteFilePath("mods.json")) + if not ue4ss_mods_txt.exists(): + with open(ue4ss_mods_txt.absoluteFilePath(), "w") as mods_txt: + for mod in DEFAULT_UE4SS_MODS: + mods_txt.write(f"{mod['mod_name']} : 1\n") + if not ue4ss_mods_json.exists(): + mods_data: list[UE4SSModInfo] = [] + for mod in DEFAULT_UE4SS_MODS: + mods_data.append({"mod_name": mod["mod_name"], "mod_enabled": True}) + with open(ue4ss_mods_json.absoluteFilePath(), "w") as mods_json: + mods_json.write(json.dumps(mods_data, indent=4)) + + def iniFiles(self): + return ["GameUserSettings.ini", "Input.ini"] - GameGogId = [1225972913, 2051029707] + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + self.write_default_mods(directory) + if not self.paksDirectory().exists(): + os.makedirs(self.paksDirectory().absolutePath()) + if not self.ue4ssDirectory().exists(): + os.makedirs(self.ue4ssDirectory().absolutePath()) + if not self.movieDirectory().exists(): + os.makedirs(self.movieDirectory().absolutePath()) + super().initializeProfile(directory, settings) \ No newline at end of file diff --git a/games/game_titanfall2.py b/games/game_titanfall2.py new file mode 100644 index 00000000..a5708d24 --- /dev/null +++ b/games/game_titanfall2.py @@ -0,0 +1,289 @@ +import os +import shutil +import json +import mobase + +from json import JSONDecodeError +from enum import IntEnum, auto +from pathlib import Path +from typing import Any, List, Set, cast +from functools import cached_property + +from ..basic_game import BasicGame + +try: + from PyQt6.QtCore import QDir, QFileInfo +except: + from PyQt5.QtCore import QDir, QFileInfo + + +class Content(IntEnum): + MATERIAL = auto() + TEXTURE = auto() + MODELS = auto() + SCRIPT = auto() + CONFIG = auto() + VIDEO = auto() + AUDIO = auto() + STARPAK = auto() + + +class Titanfall2ModDataContent(mobase.ModDataContent): + GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ + (Content.MATERIAL, "Materials", ":/MO/gui/content/interface"), + (Content.TEXTURE, "Textures", ":/MO/gui/content/texture"), + (Content.MODELS, "Models", ":/MO/gui/content/mesh"), + (Content.SCRIPT, "Scripts", ":/MO/gui/content/script"), + (Content.CONFIG, "Configs", ":/MO/gui/content/inifile"), + (Content.VIDEO, "Video", ":/MO/gui/content/modgroup"), + (Content.AUDIO, "Audio", ":/MO/gui/content/sound"), + (Content.STARPAK, "Starpak", ":/MO/gui/content/bsa"), + ] + + def getAllContents(self) -> list[mobase.ModDataContent.Content]: + return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + + contents = set() + + def walkContent(self, path: str, entry: mobase.FileTreeEntry): + if entry.isFile(): + match entry.suffix().casefold(): + case "vmt": + self.contents.add(Content.MATERIAL) + case "vtf": + self.contents.add(Content.TEXTURE) + case "mdl": + self.contents.add(Content.MODELS) + case "nut": + self.contents.add(Content.SCRIPT) + case "txt": + self.contents.add(Content.CONFIG) + case "bik": + self.contents.add(Content.VIDEO) + case "wav": + self.contents.add(Content.AUDIO) + case "rpak" | "starmap" | "starpak": + self.contents.add(Content.STARPAK) + case _: + pass + return mobase.IFileTree.WalkReturn.CONTINUE + + def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: + filetree.walk(self.walkContent, "/") + return list(self.contents) + + +class Titanfall2ModDataChecker(mobase.ModDataChecker): + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self.organizer: mobase.IOrganizer = organizer + self.organizer.modList().onModInstalled(self._Fix_Installed_Mod) + self.needsNameFix = False + + def move_overwrite_merge(self, source, destination): + if not os.path.exists(destination): + shutil.move(source, destination) + return + if os.path.isfile(source): + os.replace(source, destination) + return + for item in os.listdir(source): + s_item = os.path.join(source, item) + d_item = os.path.join(destination, item) + self.move_overwrite_merge(s_item, d_item) + os.rmdir(source) + + def _Fix_Installed_Mod(self, mod: mobase.IModInterface): + if not self.needsNameFix: + return + northstarModPath = self.organizer.managedGame().GameNorthstarPath + "/" + filetree: mobase.IFileTree = mod.fileTree() + fixed = False + modname = mod.name() + if filetree is not None and filetree.exists(northstarModPath + "FOLDERNAME", mobase.IFileTree.DIRECTORY): + path = mod.absolutePath() + json_path = os.path.join(path, northstarModPath + "FOLDERNAME/mod.json") + mod_data = json.load(open(json_path, encoding="utf-8")) + modname = mod_data["name"] + old_path = os.path.join(path, northstarModPath + "FOLDERNAME") + new_path = os.path.join(path, northstarModPath + f"{modname}") + self.move_overwrite_merge(old_path, new_path) + fixed = True + elif filetree is not None and filetree.exists(northstarModPath + "FOLDERNAME_NAME", mobase.IFileTree.DIRECTORY): + path = mod.absolutePath() + old_path = os.path.join(path, northstarModPath + "FOLDERNAME_NAME") + new_path = os.path.join(path, northstarModPath + f"{modname}") + self.move_overwrite_merge(old_path, new_path) + fixed = True + if not fixed: + return + self.needsNameFix = False + + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + if filetree.exists("R2Northstar", mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + return mobase.ModDataChecker.FIXABLE + + def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): + for branch in filetree: + if branch is not None and branch.isDir(): + for e in branch: + if e is not None and e.name() == name: + return True + return False + + def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): + entriesToMove: list[mobase.FileTreeEntry] = [] + retVal = 0 + for e in filetree: + if e is not None: + entriesToMove.append(e) + for e in entriesToMove: + filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) + retVal = 1 + return retVal + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + northstarModPath = self.organizer.managedGame().GameNorthstarPath + "/" + treefixed = 0 + if filetree.exists("mod.json", mobase.IFileTree.FILE): + treefixed = self.allMoveTo(filetree, northstarModPath + "FOLDERNAME/") + if treefixed == 1: + self.needsNameFix = True + elif self.fileExistsInNextSubDir(filetree, "mod.json"): + filetree.move(filetree[0], northstarModPath, mobase.IFileTree.MERGE) + treefixed = 1 + else: + try: + if filetree[0][0].exists("mod.json", mobase.IFileTree.FILE): + filetree.move(filetree[0][0], filetree[0].path("/"), mobase.IFileTree.REPLACE) + filetree.move(filetree[0], northstarModPath, mobase.IFileTree.MERGE) + treefixed = 1 + except TypeError: + pass + if treefixed == 0: + if len(filetree) == 1 and filetree[0].isDir: + filetree.move(filetree[0], northstarModPath, mobase.IFileTree.MERGE) + treefixed = 1 + else: + for e in filetree: + if e is not None and e.path("/").count("/") == 0: + filetree.move(e, northstarModPath + "FOLDERNAME_NAME/", mobase.IFileTree.MERGE) + treefixed = 1 + self.needsNameFix = True + if treefixed == 0: + return None + return filetree + + +class Titanfall2Game(BasicGame): + Name = "Titanfall 2 Support Plugin" + Author = "modworkshop" + Version = "1" + GameName = "Titanfall 2" + GameShortName = "titanfall-2" + GameSteamId = 1237970 + GameBinary = "Titanfall2.exe" + GameDataPath = "%GAME_PATH%" + GameNorthstarPath = "R2Northstar/mods" + NorthstarModJson = "enabledmods.json" + GameDocumentsDirectory = "%USERPROFILE%/Documents/Respawn/Titanfall2/profile" + GameSavesDirectory = "%USERPROFILE%/Documents/Respawn/Titanfall2/profile/savegames/" + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self.dataChecker = Titanfall2ModDataChecker(organizer) + self._register_feature(self.dataChecker) + self._register_feature(Titanfall2ModDataContent()) + organizer.modList().onModStateChanged(self.update_enable_mods_json) + return True + + def update_enable_mods_json(self, mods: dict[str, mobase.ModState]): + Northstar_Config_Json = self._organizer.profilePath() + "/" + self.NorthstarModJson + with open(Northstar_Config_Json, "r", encoding="utf-8") as f: + Northstar = json.load(f) + for key, value in mods.items(): + key = self._organizer.modList().getMod(key) + tree = key.fileTree() + subtree = tree.find("R2Northstar/mods", mobase.IFileTree.DIRECTORY) + if subtree is not None and subtree.isDir(): + for e in subtree: + if e is not None and e.isDir(): + if e.exists("mod.json", mobase.IFileTree.FILE): + json_path = key.absolutePath() + "/" + e.path() + "/mod.json" + with open(json_path, "r", encoding="utf-8") as f: + mod_data = json.load(f) + modname = mod_data["Name"] + if "Version" not in mod_data: + modversion = "0.0.0" + else: + modversion = mod_data["Version"] + if value == 35 and modname not in Northstar: + Northstar[modname] = {modversion: True} + if value == 33 and modname in Northstar: + removed_value = Northstar.pop(modname) + with open(Northstar_Config_Json, "w", encoding="utf-8") as f: + json.dump(Northstar, f, ensure_ascii=False, indent=4) + + def executables(self): + return [ + mobase.ExecutableInfo( + "Titanfall 2", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ), + mobase.ExecutableInfo("Northstar", QFileInfo(self.gameDirectory(), "NorthstarLauncher.exe")), + ] + + @cached_property + def _base_dlls(self) -> set[str]: + base_dir = Path(self.gameDirectory().absolutePath()) + return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + try: + efls = super().executableForcedLoads() + except AttributeError: + efls = [] + libs: set[str] = set() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + if type(tree) is not mobase.IFileTree: + return efls + for e in tree: + relpath = e.pathFrom(tree) + if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + libs.add(relpath) + exes = self.executables() + efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + return efls + + def northstarDirectory(self) -> QDir: + return QDir(self.gameDirectory().absolutePath() + self.GameNorthstarPath) + + def iniFiles(self): + return ["profile.cfg"] + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + northstar_json_path = directory.absolutePath() + "/" + self.NorthstarModJson + northstar_json_game_path = self.gameDirectory().absolutePath() + "/R2Northstar/" + self.NorthstarModJson + blank_mod_json = '{"Version": 1,"Northstar.Client": {"1.31.6": true},"Northstar.CustomServers": {"1.31.6": true},"Northstar.Custom": {"1.31.6": true}}' + if not os.path.exists(northstar_json_path) or os.path.getsize(northstar_json_path) == 0: + if os.path.exists(northstar_json_game_path): + with open(northstar_json_game_path, "r") as game_json: + game_json_content = game_json.read() + game_json.close() + with open(northstar_json_path, "w") as northstar_json: + northstar_json.write(game_json_content) + northstar_json.close() + else: + with open(northstar_json_path, "w") as northstar_json: + northstar_json.write(blank_mod_json) + northstar_json.close() + modsPath = os.path.join(self.dataDirectory().absolutePath(), self.GameNorthstarPath) + if not os.path.exists(modsPath): + os.mkdir(modsPath) + super().initializeProfile(directory, settings) + + def mappings(self) -> list[mobase.Mapping]: + return [ + mobase.Mapping(self._organizer.profilePath() + "/" + self.NorthstarModJson, self.gameDirectory().absolutePath() + "/R2Northstar/" + self.NorthstarModJson, False, False), + ] \ No newline at end of file diff --git a/games/game_zuma_deluxe.py b/games/game_zuma_deluxe.py new file mode 100644 index 00000000..6511fc4d --- /dev/null +++ b/games/game_zuma_deluxe.py @@ -0,0 +1,329 @@ +import os +import shutil +import re +import mobase + +from enum import IntEnum, auto +from pathlib import Path +from typing import Any, List, Set, cast +from functools import cached_property + +from ..basic_game import BasicGame +from ..basic_features import BasicGameSaveGameInfo + +try: + from PyQt6.QtCore import QDir, QFileInfo +except: + from PyQt5.QtCore import QDir, QFileInfo + + +class Content(IntEnum): + TEXTURE = auto() + MESH = auto() + SCRIPT = auto() + SOUND = auto() + STRING = auto() + CONFIG = auto() + + +class ZumaModDataContent(mobase.ModDataContent): + GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ + (Content.TEXTURE, "Textures", ":/MO/gui/content/texture"), + (Content.MESH, "Meshes", ":/MO/gui/content/mesh"), + (Content.SCRIPT, "Scripts", ":/MO/gui/content/script"), + (Content.SOUND, "Sounds", ":/MO/gui/content/sound"), + (Content.STRING, "Strings", ":/MO/gui/content/string"), + (Content.CONFIG, "Configs", ":/MO/gui/content/inifile"), + ] + + def getAllContents(self) -> list[mobase.ModDataContent.Content]: + return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + + contents = set() + + def walkContent(self, path: str, entry: mobase.FileTreeEntry): + if entry.isFile(): + match entry.suffix().lower(): + case "texture": + self.contents.add(Content.TEXTURE) + case "model": + self.contents.add(Content.MESH) + case "lua": + self.contents.add(Content.SCRIPT) + case "stream": + self.contents.add(Content.SOUND) + case "txt": + self.contents.add(Content.STRING) + case "json": + self.contents.add(Content.CONFIG) + case _: + pass + return mobase.IFileTree.WalkReturn.CONTINUE + + def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: + filetree.walk(self.walkContent, "/") + return list(self.contents) + + +class ZumaModDataChecker(mobase.ModDataChecker): + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self.organizer: mobase.IOrganizer = organizer + self.organizer.modList().onModInstalled(self._Fix_Installed_Mod) + self.needsNameFix = False + + def move_overwrite_merge(self, source, destination): + if not os.path.exists(destination): + shutil.move(source, destination) + return + if os.path.isfile(source): + os.replace(source, destination) + return + for item in os.listdir(source): + s_item = os.path.join(source, item) + d_item = os.path.join(destination, item) + self.move_overwrite_merge(s_item, d_item) + os.rmdir(source) + + def _Fix_Installed_Mod(self, mod: mobase.IModInterface): + if not self.needsNameFix: + return + filetree: mobase.IFileTree = mod.fileTree() + fixed = False + modname = mod.name() + if filetree is not None and filetree.exists("mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): + path = mod.absolutePath() + old_path = os.path.join(path, "mods/FOLDERNAME") + new_path = os.path.join(path, f"mods/{modname}") + self.move_overwrite_merge(old_path, new_path) + fixed = True + if not fixed: + return + self.needsNameFix = False + + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + validFolders = ["images", "levels", "music", "sounds", "fonts", "properties", "userdata"] + validFiles = ["exe"] + for e in filetree: + if e.isDir(): + for folder in validFolders: + if filetree.exists(folder, mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + elif e.isFile(): + for ext in validFiles: + if e.suffix().lower() == ext: + return mobase.ModDataChecker.VALID + else: + pass + return mobase.ModDataChecker.FIXABLE + + def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): + for branch in filetree: + if branch is not None and branch.isDir(): + for e in branch: + if e is not None and e.name() == name: + return True + return False + + def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): + entriesToMove: list[mobase.FileTreeEntry] = [] + retVal = 0 + for e in filetree: + if e is not None: + entriesToMove.append(e) + for e in entriesToMove: + filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) + retVal = 1 + return retVal + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + GameLevelsPath = self.organizer.managedGame().GameLevelsPath + validFolders = ["images", "levels", "music", "sounds", "fonts", "properties", "userdata"] + entriesToMove: list[mobase.FileTreeEntry] = [] + treefixed = 0 + if filetree.exists("map.txt", mobase.IFileTree.FILE): + treefixed = self.allMoveTo(filetree, GameLevelsPath + "/FOLDERNAME/") + if treefixed == 1: + self.needsNameFix = True + elif self.fileExistsInNextSubDir(filetree, "map.txt"): + filetree.move(filetree[0], GameLevelsPath, mobase.IFileTree.MERGE) + treefixed = 1 + else: + moveonce = 0 + for branch in filetree: + if branch is not None and branch.isDir(): + for entry in branch: + for folder in validFolders: + if entry is not None and entry.name() == folder: + moveonce = 1 + if moveonce == 1: + for branch in filetree: + if branch is not None and branch.isDir(): + for entry in branch: + entriesToMove.append(entry) + if entriesToMove is not None: + for e in entriesToMove: + filetree.move(e, "", mobase.IFileTree.MERGE) + treefixed = 1 + for branch in filetree: + if branch is not None and branch.isDir(): + if len(branch) == 0: + filetree.remove(branch) + if treefixed == 0: + return None + return filetree + + +PROGRAM_DATA = str(os.getenv("ProgramData")) + + +class ZumaGame(BasicGame, mobase.IPluginFileMapper): + Name = "Zuma Deluxe Support Plugin" + Author = "modworkshop" + Version = "1" + GameName = "Zuma Deluxe" + GameShortName = "zuma" + GameSteamId = 3330 + GameBinary = "Zuma.exe" + GameDataPath = "%GAME_PATH%" + GameLevelsPath = "levels" + GameLevelsXml = "levels/levels.xml" + ProfileLevelsXml = "levels.xml" + GameDocumentsDirectory = PROGRAM_DATA + "/Steam/Zuma/userdata" + GameSaveExtension = "sav" + + def __init__(self): + BasicGame.__init__(self) + mobase.IPluginFileMapper.__init__(self) + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self.dataChecker = ZumaModDataChecker(organizer) + self._register_feature(self.dataChecker) + self._register_feature(ZumaModDataContent()) + self._register_feature(BasicGameSaveGameInfo()) + organizer.modList().onModStateChanged(self.update_levels) + return True + + def update_levels(self, mods: dict[str, mobase.ModState]): + profile_levels_path = self._organizer.profilePath() + "/" + self.ProfileLevelsXml + game_levels_path = os.path.join(self.dataDirectory().absolutePath(), self.GameLevelsXml) + for key, value in mods.items(): + key = self._organizer.modList().getMod(key) + tree = key.fileTree() + if tree.exists("levels/levels.xml", mobase.IFileTree.FILE): + levels_txt_path = os.path.join(key.absolutePath(), "levels/levels.xml") + profile_levels_path = self._organizer.profilePath() + "/" + self.ProfileLevelsXml + if value == 35: + with open(levels_txt_path, "r") as levels_txt: + levels_txt_content = levels_txt.read() + levels_txt.close() + with open(profile_levels_path, "w") as profile_levels: + profile_levels.write(levels_txt_content) + profile_levels.close() + if value == 33: + with open(game_levels_path, "r") as game_levels: + game_levels_content = game_levels.read() + game_levels.close() + with open(profile_levels_path, "w") as profile_levels: + profile_levels.write(game_levels_content) + profile_levels.close() + for key, value in mods.items(): + key = self._organizer.modList().getMod(key) + map_txt_path = os.path.join(key.absolutePath(), "levels/map.txt") + tree = key.fileTree() + if tree.exists("levels/map.txt", mobase.IFileTree.FILE): + with open(map_txt_path, "r") as map_txt: + map_txt_content = map_txt.read() + map_txt.close() + graphics_pattern = r"(?=)" + levels_pattern = r"(?=)" + id_pattern = r'(?<=id=")(.*?)(?=")' + graphics_tag = re.findall(graphics_pattern, map_txt_content, re.DOTALL) + levels_tag = re.findall(levels_pattern, map_txt_content, re.DOTALL) + id_name = re.findall(id_pattern, map_txt_content, re.DOTALL) + with open(profile_levels_path, "r+") as profile_levels: + profile_levels_content = profile_levels.read() + profile_levels.seek(0) + if value == 35: + insert_graphics_string = "" + for graphic in graphics_tag: + insert_graphics_string += "\n\n" + graphic + insert_graphics_string += "\n\n set[str]: + base_dir = Path(self.gameDirectory().absolutePath()) + return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + try: + efls = super().executableForcedLoads() + except AttributeError: + efls = [] + libs: set[str] = set() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + if type(tree) is not mobase.IFileTree: + return efls + for e in tree: + relpath = e.pathFrom(tree) + if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + libs.add(relpath) + exes = self.executables() + efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + return efls + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + modsPath = self.dataDirectory().absolutePath() + profile_levels_path = directory.absolutePath() + "/" + self.ProfileLevelsXml + game_levels_path = os.path.join(self.dataDirectory().absolutePath(), self.GameLevelsXml) + if not os.path.exists(profile_levels_path) or os.path.getsize(profile_levels_path) == 0: + with open(game_levels_path, "r") as game_levels: + profile_levels_content = game_levels.read() + game_levels.close() + with open(profile_levels_path, "w") as profile_levels: + profile_levels.write(profile_levels_content) + profile_levels.close() + if not os.path.exists(modsPath): + os.mkdir(modsPath) + super().initializeProfile(directory, settings) + + def mappings(self) -> list[mobase.Mapping]: + return [ + mobase.Mapping(self._organizer.profilePath() + "/" + self.ProfileLevelsXml, self.gameDirectory().absolutePath() + "/" + self.GameLevelsXml, False, False), + ] \ No newline at end of file diff --git a/games/unreal_tabs/__init__.py b/games/unreal_tabs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/games/unreal_tabs/constants.py b/games/unreal_tabs/constants.py new file mode 100644 index 00000000..0b10c821 --- /dev/null +++ b/games/unreal_tabs/constants.py @@ -0,0 +1,11 @@ +from typing import TypedDict + + +class UE4SSModInfo(TypedDict): + mod_name: str + mod_enabled: bool + +DEFAULT_UE4SS_MODS: list[UE4SSModInfo] = [ + {"mod_name": "BPML_GenericFunctions", "mod_enabled": True}, + {"mod_name": "BPModLoaderMod", "mod_enabled": True}, +] diff --git a/games/unreal_tabs/manage_paks/__init__.py b/games/unreal_tabs/manage_paks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/games/unreal_tabs/manage_paks/model.py b/games/unreal_tabs/manage_paks/model.py new file mode 100644 index 00000000..9e7216f7 --- /dev/null +++ b/games/unreal_tabs/manage_paks/model.py @@ -0,0 +1,245 @@ +import itertools +import typing +from enum import IntEnum, auto +from typing import Any, TypeAlias, overload + +import mobase + +try: + from PyQt6.QtCore import (QAbstractItemModel, QByteArray, QDataStream, QDir, QFileInfo, QMimeData, QModelIndex, QObject, Qt, QVariant) + from PyQt6.QtWidgets import QWidget +except: + from PyQt5.QtCore import (QAbstractItemModel, QByteArray, QDataStream, QDir, QFileInfo, QMimeData, QModelIndex, QObject, Qt, QVariant) + from PyQt5.QtWidgets import QWidget + +_PakInfo: TypeAlias = tuple[str, str, str, str] + +class PaksColumns(IntEnum): + PRIORITY = auto() + PAK_NAME = auto() + SOURCE = auto() + + +class PaksModel(QAbstractItemModel): + def __init__(self, parent: QWidget | None, organizer: mobase.IOrganizer): + super().__init__(parent) + self.paks: dict[int, _PakInfo] = {} + self._organizer = organizer + self._init_mod_states() + + def _init_mod_states(self): + profile = QDir(self._organizer.profilePath()) + paks_txt = QFileInfo(profile.absoluteFilePath("paks.txt")) + if paks_txt.exists(): + with open(paks_txt.absoluteFilePath(), "r") as paks_file: + index = 0 + for line in paks_file: + self.paks[index] = (line, "", "", "") + index += 1 + + def set_paks(self, paks: dict[int, _PakInfo]): + self.layoutAboutToBeChanged.emit() + self.paks = paks + self.layoutChanged.emit() + self.dataChanged.emit( + self.index(0, 0), + self.index(self.rowCount(), self.columnCount()), + [Qt.ItemDataRole.DisplayRole], + ) + + def flags(self, index: QModelIndex) -> Qt.ItemFlag: + if not index.isValid(): + return ( + Qt.ItemFlag.ItemIsSelectable + | Qt.ItemFlag.ItemIsDragEnabled + | Qt.ItemFlag.ItemIsDropEnabled + | Qt.ItemFlag.ItemIsEnabled + ) + return ( + super().flags(index) + | Qt.ItemFlag.ItemIsDragEnabled + | Qt.ItemFlag.ItemIsDropEnabled & Qt.ItemFlag.ItemIsEditable + ) + + def columnCount(self, parent: QModelIndex = QModelIndex()) -> int: + return len(PaksColumns) + + def index( + self, row: int, column: int, parent: QModelIndex = QModelIndex() + ) -> QModelIndex: + if ( + row < 0 + or row >= self.rowCount() + or column < 0 + or column >= self.columnCount() + ): + return QModelIndex() + return self.createIndex(row, column, row) + + @overload + def parent(self, child: QModelIndex) -> QModelIndex: ... + @overload + def parent(self) -> QObject | None: ... + + def parent(self, child: QModelIndex | None = None) -> QModelIndex | QObject | None: + if child is None: + return super().parent() + return QModelIndex() + + def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: + return len(self.paks) + + def setData( + self, index: QModelIndex, value: Any, role: int = Qt.ItemDataRole.EditRole + ) -> bool: + return False + + def headerData( + self, + section: int, + orientation: Qt.Orientation, + role: int = Qt.ItemDataRole.DisplayRole, + ) -> typing.Any: + if ( + orientation != Qt.Orientation.Horizontal + or role != Qt.ItemDataRole.DisplayRole + ): + return QVariant() + + column = PaksColumns(section + 1) + match column: + case PaksColumns.PAK_NAME: + return "Pak Group" + case PaksColumns.PRIORITY: + return "Priority" + case PaksColumns.SOURCE: + return "Source" + + return QVariant() + + def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any: + if not index.isValid(): + return None + if index.column() + 1 == PaksColumns.PAK_NAME: + if role == Qt.ItemDataRole.DisplayRole: + return self.paks[index.row()][0] + elif index.column() + 1 == PaksColumns.PRIORITY: + if role == Qt.ItemDataRole.DisplayRole: + return index.row() + elif index.column() + 1 == PaksColumns.SOURCE: + if role == Qt.ItemDataRole.DisplayRole: + return self.paks[index.row()][1] + return QVariant() + + def canDropMimeData( + self, + data: QMimeData | None, + action: Qt.DropAction, + row: int, + column: int, + parent: QModelIndex, + ) -> bool: + if action == Qt.DropAction.MoveAction and (row != -1 or column != -1): + return True + return False + + def supportedDropActions(self) -> Qt.DropAction: + return Qt.DropAction.MoveAction + + def dropMimeData( + self, + data: QMimeData | None, + action: Qt.DropAction, + row: int, + column: int, + parent: QModelIndex, + ) -> bool: + if action == Qt.DropAction.IgnoreAction: + return True + + if data is None: + return False + + encoded: QByteArray = data.data("application/x-qabstractitemmodeldatalist") + stream: QDataStream = QDataStream(encoded, QDataStream.OpenModeFlag.ReadOnly) + source_rows: list[int] = [] + + while not stream.atEnd(): + source_row = stream.readInt() + col = stream.readInt() + size = stream.readInt() + item_data = {} + for _ in range(size): + role = stream.readInt() + value = stream.readQVariant() + item_data[role] = value + if col == 0: + source_rows.append(source_row) + + if row == -1: + row = parent.row() + + if row < 0 or row >= len(self.paks): + new_priority = len(self.paks) + else: + new_priority = row + + before_paks: list[_PakInfo] = [] + moved_paks: list[_PakInfo] = [] + after_paks: list[_PakInfo] = [] + before_paks_p: list[_PakInfo] = [] + moved_paks_p: list[_PakInfo] = [] + after_paks_p: list[_PakInfo] = [] + for row, paks in sorted(self.paks.items()): + if row < new_priority: + if row in source_rows: + if paks[0].casefold()[-2:] == "_p": + moved_paks_p.append(paks) + else: + moved_paks.append(paks) + else: + if paks[0].casefold()[-2:] == "_p": + before_paks_p.append(paks) + else: + before_paks.append(paks) + if row >= new_priority: + if row in source_rows: + if paks[0].casefold()[-2:] == "_p": + moved_paks_p.append(paks) + else: + moved_paks.append(paks) + else: + if paks[0].casefold()[-2:] == "_p": + after_paks_p.append(paks) + else: + after_paks.append(paks) + + new_paks = dict( + enumerate( + itertools.chain( + before_paks, + moved_paks, + after_paks, + before_paks_p, + moved_paks_p, + after_paks_p, + ) + ) + ) + + index = 8999 + for row, pak in new_paks.items(): + current_dir = QDir(pak[2]) + parent_dir = QDir(pak[2]) + parent_dir.cdUp() + if current_dir.exists() and parent_dir.dirName().casefold() == "~mods": + new_paks[row] = ( + pak[0], + pak[1], + pak[2], + parent_dir.absoluteFilePath(str(index).zfill(4)), + ) + index -= 1 + + self.set_paks(new_paks) + return False diff --git a/games/unreal_tabs/manage_paks/view.py b/games/unreal_tabs/manage_paks/view.py new file mode 100644 index 00000000..0e5d6385 --- /dev/null +++ b/games/unreal_tabs/manage_paks/view.py @@ -0,0 +1,37 @@ +from typing import Iterable + +try: + from PyQt6.QtCore import QModelIndex, Qt, pyqtSignal + from PyQt6.QtGui import QDropEvent + from PyQt6.QtWidgets import QAbstractItemView, QTreeView, QWidget +except: + from PyQt5.QtCore import QModelIndex, Qt, pyqtSignal + from PyQt5.QtGui import QDropEvent + from PyQt5.QtWidgets import QAbstractItemView, QTreeView, QWidget + +class PaksView(QTreeView): + data_dropped = pyqtSignal() + + def __init__(self, parent: QWidget | None): + super().__init__(parent) + self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) + self.setDragEnabled(True) + self.setAcceptDrops(True) + self.setDropIndicatorShown(True) + self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) + self.setDefaultDropAction(Qt.DropAction.MoveAction) + if (viewport := self.viewport()) is not None: + viewport.setAcceptDrops(True) + self.setItemsExpandable(False) + self.setRootIsDecorated(False) + + def dropEvent(self, e: QDropEvent | None): + super().dropEvent(e) + self.clearSelection() + self.data_dropped.emit() + + def dataChanged( + self, topLeft: QModelIndex, bottomRight: QModelIndex, roles: Iterable[int] = () + ): + super().dataChanged(topLeft, bottomRight, roles) + self.repaint() diff --git a/games/unreal_tabs/manage_paks/widget.py b/games/unreal_tabs/manage_paks/widget.py new file mode 100644 index 00000000..6c7a5eb3 --- /dev/null +++ b/games/unreal_tabs/manage_paks/widget.py @@ -0,0 +1,207 @@ +from functools import cmp_to_key +from pathlib import Path +from typing import cast + +import mobase + +from ....basic_features.utils import is_directory +from .model import PaksModel +from .view import PaksView + +try: + from PyQt6.QtWidgets import QGridLayout, QWidget + from PyQt6.QtCore import QDir, QFileInfo, Qt +except: + from PyQt5.QtWidgets import QGridLayout, QWidget + from PyQt5.QtCore import QDir, QFileInfo, Qt + +def pak_sort(a: tuple[str, str], b: tuple[str, str]) -> int: + a_pak, a_str = a[0], a[1] or a[0] + b_pak, b_str = b[0], b[1] or b[0] + + a_pak_ends_p = a_pak.casefold().endswith("_p") + b_pak_ends_p = b_pak.casefold().endswith("_p") + + if a_pak_ends_p == b_pak_ends_p: + if a_str.casefold() <= b_str.casefold(): + return 1 + return -1 + elif a_pak_ends_p: + return 1 + elif b_pak_ends_p: + return -1 + return 0 + + +class PaksTabWidget(QWidget): + def __init__(self, parent: QWidget | None, organizer: mobase.IOrganizer): + super().__init__(parent) + self._organizer = organizer + self._view = PaksView(self) + self._layout = QGridLayout(self) + self._layout.addWidget(self._view) + self._model = PaksModel(self._view, organizer) + self._view.setModel(self._model) + self._model.dataChanged.connect(self.write_paks_list) # type: ignore + self._view.data_dropped.connect(self.write_paks_list) # type: ignore + organizer.onProfileChanged(lambda profile_a, profile_b: self._parse_pak_files()) + organizer.modList().onModInstalled(lambda mod: self._parse_pak_files()) + organizer.modList().onModRemoved(lambda mod: self._parse_pak_files()) + organizer.modList().onModStateChanged(lambda mods: self._parse_pak_files()) + self._parse_pak_files() + + def load_paks_list(self) -> list[str]: + profile = QDir(self._organizer.profilePath()) + paks_txt = QFileInfo(profile.absoluteFilePath("paks.txt")) + paks_list: list[str] = [] + if paks_txt.exists(): + with open(paks_txt.absoluteFilePath(), "r") as paks_file: + for line in paks_file: + paks_list.append(line.strip()) + return paks_list + + def write_paks_list(self): + profile = QDir(self._organizer.profilePath()) + paks_txt = QFileInfo(profile.absoluteFilePath("paks.txt")) + with open(paks_txt.absoluteFilePath(), "w") as paks_file: + for _, pak in sorted(self._model.paks.items()): + name, _, _, _ = pak + paks_file.write(f"{name}\n") + self.write_pak_files() + + def write_pak_files(self): + for index, pak in sorted(self._model.paks.items()): + _, _, current_path, target_path = pak + if current_path and current_path != target_path: + path_dir = Path(current_path) + target_dir = Path(target_path) + if not target_dir.exists(): + target_dir.mkdir(parents=True, exist_ok=True) + if path_dir.exists(): + for pak_file in path_dir.glob("*.pak"): + ucas_file = pak_file.with_suffix(".ucas") + utoc_file = pak_file.with_suffix(".utoc") + for file in (pak_file, ucas_file, utoc_file): + if not file.exists(): + continue + try: + file.rename(target_dir.joinpath(file.name)) + except FileExistsError: + pass + data = self._model.paks[index] + self._model.paks[index] = ( + data[0], + data[1], + data[3], + data[3], + ) + break + if not list(path_dir.iterdir()): + path_dir.rmdir() + + def _shake_paks(self, sorted_paks: dict[str, str]) -> list[str]: + shaken_paks: list[str] = [] + shaken_paks_p: list[str] = [] + paks_list = self.load_paks_list() + for pak in paks_list: + if pak in sorted_paks.keys(): + if pak.casefold().endswith("_p"): + shaken_paks_p.append(pak) + else: + shaken_paks.append(pak) + sorted_paks.pop(pak) + for pak in sorted_paks.keys(): + if pak.casefold().endswith("_p"): + shaken_paks_p.append(pak) + else: + shaken_paks.append(pak) + return shaken_paks + shaken_paks_p + + def _parse_pak_files(self): + game = self._organizer.managedGame() + mods = self._organizer.modList().allMods() + paks: dict[str, str] = {} + pak_paths: dict[str, tuple[str, str]] = {} + pak_source: dict[str, str] = {} + for mod in mods: + mod_item = self._organizer.modList().getMod(mod) + if not self._organizer.modList().state(mod) & mobase.ModState.ACTIVE: + continue + filetree = mod_item.fileTree() + pak_mods = filetree.find(game.GameDataPakMods) + if isinstance(pak_mods, mobase.IFileTree): + for entry in pak_mods: + if is_directory(entry): + for sub_entry in entry: + if ( + sub_entry.isFile() + and sub_entry.suffix().casefold() == "pak" + ): + pak_name = sub_entry.name()[ + : -1 - len(sub_entry.suffix()) + ] + paks[pak_name] = entry.name() + pak_paths[pak_name] = ( + mod_item.absolutePath() + + "/" + + cast(mobase.IFileTree, sub_entry.parent()).path( + "/" + ), + mod_item.absolutePath() + "/" + pak_mods.path("/"), + ) + pak_source[pak_name] = mod_item.name() + else: + if entry.suffix().casefold() == "pak": + pak_name = entry.name()[: -1 - len(entry.suffix())] + paks[pak_name] = "" + pak_paths[pak_name] = ( + mod_item.absolutePath() + + "/" + + cast(mobase.IFileTree, entry.parent()).path("/"), + mod_item.absolutePath() + "/" + pak_mods.path("/"), + ) + pak_source[pak_name] = mod_item.name() + pak_mods = QFileInfo(game.paksDirectory().absolutePath()) + if pak_mods.exists() and pak_mods.isDir(): + for entry in QDir(pak_mods.absoluteFilePath()).entryInfoList( + QDir.Filter.Dirs | QDir.Filter.Files | QDir.Filter.NoDotAndDotDot + ): + if entry.isDir(): + for sub_entry in QDir(entry.absoluteFilePath()).entryInfoList( + QDir.Filter.Files + ): + if ( + sub_entry.isFile() + and sub_entry.suffix().casefold() == "pak" + ): + pak_name = sub_entry.completeBaseName() + paks[pak_name] = entry.completeBaseName() + pak_paths[pak_name] = ( + sub_entry.absolutePath(), + pak_mods.absolutePath(), + ) + pak_source[pak_name] = "Game Directory" + else: + if entry.suffix().casefold() == "pak": + pak_name = entry.completeBaseName() + paks[pak_name] = "" + pak_paths[pak_name] = ( + entry.absolutePath(), + pak_mods.absolutePath(), + ) + pak_source[pak_name] = "Game Directory" + sorted_paks = dict(sorted(paks.items(), key=cmp_to_key(pak_sort))) + shaken_paks: list[str] = self._shake_paks(sorted_paks) + final_paks: dict[str, tuple[str, str, str]] = {} + pak_index = 8999 + for pak in shaken_paks: + target_dir = pak_paths[pak][1] + "/" + str(pak_index).zfill(4) + final_paks[pak] = (pak_source[pak], pak_paths[pak][0], target_dir) + pak_index -= 1 + new_data_paks: dict[int, tuple[str, str, str, str]] = {} + i = 0 + for pak, data in final_paks.items(): + source, current_path, target_path = data + new_data_paks[i] = (pak, source, current_path, target_path) + i += 1 + self._model.set_paks(new_data_paks) diff --git a/games/unreal_tabs/manage_ue4ss/__init__.py b/games/unreal_tabs/manage_ue4ss/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/games/unreal_tabs/manage_ue4ss/model.py b/games/unreal_tabs/manage_ue4ss/model.py new file mode 100644 index 00000000..8289b019 --- /dev/null +++ b/games/unreal_tabs/manage_ue4ss/model.py @@ -0,0 +1,121 @@ +import json +from json import JSONDecodeError +from typing import Any, Iterable + +try: + from PyQt6.QtCore import (QDir, QFileInfo, QMimeData, QModelIndex, QStringListModel, Qt) + from PyQt6.QtWidgets import QWidget +except: + from PyQt5.QtCore import (QDir, QFileInfo, QMimeData, QModelIndex, QStringListModel, Qt) + from PyQt5.QtWidgets import QWidget + +import mobase + +from ..constants import DEFAULT_UE4SS_MODS + +class UE4SSListModel(QStringListModel): + def __init__(self, parent: QWidget | None, organizer: mobase.IOrganizer): + super().__init__(parent) + self._checked_items: set[str] = set() + self._organizer = organizer + self._init_mod_states() + + def _init_mod_states(self): + profile = QDir(self._organizer.profilePath()) + mods_json = QFileInfo(profile.absoluteFilePath("mods.json")) + if mods_json.exists(): + with open(mods_json.absoluteFilePath(), "r") as json_file: + try: + mod_data = json.load(json_file) + except JSONDecodeError: + mod_data = DEFAULT_UE4SS_MODS + for mod in mod_data: + if mod["mod_enabled"]: + self._checked_items.add(mod["mod_name"]) + + def _set_mod_states(self): + profile = QDir(self._organizer.profilePath()) + mods_json = QFileInfo(profile.absoluteFilePath("mods.json")) + mod_list: dict[str, bool] = {} + if mods_json.exists(): + with open(mods_json.absoluteFilePath(), "r") as json_file: + try: + mod_data = json.load(json_file) + except JSONDecodeError: + mod_data = DEFAULT_UE4SS_MODS + for mod in mod_data: + mod_list[mod["mod_name"]] = mod["mod_enabled"] + for i in range(self.rowCount()): + item = self.index(i, 0) + name = self.data(item, Qt.ItemDataRole.DisplayRole) + if name in mod_list: + self.setData( + item, + True if mod_list[name] else False, + Qt.ItemDataRole.CheckStateRole, + ) + else: + self.setData(item, True, Qt.ItemDataRole.CheckStateRole) + + def flags(self, index: QModelIndex) -> Qt.ItemFlag: + flags = super().flags(index) + if not index.isValid(): + return ( + Qt.ItemFlag.ItemIsSelectable + | Qt.ItemFlag.ItemIsDragEnabled + | Qt.ItemFlag.ItemIsDropEnabled + | Qt.ItemFlag.ItemIsEnabled + ) + return ( + flags + | Qt.ItemFlag.ItemIsUserCheckable + | Qt.ItemFlag.ItemIsDragEnabled & Qt.ItemFlag.ItemIsEditable + ) + + def setData( + self, index: QModelIndex, value: Any, role: int = Qt.ItemDataRole.EditRole + ) -> bool: + if not index.isValid() or role != Qt.ItemDataRole.CheckStateRole: + return False + + if ( + bool(value) + and self.data(index, Qt.ItemDataRole.DisplayRole) not in self._checked_items + ): + self._checked_items.add(self.data(index, Qt.ItemDataRole.DisplayRole)) + elif ( + not bool(value) + and self.data(index, Qt.ItemDataRole.DisplayRole) in self._checked_items + ): + self._checked_items.remove(self.data(index, Qt.ItemDataRole.DisplayRole)) + self.dataChanged.emit(index, index, [role]) + return True + + def setStringList(self, strings: Iterable[str | None]): + super().setStringList(strings) + self._set_mod_states() + + def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any: + if not index.isValid(): + return None + + if role == Qt.ItemDataRole.CheckStateRole: + return ( + Qt.CheckState.Checked + if self.data(index, Qt.ItemDataRole.DisplayRole) in self._checked_items + else Qt.CheckState.Unchecked + ) + + return super().data(index, role) + + def canDropMimeData( + self, + data: QMimeData | None, + action: Qt.DropAction, + row: int, + column: int, + parent: QModelIndex, + ) -> bool: + if action == Qt.DropAction.MoveAction and (row != -1 or column != -1): + return True + return False diff --git a/games/unreal_tabs/manage_ue4ss/view.py b/games/unreal_tabs/manage_ue4ss/view.py new file mode 100644 index 00000000..218468b0 --- /dev/null +++ b/games/unreal_tabs/manage_ue4ss/view.py @@ -0,0 +1,36 @@ +from typing import Iterable + +try: + from PyQt6.QtCore import QModelIndex, Qt, pyqtSignal + from PyQt6.QtGui import QDropEvent + from PyQt6.QtWidgets import QAbstractItemView, QListView, QWidget +except: + from PyQt5.QtCore import QModelIndex, Qt, pyqtSignal + from PyQt5.QtGui import QDropEvent + from PyQt5.QtWidgets import QAbstractItemView, QListView, QWidget + + +class UE4SSView(QListView): + data_dropped = pyqtSignal() + + def __init__(self, parent: QWidget | None): + super().__init__(parent) + self.setAcceptDrops(True) + self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) + self.setDragEnabled(True) + self.setAcceptDrops(True) + self.setDropIndicatorShown(True) + self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) + self.setDefaultDropAction(Qt.DropAction.MoveAction) + if (viewport := self.viewport()) is not None: + viewport.setAcceptDrops(True) + + def dropEvent(self, e: QDropEvent | None): + super().dropEvent(e) + self.data_dropped.emit() + + def dataChanged( + self, topLeft: QModelIndex, bottomRight: QModelIndex, roles: Iterable[int] = () + ): + super().dataChanged(topLeft, bottomRight, roles) + self.repaint() diff --git a/games/unreal_tabs/manage_ue4ss/widget.py b/games/unreal_tabs/manage_ue4ss/widget.py new file mode 100644 index 00000000..6d20ead6 --- /dev/null +++ b/games/unreal_tabs/manage_ue4ss/widget.py @@ -0,0 +1,168 @@ +import json +from functools import cmp_to_key +from json import JSONDecodeError +from pathlib import Path + +import mobase + +from ..constants import DEFAULT_UE4SS_MODS, UE4SSModInfo +from .model import UE4SSListModel +from .view import UE4SSView + +try: + from PyQt6.QtWidgets import QGridLayout, QWidget + from PyQt6.QtCore import QDir, QFileInfo, Qt +except: + from PyQt5.QtWidgets import QGridLayout, QWidget + from PyQt5.QtCore import QDir, QFileInfo, Qt + +class UE4SSTabWidget(QWidget): + def __init__(self, parent: QWidget | None, organizer: mobase.IOrganizer): + super().__init__(parent) + self._organizer = organizer + self._view = UE4SSView(self) + self._layout = QGridLayout(self) + self._layout.addWidget(self._view) + self._model = UE4SSListModel(self._view, organizer) + self._view.setModel(self._model) + self._model.dataChanged.connect(self.write_mod_list) # type: ignore + self._view.data_dropped.connect(self.write_mod_list) # type: ignore + organizer.onProfileChanged(lambda profile_a, profile_b: self._parse_mod_files()) + organizer.modList().onModInstalled(self.update_mod_files) + organizer.modList().onModRemoved(lambda mod: self._parse_mod_files()) + organizer.modList().onModStateChanged(self.update_mod_files) + self._parse_mod_files() + + def get_mod_list(self) -> list[str]: + mod_list: list[str] = [] + for index in range(self._model.rowCount()): + mod_list.append( + self._model.data( + self._model.index(index, 0), Qt.ItemDataRole.DisplayRole + ) + ) + return mod_list + + def update_mod_files( + self, mods: dict[str, mobase.ModState] | mobase.IModInterface | str + ): + game = self._organizer.managedGame() + mod_list: list[mobase.IModInterface] = [] + if isinstance(mods, dict): + for mod in mods.keys(): + mod_list.append(self._organizer.modList().getMod(mod)) + elif isinstance(mods, mobase.IModInterface): + mod_list.append(mods) + else: + mod_list.append(self._organizer.modList().getMod(mods)) + + for mod in mod_list: + tree = mod.fileTree() + ue4ss_files = tree.find(game.GameDataUE4SSMods) + if isinstance(ue4ss_files, mobase.IFileTree): + for entry in ue4ss_files: + if isinstance(entry, mobase.IFileTree): + if enabled_txt := entry.find("enabled.txt"): + try: + Path(mod.absolutePath(), enabled_txt.path("/")).unlink() + self._organizer.modDataChanged(mod) + except FileNotFoundError: + pass + + self._parse_mod_files() + + def _parse_mod_files(self): + game = self._organizer.managedGame() + mod_list: set[str] = set() + for mod in self._organizer.modList().allMods(): + if ( + mobase.ModState(self._organizer.modList().state(mod)) + & mobase.ModState.ACTIVE + ): + tree = self._organizer.modList().getMod(mod).fileTree() + ue4ss_files = tree.find(game.GameDataUE4SSMods) + if isinstance(ue4ss_files, mobase.IFileTree): + for entry in ue4ss_files: + if isinstance(entry, mobase.IFileTree): + if entry.find("scripts/main.lua") or entry.find("dlls/main.dll"): + mod_list.add(entry.name()) + if enabled_txt := entry.find("enabled.txt"): + try: + Path( + self._organizer.modList() + .getMod(mod) + .absolutePath(), + enabled_txt.path("/"), + ).unlink() + self._organizer.modDataChanged( + self._organizer.modList().getMod(mod) + ) + except FileNotFoundError: + pass + + if game.ue4ssDirectory().exists(): + for dir_info in game.ue4ssDirectory().entryInfoList( + QDir.Filter.Dirs | QDir.Filter.NoDotAndDotDot + ): + if QFileInfo( + QDir(dir_info.absoluteFilePath()).absoluteFilePath( + "scripts/main.lua" + ) + ).exists(): + mod_list.add(dir_info.fileName()) + if QFileInfo( + QDir(dir_info.absoluteFilePath()).absoluteFilePath( + "enabled.txt" + ) + ).exists(): + Path(dir_info.absoluteFilePath(), "enabled.txt").unlink() + + final_list = sorted(mod_list, key=cmp_to_key(self.sort_mods)) + self._model.setStringList(final_list) + + def write_mod_list(self): + mod_list: list[UE4SSModInfo] = [] + profile = QDir(self._organizer.profilePath()) + mods_txt = QFileInfo(profile.absoluteFilePath("mods.txt")) + with open(mods_txt.absoluteFilePath(), "w") as txt_file: + for i in range(self._model.rowCount()): + item = self._model.index(i, 0) + name = self._model.data(item, Qt.ItemDataRole.DisplayRole) + active = ( + self._model.data(item, Qt.ItemDataRole.CheckStateRole) + == Qt.CheckState.Checked + ) + mod_list.append({"mod_name": name, "mod_enabled": active}) + txt_file.write(f"{name} : {1 if active else 0}\n") + mods_json = QFileInfo(profile.absoluteFilePath("mods.json")) + with open(mods_json.absoluteFilePath(), "w") as json_file: + json_file.write(json.dumps(mod_list, indent=4)) + + def sort_mods(self, mod_a: str, mod_b: str) -> int: + profile = QDir(self._organizer.profilePath()) + mods_json = QFileInfo(profile.absoluteFilePath("mods.json")) + mods_list: list[str] = [] + if mods_json.exists() and mods_json.isFile(): + with open(mods_json.absoluteFilePath(), "r") as json_file: + try: + mods = json.load(json_file) + except JSONDecodeError: + mods = DEFAULT_UE4SS_MODS + for mod in mods: + if mod["mod_enabled"]: + mods_list.append(mod["mod_name"]) + index_a = -1 + if mod_a in mods_list: + index_a = mods_list.index(mod_a) + index_b = -1 + if mod_b in mods_list: + index_b = mods_list.index(mod_b) + if index_a != -1 and index_b != -1: + return index_a - index_b + if index_a != -1: + return -1 + if index_b != -1: + return 1 + if mod_a < mod_b: + return -1 + return 1 From 54fbab645bbf8f7b115a091c02b3efc8fd830de6 Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Sat, 28 Feb 2026 23:54:34 +0100 Subject: [PATCH 02/31] Removed unused variables and bare excepts. --- games/game_cassettebeasts.py | 8 ++------ games/game_crimeboss.py | 11 ++++------- games/game_hitman3.py | 8 ++------ games/game_noita.py | 7 ++----- games/game_payday1.py | 11 ++++------- games/game_payday2.py | 11 ++++------- games/game_raid2.py | 11 ++++------- games/game_roadtovostok.py | 16 ++++++---------- games/game_silenthill2remake.py | 10 +++------- 9 files changed, 31 insertions(+), 62 deletions(-) diff --git a/games/game_cassettebeasts.py b/games/game_cassettebeasts.py index 99b5bd52..8b342a3a 100644 --- a/games/game_cassettebeasts.py +++ b/games/game_cassettebeasts.py @@ -9,10 +9,7 @@ from ..basic_game import BasicGame -try: - from PyQt6.QtCore import QDir, QFileInfo -except: - from PyQt5.QtCore import QDir, QFileInfo +from PyQt6.QtCore import QDir, QFileInfo class CassetteBeastsModDataChecker(mobase.ModDataChecker): @@ -55,7 +52,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: class CassetteBeastsGame(BasicGame): appdataenv = os.getenv('APPDATA') - + Name = "Cassette Beasts Support Plugin" Author = "modworkshop" Version = "1" @@ -115,4 +112,3 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): if not os.path.exists(modsPath): os.mkdir(modsPath) super().initializeProfile(directory, settings) - \ No newline at end of file diff --git a/games/game_crimeboss.py b/games/game_crimeboss.py index 648267ed..200119a0 100644 --- a/games/game_crimeboss.py +++ b/games/game_crimeboss.py @@ -14,12 +14,8 @@ from ..basic_game import BasicGame -try: - from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget - from PyQt6.QtCore import QDir, QFileInfo -except: - from PyQt5.QtWidgets import QMainWindow, QTabWidget, QWidget - from PyQt5.QtCore import QDir, QFileInfo +from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget +from PyQt6.QtCore import QDir, QFileInfo class Content(IntEnum): @@ -142,6 +138,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods + "/" GameDataPakMods = self.organizer.managedGame().GameDataPakMods + "/" GameDataNativeMods = self.organizer.managedGame().GameDataNativeMods + "/" + GameDataMovies = self.organizer.managedGame().GameDataMovies + "/" treefixed = 0 if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): treefixed = self.allMoveTo(filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/") @@ -303,4 +300,4 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): os.makedirs(self.ue4ssDirectory().absolutePath()) if not self.nativeDirectory().exists(): os.makedirs(self.nativeDirectory().absolutePath()) - super().initializeProfile(directory, settings) \ No newline at end of file + super().initializeProfile(directory, settings) diff --git a/games/game_hitman3.py b/games/game_hitman3.py index 3c31b210..7068bbb2 100644 --- a/games/game_hitman3.py +++ b/games/game_hitman3.py @@ -11,10 +11,7 @@ from ..basic_game import BasicGame -try: - from PyQt6.QtCore import QDir, QFileInfo -except: - from PyQt5.QtCore import QDir, QFileInfo +from PyQt6.QtCore import QDir, QFileInfo class Hitman3ModDataChecker(mobase.ModDataChecker): @@ -117,7 +114,6 @@ def init(self, organizer: mobase.IOrganizer) -> bool: return True def update_smm_meta(self, mods: dict[str, mobase.ModState]): - GameSMMPath = self._organizer.managedGame().GameSMMPath SMM_Path = os.path.join(self.dataDirectory().absolutePath(), self.GameSMMPath) SMM_Config_Json = SMM_Path + "/config.json" for key, value in mods.items(): @@ -223,4 +219,4 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): modsPath = self.dataDirectory().absolutePath() if not os.path.exists(modsPath): os.mkdir(modsPath) - super().initializeProfile(directory, settings) \ No newline at end of file + super().initializeProfile(directory, settings) diff --git a/games/game_noita.py b/games/game_noita.py index 729d5b39..d58effc5 100644 --- a/games/game_noita.py +++ b/games/game_noita.py @@ -11,10 +11,7 @@ from ..basic_game import BasicGame -try: - from PyQt6.QtCore import QDir, QFileInfo -except: - from PyQt5.QtCore import QDir, QFileInfo +from PyQt6.QtCore import QDir, QFileInfo class NoitaModDataChecker(mobase.ModDataChecker): @@ -151,4 +148,4 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): modsPath = self.dataDirectory().absolutePath() if not os.path.exists(modsPath): os.mkdir(modsPath) - super().initializeProfile(directory, settings) \ No newline at end of file + super().initializeProfile(directory, settings) diff --git a/games/game_payday1.py b/games/game_payday1.py index a7766f85..a3bae8cb 100644 --- a/games/game_payday1.py +++ b/games/game_payday1.py @@ -9,10 +9,7 @@ from ..basic_game import BasicGame -try: - from PyQt6.QtCore import QDir, QFileInfo -except: - from PyQt5.QtCore import QDir, QFileInfo +from PyQt6.QtCore import QDir, QFileInfo class Content(IntEnum): @@ -219,9 +216,9 @@ def init(self, organizer: mobase.IOrganizer) -> bool: def dll_copy( self, mods: dict[str, mobase.ModState] ): - + game_path = self.dataDirectory().absolutePath() + "/" - + for key, value in mods.items(): key = self._organizer.modList().getMod(key) tree = key.fileTree() @@ -274,4 +271,4 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): modsPath = self.dataDirectory().absolutePath() if not os.path.exists(modsPath): os.mkdir(modsPath) - super().initializeProfile(directory, settings) \ No newline at end of file + super().initializeProfile(directory, settings) diff --git a/games/game_payday2.py b/games/game_payday2.py index 24b29b5f..ee532302 100644 --- a/games/game_payday2.py +++ b/games/game_payday2.py @@ -9,10 +9,7 @@ from ..basic_game import BasicGame -try: - from PyQt6.QtCore import QDir, QFileInfo -except: - from PyQt5.QtCore import QDir, QFileInfo +from PyQt6.QtCore import QDir, QFileInfo class Content(IntEnum): @@ -145,7 +142,7 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: treefixed = 0 - + if filetree.exists("mod.txt", mobase.IFileTree.FILE): treefixed = self.allMoveTo(filetree, "mods/FOLDERNAME/") if treefixed == 1: @@ -233,9 +230,9 @@ def executables(self): def dll_copy( self, mods: dict[str, mobase.ModState] ): - + game_path = self.dataDirectory().absolutePath() + "/" - + for key, value in mods.items(): key = self._organizer.modList().getMod(key) tree = key.fileTree() diff --git a/games/game_raid2.py b/games/game_raid2.py index 0d089d74..673aedb8 100644 --- a/games/game_raid2.py +++ b/games/game_raid2.py @@ -9,10 +9,7 @@ from ..basic_game import BasicGame -try: - from PyQt6.QtCore import QDir, QFileInfo -except: - from PyQt5.QtCore import QDir, QFileInfo +from PyQt6.QtCore import QDir, QFileInfo class Content(IntEnum): @@ -153,9 +150,9 @@ def init(self, organizer: mobase.IOrganizer) -> bool: def dll_copy( self, mods: dict[str, mobase.ModState] ): - + game_path = self.dataDirectory().absolutePath() + "/" - + for key, value in mods.items(): key = self._organizer.modList().getMod(key) tree = key.fileTree() @@ -208,4 +205,4 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): modsPath = self.dataDirectory().absolutePath() if not os.path.exists(modsPath): os.mkdir(modsPath) - super().initializeProfile(directory, settings) \ No newline at end of file + super().initializeProfile(directory, settings) diff --git a/games/game_roadtovostok.py b/games/game_roadtovostok.py index 81e5b23e..ccbffb53 100644 --- a/games/game_roadtovostok.py +++ b/games/game_roadtovostok.py @@ -8,11 +8,7 @@ from ..basic_game import BasicGame -try: - from PyQt6.QtCore import QDir, QFileInfo -except: - from PyQt5.QtCore import QDir, QFileInfo - +from PyQt6.QtCore import QDir, QFileInfo class RoadToVostokModDataChecker(mobase.ModDataChecker): def __init__(self, organizer: mobase.IOrganizer): @@ -20,7 +16,7 @@ def __init__(self, organizer: mobase.IOrganizer): self.organizer: mobase.IOrganizer = organizer def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: - + if filetree.exists("mods", mobase.IFileTree.DIRECTORY) and not filetree.exists("mod.txt", mobase.IFileTree.FILE): return mobase.ModDataChecker.VALID for e in filetree: @@ -31,7 +27,7 @@ def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.Ch def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: GameModsPath = self.organizer.managedGame().GameModsPath + "/" treefixed = 0 - + for branch in filetree: mod_name = filetree.name() if mod_name == "": @@ -41,14 +37,14 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: os.makedirs(os.path.join(mod_path, GameModsPath), exist_ok=True) shutil.move(os.path.join(mod_path, branch.name()), os.path.join(mod_path, GameModsPath, branch.name())) treefixed = 1 - + if treefixed == 0: return None return filetree class RoadToVostokGame(BasicGame): - + Name = "Road to Vostok Support Plugin" Author = "modworkshop" Version = "1" @@ -86,4 +82,4 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): modsPath = self.dataDirectory().absolutePath() if not os.path.exists(modsPath): os.mkdir(modsPath) - super().initializeProfile(directory, settings) \ No newline at end of file + super().initializeProfile(directory, settings) diff --git a/games/game_silenthill2remake.py b/games/game_silenthill2remake.py index 022c0e8c..3ed4c6b9 100644 --- a/games/game_silenthill2remake.py +++ b/games/game_silenthill2remake.py @@ -14,12 +14,8 @@ from ..basic_game import BasicGame -try: - from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget - from PyQt6.QtCore import QDir, QFileInfo -except: - from PyQt5.QtWidgets import QMainWindow, QTabWidget, QWidget - from PyQt5.QtCore import QDir, QFileInfo +from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget +from PyQt6.QtCore import QDir, QFileInfo class Content(IntEnum): @@ -282,4 +278,4 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): os.makedirs(self.ue4ssDirectory().absolutePath()) if not self.movieDirectory().exists(): os.makedirs(self.movieDirectory().absolutePath()) - super().initializeProfile(directory, settings) \ No newline at end of file + super().initializeProfile(directory, settings) From 8f09d133c58d39c407099089fc744316e7b182f7 Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Sun, 1 Mar 2026 00:17:47 +0100 Subject: [PATCH 03/31] Cleanup --- games/game_emuvr.py | 7 ++----- games/game_ovkwalkingdead.py | 10 +++------- games/game_pacificdrive.py | 9 +++------ games/game_payday2.py | 2 +- games/game_payday3.py | 10 +++------- games/game_titanfall2.py | 7 ++----- games/game_zuma_deluxe.py | 7 ++----- games/unreal_tabs/manage_paks/model.py | 20 +++++++++++--------- games/unreal_tabs/manage_paks/view.py | 12 ++++-------- games/unreal_tabs/manage_paks/widget.py | 9 +++------ games/unreal_tabs/manage_ue4ss/model.py | 9 +++------ games/unreal_tabs/manage_ue4ss/view.py | 11 +++-------- games/unreal_tabs/manage_ue4ss/widget.py | 8 ++------ 13 files changed, 42 insertions(+), 79 deletions(-) diff --git a/games/game_emuvr.py b/games/game_emuvr.py index e1418ba4..2a0a85d1 100644 --- a/games/game_emuvr.py +++ b/games/game_emuvr.py @@ -9,10 +9,7 @@ from ..basic_game import BasicGame -try: - from PyQt6.QtCore import QDir, QFileInfo -except: - from PyQt5.QtCore import QDir, QFileInfo +from PyQt6.QtCore import QDir, QFileInfo class EmuVRModDataChecker(mobase.ModDataChecker): @@ -112,4 +109,4 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): modsPath = self.dataDirectory().absolutePath() if not os.path.exists(modsPath): os.mkdir(modsPath) - super().initializeProfile(directory, settings) \ No newline at end of file + super().initializeProfile(directory, settings) diff --git a/games/game_ovkwalkingdead.py b/games/game_ovkwalkingdead.py index e3916d89..127f172d 100644 --- a/games/game_ovkwalkingdead.py +++ b/games/game_ovkwalkingdead.py @@ -14,12 +14,8 @@ from ..basic_game import BasicGame -try: - from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget - from PyQt6.QtCore import QDir, QFileInfo -except: - from PyQt5.QtWidgets import QMainWindow, QTabWidget, QWidget - from PyQt5.QtCore import QDir, QFileInfo +from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget +from PyQt6.QtCore import QDir, QFileInfo class Content(IntEnum): @@ -281,4 +277,4 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): os.makedirs(self.ue4ssDirectory().absolutePath()) if not self.movieDirectory().exists(): os.makedirs(self.movieDirectory().absolutePath()) - super().initializeProfile(directory, settings) \ No newline at end of file + super().initializeProfile(directory, settings) diff --git a/games/game_pacificdrive.py b/games/game_pacificdrive.py index 0c2f99e8..45b2f1bf 100644 --- a/games/game_pacificdrive.py +++ b/games/game_pacificdrive.py @@ -14,12 +14,9 @@ from ..basic_game import BasicGame -try: - from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget - from PyQt6.QtCore import QDir, QFileInfo -except: - from PyQt5.QtWidgets import QMainWindow, QTabWidget, QWidget - from PyQt5.QtCore import QDir, QFileInfo +from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget +from PyQt6.QtCore import QDir, QFileInfo + class Content(IntEnum): diff --git a/games/game_payday2.py b/games/game_payday2.py index ee532302..3da17b04 100644 --- a/games/game_payday2.py +++ b/games/game_payday2.py @@ -289,4 +289,4 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): os.makedirs(self.modsDirectory().absolutePath()) if not self.overridesDirectory().exists(): os.makedirs(self.overridesDirectory().absolutePath()) - super().initializeProfile(directory, settings) \ No newline at end of file + super().initializeProfile(directory, settings) diff --git a/games/game_payday3.py b/games/game_payday3.py index c0060bd6..9e762400 100644 --- a/games/game_payday3.py +++ b/games/game_payday3.py @@ -14,12 +14,8 @@ from ..basic_game import BasicGame -try: - from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget - from PyQt6.QtCore import QDir, QFileInfo -except: - from PyQt5.QtWidgets import QMainWindow, QTabWidget, QWidget - from PyQt5.QtCore import QDir, QFileInfo +from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget +from PyQt6.QtCore import QDir, QFileInfo class Content(IntEnum): @@ -281,4 +277,4 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): os.makedirs(self.ue4ssDirectory().absolutePath()) if not self.movieDirectory().exists(): os.makedirs(self.movieDirectory().absolutePath()) - super().initializeProfile(directory, settings) \ No newline at end of file + super().initializeProfile(directory, settings) diff --git a/games/game_titanfall2.py b/games/game_titanfall2.py index a5708d24..8ef69875 100644 --- a/games/game_titanfall2.py +++ b/games/game_titanfall2.py @@ -11,10 +11,7 @@ from ..basic_game import BasicGame -try: - from PyQt6.QtCore import QDir, QFileInfo -except: - from PyQt5.QtCore import QDir, QFileInfo +from PyQt6.QtCore import QDir, QFileInfo class Content(IntEnum): @@ -286,4 +283,4 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): def mappings(self) -> list[mobase.Mapping]: return [ mobase.Mapping(self._organizer.profilePath() + "/" + self.NorthstarModJson, self.gameDirectory().absolutePath() + "/R2Northstar/" + self.NorthstarModJson, False, False), - ] \ No newline at end of file + ] diff --git a/games/game_zuma_deluxe.py b/games/game_zuma_deluxe.py index 6511fc4d..b8c2d1c4 100644 --- a/games/game_zuma_deluxe.py +++ b/games/game_zuma_deluxe.py @@ -11,10 +11,7 @@ from ..basic_game import BasicGame from ..basic_features import BasicGameSaveGameInfo -try: - from PyQt6.QtCore import QDir, QFileInfo -except: - from PyQt5.QtCore import QDir, QFileInfo +from PyQt6.QtCore import QDir, QFileInfo class Content(IntEnum): @@ -326,4 +323,4 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): def mappings(self) -> list[mobase.Mapping]: return [ mobase.Mapping(self._organizer.profilePath() + "/" + self.ProfileLevelsXml, self.gameDirectory().absolutePath() + "/" + self.GameLevelsXml, False, False), - ] \ No newline at end of file + ] diff --git a/games/unreal_tabs/manage_paks/model.py b/games/unreal_tabs/manage_paks/model.py index 9e7216f7..a21ec9e1 100644 --- a/games/unreal_tabs/manage_paks/model.py +++ b/games/unreal_tabs/manage_paks/model.py @@ -5,12 +5,8 @@ import mobase -try: - from PyQt6.QtCore import (QAbstractItemModel, QByteArray, QDataStream, QDir, QFileInfo, QMimeData, QModelIndex, QObject, Qt, QVariant) - from PyQt6.QtWidgets import QWidget -except: - from PyQt5.QtCore import (QAbstractItemModel, QByteArray, QDataStream, QDir, QFileInfo, QMimeData, QModelIndex, QObject, Qt, QVariant) - from PyQt5.QtWidgets import QWidget +from PyQt6.QtCore import (QAbstractItemModel, QByteArray, QDataStream, QDir, QFileInfo, QMimeData, QModelIndex, QObject, Qt, QVariant) +from PyQt6.QtWidgets import QWidget _PakInfo: TypeAlias = tuple[str, str, str, str] @@ -61,12 +57,16 @@ def flags(self, index: QModelIndex) -> Qt.ItemFlag: | Qt.ItemFlag.ItemIsDropEnabled & Qt.ItemFlag.ItemIsEditable ) - def columnCount(self, parent: QModelIndex = QModelIndex()) -> int: + def columnCount(self, parent: QModelIndex) -> int: + if parent is None: + parent = QModelIndex() return len(PaksColumns) def index( - self, row: int, column: int, parent: QModelIndex = QModelIndex() + self, row: int, column: int, parent: QModelIndex ) -> QModelIndex: + if parent is None: + parent = QModelIndex() if ( row < 0 or row >= self.rowCount() @@ -86,7 +86,9 @@ def parent(self, child: QModelIndex | None = None) -> QModelIndex | QObject | No return super().parent() return QModelIndex() - def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: + def rowCount(self, parent: QModelIndex) -> int: + if parent is None: + parent = QModelIndex() return len(self.paks) def setData( diff --git a/games/unreal_tabs/manage_paks/view.py b/games/unreal_tabs/manage_paks/view.py index 0e5d6385..a56b0cdd 100644 --- a/games/unreal_tabs/manage_paks/view.py +++ b/games/unreal_tabs/manage_paks/view.py @@ -1,13 +1,9 @@ from typing import Iterable -try: - from PyQt6.QtCore import QModelIndex, Qt, pyqtSignal - from PyQt6.QtGui import QDropEvent - from PyQt6.QtWidgets import QAbstractItemView, QTreeView, QWidget -except: - from PyQt5.QtCore import QModelIndex, Qt, pyqtSignal - from PyQt5.QtGui import QDropEvent - from PyQt5.QtWidgets import QAbstractItemView, QTreeView, QWidget +from PyQt6.QtCore import QModelIndex, Qt, pyqtSignal +from PyQt6.QtGui import QDropEvent +from PyQt6.QtWidgets import QAbstractItemView, QTreeView, QWidget + class PaksView(QTreeView): data_dropped = pyqtSignal() diff --git a/games/unreal_tabs/manage_paks/widget.py b/games/unreal_tabs/manage_paks/widget.py index 6c7a5eb3..b84f99bb 100644 --- a/games/unreal_tabs/manage_paks/widget.py +++ b/games/unreal_tabs/manage_paks/widget.py @@ -8,12 +8,9 @@ from .model import PaksModel from .view import PaksView -try: - from PyQt6.QtWidgets import QGridLayout, QWidget - from PyQt6.QtCore import QDir, QFileInfo, Qt -except: - from PyQt5.QtWidgets import QGridLayout, QWidget - from PyQt5.QtCore import QDir, QFileInfo, Qt + +from PyQt6.QtWidgets import QGridLayout, QWidget +from PyQt6.QtCore import QDir, QFileInfo, Qt def pak_sort(a: tuple[str, str], b: tuple[str, str]) -> int: a_pak, a_str = a[0], a[1] or a[0] diff --git a/games/unreal_tabs/manage_ue4ss/model.py b/games/unreal_tabs/manage_ue4ss/model.py index 8289b019..82e5f2e4 100644 --- a/games/unreal_tabs/manage_ue4ss/model.py +++ b/games/unreal_tabs/manage_ue4ss/model.py @@ -2,12 +2,9 @@ from json import JSONDecodeError from typing import Any, Iterable -try: - from PyQt6.QtCore import (QDir, QFileInfo, QMimeData, QModelIndex, QStringListModel, Qt) - from PyQt6.QtWidgets import QWidget -except: - from PyQt5.QtCore import (QDir, QFileInfo, QMimeData, QModelIndex, QStringListModel, Qt) - from PyQt5.QtWidgets import QWidget +from PyQt6.QtCore import (QDir, QFileInfo, QMimeData, QModelIndex, QStringListModel, Qt) +from PyQt6.QtWidgets import QWidget + import mobase diff --git a/games/unreal_tabs/manage_ue4ss/view.py b/games/unreal_tabs/manage_ue4ss/view.py index 218468b0..bb994ca6 100644 --- a/games/unreal_tabs/manage_ue4ss/view.py +++ b/games/unreal_tabs/manage_ue4ss/view.py @@ -1,13 +1,8 @@ from typing import Iterable -try: - from PyQt6.QtCore import QModelIndex, Qt, pyqtSignal - from PyQt6.QtGui import QDropEvent - from PyQt6.QtWidgets import QAbstractItemView, QListView, QWidget -except: - from PyQt5.QtCore import QModelIndex, Qt, pyqtSignal - from PyQt5.QtGui import QDropEvent - from PyQt5.QtWidgets import QAbstractItemView, QListView, QWidget +from PyQt6.QtCore import QModelIndex, Qt, pyqtSignal +from PyQt6.QtGui import QDropEvent +from PyQt6.QtWidgets import QAbstractItemView, QListView, QWidget class UE4SSView(QListView): diff --git a/games/unreal_tabs/manage_ue4ss/widget.py b/games/unreal_tabs/manage_ue4ss/widget.py index 6d20ead6..d3732161 100644 --- a/games/unreal_tabs/manage_ue4ss/widget.py +++ b/games/unreal_tabs/manage_ue4ss/widget.py @@ -9,12 +9,8 @@ from .model import UE4SSListModel from .view import UE4SSView -try: - from PyQt6.QtWidgets import QGridLayout, QWidget - from PyQt6.QtCore import QDir, QFileInfo, Qt -except: - from PyQt5.QtWidgets import QGridLayout, QWidget - from PyQt5.QtCore import QDir, QFileInfo, Qt +from PyQt6.QtWidgets import QGridLayout, QWidget +from PyQt6.QtCore import QDir, QFileInfo, Qt class UE4SSTabWidget(QWidget): def __init__(self, parent: QWidget | None, organizer: mobase.IOrganizer): From 870fbc6533259d24175bf1d0ba4d297414ce686b Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Sun, 1 Mar 2026 00:37:14 +0100 Subject: [PATCH 04/31] More Cleanup --- games/game_cassettebeasts.py | 8 +++----- games/game_crimeboss.py | 1 - games/game_emuvr.py | 2 -- games/game_hitman3.py | 3 --- games/game_noita.py | 2 -- games/game_pacificdrive.py | 2 +- games/game_payday1.py | 1 - games/game_payday3.py | 1 - games/game_raid2.py | 1 - games/game_roadtovostok.py | 4 +--- games/game_silenthill2remake.py | 1 - games/game_titanfall2.py | 4 +--- games/game_zuma_deluxe.py | 1 - 13 files changed, 6 insertions(+), 25 deletions(-) diff --git a/games/game_cassettebeasts.py b/games/game_cassettebeasts.py index 8b342a3a..41401f56 100644 --- a/games/game_cassettebeasts.py +++ b/games/game_cassettebeasts.py @@ -2,9 +2,7 @@ import shutil import mobase -from enum import IntEnum, auto from pathlib import Path -from typing import Any, List, Set, cast from functools import cached_property from ..basic_game import BasicGame @@ -51,7 +49,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: class CassetteBeastsGame(BasicGame): - appdataenv = os.getenv('APPDATA') + appdataenv = os.getenv("APPDATA") Name = "Cassette Beasts Support Plugin" Author = "modworkshop" @@ -60,8 +58,8 @@ class CassetteBeastsGame(BasicGame): GameShortName = "cassette-beasts" GameSteamId = 1321440 GameBinary = "CassetteBeasts.exe" - GameDataPath = appdataenv + '/CassetteBeasts/mods' - GameDocumentsDirectory = appdataenv + '/CassetteBeasts' + GameDataPath = appdataenv + "/CassetteBeasts/mods" + GameDocumentsDirectory = appdataenv + "/CassetteBeasts" GameSaveExtension = "gcpf" def init(self, organizer: mobase.IOrganizer) -> bool: diff --git a/games/game_crimeboss.py b/games/game_crimeboss.py index 200119a0..bf88013a 100644 --- a/games/game_crimeboss.py +++ b/games/game_crimeboss.py @@ -5,7 +5,6 @@ from enum import IntEnum, auto from pathlib import Path -from typing import Any, List, Set, cast from functools import cached_property from .unreal_tabs.constants import DEFAULT_UE4SS_MODS, UE4SSModInfo diff --git a/games/game_emuvr.py b/games/game_emuvr.py index 2a0a85d1..1b4737b2 100644 --- a/games/game_emuvr.py +++ b/games/game_emuvr.py @@ -2,9 +2,7 @@ import shutil import mobase -from enum import IntEnum, auto from pathlib import Path -from typing import Any, List, Set, cast from functools import cached_property from ..basic_game import BasicGame diff --git a/games/game_hitman3.py b/games/game_hitman3.py index 7068bbb2..3b3ad92c 100644 --- a/games/game_hitman3.py +++ b/games/game_hitman3.py @@ -3,10 +3,7 @@ import json import mobase -from json import JSONDecodeError -from enum import IntEnum, auto from pathlib import Path -from typing import Any, List, Set, cast from functools import cached_property from ..basic_game import BasicGame diff --git a/games/game_noita.py b/games/game_noita.py index d58effc5..5a3dfeef 100644 --- a/games/game_noita.py +++ b/games/game_noita.py @@ -4,9 +4,7 @@ import mobase from json import JSONDecodeError -from enum import IntEnum, auto from pathlib import Path -from typing import Any, List, Set, cast from functools import cached_property from ..basic_game import BasicGame diff --git a/games/game_pacificdrive.py b/games/game_pacificdrive.py index 45b2f1bf..38204cb1 100644 --- a/games/game_pacificdrive.py +++ b/games/game_pacificdrive.py @@ -279,4 +279,4 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): os.makedirs(self.ue4ssDirectory().absolutePath()) if not self.movieDirectory().exists(): os.makedirs(self.movieDirectory().absolutePath()) - super().initializeProfile(directory, settings) \ No newline at end of file + super().initializeProfile(directory, settings) diff --git a/games/game_payday1.py b/games/game_payday1.py index a3bae8cb..67928066 100644 --- a/games/game_payday1.py +++ b/games/game_payday1.py @@ -4,7 +4,6 @@ from enum import IntEnum, auto from pathlib import Path -from typing import Any, List, Set, cast from functools import cached_property from ..basic_game import BasicGame diff --git a/games/game_payday3.py b/games/game_payday3.py index 9e762400..9135403c 100644 --- a/games/game_payday3.py +++ b/games/game_payday3.py @@ -5,7 +5,6 @@ from enum import IntEnum, auto from pathlib import Path -from typing import Any, List, Set, cast from functools import cached_property from .unreal_tabs.constants import DEFAULT_UE4SS_MODS, UE4SSModInfo diff --git a/games/game_raid2.py b/games/game_raid2.py index 673aedb8..105a0bdf 100644 --- a/games/game_raid2.py +++ b/games/game_raid2.py @@ -4,7 +4,6 @@ from enum import IntEnum, auto from pathlib import Path -from typing import Any, List, Set, cast from functools import cached_property from ..basic_game import BasicGame diff --git a/games/game_roadtovostok.py b/games/game_roadtovostok.py index ccbffb53..063128d8 100644 --- a/games/game_roadtovostok.py +++ b/games/game_roadtovostok.py @@ -2,9 +2,7 @@ import shutil import mobase -from enum import IntEnum, auto from pathlib import Path -from typing import Any, List, Set, cast from ..basic_game import BasicGame @@ -54,7 +52,7 @@ class RoadToVostokGame(BasicGame): GameBinary = "Road_to_Vostok_Demo.exe" GameDataPath = "%GAME_PATH%" GameModsPath = "mods" - GameDocumentsDirectory = '%APPDATA%/Godot/app_userdata/Road to Vostok' + GameDocumentsDirectory = "%APPDATA%/Godot/app_userdata/Road to Vostok" GameSaveExtension = "tres" def init(self, organizer: mobase.IOrganizer) -> bool: diff --git a/games/game_silenthill2remake.py b/games/game_silenthill2remake.py index 3ed4c6b9..9d7964b1 100644 --- a/games/game_silenthill2remake.py +++ b/games/game_silenthill2remake.py @@ -5,7 +5,6 @@ from enum import IntEnum, auto from pathlib import Path -from typing import Any, List, Set, cast from functools import cached_property from .unreal_tabs.constants import DEFAULT_UE4SS_MODS, UE4SSModInfo diff --git a/games/game_titanfall2.py b/games/game_titanfall2.py index 8ef69875..abf8afc8 100644 --- a/games/game_titanfall2.py +++ b/games/game_titanfall2.py @@ -3,10 +3,8 @@ import json import mobase -from json import JSONDecodeError from enum import IntEnum, auto from pathlib import Path -from typing import Any, List, Set, cast from functools import cached_property from ..basic_game import BasicGame @@ -218,7 +216,7 @@ def update_enable_mods_json(self, mods: dict[str, mobase.ModState]): if value == 35 and modname not in Northstar: Northstar[modname] = {modversion: True} if value == 33 and modname in Northstar: - removed_value = Northstar.pop(modname) + Northstar = Northstar.pop(modname) with open(Northstar_Config_Json, "w", encoding="utf-8") as f: json.dump(Northstar, f, ensure_ascii=False, indent=4) diff --git a/games/game_zuma_deluxe.py b/games/game_zuma_deluxe.py index b8c2d1c4..9d7da777 100644 --- a/games/game_zuma_deluxe.py +++ b/games/game_zuma_deluxe.py @@ -5,7 +5,6 @@ from enum import IntEnum, auto from pathlib import Path -from typing import Any, List, Set, cast from functools import cached_property from ..basic_game import BasicGame From ea390e467196d706d9b42e0b0b3b8fbea4061305 Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Sun, 1 Mar 2026 01:08:20 +0100 Subject: [PATCH 05/31] Additional Cleanup --- games/game_noita.py | 31 ++++-- games/game_ovkwalkingdead.py | 17 ++-- games/game_pacificdrive.py | 18 ++-- games/game_payday1.py | 12 +-- games/game_payday2.py | 13 +-- games/game_payday3.py | 63 +++++++++--- games/game_roadtovostok.py | 6 +- games/game_silenthill2remake.py | 17 ++-- games/game_titanfall2.py | 91 ++++++++++++----- games/game_zuma_deluxe.py | 124 +++++++++++++++++------ games/unreal_tabs/manage_paks/model.py | 2 +- games/unreal_tabs/manage_paks/widget.py | 6 +- games/unreal_tabs/manage_ue4ss/model.py | 4 +- games/unreal_tabs/manage_ue4ss/widget.py | 14 +-- 14 files changed, 277 insertions(+), 141 deletions(-) diff --git a/games/game_noita.py b/games/game_noita.py index 5a3dfeef..38db9331 100644 --- a/games/game_noita.py +++ b/games/game_noita.py @@ -1,16 +1,13 @@ +from functools import cached_property +from pathlib import Path import os import shutil -import json -import mobase -from json import JSONDecodeError -from pathlib import Path -from functools import cached_property +import mobase +from PyQt6.QtCore import QDir, QFileInfo from ..basic_game import BasicGame -from PyQt6.QtCore import QDir, QFileInfo - class NoitaModDataChecker(mobase.ModDataChecker): def __init__(self, organizer: mobase.IOrganizer): @@ -39,7 +36,9 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): filetree: mobase.IFileTree = mod.fileTree() fixed = False modname = mod.name() - if filetree is not None and filetree.exists(GameModsPath + "/FOLDERNAME", mobase.IFileTree.DIRECTORY): + if filetree is not None and filetree.exists( + GameModsPath + "/FOLDERNAME", mobase.IFileTree.DIRECTORY + ): path = mod.absolutePath() old_path = os.path.join(path, GameModsPath + "/FOLDERNAME") new_path = os.path.join(path, GameModsPath + f"/{modname}") @@ -49,7 +48,9 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): return self.needsNameFix = False - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: if filetree.exists("mods", mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID return mobase.ModDataChecker.FIXABLE @@ -131,7 +132,9 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: except AttributeError: efls = [] libs: set[str] = set() - tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = ( + self._organizer.virtualFileTree() + ) if type(tree) is not mobase.IFileTree: return efls for e in tree: @@ -139,7 +142,13 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: libs.add(relpath) exes = self.executables() - efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + efls = efls + [ + mobase.ExecutableForcedLoadSetting( + exe.binary().fileName(), lib + ).withEnabled(True) + for lib in libs + for exe in exes + ] return efls def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): diff --git a/games/game_ovkwalkingdead.py b/games/game_ovkwalkingdead.py index 127f172d..8437e4c5 100644 --- a/games/game_ovkwalkingdead.py +++ b/games/game_ovkwalkingdead.py @@ -1,22 +1,19 @@ +from enum import IntEnum, auto +from functools import cached_property +from pathlib import Path import json import os import shutil -import mobase -from enum import IntEnum, auto -from pathlib import Path -from typing import Any, List, Set, cast -from functools import cached_property +import mobase +from PyQt6.QtCore import QDir, QFileInfo +from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget +from ..basic_game import BasicGame from .unreal_tabs.constants import DEFAULT_UE4SS_MODS, UE4SSModInfo from .unreal_tabs.manage_paks.widget import PaksTabWidget from .unreal_tabs.manage_ue4ss.widget import UE4SSTabWidget -from ..basic_game import BasicGame - -from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget -from PyQt6.QtCore import QDir, QFileInfo - class Content(IntEnum): UCAS = auto() diff --git a/games/game_pacificdrive.py b/games/game_pacificdrive.py index 38204cb1..410fc17d 100644 --- a/games/game_pacificdrive.py +++ b/games/game_pacificdrive.py @@ -1,23 +1,19 @@ +from enum import IntEnum, auto +from functools import cached_property +from pathlib import Path import json import os import shutil -import mobase -from enum import IntEnum, auto -from pathlib import Path -from typing import Any, List, Set, cast -from functools import cached_property +import mobase +from PyQt6.QtCore import QDir, QFileInfo +from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget +from ..basic_game import BasicGame from .unreal_tabs.constants import DEFAULT_UE4SS_MODS, UE4SSModInfo from .unreal_tabs.manage_paks.widget import PaksTabWidget from .unreal_tabs.manage_ue4ss.widget import UE4SSTabWidget -from ..basic_game import BasicGame - -from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget -from PyQt6.QtCore import QDir, QFileInfo - - class Content(IntEnum): UCAS = auto() diff --git a/games/game_payday1.py b/games/game_payday1.py index 67928066..7fee5286 100644 --- a/games/game_payday1.py +++ b/games/game_payday1.py @@ -1,15 +1,13 @@ -import os -import shutil -import mobase - from enum import IntEnum, auto -from pathlib import Path from functools import cached_property +from pathlib import Path +import os +import shutil -from ..basic_game import BasicGame - +import mobase from PyQt6.QtCore import QDir, QFileInfo +from ..basic_game import BasicGame class Content(IntEnum): TEXTURE = auto() diff --git a/games/game_payday2.py b/games/game_payday2.py index 3da17b04..5998bb69 100644 --- a/games/game_payday2.py +++ b/games/game_payday2.py @@ -1,16 +1,13 @@ -import os -import shutil -import mobase - from enum import IntEnum, auto -from pathlib import Path -from typing import Any, List, Set, cast from functools import cached_property +from pathlib import Path +import os +import shutil -from ..basic_game import BasicGame - +import mobase from PyQt6.QtCore import QDir, QFileInfo +from ..basic_game import BasicGame class Content(IntEnum): TEXTURE = auto() diff --git a/games/game_payday3.py b/games/game_payday3.py index 9135403c..7410d5c9 100644 --- a/games/game_payday3.py +++ b/games/game_payday3.py @@ -38,7 +38,10 @@ class Payday3ModDataContent(mobase.ModDataContent): ] def getAllContents(self) -> list[mobase.ModDataContent.Content]: - return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + return [ + mobase.ModDataContent.Content(id, name, icon, *filter_only) + for id, name, icon, *filter_only in self.GAMECONTENTS + ] def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): @@ -83,7 +86,9 @@ def move_overwrite_merge(self, source, destination): self.move_overwrite_merge(s_item, d_item) os.rmdir(source) - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods GameDataPakMods = self.organizer.managedGame().GameDataPakMods GameDataMovies = self.organizer.managedGame().GameDataMovieMods @@ -120,10 +125,14 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: GameDataMovies = self.organizer.managedGame().GameDataMovieMods + "/" treefixed = 0 if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): - treefixed = self.allMoveTo(filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/") + treefixed = self.allMoveTo( + filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/" + ) if treefixed == 1: return filetree - if filetree.exists("Scripts", mobase.IFileTree.DIRECTORY) or filetree.exists("dlls", mobase.IFileTree.DIRECTORY): + if filetree.exists("Scripts", mobase.IFileTree.DIRECTORY) or filetree.exists( + "dlls", mobase.IFileTree.DIRECTORY + ): treefixed = self.allMoveTo(filetree, GameDataUE4SSMods) if treefixed == 1: return filetree @@ -139,14 +148,32 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: if mod_name == "": mod_name = e.name() mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if filetree.createOrphanTree("OrphanTree") is None and os.path.exists(mod_path): + if filetree.createOrphanTree( + "OrphanTree" + ) is None and os.path.exists(mod_path): match e.suffix().casefold(): case "pak" | "utoc" | "ucas": - os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) + os.makedirs( + os.path.join(mod_path, GameDataPakMods), + exist_ok=True, + ) + shutil.move( + os.path.join(mod_path, e.name()), + os.path.join( + mod_path, GameDataPakMods, e.name() + ), + ) case "bk2": - os.makedirs(os.path.join(mod_path, GameDataMovies), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovies, e.name())) + os.makedirs( + os.path.join(mod_path, GameDataMovies), + exist_ok=True, + ) + shutil.move( + os.path.join(mod_path, e.name()), + os.path.join( + mod_path, GameDataMovies, e.name() + ), + ) case _: pass treefixed = 1 @@ -158,7 +185,11 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: case "pak" | "utoc" | "ucas": filetree.move(e, GameDataPakMods, mobase.IFileTree.MERGE) case "dll": - filetree.move(e, os.path.dirname(GameDataUE4SSMods) + "/", mobase.IFileTree.MERGE) + filetree.move( + e, + os.path.dirname(GameDataUE4SSMods) + "/", + mobase.IFileTree.MERGE, + ) case "bk2": filetree.move(e, GameDataMovies, mobase.IFileTree.MERGE) case _: @@ -231,7 +262,9 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: except AttributeError: efls = [] libs: set[str] = set() - tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = ( + self._organizer.virtualFileTree() + ) if type(tree) is not mobase.IFileTree: return efls for e in tree: @@ -239,7 +272,13 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: libs.add(relpath) exes = self.executables() - efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + efls = efls + [ + mobase.ExecutableForcedLoadSetting( + exe.binary().fileName(), lib + ).withEnabled(True) + for lib in libs + for exe in exes + ] return efls def paksDirectory(self) -> QDir: diff --git a/games/game_roadtovostok.py b/games/game_roadtovostok.py index 063128d8..871e84a7 100644 --- a/games/game_roadtovostok.py +++ b/games/game_roadtovostok.py @@ -1,13 +1,11 @@ import os import shutil -import mobase -from pathlib import Path +import mobase +from PyQt6.QtCore import QDir, QFileInfo from ..basic_game import BasicGame -from PyQt6.QtCore import QDir, QFileInfo - class RoadToVostokModDataChecker(mobase.ModDataChecker): def __init__(self, organizer: mobase.IOrganizer): super().__init__() diff --git a/games/game_silenthill2remake.py b/games/game_silenthill2remake.py index 9d7964b1..fda5ee34 100644 --- a/games/game_silenthill2remake.py +++ b/games/game_silenthill2remake.py @@ -1,22 +1,19 @@ +from enum import IntEnum, auto +from functools import cached_property +from pathlib import Path import json import os import shutil -import mobase -from enum import IntEnum, auto -from pathlib import Path -from functools import cached_property +import mobase +from PyQt6.QtCore import QDir, QFileInfo +from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget +from ..basic_game import BasicGame from .unreal_tabs.constants import DEFAULT_UE4SS_MODS, UE4SSModInfo from .unreal_tabs.manage_paks.widget import PaksTabWidget from .unreal_tabs.manage_ue4ss.widget import UE4SSTabWidget -from ..basic_game import BasicGame - -from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget -from PyQt6.QtCore import QDir, QFileInfo - - class Content(IntEnum): UCAS = auto() UTOC = auto() diff --git a/games/game_titanfall2.py b/games/game_titanfall2.py index abf8afc8..f7cc5f87 100644 --- a/games/game_titanfall2.py +++ b/games/game_titanfall2.py @@ -1,16 +1,14 @@ +from enum import IntEnum, auto +from functools import cached_property +from pathlib import Path import os import shutil -import json -import mobase -from enum import IntEnum, auto -from pathlib import Path -from functools import cached_property +import mobase +from PyQt6.QtCore import QDir, QFileInfo from ..basic_game import BasicGame -from PyQt6.QtCore import QDir, QFileInfo - class Content(IntEnum): MATERIAL = auto() @@ -36,7 +34,10 @@ class Titanfall2ModDataContent(mobase.ModDataContent): ] def getAllContents(self) -> list[mobase.ModDataContent.Content]: - return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + return [ + mobase.ModDataContent.Content(id, name, icon, *filter_only) + for id, name, icon, *filter_only in self.GAMECONTENTS + ] contents = set() @@ -95,7 +96,9 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): filetree: mobase.IFileTree = mod.fileTree() fixed = False modname = mod.name() - if filetree is not None and filetree.exists(northstarModPath + "FOLDERNAME", mobase.IFileTree.DIRECTORY): + if filetree is not None and filetree.exists( + northstarModPath + "FOLDERNAME", mobase.IFileTree.DIRECTORY + ): path = mod.absolutePath() json_path = os.path.join(path, northstarModPath + "FOLDERNAME/mod.json") mod_data = json.load(open(json_path, encoding="utf-8")) @@ -104,7 +107,9 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): new_path = os.path.join(path, northstarModPath + f"{modname}") self.move_overwrite_merge(old_path, new_path) fixed = True - elif filetree is not None and filetree.exists(northstarModPath + "FOLDERNAME_NAME", mobase.IFileTree.DIRECTORY): + elif filetree is not None and filetree.exists( + northstarModPath + "FOLDERNAME_NAME", mobase.IFileTree.DIRECTORY + ): path = mod.absolutePath() old_path = os.path.join(path, northstarModPath + "FOLDERNAME_NAME") new_path = os.path.join(path, northstarModPath + f"{modname}") @@ -114,7 +119,9 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): return self.needsNameFix = False - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: if filetree.exists("R2Northstar", mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID return mobase.ModDataChecker.FIXABLE @@ -151,7 +158,9 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: else: try: if filetree[0][0].exists("mod.json", mobase.IFileTree.FILE): - filetree.move(filetree[0][0], filetree[0].path("/"), mobase.IFileTree.REPLACE) + filetree.move( + filetree[0][0], filetree[0].path("/"), mobase.IFileTree.REPLACE + ) filetree.move(filetree[0], northstarModPath, mobase.IFileTree.MERGE) treefixed = 1 except TypeError: @@ -163,7 +172,11 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: else: for e in filetree: if e is not None and e.path("/").count("/") == 0: - filetree.move(e, northstarModPath + "FOLDERNAME_NAME/", mobase.IFileTree.MERGE) + filetree.move( + e, + northstarModPath + "FOLDERNAME_NAME/", + mobase.IFileTree.MERGE, + ) treefixed = 1 self.needsNameFix = True if treefixed == 0: @@ -194,7 +207,9 @@ def init(self, organizer: mobase.IOrganizer) -> bool: return True def update_enable_mods_json(self, mods: dict[str, mobase.ModState]): - Northstar_Config_Json = self._organizer.profilePath() + "/" + self.NorthstarModJson + Northstar_Config_Json = ( + self._organizer.profilePath() + "/" + self.NorthstarModJson + ) with open(Northstar_Config_Json, "r", encoding="utf-8") as f: Northstar = json.load(f) for key, value in mods.items(): @@ -205,7 +220,9 @@ def update_enable_mods_json(self, mods: dict[str, mobase.ModState]): for e in subtree: if e is not None and e.isDir(): if e.exists("mod.json", mobase.IFileTree.FILE): - json_path = key.absolutePath() + "/" + e.path() + "/mod.json" + json_path = ( + key.absolutePath() + "/" + e.path() + "/mod.json" + ) with open(json_path, "r", encoding="utf-8") as f: mod_data = json.load(f) modname = mod_data["Name"] @@ -217,7 +234,9 @@ def update_enable_mods_json(self, mods: dict[str, mobase.ModState]): Northstar[modname] = {modversion: True} if value == 33 and modname in Northstar: Northstar = Northstar.pop(modname) - with open(Northstar_Config_Json, "w", encoding="utf-8") as f: + with open( + Northstar_Config_Json, "w", encoding="utf-8" + ) as f: json.dump(Northstar, f, ensure_ascii=False, indent=4) def executables(self): @@ -226,7 +245,9 @@ def executables(self): "Titanfall 2", QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), ), - mobase.ExecutableInfo("Northstar", QFileInfo(self.gameDirectory(), "NorthstarLauncher.exe")), + mobase.ExecutableInfo( + "Northstar", QFileInfo(self.gameDirectory(), "NorthstarLauncher.exe") + ), ] @cached_property @@ -240,7 +261,9 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: except AttributeError: efls = [] libs: set[str] = set() - tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = ( + self._organizer.virtualFileTree() + ) if type(tree) is not mobase.IFileTree: return efls for e in tree: @@ -248,7 +271,13 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: libs.add(relpath) exes = self.executables() - efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + efls = efls + [ + mobase.ExecutableForcedLoadSetting( + exe.binary().fileName(), lib + ).withEnabled(True) + for lib in libs + for exe in exes + ] return efls def northstarDirectory(self) -> QDir: @@ -259,9 +288,16 @@ def iniFiles(self): def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): northstar_json_path = directory.absolutePath() + "/" + self.NorthstarModJson - northstar_json_game_path = self.gameDirectory().absolutePath() + "/R2Northstar/" + self.NorthstarModJson + northstar_json_game_path = ( + self.gameDirectory().absolutePath() + + "/R2Northstar/" + + self.NorthstarModJson + ) blank_mod_json = '{"Version": 1,"Northstar.Client": {"1.31.6": true},"Northstar.CustomServers": {"1.31.6": true},"Northstar.Custom": {"1.31.6": true}}' - if not os.path.exists(northstar_json_path) or os.path.getsize(northstar_json_path) == 0: + if ( + not os.path.exists(northstar_json_path) + or os.path.getsize(northstar_json_path) == 0 + ): if os.path.exists(northstar_json_game_path): with open(northstar_json_game_path, "r") as game_json: game_json_content = game_json.read() @@ -273,12 +309,21 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): with open(northstar_json_path, "w") as northstar_json: northstar_json.write(blank_mod_json) northstar_json.close() - modsPath = os.path.join(self.dataDirectory().absolutePath(), self.GameNorthstarPath) + modsPath = os.path.join( + self.dataDirectory().absolutePath(), self.GameNorthstarPath + ) if not os.path.exists(modsPath): os.mkdir(modsPath) super().initializeProfile(directory, settings) def mappings(self) -> list[mobase.Mapping]: return [ - mobase.Mapping(self._organizer.profilePath() + "/" + self.NorthstarModJson, self.gameDirectory().absolutePath() + "/R2Northstar/" + self.NorthstarModJson, False, False), + mobase.Mapping( + self._organizer.profilePath() + "/" + self.NorthstarModJson, + self.gameDirectory().absolutePath() + + "/R2Northstar/" + + self.NorthstarModJson, + False, + False, + ), ] diff --git a/games/game_zuma_deluxe.py b/games/game_zuma_deluxe.py index 9d7da777..a6c3bc73 100644 --- a/games/game_zuma_deluxe.py +++ b/games/game_zuma_deluxe.py @@ -1,16 +1,15 @@ +from enum import IntEnum, auto +from functools import cached_property +from pathlib import Path import os -import shutil import re -import mobase +import shutil -from enum import IntEnum, auto -from pathlib import Path -from functools import cached_property +import mobase +from PyQt6.QtCore import QDir, QFileInfo -from ..basic_game import BasicGame from ..basic_features import BasicGameSaveGameInfo - -from PyQt6.QtCore import QDir, QFileInfo +from ..basic_game import BasicGame class Content(IntEnum): @@ -33,7 +32,10 @@ class ZumaModDataContent(mobase.ModDataContent): ] def getAllContents(self) -> list[mobase.ModDataContent.Content]: - return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + return [ + mobase.ModDataContent.Content(id, name, icon, *filter_only) + for id, name, icon, *filter_only in self.GAMECONTENTS + ] contents = set() @@ -87,7 +89,9 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): filetree: mobase.IFileTree = mod.fileTree() fixed = False modname = mod.name() - if filetree is not None and filetree.exists("mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): + if filetree is not None and filetree.exists( + "mods/FOLDERNAME", mobase.IFileTree.DIRECTORY + ): path = mod.absolutePath() old_path = os.path.join(path, "mods/FOLDERNAME") new_path = os.path.join(path, f"mods/{modname}") @@ -97,8 +101,18 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): return self.needsNameFix = False - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: - validFolders = ["images", "levels", "music", "sounds", "fonts", "properties", "userdata"] + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + validFolders = [ + "images", + "levels", + "music", + "sounds", + "fonts", + "properties", + "userdata", + ] validFiles = ["exe"] for e in filetree: if e.isDir(): @@ -134,7 +148,15 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: GameLevelsPath = self.organizer.managedGame().GameLevelsPath - validFolders = ["images", "levels", "music", "sounds", "fonts", "properties", "userdata"] + validFolders = [ + "images", + "levels", + "music", + "sounds", + "fonts", + "properties", + "userdata", + ] entriesToMove: list[mobase.FileTreeEntry] = [] treefixed = 0 if filetree.exists("map.txt", mobase.IFileTree.FILE): @@ -202,14 +224,20 @@ def init(self, organizer: mobase.IOrganizer) -> bool: return True def update_levels(self, mods: dict[str, mobase.ModState]): - profile_levels_path = self._organizer.profilePath() + "/" + self.ProfileLevelsXml - game_levels_path = os.path.join(self.dataDirectory().absolutePath(), self.GameLevelsXml) + profile_levels_path = ( + self._organizer.profilePath() + "/" + self.ProfileLevelsXml + ) + game_levels_path = os.path.join( + self.dataDirectory().absolutePath(), self.GameLevelsXml + ) for key, value in mods.items(): key = self._organizer.modList().getMod(key) tree = key.fileTree() if tree.exists("levels/levels.xml", mobase.IFileTree.FILE): levels_txt_path = os.path.join(key.absolutePath(), "levels/levels.xml") - profile_levels_path = self._organizer.profilePath() + "/" + self.ProfileLevelsXml + profile_levels_path = ( + self._organizer.profilePath() + "/" + self.ProfileLevelsXml + ) if value == 35: with open(levels_txt_path, "r") as levels_txt: levels_txt_content = levels_txt.read() @@ -246,27 +274,43 @@ def update_levels(self, mods: dict[str, mobase.ModState]): for graphic in graphics_tag: insert_graphics_string += "\n\n" + graphic insert_graphics_string += "\n\n list[mobase.ExecutableForcedLoadSetting]: except AttributeError: efls = [] libs: set[str] = set() - tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = ( + self._organizer.virtualFileTree() + ) if type(tree) is not mobase.IFileTree: return efls for e in tree: @@ -301,14 +349,25 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: libs.add(relpath) exes = self.executables() - efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + efls = efls + [ + mobase.ExecutableForcedLoadSetting( + exe.binary().fileName(), lib + ).withEnabled(True) + for lib in libs + for exe in exes + ] return efls def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): modsPath = self.dataDirectory().absolutePath() profile_levels_path = directory.absolutePath() + "/" + self.ProfileLevelsXml - game_levels_path = os.path.join(self.dataDirectory().absolutePath(), self.GameLevelsXml) - if not os.path.exists(profile_levels_path) or os.path.getsize(profile_levels_path) == 0: + game_levels_path = os.path.join( + self.dataDirectory().absolutePath(), self.GameLevelsXml + ) + if ( + not os.path.exists(profile_levels_path) + or os.path.getsize(profile_levels_path) == 0 + ): with open(game_levels_path, "r") as game_levels: profile_levels_content = game_levels.read() game_levels.close() @@ -321,5 +380,10 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): def mappings(self) -> list[mobase.Mapping]: return [ - mobase.Mapping(self._organizer.profilePath() + "/" + self.ProfileLevelsXml, self.gameDirectory().absolutePath() + "/" + self.GameLevelsXml, False, False), + mobase.Mapping( + self._organizer.profilePath() + "/" + self.ProfileLevelsXml, + self.gameDirectory().absolutePath() + "/" + self.GameLevelsXml, + False, + False, + ), ] diff --git a/games/unreal_tabs/manage_paks/model.py b/games/unreal_tabs/manage_paks/model.py index a21ec9e1..eb4802b5 100644 --- a/games/unreal_tabs/manage_paks/model.py +++ b/games/unreal_tabs/manage_paks/model.py @@ -1,6 +1,6 @@ +from enum import IntEnum, auto import itertools import typing -from enum import IntEnum, auto from typing import Any, TypeAlias, overload import mobase diff --git a/games/unreal_tabs/manage_paks/widget.py b/games/unreal_tabs/manage_paks/widget.py index b84f99bb..c0788274 100644 --- a/games/unreal_tabs/manage_paks/widget.py +++ b/games/unreal_tabs/manage_paks/widget.py @@ -3,15 +3,13 @@ from typing import cast import mobase +from PyQt6.QtWidgets import QGridLayout, QWidget +from PyQt6.QtCore import QDir, QFileInfo from ....basic_features.utils import is_directory from .model import PaksModel from .view import PaksView - -from PyQt6.QtWidgets import QGridLayout, QWidget -from PyQt6.QtCore import QDir, QFileInfo, Qt - def pak_sort(a: tuple[str, str], b: tuple[str, str]) -> int: a_pak, a_str = a[0], a[1] or a[0] b_pak, b_str = b[0], b[1] or b[0] diff --git a/games/unreal_tabs/manage_ue4ss/model.py b/games/unreal_tabs/manage_ue4ss/model.py index 82e5f2e4..b3431353 100644 --- a/games/unreal_tabs/manage_ue4ss/model.py +++ b/games/unreal_tabs/manage_ue4ss/model.py @@ -2,12 +2,10 @@ from json import JSONDecodeError from typing import Any, Iterable +import mobase from PyQt6.QtCore import (QDir, QFileInfo, QMimeData, QModelIndex, QStringListModel, Qt) from PyQt6.QtWidgets import QWidget - -import mobase - from ..constants import DEFAULT_UE4SS_MODS class UE4SSListModel(QStringListModel): diff --git a/games/unreal_tabs/manage_ue4ss/widget.py b/games/unreal_tabs/manage_ue4ss/widget.py index d3732161..f4cbcce1 100644 --- a/games/unreal_tabs/manage_ue4ss/widget.py +++ b/games/unreal_tabs/manage_ue4ss/widget.py @@ -1,16 +1,16 @@ -import json from functools import cmp_to_key +import json from json import JSONDecodeError from pathlib import Path import mobase +from PyQt6.QtCore import QDir, QFileInfo, Qt +from PyQt6.QtWidgets import QGridLayout, QWidget from ..constants import DEFAULT_UE4SS_MODS, UE4SSModInfo from .model import UE4SSListModel from .view import UE4SSView -from PyQt6.QtWidgets import QGridLayout, QWidget -from PyQt6.QtCore import QDir, QFileInfo, Qt class UE4SSTabWidget(QWidget): def __init__(self, parent: QWidget | None, organizer: mobase.IOrganizer): @@ -80,7 +80,9 @@ def _parse_mod_files(self): if isinstance(ue4ss_files, mobase.IFileTree): for entry in ue4ss_files: if isinstance(entry, mobase.IFileTree): - if entry.find("scripts/main.lua") or entry.find("dlls/main.dll"): + if entry.find("scripts/main.lua") or entry.find( + "dlls/main.dll" + ): mod_list.add(entry.name()) if enabled_txt := entry.find("enabled.txt"): try: @@ -107,9 +109,7 @@ def _parse_mod_files(self): ).exists(): mod_list.add(dir_info.fileName()) if QFileInfo( - QDir(dir_info.absoluteFilePath()).absoluteFilePath( - "enabled.txt" - ) + QDir(dir_info.absoluteFilePath()).absoluteFilePath("enabled.txt") ).exists(): Path(dir_info.absoluteFilePath(), "enabled.txt").unlink() From f138917249d12468ae17d8da8bcd6de9fda3ed6d Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Sun, 1 Mar 2026 01:11:29 +0100 Subject: [PATCH 06/31] Fixed Missing Import --- games/game_titanfall2.py | 1 + 1 file changed, 1 insertion(+) diff --git a/games/game_titanfall2.py b/games/game_titanfall2.py index f7cc5f87..51ac39a0 100644 --- a/games/game_titanfall2.py +++ b/games/game_titanfall2.py @@ -1,5 +1,6 @@ from enum import IntEnum, auto from functools import cached_property +import json from pathlib import Path import os import shutil From 8c0e785481c2de6bfb267e20f6f1b3a0e70de3c7 Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Sun, 1 Mar 2026 01:31:52 +0100 Subject: [PATCH 07/31] Bugfix --- games/unreal_tabs/manage_paks/model.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/games/unreal_tabs/manage_paks/model.py b/games/unreal_tabs/manage_paks/model.py index eb4802b5..b8f72d2c 100644 --- a/games/unreal_tabs/manage_paks/model.py +++ b/games/unreal_tabs/manage_paks/model.py @@ -57,13 +57,13 @@ def flags(self, index: QModelIndex) -> Qt.ItemFlag: | Qt.ItemFlag.ItemIsDropEnabled & Qt.ItemFlag.ItemIsEditable ) - def columnCount(self, parent: QModelIndex) -> int: + def columnCount(self, parent: QModelIndex = None) -> int: if parent is None: parent = QModelIndex() return len(PaksColumns) def index( - self, row: int, column: int, parent: QModelIndex + self, row: int, column: int, parent: QModelIndex = None ) -> QModelIndex: if parent is None: parent = QModelIndex() @@ -86,7 +86,7 @@ def parent(self, child: QModelIndex | None = None) -> QModelIndex | QObject | No return super().parent() return QModelIndex() - def rowCount(self, parent: QModelIndex) -> int: + def rowCount(self, parent: QModelIndex = None) -> int: if parent is None: parent = QModelIndex() return len(self.paks) From 5b4aa6eb59a6f452a589279923dbae00de52db47 Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Thu, 5 Mar 2026 10:31:34 +0100 Subject: [PATCH 08/31] Added saves and mod config to casette beasts --- games/game_cassettebeasts.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/games/game_cassettebeasts.py b/games/game_cassettebeasts.py index 41401f56..50d04f49 100644 --- a/games/game_cassettebeasts.py +++ b/games/game_cassettebeasts.py @@ -60,6 +60,7 @@ class CassetteBeastsGame(BasicGame): GameBinary = "CassetteBeasts.exe" GameDataPath = appdataenv + "/CassetteBeasts/mods" GameDocumentsDirectory = appdataenv + "/CassetteBeasts" + GameSavesDirectory = '%GAME_DOCUMENTS%' GameSaveExtension = "gcpf" def init(self, organizer: mobase.IOrganizer) -> bool: @@ -103,7 +104,7 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: return efls def iniFiles(self): - return ["settings.cfg"] + return ["settings.cfg", "mod_settings.cfg"] def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): modsPath = self.dataDirectory().absolutePath() From 224c6a03c80b1bcb8d6656a450da879bccbf9a4f Mon Sep 17 00:00:00 2001 From: Tsuna Date: Fri, 6 Mar 2026 00:50:11 +0100 Subject: [PATCH 09/31] Added Savegame Data for Cassette Beasts Thanks PandoraFox for the code for unpacking gcpf that this is based on. --- games/game_cassettebeasts.py | 79 ++++++++++++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 3 deletions(-) diff --git a/games/game_cassettebeasts.py b/games/game_cassettebeasts.py index 50d04f49..eb1fe576 100644 --- a/games/game_cassettebeasts.py +++ b/games/game_cassettebeasts.py @@ -1,13 +1,22 @@ +from collections.abc import Mapping +from io import BytesIO +import json +import mobase +import math import os import shutil -import mobase +import struct +import sys +import zlib from pathlib import Path from functools import cached_property +from ..basic_features import BasicLocalSavegames +from ..basic_features.basic_save_game_info import (BasicGameSaveGame,BasicGameSaveGameInfo) from ..basic_game import BasicGame -from PyQt6.QtCore import QDir, QFileInfo +from PyQt6.QtCore import QDateTime, QDir, QFile, QFileInfo class CassetteBeastsModDataChecker(mobase.ModDataChecker): @@ -47,6 +56,60 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: return None return filetree +class CassetteBlock: + def __init__(self): + compressed_size = None + data = None + +class CassetteBeastsSaveGame(BasicGameSaveGame): + def __init__(self, filepath: Path): + super().__init__(filepath) + self.name: str = "" + self.cheated: str = "" + self.lastsave: int = 0 + self.elapsed: int = 0 + info = bytearray() + data = bytes() + + with open(filepath, 'rb') as infile: + magic_string = infile.read(4) + + compression_mode, blocksize, raw_size = struct.unpack("III", infile.read(12)) + + num_blocks = math.ceil(raw_size / blocksize) + + blocks = [] + + for bnum in range(num_blocks): + block = CassetteBlock() + block.compressed_size = struct.unpack("I", infile.read(4))[0] + blocks.append(block) + + for block in blocks: + block.data = infile.read(block.compressed_size) + + magic_string = infile.read(4) + infile.close() + + for block in blocks: + data = zlib.decompress(block.data, wbits=40, bufsize=blocksize) + info = info + data + + save_data = json.load(BytesIO(info)) + self.name = save_data["party"]["player"]["custom"]["name"] + self.cheated = save_data["has_cheated"] + + def getName(self) -> str: + return self.name + + def getCheated(self) -> str: + return self.cheated + +def getMetadata(p: Path, save: mobase.ISaveGame) -> Mapping[str, str]: + return { + "Character": save.getName(), + "Cheated": save.getCheated() + } class CassetteBeastsGame(BasicGame): appdataenv = os.getenv("APPDATA") @@ -60,13 +123,16 @@ class CassetteBeastsGame(BasicGame): GameBinary = "CassetteBeasts.exe" GameDataPath = appdataenv + "/CassetteBeasts/mods" GameDocumentsDirectory = appdataenv + "/CassetteBeasts" - GameSavesDirectory = '%GAME_DOCUMENTS%' GameSaveExtension = "gcpf" def init(self, organizer: mobase.IOrganizer) -> bool: super().init(organizer) self.dataChecker = CassetteBeastsModDataChecker(organizer) self._register_feature(self.dataChecker) + self._register_feature(BasicLocalSavegames(self)) + self._register_feature( + BasicGameSaveGameInfo(None, getMetadata) + ) return True def executables(self): @@ -81,6 +147,13 @@ def executables(self): ), ] + def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]: + ext = self._mappings.savegameExtension.get() + return [ + CassetteBeastsSaveGame(path) + for path in Path(folder.absolutePath()).glob(f"*.{ext}") + ] + @cached_property def _base_dlls(self) -> set[str]: base_dir = Path(self.gameDirectory().absolutePath()) From dd51459af82c8f23ebb6f9232775f36d113f2983 Mon Sep 17 00:00:00 2001 From: Tsuna Date: Fri, 6 Mar 2026 20:19:15 +0100 Subject: [PATCH 10/31] Added Save Error Handling, Additional Save Meta and Local Save Fix Authored by Cuteness with small edits by Floofytsuna --- games/game_cassettebeasts.py | 127 ++++++++++++++++++++++++----------- 1 file changed, 88 insertions(+), 39 deletions(-) diff --git a/games/game_cassettebeasts.py b/games/game_cassettebeasts.py index eb1fe576..9c4abfdf 100644 --- a/games/game_cassettebeasts.py +++ b/games/game_cassettebeasts.py @@ -1,16 +1,17 @@ -from collections.abc import Mapping -from io import BytesIO import json -import mobase import math import os import shutil import struct -import sys import zlib +import mobase -from pathlib import Path +from collections.abc import Mapping, Sequence +from datetime import datetime from functools import cached_property +from io import BytesIO +from typing import Any, Optional +from pathlib import Path from ..basic_features import BasicLocalSavegames from ..basic_features.basic_save_game_info import (BasicGameSaveGame,BasicGameSaveGameInfo) @@ -19,6 +20,13 @@ from PyQt6.QtCore import QDateTime, QDir, QFile, QFileInfo +def json_get_me(value: Any, path: Sequence[str | int], /, default: Any) -> Any: + for part in path: + if type(part) not in (str, int) or type(value) not in (dict, list): + return default + value = value[part] + return value + class CassetteBeastsModDataChecker(mobase.ModDataChecker): def __init__(self, organizer: mobase.IOrganizer): super().__init__() @@ -64,40 +72,71 @@ def __init__(self): class CassetteBeastsSaveGame(BasicGameSaveGame): def __init__(self, filepath: Path): super().__init__(filepath) - self.name: str = "" - self.cheated: str = "" - self.lastsave: int = 0 - self.elapsed: int = 0 - info = bytearray() - data = bytes() + self.name: str = "(unknown)" + self.cheated: str = "(unknown)" + self.lastsave: str = "(unknown)" + self.elapsed: str = "(unknown)" + # This doesn't state wether the game would load it, + # only if the data was properly parsed. + self.errorMessage: str = "" + + save_data = None + try: + info = bytearray() + data = bytes() + with open(filepath, 'rb') as infile: + magic_string = infile.read(4) - with open(filepath, 'rb') as infile: - magic_string = infile.read(4) + compression_mode, blocksize, raw_size = struct.unpack("III", infile.read(12)) - compression_mode, blocksize, raw_size = struct.unpack("III", infile.read(12)) + num_blocks = math.ceil(raw_size / blocksize) - num_blocks = math.ceil(raw_size / blocksize) + blocks = [] - blocks = [] + for bnum in range(num_blocks): + block = CassetteBlock() + block.compressed_size = struct.unpack("I", infile.read(4))[0] + blocks.append(block) - for bnum in range(num_blocks): - block = CassetteBlock() - block.compressed_size = struct.unpack("I", infile.read(4))[0] - blocks.append(block) + for block in blocks: + block.data = infile.read(block.compressed_size) + magic_string = infile.read(4) + infile.close() for block in blocks: - block.data = infile.read(block.compressed_size) - - magic_string = infile.read(4) - infile.close() - - for block in blocks: - data = zlib.decompress(block.data, wbits=40, bufsize=blocksize) - info = info + data - - save_data = json.load(BytesIO(info)) - self.name = save_data["party"]["player"]["custom"]["name"] - self.cheated = save_data["has_cheated"] + data = zlib.decompress(block.data, wbits=40, bufsize=blocksize) + info = info + data + save_data = json.load(BytesIO(info)) + except (OSError, struct.error, ValueError) as err: + s = str(err) + self.errorMessage = ('{0}: {1}' if s else '{0}').format( + err.__class__.__name__, s + ) + return + x = json_get_me(save_data, ["party", "player", "custom", "name"], None) + if type(x) is str: + self.name = x + x = json_get_me(save_data, ["saved_datetime"], None) + if type(x) in (int, float): + try: + dt = datetime.fromtimestamp(float(x)) + except OSError: + pass + else: + self.lastsave = "{0:d}-{1:02d}-{2:02d} at {3:02d}:{4:02d}:{5:02d}".format( + dt.year, dt.month, dt.day, + dt.hour, dt.minute, dt.second + ) + x = json_get_me(save_data, ["play_time"], None) + if type(x) in (int, float): + a = [ 0, 0, 0, int(x * 10) ] + a[2:4] = divmod(a[3], 10) + a[1:3] = divmod(a[2], 60) + a[0:2] = divmod(a[1], 60) + self.elapsed = "{0:02d}:{1:02d}:{2:02d}.{3:01d}".format(*a) + x = json_get_me(save_data, ["has_cheated"], None) + if type(x) is bool: + self.cheated = "Yes" if x else "No" def getName(self) -> str: return self.name @@ -105,15 +144,25 @@ def getName(self) -> str: def getCheated(self) -> str: return self.cheated + def getLastSaved(self) -> str: + return self.lastsave + + def getPlayTime(self) -> str: + return self.elapsed + def getMetadata(p: Path, save: mobase.ISaveGame) -> Mapping[str, str]: + if not save.errorMessage: + return { + "Character": save.getName(), + "Last Saved": save.getLastSaved(), + "Play Time": save.getPlayTime(), + "Cheated": save.getCheated() + } return { - "Character": save.getName(), - "Cheated": save.getCheated() + "Error loading file:": save.errorMessage } class CassetteBeastsGame(BasicGame): - appdataenv = os.getenv("APPDATA") - Name = "Cassette Beasts Support Plugin" Author = "modworkshop" Version = "1" @@ -121,15 +170,15 @@ class CassetteBeastsGame(BasicGame): GameShortName = "cassette-beasts" GameSteamId = 1321440 GameBinary = "CassetteBeasts.exe" - GameDataPath = appdataenv + "/CassetteBeasts/mods" - GameDocumentsDirectory = appdataenv + "/CassetteBeasts" + GameDataPath = os.getenv("APPDATA") + "/CassetteBeasts/mods" + GameDocumentsDirectory = os.getenv("APPDATA") + "/CassetteBeasts" GameSaveExtension = "gcpf" def init(self, organizer: mobase.IOrganizer) -> bool: super().init(organizer) self.dataChecker = CassetteBeastsModDataChecker(organizer) self._register_feature(self.dataChecker) - self._register_feature(BasicLocalSavegames(self)) + self._register_feature(BasicLocalSavegames(QDir(self.GameDocumentsDirectory))) self._register_feature( BasicGameSaveGameInfo(None, getMetadata) ) From 2af7ce10274f3d6d5ade10828da16ec7371e4cca Mon Sep 17 00:00:00 2001 From: Tsuna Date: Fri, 6 Mar 2026 20:34:05 +0100 Subject: [PATCH 11/31] Refactor game_cassettebeasts.py for clarity Refactor imports and variable initializations, and improve readability by renaming loop variables. --- games/game_cassettebeasts.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/games/game_cassettebeasts.py b/games/game_cassettebeasts.py index 9c4abfdf..752aa2d0 100644 --- a/games/game_cassettebeasts.py +++ b/games/game_cassettebeasts.py @@ -1,24 +1,23 @@ +from collections.abc import Mapping, Sequence +from datetime import datetime +from functools import cached_property +from io import BytesIO +from typing import Any, Optional +from pathlib import Path import json import math import os import shutil import struct import zlib -import mobase -from collections.abc import Mapping, Sequence -from datetime import datetime -from functools import cached_property -from io import BytesIO -from typing import Any, Optional -from pathlib import Path +import mobase +from PyQt6.QtCore import QDateTime, QDir, QFile, QFileInfo from ..basic_features import BasicLocalSavegames from ..basic_features.basic_save_game_info import (BasicGameSaveGame,BasicGameSaveGameInfo) from ..basic_game import BasicGame -from PyQt6.QtCore import QDateTime, QDir, QFile, QFileInfo - def json_get_me(value: Any, path: Sequence[str | int], /, default: Any) -> Any: for part in path: @@ -66,8 +65,8 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: class CassetteBlock: def __init__(self): - compressed_size = None - data = None + compressed_size: str = "(unknown)" + data: str = "(unknown)" class CassetteBeastsSaveGame(BasicGameSaveGame): def __init__(self, filepath: Path): @@ -85,7 +84,7 @@ def __init__(self, filepath: Path): info = bytearray() data = bytes() with open(filepath, 'rb') as infile: - magic_string = infile.read(4) + infile.read(4) compression_mode, blocksize, raw_size = struct.unpack("III", infile.read(12)) @@ -93,7 +92,7 @@ def __init__(self, filepath: Path): blocks = [] - for bnum in range(num_blocks): + for _bnum in range(num_blocks): block = CassetteBlock() block.compressed_size = struct.unpack("I", infile.read(4))[0] blocks.append(block) @@ -101,7 +100,7 @@ def __init__(self, filepath: Path): for block in blocks: block.data = infile.read(block.compressed_size) - magic_string = infile.read(4) + infile.read(4) infile.close() for block in blocks: data = zlib.decompress(block.data, wbits=40, bufsize=blocksize) From 9012030002b4f6f78d9d28477b063974bd220bce Mon Sep 17 00:00:00 2001 From: Tsuna Date: Fri, 6 Mar 2026 20:37:34 +0100 Subject: [PATCH 12/31] Update CassetteBlock attributes to correct types Change attributes in CassetteBlock class to use appropriate types. --- games/game_cassettebeasts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/games/game_cassettebeasts.py b/games/game_cassettebeasts.py index 752aa2d0..de218205 100644 --- a/games/game_cassettebeasts.py +++ b/games/game_cassettebeasts.py @@ -65,8 +65,8 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: class CassetteBlock: def __init__(self): - compressed_size: str = "(unknown)" - data: str = "(unknown)" + self.compressed_size: int = 0 + self.data: bytes = b'' class CassetteBeastsSaveGame(BasicGameSaveGame): def __init__(self, filepath: Path): From db6fdcd1a90176cc93693a901d5641a556e35e7a Mon Sep 17 00:00:00 2001 From: Tsuna Date: Sat, 7 Mar 2026 20:11:46 +0100 Subject: [PATCH 13/31] Fix registration of BasicLocalSavegames feature its changed to be in line with other basic games, while there likely is a bug involving that line its better addressed in basic games itself. --- games/game_cassettebeasts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/games/game_cassettebeasts.py b/games/game_cassettebeasts.py index de218205..bd2326b7 100644 --- a/games/game_cassettebeasts.py +++ b/games/game_cassettebeasts.py @@ -177,7 +177,7 @@ def init(self, organizer: mobase.IOrganizer) -> bool: super().init(organizer) self.dataChecker = CassetteBeastsModDataChecker(organizer) self._register_feature(self.dataChecker) - self._register_feature(BasicLocalSavegames(QDir(self.GameDocumentsDirectory))) + self._register_feature(BasicLocalSavegames(self)) self._register_feature( BasicGameSaveGameInfo(None, getMetadata) ) From 81219cfb8d2e05c7015e03a4ecc4de7bd7bec575 Mon Sep 17 00:00:00 2001 From: Tsuna Date: Sat, 14 Mar 2026 19:30:45 +0100 Subject: [PATCH 14/31] Add GameSavesDirectory path for CassetteBeasts --- games/game_cassettebeasts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/games/game_cassettebeasts.py b/games/game_cassettebeasts.py index bd2326b7..90fc1847 100644 --- a/games/game_cassettebeasts.py +++ b/games/game_cassettebeasts.py @@ -171,6 +171,7 @@ class CassetteBeastsGame(BasicGame): GameBinary = "CassetteBeasts.exe" GameDataPath = os.getenv("APPDATA") + "/CassetteBeasts/mods" GameDocumentsDirectory = os.getenv("APPDATA") + "/CassetteBeasts" + GameSavesDirectory = os.getenv("APPDATA") + "/CassetteBeasts" GameSaveExtension = "gcpf" def init(self, organizer: mobase.IOrganizer) -> bool: From 18923257f8d9b8449f8fba5e866a768d597d72ba Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Mon, 16 Mar 2026 20:27:31 +0100 Subject: [PATCH 15/31] updated env variables and fix cassette beasts again --- games/game_cassettebeasts.py | 140 ++++++++++++++++++++++++++++++++--- games/game_ovkwalkingdead.py | 2 +- games/game_pacificdrive.py | 2 +- games/game_payday1.py | 2 +- games/game_payday3.py | 2 +- games/game_roadtovostok.py | 4 +- games/game_titanfall2.py | 4 +- 7 files changed, 140 insertions(+), 16 deletions(-) diff --git a/games/game_cassettebeasts.py b/games/game_cassettebeasts.py index 50d04f49..8f1d0790 100644 --- a/games/game_cassettebeasts.py +++ b/games/game_cassettebeasts.py @@ -1,14 +1,30 @@ +from collections.abc import Mapping, Sequence +from datetime import datetime +from functools import cached_property +from io import BytesIO +from typing import Any, Optional +from pathlib import Path +import json +import math import os import shutil -import mobase +import struct +import zlib -from pathlib import Path -from functools import cached_property +import mobase +from PyQt6.QtCore import QDateTime, QDir, QFile, QFileInfo +from ..basic_features import BasicLocalSavegames +from ..basic_features.basic_save_game_info import (BasicGameSaveGame,BasicGameSaveGameInfo) from ..basic_game import BasicGame -from PyQt6.QtCore import QDir, QFileInfo +def json_get_me(value: Any, path: Sequence[str | int], /, default: Any) -> Any: + for part in path: + if type(part) not in (str, int) or type(value) not in (dict, list): + return default + value = value[part] + return value class CassetteBeastsModDataChecker(mobase.ModDataChecker): def __init__(self, organizer: mobase.IOrganizer): @@ -47,10 +63,105 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: return None return filetree +class CassetteBlock: + def __init__(self): + compressed_size: str = "(unknown)" + data: str = "(unknown)" + +class CassetteBeastsSaveGame(BasicGameSaveGame): + def __init__(self, filepath: Path): + super().__init__(filepath) + self.name: str = "(unknown)" + self.cheated: str = "(unknown)" + self.lastsave: str = "(unknown)" + self.elapsed: str = "(unknown)" + # This doesn't state wether the game would load it, + # only if the data was properly parsed. + self.errorMessage: str = "" + + save_data = None + try: + info = bytearray() + data = bytes() + with open(filepath, 'rb') as infile: + infile.read(4) + + compression_mode, blocksize, raw_size = struct.unpack("III", infile.read(12)) + + num_blocks = math.ceil(raw_size / blocksize) + + blocks = [] + + for _bnum in range(num_blocks): + block = CassetteBlock() + block.compressed_size = struct.unpack("I", infile.read(4))[0] + blocks.append(block) + + for block in blocks: + block.data = infile.read(block.compressed_size) + + infile.read(4) + infile.close() + for block in blocks: + data = zlib.decompress(block.data, wbits=40, bufsize=blocksize) + info = info + data + save_data = json.load(BytesIO(info)) + except (OSError, struct.error, ValueError) as err: + s = str(err) + self.errorMessage = ('{0}: {1}' if s else '{0}').format( + err.__class__.__name__, s + ) + return + x = json_get_me(save_data, ["party", "player", "custom", "name"], None) + if type(x) is str: + self.name = x + x = json_get_me(save_data, ["saved_datetime"], None) + if type(x) in (int, float): + try: + dt = datetime.fromtimestamp(float(x)) + except OSError: + pass + else: + self.lastsave = "{0:d}-{1:02d}-{2:02d} at {3:02d}:{4:02d}:{5:02d}".format( + dt.year, dt.month, dt.day, + dt.hour, dt.minute, dt.second + ) + x = json_get_me(save_data, ["play_time"], None) + if type(x) in (int, float): + a = [ 0, 0, 0, int(x * 10) ] + a[2:4] = divmod(a[3], 10) + a[1:3] = divmod(a[2], 60) + a[0:2] = divmod(a[1], 60) + self.elapsed = "{0:02d}:{1:02d}:{2:02d}.{3:01d}".format(*a) + x = json_get_me(save_data, ["has_cheated"], None) + if type(x) is bool: + self.cheated = "Yes" if x else "No" + + def getName(self) -> str: + return self.name + + def getCheated(self) -> str: + return self.cheated + + def getLastSaved(self) -> str: + return self.lastsave + + def getPlayTime(self) -> str: + return self.elapsed + +def getMetadata(p: Path, save: mobase.ISaveGame) -> Mapping[str, str]: + if not save.errorMessage: + return { + "Character": save.getName(), + "Last Saved": save.getLastSaved(), + "Play Time": save.getPlayTime(), + "Cheated": save.getCheated() + } + return { + "Error loading file:": save.errorMessage + } class CassetteBeastsGame(BasicGame): - appdataenv = os.getenv("APPDATA") - Name = "Cassette Beasts Support Plugin" Author = "modworkshop" Version = "1" @@ -58,15 +169,19 @@ class CassetteBeastsGame(BasicGame): GameShortName = "cassette-beasts" GameSteamId = 1321440 GameBinary = "CassetteBeasts.exe" - GameDataPath = appdataenv + "/CassetteBeasts/mods" - GameDocumentsDirectory = appdataenv + "/CassetteBeasts" - GameSavesDirectory = '%GAME_DOCUMENTS%' + GameDataPath = "%USERPROFILE%/AppData/Roaming/CassetteBeasts/mods" + GameDocumentsDirectory = "%USERPROFILE%/AppData/Roaming/CassetteBeasts" + GameSavesDirectory = "%GAME_DOCUMENTS%" GameSaveExtension = "gcpf" def init(self, organizer: mobase.IOrganizer) -> bool: super().init(organizer) self.dataChecker = CassetteBeastsModDataChecker(organizer) self._register_feature(self.dataChecker) + self._register_feature(BasicLocalSavegames(self)) + self._register_feature( + BasicGameSaveGameInfo(None, getMetadata) + ) return True def executables(self): @@ -81,6 +196,13 @@ def executables(self): ), ] + def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]: + ext = self._mappings.savegameExtension.get() + return [ + CassetteBeastsSaveGame(path) + for path in Path(folder.absolutePath()).glob(f"*.{ext}") + ] + @cached_property def _base_dlls(self) -> set[str]: base_dir = Path(self.gameDirectory().absolutePath()) diff --git a/games/game_ovkwalkingdead.py b/games/game_ovkwalkingdead.py index 8437e4c5..7b2314eb 100644 --- a/games/game_ovkwalkingdead.py +++ b/games/game_ovkwalkingdead.py @@ -179,7 +179,7 @@ class OTWDGame(BasicGame): GameDataUE4SSMods = "Binaries/Win64/Mods" GameDataPakMods = "Content/Paks/~Mods" GameDataMovieMods = "Content/Movies" - GameDocumentsDirectory = "%LOCALAPPDATA%/OTWD/Saved/Config/WindowsClient" + GameDocumentsDirectory = "%USERPROFILE%/AppData/Local/OTWD/Saved/Config/WindowsClient" GameSaveExtension = "sav" _main_window: QMainWindow _ue4ss_tab: UE4SSTabWidget diff --git a/games/game_pacificdrive.py b/games/game_pacificdrive.py index 410fc17d..72ea8fff 100644 --- a/games/game_pacificdrive.py +++ b/games/game_pacificdrive.py @@ -180,7 +180,7 @@ class PacificDriveGame(BasicGame): GameDataUE4SSMods = "Binaries/Win64/Mods" GameDataPakMods = "Content/Paks/~Mods" GameDataMovieMods = "Content/Movies" - GameDocumentsDirectory = "%LOCALAPPDATA%/PenDriverPro/Saved/Config/WindowsNoEditor" + GameDocumentsDirectory = "%USERPROFILE%/AppData/Local/PenDriverPro/Saved/Config/WindowsNoEditor" GameSaveExtension = "sav" _main_window: QMainWindow _ue4ss_tab: UE4SSTabWidget diff --git a/games/game_payday1.py b/games/game_payday1.py index 7fee5286..8b20dd05 100644 --- a/games/game_payday1.py +++ b/games/game_payday1.py @@ -199,7 +199,7 @@ class Payday1Game(BasicGame): GameSteamId = 24240 GameBinary = "payday_win32_release.exe" GameDataPath = "%GAME_PATH%" - GameDocumentsDirectory = "%LOCALAPPDATA%/PAYDAY" + GameDocumentsDirectory = "%USERPROFILE%/AppData/Local/PAYDAY" _forced_libraries = ["IPHLPAPI.dll", "WSOCK32.dll"] def init(self, organizer: mobase.IOrganizer) -> bool: diff --git a/games/game_payday3.py b/games/game_payday3.py index 7410d5c9..170fb7c5 100644 --- a/games/game_payday3.py +++ b/games/game_payday3.py @@ -212,7 +212,7 @@ class Payday3Game(BasicGame): GameDataUE4SSMods = "Binaries/Win64/Mods" GameDataPakMods = "Content/Paks/~Mods" GameDataMovieMods = "Content/Movies" - GameDocumentsDirectory = "%LOCALAPPDATA%/PAYDAY3/Saved/Config/WindowsClient" + GameDocumentsDirectory = "%USERPROFILE%/AppData/Local/PAYDAY3/Saved/Config/WindowsClient" GameSaveExtension = "sav" _main_window: QMainWindow _ue4ss_tab: UE4SSTabWidget diff --git a/games/game_roadtovostok.py b/games/game_roadtovostok.py index 871e84a7..e5692fff 100644 --- a/games/game_roadtovostok.py +++ b/games/game_roadtovostok.py @@ -42,7 +42,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: class RoadToVostokGame(BasicGame): Name = "Road to Vostok Support Plugin" - Author = "modworkshop" + Author = "modworkshop" Version = "1" GameName = "Road to Vostok" GameShortName = "road-to-vostok" @@ -50,7 +50,7 @@ class RoadToVostokGame(BasicGame): GameBinary = "Road_to_Vostok_Demo.exe" GameDataPath = "%GAME_PATH%" GameModsPath = "mods" - GameDocumentsDirectory = "%APPDATA%/Godot/app_userdata/Road to Vostok" + GameDocumentsDirectory = "%USERPROFILE%/AppData/Local/Godot/app_userdata/Road to Vostok" GameSaveExtension = "tres" def init(self, organizer: mobase.IOrganizer) -> bool: diff --git a/games/game_titanfall2.py b/games/game_titanfall2.py index 51ac39a0..759cd55a 100644 --- a/games/game_titanfall2.py +++ b/games/game_titanfall2.py @@ -102,7 +102,9 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): ): path = mod.absolutePath() json_path = os.path.join(path, northstarModPath + "FOLDERNAME/mod.json") - mod_data = json.load(open(json_path, encoding="utf-8")) + with open(json_path, "r") as json_data: + mod_data = json.load(json_data) + json_data.close() modname = mod_data["name"] old_path = os.path.join(path, northstarModPath + "FOLDERNAME") new_path = os.path.join(path, northstarModPath + f"{modname}") From d84b34cdfdfbeccdf094b0cb7e7e0269b5ea8274 Mon Sep 17 00:00:00 2001 From: Tsuna Date: Thu, 19 Mar 2026 13:25:22 +0100 Subject: [PATCH 16/31] Fix GameSavesDirectory usage in feature registration --- games/game_cassettebeasts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/games/game_cassettebeasts.py b/games/game_cassettebeasts.py index 8f1d0790..fc1d29ea 100644 --- a/games/game_cassettebeasts.py +++ b/games/game_cassettebeasts.py @@ -178,7 +178,7 @@ def init(self, organizer: mobase.IOrganizer) -> bool: super().init(organizer) self.dataChecker = CassetteBeastsModDataChecker(organizer) self._register_feature(self.dataChecker) - self._register_feature(BasicLocalSavegames(self)) + self._register_feature(BasicLocalSavegames(QDir(self.GameSavesDirectory))) self._register_feature( BasicGameSaveGameInfo(None, getMetadata) ) From 0c3a8990e7ffcb3e055edaa702144a03f8e5f701 Mon Sep 17 00:00:00 2001 From: Tsuna Date: Wed, 8 Apr 2026 23:49:21 +0200 Subject: [PATCH 17/31] Updated EXE Name for Game EA Release --- games/game_roadtovostok.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/games/game_roadtovostok.py b/games/game_roadtovostok.py index e5692fff..f8b84ccd 100644 --- a/games/game_roadtovostok.py +++ b/games/game_roadtovostok.py @@ -47,7 +47,7 @@ class RoadToVostokGame(BasicGame): GameName = "Road to Vostok" GameShortName = "road-to-vostok" GameSteamId = 1963610 - GameBinary = "Road_to_Vostok_Demo.exe" + GameBinary = "RTV.exe" GameDataPath = "%GAME_PATH%" GameModsPath = "mods" GameDocumentsDirectory = "%USERPROFILE%/AppData/Local/Godot/app_userdata/Road to Vostok" From f6c4bf50a0042bbd516df66143da5cc0112154c5 Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Tue, 14 Apr 2026 00:11:17 +0200 Subject: [PATCH 18/31] Clean Code Update --- games/game_cassettebeasts.py | 54 ++++---- games/game_crimeboss.py | 87 ++++++------ games/game_emuvr.py | 25 ++-- games/game_hitman3.py | 55 +++++--- games/game_noita.py | 23 ++-- games/game_oblivion_remaster.py | 2 +- games/game_ovkwalkingdead.py | 112 ++++++++-------- games/game_pacificdrive.py | 112 ++++++++-------- games/game_payday1.py | 160 +++++++++++++---------- games/game_payday2.py | 154 +++++++++++----------- games/game_payday3.py | 141 ++++++++------------ games/game_raid2.py | 57 ++++---- games/game_roadtovostok.py | 8 +- games/game_silenthill2remake.py | 112 ++++++++-------- games/game_titanfall2.py | 114 ++++++++-------- games/game_zuma_deluxe.py | 29 ++-- games/oblivion_remaster/paks/view.py | 2 +- games/oblivion_remaster/ue4ss/view.py | 2 +- games/oblivion_remaster/ue4ss/widget.py | 4 +- games/unreal_tabs/manage_paks/model.py | 8 +- games/unreal_tabs/manage_paks/view.py | 2 +- games/unreal_tabs/manage_paks/widget.py | 86 ++++++------ games/unreal_tabs/manage_ue4ss/model.py | 6 +- games/unreal_tabs/manage_ue4ss/view.py | 2 +- games/unreal_tabs/manage_ue4ss/widget.py | 33 +++-- 25 files changed, 687 insertions(+), 703 deletions(-) diff --git a/games/game_cassettebeasts.py b/games/game_cassettebeasts.py index fc1d29ea..5f965c54 100644 --- a/games/game_cassettebeasts.py +++ b/games/game_cassettebeasts.py @@ -2,7 +2,7 @@ from datetime import datetime from functools import cached_property from io import BytesIO -from typing import Any, Optional +from typing import Any from pathlib import Path import json import math @@ -12,7 +12,7 @@ import zlib import mobase -from PyQt6.QtCore import QDateTime, QDir, QFile, QFileInfo +from PyQt6.QtCore import QDir, QFileInfo from ..basic_features import BasicLocalSavegames from ..basic_features.basic_save_game_info import (BasicGameSaveGame,BasicGameSaveGameInfo) @@ -33,30 +33,29 @@ def __init__(self, organizer: mobase.IOrganizer): def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: for e in filetree: - if e is not None and e.isFile() and e.suffix().casefold() == "pck": + if e.suffix().casefold() == "pck": return mobase.ModDataChecker.VALID return mobase.ModDataChecker.FIXABLE - def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: - GameDataPath = self.organizer.managedGame().GameDataPath + "/" + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + GameDataPath = getattr(self.organizer.managedGame(), "GameDataPath", "") + "/" treefixed = 0 for branch in filetree: mod_name = filetree.name() if mod_name == "": mod_name = branch.name() mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if filetree.createOrphanTree("OrphanTree") is None and os.path.exists(mod_path) and branch.suffix().casefold() == "pck": + if not filetree.createOrphanTree("OrphanTree") and os.path.exists(mod_path) and branch.suffix().casefold() == "pck": os.makedirs(os.path.join(mod_path, GameDataPath), exist_ok=True) shutil.move(os.path.join(mod_path, branch.name()), os.path.join(mod_path, GameDataPath, branch.name())) treefixed = 1 else: - if branch is not None: - if branch.isDir(): - for e in branch: - if e is not None and e.isFile() and e.suffix().casefold() == "pck": - filetree.move(e, GameDataPath, mobase.IFileTree.MERGE) - treefixed = 1 - elif branch.suffix().casefold() == "pck": + if isinstance(branch, mobase.IFileTree): + for e in branch: + if e.suffix().casefold() == "pck": + filetree.move(e, GameDataPath, mobase.IFileTree.MERGE) + treefixed = 1 + elif branch.suffix().casefold() == "pck": filetree.move(branch, GameDataPath, mobase.IFileTree.MERGE) treefixed = 1 if treefixed == 0: @@ -64,9 +63,8 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: return filetree class CassetteBlock: - def __init__(self): - compressed_size: str = "(unknown)" - data: str = "(unknown)" + compressed_size: int = 0 + data: bytes = b'' class CassetteBeastsSaveGame(BasicGameSaveGame): def __init__(self, filepath: Path): @@ -86,11 +84,11 @@ def __init__(self, filepath: Path): with open(filepath, 'rb') as infile: infile.read(4) - compression_mode, blocksize, raw_size = struct.unpack("III", infile.read(12)) + blocksize, raw_size = struct.unpack("III", infile.read(12)) num_blocks = math.ceil(raw_size / blocksize) - blocks = [] + blocks: list[CassetteBlock] = [] for _bnum in range(num_blocks): block = CassetteBlock() @@ -149,17 +147,23 @@ def getLastSaved(self) -> str: def getPlayTime(self) -> str: return self.elapsed -def getMetadata(p: Path, save: mobase.ISaveGame) -> Mapping[str, str]: - if not save.errorMessage: - return { +def getMetadata(p: Path, save: mobase.ISaveGame) -> Mapping[str, str] | None: + err = getattr(save, "errorMessage", "") + if err: + return {"Error loading file:": err} + + # If this is our concrete save-game class, the type checker knows the methods. + if isinstance(save, BasicGameSaveGame): + return + { "Character": save.getName(), "Last Saved": save.getLastSaved(), "Play Time": save.getPlayTime(), "Cheated": save.getCheated() } - return { - "Error loading file:": save.errorMessage - } + else: + return None + class CassetteBeastsGame(BasicGame): Name = "Cassette Beasts Support Plugin" @@ -178,7 +182,7 @@ def init(self, organizer: mobase.IOrganizer) -> bool: super().init(organizer) self.dataChecker = CassetteBeastsModDataChecker(organizer) self._register_feature(self.dataChecker) - self._register_feature(BasicLocalSavegames(QDir(self.GameSavesDirectory))) + self._register_feature(BasicLocalSavegames(QDir(self.GameSavesDirectory))) # type: ignore self._register_feature( BasicGameSaveGameInfo(None, getMetadata) ) diff --git a/games/game_crimeboss.py b/games/game_crimeboss.py index bf88013a..d030bb22 100644 --- a/games/game_crimeboss.py +++ b/games/game_crimeboss.py @@ -27,7 +27,7 @@ class Content(IntEnum): class CrimeBossModDataContent(mobase.ModDataContent): - contents: list[int] = [] + content: list[int] = [] GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ (Content.UCAS, "UCAS", ":/MO/gui/content/geometries"), (Content.UTOC, "UTOC", ":/MO/gui/content/inifile"), @@ -44,15 +44,15 @@ def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): match entry.suffix().casefold(): case "utoc": - self.contents.add(Content.UTOC) + self.content.append(Content.UTOC) case "ucas": - self.contents.add(Content.UCAS) + self.content.append(Content.UCAS) case "pak": - self.contents.add(Content.PAK) + self.content.append(Content.PAK) case "lua": - self.contents.add(Content.UE4SS) + self.content.append(Content.UE4SS) case "dll": - self.contents.add(Content.DLL) + self.content.append(Content.DLL) case "bk2": self.contents.add(Content.BK2) case _: @@ -72,7 +72,7 @@ def __init__(self, organizer: mobase.IOrganizer): self.organizer.modList().onModInstalled(self._Fix_Installed_Mod) self.needsNameFix = False - def move_overwrite_merge(self, source, destination): + def move_overwrite_merge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -88,11 +88,11 @@ def move_overwrite_merge(self, source, destination): def _Fix_Installed_Mod(self, mod: mobase.IModInterface): if not self.needsNameFix: return - GameDataNativeMods = self.organizer.managedGame().GameDataNativeMods + GameDataNativeMods = getattr(self.organizer.managedGame(), "GameDataNativeMods", "") filetree: mobase.IFileTree = mod.fileTree() fixed = False modname = mod.name() - if filetree is not None and filetree.exists(GameDataNativeMods + "/FOLDERNAME", mobase.IFileTree.DIRECTORY): + if filetree.exists(GameDataNativeMods + "/FOLDERNAME", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() old_path = os.path.join(path, GameDataNativeMods + "/FOLDERNAME") new_path = os.path.join(path, GameDataNativeMods + f"/{modname}") @@ -103,22 +103,25 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): self.needsNameFix = False def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: - GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods - GameDataPakMods = self.organizer.managedGame().GameDataPakMods - GameDataNativeMods = self.organizer.managedGame().GameDataNativeMods + GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") + GameDataNativeMods = getattr(self.organizer.managedGame(), "GameDataNativeMods", "") + GameDataMovies = getattr(self.organizer.managedGame(), "GameDataMovies", "") if filetree.exists(GameDataPakMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID if filetree.exists(os.path.dirname(GameDataUE4SSMods), mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID if filetree.exists(GameDataNativeMods, mobase.IFileTree.DIRECTORY) and not filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): return mobase.ModDataChecker.VALID + if filetree.exists(GameDataMovies, mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID return mobase.ModDataChecker.FIXABLE def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): for branch in filetree: - if branch is not None and branch.isDir(): + if isinstance(branch, mobase.IFileTree): for e in branch: - if e is not None and e.name() == name: + if e.name() == name: return True return False @@ -126,18 +129,17 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): entriesToMove: list[mobase.FileTreeEntry] = [] retVal = 0 for e in filetree: - if e is not None: - entriesToMove.append(e) + entriesToMove.append(e) for e in entriesToMove: filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) retVal = 1 return retVal - def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: - GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods + "/" - GameDataPakMods = self.organizer.managedGame().GameDataPakMods + "/" - GameDataNativeMods = self.organizer.managedGame().GameDataNativeMods + "/" - GameDataMovies = self.organizer.managedGame().GameDataMovies + "/" + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + "/" + GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") + "/" + GameDataNativeMods = getattr(self.organizer.managedGame(), "GameDataNativeMods", "") + "/" + GameDataMovies = getattr(self.organizer.managedGame(), "GameDataMovies", "") + "/" treefixed = 0 if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): treefixed = self.allMoveTo(filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/") @@ -154,28 +156,27 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: allowedUnzippedExt = ["pak", "utoc", "ucas", "bk2", "dll"] entriesToMove: list[mobase.FileTreeEntry] = [] for e in filetree: - if e is not None: - if e.isFile(): - fileext = e.suffix().casefold() - if fileext in allowedUnzippedExt: - mod_name = filetree.name() - if mod_name == "": - mod_name = e.name() - mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if filetree.createOrphanTree("OrphanTree") is None and os.path.exists(mod_path): - match e.suffix().casefold(): - case "pak" | "utoc" | "ucas": - os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) - case "bk2": - os.makedirs(os.path.join(mod_path, GameDataMovies), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovies, e.name())) - case _: - pass - treefixed = 1 - else: - entriesToMove.append(e) - if entriesToMove is not None: + if e.isFile(): + fileext = e.suffix().casefold() + if fileext in allowedUnzippedExt: + mod_name = filetree.name() + if mod_name == "": + mod_name = e.name() + mod_path = os.path.join(self.organizer.modsPath(), mod_name) + if not filetree.createOrphanTree("OrphanTree") and os.path.exists(mod_path): + match e.suffix().casefold(): + case "pak" | "utoc" | "ucas": + os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) + case "bk2": + os.makedirs(os.path.join(mod_path, GameDataMovies), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovies, e.name())) + case _: + pass + treefixed = 1 + else: + entriesToMove.append(e) + if entriesToMove: for e in entriesToMove: match e.suffix().casefold(): case "pak" | "utoc" | "ucas": diff --git a/games/game_emuvr.py b/games/game_emuvr.py index 1b4737b2..744c4b78 100644 --- a/games/game_emuvr.py +++ b/games/game_emuvr.py @@ -16,33 +16,32 @@ def __init__(self, organizer: mobase.IOrganizer): self.organizer: mobase.IOrganizer = organizer def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: - GameDataUGCMods = self.organizer.managedGame().GameDataUGCMods + GameDataUGCMods = getattr(self.organizer.managedGame(), "GameDataUGCMods", "") if filetree.exists(GameDataUGCMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID return mobase.ModDataChecker.FIXABLE - def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: - GameDataUGCMods = self.organizer.managedGame().GameDataUGCMods + "/" + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + GameDataUGCMods = getattr(self.organizer.managedGame(), "GameDataUGCMods", "") + "/" treefixed = 0 for branch in filetree: mod_name = filetree.name() if mod_name == "": mod_name = branch.name() mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if filetree.createOrphanTree("OrphanTree") is None and os.path.exists(mod_path) and branch.suffix().casefold() == "ugc": + if not filetree.createOrphanTree("OrphanTree") and os.path.exists(mod_path) and branch.suffix().casefold() == "ugc": os.makedirs(os.path.join(mod_path, GameDataUGCMods), exist_ok=True) shutil.move(os.path.join(mod_path, branch.name()), os.path.join(mod_path, GameDataUGCMods, branch.name())) treefixed = 1 else: - if branch is not None: - if branch.isDir(): - for e in branch: - if e is not None and e.isFile() and e.suffix().casefold() == "ugc": - filetree.move(e, GameDataUGCMods, mobase.IFileTree.MERGE) - treefixed = 1 - elif branch.suffix().casefold() == "ugc": - filetree.move(branch, GameDataUGCMods, mobase.IFileTree.MERGE) - treefixed = 1 + if isinstance(branch, mobase.IFileTree): + for e in branch: + if e.isFile() and e.suffix().casefold() == "ugc": + filetree.move(e, GameDataUGCMods, mobase.IFileTree.MERGE) + treefixed = 1 + elif branch.suffix().casefold() == "ugc": + filetree.move(branch, GameDataUGCMods, mobase.IFileTree.MERGE) + treefixed = 1 if treefixed == 0: return None return filetree diff --git a/games/game_hitman3.py b/games/game_hitman3.py index 3b3ad92c..996ab66a 100644 --- a/games/game_hitman3.py +++ b/games/game_hitman3.py @@ -18,7 +18,7 @@ def __init__(self, organizer: mobase.IOrganizer): self.organizer.modList().onModInstalled(self._Fix_Installed_Mod) self.needsNameFix = False - def move_overwrite_merge(self, source, destination): + def move_overwrite_merge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -34,12 +34,15 @@ def move_overwrite_merge(self, source, destination): def _Fix_Installed_Mod(self, mod: mobase.IModInterface): if not self.needsNameFix: return - GameSMMPath = self.organizer.managedGame().GameSMMPath + GameSMMPath = getattr(self.organizer.managedGame(), "GameSMMPath", "") filetree: mobase.IFileTree = mod.fileTree() fixed = False - if filetree is not None and filetree.exists(GameSMMPath + "/Mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): + if filetree.exists(GameSMMPath + "/Mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): + print("Found folder") path = mod.absolutePath() + print(path) json_path = os.path.join(path, GameSMMPath + "/Mods/FOLDERNAME/manifest.json") + print(json_path) mod_data = json.load(open(json_path, encoding="utf-8")) modname = mod_data["id"] old_path = os.path.join(path, GameSMMPath + "/Mods/FOLDERNAME") @@ -57,36 +60,46 @@ def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.Ch def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): for branch in filetree: - if branch is not None and branch.isDir(): + if isinstance(branch, mobase.IFileTree): for e in branch: - if e is not None and e.name() == name: + if e.name() == name: return True return False - def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): + def allMoveTo(self, sourcetree: mobase.IFileTree, targettree: mobase.IFileTree, toMoveTo: str): entriesToMove: list[mobase.FileTreeEntry] = [] retVal = 0 - for e in filetree: - if e is not None: - entriesToMove.append(e) + for e in sourcetree: + entriesToMove.append(e) for e in entriesToMove: - filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) + targettree.move(e, toMoveTo, mobase.IFileTree.MERGE) retVal = 1 + targettree.remove(sourcetree) return retVal - def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: - GameSMMPath = self.organizer.managedGame().GameSMMPath + def first_tree(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + for e in filetree: + if isinstance(e, mobase.IFileTree) and e.isDir(): + return e + return None + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + GameSMMPath = getattr(self.organizer.managedGame(), "GameSMMPath", "") treefixed = 0 if filetree.exists("manifest.json", mobase.IFileTree.FILE): - treefixed = self.allMoveTo(filetree, GameSMMPath + "/Mods/FOLDERNAME/") + print("Found manifest in root, moving to SMM folder") + print(GameSMMPath + "/Mods/FOLDERNAME/") + treefixed = self.allMoveTo(filetree, filetree, GameSMMPath + "/Mods/FOLDERNAME/") if treefixed == 1: self.needsNameFix = True - if treefixed == 0: - if len(filetree) == 1: - filetree = filetree.find(filetree[0].path("/")) - treefixed = self.allMoveTo(filetree, GameSMMPath + "/Mods/FOLDERNAME/") - if treefixed == 1: - self.needsNameFix = True + elif len(filetree) == 1: + firsttreelayer: mobase.IFileTree | None = self.first_tree(filetree) + if firsttreelayer is not None: + if firsttreelayer.exists("manifest.json", mobase.IFileTree.FILE): + print(GameSMMPath + "/Mods/FOLDERNAME/") + treefixed = self.allMoveTo(firsttreelayer, filetree, GameSMMPath + "/Mods/FOLDERNAME/") + if treefixed == 1: + self.needsNameFix = True if treefixed == 0: return None return filetree @@ -117,9 +130,9 @@ def update_smm_meta(self, mods: dict[str, mobase.ModState]): key = self._organizer.modList().getMod(key) tree = key.fileTree() subtree = tree.find(self.GameSMMPath + "/Mods", mobase.IFileTree.DIRECTORY) - if subtree is not None and subtree.isDir(): + if isinstance(subtree, mobase.IFileTree): for e in subtree: - if e is not None and e.isDir(): + if isinstance(e, mobase.IFileTree): if e.exists("manifest.json", mobase.IFileTree.FILE): json_path = key.absolutePath() + "/" + e.path() + "/manifest.json" mod_data = json.load(open(json_path, encoding="utf-8")) diff --git a/games/game_noita.py b/games/game_noita.py index 38db9331..ca2cac04 100644 --- a/games/game_noita.py +++ b/games/game_noita.py @@ -16,7 +16,7 @@ def __init__(self, organizer: mobase.IOrganizer): self.organizer.modList().onModInstalled(self._Fix_Installed_Mod) self.needsNameFix = False - def move_overwrite_merge(self, source, destination): + def move_overwrite_merge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -32,13 +32,11 @@ def move_overwrite_merge(self, source, destination): def _Fix_Installed_Mod(self, mod: mobase.IModInterface): if not self.needsNameFix: return - GameModsPath = self.organizer.managedGame().GameModsPath + GameModsPath = getattr(self.organizer.managedGame(), "GameModsPath", "") filetree: mobase.IFileTree = mod.fileTree() fixed = False modname = mod.name() - if filetree is not None and filetree.exists( - GameModsPath + "/FOLDERNAME", mobase.IFileTree.DIRECTORY - ): + if filetree.exists(GameModsPath + "/FOLDERNAME", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() old_path = os.path.join(path, GameModsPath + "/FOLDERNAME") new_path = os.path.join(path, GameModsPath + f"/{modname}") @@ -48,18 +46,16 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): return self.needsNameFix = False - def dataLooksValid( - self, filetree: mobase.IFileTree - ) -> mobase.ModDataChecker.CheckReturn: + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: if filetree.exists("mods", mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID return mobase.ModDataChecker.FIXABLE def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): for branch in filetree: - if branch is not None and branch.isDir(): + if isinstance(branch, mobase.IFileTree): for e in branch: - if e is not None and e.name() == name: + if e.name() == name: return True return False @@ -67,15 +63,14 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): entriesToMove: list[mobase.FileTreeEntry] = [] retVal = 0 for e in filetree: - if e is not None: - entriesToMove.append(e) + entriesToMove.append(e) for e in entriesToMove: filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) retVal = 1 return retVal - def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: - GameModsPath = self.organizer.managedGame().GameModsPath + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + GameModsPath = getattr(self.organizer.managedGame(), "GameModsPath", "") treefixed = 0 if filetree.exists("mod.xml", mobase.IFileTree.FILE): treefixed = self.allMoveTo(filetree, GameModsPath + "/FOLDERNAME/") diff --git a/games/game_oblivion_remaster.py b/games/game_oblivion_remaster.py index 218bb048..218fded0 100644 --- a/games/game_oblivion_remaster.py +++ b/games/game_oblivion_remaster.py @@ -344,7 +344,7 @@ def fullDescription(self, key: int) -> str: match key: case Problems.UE4SS_LOADER: return ( - "The UE4SS loader DLL is present (dwmapi.dll). This will not function out-of-the box with MO2's virtual filesystem.\n\n" + "The UE4SS loader DLL is present (dwmapi.dll). This will not function out-of-the-box with MO2's virtual filesystem.\n\n" + "In order to resolve this, either delete the DLL and use the OBSE UE4SS Loader plugin, or rename " + "the DLL (ex. 'ue4ss_loader.dll') and set it to force load with the game exe.\n\n" + "Do this for any executable which runs the game, such as the OBSE64 loader." diff --git a/games/game_ovkwalkingdead.py b/games/game_ovkwalkingdead.py index 7b2314eb..b702d51e 100644 --- a/games/game_ovkwalkingdead.py +++ b/games/game_ovkwalkingdead.py @@ -25,7 +25,7 @@ class Content(IntEnum): class OTWDModDataContent(mobase.ModDataContent): - contents: list[int] = [] + content: list[int] = [] GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ (Content.UCAS, "UCAS", ":/MO/gui/content/geometries"), (Content.UTOC, "UTOC", ":/MO/gui/content/inifile"), @@ -42,17 +42,17 @@ def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): match entry.suffix().casefold(): case "utoc": - self.contents.add(Content.UTOC) + self.content.append(Content.UTOC) case "ucas": - self.contents.add(Content.UCAS) + self.content.append(Content.UCAS) case "pak": - self.contents.add(Content.PAK) + self.content.append(Content.PAK) case "lua": - self.contents.add(Content.UE4SS) + self.content.append(Content.UE4SS) case "dll": - self.contents.add(Content.DLL) + self.content.append(Content.DLL) case "bk2": - self.contents.add(Content.BK2) + self.content.append(Content.BK2) case _: pass return mobase.IFileTree.WalkReturn.CONTINUE @@ -68,7 +68,7 @@ def __init__(self, organizer: mobase.IOrganizer): super().__init__() self.organizer: mobase.IOrganizer = organizer - def move_overwrite_merge(self, source, destination): + def move_overwrite_merge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -82,12 +82,12 @@ def move_overwrite_merge(self, source, destination): os.rmdir(source) def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: - GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods - GameDataPakMods = self.organizer.managedGame().GameDataPakMods - GameDataMovies = self.organizer.managedGame().GameDataMovieMods + GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") + GameDataMovieMods = getattr(self.organizer.managedGame(), "GameDataMovieMods", "") if filetree.exists(GameDataPakMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID - if filetree.exists(GameDataMovies, mobase.IFileTree.DIRECTORY): + if filetree.exists(GameDataMovieMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID if filetree.exists(GameDataUE4SSMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID @@ -95,9 +95,9 @@ def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.Ch def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): for branch in filetree: - if branch is not None and branch.isDir(): + if isinstance(branch, mobase.IFileTree): for e in branch: - if e is not None and e.name() == name: + if e.name() == name: return True return False @@ -105,17 +105,16 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): entriesToMove: list[mobase.FileTreeEntry] = [] retVal = 0 for e in filetree: - if e is not None: - entriesToMove.append(e) + entriesToMove.append(e) for e in entriesToMove: filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) retVal = 1 return retVal - def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: - GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods + "/" - GameDataPakMods = self.organizer.managedGame().GameDataPakMods + "/" - GameDataMovies = self.organizer.managedGame().GameDataMovieMods + "/" + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + "/" + GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") + "/" + GameDataMovieMods = getattr(self.organizer.managedGame(), "GameDataMovieMods", "") + "/" treefixed = 0 if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): treefixed = self.allMoveTo(filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/") @@ -129,28 +128,27 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: allowedUnzippedExt = ["pak", "utoc", "ucas", "bk2", "dll"] entriesToMove: list[mobase.FileTreeEntry] = [] for e in filetree: - if e is not None: - if e.isFile(): - fileext = e.suffix().casefold() - if fileext in allowedUnzippedExt: - mod_name = filetree.name() - if mod_name == "": - mod_name = e.name() - mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if filetree.createOrphanTree("OrphanTree") is None and os.path.exists(mod_path): - match e.suffix().casefold(): - case "pak" | "utoc" | "ucas": - os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) - case "bk2": - os.makedirs(os.path.join(mod_path, GameDataMovies), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovies, e.name())) - case _: - pass - treefixed = 1 - else: - entriesToMove.append(e) - if entriesToMove is not None: + if e.isFile(): + fileext = e.suffix().casefold() + if fileext in allowedUnzippedExt: + mod_name = filetree.name() + if mod_name == "": + mod_name = e.name() + mod_path = os.path.join(self.organizer.modsPath(), mod_name) + if not filetree.createOrphanTree("OrphanTree") and os.path.exists(mod_path): + match e.suffix().casefold(): + case "pak" | "utoc" | "ucas": + os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) + case "bk2": + os.makedirs(os.path.join(mod_path, GameDataMovieMods), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovieMods, e.name())) + case _: + pass + treefixed = 1 + else: + entriesToMove.append(e) + if entriesToMove: for e in entriesToMove: match e.suffix().casefold(): case "pak" | "utoc" | "ucas": @@ -158,7 +156,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: case "dll": filetree.move(e, os.path.dirname(GameDataUE4SSMods) + "/", mobase.IFileTree.MERGE) case "bk2": - filetree.move(e, GameDataMovies, mobase.IFileTree.MERGE) + filetree.move(e, GameDataMovieMods, mobase.IFileTree.MERGE) case _: pass treefixed = 1 @@ -240,15 +238,6 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] return efls - def paksDirectory(self) -> QDir: - return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataPakMods) - - def ue4ssDirectory(self) -> QDir: - return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataUE4SSMods) - - def movieDirectory(self) -> QDir: - return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataMovieMods) - def write_default_mods(self, profile: QDir): ue4ss_mods_txt = QFileInfo(profile.absoluteFilePath("mods.txt")) ue4ss_mods_json = QFileInfo(profile.absoluteFilePath("mods.json")) @@ -268,10 +257,17 @@ def iniFiles(self): def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): self.write_default_mods(directory) - if not self.paksDirectory().exists(): - os.makedirs(self.paksDirectory().absolutePath()) - if not self.ue4ssDirectory().exists(): - os.makedirs(self.ue4ssDirectory().absolutePath()) - if not self.movieDirectory().exists(): - os.makedirs(self.movieDirectory().absolutePath()) + + base_data_dir = self.dataDirectory().absolutePath() + + paksDirectory = QDir(base_data_dir + "/" + self.GameDataPakMods) + ue4ssDirectory = QDir(base_data_dir + "/" + self.GameDataUE4SSMods) + movieDirectory = QDir(base_data_dir + "/" + self.GameDataMovieMods) + + if not paksDirectory.exists(): + os.makedirs(paksDirectory.absolutePath()) + if not ue4ssDirectory.exists(): + os.makedirs(ue4ssDirectory.absolutePath()) + if not movieDirectory.exists(): + os.makedirs(movieDirectory.absolutePath()) super().initializeProfile(directory, settings) diff --git a/games/game_pacificdrive.py b/games/game_pacificdrive.py index 72ea8fff..90fc1873 100644 --- a/games/game_pacificdrive.py +++ b/games/game_pacificdrive.py @@ -25,7 +25,7 @@ class Content(IntEnum): class PacificDriveModDataContent(mobase.ModDataContent): - contents: list[int] = [] + content: list[int] = [] GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ (Content.UCAS, "UCAS", ":/MO/gui/content/geometries"), (Content.UTOC, "UTOC", ":/MO/gui/content/inifile"), @@ -42,17 +42,17 @@ def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): match entry.suffix().casefold(): case "utoc": - self.contents.add(Content.UTOC) + self.content.append(Content.UTOC) case "ucas": - self.contents.add(Content.UCAS) + self.content.append(Content.UCAS) case "pak": - self.contents.add(Content.PAK) + self.content.append(Content.PAK) case "lua": - self.contents.add(Content.UE4SS) + self.content.append(Content.UE4SS) case "dll": - self.contents.add(Content.DLL) + self.content.append(Content.DLL) case "bk2": - self.contents.add(Content.BK2) + self.content.append(Content.BK2) case _: pass return mobase.IFileTree.WalkReturn.CONTINUE @@ -68,7 +68,7 @@ def __init__(self, organizer: mobase.IOrganizer): super().__init__() self.organizer: mobase.IOrganizer = organizer - def move_overwrite_merge(self, source, destination): + def move_overwrite_merge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -82,12 +82,12 @@ def move_overwrite_merge(self, source, destination): os.rmdir(source) def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: - GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods - GameDataPakMods = self.organizer.managedGame().GameDataPakMods - GameDataMovies = self.organizer.managedGame().GameDataMovieMods + GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") + GameDataMovieMods = getattr(self.organizer.managedGame(), "GameDataMovieMods", "") if filetree.exists(GameDataPakMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID - if filetree.exists(GameDataMovies, mobase.IFileTree.DIRECTORY): + if filetree.exists(GameDataMovieMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID if filetree.exists(GameDataUE4SSMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID @@ -95,9 +95,9 @@ def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.Ch def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): for branch in filetree: - if branch is not None and branch.isDir(): + if isinstance(branch, mobase.IFileTree): for e in branch: - if e is not None and e.name() == name: + if e.name() == name: return True return False @@ -105,17 +105,16 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): entriesToMove: list[mobase.FileTreeEntry] = [] retVal = 0 for e in filetree: - if e is not None: - entriesToMove.append(e) + entriesToMove.append(e) for e in entriesToMove: filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) retVal = 1 return retVal - def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: - GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods + "/" - GameDataPakMods = self.organizer.managedGame().GameDataPakMods + "/" - GameDataMovies = self.organizer.managedGame().GameDataMovieMods + "/" + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + "/" + GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") + "/" + GameDataMovieMods = getattr(self.organizer.managedGame(), "GameDataMovieMods", "") + "/" treefixed = 0 if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): treefixed = self.allMoveTo(filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/") @@ -129,28 +128,27 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: allowedUnzippedExt = ["pak", "utoc", "ucas", "bk2", "dll"] entriesToMove: list[mobase.FileTreeEntry] = [] for e in filetree: - if e is not None: - if e.isFile(): - fileext = e.suffix().casefold() - if fileext in allowedUnzippedExt: - mod_name = filetree.name() - if mod_name == "": - mod_name = e.name() - mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if filetree.createOrphanTree("OrphanTree") is None and os.path.exists(mod_path): - match e.suffix().casefold(): - case "pak" | "utoc" | "ucas": - os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) - case "bk2": - os.makedirs(os.path.join(mod_path, GameDataMovies), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovies, e.name())) - case _: - pass - treefixed = 1 - else: - entriesToMove.append(e) - if entriesToMove is not None: + if e.isFile(): + fileext = e.suffix().casefold() + if fileext in allowedUnzippedExt: + mod_name = filetree.name() + if mod_name == "": + mod_name = e.name() + mod_path = os.path.join(self.organizer.modsPath(), mod_name) + if not filetree.createOrphanTree("OrphanTree") and os.path.exists(mod_path): + match e.suffix().casefold(): + case "pak" | "utoc" | "ucas": + os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) + case "bk2": + os.makedirs(os.path.join(mod_path, GameDataMovieMods), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovieMods, e.name())) + case _: + pass + treefixed = 1 + else: + entriesToMove.append(e) + if entriesToMove: for e in entriesToMove: match e.suffix().casefold(): case "pak" | "utoc" | "ucas": @@ -158,7 +156,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: case "dll": filetree.move(e, os.path.dirname(GameDataUE4SSMods) + "/", mobase.IFileTree.MERGE) case "bk2": - filetree.move(e, GameDataMovies, mobase.IFileTree.MERGE) + filetree.move(e, GameDataMovieMods, mobase.IFileTree.MERGE) case _: pass treefixed = 1 @@ -241,15 +239,6 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] return efls - def paksDirectory(self) -> QDir: - return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataPakMods) - - def ue4ssDirectory(self) -> QDir: - return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataUE4SSMods) - - def movieDirectory(self) -> QDir: - return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataMovieMods) - def write_default_mods(self, profile: QDir): ue4ss_mods_txt = QFileInfo(profile.absoluteFilePath("mods.txt")) ue4ss_mods_json = QFileInfo(profile.absoluteFilePath("mods.json")) @@ -269,10 +258,17 @@ def iniFiles(self): def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): self.write_default_mods(directory) - if not self.paksDirectory().exists(): - os.makedirs(self.paksDirectory().absolutePath()) - if not self.ue4ssDirectory().exists(): - os.makedirs(self.ue4ssDirectory().absolutePath()) - if not self.movieDirectory().exists(): - os.makedirs(self.movieDirectory().absolutePath()) + + base_data_dir = self.dataDirectory().absolutePath() + + paksDirectory = QDir(base_data_dir + "/" + self.GameDataPakMods) + ue4ssDirectory = QDir(base_data_dir + "/" + self.GameDataUE4SSMods) + movieDirectory = QDir(base_data_dir + "/" + self.GameDataMovieMods) + + if not paksDirectory.exists(): + os.makedirs(paksDirectory.absolutePath()) + if not ue4ssDirectory.exists(): + os.makedirs(ue4ssDirectory.absolutePath()) + if not movieDirectory.exists(): + os.makedirs(movieDirectory.absolutePath()) super().initializeProfile(directory, settings) diff --git a/games/game_payday1.py b/games/game_payday1.py index 8b20dd05..f4b59e19 100644 --- a/games/game_payday1.py +++ b/games/game_payday1.py @@ -19,6 +19,7 @@ class Content(IntEnum): class Payday1ModDataContent(mobase.ModDataContent): + content: list[int] = [] GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ (Content.TEXTURE, "Textures", ":/MO/gui/content/texture"), (Content.MESH, "Meshes", ":/MO/gui/content/mesh"), @@ -31,30 +32,28 @@ class Payday1ModDataContent(mobase.ModDataContent): def getAllContents(self) -> list[mobase.ModDataContent.Content]: return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] - contents = set() - def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): match entry.suffix().casefold(): case "texture": - self.contents.add(Content.TEXTURE) + self.content.append(Content.TEXTURE) case "model": - self.contents.add(Content.MESH) + self.content.append(Content.MESH) case "lua": - self.contents.add(Content.SCRIPT) + self.content.append(Content.SCRIPT) case "stream": - self.contents.add(Content.SOUND) + self.content.append(Content.SOUND) case "txt": - self.contents.add(Content.STRING) + self.content.append(Content.STRING) case "json": - self.contents.add(Content.CONFIG) + self.content.append(Content.CONFIG) case _: pass return mobase.IFileTree.WalkReturn.CONTINUE def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: filetree.walk(self.walkContent, "/") - return list(self.contents) + return list(self.content) class Payday1ModDataChecker(mobase.ModDataChecker): @@ -64,7 +63,7 @@ def __init__(self, organizer: mobase.IOrganizer): self.organizer.modList().onModInstalled(self._Fix_Installed_Mod) self.needsNameFix = False - def move_overwrite_merge(self, source, destination): + def move_overwrite_merge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -83,19 +82,19 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): filetree: mobase.IFileTree = mod.fileTree() fixed = False modname = mod.name() - if filetree is not None and filetree.exists("mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): + if filetree.exists("mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() old_path = os.path.join(path, "mods/FOLDERNAME") new_path = os.path.join(path, f"mods/{modname}") self.move_overwrite_merge(old_path, new_path) fixed = True - elif filetree is not None and filetree.exists("assets/mod_overrides/FOLDERNAME/", mobase.IFileTree.DIRECTORY): + elif filetree.exists("assets/mod_overrides/FOLDERNAME/", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() old_path = os.path.join(path, "assets/mod_overrides/FOLDERNAME") new_path = os.path.join(path, f"assets/mod_overrides/{modname}") self.move_overwrite_merge(old_path, new_path) fixed = True - elif filetree is not None and filetree.exists("maps/FOLDERNAME", mobase.IFileTree.DIRECTORY): + elif filetree.exists("maps/FOLDERNAME", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() old_path = os.path.join(path, "maps/FOLDERNAME") new_path = os.path.join(path, f"maps/{modname}") @@ -113,78 +112,86 @@ def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.Ch if filetree.exists("maps", mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID for e in filetree: - if e is not None and e.suffix().casefold() == "dll": + if e.suffix().casefold() == "dll": return mobase.ModDataChecker.VALID return mobase.ModDataChecker.FIXABLE def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): for branch in filetree: - if branch is not None and branch.isDir(): + if isinstance(branch, mobase.IFileTree): for e in branch: - if e is not None and e.name() == name: + if e.name() == name: return True return False + def first_tree(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + for e in filetree: + if isinstance(e, mobase.IFileTree) and e.isDir(): + return e + return None + def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): entriesToMove: list[mobase.FileTreeEntry] = [] retVal = 0 for e in filetree: - if e is not None: - entriesToMove.append(e) + entriesToMove.append(e) for e in entriesToMove: filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) retVal = 1 return retVal - def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: treefixed = 0 - if filetree.exists("mod.txt", mobase.IFileTree.FILE): - treefixed = self.allMoveTo(filetree, "mods/FOLDERNAME/") - if treefixed == 1: - self.needsNameFix = True - elif self.fileExistsInNextSubDir(filetree, "mod.txt"): - filetree.move(filetree[0], "mods/", mobase.IFileTree.MERGE) - treefixed = 1 - elif self.fileExistsInNextSubDir(filetree, "main.xml"): - if self.fileExistsInNextSubDir(filetree, "levels"): - filetree.move(filetree[0], "maps/", mobase.IFileTree.MERGE) - treefixed = 1 - else: - filetree.move(filetree[0], "assets/mod_overrides/", mobase.IFileTree.MERGE) - treefixed = 1 - elif filetree.exists("main.xml", mobase.IFileTree.FILE): - if filetree.exists("levels", mobase.IFileTree.DIRECTORY): - treefixed = self.move_overwrite_merge(filetree, "maps/FOLDERNAME") + firsttreelayer: mobase.IFileTree | None = self.first_tree(filetree) + if firsttreelayer is not None: + secondtreelayer: mobase.IFileTree | None = self.first_tree(firsttreelayer) + if filetree.exists("mod.txt", mobase.IFileTree.FILE): + treefixed = self.allMoveTo(filetree, "mods/FOLDERNAME/") if treefixed == 1: self.needsNameFix = True - else: - treefixed = self.allMoveTo(filetree, "assets/mod_overrides/FOLDERNAME/") - if treefixed == 1: - self.needsNameFix = True - else: - if filetree[0][0].exists("mod.txt", mobase.IFileTree.FILE): - filetree.move(filetree[0][0], filetree[0].path("/"), mobase.IFileTree.REPLACE) - filetree.move(filetree[0], "mods/", mobase.IFileTree.MERGE) + elif self.fileExistsInNextSubDir(filetree, "mod.txt"): + filetree.move(firsttreelayer, "mods/", mobase.IFileTree.MERGE) treefixed = 1 - elif filetree[0][0].exists("main.xml", mobase.IFileTree.FILE): - if filetree.exists("levels", mobase.IFileTree.DIRECTORY): - filetree.move(filetree[0][0], filetree[0].path("/"), mobase.IFileTree.REPLACE) - filetree.move(filetree[0], "maps/", mobase.IFileTree.MERGE) + elif self.fileExistsInNextSubDir(filetree, "main.xml"): + if self.fileExistsInNextSubDir(filetree, "levels"): + filetree.move(firsttreelayer, "maps/", mobase.IFileTree.MERGE) treefixed = 1 else: - filetree.move(filetree[0][0], filetree[0].path("/"), mobase.IFileTree.REPLACE) - filetree.move(filetree[0], "assets/mod_overrides/", mobase.IFileTree.MERGE) + filetree.move(firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE) treefixed = 1 - if treefixed == 0: - if len(filetree) == 1: - filetree.move(filetree[0], "assets/mod_overrides/", mobase.IFileTree.MERGE) - treefixed = 1 - else: - for e in filetree: - if e is not None and e.path("/").count("/") == 0: - filetree.move(e, "assets/mod_overrides/FOLDERNAME/", mobase.IFileTree.MERGE) - treefixed = 1 + elif filetree.exists("main.xml", mobase.IFileTree.FILE): + if filetree.exists("levels", mobase.IFileTree.DIRECTORY): + treefixed = self.allMoveTo(filetree, "maps/FOLDERNAME/") + if treefixed == 1: + self.needsNameFix = True + else: + treefixed = self.allMoveTo(filetree, "assets/mod_overrides/FOLDERNAME/") + if treefixed == 1: self.needsNameFix = True + elif secondtreelayer is not None: + if secondtreelayer.exists("mod.txt", mobase.IFileTree.FILE): + filetree.move(secondtreelayer, firsttreelayer.path("/"), mobase.IFileTree.REPLACE) + filetree.move(firsttreelayer, "mods/", mobase.IFileTree.MERGE) + treefixed = 1 + elif secondtreelayer.exists("main.xml", mobase.IFileTree.FILE): + if filetree.exists("levels", mobase.IFileTree.DIRECTORY): + filetree.move(secondtreelayer, firsttreelayer.path("/"), mobase.IFileTree.REPLACE) + filetree.move(firsttreelayer, "maps/", mobase.IFileTree.MERGE) + treefixed = 1 + else: + filetree.move(secondtreelayer, firsttreelayer.path("/"), mobase.IFileTree.REPLACE) + filetree.move(firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE) + treefixed = 1 + if treefixed == 0: + if len(filetree) == 1: + filetree.move(firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE) + treefixed = 1 + else: + for e in filetree: + if e.path("/").count("/") == 0: + filetree.move(e, "assets/mod_overrides/FOLDERNAME/", mobase.IFileTree.MERGE) + treefixed = 1 + self.needsNameFix = True if treefixed == 0: return None return filetree @@ -200,7 +207,7 @@ class Payday1Game(BasicGame): GameBinary = "payday_win32_release.exe" GameDataPath = "%GAME_PATH%" GameDocumentsDirectory = "%USERPROFILE%/AppData/Local/PAYDAY" - _forced_libraries = ["IPHLPAPI.dll", "WSOCK32.dll"] + _forced_libraries = ["IPHLPAPI.dll", "WSOCK32.dll" , "DINPUT8.dll", "PDTHModOverrides.dll"] def init(self, organizer: mobase.IOrganizer) -> bool: super().init(organizer) @@ -210,6 +217,14 @@ def init(self, organizer: mobase.IOrganizer) -> bool: organizer.modList().onModStateChanged(self.dll_copy) return True + def executables(self): + return [ + mobase.ExecutableInfo( + "Payday the Heist", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ) + ] + def dll_copy( self, mods: dict[str, mobase.ModState] ): @@ -220,7 +235,7 @@ def dll_copy( key = self._organizer.modList().getMod(key) tree = key.fileTree() for e in tree: - if e is not None and e.name() in self._forced_libraries: + if e.name() in self._forced_libraries: #add file file_path_source = key.absolutePath() + "/" + e.path() file_path_target = game_path + e.name() @@ -231,14 +246,6 @@ def dll_copy( if os.path.exists(file_path_target): os.remove(file_path_target) - def executables(self): - return [ - mobase.ExecutableInfo( - "Payday: The Heist", - QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), - ), - ] - @cached_property def _base_dlls(self) -> set[str]: base_dir = Path(self.gameDirectory().absolutePath()) @@ -265,7 +272,16 @@ def iniFiles(self): return ["renderer_settings.xml"] def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): - modsPath = self.dataDirectory().absolutePath() - if not os.path.exists(modsPath): - os.mkdir(modsPath) + base_data_dir = self.dataDirectory().absolutePath() + + mapsDirectory = QDir(base_data_dir + "/maps") + modsDirectory = QDir(base_data_dir + "/mods") + overridesDirectory = QDir(base_data_dir + "/assets/mod_overrides") + + if not mapsDirectory.exists(): + os.makedirs(mapsDirectory.absolutePath()) + if not modsDirectory.exists(): + os.makedirs(modsDirectory.absolutePath()) + if not overridesDirectory.exists(): + os.makedirs(overridesDirectory.absolutePath()) super().initializeProfile(directory, settings) diff --git a/games/game_payday2.py b/games/game_payday2.py index 5998bb69..e500d0c8 100644 --- a/games/game_payday2.py +++ b/games/game_payday2.py @@ -19,6 +19,7 @@ class Content(IntEnum): class Payday2ModDataContent(mobase.ModDataContent): + content: list[int] = [] GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ (Content.TEXTURE, "Textures", ":/MO/gui/content/texture"), (Content.MESH, "Meshes", ":/MO/gui/content/mesh"), @@ -31,30 +32,29 @@ class Payday2ModDataContent(mobase.ModDataContent): def getAllContents(self) -> list[mobase.ModDataContent.Content]: return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] - contents = set() def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): match entry.suffix().casefold(): case "texture": - self.contents.add(Content.TEXTURE) + self.content.append(Content.TEXTURE) case "model": - self.contents.add(Content.MESH) + self.content.append(Content.MESH) case "lua": - self.contents.add(Content.SCRIPT) + self.content.append(Content.SCRIPT) case "stream": - self.contents.add(Content.SOUND) + self.content.append(Content.SOUND) case "txt": - self.contents.add(Content.STRING) + self.content.append(Content.STRING) case "json": - self.contents.add(Content.CONFIG) + self.content.append(Content.CONFIG) case _: pass return mobase.IFileTree.WalkReturn.CONTINUE def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: filetree.walk(self.walkContent, "/") - return list(self.contents) + return list(self.content) class Payday2ModDataChecker(mobase.ModDataChecker): @@ -64,7 +64,7 @@ def __init__(self, organizer: mobase.IOrganizer): self.organizer.modList().onModInstalled(self._Fix_Installed_Mod) self.needsNameFix = False - def move_overwrite_merge(self, source, destination): + def move_overwrite_merge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -83,19 +83,19 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): filetree: mobase.IFileTree = mod.fileTree() fixed = False modname = mod.name() - if filetree is not None and filetree.exists("mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): + if filetree.exists("mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() old_path = os.path.join(path, "mods/FOLDERNAME") new_path = os.path.join(path, f"mods/{modname}") self.move_overwrite_merge(old_path, new_path) fixed = True - elif filetree is not None and filetree.exists("assets/mod_overrides/FOLDERNAME", mobase.IFileTree.DIRECTORY): + elif filetree.exists("assets/mod_overrides/FOLDERNAME", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() old_path = os.path.join(path, "assets/mod_overrides/FOLDERNAME") new_path = os.path.join(path, f"assets/mod_overrides/{modname}") self.move_overwrite_merge(old_path, new_path) fixed = True - elif filetree is not None and filetree.exists("maps/FOLDERNAME", mobase.IFileTree.DIRECTORY): + elif filetree.exists("maps/FOLDERNAME", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() old_path = os.path.join(path, "maps/FOLDERNAME") new_path = os.path.join(path, f"maps/{modname}") @@ -120,75 +120,80 @@ def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.Ch def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): for branch in filetree: - if branch is not None and branch.isDir(): + if isinstance(branch, mobase.IFileTree): for e in branch: - if e is not None and e.name() == name: + if e.name() == name: return True return False + def first_tree(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + for e in filetree: + if isinstance(e, mobase.IFileTree) and e.isDir(): + return e + return None + def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): entriesToMove: list[mobase.FileTreeEntry] = [] retVal = 0 for e in filetree: - if e is not None: - entriesToMove.append(e) + entriesToMove.append(e) for e in entriesToMove: filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) retVal = 1 return retVal - def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: treefixed = 0 - - if filetree.exists("mod.txt", mobase.IFileTree.FILE): - treefixed = self.allMoveTo(filetree, "mods/FOLDERNAME/") - if treefixed == 1: - self.needsNameFix = True - elif self.fileExistsInNextSubDir(filetree, "mod.txt"): - filetree.move(filetree[0], "mods/", mobase.IFileTree.MERGE) - treefixed = 1 - elif self.fileExistsInNextSubDir(filetree, "main.xml"): - if self.fileExistsInNextSubDir(filetree, "levels"): - filetree.move(filetree[0], "maps/", mobase.IFileTree.MERGE) - treefixed = 1 - else: - filetree.move(filetree[0], "assets/mod_overrides/", mobase.IFileTree.MERGE) - treefixed = 1 - elif filetree.exists("main.xml", mobase.IFileTree.FILE): - if filetree.exists("levels", mobase.IFileTree.DIRECTORY): - treefixed = self.move_overwrite_merge(filetree, "maps/FOLDERNAME") + firsttreelayer: mobase.IFileTree | None = self.first_tree(filetree) + if firsttreelayer is not None: + secondtreelayer: mobase.IFileTree | None = self.first_tree(firsttreelayer) + if filetree.exists("mod.txt", mobase.IFileTree.FILE): + treefixed = self.allMoveTo(filetree, "mods/FOLDERNAME/") if treefixed == 1: self.needsNameFix = True - else: - treefixed = self.allMoveTo(filetree, "assets/mod_overrides/FOLDERNAME/") - if treefixed == 1: - self.needsNameFix = True - else: - try: - if filetree[0][0].exists("mod.txt", mobase.IFileTree.FILE): - filetree.move(filetree[0][0], filetree[0].path("/"), mobase.IFileTree.REPLACE) - filetree.move(filetree[0], "mods/", mobase.IFileTree.MERGE) + elif self.fileExistsInNextSubDir(filetree, "mod.txt"): + filetree.move(firsttreelayer, "mods/", mobase.IFileTree.MERGE) + treefixed = 1 + elif self.fileExistsInNextSubDir(filetree, "main.xml"): + if self.fileExistsInNextSubDir(filetree, "levels"): + filetree.move(firsttreelayer, "maps/", mobase.IFileTree.MERGE) treefixed = 1 - elif filetree[0][0].exists("main.xml", mobase.IFileTree.FILE): + else: + filetree.move(firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE) + treefixed = 1 + elif filetree.exists("main.xml", mobase.IFileTree.FILE): + if filetree.exists("levels", mobase.IFileTree.DIRECTORY): + treefixed = self.allMoveTo(filetree, "maps/FOLDERNAME/") + if treefixed == 1: + self.needsNameFix = True + else: + treefixed = self.allMoveTo(filetree, "assets/mod_overrides/FOLDERNAME/") + if treefixed == 1: + self.needsNameFix = True + elif secondtreelayer is not None: + if secondtreelayer.exists("mod.txt", mobase.IFileTree.FILE): + filetree.move(secondtreelayer, firsttreelayer.path("/"), mobase.IFileTree.REPLACE) + filetree.move(firsttreelayer, "mods/", mobase.IFileTree.MERGE) + treefixed = 1 + elif secondtreelayer.exists("main.xml", mobase.IFileTree.FILE): if filetree.exists("levels", mobase.IFileTree.DIRECTORY): - filetree.move(filetree[0][0], filetree[0].path("/"), mobase.IFileTree.REPLACE) - filetree.move(filetree[0], "maps/", mobase.IFileTree.MERGE) + filetree.move(secondtreelayer, firsttreelayer.path("/"), mobase.IFileTree.REPLACE) + filetree.move(firsttreelayer, "maps/", mobase.IFileTree.MERGE) treefixed = 1 else: - filetree.move(filetree[0][0], filetree[0].path("/"), mobase.IFileTree.REPLACE) - filetree.move(filetree[0], "assets/mod_overrides/", mobase.IFileTree.MERGE) - except TypeError: - pass - if treefixed == 0: - if len(filetree) == 1 and filetree[0].isDir: - filetree.move(filetree[0], "assets/mod_overrides/", mobase.IFileTree.MERGE) - treefixed = 1 - else: - for e in filetree: - if e is not None and e.path("/").count("/") == 0: - filetree.move(e, "assets/mod_overrides/FOLDERNAME/", mobase.IFileTree.MERGE) + filetree.move(secondtreelayer, firsttreelayer.path("/"), mobase.IFileTree.REPLACE) + filetree.move(firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE) treefixed = 1 - self.needsNameFix = True + if treefixed == 0: + if len(filetree) == 1: + filetree.move(firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE) + treefixed = 1 + else: + for e in filetree: + if e.path("/").count("/") == 0: + filetree.move(e, "assets/mod_overrides/FOLDERNAME/", mobase.IFileTree.MERGE) + treefixed = 1 + self.needsNameFix = True if treefixed == 0: return None return filetree @@ -234,7 +239,7 @@ def dll_copy( key = self._organizer.modList().getMod(key) tree = key.fileTree() for e in tree: - if e is not None and e.name() in self._forced_libraries: + if e.name() in self._forced_libraries: #add file file_path_source = key.absolutePath() + "/" + e.path() file_path_target = game_path + e.name() @@ -267,23 +272,20 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] return efls - def mapsDirectory(self) -> QDir: - return QDir(self.gameDirectory().absolutePath() + "/maps") - - def modsDirectory(self) -> QDir: - return QDir(self.gameDirectory().absolutePath() + "/mods") - - def overridesDirectory(self) -> QDir: - return QDir(self.gameDirectory().absolutePath() + "/assets/mod_overrides") - def iniFiles(self): return ["renderer_settings.xml"] def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): - if not self.mapsDirectory().exists(): - os.makedirs(self.mapsDirectory().absolutePath()) - if not self.modsDirectory().exists(): - os.makedirs(self.modsDirectory().absolutePath()) - if not self.overridesDirectory().exists(): - os.makedirs(self.overridesDirectory().absolutePath()) + base_data_dir = self.dataDirectory().absolutePath() + + mapsDirectory = QDir(base_data_dir + "/maps") + modsDirectory = QDir(base_data_dir + "/mods") + overridesDirectory = QDir(base_data_dir + "/assets/mod_overrides") + + if not mapsDirectory.exists(): + os.makedirs(mapsDirectory.absolutePath()) + if not modsDirectory.exists(): + os.makedirs(modsDirectory.absolutePath()) + if not overridesDirectory.exists(): + os.makedirs(overridesDirectory.absolutePath()) super().initializeProfile(directory, settings) diff --git a/games/game_payday3.py b/games/game_payday3.py index 170fb7c5..91a6a496 100644 --- a/games/game_payday3.py +++ b/games/game_payday3.py @@ -27,7 +27,8 @@ class Content(IntEnum): class Payday3ModDataContent(mobase.ModDataContent): - contents: list[int] = [] + content: list[int] = [] + GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ (Content.UCAS, "UCAS", ":/MO/gui/content/geometries"), (Content.UTOC, "UTOC", ":/MO/gui/content/inifile"), @@ -47,17 +48,17 @@ def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): match entry.suffix().casefold(): case "utoc": - self.contents.add(Content.UTOC) + self.content.append(Content.UTOC) case "ucas": - self.contents.add(Content.UCAS) + self.content.append(Content.UCAS) case "pak": - self.contents.add(Content.PAK) + self.content.append(Content.PAK) case "lua": - self.contents.add(Content.UE4SS) + self.content.append(Content.UE4SS) case "dll": - self.contents.add(Content.DLL) + self.content.append(Content.DLL) case "bk2": - self.contents.add(Content.BK2) + self.content.append(Content.BK2) case _: pass return mobase.IFileTree.WalkReturn.CONTINUE @@ -73,7 +74,7 @@ def __init__(self, organizer: mobase.IOrganizer): super().__init__() self.organizer: mobase.IOrganizer = organizer - def move_overwrite_merge(self, source, destination): + def move_overwrite_merge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -86,15 +87,13 @@ def move_overwrite_merge(self, source, destination): self.move_overwrite_merge(s_item, d_item) os.rmdir(source) - def dataLooksValid( - self, filetree: mobase.IFileTree - ) -> mobase.ModDataChecker.CheckReturn: - GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods - GameDataPakMods = self.organizer.managedGame().GameDataPakMods - GameDataMovies = self.organizer.managedGame().GameDataMovieMods + def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") + GameDataMovieMods = getattr(self.organizer.managedGame(), "GameDataMovieMods", "") if filetree.exists(GameDataPakMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID - if filetree.exists(GameDataMovies, mobase.IFileTree.DIRECTORY): + if filetree.exists(GameDataMovieMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID if filetree.exists(GameDataUE4SSMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID @@ -102,9 +101,9 @@ def dataLooksValid( def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): for branch in filetree: - if branch is not None and branch.isDir(): + if isinstance(branch, mobase.IFileTree): for e in branch: - if e is not None and e.name() == name: + if e.name() == name: return True return False @@ -112,17 +111,16 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): entriesToMove: list[mobase.FileTreeEntry] = [] retVal = 0 for e in filetree: - if e is not None: - entriesToMove.append(e) + entriesToMove.append(e) for e in entriesToMove: filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) retVal = 1 return retVal - def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: - GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods + "/" - GameDataPakMods = self.organizer.managedGame().GameDataPakMods + "/" - GameDataMovies = self.organizer.managedGame().GameDataMovieMods + "/" + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + "/" + GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") + "/" + GameDataMovieMods = getattr(self.organizer.managedGame(), "GameDataMovieMods", "") + "/" treefixed = 0 if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): treefixed = self.allMoveTo( @@ -140,58 +138,35 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: allowedUnzippedExt = ["pak", "utoc", "ucas", "bk2", "dll"] entriesToMove: list[mobase.FileTreeEntry] = [] for e in filetree: - if e is not None: - if e.isFile(): - fileext = e.suffix().casefold() - if fileext in allowedUnzippedExt: - mod_name = filetree.name() - if mod_name == "": - mod_name = e.name() - mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if filetree.createOrphanTree( - "OrphanTree" - ) is None and os.path.exists(mod_path): - match e.suffix().casefold(): - case "pak" | "utoc" | "ucas": - os.makedirs( - os.path.join(mod_path, GameDataPakMods), - exist_ok=True, - ) - shutil.move( - os.path.join(mod_path, e.name()), - os.path.join( - mod_path, GameDataPakMods, e.name() - ), - ) - case "bk2": - os.makedirs( - os.path.join(mod_path, GameDataMovies), - exist_ok=True, - ) - shutil.move( - os.path.join(mod_path, e.name()), - os.path.join( - mod_path, GameDataMovies, e.name() - ), - ) - case _: - pass - treefixed = 1 - else: - entriesToMove.append(e) - if entriesToMove is not None: + if e.isFile(): + fileext = e.suffix().casefold() + if fileext in allowedUnzippedExt: + mod_name = filetree.name() + if mod_name == "": + mod_name = e.name() + mod_path = os.path.join(self.organizer.modsPath(), mod_name) + if not filetree.createOrphanTree("OrphanTree") and os.path.exists(mod_path): + match e.suffix().casefold(): + case "pak" | "utoc" | "ucas": + os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) + case "bk2": + os.makedirs(os.path.join(mod_path, GameDataMovieMods), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovieMods, e.name())) + case _: + pass + treefixed = 1 + else: + entriesToMove.append(e) + if entriesToMove: for e in entriesToMove: match e.suffix().casefold(): case "pak" | "utoc" | "ucas": filetree.move(e, GameDataPakMods, mobase.IFileTree.MERGE) case "dll": - filetree.move( - e, - os.path.dirname(GameDataUE4SSMods) + "/", - mobase.IFileTree.MERGE, - ) + filetree.move(e,os.path.dirname(GameDataUE4SSMods) + "/",mobase.IFileTree.MERGE) case "bk2": - filetree.move(e, GameDataMovies, mobase.IFileTree.MERGE) + filetree.move(e, GameDataMovieMods, mobase.IFileTree.MERGE) case _: pass treefixed = 1 @@ -281,15 +256,6 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: ] return efls - def paksDirectory(self) -> QDir: - return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataPakMods) - - def ue4ssDirectory(self) -> QDir: - return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataUE4SSMods) - - def movieDirectory(self) -> QDir: - return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataMovieMods) - def write_default_mods(self, profile: QDir): ue4ss_mods_txt = QFileInfo(profile.absoluteFilePath("mods.txt")) ue4ss_mods_json = QFileInfo(profile.absoluteFilePath("mods.json")) @@ -309,10 +275,17 @@ def iniFiles(self): def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): self.write_default_mods(directory) - if not self.paksDirectory().exists(): - os.makedirs(self.paksDirectory().absolutePath()) - if not self.ue4ssDirectory().exists(): - os.makedirs(self.ue4ssDirectory().absolutePath()) - if not self.movieDirectory().exists(): - os.makedirs(self.movieDirectory().absolutePath()) + + base_data_dir = self.dataDirectory().absolutePath() + + paksDirectory = QDir(base_data_dir + "/" + self.GameDataPakMods) + ue4ssDirectory = QDir(base_data_dir + "/" + self.GameDataUE4SSMods) + movieDirectory = QDir(base_data_dir + "/" + self.GameDataMovieMods) + + if not paksDirectory.exists(): + os.makedirs(paksDirectory.absolutePath()) + if not ue4ssDirectory.exists(): + os.makedirs(ue4ssDirectory.absolutePath()) + if not movieDirectory.exists(): + os.makedirs(movieDirectory.absolutePath()) super().initializeProfile(directory, settings) diff --git a/games/game_raid2.py b/games/game_raid2.py index 105a0bdf..d52ae603 100644 --- a/games/game_raid2.py +++ b/games/game_raid2.py @@ -21,6 +21,7 @@ class Content(IntEnum): class RaidWW2ModDataContent(mobase.ModDataContent): + content: list[int] = [] GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ (Content.TEXTURE, "Textures", ":/MO/gui/content/texture"), (Content.MESH, "Meshes", ":/MO/gui/content/mesh"), @@ -33,30 +34,28 @@ class RaidWW2ModDataContent(mobase.ModDataContent): def getAllContents(self) -> list[mobase.ModDataContent.Content]: return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] - contents = set() - def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): match entry.suffix().casefold(): case "texture": - self.contents.add(Content.TEXTURE) + self.content.append(Content.TEXTURE) case "model": - self.contents.add(Content.MESH) + self.content.append(Content.MESH) case "lua": - self.contents.add(Content.SCRIPT) + self.content.append(Content.SCRIPT) case "stream": - self.contents.add(Content.SOUND) + self.content.append(Content.SOUND) case "txt": - self.contents.add(Content.STRING) + self.content.append(Content.STRING) case "json": - self.contents.add(Content.CONFIG) + self.content.append(Content.CONFIG) case _: pass return mobase.IFileTree.WalkReturn.CONTINUE def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: filetree.walk(self.walkContent, "/") - return list(self.contents) + return list(self.content) class RaidWW2ModDataChecker(mobase.ModDataChecker): @@ -66,7 +65,7 @@ def __init__(self, organizer: mobase.IOrganizer): self.organizer.modList().onModInstalled(self._Fix_Installed_Mod) self.needsNameFix = False - def move_overwrite_merge(self, source, destination): + def move_overwrite_merge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -85,7 +84,7 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): filetree: mobase.IFileTree = mod.fileTree() fixed = False modname = mod.name() - if filetree is not None and filetree.exists("FOLDERNAME", mobase.IFileTree.DIRECTORY): + if filetree.exists("FOLDERNAME", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() old_path = os.path.join(path, "FOLDERNAME") new_path = os.path.join(path, f"{modname}") @@ -102,9 +101,9 @@ def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.Ch def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): for branch in filetree: - if branch is not None and branch.isDir(): + if isinstance(branch, mobase.IFileTree): for e in branch: - if e is not None and e.name() == name: + if e.name() == name: return True return False @@ -112,8 +111,7 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): entriesToMove: list[mobase.FileTreeEntry] = [] retVal = 0 for e in filetree: - if e is not None: - entriesToMove.append(e) + entriesToMove.append(e) for e in entriesToMove: filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) retVal = 1 @@ -146,6 +144,14 @@ def init(self, organizer: mobase.IOrganizer) -> bool: organizer.modList().onModStateChanged(self.dll_copy) return True + def executables(self): + return [ + mobase.ExecutableInfo( + "Raid: World War II", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ) + ] + def dll_copy( self, mods: dict[str, mobase.ModState] ): @@ -156,7 +162,7 @@ def dll_copy( key = self._organizer.modList().getMod(key) tree = key.fileTree() for e in tree: - if e is not None and e.name() in self._forced_libraries: + if e.name() in self._forced_libraries: #add file file_path_source = key.absolutePath() + "/" + e.path() file_path_target = game_path + e.name() @@ -167,14 +173,6 @@ def dll_copy( if os.path.exists(file_path_target): os.remove(file_path_target) - def executables(self): - return [ - mobase.ExecutableInfo( - "Raid: World War II", - QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), - ), - ] - @cached_property def _base_dlls(self) -> set[str]: base_dir = Path(self.gameDirectory().absolutePath()) @@ -201,7 +199,10 @@ def iniFiles(self): return ["renderer_settings.xml"] def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): - modsPath = self.dataDirectory().absolutePath() - if not os.path.exists(modsPath): - os.mkdir(modsPath) - super().initializeProfile(directory, settings) + base_data_dir = self.dataDirectory().absolutePath() + + modsDirectory = QDir(base_data_dir + "/" + self.GameDataPath) + + if not modsDirectory.exists(): + os.makedirs(modsDirectory.absolutePath()) + super().initializeProfile(directory, settings) \ No newline at end of file diff --git a/games/game_roadtovostok.py b/games/game_roadtovostok.py index f8b84ccd..9e4780fd 100644 --- a/games/game_roadtovostok.py +++ b/games/game_roadtovostok.py @@ -16,12 +16,12 @@ def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.Ch if filetree.exists("mods", mobase.IFileTree.DIRECTORY) and not filetree.exists("mod.txt", mobase.IFileTree.FILE): return mobase.ModDataChecker.VALID for e in filetree: - if e is not None and e.isFile() and e.suffix().casefold() == "pck": + if e.isFile() and e.suffix().casefold() == "pck": return mobase.ModDataChecker.VALID return mobase.ModDataChecker.FIXABLE - def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: - GameModsPath = self.organizer.managedGame().GameModsPath + "/" + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + GameModsPath = getattr(self.organizer.managedGame(), "GameModsPath", "") + "/" treefixed = 0 for branch in filetree: @@ -29,7 +29,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: if mod_name == "": mod_name = branch.name() mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if filetree.createOrphanTree("OrphanTree") is None and os.path.exists(mod_path) and branch.suffix().casefold() == "zip": + if not filetree.createOrphanTree("OrphanTree") and os.path.exists(mod_path) and branch.suffix().casefold() == "zip": os.makedirs(os.path.join(mod_path, GameModsPath), exist_ok=True) shutil.move(os.path.join(mod_path, branch.name()), os.path.join(mod_path, GameModsPath, branch.name())) treefixed = 1 diff --git a/games/game_silenthill2remake.py b/games/game_silenthill2remake.py index fda5ee34..b3dcf8d8 100644 --- a/games/game_silenthill2remake.py +++ b/games/game_silenthill2remake.py @@ -24,7 +24,7 @@ class Content(IntEnum): class SilentHill2ModDataContent(mobase.ModDataContent): - contents: list[int] = [] + content: list[int] = [] GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ (Content.UCAS, "UCAS", ":/MO/gui/content/geometries"), (Content.UTOC, "UTOC", ":/MO/gui/content/inifile"), @@ -41,17 +41,17 @@ def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): match entry.suffix().casefold(): case "utoc": - self.contents.add(Content.UTOC) + self.content.append(Content.UTOC) case "ucas": - self.contents.add(Content.UCAS) + self.content.append(Content.UCAS) case "pak": - self.contents.add(Content.PAK) + self.content.append(Content.PAK) case "lua": - self.contents.add(Content.UE4SS) + self.content.append(Content.UE4SS) case "dll": - self.contents.add(Content.DLL) + self.content.append(Content.DLL) case "bk2": - self.contents.add(Content.BK2) + self.content.append(Content.BK2) case _: pass return mobase.IFileTree.WalkReturn.CONTINUE @@ -67,7 +67,7 @@ def __init__(self, organizer: mobase.IOrganizer): super().__init__() self.organizer: mobase.IOrganizer = organizer - def move_overwrite_merge(self, source, destination): + def move_overwrite_merge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -81,12 +81,12 @@ def move_overwrite_merge(self, source, destination): os.rmdir(source) def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: - GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods - GameDataPakMods = self.organizer.managedGame().GameDataPakMods - GameDataMovies = self.organizer.managedGame().GameDataMovieMods + GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") + GameDataMovieMods = getattr(self.organizer.managedGame(), "GameDataMovieMods", "") if filetree.exists(GameDataPakMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID - if filetree.exists(GameDataMovies, mobase.IFileTree.DIRECTORY): + if filetree.exists(GameDataMovieMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID if filetree.exists(GameDataUE4SSMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID @@ -94,9 +94,9 @@ def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.Ch def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): for branch in filetree: - if branch is not None and branch.isDir(): + if isinstance(branch, mobase.IFileTree): for e in branch: - if e is not None and e.name() == name: + if e.name() == name: return True return False @@ -104,17 +104,16 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): entriesToMove: list[mobase.FileTreeEntry] = [] retVal = 0 for e in filetree: - if e is not None: - entriesToMove.append(e) + entriesToMove.append(e) for e in entriesToMove: filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) retVal = 1 return retVal - def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: - GameDataUE4SSMods = self.organizer.managedGame().GameDataUE4SSMods + "/" - GameDataPakMods = self.organizer.managedGame().GameDataPakMods + "/" - GameDataMovies = self.organizer.managedGame().GameDataMovieMods + "/" + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + "/" + GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") + "/" + GameDataMovieMods = getattr(self.organizer.managedGame(), "GameDataMovieMods", "") + "/" treefixed = 0 if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): treefixed = self.allMoveTo(filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/") @@ -128,28 +127,27 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: allowedUnzippedExt = ["pak", "utoc", "ucas", "bk2", "dll"] entriesToMove: list[mobase.FileTreeEntry] = [] for e in filetree: - if e is not None: - if e.isFile(): - fileext = e.suffix().casefold() - if fileext in allowedUnzippedExt: - mod_name = filetree.name() - if mod_name == "": - mod_name = e.name() - mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if filetree.createOrphanTree("OrphanTree") is None and os.path.exists(mod_path): - match e.suffix().casefold(): - case "pak" | "utoc" | "ucas": - os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) - case "bk2": - os.makedirs(os.path.join(mod_path, GameDataMovies), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovies, e.name())) - case _: - pass - treefixed = 1 - else: - entriesToMove.append(e) - if entriesToMove is not None: + if e.isFile(): + fileext = e.suffix().casefold() + if fileext in allowedUnzippedExt: + mod_name = filetree.name() + if mod_name == "": + mod_name = e.name() + mod_path = os.path.join(self.organizer.modsPath(), mod_name) + if not filetree.createOrphanTree("OrphanTree") and os.path.exists(mod_path): + match e.suffix().casefold(): + case "pak" | "utoc" | "ucas": + os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) + case "bk2": + os.makedirs(os.path.join(mod_path, GameDataMovieMods), exist_ok=True) + shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovieMods, e.name())) + case _: + pass + treefixed = 1 + else: + entriesToMove.append(e) + if entriesToMove: for e in entriesToMove: match e.suffix().casefold(): case "pak" | "utoc" | "ucas": @@ -157,7 +155,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: case "dll": filetree.move(e, os.path.dirname(GameDataUE4SSMods) + "/", mobase.IFileTree.MERGE) case "bk2": - filetree.move(e, GameDataMovies, mobase.IFileTree.MERGE) + filetree.move(e, GameDataMovieMods, mobase.IFileTree.MERGE) case _: pass treefixed = 1 @@ -240,15 +238,6 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] return efls - def paksDirectory(self) -> QDir: - return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataPakMods) - - def ue4ssDirectory(self) -> QDir: - return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataUE4SSMods) - - def movieDirectory(self) -> QDir: - return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataMovieMods) - def write_default_mods(self, profile: QDir): ue4ss_mods_txt = QFileInfo(profile.absoluteFilePath("mods.txt")) ue4ss_mods_json = QFileInfo(profile.absoluteFilePath("mods.json")) @@ -268,10 +257,17 @@ def iniFiles(self): def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): self.write_default_mods(directory) - if not self.paksDirectory().exists(): - os.makedirs(self.paksDirectory().absolutePath()) - if not self.ue4ssDirectory().exists(): - os.makedirs(self.ue4ssDirectory().absolutePath()) - if not self.movieDirectory().exists(): - os.makedirs(self.movieDirectory().absolutePath()) + + base_data_dir = self.dataDirectory().absolutePath() + + paksDirectory = QDir(base_data_dir + "/" + self.GameDataPakMods) + ue4ssDirectory = QDir(base_data_dir + "/" + self.GameDataUE4SSMods) + movieDirectory = QDir(base_data_dir + "/" + self.GameDataMovieMods) + + if not paksDirectory.exists(): + os.makedirs(paksDirectory.absolutePath()) + if not ue4ssDirectory.exists(): + os.makedirs(ue4ssDirectory.absolutePath()) + if not movieDirectory.exists(): + os.makedirs(movieDirectory.absolutePath()) super().initializeProfile(directory, settings) diff --git a/games/game_titanfall2.py b/games/game_titanfall2.py index 759cd55a..dd67358e 100644 --- a/games/game_titanfall2.py +++ b/games/game_titanfall2.py @@ -23,6 +23,7 @@ class Content(IntEnum): class Titanfall2ModDataContent(mobase.ModDataContent): + content: list[int] = [] GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ (Content.MATERIAL, "Materials", ":/MO/gui/content/interface"), (Content.TEXTURE, "Textures", ":/MO/gui/content/texture"), @@ -40,34 +41,33 @@ def getAllContents(self) -> list[mobase.ModDataContent.Content]: for id, name, icon, *filter_only in self.GAMECONTENTS ] - contents = set() def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): match entry.suffix().casefold(): case "vmt": - self.contents.add(Content.MATERIAL) + self.content.append(Content.MATERIAL) case "vtf": - self.contents.add(Content.TEXTURE) + self.content.append(Content.TEXTURE) case "mdl": - self.contents.add(Content.MODELS) + self.content.append(Content.MODELS) case "nut": - self.contents.add(Content.SCRIPT) + self.content.append(Content.SCRIPT) case "txt": - self.contents.add(Content.CONFIG) + self.content.append(Content.CONFIG) case "bik": - self.contents.add(Content.VIDEO) + self.content.append(Content.VIDEO) case "wav": - self.contents.add(Content.AUDIO) + self.content.append(Content.AUDIO) case "rpak" | "starmap" | "starpak": - self.contents.add(Content.STARPAK) + self.content.append(Content.STARPAK) case _: pass return mobase.IFileTree.WalkReturn.CONTINUE def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: filetree.walk(self.walkContent, "/") - return list(self.contents) + return list(self.content) class Titanfall2ModDataChecker(mobase.ModDataChecker): @@ -77,7 +77,7 @@ def __init__(self, organizer: mobase.IOrganizer): self.organizer.modList().onModInstalled(self._Fix_Installed_Mod) self.needsNameFix = False - def move_overwrite_merge(self, source, destination): + def move_overwrite_merge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -93,29 +93,25 @@ def move_overwrite_merge(self, source, destination): def _Fix_Installed_Mod(self, mod: mobase.IModInterface): if not self.needsNameFix: return - northstarModPath = self.organizer.managedGame().GameNorthstarPath + "/" + GameNorthstarPath = getattr(self.organizer.managedGame(), "GameNorthstarPath", "") + "/" filetree: mobase.IFileTree = mod.fileTree() fixed = False modname = mod.name() - if filetree is not None and filetree.exists( - northstarModPath + "FOLDERNAME", mobase.IFileTree.DIRECTORY - ): + if filetree.exists(GameNorthstarPath + "FOLDERNAME", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() - json_path = os.path.join(path, northstarModPath + "FOLDERNAME/mod.json") + json_path = os.path.join(path, GameNorthstarPath + "FOLDERNAME/mod.json") with open(json_path, "r") as json_data: mod_data = json.load(json_data) json_data.close() modname = mod_data["name"] - old_path = os.path.join(path, northstarModPath + "FOLDERNAME") - new_path = os.path.join(path, northstarModPath + f"{modname}") + old_path = os.path.join(path, GameNorthstarPath + "FOLDERNAME") + new_path = os.path.join(path, GameNorthstarPath + f"{modname}") self.move_overwrite_merge(old_path, new_path) fixed = True - elif filetree is not None and filetree.exists( - northstarModPath + "FOLDERNAME_NAME", mobase.IFileTree.DIRECTORY - ): + elif filetree.exists(GameNorthstarPath + "FOLDERNAME_NAME", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() - old_path = os.path.join(path, northstarModPath + "FOLDERNAME_NAME") - new_path = os.path.join(path, northstarModPath + f"{modname}") + old_path = os.path.join(path, GameNorthstarPath + "FOLDERNAME_NAME") + new_path = os.path.join(path, GameNorthstarPath + f"{modname}") self.move_overwrite_merge(old_path, new_path) fixed = True if not fixed: @@ -131,57 +127,55 @@ def dataLooksValid( def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): for branch in filetree: - if branch is not None and branch.isDir(): + if isinstance(branch, mobase.IFileTree): for e in branch: - if e is not None and e.name() == name: + if e.name() == name: return True return False + def first_tree(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + for e in filetree: + if isinstance(e, mobase.IFileTree) and e.isDir(): + return e + return None + def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): entriesToMove: list[mobase.FileTreeEntry] = [] retVal = 0 for e in filetree: - if e is not None: - entriesToMove.append(e) + entriesToMove.append(e) for e in entriesToMove: filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) retVal = 1 return retVal - def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: - northstarModPath = self.organizer.managedGame().GameNorthstarPath + "/" + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + GameNorthstarPath = getattr(self.organizer.managedGame(), "GameNorthstarPath", "") + "/" treefixed = 0 - if filetree.exists("mod.json", mobase.IFileTree.FILE): - treefixed = self.allMoveTo(filetree, northstarModPath + "FOLDERNAME/") - if treefixed == 1: - self.needsNameFix = True - elif self.fileExistsInNextSubDir(filetree, "mod.json"): - filetree.move(filetree[0], northstarModPath, mobase.IFileTree.MERGE) - treefixed = 1 - else: - try: - if filetree[0][0].exists("mod.json", mobase.IFileTree.FILE): - filetree.move( - filetree[0][0], filetree[0].path("/"), mobase.IFileTree.REPLACE - ) - filetree.move(filetree[0], northstarModPath, mobase.IFileTree.MERGE) - treefixed = 1 - except TypeError: - pass - if treefixed == 0: - if len(filetree) == 1 and filetree[0].isDir: - filetree.move(filetree[0], northstarModPath, mobase.IFileTree.MERGE) + firsttreelayer: mobase.IFileTree | None = self.first_tree(filetree) + if firsttreelayer is not None: + secondtreelayer: mobase.IFileTree | None = self.first_tree(firsttreelayer) + if filetree.exists("mod.json", mobase.IFileTree.FILE): + treefixed = self.allMoveTo(filetree, GameNorthstarPath + "FOLDERNAME/") + if treefixed == 1: + self.needsNameFix = True + elif self.fileExistsInNextSubDir(filetree, "mod.json"): + filetree.move(firsttreelayer, GameNorthstarPath, mobase.IFileTree.MERGE) treefixed = 1 - else: - for e in filetree: - if e is not None and e.path("/").count("/") == 0: - filetree.move( - e, - northstarModPath + "FOLDERNAME_NAME/", - mobase.IFileTree.MERGE, - ) + if secondtreelayer is not None: + if secondtreelayer.exists("mod.json", mobase.IFileTree.FILE): + filetree.move(secondtreelayer, firsttreelayer.path("/"), mobase.IFileTree.REPLACE) + filetree.move(firsttreelayer, GameNorthstarPath, mobase.IFileTree.MERGE) + treefixed = 1 + elif len(filetree) == 1: + filetree.move(firsttreelayer, GameNorthstarPath, mobase.IFileTree.MERGE) treefixed = 1 - self.needsNameFix = True + else: + for e in filetree: + if e.path("/").count("/") == 0: + filetree.move(e,GameNorthstarPath + "FOLDERNAME_NAME/",mobase.IFileTree.MERGE) + treefixed = 1 + self.needsNameFix = True if treefixed == 0: return None return filetree @@ -219,9 +213,9 @@ def update_enable_mods_json(self, mods: dict[str, mobase.ModState]): key = self._organizer.modList().getMod(key) tree = key.fileTree() subtree = tree.find("R2Northstar/mods", mobase.IFileTree.DIRECTORY) - if subtree is not None and subtree.isDir(): + if isinstance(subtree, mobase.IFileTree): for e in subtree: - if e is not None and e.isDir(): + if isinstance(e, mobase.IFileTree): if e.exists("mod.json", mobase.IFileTree.FILE): json_path = ( key.absolutePath() + "/" + e.path() + "/mod.json" diff --git a/games/game_zuma_deluxe.py b/games/game_zuma_deluxe.py index a6c3bc73..39cd3377 100644 --- a/games/game_zuma_deluxe.py +++ b/games/game_zuma_deluxe.py @@ -37,7 +37,7 @@ def getAllContents(self) -> list[mobase.ModDataContent.Content]: for id, name, icon, *filter_only in self.GAMECONTENTS ] - contents = set() + contents: set[int] = set() def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): @@ -70,7 +70,7 @@ def __init__(self, organizer: mobase.IOrganizer): self.organizer.modList().onModInstalled(self._Fix_Installed_Mod) self.needsNameFix = False - def move_overwrite_merge(self, source, destination): + def move_overwrite_merge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -89,7 +89,7 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): filetree: mobase.IFileTree = mod.fileTree() fixed = False modname = mod.name() - if filetree is not None and filetree.exists( + if filetree.exists( "mods/FOLDERNAME", mobase.IFileTree.DIRECTORY ): path = mod.absolutePath() @@ -129,9 +129,9 @@ def dataLooksValid( def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): for branch in filetree: - if branch is not None and branch.isDir(): + if isinstance(branch, mobase.IFileTree): for e in branch: - if e is not None and e.name() == name: + if e.name() == name: return True return False @@ -139,15 +139,16 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): entriesToMove: list[mobase.FileTreeEntry] = [] retVal = 0 for e in filetree: - if e is not None: - entriesToMove.append(e) + entriesToMove.append(e) for e in entriesToMove: filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) retVal = 1 return retVal - def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: - GameLevelsPath = self.organizer.managedGame().GameLevelsPath + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + GameLevelsPath: str = str( + getattr(self.organizer.managedGame(), "GameLevelsPath", "levels") +) validFolders = [ "images", "levels", @@ -169,22 +170,22 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: else: moveonce = 0 for branch in filetree: - if branch is not None and branch.isDir(): + if isinstance(branch, mobase.IFileTree): for entry in branch: for folder in validFolders: - if entry is not None and entry.name() == folder: + if entry.name() == folder: moveonce = 1 if moveonce == 1: for branch in filetree: - if branch is not None and branch.isDir(): + if isinstance(branch, mobase.IFileTree): for entry in branch: entriesToMove.append(entry) - if entriesToMove is not None: + if entriesToMove: for e in entriesToMove: filetree.move(e, "", mobase.IFileTree.MERGE) treefixed = 1 for branch in filetree: - if branch is not None and branch.isDir(): + if isinstance(branch, mobase.IFileTree): if len(branch) == 0: filetree.remove(branch) if treefixed == 0: diff --git a/games/oblivion_remaster/paks/view.py b/games/oblivion_remaster/paks/view.py index a56b0cdd..f432d42d 100644 --- a/games/oblivion_remaster/paks/view.py +++ b/games/oblivion_remaster/paks/view.py @@ -29,5 +29,5 @@ def dropEvent(self, e: QDropEvent | None): def dataChanged( self, topLeft: QModelIndex, bottomRight: QModelIndex, roles: Iterable[int] = () ): - super().dataChanged(topLeft, bottomRight, roles) + super().dataChanged(topLeft, bottomRight, roles) # type: ignore self.repaint() diff --git a/games/oblivion_remaster/ue4ss/view.py b/games/oblivion_remaster/ue4ss/view.py index bb994ca6..61822841 100644 --- a/games/oblivion_remaster/ue4ss/view.py +++ b/games/oblivion_remaster/ue4ss/view.py @@ -27,5 +27,5 @@ def dropEvent(self, e: QDropEvent | None): def dataChanged( self, topLeft: QModelIndex, bottomRight: QModelIndex, roles: Iterable[int] = () ): - super().dataChanged(topLeft, bottomRight, roles) + super().dataChanged(topLeft, bottomRight, roles) # type: ignore self.repaint() diff --git a/games/oblivion_remaster/ue4ss/widget.py b/games/oblivion_remaster/ue4ss/widget.py index 246b7fa0..071071ef 100644 --- a/games/oblivion_remaster/ue4ss/widget.py +++ b/games/oblivion_remaster/ue4ss/widget.py @@ -108,9 +108,7 @@ def _parse_mod_files(self): game = self._organizer.managedGame() if isinstance(game, OblivionRemasteredGame): if game.ue4ssDirectory().exists(): - for dir_info in game.ue4ssDirectory().entryInfoList( - QDir.Filter.Dirs | QDir.Filter.NoDotAndDotDot - ): + for dir_info in game.ue4ssDirectory().entryInfoList(QDir.Filter.Dirs | QDir.Filter.NoDotAndDotDot): # type: ignore if QFileInfo( QDir(dir_info.absoluteFilePath()).absoluteFilePath( "scripts/main.lua" diff --git a/games/unreal_tabs/manage_paks/model.py b/games/unreal_tabs/manage_paks/model.py index b8f72d2c..2edeb7b4 100644 --- a/games/unreal_tabs/manage_paks/model.py +++ b/games/unreal_tabs/manage_paks/model.py @@ -57,14 +57,12 @@ def flags(self, index: QModelIndex) -> Qt.ItemFlag: | Qt.ItemFlag.ItemIsDropEnabled & Qt.ItemFlag.ItemIsEditable ) - def columnCount(self, parent: QModelIndex = None) -> int: + def columnCount(self, parent: QModelIndex | None = None) -> int: if parent is None: parent = QModelIndex() return len(PaksColumns) - def index( - self, row: int, column: int, parent: QModelIndex = None - ) -> QModelIndex: + def index(self, row: int, column: int, parent: QModelIndex | None = None) -> QModelIndex: if parent is None: parent = QModelIndex() if ( @@ -86,7 +84,7 @@ def parent(self, child: QModelIndex | None = None) -> QModelIndex | QObject | No return super().parent() return QModelIndex() - def rowCount(self, parent: QModelIndex = None) -> int: + def rowCount(self, parent: QModelIndex | None = None) -> int: if parent is None: parent = QModelIndex() return len(self.paks) diff --git a/games/unreal_tabs/manage_paks/view.py b/games/unreal_tabs/manage_paks/view.py index a56b0cdd..05681ff4 100644 --- a/games/unreal_tabs/manage_paks/view.py +++ b/games/unreal_tabs/manage_paks/view.py @@ -29,5 +29,5 @@ def dropEvent(self, e: QDropEvent | None): def dataChanged( self, topLeft: QModelIndex, bottomRight: QModelIndex, roles: Iterable[int] = () ): - super().dataChanged(topLeft, bottomRight, roles) + super().dataChanged(topLeft, bottomRight, roles) # type: ignore self.repaint() diff --git a/games/unreal_tabs/manage_paks/widget.py b/games/unreal_tabs/manage_paks/widget.py index c0788274..5c6fcf2c 100644 --- a/games/unreal_tabs/manage_paks/widget.py +++ b/games/unreal_tabs/manage_paks/widget.py @@ -115,6 +115,10 @@ def _shake_paks(self, sorted_paks: dict[str, str]) -> list[str]: def _parse_pak_files(self): game = self._organizer.managedGame() mods = self._organizer.modList().allMods() + pak_mods = None + data_pak_mods = getattr(game, "GameDataPakMods", None) + data_path = game.dataDirectory() + pak_dir = None paks: dict[str, str] = {} pak_paths: dict[str, tuple[str, str]] = {} pak_source: dict[str, str] = {} @@ -123,7 +127,8 @@ def _parse_pak_files(self): if not self._organizer.modList().state(mod) & mobase.ModState.ACTIVE: continue filetree = mod_item.fileTree() - pak_mods = filetree.find(game.GameDataPakMods) + if data_pak_mods: + pak_mods = filetree.find(data_pak_mods) if isinstance(pak_mods, mobase.IFileTree): for entry in pak_mods: if is_directory(entry): @@ -156,47 +161,44 @@ def _parse_pak_files(self): mod_item.absolutePath() + "/" + pak_mods.path("/"), ) pak_source[pak_name] = mod_item.name() - pak_mods = QFileInfo(game.paksDirectory().absolutePath()) - if pak_mods.exists() and pak_mods.isDir(): - for entry in QDir(pak_mods.absoluteFilePath()).entryInfoList( - QDir.Filter.Dirs | QDir.Filter.Files | QDir.Filter.NoDotAndDotDot - ): - if entry.isDir(): - for sub_entry in QDir(entry.absoluteFilePath()).entryInfoList( - QDir.Filter.Files - ): - if ( - sub_entry.isFile() - and sub_entry.suffix().casefold() == "pak" - ): - pak_name = sub_entry.completeBaseName() - paks[pak_name] = entry.completeBaseName() + if data_path and data_pak_mods: + pak_dir = QFileInfo(data_path.absolutePath() + "/" + data_pak_mods) + if pak_dir.exists() and pak_dir.isDir(): + for entry in QDir(pak_dir.absoluteFilePath()).entryInfoList(QDir.Filter.Dirs | QDir.Filter.Files | QDir.Filter.NoDotAndDotDot): # type: ignore + if entry.isDir(): + for sub_entry in QDir(entry.absoluteFilePath()).entryInfoList(QDir.Filter.Files): # type: ignore + if ( + sub_entry.isFile() + and sub_entry.suffix().casefold() == "pak" + ): + pak_name = sub_entry.completeBaseName() + paks[pak_name] = entry.completeBaseName() + pak_paths[pak_name] = ( + sub_entry.absolutePath(), + pak_dir.absolutePath(), + ) + pak_source[pak_name] = "Game Directory" + else: + if entry.suffix().casefold() == "pak": + pak_name = entry.completeBaseName() + paks[pak_name] = "" pak_paths[pak_name] = ( - sub_entry.absolutePath(), - pak_mods.absolutePath(), + entry.absolutePath(), + pak_dir.absolutePath(), ) pak_source[pak_name] = "Game Directory" - else: - if entry.suffix().casefold() == "pak": - pak_name = entry.completeBaseName() - paks[pak_name] = "" - pak_paths[pak_name] = ( - entry.absolutePath(), - pak_mods.absolutePath(), - ) - pak_source[pak_name] = "Game Directory" - sorted_paks = dict(sorted(paks.items(), key=cmp_to_key(pak_sort))) - shaken_paks: list[str] = self._shake_paks(sorted_paks) - final_paks: dict[str, tuple[str, str, str]] = {} - pak_index = 8999 - for pak in shaken_paks: - target_dir = pak_paths[pak][1] + "/" + str(pak_index).zfill(4) - final_paks[pak] = (pak_source[pak], pak_paths[pak][0], target_dir) - pak_index -= 1 - new_data_paks: dict[int, tuple[str, str, str, str]] = {} - i = 0 - for pak, data in final_paks.items(): - source, current_path, target_path = data - new_data_paks[i] = (pak, source, current_path, target_path) - i += 1 - self._model.set_paks(new_data_paks) + sorted_paks = dict(sorted(paks.items(), key=cmp_to_key(pak_sort))) + shaken_paks: list[str] = self._shake_paks(sorted_paks) + final_paks: dict[str, tuple[str, str, str]] = {} + pak_index = 8999 + for pak in shaken_paks: + target_dir = pak_paths[pak][1] + "/" + str(pak_index).zfill(4) + final_paks[pak] = (pak_source[pak], pak_paths[pak][0], target_dir) + pak_index -= 1 + new_data_paks: dict[int, tuple[str, str, str, str]] = {} + i = 0 + for pak, data in final_paks.items(): + source, current_path, target_path = data + new_data_paks[i] = (pak, source, current_path, target_path) + i += 1 + self._model.set_paks(new_data_paks) diff --git a/games/unreal_tabs/manage_ue4ss/model.py b/games/unreal_tabs/manage_ue4ss/model.py index b3431353..8909d3aa 100644 --- a/games/unreal_tabs/manage_ue4ss/model.py +++ b/games/unreal_tabs/manage_ue4ss/model.py @@ -10,7 +10,7 @@ class UE4SSListModel(QStringListModel): def __init__(self, parent: QWidget | None, organizer: mobase.IOrganizer): - super().__init__(parent) + super().__init__(parent) # type: ignore self._checked_items: set[str] = set() self._organizer = organizer self._init_mod_states() @@ -86,8 +86,8 @@ def setData( self.dataChanged.emit(index, index, [role]) return True - def setStringList(self, strings: Iterable[str | None]): - super().setStringList(strings) + def setStringList(self, strings: Iterable[str | None]) -> None: + super().setStringList(strings) # type: ignore self._set_mod_states() def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any: diff --git a/games/unreal_tabs/manage_ue4ss/view.py b/games/unreal_tabs/manage_ue4ss/view.py index bb994ca6..61822841 100644 --- a/games/unreal_tabs/manage_ue4ss/view.py +++ b/games/unreal_tabs/manage_ue4ss/view.py @@ -27,5 +27,5 @@ def dropEvent(self, e: QDropEvent | None): def dataChanged( self, topLeft: QModelIndex, bottomRight: QModelIndex, roles: Iterable[int] = () ): - super().dataChanged(topLeft, bottomRight, roles) + super().dataChanged(topLeft, bottomRight, roles) # type: ignore self.repaint() diff --git a/games/unreal_tabs/manage_ue4ss/widget.py b/games/unreal_tabs/manage_ue4ss/widget.py index f4cbcce1..47b874e0 100644 --- a/games/unreal_tabs/manage_ue4ss/widget.py +++ b/games/unreal_tabs/manage_ue4ss/widget.py @@ -43,6 +43,7 @@ def update_mod_files( self, mods: dict[str, mobase.ModState] | mobase.IModInterface | str ): game = self._organizer.managedGame() + game_data_ue4ss_mods = getattr(game, "GameDataUE4SSMods", None) mod_list: list[mobase.IModInterface] = [] if isinstance(mods, dict): for mod in mods.keys(): @@ -54,7 +55,7 @@ def update_mod_files( for mod in mod_list: tree = mod.fileTree() - ue4ss_files = tree.find(game.GameDataUE4SSMods) + ue4ss_files = tree.find(game_data_ue4ss_mods) if isinstance(game_data_ue4ss_mods, str) else None if isinstance(ue4ss_files, mobase.IFileTree): for entry in ue4ss_files: if isinstance(entry, mobase.IFileTree): @@ -69,6 +70,9 @@ def update_mod_files( def _parse_mod_files(self): game = self._organizer.managedGame() + data_ue4ss_mods = getattr(game, "GameDataUE4SSMods", None) + data_path = game.dataDirectory() + ue4ss_dir = None mod_list: set[str] = set() for mod in self._organizer.modList().allMods(): if ( @@ -76,7 +80,7 @@ def _parse_mod_files(self): & mobase.ModState.ACTIVE ): tree = self._organizer.modList().getMod(mod).fileTree() - ue4ss_files = tree.find(game.GameDataUE4SSMods) + ue4ss_files = tree.find(data_ue4ss_mods) if isinstance(data_ue4ss_mods, str) else None if isinstance(ue4ss_files, mobase.IFileTree): for entry in ue4ss_files: if isinstance(entry, mobase.IFileTree): @@ -97,21 +101,16 @@ def _parse_mod_files(self): ) except FileNotFoundError: pass - - if game.ue4ssDirectory().exists(): - for dir_info in game.ue4ssDirectory().entryInfoList( - QDir.Filter.Dirs | QDir.Filter.NoDotAndDotDot - ): - if QFileInfo( - QDir(dir_info.absoluteFilePath()).absoluteFilePath( - "scripts/main.lua" - ) - ).exists(): - mod_list.add(dir_info.fileName()) - if QFileInfo( - QDir(dir_info.absoluteFilePath()).absoluteFilePath("enabled.txt") - ).exists(): - Path(dir_info.absoluteFilePath(), "enabled.txt").unlink() + if data_path and data_ue4ss_mods: + ue4ss_dir = QDir(data_path.absolutePath() + "/" + data_ue4ss_mods) + if ue4ss_dir.exists(): + for dir_info in ue4ss_dir.entryInfoList(QDir.Filter.Dirs | QDir.Filter.NoDotAndDotDot):# type: ignore + if QFileInfo(QDir(dir_info.absoluteFilePath()).absoluteFilePath("scripts/main.lua")).exists() or QFileInfo(QDir(dir_info.absoluteFilePath()).absoluteFilePath("dlls/main.dll")).exists(): + mod_list.add(dir_info.fileName()) + if QFileInfo( + QDir(dir_info.absoluteFilePath()).absoluteFilePath("enabled.txt") + ).exists(): + Path(dir_info.absoluteFilePath(), "enabled.txt").unlink() final_list = sorted(mod_list, key=cmp_to_key(self.sort_mods)) self._model.setStringList(final_list) From 3f4719df7ccf62f98c10ba3ea6379f9100cfb578 Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Tue, 14 Apr 2026 00:40:44 +0200 Subject: [PATCH 19/31] Ruff Formatting --- games/game_cassettebeasts.py | 82 ++++++++++------ games/game_crimeboss.py | 118 +++++++++++++++++------ games/game_emuvr.py | 58 ++++++++--- games/game_hitman3.py | 84 +++++++++++----- games/game_noita.py | 11 ++- games/game_ovkwalkingdead.py | 96 +++++++++++++----- games/game_pacificdrive.py | 98 ++++++++++++++----- games/game_payday1.py | 90 ++++++++++++----- games/game_payday2.py | 89 ++++++++++++----- games/game_payday3.py | 77 +++++++++++---- games/game_raid2.py | 41 +++++--- games/game_roadtovostok.py | 30 ++++-- games/game_silenthill2remake.py | 93 ++++++++++++++---- games/game_titanfall2.py | 48 ++++++--- games/game_zuma_deluxe.py | 15 ++- games/unreal_tabs/manage_paks/model.py | 24 ++++- games/unreal_tabs/manage_paks/widget.py | 14 ++- games/unreal_tabs/manage_ue4ss/model.py | 10 +- games/unreal_tabs/manage_ue4ss/widget.py | 38 ++++++-- 19 files changed, 818 insertions(+), 298 deletions(-) diff --git a/games/game_cassettebeasts.py b/games/game_cassettebeasts.py index 5f965c54..5257a2be 100644 --- a/games/game_cassettebeasts.py +++ b/games/game_cassettebeasts.py @@ -1,21 +1,25 @@ -from collections.abc import Mapping, Sequence -from datetime import datetime -from functools import cached_property -from io import BytesIO -from typing import Any -from pathlib import Path import json import math import os import shutil import struct import zlib +from collections.abc import Mapping, Sequence +from datetime import datetime +from functools import cached_property +from io import BytesIO +from pathlib import Path +from typing import Any -import mobase from PyQt6.QtCore import QDir, QFileInfo +import mobase + from ..basic_features import BasicLocalSavegames -from ..basic_features.basic_save_game_info import (BasicGameSaveGame,BasicGameSaveGameInfo) +from ..basic_features.basic_save_game_info import ( + BasicGameSaveGame, + BasicGameSaveGameInfo, +) from ..basic_game import BasicGame @@ -26,14 +30,17 @@ def json_get_me(value: Any, path: Sequence[str | int], /, default: Any) -> Any: value = value[part] return value + class CassetteBeastsModDataChecker(mobase.ModDataChecker): def __init__(self, organizer: mobase.IOrganizer): super().__init__() self.organizer: mobase.IOrganizer = organizer - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: for e in filetree: - if e.suffix().casefold() == "pck": + if e.suffix().casefold() == "pck": return mobase.ModDataChecker.VALID return mobase.ModDataChecker.FIXABLE @@ -45,9 +52,16 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: if mod_name == "": mod_name = branch.name() mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if not filetree.createOrphanTree("OrphanTree") and os.path.exists(mod_path) and branch.suffix().casefold() == "pck": + if ( + not filetree.createOrphanTree("OrphanTree") + and os.path.exists(mod_path) + and branch.suffix().casefold() == "pck" + ): os.makedirs(os.path.join(mod_path, GameDataPath), exist_ok=True) - shutil.move(os.path.join(mod_path, branch.name()), os.path.join(mod_path, GameDataPath, branch.name())) + shutil.move( + os.path.join(mod_path, branch.name()), + os.path.join(mod_path, GameDataPath, branch.name()), + ) treefixed = 1 else: if isinstance(branch, mobase.IFileTree): @@ -56,15 +70,17 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: filetree.move(e, GameDataPath, mobase.IFileTree.MERGE) treefixed = 1 elif branch.suffix().casefold() == "pck": - filetree.move(branch, GameDataPath, mobase.IFileTree.MERGE) - treefixed = 1 + filetree.move(branch, GameDataPath, mobase.IFileTree.MERGE) + treefixed = 1 if treefixed == 0: return None return filetree + class CassetteBlock: compressed_size: int = 0 - data: bytes = b'' + data: bytes = b"" + class CassetteBeastsSaveGame(BasicGameSaveGame): def __init__(self, filepath: Path): @@ -81,7 +97,7 @@ def __init__(self, filepath: Path): try: info = bytearray() data = bytes() - with open(filepath, 'rb') as infile: + with open(filepath, "rb") as infile: infile.read(4) blocksize, raw_size = struct.unpack("III", infile.read(12)) @@ -106,7 +122,7 @@ def __init__(self, filepath: Path): save_data = json.load(BytesIO(info)) except (OSError, struct.error, ValueError) as err: s = str(err) - self.errorMessage = ('{0}: {1}' if s else '{0}').format( + self.errorMessage = ("{0}: {1}" if s else "{0}").format( err.__class__.__name__, s ) return @@ -120,13 +136,14 @@ def __init__(self, filepath: Path): except OSError: pass else: - self.lastsave = "{0:d}-{1:02d}-{2:02d} at {3:02d}:{4:02d}:{5:02d}".format( - dt.year, dt.month, dt.day, - dt.hour, dt.minute, dt.second + self.lastsave = ( + "{0:d}-{1:02d}-{2:02d} at {3:02d}:{4:02d}:{5:02d}".format( + dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second + ) ) x = json_get_me(save_data, ["play_time"], None) if type(x) in (int, float): - a = [ 0, 0, 0, int(x * 10) ] + a = [0, 0, 0, int(x * 10)] a[2:4] = divmod(a[3], 10) a[1:3] = divmod(a[2], 60) a[0:2] = divmod(a[1], 60) @@ -147,6 +164,7 @@ def getLastSaved(self) -> str: def getPlayTime(self) -> str: return self.elapsed + def getMetadata(p: Path, save: mobase.ISaveGame) -> Mapping[str, str] | None: err = getattr(save, "errorMessage", "") if err: @@ -154,12 +172,12 @@ def getMetadata(p: Path, save: mobase.ISaveGame) -> Mapping[str, str] | None: # If this is our concrete save-game class, the type checker knows the methods. if isinstance(save, BasicGameSaveGame): - return + return { "Character": save.getName(), "Last Saved": save.getLastSaved(), "Play Time": save.getPlayTime(), - "Cheated": save.getCheated() + "Cheated": save.getCheated(), } else: return None @@ -182,10 +200,8 @@ def init(self, organizer: mobase.IOrganizer) -> bool: super().init(organizer) self.dataChecker = CassetteBeastsModDataChecker(organizer) self._register_feature(self.dataChecker) - self._register_feature(BasicLocalSavegames(QDir(self.GameSavesDirectory))) # type: ignore - self._register_feature( - BasicGameSaveGameInfo(None, getMetadata) - ) + self._register_feature(BasicLocalSavegames(QDir(self.GameSavesDirectory))) # type: ignore + self._register_feature(BasicGameSaveGameInfo(None, getMetadata)) return True def executables(self): @@ -218,7 +234,9 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: except AttributeError: efls = [] libs: set[str] = set() - tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = ( + self._organizer.virtualFileTree() + ) if type(tree) is not mobase.IFileTree: return efls for e in tree: @@ -226,7 +244,13 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: libs.add(relpath) exes = self.executables() - efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + efls = efls + [ + mobase.ExecutableForcedLoadSetting( + exe.binary().fileName(), lib + ).withEnabled(True) + for lib in libs + for exe in exes + ] return efls def iniFiles(self): diff --git a/games/game_crimeboss.py b/games/game_crimeboss.py index d030bb22..bf8f6106 100644 --- a/games/game_crimeboss.py +++ b/games/game_crimeboss.py @@ -1,21 +1,20 @@ import json import os import shutil -import mobase - from enum import IntEnum, auto -from pathlib import Path from functools import cached_property +from pathlib import Path + +from PyQt6.QtCore import QDir, QFileInfo +from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget +import mobase + +from ..basic_game import BasicGame from .unreal_tabs.constants import DEFAULT_UE4SS_MODS, UE4SSModInfo from .unreal_tabs.manage_paks.widget import PaksTabWidget from .unreal_tabs.manage_ue4ss.widget import UE4SSTabWidget -from ..basic_game import BasicGame - -from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget -from PyQt6.QtCore import QDir, QFileInfo - class Content(IntEnum): UCAS = auto() @@ -38,7 +37,10 @@ class CrimeBossModDataContent(mobase.ModDataContent): ] def getAllContents(self) -> list[mobase.ModDataContent.Content]: - return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + return [ + mobase.ModDataContent.Content(id, name, icon, *filter_only) + for id, name, icon, *filter_only in self.GAMECONTENTS + ] def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): @@ -88,11 +90,15 @@ def move_overwrite_merge(self, source: str, destination: str): def _Fix_Installed_Mod(self, mod: mobase.IModInterface): if not self.needsNameFix: return - GameDataNativeMods = getattr(self.organizer.managedGame(), "GameDataNativeMods", "") + GameDataNativeMods = getattr( + self.organizer.managedGame(), "GameDataNativeMods", "" + ) filetree: mobase.IFileTree = mod.fileTree() fixed = False modname = mod.name() - if filetree.exists(GameDataNativeMods + "/FOLDERNAME", mobase.IFileTree.DIRECTORY): + if filetree.exists( + GameDataNativeMods + "/FOLDERNAME", mobase.IFileTree.DIRECTORY + ): path = mod.absolutePath() old_path = os.path.join(path, GameDataNativeMods + "/FOLDERNAME") new_path = os.path.join(path, GameDataNativeMods + f"/{modname}") @@ -102,16 +108,26 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): return self.needsNameFix = False - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: - GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + GameDataUE4SSMods = getattr( + self.organizer.managedGame(), "GameDataUE4SSMods", "" + ) GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") - GameDataNativeMods = getattr(self.organizer.managedGame(), "GameDataNativeMods", "") + GameDataNativeMods = getattr( + self.organizer.managedGame(), "GameDataNativeMods", "" + ) GameDataMovies = getattr(self.organizer.managedGame(), "GameDataMovies", "") if filetree.exists(GameDataPakMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID - if filetree.exists(os.path.dirname(GameDataUE4SSMods), mobase.IFileTree.DIRECTORY): + if filetree.exists( + os.path.dirname(GameDataUE4SSMods), mobase.IFileTree.DIRECTORY + ): return mobase.ModDataChecker.VALID - if filetree.exists(GameDataNativeMods, mobase.IFileTree.DIRECTORY) and not filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): + if filetree.exists( + GameDataNativeMods, mobase.IFileTree.DIRECTORY + ) and not filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): return mobase.ModDataChecker.VALID if filetree.exists(GameDataMovies, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID @@ -136,13 +152,23 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): return retVal def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: - GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + "/" - GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") + "/" - GameDataNativeMods = getattr(self.organizer.managedGame(), "GameDataNativeMods", "") + "/" - GameDataMovies = getattr(self.organizer.managedGame(), "GameDataMovies", "") + "/" + GameDataUE4SSMods = ( + getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + "/" + ) + GameDataPakMods = ( + getattr(self.organizer.managedGame(), "GameDataPakMods", "") + "/" + ) + GameDataNativeMods = ( + getattr(self.organizer.managedGame(), "GameDataNativeMods", "") + "/" + ) + GameDataMovies = ( + getattr(self.organizer.managedGame(), "GameDataMovies", "") + "/" + ) treefixed = 0 if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): - treefixed = self.allMoveTo(filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/") + treefixed = self.allMoveTo( + filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/" + ) if treefixed == 1: return filetree if filetree.exists("Content", mobase.IFileTree.DIRECTORY): @@ -163,14 +189,32 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: if mod_name == "": mod_name = e.name() mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if not filetree.createOrphanTree("OrphanTree") and os.path.exists(mod_path): + if not filetree.createOrphanTree( + "OrphanTree" + ) and os.path.exists(mod_path): match e.suffix().casefold(): case "pak" | "utoc" | "ucas": - os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) + os.makedirs( + os.path.join(mod_path, GameDataPakMods), + exist_ok=True, + ) + shutil.move( + os.path.join(mod_path, e.name()), + os.path.join( + mod_path, GameDataPakMods, e.name() + ), + ) case "bk2": - os.makedirs(os.path.join(mod_path, GameDataMovies), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovies, e.name())) + os.makedirs( + os.path.join(mod_path, GameDataMovies), + exist_ok=True, + ) + shutil.move( + os.path.join(mod_path, e.name()), + os.path.join( + mod_path, GameDataMovies, e.name() + ), + ) case _: pass treefixed = 1 @@ -182,7 +226,11 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: case "pak" | "utoc" | "ucas": filetree.move(e, GameDataPakMods, mobase.IFileTree.MERGE) case "dll": - filetree.move(e, os.path.dirname(GameDataUE4SSMods) + "/", mobase.IFileTree.MERGE) + filetree.move( + e, + os.path.dirname(GameDataUE4SSMods) + "/", + mobase.IFileTree.MERGE, + ) case "bk2": filetree.move(e, GameDataMovies, mobase.IFileTree.MERGE) case _: @@ -205,7 +253,9 @@ class CrimeBossGame(BasicGame): GameDataUE4SSMods = "Binaries/Win64/Mods" GameDataNativeMods = "Mods" GameDataPakMods = "Content/Paks/~Mods" - GameDocumentsDirectory = "%USERPROFILE%/Saved Games/CrimeBoss/Steam/Saved/Config/WindowsNoEditor" + GameDocumentsDirectory = ( + "%USERPROFILE%/Saved Games/CrimeBoss/Steam/Saved/Config/WindowsNoEditor" + ) GameSaveExtension = "sav" _main_window: QMainWindow _ue4ss_tab: UE4SSTabWidget @@ -255,7 +305,9 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: except AttributeError: efls = [] libs: set[str] = set() - tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = ( + self._organizer.virtualFileTree() + ) if type(tree) is not mobase.IFileTree: return efls for e in tree: @@ -263,7 +315,13 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: libs.add(relpath) exes = self.executables() - efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + efls = efls + [ + mobase.ExecutableForcedLoadSetting( + exe.binary().fileName(), lib + ).withEnabled(True) + for lib in libs + for exe in exes + ] return efls def paksDirectory(self) -> QDir: diff --git a/games/game_emuvr.py b/games/game_emuvr.py index 744c4b78..f5f26c90 100644 --- a/games/game_emuvr.py +++ b/games/game_emuvr.py @@ -1,37 +1,48 @@ import os import shutil -import mobase - -from pathlib import Path from functools import cached_property - -from ..basic_game import BasicGame +from pathlib import Path from PyQt6.QtCore import QDir, QFileInfo +import mobase + +from ..basic_game import BasicGame + class EmuVRModDataChecker(mobase.ModDataChecker): def __init__(self, organizer: mobase.IOrganizer): super().__init__() self.organizer: mobase.IOrganizer = organizer - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: GameDataUGCMods = getattr(self.organizer.managedGame(), "GameDataUGCMods", "") if filetree.exists(GameDataUGCMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID return mobase.ModDataChecker.FIXABLE def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: - GameDataUGCMods = getattr(self.organizer.managedGame(), "GameDataUGCMods", "") + "/" + GameDataUGCMods = ( + getattr(self.organizer.managedGame(), "GameDataUGCMods", "") + "/" + ) treefixed = 0 for branch in filetree: mod_name = filetree.name() if mod_name == "": mod_name = branch.name() mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if not filetree.createOrphanTree("OrphanTree") and os.path.exists(mod_path) and branch.suffix().casefold() == "ugc": + if ( + not filetree.createOrphanTree("OrphanTree") + and os.path.exists(mod_path) + and branch.suffix().casefold() == "ugc" + ): os.makedirs(os.path.join(mod_path, GameDataUGCMods), exist_ok=True) - shutil.move(os.path.join(mod_path, branch.name()), os.path.join(mod_path, GameDataUGCMods, branch.name())) + shutil.move( + os.path.join(mod_path, branch.name()), + os.path.join(mod_path, GameDataUGCMods, branch.name()), + ) treefixed = 1 else: if isinstance(branch, mobase.IFileTree): @@ -71,10 +82,19 @@ def executables(self): "Emu VR", QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), ), - mobase.ExecutableInfo("Force SteamVR", QFileInfo(self.gameDirectory(), "Force SteamVR.exe")), - mobase.ExecutableInfo("Force Oculus", QFileInfo(self.gameDirectory(), "Force Oculus.exe")), - mobase.ExecutableInfo("Force Virtual Desktop Streamer", QFileInfo(self.gameDirectory(), "Force Virtual Desktop Streamer.exe")), - mobase.ExecutableInfo("Force Desktop", QFileInfo(self.gameDirectory(), "Force Desktop.exe")), + mobase.ExecutableInfo( + "Force SteamVR", QFileInfo(self.gameDirectory(), "Force SteamVR.exe") + ), + mobase.ExecutableInfo( + "Force Oculus", QFileInfo(self.gameDirectory(), "Force Oculus.exe") + ), + mobase.ExecutableInfo( + "Force Virtual Desktop Streamer", + QFileInfo(self.gameDirectory(), "Force Virtual Desktop Streamer.exe"), + ), + mobase.ExecutableInfo( + "Force Desktop", QFileInfo(self.gameDirectory(), "Force Desktop.exe") + ), ] def iniFiles(self): @@ -91,7 +111,9 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: except AttributeError: efls = [] libs: set[str] = set() - tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = ( + self._organizer.virtualFileTree() + ) if type(tree) is not mobase.IFileTree: return efls for e in tree: @@ -99,7 +121,13 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: libs.add(relpath) exes = self.executables() - efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + efls = efls + [ + mobase.ExecutableForcedLoadSetting( + exe.binary().fileName(), lib + ).withEnabled(True) + for lib in libs + for exe in exes + ] return efls def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): diff --git a/games/game_hitman3.py b/games/game_hitman3.py index 996ab66a..7003538d 100644 --- a/games/game_hitman3.py +++ b/games/game_hitman3.py @@ -1,15 +1,15 @@ +import json import os import shutil -import json -import mobase - -from pathlib import Path from functools import cached_property - -from ..basic_game import BasicGame +from pathlib import Path from PyQt6.QtCore import QDir, QFileInfo +import mobase + +from ..basic_game import BasicGame + class Hitman3ModDataChecker(mobase.ModDataChecker): def __init__(self, organizer: mobase.IOrganizer): @@ -37,11 +37,15 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): GameSMMPath = getattr(self.organizer.managedGame(), "GameSMMPath", "") filetree: mobase.IFileTree = mod.fileTree() fixed = False - if filetree.exists(GameSMMPath + "/Mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): + if filetree.exists( + GameSMMPath + "/Mods/FOLDERNAME", mobase.IFileTree.DIRECTORY + ): print("Found folder") path = mod.absolutePath() print(path) - json_path = os.path.join(path, GameSMMPath + "/Mods/FOLDERNAME/manifest.json") + json_path = os.path.join( + path, GameSMMPath + "/Mods/FOLDERNAME/manifest.json" + ) print(json_path) mod_data = json.load(open(json_path, encoding="utf-8")) modname = mod_data["id"] @@ -53,7 +57,9 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): return self.needsNameFix = False - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: if filetree.exists("Simple Mod Framework", mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID return mobase.ModDataChecker.FIXABLE @@ -66,7 +72,9 @@ def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): return True return False - def allMoveTo(self, sourcetree: mobase.IFileTree, targettree: mobase.IFileTree, toMoveTo: str): + def allMoveTo( + self, sourcetree: mobase.IFileTree, targettree: mobase.IFileTree, toMoveTo: str + ): entriesToMove: list[mobase.FileTreeEntry] = [] retVal = 0 for e in sourcetree: @@ -89,7 +97,9 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: if filetree.exists("manifest.json", mobase.IFileTree.FILE): print("Found manifest in root, moving to SMM folder") print(GameSMMPath + "/Mods/FOLDERNAME/") - treefixed = self.allMoveTo(filetree, filetree, GameSMMPath + "/Mods/FOLDERNAME/") + treefixed = self.allMoveTo( + filetree, filetree, GameSMMPath + "/Mods/FOLDERNAME/" + ) if treefixed == 1: self.needsNameFix = True elif len(filetree) == 1: @@ -97,7 +107,9 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: if firsttreelayer is not None: if firsttreelayer.exists("manifest.json", mobase.IFileTree.FILE): print(GameSMMPath + "/Mods/FOLDERNAME/") - treefixed = self.allMoveTo(firsttreelayer, filetree, GameSMMPath + "/Mods/FOLDERNAME/") + treefixed = self.allMoveTo( + firsttreelayer, filetree, GameSMMPath + "/Mods/FOLDERNAME/" + ) if treefixed == 1: self.needsNameFix = True if treefixed == 0: @@ -134,7 +146,9 @@ def update_smm_meta(self, mods: dict[str, mobase.ModState]): for e in subtree: if isinstance(e, mobase.IFileTree): if e.exists("manifest.json", mobase.IFileTree.FILE): - json_path = key.absolutePath() + "/" + e.path() + "/manifest.json" + json_path = ( + key.absolutePath() + "/" + e.path() + "/manifest.json" + ) mod_data = json.load(open(json_path, encoding="utf-8")) modname = mod_data["id"] if value == 35: @@ -147,13 +161,21 @@ def update_smm_meta(self, mods: dict[str, mobase.ModState]): config_json_content = bad_code if modname not in config_json_content: substr = "knownMods:[" - config_json_content = config_json_content.replace(substr, substr + "'" + modname + "',") + config_json_content = config_json_content.replace( + substr, substr + "'" + modname + "'," + ) substr = "loadOrder:[" - config_json_content = config_json_content.replace(substr, substr + "'" + modname + "',") + config_json_content = config_json_content.replace( + substr, substr + "'" + modname + "'," + ) substr = ",],modOptions" - config_json_content = config_json_content.replace(substr, "],modOptions") + config_json_content = config_json_content.replace( + substr, "],modOptions" + ) substr = ",],developer" - config_json_content = config_json_content.replace(substr, "],developer") + config_json_content = config_json_content.replace( + substr, "],developer" + ) with open(SMM_Config_Json, "w") as config_json: config_json.write(config_json_content) config_json.close() @@ -163,12 +185,20 @@ def update_smm_meta(self, mods: dict[str, mobase.ModState]): config_json_content = config_json.read() config_json.close() if modname in config_json_content: - config_json_content = config_json_content.replace("'" + modname + "',", "") - config_json_content = config_json_content.replace(",,", ",") + config_json_content = config_json_content.replace( + "'" + modname + "',", "" + ) + config_json_content = config_json_content.replace( + ",,", "," + ) substr = ",],modOptions" - config_json_content = config_json_content.replace(substr, "],modOptions") + config_json_content = config_json_content.replace( + substr, "],modOptions" + ) substr = ",],developer" - config_json_content = config_json_content.replace(substr, "],developer") + config_json_content = config_json_content.replace( + substr, "],developer" + ) with open(SMM_Config_Json, "w") as config_json: config_json.write(config_json_content) config_json.close() @@ -214,7 +244,9 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: except AttributeError: efls = [] libs: set[str] = set() - tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = ( + self._organizer.virtualFileTree() + ) if type(tree) is not mobase.IFileTree: return efls for e in tree: @@ -222,7 +254,13 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: libs.add(relpath) exes = self.executables() - efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + efls = efls + [ + mobase.ExecutableForcedLoadSetting( + exe.binary().fileName(), lib + ).withEnabled(True) + for lib in libs + for exe in exes + ] return efls def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): diff --git a/games/game_noita.py b/games/game_noita.py index ca2cac04..78b9a993 100644 --- a/games/game_noita.py +++ b/games/game_noita.py @@ -1,11 +1,12 @@ -from functools import cached_property -from pathlib import Path import os import shutil +from functools import cached_property +from pathlib import Path -import mobase from PyQt6.QtCore import QDir, QFileInfo +import mobase + from ..basic_game import BasicGame @@ -46,7 +47,9 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): return self.needsNameFix = False - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: if filetree.exists("mods", mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID return mobase.ModDataChecker.FIXABLE diff --git a/games/game_ovkwalkingdead.py b/games/game_ovkwalkingdead.py index b702d51e..050e2b26 100644 --- a/games/game_ovkwalkingdead.py +++ b/games/game_ovkwalkingdead.py @@ -1,14 +1,15 @@ -from enum import IntEnum, auto -from functools import cached_property -from pathlib import Path import json import os import shutil +from enum import IntEnum, auto +from functools import cached_property +from pathlib import Path -import mobase from PyQt6.QtCore import QDir, QFileInfo from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget +import mobase + from ..basic_game import BasicGame from .unreal_tabs.constants import DEFAULT_UE4SS_MODS, UE4SSModInfo from .unreal_tabs.manage_paks.widget import PaksTabWidget @@ -36,7 +37,10 @@ class OTWDModDataContent(mobase.ModDataContent): ] def getAllContents(self) -> list[mobase.ModDataContent.Content]: - return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + return [ + mobase.ModDataContent.Content(id, name, icon, *filter_only) + for id, name, icon, *filter_only in self.GAMECONTENTS + ] def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): @@ -81,10 +85,16 @@ def move_overwrite_merge(self, source: str, destination: str): self.move_overwrite_merge(s_item, d_item) os.rmdir(source) - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: - GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + GameDataUE4SSMods = getattr( + self.organizer.managedGame(), "GameDataUE4SSMods", "" + ) GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") - GameDataMovieMods = getattr(self.organizer.managedGame(), "GameDataMovieMods", "") + GameDataMovieMods = getattr( + self.organizer.managedGame(), "GameDataMovieMods", "" + ) if filetree.exists(GameDataPakMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID if filetree.exists(GameDataMovieMods, mobase.IFileTree.DIRECTORY): @@ -112,15 +122,25 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): return retVal def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: - GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + "/" - GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") + "/" - GameDataMovieMods = getattr(self.organizer.managedGame(), "GameDataMovieMods", "") + "/" + GameDataUE4SSMods = ( + getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + "/" + ) + GameDataPakMods = ( + getattr(self.organizer.managedGame(), "GameDataPakMods", "") + "/" + ) + GameDataMovieMods = ( + getattr(self.organizer.managedGame(), "GameDataMovieMods", "") + "/" + ) treefixed = 0 if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): - treefixed = self.allMoveTo(filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/") + treefixed = self.allMoveTo( + filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/" + ) if treefixed == 1: return filetree - if filetree.exists("Scripts", mobase.IFileTree.DIRECTORY) or filetree.exists("dlls", mobase.IFileTree.DIRECTORY): + if filetree.exists("Scripts", mobase.IFileTree.DIRECTORY) or filetree.exists( + "dlls", mobase.IFileTree.DIRECTORY + ): treefixed = self.allMoveTo(filetree, GameDataUE4SSMods) if treefixed == 1: return filetree @@ -135,14 +155,32 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: if mod_name == "": mod_name = e.name() mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if not filetree.createOrphanTree("OrphanTree") and os.path.exists(mod_path): + if not filetree.createOrphanTree( + "OrphanTree" + ) and os.path.exists(mod_path): match e.suffix().casefold(): case "pak" | "utoc" | "ucas": - os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) + os.makedirs( + os.path.join(mod_path, GameDataPakMods), + exist_ok=True, + ) + shutil.move( + os.path.join(mod_path, e.name()), + os.path.join( + mod_path, GameDataPakMods, e.name() + ), + ) case "bk2": - os.makedirs(os.path.join(mod_path, GameDataMovieMods), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovieMods, e.name())) + os.makedirs( + os.path.join(mod_path, GameDataMovieMods), + exist_ok=True, + ) + shutil.move( + os.path.join(mod_path, e.name()), + os.path.join( + mod_path, GameDataMovieMods, e.name() + ), + ) case _: pass treefixed = 1 @@ -154,7 +192,11 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: case "pak" | "utoc" | "ucas": filetree.move(e, GameDataPakMods, mobase.IFileTree.MERGE) case "dll": - filetree.move(e, os.path.dirname(GameDataUE4SSMods) + "/", mobase.IFileTree.MERGE) + filetree.move( + e, + os.path.dirname(GameDataUE4SSMods) + "/", + mobase.IFileTree.MERGE, + ) case "bk2": filetree.move(e, GameDataMovieMods, mobase.IFileTree.MERGE) case _: @@ -177,7 +219,9 @@ class OTWDGame(BasicGame): GameDataUE4SSMods = "Binaries/Win64/Mods" GameDataPakMods = "Content/Paks/~Mods" GameDataMovieMods = "Content/Movies" - GameDocumentsDirectory = "%USERPROFILE%/AppData/Local/OTWD/Saved/Config/WindowsClient" + GameDocumentsDirectory = ( + "%USERPROFILE%/AppData/Local/OTWD/Saved/Config/WindowsClient" + ) GameSaveExtension = "sav" _main_window: QMainWindow _ue4ss_tab: UE4SSTabWidget @@ -227,7 +271,9 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: except AttributeError: efls = [] libs: set[str] = set() - tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = ( + self._organizer.virtualFileTree() + ) if type(tree) is not mobase.IFileTree: return efls for e in tree: @@ -235,7 +281,13 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: libs.add(relpath) exes = self.executables() - efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + efls = efls + [ + mobase.ExecutableForcedLoadSetting( + exe.binary().fileName(), lib + ).withEnabled(True) + for lib in libs + for exe in exes + ] return efls def write_default_mods(self, profile: QDir): diff --git a/games/game_pacificdrive.py b/games/game_pacificdrive.py index 90fc1873..218ec86b 100644 --- a/games/game_pacificdrive.py +++ b/games/game_pacificdrive.py @@ -1,14 +1,15 @@ -from enum import IntEnum, auto -from functools import cached_property -from pathlib import Path import json import os import shutil +from enum import IntEnum, auto +from functools import cached_property +from pathlib import Path -import mobase from PyQt6.QtCore import QDir, QFileInfo from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget +import mobase + from ..basic_game import BasicGame from .unreal_tabs.constants import DEFAULT_UE4SS_MODS, UE4SSModInfo from .unreal_tabs.manage_paks.widget import PaksTabWidget @@ -36,7 +37,10 @@ class PacificDriveModDataContent(mobase.ModDataContent): ] def getAllContents(self) -> list[mobase.ModDataContent.Content]: - return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + return [ + mobase.ModDataContent.Content(id, name, icon, *filter_only) + for id, name, icon, *filter_only in self.GAMECONTENTS + ] def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): @@ -68,7 +72,7 @@ def __init__(self, organizer: mobase.IOrganizer): super().__init__() self.organizer: mobase.IOrganizer = organizer - def move_overwrite_merge(self, source: str, destination: str): + def move_overwrite_merge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -81,10 +85,16 @@ def move_overwrite_merge(self, source: str, destination: str): self.move_overwrite_merge(s_item, d_item) os.rmdir(source) - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: - GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + GameDataUE4SSMods = getattr( + self.organizer.managedGame(), "GameDataUE4SSMods", "" + ) GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") - GameDataMovieMods = getattr(self.organizer.managedGame(), "GameDataMovieMods", "") + GameDataMovieMods = getattr( + self.organizer.managedGame(), "GameDataMovieMods", "" + ) if filetree.exists(GameDataPakMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID if filetree.exists(GameDataMovieMods, mobase.IFileTree.DIRECTORY): @@ -112,15 +122,25 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): return retVal def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: - GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + "/" - GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") + "/" - GameDataMovieMods = getattr(self.organizer.managedGame(), "GameDataMovieMods", "") + "/" + GameDataUE4SSMods = ( + getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + "/" + ) + GameDataPakMods = ( + getattr(self.organizer.managedGame(), "GameDataPakMods", "") + "/" + ) + GameDataMovieMods = ( + getattr(self.organizer.managedGame(), "GameDataMovieMods", "") + "/" + ) treefixed = 0 if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): - treefixed = self.allMoveTo(filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/") + treefixed = self.allMoveTo( + filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/" + ) if treefixed == 1: return filetree - if filetree.exists("Scripts", mobase.IFileTree.DIRECTORY) or filetree.exists("dlls", mobase.IFileTree.DIRECTORY): + if filetree.exists("Scripts", mobase.IFileTree.DIRECTORY) or filetree.exists( + "dlls", mobase.IFileTree.DIRECTORY + ): treefixed = self.allMoveTo(filetree, GameDataUE4SSMods) if treefixed == 1: return filetree @@ -135,14 +155,32 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: if mod_name == "": mod_name = e.name() mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if not filetree.createOrphanTree("OrphanTree") and os.path.exists(mod_path): + if not filetree.createOrphanTree( + "OrphanTree" + ) and os.path.exists(mod_path): match e.suffix().casefold(): case "pak" | "utoc" | "ucas": - os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) + os.makedirs( + os.path.join(mod_path, GameDataPakMods), + exist_ok=True, + ) + shutil.move( + os.path.join(mod_path, e.name()), + os.path.join( + mod_path, GameDataPakMods, e.name() + ), + ) case "bk2": - os.makedirs(os.path.join(mod_path, GameDataMovieMods), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovieMods, e.name())) + os.makedirs( + os.path.join(mod_path, GameDataMovieMods), + exist_ok=True, + ) + shutil.move( + os.path.join(mod_path, e.name()), + os.path.join( + mod_path, GameDataMovieMods, e.name() + ), + ) case _: pass treefixed = 1 @@ -154,7 +192,11 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: case "pak" | "utoc" | "ucas": filetree.move(e, GameDataPakMods, mobase.IFileTree.MERGE) case "dll": - filetree.move(e, os.path.dirname(GameDataUE4SSMods) + "/", mobase.IFileTree.MERGE) + filetree.move( + e, + os.path.dirname(GameDataUE4SSMods) + "/", + mobase.IFileTree.MERGE, + ) case "bk2": filetree.move(e, GameDataMovieMods, mobase.IFileTree.MERGE) case _: @@ -178,7 +220,9 @@ class PacificDriveGame(BasicGame): GameDataUE4SSMods = "Binaries/Win64/Mods" GameDataPakMods = "Content/Paks/~Mods" GameDataMovieMods = "Content/Movies" - GameDocumentsDirectory = "%USERPROFILE%/AppData/Local/PenDriverPro/Saved/Config/WindowsNoEditor" + GameDocumentsDirectory = ( + "%USERPROFILE%/AppData/Local/PenDriverPro/Saved/Config/WindowsNoEditor" + ) GameSaveExtension = "sav" _main_window: QMainWindow _ue4ss_tab: UE4SSTabWidget @@ -228,7 +272,9 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: except AttributeError: efls = [] libs: set[str] = set() - tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = ( + self._organizer.virtualFileTree() + ) if type(tree) is not mobase.IFileTree: return efls for e in tree: @@ -236,7 +282,13 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: libs.add(relpath) exes = self.executables() - efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + efls = efls + [ + mobase.ExecutableForcedLoadSetting( + exe.binary().fileName(), lib + ).withEnabled(True) + for lib in libs + for exe in exes + ] return efls def write_default_mods(self, profile: QDir): diff --git a/games/game_payday1.py b/games/game_payday1.py index f4b59e19..b4efb18d 100644 --- a/games/game_payday1.py +++ b/games/game_payday1.py @@ -1,14 +1,16 @@ +import os +import shutil from enum import IntEnum, auto from functools import cached_property from pathlib import Path -import os -import shutil -import mobase from PyQt6.QtCore import QDir, QFileInfo +import mobase + from ..basic_game import BasicGame + class Content(IntEnum): TEXTURE = auto() MESH = auto() @@ -30,7 +32,10 @@ class Payday1ModDataContent(mobase.ModDataContent): ] def getAllContents(self) -> list[mobase.ModDataContent.Content]: - return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + return [ + mobase.ModDataContent.Content(id, name, icon, *filter_only) + for id, name, icon, *filter_only in self.GAMECONTENTS + ] def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): @@ -88,7 +93,9 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): new_path = os.path.join(path, f"mods/{modname}") self.move_overwrite_merge(old_path, new_path) fixed = True - elif filetree.exists("assets/mod_overrides/FOLDERNAME/", mobase.IFileTree.DIRECTORY): + elif filetree.exists( + "assets/mod_overrides/FOLDERNAME/", mobase.IFileTree.DIRECTORY + ): path = mod.absolutePath() old_path = os.path.join(path, "assets/mod_overrides/FOLDERNAME") new_path = os.path.join(path, f"assets/mod_overrides/{modname}") @@ -104,7 +111,9 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): return self.needsNameFix = False - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: if filetree.exists("assets/mod_overrides", mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID if filetree.exists("mods", mobase.IFileTree.DIRECTORY): @@ -157,7 +166,9 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: filetree.move(firsttreelayer, "maps/", mobase.IFileTree.MERGE) treefixed = 1 else: - filetree.move(firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE) + filetree.move( + firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE + ) treefixed = 1 elif filetree.exists("main.xml", mobase.IFileTree.FILE): if filetree.exists("levels", mobase.IFileTree.DIRECTORY): @@ -165,31 +176,55 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: if treefixed == 1: self.needsNameFix = True else: - treefixed = self.allMoveTo(filetree, "assets/mod_overrides/FOLDERNAME/") + treefixed = self.allMoveTo( + filetree, "assets/mod_overrides/FOLDERNAME/" + ) if treefixed == 1: self.needsNameFix = True elif secondtreelayer is not None: if secondtreelayer.exists("mod.txt", mobase.IFileTree.FILE): - filetree.move(secondtreelayer, firsttreelayer.path("/"), mobase.IFileTree.REPLACE) + filetree.move( + secondtreelayer, + firsttreelayer.path("/"), + mobase.IFileTree.REPLACE, + ) filetree.move(firsttreelayer, "mods/", mobase.IFileTree.MERGE) treefixed = 1 elif secondtreelayer.exists("main.xml", mobase.IFileTree.FILE): if filetree.exists("levels", mobase.IFileTree.DIRECTORY): - filetree.move(secondtreelayer, firsttreelayer.path("/"), mobase.IFileTree.REPLACE) + filetree.move( + secondtreelayer, + firsttreelayer.path("/"), + mobase.IFileTree.REPLACE, + ) filetree.move(firsttreelayer, "maps/", mobase.IFileTree.MERGE) treefixed = 1 else: - filetree.move(secondtreelayer, firsttreelayer.path("/"), mobase.IFileTree.REPLACE) - filetree.move(firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE) + filetree.move( + secondtreelayer, + firsttreelayer.path("/"), + mobase.IFileTree.REPLACE, + ) + filetree.move( + firsttreelayer, + "assets/mod_overrides/", + mobase.IFileTree.MERGE, + ) treefixed = 1 if treefixed == 0: if len(filetree) == 1: - filetree.move(firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE) + filetree.move( + firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE + ) treefixed = 1 else: for e in filetree: if e.path("/").count("/") == 0: - filetree.move(e, "assets/mod_overrides/FOLDERNAME/", mobase.IFileTree.MERGE) + filetree.move( + e, + "assets/mod_overrides/FOLDERNAME/", + mobase.IFileTree.MERGE, + ) treefixed = 1 self.needsNameFix = True if treefixed == 0: @@ -207,7 +242,12 @@ class Payday1Game(BasicGame): GameBinary = "payday_win32_release.exe" GameDataPath = "%GAME_PATH%" GameDocumentsDirectory = "%USERPROFILE%/AppData/Local/PAYDAY" - _forced_libraries = ["IPHLPAPI.dll", "WSOCK32.dll" , "DINPUT8.dll", "PDTHModOverrides.dll"] + _forced_libraries = [ + "IPHLPAPI.dll", + "WSOCK32.dll", + "DINPUT8.dll", + "PDTHModOverrides.dll", + ] def init(self, organizer: mobase.IOrganizer) -> bool: super().init(organizer) @@ -225,9 +265,7 @@ def executables(self): ) ] - def dll_copy( - self, mods: dict[str, mobase.ModState] - ): + def dll_copy(self, mods: dict[str, mobase.ModState]): game_path = self.dataDirectory().absolutePath() + "/" @@ -236,12 +274,12 @@ def dll_copy( tree = key.fileTree() for e in tree: if e.name() in self._forced_libraries: - #add file + # add file file_path_source = key.absolutePath() + "/" + e.path() file_path_target = game_path + e.name() if value == 35: shutil.copyfile(file_path_source, file_path_target) - #remove file + # remove file if value == 33: if os.path.exists(file_path_target): os.remove(file_path_target) @@ -257,7 +295,9 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: except AttributeError: efls = [] libs: set[str] = set() - tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = ( + self._organizer.virtualFileTree() + ) if type(tree) is not mobase.IFileTree: return efls for e in tree: @@ -265,7 +305,13 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: libs.add(relpath) exes = self.executables() - efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + efls = efls + [ + mobase.ExecutableForcedLoadSetting( + exe.binary().fileName(), lib + ).withEnabled(True) + for lib in libs + for exe in exes + ] return efls def iniFiles(self): diff --git a/games/game_payday2.py b/games/game_payday2.py index e500d0c8..5efe083e 100644 --- a/games/game_payday2.py +++ b/games/game_payday2.py @@ -1,14 +1,16 @@ +import os +import shutil from enum import IntEnum, auto from functools import cached_property from pathlib import Path -import os -import shutil -import mobase from PyQt6.QtCore import QDir, QFileInfo +import mobase + from ..basic_game import BasicGame + class Content(IntEnum): TEXTURE = auto() MESH = auto() @@ -30,8 +32,10 @@ class Payday2ModDataContent(mobase.ModDataContent): ] def getAllContents(self) -> list[mobase.ModDataContent.Content]: - return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] - + return [ + mobase.ModDataContent.Content(id, name, icon, *filter_only) + for id, name, icon, *filter_only in self.GAMECONTENTS + ] def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): @@ -89,7 +93,9 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): new_path = os.path.join(path, f"mods/{modname}") self.move_overwrite_merge(old_path, new_path) fixed = True - elif filetree.exists("assets/mod_overrides/FOLDERNAME", mobase.IFileTree.DIRECTORY): + elif filetree.exists( + "assets/mod_overrides/FOLDERNAME", mobase.IFileTree.DIRECTORY + ): path = mod.absolutePath() old_path = os.path.join(path, "assets/mod_overrides/FOLDERNAME") new_path = os.path.join(path, f"assets/mod_overrides/{modname}") @@ -105,7 +111,9 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): return self.needsNameFix = False - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: if filetree.exists("assets/mod_overrides", mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID if filetree.exists("mods", mobase.IFileTree.DIRECTORY): @@ -159,7 +167,9 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: filetree.move(firsttreelayer, "maps/", mobase.IFileTree.MERGE) treefixed = 1 else: - filetree.move(firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE) + filetree.move( + firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE + ) treefixed = 1 elif filetree.exists("main.xml", mobase.IFileTree.FILE): if filetree.exists("levels", mobase.IFileTree.DIRECTORY): @@ -167,31 +177,55 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: if treefixed == 1: self.needsNameFix = True else: - treefixed = self.allMoveTo(filetree, "assets/mod_overrides/FOLDERNAME/") + treefixed = self.allMoveTo( + filetree, "assets/mod_overrides/FOLDERNAME/" + ) if treefixed == 1: self.needsNameFix = True elif secondtreelayer is not None: if secondtreelayer.exists("mod.txt", mobase.IFileTree.FILE): - filetree.move(secondtreelayer, firsttreelayer.path("/"), mobase.IFileTree.REPLACE) + filetree.move( + secondtreelayer, + firsttreelayer.path("/"), + mobase.IFileTree.REPLACE, + ) filetree.move(firsttreelayer, "mods/", mobase.IFileTree.MERGE) treefixed = 1 elif secondtreelayer.exists("main.xml", mobase.IFileTree.FILE): if filetree.exists("levels", mobase.IFileTree.DIRECTORY): - filetree.move(secondtreelayer, firsttreelayer.path("/"), mobase.IFileTree.REPLACE) + filetree.move( + secondtreelayer, + firsttreelayer.path("/"), + mobase.IFileTree.REPLACE, + ) filetree.move(firsttreelayer, "maps/", mobase.IFileTree.MERGE) treefixed = 1 else: - filetree.move(secondtreelayer, firsttreelayer.path("/"), mobase.IFileTree.REPLACE) - filetree.move(firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE) + filetree.move( + secondtreelayer, + firsttreelayer.path("/"), + mobase.IFileTree.REPLACE, + ) + filetree.move( + firsttreelayer, + "assets/mod_overrides/", + mobase.IFileTree.MERGE, + ) treefixed = 1 if treefixed == 0: if len(filetree) == 1: - filetree.move(firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE) + filetree.move( + firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE + ) treefixed = 1 else: for e in filetree: if e.path("/").count("/") == 0: - filetree.move(e, "assets/mod_overrides/FOLDERNAME/", mobase.IFileTree.MERGE) + filetree.move( + e, + "assets/mod_overrides/FOLDERNAME/", + mobase.IFileTree.MERGE, + ) treefixed = 1 self.needsNameFix = True if treefixed == 0: @@ -226,12 +260,13 @@ def executables(self): "Payday 2", QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), ), - mobase.ExecutableInfo("Payday 2 VR", QFileInfo(self.gameDirectory(), "payday2_win32_release_vr.exe")), + mobase.ExecutableInfo( + "Payday 2 VR", + QFileInfo(self.gameDirectory(), "payday2_win32_release_vr.exe"), + ), ] - def dll_copy( - self, mods: dict[str, mobase.ModState] - ): + def dll_copy(self, mods: dict[str, mobase.ModState]): game_path = self.dataDirectory().absolutePath() + "/" @@ -240,12 +275,12 @@ def dll_copy( tree = key.fileTree() for e in tree: if e.name() in self._forced_libraries: - #add file + # add file file_path_source = key.absolutePath() + "/" + e.path() file_path_target = game_path + e.name() if value == 35: shutil.copyfile(file_path_source, file_path_target) - #remove file + # remove file if value == 33: if os.path.exists(file_path_target): os.remove(file_path_target) @@ -261,7 +296,9 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: except AttributeError: efls = [] libs: set[str] = set() - tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = ( + self._organizer.virtualFileTree() + ) if type(tree) is not mobase.IFileTree: return efls for e in tree: @@ -269,7 +306,13 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: libs.add(relpath) exes = self.executables() - efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + efls = efls + [ + mobase.ExecutableForcedLoadSetting( + exe.binary().fileName(), lib + ).withEnabled(True) + for lib in libs + for exe in exes + ] return efls def iniFiles(self): diff --git a/games/game_payday3.py b/games/game_payday3.py index 91a6a496..a94afebf 100644 --- a/games/game_payday3.py +++ b/games/game_payday3.py @@ -1,21 +1,20 @@ import json import os import shutil -import mobase - from enum import IntEnum, auto -from pathlib import Path from functools import cached_property +from pathlib import Path + +from PyQt6.QtCore import QDir, QFileInfo +from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget +import mobase + +from ..basic_game import BasicGame from .unreal_tabs.constants import DEFAULT_UE4SS_MODS, UE4SSModInfo from .unreal_tabs.manage_paks.widget import PaksTabWidget from .unreal_tabs.manage_ue4ss.widget import UE4SSTabWidget -from ..basic_game import BasicGame - -from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget -from PyQt6.QtCore import QDir, QFileInfo - class Content(IntEnum): UCAS = auto() @@ -87,10 +86,16 @@ def move_overwrite_merge(self, source: str, destination: str): self.move_overwrite_merge(s_item, d_item) os.rmdir(source) - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: - GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + GameDataUE4SSMods = getattr( + self.organizer.managedGame(), "GameDataUE4SSMods", "" + ) GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") - GameDataMovieMods = getattr(self.organizer.managedGame(), "GameDataMovieMods", "") + GameDataMovieMods = getattr( + self.organizer.managedGame(), "GameDataMovieMods", "" + ) if filetree.exists(GameDataPakMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID if filetree.exists(GameDataMovieMods, mobase.IFileTree.DIRECTORY): @@ -118,9 +123,15 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): return retVal def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: - GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + "/" - GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") + "/" - GameDataMovieMods = getattr(self.organizer.managedGame(), "GameDataMovieMods", "") + "/" + GameDataUE4SSMods = ( + getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + "/" + ) + GameDataPakMods = ( + getattr(self.organizer.managedGame(), "GameDataPakMods", "") + "/" + ) + GameDataMovieMods = ( + getattr(self.organizer.managedGame(), "GameDataMovieMods", "") + "/" + ) treefixed = 0 if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): treefixed = self.allMoveTo( @@ -145,14 +156,32 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: if mod_name == "": mod_name = e.name() mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if not filetree.createOrphanTree("OrphanTree") and os.path.exists(mod_path): + if not filetree.createOrphanTree( + "OrphanTree" + ) and os.path.exists(mod_path): match e.suffix().casefold(): case "pak" | "utoc" | "ucas": - os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) + os.makedirs( + os.path.join(mod_path, GameDataPakMods), + exist_ok=True, + ) + shutil.move( + os.path.join(mod_path, e.name()), + os.path.join( + mod_path, GameDataPakMods, e.name() + ), + ) case "bk2": - os.makedirs(os.path.join(mod_path, GameDataMovieMods), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovieMods, e.name())) + os.makedirs( + os.path.join(mod_path, GameDataMovieMods), + exist_ok=True, + ) + shutil.move( + os.path.join(mod_path, e.name()), + os.path.join( + mod_path, GameDataMovieMods, e.name() + ), + ) case _: pass treefixed = 1 @@ -164,7 +193,11 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: case "pak" | "utoc" | "ucas": filetree.move(e, GameDataPakMods, mobase.IFileTree.MERGE) case "dll": - filetree.move(e,os.path.dirname(GameDataUE4SSMods) + "/",mobase.IFileTree.MERGE) + filetree.move( + e, + os.path.dirname(GameDataUE4SSMods) + "/", + mobase.IFileTree.MERGE, + ) case "bk2": filetree.move(e, GameDataMovieMods, mobase.IFileTree.MERGE) case _: @@ -187,7 +220,9 @@ class Payday3Game(BasicGame): GameDataUE4SSMods = "Binaries/Win64/Mods" GameDataPakMods = "Content/Paks/~Mods" GameDataMovieMods = "Content/Movies" - GameDocumentsDirectory = "%USERPROFILE%/AppData/Local/PAYDAY3/Saved/Config/WindowsClient" + GameDocumentsDirectory = ( + "%USERPROFILE%/AppData/Local/PAYDAY3/Saved/Config/WindowsClient" + ) GameSaveExtension = "sav" _main_window: QMainWindow _ue4ss_tab: UE4SSTabWidget diff --git a/games/game_raid2.py b/games/game_raid2.py index d52ae603..1869911e 100644 --- a/games/game_raid2.py +++ b/games/game_raid2.py @@ -1,15 +1,15 @@ import os import shutil -import mobase - from enum import IntEnum, auto -from pathlib import Path from functools import cached_property - -from ..basic_game import BasicGame +from pathlib import Path from PyQt6.QtCore import QDir, QFileInfo +import mobase + +from ..basic_game import BasicGame + class Content(IntEnum): TEXTURE = auto() @@ -32,7 +32,10 @@ class RaidWW2ModDataContent(mobase.ModDataContent): ] def getAllContents(self) -> list[mobase.ModDataContent.Content]: - return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + return [ + mobase.ModDataContent.Content(id, name, icon, *filter_only) + for id, name, icon, *filter_only in self.GAMECONTENTS + ] def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): @@ -94,7 +97,9 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): return self.needsNameFix = False - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: if len(filetree) == 1: return mobase.ModDataChecker.VALID return mobase.ModDataChecker.FIXABLE @@ -152,9 +157,7 @@ def executables(self): ) ] - def dll_copy( - self, mods: dict[str, mobase.ModState] - ): + def dll_copy(self, mods: dict[str, mobase.ModState]): game_path = self.dataDirectory().absolutePath() + "/" @@ -163,12 +166,12 @@ def dll_copy( tree = key.fileTree() for e in tree: if e.name() in self._forced_libraries: - #add file + # add file file_path_source = key.absolutePath() + "/" + e.path() file_path_target = game_path + e.name() if value == 35: shutil.copyfile(file_path_source, file_path_target) - #remove file + # remove file if value == 33: if os.path.exists(file_path_target): os.remove(file_path_target) @@ -184,7 +187,9 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: except AttributeError: efls = [] libs: set[str] = set() - tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = ( + self._organizer.virtualFileTree() + ) if type(tree) is not mobase.IFileTree: return efls for e in tree: @@ -192,7 +197,13 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: libs.add(relpath) exes = self.executables() - efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + efls = efls + [ + mobase.ExecutableForcedLoadSetting( + exe.binary().fileName(), lib + ).withEnabled(True) + for lib in libs + for exe in exes + ] return efls def iniFiles(self): @@ -205,4 +216,4 @@ def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): if not modsDirectory.exists(): os.makedirs(modsDirectory.absolutePath()) - super().initializeProfile(directory, settings) \ No newline at end of file + super().initializeProfile(directory, settings) diff --git a/games/game_roadtovostok.py b/games/game_roadtovostok.py index 9e4780fd..1df7db7f 100644 --- a/games/game_roadtovostok.py +++ b/games/game_roadtovostok.py @@ -1,19 +1,25 @@ import os import shutil -import mobase from PyQt6.QtCore import QDir, QFileInfo +import mobase + from ..basic_game import BasicGame + class RoadToVostokModDataChecker(mobase.ModDataChecker): def __init__(self, organizer: mobase.IOrganizer): super().__init__() self.organizer: mobase.IOrganizer = organizer - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: - if filetree.exists("mods", mobase.IFileTree.DIRECTORY) and not filetree.exists("mod.txt", mobase.IFileTree.FILE): + if filetree.exists("mods", mobase.IFileTree.DIRECTORY) and not filetree.exists( + "mod.txt", mobase.IFileTree.FILE + ): return mobase.ModDataChecker.VALID for e in filetree: if e.isFile() and e.suffix().casefold() == "pck": @@ -29,9 +35,16 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: if mod_name == "": mod_name = branch.name() mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if not filetree.createOrphanTree("OrphanTree") and os.path.exists(mod_path) and branch.suffix().casefold() == "zip": + if ( + not filetree.createOrphanTree("OrphanTree") + and os.path.exists(mod_path) + and branch.suffix().casefold() == "zip" + ): os.makedirs(os.path.join(mod_path, GameModsPath), exist_ok=True) - shutil.move(os.path.join(mod_path, branch.name()), os.path.join(mod_path, GameModsPath, branch.name())) + shutil.move( + os.path.join(mod_path, branch.name()), + os.path.join(mod_path, GameModsPath, branch.name()), + ) treefixed = 1 if treefixed == 0: @@ -40,9 +53,8 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: class RoadToVostokGame(BasicGame): - Name = "Road to Vostok Support Plugin" - Author = "modworkshop" + Author = "modworkshop" Version = "1" GameName = "Road to Vostok" GameShortName = "road-to-vostok" @@ -50,7 +62,9 @@ class RoadToVostokGame(BasicGame): GameBinary = "RTV.exe" GameDataPath = "%GAME_PATH%" GameModsPath = "mods" - GameDocumentsDirectory = "%USERPROFILE%/AppData/Local/Godot/app_userdata/Road to Vostok" + GameDocumentsDirectory = ( + "%USERPROFILE%/AppData/Local/Godot/app_userdata/Road to Vostok" + ) GameSaveExtension = "tres" def init(self, organizer: mobase.IOrganizer) -> bool: diff --git a/games/game_silenthill2remake.py b/games/game_silenthill2remake.py index b3dcf8d8..83fe8d4f 100644 --- a/games/game_silenthill2remake.py +++ b/games/game_silenthill2remake.py @@ -1,19 +1,21 @@ -from enum import IntEnum, auto -from functools import cached_property -from pathlib import Path import json import os import shutil +from enum import IntEnum, auto +from functools import cached_property +from pathlib import Path -import mobase from PyQt6.QtCore import QDir, QFileInfo from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget +import mobase + from ..basic_game import BasicGame from .unreal_tabs.constants import DEFAULT_UE4SS_MODS, UE4SSModInfo from .unreal_tabs.manage_paks.widget import PaksTabWidget from .unreal_tabs.manage_ue4ss.widget import UE4SSTabWidget + class Content(IntEnum): UCAS = auto() UTOC = auto() @@ -35,7 +37,10 @@ class SilentHill2ModDataContent(mobase.ModDataContent): ] def getAllContents(self) -> list[mobase.ModDataContent.Content]: - return [mobase.ModDataContent.Content(id, name, icon, *filter_only) for id, name, icon, *filter_only in self.GAMECONTENTS] + return [ + mobase.ModDataContent.Content(id, name, icon, *filter_only) + for id, name, icon, *filter_only in self.GAMECONTENTS + ] def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): @@ -80,10 +85,16 @@ def move_overwrite_merge(self, source: str, destination: str): self.move_overwrite_merge(s_item, d_item) os.rmdir(source) - def dataLooksValid(self, filetree: mobase.IFileTree) -> mobase.ModDataChecker.CheckReturn: - GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + GameDataUE4SSMods = getattr( + self.organizer.managedGame(), "GameDataUE4SSMods", "" + ) GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") - GameDataMovieMods = getattr(self.organizer.managedGame(), "GameDataMovieMods", "") + GameDataMovieMods = getattr( + self.organizer.managedGame(), "GameDataMovieMods", "" + ) if filetree.exists(GameDataPakMods, mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID if filetree.exists(GameDataMovieMods, mobase.IFileTree.DIRECTORY): @@ -111,15 +122,25 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): return retVal def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: - GameDataUE4SSMods = getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + "/" - GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") + "/" - GameDataMovieMods = getattr(self.organizer.managedGame(), "GameDataMovieMods", "") + "/" + GameDataUE4SSMods = ( + getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + "/" + ) + GameDataPakMods = ( + getattr(self.organizer.managedGame(), "GameDataPakMods", "") + "/" + ) + GameDataMovieMods = ( + getattr(self.organizer.managedGame(), "GameDataMovieMods", "") + "/" + ) treefixed = 0 if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): - treefixed = self.allMoveTo(filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/") + treefixed = self.allMoveTo( + filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/" + ) if treefixed == 1: return filetree - if filetree.exists("Scripts", mobase.IFileTree.DIRECTORY) or filetree.exists("dlls", mobase.IFileTree.DIRECTORY): + if filetree.exists("Scripts", mobase.IFileTree.DIRECTORY) or filetree.exists( + "dlls", mobase.IFileTree.DIRECTORY + ): treefixed = self.allMoveTo(filetree, GameDataUE4SSMods) if treefixed == 1: return filetree @@ -134,14 +155,32 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: if mod_name == "": mod_name = e.name() mod_path = os.path.join(self.organizer.modsPath(), mod_name) - if not filetree.createOrphanTree("OrphanTree") and os.path.exists(mod_path): + if not filetree.createOrphanTree( + "OrphanTree" + ) and os.path.exists(mod_path): match e.suffix().casefold(): case "pak" | "utoc" | "ucas": - os.makedirs(os.path.join(mod_path, GameDataPakMods), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataPakMods, e.name())) + os.makedirs( + os.path.join(mod_path, GameDataPakMods), + exist_ok=True, + ) + shutil.move( + os.path.join(mod_path, e.name()), + os.path.join( + mod_path, GameDataPakMods, e.name() + ), + ) case "bk2": - os.makedirs(os.path.join(mod_path, GameDataMovieMods), exist_ok=True) - shutil.move(os.path.join(mod_path, e.name()), os.path.join(mod_path, GameDataMovieMods, e.name())) + os.makedirs( + os.path.join(mod_path, GameDataMovieMods), + exist_ok=True, + ) + shutil.move( + os.path.join(mod_path, e.name()), + os.path.join( + mod_path, GameDataMovieMods, e.name() + ), + ) case _: pass treefixed = 1 @@ -153,7 +192,11 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: case "pak" | "utoc" | "ucas": filetree.move(e, GameDataPakMods, mobase.IFileTree.MERGE) case "dll": - filetree.move(e, os.path.dirname(GameDataUE4SSMods) + "/", mobase.IFileTree.MERGE) + filetree.move( + e, + os.path.dirname(GameDataUE4SSMods) + "/", + mobase.IFileTree.MERGE, + ) case "bk2": filetree.move(e, GameDataMovieMods, mobase.IFileTree.MERGE) case _: @@ -227,7 +270,9 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: except AttributeError: efls = [] libs: set[str] = set() - tree: mobase.IFileTree | mobase.FileTreeEntry | None = self._organizer.virtualFileTree() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = ( + self._organizer.virtualFileTree() + ) if type(tree) is not mobase.IFileTree: return efls for e in tree: @@ -235,7 +280,13 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: libs.add(relpath) exes = self.executables() - efls = efls + [mobase.ExecutableForcedLoadSetting(exe.binary().fileName(), lib).withEnabled(True) for lib in libs for exe in exes] + efls = efls + [ + mobase.ExecutableForcedLoadSetting( + exe.binary().fileName(), lib + ).withEnabled(True) + for lib in libs + for exe in exes + ] return efls def write_default_mods(self, profile: QDir): diff --git a/games/game_titanfall2.py b/games/game_titanfall2.py index dd67358e..da931ddf 100644 --- a/games/game_titanfall2.py +++ b/games/game_titanfall2.py @@ -1,13 +1,14 @@ -from enum import IntEnum, auto -from functools import cached_property import json -from pathlib import Path import os import shutil +from enum import IntEnum, auto +from functools import cached_property +from pathlib import Path -import mobase from PyQt6.QtCore import QDir, QFileInfo +import mobase + from ..basic_game import BasicGame @@ -41,7 +42,6 @@ def getAllContents(self) -> list[mobase.ModDataContent.Content]: for id, name, icon, *filter_only in self.GAMECONTENTS ] - def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): match entry.suffix().casefold(): @@ -93,11 +93,15 @@ def move_overwrite_merge(self, source: str, destination: str): def _Fix_Installed_Mod(self, mod: mobase.IModInterface): if not self.needsNameFix: return - GameNorthstarPath = getattr(self.organizer.managedGame(), "GameNorthstarPath", "") + "/" + GameNorthstarPath = ( + getattr(self.organizer.managedGame(), "GameNorthstarPath", "") + "/" + ) filetree: mobase.IFileTree = mod.fileTree() fixed = False modname = mod.name() - if filetree.exists(GameNorthstarPath + "FOLDERNAME", mobase.IFileTree.DIRECTORY): + if filetree.exists( + GameNorthstarPath + "FOLDERNAME", mobase.IFileTree.DIRECTORY + ): path = mod.absolutePath() json_path = os.path.join(path, GameNorthstarPath + "FOLDERNAME/mod.json") with open(json_path, "r") as json_data: @@ -108,7 +112,9 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): new_path = os.path.join(path, GameNorthstarPath + f"{modname}") self.move_overwrite_merge(old_path, new_path) fixed = True - elif filetree.exists(GameNorthstarPath + "FOLDERNAME_NAME", mobase.IFileTree.DIRECTORY): + elif filetree.exists( + GameNorthstarPath + "FOLDERNAME_NAME", mobase.IFileTree.DIRECTORY + ): path = mod.absolutePath() old_path = os.path.join(path, GameNorthstarPath + "FOLDERNAME_NAME") new_path = os.path.join(path, GameNorthstarPath + f"{modname}") @@ -150,7 +156,9 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): return retVal def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: - GameNorthstarPath = getattr(self.organizer.managedGame(), "GameNorthstarPath", "") + "/" + GameNorthstarPath = ( + getattr(self.organizer.managedGame(), "GameNorthstarPath", "") + "/" + ) treefixed = 0 firsttreelayer: mobase.IFileTree | None = self.first_tree(filetree) if firsttreelayer is not None: @@ -164,16 +172,28 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: treefixed = 1 if secondtreelayer is not None: if secondtreelayer.exists("mod.json", mobase.IFileTree.FILE): - filetree.move(secondtreelayer, firsttreelayer.path("/"), mobase.IFileTree.REPLACE) - filetree.move(firsttreelayer, GameNorthstarPath, mobase.IFileTree.MERGE) - treefixed = 1 + filetree.move( + secondtreelayer, + firsttreelayer.path("/"), + mobase.IFileTree.REPLACE, + ) + filetree.move( + firsttreelayer, GameNorthstarPath, mobase.IFileTree.MERGE + ) + treefixed = 1 elif len(filetree) == 1: - filetree.move(firsttreelayer, GameNorthstarPath, mobase.IFileTree.MERGE) + filetree.move( + firsttreelayer, GameNorthstarPath, mobase.IFileTree.MERGE + ) treefixed = 1 else: for e in filetree: if e.path("/").count("/") == 0: - filetree.move(e,GameNorthstarPath + "FOLDERNAME_NAME/",mobase.IFileTree.MERGE) + filetree.move( + e, + GameNorthstarPath + "FOLDERNAME_NAME/", + mobase.IFileTree.MERGE, + ) treefixed = 1 self.needsNameFix = True if treefixed == 0: diff --git a/games/game_zuma_deluxe.py b/games/game_zuma_deluxe.py index 39cd3377..ef05ee4a 100644 --- a/games/game_zuma_deluxe.py +++ b/games/game_zuma_deluxe.py @@ -1,13 +1,14 @@ -from enum import IntEnum, auto -from functools import cached_property -from pathlib import Path import os import re import shutil +from enum import IntEnum, auto +from functools import cached_property +from pathlib import Path -import mobase from PyQt6.QtCore import QDir, QFileInfo +import mobase + from ..basic_features import BasicGameSaveGameInfo from ..basic_game import BasicGame @@ -89,9 +90,7 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): filetree: mobase.IFileTree = mod.fileTree() fixed = False modname = mod.name() - if filetree.exists( - "mods/FOLDERNAME", mobase.IFileTree.DIRECTORY - ): + if filetree.exists("mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() old_path = os.path.join(path, "mods/FOLDERNAME") new_path = os.path.join(path, f"mods/{modname}") @@ -148,7 +147,7 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: GameLevelsPath: str = str( getattr(self.organizer.managedGame(), "GameLevelsPath", "levels") -) + ) validFolders = [ "images", "levels", diff --git a/games/unreal_tabs/manage_paks/model.py b/games/unreal_tabs/manage_paks/model.py index 2edeb7b4..9fe09e94 100644 --- a/games/unreal_tabs/manage_paks/model.py +++ b/games/unreal_tabs/manage_paks/model.py @@ -1,15 +1,27 @@ -from enum import IntEnum, auto import itertools import typing +from enum import IntEnum, auto from typing import Any, TypeAlias, overload -import mobase - -from PyQt6.QtCore import (QAbstractItemModel, QByteArray, QDataStream, QDir, QFileInfo, QMimeData, QModelIndex, QObject, Qt, QVariant) +from PyQt6.QtCore import ( + QAbstractItemModel, + QByteArray, + QDataStream, + QDir, + QFileInfo, + QMimeData, + QModelIndex, + QObject, + Qt, + QVariant, +) from PyQt6.QtWidgets import QWidget +import mobase + _PakInfo: TypeAlias = tuple[str, str, str, str] + class PaksColumns(IntEnum): PRIORITY = auto() PAK_NAME = auto() @@ -62,7 +74,9 @@ def columnCount(self, parent: QModelIndex | None = None) -> int: parent = QModelIndex() return len(PaksColumns) - def index(self, row: int, column: int, parent: QModelIndex | None = None) -> QModelIndex: + def index( + self, row: int, column: int, parent: QModelIndex | None = None + ) -> QModelIndex: if parent is None: parent = QModelIndex() if ( diff --git a/games/unreal_tabs/manage_paks/widget.py b/games/unreal_tabs/manage_paks/widget.py index 5c6fcf2c..2789a6e4 100644 --- a/games/unreal_tabs/manage_paks/widget.py +++ b/games/unreal_tabs/manage_paks/widget.py @@ -2,14 +2,16 @@ from pathlib import Path from typing import cast -import mobase -from PyQt6.QtWidgets import QGridLayout, QWidget from PyQt6.QtCore import QDir, QFileInfo +from PyQt6.QtWidgets import QGridLayout, QWidget + +import mobase from ....basic_features.utils import is_directory from .model import PaksModel from .view import PaksView + def pak_sort(a: tuple[str, str], b: tuple[str, str]) -> int: a_pak, a_str = a[0], a[1] or a[0] b_pak, b_str = b[0], b[1] or b[0] @@ -164,9 +166,13 @@ def _parse_pak_files(self): if data_path and data_pak_mods: pak_dir = QFileInfo(data_path.absolutePath() + "/" + data_pak_mods) if pak_dir.exists() and pak_dir.isDir(): - for entry in QDir(pak_dir.absoluteFilePath()).entryInfoList(QDir.Filter.Dirs | QDir.Filter.Files | QDir.Filter.NoDotAndDotDot): # type: ignore + for entry in QDir(pak_dir.absoluteFilePath()).entryInfoList( + QDir.Filter.Dirs | QDir.Filter.Files | QDir.Filter.NoDotAndDotDot + ): # type: ignore if entry.isDir(): - for sub_entry in QDir(entry.absoluteFilePath()).entryInfoList(QDir.Filter.Files): # type: ignore + for sub_entry in QDir(entry.absoluteFilePath()).entryInfoList( + QDir.Filter.Files + ): # type: ignore if ( sub_entry.isFile() and sub_entry.suffix().casefold() == "pak" diff --git a/games/unreal_tabs/manage_ue4ss/model.py b/games/unreal_tabs/manage_ue4ss/model.py index 8909d3aa..7cad001e 100644 --- a/games/unreal_tabs/manage_ue4ss/model.py +++ b/games/unreal_tabs/manage_ue4ss/model.py @@ -2,15 +2,17 @@ from json import JSONDecodeError from typing import Any, Iterable -import mobase -from PyQt6.QtCore import (QDir, QFileInfo, QMimeData, QModelIndex, QStringListModel, Qt) +from PyQt6.QtCore import QDir, QFileInfo, QMimeData, QModelIndex, QStringListModel, Qt from PyQt6.QtWidgets import QWidget +import mobase + from ..constants import DEFAULT_UE4SS_MODS + class UE4SSListModel(QStringListModel): def __init__(self, parent: QWidget | None, organizer: mobase.IOrganizer): - super().__init__(parent) # type: ignore + super().__init__(parent) # type: ignore self._checked_items: set[str] = set() self._organizer = organizer self._init_mod_states() @@ -87,7 +89,7 @@ def setData( return True def setStringList(self, strings: Iterable[str | None]) -> None: - super().setStringList(strings) # type: ignore + super().setStringList(strings) # type: ignore self._set_mod_states() def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any: diff --git a/games/unreal_tabs/manage_ue4ss/widget.py b/games/unreal_tabs/manage_ue4ss/widget.py index 47b874e0..69759393 100644 --- a/games/unreal_tabs/manage_ue4ss/widget.py +++ b/games/unreal_tabs/manage_ue4ss/widget.py @@ -1,12 +1,13 @@ -from functools import cmp_to_key import json +from functools import cmp_to_key from json import JSONDecodeError from pathlib import Path -import mobase from PyQt6.QtCore import QDir, QFileInfo, Qt from PyQt6.QtWidgets import QGridLayout, QWidget +import mobase + from ..constants import DEFAULT_UE4SS_MODS, UE4SSModInfo from .model import UE4SSListModel from .view import UE4SSView @@ -55,7 +56,11 @@ def update_mod_files( for mod in mod_list: tree = mod.fileTree() - ue4ss_files = tree.find(game_data_ue4ss_mods) if isinstance(game_data_ue4ss_mods, str) else None + ue4ss_files = ( + tree.find(game_data_ue4ss_mods) + if isinstance(game_data_ue4ss_mods, str) + else None + ) if isinstance(ue4ss_files, mobase.IFileTree): for entry in ue4ss_files: if isinstance(entry, mobase.IFileTree): @@ -80,7 +85,11 @@ def _parse_mod_files(self): & mobase.ModState.ACTIVE ): tree = self._organizer.modList().getMod(mod).fileTree() - ue4ss_files = tree.find(data_ue4ss_mods) if isinstance(data_ue4ss_mods, str) else None + ue4ss_files = ( + tree.find(data_ue4ss_mods) + if isinstance(data_ue4ss_mods, str) + else None + ) if isinstance(ue4ss_files, mobase.IFileTree): for entry in ue4ss_files: if isinstance(entry, mobase.IFileTree): @@ -104,11 +113,26 @@ def _parse_mod_files(self): if data_path and data_ue4ss_mods: ue4ss_dir = QDir(data_path.absolutePath() + "/" + data_ue4ss_mods) if ue4ss_dir.exists(): - for dir_info in ue4ss_dir.entryInfoList(QDir.Filter.Dirs | QDir.Filter.NoDotAndDotDot):# type: ignore - if QFileInfo(QDir(dir_info.absoluteFilePath()).absoluteFilePath("scripts/main.lua")).exists() or QFileInfo(QDir(dir_info.absoluteFilePath()).absoluteFilePath("dlls/main.dll")).exists(): + for dir_info in ue4ss_dir.entryInfoList( + QDir.Filter.Dirs | QDir.Filter.NoDotAndDotDot + ): # type: ignore + if ( + QFileInfo( + QDir(dir_info.absoluteFilePath()).absoluteFilePath( + "scripts/main.lua" + ) + ).exists() + or QFileInfo( + QDir(dir_info.absoluteFilePath()).absoluteFilePath( + "dlls/main.dll" + ) + ).exists() + ): mod_list.add(dir_info.fileName()) if QFileInfo( - QDir(dir_info.absoluteFilePath()).absoluteFilePath("enabled.txt") + QDir(dir_info.absoluteFilePath()).absoluteFilePath( + "enabled.txt" + ) ).exists(): Path(dir_info.absoluteFilePath(), "enabled.txt").unlink() From 3963c3b035c5455dd3c7996008630a52c1653c83 Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Tue, 14 Apr 2026 01:13:38 +0200 Subject: [PATCH 20/31] Ruff Run --- games/game_cyberpunk2077.py | 5 +++-- games/oblivion_remaster/ue4ss/widget.py | 4 +++- games/unreal_tabs/constants.py | 1 + games/unreal_tabs/manage_paks/view.py | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/games/game_cyberpunk2077.py b/games/game_cyberpunk2077.py index ab44b8b9..83587969 100644 --- a/games/game_cyberpunk2077.py +++ b/games/game_cyberpunk2077.py @@ -551,8 +551,9 @@ def _onFinishedRun(self, path: str, exit_code: int) -> None: hide_cb.setToolTip(f"Settings/Plugins/{self.name()}/crash_message") crash_message.setCheckBox(hide_cb) crash_message.open( # type: ignore - lambda: hide_cb.isChecked() - and self._set_setting("crash_message", False) + lambda: ( + hide_cb.isChecked() and self._set_setting("crash_message", False) + ) ) def _check_redmod_result(self, result: tuple[bool, int]) -> bool: diff --git a/games/oblivion_remaster/ue4ss/widget.py b/games/oblivion_remaster/ue4ss/widget.py index 071071ef..4ece7981 100644 --- a/games/oblivion_remaster/ue4ss/widget.py +++ b/games/oblivion_remaster/ue4ss/widget.py @@ -108,7 +108,9 @@ def _parse_mod_files(self): game = self._organizer.managedGame() if isinstance(game, OblivionRemasteredGame): if game.ue4ssDirectory().exists(): - for dir_info in game.ue4ssDirectory().entryInfoList(QDir.Filter.Dirs | QDir.Filter.NoDotAndDotDot): # type: ignore + for dir_info in game.ue4ssDirectory().entryInfoList( + QDir.Filter.Dirs | QDir.Filter.NoDotAndDotDot + ): # type: ignore if QFileInfo( QDir(dir_info.absoluteFilePath()).absoluteFilePath( "scripts/main.lua" diff --git a/games/unreal_tabs/constants.py b/games/unreal_tabs/constants.py index 0b10c821..d29fded3 100644 --- a/games/unreal_tabs/constants.py +++ b/games/unreal_tabs/constants.py @@ -5,6 +5,7 @@ class UE4SSModInfo(TypedDict): mod_name: str mod_enabled: bool + DEFAULT_UE4SS_MODS: list[UE4SSModInfo] = [ {"mod_name": "BPML_GenericFunctions", "mod_enabled": True}, {"mod_name": "BPModLoaderMod", "mod_enabled": True}, diff --git a/games/unreal_tabs/manage_paks/view.py b/games/unreal_tabs/manage_paks/view.py index 05681ff4..f432d42d 100644 --- a/games/unreal_tabs/manage_paks/view.py +++ b/games/unreal_tabs/manage_paks/view.py @@ -29,5 +29,5 @@ def dropEvent(self, e: QDropEvent | None): def dataChanged( self, topLeft: QModelIndex, bottomRight: QModelIndex, roles: Iterable[int] = () ): - super().dataChanged(topLeft, bottomRight, roles) # type: ignore + super().dataChanged(topLeft, bottomRight, roles) # type: ignore self.repaint() From b6793be706d746e902688141fc8312bba203b57a Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Tue, 14 Apr 2026 01:40:42 +0200 Subject: [PATCH 21/31] Ruff 0.12.2 Reformat --- games/game_payday1.py | 1 - games/game_payday2.py | 1 - games/game_raid2.py | 1 - games/game_roadtovostok.py | 1 - 4 files changed, 4 deletions(-) diff --git a/games/game_payday1.py b/games/game_payday1.py index b4efb18d..0d6d24cf 100644 --- a/games/game_payday1.py +++ b/games/game_payday1.py @@ -266,7 +266,6 @@ def executables(self): ] def dll_copy(self, mods: dict[str, mobase.ModState]): - game_path = self.dataDirectory().absolutePath() + "/" for key, value in mods.items(): diff --git a/games/game_payday2.py b/games/game_payday2.py index 5efe083e..43ef3f0d 100644 --- a/games/game_payday2.py +++ b/games/game_payday2.py @@ -267,7 +267,6 @@ def executables(self): ] def dll_copy(self, mods: dict[str, mobase.ModState]): - game_path = self.dataDirectory().absolutePath() + "/" for key, value in mods.items(): diff --git a/games/game_raid2.py b/games/game_raid2.py index 1869911e..c9749a06 100644 --- a/games/game_raid2.py +++ b/games/game_raid2.py @@ -158,7 +158,6 @@ def executables(self): ] def dll_copy(self, mods: dict[str, mobase.ModState]): - game_path = self.dataDirectory().absolutePath() + "/" for key, value in mods.items(): diff --git a/games/game_roadtovostok.py b/games/game_roadtovostok.py index 1df7db7f..88a8da1f 100644 --- a/games/game_roadtovostok.py +++ b/games/game_roadtovostok.py @@ -16,7 +16,6 @@ def __init__(self, organizer: mobase.IOrganizer): def dataLooksValid( self, filetree: mobase.IFileTree ) -> mobase.ModDataChecker.CheckReturn: - if filetree.exists("mods", mobase.IFileTree.DIRECTORY) and not filetree.exists( "mod.txt", mobase.IFileTree.FILE ): From 0fb6897146141a0eab85b8a6764adc1c5a268d8d Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Wed, 22 Apr 2026 18:34:33 +0200 Subject: [PATCH 22/31] Updated rtv vmz support, changed behaviour when mod cant be fixed --- games/game_cassettebeasts.py | 6 ------ games/game_crimeboss.py | 2 -- games/game_emuvr.py | 6 ------ games/game_hitman3.py | 2 -- games/game_noita.py | 2 -- games/game_ovkwalkingdead.py | 2 -- games/game_pacificdrive.py | 2 -- games/game_payday1.py | 23 +++++------------------ games/game_payday2.py | 23 +++++------------------ games/game_payday3.py | 2 -- games/game_roadtovostok.py | 20 +++++--------------- games/game_silenthill2remake.py | 2 -- games/game_titanfall2.py | 2 -- games/game_zuma_deluxe.py | 2 -- 14 files changed, 15 insertions(+), 81 deletions(-) diff --git a/games/game_cassettebeasts.py b/games/game_cassettebeasts.py index 5257a2be..99b0b89f 100644 --- a/games/game_cassettebeasts.py +++ b/games/game_cassettebeasts.py @@ -46,7 +46,6 @@ def dataLooksValid( def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: GameDataPath = getattr(self.organizer.managedGame(), "GameDataPath", "") + "/" - treefixed = 0 for branch in filetree: mod_name = filetree.name() if mod_name == "": @@ -62,18 +61,13 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: os.path.join(mod_path, branch.name()), os.path.join(mod_path, GameDataPath, branch.name()), ) - treefixed = 1 else: if isinstance(branch, mobase.IFileTree): for e in branch: if e.suffix().casefold() == "pck": filetree.move(e, GameDataPath, mobase.IFileTree.MERGE) - treefixed = 1 elif branch.suffix().casefold() == "pck": filetree.move(branch, GameDataPath, mobase.IFileTree.MERGE) - treefixed = 1 - if treefixed == 0: - return None return filetree diff --git a/games/game_crimeboss.py b/games/game_crimeboss.py index bf8f6106..72157585 100644 --- a/games/game_crimeboss.py +++ b/games/game_crimeboss.py @@ -236,8 +236,6 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: case _: pass treefixed = 1 - if treefixed == 0: - return None return filetree diff --git a/games/game_emuvr.py b/games/game_emuvr.py index f5f26c90..375923b3 100644 --- a/games/game_emuvr.py +++ b/games/game_emuvr.py @@ -27,7 +27,6 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: GameDataUGCMods = ( getattr(self.organizer.managedGame(), "GameDataUGCMods", "") + "/" ) - treefixed = 0 for branch in filetree: mod_name = filetree.name() if mod_name == "": @@ -43,18 +42,13 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: os.path.join(mod_path, branch.name()), os.path.join(mod_path, GameDataUGCMods, branch.name()), ) - treefixed = 1 else: if isinstance(branch, mobase.IFileTree): for e in branch: if e.isFile() and e.suffix().casefold() == "ugc": filetree.move(e, GameDataUGCMods, mobase.IFileTree.MERGE) - treefixed = 1 elif branch.suffix().casefold() == "ugc": filetree.move(branch, GameDataUGCMods, mobase.IFileTree.MERGE) - treefixed = 1 - if treefixed == 0: - return None return filetree diff --git a/games/game_hitman3.py b/games/game_hitman3.py index 7003538d..34c4f06f 100644 --- a/games/game_hitman3.py +++ b/games/game_hitman3.py @@ -112,8 +112,6 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: ) if treefixed == 1: self.needsNameFix = True - if treefixed == 0: - return None return filetree diff --git a/games/game_noita.py b/games/game_noita.py index 78b9a993..5e9ec2fc 100644 --- a/games/game_noita.py +++ b/games/game_noita.py @@ -82,8 +82,6 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: if self.fileExistsInNextSubDir(filetree, "mod.xml"): filetree.move(filetree[0], GameModsPath + "/", mobase.IFileTree.MERGE) treefixed = 1 - if treefixed == 0: - return None return filetree diff --git a/games/game_ovkwalkingdead.py b/games/game_ovkwalkingdead.py index 050e2b26..fdc11fdf 100644 --- a/games/game_ovkwalkingdead.py +++ b/games/game_ovkwalkingdead.py @@ -202,8 +202,6 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: case _: pass treefixed = 1 - if treefixed == 0: - return None return filetree diff --git a/games/game_pacificdrive.py b/games/game_pacificdrive.py index 218ec86b..c22e07f4 100644 --- a/games/game_pacificdrive.py +++ b/games/game_pacificdrive.py @@ -202,8 +202,6 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: case _: pass treefixed = 1 - if treefixed == 0: - return None return filetree diff --git a/games/game_payday1.py b/games/game_payday1.py index 0d6d24cf..375be110 100644 --- a/games/game_payday1.py +++ b/games/game_payday1.py @@ -211,24 +211,11 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: mobase.IFileTree.MERGE, ) treefixed = 1 - if treefixed == 0: - if len(filetree) == 1: - filetree.move( - firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE - ) - treefixed = 1 - else: - for e in filetree: - if e.path("/").count("/") == 0: - filetree.move( - e, - "assets/mod_overrides/FOLDERNAME/", - mobase.IFileTree.MERGE, - ) - treefixed = 1 - self.needsNameFix = True - if treefixed == 0: - return None + if treefixed == 0 and len(filetree) == 1: + filetree.move( + firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE + ) + treefixed = 1 return filetree diff --git a/games/game_payday2.py b/games/game_payday2.py index 43ef3f0d..90261d50 100644 --- a/games/game_payday2.py +++ b/games/game_payday2.py @@ -212,24 +212,11 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: mobase.IFileTree.MERGE, ) treefixed = 1 - if treefixed == 0: - if len(filetree) == 1: - filetree.move( - firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE - ) - treefixed = 1 - else: - for e in filetree: - if e.path("/").count("/") == 0: - filetree.move( - e, - "assets/mod_overrides/FOLDERNAME/", - mobase.IFileTree.MERGE, - ) - treefixed = 1 - self.needsNameFix = True - if treefixed == 0: - return None + if treefixed == 0 and len(filetree) == 1: + filetree.move( + firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE + ) + treefixed = 1 return filetree diff --git a/games/game_payday3.py b/games/game_payday3.py index a94afebf..96c269a5 100644 --- a/games/game_payday3.py +++ b/games/game_payday3.py @@ -203,8 +203,6 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: case _: pass treefixed = 1 - if treefixed == 0: - return None return filetree diff --git a/games/game_roadtovostok.py b/games/game_roadtovostok.py index 88a8da1f..8fbb80eb 100644 --- a/games/game_roadtovostok.py +++ b/games/game_roadtovostok.py @@ -27,7 +27,7 @@ def dataLooksValid( def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: GameModsPath = getattr(self.organizer.managedGame(), "GameModsPath", "") + "/" - treefixed = 0 + allowedUnzippedExt = ["zip", "vmz"] for branch in filetree: mod_name = filetree.name() @@ -37,17 +37,13 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: if ( not filetree.createOrphanTree("OrphanTree") and os.path.exists(mod_path) - and branch.suffix().casefold() == "zip" + and branch.suffix().casefold() in allowedUnzippedExt ): 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 @@ -56,14 +52,12 @@ class RoadToVostokGame(BasicGame): Author = "modworkshop" Version = "1" GameName = "Road to Vostok" - GameShortName = "road-to-vostok" + GameShortName = "roadtovostok" GameSteamId = 1963610 GameBinary = "RTV.exe" GameDataPath = "%GAME_PATH%" GameModsPath = "mods" - GameDocumentsDirectory = ( - "%USERPROFILE%/AppData/Local/Godot/app_userdata/Road to Vostok" - ) + GameDocumentsDirectory = "%USERPROFILE%/AppData/Roaming/Road to Vostok" GameSaveExtension = "tres" def init(self, organizer: mobase.IOrganizer) -> bool: @@ -75,11 +69,7 @@ def init(self, organizer: mobase.IOrganizer) -> bool: 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)", + "Road to Vostok", QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), ), ] diff --git a/games/game_silenthill2remake.py b/games/game_silenthill2remake.py index 83fe8d4f..832a6526 100644 --- a/games/game_silenthill2remake.py +++ b/games/game_silenthill2remake.py @@ -202,8 +202,6 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: case _: pass treefixed = 1 - if treefixed == 0: - return None return filetree diff --git a/games/game_titanfall2.py b/games/game_titanfall2.py index da931ddf..20731a8e 100644 --- a/games/game_titanfall2.py +++ b/games/game_titanfall2.py @@ -196,8 +196,6 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: ) treefixed = 1 self.needsNameFix = True - if treefixed == 0: - return None return filetree diff --git a/games/game_zuma_deluxe.py b/games/game_zuma_deluxe.py index ef05ee4a..b52f8e94 100644 --- a/games/game_zuma_deluxe.py +++ b/games/game_zuma_deluxe.py @@ -187,8 +187,6 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: if isinstance(branch, mobase.IFileTree): if len(branch) == 0: filetree.remove(branch) - if treefixed == 0: - return None return filetree From ebff4493f10abb2f0492d0ad15da440cd49eb23e Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Fri, 1 May 2026 22:05:20 +0200 Subject: [PATCH 23/31] Standardize author fields, add Windrose game support - Update Author fields to "ModWorkshop" for consistency. - Change BK2 "Video" icon path to ":/MO/gui/content/modgroup". - Update Payday 3 GameDocumentsDirectory to use %LOCALAPPDATA%. - Add Windrose game support: mod data/content classes, UI tabs, forced DLL loading, and profile initialization. - Improve code consistency and mod content handling. --- games/game_cassettebeasts.py | 2 +- games/game_crimeboss.py | 4 +- games/game_emuvr.py | 2 +- games/game_hitman3.py | 2 +- games/game_noita.py | 2 +- games/game_ovkwalkingdead.py | 4 +- games/game_pacificdrive.py | 4 +- games/game_payday1.py | 2 +- games/game_payday2.py | 2 +- games/game_payday3.py | 9 +- games/game_raid2.py | 2 +- games/game_roadtovostok.py | 2 +- games/game_silenthill2remake.py | 4 +- games/game_titanfall2.py | 2 +- games/game_windrose.py | 323 ++++++++++++++++++++++++++++++++ games/game_zuma_deluxe.py | 2 +- 16 files changed, 345 insertions(+), 23 deletions(-) create mode 100644 games/game_windrose.py diff --git a/games/game_cassettebeasts.py b/games/game_cassettebeasts.py index 99b0b89f..76f707af 100644 --- a/games/game_cassettebeasts.py +++ b/games/game_cassettebeasts.py @@ -179,7 +179,7 @@ def getMetadata(p: Path, save: mobase.ISaveGame) -> Mapping[str, str] | None: class CassetteBeastsGame(BasicGame): Name = "Cassette Beasts Support Plugin" - Author = "modworkshop" + Author = "ModWorkshop" Version = "1" GameName = "Cassette Beasts" GameShortName = "cassette-beasts" diff --git a/games/game_crimeboss.py b/games/game_crimeboss.py index 72157585..f8114f28 100644 --- a/games/game_crimeboss.py +++ b/games/game_crimeboss.py @@ -33,7 +33,7 @@ class CrimeBossModDataContent(mobase.ModDataContent): (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"), + (Content.BK2, "Video", ":/MO/gui/content/modgroup"), ] def getAllContents(self) -> list[mobase.ModDataContent.Content]: @@ -241,7 +241,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: class CrimeBossGame(BasicGame): Name = "Crime Boss Support Plugin" - Author = "modworkshop, MaskPlague and Silarn" + Author = "ModWorkshop, MaskPlague and Silarn" Version = "1" GameName = "Crime Boss Rockay City" GameShortName = "crimeboss" diff --git a/games/game_emuvr.py b/games/game_emuvr.py index 375923b3..f82cd174 100644 --- a/games/game_emuvr.py +++ b/games/game_emuvr.py @@ -54,7 +54,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: class EmuVRGame(BasicGame): Name = "Emu VR Support Plugin" - Author = "modworkshop" + Author = "ModWorkshop" Version = "1" GameName = "Emu VR" GameShortName = "emuvr" diff --git a/games/game_hitman3.py b/games/game_hitman3.py index 34c4f06f..e6cdc4ba 100644 --- a/games/game_hitman3.py +++ b/games/game_hitman3.py @@ -117,7 +117,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: class Hitman3Game(BasicGame): Name = "Hitman 3 Support Plugin" - Author = "modworkshop" + Author = "ModWorkshop" Version = "1" GameName = "Hitman: World of Assassination" GameShortName = "hitman3" diff --git a/games/game_noita.py b/games/game_noita.py index 5e9ec2fc..cd10515b 100644 --- a/games/game_noita.py +++ b/games/game_noita.py @@ -87,7 +87,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: class NoitaGame(BasicGame): Name = "Noita Support Plugin" - Author = "modworkshop" + Author = "ModWorkshop" Version = "1" GameName = "Noita" GameShortName = "noita" diff --git a/games/game_ovkwalkingdead.py b/games/game_ovkwalkingdead.py index fdc11fdf..ff1aec4f 100644 --- a/games/game_ovkwalkingdead.py +++ b/games/game_ovkwalkingdead.py @@ -33,7 +33,7 @@ class OTWDModDataContent(mobase.ModDataContent): (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"), + (Content.BK2, "Video", ":/MO/gui/content/modgroup"), ] def getAllContents(self) -> list[mobase.ModDataContent.Content]: @@ -207,7 +207,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: class OTWDGame(BasicGame): Name = "OVERKILL's The Walking Dead Support Plugin" - Author = "modworkshop, MaskPlague and Silarn" + Author = "ModWorkshop, MaskPlague and Silarn" Version = "1" GameName = "OVERKILL's The Walking Dead" GameShortName = "otwd" diff --git a/games/game_pacificdrive.py b/games/game_pacificdrive.py index c22e07f4..ed149b2c 100644 --- a/games/game_pacificdrive.py +++ b/games/game_pacificdrive.py @@ -33,7 +33,7 @@ class PacificDriveModDataContent(mobase.ModDataContent): (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"), + (Content.BK2, "Video", ":/MO/gui/content/modgroup"), ] def getAllContents(self) -> list[mobase.ModDataContent.Content]: @@ -207,7 +207,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: class PacificDriveGame(BasicGame): Name = "Pacific Drive Support Plugin" - Author = "modworkshop" + Author = "ModWorkshop" Version = "1" GameName = "Pacific Drive" GameLauncher = "PenDriverPro.exe" diff --git a/games/game_payday1.py b/games/game_payday1.py index 375be110..939c8791 100644 --- a/games/game_payday1.py +++ b/games/game_payday1.py @@ -221,7 +221,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: class Payday1Game(BasicGame): Name = "Payday 1 Support Plugin" - Author = "modworkshop" + Author = "ModWorkshop" Version = "1" GameName = "Payday: The Heist" GameShortName = "pdth" diff --git a/games/game_payday2.py b/games/game_payday2.py index 90261d50..9bce5aa5 100644 --- a/games/game_payday2.py +++ b/games/game_payday2.py @@ -222,7 +222,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: class Payday2Game(BasicGame): Name = "Payday 2 Support Plugin" - Author = "modworkshop" + Author = "ModWorkshop" Version = "1" GameName = "Payday 2" GameShortName = "payday-2" diff --git a/games/game_payday3.py b/games/game_payday3.py index 96c269a5..80180b1c 100644 --- a/games/game_payday3.py +++ b/games/game_payday3.py @@ -34,7 +34,7 @@ class Payday3ModDataContent(mobase.ModDataContent): (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"), + (Content.BK2, "Video", ":/MO/gui/content/modgroup"), ] def getAllContents(self) -> list[mobase.ModDataContent.Content]: @@ -208,9 +208,10 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: class Payday3Game(BasicGame): Name = "Payday 3 Support Plugin" - Author = "modworkshop, MaskPlague and Silarn" + Author = "ModWorkshop, MaskPlague and Silarn" Version = "1" GameName = "Payday 3" + GameLauncher = "PAYDAY3.exe" GameShortName = "payday-3" GameSteamId = 1272080 GameBinary = "PAYDAY3/Binaries/Win64/PAYDAY3-Win64-Shipping.exe" @@ -218,9 +219,7 @@ class Payday3Game(BasicGame): GameDataUE4SSMods = "Binaries/Win64/Mods" GameDataPakMods = "Content/Paks/~Mods" GameDataMovieMods = "Content/Movies" - GameDocumentsDirectory = ( - "%USERPROFILE%/AppData/Local/PAYDAY3/Saved/Config/WindowsClient" - ) + GameDocumentsDirectory = "%LOCALAPPDATA%/PAYDAY3/Saved/Config/WindowsClient" GameSaveExtension = "sav" _main_window: QMainWindow _ue4ss_tab: UE4SSTabWidget diff --git a/games/game_raid2.py b/games/game_raid2.py index c9749a06..1bdbe453 100644 --- a/games/game_raid2.py +++ b/games/game_raid2.py @@ -131,7 +131,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree: class RaidWW2Game(BasicGame): Name = "RAID World War II Support Plugin" - Author = "modworkshop" + Author = "ModWorkshop" Version = "1" GameName = "RAID World War II" GameShortName = "raidww2" diff --git a/games/game_roadtovostok.py b/games/game_roadtovostok.py index 8fbb80eb..2dfdef5a 100644 --- a/games/game_roadtovostok.py +++ b/games/game_roadtovostok.py @@ -49,7 +49,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: class RoadToVostokGame(BasicGame): Name = "Road to Vostok Support Plugin" - Author = "modworkshop" + Author = "ModWorkshop" Version = "1" GameName = "Road to Vostok" GameShortName = "roadtovostok" diff --git a/games/game_silenthill2remake.py b/games/game_silenthill2remake.py index 832a6526..e9fc77e4 100644 --- a/games/game_silenthill2remake.py +++ b/games/game_silenthill2remake.py @@ -33,7 +33,7 @@ class SilentHill2ModDataContent(mobase.ModDataContent): (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"), + (Content.BK2, "Video", ":/MO/gui/content/modgroup"), ] def getAllContents(self) -> list[mobase.ModDataContent.Content]: @@ -207,7 +207,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: class SilentHill2Game(BasicGame): Name = "Silent Hill 2 Support Plugin" - Author = "modworkshop" + Author = "ModWorkshop" Version = "1" GameName = "Silent Hill 2 Remake" GameLauncher = "SHProto.exe" diff --git a/games/game_titanfall2.py b/games/game_titanfall2.py index 20731a8e..968bb53d 100644 --- a/games/game_titanfall2.py +++ b/games/game_titanfall2.py @@ -201,7 +201,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: class Titanfall2Game(BasicGame): Name = "Titanfall 2 Support Plugin" - Author = "modworkshop" + Author = "ModWorkshop" Version = "1" GameName = "Titanfall 2" GameShortName = "titanfall-2" diff --git a/games/game_windrose.py b/games/game_windrose.py new file mode 100644 index 00000000..37573b8d --- /dev/null +++ b/games/game_windrose.py @@ -0,0 +1,323 @@ +import json +import os +import shutil +from enum import IntEnum, auto +from functools import cached_property +from pathlib import Path + +from PyQt6.QtCore import QDir, QFileInfo +from PyQt6.QtWidgets import QMainWindow, QTabWidget, QWidget + +import mobase + +from ..basic_game import BasicGame +from .unreal_tabs.constants import DEFAULT_UE4SS_MODS, UE4SSModInfo +from .unreal_tabs.manage_paks.widget import PaksTabWidget +from .unreal_tabs.manage_ue4ss.widget import UE4SSTabWidget + + +class Content(IntEnum): + UCAS = auto() + UTOC = auto() + PAK = auto() + UE4SS = auto() + DLL = auto() + BK2 = auto() + + +class WindroseModDataContent(mobase.ModDataContent): + content: list[int] = [] + GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ + (Content.UCAS, "UCAS", ":/MO/gui/content/geometries"), + (Content.UTOC, "UTOC", ":/MO/gui/content/inifile"), + (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/modgroup"), + ] + + 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.content.append(Content.UTOC) + case "ucas": + self.content.append(Content.UCAS) + case "pak": + self.content.append(Content.PAK) + case "lua": + self.content.append(Content.UE4SS) + case "dll": + self.content.append(Content.DLL) + case "bk2": + self.content.append(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 WindroseModDataChecker(mobase.ModDataChecker): + def __init__(self, organizer: mobase.IOrganizer): + super().__init__() + self.organizer: mobase.IOrganizer = organizer + + def move_overwrite_merge(self, source: str, destination: str): + 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 = getattr( + self.organizer.managedGame(), "GameDataUE4SSMods", "" + ) + GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "") + GameDataMovieMods = getattr( + self.organizer.managedGame(), "GameDataMovieMods", "" + ) + if filetree.exists(GameDataPakMods, mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID + if filetree.exists(GameDataMovieMods, 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 isinstance(branch, mobase.IFileTree): + for e in branch: + if 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: + 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 | None: + GameDataUE4SSMods = ( + getattr(self.organizer.managedGame(), "GameDataUE4SSMods", "") + "/" + ) + GameDataPakMods = ( + getattr(self.organizer.managedGame(), "GameDataPakMods", "") + "/" + ) + GameDataMovieMods = ( + getattr(self.organizer.managedGame(), "GameDataMovieMods", "") + "/" + ) + treefixed = 0 + if filetree.exists("UE4SS.dll", mobase.IFileTree.FILE): + treefixed = self.allMoveTo( + filetree, os.path.dirname(os.path.dirname(GameDataUE4SSMods)) + "/" + ) + 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.isFile(): + fileext = e.suffix().casefold() + if fileext in allowedUnzippedExt: + mod_name = filetree.name() + if mod_name == "": + mod_name = e.name() + mod_path = os.path.join(self.organizer.modsPath(), mod_name) + if not filetree.createOrphanTree( + "OrphanTree" + ) and os.path.exists(mod_path): + match e.suffix().casefold(): + case "pak" | "utoc" | "ucas": + os.makedirs( + os.path.join(mod_path, GameDataPakMods), + exist_ok=True, + ) + shutil.move( + os.path.join(mod_path, e.name()), + os.path.join( + mod_path, GameDataPakMods, e.name() + ), + ) + case "bk2": + os.makedirs( + os.path.join(mod_path, GameDataMovieMods), + exist_ok=True, + ) + shutil.move( + os.path.join(mod_path, e.name()), + os.path.join( + mod_path, GameDataMovieMods, e.name() + ), + ) + case _: + pass + treefixed = 1 + else: + entriesToMove.append(e) + if entriesToMove: + for e in entriesToMove: + match e.suffix().casefold(): + case "pak" | "utoc" | "ucas": + filetree.move(e, GameDataPakMods, mobase.IFileTree.MERGE) + case "dll": + filetree.move( + e, + os.path.dirname(GameDataUE4SSMods) + "/", + mobase.IFileTree.MERGE, + ) + case "bk2": + filetree.move(e, GameDataMovieMods, mobase.IFileTree.MERGE) + case _: + pass + treefixed = 1 + return filetree + + +class WindroseGame(BasicGame): + Name = "Windrose Support Plugin" + Author = "ModWorkshop" + Version = "1" + GameName = "Windrose" + GameLauncher = "Windrose.exe" + GameShortName = "windrose" + GameSteamId = 3041230 + GameBinary = "R5/Binaries/Win64/WindroseWin64-Shipping.exe" + GameDataPath = "R5" + GameDataUE4SSMods = "Binaries/Win64/Mods" + GameDataPakMods = "Content/Paks/~Mods" + GameDataMovieMods = "Content/Movies" + GameDocumentsDirectory = "%LOCALAPPDATA%/R5/Saved/Config/Windows" + GameSavesDirectory = "%LOCALAPPDATA%/R5/Saved/SaveGames" + GameSaveExtension = "sav" + _main_window: QMainWindow + _ue4ss_tab: UE4SSTabWidget + _paks_tab: PaksTabWidget + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self.dataChecker = WindroseModDataChecker(organizer) + self._register_feature(self.dataChecker) + self._register_feature(WindroseModDataContent()) + 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( + "Windrose", + 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 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", "Engine.ini"] + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + self.write_default_mods(directory) + + base_data_dir = self.dataDirectory().absolutePath() + + paksDirectory = QDir(base_data_dir + "/" + self.GameDataPakMods) + ue4ssDirectory = QDir(base_data_dir + "/" + self.GameDataUE4SSMods) + movieDirectory = QDir(base_data_dir + "/" + self.GameDataMovieMods) + + if not paksDirectory.exists(): + os.makedirs(paksDirectory.absolutePath()) + if not ue4ssDirectory.exists(): + os.makedirs(ue4ssDirectory.absolutePath()) + if not movieDirectory.exists(): + os.makedirs(movieDirectory.absolutePath()) + super().initializeProfile(directory, settings) diff --git a/games/game_zuma_deluxe.py b/games/game_zuma_deluxe.py index b52f8e94..001896b2 100644 --- a/games/game_zuma_deluxe.py +++ b/games/game_zuma_deluxe.py @@ -195,7 +195,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: class ZumaGame(BasicGame, mobase.IPluginFileMapper): Name = "Zuma Deluxe Support Plugin" - Author = "modworkshop" + Author = "ModWorkshop" Version = "1" GameName = "Zuma Deluxe" GameShortName = "zuma" From cc18aa9e8e492018fdb2a0f7e550acbb0957cd2a Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Fri, 1 May 2026 22:59:08 +0200 Subject: [PATCH 24/31] Update WindroseGame GameBinary path --- games/game_windrose.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/games/game_windrose.py b/games/game_windrose.py index 37573b8d..6ec2025d 100644 --- a/games/game_windrose.py +++ b/games/game_windrose.py @@ -213,7 +213,7 @@ class WindroseGame(BasicGame): GameLauncher = "Windrose.exe" GameShortName = "windrose" GameSteamId = 3041230 - GameBinary = "R5/Binaries/Win64/WindroseWin64-Shipping.exe" + GameBinary = "R5/Binaries/Win64/Windrose-Win64-Shipping.exe" GameDataPath = "R5" GameDataUE4SSMods = "Binaries/Win64/Mods" GameDataPakMods = "Content/Paks/~Mods" From b8c6e500a0edcd7f66a05a17e70420baffed8687 Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Tue, 5 May 2026 18:24:13 +0200 Subject: [PATCH 25/31] Updated P1, P2, R2 to handle Mods with multiple Components and Folder Choices Additional Sanity checks for Naming and Mod Structure --- games/game_payday1.py | 340 +++++++++++++++++++++++++++++++----------- games/game_payday2.py | 338 ++++++++++++++++++++++++++++++----------- games/game_raid2.py | 207 ++++++++++++++++++++++++- 3 files changed, 706 insertions(+), 179 deletions(-) diff --git a/games/game_payday1.py b/games/game_payday1.py index 939c8791..9d9995dc 100644 --- a/games/game_payday1.py +++ b/games/game_payday1.py @@ -3,14 +3,38 @@ from enum import IntEnum, auto from functools import cached_property from pathlib import Path +from typing import TypedDict -from PyQt6.QtCore import QDir, QFileInfo +from PyQt6.QtCore import QDir, QFileInfo, Qt +from PyQt6.QtWidgets import ( + QDialog, + QDialogButtonBox, + QLabel, + QListWidget, + QListWidgetItem, + QVBoxLayout, +) import mobase from ..basic_game import BasicGame +def sanitize_folder_name(name: str) -> str: + # Remove invalid characters for Windows folder names + invalid_chars = '+&<>:"|?*\\/' + for char in invalid_chars: + name = name.replace(char, "") + # Remove control characters (ASCII 0-31) + name = "".join(c for c in name if ord(c) >= 32) + # Remove trailing periods and spaces + name = name.rstrip(". ") + # If name is empty after sanitization, use a default + if not name: + name = "Unnamed" + return name + + class Content(IntEnum): TEXTURE = auto() MESH = auto() @@ -61,14 +85,40 @@ def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: return list(self.content) +class ModDetectionCandidate(TypedDict): + tree: mobase.IFileTree + name: str + display: str + destination: str + + 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.organizer.modList().onModInstalled(self.fixInstalledMod) self.needsNameFix = False + self.modDetectionCandidates: list[ModDetectionCandidate] = [] + + folderList = [ + "anims", + "core", + "effects", + "environments", + "fonts", + "gamedata", + "guis", + "lib", + "movies", + "physic_effects", + "settings", + "shaders", + "soundbanks", + "strings", + "units", + ] - def move_overwrite_merge(self, source: str, destination: str): + def moveOverwriteMerge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -78,34 +128,34 @@ def move_overwrite_merge(self, source: str, destination: str): 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) + self.moveOverwriteMerge(s_item, d_item) os.rmdir(source) - def _Fix_Installed_Mod(self, mod: mobase.IModInterface): + def fixInstalledMod(self, mod: mobase.IModInterface): if not self.needsNameFix: return filetree: mobase.IFileTree = mod.fileTree() fixed = False - modname = mod.name() + modname = sanitize_folder_name(mod.name()) if filetree.exists("mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() old_path = os.path.join(path, "mods/FOLDERNAME") new_path = os.path.join(path, f"mods/{modname}") - self.move_overwrite_merge(old_path, new_path) + self.moveOverwriteMerge(old_path, new_path) fixed = True elif filetree.exists( - "assets/mod_overrides/FOLDERNAME/", mobase.IFileTree.DIRECTORY + "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) + self.moveOverwriteMerge(old_path, new_path) fixed = True elif filetree.exists("maps/FOLDERNAME", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() old_path = os.path.join(path, "maps/FOLDERNAME") new_path = os.path.join(path, f"maps/{modname}") - self.move_overwrite_merge(old_path, new_path) + self.moveOverwriteMerge(old_path, new_path) fixed = True if not fixed: return @@ -114,15 +164,21 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): def dataLooksValid( self, filetree: mobase.IFileTree ) -> mobase.ModDataChecker.CheckReturn: + validList = {"assets", "mods", "maps"} + for e in filetree: + if isinstance(e, mobase.IFileTree) and e.isDir(): + if e.name().casefold() not in validList: + return mobase.ModDataChecker.FIXABLE 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.suffix().casefold() == "dll": - 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): @@ -133,90 +189,200 @@ def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): return True return False - def first_tree(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + def firstTree(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: for e in filetree: if isinstance(e, mobase.IFileTree) and e.isDir(): return e return None - def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): + def moveTreeContent( + self, + sourcetree: mobase.IFileTree, + targettree: mobase.IFileTree, + destination: str, + ) -> bool: entriesToMove: list[mobase.FileTreeEntry] = [] - retVal = 0 - for e in filetree: + for e in sourcetree: entriesToMove.append(e) for e in entriesToMove: - filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) - retVal = 1 - return retVal + targettree.move(e, destination, mobase.IFileTree.MERGE) + targettree.remove(sourcetree) + return bool(entriesToMove) - def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: - treefixed = 0 - firsttreelayer: mobase.IFileTree | None = self.first_tree(filetree) - if firsttreelayer is not None: - secondtreelayer: mobase.IFileTree | None = self.first_tree(firsttreelayer) - if filetree.exists("mod.txt", mobase.IFileTree.FILE): - treefixed = self.allMoveTo(filetree, "mods/FOLDERNAME/") - if treefixed == 1: - self.needsNameFix = True - elif self.fileExistsInNextSubDir(filetree, "mod.txt"): - filetree.move(firsttreelayer, "mods/", mobase.IFileTree.MERGE) - treefixed = 1 - elif self.fileExistsInNextSubDir(filetree, "main.xml"): - if self.fileExistsInNextSubDir(filetree, "levels"): - filetree.move(firsttreelayer, "maps/", mobase.IFileTree.MERGE) - treefixed = 1 - else: - filetree.move( - firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE - ) - treefixed = 1 - elif filetree.exists("main.xml", mobase.IFileTree.FILE): - if filetree.exists("levels", mobase.IFileTree.DIRECTORY): - treefixed = self.allMoveTo(filetree, "maps/FOLDERNAME/") - if treefixed == 1: - self.needsNameFix = True - else: - treefixed = self.allMoveTo( - filetree, "assets/mod_overrides/FOLDERNAME/" - ) - if treefixed == 1: - self.needsNameFix = True - elif secondtreelayer is not None: - if secondtreelayer.exists("mod.txt", mobase.IFileTree.FILE): - filetree.move( - secondtreelayer, - firsttreelayer.path("/"), - mobase.IFileTree.REPLACE, - ) - filetree.move(firsttreelayer, "mods/", mobase.IFileTree.MERGE) - treefixed = 1 - elif secondtreelayer.exists("main.xml", mobase.IFileTree.FILE): - if filetree.exists("levels", mobase.IFileTree.DIRECTORY): - filetree.move( - secondtreelayer, - firsttreelayer.path("/"), - mobase.IFileTree.REPLACE, - ) - filetree.move(firsttreelayer, "maps/", mobase.IFileTree.MERGE) - treefixed = 1 - else: - filetree.move( - secondtreelayer, - firsttreelayer.path("/"), - mobase.IFileTree.REPLACE, - ) - filetree.move( - firsttreelayer, - "assets/mod_overrides/", - mobase.IFileTree.MERGE, - ) - treefixed = 1 - if treefixed == 0 and len(filetree) == 1: - filetree.move( - firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE + def addModDetectionCandidate( + self, + tree: mobase.IFileTree, + name: str, + category: str, + destination: str, + ) -> None: + debug_name = name or tree.name() + debug_path = "" + if hasattr(tree, "path"): + try: + debug_path = tree.path() + except Exception: + debug_path = "" + if not debug_path and hasattr(tree, "name"): + debug_path = tree.name() + + print( + f"[Payday1ModDataChecker] Detected mod candidate: {debug_name} | " + f"path={debug_path} | category={category} | destination={destination}" + ) + self.modDetectionCandidates.append( + { + "tree": tree, + "name": name, + "display": f"{name} ({category})", + "destination": destination, + } + ) + + def showModDetectionDialog(self) -> set[int] | None: + if not self.modDetectionCandidates: + return set() + + dialog = QDialog() + dialog.setWindowTitle("Found Mods") + + layout = QVBoxLayout(dialog) + layout.addWidget(QLabel("Select the mods to install:")) + + listWidget = QListWidget() + listWidget.setSelectionMode(QListWidget.SelectionMode.NoSelection) + for candidate in self.modDetectionCandidates: + item = QListWidgetItem(candidate["display"]) + item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable) + item.setCheckState(Qt.CheckState.Checked) + listWidget.addItem(item) + + layout.addWidget(listWidget) + + buttonBox = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel + ) + buttonBox.accepted.connect(lambda: dialog.accept()) # type: ignore + buttonBox.rejected.connect(lambda: dialog.reject()) # type: ignore + layout.addWidget(buttonBox) + + if dialog.exec() != QDialog.DialogCode.Accepted: + return None + + selectedIndexes: set[int] = set() + for index in range(listWidget.count()): + item = listWidget.item(index) + if ( + isinstance(item, QListWidgetItem) + and item.checkState() == Qt.CheckState.Checked + ): + selectedIndexes.add(index) + + return selectedIndexes + + def collectModCandidates( + self, tree: mobase.IFileTree | mobase.FileTreeEntry + ) -> bool: + hasDisallowedPath = False + disallowedFolders = {"assets", "levels", "lua"} + tree_path = tree.path() + tree_path_lower = ( + tree_path.replace("\\", "/").casefold() + if isinstance(tree_path, str) + else "" + ) + if disallowedFolders & set(tree_path_lower.split("/")): + hasDisallowedPath = True + hasFolderListSubfolder = any( + tree.exists(validFolder, mobase.IFileTree.DIRECTORY) + for validFolder in self.folderList + ) + if tree.exists("mod.txt", mobase.IFileTree.FILE) and not hasFolderListSubfolder: + self.addModDetectionCandidate( + tree, + sanitize_folder_name(tree.name()), + "Mods Folder with mod.txt", + "mods/FOLDERNAME/", + ) + return True + elif ( + tree.exists("main.xml", mobase.IFileTree.FILE) + and tree.exists("levels", mobase.IFileTree.DIRECTORY) + and not hasDisallowedPath + ): + self.addModDetectionCandidate( + tree, + sanitize_folder_name(tree.name()), + "Maps Folder with main.xml and levels folder", + "maps/FOLDERNAME/", + ) + return True + elif ( + tree.exists("main.xml", mobase.IFileTree.FILE) + or tree.exists("add.xml", mobase.IFileTree.FILE) + and not hasDisallowedPath + ): + self.addModDetectionCandidate( + tree, + sanitize_folder_name(tree.name()), + "Mod Override Folder with main.xml/add.xml", + "assets/mod_overrides/FOLDERNAME/", + ) + return True + elif tree.exists("mod_overrides", mobase.IFileTree.DIRECTORY): + sourcetree = tree.find("mod_overrides", mobase.IFileTree.DIRECTORY) + if isinstance(sourcetree, mobase.IFileTree): + self.addModDetectionCandidate( + sourcetree, + sanitize_folder_name(sourcetree.name()), + "Mod Override Folder", + "assets/mod_overrides/FOLDERNAME/", ) - treefixed = 1 - return filetree + return True + elif not hasDisallowedPath: + for validFolder in self.folderList: + if tree.exists(validFolder, mobase.IFileTree.DIRECTORY): + self.addModDetectionCandidate( + tree, + sanitize_folder_name(tree.name()), + "Fallback Mod Override Folder", + "assets/mod_overrides/FOLDERNAME/", + ) + return True + return False + + def walk_entry(self, path: str, entry: mobase.FileTreeEntry): + if entry.isDir(): + if isinstance(entry, mobase.IFileTree): + self.collectModCandidates(entry) + return mobase.IFileTree.WalkReturn.CONTINUE + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + self.modDetectionCandidates = [] + newtree = filetree.createOrphanTree("Fixed Tree") + + filetree.walk(self.walk_entry, "/") + + if len(self.modDetectionCandidates) == 1: + selectedIndexes = {0} + else: + selectedIndexes = self.showModDetectionDialog() + if selectedIndexes is None: + return None + + for index in selectedIndexes: + candidate = self.modDetectionCandidates[index] + candidate["destination"] = candidate["destination"].replace( + "FOLDERNAME", + candidate["name"], + ) + print(f"Installing Mod: {candidate['name']} to {candidate['destination']}") + if self.moveTreeContent( + candidate["tree"], newtree, candidate["destination"] + ): + self.needsNameFix = True + + return newtree if len(newtree) > 0 else filetree class Payday1Game(BasicGame): diff --git a/games/game_payday2.py b/games/game_payday2.py index 9bce5aa5..688668a2 100644 --- a/games/game_payday2.py +++ b/games/game_payday2.py @@ -3,14 +3,38 @@ from enum import IntEnum, auto from functools import cached_property from pathlib import Path +from typing import TypedDict -from PyQt6.QtCore import QDir, QFileInfo +from PyQt6.QtCore import QDir, QFileInfo, Qt +from PyQt6.QtWidgets import ( + QDialog, + QDialogButtonBox, + QLabel, + QListWidget, + QListWidgetItem, + QVBoxLayout, +) import mobase from ..basic_game import BasicGame +def sanitize_folder_name(name: str) -> str: + # Remove invalid characters for Windows folder names + invalid_chars = '+&<>:"|?*\\/' + for char in invalid_chars: + name = name.replace(char, "") + # Remove control characters (ASCII 0-31) + name = "".join(c for c in name if ord(c) >= 32) + # Remove trailing periods and spaces + name = name.rstrip(". ") + # If name is empty after sanitization, use a default + if not name: + name = "Unnamed" + return name + + class Content(IntEnum): TEXTURE = auto() MESH = auto() @@ -61,14 +85,40 @@ def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: return list(self.content) +class ModDetectionCandidate(TypedDict): + tree: mobase.IFileTree + name: str + display: str + destination: str + + 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.organizer.modList().onModInstalled(self.fixInstalledMod) self.needsNameFix = False + self.modDetectionCandidates: list[ModDetectionCandidate] = [] + + folderList = [ + "anims", + "core", + "effects", + "environments", + "fonts", + "gamedata", + "guis", + "lib", + "movies", + "physic_effects", + "settings", + "shaders", + "soundbanks", + "strings", + "units", + ] - def move_overwrite_merge(self, source: str, destination: str): + def moveOverwriteMerge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -78,20 +128,20 @@ def move_overwrite_merge(self, source: str, destination: str): 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) + self.moveOverwriteMerge(s_item, d_item) os.rmdir(source) - def _Fix_Installed_Mod(self, mod: mobase.IModInterface): + def fixInstalledMod(self, mod: mobase.IModInterface): if not self.needsNameFix: return filetree: mobase.IFileTree = mod.fileTree() fixed = False - modname = mod.name() + modname = sanitize_folder_name(mod.name()) if filetree.exists("mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() old_path = os.path.join(path, "mods/FOLDERNAME") new_path = os.path.join(path, f"mods/{modname}") - self.move_overwrite_merge(old_path, new_path) + self.moveOverwriteMerge(old_path, new_path) fixed = True elif filetree.exists( "assets/mod_overrides/FOLDERNAME", mobase.IFileTree.DIRECTORY @@ -99,13 +149,13 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): 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) + self.moveOverwriteMerge(old_path, new_path) fixed = True elif filetree.exists("maps/FOLDERNAME", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() old_path = os.path.join(path, "maps/FOLDERNAME") new_path = os.path.join(path, f"maps/{modname}") - self.move_overwrite_merge(old_path, new_path) + self.moveOverwriteMerge(old_path, new_path) fixed = True if not fixed: return @@ -114,6 +164,11 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): def dataLooksValid( self, filetree: mobase.IFileTree ) -> mobase.ModDataChecker.CheckReturn: + validList = {"assets", "mods", "maps"} + for e in filetree: + if isinstance(e, mobase.IFileTree) and e.isDir(): + if e.name().casefold() not in validList: + return mobase.ModDataChecker.FIXABLE if filetree.exists("assets/mod_overrides", mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID if filetree.exists("mods", mobase.IFileTree.DIRECTORY): @@ -134,90 +189,203 @@ def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): return True return False - def first_tree(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + def firstTree(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: for e in filetree: if isinstance(e, mobase.IFileTree) and e.isDir(): return e return None - def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): + def moveTreeContent( + self, + sourcetree: mobase.IFileTree, + targettree: mobase.IFileTree, + destination: str, + ) -> bool: entriesToMove: list[mobase.FileTreeEntry] = [] - retVal = 0 - for e in filetree: + for e in sourcetree: entriesToMove.append(e) for e in entriesToMove: - filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) - retVal = 1 - return retVal + targettree.move(e, destination, mobase.IFileTree.MERGE) + targettree.remove(sourcetree) + return bool(entriesToMove) - def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: - treefixed = 0 - firsttreelayer: mobase.IFileTree | None = self.first_tree(filetree) - if firsttreelayer is not None: - secondtreelayer: mobase.IFileTree | None = self.first_tree(firsttreelayer) - if filetree.exists("mod.txt", mobase.IFileTree.FILE): - treefixed = self.allMoveTo(filetree, "mods/FOLDERNAME/") - if treefixed == 1: - self.needsNameFix = True - elif self.fileExistsInNextSubDir(filetree, "mod.txt"): - filetree.move(firsttreelayer, "mods/", mobase.IFileTree.MERGE) - treefixed = 1 - elif self.fileExistsInNextSubDir(filetree, "main.xml"): - if self.fileExistsInNextSubDir(filetree, "levels"): - filetree.move(firsttreelayer, "maps/", mobase.IFileTree.MERGE) - treefixed = 1 - else: - filetree.move( - firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE - ) - treefixed = 1 - elif filetree.exists("main.xml", mobase.IFileTree.FILE): - if filetree.exists("levels", mobase.IFileTree.DIRECTORY): - treefixed = self.allMoveTo(filetree, "maps/FOLDERNAME/") - if treefixed == 1: - self.needsNameFix = True - else: - treefixed = self.allMoveTo( - filetree, "assets/mod_overrides/FOLDERNAME/" - ) - if treefixed == 1: - self.needsNameFix = True - elif secondtreelayer is not None: - if secondtreelayer.exists("mod.txt", mobase.IFileTree.FILE): - filetree.move( - secondtreelayer, - firsttreelayer.path("/"), - mobase.IFileTree.REPLACE, - ) - filetree.move(firsttreelayer, "mods/", mobase.IFileTree.MERGE) - treefixed = 1 - elif secondtreelayer.exists("main.xml", mobase.IFileTree.FILE): - if filetree.exists("levels", mobase.IFileTree.DIRECTORY): - filetree.move( - secondtreelayer, - firsttreelayer.path("/"), - mobase.IFileTree.REPLACE, - ) - filetree.move(firsttreelayer, "maps/", mobase.IFileTree.MERGE) - treefixed = 1 - else: - filetree.move( - secondtreelayer, - firsttreelayer.path("/"), - mobase.IFileTree.REPLACE, - ) - filetree.move( - firsttreelayer, - "assets/mod_overrides/", - mobase.IFileTree.MERGE, - ) - treefixed = 1 - if treefixed == 0 and len(filetree) == 1: - filetree.move( - firsttreelayer, "assets/mod_overrides/", mobase.IFileTree.MERGE + def addModDetectionCandidate( + self, + tree: mobase.IFileTree, + name: str, + category: str, + destination: str, + ) -> None: + debug_name = name or tree.name() + debug_path = "" + if hasattr(tree, "path"): + try: + debug_path = tree.path() + except Exception: + debug_path = "" + if not debug_path and hasattr(tree, "name"): + debug_path = tree.name() + + print( + f"[Payday2ModDataChecker] Detected mod candidate: {debug_name} | " + f"path={debug_path} | category={category} | destination={destination}" + ) + self.modDetectionCandidates.append( + { + "tree": tree, + "name": name, + "display": f"{name} ({category})", + "destination": destination, + } + ) + + def showModDetectionDialog(self) -> set[int] | None: + if not self.modDetectionCandidates: + return set() + + dialog = QDialog() + dialog.setWindowTitle("Found Mods") + + layout = QVBoxLayout(dialog) + layout.addWidget(QLabel("Select the mods to install:")) + + listWidget = QListWidget() + listWidget.setSelectionMode(QListWidget.SelectionMode.NoSelection) + for candidate in self.modDetectionCandidates: + item = QListWidgetItem(candidate["display"]) + item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable) + item.setCheckState(Qt.CheckState.Checked) + listWidget.addItem(item) + + layout.addWidget(listWidget) + + buttonBox = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel + ) + buttonBox.accepted.connect(lambda: dialog.accept()) # type: ignore + buttonBox.rejected.connect(lambda: dialog.reject()) # type: ignore + layout.addWidget(buttonBox) + + if dialog.exec() != QDialog.DialogCode.Accepted: + return None + + selectedIndexes: set[int] = set() + for index in range(listWidget.count()): + item = listWidget.item(index) + if ( + isinstance(item, QListWidgetItem) + and item.checkState() == Qt.CheckState.Checked + ): + selectedIndexes.add(index) + + return selectedIndexes + + def collectModCandidates( + self, tree: mobase.IFileTree | mobase.FileTreeEntry + ) -> bool: + hasDisallowedPath = False + disallowedFolders = {"assets", "levels", "lua"} + tree_path = tree.path() + tree_path_lower = ( + tree_path.replace("\\", "/").casefold() + if isinstance(tree_path, str) + else "" + ) + if disallowedFolders & set(tree_path_lower.split("/")): + hasDisallowedPath = True + hasFolderListSubfolder = any( + tree.exists(validFolder, mobase.IFileTree.DIRECTORY) + for validFolder in self.folderList + ) + if tree.exists("mod.txt", mobase.IFileTree.FILE) and not hasFolderListSubfolder: + self.addModDetectionCandidate( + tree, + sanitize_folder_name(tree.name()), + "Mods Folder with mod.txt", + "mods/FOLDERNAME/", + ) + return True + elif ( + tree.exists("main.xml", mobase.IFileTree.FILE) + and tree.exists("levels", mobase.IFileTree.DIRECTORY) + and not hasDisallowedPath + ): + self.addModDetectionCandidate( + tree, + sanitize_folder_name(tree.name()), + "Maps Folder with main.xml and levels folder", + "maps/FOLDERNAME/", + ) + return True + elif ( + tree.exists("main.xml", mobase.IFileTree.FILE) + or tree.exists("add.xml", mobase.IFileTree.FILE) + and not hasDisallowedPath + ): + self.addModDetectionCandidate( + tree, + sanitize_folder_name(tree.name()), + "Mod Override Folder with main.xml/add.xml", + "assets/mod_overrides/FOLDERNAME/", + ) + return True + elif tree.exists("mod_overrides", mobase.IFileTree.DIRECTORY): + sourcetree = tree.find("mod_overrides", mobase.IFileTree.DIRECTORY) + if isinstance(sourcetree, mobase.IFileTree): + self.addModDetectionCandidate( + sourcetree, + sanitize_folder_name(sourcetree.name()), + "Mod Override Folder", + "assets/mod_overrides/FOLDERNAME/", ) - treefixed = 1 - return filetree + return True + elif not hasDisallowedPath: + for validFolder in self.folderList: + if tree.exists(validFolder, mobase.IFileTree.DIRECTORY): + self.addModDetectionCandidate( + tree, + sanitize_folder_name(tree.name()), + "Fallback Mod Override Folder", + "assets/mod_overrides/FOLDERNAME/", + ) + return True + return False + + def walk_entry(self, path: str, entry: mobase.FileTreeEntry): + if entry.isDir(): + if isinstance(entry, mobase.IFileTree): + self.collectModCandidates(entry) + return mobase.IFileTree.WalkReturn.CONTINUE + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + self.modDetectionCandidates = [] + newtree = filetree.createOrphanTree("Fixed Tree") + + filetree.walk(self.walk_entry, "/") + + if len(self.modDetectionCandidates) == 1: + selectedIndexes = {0} + else: + selectedIndexes = self.showModDetectionDialog() + if selectedIndexes is None: + return None + + for index in selectedIndexes: + candidate = self.modDetectionCandidates[index] + candidate["destination"] = candidate["destination"].replace( + "FOLDERNAME", + candidate["name"], + ) + print( + f"[Payday2ModDataChecker] Installing candidate: {candidate['name']} " + f"to {candidate['destination']}" + ) + if self.moveTreeContent( + candidate["tree"], newtree, candidate["destination"] + ): + self.needsNameFix = True + + return newtree if len(newtree) > 0 else filetree class Payday2Game(BasicGame): @@ -238,7 +406,7 @@ def init(self, organizer: mobase.IOrganizer) -> bool: self.dataChecker = Payday2ModDataChecker(organizer) self._register_feature(self.dataChecker) self._register_feature(Payday2ModDataContent()) - organizer.modList().onModStateChanged(self.dll_copy) + organizer.modList().onModStateChanged(self.dllCopy) return True def executables(self): @@ -253,7 +421,7 @@ def executables(self): ), ] - def dll_copy(self, mods: dict[str, mobase.ModState]): + def dllCopy(self, mods: dict[str, mobase.ModState]): game_path = self.dataDirectory().absolutePath() + "/" for key, value in mods.items(): diff --git a/games/game_raid2.py b/games/game_raid2.py index 1bdbe453..00c419e1 100644 --- a/games/game_raid2.py +++ b/games/game_raid2.py @@ -3,14 +3,38 @@ from enum import IntEnum, auto from functools import cached_property from pathlib import Path +from typing import TypedDict -from PyQt6.QtCore import QDir, QFileInfo +from PyQt6.QtCore import QDir, QFileInfo, Qt +from PyQt6.QtWidgets import ( + QDialog, + QDialogButtonBox, + QLabel, + QListWidget, + QListWidgetItem, + QVBoxLayout, +) import mobase from ..basic_game import BasicGame +def sanitize_folder_name(name: str) -> str: + # Remove invalid characters for Windows folder names + invalid_chars = '+&<>:"|?*\\/' + for char in invalid_chars: + name = name.replace(char, "") + # Remove control characters (ASCII 0-31) + name = "".join(c for c in name if ord(c) >= 32) + # Remove trailing periods and spaces + name = name.rstrip(". ") + # If name is empty after sanitization, use a default + if not name: + name = "Unnamed" + return name + + class Content(IntEnum): TEXTURE = auto() MESH = auto() @@ -61,12 +85,38 @@ def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: return list(self.content) +class ModDetectionCandidate(TypedDict): + tree: mobase.IFileTree + name: str + display: str + destination: str + + 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 + self.modDetectionCandidates: list[ModDetectionCandidate] = [] + + folderList = [ + "anims", + "core", + "effects", + "environments", + "fonts", + "gamedata", + "guis", + "lib", + "movies", + "physic_effects", + "settings", + "shaders", + "soundbanks", + "strings", + "units", + ] def move_overwrite_merge(self, source: str, destination: str): if not os.path.exists(destination): @@ -86,7 +136,7 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): return filetree: mobase.IFileTree = mod.fileTree() fixed = False - modname = mod.name() + modname = sanitize_folder_name(mod.name()) if filetree.exists("FOLDERNAME", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() old_path = os.path.join(path, "FOLDERNAME") @@ -122,11 +172,154 @@ def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str): 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 + def addModDetectionCandidate( + self, + tree: mobase.IFileTree, + name: str, + category: str, + destination: str, + ) -> None: + debug_name = name or tree.name() + debug_path = "" + if hasattr(tree, "path"): + try: + debug_path = tree.path() + except Exception: + debug_path = "" + if not debug_path and hasattr(tree, "name"): + debug_path = tree.name() + + print( + f"[RaidWW2ModDataChecker] Detected mod candidate: {debug_name} | " + f"path={debug_path} | category={category} | destination={destination}" + ) + self.modDetectionCandidates.append( + { + "tree": tree, + "name": name, + "display": f"{name} ({category})", + "destination": destination, + } + ) + + def showModDetectionDialog(self) -> set[int] | None: + if not self.modDetectionCandidates: + return set() + + dialog = QDialog() + dialog.setWindowTitle("Found Mods") + + layout = QVBoxLayout(dialog) + layout.addWidget(QLabel("Select the mods to install:")) + + listWidget = QListWidget() + listWidget.setSelectionMode(QListWidget.SelectionMode.NoSelection) + for candidate in self.modDetectionCandidates: + item = QListWidgetItem(candidate["display"]) + item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable) + item.setCheckState(Qt.CheckState.Checked) + listWidget.addItem(item) + + layout.addWidget(listWidget) + + buttonBox = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel + ) + buttonBox.accepted.connect(lambda: dialog.accept()) # type: ignore + buttonBox.rejected.connect(lambda: dialog.reject()) # type: ignore + layout.addWidget(buttonBox) + + if dialog.exec() != QDialog.DialogCode.Accepted: + return None + + selectedIndexes: set[int] = set() + for index in range(listWidget.count()): + item = listWidget.item(index) + if ( + isinstance(item, QListWidgetItem) + and item.checkState() == Qt.CheckState.Checked + ): + selectedIndexes.add(index) + + return selectedIndexes + + def collectModCandidates( + self, tree: mobase.IFileTree | mobase.FileTreeEntry + ) -> bool: + if tree.exists("mod.txt", mobase.IFileTree.FILE): + self.addModDetectionCandidate( + tree, + sanitize_folder_name(tree.name()), + "Mods Folder with mod.txt", + "mods/FOLDERNAME/", + ) + return True + elif tree.exists("main.xml", mobase.IFileTree.FILE): + self.addModDetectionCandidate( + tree, + sanitize_folder_name(tree.name()), + "Mods Folder with main.xml", + "mods/FOLDERNAME/", + ) + return True + else: + for validFolder in self.folderList: + if tree.exists(validFolder, mobase.IFileTree.DIRECTORY): + self.addModDetectionCandidate( + tree, + sanitize_folder_name(tree.name()), + "Mods Folder with game folders", + "mods/FOLDERNAME/", + ) + return True + return False + + def walk_entry(self, path: str, entry: mobase.FileTreeEntry): + if entry.isDir(): + if isinstance(entry, mobase.IFileTree): + self.collectModCandidates(entry) + return mobase.IFileTree.WalkReturn.CONTINUE + + def moveTreeContent( + self, + sourcetree: mobase.IFileTree, + targettree: mobase.IFileTree, + destination: str, + ) -> bool: + entriesToMove: list[mobase.FileTreeEntry] = [] + for e in sourcetree: + entriesToMove.append(e) + for e in entriesToMove: + targettree.move(e, destination, mobase.IFileTree.MERGE) + targettree.remove(sourcetree) + return bool(entriesToMove) + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + self.modDetectionCandidates = [] + newtree = filetree.createOrphanTree("Fixed Tree") + + filetree.walk(self.walk_entry, "/") + + if len(self.modDetectionCandidates) == 1: + selectedIndexes = {0} + else: + selectedIndexes = self.showModDetectionDialog() + if selectedIndexes is None: + return None + + for index in selectedIndexes: + candidate = self.modDetectionCandidates[index] + candidate["destination"] = candidate["destination"].replace( + "FOLDERNAME", + candidate["name"], + ) + print(f"Installing Mod: {candidate['name']} to {candidate['destination']}") + if self.moveTreeContent( + candidate["tree"], newtree, candidate["destination"] + ): + self.needsNameFix = True + + return newtree if len(newtree) > 0 else filetree class RaidWW2Game(BasicGame): From 19b1ee1f9219a9408fba9b68e4f2ac671a991d03 Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Wed, 6 May 2026 00:14:29 +0200 Subject: [PATCH 26/31] Cleanup and fixes. --- games/game_payday1.py | 145 ++++++++++++++++++++--------------------- games/game_payday2.py | 148 ++++++++++++++++++++---------------------- games/game_raid2.py | 133 +++++++++++++++---------------------- 3 files changed, 196 insertions(+), 230 deletions(-) diff --git a/games/game_payday1.py b/games/game_payday1.py index 9d9995dc..d6700459 100644 --- a/games/game_payday1.py +++ b/games/game_payday1.py @@ -86,7 +86,7 @@ def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: class ModDetectionCandidate(TypedDict): - tree: mobase.IFileTree + tree: mobase.IFileTree | mobase.FileTreeEntry name: str display: str destination: str @@ -216,24 +216,17 @@ def addModDetectionCandidate( category: str, destination: str, ) -> None: - debug_name = name or tree.name() - debug_path = "" - if hasattr(tree, "path"): - try: - debug_path = tree.path() - except Exception: - debug_path = "" - if not debug_path and hasattr(tree, "name"): - debug_path = tree.name() + tree_name = tree.name() + tree_path = tree.path() print( - f"[Payday1ModDataChecker] Detected mod candidate: {debug_name} | " - f"path={debug_path} | category={category} | destination={destination}" + f"Detected mod candidate: {tree_name} | " + f"path={tree_path} | category={category} | destination={destination}" ) self.modDetectionCandidates.append( { "tree": tree, - "name": name, + "name": tree_name, "display": f"{name} ({category})", "destination": destination, } @@ -283,72 +276,73 @@ def showModDetectionDialog(self) -> set[int] | None: def collectModCandidates( self, tree: mobase.IFileTree | mobase.FileTreeEntry ) -> bool: - hasDisallowedPath = False - disallowedFolders = {"assets", "levels", "lua"} - tree_path = tree.path() - tree_path_lower = ( - tree_path.replace("\\", "/").casefold() - if isinstance(tree_path, str) - else "" - ) - if disallowedFolders & set(tree_path_lower.split("/")): - hasDisallowedPath = True - hasFolderListSubfolder = any( - tree.exists(validFolder, mobase.IFileTree.DIRECTORY) - for validFolder in self.folderList - ) - if tree.exists("mod.txt", mobase.IFileTree.FILE) and not hasFolderListSubfolder: - self.addModDetectionCandidate( - tree, - sanitize_folder_name(tree.name()), - "Mods Folder with mod.txt", - "mods/FOLDERNAME/", - ) - return True - elif ( - tree.exists("main.xml", mobase.IFileTree.FILE) - and tree.exists("levels", mobase.IFileTree.DIRECTORY) - and not hasDisallowedPath - ): - self.addModDetectionCandidate( - tree, - sanitize_folder_name(tree.name()), - "Maps Folder with main.xml and levels folder", - "maps/FOLDERNAME/", + if isinstance(tree, mobase.IFileTree): + hasDisallowedPath = False + disallowedFolders = {"assets", "levels", "lua", "map_replacements"} + tree_path = tree.path() + tree_path_lower = tree_path.replace("\\", "/").casefold() + if disallowedFolders & set(tree_path_lower.split("/")): + hasDisallowedPath = True + hasFolderListSubfolder = any( + tree.exists(validFolder, mobase.IFileTree.DIRECTORY) + for validFolder in self.folderList ) - return True - elif ( - tree.exists("main.xml", mobase.IFileTree.FILE) - or tree.exists("add.xml", mobase.IFileTree.FILE) - and not hasDisallowedPath - ): - self.addModDetectionCandidate( - tree, - sanitize_folder_name(tree.name()), - "Mod Override Folder with main.xml/add.xml", - "assets/mod_overrides/FOLDERNAME/", - ) - return True - elif tree.exists("mod_overrides", mobase.IFileTree.DIRECTORY): - sourcetree = tree.find("mod_overrides", mobase.IFileTree.DIRECTORY) - if isinstance(sourcetree, mobase.IFileTree): + if ( + tree.exists("mod.txt", mobase.IFileTree.FILE) + and not hasFolderListSubfolder + ): + self.addModDetectionCandidate( + tree, + sanitize_folder_name(tree.name()), + "Mods Folder with mod.txt", + "mods/FOLDERNAME/", + ) + return True + elif ( + tree.exists("main.xml", mobase.IFileTree.FILE) + and tree.exists("levels", mobase.IFileTree.DIRECTORY) + and not hasDisallowedPath + ): self.addModDetectionCandidate( - sourcetree, - sanitize_folder_name(sourcetree.name()), - "Mod Override Folder", - "assets/mod_overrides/FOLDERNAME/", + tree, + sanitize_folder_name(tree.name()), + "Maps Folder with main.xml and levels folder", + "maps/FOLDERNAME/", ) return True - elif not hasDisallowedPath: - for validFolder in self.folderList: - if tree.exists(validFolder, mobase.IFileTree.DIRECTORY): + elif ( + tree.exists("main.xml", mobase.IFileTree.FILE) + or tree.exists("add.xml", mobase.IFileTree.FILE) + or tree.exists("supermod.xml", mobase.IFileTree.FILE) + ): + if not hasDisallowedPath: self.addModDetectionCandidate( tree, sanitize_folder_name(tree.name()), - "Fallback Mod Override Folder", + "Mod Override Folder with main.xml/add.xml", "assets/mod_overrides/FOLDERNAME/", ) return True + elif tree.exists("mod_overrides", mobase.IFileTree.DIRECTORY): + sourcetree = tree.find("mod_overrides", mobase.IFileTree.DIRECTORY) + if isinstance(sourcetree, mobase.IFileTree): + self.addModDetectionCandidate( + sourcetree, + sanitize_folder_name(sourcetree.name()), + "Mod Override Folder", + "assets/mod_overrides/FOLDERNAME/", + ) + return True + elif not hasDisallowedPath: + for validFolder in self.folderList: + if tree.exists(validFolder, mobase.IFileTree.DIRECTORY): + self.addModDetectionCandidate( + tree, + sanitize_folder_name(tree.name()), + "Fallback Mod Override Folder", + "assets/mod_overrides/FOLDERNAME/", + ) + return True return False def walk_entry(self, path: str, entry: mobase.FileTreeEntry): @@ -376,11 +370,14 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: "FOLDERNAME", candidate["name"], ) - print(f"Installing Mod: {candidate['name']} to {candidate['destination']}") - if self.moveTreeContent( - candidate["tree"], newtree, candidate["destination"] - ): - self.needsNameFix = True + if isinstance(candidate["tree"], mobase.IFileTree): + print( + f"Installing Mod: {candidate['name']} to {candidate['destination']}" + ) + if self.moveTreeContent( + candidate["tree"], newtree, candidate["destination"] + ): + self.needsNameFix = True return newtree if len(newtree) > 0 else filetree diff --git a/games/game_payday2.py b/games/game_payday2.py index 688668a2..86b709f5 100644 --- a/games/game_payday2.py +++ b/games/game_payday2.py @@ -86,7 +86,7 @@ def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: class ModDetectionCandidate(TypedDict): - tree: mobase.IFileTree + tree: mobase.IFileTree | mobase.FileTreeEntry name: str display: str destination: str @@ -216,24 +216,17 @@ def addModDetectionCandidate( category: str, destination: str, ) -> None: - debug_name = name or tree.name() - debug_path = "" - if hasattr(tree, "path"): - try: - debug_path = tree.path() - except Exception: - debug_path = "" - if not debug_path and hasattr(tree, "name"): - debug_path = tree.name() + tree_name = tree.name() + tree_path = tree.path() print( - f"[Payday2ModDataChecker] Detected mod candidate: {debug_name} | " - f"path={debug_path} | category={category} | destination={destination}" + f"Detected mod candidate: {tree_name} | " + f"path={tree_path} | category={category} | destination={destination}" ) self.modDetectionCandidates.append( { "tree": tree, - "name": name, + "name": tree_name, "display": f"{name} ({category})", "destination": destination, } @@ -283,72 +276,73 @@ def showModDetectionDialog(self) -> set[int] | None: def collectModCandidates( self, tree: mobase.IFileTree | mobase.FileTreeEntry ) -> bool: - hasDisallowedPath = False - disallowedFolders = {"assets", "levels", "lua"} - tree_path = tree.path() - tree_path_lower = ( - tree_path.replace("\\", "/").casefold() - if isinstance(tree_path, str) - else "" - ) - if disallowedFolders & set(tree_path_lower.split("/")): - hasDisallowedPath = True - hasFolderListSubfolder = any( - tree.exists(validFolder, mobase.IFileTree.DIRECTORY) - for validFolder in self.folderList - ) - if tree.exists("mod.txt", mobase.IFileTree.FILE) and not hasFolderListSubfolder: - self.addModDetectionCandidate( - tree, - sanitize_folder_name(tree.name()), - "Mods Folder with mod.txt", - "mods/FOLDERNAME/", - ) - return True - elif ( - tree.exists("main.xml", mobase.IFileTree.FILE) - and tree.exists("levels", mobase.IFileTree.DIRECTORY) - and not hasDisallowedPath - ): - self.addModDetectionCandidate( - tree, - sanitize_folder_name(tree.name()), - "Maps Folder with main.xml and levels folder", - "maps/FOLDERNAME/", + if isinstance(tree, mobase.IFileTree): + hasDisallowedPath = False + disallowedFolders = {"assets", "levels", "lua", "map_replacements"} + tree_path = tree.path() + tree_path_lower = tree_path.replace("\\", "/").casefold() + if disallowedFolders & set(tree_path_lower.split("/")): + hasDisallowedPath = True + hasFolderListSubfolder = any( + tree.exists(validFolder, mobase.IFileTree.DIRECTORY) + for validFolder in self.folderList ) - return True - elif ( - tree.exists("main.xml", mobase.IFileTree.FILE) - or tree.exists("add.xml", mobase.IFileTree.FILE) - and not hasDisallowedPath - ): - self.addModDetectionCandidate( - tree, - sanitize_folder_name(tree.name()), - "Mod Override Folder with main.xml/add.xml", - "assets/mod_overrides/FOLDERNAME/", - ) - return True - elif tree.exists("mod_overrides", mobase.IFileTree.DIRECTORY): - sourcetree = tree.find("mod_overrides", mobase.IFileTree.DIRECTORY) - if isinstance(sourcetree, mobase.IFileTree): + if ( + tree.exists("mod.txt", mobase.IFileTree.FILE) + and not hasFolderListSubfolder + ): self.addModDetectionCandidate( - sourcetree, - sanitize_folder_name(sourcetree.name()), - "Mod Override Folder", - "assets/mod_overrides/FOLDERNAME/", + tree, + sanitize_folder_name(tree.name()), + "Mods Folder with mod.txt", + "mods/FOLDERNAME/", ) return True - elif not hasDisallowedPath: - for validFolder in self.folderList: - if tree.exists(validFolder, mobase.IFileTree.DIRECTORY): + elif ( + tree.exists("main.xml", mobase.IFileTree.FILE) + and tree.exists("levels", mobase.IFileTree.DIRECTORY) + and not hasDisallowedPath + ): + self.addModDetectionCandidate( + tree, + sanitize_folder_name(tree.name()), + "Maps Folder with main.xml and levels folder", + "maps/FOLDERNAME/", + ) + return True + elif ( + tree.exists("main.xml", mobase.IFileTree.FILE) + or tree.exists("add.xml", mobase.IFileTree.FILE) + or tree.exists("supermod.xml", mobase.IFileTree.FILE) + ): + if not hasDisallowedPath: self.addModDetectionCandidate( tree, sanitize_folder_name(tree.name()), - "Fallback Mod Override Folder", + "Mod Override Folder with main.xml/add.xml", "assets/mod_overrides/FOLDERNAME/", ) return True + elif tree.exists("mod_overrides", mobase.IFileTree.DIRECTORY): + sourcetree = tree.find("mod_overrides", mobase.IFileTree.DIRECTORY) + if isinstance(sourcetree, mobase.IFileTree): + self.addModDetectionCandidate( + sourcetree, + sanitize_folder_name(sourcetree.name()), + "Mod Override Folder", + "assets/mod_overrides/FOLDERNAME/", + ) + return True + elif not hasDisallowedPath: + for validFolder in self.folderList: + if tree.exists(validFolder, mobase.IFileTree.DIRECTORY): + self.addModDetectionCandidate( + tree, + sanitize_folder_name(tree.name()), + "Fallback Mod Override Folder", + "assets/mod_overrides/FOLDERNAME/", + ) + return True return False def walk_entry(self, path: str, entry: mobase.FileTreeEntry): @@ -376,14 +370,14 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: "FOLDERNAME", candidate["name"], ) - print( - f"[Payday2ModDataChecker] Installing candidate: {candidate['name']} " - f"to {candidate['destination']}" - ) - if self.moveTreeContent( - candidate["tree"], newtree, candidate["destination"] - ): - self.needsNameFix = True + if isinstance(candidate["tree"], mobase.IFileTree): + print( + f"Installing Mod: {candidate['name']} to {candidate['destination']}" + ) + if self.moveTreeContent( + candidate["tree"], newtree, candidate["destination"] + ): + self.needsNameFix = True return newtree if len(newtree) > 0 else filetree diff --git a/games/game_raid2.py b/games/game_raid2.py index 00c419e1..94662198 100644 --- a/games/game_raid2.py +++ b/games/game_raid2.py @@ -86,17 +86,15 @@ def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: class ModDetectionCandidate(TypedDict): - tree: mobase.IFileTree + tree: mobase.IFileTree | mobase.FileTreeEntry name: str display: str - destination: str 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 self.modDetectionCandidates: list[ModDetectionCandidate] = [] @@ -110,6 +108,7 @@ def __init__(self, organizer: mobase.IOrganizer): "guis", "lib", "movies", + "levels", "physic_effects", "settings", "shaders", @@ -118,7 +117,7 @@ def __init__(self, organizer: mobase.IOrganizer): "units", ] - def move_overwrite_merge(self, source: str, destination: str): + def moveOverwriteMerge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -128,30 +127,25 @@ def move_overwrite_merge(self, source: str, destination: str): 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) + self.moveOverwriteMerge(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 = sanitize_folder_name(mod.name()) - if filetree.exists("FOLDERNAME", mobase.IFileTree.DIRECTORY): - path = mod.absolutePath() - old_path = os.path.join(path, "FOLDERNAME") - new_path = os.path.join(path, f"{modname}") - 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 + for e in filetree: + if isinstance(e, mobase.IFileTree): + if e.exists("mod.txt", mobase.IFileTree.FILE): + return mobase.ModDataChecker.VALID + if e.exists("mod.xml", mobase.IFileTree.FILE): + return mobase.ModDataChecker.VALID + if e.exists("main.xml", mobase.IFileTree.FILE): + return mobase.ModDataChecker.VALID + if e.exists("supermod.xml", mobase.IFileTree.FILE): + return mobase.ModDataChecker.VALID + for folder in self.folderList: + if e.exists(folder, mobase.IFileTree.DIRECTORY): + return mobase.ModDataChecker.VALID return mobase.ModDataChecker.FIXABLE def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): @@ -177,29 +171,17 @@ def addModDetectionCandidate( tree: mobase.IFileTree, name: str, category: str, - destination: str, ) -> None: - debug_name = name or tree.name() - debug_path = "" - if hasattr(tree, "path"): - try: - debug_path = tree.path() - except Exception: - debug_path = "" - if not debug_path and hasattr(tree, "name"): - debug_path = tree.name() + tree_name = tree.name() + tree_path = tree.path() print( - f"[RaidWW2ModDataChecker] Detected mod candidate: {debug_name} | " - f"path={debug_path} | category={category} | destination={destination}" + f"Detected mod candidate: {tree_name} | " + f"path={tree_path} | category={category}" ) + self.modDetectionCandidates.append( - { - "tree": tree, - "name": name, - "display": f"{name} ({category})", - "destination": destination, - } + {"tree": tree, "name": tree_name, "display": f"{name} ({category})"} ) def showModDetectionDialog(self) -> set[int] | None: @@ -246,32 +228,30 @@ def showModDetectionDialog(self) -> set[int] | None: def collectModCandidates( self, tree: mobase.IFileTree | mobase.FileTreeEntry ) -> bool: - if tree.exists("mod.txt", mobase.IFileTree.FILE): - self.addModDetectionCandidate( - tree, - sanitize_folder_name(tree.name()), - "Mods Folder with mod.txt", - "mods/FOLDERNAME/", - ) - return True - elif tree.exists("main.xml", mobase.IFileTree.FILE): - self.addModDetectionCandidate( - tree, - sanitize_folder_name(tree.name()), - "Mods Folder with main.xml", - "mods/FOLDERNAME/", - ) - return True - else: - for validFolder in self.folderList: - if tree.exists(validFolder, mobase.IFileTree.DIRECTORY): - self.addModDetectionCandidate( - tree, - sanitize_folder_name(tree.name()), - "Mods Folder with game folders", - "mods/FOLDERNAME/", - ) - return True + if isinstance(tree, mobase.IFileTree): + if tree.exists("mod.txt", mobase.IFileTree.FILE): + self.addModDetectionCandidate( + tree, + sanitize_folder_name(tree.name()), + "Mods Folder with mod.txt", + ) + return True + elif tree.exists("main.xml", mobase.IFileTree.FILE): + self.addModDetectionCandidate( + tree, + sanitize_folder_name(tree.name()), + "Mods Folder with main.xml", + ) + return True + else: + for validFolder in self.folderList: + if tree.exists(validFolder, mobase.IFileTree.DIRECTORY): + self.addModDetectionCandidate( + tree, + sanitize_folder_name(tree.name()), + "Mods Folder with game folders", + ) + return True return False def walk_entry(self, path: str, entry: mobase.FileTreeEntry): @@ -309,15 +289,10 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: for index in selectedIndexes: candidate = self.modDetectionCandidates[index] - candidate["destination"] = candidate["destination"].replace( - "FOLDERNAME", - candidate["name"], - ) - print(f"Installing Mod: {candidate['name']} to {candidate['destination']}") - if self.moveTreeContent( - candidate["tree"], newtree, candidate["destination"] - ): - self.needsNameFix = True + if isinstance(candidate["tree"], mobase.IFileTree): + print(f"Installing Mod: {candidate['name']}") + if self.moveTreeContent(candidate["tree"], newtree, ""): + self.needsNameFix = True return newtree if len(newtree) > 0 else filetree @@ -339,7 +314,7 @@ def init(self, organizer: mobase.IOrganizer) -> bool: self.dataChecker = RaidWW2ModDataChecker(organizer) self._register_feature(self.dataChecker) self._register_feature(RaidWW2ModDataContent()) - organizer.modList().onModStateChanged(self.dll_copy) + organizer.modList().onModStateChanged(self.dllCopy) return True def executables(self): @@ -350,7 +325,7 @@ def executables(self): ) ] - def dll_copy(self, mods: dict[str, mobase.ModState]): + def dllCopy(self, mods: dict[str, mobase.ModState]): game_path = self.dataDirectory().absolutePath() + "/" for key, value in mods.items(): @@ -402,7 +377,7 @@ def iniFiles(self): return ["renderer_settings.xml"] def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): - base_data_dir = self.dataDirectory().absolutePath() + base_data_dir = self.gameDirectory().absolutePath() modsDirectory = QDir(base_data_dir + "/" + self.GameDataPath) From 1dc56f793230814a1d79862a1110f932458b8ded Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Wed, 6 May 2026 01:47:03 +0200 Subject: [PATCH 27/31] Removed Redundant Code, Fixes for Raid Mod Installs --- .gitignore | 9 +++ games/game_payday1.py | 117 ++++++++++++++---------------------- games/game_payday2.py | 109 +++++++++++++-------------------- games/game_raid2.py | 136 +++++++++++++++++++++++------------------- 4 files changed, 169 insertions(+), 202 deletions(-) diff --git a/.gitignore b/.gitignore index 910d2b5a..14814532 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,12 @@ venv mob.log vsbuild lib +/.vs/modorganizer-basic_games/CopilotIndices +/pyproject.toml +/pyrightconfig.json +/.vs/VSWorkspaceState.json +/.vs/slnx.sqlite +/.vs/modorganizer-basic_games.slnx/copilot-chat/5bceb36a/sessions +/.vs/modorganizer-basic_games.slnx/FileContentIndex +*.json +*.wsuo diff --git a/games/game_payday1.py b/games/game_payday1.py index d6700459..dc08213d 100644 --- a/games/game_payday1.py +++ b/games/game_payday1.py @@ -20,7 +20,7 @@ from ..basic_game import BasicGame -def sanitize_folder_name(name: str) -> str: +def sanitizeFolderName(name: str) -> str: # Remove invalid characters for Windows folder names invalid_chars = '+&<>:"|?*\\/' for char in invalid_chars: @@ -136,7 +136,7 @@ def fixInstalledMod(self, mod: mobase.IModInterface): return filetree: mobase.IFileTree = mod.fileTree() fixed = False - modname = sanitize_folder_name(mod.name()) + modname = sanitizeFolderName(mod.name()) if filetree.exists("mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() old_path = os.path.join(path, "mods/FOLDERNAME") @@ -181,20 +181,6 @@ def dataLooksValid( return mobase.ModDataChecker.VALID return mobase.ModDataChecker.FIXABLE - def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): - for branch in filetree: - if isinstance(branch, mobase.IFileTree): - for e in branch: - if e.name() == name: - return True - return False - - def firstTree(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: - for e in filetree: - if isinstance(e, mobase.IFileTree) and e.isDir(): - return e - return None - def moveTreeContent( self, sourcetree: mobase.IFileTree, @@ -277,6 +263,7 @@ def collectModCandidates( self, tree: mobase.IFileTree | mobase.FileTreeEntry ) -> bool: if isinstance(tree, mobase.IFileTree): + sanitizedName = sanitizeFolderName(tree.name()) hasDisallowedPath = False disallowedFolders = {"assets", "levels", "lua", "map_replacements"} tree_path = tree.path() @@ -287,65 +274,53 @@ def collectModCandidates( tree.exists(validFolder, mobase.IFileTree.DIRECTORY) for validFolder in self.folderList ) - if ( - tree.exists("mod.txt", mobase.IFileTree.FILE) - and not hasFolderListSubfolder - ): - self.addModDetectionCandidate( - tree, - sanitize_folder_name(tree.name()), - "Mods Folder with mod.txt", - "mods/FOLDERNAME/", - ) - return True - elif ( - tree.exists("main.xml", mobase.IFileTree.FILE) - and tree.exists("levels", mobase.IFileTree.DIRECTORY) - and not hasDisallowedPath - ): - self.addModDetectionCandidate( - tree, - sanitize_folder_name(tree.name()), - "Maps Folder with main.xml and levels folder", - "maps/FOLDERNAME/", - ) - return True - elif ( - tree.exists("main.xml", mobase.IFileTree.FILE) - or tree.exists("add.xml", mobase.IFileTree.FILE) - or tree.exists("supermod.xml", mobase.IFileTree.FILE) - ): - if not hasDisallowedPath: + if not hasDisallowedPath: + if ( + tree.exists("mod.txt", mobase.IFileTree.FILE) + and not hasFolderListSubfolder + ): + self.addModDetectionCandidate( + tree, + sanitizeFolderName(tree.name()), + "BLT", + "mods/" + sanitizedName + "/", + ) + return True + elif tree.exists("main.xml", mobase.IFileTree.FILE) and tree.exists( + "levels", mobase.IFileTree.DIRECTORY + ): self.addModDetectionCandidate( tree, - sanitize_folder_name(tree.name()), - "Mod Override Folder with main.xml/add.xml", - "assets/mod_overrides/FOLDERNAME/", + sanitizeFolderName(tree.name()), + "Map Core", + "maps/" + sanitizedName + "/", ) return True - elif tree.exists("mod_overrides", mobase.IFileTree.DIRECTORY): - sourcetree = tree.find("mod_overrides", mobase.IFileTree.DIRECTORY) - if isinstance(sourcetree, mobase.IFileTree): + elif ( + tree.exists("main.xml", mobase.IFileTree.FILE) + or tree.exists("add.xml", mobase.IFileTree.FILE) + or tree.exists("supermod.xml", mobase.IFileTree.FILE) + ): self.addModDetectionCandidate( - sourcetree, - sanitize_folder_name(sourcetree.name()), - "Mod Override Folder", - "assets/mod_overrides/FOLDERNAME/", + tree, + sanitizeFolderName(tree.name()), + "SuperBLT", + "assets/mod_overrides/" + sanitizedName + "/", ) return True - elif not hasDisallowedPath: - for validFolder in self.folderList: - if tree.exists(validFolder, mobase.IFileTree.DIRECTORY): - self.addModDetectionCandidate( - tree, - sanitize_folder_name(tree.name()), - "Fallback Mod Override Folder", - "assets/mod_overrides/FOLDERNAME/", - ) - return True + else: + for validFolder in self.folderList: + if tree.exists(validFolder, mobase.IFileTree.DIRECTORY): + self.addModDetectionCandidate( + tree, + sanitizeFolderName(tree.name()), + "Override", + "assets/mod_overrides/" + sanitizedName + "/", + ) + return True return False - def walk_entry(self, path: str, entry: mobase.FileTreeEntry): + def walkEntry(self, path: str, entry: mobase.FileTreeEntry): if entry.isDir(): if isinstance(entry, mobase.IFileTree): self.collectModCandidates(entry) @@ -355,7 +330,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: self.modDetectionCandidates = [] newtree = filetree.createOrphanTree("Fixed Tree") - filetree.walk(self.walk_entry, "/") + filetree.walk(self.walkEntry, "/") if len(self.modDetectionCandidates) == 1: selectedIndexes = {0} @@ -366,10 +341,6 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: for index in selectedIndexes: candidate = self.modDetectionCandidates[index] - candidate["destination"] = candidate["destination"].replace( - "FOLDERNAME", - candidate["name"], - ) if isinstance(candidate["tree"], mobase.IFileTree): print( f"Installing Mod: {candidate['name']} to {candidate['destination']}" @@ -404,7 +375,7 @@ def init(self, organizer: mobase.IOrganizer) -> bool: self.dataChecker = Payday1ModDataChecker(organizer) self._register_feature(self.dataChecker) self._register_feature(Payday1ModDataContent()) - organizer.modList().onModStateChanged(self.dll_copy) + organizer.modList().onModStateChanged(self.dllCopy) return True def executables(self): @@ -415,7 +386,7 @@ def executables(self): ) ] - def dll_copy(self, mods: dict[str, mobase.ModState]): + def dllCopy(self, mods: dict[str, mobase.ModState]): game_path = self.dataDirectory().absolutePath() + "/" for key, value in mods.items(): diff --git a/games/game_payday2.py b/games/game_payday2.py index 86b709f5..e91f10f4 100644 --- a/games/game_payday2.py +++ b/games/game_payday2.py @@ -20,7 +20,7 @@ from ..basic_game import BasicGame -def sanitize_folder_name(name: str) -> str: +def sanitizeFolderName(name: str) -> str: # Remove invalid characters for Windows folder names invalid_chars = '+&<>:"|?*\\/' for char in invalid_chars: @@ -136,7 +136,7 @@ def fixInstalledMod(self, mod: mobase.IModInterface): return filetree: mobase.IFileTree = mod.fileTree() fixed = False - modname = sanitize_folder_name(mod.name()) + modname = sanitizeFolderName(mod.name()) if filetree.exists("mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() old_path = os.path.join(path, "mods/FOLDERNAME") @@ -181,20 +181,6 @@ def dataLooksValid( return mobase.ModDataChecker.VALID return mobase.ModDataChecker.FIXABLE - def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): - for branch in filetree: - if isinstance(branch, mobase.IFileTree): - for e in branch: - if e.name() == name: - return True - return False - - def firstTree(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: - for e in filetree: - if isinstance(e, mobase.IFileTree) and e.isDir(): - return e - return None - def moveTreeContent( self, sourcetree: mobase.IFileTree, @@ -277,6 +263,7 @@ def collectModCandidates( self, tree: mobase.IFileTree | mobase.FileTreeEntry ) -> bool: if isinstance(tree, mobase.IFileTree): + sanitizedName = sanitizeFolderName(tree.name()) hasDisallowedPath = False disallowedFolders = {"assets", "levels", "lua", "map_replacements"} tree_path = tree.path() @@ -287,62 +274,50 @@ def collectModCandidates( tree.exists(validFolder, mobase.IFileTree.DIRECTORY) for validFolder in self.folderList ) - if ( - tree.exists("mod.txt", mobase.IFileTree.FILE) - and not hasFolderListSubfolder - ): - self.addModDetectionCandidate( - tree, - sanitize_folder_name(tree.name()), - "Mods Folder with mod.txt", - "mods/FOLDERNAME/", - ) - return True - elif ( - tree.exists("main.xml", mobase.IFileTree.FILE) - and tree.exists("levels", mobase.IFileTree.DIRECTORY) - and not hasDisallowedPath - ): - self.addModDetectionCandidate( - tree, - sanitize_folder_name(tree.name()), - "Maps Folder with main.xml and levels folder", - "maps/FOLDERNAME/", - ) - return True - elif ( - tree.exists("main.xml", mobase.IFileTree.FILE) - or tree.exists("add.xml", mobase.IFileTree.FILE) - or tree.exists("supermod.xml", mobase.IFileTree.FILE) - ): - if not hasDisallowedPath: + if not hasDisallowedPath: + if ( + tree.exists("mod.txt", mobase.IFileTree.FILE) + and not hasFolderListSubfolder + ): + self.addModDetectionCandidate( + tree, + sanitizeFolderName(tree.name()), + "SuperBLT", + "mods/" + sanitizedName + "/", + ) + return True + elif tree.exists("main.xml", mobase.IFileTree.FILE) and tree.exists( + "levels", mobase.IFileTree.DIRECTORY + ): self.addModDetectionCandidate( tree, - sanitize_folder_name(tree.name()), - "Mod Override Folder with main.xml/add.xml", - "assets/mod_overrides/FOLDERNAME/", + sanitizeFolderName(tree.name()), + "Map Core", + "maps/" + sanitizedName + "/", ) return True - elif tree.exists("mod_overrides", mobase.IFileTree.DIRECTORY): - sourcetree = tree.find("mod_overrides", mobase.IFileTree.DIRECTORY) - if isinstance(sourcetree, mobase.IFileTree): + elif ( + tree.exists("main.xml", mobase.IFileTree.FILE) + or tree.exists("add.xml", mobase.IFileTree.FILE) + or tree.exists("supermod.xml", mobase.IFileTree.FILE) + ): self.addModDetectionCandidate( - sourcetree, - sanitize_folder_name(sourcetree.name()), - "Mod Override Folder", - "assets/mod_overrides/FOLDERNAME/", + tree, + sanitizeFolderName(tree.name()), + "Beard Lib", + "assets/mod_overrides/" + sanitizedName + "/", ) return True - elif not hasDisallowedPath: - for validFolder in self.folderList: - if tree.exists(validFolder, mobase.IFileTree.DIRECTORY): - self.addModDetectionCandidate( - tree, - sanitize_folder_name(tree.name()), - "Fallback Mod Override Folder", - "assets/mod_overrides/FOLDERNAME/", - ) - return True + else: + for validFolder in self.folderList: + if tree.exists(validFolder, mobase.IFileTree.DIRECTORY): + self.addModDetectionCandidate( + tree, + sanitizeFolderName(tree.name()), + "Override", + "assets/mod_overrides/" + sanitizedName + "/", + ) + return True return False def walk_entry(self, path: str, entry: mobase.FileTreeEntry): @@ -366,10 +341,6 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: for index in selectedIndexes: candidate = self.modDetectionCandidates[index] - candidate["destination"] = candidate["destination"].replace( - "FOLDERNAME", - candidate["name"], - ) if isinstance(candidate["tree"], mobase.IFileTree): print( f"Installing Mod: {candidate['name']} to {candidate['destination']}" diff --git a/games/game_raid2.py b/games/game_raid2.py index 94662198..afefc955 100644 --- a/games/game_raid2.py +++ b/games/game_raid2.py @@ -20,7 +20,7 @@ from ..basic_game import BasicGame -def sanitize_folder_name(name: str) -> str: +def sanitizeFolderName(name: str) -> str: # Remove invalid characters for Windows folder names invalid_chars = '+&<>:"|?*\\/' for char in invalid_chars: @@ -89,12 +89,14 @@ class ModDetectionCandidate(TypedDict): tree: mobase.IFileTree | mobase.FileTreeEntry name: str display: str + destination: str class RaidWW2ModDataChecker(mobase.ModDataChecker): def __init__(self, organizer: mobase.IOrganizer): super().__init__() self.organizer: mobase.IOrganizer = organizer + self.organizer.modList().onModInstalled(self.fixInstalledMod) self.needsNameFix = False self.modDetectionCandidates: list[ModDetectionCandidate] = [] @@ -130,47 +132,35 @@ def moveOverwriteMerge(self, source: str, destination: str): self.moveOverwriteMerge(s_item, d_item) os.rmdir(source) + def fixInstalledMod(self, mod: mobase.IModInterface): + if not self.needsNameFix: + return + filetree: mobase.IFileTree = mod.fileTree() + fixed = False + modname = sanitizeFolderName(mod.name()) + if filetree.exists("mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): + path = mod.absolutePath() + old_path = os.path.join(path, "mods/FOLDERNAME") + new_path = os.path.join(path, f"mods/{modname}") + self.moveOverwriteMerge(old_path, new_path) + fixed = True + if not fixed: + return + self.needsNameFix = False + def dataLooksValid( self, filetree: mobase.IFileTree ) -> mobase.ModDataChecker.CheckReturn: - for e in filetree: - if isinstance(e, mobase.IFileTree): - if e.exists("mod.txt", mobase.IFileTree.FILE): - return mobase.ModDataChecker.VALID - if e.exists("mod.xml", mobase.IFileTree.FILE): - return mobase.ModDataChecker.VALID - if e.exists("main.xml", mobase.IFileTree.FILE): - return mobase.ModDataChecker.VALID - if e.exists("supermod.xml", mobase.IFileTree.FILE): - return mobase.ModDataChecker.VALID - for folder in self.folderList: - if e.exists(folder, mobase.IFileTree.DIRECTORY): - return mobase.ModDataChecker.VALID + 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 isinstance(branch, mobase.IFileTree): - for e in branch: - if 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: - entriesToMove.append(e) - for e in entriesToMove: - filetree.move(e, toMoveTo, mobase.IFileTree.MERGE) - retVal = 1 - return retVal - def addModDetectionCandidate( self, tree: mobase.IFileTree, name: str, category: str, + destination: str, ) -> None: tree_name = tree.name() tree_path = tree.path() @@ -181,7 +171,12 @@ def addModDetectionCandidate( ) self.modDetectionCandidates.append( - {"tree": tree, "name": tree_name, "display": f"{name} ({category})"} + { + "tree": tree, + "name": tree_name, + "display": f"{name} ({category})", + "destination": destination, + } ) def showModDetectionDialog(self) -> set[int] | None: @@ -228,30 +223,47 @@ def showModDetectionDialog(self) -> set[int] | None: def collectModCandidates( self, tree: mobase.IFileTree | mobase.FileTreeEntry ) -> bool: - if isinstance(tree, mobase.IFileTree): - if tree.exists("mod.txt", mobase.IFileTree.FILE): - self.addModDetectionCandidate( - tree, - sanitize_folder_name(tree.name()), - "Mods Folder with mod.txt", - ) - return True - elif tree.exists("main.xml", mobase.IFileTree.FILE): - self.addModDetectionCandidate( - tree, - sanitize_folder_name(tree.name()), - "Mods Folder with main.xml", - ) - return True - else: - for validFolder in self.folderList: - if tree.exists(validFolder, mobase.IFileTree.DIRECTORY): - self.addModDetectionCandidate( - tree, - sanitize_folder_name(tree.name()), - "Mods Folder with game folders", - ) - return True + hasDisallowedPath = False + disallowedFolders = {"assets", "lua", "map_replacements"} + tree_path = tree.path() + tree_path_lower = tree_path.replace("\\", "/").casefold() + if disallowedFolders & set(tree_path_lower.split("/")): + hasDisallowedPath = True + if not hasDisallowedPath: + if isinstance(tree, mobase.IFileTree): + sanitizedName = sanitizeFolderName(tree.name()) + if ( + tree.exists("mod.txt", mobase.IFileTree.FILE) + or tree.exists("mod.xml", mobase.IFileTree.FILE) + or tree.exists("supermod.xml", mobase.IFileTree.FILE) + ): + self.addModDetectionCandidate( + tree, + sanitizedName, + "SuperBLT", + "mods/" + sanitizedName + "/", + ) + return True + elif tree.exists("main.xml", mobase.IFileTree.FILE) or tree.exists( + "add.xml", mobase.IFileTree.FILE + ): + self.addModDetectionCandidate( + tree, + sanitizeFolderName(tree.name()), + "Beard Lib", + "mods/" + sanitizedName + "/", + ) + return True + else: + for validFolder in self.folderList: + if tree.exists(validFolder, mobase.IFileTree.DIRECTORY): + self.addModDetectionCandidate( + tree, + sanitizeFolderName(tree.name()), + "Override", + "mods/" + sanitizedName + "/", + ) + return True return False def walk_entry(self, path: str, entry: mobase.FileTreeEntry): @@ -290,8 +302,12 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: for index in selectedIndexes: candidate = self.modDetectionCandidates[index] if isinstance(candidate["tree"], mobase.IFileTree): - print(f"Installing Mod: {candidate['name']}") - if self.moveTreeContent(candidate["tree"], newtree, ""): + print( + f"Installing Mod: {candidate['name']} to {candidate['destination']}" + ) + if self.moveTreeContent( + candidate["tree"], newtree, candidate["destination"] + ): self.needsNameFix = True return newtree if len(newtree) > 0 else filetree @@ -305,7 +321,7 @@ class RaidWW2Game(BasicGame): GameShortName = "raidww2" GameSteamId = 414740 GameBinary = "raid_win64_release.exe" - GameDataPath = "mods" + GameDataPath = "%GAME_PATH%" GameDocumentsDirectory = "%LOCALAPPDATA%/RAID WW2" _forced_libraries = ["IPHLPAPI.dll", "WSOCK32.dll"] From 23f7facb57c36fe3d7aeb539051f2b3f1e234fff Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Wed, 6 May 2026 02:02:25 +0200 Subject: [PATCH 28/31] Allowed for installation of mods without subfolders. Finalized Raid Install Process. --- games/game_payday1.py | 44 +++++++++++++++++------------------ games/game_payday2.py | 53 +++++++++++++++++++++---------------------- games/game_raid2.py | 44 +++++++++++++++++------------------ 3 files changed, 70 insertions(+), 71 deletions(-) diff --git a/games/game_payday1.py b/games/game_payday1.py index dc08213d..b69c997c 100644 --- a/games/game_payday1.py +++ b/games/game_payday1.py @@ -20,21 +20,6 @@ from ..basic_game import BasicGame -def sanitizeFolderName(name: str) -> str: - # Remove invalid characters for Windows folder names - invalid_chars = '+&<>:"|?*\\/' - for char in invalid_chars: - name = name.replace(char, "") - # Remove control characters (ASCII 0-31) - name = "".join(c for c in name if ord(c) >= 32) - # Remove trailing periods and spaces - name = name.rstrip(". ") - # If name is empty after sanitization, use a default - if not name: - name = "Unnamed" - return name - - class Content(IntEnum): TEXTURE = auto() MESH = auto() @@ -118,6 +103,21 @@ def __init__(self, organizer: mobase.IOrganizer): "units", ] + def sanitizeFolderName(self, name: str) -> str: + # Remove invalid characters for Windows folder names + invalid_chars = '+&<>:"|?*\\/' + for char in invalid_chars: + name = name.replace(char, "") + # Remove control characters (ASCII 0-31) + name = "".join(c for c in name if ord(c) >= 32) + # Remove trailing periods and spaces + name = name.rstrip(". ") + # If name is empty after sanitization, use a default + if not name: + name = "FOLDERNAME" + self.needsNameFix = True + return name + def moveOverwriteMerge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) @@ -136,7 +136,7 @@ def fixInstalledMod(self, mod: mobase.IModInterface): return filetree: mobase.IFileTree = mod.fileTree() fixed = False - modname = sanitizeFolderName(mod.name()) + modname = self.sanitizeFolderName(mod.name()) if filetree.exists("mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() old_path = os.path.join(path, "mods/FOLDERNAME") @@ -263,7 +263,7 @@ def collectModCandidates( self, tree: mobase.IFileTree | mobase.FileTreeEntry ) -> bool: if isinstance(tree, mobase.IFileTree): - sanitizedName = sanitizeFolderName(tree.name()) + sanitizedName = self.sanitizeFolderName(tree.name()) hasDisallowedPath = False disallowedFolders = {"assets", "levels", "lua", "map_replacements"} tree_path = tree.path() @@ -281,7 +281,7 @@ def collectModCandidates( ): self.addModDetectionCandidate( tree, - sanitizeFolderName(tree.name()), + self.sanitizeFolderName(tree.name()), "BLT", "mods/" + sanitizedName + "/", ) @@ -291,7 +291,7 @@ def collectModCandidates( ): self.addModDetectionCandidate( tree, - sanitizeFolderName(tree.name()), + self.sanitizeFolderName(tree.name()), "Map Core", "maps/" + sanitizedName + "/", ) @@ -303,7 +303,7 @@ def collectModCandidates( ): self.addModDetectionCandidate( tree, - sanitizeFolderName(tree.name()), + self.sanitizeFolderName(tree.name()), "SuperBLT", "assets/mod_overrides/" + sanitizedName + "/", ) @@ -313,7 +313,7 @@ def collectModCandidates( if tree.exists(validFolder, mobase.IFileTree.DIRECTORY): self.addModDetectionCandidate( tree, - sanitizeFolderName(tree.name()), + self.sanitizeFolderName(tree.name()), "Override", "assets/mod_overrides/" + sanitizedName + "/", ) @@ -329,7 +329,7 @@ def walkEntry(self, path: str, entry: mobase.FileTreeEntry): def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: self.modDetectionCandidates = [] newtree = filetree.createOrphanTree("Fixed Tree") - + self.collectModCandidates(filetree) filetree.walk(self.walkEntry, "/") if len(self.modDetectionCandidates) == 1: diff --git a/games/game_payday2.py b/games/game_payday2.py index e91f10f4..154586c5 100644 --- a/games/game_payday2.py +++ b/games/game_payday2.py @@ -20,21 +20,6 @@ from ..basic_game import BasicGame -def sanitizeFolderName(name: str) -> str: - # Remove invalid characters for Windows folder names - invalid_chars = '+&<>:"|?*\\/' - for char in invalid_chars: - name = name.replace(char, "") - # Remove control characters (ASCII 0-31) - name = "".join(c for c in name if ord(c) >= 32) - # Remove trailing periods and spaces - name = name.rstrip(". ") - # If name is empty after sanitization, use a default - if not name: - name = "Unnamed" - return name - - class Content(IntEnum): TEXTURE = auto() MESH = auto() @@ -118,6 +103,21 @@ def __init__(self, organizer: mobase.IOrganizer): "units", ] + def sanitizeFolderName(self, name: str) -> str: + # Remove invalid characters for Windows folder names + invalid_chars = '+&<>:"|?*\\/' + for char in invalid_chars: + name = name.replace(char, "") + # Remove control characters (ASCII 0-31) + name = "".join(c for c in name if ord(c) >= 32) + # Remove trailing periods and spaces + name = name.rstrip(". ") + # If name is empty after sanitization, use a default + if not name: + name = "FOLDERNAME" + self.needsNameFix = True + return name + def moveOverwriteMerge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) @@ -136,7 +136,7 @@ def fixInstalledMod(self, mod: mobase.IModInterface): return filetree: mobase.IFileTree = mod.fileTree() fixed = False - modname = sanitizeFolderName(mod.name()) + modname = self.sanitizeFolderName(mod.name()) if filetree.exists("mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() old_path = os.path.join(path, "mods/FOLDERNAME") @@ -263,7 +263,7 @@ def collectModCandidates( self, tree: mobase.IFileTree | mobase.FileTreeEntry ) -> bool: if isinstance(tree, mobase.IFileTree): - sanitizedName = sanitizeFolderName(tree.name()) + sanitizedName = self.sanitizeFolderName(tree.name()) hasDisallowedPath = False disallowedFolders = {"assets", "levels", "lua", "map_replacements"} tree_path = tree.path() @@ -281,7 +281,7 @@ def collectModCandidates( ): self.addModDetectionCandidate( tree, - sanitizeFolderName(tree.name()), + self.sanitizeFolderName(tree.name()), "SuperBLT", "mods/" + sanitizedName + "/", ) @@ -291,7 +291,7 @@ def collectModCandidates( ): self.addModDetectionCandidate( tree, - sanitizeFolderName(tree.name()), + self.sanitizeFolderName(tree.name()), "Map Core", "maps/" + sanitizedName + "/", ) @@ -303,7 +303,7 @@ def collectModCandidates( ): self.addModDetectionCandidate( tree, - sanitizeFolderName(tree.name()), + self.sanitizeFolderName(tree.name()), "Beard Lib", "assets/mod_overrides/" + sanitizedName + "/", ) @@ -313,14 +313,14 @@ def collectModCandidates( if tree.exists(validFolder, mobase.IFileTree.DIRECTORY): self.addModDetectionCandidate( tree, - sanitizeFolderName(tree.name()), + self.sanitizeFolderName(tree.name()), "Override", "assets/mod_overrides/" + sanitizedName + "/", ) return True return False - def walk_entry(self, path: str, entry: mobase.FileTreeEntry): + def walkEntry(self, path: str, entry: mobase.FileTreeEntry): if entry.isDir(): if isinstance(entry, mobase.IFileTree): self.collectModCandidates(entry) @@ -329,8 +329,8 @@ def walk_entry(self, path: str, entry: mobase.FileTreeEntry): def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: self.modDetectionCandidates = [] newtree = filetree.createOrphanTree("Fixed Tree") - - filetree.walk(self.walk_entry, "/") + self.collectModCandidates(filetree) + filetree.walk(self.walkEntry, "/") if len(self.modDetectionCandidates) == 1: selectedIndexes = {0} @@ -345,10 +345,9 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: print( f"Installing Mod: {candidate['name']} to {candidate['destination']}" ) - if self.moveTreeContent( + self.moveTreeContent( candidate["tree"], newtree, candidate["destination"] - ): - self.needsNameFix = True + ) return newtree if len(newtree) > 0 else filetree diff --git a/games/game_raid2.py b/games/game_raid2.py index afefc955..bba7f646 100644 --- a/games/game_raid2.py +++ b/games/game_raid2.py @@ -20,21 +20,6 @@ from ..basic_game import BasicGame -def sanitizeFolderName(name: str) -> str: - # Remove invalid characters for Windows folder names - invalid_chars = '+&<>:"|?*\\/' - for char in invalid_chars: - name = name.replace(char, "") - # Remove control characters (ASCII 0-31) - name = "".join(c for c in name if ord(c) >= 32) - # Remove trailing periods and spaces - name = name.rstrip(". ") - # If name is empty after sanitization, use a default - if not name: - name = "Unnamed" - return name - - class Content(IntEnum): TEXTURE = auto() MESH = auto() @@ -119,6 +104,21 @@ def __init__(self, organizer: mobase.IOrganizer): "units", ] + def sanitizeFolderName(self, name: str) -> str: + # Remove invalid characters for Windows folder names + invalid_chars = '+&<>:"|?*\\/' + for char in invalid_chars: + name = name.replace(char, "") + # Remove control characters (ASCII 0-31) + name = "".join(c for c in name if ord(c) >= 32) + # Remove trailing periods and spaces + name = name.rstrip(". ") + # If name is empty after sanitization, use a default + if not name: + name = "FOLDERNAME" + self.needsNameFix = True + return name + def moveOverwriteMerge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) @@ -137,7 +137,7 @@ def fixInstalledMod(self, mod: mobase.IModInterface): return filetree: mobase.IFileTree = mod.fileTree() fixed = False - modname = sanitizeFolderName(mod.name()) + modname = self.sanitizeFolderName(mod.name()) if filetree.exists("mods/FOLDERNAME", mobase.IFileTree.DIRECTORY): path = mod.absolutePath() old_path = os.path.join(path, "mods/FOLDERNAME") @@ -231,7 +231,7 @@ def collectModCandidates( hasDisallowedPath = True if not hasDisallowedPath: if isinstance(tree, mobase.IFileTree): - sanitizedName = sanitizeFolderName(tree.name()) + sanitizedName = self.sanitizeFolderName(tree.name()) if ( tree.exists("mod.txt", mobase.IFileTree.FILE) or tree.exists("mod.xml", mobase.IFileTree.FILE) @@ -249,7 +249,7 @@ def collectModCandidates( ): self.addModDetectionCandidate( tree, - sanitizeFolderName(tree.name()), + self.sanitizeFolderName(tree.name()), "Beard Lib", "mods/" + sanitizedName + "/", ) @@ -259,14 +259,14 @@ def collectModCandidates( if tree.exists(validFolder, mobase.IFileTree.DIRECTORY): self.addModDetectionCandidate( tree, - sanitizeFolderName(tree.name()), + self.sanitizeFolderName(tree.name()), "Override", "mods/" + sanitizedName + "/", ) return True return False - def walk_entry(self, path: str, entry: mobase.FileTreeEntry): + def walkEntry(self, path: str, entry: mobase.FileTreeEntry): if entry.isDir(): if isinstance(entry, mobase.IFileTree): self.collectModCandidates(entry) @@ -289,8 +289,8 @@ def moveTreeContent( def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: self.modDetectionCandidates = [] newtree = filetree.createOrphanTree("Fixed Tree") - - filetree.walk(self.walk_entry, "/") + self.collectModCandidates(filetree) + filetree.walk(self.walkEntry, "/") if len(self.modDetectionCandidates) == 1: selectedIndexes = {0} From 3b70bc8f45ae471a10d4179c033856039071c939 Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Wed, 6 May 2026 23:41:44 +0200 Subject: [PATCH 29/31] Extra variable for optional MWS category support Fixed Content Flags being set incorrectly --- games/game_cassettebeasts.py | 1 + games/game_crimeboss.py | 15 ++++++++------- games/game_emuvr.py | 1 + games/game_noita.py | 1 + games/game_ovkwalkingdead.py | 15 ++++++++------- games/game_pacificdrive.py | 14 +++++++------- games/game_payday1.py | 20 +++++++++++++------- games/game_payday2.py | 16 +++++++++------- games/game_payday3.py | 15 ++++++++------- games/game_raid2.py | 16 +++++++++------- games/game_roadtovostok.py | 1 + games/game_silenthill2remake.py | 14 +++++++------- games/game_stalkeranomaly.py | 16 ++++++++-------- games/game_titanfall2.py | 20 +++++++++++--------- games/game_windrose.py | 15 ++++++++------- games/game_zuma_deluxe.py | 17 +++++++++-------- 16 files changed, 109 insertions(+), 88 deletions(-) diff --git a/games/game_cassettebeasts.py b/games/game_cassettebeasts.py index 76f707af..934c39a6 100644 --- a/games/game_cassettebeasts.py +++ b/games/game_cassettebeasts.py @@ -180,6 +180,7 @@ def getMetadata(p: Path, save: mobase.ISaveGame) -> Mapping[str, str] | None: class CassetteBeastsGame(BasicGame): Name = "Cassette Beasts Support Plugin" Author = "ModWorkshop" + CategorySource = "modworkshop" Version = "1" GameName = "Cassette Beasts" GameShortName = "cassette-beasts" diff --git a/games/game_crimeboss.py b/games/game_crimeboss.py index f8114f28..01e6b629 100644 --- a/games/game_crimeboss.py +++ b/games/game_crimeboss.py @@ -46,23 +46,23 @@ def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): match entry.suffix().casefold(): case "utoc": - self.content.append(Content.UTOC) + self.contents.append(Content.UTOC) case "ucas": - self.content.append(Content.UCAS) + self.contents.append(Content.UCAS) case "pak": - self.content.append(Content.PAK) + self.contents.append(Content.PAK) case "lua": - self.content.append(Content.UE4SS) + self.contents.append(Content.UE4SS) case "dll": - self.content.append(Content.DLL) + self.contents.append(Content.DLL) case "bk2": - self.contents.add(Content.BK2) + self.contents.append(Content.BK2) case _: pass return mobase.IFileTree.WalkReturn.CONTINUE def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: - self.contents: set[int] = set() + self.contents: list[int] = [] filetree.walk(self.walkContent, "/") return list(self.contents) @@ -242,6 +242,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: class CrimeBossGame(BasicGame): Name = "Crime Boss Support Plugin" Author = "ModWorkshop, MaskPlague and Silarn" + CategorySource = "modworkshop" Version = "1" GameName = "Crime Boss Rockay City" GameShortName = "crimeboss" diff --git a/games/game_emuvr.py b/games/game_emuvr.py index f82cd174..17e1c417 100644 --- a/games/game_emuvr.py +++ b/games/game_emuvr.py @@ -55,6 +55,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: class EmuVRGame(BasicGame): Name = "Emu VR Support Plugin" Author = "ModWorkshop" + CategorySource = "modworkshop" Version = "1" GameName = "Emu VR" GameShortName = "emuvr" diff --git a/games/game_noita.py b/games/game_noita.py index cd10515b..b0b86f6f 100644 --- a/games/game_noita.py +++ b/games/game_noita.py @@ -88,6 +88,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: class NoitaGame(BasicGame): Name = "Noita Support Plugin" Author = "ModWorkshop" + CategorySource = "modworkshop" Version = "1" GameName = "Noita" GameShortName = "noita" diff --git a/games/game_ovkwalkingdead.py b/games/game_ovkwalkingdead.py index ff1aec4f..74b6bd01 100644 --- a/games/game_ovkwalkingdead.py +++ b/games/game_ovkwalkingdead.py @@ -46,23 +46,23 @@ def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): match entry.suffix().casefold(): case "utoc": - self.content.append(Content.UTOC) + self.contents.append(Content.UTOC) case "ucas": - self.content.append(Content.UCAS) + self.contents.append(Content.UCAS) case "pak": - self.content.append(Content.PAK) + self.contents.append(Content.PAK) case "lua": - self.content.append(Content.UE4SS) + self.contents.append(Content.UE4SS) case "dll": - self.content.append(Content.DLL) + self.contents.append(Content.DLL) case "bk2": - self.content.append(Content.BK2) + self.contents.append(Content.BK2) case _: pass return mobase.IFileTree.WalkReturn.CONTINUE def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: - self.contents: set[int] = set() + self.contents: list[int] = [] filetree.walk(self.walkContent, "/") return list(self.contents) @@ -208,6 +208,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: class OTWDGame(BasicGame): Name = "OVERKILL's The Walking Dead Support Plugin" Author = "ModWorkshop, MaskPlague and Silarn" + CategorySource = "modworkshop" Version = "1" GameName = "OVERKILL's The Walking Dead" GameShortName = "otwd" diff --git a/games/game_pacificdrive.py b/games/game_pacificdrive.py index ed149b2c..c072d5d2 100644 --- a/games/game_pacificdrive.py +++ b/games/game_pacificdrive.py @@ -46,23 +46,23 @@ def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): match entry.suffix().casefold(): case "utoc": - self.content.append(Content.UTOC) + self.contents.append(Content.UTOC) case "ucas": - self.content.append(Content.UCAS) + self.contents.append(Content.UCAS) case "pak": - self.content.append(Content.PAK) + self.contents.append(Content.PAK) case "lua": - self.content.append(Content.UE4SS) + self.contents.append(Content.UE4SS) case "dll": - self.content.append(Content.DLL) + self.contents.append(Content.DLL) case "bk2": - self.content.append(Content.BK2) + self.contents.append(Content.BK2) case _: pass return mobase.IFileTree.WalkReturn.CONTINUE def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: - self.contents: set[int] = set() + self.contents: list[int] = [] filetree.walk(self.walkContent, "/") return list(self.contents) diff --git a/games/game_payday1.py b/games/game_payday1.py index b69c997c..f7802cc8 100644 --- a/games/game_payday1.py +++ b/games/game_payday1.py @@ -50,24 +50,25 @@ def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): match entry.suffix().casefold(): case "texture": - self.content.append(Content.TEXTURE) + self.contents.append(Content.TEXTURE) case "model": - self.content.append(Content.MESH) + self.contents.append(Content.MESH) case "lua": - self.content.append(Content.SCRIPT) + self.contents.append(Content.SCRIPT) case "stream": - self.content.append(Content.SOUND) + self.contents.append(Content.SOUND) case "txt": - self.content.append(Content.STRING) + self.contents.append(Content.STRING) case "json": - self.content.append(Content.CONFIG) + self.contents.append(Content.CONFIG) case _: pass return mobase.IFileTree.WalkReturn.CONTINUE def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: + self.contents: list[int] = [] filetree.walk(self.walkContent, "/") - return list(self.content) + return list(self.contents) class ModDetectionCandidate(TypedDict): @@ -179,6 +180,10 @@ def dataLooksValid( return mobase.ModDataChecker.VALID if filetree.exists("WSOCK32.dll", mobase.IFileTree.FILE): return mobase.ModDataChecker.VALID + if filetree.exists("DINPUT8.dll", mobase.IFileTree.FILE): + return mobase.ModDataChecker.VALID + if filetree.exists("PDTHModOverrides.dll", mobase.IFileTree.FILE): + return mobase.ModDataChecker.VALID return mobase.ModDataChecker.FIXABLE def moveTreeContent( @@ -356,6 +361,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: class Payday1Game(BasicGame): Name = "Payday 1 Support Plugin" Author = "ModWorkshop" + CategorySource = "modworkshop" Version = "1" GameName = "Payday: The Heist" GameShortName = "pdth" diff --git a/games/game_payday2.py b/games/game_payday2.py index 154586c5..871bb805 100644 --- a/games/game_payday2.py +++ b/games/game_payday2.py @@ -50,24 +50,25 @@ def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): match entry.suffix().casefold(): case "texture": - self.content.append(Content.TEXTURE) + self.contents.append(Content.TEXTURE) case "model": - self.content.append(Content.MESH) + self.contents.append(Content.MESH) case "lua": - self.content.append(Content.SCRIPT) + self.contents.append(Content.SCRIPT) case "stream": - self.content.append(Content.SOUND) + self.contents.append(Content.SOUND) case "txt": - self.content.append(Content.STRING) + self.contents.append(Content.STRING) case "json": - self.content.append(Content.CONFIG) + self.contents.append(Content.CONFIG) case _: pass return mobase.IFileTree.WalkReturn.CONTINUE def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: + self.contents: list[int] = [] filetree.walk(self.walkContent, "/") - return list(self.content) + return list(self.contents) class ModDetectionCandidate(TypedDict): @@ -355,6 +356,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: class Payday2Game(BasicGame): Name = "Payday 2 Support Plugin" Author = "ModWorkshop" + CategorySource = "modworkshop" Version = "1" GameName = "Payday 2" GameShortName = "payday-2" diff --git a/games/game_payday3.py b/games/game_payday3.py index 80180b1c..a0f032cc 100644 --- a/games/game_payday3.py +++ b/games/game_payday3.py @@ -47,23 +47,23 @@ def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): match entry.suffix().casefold(): case "utoc": - self.content.append(Content.UTOC) + self.contents.append(Content.UTOC) case "ucas": - self.content.append(Content.UCAS) + self.contents.append(Content.UCAS) case "pak": - self.content.append(Content.PAK) + self.contents.append(Content.PAK) case "lua": - self.content.append(Content.UE4SS) + self.contents.append(Content.UE4SS) case "dll": - self.content.append(Content.DLL) + self.contents.append(Content.DLL) case "bk2": - self.content.append(Content.BK2) + self.contents.append(Content.BK2) case _: pass return mobase.IFileTree.WalkReturn.CONTINUE def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: - self.contents: set[int] = set() + self.contents: list[int] = [] filetree.walk(self.walkContent, "/") return list(self.contents) @@ -209,6 +209,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: class Payday3Game(BasicGame): Name = "Payday 3 Support Plugin" Author = "ModWorkshop, MaskPlague and Silarn" + CategorySource = "modworkshop" Version = "1" GameName = "Payday 3" GameLauncher = "PAYDAY3.exe" diff --git a/games/game_raid2.py b/games/game_raid2.py index bba7f646..fe36166d 100644 --- a/games/game_raid2.py +++ b/games/game_raid2.py @@ -50,24 +50,25 @@ def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): match entry.suffix().casefold(): case "texture": - self.content.append(Content.TEXTURE) + self.contents.append(Content.TEXTURE) case "model": - self.content.append(Content.MESH) + self.contents.append(Content.MESH) case "lua": - self.content.append(Content.SCRIPT) + self.contents.append(Content.SCRIPT) case "stream": - self.content.append(Content.SOUND) + self.contents.append(Content.SOUND) case "txt": - self.content.append(Content.STRING) + self.contents.append(Content.STRING) case "json": - self.content.append(Content.CONFIG) + self.contents.append(Content.CONFIG) case _: pass return mobase.IFileTree.WalkReturn.CONTINUE def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: + self.contents: list[int] = [] filetree.walk(self.walkContent, "/") - return list(self.content) + return list(self.contents) class ModDetectionCandidate(TypedDict): @@ -316,6 +317,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: class RaidWW2Game(BasicGame): Name = "RAID World War II Support Plugin" Author = "ModWorkshop" + CategorySource = "modworkshop" Version = "1" GameName = "RAID World War II" GameShortName = "raidww2" diff --git a/games/game_roadtovostok.py b/games/game_roadtovostok.py index 2dfdef5a..627d6f5f 100644 --- a/games/game_roadtovostok.py +++ b/games/game_roadtovostok.py @@ -50,6 +50,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: class RoadToVostokGame(BasicGame): Name = "Road to Vostok Support Plugin" Author = "ModWorkshop" + CategorySource = "modworkshop" Version = "1" GameName = "Road to Vostok" GameShortName = "roadtovostok" diff --git a/games/game_silenthill2remake.py b/games/game_silenthill2remake.py index e9fc77e4..0513d0fa 100644 --- a/games/game_silenthill2remake.py +++ b/games/game_silenthill2remake.py @@ -46,23 +46,23 @@ def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): match entry.suffix().casefold(): case "utoc": - self.content.append(Content.UTOC) + self.contents.append(Content.UTOC) case "ucas": - self.content.append(Content.UCAS) + self.contents.append(Content.UCAS) case "pak": - self.content.append(Content.PAK) + self.contents.append(Content.PAK) case "lua": - self.content.append(Content.UE4SS) + self.contents.append(Content.UE4SS) case "dll": - self.content.append(Content.DLL) + self.contents.append(Content.DLL) case "bk2": - self.content.append(Content.BK2) + self.contents.append(Content.BK2) case _: pass return mobase.IFileTree.WalkReturn.CONTINUE def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: - self.contents: set[int] = set() + self.contents: list[int] = [] filetree.walk(self.walkContent, "/") return list(self.contents) diff --git a/games/game_stalkeranomaly.py b/games/game_stalkeranomaly.py index bb40575d..41d823ea 100644 --- a/games/game_stalkeranomaly.py +++ b/games/game_stalkeranomaly.py @@ -104,21 +104,21 @@ def walkContent( if entry.isFile(): ext = entry.suffix().lower() if ext in ["dds", "thm"]: - self.content.append(Content.TEXTURE) + self.contents.append(Content.TEXTURE) if path.startswith("gamedata/textures/ui"): - self.content.append(Content.INTERFACE) + self.contents.append(Content.INTERFACE) elif ext in ["omf", "ogf"]: - self.content.append(Content.MESH) + self.contents.append(Content.MESH) elif ext in ["script"]: - self.content.append(Content.SCRIPT) + self.contents.append(Content.SCRIPT) if "_mcm" in name: - self.content.append(Content.MCM) + self.contents.append(Content.MCM) elif ext in ["ogg"]: - self.content.append(Content.SOUND) + self.contents.append(Content.SOUND) elif ext in ["ltx", "xml"]: - self.content.append(Content.CONFIG) + self.contents.append(Content.CONFIG) if path.startswith("gamedata/configs/ui"): - self.content.append(Content.INTERFACE) + self.contents.append(Content.INTERFACE) return mobase.IFileTree.WalkReturn.CONTINUE diff --git a/games/game_titanfall2.py b/games/game_titanfall2.py index 968bb53d..9cd7eff3 100644 --- a/games/game_titanfall2.py +++ b/games/game_titanfall2.py @@ -46,28 +46,29 @@ def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): match entry.suffix().casefold(): case "vmt": - self.content.append(Content.MATERIAL) + self.contents.append(Content.MATERIAL) case "vtf": - self.content.append(Content.TEXTURE) + self.contents.append(Content.TEXTURE) case "mdl": - self.content.append(Content.MODELS) + self.contents.append(Content.MODELS) case "nut": - self.content.append(Content.SCRIPT) + self.contents.append(Content.SCRIPT) case "txt": - self.content.append(Content.CONFIG) + self.contents.append(Content.CONFIG) case "bik": - self.content.append(Content.VIDEO) + self.contents.append(Content.VIDEO) case "wav": - self.content.append(Content.AUDIO) + self.contents.append(Content.AUDIO) case "rpak" | "starmap" | "starpak": - self.content.append(Content.STARPAK) + self.contents.append(Content.STARPAK) case _: pass return mobase.IFileTree.WalkReturn.CONTINUE def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: + self.contents: list[int] = [] filetree.walk(self.walkContent, "/") - return list(self.content) + return list(self.contents) class Titanfall2ModDataChecker(mobase.ModDataChecker): @@ -202,6 +203,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: class Titanfall2Game(BasicGame): Name = "Titanfall 2 Support Plugin" Author = "ModWorkshop" + CategorySource = "modworkshop" Version = "1" GameName = "Titanfall 2" GameShortName = "titanfall-2" diff --git a/games/game_windrose.py b/games/game_windrose.py index 6ec2025d..dc548893 100644 --- a/games/game_windrose.py +++ b/games/game_windrose.py @@ -46,23 +46,23 @@ def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): match entry.suffix().casefold(): case "utoc": - self.content.append(Content.UTOC) + self.contents.append(Content.UTOC) case "ucas": - self.content.append(Content.UCAS) + self.contents.append(Content.UCAS) case "pak": - self.content.append(Content.PAK) + self.contents.append(Content.PAK) case "lua": - self.content.append(Content.UE4SS) + self.contents.append(Content.UE4SS) case "dll": - self.content.append(Content.DLL) + self.contents.append(Content.DLL) case "bk2": - self.content.append(Content.BK2) + self.contents.append(Content.BK2) case _: pass return mobase.IFileTree.WalkReturn.CONTINUE def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: - self.contents: set[int] = set() + self.contents: list[int] = [] filetree.walk(self.walkContent, "/") return list(self.contents) @@ -208,6 +208,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: class WindroseGame(BasicGame): Name = "Windrose Support Plugin" Author = "ModWorkshop" + CategorySource = "modworkshop" Version = "1" GameName = "Windrose" GameLauncher = "Windrose.exe" diff --git a/games/game_zuma_deluxe.py b/games/game_zuma_deluxe.py index 001896b2..86e8b8e5 100644 --- a/games/game_zuma_deluxe.py +++ b/games/game_zuma_deluxe.py @@ -23,6 +23,7 @@ class Content(IntEnum): class ZumaModDataContent(mobase.ModDataContent): + content: list[int] = [] GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [ (Content.TEXTURE, "Textures", ":/MO/gui/content/texture"), (Content.MESH, "Meshes", ":/MO/gui/content/mesh"), @@ -38,28 +39,27 @@ def getAllContents(self) -> list[mobase.ModDataContent.Content]: for id, name, icon, *filter_only in self.GAMECONTENTS ] - contents: set[int] = set() - def walkContent(self, path: str, entry: mobase.FileTreeEntry): if entry.isFile(): match entry.suffix().lower(): case "texture": - self.contents.add(Content.TEXTURE) + self.contents.append(Content.TEXTURE) case "model": - self.contents.add(Content.MESH) + self.contents.append(Content.MESH) case "lua": - self.contents.add(Content.SCRIPT) + self.contents.append(Content.SCRIPT) case "stream": - self.contents.add(Content.SOUND) + self.contents.append(Content.SOUND) case "txt": - self.contents.add(Content.STRING) + self.contents.append(Content.STRING) case "json": - self.contents.add(Content.CONFIG) + self.contents.append(Content.CONFIG) case _: pass return mobase.IFileTree.WalkReturn.CONTINUE def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: + self.contents: list[int] = [] filetree.walk(self.walkContent, "/") return list(self.contents) @@ -196,6 +196,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: class ZumaGame(BasicGame, mobase.IPluginFileMapper): Name = "Zuma Deluxe Support Plugin" Author = "ModWorkshop" + CategorySource = "modworkshop" Version = "1" GameName = "Zuma Deluxe" GameShortName = "zuma" From 5cc4b06049170221fc6e30bb7298e05e806bbabc Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Thu, 7 May 2026 00:09:21 +0200 Subject: [PATCH 30/31] Cleanup. Fixed Stalker Anomaly Content Indexing. --- games/game_cassettebeasts.py | 14 +++++++------- games/game_crimeboss.py | 22 +++++++++++----------- games/game_emuvr.py | 4 ++-- games/game_hitman3.py | 22 +++++++++++----------- games/game_noita.py | 14 +++++++------- games/game_oblivion_remaster.py | 8 ++++---- games/game_ovkwalkingdead.py | 16 ++++++++-------- games/game_pacificdrive.py | 16 ++++++++-------- games/game_payday1.py | 4 ++-- games/game_payday2.py | 4 ++-- games/game_payday3.py | 16 ++++++++-------- games/game_raid2.py | 4 ++-- games/game_silenthill2remake.py | 16 ++++++++-------- games/game_stalkeranomaly.py | 4 ++-- games/game_titanfall2.py | 26 +++++++++++++------------- games/game_windrose.py | 16 ++++++++-------- games/game_zuma_deluxe.py | 18 +++++++++--------- 17 files changed, 112 insertions(+), 112 deletions(-) diff --git a/games/game_cassettebeasts.py b/games/game_cassettebeasts.py index 934c39a6..7d6b0025 100644 --- a/games/game_cassettebeasts.py +++ b/games/game_cassettebeasts.py @@ -23,7 +23,7 @@ from ..basic_game import BasicGame -def json_get_me(value: Any, path: Sequence[str | int], /, default: Any) -> Any: +def jsonGetMe(value: Any, path: Sequence[str | int], /, default: Any) -> Any: for part in path: if type(part) not in (str, int) or type(value) not in (dict, list): return default @@ -120,10 +120,10 @@ def __init__(self, filepath: Path): err.__class__.__name__, s ) return - x = json_get_me(save_data, ["party", "player", "custom", "name"], None) + x = jsonGetMe(save_data, ["party", "player", "custom", "name"], None) if type(x) is str: self.name = x - x = json_get_me(save_data, ["saved_datetime"], None) + x = jsonGetMe(save_data, ["saved_datetime"], None) if type(x) in (int, float): try: dt = datetime.fromtimestamp(float(x)) @@ -135,14 +135,14 @@ def __init__(self, filepath: Path): dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second ) ) - x = json_get_me(save_data, ["play_time"], None) + x = jsonGetMe(save_data, ["play_time"], None) if type(x) in (int, float): a = [0, 0, 0, int(x * 10)] a[2:4] = divmod(a[3], 10) a[1:3] = divmod(a[2], 60) a[0:2] = divmod(a[1], 60) self.elapsed = "{0:02d}:{1:02d}:{2:02d}.{3:01d}".format(*a) - x = json_get_me(save_data, ["has_cheated"], None) + x = jsonGetMe(save_data, ["has_cheated"], None) if type(x) is bool: self.cheated = "Yes" if x else "No" @@ -219,7 +219,7 @@ def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]: ] @cached_property - def _base_dlls(self) -> set[str]: + def baseDlls(self) -> set[str]: base_dir = Path(self.gameDirectory().absolutePath()) return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} @@ -236,7 +236,7 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: return efls for e in tree: relpath = e.pathFrom(tree) - if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + if relpath and e.hasSuffix("dll") and relpath not in self.baseDlls: libs.add(relpath) exes = self.executables() efls = efls + [ diff --git a/games/game_crimeboss.py b/games/game_crimeboss.py index 01e6b629..f3b85238 100644 --- a/games/game_crimeboss.py +++ b/games/game_crimeboss.py @@ -71,10 +71,10 @@ 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.organizer.modList().onModInstalled(self.fixInstalledMod) self.needsNameFix = False - def move_overwrite_merge(self, source: str, destination: str): + def moveOverwriteMerge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -84,10 +84,10 @@ def move_overwrite_merge(self, source: str, destination: str): 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) + self.moveOverwriteMerge(s_item, d_item) os.rmdir(source) - def _Fix_Installed_Mod(self, mod: mobase.IModInterface): + def fixInstalledMod(self, mod: mobase.IModInterface): if not self.needsNameFix: return GameDataNativeMods = getattr( @@ -102,7 +102,7 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): 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) + self.moveOverwriteMerge(old_path, new_path) fixed = True if not fixed: return @@ -265,10 +265,10 @@ def init(self, organizer: mobase.IOrganizer) -> bool: self.dataChecker = CrimeBossModDataChecker(organizer) self._register_feature(self.dataChecker) self._register_feature(CrimeBossModDataContent()) - organizer.onUserInterfaceInitialized(self.init_tab) + organizer.onUserInterfaceInitialized(self.initTab) return True - def init_tab(self, main_window: QMainWindow): + def initTab(self, main_window: QMainWindow): if self._organizer.managedGame() != self: return self._main_window = main_window @@ -294,7 +294,7 @@ def executables(self): ] @cached_property - def _base_dlls(self) -> set[str]: + def baseDlls(self) -> set[str]: base_dir = Path(self.gameDirectory().absolutePath()) return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} @@ -311,7 +311,7 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: return efls for e in tree: relpath = e.pathFrom(tree) - if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + if relpath and e.hasSuffix("dll") and relpath not in self.baseDlls: libs.add(relpath) exes = self.executables() efls = efls + [ @@ -332,7 +332,7 @@ def ue4ssDirectory(self) -> QDir: def nativeDirectory(self) -> QDir: return QDir(self.dataDirectory().absolutePath() + "/" + self.GameDataNativeMods) - def write_default_mods(self, profile: QDir): + def writeDefaultMods(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(): @@ -350,7 +350,7 @@ def iniFiles(self): return ["GameUserSettings.ini", "Input.ini"] def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): - self.write_default_mods(directory) + self.writeDefaultMods(directory) if not self.paksDirectory().exists(): os.makedirs(self.paksDirectory().absolutePath()) if not self.ue4ssDirectory().exists(): diff --git a/games/game_emuvr.py b/games/game_emuvr.py index 17e1c417..174820ec 100644 --- a/games/game_emuvr.py +++ b/games/game_emuvr.py @@ -96,7 +96,7 @@ def iniFiles(self): return ["settings.ini"] @cached_property - def _base_dlls(self) -> set[str]: + def baseDlls(self) -> set[str]: base_dir = Path(self.gameDirectory().absolutePath()) return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} @@ -113,7 +113,7 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: return efls for e in tree: relpath = e.pathFrom(tree) - if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + if relpath and e.hasSuffix("dll") and relpath not in self.baseDlls: libs.add(relpath) exes = self.executables() efls = efls + [ diff --git a/games/game_hitman3.py b/games/game_hitman3.py index e6cdc4ba..a1d033ee 100644 --- a/games/game_hitman3.py +++ b/games/game_hitman3.py @@ -15,10 +15,10 @@ 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.organizer.modList().onModInstalled(self.fixInstalledMod) self.needsNameFix = False - def move_overwrite_merge(self, source: str, destination: str): + def moveOverwriteMerge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -28,10 +28,10 @@ def move_overwrite_merge(self, source: str, destination: str): 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) + self.moveOverwriteMerge(s_item, d_item) os.rmdir(source) - def _Fix_Installed_Mod(self, mod: mobase.IModInterface): + def fixInstalledMod(self, mod: mobase.IModInterface): if not self.needsNameFix: return GameSMMPath = getattr(self.organizer.managedGame(), "GameSMMPath", "") @@ -51,7 +51,7 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): 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) + self.moveOverwriteMerge(old_path, new_path) fixed = True if not fixed: return @@ -85,7 +85,7 @@ def allMoveTo( targettree.remove(sourcetree) return retVal - def first_tree(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + def firstTree(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: for e in filetree: if isinstance(e, mobase.IFileTree) and e.isDir(): return e @@ -103,7 +103,7 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: if treefixed == 1: self.needsNameFix = True elif len(filetree) == 1: - firsttreelayer: mobase.IFileTree | None = self.first_tree(filetree) + firsttreelayer: mobase.IFileTree | None = self.firstTree(filetree) if firsttreelayer is not None: if firsttreelayer.exists("manifest.json", mobase.IFileTree.FILE): print(GameSMMPath + "/Mods/FOLDERNAME/") @@ -130,10 +130,10 @@ 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) + organizer.modList().onModStateChanged(self.updateSmmMeta) return True - def update_smm_meta(self, mods: dict[str, mobase.ModState]): + def updateSmmMeta(self, mods: dict[str, mobase.ModState]): SMM_Path = os.path.join(self.dataDirectory().absolutePath(), self.GameSMMPath) SMM_Config_Json = SMM_Path + "/config.json" for key, value in mods.items(): @@ -232,7 +232,7 @@ def executables(self): ] @cached_property - def _base_dlls(self) -> set[str]: + def baseDlls(self) -> set[str]: base_dir = Path(self.gameDirectory().absolutePath()) return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} @@ -249,7 +249,7 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: return efls for e in tree: relpath = e.pathFrom(tree) - if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + if relpath and e.hasSuffix("dll") and relpath not in self.baseDlls: libs.add(relpath) exes = self.executables() efls = efls + [ diff --git a/games/game_noita.py b/games/game_noita.py index b0b86f6f..5e16a880 100644 --- a/games/game_noita.py +++ b/games/game_noita.py @@ -14,10 +14,10 @@ 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.organizer.modList().onModInstalled(self.fixInstalledMod) self.needsNameFix = False - def move_overwrite_merge(self, source: str, destination: str): + def moveOverwriteMerge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -27,10 +27,10 @@ def move_overwrite_merge(self, source: str, destination: str): 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) + self.moveOverwriteMerge(s_item, d_item) os.rmdir(source) - def _Fix_Installed_Mod(self, mod: mobase.IModInterface): + def fixInstalledMod(self, mod: mobase.IModInterface): if not self.needsNameFix: return GameModsPath = getattr(self.organizer.managedGame(), "GameModsPath", "") @@ -41,7 +41,7 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): 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) + self.moveOverwriteMerge(old_path, new_path) fixed = True if not fixed: return @@ -119,7 +119,7 @@ def executables(self): ] @cached_property - def _base_dlls(self) -> set[str]: + def baseDlls(self) -> set[str]: base_dir = Path(self.gameDirectory().absolutePath()) return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} @@ -136,7 +136,7 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: return efls for e in tree: relpath = e.pathFrom(tree) - if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + if relpath and e.hasSuffix("dll") and relpath not in self.baseDlls: libs.add(relpath) exes = self.executables() efls = efls + [ diff --git a/games/game_oblivion_remaster.py b/games/game_oblivion_remaster.py index 218fded0..ceda2fb5 100644 --- a/games/game_oblivion_remaster.py +++ b/games/game_oblivion_remaster.py @@ -111,10 +111,10 @@ def init(self, organizer: mobase.IOrganizer) -> bool: self._register_feature(OblivionRemasteredScriptExtender(self)) self._register_feature(OblivionRemasteredDataContent()) - organizer.onUserInterfaceInitialized(self.init_tab) + organizer.onUserInterfaceInitialized(self.initTab) return True - def init_tab(self, main_window: QMainWindow): + def initTab(self, main_window: QMainWindow): """ Initializes tabs unique to Oblivion Remastered. The "UE4SS Mods" tab and "Paks" tab. @@ -261,7 +261,7 @@ def initializeProfile( else: Path(profile_ini).touch() # Initialize a default UE4SS mods.ini and mods.json with the core mods included - self.write_default_mods(directory) + self.writeDefaultMods(directory) # Bootstrap common mod directories used by the USVFS map if ( self._organizer.managedGame() @@ -274,7 +274,7 @@ def initializeProfile( if not self.ue4ssDirectory().exists(): os.makedirs(self.ue4ssDirectory().absolutePath()) - def write_default_mods(self, profile: QDir): + def writeDefaultMods(self, profile: QDir): """ Writer for the default UE4SS 'mods.txt' and 'mods.json' profile files. """ diff --git a/games/game_ovkwalkingdead.py b/games/game_ovkwalkingdead.py index 74b6bd01..3698b2d3 100644 --- a/games/game_ovkwalkingdead.py +++ b/games/game_ovkwalkingdead.py @@ -72,7 +72,7 @@ def __init__(self, organizer: mobase.IOrganizer): super().__init__() self.organizer: mobase.IOrganizer = organizer - def move_overwrite_merge(self, source: str, destination: str): + def moveOverwriteMerge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -82,7 +82,7 @@ def move_overwrite_merge(self, source: str, destination: str): 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) + self.moveOverwriteMerge(s_item, d_item) os.rmdir(source) def dataLooksValid( @@ -231,10 +231,10 @@ def init(self, organizer: mobase.IOrganizer) -> bool: self.dataChecker = OTWDModDataChecker(organizer) self._register_feature(self.dataChecker) self._register_feature(OTWDModDataContent()) - organizer.onUserInterfaceInitialized(self.init_tab) + organizer.onUserInterfaceInitialized(self.initTab) return True - def init_tab(self, main_window: QMainWindow): + def initTab(self, main_window: QMainWindow): if self._organizer.managedGame() != self: return self._main_window = main_window @@ -260,7 +260,7 @@ def executables(self): ] @cached_property - def _base_dlls(self) -> set[str]: + def baseDlls(self) -> set[str]: base_dir = Path(self.gameDirectory().absolutePath()) return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} @@ -277,7 +277,7 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: return efls for e in tree: relpath = e.pathFrom(tree) - if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + if relpath and e.hasSuffix("dll") and relpath not in self.baseDlls: libs.add(relpath) exes = self.executables() efls = efls + [ @@ -289,7 +289,7 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: ] return efls - def write_default_mods(self, profile: QDir): + def writeDefaultMods(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(): @@ -307,7 +307,7 @@ def iniFiles(self): return ["GameUserSettings.ini", "Input.ini"] def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): - self.write_default_mods(directory) + self.writeDefaultMods(directory) base_data_dir = self.dataDirectory().absolutePath() diff --git a/games/game_pacificdrive.py b/games/game_pacificdrive.py index c072d5d2..d9247d5a 100644 --- a/games/game_pacificdrive.py +++ b/games/game_pacificdrive.py @@ -72,7 +72,7 @@ def __init__(self, organizer: mobase.IOrganizer): super().__init__() self.organizer: mobase.IOrganizer = organizer - def move_overwrite_merge(self, source: str, destination: str): + def moveOverwriteMerge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -82,7 +82,7 @@ def move_overwrite_merge(self, source: str, destination: str): 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) + self.moveOverwriteMerge(s_item, d_item) os.rmdir(source) def dataLooksValid( @@ -231,10 +231,10 @@ def init(self, organizer: mobase.IOrganizer) -> bool: self.dataChecker = PacificDriveModDataChecker(organizer) self._register_feature(self.dataChecker) self._register_feature(PacificDriveModDataContent()) - organizer.onUserInterfaceInitialized(self.init_tab) + organizer.onUserInterfaceInitialized(self.initTab) return True - def init_tab(self, main_window: QMainWindow): + def initTab(self, main_window: QMainWindow): if self._organizer.managedGame() != self: return self._main_window = main_window @@ -260,7 +260,7 @@ def executables(self): ] @cached_property - def _base_dlls(self) -> set[str]: + def baseDlls(self) -> set[str]: base_dir = Path(self.gameDirectory().absolutePath()) return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} @@ -277,7 +277,7 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: return efls for e in tree: relpath = e.pathFrom(tree) - if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + if relpath and e.hasSuffix("dll") and relpath not in self.baseDlls: libs.add(relpath) exes = self.executables() efls = efls + [ @@ -289,7 +289,7 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: ] return efls - def write_default_mods(self, profile: QDir): + def writeDefaultMods(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(): @@ -307,7 +307,7 @@ def iniFiles(self): return ["GameUserSettings.ini", "Input.ini"] def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): - self.write_default_mods(directory) + self.writeDefaultMods(directory) base_data_dir = self.dataDirectory().absolutePath() diff --git a/games/game_payday1.py b/games/game_payday1.py index f7802cc8..12c01927 100644 --- a/games/game_payday1.py +++ b/games/game_payday1.py @@ -411,7 +411,7 @@ def dllCopy(self, mods: dict[str, mobase.ModState]): os.remove(file_path_target) @cached_property - def _base_dlls(self) -> set[str]: + def baseDlls(self) -> set[str]: base_dir = Path(self.gameDirectory().absolutePath()) return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} @@ -428,7 +428,7 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: return efls for e in tree: relpath = e.pathFrom(tree) - if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + if relpath and e.hasSuffix("dll") and relpath not in self.baseDlls: libs.add(relpath) exes = self.executables() efls = efls + [ diff --git a/games/game_payday2.py b/games/game_payday2.py index 871bb805..39974c9c 100644 --- a/games/game_payday2.py +++ b/games/game_payday2.py @@ -406,7 +406,7 @@ def dllCopy(self, mods: dict[str, mobase.ModState]): os.remove(file_path_target) @cached_property - def _base_dlls(self) -> set[str]: + def baseDlls(self) -> set[str]: base_dir = Path(self.gameDirectory().absolutePath()) return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} @@ -423,7 +423,7 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: return efls for e in tree: relpath = e.pathFrom(tree) - if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + if relpath and e.hasSuffix("dll") and relpath not in self.baseDlls: libs.add(relpath) exes = self.executables() efls = efls + [ diff --git a/games/game_payday3.py b/games/game_payday3.py index a0f032cc..51440523 100644 --- a/games/game_payday3.py +++ b/games/game_payday3.py @@ -73,7 +73,7 @@ def __init__(self, organizer: mobase.IOrganizer): super().__init__() self.organizer: mobase.IOrganizer = organizer - def move_overwrite_merge(self, source: str, destination: str): + def moveOverwriteMerge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -83,7 +83,7 @@ def move_overwrite_merge(self, source: str, destination: str): 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) + self.moveOverwriteMerge(s_item, d_item) os.rmdir(source) def dataLooksValid( @@ -231,10 +231,10 @@ def init(self, organizer: mobase.IOrganizer) -> bool: self.dataChecker = Payday3ModDataChecker(organizer) self._register_feature(self.dataChecker) self._register_feature(Payday3ModDataContent()) - organizer.onUserInterfaceInitialized(self.init_tab) + organizer.onUserInterfaceInitialized(self.initTab) return True - def init_tab(self, main_window: QMainWindow): + def initTab(self, main_window: QMainWindow): if self._organizer.managedGame() != self: return self._main_window = main_window @@ -260,7 +260,7 @@ def executables(self): ] @cached_property - def _base_dlls(self) -> set[str]: + def baseDlls(self) -> set[str]: base_dir = Path(self.gameDirectory().absolutePath()) return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} @@ -277,7 +277,7 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: return efls for e in tree: relpath = e.pathFrom(tree) - if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + if relpath and e.hasSuffix("dll") and relpath not in self.baseDlls: libs.add(relpath) exes = self.executables() efls = efls + [ @@ -289,7 +289,7 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: ] return efls - def write_default_mods(self, profile: QDir): + def writeDefaultMods(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(): @@ -307,7 +307,7 @@ def iniFiles(self): return ["GameUserSettings.ini", "Input.ini"] def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): - self.write_default_mods(directory) + self.writeDefaultMods(directory) base_data_dir = self.dataDirectory().absolutePath() diff --git a/games/game_raid2.py b/games/game_raid2.py index fe36166d..2ddf6e9c 100644 --- a/games/game_raid2.py +++ b/games/game_raid2.py @@ -362,7 +362,7 @@ def dllCopy(self, mods: dict[str, mobase.ModState]): os.remove(file_path_target) @cached_property - def _base_dlls(self) -> set[str]: + def baseDlls(self) -> set[str]: base_dir = Path(self.gameDirectory().absolutePath()) return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} @@ -379,7 +379,7 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: return efls for e in tree: relpath = e.pathFrom(tree) - if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + if relpath and e.hasSuffix("dll") and relpath not in self.baseDlls: libs.add(relpath) exes = self.executables() efls = efls + [ diff --git a/games/game_silenthill2remake.py b/games/game_silenthill2remake.py index 0513d0fa..91f19116 100644 --- a/games/game_silenthill2remake.py +++ b/games/game_silenthill2remake.py @@ -72,7 +72,7 @@ def __init__(self, organizer: mobase.IOrganizer): super().__init__() self.organizer: mobase.IOrganizer = organizer - def move_overwrite_merge(self, source: str, destination: str): + def moveOverwriteMerge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -82,7 +82,7 @@ def move_overwrite_merge(self, source: str, destination: str): 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) + self.moveOverwriteMerge(s_item, d_item) os.rmdir(source) def dataLooksValid( @@ -229,10 +229,10 @@ def init(self, organizer: mobase.IOrganizer) -> bool: self.dataChecker = SilentHill2ModDataChecker(organizer) self._register_feature(self.dataChecker) self._register_feature(SilentHill2ModDataContent()) - organizer.onUserInterfaceInitialized(self.init_tab) + organizer.onUserInterfaceInitialized(self.initTab) return True - def init_tab(self, main_window: QMainWindow): + def initTab(self, main_window: QMainWindow): if self._organizer.managedGame() != self: return self._main_window = main_window @@ -258,7 +258,7 @@ def executables(self): ] @cached_property - def _base_dlls(self) -> set[str]: + def baseDlls(self) -> set[str]: base_dir = Path(self.gameDirectory().absolutePath()) return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} @@ -275,7 +275,7 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: return efls for e in tree: relpath = e.pathFrom(tree) - if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + if relpath and e.hasSuffix("dll") and relpath not in self.baseDlls: libs.add(relpath) exes = self.executables() efls = efls + [ @@ -287,7 +287,7 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: ] return efls - def write_default_mods(self, profile: QDir): + def writeDefaultMods(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(): @@ -305,7 +305,7 @@ def iniFiles(self): return ["GameUserSettings.ini", "Input.ini"] def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): - self.write_default_mods(directory) + self.writeDefaultMods(directory) base_data_dir = self.dataDirectory().absolutePath() diff --git a/games/game_stalkeranomaly.py b/games/game_stalkeranomaly.py index 41d823ea..53e3b785 100644 --- a/games/game_stalkeranomaly.py +++ b/games/game_stalkeranomaly.py @@ -123,9 +123,9 @@ def walkContent( return mobase.IFileTree.WalkReturn.CONTINUE def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]: - self.content = [] + self.contents: list[int] = [] filetree.walk(self.walkContent, "/") - return self.content + return self.contents class StalkerAnomalySaveGame(BasicGameSaveGame): diff --git a/games/game_titanfall2.py b/games/game_titanfall2.py index 9cd7eff3..78c3ac25 100644 --- a/games/game_titanfall2.py +++ b/games/game_titanfall2.py @@ -75,10 +75,10 @@ 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.organizer.modList().onModInstalled(self.fixInstalledMod) self.needsNameFix = False - def move_overwrite_merge(self, source: str, destination: str): + def moveOverwriteMerge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -88,10 +88,10 @@ def move_overwrite_merge(self, source: str, destination: str): 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) + self.moveOverwriteMerge(s_item, d_item) os.rmdir(source) - def _Fix_Installed_Mod(self, mod: mobase.IModInterface): + def fixInstalledMod(self, mod: mobase.IModInterface): if not self.needsNameFix: return GameNorthstarPath = ( @@ -111,7 +111,7 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): modname = mod_data["name"] old_path = os.path.join(path, GameNorthstarPath + "FOLDERNAME") new_path = os.path.join(path, GameNorthstarPath + f"{modname}") - self.move_overwrite_merge(old_path, new_path) + self.moveOverwriteMerge(old_path, new_path) fixed = True elif filetree.exists( GameNorthstarPath + "FOLDERNAME_NAME", mobase.IFileTree.DIRECTORY @@ -119,7 +119,7 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): path = mod.absolutePath() old_path = os.path.join(path, GameNorthstarPath + "FOLDERNAME_NAME") new_path = os.path.join(path, GameNorthstarPath + f"{modname}") - self.move_overwrite_merge(old_path, new_path) + self.moveOverwriteMerge(old_path, new_path) fixed = True if not fixed: return @@ -140,7 +140,7 @@ def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str): return True return False - def first_tree(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + def firstTree(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: for e in filetree: if isinstance(e, mobase.IFileTree) and e.isDir(): return e @@ -161,9 +161,9 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: getattr(self.organizer.managedGame(), "GameNorthstarPath", "") + "/" ) treefixed = 0 - firsttreelayer: mobase.IFileTree | None = self.first_tree(filetree) + firsttreelayer: mobase.IFileTree | None = self.firstTree(filetree) if firsttreelayer is not None: - secondtreelayer: mobase.IFileTree | None = self.first_tree(firsttreelayer) + secondtreelayer: mobase.IFileTree | None = self.firstTree(firsttreelayer) if filetree.exists("mod.json", mobase.IFileTree.FILE): treefixed = self.allMoveTo(filetree, GameNorthstarPath + "FOLDERNAME/") if treefixed == 1: @@ -220,10 +220,10 @@ def init(self, organizer: mobase.IOrganizer) -> bool: self.dataChecker = Titanfall2ModDataChecker(organizer) self._register_feature(self.dataChecker) self._register_feature(Titanfall2ModDataContent()) - organizer.modList().onModStateChanged(self.update_enable_mods_json) + organizer.modList().onModStateChanged(self.updateEnableModsJson) return True - def update_enable_mods_json(self, mods: dict[str, mobase.ModState]): + def updateEnableModsJson(self, mods: dict[str, mobase.ModState]): Northstar_Config_Json = ( self._organizer.profilePath() + "/" + self.NorthstarModJson ) @@ -268,7 +268,7 @@ def executables(self): ] @cached_property - def _base_dlls(self) -> set[str]: + def baseDlls(self) -> set[str]: base_dir = Path(self.gameDirectory().absolutePath()) return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} @@ -285,7 +285,7 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: return efls for e in tree: relpath = e.pathFrom(tree) - if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + if relpath and e.hasSuffix("dll") and relpath not in self.baseDlls: libs.add(relpath) exes = self.executables() efls = efls + [ diff --git a/games/game_windrose.py b/games/game_windrose.py index dc548893..ea00be07 100644 --- a/games/game_windrose.py +++ b/games/game_windrose.py @@ -72,7 +72,7 @@ def __init__(self, organizer: mobase.IOrganizer): super().__init__() self.organizer: mobase.IOrganizer = organizer - def move_overwrite_merge(self, source: str, destination: str): + def moveOverwriteMerge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -82,7 +82,7 @@ def move_overwrite_merge(self, source: str, destination: str): 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) + self.moveOverwriteMerge(s_item, d_item) os.rmdir(source) def dataLooksValid( @@ -231,10 +231,10 @@ def init(self, organizer: mobase.IOrganizer) -> bool: self.dataChecker = WindroseModDataChecker(organizer) self._register_feature(self.dataChecker) self._register_feature(WindroseModDataContent()) - organizer.onUserInterfaceInitialized(self.init_tab) + organizer.onUserInterfaceInitialized(self.initTab) return True - def init_tab(self, main_window: QMainWindow): + def initTab(self, main_window: QMainWindow): if self._organizer.managedGame() != self: return self._main_window = main_window @@ -260,7 +260,7 @@ def executables(self): ] @cached_property - def _base_dlls(self) -> set[str]: + def baseDlls(self) -> set[str]: base_dir = Path(self.gameDirectory().absolutePath()) return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} @@ -277,7 +277,7 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: return efls for e in tree: relpath = e.pathFrom(tree) - if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + if relpath and e.hasSuffix("dll") and relpath not in self.baseDlls: libs.add(relpath) exes = self.executables() efls = efls + [ @@ -289,7 +289,7 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: ] return efls - def write_default_mods(self, profile: QDir): + def writeDefaultMods(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(): @@ -307,7 +307,7 @@ def iniFiles(self): return ["GameUserSettings.ini", "Engine.ini"] def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): - self.write_default_mods(directory) + self.writeDefaultMods(directory) base_data_dir = self.dataDirectory().absolutePath() diff --git a/games/game_zuma_deluxe.py b/games/game_zuma_deluxe.py index 86e8b8e5..cdbc7b09 100644 --- a/games/game_zuma_deluxe.py +++ b/games/game_zuma_deluxe.py @@ -68,10 +68,10 @@ 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.organizer.modList().onModInstalled(self.fixInstalledMod) self.needsNameFix = False - def move_overwrite_merge(self, source: str, destination: str): + def moveOverwriteMerge(self, source: str, destination: str): if not os.path.exists(destination): shutil.move(source, destination) return @@ -81,10 +81,10 @@ def move_overwrite_merge(self, source: str, destination: str): 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) + self.moveOverwriteMerge(s_item, d_item) os.rmdir(source) - def _Fix_Installed_Mod(self, mod: mobase.IModInterface): + def fixInstalledMod(self, mod: mobase.IModInterface): if not self.needsNameFix: return filetree: mobase.IFileTree = mod.fileTree() @@ -94,7 +94,7 @@ def _Fix_Installed_Mod(self, mod: mobase.IModInterface): 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) + self.moveOverwriteMerge(old_path, new_path) fixed = True if not fixed: return @@ -219,10 +219,10 @@ def init(self, organizer: mobase.IOrganizer) -> bool: self._register_feature(self.dataChecker) self._register_feature(ZumaModDataContent()) self._register_feature(BasicGameSaveGameInfo()) - organizer.modList().onModStateChanged(self.update_levels) + organizer.modList().onModStateChanged(self.updateLevels) return True - def update_levels(self, mods: dict[str, mobase.ModState]): + def updateLevels(self, mods: dict[str, mobase.ModState]): profile_levels_path = ( self._organizer.profilePath() + "/" + self.ProfileLevelsXml ) @@ -328,7 +328,7 @@ def executables(self): ] @cached_property - def _base_dlls(self) -> set[str]: + def baseDlls(self) -> set[str]: base_dir = Path(self.gameDirectory().absolutePath()) return {str(f.relative_to(base_dir)) for f in base_dir.glob("*.dll")} @@ -345,7 +345,7 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: return efls for e in tree: relpath = e.pathFrom(tree) - if relpath and e.hasSuffix("dll") and relpath not in self._base_dlls: + if relpath and e.hasSuffix("dll") and relpath not in self.baseDlls: libs.add(relpath) exes = self.executables() efls = efls + [ From d236f48a545424c01784cf6382d351ebad768c75 Mon Sep 17 00:00:00 2001 From: TsunaMoo Date: Thu, 7 May 2026 23:44:49 +0200 Subject: [PATCH 31/31] Rewrote Road to Vostok to Support Archives with multiple Mods --- games/game_roadtovostok.py | 194 ++++++++++++++++++++++++++++++++----- 1 file changed, 171 insertions(+), 23 deletions(-) diff --git a/games/game_roadtovostok.py b/games/game_roadtovostok.py index 627d6f5f..d30a2cd0 100644 --- a/games/game_roadtovostok.py +++ b/games/game_roadtovostok.py @@ -1,49 +1,197 @@ import os import shutil +from typing import TypedDict -from PyQt6.QtCore import QDir, QFileInfo +from PyQt6.QtCore import QDir, QFileInfo, Qt +from PyQt6.QtWidgets import ( + QDialog, + QDialogButtonBox, + QLabel, + QListWidget, + QListWidgetItem, + QVBoxLayout, +) import mobase from ..basic_game import BasicGame +class ModDetectionCandidate(TypedDict): + tree: mobase.IFileTree | mobase.FileTreeEntry + name: str + display: str + destination: str + + class RoadToVostokModDataChecker(mobase.ModDataChecker): def __init__(self, organizer: mobase.IOrganizer): super().__init__() self.organizer: mobase.IOrganizer = organizer + self.modDetectionCandidates: list[ModDetectionCandidate] = [] + + def moveOverwriteMerge(self, source: str, destination: str): + 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.moveOverwriteMerge(s_item, d_item) + os.rmdir(source) + + def sanitizeFolderName(self, name: str) -> str: + # Remove invalid characters for Windows folder names + invalid_chars = '+&<>:"|?*\\/' + for char in invalid_chars: + name = name.replace(char, "") + # Remove control characters (ASCII 0-31) + name = "".join(c for c in name if ord(c) >= 32) + # Remove trailing periods and spaces + name = name.rstrip(". ") + # If name is empty after sanitization, use a default + if not name: + name = "FOLDERNAME" + self.needsNameFix = True + return name def dataLooksValid( self, filetree: mobase.IFileTree ) -> mobase.ModDataChecker.CheckReturn: - if filetree.exists("mods", mobase.IFileTree.DIRECTORY) and not filetree.exists( - "mod.txt", mobase.IFileTree.FILE - ): + if filetree.exists("mods", mobase.IFileTree.DIRECTORY): return mobase.ModDataChecker.VALID - for e in filetree: - if e.isFile() and e.suffix().casefold() == "pck": - return mobase.ModDataChecker.VALID return mobase.ModDataChecker.FIXABLE - def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + def moveTreeContent( + self, + filetree: mobase.IFileTree, + file: mobase.IFileTree | mobase.FileTreeEntry, + ) -> None: GameModsPath = getattr(self.organizer.managedGame(), "GameModsPath", "") + "/" - allowedUnzippedExt = ["zip", "vmz"] - - for branch in filetree: + if filetree.name() == "": + filetree.move(file, GameModsPath, mobase.IFileTree.MERGE) + else: mod_name = filetree.name() - if mod_name == "": - mod_name = branch.name() + mod_file = file.name() mod_path = os.path.join(self.organizer.modsPath(), mod_name) + insideMods = os.path.join(mod_path, GameModsPath) + os.makedirs(insideMods, exist_ok=True) + src = os.path.join(mod_path, mod_file) + dst = os.path.join(mod_path, GameModsPath, mod_file) + print( + f"Mod: {mod_name} with File: {mod_file} at {mod_path} is being moved to: {insideMods}" + ) + print(f"Moving {src} to {dst}") + shutil.move( + src, + dst, + ) + return None + + def addModDetectionCandidate( + self, + tree: mobase.IFileTree | mobase.FileTreeEntry, + name: str, + category: str, + destination: str, + ) -> None: + tree_name = tree.name() + tree_path = tree.path() + + print( + f"Detected mod candidate: {tree_name} | " + f"path={tree_path} | category={category} | destination={destination}" + ) + self.modDetectionCandidates.append( + { + "tree": tree, + "name": tree_name, + "display": f"{name} ({category})", + "destination": destination, + } + ) + + def showModDetectionDialog(self) -> set[int] | None: + if not self.modDetectionCandidates: + return set() + + dialog = QDialog() + dialog.setWindowTitle("Found Mods") + + layout = QVBoxLayout(dialog) + layout.addWidget(QLabel("Select the mods to install:")) + + listWidget = QListWidget() + listWidget.setSelectionMode(QListWidget.SelectionMode.NoSelection) + for candidate in self.modDetectionCandidates: + item = QListWidgetItem(candidate["display"]) + item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable) + item.setCheckState(Qt.CheckState.Checked) + listWidget.addItem(item) + + layout.addWidget(listWidget) + + buttonBox = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel + ) + buttonBox.accepted.connect(lambda: dialog.accept()) # type: ignore + buttonBox.rejected.connect(lambda: dialog.reject()) # type: ignore + layout.addWidget(buttonBox) + + if dialog.exec() != QDialog.DialogCode.Accepted: + return None + + selectedIndexes: set[int] = set() + for index in range(listWidget.count()): + item = listWidget.item(index) if ( - not filetree.createOrphanTree("OrphanTree") - and os.path.exists(mod_path) - and branch.suffix().casefold() in allowedUnzippedExt + isinstance(item, QListWidgetItem) + and item.checkState() == Qt.CheckState.Checked ): - 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()), - ) + selectedIndexes.add(index) + + return selectedIndexes + + def collectModCandidates( + self, tree: mobase.IFileTree | mobase.FileTreeEntry + ) -> bool: + print(f"Collecting mod candidates in: {tree.path()}") + if os.path.splitext(tree.path())[1] == ".vmz": + print(f"Found vmz file: {tree.name()}") + self.addModDetectionCandidate( + tree, + self.sanitizeFolderName(tree.name()), + "VMZ Archive", + "mods/", + ) + return True + return False + + def walkEntry(self, path: str, entry: mobase.FileTreeEntry): + self.collectModCandidates(entry) + return mobase.IFileTree.WalkReturn.CONTINUE + + def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None: + self.modDetectionCandidates = [] + + self.collectModCandidates(filetree) + filetree.walk(self.walkEntry, "/") + + if len(self.modDetectionCandidates) == 1: + selectedIndexes = {0} + else: + selectedIndexes = self.showModDetectionDialog() + if selectedIndexes is None: + return None + + for index in selectedIndexes: + candidate = self.modDetectionCandidates[index] + print(f"Installing Mod: {candidate['name']}") + self.moveTreeContent(filetree, candidate["tree"]) + return filetree @@ -56,8 +204,8 @@ class RoadToVostokGame(BasicGame): GameShortName = "roadtovostok" GameSteamId = 1963610 GameBinary = "RTV.exe" - GameDataPath = "%GAME_PATH%" GameModsPath = "mods" + GameDataPath = "%GAME_PATH%" GameDocumentsDirectory = "%USERPROFILE%/AppData/Roaming/Road to Vostok" GameSaveExtension = "tres" @@ -79,7 +227,7 @@ def iniFiles(self): return ["settings.cfg"] def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): - modsPath = self.dataDirectory().absolutePath() + modsPath = self.dataDirectory().absolutePath() + "/mods" if not os.path.exists(modsPath): os.mkdir(modsPath) super().initializeProfile(directory, settings)