1
0
Fork 0
mirror of synced 2024-06-28 03:00:49 +12:00

Merge pull request #312 from loathingKernel/fixups

https://github.com/RareDevs/Rare/pull/312#issue-2002162120
This commit is contained in:
Stelios Tsampas 2023-12-03 11:45:14 +02:00 committed by GitHub
commit c7efe3615a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 822 additions and 495 deletions

View file

@ -1,5 +1,5 @@
name: "Development Snapshot"
name: "Snapshot"
on:
@ -7,7 +7,7 @@ on:
jobs:
version:
name: "Describe version"
name: "Version"
runs-on: ubuntu-latest
outputs:
tag_abbrev: ${{ steps.version.outputs.tag_abbrev }}
@ -239,7 +239,8 @@ jobs:
git clone https://github.com/create-dmg/create-dmg
create-dmg/create-dmg Rare.dmg dist/Rare.App --volname Rare --volicon rare/resources/images/Rare.icns
- uses: actions/upload-artifact@v3
- name: Upload to Artifacts
uses: actions/upload-artifact@v3
with:
name: Rare-${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}.dmg
path: Rare.dmg

View file

@ -14,8 +14,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
ref: "release"
- name: Set up Python
uses: actions/setup-python@v4
with:
@ -65,8 +63,6 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
with:
ref: "release"
- name: Install build dependencies
run: |
sudo apt update
@ -105,8 +101,6 @@ jobs:
runs-on: "windows-latest"
steps:
- uses: actions/checkout@v3
with:
ref: "release"
- uses: actions/setup-python@v4
with:
cache: pip
@ -170,8 +164,6 @@ jobs:
runs-on: "windows-latest"
steps:
- uses: actions/checkout@v3
with:
ref: "release"
- uses: actions/setup-python@v4
with:
cache: pip
@ -202,8 +194,6 @@ jobs:
runs-on: "windows-latest"
steps:
- uses: actions/checkout@v3
with:
ref: "release"
- uses: actions/setup-python@v4
with:
cache: pip
@ -236,8 +226,6 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
with:
ref: "release"
- uses: actions/setup-python@v4
with:
cache: pip
@ -264,7 +252,7 @@ jobs:
git clone https://github.com/create-dmg/create-dmg
create-dmg/create-dmg Rare.dmg dist/Rare.App --volname Rare --volicon rare/resources/images/Rare.icns
- name: upload to GitHub
- name: Upload to Releases
uses: svenstaro/upload-release-action@2.2.1
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}

View file

@ -1,2 +1,2 @@
__version__ = "1.10.3"
__version__ = "1.10.4"
__codename__ = "Garlic Crab"

View file

@ -5,7 +5,7 @@ from typing import Tuple, List, Union, Optional
from PyQt5.QtCore import Qt, QThreadPool, QSettings, QCoreApplication
from PyQt5.QtCore import pyqtSignal, pyqtSlot
from PyQt5.QtGui import QCloseEvent, QKeyEvent
from PyQt5.QtGui import QCloseEvent, QKeyEvent, QShowEvent
from PyQt5.QtWidgets import QDialog, QFileDialog, QCheckBox, QLayout, QWidget, QVBoxLayout
from legendary.utils.selective_dl import get_sdl_appname
@ -66,6 +66,8 @@ class InstallDialog(QDialog):
header = self.tr("Repair and update")
elif options.update:
header = self.tr("Update")
elif options.reset_sdl:
header = self.tr("Modify")
else:
header = self.tr("Install")
self.ui.install_dialog_label.setText(f'<h3>{header} "{self.rgame.app_title}"</h3>')
@ -95,12 +97,8 @@ class InstallDialog(QDialog):
self.error_box()
platforms = ["Windows"]
if self.rgame.is_win32:
platforms.append("Win32")
if self.rgame.is_mac:
platforms.append("Mac")
self.ui.platform_combo.addItems(platforms)
platforms = self.rgame.platforms
self.ui.platform_combo.addItems(reversed(platforms))
self.ui.platform_combo.currentIndexChanged.connect(lambda: self.option_changed(None))
self.ui.platform_combo.currentIndexChanged.connect(lambda: self.error_box())
self.ui.platform_combo.currentIndexChanged.connect(
@ -113,8 +111,11 @@ class InstallDialog(QDialog):
if (self.ui.platform_combo.currentText() == "Mac" and pf.system() != "Darwin")
else None
)
if pf.system() == "Darwin" and "Mac" in platforms:
self.ui.platform_combo.setCurrentIndex(platforms.index("Mac"))
self.ui.platform_combo.setCurrentIndex(
self.ui.platform_combo.findText(
"Mac" if (pf.system() == "Darwin" and "Mac" in platforms) else "Windows"
)
)
self.ui.platform_combo.currentTextChanged.connect(self.setup_sdl_list)
self.advanced.ui.max_workers_spin.setValue(self.core.lgd.config.getint("Legendary", "max_workers", fallback=0))
@ -135,7 +136,7 @@ class InstallDialog(QDialog):
self.selectable_checks: List[TagCheckBox] = []
self.config_tags: Optional[List[str]] = None
self.setup_sdl_list("Mac" if pf.system() == "Darwin" and "Mac" in platforms else "Windows")
self.setup_sdl_list(self.ui.platform_combo.currentText())
self.ui.install_button.setEnabled(False)
@ -171,6 +172,12 @@ class InstallDialog(QDialog):
self.ui.install_dialog_layout.setSizeConstraint(QLayout.SetFixedSize)
def showEvent(self, a0: QShowEvent) -> None:
if a0.spontaneous():
return super().showEvent(a0)
self.save_install_edit(self.install_dir_edit.text())
super().showEvent(a0)
def execute(self):
if self.options.silent:
self.reject_close = False
@ -192,7 +199,6 @@ class InstallDialog(QDialog):
config_disable_sdl = self.core.lgd.config.getboolean(self.rgame.app_name, 'disable_sdl', fallback=False)
sdl_name = get_sdl_appname(self.rgame.app_name)
if not config_disable_sdl and sdl_name is not None:
# FIXME: this should be updated whenever platform changes
sdl_data = self.core.get_sdl_data(sdl_name, platform=platform)
if sdl_data:
widget = QWidget(self.selectable)

View file

@ -143,15 +143,6 @@ 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()

View file

@ -102,7 +102,7 @@ class DownloadsTab(QWidget):
@pyqtSlot(str)
@pyqtSlot(RareGame)
def __add_update(self, update: Union[str,RareGame]):
def __add_update(self, update: Union[str, RareGame]):
if isinstance(update, str):
update = self.rcore.get_game(update)
if update.metadata.auto_update or QSettings().value("auto_update", False, bool):
@ -192,12 +192,12 @@ class DownloadsTab(QWidget):
if item.expired:
self.__refresh_download(item)
return
thread = DlThread(item, self.rcore.get_game(item.options.app_name), self.core, self.args.debug)
thread.result.connect(self.__on_download_result)
thread.progress.connect(self.__on_download_progress)
thread.finished.connect(thread.deleteLater)
thread.start()
self.__thread = thread
dl_thread = DlThread(item, self.rcore.get_game(item.options.app_name), self.core, self.args.debug)
dl_thread.result.connect(self.__on_download_result)
dl_thread.progress.connect(self.__on_download_progress)
dl_thread.finished.connect(dl_thread.deleteLater)
dl_thread.start()
self.__thread = dl_thread
self.download_widget.ui.kill_button.setDisabled(False)
self.download_widget.ui.dl_name.setText(item.download.game.app_title)
self.download_widget.setPixmap(

View file

@ -9,7 +9,7 @@ from rare.ui.components.tabs.games.game_info.game_dlc import Ui_GameDlc
from rare.ui.components.tabs.games.game_info.game_dlc_widget import Ui_GameDlcWidget
from rare.widgets.image_widget import ImageWidget, ImageSize
from rare.widgets.side_tab import SideTabContents
from rare.utils.misc import widget_object_name
from rare.utils.misc import widget_object_name, icon
class GameDlcWidget(QFrame):
@ -48,6 +48,7 @@ class InstalledGameDlcWidget(GameDlcWidget):
self.ui.action_button.setObjectName("UninstallButton")
self.ui.action_button.clicked.connect(self.uninstall_dlc)
self.ui.action_button.setText(self.tr("Uninstall DLC"))
self.ui.action_button.setIcon(icon("ri.uninstall-line"))
# lk: don't reference `self.rdlc` here because the object has been deleted
rdlc.signals.game.uninstalled.connect(self.__uninstalled)
@ -68,6 +69,8 @@ class AvailableGameDlcWidget(GameDlcWidget):
self.ui.action_button.setObjectName("InstallButton")
self.ui.action_button.clicked.connect(self.install_dlc)
self.ui.action_button.setText(self.tr("Install DLC"))
self.ui.action_button.setIcon(icon("ri.install-line"))
# lk: don't reference `self.rdlc` here because the object has been deleted
rdlc.signals.game.installed.connect(self.__installed)

View file

@ -20,7 +20,7 @@ from rare.models.game import RareGame
from rare.shared import RareCore
from rare.shared.workers import VerifyWorker, MoveWorker
from rare.ui.components.tabs.games.game_info.game_info import Ui_GameInfo
from rare.utils.misc import format_size
from rare.utils.misc import format_size, icon
from rare.widgets.image_widget import ImageWidget, ImageSize
from rare.widgets.side_tab import SideTabContents
from .move_game import MoveGamePopUp, is_game_dir
@ -38,8 +38,18 @@ class GameInfo(QWidget, SideTabContents):
self.ui.setupUi(self)
# lk: set object names for CSS properties
self.ui.install_button.setObjectName("InstallButton")
self.ui.modify_button.setObjectName("InstallButton")
self.ui.uninstall_button.setObjectName("UninstallButton")
self.ui.install_button.setIcon(icon("ri.install-line"))
self.ui.import_button.setIcon(icon("mdi.file-import"))
self.ui.modify_button.setIcon(icon("fa.gear"))
self.ui.verify_button.setIcon(icon("fa.check"))
self.ui.repair_button.setIcon(icon("fa.wrench"))
self.ui.move_button.setIcon(icon("mdi.folder-move"))
self.ui.uninstall_button.setIcon(icon("ri.uninstall-line"))
self.rcore = RareCore.instance()
self.core = RareCore.instance().core()
self.args = RareCore.instance().args()
@ -53,6 +63,7 @@ class GameInfo(QWidget, SideTabContents):
self.ui.install_button.clicked.connect(self.__on_install)
self.ui.import_button.clicked.connect(self.__on_import)
self.ui.modify_button.clicked.connect(self.__on_modify)
self.ui.verify_button.clicked.connect(self.__on_verify)
self.ui.repair_button.clicked.connect(self.__on_repair)
self.ui.uninstall_button.clicked.connect(self.__on_uninstall)
@ -78,7 +89,7 @@ class GameInfo(QWidget, SideTabContents):
"na": self.tr("Not applicable"),
}
# lk: requirements is unused so hide it
# lk: hide unfinished things
self.ui.requirements_group.setVisible(False)
@pyqtSlot()
@ -97,6 +108,11 @@ class GameInfo(QWidget, SideTabContents):
""" This method is to be called from the button only """
self.rgame.uninstall()
@pyqtSlot()
def __on_modify(self):
""" This method is to be called from the button only """
self.rgame.modify()
@pyqtSlot()
def __on_repair(self):
""" This method is to be called from the button only """
@ -286,6 +302,13 @@ class GameInfo(QWidget, SideTabContents):
(not self.rgame.is_installed or self.rgame.is_non_asset) and self.rgame.is_idle
)
self.ui.modify_button.setEnabled(
self.rgame.is_installed
and (not self.rgame.is_non_asset)
and self.rgame.is_idle
and self.rgame.sdl_name is not None
)
self.ui.verify_button.setEnabled(
self.rgame.is_installed and (not self.rgame.is_non_asset) and self.rgame.is_idle
)

View file

@ -1,8 +1,9 @@
import platform
import random
from logging import getLogger
from PyQt5.QtCore import pyqtSignal, Qt, pyqtSlot, QObject, QEvent
from PyQt5.QtGui import QMouseEvent
from PyQt5.QtCore import pyqtSignal, Qt, pyqtSlot, QObject, QEvent, QTimer
from PyQt5.QtGui import QMouseEvent, QShowEvent
from PyQt5.QtWidgets import QMessageBox, QAction
from rare.models.game import RareGame
@ -38,9 +39,6 @@ class GameWidget(LibraryWidget):
self.install_action = QAction(self.tr("Install"), self)
self.install_action.triggered.connect(self._install)
# self.sync_action = QAction(self.tr("Sync with cloud"), self)
# self.sync_action.triggered.connect(self.sync_saves)
self.desktop_link_action = QAction(self)
self.desktop_link_action.triggered.connect(
lambda: self._create_link(self.rgame.folder_name, "desktop")
@ -108,6 +106,13 @@ class GameWidget(LibraryWidget):
# lk: attributes as `GameWidgetUi` class
__slots__ = "ui"
def showEvent(self, a0: QShowEvent) -> None:
if a0.spontaneous():
return super().showEvent(a0)
if self.rgame.pixmap.isNull():
QTimer.singleShot(random.randrange(42, 361, 7), self.rgame.load_pixmap)
super().showEvent(a0)
@pyqtSlot()
def update_state(self):
if self.rgame.is_idle:
@ -144,9 +149,6 @@ class GameWidget(LibraryWidget):
else:
self.addAction(self.install_action)
# if self.rgame.game.supports_cloud_saves:
# self.addAction(self.sync_action)
if desktop_links_supported() and self.rgame.is_installed:
if desktop_link_path(self.rgame.folder_name, "desktop").exists():
self.desktop_link_action.setText(self.tr("Remove Desktop link"))
@ -209,8 +211,6 @@ class GameWidget(LibraryWidget):
def _launch(self, offline=False, skip_version_check=False):
if offline or (self.rgame.is_foreign and self.rgame.can_run_offline):
offline = True
# if self.rgame.game.supports_cloud_saves and not offline:
# self.syncing_cloud_saves = True
if self.rgame.has_update:
skip_version_check = True
self.rgame.launch(
@ -261,21 +261,3 @@ class GameWidget(LibraryWidget):
self.desktop_link_action.setText(self.tr("Create Desktop link"))
elif link_type == "start_menu":
self.menu_link_action.setText(self.tr("Create Start Menu link"))
# def sync_finished(self, app_name):
# self.syncing_cloud_saves = False
# def sync_game(self):
# try:
# sync = self.game_utils.cloud_save_utils.sync_before_launch_game(
# self.rgame.app_name, True
# )
# except Exception:
# sync = False
# if sync:
# self.syncing_cloud_saves = True
# def game_finished(self, app_name, error):
# if error:
# QMessageBox.warning(self, "Error", error)
# self.game_running = False

View file

@ -32,7 +32,10 @@ class ProgressLabel(QLabel):
return super().event(e)
def showEvent(self, a0: QShowEvent) -> None:
if a0.spontaneous():
return super().showEvent(a0)
self.__center_on_parent()
super().showEvent(a0)
def eventFilter(self, a0: QObject, a1: QEvent) -> bool:
if a0 is self.parent() and a1.type() == QEvent.Resize:

View file

@ -36,8 +36,8 @@ class IntegrationsTabs(SideTabWidget):
)
self.ubisoft_group = UbisoftGroup(self.eos_ubisoft)
self.eos_group = EOSGroup(self.eos_ubisoft)
self.eos_ubisoft.addWidget(self.ubisoft_group)
self.eos_ubisoft.addWidget(self.eos_group)
self.eos_ubisoft.addWidget(self.ubisoft_group)
self.eos_ubisoft_index = self.addTab(self.eos_ubisoft, self.tr("Epic Overlay and Ubisoft"))
self.setCurrentIndex(self.import_index)

View file

@ -5,6 +5,7 @@ from logging import getLogger
from typing import Tuple, Iterable, List, Union
from PyQt5.QtCore import Qt, QThreadPool, QRunnable, pyqtSlot, pyqtSignal
from PyQt5.QtGui import QShowEvent
from PyQt5.QtWidgets import QGroupBox, QListWidgetItem, QFileDialog, QMessageBox, QFrame, QLabel
from legendary.models.egl import EGLManifest
from legendary.models.game import InstalledGame
@ -15,8 +16,8 @@ from rare.shared import RareCore
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.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
from rare.widgets.elide_label import ElideLabel
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
logger = getLogger("EGLSync")
@ -28,8 +29,20 @@ class EGLSyncGroup(QGroupBox):
self.ui.setupUi(self)
self.core = RareCore.instance().core()
self.egl_path_edit = PathEdit(
path=self.core.egl.programdata_path,
placeholder=self.tr(
"Path to the Wine prefix where EGL is installed, or the Manifests folder"
),
file_mode=QFileDialog.DirectoryOnly,
edit_func=self.egl_path_edit_edit_cb,
save_func=self.egl_path_edit_save_cb,
parent=self,
)
self.ui.egl_path_edit_layout.addWidget(self.egl_path_edit)
self.egl_path_info_label = QLabel(self.tr("Estimated path"), self)
self.egl_path_info = ElideLabel("", parent=self)
self.egl_path_info = ElideLabel(parent=self)
self.egl_path_info.setProperty("infoLabel", 1)
self.ui.egl_sync_layout.insertRow(
self.ui.egl_sync_layout.indexOf(self.ui.egl_path_edit_label) + 1,
@ -37,33 +50,15 @@ class EGLSyncGroup(QGroupBox):
)
if platform.system() == "Windows":
self.ui.egl_path_edit_label.setVisible(False)
self.egl_path_info_label.setVisible(False)
self.egl_path_info.setVisible(False)
self.ui.egl_path_edit_label.setEnabled(False)
self.egl_path_edit.setEnabled(False)
self.egl_path_info_label.setEnabled(False)
self.egl_path_info.setEnabled(False)
else:
self.egl_path_edit = PathEdit(
path=self.core.egl.programdata_path,
placeholder=self.tr(
"Path to the Wine prefix where EGL is installed, or the Manifests folder"
),
file_mode=QFileDialog.DirectoryOnly,
edit_func=self.egl_path_edit_edit_cb,
save_func=self.egl_path_edit_save_cb,
parent=self,
)
self.egl_path_edit.textChanged.connect(self.egl_path_changed)
self.ui.egl_path_edit_layout.addWidget(self.egl_path_edit)
if not self.core.egl.programdata_path:
self.egl_path_info.setText(self.tr("Updating..."))
wine_resolver = WineResolver(
self.core, PathSpec.egl_programdata, "default"
)
wine_resolver.signals.result_ready.connect(self.wine_resolver_cb)
QThreadPool.globalInstance().start(wine_resolver)
else:
self.egl_path_info_label.setVisible(False)
self.egl_path_info.setVisible(False)
if self.core.egl.programdata_path:
self.egl_path_info_label.setEnabled(True)
self.egl_path_info.setEnabled(True)
self.ui.egl_sync_check.setChecked(self.core.egl_sync_enabled)
self.ui.egl_sync_check.stateChanged.connect(self.egl_sync_changed)
@ -76,9 +71,25 @@ class EGLSyncGroup(QGroupBox):
# self.egl_watcher = QFileSystemWatcher([self.egl_path_edit.text()], self)
# self.egl_watcher.directoryChanged.connect(self.update_lists)
def showEvent(self, a0: QShowEvent) -> None:
if a0.spontaneous():
return super().showEvent(a0)
if not self.core.egl.programdata_path:
self.__run_wine_resolver()
self.update_lists()
super().showEvent(a0)
def wine_resolver_cb(self, path):
def __run_wine_resolver(self):
self.egl_path_info.setText(self.tr("Updating..."))
wine_resolver = WineResolver(
self.core,
PathSpec.egl_programdata,
"default"
)
wine_resolver.signals.result_ready.connect(self.__on_wine_resolver_result)
QThreadPool.globalInstance().start(wine_resolver)
def __on_wine_resolver_result(self, path):
self.egl_path_info.setText(path)
if not path:
self.egl_path_info.setText(
@ -162,9 +173,13 @@ class EGLSyncGroup(QGroupBox):
def update_lists(self):
# self.egl_watcher.blockSignals(True)
if have_path := bool(self.core.egl.programdata_path) and os.path.exists(
self.core.egl.programdata_path
):
have_path = False
if self.core.egl.programdata_path:
have_path = os.path.exists(self.core.egl.programdata_path)
if not have_path and os.path.isdir(os.path.dirname(self.core.egl.programdata_path)):
# NOTE: This might happen if EGL is installed but no games have been installed through it
os.mkdir(self.core.egl.programdata_path)
have_path = os.path.isdir(self.core.egl.programdata_path)
# NOTE: need to clear known manifests to force refresh
self.core.egl.manifests.clear()
self.ui.egl_sync_check_label.setEnabled(have_path)

View file

@ -1,14 +1,28 @@
import time
import webbrowser
from logging import getLogger
from typing import Optional
from PyQt5.QtCore import QObject, pyqtSignal, QThreadPool, QSize
from PyQt5.QtWidgets import QWidget, QLabel, QHBoxLayout, QSizePolicy, QPushButton, QGroupBox, QVBoxLayout
from PyQt5.QtCore import QObject, pyqtSignal, QThreadPool, QSize, pyqtSlot, Qt
from PyQt5.QtGui import QShowEvent
from PyQt5.QtWidgets import (
QFrame,
QLabel,
QHBoxLayout,
QSizePolicy,
QPushButton,
QGroupBox,
QVBoxLayout,
)
from legendary.models.game import Game
from rare.shared import LegendaryCoreSingleton, ArgumentsSingleton
from rare.lgndr.core import LegendaryCore
from rare.shared import RareCore
from rare.shared.workers.worker import Worker
from rare.utils.metrics import timelogger
from rare.utils.misc import icon
from rare.widgets.elide_label import ElideLabel
from rare.widgets.loading_widget import LoadingWidget
logger = getLogger("Ubisoft")
@ -17,15 +31,16 @@ class UbiGetInfoWorker(Worker):
class Signals(QObject):
worker_finished = pyqtSignal(set, set, str)
def __init__(self):
def __init__(self, core: LegendaryCore):
super(UbiGetInfoWorker, self).__init__()
self.signals = UbiGetInfoWorker.Signals()
self.setAutoDelete(True)
self.core = LegendaryCoreSingleton()
self.core = core
def run_real(self) -> None:
try:
external_auths = self.core.egs.get_external_auths()
with timelogger(logger, "Request external auths"):
external_auths = self.core.egs.get_external_auths()
for ext_auth in external_auths:
if ext_auth["type"] != "ubisoft":
continue
@ -35,12 +50,17 @@ class UbiGetInfoWorker(Worker):
self.signals.worker_finished.emit(set(), set(), "")
return
uplay_keys = self.core.egs.store_get_uplay_codes()
with timelogger(logger, "Request uplay codes"):
uplay_keys = self.core.egs.store_get_uplay_codes()
key_list = uplay_keys["data"]["PartnerIntegration"]["accountUplayCodes"]
redeemed = {k["gameId"] for k in key_list if k["redeemedOnUplay"]}
entitlements = self.core.egs.get_user_entitlements()
if (entitlements := self.core.lgd.entitlements) is None:
with timelogger(logger, "Request entitlements"):
entitlements = self.core.egs.get_user_entitlements()
self.core.lgd.entitlements = entitlements
entitlements = {i["entitlementName"] for i in entitlements}
self.signals.worker_finished.emit(redeemed, entitlements, ubi_account_id)
except Exception as e:
logger.error(str(e))
@ -51,11 +71,11 @@ class UbiConnectWorker(Worker):
class Signals(QObject):
linked = pyqtSignal(str)
def __init__(self, ubi_account_id, partner_link_id):
def __init__(self, core: LegendaryCore, ubi_account_id, partner_link_id):
super(UbiConnectWorker, self).__init__()
self.signals = UbiConnectWorker.Signals()
self.setAutoDelete(True)
self.core = LegendaryCoreSingleton()
self.core = core
self.ubi_account_id = ubi_account_id
self.partner_link_id = partner_link_id
@ -65,9 +85,7 @@ class UbiConnectWorker(Worker):
self.signals.linked.emit("")
return
try:
self.core.egs.store_claim_uplay_code(
self.ubi_account_id, self.partner_link_id
)
self.core.egs.store_claim_uplay_code(self.ubi_account_id, self.partner_link_id)
self.core.egs.store_redeem_uplay_codes(self.ubi_account_id)
except Exception as e:
self.signals.linked.emit(str(e))
@ -76,55 +94,58 @@ class UbiConnectWorker(Worker):
self.signals.linked.emit("")
class UbiLinkWidget(QWidget):
def __init__(self, game: Game, ubi_account_id):
super(UbiLinkWidget, self).__init__()
self.args = ArgumentsSingleton()
layout = QHBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
class UbiLinkWidget(QFrame):
def __init__(self, game: Game, ubi_account_id, activated: bool = False, parent=None):
super(UbiLinkWidget, self).__init__(parent=parent)
self.setFrameShape(QFrame.StyledPanel)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.args = RareCore.instance().args()
self.game = game
self.ubi_account_id = ubi_account_id
self.title_label = QLabel(game.app_title)
layout.addWidget(self.title_label, stretch=1)
self.ok_indicator = QLabel()
self.ok_indicator.setPixmap(icon("fa.info-circle", color="grey").pixmap(20, 20))
self.ok_indicator = QLabel(parent=self)
self.ok_indicator.setPixmap(icon("fa.circle-o", color="grey").pixmap(20, 20))
self.ok_indicator.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Preferred)
layout.addWidget(self.ok_indicator)
self.link_button = QPushButton(
self.tr("Redeem to Ubisoft") + ": Test" if self.args.debug else ""
)
layout.addWidget(self.link_button)
self.title_label = ElideLabel(game.app_title, parent=self)
self.link_button = QPushButton(self.tr("Redeem in Ubisoft"), parent=self)
self.link_button.setMinimumWidth(150)
self.link_button.clicked.connect(self.activate)
self.setLayout(layout)
if activated:
self.link_button.setText(self.tr("Already activated"))
self.link_button.setDisabled(True)
self.ok_indicator.setPixmap(icon("fa.check-circle-o", color="green").pixmap(QSize(20, 20)))
layout = QHBoxLayout(self)
layout.setContentsMargins(-1, 0, 0, 0)
layout.addWidget(self.ok_indicator)
layout.addWidget(self.title_label, stretch=1)
layout.addWidget(self.link_button)
def activate(self):
self.link_button.setDisabled(True)
# self.ok_indicator.setPixmap(icon("mdi.loading", color="grey").pixmap(20, 20))
self.ok_indicator.setPixmap(
icon("mdi.transit-connection-horizontal", color="grey").pixmap(20, 20)
)
self.ok_indicator.setPixmap(icon("mdi.transit-connection-horizontal", color="grey").pixmap(20, 20))
if self.args.debug:
worker = UbiConnectWorker(None, None)
worker = UbiConnectWorker(RareCore.instance().core(), None, None)
else:
worker = UbiConnectWorker(self.ubi_account_id, self.game.partner_link_id)
worker = UbiConnectWorker(
RareCore.instance().core(), self.ubi_account_id, self.game.partner_link_id
)
worker.signals.linked.connect(self.worker_finished)
QThreadPool.globalInstance().start(worker)
def worker_finished(self, error):
if not error:
self.ok_indicator.setPixmap(
icon("ei.ok-circle", color="green").pixmap(QSize(20, 20))
)
self.ok_indicator.setPixmap(icon("fa.check-circle-o", color="green").pixmap(QSize(20, 20)))
self.link_button.setDisabled(True)
self.link_button.setText(self.tr("Already activated"))
else:
self.ok_indicator.setPixmap(
icon("fa.info-circle", color="red").pixmap(QSize(20, 20))
)
self.ok_indicator.setPixmap(icon("fa.times-circle-o", color="red").pixmap(QSize(20, 20)))
self.ok_indicator.setToolTip(error)
self.link_button.setText(self.tr("Try again"))
self.link_button.setDisabled(False)
@ -134,86 +155,120 @@ class UbisoftGroup(QGroupBox):
def __init__(self, parent=None):
super(UbisoftGroup, self).__init__(parent=parent)
self.setTitle(self.tr("Link Ubisoft Games"))
self.setLayout(QVBoxLayout())
self.core = LegendaryCoreSingleton()
self.args = ArgumentsSingleton()
self.rcore = RareCore.instance()
self.core = RareCore.instance().core()
self.args = RareCore.instance().args()
self.thread_pool = QThreadPool.globalInstance()
worker = UbiGetInfoWorker()
worker.signals.worker_finished.connect(self.show_ubi_games)
self.thread_pool.start(worker)
self.worker: Optional[UbiGetInfoWorker] = None
self.info_label = QLabel(parent=self)
self.info_label.setText(self.tr("Getting information about your redeemable Ubisoft games."))
self.browser_button = QPushButton(self.tr("Link Ubisoft acccount"), parent=self)
self.browser_button.setMinimumWidth(140)
self.browser_button.clicked.connect(
lambda: webbrowser.open("https://www.epicgames.com/id/link/ubisoft")
)
self.browser_button.setEnabled(False)
self.loading_widget = LoadingWidget(self)
self.loading_widget.stop()
header_layout = QHBoxLayout()
header_layout.addWidget(self.info_label, stretch=1)
header_layout.addWidget(self.browser_button)
layout = QVBoxLayout(self)
layout.addLayout(header_layout)
layout.addWidget(self.loading_widget)
def showEvent(self, a0: QShowEvent) -> None:
if a0.spontaneous():
return super().showEvent(a0)
if self.worker is not None:
return
for widget in self.findChildren(UbiLinkWidget, options=Qt.FindDirectChildrenOnly):
widget.deleteLater()
self.loading_widget.start()
self.worker = UbiGetInfoWorker(self.core)
self.worker.signals.worker_finished.connect(self.show_ubi_games)
self.thread_pool.start(self.worker)
super().showEvent(a0)
@pyqtSlot(set, set, str)
def show_ubi_games(self, redeemed: set, entitlements: set, ubi_account_id: str):
self.worker = None
if not redeemed and ubi_account_id != "error":
logger.error(
"No linked ubisoft account found! Link your accounts via your browser and try again."
)
self.layout().addWidget(
QLabel(
self.tr(
"Your account is not linked with Ubisoft. Please link your account first"
)
)
self.info_label.setText(
self.tr("Your account is not linked with Ubisoft. Please link your account and try again.")
)
open_browser_button = QPushButton(self.tr("Open link page"))
open_browser_button.clicked.connect(
lambda: webbrowser.open("https://www.epicgames.com/id/link/ubisoft")
)
self.layout().addWidget(open_browser_button)
return
self.browser_button.setEnabled(True)
elif ubi_account_id == "error":
self.layout().addWidget(QLabel(self.tr("An error occurred")))
return
self.info_label.setText(
self.tr("An error has occurred while requesting your account's Ubisoft information.")
)
self.browser_button.setEnabled(True)
else:
self.browser_button.setEnabled(False)
games = self.core.get_game_list(False)
uplay_games = []
activated = 0
for game in games:
for dlc_data in game.metadata.get("dlcItemList", []):
if dlc_data["entitlementName"] not in entitlements:
uplay_games = []
activated = 0
for rgame in self.rcore.ubisoft_games:
game = rgame.game
for dlc_data in game.metadata.get("dlcItemList", []):
if dlc_data["entitlementName"] not in entitlements:
continue
try:
app_name = dlc_data["releaseInfo"][0]["appId"]
except (IndexError, KeyError):
app_name = "unknown"
dlc_game = Game(app_name=app_name, app_title=dlc_data["title"], metadata=dlc_data)
if dlc_game.partner_link_type != "ubisoft":
continue
if dlc_game.partner_link_id in redeemed:
continue
uplay_games.append(dlc_game)
if game.partner_link_type != "ubisoft":
continue
if game.partner_link_id in redeemed:
activated += 1
uplay_games.append(game)
try:
app_name = dlc_data["releaseInfo"][0]["appId"]
except (IndexError, KeyError):
app_name = "unknown"
dlc_game = Game(
app_name=app_name, app_title=dlc_data["title"], metadata=dlc_data
)
if dlc_game.partner_link_type != "ubisoft":
continue
if dlc_game.partner_link_id in redeemed:
continue
uplay_games.append(dlc_game)
if game.partner_link_type != "ubisoft":
continue
if game.partner_link_id in redeemed:
activated += 1
continue
uplay_games.append(game)
if not uplay_games:
if activated >= 1:
self.layout().addWidget(
QLabel(
self.tr("All your Ubisoft games have already been activated")
)
)
if not uplay_games:
self.info_label.setText(self.tr("You don't own any Ubisoft games."))
else:
self.layout().addWidget(
QLabel(self.tr("You don't own any Ubisoft games"))
)
if self.args.debug:
if activated == len(uplay_games):
self.info_label.setText(self.tr("All your Ubisoft games have already been activated."))
else:
self.info_label.setText(
self.tr("You have <b>{}</b> games available to redeem.").format(
len(uplay_games) - activated
)
)
logger.info(f"Found {len(uplay_games) - activated} game(s) to redeem.")
self.loading_widget.stop()
for game in uplay_games:
widget = UbiLinkWidget(
Game(app_name="Test", app_title="This is a test game"),
ubi_account_id,
game, ubi_account_id, activated=game.partner_link_id in redeemed, parent=self
)
self.layout().addWidget(widget)
return
logger.info(f"Found {len(uplay_games)} game(s) to redeem")
for game in uplay_games:
widget = UbiLinkWidget(game, ubi_account_id)
self.layout().addWidget(widget)
if self.args.debug:
widget = UbiLinkWidget(
Game(app_name="RareTestGame", app_title="Super Fake Super Rare Super Test (Super?) Game"),
ubi_account_id,
activated=False,
parent=self,
)
self.layout().addWidget(widget)
self.browser_button.setEnabled(True)

View file

@ -14,6 +14,7 @@ class DxvkSettings(OverlaySettings):
("devinfo", QCoreApplication.translate("DxvkSettings", "Show Device info")),
("version", QCoreApplication.translate("DxvkSettings", "DXVK Version")),
("api", QCoreApplication.translate("DxvkSettings", "D3D feature level")),
("compiler", QCoreApplication.translate("DxvkSettings", "Compiler activity")),
],
[
(CustomOption.number_input("scale", 1, True), QCoreApplication.translate("DxvkSettings", "Scale"))

View file

@ -3,10 +3,11 @@ import sys
from collections import ChainMap
from typing import Any, Union
from rare.utils.misc import icon
from PyQt5.QtCore import Qt, QModelIndex, QAbstractTableModel, pyqtSlot
from PyQt5.QtGui import QFont
from rare.lgndr.core import LegendaryCore
from rare.utils.misc import icon
class EnvVarsTableModel(QAbstractTableModel):
@ -54,7 +55,6 @@ class EnvVarsTableModel(QAbstractTableModel):
try:
return list(self.__data_map)[index]
except Exception as e:
print(e, index)
return ""
def __is_local(self, index: Union[QModelIndex, int]):
@ -220,7 +220,6 @@ class EnvVarsTableModel(QAbstractTableModel):
self.core.lgd.save_config()
self.dataChanged.emit(self.index(index.row(), 0), index, [])
self.headerDataChanged.emit(Qt.Vertical, index.row(), index.row())
# pprint([item for item in self.__data_map.items()])
return True
def removeRow(self, row: int, parent: QModelIndex = None) -> bool:
@ -244,7 +243,6 @@ class EnvVarsTableModel(QAbstractTableModel):
del self.__data_map[self.__key(row)]
self.core.lgd.save_config()
self.endRemoveRows()
# pprint([item for item in self.__data_map.items()])
return True

View file

@ -10,7 +10,7 @@ from rare.shared import LegendaryCoreSingleton
from rare.ui.components.tabs.settings.widgets.overlay import Ui_OverlaySettings
from rare.utils import config_helper
logger = getLogger("Overlay")
logger = getLogger("GameOverlays")
class TextInputField(QLineEdit):

View file

@ -124,7 +124,6 @@ class ProtonSettings(QGroupBox):
proton = proton.replace('"', "")
self.proton_prefix.setEnabled(bool(proton))
if proton:
print(proton)
self.ui.proton_combo.setCurrentText(
f'"{proton.replace(" run", "")}" run'
)

View file

@ -1,20 +1,22 @@
import re
import shutil
from logging import getLogger
from typing import Dict, List
from typing import Dict, Optional
from PyQt5.QtCore import pyqtSignal, QSettings, QSize, Qt, QMimeData, pyqtSlot, QCoreApplication
from PyQt5.QtGui import QDrag, QDropEvent, QDragEnterEvent, QDragMoveEvent, QFont
from PyQt5.QtGui import QDrag, QDropEvent, QDragEnterEvent, QDragMoveEvent, QFont, QMouseEvent
from PyQt5.QtWidgets import (
QHBoxLayout,
QLabel,
QPushButton,
QInputDialog,
QFrame,
QMessageBox,
QSizePolicy,
QWidget,
QScrollArea,
QAction,
QToolButton,
QMenu,
)
from rare.shared import RareCore
@ -30,60 +32,97 @@ extra_wrapper_regex = {
}
class Wrapper:
pass
class WrapperWidget(QFrame):
update_wrapper = pyqtSignal(str, str)
delete_wrapper = pyqtSignal(str)
def __init__(self, text: str, show_text=None, parent=None):
super(WrapperWidget, self).__init__(parent=parent)
if not show_text:
show_text = text
show_text = text.split()[0]
self.setFrameShape(QFrame.StyledPanel)
self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
self.text = text
self.text_lbl = QLabel(show_text, parent=self)
self.text_lbl.setFont(QFont("monospace"))
self.image_lbl = QLabel(parent=self)
self.image_lbl.setPixmap(icon("mdi.drag-vertical").pixmap(QSize(20, 20)))
self.setToolTip(text)
self.delete_button = QPushButton(icon("ei.remove"), "", parent=self)
if show_text in extra_wrapper_regex.keys():
self.delete_button.setDisabled(True)
self.delete_button.setToolTip(self.tr("Disable it in settings"))
self.delete_button.clicked.connect(self.delete)
unmanaged = show_text in extra_wrapper_regex.keys()
layout = QHBoxLayout()
text_lbl = QLabel(show_text, parent=self)
text_lbl.setFont(QFont("monospace"))
text_lbl.setDisabled(unmanaged)
image_lbl = QLabel(parent=self)
image_lbl.setPixmap(icon("mdi.drag-vertical").pixmap(QSize(20, 20)))
edit_action = QAction("Edit", parent=self)
edit_action.triggered.connect(self.__edit)
delete_action = QAction("Delete", parent=self)
delete_action.triggered.connect(self.__delete)
manage_menu = QMenu(parent=self)
manage_menu.addActions([edit_action, delete_action])
manage_button = QToolButton(parent=self)
manage_button.setIcon(icon("mdi.menu"))
manage_button.setMenu(manage_menu)
manage_button.setPopupMode(QToolButton.InstantPopup)
manage_button.setDisabled(unmanaged)
if unmanaged:
manage_button.setToolTip(self.tr("Manage through settings"))
else:
manage_button.setToolTip(self.tr("Manage"))
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self.image_lbl)
layout.addWidget(self.text_lbl)
layout.addWidget(self.delete_button)
layout.addWidget(image_lbl)
layout.addWidget(text_lbl)
layout.addWidget(manage_button)
self.setLayout(layout)
# lk: set object names for the stylesheet
self.setObjectName(type(self).__name__)
self.delete_button.setObjectName(f"{self.objectName()}Button")
manage_button.setObjectName(f"{self.objectName()}Button")
def delete(self):
@pyqtSlot()
def __delete(self):
self.delete_wrapper.emit(self.text)
def mouseMoveEvent(self, e):
if e.buttons() == Qt.LeftButton:
def __edit(self) -> None:
dialog = QInputDialog(self)
dialog.setWindowTitle(f"{self.tr('Edit wrapper')} - {QCoreApplication.instance().applicationName()}")
dialog.setLabelText(self.tr("Edit wrapper command"))
dialog.setTextValue(self.text)
accepted = dialog.exec()
wrapper = dialog.textValue()
dialog.deleteLater()
if accepted and wrapper:
self.update_wrapper.emit(self.text, wrapper)
def mouseMoveEvent(self, a0: QMouseEvent) -> None:
if a0.buttons() == Qt.LeftButton:
a0.accept()
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
drag.exec_(Qt.MoveAction)
class WrapperSettings(QWidget, Ui_WrapperSettings):
class WrapperSettings(QWidget):
def __init__(self):
super(WrapperSettings, self).__init__()
self.setupUi(self)
self.ui = Ui_WrapperSettings()
self.ui.setupUi(self)
self.wrappers: Dict[str, WrapperWidget] = {}
self.app_name: str
self.app_name: str = "default"
self.wrapper_scroll = QScrollArea(self.widget_stack)
self.wrapper_scroll = QScrollArea(self.ui.widget_stack)
self.wrapper_scroll.setWidgetResizable(True)
self.wrapper_scroll.setSizeAdjustPolicy(QScrollArea.AdjustToContents)
self.wrapper_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
@ -92,18 +131,18 @@ class WrapperSettings(QWidget, Ui_WrapperSettings):
save_cb=self.save, parent=self.wrapper_scroll
)
self.wrapper_scroll.setWidget(self.scroll_content)
self.widget_stack.insertWidget(0, self.wrapper_scroll)
self.ui.widget_stack.insertWidget(0, self.wrapper_scroll)
self.core = RareCore.instance().core()
self.add_button.clicked.connect(self.add_button_pressed)
self.ui.add_button.clicked.connect(self.add_button_pressed)
self.settings = QSettings()
self.wrapper_scroll.horizontalScrollBar().rangeChanged.connect(self.adjust_scrollarea)
# lk: set object names for the stylesheet
self.setObjectName(type(self).__name__)
self.no_wrapper_label.setObjectName(f"{self.objectName()}Label")
self.ui.no_wrapper_label.setObjectName(f"{self.objectName()}Label")
self.wrapper_scroll.setObjectName(f"{self.objectName()}Scroll")
self.wrapper_scroll.horizontalScrollBar().setObjectName(
f"{self.wrapper_scroll.objectName()}Bar")
@ -135,78 +174,104 @@ class WrapperSettings(QWidget, Ui_WrapperSettings):
return " ".join(self.get_wrapper_list())
def get_wrapper_list(self):
data: List[str] = []
for w in self.wrappers.values():
# Get the widget at each index in turn.
try:
data.append(w.text)
except AttributeError:
pass
return data
wrappers = list(self.wrappers.values())
wrappers.sort(key=lambda x: self.scroll_content.layout().indexOf(x))
return [w.text for w in wrappers]
def add_button_pressed(self):
header = self.tr("Add wrapper")
wrapper, done = QInputDialog.getText(
self, f"{header} - {QCoreApplication.instance().applicationName()}", self.tr("Insert wrapper executable")
)
if not done:
return
self.add_wrapper(wrapper)
dialog = QInputDialog(self)
dialog.setWindowTitle(f"{self.tr('Add wrapper')} - {QCoreApplication.instance().applicationName()}")
dialog.setLabelText(self.tr("Enter wrapper command"))
accepted = dialog.exec()
wrapper = dialog.textValue()
dialog.deleteLater()
if accepted:
self.add_wrapper(wrapper)
def add_wrapper(self, text: str, from_load=False):
def add_wrapper(self, text: str, position: int = -1, from_load: bool = False):
if text == "mangohud" and self.wrappers.get("mangohud"):
return
show_text = text
show_text = ""
for key, extra_wrapper in extra_wrapper_regex.items():
if re.match(extra_wrapper, text):
show_text = key
if not show_text:
show_text = text.split()[0]
# validate
if not text.strip(): # is empty
return
if not from_load:
if self.wrappers.get(text):
QMessageBox.warning(self, "Warning", self.tr("Wrapper is already in the list"))
QMessageBox.warning(
self, self.tr("Warning"), self.tr("Wrapper <b>{0}</b> is already in the list").format(text)
)
return
if show_text != "proton" and not shutil.which(text.split()[0]):
if QMessageBox.question(self, "Warning", self.tr("Wrapper is not in $PATH. Ignore? "),
QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.No:
if (
QMessageBox.question(
self,
self.tr("Warning"),
self.tr("Wrapper <b>{0}</b> is not in $PATH. Add it anyway?").format(show_text),
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No,
)
== QMessageBox.No
):
return
if text == "proton":
QMessageBox.warning(self, "Warning", self.tr("Do not insert proton manually. Add it in proton settings"))
QMessageBox.warning(
self,
self.tr("Warning"),
self.tr("Do not insert <b>proton</b> manually. Add it through Proton settings"),
)
return
self.widget_stack.setCurrentIndex(0)
self.ui.widget_stack.setCurrentIndex(0)
if widget := self.wrappers.get(show_text, None):
widget.deleteLater()
widget = WrapperWidget(text, show_text, self.scroll_content)
self.scroll_content.layout().addWidget(widget)
if position < 0:
self.scroll_content.layout().addWidget(widget)
else:
self.scroll_content.layout().insertWidget(position, widget)
self.adjust_scrollarea(
self.wrapper_scroll.horizontalScrollBar().minimum(),
self.wrapper_scroll.horizontalScrollBar().maximum()
self.wrapper_scroll.horizontalScrollBar().maximum(),
)
widget.update_wrapper.connect(self.update_wrapper)
widget.delete_wrapper.connect(self.delete_wrapper)
self.wrappers[show_text] = widget
if not from_load:
self.save()
@pyqtSlot(str)
def delete_wrapper(self, text: str):
text = text.split()[0]
widget = self.wrappers.get(text, None)
if widget:
self.wrappers.pop(text)
widget.deleteLater()
if not self.wrappers:
self.wrapper_scroll.setMaximumHeight(self.label_page.sizeHint().height())
self.widget_stack.setCurrentIndex(1)
self.wrapper_scroll.setMaximumHeight(self.ui.label_page.sizeHint().height())
self.ui.widget_stack.setCurrentIndex(1)
self.save()
@pyqtSlot(str, str)
def update_wrapper(self, old: str, new: str):
key = old.split()[0]
idx = self.scroll_content.layout().indexOf(self.wrappers[key])
self.delete_wrapper(key)
self.add_wrapper(new, position=idx)
def save(self):
# save wrappers twice, to support wrappers with spaces
if len(self.wrappers) == 0:
@ -231,19 +296,18 @@ class WrapperSettings(QWidget, Ui_WrapperSettings):
wrappers = pattern.split(cfg)[1::2]
for wrapper in wrappers:
self.add_wrapper(wrapper, True)
self.add_wrapper(wrapper, from_load=True)
if not self.wrappers:
self.wrapper_scroll.setMaximumHeight(self.label_page.sizeHint().height())
self.widget_stack.setCurrentIndex(1)
self.wrapper_scroll.setMaximumHeight(self.ui.label_page.sizeHint().height())
self.ui.widget_stack.setCurrentIndex(1)
else:
self.widget_stack.setCurrentIndex(0)
self.ui.widget_stack.setCurrentIndex(0)
self.save()
class WrapperContainer(QWidget):
drag_widget: QWidget
def __init__(self, save_cb, parent=None):
super(WrapperContainer, self).__init__(parent=parent)
@ -254,6 +318,8 @@ class WrapperContainer(QWidget):
layout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
self.setLayout(layout)
self.drag_widget: Optional[QWidget] = None
# lk: set object names for the stylesheet
self.setObjectName(type(self).__name__)

View file

@ -10,19 +10,19 @@ from logging import getLogger
from signal import signal, SIGINT, SIGTERM, strsignal
from typing import Union, Optional
from PyQt5.QtCore import QObject, QProcess, pyqtSignal, QUrl, QRunnable, QThreadPool, QSettings, Qt
from PyQt5.QtCore import QObject, QProcess, pyqtSignal, QUrl, QRunnable, QThreadPool, QSettings, Qt, pyqtSlot
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtNetwork import QLocalServer, QLocalSocket
from PyQt5.QtWidgets import QApplication
from legendary.models.game import SaveGameStatus
from rare.components.dialogs.cloud_save_dialog import CloudSaveDialog
from rare.lgndr.core import LegendaryCore
from rare.models.base_game import RareGameSlim
from rare.models.launcher import ErrorModel, Actions, FinishedModel, BaseModel, StateChangedModel
from rare.widgets.rare_app import RareApp, RareAppException
from .console import Console
from .lgd_helper import get_launch_args, InitArgs, get_configured_process, LaunchArgs, GameArgsError
from ..models.base_game import RareGameSlim
from rare.components.dialogs.cloud_save_dialog import CloudSaveDialog
logger = logging.getLogger("RareLauncher")
@ -41,7 +41,6 @@ class PreLaunchThread(QRunnable):
def __init__(self, core: LegendaryCore, args: InitArgs, rgame: RareGameSlim, sync_action=None):
super(PreLaunchThread, self).__init__()
self.core = core
self.app_name = args.app_name
self.signals = self.Signals()
self.args = args
self.rgame = rgame
@ -119,45 +118,30 @@ class RareLauncherException(RareAppException):
class RareLauncher(RareApp):
game_process: QProcess
server: QLocalServer
socket: Optional[QLocalSocket] = None
exit_app = pyqtSignal()
console: Optional[Console] = None
success: bool = True
def __init__(self, args: InitArgs):
log_file = f"Rare_Launcher_{args.app_name}" + "_{0}.log"
super(RareLauncher, self).__init__(args, log_file)
self._hook.deleteLater()
self._hook = RareLauncherException(self, args, self)
self.game_process = QProcess()
self.app_name = args.app_name
self.logger = getLogger(self.app_name)
self.core = LegendaryCore()
self.args = args
self.logger = getLogger(f"Launcher_{args.app_name}")
self.success: bool = True
self.no_sync_on_exit = False
game = self.core.get_game(self.app_name)
self.rgame = RareGameSlim(self.core, game)
self.args = args
self.core = LegendaryCore()
self.rgame = RareGameSlim(self.core, self.core.get_game(args.app_name))
lang = self.settings.value("language", self.core.language_code, type=str)
self.load_translator(lang)
self.console: Optional[Console] = None
if QSettings().value("show_console", False, bool):
self.console = Console()
self.console.show()
self.server = QLocalServer()
ret = self.server.listen(f"rare_{self.app_name}")
if not ret:
self.logger.error(self.server.errorString())
self.logger.info("Server is running")
self.server.close()
self.success = False
return
self.server.newConnection.connect(self.new_server_connection)
self.game_process: QProcess = QProcess(self)
self.game_process.finished.connect(self.game_finished)
self.game_process.errorOccurred.connect(
lambda err: self.error_occurred(self.game_process.errorString()))
@ -172,11 +156,30 @@ class RareLauncher(RareApp):
self.game_process.readAllStandardError().data().decode("utf-8", "ignore")
)
)
self.console.term.connect(lambda: self.game_process.terminate())
self.console.kill.connect(lambda: self.game_process.kill())
self.console.term.connect(self.__proc_term)
self.console.kill.connect(self.__proc_kill)
self.socket: Optional[QLocalSocket] = None
self.server: QLocalServer = QLocalServer(self)
ret = self.server.listen(f"rare_{args.app_name}")
if not ret:
self.logger.error(self.server.errorString())
self.logger.info("Server is running")
self.server.close()
self.success = False
return
self.server.newConnection.connect(self.new_server_connection)
self.start_time = time.time()
@pyqtSlot()
def __proc_term(self):
self.game_process.terminate()
@pyqtSlot()
def __proc_kill(self):
self.game_process.kill()
def new_server_connection(self):
if self.socket is not None:
try:
@ -221,7 +224,6 @@ class RareLauncher(RareApp):
else:
self.on_exit(exit_code)
def game_finished(self, exit_code):
self.logger.info("Game finished")
@ -232,12 +234,12 @@ class RareLauncher(RareApp):
def on_exit(self, exit_code: int):
if self.console:
self.console.on_process_exit(self.core.get_game(self.app_name).app_title, exit_code)
self.console.on_process_exit(self.core.get_game(self.rgame.app_name).app_title, exit_code)
self.send_message(
FinishedModel(
action=Actions.finished,
app_name=self.app_name,
app_name=self.rgame.app_name,
exit_code=exit_code,
playtime=int(time.time() - self.start_time)
)
@ -265,11 +267,11 @@ class RareLauncher(RareApp):
# send start message after process started
self.game_process.started.connect(lambda: self.send_message(
StateChangedModel(
action=Actions.state_update, app_name=self.app_name,
action=Actions.state_update, app_name=self.rgame.app_name,
new_state=StateChangedModel.States.started
)
))
if self.app_name in DETACHED_APP_NAMES and platform.system() == "Windows":
if self.rgame.app_name in DETACHED_APP_NAMES and platform.system() == "Windows":
self.game_process.deleteLater()
subprocess.Popen([args.executable] + args.args, cwd=args.cwd,
env={i: args.env.value(i) for i in args.env.keys()})
@ -281,7 +283,7 @@ class RareLauncher(RareApp):
logger.info("Dry run activated")
if self.console:
self.console.log(f"{args.executable} {' '.join(args.args)}")
self.console.log(f"Do not start {self.app_name}")
self.console.log(f"Do not start {self.rgame.app_name}")
self.console.accept_close = True
print(args.executable, " ".join(args.args))
self.stop()
@ -291,9 +293,9 @@ class RareLauncher(RareApp):
def error_occurred(self, error_str: str):
self.logger.warning(error_str)
if self.console:
self.console.on_process_exit(self.core.get_game(self.app_name).app_title, error_str)
self.console.on_process_exit(self.core.get_game(self.rgame.app_name).app_title, error_str)
self.send_message(ErrorModel(
error_string=error_str, app_name=self.app_name,
error_string=error_str, app_name=self.rgame.app_name,
action=Actions.error)
)
self.stop()
@ -364,7 +366,7 @@ class RareLauncher(RareApp):
if not self.console:
self.exit()
else:
self.console.on_process_exit(self.app_name, 0)
self.console.on_process_exit(self.rgame.app_name, 0)
def start_game(args: Namespace):

View file

@ -84,7 +84,7 @@ def get_game_params(core: LegendaryCore, igame: InstalledGame, args: InitArgs,
launch_args: LaunchArgs) -> LaunchArgs:
if not args.offline: # skip for update
if not args.skip_update_check and not core.is_noupdate_game(igame.app_name):
print("Checking for updates...")
# print("Checking for updates...")
# check updates
try:
latest = core.get_asset(

View file

@ -1,3 +1,4 @@
import functools
import logging
import os
import queue
@ -6,6 +7,7 @@ import time
from typing import Optional, Union, Tuple
from legendary.cli import LegendaryCLI as LegendaryCLIReal
from legendary.lfs.wine_helpers import case_insensitive_file_search
from legendary.models.downloading import AnalysisResult, ConditionCheckResult
from legendary.models.game import Game, InstalledGame, VerifyResult
from legendary.lfs.utils import validate_files
@ -30,21 +32,35 @@ class LegendaryCLI(LegendaryCLIReal):
# noinspection PyMissingConstructor
def __init__(self, core: LegendaryCore):
self.core = core
self.logger = logging.getLogger('Cli')
self.logger = logging.getLogger('cli')
self.logging_queue = None
self.ql = self.setup_threaded_logging()
def __del__(self):
self.ql.stop()
@staticmethod
def unlock_installed(func):
@functools.wraps(func)
def unlock(self, *args, **kwargs):
ret = func(self, *args, **kwargs)
self.core.lgd._installed_lock.release(force=True)
return ret
return unlock
def resolve_aliases(self, name):
return super(LegendaryCLI, self)._resolve_aliases(name)
@unlock_installed
def install_game(self, args: LgndrInstallGameArgs) -> Optional[Tuple[DLManager, AnalysisResult, InstalledGame, Game, bool, Optional[str], ConditionCheckResult]]:
# Override logger for the local context to use message as part of the indirect return value
logger = LgndrIndirectLogger(args.indirect_status, self.logger)
get_boolean_choice = args.get_boolean_choice
sdl_prompt = args.sdl_prompt
if not self.core.lgd.lock_installed():
logger.fatal('Failed to acquire installed data lock, only one instance of Legendary may '
'install/import/move applications at a time.')
return
args.app_name = self._resolve_aliases(args.app_name)
if self.core.is_installed(args.app_name):
@ -131,7 +147,7 @@ class LegendaryCLI(LegendaryCLIReal):
if config_tags:
self.core.lgd.config.remove_option(game.app_name, 'install_tags')
config_tags = None
self.core.lgd.config.set(game.app_name, 'disable_sdl', True)
self.core.lgd.config.set(game.app_name, 'disable_sdl', 'true')
sdl_enabled = False
# just disable SDL, but keep config tags that have been manually specified
elif config_disable_sdl or args.disable_sdl:
@ -205,10 +221,15 @@ class LegendaryCLI(LegendaryCLIReal):
return dlm, analysis, igame, game, args.repair_mode, repair_file, res
# Rare: This is currently handled in DownloadThread, this is a trial
@unlock_installed
def install_game_real(self, args: LgndrInstallGameRealArgs, dlm: DLManager, game: Game, igame: InstalledGame) -> LgndrInstallGameRealRet:
# Override logger for the local context to use message as part of the indirect return value
logger = LgndrIndirectLogger(args.indirect_status, self.logger)
ret = LgndrInstallGameRealRet(game.app_name)
if not self.core.lgd.lock_installed():
logger.fatal('Failed to acquire installed data lock, only one instance of Legendary may '
'install/import/move applications at a time.')
return ret
start_t = time.time()
@ -286,9 +307,14 @@ class LegendaryCLI(LegendaryCLIReal):
return ret
@unlock_installed
def install_game_cleanup(self, game: Game, igame: InstalledGame, repair_mode: bool = False, repair_file: str = '') -> None:
# Override logger for the local context to use message as part of the indirect return value
logger = LgndrIndirectLogger(LgndrIndirectStatus(), self.logger)
if not self.core.lgd.lock_installed():
logger.fatal('Failed to acquire installed data lock, only one instance of Legendary may '
'install/import/move applications at a time.')
return
old_igame = self.core.get_installed_game(game.app_name)
if old_igame and repair_mode and os.path.exists(repair_file):
@ -310,6 +336,11 @@ class LegendaryCLI(LegendaryCLIReal):
self.core.lgd.config.set(game.app_name, 'install_tags', ','.join(old_igame.install_tags))
self.core.lgd.save_config()
# check if the version changed, this can happen for DLC that gets a version bump with no actual file changes
if old_igame and old_igame.version != igame.version:
old_igame.version = igame.version
self.core.install_game(old_igame)
def _handle_postinstall(self, postinstall, igame, skip_prereqs=False, choice=True):
# Override logger for the local context to use message as part of the indirect return value
logger = LgndrIndirectLogger(LgndrIndirectStatus(), self.logger)
@ -347,10 +378,16 @@ class LegendaryCLI(LegendaryCLIReal):
else:
logger.info('Automatic installation not available on Linux.')
@unlock_installed
def uninstall_game(self, args: LgndrUninstallGameArgs) -> None:
# Override logger for the local context to use message as part of the indirect return value
logger = LgndrIndirectLogger(args.indirect_status, self.logger, logging.WARNING)
get_boolean_choice = args.get_boolean_choice
get_boolean_choice = args.get_boolean_choice_main
# def get_boolean_choice(x, default): return True
if not self.core.lgd.lock_installed():
logger.fatal('Failed to acquire installed data lock, only one instance of Legendary may '
'install/import/move applications at a time.')
return
args.app_name = self._resolve_aliases(args.app_name)
igame = self.core.get_installed_game(args.app_name)
@ -362,6 +399,9 @@ class LegendaryCLI(LegendaryCLIReal):
if not get_boolean_choice(f'Do you wish to uninstall "{igame.title}"?', default=False):
return
if os.name == 'nt' and igame.uninstaller and not args.skip_uninstaller:
self._handle_uninstaller(igame, args)
try:
if not igame.is_dlc:
# Remove DLC first so directory is empty when game uninstall runs
@ -380,6 +420,31 @@ class LegendaryCLI(LegendaryCLIReal):
logger.warning(f'Removing game failed: {e!r}, please remove {igame.install_path} manually.')
return
def _handle_uninstaller(self, igame: InstalledGame, args: LgndrUninstallGameArgs):
# Override logger for the local context to use message as part of the indirect return value
logger = LgndrIndirectLogger(args.indirect_status, self.logger, logging.WARNING)
yes = args.yes
get_boolean_choice = args.get_boolean_choice_handler
# def get_boolean_choice(x, default): return True
# noinspection PyShadowingBuiltins
def print(x): self.logger.info(x) if x else None
uninstaller = igame.uninstaller
print('\nThis game provides the following uninstaller:')
print(f'- {uninstaller["path"]} {uninstaller["args"]}\n')
if yes or get_boolean_choice('Do you wish to run the uninstaller?', default=True):
logger.info('Running uninstaller...')
req_path, req_exec = os.path.split(uninstaller['path'])
work_dir = os.path.join(igame.install_path, req_path)
fullpath = os.path.join(work_dir, req_exec)
try:
p = subprocess.Popen([fullpath, uninstaller['args']], cwd=work_dir, shell=True)
p.wait()
except Exception as e:
logger.error(f'Failed to run uninstaller: {e!r}')
def verify_game(self, args: Union[LgndrVerifyGameArgs, LgndrInstallGameArgs], print_command=True, repair_mode=False, repair_online=False) -> Optional[Tuple[int, int]]:
# Override logger for the local context to use message as part of the indirect return value
logger = LgndrIndirectLogger(args.indirect_status, self.logger)
@ -492,10 +557,15 @@ class LegendaryCLI(LegendaryCLIReal):
logger.info(f'Run "legendary repair {args.app_name}" to repair your game installation.')
return len(failed), len(missing)
@unlock_installed
def import_game(self, args: LgndrImportGameArgs) -> None:
# Override logger for the local context to use message as part of the indirect return value
logger = LgndrIndirectLogger(args.indirect_status, self.logger)
get_boolean_choice = args.get_boolean_choice
if not self.core.lgd.lock_installed():
logger.fatal('Failed to acquire installed data lock, only one instance of Legendary may '
'install/import/move applications at a time.')
return
# make sure path is absolute
args.app_path = os.path.abspath(args.app_path)
@ -535,6 +605,8 @@ class LegendaryCLI(LegendaryCLIReal):
# get everything needed for import from core, then run additional checks.
manifest, igame = self.core.import_game(game, args.app_path, platform=args.platform)
exe_path = os.path.join(args.app_path, manifest.meta.launch_exe.lstrip('/'))
if os.name != 'nt':
exe_path = case_insensitive_file_search(exe_path)
# check if most files at least exist or if user might have specified the wrong directory
total = len(manifest.file_manifest_list.elements)
found = sum(os.path.exists(os.path.join(args.app_path, f.filename))
@ -590,9 +662,18 @@ class LegendaryCLI(LegendaryCLIReal):
logger.info(f'{"DLC" if game.is_dlc else "Game"} "{game.app_title}" has been imported.')
return
@unlock_installed
def egs_sync(self, args):
return super(LegendaryCLI, self).egs_sync(args)
@unlock_installed
def move(self, args):
# Override logger for the local context to use message as part of the indirect return value
logger = LgndrIndirectLogger(args.indirect_status, self.logger)
if not self.core.lgd.lock_installed():
logger.fatal('Failed to acquire installed data lock, only one instance of Legendary may '
'install/import/move applications at a time.')
return
app_name = self._resolve_aliases(args.app_name)
igame = self.core.get_installed_game(app_name, skip_sync=True)

View file

@ -1,7 +1,10 @@
import functools
import json
import os
from datetime import datetime
from multiprocessing import Queue
from uuid import uuid4
from requests.exceptions import HTTPError, ConnectionError
# On Windows the monkeypatching of `run_real` below doesn't work like on Linux
# This has the side effect of emitting the UIUpdate in DownloadThread complaining with a TypeError
@ -11,6 +14,7 @@ from legendary.core import LegendaryCore as LegendaryCoreReal
from legendary.lfs.utils import delete_folder
from legendary.models.downloading import AnalysisResult
from legendary.models.egl import EGLManifest
from legendary.models.exceptions import InvalidCredentialsError
from legendary.models.game import Game, InstalledGame
from legendary.models.manifest import ManifestMeta
@ -28,6 +32,84 @@ class LegendaryCore(LegendaryCoreReal):
self.handler = LgndrCoreLogHandler()
self.log.addHandler(self.handler)
@staticmethod
def unlock_installed(func):
@functools.wraps(func)
def unlock(self, *args, **kwargs):
ret = func(self, *args, **kwargs)
self.lgd._installed_lock.release(force=True)
return ret
return unlock
def _login(self, lock, force_refresh=False) -> bool:
"""
Attempts logging in with existing credentials.
raises ValueError if no existing credentials or InvalidCredentialsError if the API return an error
"""
if not lock.data:
raise ValueError('No saved credentials')
elif self.logged_in and lock.data['expires_at']:
dt_exp = datetime.fromisoformat(lock.data['expires_at'][:-1])
dt_now = datetime.utcnow()
td = dt_now - dt_exp
# if session still has at least 10 minutes left we can re-use it.
if dt_exp > dt_now and abs(td.total_seconds()) > 600:
return True
else:
self.logged_in = False
# run update check
if self.update_check_enabled():
try:
self.check_for_updates()
except Exception as e:
self.log.warning(f'Checking for Legendary updates failed: {e!r}')
else:
self.apply_lgd_config()
# check for overlay updates
if self.is_overlay_installed():
try:
self.check_for_overlay_updates()
except Exception as e:
self.log.warning(f'Checking for EOS Overlay updates failed: {e!r}')
if lock.data['expires_at'] and not force_refresh:
dt_exp = datetime.fromisoformat(lock.data['expires_at'][:-1])
dt_now = datetime.utcnow()
td = dt_now - dt_exp
# if session still has at least 10 minutes left we can re-use it.
if dt_exp > dt_now and abs(td.total_seconds()) > 600:
self.log.info('Trying to re-use existing login session...')
try:
self.egs.resume_session(lock.data)
self.logged_in = True
return True
except InvalidCredentialsError as e:
self.log.warning(f'Resuming failed due to invalid credentials: {e!r}')
except Exception as e:
self.log.warning(f'Resuming failed for unknown reason: {e!r}')
# If verify fails just continue the normal authentication process
self.log.info('Falling back to using refresh token...')
try:
self.log.info('Logging in...')
userdata = self.egs.start_session(lock.data['refresh_token'])
except InvalidCredentialsError:
self.log.error('Stored credentials are no longer valid! Please login again.')
lock.clear()
return False
except (HTTPError, ConnectionError) as e:
self.log.error(f'HTTP request for login failed: {e!r}, please try again later.')
return False
lock.data = userdata
self.logged_in = True
return True
# skip_sync defaults to false but since Rare is persistent, skip by default
# def get_installed_game(self, app_name, skip_sync=True) -> InstalledGame:
# return super(LegendaryCore, self).get_installed_game(app_name, skip_sync)
@ -76,6 +158,7 @@ class LegendaryCore(LegendaryCoreReal):
finally:
pass
@unlock_installed
def egl_import(self, app_name):
try:
super(LegendaryCore, self).egl_import(app_name)
@ -121,6 +204,7 @@ class LegendaryCore(LegendaryCoreReal):
if delete_files:
delete_folder(os.path.join(igame.install_path, '.egstore'))
@unlock_installed
def egl_export(self, app_name):
try:
super(LegendaryCore, self).egl_export(app_name)

View file

@ -72,7 +72,7 @@ class DLManager(DLManagerReal):
self.conditions = [shm_cond, task_cond]
# start threads
s_time = time.time()
s_time = time.perf_counter()
self.threads.append(Thread(target=self.download_job_manager, args=(task_cond, shm_cond)))
self.threads.append(Thread(target=self.dl_results_handler, args=(task_cond,)))
self.threads.append(Thread(target=self.fw_results_handler, args=(shm_cond,)))
@ -80,13 +80,13 @@ class DLManager(DLManagerReal):
for t in self.threads:
t.start()
last_update = time.time()
last_update = time.perf_counter()
# Rare: kill requested
kill_request = False
while processed_tasks < num_tasks:
delta = time.time() - last_update
delta = time.perf_counter() - last_update
if not delta:
time.sleep(self.update_interval)
continue
@ -108,10 +108,10 @@ class DLManager(DLManagerReal):
self.bytes_read_since_last = self.bytes_written_since_last = 0
self.bytes_downloaded_since_last = self.num_processed_since_last = 0
self.bytes_decompressed_since_last = self.num_tasks_processed_since_last = 0
last_update = time.time()
last_update = time.perf_counter()
perc = (processed_chunks / num_chunk_tasks) * 100
runtime = time.time() - s_time
runtime = time.perf_counter() - s_time
total_avail = len(self.sms)
total_used = (num_shared_memory_segments - total_avail) * (self.analysis.biggest_chunk / 1024 / 1024)

View file

@ -40,10 +40,12 @@ class LgndrImportGameArgs:
class LgndrUninstallGameArgs:
app_name: str
keep_files: bool = False
skip_uninstaller: bool = False
yes: bool = False
# Rare: Extra arguments
indirect_status: LgndrIndirectStatus = field(default_factory=LgndrIndirectStatus)
get_boolean_choice: GetBooleanChoiceProtocol = get_boolean_choice
get_boolean_choice_main: GetBooleanChoiceProtocol = get_boolean_choice
get_boolean_choice_handler: GetBooleanChoiceProtocol = get_boolean_choice
@dataclass

View file

@ -5,8 +5,8 @@ from typing import Optional
@dataclass
class UIUpdate:
"""
Status update object sent from the manager to the CLI/GUI to update status indicators
Inheritance doesn't work due to optional arguments in UIUpdate proper
Status update object sent from the manager to the CLI/GUI to update status indicators
Inheritance doesn't work due to optional arguments in UIUpdate proper
"""
progress: float
download_speed: float

View file

@ -6,6 +6,7 @@ from logging import getLogger
from typing import Optional, List, Tuple
from PyQt5.QtCore import QObject, pyqtSignal, QRunnable, QThreadPool, QSettings
from legendary.lfs import eos
from legendary.models.game import SaveGameFile, SaveGameStatus, Game, InstalledGame
from rare.lgndr.core import LegendaryCore
@ -116,30 +117,13 @@ class RareGameBase(QObject):
pass
@property
@abstractmethod
def is_mac(self) -> bool:
pass
def platforms(self) -> Tuple:
"""!
@brief Property that holds the platforms a game is available for
@property
@abstractmethod
def is_win32(self) -> bool:
pass
class RareGameSlim(RareGameBase):
def __init__(self, legendary_core: LegendaryCore, game: Game):
super(RareGameSlim, self).__init__(legendary_core, game)
# None if origin or not installed
self.igame: Optional[InstalledGame] = self.core.get_installed_game(game.app_name)
self.saves: List[RareSaveGame] = []
@property
def is_installed(self) -> bool:
return True
def set_installed(self, installed: bool) -> None:
pass
@return Tuple
"""
return tuple(self.game.asset_infos.keys())
@property
def is_mac(self) -> bool:
@ -159,6 +143,60 @@ class RareGameSlim(RareGameBase):
"""
return "Win32" in self.game.asset_infos.keys()
@property
def is_origin(self) -> bool:
"""!
@brief Property to report if a Game is an Origin game
Legendary and by extenstion Rare can't launch Origin games directly,
it just launches the Origin client and thus requires a bit of a special
handling to let the user know.
@return bool If the game is an Origin game
"""
return (
self.game.metadata.get("customAttributes", {}).get("ThirdPartyManagedApp", {}).get("value") == "Origin"
)
@property
def is_overlay(self):
return self.app_name == eos.EOSOverlayApp.app_name
@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 install_path(self) -> Optional[str]:
if self.igame:
return self.igame.install_path
return None
class RareGameSlim(RareGameBase):
def __init__(self, legendary_core: LegendaryCore, game: Game):
super(RareGameSlim, self).__init__(legendary_core, game)
# None if origin or not installed
self.igame: Optional[InstalledGame] = self.core.get_installed_game(game.app_name)
self.saves: List[RareSaveGame] = []
@property
def is_installed(self) -> bool:
return True
def set_installed(self, installed: bool) -> None:
pass
@property
def auto_sync_saves(self):
return self.supports_cloud_saves and QSettings().value(

View file

@ -10,6 +10,7 @@ from typing import List, Optional, Dict, Set
from PyQt5.QtCore import QRunnable, pyqtSlot, QProcess, QThreadPool
from PyQt5.QtGui import QPixmap
from legendary.models.game import Game, InstalledGame
from legendary.utils.selective_dl import get_sdl_appname
from rare.lgndr.core import LegendaryCore
from rare.models.install import InstallOptionsModel, UninstallOptionsModel
@ -93,6 +94,13 @@ class RareGame(RareGameSlim):
if self.is_installed and not self.is_dlc:
self.game_process.connect_to_server(on_startup=True)
def add_dlc(self, dlc) -> None:
# lk: plug dlc progress signals to the game's
dlc.signals.progress.start.connect(self.signals.progress.start)
dlc.signals.progress.update.connect(self.signals.progress.update)
dlc.signals.progress.finish.connect(self.signals.progress.finish)
self.owned_dlcs.add(dlc)
def __on_progress_update(self, progress: int):
self.progress = progress
@ -194,12 +202,10 @@ class RareGame(RareGameSlim):
@property
def install_path(self) -> Optional[str]:
if self.igame:
return self.igame.install_path
elif self.is_origin:
if self.is_origin:
# TODO Linux is also C:\\...
return self.__origin_install_path
return None
return super(RareGame, self).install_path
@install_path.setter
def install_path(self, path: str) -> None:
@ -209,19 +215,6 @@ class RareGame(RareGameSlim):
elif self.is_origin:
self.__origin_install_path = path
@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:
"""!
@ -415,21 +408,6 @@ class RareGame(RareGameSlim):
# Asset infos are usually None, but there was a bug, that it was an empty GameAsset class
return not self.game.asset_infos or not next(iter(self.game.asset_infos.values())).app_name
@property
def is_origin(self) -> bool:
"""!
@brief Property to report if a Game is an Origin game
Legendary and by extenstion Rare can't launch Origin games directly,
it just launches the Origin client and thus requires a bit of a special
handling to let the user know.
@return bool If the game is an Origin game
"""
return (
self.game.metadata.get("customAttributes", {}).get("ThirdPartyManagedApp", {}).get("value") == "Origin"
)
@property
def is_ubisoft(self) -> bool:
return (
@ -444,6 +422,10 @@ class RareGame(RareGameSlim):
else self.app_title
)
@property
def sdl_name(self) -> Optional[str]:
return get_sdl_appname(self.app_name)
@property
def save_path(self) -> Optional[str]:
return super(RareGame, self).save_path
@ -482,12 +464,19 @@ class RareGame(RareGameSlim):
grant_date = datetime.fromisoformat(
entitlement["grantDate"].replace("Z", "+00:00")
) if entitlement else None
if force:
print(grant_date)
self.metadata.grant_date = grant_date
self.__save_metadata()
return self.metadata.grant_date
def set_origin_attributes(self, path: str, size: int = 0) -> None:
self.__origin_install_path = path
self.__origin_install_size = size
if self.install_path and self.install_size:
self.signals.game.installed.emit(self.app_name)
else:
self.signals.game.uninstalled.emit(self.app_name)
self.set_pixmap()
@property
def can_launch(self) -> bool:
if self.is_idle and self.is_origin:
@ -521,14 +510,15 @@ class RareGame(RareGameSlim):
)
return True
def set_origin_attributes(self, path: str, size: int = 0) -> None:
self.__origin_install_path = path
self.__origin_install_size = size
if self.install_path and self.install_size:
self.signals.game.installed.emit(self.app_name)
else:
self.signals.game.uninstalled.emit(self.app_name)
self.set_pixmap()
def modify(self) -> bool:
if not self.is_idle:
return False
self.signals.game.install.emit(
InstallOptionsModel(
app_name=self.app_name, reset_sdl=True
)
)
return True
def repair(self, repair_and_update) -> bool:
if not self.is_idle:
@ -592,11 +582,3 @@ class RareEosOverlay(RareGameBase):
else:
self.igame = None
self.signals.game.uninstalled.emit(self.app_name)
@property
def is_mac(self) -> bool:
return False
@property
def is_win32(self) -> bool:
return False

View file

@ -24,6 +24,7 @@ class InstallOptionsModel:
repair_and_update: bool = False
no_install: bool = False
ignore_space: bool = False
reset_sdl: bool = False
skip_dlcs: bool = False
with_dlcs: bool = False
# Rare's internal arguments

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 KiB

Binary file not shown.

View file

@ -13,5 +13,6 @@
<qresource prefix="images">
<file alias="Rare.png">images/Rare.png</file>
<file alias="loader.gif">images/loader.gif</file>
<file alias="loader.webp">images/loader.webp</file>
</qresource>
</RCC>

View file

@ -57,7 +57,7 @@ def processResourceFile(filenamesIn, filenameOut, listFiles):
return library.output(filenameOut)
def css_name(widget: Union[wrappertype,QObject,Type], subwidget: str = ""):
def css_name(widget: Union[wrappertype, QObject, Type], subwidget: str = ""):
return f"#{widget_object_name(widget, '')}{subwidget}"

View file

@ -721,10 +721,12 @@ QBalloonTip {
}
/* Wrapper settings styling */
QPushButton#WrapperWidgetButton {
QPushButton#WrapperWidgetButton,
QToolButton#WrapperWidgetButton {
border-color: #DADDDE;
}
QPushButton#WrapperWidgetButton:disabled {
QPushButton#WrapperWidgetButton:disabled,
QToolButton#WrapperWidgetButton:disabled {
border-color: #A8AAAB;
}
QScrollArea#WrapperSettingsScroll {

View file

@ -721,10 +721,12 @@ QBalloonTip {
}
/* Wrapper settings styling */
QPushButton#WrapperWidgetButton {
QPushButton#WrapperWidgetButton,
QToolButton#WrapperWidgetButton {
border-color: rgb( 51, 54, 59);
}
QPushButton#WrapperWidgetButton:disabled {
QPushButton#WrapperWidgetButton:disabled,
QToolButton#WrapperWidgetButton:disabled {
border-color: rgb( 41, 43, 47);
}
QScrollArea#WrapperSettingsScroll {

View file

@ -61,7 +61,7 @@ class ImageManager(QObject):
def run(self):
self.func(self.updates, self.json_data, self.game)
logger.debug(f" Emitting singal for {self.game.app_name} ({self.game.app_title})")
logger.debug(f"Emitting singal for {self.game.app_name} ({self.game.app_title})")
self.signals.completed.emit(self.game)
def __init__(self, signals: GlobalSignals, core: LegendaryCore):
@ -82,7 +82,7 @@ class ImageManager(QObject):
self.device = ImageSize.Preset(1, QApplication.instance().devicePixelRatio())
self.threadpool = QThreadPool()
self.threadpool.setMaxThreadCount(8)
self.threadpool.setMaxThreadCount(6)
def __img_dir(self, app_name: str) -> Path:
return self.image_dir.joinpath(app_name)
@ -182,8 +182,12 @@ class ImageManager(QObject):
logger.info(f"Downloading {image['type']} for {game.app_name} ({game.app_title})")
json_data[image["type"]] = image["md5"]
payload = {"resize": 1, "w": ImageSize.Image.size.width(), "h": ImageSize.Image.size.height()}
# cache_data[image["type"]] = requests.get(image["url"], params=payload, timeout=2).content
cache_data[image["type"]] = requests.get(image["url"], params=payload).content
try:
# cache_data[image["type"]] = requests.get(image["url"], params=payload).content
cache_data[image["type"]] = requests.get(image["url"], params=payload, timeout=10).content
except Exception as e:
logger.error(e)
return False
self.__convert(game, cache_data)
# lk: don't keep the cache if there is no logo (kept for me)

View file

@ -238,27 +238,28 @@ class RareCore(QObject):
rgame.update_rgame()
else:
rgame = RareGame(self.__core, self.__image_manager, game)
self.__add_game(rgame)
return rgame
def __add_games_and_dlcs(self, games: List[Game], dlcs_dict: Dict[str, List]) -> None:
length = len(games)
for idx, game in enumerate(games):
rgame = self.__create_or_update_rgame(game)
# lk: since loading has to know about game state,
# validate installation just adding each RareGame
# TODO: this should probably be moved into RareGame
if rgame.is_installed and not (rgame.is_dlc or rgame.is_non_asset):
self.__validate_install(rgame)
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.add(rdlc)
self.__add_game(rdlc)
self.__add_game(rgame)
if rdlc not in rgame.owned_dlcs:
rgame.add_dlc(rdlc)
# lk: since loading has to know about game state,
# validate installation just adding each RareGamesu
# TODO: this should probably be moved into RareGame
if rgame.is_installed and not (rgame.is_dlc or rgame.is_non_asset):
try:
self.__validate_install(rgame)
except FileNotFoundError as e:
logger.info(f'Marking "{rgame.app_title}" as not installed because an exception has occurred...')
logger.error(e)
rgame.set_installed(False)
self.progress.emit(int(idx/length * 80) + 20, self.tr("Loaded <b>{}</b>").format(rgame.app_title))
@pyqtSlot(object, int)
@ -329,31 +330,6 @@ class RareCore(QObject):
self.fetch_entitlements()
self.resolve_origin()
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
"""
def __load_pixmaps() -> None:
# time.sleep(0.1)
for rgame in self.__library.values():
# self.__image_manager.download_image(rgame.game, rgame.set_pixmap, 0, False)
rgame.load_pixmap()
# lk: activity perception delay
time.sleep(0.0005)
pixmap_worker = QRunnable.create(__load_pixmaps)
QThreadPool.globalInstance().start(pixmap_worker)
@property
def games_and_dlcs(self) -> Iterator[RareGame]:
for app_name in self.__library:
@ -371,6 +347,10 @@ class RareCore(QObject):
def origin_games(self) -> Iterator[RareGame]:
return self.__filter_games(lambda game: game.is_origin and not game.is_dlc)
@property
def ubisoft_games(self) -> Iterator[RareGame]:
return self.__filter_games(lambda game: game.is_ubisoft and not game.is_dlc)
@property
def game_list(self) -> Iterator[Game]:
for game in self.games:

View file

@ -14,6 +14,7 @@ from .worker import Worker
logger = getLogger("UninstallWorker")
# TODO: You can use RareGame directly here once this is called inside RareCore and skip metadata fetch
def uninstall_game(core: LegendaryCore, app_name: str, keep_files=False, keep_config=False):
game = core.get_game(app_name)
@ -32,8 +33,9 @@ def uninstall_game(core: LegendaryCore, app_name: str, keep_files=False, keep_co
LgndrUninstallGameArgs(
app_name=app_name,
keep_files=keep_files,
indirect_status=status,
skip_uninstaller=False,
yes=True,
indirect_status=status,
)
)
if not keep_config:

View file

@ -2,7 +2,7 @@
# Form implementation generated from reading ui file 'rare/ui/components/dialogs/login/landing_page.ui'
#
# Created by: PyQt5 UI code generator 5.15.7
# Created by: PyQt5 UI code generator 5.15.9
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
@ -34,12 +34,6 @@ class Ui_LandingPage(object):
self.login_browser_radio.setSizePolicy(sizePolicy)
self.login_browser_radio.setObjectName("login_browser_radio")
self.landing_layout.addWidget(self.login_browser_radio, 1, 0, 1, 1)
self.login_browser_label = QtWidgets.QLabel(LandingPage)
font = QtGui.QFont()
font.setItalic(True)
self.login_browser_label.setFont(font)
self.login_browser_label.setObjectName("login_browser_label")
self.landing_layout.addWidget(self.login_browser_label, 1, 1, 1, 1)
self.login_import_radio = QtWidgets.QRadioButton(LandingPage)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
@ -48,14 +42,19 @@ class Ui_LandingPage(object):
self.login_import_radio.setSizePolicy(sizePolicy)
self.login_import_radio.setObjectName("login_import_radio")
self.landing_layout.addWidget(self.login_import_radio, 2, 0, 1, 1)
self.login_browser_label = QtWidgets.QLabel(LandingPage)
font = QtGui.QFont()
font.setItalic(True)
self.login_browser_label.setFont(font)
self.login_browser_label.setObjectName("login_browser_label")
self.landing_layout.addWidget(self.login_browser_label, 1, 1, 1, 2)
self.login_import_label = QtWidgets.QLabel(LandingPage)
font = QtGui.QFont()
font.setItalic(True)
self.login_import_label.setFont(font)
self.login_import_label.setObjectName("login_import_label")
self.landing_layout.addWidget(self.login_import_label, 2, 1, 1, 1)
spacerItem = QtWidgets.QSpacerItem(0, 0, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.landing_layout.addItem(spacerItem, 1, 2, 2, 1)
self.landing_layout.addWidget(self.login_import_label, 2, 1, 1, 2)
self.landing_layout.setColumnStretch(2, 1)
self.landing_layout.setRowStretch(1, 1)
self.landing_layout.setRowStretch(2, 1)
@ -65,8 +64,8 @@ class Ui_LandingPage(object):
_translate = QtCore.QCoreApplication.translate
self.login_label.setText(_translate("LandingPage", "Select login method"))
self.login_browser_radio.setText(_translate("LandingPage", "Browser"))
self.login_browser_label.setText(_translate("LandingPage", "Login using a browser."))
self.login_import_radio.setText(_translate("LandingPage", "Import"))
self.login_browser_label.setText(_translate("LandingPage", "Login using a browser."))
self.login_import_label.setText(_translate("LandingPage", "Import from Epic Games Launcher"))

View file

@ -13,7 +13,7 @@
<property name="windowTitle">
<string notr="true">LandingPage</string>
</property>
<layout class="QGridLayout" name="landing_layout" rowstretch="0,1,1">
<layout class="QGridLayout" name="landing_layout" rowstretch="0,1,1" columnstretch="0,0,1">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
@ -43,18 +43,6 @@
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="login_browser_label">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string>Login using a browser.</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QRadioButton" name="login_import_radio">
<property name="sizePolicy">
@ -68,7 +56,19 @@
</property>
</widget>
</item>
<item row="2" column="1">
<item row="1" column="1" colspan="2">
<widget class="QLabel" name="login_browser_label">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string>Login using a browser.</string>
</property>
</widget>
</item>
<item row="2" column="1" colspan="2">
<widget class="QLabel" name="login_import_label">
<property name="font">
<font>
@ -80,19 +80,6 @@
</property>
</widget>
</item>
<item row="1" column="2" rowspan="2">
<spacer name="login_hspacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>

View file

@ -14,7 +14,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_GameInfo(object):
def setupUi(self, GameInfo):
GameInfo.setObjectName("GameInfo")
GameInfo.resize(408, 340)
GameInfo.resize(419, 404)
self.main_layout = QtWidgets.QHBoxLayout(GameInfo)
self.main_layout.setObjectName("main_layout")
self.left_layout = QtWidgets.QVBoxLayout()
@ -180,6 +180,9 @@ class Ui_GameInfo(object):
self.installed_layout = QtWidgets.QVBoxLayout(self.installed_page)
self.installed_layout.setContentsMargins(0, 0, 0, 0)
self.installed_layout.setObjectName("installed_layout")
self.modify_button = QtWidgets.QPushButton(self.installed_page)
self.modify_button.setObjectName("modify_button")
self.installed_layout.addWidget(self.modify_button)
self.verify_stack = QtWidgets.QStackedWidget(self.installed_page)
self.verify_stack.setObjectName("verify_stack")
self.verify_button_page = QtWidgets.QWidget()
@ -276,7 +279,7 @@ class Ui_GameInfo(object):
self.main_layout.setStretch(1, 1)
self.retranslateUi(GameInfo)
self.game_actions_stack.setCurrentIndex(1)
self.game_actions_stack.setCurrentIndex(0)
self.verify_stack.setCurrentIndex(0)
self.move_stack.setCurrentIndex(0)
@ -291,6 +294,7 @@ class Ui_GameInfo(object):
self.lbl_install_path.setText(_translate("GameInfo", "Installation Path"))
self.lbl_platform.setText(_translate("GameInfo", "Platform"))
self.lbl_game_actions.setText(_translate("GameInfo", "Actions"))
self.modify_button.setText(_translate("GameInfo", "Modify Installation"))
self.verify_button.setText(_translate("GameInfo", "Verify Installation"))
self.repair_button.setText(_translate("GameInfo", "Repair Installation"))
self.move_button.setText(_translate("GameInfo", "Move Installation"))

View file

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>408</width>
<height>340</height>
<width>419</width>
<height>404</height>
</rect>
</property>
<property name="windowTitle">
@ -309,7 +309,7 @@
</size>
</property>
<property name="currentIndex">
<number>1</number>
<number>0</number>
</property>
<widget class="QWidget" name="installed_page">
<layout class="QVBoxLayout" name="installed_layout">
@ -325,6 +325,13 @@
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QPushButton" name="modify_button">
<property name="text">
<string>Modify Installation</string>
</property>
</widget>
</item>
<item>
<widget class="QStackedWidget" name="verify_stack">
<property name="currentIndex">

10
rare/utils/metrics.py Normal file
View file

@ -0,0 +1,10 @@
import time
from contextlib import contextmanager
from logging import Logger
@contextmanager
def timelogger(logger: Logger, title: str):
start = time.perf_counter()
yield
logger.debug("%s: %s seconds", title, time.perf_counter() - start)

View file

@ -132,6 +132,8 @@ def get_rare_executable() -> List[str]:
# lk: detect if nuitka
if "__compiled__" in globals():
executable = [sys.executable]
elif sys.argv[0].endswith("__main__.py"):
executable = [sys.executable, "-m", "rare"]
elif platform.system() == "Linux" or platform.system() == "Darwin" or platform.system() == "FreeBSD":
if p := os.environ.get("APPIMAGE"):
executable = [p]

View file

@ -165,6 +165,8 @@ class IndicatorLineEdit(QWidget):
self.is_valid = False
self.edit_func = edit_func
self.save_func = save_func
if text:
self.line_edit.setText(text)
self.line_edit.textChanged.connect(self.__edit)
if self.edit_func is None:
self.line_edit.textChanged.connect(self.__save)
@ -175,8 +177,8 @@ class IndicatorLineEdit(QWidget):
# lk: however it is going to edit any "understood" bad input to good input
# lk: and we might not want that (but the validity check reports on the edited string)
# lk: it is also going to trigger this widget's textChanged signal but that gets lost
if text:
self.line_edit.setText(text)
# if text:
# self.line_edit.setText(text)
def deleteLater(self) -> None:
if self.__thread is not None:

View file

@ -8,8 +8,9 @@ class LoadingWidget(QLabel):
super(LoadingWidget, self).__init__(parent=parent)
self.setObjectName(type(self).__name__)
self.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
self.movie = QMovie(":/images/loader.gif", parent=self)
self.setFixedSize(128, 128)
self.movie = QMovie(":/images/loader.webp", parent=self)
# The animation's exact size is 94x94
self.setFixedSize(96, 96)
self.setMovie(self.movie)
if self.parent() is not None:
self.parent().installEventFilter(self)
@ -31,7 +32,10 @@ class LoadingWidget(QLabel):
return super().event(e)
def showEvent(self, a0: QShowEvent) -> None:
if a0.spontaneous():
return super().showEvent(a0)
self.__center_on_parent()
super().showEvent(a0)
def eventFilter(self, a0: QObject, a1: QEvent) -> bool:
if a0 is self.parent() and a1.type() == QEvent.Resize:

View file

@ -3,5 +3,5 @@ requests
PyQt5
QtAwesome
setuptools
legendary-gl>=0.20.32
legendary-gl>=0.20.33
pywin32; platform_system == "Windows"