Add generic Origin DRM bypass

This commit is contained in:
Chris Bessent
2021-11-24 20:55:11 -07:00
parent f48495fd72
commit 88125ddac4
5 changed files with 109 additions and 79 deletions
+5 -2
View File
@@ -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)
+30 -1
View File
@@ -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:
+6 -62
View File
@@ -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",
)
+67 -13
View File
@@ -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))
print("Found game with id {} at {}.".format(k, v))
+1 -1
View File
@@ -46,4 +46,4 @@ deps =
commands =
black --check --diff .
flake8 . --exclude "lib,.tox"
mypy .
mypy . --exclude "lib"