mirror of
https://github.com/ModOrganizer2/modorganizer-basic_games.git
synced 2026-07-27 14:07:29 -07:00
Add generic Origin DRM bypass
This commit is contained in:
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user