1
0
Fork 0
mirror of synced 2024-05-19 12:02:54 +12:00

RareCore: Manage initialization in RareCore instead of LaunchDialog

This is the last change of the `backend_refactor` branch. This makes
`RareCore` the centerpiece of Rare by moving initialization before the UI
is brought up. RareCore is now in control of creating and querying `RareGame`
objects, re-introducing the ability (incomplete) to refresh the games library.
As a result, ApiResults has been removed.

Signed-off-by: loathingKernel <142770+loathingKernel@users.noreply.github.com>
This commit is contained in:
loathingKernel 2023-03-04 13:23:18 +02:00
parent 9d4e0995fd
commit 5748d0e6ee
No known key found for this signature in database
GPG key ID: CE0C72D0B53821FD
15 changed files with 371 additions and 342 deletions

View file

@ -130,5 +130,3 @@ def start(args):
del app
if exit_code != -133742:
break

View file

@ -1,167 +1,21 @@
import os
import platform
from logging import getLogger
from PyQt5.QtCore import Qt, pyqtSignal, QRunnable, QObject, QThreadPool, QSettings, pyqtSlot
from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import QDialog, QApplication
from legendary.models.game import Game
from requests.exceptions import ConnectionError, HTTPError
from rare.components.dialogs.login import LoginDialog
from rare.models.apiresults import ApiResults
from rare.shared import LegendaryCoreSingleton, ArgumentsSingleton, ApiResultsSingleton, ImageManagerSingleton
from rare.shared.workers.uninstall import uninstall_game
from rare.shared import RareCore
from rare.ui.components.dialogs.launch_dialog import Ui_LaunchDialog
from rare.utils.misc import CloudWorker
from rare.widgets.elide_label import ElideLabel
logger = getLogger("LaunchDialog")
class LaunchWorker(QRunnable):
class Signals(QObject):
progress = pyqtSignal(int, str)
result = pyqtSignal(object, str)
finished = pyqtSignal()
def __init__(self):
super(LaunchWorker, self).__init__()
self.setAutoDelete(True)
self.signals = LaunchWorker.Signals()
self.core = LegendaryCoreSingleton()
def run_real(self):
pass
def run(self):
self.run_real()
self.signals.deleteLater()
class ImageWorker(LaunchWorker):
# FIXME: this is a middle-ground solution for concurrent downloads
class DownloadSlot(QObject):
def __init__(self, signals: LaunchWorker.Signals):
super(ImageWorker.DownloadSlot, self).__init__()
self.signals = signals
self.counter = 0
self.length = 0
@pyqtSlot(object)
def counter_inc(self, game: Game):
self.signals.progress.emit(
int(self.counter / self.length * 75),
self.tr("Downloading image for <b>{}</b>").format(game.app_title)
)
self.counter += 1
def __init__(self):
super(ImageWorker, self).__init__()
# FIXME: this is a middle-ground solution for concurrent downloads
self.dl_slot = ImageWorker.DownloadSlot(self.signals)
self.image_manager = ImageManagerSingleton()
def tr(self, t) -> str:
return QApplication.instance().translate(self.__class__.__name__, t)
def run_real(self):
# Download Images
games, dlcs = self.core.get_game_and_dlc_list(update_assets=True, skip_ue=False)
self.signals.result.emit((games, dlcs), "gamelist")
dlc_list = [dlc[0] for dlc in dlcs.values()]
na_games, na_dlcs = self.core.get_non_asset_library_items(force_refresh=False, skip_ue=False)
self.signals.result.emit((na_games, na_dlcs), "no_assets")
na_dlc_list = [dlc[0] for dlc in na_dlcs.values()]
game_list = games + dlc_list + na_games + na_dlc_list
self.dl_slot.length = len(game_list)
for i, game in enumerate(game_list):
if game.app_title == "Unreal Engine":
game.app_title += f" {game.app_name.split('_')[-1]}"
self.core.lgd.set_game_meta(game.app_name, game)
# self.image_manager.download_image_blocking(game)
self.image_manager.download_image(game, self.dl_slot.counter_inc, priority=0)
# FIXME: this is a middle-ground solution for concurrent downloads
while self.dl_slot.counter < len(game_list):
QApplication.instance().processEvents()
self.dl_slot.deleteLater()
igame_list = self.core.get_installed_list(include_dlc=True)
# FIXME: incorporate installed game status checking here for now, still slow
for i, igame in enumerate(igame_list):
# lk: do not check dlcs, they do not specify an executable
if igame.is_dlc:
continue
self.signals.progress.emit(
int(i / len(igame_list) * 25) + 75,
self.tr("Validating install for <b>{}</b>").format(igame.title)
)
if not os.path.exists(igame.install_path):
# lk; since install_path is lost anyway, set keep_files to True
# lk: to avoid spamming the log with "file not found" errors
uninstall_game(self.core, igame.app_name, keep_files=True)
logger.info(f"Uninstalled {igame.title}, because no game files exist")
continue
# lk: games that don't have an override and can't find their executable due to case sensitivity
# lk: will still erroneously require verification. This might need to be removed completely
# lk: or be decoupled from the verification requirement
if override_exe := self.core.lgd.config.get(igame.app_name, "override_exe", fallback=""):
igame_executable = override_exe
else:
igame_executable = igame.executable
# lk: Case-insensitive search for the game's executable (example: Brothers - A Tale of two Sons)
executable_path = os.path.join(igame.install_path, igame_executable.replace("\\", "/").lstrip("/"))
file_list = map(str.lower, os.listdir(os.path.dirname(executable_path)))
if not os.path.basename(executable_path).lower() in file_list:
igame.needs_verification = True
self.core.lgd.set_installed_game(igame.app_name, igame)
logger.info(f"{igame.title} needs verification")
# FIXME: end
self.signals.progress.emit(100, self.tr("Launching Rare"))
self.signals.finished.emit()
class ApiRequestWorker(LaunchWorker):
def __init__(self):
super(ApiRequestWorker, self).__init__()
self.settings = QSettings()
def run_real(self) -> None:
if self.settings.value("mac_meta", platform.system() == "Darwin", bool):
try:
result = self.core.get_game_and_dlc_list(update_assets=False, platform="Mac")
except HTTPError:
result = [], {}
else:
result = [], {}
self.signals.result.emit(result, "mac")
if self.settings.value("win32_meta", False, bool):
try:
result = self.core.get_game_and_dlc_list(update_assets=False, platform="Win32")
except HTTPError:
result = [], {}
else:
result = [], {}
self.signals.result.emit(result, "32bit")
try:
# FIXME: Add this to RareCore
self.core.lgd.entitlements = self.core.egs.get_user_entitlements()
except HTTPError:
logger.error("Failed to retrieve user entitlements")
class LaunchDialog(QDialog):
quit_app = pyqtSignal(int)
start_app = pyqtSignal()
completed = 0
def __init__(self, parent=None):
super(LaunchDialog, self).__init__(parent=parent)
@ -179,14 +33,17 @@ class LaunchDialog(QDialog):
self.ui = Ui_LaunchDialog()
self.ui.setupUi(self)
self.accept_close = False
self.progress_info = ElideLabel(parent=self)
self.progress_info.setFixedHeight(False)
self.ui.launch_layout.addWidget(self.progress_info)
self.core = LegendaryCoreSingleton()
self.args = ArgumentsSingleton()
self.thread_pool = QThreadPool().globalInstance()
self.api_results = ApiResults()
self.rcore = RareCore.instance()
self.rcore.progress.connect(self.__on_progress)
self.rcore.completed.connect(self.__on_completed)
self.core = self.rcore.core()
self.args = self.rcore.args()
self.login_dialog = LoginDialog(core=self.core, parent=parent)
@ -220,91 +77,22 @@ class LaunchDialog(QDialog):
else:
self.quit_app.emit(0)
def start_api_requests(self):
# gamelist and no_asset games are from Image worker
api_worker = ApiRequestWorker()
api_worker.signals.result.connect(self.handle_api_worker_result)
self.thread_pool.start(api_worker)
def launch(self):
self.progress_info.setText(self.tr("Preparing Rare"))
if not self.args.offline:
image_worker = ImageWorker()
image_worker.signals.result.connect(self.handle_api_worker_result)
image_worker.signals.progress.connect(self.update_progress)
# lk: start the api requests worker after the manifests have been downloaded
# lk: to avoid force updating the assets twice and causing inconsistencies
image_worker.signals.finished.connect(self.start_api_requests)
image_worker.signals.finished.connect(self.finish)
self.thread_pool.start(image_worker)
# cloud save from another worker, because it is used in cloud_save_utils too
cloud_worker = CloudWorker(self.core)
cloud_worker.signals.result_ready.connect(lambda x: self.handle_api_worker_result(x, "saves"))
self.thread_pool.start(cloud_worker)
else:
self.completed = 2
if self.core.lgd.assets:
self.api_results.games, self.api_results.dlcs = self.core.get_game_and_dlc_list(
update_assets=False, skip_ue=False
)
self.api_results.bit32_games = list(
map(lambda i: i.app_name, self.core.get_game_list(False, "Win32"))
)
self.api_results.mac_games = list(
map(lambda i: i.app_name, self.core.get_game_list(False, "Mac"))
)
else:
logger.warning("No assets found. Falling back to empty game lists")
self.api_results.games, self.api_results.dlcs = [], {}
self.api_results.mac_games, self.api_results.bit32_games = [], []
self.api_results.na_games, self.api_results.na_dlcs = [], {}
self.api_results.saves = []
self.finish()
def handle_api_worker_result(self, result, text):
logger.debug(f"Api Request got from {text}")
if text == "gamelist":
if result:
self.api_results.games, self.api_results.dlcs = result
else:
self.api_results.games, self.api_results.dlcs = self.core.get_game_and_dlc_list(
update_assets=False, skip_ue=False
)
elif text == "no_assets":
if result:
self.api_results.na_games, self.api_results.na_dlcs = result
else:
self.api_results.na_games, self.api_results.na_dlcs = self.core.get_non_asset_library_items(
force_refresh=False, skip_ue=False
)
elif text == "32bit":
self.api_results.bit32_games = [i.app_name for i in result[0]] if result else []
elif text == "mac":
self.api_results.mac_games = [i.app_name for i in result[0]] if result else []
elif text == "saves":
self.api_results.saves = result
if self.api_results:
self.finish()
self.rcore.fetch()
@pyqtSlot(int, str)
def update_progress(self, i: int, m: str):
def __on_progress(self, i: int, m: str):
self.ui.progress_bar.setValue(i)
self.progress_info.setText(m)
def finish(self):
self.completed += 1
if self.completed >= 2:
logger.info("App starting")
ApiResultsSingleton(self.api_results)
self.completed += 1
self.start_app.emit()
def __on_completed(self):
logger.info("App starting")
self.accept_close = True
self.start_app.emit()
def reject(self) -> None:
if self.completed >= 3:
if self.accept_close:
super(LaunchDialog, self).reject()
else:
pass

View file

@ -2,7 +2,7 @@ import os
from logging import getLogger
from PyQt5.QtCore import Qt, QSettings, QTimer, QSize, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QCloseEvent, QCursor
from PyQt5.QtGui import QCloseEvent, QCursor, QShowEvent
from PyQt5.QtWidgets import (
QMainWindow,
QApplication,
@ -107,8 +107,9 @@ class MainWindow(QMainWindow):
logger.warning("Discord RPC module not found")
self.timer = QTimer()
self.timer.setInterval(1000)
self.timer.timeout.connect(self.timer_finished)
self.timer.start(1000)
self.timer.start()
self.tray_icon: TrayIcon = TrayIcon(self)
self.tray_icon.exit_app.connect(self.on_exit_app)
@ -142,6 +143,15 @@ class MainWindow(QMainWindow):
self.resize(window_size)
self.move(screen_rect.center() - self.rect().adjusted(0, 0, decor_width, decor_height).center())
# lk: For the gritty details see `RareCore.load_pixmaps()` method
# Just before the window is shown, fire a timer to load game icons
# This is by nature a little iffy because we don't really know if the
# has been shown, and it might make the window delay as widgets being are updated.
# Still better than showing a hanged window frame for a few seconds.
def showEvent(self, a0: QShowEvent) -> None:
if not self._window_launched:
QTimer.singleShot(100, self.rcore.load_pixmaps)
@pyqtSlot()
def show(self) -> None:
super(MainWindow, self).show()
@ -198,7 +208,7 @@ class MainWindow(QMainWindow):
if action.startswith("show"):
self.show()
os.remove(file_path)
self.timer.start(1000)
self.timer.start()
@pyqtSlot()
@pyqtSlot(int)

View file

@ -2,14 +2,12 @@ from logging import getLogger
from PyQt5.QtCore import QSettings, Qt, pyqtSlot
from PyQt5.QtWidgets import QStackedWidget, QVBoxLayout, QWidget, QScrollArea, QFrame
from legendary.models.game import Game
from rare.models.game import RareGame
from rare.shared import (
LegendaryCoreSingleton,
GlobalSignalsSingleton,
ArgumentsSingleton,
ApiResultsSingleton,
ImageManagerSingleton,
)
from rare.shared import RareCore
@ -32,7 +30,6 @@ class GamesTab(QStackedWidget):
self.core = LegendaryCoreSingleton()
self.signals = GlobalSignalsSingleton()
self.args = ArgumentsSingleton()
self.api_results = ApiResultsSingleton()
self.image_manager = ImageManagerSingleton()
self.settings = QSettings()
@ -142,37 +139,18 @@ class GamesTab(QStackedWidget):
@pyqtSlot()
def update_count_games_label(self):
self.head_bar.set_games_count(
len(self.core.get_installed_list()), len(self.api_results.games + self.api_results.na_games)
len([game for game in self.rcore.games if game.is_installed]),
len([game for game in self.rcore.games])
)
# FIXME: Remove this when RareCore is in place
def __create_game_with_dlcs(self, game: Game) -> RareGame:
rgame = RareGame(self.core, self.image_manager, game)
rgame.saves = [save for save in self.api_results.saves if save.app_name == game.app_name]
for dlc_dict in [self.api_results.dlcs, self.api_results.na_dlcs]:
if game_dlcs := dlc_dict.get(rgame.game.catalog_item_id, False):
for dlc in game_dlcs:
rdlc = RareGame(self.core, self.image_manager, dlc)
self.rcore.add_game(rdlc)
# lk: plug dlc progress signals to the game's
rdlc.signals.progress.start.connect(rgame.signals.progress.start)
rdlc.signals.progress.update.connect(rgame.signals.progress.update)
rdlc.signals.progress.finish.connect(rgame.signals.progress.finish)
rdlc.set_pixmap()
rgame.owned_dlcs.append(rdlc)
return rgame
def setup_game_list(self):
for game in self.api_results.games + self.api_results.na_games:
rgame = self.__create_game_with_dlcs(game)
self.rcore.add_game(rgame)
for rgame in self.rcore.games:
icon_widget, list_widget = self.add_library_widget(rgame)
if not icon_widget or not list_widget:
logger.warning(f"Excluding {rgame.app_name} from the game list")
continue
self.icon_view.layout().addWidget(icon_widget)
self.list_view.layout().addWidget(list_widget)
rgame.set_pixmap()
self.filter_games(self.active_filter)
self.update_count_games_label()

View file

@ -1,10 +1,9 @@
from typing import Tuple, List, Union
from typing import Tuple, List, Union, Optional
from PyQt5.QtCore import QObject, pyqtSlot
from PyQt5.QtWidgets import QWidget
from rare.lgndr.core import LegendaryCore
from rare.models.apiresults import ApiResults
from rare.models.game import RareGame
from rare.models.signals import GlobalSignals
from rare.shared import RareCore
@ -20,7 +19,6 @@ class LibraryWidgetController(QObject):
self.rcore = RareCore.instance()
self.core: LegendaryCore = self.rcore.core()
self.signals: GlobalSignals = self.rcore.signals()
self.api_results: ApiResults = self.rcore.api_results()
self.signals.game.installed.connect(self.sort_list)
self.signals.game.uninstalled.connect(self.sort_list)
@ -115,7 +113,7 @@ class LibraryWidgetController(QObject):
list_widgets = self._list_container.findChildren(ListGameWidget)
icon_app_names = set([iw.rgame.app_name for iw in icon_widgets])
list_app_names = set([lw.rgame.app_name for lw in list_widgets])
games = self.api_results.games + self.api_results.na_games
games = list(self.rcore.games)
game_app_names = set([g.app_name for g in games])
new_icon_app_names = game_app_names.difference(icon_app_names)
new_list_app_names = game_app_names.difference(list_app_names)

View file

@ -1,4 +1,4 @@
from PyQt5.QtCore import QSettings, pyqtSignal
from PyQt5.QtCore import QSettings, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import (
QLabel,
QPushButton,
@ -8,7 +8,7 @@ from PyQt5.QtWidgets import (
)
from qtawesome import IconWidget
from rare.shared import ApiResultsSingleton
from rare.shared import RareCore
from rare.utils.extra_widgets import SelectViewWidget, ButtonLineEdit
from rare.utils.misc import icon
@ -21,7 +21,7 @@ class GameListHeadBar(QWidget):
def __init__(self, parent=None):
super(GameListHeadBar, self).__init__(parent=parent)
self.api_results = ApiResultsSingleton()
self.rcore = RareCore.instance()
self.settings = QSettings()
self.filter = QComboBox()
@ -38,15 +38,15 @@ class GameListHeadBar(QWidget):
"installed",
"offline",
]
if self.api_results.bit32_games:
if self.rcore.bit32_games:
self.filter.addItem(self.tr("32 Bit Games"))
self.available_filters.append("32bit")
if self.api_results.mac_games:
if self.rcore.mac_games:
self.filter.addItem(self.tr("Mac games"))
self.available_filters.append("mac")
if self.api_results.na_games:
if self.rcore.no_asset_games:
self.filter.addItem(self.tr("Exclude Origin"))
self.available_filters.append("installable")
@ -107,6 +107,7 @@ class GameListHeadBar(QWidget):
self.refresh_list = QPushButton()
self.refresh_list.setIcon(icon("fa.refresh")) # Reload icon
self.refresh_list.clicked.connect(self.rcore.fetch)
layout = QHBoxLayout()
layout.setContentsMargins(0, 5, 0, 5)
@ -130,6 +131,11 @@ class GameListHeadBar(QWidget):
self.installed_label.setText(str(inst))
self.available_label.setText(str(avail))
def filter_changed(self, i):
@pyqtSlot()
def refresh_clicked(self):
self.rcore.fetch()
@pyqtSlot(int)
def filter_changed(self, i: int):
self.filterChanged.emit(self.available_filters[i])
self.settings.setValue("filter", i)

View file

@ -162,16 +162,9 @@ class ImportGroup(QGroupBox):
self.ui.setupUi(self)
self.rcore = RareCore.instance()
self.core = RareCore.instance().core()
self.api_results = RareCore.instance().api_results()
#self.app_name_list = [rgame.app_name for rgame in self.rcore.games]
self.app_name_list = [game.app_name for game in self.api_results.games]
#self.install_dir_list = [rgame.folder_name for rgame in self.rcore.games if not rgame.is_dlc]
self.install_dir_list = [
game.metadata.get("customAttributes", {}).get("FolderName", {}).get("value", game.app_name)
for game in self.api_results.games
if not game.is_dlc
]
self.app_name_list = [rgame.app_name for rgame in self.rcore.games]
self.install_dir_list = [rgame.folder_name for rgame in self.rcore.games if not rgame.is_dlc]
self.path_edit = PathEdit(
self.core.get_default_install_dir(),
@ -185,8 +178,8 @@ class ImportGroup(QGroupBox):
self.app_name_edit = IndicatorLineEdit(
placeholder=self.tr("Use in case the app name was not found automatically"),
completer=AppNameCompleter(
# app_names=[(rgame.app_name, rgame.app_title) for rgame in self.rcore.games]
app_names = [(i.app_name, i.app_title) for i in self.api_results.games]),
app_names=[(rgame.app_name, rgame.app_title) for rgame in self.rcore.games],
),
edit_func=self.app_name_edit_callback,
parent=self,
)

View file

@ -1,7 +1,7 @@
from PyQt5.QtWidgets import QStackedWidget, QTabWidget
from legendary.core import LegendaryCore
from rare.shared import ApiResultsSingleton
from rare.shared.rare_core import RareCore
from rare.utils.paths import cache_dir
from .game_info import ShopGameInfo
from .search_results import SearchResults
@ -16,7 +16,7 @@ class Shop(QStackedWidget):
def __init__(self, core: LegendaryCore):
super(Shop, self).__init__()
self.core = core
self.api_results = ApiResultsSingleton()
self.rcore = RareCore.instance()
self.api_core = ShopApiCore(
self.core.egs.session.headers["Authorization"],
self.core.language_code,
@ -36,7 +36,7 @@ class Shop(QStackedWidget):
self.addWidget(self.search_results)
self.search_results.show_info.connect(self.show_game_info)
self.info = ShopGameInfo(
[i.asset_infos["Windows"].namespace for i in self.api_results.games],
[i.asset_infos["Windows"].namespace for i in self.rcore.game_list if bool(i.asset_infos)],
self.api_core,
)
self.addWidget(self.info)

View file

@ -1,26 +0,0 @@
from dataclasses import dataclass
from typing import Optional, List, Dict
from legendary.models.game import Game, SaveGameFile
@dataclass
class ApiResults:
games: Optional[List[Game]] = None
dlcs: Optional[Dict[str, List[Game]]] = None
bit32_games: Optional[List] = None
mac_games: Optional[List] = None
na_games: Optional[List[Game]] = None
na_dlcs: Optional[Dict[str, List[Game]]] = None
saves: Optional[List[SaveGameFile]] = None
def __bool__(self):
return (
self.games is not None
and self.dlcs is not None
and self.bit32_games is not None
and self.mac_games is not None
and self.na_games is not None
and self.na_dlcs is not None
and self.saves is not None
)

View file

@ -10,7 +10,6 @@ from argparse import Namespace
from typing import Optional
from rare.lgndr.core import LegendaryCore
from rare.models.apiresults import ApiResults
from rare.models.signals import GlobalSignals
from .image_manager import ImageManager
from .rare_core import RareCore
@ -32,7 +31,3 @@ def LegendaryCoreSingleton() -> LegendaryCore:
def ImageManagerSingleton() -> ImageManager:
return RareCore.instance().image_manager()
def ApiResultsSingleton(res: ApiResults = None) -> Optional[ApiResults]:
return RareCore.instance().api_results(res)

View file

@ -387,11 +387,11 @@ class ImageManager(QObject):
return data
def download_image(
self, game: Game, load_callback: Callable[[Game], None], priority: int, force: bool = False
self, game: Game, load_callback: Callable[[], None], priority: int, force: bool = False
) -> None:
updates, json_data = self.__prepare_download(game, force)
if not updates:
load_callback(game)
load_callback()
return
if updates and game.app_name not in self.__worker_app_names:
image_worker = ImageManager.Worker(self.__download, updates, json_data, game)

View file

@ -1,26 +1,43 @@
import configparser
import os
import platform
import time
from argparse import Namespace
from itertools import chain
from logging import getLogger
from typing import Optional, Dict, Iterator, Callable, List, Union
from typing import Dict, Iterator, Callable, Tuple, Optional, List, Union
from PyQt5.QtCore import QObject, QThreadPool
from PyQt5.QtCore import QObject, pyqtSignal, QSettings, pyqtSlot, QThreadPool
from legendary.lfs.eos import EOSOverlayApp
from legendary.models.game import Game, SaveGameFile
from rare.lgndr.core import LegendaryCore
from rare.models.apiresults import ApiResults
from rare.models.game import RareGame, RareEosOverlay
from rare.models.signals import GlobalSignals
from .workers import QueueWorker, VerifyWorker, MoveWorker
from .image_manager import ImageManager
from .workers import (
QueueWorker,
VerifyWorker,
MoveWorker,
SavesWorker,
GamesWorker,
NonAssetWorker,
EntitlementsWorker,
Win32Worker,
MacOSWorker,
FetchWorker,
)
from .workers.uninstall import uninstall_game
from .workers.worker import QueueWorkerInfo, QueueWorkerState
logger = getLogger("RareCore")
class RareCore(QObject):
progress = pyqtSignal(int, str)
completed = pyqtSignal()
# lk: special case class attribute, this has to be here
__instance: Optional['RareCore'] = None
def __init__(self, args: Namespace):
@ -31,13 +48,21 @@ class RareCore(QObject):
self.__signals: Optional[GlobalSignals] = None
self.__core: Optional[LegendaryCore] = None
self.__image_manager: Optional[ImageManager] = None
self.__api_results: Optional[ApiResults] = None
self.__games_fetched: bool = False
self.__non_asset_fetched: bool = False
self.__win32_fetched: bool = False
self.__macos_fetched: bool = False
self.__saves_fetched: bool = False
self.__entitlements_fetched: bool = False
self.args(args)
self.signals(init=True)
self.core(init=True)
self.image_manager(init=True)
self.settings = QSettings()
self.queue_workers: List[QueueWorker] = []
self.queue_threadpool = QThreadPool()
self.queue_threadpool.setMaxThreadCount(2)
@ -142,19 +167,7 @@ class RareCore(QObject):
self.__image_manager = ImageManager(self.signals(), self.core())
return self.__image_manager
def api_results(self, res: ApiResults = None) -> Optional[ApiResults]:
if self.__api_results is None and res is None:
raise RuntimeError("Uninitialized use of ApiResultsSingleton")
if self.__api_results is not None and res is not None:
raise RuntimeError("ApiResults already initialized")
if res is not None:
self.__api_results = res
return self.__api_results
def deleteLater(self) -> None:
del self.__api_results
self.__api_results = None
self.__image_manager.deleteLater()
del self.__image_manager
self.__image_manager = None
@ -174,12 +187,51 @@ class RareCore(QObject):
super(RareCore, self).deleteLater()
def __validate_installed(self):
filter_lambda = lambda rg: rg.is_installed and not (rg.is_dlc or rg.is_non_asset)
length = len(list(self.__filter_games(filter_lambda)))
for i, rgame in enumerate(self.__filter_games(filter_lambda)):
self.progress.emit(
int(i / length * 25) + 75,
self.tr("Validating install for <b>{}</b>").format(rgame.app_title)
)
if not os.path.exists(rgame.igame.install_path):
# lk: since install_path is lost anyway, set keep_files to True
# lk: to avoid spamming the log with "file not found" errors
for dlc in rgame.owned_dlcs:
if dlc.is_installed:
logger.info(f'Uninstalling DLC "{dlc.app_name}" ({dlc.app_title})...')
uninstall_game(self.__core, dlc.app_name, keep_files=True)
dlc.igame = None
logger.info(
f'Removing "{rgame.app_title}" because "{rgame.igame.install_path}" does not exist...'
)
uninstall_game(self.__core, rgame.app_name, keep_files=True)
logger.info(f"Uninstalled {rgame.app_title}, because no game files exist")
rgame.igame = None
continue
# lk: games that don't have an override and can't find their executable due to case sensitivity
# lk: will still erroneously require verification. This might need to be removed completely
# lk: or be decoupled from the verification requirement
if override_exe := self.__core.lgd.config.get(rgame.app_name, "override_exe", fallback=""):
igame_executable = override_exe
else:
igame_executable = rgame.igame.executable
# lk: Case-insensitive search for the game's executable (example: Brothers - A Tale of two Sons)
executable_path = os.path.join(rgame.igame.install_path, igame_executable.replace("\\", "/").lstrip("/"))
file_list = map(str.lower, os.listdir(os.path.dirname(executable_path)))
if not os.path.basename(executable_path).lower() in file_list:
rgame.igame.needs_verification = True
self.__core.lgd.set_installed_game(rgame.app_name, rgame.igame)
rgame.update_igame()
logger.info(f"{rgame.app_title} needs verification")
def get_game(self, app_name: str) -> Union[RareEosOverlay, RareGame]:
if app_name == EOSOverlayApp.app_name:
return self.__eos_overlay_rgame
return self.__games[app_name]
def add_game(self, rgame: RareGame) -> None:
def __add_game(self, rgame: RareGame) -> None:
rgame.signals.download.enqueue.connect(self.__signals.download.enqueue)
rgame.signals.download.dequeue.connect(self.__signals.download.dequeue)
rgame.signals.game.install.connect(self.__signals.game.install)
@ -193,6 +245,146 @@ class RareCore(QObject):
def __filter_games(self, condition: Callable[[RareGame], bool]) -> Iterator[RareGame]:
return filter(condition, self.__games.values())
def __create_or_update_rgame(self, game: Game) -> RareGame:
if rgame := self.__games.get(game.app_name, False):
logger.warning(f"{rgame.app_name} already present in {type(self).__name__}")
logger.info(f"Updating Game for {rgame.app_name}")
rgame.update_rgame()
else:
rgame = RareGame(self.__core, self.__image_manager, game)
return rgame
def __add_games_and_dlcs(self, games: List[Game], dlcs_dict: Dict[str, List]) -> None:
for game in games:
rgame = self.__create_or_update_rgame(game)
if game_dlcs := dlcs_dict.get(rgame.game.catalog_item_id, False):
for dlc in game_dlcs:
rdlc = self.__create_or_update_rgame(dlc)
# lk: plug dlc progress signals to the game's
rdlc.signals.progress.start.connect(rgame.signals.progress.start)
rdlc.signals.progress.update.connect(rgame.signals.progress.update)
rdlc.signals.progress.finish.connect(rgame.signals.progress.finish)
rgame.owned_dlcs.append(rdlc)
self.__add_game(rdlc)
self.__add_game(rgame)
@pyqtSlot(object, int)
def handle_result(self, result: Tuple, res_type: int):
status = ""
if res_type == FetchWorker.Result.GAMES:
games, dlc_dict = result
self.__add_games_and_dlcs(games, dlc_dict)
self.__games_fetched = True
status = "Loaded games for Windows"
if res_type == FetchWorker.Result.NON_ASSET:
games, dlc_dict = result
self.__add_games_and_dlcs(games, dlc_dict)
self.__non_asset_fetched = True
status = "Loaded games without assets"
if res_type == FetchWorker.Result.WIN32:
self.__win32_fetched = True
status = "Loaded games for Windows (32bit)"
if res_type == FetchWorker.Result.MACOS:
self.__macos_fetched = True
status = "Loaded games for MacOS"
if res_type == FetchWorker.Result.SAVES:
saves, _ = result
for save in saves:
self.__games[save.app_name].saves.append(save)
self.__saves_fetched = True
status = "Loaded save games"
if res_type == FetchWorker.Result.ENTITLEMENTS:
self.__core.lgd.entitlements = result
self.__entitlements_fetched = True
status = "Loaded game entitlements"
logger.debug(f"Got API results for {FetchWorker.Result(res_type).name}")
fetched = [
self.__games_fetched,
self.__non_asset_fetched,
self.__win32_fetched,
self.__macos_fetched,
self.__saves_fetched,
self.__entitlements_fetched,
]
self.progress.emit(sum(fetched) * 10, status)
if all(fetched):
self.progress.emit(75, self.tr("Validating game installations"))
self.__validate_installed()
self.progress.emit(100, self.tr("Launching Rare"))
logger.debug(f"Fetch time {time.time() - self.start_time} seconds")
self.completed.emit()
def fetch(self):
self.__games_fetched: bool = False
self.__non_asset_fetched: bool = False
self.__win32_fetched: bool = False
self.__macos_fetched: bool = False
self.__saves_fetched: bool = False
self.__entitlements_fetched: bool = False
self.start_time = time.time()
games_worker = GamesWorker(self.__core, self.__args)
games_worker.signals.result.connect(self.handle_result)
games_worker.signals.finished.connect(self.fetch_saves)
games_worker.signals.finished.connect(self.fetch_extra)
QThreadPool.globalInstance().start(games_worker)
def fetch_saves(self):
if not self.__args.offline:
saves_worker = SavesWorker(self.__core, self.__args)
saves_worker.signals.result.connect(self.handle_result)
QThreadPool.globalInstance().start(saves_worker)
else:
self.__saves_fetched = True
def fetch_extra(self):
non_asset_worker = NonAssetWorker(self.__core, self.__args)
non_asset_worker.signals.result.connect(self.handle_result)
QThreadPool.globalInstance().start(non_asset_worker)
entitlements_worker = EntitlementsWorker(self.__core, self.__args)
entitlements_worker.signals.result.connect(self.handle_result)
QThreadPool.globalInstance().start(entitlements_worker)
if self.settings.value("win32_meta", False, bool) and not self.__win32_fetched:
win32_worker = Win32Worker(self.__core, self.__args)
win32_worker.signals.result.connect(self.handle_result)
QThreadPool.globalInstance().start(win32_worker)
else:
self.__win32_fetched = True
if self.settings.value("mac_meta", platform.system() == "Darwin", bool) and not self.__macos_fetched:
macos_worker = MacOSWorker(self.__core, self.__args)
macos_worker.signals.result.connect(self.handle_result)
QThreadPool.globalInstance().start(macos_worker)
else:
self.__macos_fetched = True
def load_pixmaps(self) -> None:
"""
Load pixmaps for all games
This exists here solely to fight signal and possibly threading issues.
The initial image loading at startup should not be done in the RareGame class
for two reasons. It will delay startup due to widget updates and the image
might become availabe before the UI is brought up. In case of the second, we
will get both a long queue of signals to be serviced and some of them might
be not connected yet so the widget won't be updated. So do the loading here
by calling this after the MainWindow has finished initializing.
@return: None
"""
QThreadPool.globalInstance().start(self.__load_pixmaps)
def __load_pixmaps(self) -> None:
# time.sleep(0.1)
for rgame in self.__games.values():
rgame.set_pixmap()
# time.sleep(0.0001)
@property
def games_and_dlcs(self) -> Iterator[RareGame]:
for app_name in self.__games:

View file

@ -1,3 +1,4 @@
from .fetch import FetchWorker, GamesWorker, NonAssetWorker, EntitlementsWorker, Win32Worker, MacOSWorker, SavesWorker
from .install_info import InstallInfoWorker
from .move import MoveWorker
from .uninstall import UninstallWorker

View file

@ -0,0 +1,104 @@
import time
from argparse import Namespace
from enum import IntEnum
from logging import getLogger
from PyQt5.QtCore import QObject, pyqtSignal
from requests.exceptions import ConnectionError, HTTPError
from rare.lgndr.core import LegendaryCore
from .worker import Worker
logger = getLogger("FetchWorker")
class FetchWorker(Worker):
class Result(IntEnum):
GAMES = 1
NON_ASSET = 2
WIN32 = 3
MACOS = 4
SAVES = 5
ENTITLEMENTS = 6
class Signals(QObject):
result = pyqtSignal(object, int)
finished = pyqtSignal()
def __init__(self, core: LegendaryCore, args: Namespace):
super(Worker, self).__init__()
self.signals = FetchWorker.Signals()
self.core = core
self.args = args
class GamesWorker(FetchWorker):
def run_real(self):
start_time = time.time()
result = self.core.get_game_and_dlc_list(update_assets=not self.args.offline, platform="Windows", skip_ue=False)
self.signals.result.emit(result, FetchWorker.Result.GAMES)
logger.debug(f"Games: {len(result[0])}, DLCs {len(result[1])}")
logger.debug(f"Request Games: {time.time() - start_time} seconds")
self.signals.finished.emit()
class NonAssetWorker(FetchWorker):
def run_real(self):
start_time = time.time()
try:
result = self.core.get_non_asset_library_items(force_refresh=False, skip_ue=False)
except (HTTPError, ConnectionError) as e:
logger.warning(f"Exception while fetching non asset games from EGS: {e}")
result = ([], {})
self.signals.result.emit(result, FetchWorker.Result.NON_ASSET)
logger.debug(f"Non asset: {len(result[0])}, DLCs {len(result[1])}")
logger.debug(f"Request Non Asset: {time.time() - start_time} seconds")
self.signals.finished.emit()
class EntitlementsWorker(FetchWorker):
def run_real(self):
start_time = time.time()
try:
entitlements = self.core.egs.get_user_entitlements()
except (HTTPError, ConnectionError) as e:
logger.error(f"Failed to retrieve user entitlements from EGS: {e}")
entitlements = {}
self.signals.result.emit(entitlements, FetchWorker.Result.ENTITLEMENTS)
logger.debug(f"Entitlements: {len(list(entitlements))}")
logger.debug(f"Request Entitlements: {time.time() - start_time} seconds")
self.signals.finished.emit()
class Win32Worker(FetchWorker):
def run_real(self):
start_time = time.time()
result = self.core.get_game_and_dlc_list(update_assets=False, platform="Win32")
self.signals.result.emit(([], {}), FetchWorker.Result.WIN32)
logger.debug(f"Win32: {len(result[0])}, DLCs {len(result[1])}")
logger.debug(f"Request Win32: {time.time() - start_time} seconds")
self.signals.finished.emit()
class MacOSWorker(FetchWorker):
def run_real(self):
start_time = time.time()
result = self.core.get_game_and_dlc_list(update_assets=False, platform="Mac")
self.signals.result.emit(([], {}), FetchWorker.Result.MACOS)
logger.debug(f"MacOS: {len(result[0])}, DLCs {len(result[1])}")
logger.debug(f"Request MacOS: {time.time() - start_time} seconds")
self.signals.finished.emit()
class SavesWorker(FetchWorker):
def run_real(self):
start_time = time.time()
try:
result = self.core.get_save_games()
except (HTTPError, ConnectionError) as e:
logger.warning(f"Exception while fetching saves fromt EGS: {e}")
result = list()
self.signals.result.emit((result, {}), FetchWorker.Result.SAVES)
logger.debug(f"Saves: {len(result)}")
logger.debug(f"Request saves: {time.time() - start_time} seconds")
self.signals.finished.emit()

View file

@ -21,7 +21,6 @@ from legendary.core import LegendaryCore
from legendary.models.game import Game
from requests.exceptions import HTTPError
from rare.models.apiresults import ApiResults
from rare.utils.paths import resources_path
logger = getLogger("Utils")
@ -194,13 +193,6 @@ def get_raw_save_path(game: Game):
)
def get_default_platform(app_name, api_results: ApiResults):
if platform.system() != "Darwin" or app_name not in api_results.mac_games:
return "Windows"
else:
return "Mac"
def icon(icn_str: str, fallback: str = None, **kwargs):
try:
return qtawesome.icon(icn_str, **kwargs)