2022-09-05 05:38:24 +12:00
|
|
|
import configparser
|
|
|
|
import os
|
|
|
|
from argparse import Namespace
|
2023-01-05 09:38:05 +13:00
|
|
|
from itertools import chain
|
2022-09-05 05:38:24 +12:00
|
|
|
from logging import getLogger
|
2023-02-01 02:43:26 +13:00
|
|
|
from typing import Optional, Dict, Iterator, Callable, List
|
2022-09-05 05:38:24 +12:00
|
|
|
|
2023-02-01 02:43:26 +13:00
|
|
|
from PyQt5.QtCore import QObject, QThreadPool
|
2023-01-21 13:15:06 +13:00
|
|
|
from legendary.lfs.eos import EOSOverlayApp
|
2023-01-05 09:38:05 +13:00
|
|
|
from legendary.models.game import Game, SaveGameFile
|
2022-09-05 05:38:24 +12:00
|
|
|
|
|
|
|
from rare.lgndr.core import LegendaryCore
|
|
|
|
from rare.models.apiresults import ApiResults
|
2023-02-09 12:37:53 +13:00
|
|
|
from rare.models.game import RareGameBase, RareGame, RareEosOverlay
|
2022-09-05 05:38:24 +12:00
|
|
|
from rare.models.signals import GlobalSignals
|
2023-02-01 02:43:26 +13:00
|
|
|
from .workers import QueueWorker, VerifyWorker, MoveWorker
|
2022-09-05 05:38:24 +12:00
|
|
|
from .image_manager import ImageManager
|
2023-02-16 21:50:53 +13:00
|
|
|
from .workers.worker import QueueWorkerInfo, QueueWorkerState
|
2022-09-05 05:38:24 +12:00
|
|
|
|
|
|
|
logger = getLogger("RareCore")
|
|
|
|
|
|
|
|
|
|
|
|
class RareCore(QObject):
|
2023-02-06 06:12:50 +13:00
|
|
|
__instance: Optional['RareCore'] = None
|
2022-09-05 05:38:24 +12:00
|
|
|
|
|
|
|
def __init__(self, args: Namespace):
|
2023-02-06 06:12:50 +13:00
|
|
|
if self.__instance is not None:
|
2022-09-05 05:38:24 +12:00
|
|
|
raise RuntimeError("RareCore already initialized")
|
|
|
|
super(RareCore, self).__init__()
|
2023-01-21 13:15:06 +13:00
|
|
|
self.__args: Optional[Namespace] = None
|
|
|
|
self.__signals: Optional[GlobalSignals] = None
|
|
|
|
self.__core: Optional[LegendaryCore] = None
|
|
|
|
self.__image_manager: Optional[ImageManager] = None
|
|
|
|
self.__api_results: Optional[ApiResults] = None
|
2022-09-05 05:38:24 +12:00
|
|
|
|
|
|
|
self.args(args)
|
|
|
|
self.signals(init=True)
|
|
|
|
self.core(init=True)
|
|
|
|
self.image_manager(init=True)
|
|
|
|
|
2023-02-01 02:43:26 +13:00
|
|
|
self.queue_workers: List[QueueWorker] = []
|
|
|
|
self.queue_threadpool = QThreadPool()
|
|
|
|
self.queue_threadpool.setMaxThreadCount(2)
|
|
|
|
|
2023-01-05 09:38:05 +13:00
|
|
|
self.__games: Dict[str, RareGame] = {}
|
|
|
|
|
2023-02-09 12:37:53 +13:00
|
|
|
self.__eos_overlay_rgame = RareEosOverlay(self.__core, EOSOverlayApp)
|
2023-01-21 13:15:06 +13:00
|
|
|
|
2023-02-06 06:12:50 +13:00
|
|
|
RareCore.__instance = self
|
2022-09-05 05:38:24 +12:00
|
|
|
|
2023-02-01 02:43:26 +13:00
|
|
|
def enqueue_worker(self, rgame: RareGame, worker: QueueWorker):
|
|
|
|
if isinstance(worker, VerifyWorker):
|
|
|
|
rgame.state = RareGame.State.VERIFYING
|
|
|
|
if isinstance(worker, MoveWorker):
|
|
|
|
rgame.state = RareGame.State.MOVING
|
2023-02-03 21:55:56 +13:00
|
|
|
rgame.set_worker(worker)
|
|
|
|
worker.feedback.started.connect(self.__signals.application.update_statusbar)
|
|
|
|
worker.feedback.finished.connect(lambda: rgame.set_worker(None))
|
2023-02-01 02:43:26 +13:00
|
|
|
worker.feedback.finished.connect(lambda: self.queue_workers.remove(worker))
|
2023-02-03 21:55:56 +13:00
|
|
|
worker.feedback.finished.connect(self.__signals.application.update_statusbar)
|
2023-02-01 02:43:26 +13:00
|
|
|
self.queue_workers.append(worker)
|
2023-02-03 21:55:56 +13:00
|
|
|
self.queue_threadpool.start(worker, priority=0)
|
|
|
|
self.__signals.application.update_statusbar.emit()
|
2023-02-01 02:43:26 +13:00
|
|
|
|
2023-02-17 04:07:02 +13:00
|
|
|
def dequeue_worker(self, worker: QueueWorker):
|
|
|
|
rgame = self.__games[worker.worker_info().app_name]
|
|
|
|
rgame.set_worker(None)
|
|
|
|
self.queue_workers.remove(worker)
|
|
|
|
self.__signals.application.update_statusbar.emit()
|
|
|
|
|
2023-02-16 21:50:53 +13:00
|
|
|
def active_workers(self) -> Iterator[QueueWorker]:
|
2023-02-17 03:42:32 +13:00
|
|
|
return list(filter(lambda w: w.state == QueueWorkerState.ACTIVE, self.queue_workers))
|
2023-02-16 21:50:53 +13:00
|
|
|
|
|
|
|
def queued_workers(self) -> Iterator[QueueWorker]:
|
2023-02-17 03:42:32 +13:00
|
|
|
return list(filter(lambda w: w.state == QueueWorkerState.QUEUED, self.queue_workers))
|
2023-02-16 21:50:53 +13:00
|
|
|
|
2023-02-03 21:55:56 +13:00
|
|
|
def queue_info(self) -> List[QueueWorkerInfo]:
|
2023-02-01 02:43:26 +13:00
|
|
|
return [w.worker_info() for w in self.queue_workers]
|
|
|
|
|
2022-09-05 05:38:24 +12:00
|
|
|
@staticmethod
|
|
|
|
def instance() -> 'RareCore':
|
2023-02-06 06:12:50 +13:00
|
|
|
if RareCore.__instance is None:
|
2022-09-05 05:38:24 +12:00
|
|
|
raise RuntimeError("Uninitialized use of RareCore")
|
2023-02-06 06:12:50 +13:00
|
|
|
return RareCore.__instance
|
2022-09-05 05:38:24 +12:00
|
|
|
|
|
|
|
def signals(self, init: bool = False) -> GlobalSignals:
|
2023-01-21 13:15:06 +13:00
|
|
|
if self.__signals is None and not init:
|
2022-09-05 05:38:24 +12:00
|
|
|
raise RuntimeError("Uninitialized use of GlobalSignalsSingleton")
|
2023-01-21 13:15:06 +13:00
|
|
|
if self.__signals is not None and init:
|
2022-09-05 05:38:24 +12:00
|
|
|
raise RuntimeError("GlobalSignals already initialized")
|
|
|
|
if init:
|
2023-01-21 13:15:06 +13:00
|
|
|
self.__signals = GlobalSignals()
|
|
|
|
return self.__signals
|
2022-09-05 05:38:24 +12:00
|
|
|
|
|
|
|
def args(self, args: Namespace = None) -> Optional[Namespace]:
|
2023-01-21 13:15:06 +13:00
|
|
|
if self.__args is None and args is None:
|
2022-09-05 05:38:24 +12:00
|
|
|
raise RuntimeError("Uninitialized use of ArgumentsSingleton")
|
2023-01-21 13:15:06 +13:00
|
|
|
if self.__args is not None and args is not None:
|
2022-09-05 05:38:24 +12:00
|
|
|
raise RuntimeError("Arguments already initialized")
|
|
|
|
if args is not None:
|
2023-01-21 13:15:06 +13:00
|
|
|
self.__args = args
|
|
|
|
return self.__args
|
2022-09-05 05:38:24 +12:00
|
|
|
|
|
|
|
def core(self, init: bool = False) -> LegendaryCore:
|
2023-01-21 13:15:06 +13:00
|
|
|
if self.__core is None and not init:
|
2022-09-05 05:38:24 +12:00
|
|
|
raise RuntimeError("Uninitialized use of LegendaryCoreSingleton")
|
2023-01-21 13:15:06 +13:00
|
|
|
if self.__core is not None and init:
|
2022-09-05 05:38:24 +12:00
|
|
|
raise RuntimeError("LegendaryCore already initialized")
|
|
|
|
if init:
|
|
|
|
try:
|
2023-01-21 13:15:06 +13:00
|
|
|
self.__core = LegendaryCore()
|
2022-09-05 05:38:24 +12:00
|
|
|
except configparser.MissingSectionHeaderError as e:
|
|
|
|
logger.warning(f"Config is corrupt: {e}")
|
|
|
|
if config_path := os.environ.get("XDG_CONFIG_HOME"):
|
|
|
|
path = os.path.join(config_path, "legendary")
|
|
|
|
else:
|
|
|
|
path = os.path.expanduser("~/.config/legendary")
|
|
|
|
with open(os.path.join(path, "config.ini"), "w") as config_file:
|
|
|
|
config_file.write("[Legendary]")
|
2023-01-21 13:15:06 +13:00
|
|
|
self.__core = LegendaryCore()
|
|
|
|
if "Legendary" not in self.__core.lgd.config.sections():
|
|
|
|
self.__core.lgd.config.add_section("Legendary")
|
|
|
|
self.__core.lgd.save_config()
|
2022-09-05 05:38:24 +12:00
|
|
|
# workaround if egl sync enabled, but no programdata_path
|
|
|
|
# programdata_path might be unset if logging in through the browser
|
2023-01-21 13:15:06 +13:00
|
|
|
if self.__core.egl_sync_enabled:
|
|
|
|
if self.__core.egl.programdata_path is None:
|
|
|
|
self.__core.lgd.config.remove_option("Legendary", "egl_sync")
|
|
|
|
self.__core.lgd.save_config()
|
2022-09-05 05:38:24 +12:00
|
|
|
else:
|
2023-01-21 13:15:06 +13:00
|
|
|
if not os.path.exists(self.__core.egl.programdata_path):
|
|
|
|
self.__core.lgd.config.remove_option("Legendary", "egl_sync")
|
|
|
|
self.__core.lgd.save_config()
|
|
|
|
return self.__core
|
2022-09-05 05:38:24 +12:00
|
|
|
|
|
|
|
def image_manager(self, init: bool = False) -> ImageManager:
|
2023-01-21 13:15:06 +13:00
|
|
|
if self.__image_manager is None and not init:
|
2022-09-05 05:38:24 +12:00
|
|
|
raise RuntimeError("Uninitialized use of ImageManagerSingleton")
|
2023-01-21 13:15:06 +13:00
|
|
|
if self.__image_manager is not None and init:
|
2022-09-05 05:38:24 +12:00
|
|
|
raise RuntimeError("ImageManager already initialized")
|
2023-01-21 13:15:06 +13:00
|
|
|
if self.__image_manager is None:
|
|
|
|
self.__image_manager = ImageManager(self.signals(), self.core())
|
|
|
|
return self.__image_manager
|
2022-09-05 05:38:24 +12:00
|
|
|
|
|
|
|
def api_results(self, res: ApiResults = None) -> Optional[ApiResults]:
|
2023-01-21 13:15:06 +13:00
|
|
|
if self.__api_results is None and res is None:
|
2022-09-05 05:38:24 +12:00
|
|
|
raise RuntimeError("Uninitialized use of ApiResultsSingleton")
|
2023-01-21 13:15:06 +13:00
|
|
|
if self.__api_results is not None and res is not None:
|
2022-09-05 05:38:24 +12:00
|
|
|
raise RuntimeError("ApiResults already initialized")
|
|
|
|
if res is not None:
|
2023-01-21 13:15:06 +13:00
|
|
|
self.__api_results = res
|
|
|
|
return self.__api_results
|
2022-09-05 05:38:24 +12:00
|
|
|
|
|
|
|
def deleteLater(self) -> None:
|
2023-01-21 13:15:06 +13:00
|
|
|
del self.__api_results
|
|
|
|
self.__api_results = None
|
2022-09-05 05:38:24 +12:00
|
|
|
|
2023-01-21 13:15:06 +13:00
|
|
|
self.__image_manager.deleteLater()
|
|
|
|
del self.__image_manager
|
|
|
|
self.__image_manager = None
|
2022-09-05 05:38:24 +12:00
|
|
|
|
2023-01-21 13:15:06 +13:00
|
|
|
self.__core.exit()
|
|
|
|
del self.__core
|
|
|
|
self.__core = None
|
2022-09-05 05:38:24 +12:00
|
|
|
|
2023-01-21 13:15:06 +13:00
|
|
|
self.__signals.deleteLater()
|
|
|
|
del self.__signals
|
|
|
|
self.__signals = None
|
2022-09-05 05:38:24 +12:00
|
|
|
|
2023-01-21 13:15:06 +13:00
|
|
|
del self.__args
|
|
|
|
self.__args = None
|
2022-09-05 05:38:24 +12:00
|
|
|
|
2023-02-06 06:12:50 +13:00
|
|
|
RareCore.__instance = None
|
2022-09-05 05:38:24 +12:00
|
|
|
|
|
|
|
super(RareCore, self).deleteLater()
|
|
|
|
|
2023-02-09 12:37:53 +13:00
|
|
|
def get_game(self, app_name: str) -> RareGameBase:
|
2023-01-21 13:15:06 +13:00
|
|
|
if app_name == EOSOverlayApp.app_name:
|
|
|
|
return self.__eos_overlay_rgame
|
2023-01-05 09:38:05 +13:00
|
|
|
return self.__games[app_name]
|
|
|
|
|
|
|
|
def add_game(self, rgame: RareGame) -> None:
|
2023-01-27 15:11:10 +13:00
|
|
|
rgame.signals.download.enqueue.connect(self.__signals.download.enqueue)
|
|
|
|
rgame.signals.download.dequeue.connect(self.__signals.download.dequeue)
|
2023-01-21 13:15:06 +13:00
|
|
|
rgame.signals.game.install.connect(self.__signals.game.install)
|
|
|
|
rgame.signals.game.installed.connect(self.__signals.game.installed)
|
2023-01-27 15:11:10 +13:00
|
|
|
rgame.signals.game.uninstall.connect(self.__signals.game.uninstall)
|
2023-01-21 13:15:06 +13:00
|
|
|
rgame.signals.game.uninstalled.connect(self.__signals.game.uninstalled)
|
|
|
|
rgame.signals.game.finished.connect(self.__signals.application.update_tray)
|
|
|
|
rgame.signals.game.finished.connect(lambda: self.__signals.discord_rpc.set_title.emit(""))
|
2023-01-05 09:38:05 +13:00
|
|
|
self.__games[rgame.app_name] = rgame
|
|
|
|
|
|
|
|
def __filter_games(self, condition: Callable[[RareGame], bool]) -> Iterator[RareGame]:
|
|
|
|
return filter(condition, self.__games.values())
|
|
|
|
|
|
|
|
@property
|
|
|
|
def games_and_dlcs(self) -> Iterator[RareGame]:
|
|
|
|
for app_name in self.__games:
|
|
|
|
yield self.__games[app_name]
|
|
|
|
|
|
|
|
@property
|
|
|
|
def games(self) -> Iterator[RareGame]:
|
|
|
|
return self.__filter_games(lambda game: not game.is_dlc)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def installed_games(self) -> Iterator[RareGame]:
|
|
|
|
return self.__filter_games(lambda game: game.is_installed and not game.is_dlc)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def game_list(self) -> Iterator[Game]:
|
|
|
|
for game in self.games:
|
|
|
|
yield game.game
|
|
|
|
|
|
|
|
@property
|
|
|
|
def dlcs(self) -> Dict[str, Game]:
|
|
|
|
"""!
|
|
|
|
RareGames that ARE DLCs themselves
|
|
|
|
"""
|
|
|
|
return {game.game.catalog_item_id: game.owned_dlcs for game in self.has_dlcs}
|
|
|
|
# return self.__filter_games(lambda game: game.is_dlc)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def has_dlcs(self) -> Iterator[RareGame]:
|
|
|
|
"""!
|
|
|
|
RareGames that HAVE DLCs associated with them
|
|
|
|
"""
|
|
|
|
return self.__filter_games(lambda game: bool(game.owned_dlcs))
|
|
|
|
|
|
|
|
@property
|
|
|
|
def bit32_games(self) -> Iterator[RareGame]:
|
|
|
|
return self.__filter_games(lambda game: game.is_win32)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def mac_games(self) -> Iterator[RareGame]:
|
|
|
|
return self.__filter_games(lambda game: game.is_mac)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def no_asset_games(self) -> Iterator[RareGame]:
|
|
|
|
return self.__filter_games(lambda game: game.is_non_asset)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def unreal_engine(self) -> Iterator[RareGame]:
|
|
|
|
return self.__filter_games(lambda game: game.is_unreal)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def updates(self) -> Iterator[RareGame]:
|
|
|
|
return self.__filter_games(lambda game: game.has_update)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def saves(self) -> Iterator[SaveGameFile]:
|
|
|
|
"""!
|
|
|
|
SaveGameFiles across games
|
|
|
|
"""
|
|
|
|
return chain.from_iterable([game.saves for game in self.has_saves])
|
|
|
|
|
|
|
|
@property
|
|
|
|
def has_saves(self) -> Iterator[RareGame]:
|
|
|
|
"""!
|
|
|
|
RareGames that have SaveGameFiles associated with them
|
|
|
|
"""
|
|
|
|
return self.__filter_games(lambda game: bool(game.saves))
|
|
|
|
|