mirror of
https://github.com/ModOrganizer2/modorganizer-basic_games.git
synced 2026-07-27 14:07:29 -07:00
Merge pull request #6 from Holt59/add-gog-detection
Add automatic GOG games detection and support for multiple IDs.
This commit is contained in:
+112
-18
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
+5
-4
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user