diff --git a/rare/components/tabs/games/__init__.py b/rare/components/tabs/games/__init__.py
index c206b4e31..fc161c36c 100644
--- a/rare/components/tabs/games/__init__.py
+++ b/rare/components/tabs/games/__init__.py
@@ -14,7 +14,7 @@
from rare.widgets.library_layout import LibraryLayout
from rare.widgets.sliding_stack import SlidingStackedWidget
from .game_info import GameInfoTabs
-from .game_widgets import LibraryWidgetController
+from .game_widgets import LibraryWidgetController, LibraryFilter, LibraryOrder
from .game_widgets.icon_game_widget import IconGameWidget
from .game_widgets.list_game_widget import ListGameWidget
from .head_bar import GameListHeadBar
@@ -33,8 +33,6 @@ def __init__(self, parent=None):
self.image_manager = ImageManagerSingleton()
self.settings = QSettings()
- self.active_filter: int = 0
-
self.games_page = QWidget(parent=self)
games_page_layout = QVBoxLayout(self.games_page)
self.addWidget(self.games_page)
@@ -94,14 +92,11 @@ def __init__(self, parent=None):
self.head_bar.search_bar.textChanged.connect(self.scroll_to_top)
self.head_bar.filterChanged.connect(self.filter_games)
self.head_bar.filterChanged.connect(self.scroll_to_top)
- self.head_bar.refresh_list.clicked.connect(self.library_controller.update_list)
+ self.head_bar.orderChanged.connect(self.order_games)
+ self.head_bar.orderChanged.connect(self.scroll_to_top)
+ self.head_bar.refresh_list.clicked.connect(self.library_controller.update_game_views)
self.head_bar.view.toggled.connect(self.toggle_view)
- f = self.settings.value("filter", 0, int)
- if f >= len(self.head_bar.available_filters):
- f = 0
- self.active_filter = self.head_bar.available_filters[f]
-
# signals
self.signals.game.installed.connect(self.update_count_games_label)
self.signals.game.uninstalled.connect(self.update_count_games_label)
@@ -153,7 +148,7 @@ def setup_game_list(self):
continue
self.icon_view.layout().addWidget(icon_widget)
self.list_view.layout().addWidget(list_widget)
- self.filter_games(self.active_filter)
+ self.filter_games(self.head_bar.current_filter())
self.update_count_games_label()
def add_library_widget(self, rgame: RareGame):
@@ -167,18 +162,26 @@ def add_library_widget(self, rgame: RareGame):
list_widget.show_info.connect(self.show_game_info)
return icon_widget, list_widget
- @pyqtSlot(str)
- @pyqtSlot(str, str)
- def filter_games(self, filter_name="all", search_text: str = ""):
+ @pyqtSlot(int)
+ @pyqtSlot(int, str)
+ def filter_games(self, library_filter: LibraryFilter = LibraryFilter.ALL, search_text: str = ""):
if not search_text and (t := self.head_bar.search_bar.text()):
search_text = t
- if filter_name:
- self.active_filter = filter_name
- if not filter_name and (t := self.active_filter):
- filter_name = t
+ # if library_filter:
+ # self.active_filter = filter_type
+ # if not library_filter and (t := self.active_filter):
+ # library_filter = t
+
+ self.library_controller.filter_game_views(library_filter, search_text.lower())
+
+ @pyqtSlot(int)
+ @pyqtSlot(int, str)
+ def order_games(self, library_order: LibraryOrder = LibraryFilter.ALL, search_text: str = ""):
+ if not search_text and (t := self.head_bar.search_bar.text()):
+ search_text = t
- self.library_controller.filter_list(filter_name, search_text.lower())
+ self.library_controller.order_game_views(library_order, search_text.lower())
def toggle_view(self):
self.settings.setValue("icon_view", not self.head_bar.view.isChecked())
diff --git a/rare/components/tabs/games/game_info/cloud_saves.py b/rare/components/tabs/games/game_info/cloud_saves.py
index a39d492b6..86bac7aa9 100644
--- a/rare/components/tabs/games/game_info/cloud_saves.py
+++ b/rare/components/tabs/games/game_info/cloud_saves.py
@@ -3,7 +3,7 @@
from logging import getLogger
from typing import Tuple
-from PyQt5.QtCore import QThreadPool, QSettings
+from PyQt5.QtCore import QThreadPool, QSettings, pyqtSlot
from PyQt5.QtWidgets import (
QWidget,
QFileDialog,
@@ -19,10 +19,11 @@
from rare.models.game import RareGame
from rare.shared import RareCore
-from rare.shared.workers.wine_resolver import WineResolver
+from rare.shared.workers.wine_resolver import WineSavePathResolver
from rare.ui.components.tabs.games.game_info.cloud_widget import Ui_CloudWidget
from rare.ui.components.tabs.games.game_info.sync_widget import Ui_SyncWidget
from rare.utils.misc import icon
+from rare.utils.metrics import timelogger
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
from rare.widgets.loading_widget import LoadingWidget
from rare.widgets.side_tab import SideTabContents
@@ -110,13 +111,14 @@ def download(self):
def compute_save_path(self):
if self.rgame.is_installed and self.rgame.game.supports_cloud_saves:
try:
- new_path = self.core.get_save_path(self.rgame.app_name)
+ with timelogger(logger, "Detecting save path"):
+ new_path = self.core.get_save_path(self.rgame.app_name)
if platform.system() != "Windows" and not os.path.exists(new_path):
raise ValueError(f'Path "{new_path}" does not exist.')
except Exception as e:
logger.warning(str(e))
- resolver = WineResolver(self.core, self.rgame.raw_save_path, self.rgame.app_name)
- if not resolver.wine_env.get("WINEPREFIX"):
+ resolver = WineSavePathResolver(self.core, self.rgame)
+ if not resolver.environment.get("WINEPREFIX"):
del resolver
self.cloud_save_path_edit.setText("")
QMessageBox.warning(self, "Warning", "No wine prefix selected. Please set it in settings")
@@ -125,21 +127,21 @@ def compute_save_path(self):
self.cloud_save_path_edit.setDisabled(True)
self.compute_save_path_button.setDisabled(True)
- app_name = self.rgame.app_name
- resolver.signals.result_ready.connect(lambda x: self.wine_resolver_finished(x, app_name))
+ resolver.signals.result_ready.connect(self.__on_wine_resolver_result)
QThreadPool.globalInstance().start(resolver)
return
else:
self.cloud_save_path_edit.setText(new_path)
- def wine_resolver_finished(self, path, app_name):
+ @pyqtSlot(str, str)
+ def __on_wine_resolver_result(self, path, app_name):
logger.info(f"Wine resolver finished for {app_name}. Computed save path: {path}")
if app_name == self.rgame.app_name:
self.cloud_save_path_edit.setDisabled(False)
self.compute_save_path_button.setDisabled(False)
if path and not os.path.exists(path):
try:
- os.makedirs(path)
+ os.makedirs(path, exist_ok=True)
except PermissionError:
self.cloud_save_path_edit.setText("")
QMessageBox.warning(
@@ -154,8 +156,6 @@ def wine_resolver_finished(self, path, app_name):
self.cloud_save_path_edit.setText("")
return
self.cloud_save_path_edit.setText(path)
- elif path:
- self.rcore.get_game(app_name).save_path = path
def __update_widget(self):
supports_saves = self.rgame.igame is not None and (
diff --git a/rare/components/tabs/games/game_widgets/__init__.py b/rare/components/tabs/games/game_widgets/__init__.py
index b1df4b3dc..62da8c3f2 100644
--- a/rare/components/tabs/games/game_widgets/__init__.py
+++ b/rare/components/tabs/games/game_widgets/__init__.py
@@ -1,4 +1,5 @@
-from typing import Tuple, List, Union, Optional
+from enum import IntEnum
+from typing import Tuple, List, Union
from PyQt5.QtCore import QObject, pyqtSlot
from PyQt5.QtWidgets import QWidget
@@ -11,6 +12,24 @@
from .list_game_widget import ListGameWidget
+class LibraryFilter(IntEnum):
+ ALL = 1
+ INSTALLED = 2
+ OFFLINE = 3
+ HIDDEN = 4
+ WIN32 = 5
+ MAC = 6
+ INSTALLABLE = 7
+ INCLUDE_UE = 8
+
+
+class LibraryOrder(IntEnum):
+ TITLE = 1
+ RECENT = 2
+ NEWEST = 3
+ OLDEST = 4
+
+
class LibraryWidgetController(QObject):
def __init__(self, icon_container: QWidget, list_container: QWidget, parent: QWidget = None):
super(LibraryWidgetController, self).__init__(parent=parent)
@@ -20,8 +39,8 @@ def __init__(self, icon_container: QWidget, list_container: QWidget, parent: QWi
self.core: LegendaryCore = self.rcore.core()
self.signals: GlobalSignals = self.rcore.signals()
- self.signals.game.installed.connect(self.sort_list)
- self.signals.game.uninstalled.connect(self.sort_list)
+ self.signals.game.installed.connect(self.order_game_views)
+ self.signals.game.uninstalled.connect(self.order_game_views)
def add_game(self, rgame: RareGame):
return self.add_widgets(rgame)
@@ -32,24 +51,26 @@ def add_widgets(self, rgame: RareGame) -> Tuple[IconGameWidget, ListGameWidget]:
return icon_widget, list_widget
@staticmethod
- def __visibility(widget: Union[IconGameWidget,ListGameWidget], filter_name, search_text) -> Tuple[bool, float]:
- if filter_name == "hidden":
+ def __visibility(
+ widget: Union[IconGameWidget, ListGameWidget], library_filter, search_text
+ ) -> Tuple[bool, float]:
+ if library_filter == LibraryFilter.HIDDEN:
visible = "hidden" in widget.rgame.metadata.tags
elif "hidden" in widget.rgame.metadata.tags:
visible = False
- elif filter_name == "installed":
+ elif library_filter == LibraryFilter.INSTALLED:
visible = widget.rgame.is_installed
- elif filter_name == "offline":
+ elif library_filter == LibraryFilter.OFFLINE:
visible = widget.rgame.can_run_offline
- elif filter_name == "32bit":
+ elif library_filter == LibraryFilter.WIN32:
visible = widget.rgame.is_win32
- elif filter_name == "mac":
+ elif library_filter == LibraryFilter.MAC:
visible = widget.rgame.is_mac
- elif filter_name == "installable":
+ elif library_filter == LibraryFilter.INSTALLABLE:
visible = not widget.rgame.is_non_asset
- elif filter_name == "include_ue":
+ elif library_filter == LibraryFilter.INCLUDE_UE:
visible = True
- elif filter_name == "all":
+ elif library_filter == LibraryFilter.ALL:
visible = not widget.rgame.is_unreal
else:
visible = True
@@ -64,7 +85,7 @@ def __visibility(widget: Union[IconGameWidget,ListGameWidget], filter_name, sear
return visible, opacity
- def filter_list(self, filter_name="all", search_text: str = ""):
+ def filter_game_views(self, filter_name="all", search_text: str = ""):
icon_widgets = self._icon_container.findChildren(IconGameWidget)
list_widgets = self._list_container.findChildren(ListGameWidget)
for iw in icon_widgets:
@@ -75,42 +96,52 @@ def filter_list(self, filter_name="all", search_text: str = ""):
visibility, opacity = self.__visibility(lw, filter_name, search_text)
lw.setOpacity(opacity)
lw.setVisible(visibility)
- self.sort_list(search_text)
+ self.order_game_views(search_text=search_text)
@pyqtSlot()
- def sort_list(self, sort_by: str = ""):
- # lk: this is the existing sorting implemenation
- # lk: it sorts by installed then by title
- if sort_by:
- self._icon_container.layout().sort(lambda x: (sort_by not in x.widget().rgame.app_title.lower(),))
- else:
+ def order_game_views(self, order_by: LibraryOrder = LibraryOrder.TITLE, search_text: str = ""):
+ list_widgets = self._list_container.findChildren(ListGameWidget)
+ if search_text:
self._icon_container.layout().sort(
- key=lambda x: (
- # Sort by grant date
- # x.widget().rgame.is_installed,
- # not x.widget().rgame.is_non_asset,
- # x.widget().rgame.grant_date(),
- # ), reverse=True
- not x.widget().rgame.is_installed,
- x.widget().rgame.is_non_asset,
- x.widget().rgame.app_title,
- )
+ lambda x: (search_text not in x.widget().rgame.app_title.lower(),)
)
- list_widgets = self._list_container.findChildren(ListGameWidget)
- if sort_by:
- list_widgets.sort(key=lambda x: (sort_by not in x.rgame.app_title.lower(),))
+ list_widgets.sort(key=lambda x: (search_text not in x.rgame.app_title.lower(),))
else:
- list_widgets.sort(
+ if (newest := order_by == LibraryOrder.NEWEST) or order_by == LibraryOrder.OLDEST:
# Sort by grant date
- # key=lambda x: (x.rgame.is_installed, not x.rgame.is_non_asset, x.rgame.grant_date()), reverse=True
- key=lambda x: (not x.rgame.is_installed, x.rgame.is_non_asset, x.rgame.app_title)
- )
+ self._icon_container.layout().sort(
+ key=lambda x: (x.widget().rgame.is_installed, not x.widget().rgame.is_non_asset, x.widget().rgame.grant_date()),
+ reverse=newest,
+ )
+ list_widgets.sort(
+ key=lambda x: (x.rgame.is_installed, not x.rgame.is_non_asset, x.rgame.grant_date()),
+ reverse=newest,
+ )
+ elif order_by == LibraryOrder.RECENT:
+ # Sort by recently played
+ self._icon_container.layout().sort(
+ key=lambda x: (not x.widget().rgame.is_installed, x.widget().rgame.is_non_asset, x.widget().rgame.metadata.last_played),
+ reverse=True,
+ )
+ list_widgets.sort(
+ key=lambda x: (not x.rgame.is_installed, x.rgame.is_non_asset, x.rgame.metadata.last_played),
+ reverse=True,
+ )
+ else:
+ # Sort by title
+ self._icon_container.layout().sort(
+ key=lambda x: (not x.widget().rgame.is_installed, x.widget().rgame.is_non_asset, x.widget().rgame.app_title)
+ )
+ list_widgets.sort(
+ key=lambda x: (not x.rgame.is_installed, x.rgame.is_non_asset, x.rgame.app_title)
+ )
+
for idx, wl in enumerate(list_widgets):
self._list_container.layout().insertWidget(idx, wl)
@pyqtSlot()
@pyqtSlot(list)
- def update_list(self, app_names: List[str] = None):
+ def update_game_views(self, app_names: List[str] = None):
if not app_names:
# lk: base it on icon widgets, the two lists should be identical
icon_widgets = self._icon_container.findChildren(IconGameWidget)
@@ -129,7 +160,7 @@ def update_list(self, app_names: List[str] = None):
game = self.rcore.get_game(app_name)
lw = ListGameWidget(game)
self._list_container.layout().addWidget(lw)
- self.sort_list()
+ self.order_game_views()
def __find_widget(self, app_name: str) -> Tuple[Union[IconGameWidget, None], Union[ListGameWidget, None]]:
iw = self._icon_container.findChild(IconGameWidget, app_name)
diff --git a/rare/components/tabs/games/head_bar.py b/rare/components/tabs/games/head_bar.py
index 1178108c2..b61c1e5a4 100644
--- a/rare/components/tabs/games/head_bar.py
+++ b/rare/components/tabs/games/head_bar.py
@@ -1,20 +1,24 @@
-from PyQt5.QtCore import QSettings, pyqtSignal, pyqtSlot
+from PyQt5.QtCore import QSettings, pyqtSignal, pyqtSlot, QSize, Qt
from PyQt5.QtWidgets import (
QLabel,
QPushButton,
QWidget,
QHBoxLayout,
- QComboBox, QToolButton, QMenu, QAction,
+ QComboBox,
+ QToolButton,
+ QMenu,
+ QAction,
)
-from qtawesome import IconWidget
from rare.shared import RareCore
from rare.utils.extra_widgets import SelectViewWidget, ButtonLineEdit
from rare.utils.misc import icon
+from .game_widgets import LibraryFilter, LibraryOrder
class GameListHeadBar(QWidget):
- filterChanged: pyqtSignal = pyqtSignal(str)
+ filterChanged: pyqtSignal = pyqtSignal(int)
+ orderChanged: pyqtSignal = pyqtSignal(int)
goto_import: pyqtSignal = pyqtSignal()
goto_egl_sync: pyqtSignal = pyqtSignal()
goto_eos_ubisoft: pyqtSignal = pyqtSignal()
@@ -24,61 +28,65 @@ def __init__(self, parent=None):
self.rcore = RareCore.instance()
self.settings = QSettings()
- self.filter = QComboBox()
- self.filter.addItems(
- [
- self.tr("All games"),
- self.tr("Installed only"),
- self.tr("Offline Games"),
- # self.tr("Hidden")
- ]
- )
-
- self.available_filters = [
- "all",
- "installed",
- "offline",
- # "hidden"
- ]
+ self.filter = QComboBox(parent=self)
+ filters = {
+ LibraryFilter.ALL: self.tr("All games"),
+ LibraryFilter.INSTALLED: self.tr("Installed only"),
+ LibraryFilter.OFFLINE: self.tr("Offline Games"),
+ # LibraryFilterTypes.HIDDEN: self.tr("Hidden"),
+ }
+ for data, text in filters.items():
+ self.filter.addItem(text, data)
if self.rcore.bit32_games:
- self.filter.addItem(self.tr("32 Bit Games"))
- self.available_filters.append("32bit")
-
+ self.filter.addItem(self.tr("32 Bit Games"), LibraryFilter.WIN32)
if self.rcore.mac_games:
- self.filter.addItem(self.tr("Mac games"))
- self.available_filters.append("mac")
-
+ self.filter.addItem(self.tr("Mac games"), LibraryFilter.MAC)
if self.rcore.origin_games:
- self.filter.addItem(self.tr("Exclude Origin"))
- self.available_filters.append("installable")
-
- self.filter.addItem(self.tr("Include Unreal Engine"))
- self.available_filters.append("include_ue")
+ self.filter.addItem(self.tr("Exclude Origin"), LibraryFilter.INSTALLABLE)
+ self.filter.addItem(self.tr("Include Unreal Engine"), LibraryFilter.INCLUDE_UE)
try:
- self.filter.setCurrentIndex(self.settings.value("filter", 0, int))
- except TypeError:
- self.settings.setValue("filter", 0)
- self.filter.setCurrentIndex(0)
-
- self.filter.currentIndexChanged.connect(self.filter_changed)
-
- integrations_menu = QMenu(self)
- import_action = QAction(icon("mdi.import", "fa.arrow-down"), self.tr("Import Game"), integrations_menu)
+ self.filter.setCurrentIndex(
+ self.filter.findData(
+ LibraryFilter(self.settings.value("library_filter", int(LibraryFilter.ALL), int))
+ )
+ )
+ except (TypeError, ValueError):
+ self.settings.setValue("library_filter", int(LibraryFilter.ALL))
+ self.filter.setCurrentIndex(self.filter.findData(LibraryFilter.ALL))
+
+ self.filter.currentIndexChanged.connect(self.__filter_changed)
+
+ self.order = QComboBox(parent=self)
+ sortings = {
+ LibraryOrder.TITLE: self.tr("Title"),
+ LibraryOrder.RECENT: self.tr("Recently played"),
+ LibraryOrder.NEWEST: self.tr("Newest"),
+ LibraryOrder.OLDEST: self.tr("Oldest"),
+ }
+ for data, text in sortings.items():
+ self.order.addItem(text, data)
+ self.order.currentIndexChanged.connect(self.__order_changed)
+
+ integrations_menu = QMenu(parent=self)
+ import_action = QAction(
+ icon("mdi.import", "fa.arrow-down"), self.tr("Import Game"), integrations_menu
+ )
import_action.triggered.connect(self.goto_import)
egl_sync_action = QAction(icon("mdi.sync", "fa.refresh"), self.tr("Sync with EGL"), integrations_menu)
egl_sync_action.triggered.connect(self.goto_egl_sync)
- eos_ubisoft_action = QAction(icon("mdi.rocket", "fa.rocket"), self.tr("Epic Overlay and Ubisoft"),
- integrations_menu)
+ eos_ubisoft_action = QAction(
+ icon("mdi.rocket", "fa.rocket"), self.tr("Epic Overlay and Ubisoft"), integrations_menu
+ )
eos_ubisoft_action.triggered.connect(self.goto_eos_ubisoft)
integrations_menu.addAction(import_action)
integrations_menu.addAction(egl_sync_action)
integrations_menu.addAction(eos_ubisoft_action)
- integrations = QToolButton(self)
+ integrations = QToolButton(parent=self)
integrations.setText(self.tr("Integrations"))
integrations.setMenu(integrations_menu)
integrations.setPopupMode(QToolButton.InstantPopup)
@@ -91,8 +99,8 @@ def __init__(self, parent=None):
checked = QSettings().value("icon_view", True, bool)
installed_tooltip = self.tr("Installed games")
- self.installed_icon = IconWidget(parent=self)
- self.installed_icon.setIcon(icon("ph.floppy-disk-back-fill"))
+ self.installed_icon = QLabel(parent=self)
+ self.installed_icon.setPixmap(icon("ph.floppy-disk-back-fill").pixmap(QSize(16, 16)))
self.installed_icon.setToolTip(installed_tooltip)
self.installed_label = QLabel(parent=self)
font = self.installed_label.font()
@@ -100,24 +108,25 @@ def __init__(self, parent=None):
self.installed_label.setFont(font)
self.installed_label.setToolTip(installed_tooltip)
available_tooltip = self.tr("Available games")
- self.available_icon = IconWidget(parent=self)
- self.available_icon.setIcon(icon("ph.floppy-disk-back-light"))
+ self.available_icon = QLabel(parent=self)
+ self.available_icon.setPixmap(icon("ph.floppy-disk-back-light").pixmap(QSize(16, 16)))
self.available_icon.setToolTip(available_tooltip)
self.available_label = QLabel(parent=self)
self.available_label.setToolTip(available_tooltip)
self.view = SelectViewWidget(checked)
- self.refresh_list = QPushButton()
+ self.refresh_list = QPushButton(parent=self)
self.refresh_list.setIcon(icon("fa.refresh")) # Reload icon
- self.refresh_list.clicked.connect(self.refresh_clicked)
+ self.refresh_list.clicked.connect(self.__refresh_clicked)
- layout = QHBoxLayout()
+ layout = QHBoxLayout(self)
layout.setContentsMargins(0, 5, 0, 5)
layout.addWidget(self.filter)
+ layout.addWidget(self.order)
layout.addStretch(0)
layout.addWidget(integrations)
- layout.addStretch(5)
+ layout.addStretch(2)
layout.addWidget(self.search_bar)
layout.addStretch(2)
layout.addWidget(self.installed_icon)
@@ -128,17 +137,29 @@ def __init__(self, parent=None):
layout.addWidget(self.view)
layout.addStretch(2)
layout.addWidget(self.refresh_list)
- self.setLayout(layout)
def set_games_count(self, inst: int, avail: int) -> None:
self.installed_label.setText(str(inst))
self.available_label.setText(str(avail))
@pyqtSlot()
- def refresh_clicked(self):
+ def __refresh_clicked(self):
self.rcore.fetch()
+ def current_filter(self) -> int:
+ return int(self.filter.currentData(Qt.UserRole))
+
+ @pyqtSlot(int)
+ def __filter_changed(self, index: int):
+ data = int(self.filter.itemData(index, Qt.UserRole))
+ self.filterChanged.emit(data)
+ self.settings.setValue("library_filter", data)
+
+ def current_order(self) -> int:
+ return int(self.order.currentData(Qt.UserRole))
+
@pyqtSlot(int)
- def filter_changed(self, i: int):
- self.filterChanged.emit(self.available_filters[i])
- self.settings.setValue("filter", i)
+ def __order_changed(self, index: int):
+ data = int(self.order.itemData(index, Qt.UserRole))
+ self.orderChanged.emit(data)
+ self.settings.setValue("library_order", data)
diff --git a/rare/components/tabs/games/integrations/__init__.py b/rare/components/tabs/games/integrations/__init__.py
index 0ecbc0880..0e9eb6bb2 100644
--- a/rare/components/tabs/games/integrations/__init__.py
+++ b/rare/components/tabs/games/integrations/__init__.py
@@ -1,7 +1,7 @@
from typing import Optional
from PyQt5.QtCore import Qt
-from PyQt5.QtWidgets import QVBoxLayout, QWidget, QLabel, QSpacerItem, QSizePolicy
+from PyQt5.QtWidgets import QVBoxLayout, QWidget, QLabel, QSizePolicy
from rare.widgets.side_tab import SideTabWidget
from .egl_sync_group import EGLSyncGroup
@@ -34,8 +34,8 @@ def __init__(self, parent=None):
self.tr(""),
self,
)
- self.ubisoft_group = UbisoftGroup(self.eos_ubisoft)
self.eos_group = EosGroup(self.eos_ubisoft)
+ self.ubisoft_group = UbisoftGroup(self.eos_ubisoft)
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"))
diff --git a/rare/components/tabs/games/integrations/egl_sync_group.py b/rare/components/tabs/games/integrations/egl_sync_group.py
index 94970641b..f3842c872 100644
--- a/rare/components/tabs/games/integrations/egl_sync_group.py
+++ b/rare/components/tabs/games/integrations/egl_sync_group.py
@@ -13,9 +13,10 @@
from rare.lgndr.glue.exception import LgndrException
from rare.models.pathspec import PathSpec
from rare.shared import RareCore
-from rare.shared.workers.wine_resolver import WineResolver
+from rare.shared.workers.wine_resolver import WinePathResolver
from rare.ui.components.tabs.games.integrations.egl_sync_group import Ui_EGLSyncGroup
from rare.ui.components.tabs.games.integrations.egl_sync_list_group import Ui_EGLSyncListGroup
+from rare.utils import runners
from rare.widgets.elide_label import ElideLabel
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
@@ -56,16 +57,9 @@ def __init__(self, parent=None):
self.egl_path_info.setEnabled(False)
else:
self.egl_path_edit.textChanged.connect(self.egl_path_changed)
- 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)
@@ -79,10 +73,24 @@ def __init__(self, parent=None):
# 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 = WinePathResolver(
+ self.core.get_app_launch_command("default"),
+ runners.get_environment(self.core.get_app_environment("default")),
+ PathSpec.egl_programdata()
+ )
+ 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(
@@ -109,14 +117,8 @@ def egl_path_edit_edit_cb(path) -> Tuple[bool, str, int]:
os.path.join(path, "dosdevices/c:")
):
# path is a wine prefix
- path = os.path.join(
- path,
- "dosdevices/c:",
- "ProgramData/Epic/EpicGamesLauncher/Data/Manifests",
- )
- elif not path.rstrip("/").endswith(
- "ProgramData/Epic/EpicGamesLauncher/Data/Manifests"
- ):
+ path = PathSpec.prefix_egl_programdata(path)
+ elif not path.rstrip("/").endswith(PathSpec.wine_egl_programdata()):
# lower() might or might not be needed in the check
return False, path, IndicatorReasonsCommon.WRONG_FORMAT
if os.path.exists(path):
@@ -166,13 +168,15 @@ def egl_sync_changed(self, state):
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()
- elif 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 = bool(self.core.egl.programdata_path) and os.path.isdir(self.core.egl.programdata_path)
self.ui.egl_sync_check_label.setEnabled(have_path)
self.ui.egl_sync_check.setEnabled(have_path)
self.import_list.populate(have_path)
diff --git a/rare/components/tabs/games/integrations/eos_group.py b/rare/components/tabs/games/integrations/eos_group.py
index 7cd8c1633..bd050201c 100644
--- a/rare/components/tabs/games/integrations/eos_group.py
+++ b/rare/components/tabs/games/integrations/eos_group.py
@@ -20,7 +20,7 @@
from rare.models.game import RareEosOverlay
from rare.shared import RareCore
from rare.ui.components.tabs.games.integrations.eos_widget import Ui_EosWidget
-from rare.utils import config_helper
+from rare.utils import config_helper as config
from rare.utils.misc import icon
from rare.widgets.elide_label import ElideLabel
@@ -51,7 +51,10 @@ def __init__(self, overlay: RareEosOverlay, prefix: Optional[str], parent=None):
self.indicator = QLabel(parent=self)
self.indicator.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Preferred)
- self.prefix_label = ElideLabel(prefix if prefix is not None else overlay.app_title, parent=self)
+ self.prefix_label = ElideLabel(
+ prefix.replace(os.path.expanduser("~"), "~") if prefix is not None else overlay.app_title,
+ parent=self,
+ )
self.overlay_label = ElideLabel(parent=self)
self.overlay_label.setDisabled(True)
@@ -128,10 +131,14 @@ def action(self) -> None:
if self.overlay.is_enabled(self.prefix) and (path == active_path):
if not self.overlay.disable(prefix=self.prefix):
QMessageBox.warning(
- self, "Warning",
+ self,
+ "Warning",
self.tr("Failed to completely disable the active EOS Overlay.{}").format(
- self.tr(" Since the previous overlay was managed by EGL you can safely ignore this is.")
- if active_path != install_path else ""
+ self.tr(
+ " Since the previous overlay was managed by EGL you can safely ignore this is."
+ )
+ if active_path != install_path
+ else ""
),
)
else:
@@ -141,7 +148,9 @@ def action(self) -> None:
self,
"Warning",
self.tr("Failed to completely enable EOS overlay.{}").format(
- self.tr(" Since the previous overlay was managed by EGL you can safely ignore this is.")
+ self.tr(
+ " Since the previous overlay was managed by EGL you can safely ignore this is."
+ )
if active_path != install_path
else ""
),
@@ -191,8 +200,11 @@ def __init__(self, parent=None):
self.ui.update_button.setEnabled(False)
self.threadpool = QThreadPool.globalInstance()
+ self.worker: Optional[CheckForUpdateWorker] = None
def showEvent(self, a0) -> None:
+ if a0.spontaneous():
+ return super().showEvent(a0)
self.check_for_update()
self.update_prefixes()
super().showEvent(a0)
@@ -202,7 +214,8 @@ def update_prefixes(self):
widget.deleteLater()
if platform.system() != "Windows":
- prefixes = config_helper.get_wine_prefixes()
+ prefixes = config.get_prefixes()
+ prefixes = {prefix for prefix in prefixes if config.prefix_exists(prefix)}
if platform.system() == "Darwin":
# TODO: add crossover support
pass
@@ -214,16 +227,21 @@ def update_prefixes(self):
widget = EosPrefixWidget(self.overlay, None)
self.ui.eos_layout.addWidget(widget)
+ @pyqtSlot(bool)
+ def worker_finished(self, update_available: bool):
+ self.worker = None
+ self.ui.update_button.setEnabled(update_available)
+
def check_for_update(self):
if not self.overlay.is_installed:
return
- def worker_finished(update_available):
- self.ui.update_button.setEnabled(update_available)
+ if self.worker is not None:
+ return
- worker = CheckForUpdateWorker(self.core)
- worker.signals.update_available.connect(worker_finished)
- QThreadPool.globalInstance().start(worker)
+ self.worker = CheckForUpdateWorker(self.core)
+ self.worker.signals.update_available.connect(self.worker_finished)
+ QThreadPool.globalInstance().start(self.worker)
@pyqtSlot()
def install_finished(self):
diff --git a/rare/components/tabs/games/integrations/ubisoft_group.py b/rare/components/tabs/games/integrations/ubisoft_group.py
index ca4070d36..eddd303df 100644
--- a/rare/components/tabs/games/integrations/ubisoft_group.py
+++ b/rare/components/tabs/games/integrations/ubisoft_group.py
@@ -5,18 +5,25 @@
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 PyQt5.QtWidgets import (
+ QFrame,
+ QLabel,
+ QHBoxLayout,
+ QSizePolicy,
+ QPushButton,
+ QGroupBox,
+ QVBoxLayout,
+)
from legendary.models.game import Game
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
-from rare.utils.metrics import timelogger
-
logger = getLogger("Ubisoft")
@@ -78,9 +85,7 @@ def run_real(self) -> None:
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))
@@ -112,9 +117,7 @@ def __init__(self, game: Game, ubi_account_id, activated: bool = False, parent=N
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))
- )
+ self.ok_indicator.setPixmap(icon("fa.check-circle-o", color="green").pixmap(QSize(20, 20)))
layout = QHBoxLayout(self)
layout.setContentsMargins(-1, 0, 0, 0)
@@ -125,28 +128,24 @@ def __init__(self, game: Game, ubi_account_id, activated: bool = False, parent=N
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(RareCore.instance().core(), None, None)
else:
- worker = UbiConnectWorker(RareCore.instance().core(), 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("fa.check-circle-o", 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.times-circle-o", 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)
@@ -203,7 +202,9 @@ def showEvent(self, a0: QShowEvent) -> None:
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.")
+ logger.error(
+ "No linked ubisoft account found! Link your accounts via your browser and try again."
+ )
self.info_label.setText(
self.tr("Your account is not linked with Ubisoft. Please link your account and try again.")
)
@@ -228,9 +229,7 @@ def show_ubi_games(self, redeemed: set, entitlements: set, ubi_account_id: str):
except (IndexError, KeyError):
app_name = "unknown"
- dlc_game = Game(
- app_name=app_name, app_title=dlc_data["title"], metadata=dlc_data
- )
+ 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:
@@ -244,24 +243,24 @@ def show_ubi_games(self, redeemed: set, entitlements: set, ubi_account_id: str):
uplay_games.append(game)
if not uplay_games:
- self.info_label.setText(
- self.tr("You don't own any Ubisoft games.")
- )
+ self.info_label.setText(self.tr("You don't own any Ubisoft games."))
else:
if activated == len(uplay_games):
- self.info_label.setText(
- self.tr("All your Ubisoft games have already been activated.")
- )
+ self.info_label.setText(self.tr("All your Ubisoft games have already been activated."))
else:
self.info_label.setText(
- self.tr("You have {} games available to redeem.").format(len(uplay_games) - activated)
+ self.tr("You have {} 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, ubi_account_id, activated=game.partner_link_id in redeemed, parent=self)
+ widget = UbiLinkWidget(
+ game, ubi_account_id, activated=game.partner_link_id in redeemed, parent=self
+ )
self.layout().addWidget(widget)
if self.args.debug:
@@ -269,7 +268,7 @@ def show_ubi_games(self, redeemed: set, entitlements: set, ubi_account_id: str):
Game(app_name="RareTestGame", app_title="Super Fake Super Rare Super Test (Super?) Game"),
ubi_account_id,
activated=False,
- parent=self
+ parent=self,
)
self.layout().addWidget(widget)
self.browser_button.setEnabled(True)
diff --git a/rare/components/tabs/settings/game_settings.py b/rare/components/tabs/settings/game_settings.py
index 201f5fee1..ea012dd4d 100644
--- a/rare/components/tabs/settings/game_settings.py
+++ b/rare/components/tabs/settings/game_settings.py
@@ -2,10 +2,7 @@
from logging import getLogger
from PyQt5.QtCore import QSettings, Qt
-from PyQt5.QtWidgets import (
- QWidget,
- QLabel
-)
+from PyQt5.QtWidgets import QWidget, QLabel
from rare.components.tabs.settings.widgets.env_vars import EnvVars
from rare.components.tabs.settings.widgets.linux import LinuxSettings
@@ -31,20 +28,18 @@ def __init__(self, is_default, parent=None):
self.wrapper_settings = WrapperSettings()
- self.ui.launch_settings_group.layout().addRow(
- QLabel("Wrapper"), self.wrapper_settings
- )
+ self.ui.launch_settings_group.layout().addRow(QLabel("Wrapper"), self.wrapper_settings)
self.env_vars = EnvVars(self)
self.ui.game_settings_layout.addWidget(self.env_vars)
if platform.system() != "Windows":
- self.linux_settings = LinuxAppSettings()
- self.proton_settings = ProtonSettings(self.linux_settings, self.wrapper_settings)
+ self.linux_settings = LinuxAppSettings(self)
+ self.proton_settings = ProtonSettings(self)
self.ui.proton_layout.addWidget(self.proton_settings)
# FIXME: Remove the spacerItem and margins from the linux settings
- # FIXME: This should be handled differently at soem point in the future
+ # FIXME: This should be handled differently at some point in the future
# NOTE: specerItem has been removed
self.linux_settings.layout().setContentsMargins(0, 0, 0, 0)
# FIXME: End of FIXME
@@ -53,11 +48,10 @@ def __init__(self, is_default, parent=None):
self.ui.game_settings_layout.setAlignment(Qt.AlignTop)
- self.linux_settings.mangohud.set_wrapper_activated.connect(
- lambda active: self.wrapper_settings.add_wrapper("mangohud")
- if active else self.wrapper_settings.delete_wrapper("mangohud"))
self.linux_settings.environ_changed.connect(self.env_vars.reset_model)
self.proton_settings.environ_changed.connect(self.env_vars.reset_model)
+ self.proton_settings.tool_enabled.connect(self.wrapper_settings.update_state)
+ self.proton_settings.tool_enabled.connect(self.linux_settings.tool_enabled)
else:
self.ui.linux_settings_widget.setVisible(False)
@@ -73,27 +67,29 @@ def load_settings(self, app_name):
self.app_name = app_name
self.wrapper_settings.load_settings(app_name)
if platform.system() != "Windows":
- self.linux_settings.update_game(app_name)
- proton = self.wrapper_settings.wrappers.get("proton", "")
- if proton:
- proton = proton.text
- self.proton_settings.load_settings(app_name, proton)
- if proton:
- self.linux_settings.ui.wine_groupbox.setEnabled(False)
- else:
- self.linux_settings.ui.wine_groupbox.setEnabled(True)
+ self.linux_settings.load_settings(app_name)
+ # proton = self.wrapper_settings.wrappers.get("proton", "")
+ # if proton:
+ # proton = proton.text
+ self.proton_settings.load_settings(app_name)
+ # proton = False
+ # if proton:
+ # self.linux_settings.ui.wine_groupbox.setEnabled(False)
+ # else:
+ # self.linux_settings.ui.wine_groupbox.setEnabled(True)
self.env_vars.update_game(app_name)
class LinuxAppSettings(LinuxSettings):
- def __init__(self):
- super(LinuxAppSettings, self).__init__()
+ def __init__(self, parent=None):
+ super(LinuxAppSettings, self).__init__(parent=parent)
+
+ def load_settings(self, app_name):
+ self.app_name = app_name
- def update_game(self, app_name):
- self.name = app_name
self.wine_prefix.setText(self.load_prefix())
- self.wine_exec.setText(self.load_setting(self.name, "wine_executable"))
+ self.wine_exec.setText(self.load_setting(self.app_name, "wine_executable"))
- self.dxvk.load_settings(self.name)
+ self.dxvk.load_settings(self.app_name)
- self.mangohud.load_settings(self.name)
+ self.mangohud.load_settings(self.app_name)
diff --git a/rare/components/tabs/settings/widgets/env_vars_model.py b/rare/components/tabs/settings/widgets/env_vars_model.py
index 795533e7d..7f2f9fce6 100644
--- a/rare/components/tabs/settings/widgets/env_vars_model.py
+++ b/rare/components/tabs/settings/widgets/env_vars_model.py
@@ -8,11 +8,12 @@
from rare.lgndr.core import LegendaryCore
from rare.utils.misc import icon
-from rare.utils import proton
+from rare.utils.runners.proton import get_steam_environment
+from rare.utils.runners.wine import get_wine_environment
class EnvVarsTableModel(QAbstractTableModel):
- def __init__(self, core: LegendaryCore, parent = None):
+ def __init__(self, core: LegendaryCore, parent=None):
super(EnvVarsTableModel, self).__init__(parent=parent)
self.core = core
@@ -23,12 +24,11 @@ def __init__(self, core: LegendaryCore, parent = None):
self.__data_map: ChainMap = ChainMap()
self.__readonly = [
- "STEAM_COMPAT_DATA_PATH",
- "WINEPREFIX",
"DXVK_HUD",
"MANGOHUD_CONFIG",
]
- self.__readonly.extend(proton.get_steam_environment(None).keys())
+ self.__readonly.extend(get_steam_environment().keys())
+ self.__readonly.extend(get_wine_environment().keys())
self.__default: str = "default"
self.__appname: str = None
@@ -250,8 +250,6 @@ def removeRow(self, row: int, parent: QModelIndex = None) -> bool:
if __name__ == "__main__":
from PyQt5.QtWidgets import QApplication, QDialog, QVBoxLayout, QTableView, QHeaderView
- from rare.resources import static_css
- from rare.resources.stylesheets import RareStyle
from rare.utils.misc import set_style_sheet
from legendary.core import LegendaryCore
diff --git a/rare/components/tabs/settings/widgets/linux.py b/rare/components/tabs/settings/widgets/linux.py
index b3a322bd4..2b7628df9 100644
--- a/rare/components/tabs/settings/widgets/linux.py
+++ b/rare/components/tabs/settings/widgets/linux.py
@@ -1,16 +1,15 @@
import os
-import shutil
from logging import getLogger
-from PyQt5.QtCore import pyqtSignal
+from PyQt5.QtCore import pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import QFileDialog, QWidget
from rare.components.tabs.settings.widgets.dxvk import DxvkSettings
from rare.components.tabs.settings.widgets.mangohud import MangoHudSettings
-from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
+from rare.shared.rare_core import RareCore
from rare.ui.components.tabs.settings.linux import Ui_LinuxSettings
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
-from rare.utils import config_helper
+from rare.utils import config_helper as config
logger = getLogger("LinuxSettings")
@@ -19,15 +18,15 @@ class LinuxSettings(QWidget):
# str: option key
environ_changed = pyqtSignal(str)
- def __init__(self, name=None, parent=None):
+ def __init__(self, app_name: str = None, parent=None):
super(LinuxSettings, self).__init__(parent=parent)
self.ui = Ui_LinuxSettings()
self.ui.setupUi(self)
- self.core = LegendaryCoreSingleton()
- self.signals = GlobalSignalsSingleton()
+ self.core = RareCore.instance().core()
+ self.signals = RareCore.instance().signals()
- self.name = name if name is not None else "default"
+ self.app_name = app_name if app_name is not None else "default"
# Wine prefix
self.wine_prefix = PathEdit(
@@ -40,12 +39,12 @@ def __init__(self, name=None, parent=None):
# Wine executable
self.wine_exec = PathEdit(
- self.load_setting(self.name, "wine_executable"),
+ self.load_setting(self.app_name, "wine_executable"),
file_mode=QFileDialog.ExistingFile,
name_filters=["wine", "wine64"],
edit_func=lambda text: (os.path.exists(text) or not text, text, IndicatorReasonsCommon.DIR_NOT_EXISTS),
save_func=lambda text: self.save_setting(
- text, section=self.name, setting="wine_executable"
+ text, section=self.app_name, setting="wine_executable"
),
)
self.ui.exec_layout.addWidget(self.wine_exec)
@@ -54,34 +53,45 @@ def __init__(self, name=None, parent=None):
self.dxvk = DxvkSettings()
self.dxvk.environ_changed.connect(self.environ_changed)
self.ui.linux_layout.addWidget(self.dxvk)
- self.dxvk.load_settings(self.name)
+ self.dxvk.load_settings(self.app_name)
self.mangohud = MangoHudSettings()
self.mangohud.environ_changed.connect(self.environ_changed)
self.ui.linux_layout.addWidget(self.mangohud)
- self.mangohud.load_settings(self.name)
+ self.mangohud.load_settings(self.app_name)
+
+ @pyqtSlot(bool)
+ def tool_enabled(self, enabled: bool):
+ if enabled:
+ config.remove_option(self.app_name, "no_wine")
+ else:
+ config.add_option(self.app_name, "no_wine", "true")
+ self.ui.wine_groupbox.setEnabled(not enabled)
+ self.wine_exec.setText("")
+ self.wine_prefix.setText("")
def load_prefix(self) -> str:
return self.load_setting(
- f"{self.name}.env",
+ f"{self.app_name}.env",
"WINEPREFIX",
- fallback=self.load_setting(self.name, "wine_prefix"),
+ fallback=self.load_setting(self.app_name, "wine_prefix"),
)
def save_prefix(self, text: str):
- self.save_setting(text, f"{self.name}.env", "WINEPREFIX")
+ self.save_setting(text, f"{self.app_name}.env", "WINEPREFIX")
self.environ_changed.emit("WINEPREFIX")
- self.save_setting(text, self.name, "wine_prefix")
+ self.save_setting(text, self.app_name, "wine_prefix")
self.signals.application.prefix_updated.emit()
def load_setting(self, section: str, setting: str, fallback: str = ""):
return self.core.lgd.config.get(section, setting, fallback=fallback)
- def save_setting(self, text: str, section: str, setting: str):
+ @staticmethod
+ def save_setting(text: str, section: str, setting: str):
if text:
- config_helper.add_option(section, setting, text)
+ config.add_option(section, setting, text)
logger.debug(f"Set {setting} in {f'[{section}]'} to {text}")
else:
- config_helper.remove_option(section, setting)
+ config.remove_option(section, setting)
logger.debug(f"Unset {setting} from {f'[{section}]'}")
- config_helper.save_config()
+ config.save_config()
diff --git a/rare/components/tabs/settings/widgets/mangohud.py b/rare/components/tabs/settings/widgets/mangohud.py
index e99570aaa..fd26ea469 100644
--- a/rare/components/tabs/settings/widgets/mangohud.py
+++ b/rare/components/tabs/settings/widgets/mangohud.py
@@ -13,7 +13,6 @@
class MangoHudSettings(OverlaySettings):
-
set_wrapper_activated = pyqtSignal(bool)
def __init__(self):
diff --git a/rare/components/tabs/settings/widgets/proton.py b/rare/components/tabs/settings/widgets/proton.py
index 658aaf872..0a6237659 100644
--- a/rare/components/tabs/settings/widgets/proton.py
+++ b/rare/components/tabs/settings/widgets/proton.py
@@ -1,85 +1,107 @@
import os
from logging import getLogger
-from pathlib import Path
-from typing import Tuple
+from typing import Tuple, Union, Optional
from PyQt5.QtCore import pyqtSignal
+from PyQt5.QtGui import QShowEvent
from PyQt5.QtWidgets import QGroupBox, QFileDialog
-from rare.components.tabs.settings import LinuxSettings
-from rare.shared import LegendaryCoreSingleton
+from rare.models.wrapper import Wrapper, WrapperType
+from rare.shared import RareCore
+from rare.shared.wrappers import Wrappers
from rare.ui.components.tabs.settings.proton import Ui_ProtonSettings
-from rare.utils import config_helper, proton
+from rare.utils import config_helper as config
+from rare.utils.runners import proton
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
-from .wrapper import WrapperSettings
-logger = getLogger("Proton")
+logger = getLogger("ProtonSettings")
class ProtonSettings(QGroupBox):
# str: option key
- environ_changed = pyqtSignal(str)
- app_name: str
- changeable = True
+ environ_changed: pyqtSignal = pyqtSignal(str)
+ # bool: state
+ tool_enabled: pyqtSignal = pyqtSignal(bool)
- def __init__(self, linux_settings: LinuxSettings, wrapper_settings: WrapperSettings):
- super(ProtonSettings, self).__init__()
+ def __init__(self, parent=None):
+ super(ProtonSettings, self).__init__(parent=parent)
self.ui = Ui_ProtonSettings()
self.ui.setupUi(self)
- self._linux_settings = linux_settings
- self._wrapper_settings = wrapper_settings
- self.core = LegendaryCoreSingleton()
- self.possible_proton_combos = proton.find_proton_combos()
-
- self.ui.proton_combo.addItems(self.possible_proton_combos)
- self.ui.proton_combo.currentIndexChanged.connect(self.change_proton)
+ self.ui.proton_combo.currentIndexChanged.connect(self.__on_proton_changed)
self.proton_prefix = PathEdit(
file_mode=QFileDialog.DirectoryOnly,
edit_func=self.proton_prefix_edit,
save_func=self.proton_prefix_save,
- placeholder=self.tr("Please select path for proton prefix")
+ placeholder=self.tr("Please select path for proton prefix"),
)
self.ui.prefix_layout.addWidget(self.proton_prefix)
- def change_proton(self, i):
- if not self.changeable:
- return
- # First combo box entry: Don't use Proton
- if i == 0:
- self._wrapper_settings.delete_wrapper("proton")
- config_helper.remove_option(self.app_name, "no_wine")
- config_helper.remove_option(f"{self.app_name}.env", "STEAM_COMPAT_DATA_PATH")
- self.environ_changed.emit("STEAM_COMPAT_DATA_PATH")
- config_helper.remove_option(f"{self.app_name}.env", "STEAM_COMPAT_CLIENT_INSTALL_PATH")
- self.environ_changed.emit("STEAM_COMPAT_CLIENT_INSTALL_PATH")
-
- self.proton_prefix.setEnabled(False)
- self.proton_prefix.setText("")
-
- self._linux_settings.ui.wine_groupbox.setEnabled(True)
+ self.app_name: str = "default"
+ self.core = RareCore.instance().core()
+ self.wrappers: Wrappers = RareCore.instance().wrappers()
+ self.tool_wrapper: Optional[Wrapper] = None
+
+ def showEvent(self, a0: QShowEvent) -> None:
+ if a0.spontaneous():
+ return super().showEvent(a0)
+ self.ui.proton_combo.blockSignals(True)
+ self.ui.proton_combo.clear()
+ self.ui.proton_combo.addItem(self.tr("Don't use a compatibility tool"), None)
+ tools = proton.find_tools()
+ for tool in tools:
+ self.ui.proton_combo.addItem(tool.name, tool)
+ try:
+ wrapper = next(
+ filter(lambda w: w.is_compat_tool, self.wrappers.get_game_wrapper_list(self.app_name))
+ )
+ self.tool_wrapper = wrapper
+ tool = next(filter(lambda t: t.checksum == wrapper.checksum, tools))
+ index = self.ui.proton_combo.findData(tool)
+ except StopIteration:
+ index = 0
+ self.ui.proton_combo.setCurrentIndex(index)
+ self.ui.proton_combo.blockSignals(False)
+ enabled = bool(self.ui.proton_combo.currentIndex())
+ self.proton_prefix.blockSignals(True)
+ self.proton_prefix.setText(config.get_envvar(self.app_name, "STEAM_COMPAT_DATA_PATH", fallback=""))
+ self.proton_prefix.setEnabled(enabled)
+ self.proton_prefix.blockSignals(False)
+ self.tool_enabled.emit(enabled)
+ super().showEvent(a0)
+
+ def __on_proton_changed(self, index):
+ steam_tool: Union[proton.ProtonTool, proton.CompatibilityTool] = self.ui.proton_combo.itemData(index)
+
+ steam_environ = proton.get_steam_environment(steam_tool)
+ for key, value in steam_environ.items():
+ if not value:
+ config.remove_envvar(self.app_name, key)
+ else:
+ config.add_envvar(self.app_name, key, value)
+ self.environ_changed.emit(key)
+
+ wrappers = self.wrappers.get_game_wrapper_list(self.app_name)
+ if self.tool_wrapper and self.tool_wrapper in wrappers:
+ wrappers.remove(self.tool_wrapper)
+ if steam_tool is None:
+ self.tool_wrapper = None
else:
- self.proton_prefix.setEnabled(True)
- self._linux_settings.ui.wine_groupbox.setEnabled(False)
- wrapper = self.possible_proton_combos[i - 1]
- self._wrapper_settings.add_wrapper(wrapper)
- config_helper.add_option(self.app_name, "no_wine", "true")
- config_helper.add_option(
- f"{self.app_name}.env",
- "STEAM_COMPAT_CLIENT_INSTALL_PATH",
- str(Path.home().joinpath(".steam", "steam"))
+ wrapper = Wrapper(
+ command=steam_tool.command(), name=steam_tool.name, wtype=WrapperType.COMPAT_TOOL
)
- self.environ_changed.emit("STEAM_COMPAT_CLIENT_INSTALL_PATH")
+ wrappers.append(wrapper)
+ self.tool_wrapper = wrapper
+ self.wrappers.set_game_wrapper_list(self.app_name, wrappers)
- self.proton_prefix.setText(os.path.expanduser("~/.proton"))
+ self.proton_prefix.setEnabled(steam_tool is not None)
+ self.proton_prefix.setText(os.path.expanduser("~/.proton") if steam_tool is not None else "")
- # Don't use Wine
- self._linux_settings.wine_exec.setText("")
- self._linux_settings.wine_prefix.setText("")
+ self.tool_enabled.emit(steam_tool is not None)
+ config.save_config()
- config_helper.save_config()
-
- def proton_prefix_edit(self, text: str) -> Tuple[bool, str, int]:
+ @staticmethod
+ def proton_prefix_edit(text: str) -> Tuple[bool, str, int]:
if not text:
return False, text, IndicatorReasonsCommon.EMPTY
parent_dir = os.path.dirname(text)
@@ -88,28 +110,9 @@ def proton_prefix_edit(self, text: str) -> Tuple[bool, str, int]:
def proton_prefix_save(self, text: str):
if not text:
return
- config_helper.add_option(
- f"{self.app_name}.env", "STEAM_COMPAT_DATA_PATH", text
- )
+ config.add_envvar(self.app_name, "STEAM_COMPAT_DATA_PATH", text)
self.environ_changed.emit("STEAM_COMPAT_DATA_PATH")
- config_helper.save_config()
+ config.save_config()
- def load_settings(self, app_name: str, proton: str):
- self.changeable = False
+ def load_settings(self, app_name: str):
self.app_name = app_name
- proton = proton.replace('"', "")
- self.proton_prefix.setEnabled(bool(proton))
- if proton:
- self.ui.proton_combo.setCurrentText(
- f'"{proton.replace(" run", "")}" run'
- )
- else:
- self.ui.proton_combo.setCurrentIndex(0)
-
- proton_prefix = self.core.lgd.config.get(
- f"{app_name}.env",
- "STEAM_COMPAT_DATA_PATH",
- fallback="",
- )
- self.proton_prefix.setText(proton_prefix)
- self.changeable = True
diff --git a/rare/components/tabs/settings/widgets/wrapper.py b/rare/components/tabs/settings/widgets/wrapper.py
index b8b35c435..f34b5fa1e 100644
--- a/rare/components/tabs/settings/widgets/wrapper.py
+++ b/rare/components/tabs/settings/widgets/wrapper.py
@@ -1,9 +1,10 @@
-import re
+import shlex
+import shlex
import shutil
from logging import getLogger
-from typing import Dict, Optional
+from typing import Optional
-from PyQt5.QtCore import pyqtSignal, QSettings, QSize, Qt, QMimeData, pyqtSlot, QCoreApplication
+from PyQt5.QtCore import pyqtSignal, QSize, Qt, QMimeData, pyqtSlot, QCoreApplication
from PyQt5.QtGui import QDrag, QDropEvent, QDragEnterEvent, QDragMoveEvent, QFont, QMouseEvent
from PyQt5.QtWidgets import (
QHBoxLayout,
@@ -16,46 +17,41 @@
QScrollArea,
QAction,
QToolButton,
- QMenu,
+ QMenu, QDialog,
)
+from rare.models.wrapper import Wrapper
from rare.shared import RareCore
from rare.ui.components.tabs.settings.widgets.wrapper import Ui_WrapperSettings
-from rare.utils import config_helper
from rare.utils.misc import icon
+from rare.utils.runners import proton
logger = getLogger("WrapperSettings")
-extra_wrapper_regex = {
- "proton": "\".*proton\" run", # proton
- "mangohud": "mangohud" # mangohud
-}
+# extra_wrapper_regex = {
+# "proton": "\".*proton\" run", # proton
+# }
-class Wrapper:
+class WrapperDialog(QDialog):
pass
class WrapperWidget(QFrame):
- update_wrapper = pyqtSignal(str, str)
- delete_wrapper = pyqtSignal(str)
+ # object: current, object: new
+ update_wrapper = pyqtSignal(object, object)
+ # object: current
+ delete_wrapper = pyqtSignal(object)
- def __init__(self, text: str, show_text=None, parent=None):
+ def __init__(self, wrapper: Wrapper, parent=None):
super(WrapperWidget, self).__init__(parent=parent)
- if not show_text:
- show_text = text.split()[0]
-
self.setFrameShape(QFrame.StyledPanel)
self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
+ self.setToolTip(wrapper.command)
- self.text = text
- self.setToolTip(text)
-
- unmanaged = show_text in extra_wrapper_regex.keys()
-
- text_lbl = QLabel(show_text, parent=self)
+ text_lbl = QLabel(wrapper.name, parent=self)
text_lbl.setFont(QFont("monospace"))
- text_lbl.setDisabled(unmanaged)
+ text_lbl.setEnabled(wrapper.is_editable)
image_lbl = QLabel(parent=self)
image_lbl.setPixmap(icon("mdi.drag-vertical").pixmap(QSize(20, 20)))
@@ -72,8 +68,8 @@ def __init__(self, text: str, show_text=None, parent=None):
manage_button.setIcon(icon("mdi.menu"))
manage_button.setMenu(manage_menu)
manage_button.setPopupMode(QToolButton.InstantPopup)
- manage_button.setDisabled(unmanaged)
- if unmanaged:
+ manage_button.setEnabled(wrapper.is_editable)
+ if not wrapper.is_editable:
manage_button.setToolTip(self.tr("Manage through settings"))
else:
manage_button.setToolTip(self.tr("Manage"))
@@ -85,28 +81,39 @@ def __init__(self, text: str, show_text=None, parent=None):
layout.addWidget(manage_button)
self.setLayout(layout)
+ self.wrapper = wrapper
+
# lk: set object names for the stylesheet
self.setObjectName(type(self).__name__)
manage_button.setObjectName(f"{self.objectName()}Button")
+ def data(self) -> Wrapper:
+ return self.wrapper
+
@pyqtSlot()
- def __delete(self):
- self.delete_wrapper.emit(self.text)
+ def __delete(self) -> None:
+ self.delete_wrapper.emit(self.wrapper)
+ self.deleteLater()
+ @pyqtSlot()
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)
+ dialog.setTextValue(self.wrapper.command)
accepted = dialog.exec()
- wrapper = dialog.textValue()
+ command = dialog.textValue()
dialog.deleteLater()
- if accepted and wrapper:
- self.update_wrapper.emit(self.text, wrapper)
+ if accepted and command:
+ new_wrapper = Wrapper(command=shlex.split(command))
+ self.update_wrapper.emit(self.wrapper, new_wrapper)
+ self.deleteLater()
def mouseMoveEvent(self, a0: QMouseEvent) -> None:
if a0.buttons() == Qt.LeftButton:
a0.accept()
+ if self.wrapper.is_compat_tool:
+ return
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
@@ -119,25 +126,17 @@ def __init__(self):
self.ui = Ui_WrapperSettings()
self.ui.setupUi(self)
- self.wrappers: Dict[str, WrapperWidget] = {}
- self.app_name: str = "default"
-
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)
self.wrapper_scroll.setProperty("no_kinetic_scroll", True)
- self.scroll_content = WrapperContainer(
- save_cb=self.save, parent=self.wrapper_scroll
- )
- self.wrapper_scroll.setWidget(self.scroll_content)
+ self.wrapper_container = WrapperContainer(parent=self.wrapper_scroll)
+ self.wrapper_container.orderChanged.connect(self.__on_order_changed)
+ self.wrapper_scroll.setWidget(self.wrapper_container)
self.ui.widget_stack.insertWidget(0, self.wrapper_scroll)
- self.core = RareCore.instance().core()
-
- self.ui.add_button.clicked.connect(self.add_button_pressed)
- self.settings = QSettings()
-
+ self.ui.add_button.clicked.connect(self.__on_add_button_pressed)
self.wrapper_scroll.horizontalScrollBar().rangeChanged.connect(self.adjust_scrollarea)
# lk: set object names for the stylesheet
@@ -149,18 +148,22 @@ def __init__(self):
self.wrapper_scroll.verticalScrollBar().setObjectName(
f"{self.wrapper_scroll.objectName()}Bar")
+ self.app_name: str = "default"
+ self.core = RareCore.instance().core()
+ self.wrappers = RareCore.instance().wrappers()
+
@pyqtSlot(int, int)
- def adjust_scrollarea(self, min: int, max: int):
- wrapper_widget = self.scroll_content.findChild(WrapperWidget)
+ def adjust_scrollarea(self, minh: int, maxh: int):
+ wrapper_widget = self.wrapper_container.findChild(WrapperWidget)
if not wrapper_widget:
- return
+ return
# lk: when the scrollbar is not visible, min and max are 0
- if max > min:
+ if maxh > minh:
self.wrapper_scroll.setMaximumHeight(
wrapper_widget.sizeHint().height()
+ self.wrapper_scroll.rect().height() // 2
- self.wrapper_scroll.contentsRect().height() // 2
- + self.scroll_content.layout().spacing()
+ + self.wrapper_container.layout().spacing()
+ self.wrapper_scroll.horizontalScrollBar().sizeHint().height()
)
else:
@@ -170,187 +173,183 @@ def adjust_scrollarea(self, min: int, max: int):
- self.wrapper_scroll.contentsRect().height()
)
- def get_wrapper_string(self):
- return " ".join(self.get_wrapper_list())
-
- def get_wrapper_list(self):
- wrappers = list(self.wrappers.values())
- wrappers.sort(key=lambda x: self.scroll_content.layout().indexOf(x))
- return [w.text for w in wrappers]
+ @pyqtSlot(QWidget, int)
+ def __on_order_changed(self, widget: WrapperWidget, new_index: int):
+ wrapper = widget.data()
+ wrappers = self.wrappers.get_game_wrapper_list(self.app_name)
+ wrappers.remove(wrapper)
+ wrappers.insert(new_index, wrapper)
+ self.wrappers.set_game_wrapper_list(self.app_name, wrappers)
- def add_button_pressed(self):
+ @pyqtSlot()
+ def __on_add_button_pressed(self):
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()
+ command = dialog.textValue()
dialog.deleteLater()
if accepted:
- self.add_wrapper(wrapper)
-
- def add_wrapper(self, text: str, position: int = -1, from_load: bool = False):
- if text == "mangohud" and self.wrappers.get("mangohud"):
- return
- 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, self.tr("Warning"), self.tr("Wrapper {0} is already in the list").format(text)
- )
- return
-
- if show_text != "proton" and not shutil.which(text.split()[0]):
- if (
- QMessageBox.question(
- self,
- self.tr("Warning"),
- self.tr("Wrapper {0} 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,
- self.tr("Warning"),
- self.tr("Do not insert proton manually. Add it through Proton settings"),
- )
- return
+ wrapper = Wrapper(shlex.split(command))
+ self.add_user_wrapper(wrapper)
+ def __add_wrapper(self, wrapper: Wrapper, position: int = -1):
self.ui.widget_stack.setCurrentIndex(0)
-
- if widget := self.wrappers.get(show_text, None):
- widget.deleteLater()
-
- widget = WrapperWidget(text, show_text, self.scroll_content)
+ widget = WrapperWidget(wrapper, self.wrapper_container)
if position < 0:
- self.scroll_content.layout().addWidget(widget)
+ self.wrapper_container.addWidget(widget)
else:
- self.scroll_content.layout().insertWidget(position, widget)
+ self.wrapper_container.insertWidget(position, widget)
self.adjust_scrollarea(
self.wrapper_scroll.horizontalScrollBar().minimum(),
self.wrapper_scroll.horizontalScrollBar().maximum(),
)
- widget.update_wrapper.connect(self.update_wrapper)
- widget.delete_wrapper.connect(self.delete_wrapper)
-
- self.wrappers[show_text] = widget
+ widget.update_wrapper.connect(self.__update_wrapper)
+ widget.delete_wrapper.connect(self.__delete_wrapper)
- 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.ui.label_page.sizeHint().height())
- self.ui.widget_stack.setCurrentIndex(1)
+ def add_wrapper(self, wrapper: Wrapper, position: int = -1):
+ wrappers = self.wrappers.get_game_wrapper_list(self.app_name)
+ if position < 0 or wrapper.is_compat_tool:
+ wrappers.append(wrapper)
+ else:
+ wrappers.insert(position, wrapper)
+ self.wrappers.set_game_wrapper_list(self.app_name, wrappers)
+ self.__add_wrapper(wrapper, position)
- self.save()
+ def add_user_wrapper(self, wrapper: Wrapper, position: int = -1):
+ if not wrapper:
+ return
- @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)
+ compat_cmds = [tool.command() for tool in proton.find_tools()]
+ if wrapper.command in compat_cmds:
+ QMessageBox.warning(
+ self,
+ self.tr("Warning"),
+ self.tr("Do not insert compatibility tools manually. Add them through Proton settings"),
+ )
+ return
- def save(self):
- # save wrappers twice, to support wrappers with spaces
- if len(self.wrappers) == 0:
- config_helper.remove_option(self.app_name, "wrapper")
- self.settings.remove(f"{self.app_name}/wrapper")
- else:
- config_helper.add_option(self.app_name, "wrapper", self.get_wrapper_string())
- self.settings.setValue(f"{self.app_name}/wrapper", self.get_wrapper_list())
+ # if text == "mangohud" and self.wrappers.get("mangohud"):
+ # return
+ # 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]
+
+ if wrapper.checksum in self.wrappers.get_game_md5sum_list(self.app_name):
+ QMessageBox.warning(
+ self, self.tr("Warning"), self.tr("Wrapper {0} is already in the list").format(wrapper.command)
+ )
+ return
- def load_settings(self, app_name: str):
- self.app_name = app_name
- for i in self.wrappers.values():
- i.deleteLater()
- self.wrappers.clear()
+ if not shutil.which(wrapper.executable):
+ ans = QMessageBox.question(
+ self,
+ self.tr("Warning"),
+ self.tr("Wrapper {0} is not in $PATH. Add it anyway?").format(wrapper.executable),
+ QMessageBox.Yes | QMessageBox.No,
+ QMessageBox.No
+ )
+ if ans == QMessageBox.No:
+ return
- wrappers = self.settings.value(f"{self.app_name}/wrapper", [], str)
+ self.add_wrapper(wrapper, position)
- if not wrappers and (cfg := self.core.lgd.config.get(self.app_name, "wrapper", fallback="")):
- logger.info("Loading wrappers from legendary config")
- # no qt wrapper, but legendary wrapper, to have backward compatibility
- pattern = re.compile(r'''((?:[^ "']|"[^"]*"|'[^']*')+)''')
- wrappers = pattern.split(cfg)[1::2]
+ @pyqtSlot(object)
+ def __delete_wrapper(self, wrapper: Wrapper):
+ wrappers = self.wrappers.get_game_wrapper_list(self.app_name)
+ wrappers.remove(wrapper)
+ self.wrappers.set_game_wrapper_list(self.app_name, wrappers)
+ if not wrappers:
+ self.wrapper_scroll.setMaximumHeight(self.ui.label_page.sizeHint().height())
+ self.ui.widget_stack.setCurrentIndex(1)
- for wrapper in wrappers:
- self.add_wrapper(wrapper, from_load=True)
+ @pyqtSlot(object, object)
+ def __update_wrapper(self, old: Wrapper, new: Wrapper):
+ wrappers = self.wrappers.get_game_wrapper_list(self.app_name)
+ index = wrappers.index(old)
+ wrappers.remove(old)
+ wrappers.insert(index, new)
+ self.wrappers.set_game_wrapper_list(self.app_name, wrappers)
+ self.__add_wrapper(new, index)
- if not self.wrappers:
+ @pyqtSlot()
+ def update_state(self):
+ for w in self.wrapper_container.findChildren(WrapperWidget, options=Qt.FindDirectChildrenOnly):
+ w.deleteLater()
+ wrappers = self.wrappers.get_game_wrapper_list(self.app_name)
+ if not wrappers:
self.wrapper_scroll.setMaximumHeight(self.ui.label_page.sizeHint().height())
self.ui.widget_stack.setCurrentIndex(1)
else:
self.ui.widget_stack.setCurrentIndex(0)
+ for wrapper in wrappers:
+ self.__add_wrapper(wrapper)
- self.save()
+ def load_settings(self, app_name: str):
+ self.app_name = app_name
+ self.update_state()
class WrapperContainer(QWidget):
+ # QWidget: moving widget, int: new index
+ orderChanged: pyqtSignal = pyqtSignal(QWidget, int)
- def __init__(self, save_cb, parent=None):
+ def __init__(self, parent=None):
super(WrapperContainer, self).__init__(parent=parent)
self.setAcceptDrops(True)
- self.save = save_cb
- layout = QHBoxLayout()
- layout.setContentsMargins(0, 0, 0, 0)
- layout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
- self.setLayout(layout)
+ self.__layout = QHBoxLayout(self)
+ self.__layout.setContentsMargins(0, 0, 0, 0)
+ self.__layout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
- self.drag_widget: Optional[QWidget] = None
+ self.__drag_widget: Optional[QWidget] = None
# lk: set object names for the stylesheet
self.setObjectName(type(self).__name__)
+ # def count(self) -> int:
+ # return self.__layout.count()
+ #
+ # def itemData(self, index: int) -> Any:
+ # widget: WrapperWidget = self.__layout.itemAt(index).widget()
+ # return widget.data()
+
+ def addWidget(self, widget: WrapperWidget):
+ self.__layout.addWidget(widget)
+
+ def insertWidget(self, index: int, widget: WrapperWidget):
+ self.__layout.insertWidget(index, widget)
+
def dragEnterEvent(self, e: QDragEnterEvent):
widget = e.source()
- self.drag_widget = widget
+ self.__drag_widget = widget
e.accept()
- def _get_drop_index(self, x):
- drag_idx = self.layout().indexOf(self.drag_widget)
+ def __get_drop_index(self, x) -> int:
+ drag_idx = self.__layout.indexOf(self.__drag_widget)
if drag_idx > 0:
- prev_widget = self.layout().itemAt(drag_idx - 1).widget()
- if x < self.drag_widget.x() - prev_widget.width() // 2:
+ prev_widget = self.__layout.itemAt(drag_idx - 1).widget()
+ if x < self.__drag_widget.x() - prev_widget.width() // 2:
return drag_idx - 1
- if drag_idx < self.layout().count() - 1:
- next_widget = self.layout().itemAt(drag_idx + 1).widget()
- if x > self.drag_widget.x() + self.drag_widget.width() + next_widget.width() // 2:
+ if drag_idx < self.__layout.count() - 1:
+ next_widget = self.__layout.itemAt(drag_idx + 1).widget()
+ if x > self.__drag_widget.x() + self.__drag_widget.width() + next_widget.width() // 2:
return drag_idx + 1
return drag_idx
def dragMoveEvent(self, e: QDragMoveEvent) -> None:
- i = self._get_drop_index(e.pos().x())
- self.layout().insertWidget(i, self.drag_widget)
+ new_x = self.__get_drop_index(e.pos().x())
+ self.__layout.insertWidget(new_x, self.__drag_widget)
def dropEvent(self, e: QDropEvent):
pos = e.pos()
widget = e.source()
- index = self._get_drop_index(pos.x())
- self.layout().insertWidget(index, widget)
- self.drag_widget = None
+ new_x = self.__get_drop_index(pos.x())
+ self.__layout.insertWidget(new_x, widget)
+ self.__drag_widget = None
+ self.orderChanged.emit(widget, new_x)
e.accept()
- self.save()
diff --git a/rare/lgndr/cli.py b/rare/lgndr/cli.py
index 54c9262db..49e2747b9 100644
--- a/rare/lgndr/cli.py
+++ b/rare/lgndr/cli.py
@@ -6,6 +6,7 @@
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
@@ -535,6 +536,8 @@ def import_game(self, args: LgndrImportGameArgs) -> None:
# 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))
diff --git a/rare/lgndr/downloader/mp/manager.py b/rare/lgndr/downloader/mp/manager.py
index 9a7836683..2168f4b4f 100644
--- a/rare/lgndr/downloader/mp/manager.py
+++ b/rare/lgndr/downloader/mp/manager.py
@@ -72,7 +72,7 @@ def run_real(self):
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 @@ def run_real(self):
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 @@ def run_real(self):
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)
diff --git a/rare/main.py b/rare/main.py
index 54c2d661e..74d67fd93 100755
--- a/rare/main.py
+++ b/rare/main.py
@@ -18,6 +18,8 @@ def main() -> int:
sys.stderr = open(os.devnull, 'w')
os.environ["QT_QPA_PLATFORMTHEME"] = ""
+ os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1"
+ os.environ["QT_SCALE_FACTOR_ROUNDING_POLICY"] = "Floor"
# fix cx_freeze
multiprocessing.freeze_support()
diff --git a/rare/models/game.py b/rare/models/game.py
index dd4dea655..96ea63f6a 100644
--- a/rare/models/game.py
+++ b/rare/models/game.py
@@ -2,7 +2,7 @@
import os
import platform
from dataclasses import dataclass, field
-from datetime import datetime
+from datetime import datetime, UTC
from logging import getLogger
from threading import Lock
from typing import List, Optional, Dict, Set
@@ -31,7 +31,7 @@ class Metadata:
queued: bool = False
queue_pos: Optional[int] = None
last_played: datetime = datetime.min
- grant_date: Optional[datetime] = None
+ grant_date: datetime = datetime.min
steam_appid: Optional[int] = None
steam_grade: Optional[str] = None
steam_date: datetime = datetime.min
@@ -43,7 +43,7 @@ def from_dict(cls, data: Dict):
queued=data.get("queued", False),
queue_pos=data.get("queue_pos", None),
last_played=datetime.fromisoformat(x) if (x := data.get("last_played", None)) else datetime.min,
- grant_date=datetime.fromisoformat(x) if (x := data.get("grant_date", None)) else None,
+ grant_date=datetime.fromisoformat(x) if (x := data.get("grant_date", None)) else datetime.min,
steam_appid=data.get("steam_appid", None),
steam_grade=data.get("steam_grade", None),
steam_date=datetime.fromisoformat(x) if (x := data.get("steam_date", None)) else datetime.min,
@@ -56,7 +56,7 @@ def __dict__(self):
queued=self.queued,
queue_pos=self.queue_pos,
last_played=self.last_played.isoformat() if self.last_played else datetime.min,
- grant_date=self.grant_date.isoformat() if self.grant_date else None,
+ grant_date=self.grant_date.isoformat() if self.grant_date else datetime.min,
steam_appid=self.steam_appid,
steam_grade=self.steam_grade,
steam_date=self.steam_date.isoformat() if self.steam_date else datetime.min,
@@ -80,8 +80,7 @@ def __init__(self, legendary_core: LegendaryCore, image_manager: ImageManager, g
self.pixmap: QPixmap = QPixmap()
self.metadata: RareGame.Metadata = RareGame.Metadata()
self.__load_metadata()
- if self.metadata.grant_date is None:
- self.grant_date()
+ self.grant_date()
self.owned_dlcs: Set[RareGame] = set()
@@ -479,17 +478,17 @@ def set_steam_grade(self, appid: int, grade: str) -> None:
def grant_date(self, force=False) -> datetime:
if (entitlements := self.core.lgd.entitlements) is None:
- return self.metadata.grant_date
- if self.metadata.grant_date is None or force:
+ return self.metadata.grant_date.replace(tzinfo=UTC)
+ if self.metadata.grant_date == datetime.min.replace(tzinfo=UTC) or force:
logger.debug("Grant date for %s not found in metadata, resolving", self.app_name)
matching = filter(lambda ent: ent["namespace"] == self.game.namespace, entitlements)
entitlement = next(matching, None)
grant_date = datetime.fromisoformat(
entitlement["grantDate"].replace("Z", "+00:00")
- ) if entitlement else None
+ ) if entitlement else datetime.min.replace(tzinfo=UTC)
self.metadata.grant_date = grant_date
self.__save_metadata()
- return self.metadata.grant_date
+ return self.metadata.grant_date.replace(tzinfo=UTC)
def set_origin_attributes(self, path: str, size: int = 0) -> None:
self.__origin_install_path = path
diff --git a/rare/models/pathspec.py b/rare/models/pathspec.py
index 24c9d1a5e..5249476ef 100644
--- a/rare/models/pathspec.py
+++ b/rare/models/pathspec.py
@@ -2,46 +2,72 @@
from typing import Union, List
from legendary.core import LegendaryCore
+from legendary.models.game import InstalledGame
+
+from rare.utils.config_helper import get_prefixes
class PathSpec:
- __egl_path_vars = {
- "{appdata}": os.path.expandvars("%LOCALAPPDATA%"),
- "{userdir}": os.path.expandvars("%USERPROFILE%/Documents"),
- # '{userprofile}': os.path.expandvars('%userprofile%'), # possibly wrong
- "{usersavedgames}": os.path.expandvars("%USERPROFILE%/Saved Games"),
- }
- egl_appdata: str = r"%LOCALAPPDATA%\EpicGamesLauncher\Saved\Config\Windows"
- egl_programdata: str = r"%PROGRAMDATA%\Epic\EpicGamesLauncher\Data\Manifests"
- wine_programdata: str = r"dosdevices/c:/ProgramData"
-
- def __init__(self, core: LegendaryCore = None, app_name: str = "default"):
- if core is not None:
- self.__egl_path_vars.update({"{epicid}": core.lgd.userdata["account_id"]})
- self.app_name = app_name
- def cook(self, path: str) -> str:
- cooked_path = [self.__egl_path_vars.get(p.lower(), p) for p in path.split("/")]
- return os.path.join(*cooked_path)
+ @staticmethod
+ def egl_appdata() -> str:
+ return r"%LOCALAPPDATA%\EpicGamesLauncher\Saved\Config\Windows"
+
+ @staticmethod
+ def egl_programdata() -> str:
+ return r"%PROGRAMDATA%\Epic\EpicGamesLauncher\Data\Manifests"
+
+ @staticmethod
+ def wine_programdata() -> str:
+ return r"ProgramData"
+
+ @staticmethod
+ def wine_egl_programdata() -> str:
+ return PathSpec.egl_programdata(
+ ).replace(
+ "\\", "/"
+ ).replace(
+ "%PROGRAMDATA%", PathSpec.wine_programdata()
+ )
- @property
- def wine_egl_programdata(self):
- return self.egl_programdata.replace("\\", "/").replace("%PROGRAMDATA%", self.wine_programdata)
+ @staticmethod
+ def prefix_egl_programdata(prefix: str) -> str:
+ return os.path.join(prefix, "dosdevices/c:", PathSpec.wine_egl_programdata())
- def wine_egl_prefixes(self, results: int = 0) -> Union[List[str], str]:
- possible_prefixes = [
- os.path.expanduser("~/.wine"),
- os.path.expanduser("~/Games/epic-games-store"),
- ]
+ @staticmethod
+ def wine_egl_prefixes(results: int = 0) -> Union[List[str], str]:
+ possible_prefixes = get_prefixes()
prefixes = []
for prefix in possible_prefixes:
- if os.path.exists(os.path.join(prefix, self.wine_egl_programdata)):
+ if os.path.exists(os.path.join(prefix, PathSpec.wine_egl_programdata())):
prefixes.append(prefix)
if not prefixes:
- return str()
+ return ""
if not results:
return prefixes
elif results == 1:
return prefixes[0]
else:
return prefixes[:results]
+
+ def __init__(self, core: LegendaryCore = None, igame: InstalledGame = None):
+ self.__egl_path_vars = {
+ "{appdata}": os.path.expandvars("%LOCALAPPDATA%"),
+ "{userdir}": os.path.expandvars("%USERPROFILE%/Documents"),
+ "{userprofile}": os.path.expandvars("%userprofile%"), # possibly wrong
+ "{usersavedgames}": os.path.expandvars("%USERPROFILE%/Saved Games"),
+ }
+
+ if core is not None:
+ self.__egl_path_vars.update({
+ "{epicid}": core.lgd.userdata["account_id"]
+ })
+
+ if igame is not None:
+ self.__egl_path_vars.update({
+ "{installdir}": igame.install_path,
+ })
+
+ def resolve_egl_path_vars(self, path: str) -> str:
+ cooked_path = [self.__egl_path_vars.get(p.lower(), p) for p in path.split("/")]
+ return os.path.join(*cooked_path)
diff --git a/rare/models/wrapper.py b/rare/models/wrapper.py
new file mode 100644
index 000000000..09a0fc9e2
--- /dev/null
+++ b/rare/models/wrapper.py
@@ -0,0 +1,74 @@
+import os
+import shlex
+from hashlib import md5
+from enum import IntEnum
+from typing import Dict, List, Union
+
+
+class WrapperType(IntEnum):
+ NONE = 0
+ COMPAT_TOOL = 1
+ LEGENDARY_IMPORT = 8
+ USER_DEFINED = 9
+
+
+class Wrapper:
+ def __init__(self, command: Union[str, List[str]], name: str = None, wtype: WrapperType = None):
+ self.__command: List[str] = shlex.split(command) if isinstance(command, str) else command
+ self.__name: str = name if name is not None else os.path.basename(self.__command[0])
+ self.__wtype: WrapperType = wtype if wtype is not None else WrapperType.USER_DEFINED
+
+ @property
+ def is_compat_tool(self) -> bool:
+ return self.__wtype == WrapperType.COMPAT_TOOL
+
+ @property
+ def is_editable(self) -> bool:
+ return self.__wtype == WrapperType.USER_DEFINED or self.__wtype == WrapperType.LEGENDARY_IMPORT
+
+ @property
+ def checksum(self) -> str:
+ return md5(self.command.encode("utf-8")).hexdigest()
+
+ @property
+ def executable(self) -> str:
+ return shlex.quote(self.__command[0])
+
+ @property
+ def command(self) -> str:
+ return " ".join(shlex.quote(part) for part in self.__command)
+
+ @property
+ def name(self) -> str:
+ return self.__name
+
+ @property
+ def type(self) -> WrapperType:
+ return self.__wtype
+
+ def __eq__(self, other) -> bool:
+ return self.command == other.command
+
+ def __hash__(self):
+ return hash(self.__command)
+
+ def __bool__(self) -> bool:
+ if not self.is_editable:
+ return True
+ return bool(self.command.strip())
+
+ @classmethod
+ def from_dict(cls, data: Dict):
+ return cls(
+ command=data.get("command"),
+ name=data.get("name"),
+ wtype=WrapperType(data.get("wtype", WrapperType.USER_DEFINED))
+ )
+
+ @property
+ def __dict__(self):
+ return dict(
+ command=self.__command,
+ name=self.__name,
+ wtype=int(self.__wtype)
+ )
diff --git a/rare/shared/game_process.py b/rare/shared/game_process.py
index 784b2f9af..be69f335b 100644
--- a/rare/shared/game_process.py
+++ b/rare/shared/game_process.py
@@ -117,7 +117,8 @@ def __on_error(self, _: QLocalSocket.LocalSocketError):
self.timer.stop()
self.__close()
self.__game_finished(GameProcess.Code.ON_STARTUP) # 1234 is exit code for startup
- logger.error(f"{self.game.app_name} ({self.game.app_title}): {self.socket.errorString()}")
+ else:
+ logger.error(f"{self.game.app_name} ({self.game.app_title}): {self.socket.errorString()}")
def __game_finished(self, exit_code: int):
self.finished.emit(exit_code)
diff --git a/rare/shared/rare_core.py b/rare/shared/rare_core.py
index 86799a6e1..d57c9dfa2 100644
--- a/rare/shared/rare_core.py
+++ b/rare/shared/rare_core.py
@@ -29,6 +29,7 @@
)
from .workers.uninstall import uninstall_game
from .workers.worker import QueueWorkerInfo, QueueWorkerState
+from .wrappers import Wrappers
logger = getLogger("RareCore")
@@ -52,6 +53,8 @@ def __init__(self, args: Namespace):
self.__signals: Optional[GlobalSignals] = None
self.__core: Optional[LegendaryCore] = None
self.__image_manager: Optional[ImageManager] = None
+ self.__settings: Optional[QSettings] = None
+ self.__wrappers: Optional[Wrappers] = None
self.__start_time = time.perf_counter()
@@ -60,8 +63,8 @@ def __init__(self, args: Namespace):
self.core(init=True)
config_helper.init_config_handler(self.__core)
self.image_manager(init=True)
-
- self.settings = QSettings()
+ self.__settings = QSettings()
+ self.__wrappers = Wrappers()
self.queue_workers: List[QueueWorker] = []
self.queue_threadpool = QThreadPool()
@@ -171,6 +174,12 @@ def image_manager(self, init: bool = False) -> ImageManager:
self.__image_manager = ImageManager(self.signals(), self.core())
return self.__image_manager
+ def wrappers(self) -> Wrappers:
+ return self.__wrappers
+
+ def settings(self) -> QSettings:
+ return self.__settings
+
def deleteLater(self) -> None:
self.__image_manager.deleteLater()
del self.__image_manager
@@ -295,6 +304,9 @@ def __on_fetch_result(self, result: Tuple, result_type: int):
if all([self.__fetched_games_dlcs, self.__fetched_entitlements]):
logger.debug(f"Fetch time {time.perf_counter() - self.__start_time} seconds")
+ self.__wrappers.import_wrappers(
+ self.__core, self.__settings, [rgame.app_name for rgame in self.games]
+ )
self.progress.emit(100, self.tr("Launching Rare"))
self.completed.emit()
QTimer.singleShot(100, self.__post_init)
diff --git a/rare/shared/workers/wine_resolver.py b/rare/shared/workers/wine_resolver.py
index 891bd71aa..38b9bb131 100644
--- a/rare/shared/workers/wine_resolver.py
+++ b/rare/shared/workers/wine_resolver.py
@@ -3,50 +3,71 @@
import time
from configparser import ConfigParser
from logging import getLogger
-from typing import Union, Iterable
+from typing import Union, Iterable, Mapping, List
from PyQt5.QtCore import pyqtSignal, QObject, QRunnable
-import rare.utils.wine as wine
from rare.lgndr.core import LegendaryCore
from rare.models.game import RareGame
from rare.models.pathspec import PathSpec
+from rare.utils import runners, config_helper as config
from rare.utils.misc import path_size, format_size
from .worker import Worker
if platform.system() == "Windows":
# noinspection PyUnresolvedReferences
- import winreg # pylint: disable=E0401
+ import winreg # pylint: disable=E0401
from legendary.lfs import windows_helpers
logger = getLogger("WineResolver")
-class WineResolver(Worker):
+class WinePathResolver(Worker):
class Signals(QObject):
- result_ready = pyqtSignal(str)
+ result_ready = pyqtSignal(str, str)
+
+ def __init__(self, command: List[str], environ: Mapping, path: str):
+ super(WinePathResolver, self). __init__()
+ self.signals = WinePathResolver.Signals()
+ self.command = command
+ self.environ = environ
+ self.path = path
+
+ @staticmethod
+ def _resolve_unix_path(cmd, env, path: str) -> str:
+ logger.info("Resolving path '%s'", path)
+ wine_path = runners.resolve_path(cmd, env, path)
+ logger.debug("Resolved Wine path '%s'", path)
+ unix_path = runners.convert_to_unix_path(cmd, env, wine_path)
+ logger.debug("Resolved Unix path '%s'", unix_path)
+ return unix_path
+
+ def run_real(self):
+ path = self._resolve_unix_path(self.command, self.environ, self.path)
+ self.signals.result_ready.emit(path, "default")
+ return
- def __init__(self, core: LegendaryCore, path: str, app_name: str):
- super(WineResolver, self).__init__()
- self.signals = WineResolver.Signals()
- self.wine_env = wine.environ(core, app_name)
- self.wine_exec = wine.wine(core, app_name)
- self.path = PathSpec(core, app_name).cook(path)
+
+class WineSavePathResolver(WinePathResolver):
+
+ def __init__(self, core: LegendaryCore, rgame: RareGame):
+ cmd = core.get_app_launch_command(rgame.app_name)
+ env = core.get_app_environment(rgame.app_name)
+ env = runners.get_environment(env, silent=True)
+ path = PathSpec(core, rgame.igame).resolve_egl_path_vars(rgame.raw_save_path)
+ if not (cmd and env and path):
+ raise RuntimeError(f"Cannot setup {type(self).__name__}, missing infomation")
+ super(WineSavePathResolver, self).__init__(cmd, env, path)
+ self.rgame = rgame
def run_real(self):
- if "WINEPREFIX" not in self.wine_env or not os.path.exists(self.wine_env["WINEPREFIX"]):
- # pylint: disable=E1136
- self.signals.result_ready[str].emit("")
- return
- if not os.path.exists(self.wine_exec):
- # pylint: disable=E1136
- self.signals.result_ready[str].emit("")
- return
- path = wine.resolve_path(self.wine_exec, self.wine_env, self.path)
+ logger.info("Resolving save path for %s (%s)", self.rgame.app_title, self.rgame.app_name)
+ path = self._resolve_unix_path(self.command, self.environ, self.path)
# Clean wine output
- real_path = wine.convert_to_unix_path(self.wine_exec, self.wine_env, path)
# pylint: disable=E1136
- self.signals.result_ready[str].emit(real_path)
+ if os.path.exists(path):
+ self.rgame.save_path = path
+ self.signals.result_ready.emit(path, self.rgame.app_name)
return
@@ -55,9 +76,7 @@ def __init__(self, core: LegendaryCore, games: Union[Iterable[RareGame], RareGam
super(OriginWineWorker, self).__init__()
self.__cache: dict[str, ConfigParser] = {}
self.core = core
- if isinstance(games, RareGame):
- games = [games]
- self.games = games
+ self.games = [games] if isinstance(games, RareGame) else games
def run(self) -> None:
t = time.time()
@@ -79,15 +98,19 @@ def run(self) -> None:
if platform.system() == "Windows":
install_dir = windows_helpers.query_registry_value(winreg.HKEY_LOCAL_MACHINE, reg_path, reg_key)
else:
- wine_env = wine.environ(self.core, rgame.app_name)
- wine_exec = wine.wine(self.core, rgame.app_name)
+ command = self.core.get_app_launch_command(rgame.app_name)
+ environ = self.core.get_app_environment(rgame.app_name)
+ environ = runners.get_environment(environ, silent=True)
+
+ prefix = config.get_prefix(rgame.app_name)
+ if not prefix:
+ return
use_wine = False
if not use_wine:
- # lk: this is the original way of gettijng the path by parsing "system.reg"
- wine_prefix = wine.prefix(self.core, rgame.app_name)
- reg = self.__cache.get(wine_prefix, None) or wine.read_registry("system.reg", wine_prefix)
- self.__cache[wine_prefix] = reg
+ # lk: this is the original way of getting the path by parsing "system.reg"
+ reg = self.__cache.get(prefix, None) or runners.read_registry("system.reg", prefix)
+ self.__cache[prefix] = reg
reg_path = reg_path.replace("SOFTWARE", "Software").replace("WOW6432Node", "Wow6432Node")
# lk: split and rejoin the registry path to avoid slash expansion
@@ -96,11 +119,11 @@ def run(self) -> None:
install_dir = reg.get(reg_path, f'"{reg_key}"', fallback=None)
else:
# lk: this is the alternative way of getting the path by using wine itself
- install_dir = wine.query_reg_key(wine_exec, wine_env, f"HKLM\\{reg_path}", reg_key)
+ install_dir = runners.query_reg_key(command, environ, f"HKLM\\{reg_path}", reg_key)
if install_dir:
logger.debug("Found Wine install directory %s", install_dir)
- install_dir = wine.convert_to_unix_path(wine_exec, wine_env, install_dir)
+ install_dir = runners.convert_to_unix_path(command, environ, install_dir)
if install_dir:
logger.debug("Found Unix install directory %s", install_dir)
else:
diff --git a/rare/shared/wrappers.py b/rare/shared/wrappers.py
new file mode 100644
index 000000000..3d1469198
--- /dev/null
+++ b/rare/shared/wrappers.py
@@ -0,0 +1,169 @@
+import json
+import os
+from logging import getLogger
+import shlex
+from typing import List, Dict, Iterable
+from rare.utils import config_helper as config
+
+from PyQt5.QtCore import QSettings
+
+from rare.lgndr.core import LegendaryCore
+from rare.models.wrapper import Wrapper, WrapperType
+from rare.utils.paths import config_dir
+
+logger = getLogger("Wrappers")
+
+
+class Wrappers:
+ def __init__(self):
+ self.__file = os.path.join(config_dir(), "wrappers.json")
+ self.__wrappers_dict = {}
+ try:
+ with open(self.__file) as f:
+ self.__wrappers_dict = json.load(f)
+ except FileNotFoundError:
+ logger.info("%s does not exist", self.__file)
+ except json.JSONDecodeError:
+ logger.warning("%s is corrupt", self.__file)
+
+ self.__wrappers: Dict[str, Wrapper] = {}
+ for wrap_id, wrapper in self.__wrappers_dict.get("wrappers", {}).items():
+ self.__wrappers.update({wrap_id: Wrapper.from_dict(wrapper)})
+
+ self.__applists: Dict[str, List[str]] = {}
+ for app_name, wrapper_list in self.__wrappers_dict.get("applists", {}).items():
+ self.__applists.update({app_name: wrapper_list})
+
+ def import_wrappers(self, core: LegendaryCore, settings: QSettings, app_names: List):
+ for app_name in app_names:
+ wrappers = self.get_game_wrapper_list(app_name)
+ if not wrappers and (commands := settings.value(f"{app_name}/wrapper", [], list)):
+ logger.info("Importing wrappers from Rare's config")
+ settings.remove(f"{app_name}/wrapper")
+ for command in commands:
+ wrapper = Wrapper(command=shlex.split(command))
+ wrappers.append(wrapper)
+ self.set_game_wrapper_list(app_name, wrappers)
+ logger.debug("Imported previous wrappers in %s Rare: %s", app_name, wrapper.name)
+
+ # NOTE: compatibility with Legendary
+ if not wrappers and (command := core.lgd.config.get(app_name, "wrapper", fallback="")):
+ logger.info("Importing wrappers from legendary's config")
+ # no qt wrapper, but legendary wrapper, to have backward compatibility
+ # pattern = re.compile(r'''((?:[^ "']|"[^"]*"|'[^']*')+)''')
+ # wrappers = pattern.split(command)[1::2]
+ wrapper = Wrapper(
+ command=shlex.split(command),
+ name="Imported from Legendary",
+ wtype=WrapperType.LEGENDARY_IMPORT
+ )
+ wrappers = [wrapper]
+ self.set_game_wrapper_list(app_name, wrappers)
+ logger.debug("Imported existing wrappers in %s legendary: %s", app_name, wrapper.name)
+
+ @property
+ def user_wrappers(self) -> Iterable[Wrapper]:
+ return filter(lambda w: w.is_editable, self.__wrappers.values())
+ # for wrap in self.__wrappers.values():
+ # if wrap.is_user_defined:
+ # yield wrap
+
+ def get_game_wrapper_string(self, app_name: str) -> str:
+ commands = [wrapper.command for wrapper in self.get_game_wrapper_list(app_name)]
+ return " ".join(commands)
+
+ def get_game_wrapper_list(self, app_name: str) -> List[Wrapper]:
+ _wrappers = []
+ for wrap_id in self.__applists.get(app_name, []):
+ if wrap := self.__wrappers.get(wrap_id, None):
+ _wrappers.append(wrap)
+ return _wrappers
+
+ def get_game_md5sum_list(self, app_name: str) -> List[str]:
+ return self.__applists.get(app_name, [])
+
+ def set_game_wrapper_list(self, app_name: str, wrappers: List[Wrapper]) -> None:
+ _wrappers = sorted(wrappers, key=lambda w: w.is_compat_tool)
+ for w in _wrappers:
+ if (md5sum := w.checksum) in self.__wrappers.keys():
+ if w != self.__wrappers[md5sum]:
+ logger.error(
+ "Non-unique md5sum for different wrappers %s, %s",
+ w.name,
+ self.__wrappers[md5sum].name,
+ )
+ if w.is_compat_tool:
+ self.__wrappers.update({md5sum: w})
+ else:
+ self.__wrappers.update({md5sum: w})
+ self.__applists[app_name] = [w.checksum for w in _wrappers]
+ self.__save_config(app_name)
+ self.__save_wrappers()
+
+ def __save_config(self, app_name: str):
+ command_string = self.get_game_wrapper_string(app_name)
+ if command_string:
+ config.add_option(app_name, "wrapper", command_string)
+ else:
+ config.remove_option(app_name, "wrapper")
+ config.save_config()
+
+ def __save_wrappers(self):
+ existing = {wrap_id for wrap_id in self.__wrappers.keys()}
+ in_use = {wrap_id for wrappers in self.__applists.values() for wrap_id in wrappers}
+
+ for redudant in existing.difference(in_use):
+ del self.__wrappers[redudant]
+
+ self.__wrappers_dict["wrappers"] = self.__wrappers
+ self.__wrappers_dict["applists"] = self.__applists
+
+ with open(os.path.join(self.__file), "w+") as f:
+ json.dump(self.__wrappers_dict, f, default=lambda o: vars(o), indent=2)
+
+
+if __name__ == "__main__":
+ from pprint import pprint
+ from argparse import Namespace
+
+ from rare.utils.runners import proton
+
+ global config_dir
+ config_dir = os.getcwd
+ global config
+ config = Namespace()
+ config.add_option = lambda x, y, z: print(x, y, z)
+ config.remove_option = lambda x, y: print(x, y)
+ config.save_config = lambda: print()
+
+ wr = Wrappers()
+
+ w1 = Wrapper(command=["/usr/bin/w1"], wtype=WrapperType.NONE)
+ w2 = Wrapper(command=["/usr/bin/w2"], wtype=WrapperType.COMPAT_TOOL)
+ w3 = Wrapper(command=["/usr/bin/w3"], wtype=WrapperType.USER_DEFINED)
+ w4 = Wrapper(command=["/usr/bin/w4"], wtype=WrapperType.USER_DEFINED)
+ wr.set_game_wrapper_list("testgame", [w1, w2, w3, w4])
+
+ w5 = Wrapper(command=["/usr/bin/w5"], wtype=WrapperType.COMPAT_TOOL)
+ wr.set_game_wrapper_list("testgame2", [w2, w1, w5])
+
+ w6 = Wrapper(command=["/usr/bin/w 6", "-w", "-t"], wtype=WrapperType.USER_DEFINED)
+ wr.set_game_wrapper_list("testgame", [w1, w2, w3, w6])
+
+ w7 = Wrapper(command=["/usr/bin/w2"], wtype=WrapperType.COMPAT_TOOL)
+ wrs = wr.get_game_wrapper_list("testgame")
+ wrs.remove(w7)
+ wr.set_game_wrapper_list("testgame", wrs)
+
+ game_wrappers = wr.get_game_wrapper_list("testgame")
+ pprint(game_wrappers)
+ game_wrappers = wr.get_game_wrapper_list("testgame2")
+ pprint(game_wrappers)
+
+ for i, tool in enumerate(proton.find_tools()):
+ wt = Wrapper(command=tool.command(), name=tool.name, wtype=WrapperType.COMPAT_TOOL)
+ wr.set_game_wrapper_list(f"compat_game_{i}", [wt])
+ print(wt.command)
+
+ for wrp in wr.user_wrappers:
+ pprint(wrp)
diff --git a/rare/utils/config_helper.py b/rare/utils/config_helper.py
index 9a7cdbef4..66b7ac5ee 100644
--- a/rare/utils/config_helper.py
+++ b/rare/utils/config_helper.py
@@ -44,6 +44,14 @@ def remove_envvar(app_name: str, option: str) -> None:
remove_option(f"{app_name}.env", option)
+def get_option(app_name: str, option: str, fallback: Any = None) -> str:
+ return _config.get(app_name, option, fallback=fallback)
+
+
+def get_envvar(app_name: str, option: str, fallback: Any = None) -> str:
+ return get_option(f"{app_name}.env", option, fallback=fallback)
+
+
def remove_section(app_name: str) -> None:
return
# Disabled due to env variables implementation
diff --git a/rare/utils/wine.py b/rare/utils/runners/__init__.py
similarity index 57%
rename from rare/utils/wine.py
rename to rare/utils/runners/__init__.py
index 9106884ff..bbc1b1e5a 100644
--- a/rare/utils/wine.py
+++ b/rare/utils/runners/__init__.py
@@ -1,41 +1,42 @@
import os
-import shutil
import subprocess
from configparser import ConfigParser
from logging import getLogger
-from typing import Mapping, Dict, List, Tuple
+from typing import Mapping, Dict, List, Tuple, Optional
-from rare.lgndr.core import LegendaryCore
+from rare.utils import config_helper as config
+from . import proton
+from . import wine
-logger = getLogger("Wine")
+logger = getLogger("Runners")
# this is a copied function from legendary.utils.wine_helpers, but registry file can be specified
-def read_registry(registry: str, wine_pfx: str) -> ConfigParser:
+def read_registry(registry: str, prefix: str) -> ConfigParser:
accepted = ["system.reg", "user.reg"]
if registry not in accepted:
raise RuntimeError(f'Unknown target "{registry}" not in {accepted}')
reg = ConfigParser(comment_prefixes=(';', '#', '/', 'WINE'), allow_no_value=True,
strict=False)
reg.optionxform = str
- reg.read(os.path.join(wine_pfx, 'system.reg'))
+ reg.read(os.path.join(prefix, 'system.reg'))
return reg
-def execute(cmd: List, wine_env: Mapping) -> Tuple[str, str]:
+def execute(command: List[str], environment: Mapping) -> Tuple[str, str]:
if os.environ.get("container") == "flatpak":
- flatpak_cmd = ["flatpak-spawn", "--host"]
- for name, value in wine_env.items():
- flatpak_cmd.append(f"--env={name}={value}")
- cmd = flatpak_cmd + cmd
+ flatpak = ["flatpak-spawn", "--host"]
+ for name, value in environment.items():
+ flatpak.append(f"--env={name}={value}")
+ command = flatpak + command
try:
proc = subprocess.Popen(
- cmd,
+ command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
# Use the current environment if we are in flatpak or our own if we are on host
# In flatpak our environment is passed through `flatpak-spawn` arguments
- env=os.environ.copy() if os.environ.get("container") == "flatpak" else wine_env,
+ env=os.environ.copy() if os.environ.get("container") == "flatpak" else environment,
shell=False,
text=True,
)
@@ -45,13 +46,13 @@ def execute(cmd: List, wine_env: Mapping) -> Tuple[str, str]:
return res
-def resolve_path(wine_exec: str, wine_env: Mapping, path: str) -> str:
+def resolve_path(command: List[str], environment: Mapping, path: str) -> str:
path = path.strip().replace("/", "\\")
# lk: if path does not exist form
- cmd = [wine_exec, "cmd", "/c", "echo", path]
+ cmd = command + ["cmd", "/c", "echo", path]
# lk: if path exists and needs a case-sensitive interpretation form
# cmd = [wine_cmd, 'cmd', '/c', f'cd {path} & cd']
- out, err = execute(cmd, wine_env)
+ out, err = execute(cmd, environment)
out, err = out.strip(), err.strip()
if not out:
logger.error("Failed to resolve wine path due to \"%s\"", err)
@@ -63,9 +64,9 @@ def query_reg_path(wine_exec: str, wine_env: Mapping, reg_path: str):
raise NotImplementedError
-def query_reg_key(wine_exec: str, wine_env: Mapping, reg_path: str, reg_key) -> str:
- cmd = [wine_exec, "reg", "query", reg_path, "/v", reg_key]
- out, err = execute(cmd, wine_env)
+def query_reg_key(command: List[str], environment: Mapping, reg_path: str, reg_key) -> str:
+ cmd = command + ["reg", "query", reg_path, "/v", reg_key]
+ out, err = execute(cmd, environment)
out, err = out.strip(), err.strip()
if not out:
logger.error("Failed to query registry key due to \"%s\"", err)
@@ -83,40 +84,23 @@ def convert_to_windows_path(wine_exec: str, wine_env: Mapping, path: str) -> str
raise NotImplementedError
-def convert_to_unix_path(wine_exec: str, wine_env: Mapping, path: str) -> str:
+def convert_to_unix_path(command: List[str], environment: Mapping, path: str) -> str:
path = path.strip().strip('"')
- cmd = [wine_exec, "winepath.exe", "-u", path]
- out, err = execute(cmd, wine_env)
+ cmd = command + ["winepath.exe", "-u", path]
+ out, err = execute(cmd, environment)
out, err = out.strip(), err.strip()
if not out:
logger.error("Failed to convert to unix path due to \"%s\"", err)
return os.path.realpath(out) if (out := out.strip()) else out
-def wine(core: LegendaryCore, app_name: str = "default") -> str:
- _wine = core.lgd.config.get(
- app_name, "wine_executable", fallback=core.lgd.config.get(
- "default", "wine_executable", fallback=shutil.which("wine")
- )
- )
- return _wine
-
-
-def environ(core: LegendaryCore, app_name: str = "default") -> Dict:
- # Get a clean environment if we are in flatpak, this environment will be pass
+def get_environment(app_environment: Dict, silent: bool = True) -> Dict:
+ # Get a clean environment if we are in flatpak, this environment will be passed
# to `flatpak-spawn`, otherwise use the system's.
_environ = {} if os.environ.get("container") == "flatpak" else os.environ.copy()
- _environ.update(core.get_app_environment(app_name))
- _environ["WINEDEBUG"] = "-all"
- _environ["WINEDLLOVERRIDES"] = "winemenubuilder=d;mscoree=d;mshtml=d;"
- _environ["DISPLAY"] = ""
+ _environ.update(app_environment)
+ if silent:
+ _environ["WINEDEBUG"] = "-all"
+ _environ["WINEDLLOVERRIDES"] = "winemenubuilder=d;mscoree=d;mshtml=d;"
+ _environ["DISPLAY"] = ""
return _environ
-
-
-def prefix(core: LegendaryCore, app_name: str = "default") -> str:
- _prefix = core.lgd.config.get(
- app_name, "wine_prefix", fallback=core.lgd.config.get(
- "default", "wine_prefix", fallback=os.path.expanduser("~/.wine")
- )
- )
- return _prefix if os.path.isdir(_prefix) else ""
diff --git a/rare/utils/proton.py b/rare/utils/runners/proton.py
similarity index 76%
rename from rare/utils/proton.py
rename to rare/utils/runners/proton.py
index c0e3fb60f..edd870e97 100644
--- a/rare/utils/proton.py
+++ b/rare/utils/runners/proton.py
@@ -1,5 +1,7 @@
import os
+import shlex
from dataclasses import dataclass
+from hashlib import md5
from logging import getLogger
from typing import Optional, Union, List, Dict
@@ -25,11 +27,22 @@ def find_libraries(steam_path: str) -> List[str]:
return libraries
+# Notes:
+# Anything older than 'Proton 5.13' doesn't have the 'require_tool_appid' attribute.
+# Anything older than 'Proton 7.0' doesn't have the 'compatmanager_layer_name' attribute.
+# In addition to that, the 'Steam Linux Runtime 1.0 (scout)' runtime lists the
+# 'Steam Linux Runtime 2.0 (soldier)' runtime as a dependency and is probably what was
+# being used for any version before 5.13.
+#
+# As a result the following implementation will list versions from 7.0 onwards which honestly
+# is a good trade-off for the amount of complexity supporting everything would ensue.
+
+
@dataclass
class SteamBase:
steam_path: str
tool_path: str
- toolmanifest: dict
+ toolmanifest: Dict
def __eq__(self, other):
return self.tool_path == other.tool_path
@@ -37,23 +50,36 @@ def __eq__(self, other):
def __hash__(self):
return hash(self.tool_path)
- def commandline(self):
- cmd = "".join([f"'{self.tool_path}'", self.toolmanifest["manifest"]["commandline"]])
- cmd = os.path.normpath(cmd)
+ @property
+ def required_tool(self) -> Optional[str]:
+ return self.toolmanifest["manifest"].get("require_tool_appid", None)
+
+ def command(self, setup: bool = False) -> List[str]:
+ tool_path = os.path.normpath(self.tool_path)
+ cmd = "".join([shlex.quote(tool_path), self.toolmanifest["manifest"]["commandline"]])
# NOTE: "waitforexitandrun" seems to be the verb used in by steam to execute stuff
- cmd = cmd.replace("%verb%", "waitforexitandrun")
- return cmd
+ # `run` is used when setting up the environment, so use that if we are setting up the prefix.
+ verb = "run" if setup else "waitforexitandrun"
+ cmd = cmd.replace("%verb%", verb)
+ return shlex.split(cmd)
+
+ @property
+ def checksum(self) -> str:
+ command = " ".join(shlex.quote(part) for part in self.command(setup=False))
+ return md5(command.encode("utf-8")).hexdigest()
@dataclass
class SteamRuntime(SteamBase):
steam_library: str
- appmanifest: dict
+ appmanifest: Dict
- def name(self):
+ @property
+ def name(self) -> str:
return self.appmanifest["AppState"]["name"]
- def appid(self):
+ @property
+ def appid(self) -> str:
return self.appmanifest["AppState"]["appid"]
@@ -61,33 +87,36 @@ def appid(self):
class ProtonTool(SteamRuntime):
runtime: SteamRuntime = None
- def __bool__(self):
- if appid := self.toolmanifest.get("require_tool_appid", False):
- return self.runtime is not None and self.runtime.appid() == appid
+ def __bool__(self) -> bool:
+ if appid := self.required_tool:
+ return self.runtime is not None and self.runtime.appid == appid
+ return True
- def commandline(self):
- runtime_cmd = self.runtime.commandline()
- cmd = super().commandline()
- return " ".join([runtime_cmd, cmd])
+ def command(self, setup: bool = False) -> List[str]:
+ cmd = self.runtime.command(setup)
+ cmd.extend(super().command(setup))
+ return cmd
@dataclass
class CompatibilityTool(SteamBase):
- compatibilitytool: dict
+ compatibilitytool: Dict
runtime: SteamRuntime = None
- def __bool__(self):
- if appid := self.toolmanifest.get("require_tool_appid", False):
- return self.runtime is not None and self.runtime.appid() == appid
+ def __bool__(self) -> bool:
+ if appid := self.required_tool:
+ return self.runtime is not None and self.runtime.appid == appid
+ return True
- def name(self):
+ @property
+ def name(self) -> str:
name, data = list(self.compatibilitytool["compatibilitytools"]["compat_tools"].items())[0]
return data["display_name"]
- def commandline(self):
- runtime_cmd = self.runtime.commandline() if self.runtime is not None else ""
- cmd = super().commandline()
- return " ".join([runtime_cmd, cmd])
+ def command(self, setup: bool = False) -> List[str]:
+ cmd = self.runtime.command(setup) if self.runtime is not None else []
+ cmd.extend(super().command(setup))
+ return cmd
def find_appmanifests(library: str) -> List[dict]:
@@ -184,16 +213,18 @@ def find_runtimes(steam_path: str, library: str) -> Dict[str, SteamRuntime]:
def find_runtime(
tool: Union[ProtonTool, CompatibilityTool], runtimes: Dict[str, SteamRuntime]
) -> Optional[SteamRuntime]:
- required_tool = tool.toolmanifest["manifest"].get("require_tool_appid")
+ required_tool = tool.required_tool
if required_tool is None:
return None
- return runtimes[required_tool]
+ return runtimes.get(required_tool, None)
-def get_steam_environment(tool: Optional[Union[ProtonTool, CompatibilityTool]], app_name: str = None) -> Dict:
- environ = {}
+def get_steam_environment(
+ tool: Optional[Union[ProtonTool, CompatibilityTool]] = None, compat_path: Optional[str] = None
+) -> Dict:
# If the tool is unset, return all affected env variable names
# IMPORTANT: keep this in sync with the code below
+ environ = {"STEAM_COMPAT_DATA_PATH": compat_path if compat_path else ""}
if tool is None:
environ["STEAM_COMPAT_CLIENT_INSTALL_PATH"] = ""
environ["STEAM_COMPAT_LIBRARY_PATHS"] = ""
@@ -218,7 +249,8 @@ def find_tools() -> List[Union[ProtonTool, CompatibilityTool]]:
steam_path = find_steam()
logger.debug("Using Steam install in %s", steam_path)
steam_libraries = find_libraries(steam_path)
- logger.debug("Searching for tools in libraries %s", steam_libraries)
+ logger.debug("Searching for tools in libraries:")
+ logger.debug("%s", steam_libraries)
runtimes = {}
for library in steam_libraries:
@@ -233,6 +265,8 @@ def find_tools() -> List[Union[ProtonTool, CompatibilityTool]]:
runtime = find_runtime(tool, runtimes)
tool.runtime = runtime
+ tools = list(filter(lambda t: bool(t), tools))
+
return tools
@@ -244,7 +278,9 @@ def find_tools() -> List[Union[ProtonTool, CompatibilityTool]]:
for tool in _tools:
print(get_steam_environment(tool))
- print(tool.name(), tool.commandline())
+ print(tool.name)
+ print(tool.command())
+ print(" ".join(tool.command()))
def find_proton_combos():
diff --git a/rare/utils/runners/wine.py b/rare/utils/runners/wine.py
new file mode 100644
index 000000000..2b87332bf
--- /dev/null
+++ b/rare/utils/runners/wine.py
@@ -0,0 +1,88 @@
+import os
+from dataclasses import dataclass
+from logging import getLogger
+from typing import Dict, Tuple, List, Optional
+
+logger = getLogger("Wine")
+
+lutris_runtime_paths = [
+ os.path.expanduser("~/.local/share/lutris")
+]
+
+__lutris_runtime: str = None
+__lutris_wine: str = None
+
+
+def find_lutris() -> Tuple[str, str]:
+ global __lutris_runtime, __lutris_wine
+ for path in lutris_runtime_paths:
+ runtime_path = os.path.join(path, "runtime")
+ wine_path = os.path.join(path, "runners", "wine")
+ if os.path.isdir(path) and os.path.isdir(runtime_path) and os.path.isdir(wine_path):
+ __lutris_runtime, __lutris_wine = runtime_path, wine_path
+ return runtime_path, wine_path
+
+
+@dataclass
+class WineRuntime:
+ name: str
+ path: str
+ environ: Dict
+
+
+@dataclass
+class WineRunner:
+ name: str
+ path: str
+ environ: Dict
+ runtime: Optional[WineRuntime] = None
+
+
+def find_lutris_wines(runtime_path: str = None, wine_path: str = None) -> List[WineRunner]:
+ runners = []
+ if not runtime_path and not wine_path:
+ return runners
+
+
+def __get_lib_path(executable: str, basename: str = "") -> str:
+ path = os.path.dirname(os.path.dirname(executable))
+ lib32 = os.path.realpath(os.path.join(path, "lib32", basename))
+ lib64 = os.path.realpath(os.path.join(path, "lib64", basename))
+ lib = os.path.realpath(os.path.join(path, "lib", basename))
+ if lib32 == lib or not os.path.exists(lib32):
+ ldpath = ":".join([lib64, lib])
+ elif lib64 == lib or not os.path.exists(lib64):
+ ldpath = ":".join([lib, lib32])
+ else:
+ ldpath = lib if os.path.exists(lib) else lib64
+ return ldpath
+
+
+def get_wine_environment(executable: str = None, prefix: str = None) -> Dict:
+ # If the tool is unset, return all affected env variable names
+ # IMPORTANT: keep this in sync with the code below
+ environ = {"WINEPREFIX": prefix if prefix is not None else ""}
+ if executable is None:
+ environ["WINEDLLPATH"] = ""
+ environ["LD_LIBRARY_PATH"] = ""
+ else:
+ winedllpath = __get_lib_path(executable, "wine")
+ environ["WINEDLLPATH"] = winedllpath
+ librarypath = __get_lib_path(executable, "")
+ environ["LD_LIBRARY_PATH"] = librarypath
+ return environ
+
+
+if __name__ == "__main__":
+ from pprint import pprint
+
+ pprint(get_wine_environment(
+ "/opt/wine-ge-custom/bin/wine", None))
+ pprint(get_wine_environment(
+ "/usr/bin/wine", None))
+ pprint(get_wine_environment(
+ "/usr/share/steam/compatitiblitytools.d/dist/bin/wine", None))
+ pprint(get_wine_environment(
+ os.path.expanduser("~/.local/share/Steam/compatibilitytools.d/GE-Proton8-14/files/bin/wine"), None))
+ pprint(get_wine_environment(
+ os.path.expanduser("~/.local/share/lutris/runners/wine/lutris-GE-Proton8-14-x86_64/bin/wine"), None))
diff --git a/rare/utils/steam_grades.py b/rare/utils/steam_grades.py
index a29b7dcfd..3e341cd74 100644
--- a/rare/utils/steam_grades.py
+++ b/rare/utils/steam_grades.py
@@ -39,8 +39,8 @@ def get_grade(steam_code):
url = "https://www.protondb.com/api/v1/reports/summaries/"
res = requests.get(f"{url}{steam_code}.json")
try:
- lista = orjson.loads(res.text)
- except orjson.JSONDecodeError:
+ lista = orjson.loads(res.text) # pylint: disable=maybe-no-member
+ except orjson.JSONDecodeError: # pylint: disable=maybe-no-member
return "fail"
return lista["tier"]
@@ -57,15 +57,15 @@ def load_json() -> dict:
__active_download = True
response = requests.get(url)
__active_download = False
- steam_ids = orjson.loads(response.text)["applist"]["apps"]
+ steam_ids = orjson.loads(response.text)["applist"]["apps"] # pylint: disable=maybe-no-member
ids = {}
for game in steam_ids:
ids[game["name"]] = game["appid"]
with open(file, "w") as f:
- f.write(orjson.dumps(ids).decode("utf-8"))
+ f.write(orjson.dumps(ids).decode("utf-8")) # pylint: disable=maybe-no-member
return ids
else:
- return orjson.loads(open(file, "r").read())
+ return orjson.loads(open(file, "r").read()) # pylint: disable=maybe-no-member
def get_steam_id(title: str) -> int: