diff --git a/README.md b/README.md
index 5e1abd2..704d8b8 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) |
- save game preview
- mod data checker
|
| 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) | - profile specific cfg files
|
+| 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) | - profile specific cfg files
|
| 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) | - Empire at War: [game_starwars-empire-at-war.py](games/game_starwars-empire-at-war.py)
- Force of Corruption: [game_starwars-empire-at-war-foc.py](games/game_starwars-empire-at-war-foc.py)
| |
+| 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) | - mod data checker
- overwrite config sync
- save game support (no preview)
| 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) | - save game parsing (no preview)
|
| 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) | |
@@ -155,6 +156,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 +168,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 +179,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 0d85ebb..9d091e0 100644
--- a/basic_game.py
+++ b/basic_game.py
@@ -209,6 +209,7 @@ class BasicGameMappings:
gogAPPId: BasicGameOptionsMapping[str]
originManifestIds: BasicGameOptionsMapping[str]
originWatcherExecutables: BasicGameMapping[List[str]]
+ epicAPPId: BasicGameOptionsMapping[str]
@staticmethod
def _default_documents_directory(game):
@@ -324,6 +325,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):
@@ -332,13 +336,15 @@ 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 .epic_utils import find_games as find_epic_games
from .gog_utils import find_games as find_gog_games
from .origin_utils import find_games as find_origin_games
from .steam_utils import find_games as find_steam_games
@@ -346,6 +352,7 @@ class BasicGame(mobase.IPluginGame):
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
@@ -380,6 +387,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:
@@ -442,6 +452,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()
@@ -471,6 +486,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()
@@ -581,6 +599,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))
diff --git a/games/game_kingdomcomedeliverance.py b/games/game_kingdomcomedeliverance.py
index fbfd594..bb54647 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%"