diff --git a/README.md b/README.md index d7995b0..666dd8b 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,8 @@ class Witcher3Game(BasicGame): | GameSaveExtension | Save file extension (Optional) `savegameExtension` | `str` | | GameSteamId | Steam ID of the game (Optional) | `steamAPPId` | `List[str]` or `str` or `int` | | 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` | You can use the following variables for `str`: @@ -162,8 +164,8 @@ You can use the following variables for `str`: The meta-plugin provides some useful extra feature: -1. **Automatic Steam and GOG game detection:** If you provide Steam or GOG IDs for the game (via - `GameSteamId` or `GameGogId`), the game will be listed in the list of available games when creating a new +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). 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`. @@ -173,3 +175,4 @@ 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) diff --git a/basic_game.py b/basic_game.py index a4fc442..375af49 100644 --- a/basic_game.py +++ b/basic_game.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- import shutil +import sys from pathlib import Path from typing import Callable, Dict, Generic, List, Optional, TypeVar, Union @@ -203,6 +204,7 @@ class BasicGameMappings: steamAPPId: BasicGameOptionsMapping[str] gogAPPId: BasicGameOptionsMapping[str] originManifestIds: BasicGameOptionsMapping[str] + originWatcherExecutables: BasicGameMapping[List[str]] @staticmethod def _default_documents_directory(game): @@ -301,7 +303,18 @@ class BasicGameMappings: game, "GameGogId", "gogAPPId", default=lambda g: "", apply_fn=ids_apply ) self.originManifestIds = BasicGameOptionsMapping( - game, "GameOriginManifestIds", "originManifestIds", default=lambda g: "", apply_fn=ids_apply + game, + "GameOriginManifestIds", + "originManifestIds", + default=lambda g: "", + apply_fn=ids_apply, + ) + self.originWatcherExecutables = BasicGameMapping( + game, + "GameOriginWatcherExecutables", + "originWatcherExecutables", + apply_fn=lambda s: [s] if isinstance(s, str) else s, + default=lambda g: [], ) @@ -363,6 +376,22 @@ class BasicGame(mobase.IPluginGame): def init(self, organizer: mobase.IOrganizer) -> bool: self._organizer = organizer + if self._mappings.originWatcherExecutables.get(): + from .origin_utils import OriginWatcher + + self.origin_watcher = OriginWatcher( + self._mappings.originWatcherExecutables.get() + ) + if not self._organizer.onAboutToRun( + lambda appName: self.origin_watcher.spawn_origin_watcher() + ): + print("Failed to register onAboutToRun callback!", file=sys.stderr) + return False + if not self._organizer.onFinishedRun( + lambda appName, result: self.origin_watcher.stop_origin_watcher() + ): + print("Failed to register onFinishedRun callback!", file=sys.stderr) + return False return True def name(self) -> str: diff --git a/games/game_masseffectlegendary.py b/games/game_masseffectlegendary.py index ff28e07..4b9ef71 100644 --- a/games/game_masseffectlegendary.py +++ b/games/game_masseffectlegendary.py @@ -1,11 +1,5 @@ from ..basic_game import BasicGame -import mobase -import sys -import psutil -import threading -import time - class MassEffectLegendaryGame(BasicGame): @@ -23,59 +17,9 @@ class MassEffectLegendaryGame(BasicGame): ) GameSaveExtension = "pcsav" GameSteamId = 1328670 - - def init(self, organizer: mobase.IOrganizer) -> bool: - if not super().init(organizer): - return False - if not self._organizer.onAboutToRun(lambda appName: self._spawnOriginWatcher()): - print("Failed to register onAboutToRun callback!", file=sys.stderr) - return False - if not self._organizer.onFinishedRun( - lambda appName, result: self._stopOriginWatcher() - ): - print("Failed to register onFinishedRun callback!", file=sys.stderr) - return False - return True - - def _spawnOriginWatcher(self) -> bool: - _killOrigin() - self.worker = threading.Thread(target=_workerFunc) - self.worker.start() - return True - - def _stopOriginWatcher(self) -> None: - self.worker.join(10.0) - - -def _killOrigin(): - # Kill Origin if it's alive - for proc in psutil.process_iter(): - if proc.name().lower() == "origin.exe": - proc.kill() - - -def _workerFunc(): - gameAliveCount = 300 # Large number to allow Origin and the game to launch - keepGoing = True - while keepGoing: - gameAlive = False - # See if the game is alive - for proc in psutil.process_iter(): - if proc.name().lower() in [ - "masseffectlauncher.exe", - "masseffect1.exe", - "masseffect2.exe", - "masseffect3.exe", - ]: - gameAlive = True - break - if gameAlive: - # Game is alive, sleep and keep monitoring - time.sleep(1) - gameAliveCount = 10 - else: - gameAliveCount -= 1 - if gameAliveCount <= 0: - _killOrigin() - # Exit the thread - keepGoing = False + GameOriginWatcherExecutables = ( + "masseffectlauncher.exe", + "masseffect1.exe", + "masseffect2.exe", + "masseffect3.exe", + ) diff --git a/origin_utils.py b/origin_utils.py index 3f7a306..3e846ec 100644 --- a/origin_utils.py +++ b/origin_utils.py @@ -3,12 +3,64 @@ # Heavily influenced by https://github.com/erri120/GameFinder import os +import psutil +import threading +import time +import sys from pathlib import Path -from typing import Dict +from typing import Dict, List from urllib import parse -from PyQt5.QtCore import QDir, QFileInfo, QStandardPaths + +class OriginWatcher: + """ + This is a class to control killing Origin when needed. This is used in + order to hook and unhook Origin to get around the Origin DRM. Support + for launching Origin is not included as it's intended for the game's + DRM to launch Origin as needed. + """ + + def __init__(self, executables: List[str] = []): + self.executables = list(map(lambda s: s.lower(), executables)) + + def spawn_origin_watcher(self) -> bool: + self.kill_origin() + self.worker_alive = True + self.worker = threading.Thread(target=self._workerFunc) + self.worker.start() + return True + + def stop_origin_watcher(self) -> None: + self.worker_alive = False + self.worker.join(10.0) + + def kill_origin(self) -> None: + """ + Kills the Origin application + """ + for proc in psutil.process_iter(): + if proc.name().lower() == "origin.exe": + proc.kill() + + def _workerFunc(self) -> None: + gameAliveCount = 300 # Large number to allow Origin and the game to launch + while self.worker_alive: + gameAlive = False + # See if the game is still alive + for proc in psutil.process_iter(): + if proc.name().lower() in self.executables: + gameAlive = True + break + if gameAlive: + # Game is alive, sleep and keep monitoring at faster pace + gameAliveCount = 5 + else: + gameAliveCount -= 1 + if gameAliveCount <= 0: + self.kill_origin() + self.worker_alive = False + time.sleep(1) def find_games() -> Dict[str, Path]: @@ -20,32 +72,34 @@ def find_games() -> Dict[str, Path]: Origin games. """ games: Dict[str, Path] = {} - - local_content_path = Path(os.path.expandvars("%PROGRAMDATA%")).joinpath("Origin", "LocalContent") + + program_data_path = os.path.expandvars("%PROGRAMDATA%") + local_content_path = Path(program_data_path).joinpath("Origin", "LocalContent") for manifest in local_content_path.glob("**/*.mfst"): # Skip any manifest file with '@steam' - if '@steam' in manifest.name.lower(): + if "@steam" in manifest.name.lower(): continue # Read the file and look for &id= and &dipinstallpath= - with open(manifest, 'r') as f: + with open(manifest, "r") as f: manifest_query = f.read() url = parse.urlparse(manifest_query) query = parse.parse_qs(url.query) - if 'id' not in query: + if "id" not in query: # If id is not present, we have no clue what to do. continue - if 'dipinstallpath' not in query: + if "dipinstallpath" not in query: # We could query the Origin server for the install location but... no? continue - - for id_ in query['id']: - for path_ in query['dipinstallpath']: + + for id_ in query["id"]: + for path_ in query["dipinstallpath"]: games[id_] = Path(path_) - + return games + if __name__ == "__main__": games = find_games() for k, v in games.items(): - print("Found game with id {} at {}.".format(k, v)) \ No newline at end of file + print("Found game with id {} at {}.".format(k, v)) diff --git a/setup.cfg b/setup.cfg index ff68e6a..7b31129 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,4 +46,4 @@ deps = commands = black --check --diff . flake8 . --exclude "lib,.tox" - mypy . + mypy . --exclude "lib"