From 0fd4d87c7a28448fb438b82ca64940b439b3aab8 Mon Sep 17 00:00:00 2001 From: Zash Date: Tue, 12 Apr 2022 14:01:38 +0200 Subject: [PATCH 1/2] Game Detection: Epic Games and Legendary launcher support --- README.md | 14 ++++++++--- basic_game.py | 23 ++++++++++++++++- epic_utils.py | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 5 deletions(-) create mode 100644 epic_utils.py diff --git a/README.md b/README.md index 961ed8d..1e0f1e5 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,7 @@ class Witcher3Game(BasicGame): | GameGogId | GOG ID of the game (Optional) | `gogAPPId` | `List[str]` or `str` or `int` | | GameOriginManifestIds | Origin Manifest ID of the game (Optional) | `originManifestIds` | `List[str]` or `str` | | GameOriginWatcherExecutables | Executables to watch for Origin DRM (Optional) | `originWatcherExecutables` | `List[str]` or `str` | +| GameEpicId | Epic ID (`AppName`) of the game (Optional) | `epicAPPId` | `List[str]` or `str` | You can use the following variables for `str`: @@ -166,9 +167,9 @@ You can use the following variables for `str`: The meta-plugin provides some useful extra feature: -1. **Automatic Steam, GOG, and Origin game detection:** If you provide Steam, GOG, or Origin IDs for the game (via - `GameSteamId`, `GameGogId`, or `GameOriginManifestIds`), the game will be listed in the list of available games when creating a new - MO2 instance (if the game is installed via Steam or GOG). +1. **Automatic Steam, GOG, Origin and Epic game detection:** If you provide Steam, GOG, Origin or Epic IDs for the game + (via `GameSteamId`, `GameGogId`, `GameOriginManifestIds`, or `GameEpicId`), the game will be listed in the list of + available games when creating a new MO2 instance (if the game is installed via Steam, GOG, Origin or Epic Games / Legendary). 2. **Basic save game preview:** If you use the Python version, and if you can easily obtain a picture (file) for any saves, you can provide basic save-game preview by using the `BasicGameSaveGameInfo`. See [games/game_witcher3.py](games/game_witcher3.py) for more details. @@ -177,4 +178,9 @@ Game IDs can be found here: - For Steam on [Steam Database](https://steamdb.info/) - For GOG on [GOG Database](https://www.gogdb.org/) -- For Origin from C:\ProgramData\Origin\LocalContent (.mfst files) +- For Origin from `C:\ProgramData\Origin\LocalContent` (.mfst files) +- For Epic Games (`AppName`) from: + - `C:\ProgramData\Epic\EpicGamesLauncher\Data\Manifests\` (.item files) + - or: `C:\ProgramData\Epic\EpicGamesLauncher\UnrealEngineLauncher\LauncherInstalled.dat` +- For Legendary (alt. Epic launcher) via command `legendary list-games` + or from: `%USERPROFILE%\.config\legendary\installed.json` diff --git a/basic_game.py b/basic_game.py index 375af49..f3ebc41 100644 --- a/basic_game.py +++ b/basic_game.py @@ -205,6 +205,7 @@ class BasicGameMappings: gogAPPId: BasicGameOptionsMapping[str] originManifestIds: BasicGameOptionsMapping[str] originWatcherExecutables: BasicGameMapping[List[str]] + epicAPPId: BasicGameOptionsMapping[str] @staticmethod def _default_documents_directory(game): @@ -316,6 +317,9 @@ class BasicGameMappings: apply_fn=lambda s: [s] if isinstance(s, str) else s, default=lambda g: [], ) + self.epicAPPId = BasicGameOptionsMapping( + game, "GameEpicId", "epicAPPId", default=lambda g: "", apply_fn=ids_apply + ) class BasicGame(mobase.IPluginGame): @@ -324,20 +328,23 @@ 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, GOG, and origin games: + # List of steam, GOG, origin and Epic games: steam_games: Dict[str, Path] gog_games: Dict[str, Path] origin_games: Dict[str, Path] + epic_games: Dict[str, Path] @staticmethod def setup(): from .gog_utils import find_games as find_gog_games from .steam_utils import find_games as find_steam_games from .origin_utils import find_games as find_origin_games + from .epic_utils import find_games as find_epic_games BasicGame.steam_games = find_steam_games() BasicGame.gog_games = find_gog_games() BasicGame.origin_games = find_origin_games() + BasicGame.epic_games = find_epic_games() # File containing the plugin: _fromName: str @@ -372,6 +379,9 @@ class BasicGame(mobase.IPluginGame): def is_origin(self) -> bool: return self._mappings.originManifestIds.has_value() + def is_epic(self) -> bool: + return self._mappings.epicAPPId.has_value() + # IPlugin interface: def init(self, organizer: mobase.IOrganizer) -> bool: @@ -434,6 +444,11 @@ class BasicGame(mobase.IPluginGame): self.setGamePath(BasicGame.origin_games[origin_manifest_id]) return + for epic_id in self._mappings.epicAPPId.get(): + if epic_id in BasicGame.epic_games: + self.setGamePath(BasicGame.epic_games[epic_id]) + return + def gameName(self) -> str: return self._mappings.gameName.get() @@ -463,6 +478,9 @@ class BasicGame(mobase.IPluginGame): def gogAPPId(self) -> str: return self._mappings.gogAPPId.current() + def epicAPPId(self) -> str: + return self._mappings.epicAPPId.current() + def binaryName(self) -> str: return self._mappings.binaryName.get() @@ -570,6 +588,9 @@ class BasicGame(mobase.IPluginGame): for originid, originpath in BasicGame.origin_games.items(): if originpath == path: self._mappings.originManifestIds.set_value(originid) + for epicid, epicpath in BasicGame.epic_games.items(): + if epicpath == path: + self._mappings.epicAPPId.set_value(epicid) def documentsDirectory(self) -> QDir: return self._mappings.documentsDirectory.get() diff --git a/epic_utils.py b/epic_utils.py new file mode 100644 index 0000000..6612a57 --- /dev/null +++ b/epic_utils.py @@ -0,0 +1,70 @@ +# -*- encoding: utf-8 -*- +from __future__ import annotations + +import itertools +import json +import os +import sys +import winreg +from collections.abc import Iterable +from pathlib import Path + + +def find_epic_games() -> Iterable[tuple[str, Path]]: + try: + with winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, + r"Software\Wow6432Node\Epic Games\EpicGamesLauncher", + ) as key: + epic_app_data_path, _ = winreg.QueryValueEx(key, "AppDataPath") + except FileNotFoundError: + return + + manifests_path = Path(os.path.expandvars(epic_app_data_path)).joinpath("Manifests") + if manifests_path.exists(): + for manifest_file_path in manifests_path.glob("*.item"): + try: + with open(manifest_file_path, encoding="utf-8") as manifest_file: + manifest_file_data = json.load(manifest_file) + yield manifest_file_data["AppName"], Path( + manifest_file_data["InstallLocation"] + ) + except (json.JSONDecodeError, KeyError): + print( + "Unable to parse Epic Games manifest file", + manifest_file_path, + file=sys.stderr, + ) + + +def find_legendary_games() -> Iterable[tuple[str, Path]]: + # Based on legendary source: + # https://github.com/derrod/legendary/blob/master/legendary/lfs/lgndry.py + if config_path := os.environ.get("XDG_CONFIG_HOME"): + legendary_config_path = Path(config_path, "legendary") + else: + legendary_config_path = Path("~/.config/legendary").expanduser() + + installed_path = legendary_config_path / "installed.json" + if installed_path.exists(): + try: + with open(installed_path, encoding="utf-8") as installed_file: + installed_games = json.load(installed_file) + for game in installed_games.values(): + yield game["app_name"], Path(game["install_path"]) + except (json.JSONDecodeError, AttributeError, KeyError): + print( + "Unable to parse installed games from Legendary", + installed_path, + file=sys.stderr, + ) + + +def find_games() -> dict[str, Path]: + return dict(itertools.chain(find_epic_games(), find_legendary_games())) + + +if __name__ == "__main__": + games = find_games() + for k, v in games.items(): + print("Found game with id {} at {}.".format(k, v)) From 237065ef17ed494b7ae9a7854dd6a62ad9d780a7 Mon Sep 17 00:00:00 2001 From: Zash Date: Tue, 12 Apr 2022 23:24:47 +0200 Subject: [PATCH 2/2] Epic detection (ids) for: Subnautica, Kingdom Come: Deliverance + urls in README --- README.md | 3 ++- games/game_kingdomcomedeliverance.py | 1 + games/game_subnautica.py | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1e0f1e5..4713d39 100644 --- a/README.md +++ b/README.md @@ -55,13 +55,14 @@ You can rename `modorganizer-basic_games-xxx` to whatever you want (e.g., `basic | Divinity: Original Sin (Enhanced Edition) — [STEAM](https://store.steampowered.com/app/373420/Divinity_Original_Sin__Enhanced_Edition/) | [LostDragonist](https://github.com/LostDragonist/) | [game_divinityoriginalsinee.py](games/game_divinityoriginalsinee.py) | | | Dragon's Dogma: Dark Arisen — [GOG](https://www.gog.com/game/dragons_dogma_dark_arisen) / [STEAM](https://store.steampowered.com/app/367500/Dragons_Dogma_Dark_Arisen/) | [EzioTheDeadPoet](https://github.com/EzioTheDeadPoet) | [game_dragonsdogmadarkarisen.py](games/game_dragonsdogmadarkarisen.py) | | | Dungeon Siege II — [GOG](https://www.gog.com/game/dungeon_siege_collection) / [STEAM](https://store.steampowered.com/app/39200/Dungeon_Siege_II/) | [Holt59](https://github.com/holt59/) | [game_dungeonsiege2.py](games/game_dungeonsiege2.py) | | -| Kingdom Come: Deliverance — [GOG](https://www.gog.com/game/kingdom_come_deliverance) / [STEAM](https://store.steampowered.com/app/379430/Kingdom_Come_Deliverance/)| [Silencer711](https://github.com/Silencer711) | [game_kingdomcomedeliverance.py](games/game_kingdomcomedeliverance.py) | | +| Kingdom Come: Deliverance — [GOG](https://www.gog.com/game/kingdom_come_deliverance) / [STEAM](https://store.steampowered.com/app/379430/Kingdom_Come_Deliverance/) / [Epic](https://store.epicgames.com/p/kingdom-come-deliverance) | [Silencer711](https://github.com/Silencer711) | [game_kingdomcomedeliverance.py](games/game_kingdomcomedeliverance.py) | | | Mirror's Edge — [GOG](https://www.gog.com/game/mirrors_edge) / [STEAM](https://store.steampowered.com/app/17410/Mirrors_Edge)|[EzioTheDeadPoet](https://eziothedeadpoet.github.io/AboutMe/)|[game_mirrorsedge.py](games/game_mirrorsedge.py)| | | Mount & Blade II: Bannerlord — [GOG](https://www.gog.com/game/mount_blade_ii_bannerlord) / [STEAM](https://store.steampowered.com/app/261550/Mount__Blade_II_Bannerlord/) | [Holt59](https://github.com/holt59/) | [game_mountandblade2.py](games/game_mountandblade2.py) | | | No Man's Sky - [GOG](https://www.gog.com/game/no_mans_sky) / [Steam](https://store.steampowered.com/app/275850/No_Mans_Sky/)|[EzioTheDeadPoet](https://eziothedeadpoet.github.io/AboutMe/)|[game_nomanssky.py](games/game_nomanssky.py)| | | S.T.A.L.K.E.R. Anomaly — [MOD](https://www.stalker-anomaly.com/) | [Qudix](https://github.com/Qudix) | [game_stalkeranomaly.py](games/game_stalkeranomaly.py) | | | Stardew Valley — [GOG](https://www.gog.com/game/stardew_valley) / [STEAM](https://store.steampowered.com/app/413150/Stardew_Valley/) | [Syer10](https://github.com/Syer10), [Holt59](https://github.com/holt59/) | [game_stardewvalley.py](games/game_stardewvalley.py) | | | STAR WARS™ Empire at War: Gold Pack - [GOG](https://www.gog.com/game/star_wars_empire_at_war_gold_pack) / [STEAM](https://store.steampowered.com/app/32470/) | [erri120](https://github.com/erri120) | | | +| Subnautica — [STEAM](https://store.steampowered.com/app/264710/) / [Epic](https://store.epicgames.com/p/subnautica) | dekart811 | [game_subnautica.py](games/game_subnautica.py) | | | Valheim — [STEAM](https://store.steampowered.com/app/892970/Valheim/) | [Zash](https://github.com/ZashIn) | [game_valheim.py](games/game_valheim.py) | | The Witcher: Enhanced Edition - [GOG](https://www.gog.com/game/the_witcher) / [STEAM](https://store.steampowered.com/app/20900/The_Witcher_Enhanced_Edition_Directors_Cut/) | [erri120](https://github.com/erri120) | [game_witcher1.py](games/game_witcher1.py) | | | The Witcher 3: Wild Hunt — [GOG](https://www.gog.com/game/the_witcher_3_wild_hunt) / [STEAM](https://store.steampowered.com/app/292030/The_Witcher_3_Wild_Hunt/) | [Holt59](https://github.com/holt59/) | [game_witcher3.py](games/game_witcher3.py) | | diff --git a/games/game_kingdomcomedeliverance.py b/games/game_kingdomcomedeliverance.py index 6487b6b..d696302 100644 --- a/games/game_kingdomcomedeliverance.py +++ b/games/game_kingdomcomedeliverance.py @@ -18,6 +18,7 @@ class KingdomComeDeliveranceGame(BasicGame): GameNexusId = 2298 GameSteamId = [379430] GameGogId = [1719198803] + GameEpicId = "Eel" GameBinary = "bin/Win64/KingdomCome.exe" GameDataPath = "mods" GameSaveExtension = "whs" diff --git a/games/game_subnautica.py b/games/game_subnautica.py index 31bc574..b7ef65d 100644 --- a/games/game_subnautica.py +++ b/games/game_subnautica.py @@ -11,6 +11,7 @@ class SubnauticaGame(BasicGame): GameShortName = "subnautica" GameNexusName = "subnautica" GameSteamId = 264710 + GameEpicId = "Jaguar" GameBinary = "Subnautica.exe" GameDataPath = "QMods" GameDocumentsDirectory = "%GAME_PATH%"