RareGame: Introduce RareGame
from refactor_backend
branch
Shared: Move WineResolver to `rare/shared/workers` PathSpec: Move to `rare/models` Signed-off-by: loathingKernel <142770+loathingKernel@users.noreply.github.com>
This commit is contained in:
parent
db1691b694
commit
652968b6bf
|
@ -8,9 +8,10 @@ from legendary.models.game import Game, InstalledGame
|
|||
|
||||
from rare.components.tabs.settings import DefaultGameSettings
|
||||
from rare.components.tabs.settings.widgets.pre_launch import PreLaunchSettings
|
||||
from rare.shared.workers.wine_resolver import WineResolver
|
||||
from rare.utils import config_helper
|
||||
from rare.utils.extra_widgets import PathEdit
|
||||
from rare.utils.misc import icon, WineResolver, get_raw_save_path
|
||||
from rare.utils.misc import icon, get_raw_save_path
|
||||
|
||||
logger = getLogger("GameSettings")
|
||||
|
||||
|
|
|
@ -7,14 +7,14 @@ from PyQt5.QtCore import Qt, QThreadPool, QRunnable, pyqtSlot, pyqtSignal
|
|||
from PyQt5.QtWidgets import QGroupBox, QListWidgetItem, QFileDialog, QMessageBox, QFrame
|
||||
|
||||
from rare.lgndr.glue.exception import LgndrException
|
||||
from rare.models.pathspec import PathSpec
|
||||
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
|
||||
from rare.shared.workers.wine_resolver import WineResolver
|
||||
from rare.ui.components.tabs.games.integrations.egl_sync_group import Ui_EGLSyncGroup
|
||||
from rare.ui.components.tabs.games.integrations.egl_sync_list_group import (
|
||||
Ui_EGLSyncListGroup,
|
||||
)
|
||||
from rare.utils.extra_widgets import PathEdit
|
||||
from rare.utils.misc import WineResolver
|
||||
from rare.utils.models import PathSpec
|
||||
|
||||
logger = getLogger("EGLSync")
|
||||
|
||||
|
|
361
rare/models/game.py
Normal file
361
rare/models/game.py
Normal file
|
@ -0,0 +1,361 @@
|
|||
import os
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from enum import IntEnum
|
||||
from logging import getLogger
|
||||
from typing import List, Optional
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSignal, QRunnable, QThreadPool, QMetaObject
|
||||
from PyQt5.QtGui import QPixmap
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
from legendary.models.game import Game, InstalledGame, SaveGameFile
|
||||
|
||||
from rare.models.install import InstallOptionsModel
|
||||
from rare.lgndr.core import LegendaryCore
|
||||
from rare.shared.image_manager import ImageManager
|
||||
# from rare.utils.legendary_utils import VerificationWorker
|
||||
|
||||
logger = getLogger("RareGame")
|
||||
|
||||
|
||||
class RareGameState(IntEnum):
|
||||
IDLE = 0,
|
||||
RUNNING = 1,
|
||||
|
||||
|
||||
class RareGame(QObject):
|
||||
|
||||
@dataclass
|
||||
class Metadata:
|
||||
queued: bool = False
|
||||
queue_pos: Optional[int] = None
|
||||
last_played: Optional[datetime] = None
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, data):
|
||||
return cls(
|
||||
queued=data.get("queued", False),
|
||||
queue_pos=data.get("queue_pos", None),
|
||||
last_played=datetime.strptime(data.get("last_played", "None"), "%Y-%m-%dT%H:%M:%S.%f"),
|
||||
)
|
||||
|
||||
def __dict__(self):
|
||||
return dict(
|
||||
queued=self.queued,
|
||||
queue_pos=self.queue_pos,
|
||||
last_played=self.last_played.strftime("%Y-%m-%dT%H:%M:%S.%f"),
|
||||
)
|
||||
|
||||
def __bool__(self):
|
||||
return self.queued or self.queue_pos is not None or self.last_played is not None
|
||||
|
||||
class Signals:
|
||||
class Progress(QObject):
|
||||
start = pyqtSignal()
|
||||
update = pyqtSignal(int)
|
||||
finish = pyqtSignal(bool)
|
||||
|
||||
class Widget(QObject):
|
||||
update = pyqtSignal()
|
||||
|
||||
class Game(QObject):
|
||||
install = pyqtSignal(InstallOptionsModel)
|
||||
uninstalled = pyqtSignal()
|
||||
|
||||
def __init__(self):
|
||||
super(RareGame.Signals, self).__init__()
|
||||
self.progress = RareGame.Signals.Progress()
|
||||
self.widget = RareGame.Signals.Widget()
|
||||
self.game = RareGame.Signals.Game()
|
||||
|
||||
progress: int = 0
|
||||
active_thread: Optional[QRunnable] = None
|
||||
|
||||
def __init__(self, legendary_core: LegendaryCore, image_manager: ImageManager, game: Game):
|
||||
super(RareGame, self).__init__()
|
||||
self.signals = RareGame.Signals()
|
||||
|
||||
self.core = legendary_core
|
||||
self.image_manager = image_manager
|
||||
|
||||
# Update names for Unreal Engine
|
||||
if game.app_title == "Unreal Engine":
|
||||
game.app_title += f" {game.app_name.split('_')[-1]}"
|
||||
self.game: Game = game
|
||||
|
||||
# None if origin or not installed
|
||||
self.igame: Optional[InstalledGame] = self.core.get_installed_game(game.app_name)
|
||||
|
||||
self.pixmap: QPixmap = QPixmap()
|
||||
self.metadata: RareGame.Metadata = RareGame.Metadata()
|
||||
|
||||
self.owned_dlcs: List[RareGame] = []
|
||||
self.saves: List[SaveGameFile] = []
|
||||
|
||||
if self.has_update:
|
||||
logger.info(f"Update available for game: {self.app_name} ({self.app_title})")
|
||||
|
||||
self.threadpool = QThreadPool.globalInstance()
|
||||
|
||||
self.game_running = False
|
||||
|
||||
@property
|
||||
def app_name(self) -> str:
|
||||
return self.igame.app_name if self.igame is not None else self.game.app_name
|
||||
|
||||
@property
|
||||
def app_title(self) -> str:
|
||||
return self.igame.title if self.igame is not None else self.game.app_title
|
||||
|
||||
@property
|
||||
def title(self) -> str:
|
||||
return self.app_title
|
||||
|
||||
|
||||
@property
|
||||
def developer(self) -> str:
|
||||
"""!
|
||||
@brief Property to report the developer of a Game
|
||||
|
||||
@return str
|
||||
"""
|
||||
return self.game.metadata["developer"]
|
||||
|
||||
@property
|
||||
def install_size(self) -> int:
|
||||
"""!
|
||||
@brief Property to report the installation size of an InstalledGame
|
||||
|
||||
@return int The size of the installation
|
||||
"""
|
||||
return self.igame.install_size if self.igame is not None else 0
|
||||
|
||||
@property
|
||||
def version(self) -> str:
|
||||
"""!
|
||||
@brief Reports the currently installed version of the Game
|
||||
|
||||
If InstalledGame reports the currently installed version, which might be
|
||||
different from the remote version available from EGS. For not installed Games
|
||||
it reports the already known version.
|
||||
|
||||
@return str The current version of the game
|
||||
"""
|
||||
return self.igame.version if self.igame is not None else self.game.app_version()
|
||||
|
||||
@property
|
||||
def remote_version(self) -> str:
|
||||
"""!
|
||||
@brief Property to report the remote version of an InstalledGame
|
||||
|
||||
If the Game is installed, requests the latest version string from EGS,
|
||||
otherwise it reports the already known version of the Game for Windows.
|
||||
|
||||
@return str The current version from EGS
|
||||
"""
|
||||
if self.igame is not None:
|
||||
return self.game.app_version(self.igame.platform)
|
||||
else:
|
||||
return self.game.app_version()
|
||||
|
||||
@property
|
||||
def has_update(self) -> bool:
|
||||
"""!
|
||||
@brief Property to report if an InstalledGame has updates available
|
||||
|
||||
Games have to be installed and have assets available to have
|
||||
updates
|
||||
|
||||
@return bool If there is an update available
|
||||
"""
|
||||
if self.igame is not None and self.core.lgd.assets is not None:
|
||||
try:
|
||||
if self.remote_version != self.igame.version:
|
||||
return True
|
||||
except ValueError:
|
||||
logger.error(f"Asset error for {self.game.app_title}")
|
||||
return False
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_installed(self) -> bool:
|
||||
"""!
|
||||
@brief Property to report if a game is installed
|
||||
|
||||
This returns True if InstalledGame data have been loaded for the game
|
||||
or if the game is a game without assets, for example an Origin game.
|
||||
|
||||
@return bool If the game should be considered installed
|
||||
"""
|
||||
return (self.igame is not None) or self.is_non_asset
|
||||
|
||||
def set_installed(self, installed: bool) -> None:
|
||||
"""!
|
||||
@brief Sets the installation status of a game
|
||||
|
||||
If this is set to True the InstalledGame data is fetched
|
||||
for the game, if set to False the igame attribute is cleared.
|
||||
|
||||
@param installed The installation status of the game
|
||||
@return None
|
||||
"""
|
||||
if installed:
|
||||
self.igame = self.core.get_installed_game(self.app_name)
|
||||
else:
|
||||
self.igame = None
|
||||
self.set_pixmap()
|
||||
|
||||
@property
|
||||
def can_run_offline(self) -> bool:
|
||||
"""!
|
||||
@brief Property to report if a game can run offline
|
||||
|
||||
Checks if the game can run without connectin the internet.
|
||||
It's a simple wrapper around legendary provided information,
|
||||
with handling of not installed games.
|
||||
|
||||
@return bool If the games can run without network
|
||||
"""
|
||||
return self.igame.can_run_offline if self.igame is not None else False
|
||||
|
||||
@property
|
||||
def is_foreign(self) -> bool:
|
||||
"""!
|
||||
@brief Property to report if a game doesn't belong to the current account
|
||||
|
||||
Checks if a game belongs to the currently logged in account. Games that require
|
||||
a network connection or remote authentication will fail to run from another account
|
||||
despite being installed. On the other hand, games that do not require network,
|
||||
can be executed, facilitating a rudimentary game sharing option on the same computer.
|
||||
|
||||
@return bool If the game belongs to another count or not
|
||||
"""
|
||||
ret = True
|
||||
try:
|
||||
if self.igame is not None:
|
||||
_ = self.core.get_asset(self.game.app_name, platform=self.igame.platform).build_version
|
||||
ret = False
|
||||
except ValueError:
|
||||
logger.warning(f"Game {self.game.app_title} has no metadata. Set offline true")
|
||||
except AttributeError:
|
||||
ret = False
|
||||
return ret
|
||||
|
||||
@property
|
||||
def needs_verification(self) -> bool:
|
||||
"""!
|
||||
@brief Property to report if a games requires to be verified
|
||||
|
||||
Simple wrapper around legendary's attribute with installation
|
||||
status check
|
||||
|
||||
@return bool If the games needs to be verified
|
||||
"""
|
||||
if self.igame is not None:
|
||||
return self.igame.needs_verification
|
||||
else:
|
||||
return False
|
||||
|
||||
@needs_verification.setter
|
||||
def needs_verification(self, not_update: bool) -> None:
|
||||
"""!
|
||||
@brief Sets the verification status of a game.
|
||||
|
||||
The operation here is reversed. since the property is
|
||||
named like this. After the verification, set this to 'False'
|
||||
to update the InstalledGame in the widget.
|
||||
|
||||
@param not_update If the game requires verification
|
||||
@return None
|
||||
"""
|
||||
if not not_update:
|
||||
self.igame = self.core.get_installed_game(self.game.app_name)
|
||||
|
||||
@property
|
||||
def is_dlc(self) -> bool:
|
||||
"""!
|
||||
@brief Property to report if Game is a dlc
|
||||
|
||||
@return bool
|
||||
"""
|
||||
return self.game.is_dlc
|
||||
|
||||
@property
|
||||
def is_mac(self) -> bool:
|
||||
"""!
|
||||
@brief Property to report if Game has a mac version
|
||||
|
||||
@return bool
|
||||
"""
|
||||
return "Mac" in self.game.asset_infos.keys()
|
||||
|
||||
@property
|
||||
def is_win32(self) -> bool:
|
||||
"""!
|
||||
@brief Property to report if Game is 32bit game
|
||||
|
||||
@return bool
|
||||
"""
|
||||
return "Win32" in self.game.asset_infos.keys()
|
||||
|
||||
@property
|
||||
def is_unreal(self) -> bool:
|
||||
"""!
|
||||
@brief Property to report if a Game is an Unreal Engine bundle
|
||||
|
||||
@return bool
|
||||
"""
|
||||
if not self.is_non_asset:
|
||||
return self.game.asset_infos["Windows"].namespace == "ue"
|
||||
else:
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_non_asset(self) -> bool:
|
||||
"""!
|
||||
@brief Property to report if a Game doesn't have assets
|
||||
|
||||
Typically, games have assets, however some games that require
|
||||
other launchers do not have them. Rare treats these games as installed
|
||||
offering to execute their launcher.
|
||||
|
||||
@return bool If the game doesn't have assets
|
||||
"""
|
||||
return not self.game.asset_infos
|
||||
|
||||
@property
|
||||
def is_origin(self) -> bool:
|
||||
return self.game.metadata.get("customAttributes", {}).get("ThirdPartyManagedApp", {}).get("value") == "Origin"
|
||||
|
||||
@property
|
||||
def can_launch(self) -> bool:
|
||||
if self.is_installed:
|
||||
if self.is_non_asset:
|
||||
return True
|
||||
elif self.game_running or self.needs_verification:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def set_pixmap(self):
|
||||
self.pixmap = self.image_manager.get_pixmap(self.app_name, self.is_installed)
|
||||
if self.pixmap.isNull():
|
||||
self.image_manager.download_image(self.game, self.set_pixmap, 0, False)
|
||||
else:
|
||||
self.signals.widget.update.emit()
|
||||
|
||||
def refresh_pixmap(self):
|
||||
self.image_manager.download_image(self.game, self.set_pixmap, 0, True)
|
||||
|
||||
def start_progress(self):
|
||||
self.signals.progress.start.emit()
|
||||
|
||||
def update_progress(self, progress: int):
|
||||
self.progress = progress
|
||||
self.signals.progress.update.emit(progress)
|
||||
|
||||
def finish_progress(self, fail: bool, miss: int, app: str):
|
||||
self.set_installed(True)
|
||||
self.signals.progress.finish.emit(fail)
|
0
rare/shared/workers/__init__.py
Normal file
0
rare/shared/workers/__init__.py
Normal file
69
rare/shared/workers/wine_resolver.py
Normal file
69
rare/shared/workers/wine_resolver.py
Normal file
|
@ -0,0 +1,69 @@
|
|||
import os
|
||||
import subprocess
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, QRunnable, QObject, pyqtSlot
|
||||
|
||||
from rare.models.pathspec import PathSpec
|
||||
|
||||
|
||||
class WineResolver(QRunnable):
|
||||
class Signals(QObject):
|
||||
result_ready = pyqtSignal(str)
|
||||
|
||||
def __init__(self, rare_core, path: str, app_name: str):
|
||||
super(WineResolver, self).__init__()
|
||||
self.signals = WineResolver.Signals()
|
||||
self.setAutoDelete(True)
|
||||
self.wine_env = os.environ.copy()
|
||||
core = rare_core.core()
|
||||
self.wine_env.update(core.get_app_environment(app_name))
|
||||
self.wine_env["WINEDLLOVERRIDES"] = "winemenubuilder=d;mscoree=d;mshtml=d;"
|
||||
self.wine_env["DISPLAY"] = ""
|
||||
|
||||
self.wine_binary = core.lgd.config.get(
|
||||
app_name,
|
||||
"wine_executable",
|
||||
fallback=core.lgd.config.get("default", "wine_executable", fallback="wine"),
|
||||
)
|
||||
self.winepath_binary = os.path.join(os.path.dirname(self.wine_binary), "winepath")
|
||||
self.path = PathSpec(core, app_name).cook(path)
|
||||
|
||||
@pyqtSlot()
|
||||
def run(self):
|
||||
if "WINEPREFIX" not in self.wine_env or not os.path.exists(self.wine_env["WINEPREFIX"]):
|
||||
# pylint: disable=E1136
|
||||
self.signals.result_ready[str].emit("")
|
||||
return
|
||||
if not os.path.exists(self.wine_binary) or not os.path.exists(self.winepath_binary):
|
||||
# pylint: disable=E1136
|
||||
self.signals.result_ready[str].emit("")
|
||||
return
|
||||
path = self.path.strip().replace("/", "\\")
|
||||
# lk: if path does not exist form
|
||||
cmd = [self.wine_binary, "cmd", "/c", "echo", path]
|
||||
# lk: if path exists and needs a case-sensitive interpretation form
|
||||
# cmd = [self.wine_binary, 'cmd', '/c', f'cd {path} & cd']
|
||||
proc = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
env=self.wine_env,
|
||||
shell=False,
|
||||
text=True,
|
||||
)
|
||||
out, err = proc.communicate()
|
||||
# Clean wine output
|
||||
out = out.strip().strip('"')
|
||||
proc = subprocess.Popen(
|
||||
[self.winepath_binary, "-u", out],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
env=self.wine_env,
|
||||
shell=False,
|
||||
text=True,
|
||||
)
|
||||
out, err = proc.communicate()
|
||||
real_path = os.path.realpath(out.strip())
|
||||
# pylint: disable=E1136
|
||||
self.signals.result_ready[str].emit(real_path)
|
||||
return
|
|
@ -1,7 +1,6 @@
|
|||
import os
|
||||
import platform
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
from logging import getLogger
|
||||
from typing import List, Union
|
||||
|
@ -10,7 +9,6 @@ import qtawesome
|
|||
import requests
|
||||
from PyQt5.QtCore import (
|
||||
pyqtSignal,
|
||||
pyqtSlot,
|
||||
QObject,
|
||||
QRunnable,
|
||||
QSettings,
|
||||
|
@ -23,8 +21,6 @@ from PyQt5.QtWidgets import qApp, QStyleFactory
|
|||
from legendary.models.game import Game
|
||||
from requests.exceptions import HTTPError
|
||||
|
||||
from .models import PathSpec
|
||||
|
||||
# Windows
|
||||
|
||||
if platform.system() == "Windows":
|
||||
|
@ -310,76 +306,6 @@ def create_desktop_link(app_name=None, core: LegendaryCore = None, type_of_link=
|
|||
return False
|
||||
|
||||
|
||||
class WineResolverSignals(QObject):
|
||||
result_ready = pyqtSignal(str)
|
||||
|
||||
|
||||
class WineResolver(QRunnable):
|
||||
def __init__(self, path: str, app_name: str):
|
||||
super(WineResolver, self).__init__()
|
||||
self.signals = WineResolverSignals()
|
||||
self.setAutoDelete(True)
|
||||
self.wine_env = os.environ.copy()
|
||||
core = LegendaryCoreSingleton()
|
||||
self.wine_env.update(core.get_app_environment(app_name))
|
||||
self.wine_env["WINEDLLOVERRIDES"] = "winemenubuilder=d;mscoree=d;mshtml=d;"
|
||||
self.wine_env["DISPLAY"] = ""
|
||||
|
||||
self.wine_binary = core.lgd.config.get(
|
||||
app_name,
|
||||
"wine_executable",
|
||||
fallback=core.lgd.config.get("default", "wine_executable", fallback="wine"),
|
||||
)
|
||||
self.winepath_binary = os.path.join(
|
||||
os.path.dirname(self.wine_binary), "winepath"
|
||||
)
|
||||
self.path = PathSpec(core, app_name).cook(path)
|
||||
|
||||
@pyqtSlot()
|
||||
def run(self):
|
||||
if "WINEPREFIX" not in self.wine_env or not os.path.exists(
|
||||
self.wine_env["WINEPREFIX"]
|
||||
):
|
||||
# pylint: disable=E1136
|
||||
self.signals.result_ready[str].emit(str())
|
||||
return
|
||||
if not os.path.exists(self.wine_binary) or not os.path.exists(
|
||||
self.winepath_binary
|
||||
):
|
||||
# pylint: disable=E1136
|
||||
self.signals.result_ready[str].emit(str())
|
||||
return
|
||||
path = self.path.strip().replace("/", "\\")
|
||||
# lk: if path does not exist form
|
||||
cmd = [self.wine_binary, "cmd", "/c", "echo", path]
|
||||
# lk: if path exists and needs a case sensitive interpretation form
|
||||
# cmd = [self.wine_binary, 'cmd', '/c', f'cd {path} & cd']
|
||||
proc = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
env=self.wine_env,
|
||||
shell=False,
|
||||
text=True,
|
||||
)
|
||||
out, err = proc.communicate()
|
||||
# Clean wine output
|
||||
out = out.strip().strip('"')
|
||||
proc = subprocess.Popen(
|
||||
[self.winepath_binary, "-u", out],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
env=self.wine_env,
|
||||
shell=False,
|
||||
text=True,
|
||||
)
|
||||
out, err = proc.communicate()
|
||||
real_path = os.path.realpath(out.strip())
|
||||
# pylint: disable=E1136
|
||||
self.signals.result_ready[str].emit(real_path)
|
||||
return
|
||||
|
||||
|
||||
class CloudSignals(QObject):
|
||||
result_ready = pyqtSignal(list) # List[SaveGameFile]
|
||||
|
||||
|
|
Loading…
Reference in a new issue