diff --git a/games/game_arkhamcity.py b/games/game_arkhamcity.py new file mode 100644 index 0000000..acc2d28 --- /dev/null +++ b/games/game_arkhamcity.py @@ -0,0 +1,86 @@ +import mobase +from PyQt6.QtCore import QDir, QFileInfo + +from ..basic_features import BasicLocalSavegames +from ..basic_game import BasicGame +from ..steam_utils import find_steam_path + + +# Lifted from https://github.com/ModOrganizer2/modorganizer-basic_games/blob/71dbb8c557d43cba9d290674a332e7ecd1650261/games/game_darkestdungeon.py +class ArkhamCityModDataChecker(mobase.ModDataChecker): + def __init__(self): + super().__init__() + self.validDirNames = [ + "config", + "cookedpcconsole", + "localization", + "movies", + "moviesstereo", + "splash", + ] + + def dataLooksValid( + self, filetree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + for entry in filetree: + if not entry.isDir(): + continue + if entry.name().casefold() in self.validDirNames: + return mobase.ModDataChecker.VALID + return mobase.ModDataChecker.INVALID + + +class ArkhamCityGame(BasicGame): + Name = "Batman: Arkham City Plugin" + Author = "Paynamia" + Version = "0.5.3" + + GameName = "Batman: Arkham City" + GameShortName = "batmanarkhamcity" + GameNexusId = 372 + GameSteamId = 200260 + GameGogId = 1260066469 + GameEpicId = "Egret" + GameBinary = "Binaries/Win32/BatmanAC.exe" + GameLauncher = "Binaries/Win32/BmLauncher.exe" + GameDataPath = "BmGame" + GameDocumentsDirectory = ( + "%DOCUMENTS%/WB Games/Batman Arkham City GOTY/BmGame/Config" + ) + GameIniFiles = ["UserEngine.ini", "UserGame.ini", "UserInput.ini"] + GameSaveExtension = "sgd" + + # This will only detect saves from the earliest-created Steam profile on the user's PC. + def savesDirectory(self) -> QDir: + docSaves = QDir(self.documentsDirectory().cleanPath("../../SaveData")) + if self.is_steam(): + if (steamDir := find_steam_path()) is None: + return docSaves + for child in steamDir.joinpath("userdata").iterdir(): + if not child.is_dir() or child.name == "0": + continue + steamSaves = child.joinpath("200260", "remote") + if steamSaves.is_dir(): + return QDir(str(steamSaves)) + else: + return docSaves + else: + return docSaves + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self._register_feature(ArkhamCityModDataChecker()) + self._register_feature(BasicLocalSavegames(self.savesDirectory())) + return True + + def executables(self): + return [ + mobase.ExecutableInfo( + "Batman: Arkham City", + QFileInfo(self.gameDirectory(), "Binaries/Win32/BatmanAC.exe"), + ), + mobase.ExecutableInfo( + "Arkham City Launcher", + QFileInfo(self.gameDirectory(), "Binaries/Win32/BmLauncher.exe"), + ), + ] diff --git a/games/game_borderlands1.py b/games/game_borderlands1.py new file mode 100644 index 0000000..4b90b51 --- /dev/null +++ b/games/game_borderlands1.py @@ -0,0 +1,153 @@ +import re + +import mobase +from mobase import FileTreeEntry, IFileTree, ModDataChecker + +from ..basic_features import BasicModDataChecker +from ..basic_game import BasicGame + +_extention_pattern = re.compile("\\.(upk|umap|u|int|dll|exe)$", re.I) +_mapslot_pattern = re.compile("^Mapslot\\d\\d?\\.umap$", re.I) + +_mod_dirs = { + "Binaries".casefold(): "/", + "WillowGame".casefold(): "/", + "CookedPC".casefold(): "WillowGame/", + "Localization".casefold(): "WillowGame/", + "Maps".casefold(): "WillowGame/CookedPC/", +} + +_slots_path = "WillowGame/CookedPC/Maps/MapSlots" + + +def _check_filetree( + filetree: IFileTree | FileTreeEntry, recurse: bool +) -> ModDataChecker.CheckReturn: + if filetree.isFile(): + if _extention_pattern.search(filetree.name()): + if _mapslot_pattern.search(filetree.name()): + return ModDataChecker.FIXABLE + else: + return ModDataChecker.INVALID + elif recurse and isinstance(filetree, IFileTree): + status = ModDataChecker.VALID + for child in filetree: + child_status = _check_filetree(child, True) + if child_status is ModDataChecker.INVALID: + return ModDataChecker.INVALID + elif child_status is ModDataChecker.FIXABLE: + status = ModDataChecker.FIXABLE + return status + + return ModDataChecker.VALID + + +def _get_nest(filetree: IFileTree) -> IFileTree | None: + children = tuple(filetree) + if ( + len(children) == 1 + and children[0].isDir() + and isinstance(children[0], IFileTree) + and _mod_dirs.get(children[0].name().casefold()) is None + ): + return children[0] + return None + + +def _get_slotstree(roottree: IFileTree) -> IFileTree: + slotstree: IFileTree | None = roottree.find(_slots_path) # type: ignore + if slotstree is None: + slotstree = roottree.addDirectory(_slots_path) + return slotstree + + +def _fix_mapslots(filetree: IFileTree, roottree: IFileTree) -> None: + for child in filetree: + if child.isDir() and isinstance(child, IFileTree): + _fix_mapslots(child, roottree) + elif _mapslot_pattern.search(child.name()): + child.moveTo(_get_slotstree(roottree)) + + +class Borderlands1ModDataChecker(BasicModDataChecker): + def dataLooksValid(self, filetree: IFileTree) -> ModDataChecker.CheckReturn: + parent = filetree.parent() + if parent is not None: + return self.dataLooksValid(parent) + + status = ModDataChecker.VALID + + nest = _get_nest(filetree) + if nest is not None: + status = ModDataChecker.FIXABLE + filetree = nest + + if _check_filetree(filetree, False) is ModDataChecker.INVALID: + return ModDataChecker.INVALID + + slotstree = filetree.find(_slots_path) + if slotstree is not None and not slotstree.isDir(): + return ModDataChecker.INVALID + + for child in filetree: + if child.isDir(): + destination = _mod_dirs.get(child.name().casefold()) + if destination is None: + child_status = _check_filetree(child, True) + if child_status is ModDataChecker.INVALID: + return ModDataChecker.INVALID + elif child_status is ModDataChecker.FIXABLE: + status = ModDataChecker.FIXABLE + elif destination != "/": + status = ModDataChecker.FIXABLE + elif _mapslot_pattern.search(child.name()): + status = ModDataChecker.FIXABLE + elif _extention_pattern.search(child.name()): + return ModDataChecker.INVALID + return status + + def fix(self, filetree: IFileTree) -> IFileTree: + nest = _get_nest(filetree) + if nest is not None: + conflict: FileTreeEntry | None = None + for child in tuple(nest): + if not child.moveTo(filetree): + conflict = child + conflict.detach() + filetree.remove(nest) + if conflict is not None: + conflict.moveTo(filetree) + + for child in tuple(filetree): + if child.isDir() and isinstance(child, IFileTree): + destination = _mod_dirs.get(child.name().casefold()) + if destination is None: + _fix_mapslots(child, filetree) + elif destination != "/": + filetree.move(child, destination) + elif _mapslot_pattern.search(child.name()): + child.moveTo(_get_slotstree(filetree)) + + return filetree + + +class Borderlands1Game(BasicGame): + Name = "Borderlands 1 Support Plugin" + Author = "Miner Of Worlds, RedxYeti, mopioid" + Version = "1.0.0" + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self._register_feature(Borderlands1ModDataChecker()) + return True + + GameName = "Borderlands" + GameShortName = "Borderlands" + GameNexusName = "Borderlands GOTY" + GameSteamId = 8980 + GameBinary = "Binaries/Borderlands.exe" + GameDataPath = "." + GameSaveExtension = "sav" + GameDocumentsDirectory = "%DOCUMENTS%/My Games/Borderlands/" + GameSavesDirectory = "%GAME_DOCUMENTS%/savedata" + GameIniFiles = "%GAME_DOCUMENTS%/WillowGame/Config" diff --git a/games/game_cyberpunk2077.py b/games/game_cyberpunk2077.py index 10da96c..7a2ed94 100644 --- a/games/game_cyberpunk2077.py +++ b/games/game_cyberpunk2077.py @@ -327,7 +327,7 @@ def init(self, organizer: mobase.IOrganizer) -> bool: organizer, archive=ModListFile( Path("archive/pc/mod/modlist.txt"), - "archive/pc/mod/*", + "archive/pc/mod/*.archive", reversed_priority=bool(self._get_setting("reverse_archive_load_order")), ), redmod=ModListFile(