diff --git a/basic_game.py b/basic_game.py index 78ac74c..94e6351 100644 --- a/basic_game.py +++ b/basic_game.py @@ -2,6 +2,7 @@ import shutil +from pathlib import Path from typing import List, Union, Optional, TypeVar, Callable, Generic, Dict @@ -47,7 +48,7 @@ class BasicGameMapping(Generic[T]): _required: bool # Callable returning a default value (if not required): - _default: Optional[Callable[["BasicGame"], T]] + _default: Callable[["BasicGame"], T] # Function to apply to the value: _apply_fn: Optional[Callable[[Union[T, str]], T]] @@ -64,7 +65,6 @@ class BasicGameMapping(Generic[T]): self._game = game self._exposed_name = exposed_name self._internal_method_name = internal_method - self._default = default self._apply_fn = apply_fn if hasattr(game, self._exposed_name): @@ -80,9 +80,8 @@ class BasicGameMapping(Generic[T]): ) ) self._default = lambda game: value # type: ignore - elif self._default is not None: - # Not required, ok! - pass + elif default is not None: + self._default = default # type: ignore elif getattr(game.__class__, self._internal_method_name) is getattr( BasicGame, self._internal_method_name ): @@ -104,6 +103,68 @@ class BasicGameMapping(Generic[T]): return value +class BasicGameOptionsMapping(BasicGameMapping[List[T]]): + + """ + Represents a game mappings for which multiple options are possible. The game + plugin is responsible to choose the right option depending on the context. + """ + + _index: int + + def __init__( + self, + game, + exposed_name, + internal_method, + default: Optional[Callable[["BasicGame"], T]] = None, + apply_fn: Optional[Callable[[Union[List[T], str]], List[T]]] = None, + ): + super().__init__(game, exposed_name, internal_method, lambda g: [], apply_fn) + self._index = -1 + self._current_default = default + + def set_index(self, index: int): + """ + Set the index of the option to use. + + Args: + index: Index of the option to use. + """ + self._index = index + + def set_value(self, value: T): + """ + Set the index corresponding of the given value. If the value is not present, + the index is set to -1. + + Args: + value: The value to set the index to. + """ + try: + self._index = self.get().index(value) + except ValueError: + self._index = -1 + + def current(self) -> T: + values = self._default(self._game) # type: ignore + + if not values: + return self._current_default(self._game) # type: ignore + + if self._index == -1: + value = values[0] + else: + value = values[self._index] + + if isinstance(value, str): + return replace_variables(value, self._game) # type: ignore + elif isinstance(value, QDir): + return QDir(replace_variables(value.path(), self._game)) # type: ignore + + return value + + class BasicGameMappings: name: BasicGameMapping[str] @@ -121,7 +182,8 @@ class BasicGameMappings: documentsDirectory: BasicGameMapping[QDir] savesDirectory: BasicGameMapping[QDir] savegameExtension: BasicGameMapping[str] - steamAPPId: BasicGameMapping[str] + steamAPPId: BasicGameOptionsMapping[str] + gogAPPId: BasicGameOptionsMapping[str] @staticmethod def _default_documents_directory(game): @@ -200,8 +262,18 @@ class BasicGameMappings: self.savegameExtension = BasicGameMapping( game, "GameSaveExtension", "savegameExtension", default=lambda g: "save" ) - self.steamAPPId = BasicGameMapping( - game, "GameSteamId", "steamAPPId", default=lambda g: "", apply_fn=str + + # Convert Union[int, str, List[Union[int, str]]] to List[str]. + def ids_apply(v) -> List[str]: + if isinstance(v, (int, str)): + v = [v] + return [str(x) for x in v] + + self.steamAPPId = BasicGameOptionsMapping( + game, "GameSteamId", "steamAPPId", default=lambda g: "", apply_fn=ids_apply + ) + self.gogAPPId = BasicGameOptionsMapping( + game, "GameGogId", "gogAPPId", default=lambda g: "", apply_fn=ids_apply ) @@ -211,14 +283,17 @@ class BasicGame(mobase.IPluginGame): to make it easier to create game plugins without having to implement all the methods of mobase.IPluginGame.""" - # List of steam games: - steam_games: Dict[str, str] + # List of steam and GOG games: + steam_games: Dict[str, Path] + gog_games: Dict[str, Path] @staticmethod def setup(): - from .steam_utils import find_games + from .steam_utils import find_games as find_steam_games + from .gog_utils import find_games as find_gog_games - BasicGame.steam_games = find_games() + BasicGame.steam_games = find_steam_games() + BasicGame.gog_games = find_gog_games() # File containing the plugin: _fromName: str @@ -293,7 +368,10 @@ class BasicGame(mobase.IPluginGame): return self.mappings.nexusGameId.get() def steamAPPId(self) -> str: - return self.mappings.steamAPPId.get() + return self.mappings.steamAPPId.current() + + def gogAPPId(self) -> str: + return self.mappings.gogAPPId.current() def binaryName(self) -> str: return self.mappings.binaryName.get() @@ -373,9 +451,15 @@ class BasicGame(mobase.IPluginGame): return aQDir.exists(self.binaryName()) def isInstalled(self) -> bool: - if self.steamAPPId() in BasicGame.steam_games: - self.setGamePath(BasicGame.steam_games[self.steamAPPId()]) - return True + for steam_id in self.mappings.steamAPPId.get(): + if steam_id in BasicGame.steam_games: + self.setGamePath(BasicGame.steam_games[steam_id]) + return True + + for gog_id in self.mappings.gogAPPId.get(): + if gog_id in BasicGame.gog_games: + self.setGamePath(BasicGame.gog_games[gog_id]) + return True return False @@ -390,8 +474,18 @@ class BasicGame(mobase.IPluginGame): self.gameDirectory().absoluteFilePath(self.mappings.dataDirectory.get()) ) - def setGamePath(self, pathStr: str): - self._gamePath = pathStr + def setGamePath(self, path: Union[Path, str]): + self._gamePath = str(path) + + path = Path(path) + + # Check if we have a matching steam ID or GOG id and set the index accordingly: + for steamid, steampath in BasicGame.steam_games.items(): + if steampath == path: + self.mappings.steamAPPId.set_value(steamid) + for gogid, gogpath in BasicGame.gog_games.items(): + if gogpath == path: + self.mappings.steamAPPId.set_value(gogid) def documentsDirectory(self) -> QDir: return self.mappings.documentsDirectory.get() diff --git a/games/game_witcher3.py b/games/game_witcher3.py index b153a2a..383d2f2 100644 --- a/games/game_witcher3.py +++ b/games/game_witcher3.py @@ -12,13 +12,13 @@ class Witcher3Game(BasicGame): Name = "Witcher 3 Support Plugin" Author = "Holt59" Version = "1.0.0a" - Description = "The Description Of The Dead" GameName = "The Witcher 3: Wild Hunt" GameShortName = "witcher3" GaneNexusHame = "witcher3" GameNexusId = 952 - GameSteamId = 292030 + GameSteamId = [499450, 292030] + GameGogId = [1640424747, 1495134320, 1207664663, 1207664643] GameBinary = "bin/x64/witcher3.exe" GameDataPath = "Mods" GameSaveExtension = "sav" diff --git a/games/game_zeusandposeidon.py b/games/game_zeusandposeidon.py new file mode 100644 index 0000000..6794e65 --- /dev/null +++ b/games/game_zeusandposeidon.py @@ -0,0 +1,69 @@ +# -*- encoding: utf-8 -*- + +from typing import Optional, List + +import mobase + +from ..basic_game import BasicGame + + +class ZeusAndPoseidonModDataChecker(mobase.ModDataChecker): + def __init__(self): + super().__init__() + + def dataLooksValid( + self, tree: mobase.IFileTree + ) -> mobase.ModDataChecker.CheckReturn: + + folders: List[mobase.IFileTree] = [] + files: List[mobase.FileTreeEntry] = [] + + for entry in tree: + if isinstance(entry, mobase.IFileTree): + folders.append(entry) + else: + files.append(entry) + + if len(folders) != 1: + return mobase.ModDataChecker.INVALID + + folder = folders[0] + pakfile = folder.name() + ".pak" + if folder.exists(pakfile): + if tree.exists(pakfile): + return mobase.ModDataChecker.VALID + else: + return mobase.ModDataChecker.FIXABLE + + return mobase.ModDataChecker.INVALID + + def fix(self, tree: mobase.IFileTree) -> Optional[mobase.IFileTree]: + if not isinstance(tree[0], mobase.IFileTree): + return None + entry = tree[0].find(tree[0].name() + ".pak") + if entry is None: + return None + tree.copy(entry, "", mobase.IFileTree.InsertPolicy.FAIL_IF_EXISTS) + return tree + + +class ZeusAndPoseidonGame(BasicGame): + + Name = "Zeus and Poseidon Support Plugin" + Author = "Holt59" + Version = "1.0.0a" + + GameName = "Zeus and Poseidon" + GameShortName = "zeusandposeidon" # No Nexus support + GameSteamId = 566050 + GameGogId = 1207659039 + GameBinary = "Zeus.exe" + GameDataPath = "Adventures" + GameDocumentsDirectory = "%GAME_PATH%" + GameSavesDirectory = "%GAME_PATH%/Save" + GameSaveExtension = "sav" + + def init(self, organizer: mobase.IOrganizer): + super().init(organizer) + self._featureMap[mobase.ModDataChecker] = ZeusAndPoseidonModDataChecker() + return True diff --git a/gog_utils.py b/gog_utils.py new file mode 100644 index 0000000..3c11e41 --- /dev/null +++ b/gog_utils.py @@ -0,0 +1,40 @@ +# -*- encoding: utf-8 -*- + +# Code adapted from EzioTheDeadPoet / erri120: +# https://github.com/ModOrganizer2/modorganizer-basic_games/pull/5 + +import winreg # type: ignore + +from pathlib import Path +from typing import Dict + + +def find_games() -> Dict[str, Path]: + + # List the game IDs from the registry: + game_ids = [] + try: + with winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, r"Software\Wow6432Node\GOG.com\Games" + ) as key: + nkeys = winreg.QueryInfoKey(key)[0] + for ik in range(nkeys): + game_key = winreg.EnumKey(key, ik) + if game_key.isdigit(): + game_ids.append(game_key) + except FileNotFoundError: + return {} + + # For each game, query the path: + games: Dict[str, Path] = {} + for game_id in game_ids: + try: + with winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, + f"Software\\Wow6432Node\\GOG.com\\Games\\{game_id}", + ) as key: + games[game_id] = Path(winreg.QueryValueEx(key, "path")[0]) + except FileNotFoundError: + pass + + return games diff --git a/steam_utils.py b/steam_utils.py index 3f7ee96..356b579 100644 --- a/steam_utils.py +++ b/steam_utils.py @@ -5,6 +5,7 @@ import os import winreg # type: ignore +from pathlib import Path from typing import Dict @@ -97,7 +98,7 @@ def parse_library_info(library_vdf_path): return library_folders -def find_games() -> Dict[str, str]: +def find_games() -> Dict[str, Path]: try: with winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Valve\\Steam") as key: value = winreg.QueryValueEx(key, "SteamExe") @@ -112,11 +113,11 @@ def find_games() -> Dict[str, str]: library_folders = parse_library_info(library_vdf_path) library_folders.append(LibraryFolder(os.path.dirname(steam_path))) - games: Dict[str, str] = {} + games: Dict[str, Path] = {} for library in library_folders: for game in library.games: - games[game.appid] = os.path.join( - library.path, "steamapps", "common", game.installdir + games[game.appid] = Path(library.path).joinpath( + "steamapps", "common", game.installdir ) return games