diff --git a/rare/components/main_window.py b/rare/components/main_window.py
index b26928bd..2399a6f4 100644
--- a/rare/components/main_window.py
+++ b/rare/components/main_window.py
@@ -16,6 +16,7 @@ from PyQt5.QtWidgets import (
QHBoxLayout,
)
+from rare.models.options import options
from rare.components.tabs import MainTabWidget
from rare.components.tray_icon import TrayIcon
from rare.shared import RareCore
@@ -93,8 +94,8 @@ class MainWindow(QMainWindow):
# self.status_timer.start()
width, height = 1280, 720
- if self.settings.value("save_size", False, bool):
- width, height = self.settings.value("window_size", (width, height), tuple)
+ if self.settings.value(*options.save_size):
+ width, height = self.settings.value(*options.window_size)
self.resize(width, height)
@@ -151,9 +152,9 @@ class MainWindow(QMainWindow):
self._window_launched = True
def hide(self) -> None:
- if self.settings.value("save_size", False, bool):
+ if self.settings.value(*options.save_size):
size = self.size().width(), self.size().height()
- self.settings.setValue("window_size", size)
+ self.settings.setValue(options.window_size.key, size)
super(MainWindow, self).hide()
def toggle(self):
@@ -214,7 +215,7 @@ class MainWindow(QMainWindow):
# lk: `accept_close` is set to `True` by the `close()` method, overrides exiting to tray in `closeEvent()`
# lk: ensures exiting instead of hiding when `close()` is called programmatically
if not self.__accept_close:
- if self.settings.value("sys_tray", True, bool):
+ if self.settings.value(*options.sys_tray):
self.hide()
e.ignore()
return
diff --git a/rare/components/tabs/downloads/__init__.py b/rare/components/tabs/downloads/__init__.py
index 0145066f..57cdec38 100644
--- a/rare/components/tabs/downloads/__init__.py
+++ b/rare/components/tabs/downloads/__init__.py
@@ -15,6 +15,7 @@ from rare.components.dialogs.uninstall_dialog import UninstallDialog
from rare.lgndr.models.downloading import UIUpdate
from rare.models.game import RareGame
from rare.models.install import InstallOptionsModel, InstallQueueItemModel, UninstallOptionsModel
+from rare.models.options import options
from rare.shared import RareCore
from rare.shared.workers.install_info import InstallInfoWorker
from rare.shared.workers.uninstall import UninstallWorker
@@ -105,9 +106,13 @@ class DownloadsTab(QWidget):
def __add_update(self, update: Union[str, RareGame]):
if isinstance(update, str):
update = self.rcore.get_game(update)
- if QSettings().value(
- f"{update.app_name}/auto_update", False, bool
- ) or QSettings().value("auto_update", False, bool):
+
+ auto_update = QSettings(self).value(
+ f"{update.app_name}/{options.auto_update.key}",
+ QSettings(self).value(*options.auto_update),
+ options.auto_update.dtype
+ )
+ if auto_update:
self.__get_install_options(
InstallOptionsModel(app_name=update.app_name, update=True, silent=True)
)
diff --git a/rare/components/tabs/games/__init__.py b/rare/components/tabs/games/__init__.py
index 2e183fd4..a31daecd 100644
--- a/rare/components/tabs/games/__init__.py
+++ b/rare/components/tabs/games/__init__.py
@@ -15,7 +15,7 @@ from rare.shared import RareCore
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
@@ -94,11 +94,11 @@ class GamesTab(QStackedWidget):
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)
- self.active_filter: str = self.head_bar.filter.currentData(Qt.UserRole)
-
# signals
self.signals.game.installed.connect(self.update_count_games_label)
self.signals.game.uninstalled.connect(self.update_count_games_label)
@@ -157,7 +157,7 @@ class GamesTab(QStackedWidget):
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):
@@ -170,18 +170,26 @@ class GamesTab(QStackedWidget):
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_list(filter_name, search_text.lower())
+ 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.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 7006eb6a..72241692 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 @@ import platform
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 legendary.models.game import SaveGameStatus
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_settings_widget import Ui_CloudSettingsWidget
from rare.ui.components.tabs.games.game_info.cloud_sync_widget import Ui_CloudSyncWidget
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
@@ -114,13 +115,14 @@ class CloudSaves(QWidget, SideTabContents):
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")
@@ -129,14 +131,14 @@ class CloudSaves(QWidget, SideTabContents):
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)
@@ -158,8 +160,6 @@ class CloudSaves(QWidget, SideTabContents):
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_info/game_settings.py b/rare/components/tabs/games/game_info/game_settings.py
index 702ba666..c5e49a19 100644
--- a/rare/components/tabs/games/game_info/game_settings.py
+++ b/rare/components/tabs/games/game_info/game_settings.py
@@ -4,7 +4,7 @@ from logging import getLogger
from typing import Tuple
from PyQt5.QtCore import Qt
-from PyQt5.QtWidgets import QLabel, QFileDialog
+from PyQt5.QtWidgets import QLabel, QFileDialog, QFormLayout
from legendary.models.game import Game, InstalledGame
from rare.components.tabs.settings import DefaultGameSettings
@@ -20,10 +20,6 @@ logger = getLogger("GameSettings")
class GameSettings(DefaultGameSettings, SideTabContents):
def __init__(self, parent=None):
super(GameSettings, self).__init__(False, parent=parent)
- self.pre_launch_settings = PreLaunchSettings()
- self.ui.launch_settings_group.layout().addRow(
- QLabel(self.tr("Pre-launch command")), self.pre_launch_settings
- )
self.ui.skip_update.currentIndexChanged.connect(
lambda x: self.update_combobox("skip_update_check", x)
@@ -43,9 +39,17 @@ class GameSettings(DefaultGameSettings, SideTabContents):
save_func=self.override_exe_save_callback,
parent=self
)
- self.ui.launch_settings_layout.insertRow(
- self.ui.launch_settings_layout.getWidgetPosition(self.ui.launch_params)[0] + 1,
- QLabel(self.tr("Override executable"), self), self.override_exe_edit
+ self.ui.launch_settings_layout.setWidget(
+ self.ui.launch_settings_layout.getWidgetPosition(self.ui.override_exe_label)[0],
+ QFormLayout.FieldRole,
+ self.override_exe_edit
+ )
+
+ self.pre_launch_settings = PreLaunchSettings(parent=self)
+ self.ui.launch_settings_layout.setWidget(
+ self.ui.launch_settings_layout.getWidgetPosition(self.ui.pre_launch_label)[0],
+ QFormLayout.FieldRole,
+ self.pre_launch_settings
)
self.ui.game_settings_layout.setAlignment(Qt.AlignTop)
@@ -126,9 +130,9 @@ class GameSettings(DefaultGameSettings, SideTabContents):
self.set_title.emit(self.game.app_title)
if platform.system() != "Windows":
if self.igame and self.igame.platform == "Mac":
- self.ui.linux_settings_widget.setVisible(False)
+ self.linux_settings.setVisible(False)
else:
- self.ui.linux_settings_widget.setVisible(True)
+ self.linux_settings.setVisible(True)
self.ui.launch_params.setText(self.core.lgd.config.get(self.game.app_name, "start_params", fallback=""))
self.override_exe_edit.setText(
diff --git a/rare/components/tabs/games/game_widgets/__init__.py b/rare/components/tabs/games/game_widgets/__init__.py
index a5d5b58b..b97eb453 100644
--- a/rare/components/tabs/games/game_widgets/__init__.py
+++ b/rare/components/tabs/games/game_widgets/__init__.py
@@ -1,4 +1,4 @@
-from typing import Tuple, List, Union, Optional
+from typing import Tuple, List, Union
from PyQt5.QtCore import QObject, pyqtSlot
from PyQt5.QtWidgets import QWidget
@@ -6,6 +6,7 @@ from PyQt5.QtWidgets import QWidget
from rare.lgndr.core import LegendaryCore
from rare.models.game import RareGame
from rare.models.signals import GlobalSignals
+from rare.models.library import LibraryFilter, LibraryOrder
from rare.shared import RareCore
from .icon_game_widget import IconGameWidget
from .list_game_widget import ListGameWidget
@@ -20,8 +21,8 @@ class LibraryWidgetController(QObject):
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 +33,26 @@ class LibraryWidgetController(QObject):
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 and not widget.rgame.is_unreal
- elif filter_name == "offline":
+ elif library_filter == LibraryFilter.OFFLINE:
visible = widget.rgame.can_run_offline and not widget.rgame.is_unreal
- elif filter_name == "32bit":
+ elif library_filter == LibraryFilter.WIN32:
visible = widget.rgame.is_win32 and not widget.rgame.is_unreal
- elif filter_name == "mac":
+ elif library_filter == LibraryFilter.MAC:
visible = widget.rgame.is_mac and not widget.rgame.is_unreal
- elif filter_name == "installable":
+ elif library_filter == LibraryFilter.INSTALLABLE:
visible = not widget.rgame.is_non_asset and not widget.rgame.is_unreal
- 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 +67,7 @@ class LibraryWidgetController(QObject):
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 +78,52 @@ class LibraryWidgetController(QObject):
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:
- 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,
- )
- )
+ def order_game_views(self, order_by: LibraryOrder = LibraryOrder.TITLE, search_text: str = ""):
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(),))
- else:
- list_widgets.sort(
- # 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)
+ if search_text:
+ self._icon_container.layout().sort(
+ lambda x: (search_text not in x.widget().rgame.app_title.lower(),)
)
+ list_widgets.sort(key=lambda x: (search_text not in x.rgame.app_title.lower(),))
+ else:
+ if (newest := order_by == LibraryOrder.NEWEST) or order_by == LibraryOrder.OLDEST:
+ # Sort by grant date
+ 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 +142,7 @@ class LibraryWidgetController(QObject):
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 2311d97c..f2f583e0 100644
--- a/rare/components/tabs/games/head_bar.py
+++ b/rare/components/tabs/games/head_bar.py
@@ -1,22 +1,25 @@
-import platform as pf
-
-from PyQt5.QtCore import QSettings, pyqtSignal, pyqtSlot, Qt
+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.models.options import options
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()
@@ -27,43 +30,63 @@ class GameListHeadBar(QWidget):
self.settings = QSettings(self)
self.filter = QComboBox(self)
- self.filter.addItem(self.tr("All games"), "all")
- self.filter.addItem(self.tr("Installed"), "installed")
- self.filter.addItem(self.tr("Offline"), "offline")
- # self.filter.addItem(self.tr("Hidden"), "hidden")
+ self.filter.addItem(self.tr("All games"), LibraryFilter.ALL)
+ self.filter.addItem(self.tr("Installed"), LibraryFilter.INSTALLED)
+ self.filter.addItem(self.tr("Offline"), LibraryFilter.OFFLINE)
+ # self.filter.addItem(self.tr("Hidden"), LibraryFilter.HIDDEN)
if self.rcore.bit32_games:
- self.filter.addItem(self.tr("32bit games"), "32bit")
+ self.filter.addItem(self.tr("32bit games"), LibraryFilter.WIN32)
if self.rcore.mac_games:
- self.filter.addItem(self.tr("macOS games"), "mac")
+ self.filter.addItem(self.tr("macOS games"), LibraryFilter.MAC)
if self.rcore.origin_games:
- self.filter.addItem(self.tr("Exclude Origin"), "installable")
- self.filter.addItem(self.tr("Include Unreal"), "include_ue")
-
- filter_default = "mac" if pf.system() == "Darwin" else "all"
- filter_index = i if (i := self.filter.findData(filter_default, Qt.UserRole)) >= 0 else 0
+ self.filter.addItem(self.tr("Exclude Origin"), LibraryFilter.INSTALLABLE)
+ self.filter.addItem(self.tr("Include Unreal"), LibraryFilter.INCLUDE_UE)
try:
- self.filter.setCurrentIndex(self.settings.value("library_filter", filter_index, int))
- except TypeError:
- self.settings.setValue("library_filter", filter_index)
- self.filter.setCurrentIndex(filter_index)
- self.filter.currentIndexChanged.connect(self.filter_changed)
+ self.filter.setCurrentIndex(self.filter.findData(
+ LibraryFilter(self.settings.value(*options.library_filter))
+ ))
+ except (TypeError, ValueError):
+ self.settings.setValue(options.library_filter.key, options.library_filter.default)
+ self.filter.setCurrentIndex(self.filter.findData(options.library_filter.default))
+ 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.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)
+ try:
+ self.order.setCurrentIndex(self.order.findData(
+ LibraryOrder(self.settings.value(*options.library_order))
+ ))
+ except (TypeError, ValueError):
+ self.settings.setValue(options.library_order.key, options.library_order.default)
+ self.order.setCurrentIndex(self.order.findData(options.library_order.default))
+ 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)
@@ -76,8 +99,8 @@ class GameListHeadBar(QWidget):
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()
@@ -85,24 +108,25 @@ class GameListHeadBar(QWidget):
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)
@@ -113,17 +137,29 @@ class GameListHeadBar(QWidget):
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):
- self.filterChanged.emit(self.filter.itemData(index, Qt.UserRole))
- self.settings.setValue("library_filter", index)
+ def __filter_changed(self, index: int):
+ data = int(self.filter.itemData(index, Qt.UserRole))
+ self.filterChanged.emit(data)
+ self.settings.setValue(options.library_filter.key, data)
+
+ def current_order(self) -> int:
+ return int(self.order.currentData(Qt.UserRole))
+
+ @pyqtSlot(int)
+ def __order_changed(self, index: int):
+ data = int(self.order.itemData(index, Qt.UserRole))
+ self.orderChanged.emit(data)
+ self.settings.setValue(options.library_order.key, data)
diff --git a/rare/components/tabs/games/integrations/egl_sync_group.py b/rare/components/tabs/games/integrations/egl_sync_group.py
index 31ff8637..3077aaa9 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 legendary.models.game import InstalledGame
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
@@ -87,10 +88,10 @@ class EGLSyncGroup(QGroupBox):
def __run_wine_resolver(self):
self.egl_path_info.setText(self.tr("Updating..."))
- wine_resolver = WineResolver(
- self.core,
- PathSpec.egl_programdata,
- "default"
+ 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)
@@ -122,14 +123,8 @@ class EGLSyncGroup(QGroupBox):
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):
diff --git a/rare/components/tabs/settings/__init__.py b/rare/components/tabs/settings/__init__.py
index c769a600..9142e426 100644
--- a/rare/components/tabs/settings/__init__.py
+++ b/rare/components/tabs/settings/__init__.py
@@ -1,9 +1,9 @@
-from rare.components.tabs.settings.widgets.linux import LinuxSettings
+from rare.components.tabs.settings.widgets.wine import LinuxSettings
from rare.shared import ArgumentsSingleton
from rare.widgets.side_tab import SideTabWidget
from .about import About
from .debug import DebugSettings
-from .game_settings import DefaultGameSettings
+from .game import DefaultGameSettings
from .legendary import LegendarySettings
from .rare import RareSettings
@@ -13,9 +13,14 @@ class SettingsTab(SideTabWidget):
super(SettingsTab, self).__init__(parent=parent)
self.args = ArgumentsSingleton()
- self.rare_index = self.addTab(RareSettings(self), "Rare")
- self.legendary_index = self.addTab(LegendarySettings(self), "Legendary")
- self.settings_index = self.addTab(DefaultGameSettings(True, self), self.tr("Default Settings"))
+ rare_settings = RareSettings(self)
+ self.rare_index = self.addTab(rare_settings, "Rare")
+
+ legendary_settings = LegendarySettings(self)
+ self.legendary_index = self.addTab(legendary_settings, "Legendary")
+
+ game_settings = DefaultGameSettings(True, self)
+ self.settings_index = self.addTab(game_settings, self.tr("Defaults"))
self.about = About(self)
self.about_index = self.addTab(self.about, "About", "About")
diff --git a/rare/components/tabs/settings/game.py b/rare/components/tabs/settings/game.py
new file mode 100644
index 00000000..b31d5f07
--- /dev/null
+++ b/rare/components/tabs/settings/game.py
@@ -0,0 +1,95 @@
+import platform as pf
+from logging import getLogger
+
+from PyQt5.QtCore import QSettings, Qt
+from PyQt5.QtGui import QShowEvent
+from PyQt5.QtWidgets import (
+ QWidget,
+ QLabel, QFormLayout
+)
+
+from components.tabs.settings.widgets.dxvk import DxvkSettings
+from components.tabs.settings.widgets.mangohud import MangoHudSettings
+from rare.components.tabs.settings.widgets.env_vars import EnvVars
+from rare.components.tabs.settings.widgets.wine import LinuxSettings
+from rare.components.tabs.settings.widgets.proton import ProtonSettings
+from rare.components.tabs.settings.widgets.wrapper import WrapperSettings
+from rare.shared import LegendaryCoreSingleton
+from rare.ui.components.tabs.settings.game import Ui_GameSettings
+
+logger = getLogger("GameSettings")
+
+
+class DefaultGameSettings(QWidget):
+ # variable to no update when changing game
+ change = False
+ app_name: str
+
+ def __init__(self, is_default, parent=None):
+ super(DefaultGameSettings, self).__init__(parent=parent)
+ self.ui = Ui_GameSettings()
+ self.ui.setupUi(self)
+ self.core = LegendaryCoreSingleton()
+ self.settings = QSettings(self)
+
+ self.wrapper_settings = WrapperSettings(self)
+ self.ui.launch_layout.setWidget(
+ self.ui.launch_layout.getWidgetPosition(self.ui.wrapper_label)[0],
+ QFormLayout.FieldRole,
+ self.wrapper_settings
+ )
+
+ self.env_vars = EnvVars(self)
+ # dxvk
+ self.dxvk = DxvkSettings(self)
+ self.dxvk.environ_changed.connect(self.env_vars.reset_model)
+ self.dxvk.load_settings(self.app_name)
+
+ self.mangohud = MangoHudSettings(self)
+ self.mangohud.environ_changed.connect(self.environ_changed)
+ self.mangohud.load_settings(self.name)
+
+ if pf.system() != "Windows":
+ self.linux_settings = LinuxAppSettings(self)
+ self.ui.game_settings_layout.addWidget(self.linux_settings)
+
+ 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)
+
+ if pf.system() != "Darwin":
+ self.proton_settings = ProtonSettings(self.linux_settings, self.wrapper_settings)
+ self.linux_settings.ui.linux_settings_layout.insertWidget(0, self.proton_settings)
+ self.proton_settings.environ_changed.connect(self.env_vars.reset_model)
+
+ self.ui.game_settings_layout.setAlignment(Qt.AlignTop)
+
+ self.ui.main_layout.addWidget(self.dxvk)
+ self.ui.main_layout.addWidget(self.mangohud)
+ self.ui.main_layout.addWidget(self.env_vars)
+
+ if is_default:
+ self.ui.launch_layout.removeRow(self.ui.skip_update_label)
+ self.ui.launch_layout.removeRow(self.ui.offline_label)
+ self.ui.launch_layout.removeRow(self.ui.launch_params_label)
+ self.ui.launch_layout.removeRow(self.ui.override_exe_label)
+ self.ui.launch_layout.removeRow(self.ui.pre_launch_label)
+ self.load_settings("default")
+
+ def load_settings(self, app_name):
+ self.app_name = app_name
+ self.wrapper_settings.load_settings(app_name)
+
+ if pf.system() != "Windows":
+ self.linux_settings.update_game(app_name)
+ if pf.system() != "Darwin":
+ proton = self.wrapper_settings.wrappers.get("proton", "")
+ if proton:
+ proton = proton.text
+ self.proton_settings.load_settings(app_name, proton)
+ else:
+ proton = ""
+ self.linux_settings.ui.wine_groupbox.setDisabled(bool(proton))
+
+ self.env_vars.update_game(app_name)
diff --git a/rare/components/tabs/settings/game_settings.py b/rare/components/tabs/settings/game_settings.py
deleted file mode 100644
index 6b96d5f6..00000000
--- a/rare/components/tabs/settings/game_settings.py
+++ /dev/null
@@ -1,106 +0,0 @@
-import platform
-from logging import getLogger
-
-from PyQt5.QtCore import QSettings, Qt
-from PyQt5.QtWidgets import (
- QWidget,
- QLabel
-)
-
-from rare.components.tabs.settings.widgets.env_vars import EnvVars
-from rare.components.tabs.settings.widgets.wrapper import WrapperSettings
-from rare.shared import LegendaryCoreSingleton
-from rare.ui.components.tabs.settings.game_settings import Ui_GameSettings
-
-if platform.system() != "Windows":
- from rare.components.tabs.settings.widgets.linux import LinuxSettings
- if platform.system() != "Darwin":
- from rare.components.tabs.settings.widgets.proton import ProtonSettings
-
-logger = getLogger("GameSettings")
-
-
-class DefaultGameSettings(QWidget):
- # variable to no update when changing game
- change = False
- app_name: str
-
- def __init__(self, is_default, parent=None):
- super(DefaultGameSettings, self).__init__(parent=parent)
- self.ui = Ui_GameSettings()
- self.ui.setupUi(self)
- self.core = LegendaryCoreSingleton()
- self.settings = QSettings()
-
- self.wrapper_settings = WrapperSettings()
-
- 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()
- if platform.system() != "Darwin":
- self.proton_settings = ProtonSettings(self.linux_settings, self.wrapper_settings)
- self.ui.proton_layout.addWidget(self.proton_settings)
- self.proton_settings.environ_changed.connect(self.env_vars.reset_model)
-
- # FIXME: Remove the spacerItem and margins from the linux settings
- # FIXME: This should be handled differently at soem point in the future
- # NOTE: specerItem has been removed
- self.linux_settings.layout().setContentsMargins(0, 0, 0, 0)
- # FIXME: End of FIXME
- self.ui.linux_settings_layout.addWidget(self.linux_settings)
- self.ui.linux_settings_layout.setAlignment(Qt.AlignTop)
-
- 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)
- else:
- self.ui.linux_settings_widget.setVisible(False)
-
- if is_default:
- self.ui.launch_settings_layout.removeRow(self.ui.skip_update)
- self.ui.launch_settings_layout.removeRow(self.ui.offline)
- self.ui.launch_settings_layout.removeRow(self.ui.launch_params)
-
- self.load_settings("default")
-
- 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
- if platform.system() != "Darwin":
- self.proton_settings.load_settings(app_name, proton)
- else:
- proton = ""
- 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)
-
-
-if platform.system() != "Windows":
- class LinuxAppSettings(LinuxSettings):
- def __init__(self):
- super(LinuxAppSettings, self).__init__()
-
- 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.dxvk.load_settings(self.name)
-
- self.mangohud.load_settings(self.name)
diff --git a/rare/components/tabs/settings/legendary.py b/rare/components/tabs/settings/legendary.py
index 87db4c17..c88594dd 100644
--- a/rare/components/tabs/settings/legendary.py
+++ b/rare/components/tabs/settings/legendary.py
@@ -6,6 +6,7 @@ from typing import Tuple, List
from PyQt5.QtCore import QObject, pyqtSignal, QThreadPool, QSettings
from PyQt5.QtWidgets import QSizePolicy, QWidget, QFileDialog, QMessageBox
+from rare.models.options import options
from rare.shared import LegendaryCoreSingleton
from rare.shared.workers.worker import Worker
from rare.ui.components.tabs.settings.legendary import Ui_LegendarySettings
@@ -100,33 +101,30 @@ class LegendarySettings(QWidget, Ui_LegendarySettings):
)
self.locale_layout.addWidget(self.locale_edit)
- self.fetch_win32_check.setChecked(self.settings.value("win32_meta", False, bool))
+ self.fetch_win32_check.setChecked(self.settings.value(*options.win32_meta))
self.fetch_win32_check.stateChanged.connect(
- lambda: self.settings.setValue("win32_meta", self.fetch_win32_check.isChecked())
+ lambda: self.settings.setValue(options.win32_meta.key, self.fetch_win32_check.isChecked())
)
- self.fetch_macos_check.setChecked(self.settings.value("macos_meta", pf.system() == "Darwin", bool))
+ self.fetch_macos_check.setChecked(self.settings.value(*options.macos_meta))
self.fetch_macos_check.stateChanged.connect(
- lambda: self.settings.setValue("macos_meta", self.fetch_macos_check.isChecked())
+ lambda: self.settings.setValue(options.macos_meta.key, self.fetch_macos_check.isChecked())
)
self.fetch_macos_check.setDisabled(pf.system() == "Darwin")
- self.fetch_unreal_check.setChecked(self.settings.value("unreal_meta", False, bool))
+ self.fetch_unreal_check.setChecked(self.settings.value(*options.unreal_meta))
self.fetch_unreal_check.stateChanged.connect(
- lambda: self.settings.setValue("unreal_meta", self.fetch_unreal_check.isChecked())
+ lambda: self.settings.setValue(options.unreal_meta.key, self.fetch_unreal_check.isChecked())
)
- self.exclude_non_asset_check.setChecked(
- self.settings.value("exclude_non_asset", False, bool)
- )
+ self.exclude_non_asset_check.setChecked(self.settings.value(*options.exclude_non_asset))
self.exclude_non_asset_check.stateChanged.connect(
- lambda: self.settings.setValue("exclude_non_asset", self.exclude_non_asset_check.isChecked())
- )
- self.exclude_entitlements_check.setChecked(
- self.settings.value("exclude_entitlements", False, bool)
+ lambda: self.settings.setValue(options.exclude_non_asset.key, self.exclude_non_asset_check.isChecked())
)
+
+ self.exclude_entitlements_check.setChecked(self.settings.value(*options.exclude_entitlements))
self.exclude_entitlements_check.stateChanged.connect(
- lambda: self.settings.setValue("exclude_entitlements", self.exclude_entitlements_check.isChecked())
+ lambda: self.settings.setValue(options.exclude_entitlements.key, self.exclude_entitlements_check.isChecked())
)
self.refresh_metadata_button.clicked.connect(self.refresh_metadata)
diff --git a/rare/components/tabs/settings/rare.py b/rare/components/tabs/settings/rare.py
index 1c42040a..b366ba38 100644
--- a/rare/components/tabs/settings/rare.py
+++ b/rare/components/tabs/settings/rare.py
@@ -8,6 +8,7 @@ from PyQt5.QtCore import QSettings, Qt
from PyQt5.QtWidgets import QWidget, QMessageBox
from rare.components.tabs.settings.widgets.rpc import RPCSettings
+from rare.models.options import options
from rare.shared import LegendaryCoreSingleton
from rare.ui.components.tabs.settings.rare import Ui_RareSettings
from rare.utils.misc import (
@@ -39,18 +40,8 @@ class RareSettings(QWidget, Ui_RareSettings):
super(RareSettings, self).__init__(parent=parent)
self.setupUi(self)
self.core = LegendaryCoreSingleton()
- # (widget_name, option_name, default)
- self.checkboxes = [
- (self.sys_tray, "sys_tray", True),
- (self.auto_update, "auto_update", False),
- (self.confirm_start, "confirm_start", False),
- (self.auto_sync_cloud, "auto_sync_cloud", False),
- (self.notification, "notification", True),
- (self.save_size, "save_size", False),
- (self.log_games, "show_console", False),
- ]
+ self.settings = QSettings(self)
- self.settings = QSettings()
language = self.settings.value("language", self.core.language_code, type=str)
# Select lang
@@ -85,29 +76,37 @@ class RareSettings(QWidget, Ui_RareSettings):
self.rpc = RPCSettings(self)
self.right_layout.insertWidget(1, self.rpc, alignment=Qt.AlignTop)
- self.init_checkboxes(self.checkboxes)
+ self.sys_tray.setChecked(self.settings.value(*options.sys_tray))
self.sys_tray.stateChanged.connect(
- lambda: self.settings.setValue("sys_tray", self.sys_tray.isChecked())
+ lambda: self.settings.setValue(options.sys_tray.key, self.sys_tray.isChecked())
)
+
+ self.auto_update.setChecked(self.settings.value(*options.auto_update))
self.auto_update.stateChanged.connect(
- lambda: self.settings.setValue("auto_update", self.auto_update.isChecked())
+ lambda: self.settings.setValue(options.auto_update.key, self.auto_update.isChecked())
)
+
+ self.confirm_start.setChecked(self.settings.value(*options.confirm_start))
self.confirm_start.stateChanged.connect(
- lambda: self.settings.setValue(
- "confirm_start", self.confirm_start.isChecked()
- )
+ lambda: self.settings.setValue(options.confirm_start.key, self.confirm_start.isChecked())
)
+
+ self.auto_sync_cloud.setChecked(self.settings.value(*options.auto_sync_cloud))
self.auto_sync_cloud.stateChanged.connect(
- lambda: self.settings.setValue(
- "auto_sync_cloud", self.auto_sync_cloud.isChecked()
- )
+ lambda: self.settings.setValue(options.auto_sync_cloud.key, self.auto_sync_cloud.isChecked())
)
+
+ self.notification.setChecked(self.settings.value(*options.notification))
self.notification.stateChanged.connect(
- lambda: self.settings.setValue("notification", self.notification.isChecked())
+ lambda: self.settings.setValue(options.notification.key, self.notification.isChecked())
)
+
+ self.save_size.setChecked(self.settings.value(*options.save_size))
self.save_size.stateChanged.connect(self.save_window_size)
+
+ self.log_games.setChecked(self.settings.value(*options.log_games))
self.log_games.stateChanged.connect(
- lambda: self.settings.setValue("show_console", self.log_games.isChecked())
+ lambda: self.settings.setValue(options.log_games.key, self.log_games.isChecked())
)
if desktop_links_supported():
@@ -221,13 +220,8 @@ class RareSettings(QWidget, Ui_RareSettings):
subprocess.Popen([opener, log_dir()])
def save_window_size(self):
- self.settings.setValue("save_size", self.save_size.isChecked())
- self.settings.remove("window_size")
+ self.settings.setValue(options.save_size.key, self.save_size.isChecked())
+ self.settings.remove(options.window_size.key)
def update_lang(self, i: int):
self.settings.setValue("language", languages[i][0])
-
- def init_checkboxes(self, checkboxes):
- for cb in checkboxes:
- widget, option, default = cb
- widget.setChecked(self.settings.value(option, default, bool))
diff --git a/rare/components/tabs/settings/widgets/dxvk.py b/rare/components/tabs/settings/widgets/dxvk.py
index 2d3e0d56..372b0bac 100644
--- a/rare/components/tabs/settings/widgets/dxvk.py
+++ b/rare/components/tabs/settings/widgets/dxvk.py
@@ -1,10 +1,10 @@
from PyQt5.QtCore import QCoreApplication
-from .overlay_settings import OverlaySettings, CustomOption
+from .overlays import OverlaySettings, CustomOption
class DxvkSettings(OverlaySettings):
- def __init__(self):
+ def __init__(self, parent=None):
super(DxvkSettings, self).__init__(
[
("fps", QCoreApplication.translate("DxvkSettings", "FPS")),
@@ -19,7 +19,8 @@ class DxvkSettings(OverlaySettings):
[
(CustomOption.number_input("scale", 1, True), QCoreApplication.translate("DxvkSettings", "Scale"))
],
- "DXVK_HUD", "0"
+ "DXVK_HUD", "0",
+ parent=parent
)
self.setTitle(self.tr("DXVK Settings"))
diff --git a/rare/components/tabs/settings/widgets/env_vars_model.py b/rare/components/tabs/settings/widgets/env_vars_model.py
index 08ba8a9c..f8b7364b 100644
--- a/rare/components/tabs/settings/widgets/env_vars_model.py
+++ b/rare/components/tabs/settings/widgets/env_vars_model.py
@@ -11,12 +11,14 @@ from rare.lgndr.core import LegendaryCore
from rare.utils.misc import icon
if platform.system() != "Windows":
+ from rare.utils.runners.wine import get_wine_environment
if platform.system() != "Darwin":
- from rare.utils import proton
+ from rare.utils.runners.proton import get_steam_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
@@ -27,14 +29,13 @@ class EnvVarsTableModel(QAbstractTableModel):
self.__data_map: ChainMap = ChainMap()
self.__readonly = [
- "STEAM_COMPAT_DATA_PATH",
- "WINEPREFIX",
"DXVK_HUD",
"MANGOHUD_CONFIG",
]
if platform.system() != "Windows":
+ self.__readonly.extend(get_wine_environment().keys())
if platform.system() != "Darwin":
- self.__readonly.extend(proton.get_steam_environment(None).keys())
+ self.__readonly.extend(get_steam_environment().keys())
self.__default: str = "default"
self.__appname: str = None
@@ -256,8 +257,6 @@ class EnvVarsTableModel(QAbstractTableModel):
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/mangohud.py b/rare/components/tabs/settings/widgets/mangohud.py
index e99570aa..a9394c4e 100644
--- a/rare/components/tabs/settings/widgets/mangohud.py
+++ b/rare/components/tabs/settings/widgets/mangohud.py
@@ -5,18 +5,17 @@ from PyQt5.QtCore import QCoreApplication, pyqtSignal
from PyQt5.QtWidgets import QMessageBox
from rare.shared import LegendaryCoreSingleton
-from .overlay_settings import OverlaySettings, CustomOption, ActivationStates
from rare.utils import config_helper
+from .overlays import OverlaySettings, CustomOption, ActivationStates
position_values = ["default", "top-left", "top-right", "middle-left", "middle-right", "bottom-left",
"bottom-right", "top-center"]
class MangoHudSettings(OverlaySettings):
-
set_wrapper_activated = pyqtSignal(bool)
- def __init__(self):
+ def __init__(self, parent=None):
super(MangoHudSettings, self).__init__(
[
("fps", QCoreApplication.translate("MangoSettings", "FPS")),
@@ -45,7 +44,8 @@ class MangoHudSettings(OverlaySettings):
QCoreApplication.translate("MangoSettings", "Position")
)
],
- "MANGOHUD_CONFIG", "no_display", set_activation_state=self.set_activation_state
+ "MANGOHUD_CONFIG", "no_display", set_activation_state=self.set_activation_state,
+ parent=parent
)
self.core = LegendaryCoreSingleton()
self.setTitle(self.tr("MangoHud Settings"))
diff --git a/rare/components/tabs/settings/widgets/overlay_settings.py b/rare/components/tabs/settings/widgets/overlays.py
similarity index 98%
rename from rare/components/tabs/settings/widgets/overlay_settings.py
rename to rare/components/tabs/settings/widgets/overlays.py
index 31c50727..5fd85bf5 100644
--- a/rare/components/tabs/settings/widgets/overlay_settings.py
+++ b/rare/components/tabs/settings/widgets/overlays.py
@@ -82,8 +82,8 @@ class OverlaySettings(QGroupBox, Ui_OverlaySettings):
def __init__(self, checkboxes_map: List[Tuple[str, str]], value_map: List[Tuple[CustomOption, str]],
config_env_var_name: str, no_display_value: str,
- set_activation_state: Callable[[Enum], None] = lambda x: None):
- super(OverlaySettings, self).__init__()
+ set_activation_state: Callable[[Enum], None] = lambda x: None, parent=None):
+ super(OverlaySettings, self).__init__(parent=parent)
self.setupUi(self)
self.core = LegendaryCoreSingleton()
self.config_env_var_name = config_env_var_name
diff --git a/rare/components/tabs/settings/widgets/pre_launch.py b/rare/components/tabs/settings/widgets/pre_launch.py
index 006f1cee..411e9ce1 100644
--- a/rare/components/tabs/settings/widgets/pre_launch.py
+++ b/rare/components/tabs/settings/widgets/pre_launch.py
@@ -2,18 +2,18 @@ import os
import shutil
from typing import Tuple
-from PyQt5.QtWidgets import QHBoxLayout, QCheckBox, QFileDialog
+from PyQt5.QtWidgets import QHBoxLayout, QCheckBox, QFileDialog, QWidget
from rare.shared import LegendaryCoreSingleton
from rare.utils import config_helper
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
-class PreLaunchSettings(QHBoxLayout):
+class PreLaunchSettings(QWidget):
app_name: str
- def __init__(self):
- super(PreLaunchSettings, self).__init__()
+ def __init__(self, parent=None):
+ super(PreLaunchSettings, self).__init__(parent=parent)
self.core = LegendaryCoreSingleton()
self.edit = PathEdit(
path="",
@@ -22,12 +22,15 @@ class PreLaunchSettings(QHBoxLayout):
edit_func=self.edit_command,
save_func=self.save_pre_launch_command,
)
- self.layout().addWidget(self.edit)
self.wait_check = QCheckBox(self.tr("Wait for finish"))
- self.layout().addWidget(self.wait_check)
self.wait_check.stateChanged.connect(self.save_wait_finish)
+ layout = QHBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.addWidget(self.edit)
+ layout.addWidget(self.wait_check)
+
def edit_command(self, text: str) -> Tuple[bool, str, int]:
if not text.strip():
return True, text, IndicatorReasonsCommon.VALID
diff --git a/rare/components/tabs/settings/widgets/proton.py b/rare/components/tabs/settings/widgets/proton.py
index 658aaf87..4505bcfe 100644
--- a/rare/components/tabs/settings/widgets/proton.py
+++ b/rare/components/tabs/settings/widgets/proton.py
@@ -1,85 +1,106 @@
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.app_name: str = "default"
+ self.core = RareCore.instance().core()
+ self.wrappers: Wrappers = RareCore.instance().wrappers()
+ self.tool_wrapper: Optional[Wrapper] = None
- self.proton_prefix.setEnabled(False)
- self.proton_prefix.setText("")
-
- self._linux_settings.ui.wine_groupbox.setEnabled(True)
- 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"))
+ 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.environ_changed.emit("STEAM_COMPAT_CLIENT_INSTALL_PATH")
+ 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)
+ super().showEvent(a0)
- self.proton_prefix.setText(os.path.expanduser("~/.proton"))
+ def __on_proton_changed(self, index):
+ steam_tool: Union[proton.ProtonTool, proton.CompatibilityTool] = self.ui.proton_combo.itemData(index)
- # Don't use Wine
- self._linux_settings.wine_exec.setText("")
- self._linux_settings.wine_prefix.setText("")
+ 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)
- config_helper.save_config()
+ 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:
+ wrapper = Wrapper(
+ command=steam_tool.command(), name=steam_tool.name, wtype=WrapperType.COMPAT_TOOL
+ )
+ wrappers.append(wrapper)
+ self.tool_wrapper = wrapper
+ self.wrappers.set_game_wrapper_list(self.app_name, wrappers)
- def proton_prefix_edit(self, text: str) -> Tuple[bool, str, int]:
+ self.proton_prefix.setEnabled(steam_tool is not None)
+ self.proton_prefix.setText(os.path.expanduser("~/.proton") if steam_tool is not None else "")
+
+ self.tool_enabled.emit(steam_tool is not None)
+ config.save_config()
+
+ @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 +109,9 @@ class ProtonSettings(QGroupBox):
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/rpc.py b/rare/components/tabs/settings/widgets/rpc.py
index 3d2ad312..e3a3a7e5 100644
--- a/rare/components/tabs/settings/widgets/rpc.py
+++ b/rare/components/tabs/settings/widgets/rpc.py
@@ -2,6 +2,7 @@ from PyQt5.QtCore import QSettings
from PyQt5.QtWidgets import QGroupBox
from rare.shared import GlobalSignalsSingleton
+from rare.models.options import options
from rare.ui.components.tabs.settings.widgets.rpc import Ui_RPCSettings
@@ -13,22 +14,22 @@ class RPCSettings(QGroupBox, Ui_RPCSettings):
self.settings = QSettings()
- self.enable.setCurrentIndex(self.settings.value("rpc_enable", 0, int))
- self.enable.currentIndexChanged.connect(self.changed)
+ self.enable.setCurrentIndex(self.settings.value(*options.rpc_enable))
+ self.enable.currentIndexChanged.connect(self.__enable_changed)
- self.show_game.setChecked((self.settings.value("rpc_name", True, bool)))
+ self.show_game.setChecked((self.settings.value(*options.rpc_name)))
self.show_game.stateChanged.connect(
- lambda: self.settings.setValue("rpc_game", self.show_game.isChecked())
+ lambda: self.settings.setValue(options.rpc_name.key, self.show_game.isChecked())
)
- self.show_os.setChecked((self.settings.value("rpc_os", True, bool)))
+ self.show_os.setChecked((self.settings.value(*options.rpc_os)))
self.show_os.stateChanged.connect(
- lambda: self.settings.setValue("rpc_os", self.show_os.isChecked())
+ lambda: self.settings.setValue(options.rpc_os.key, self.show_os.isChecked())
)
- self.show_time.setChecked((self.settings.value("rpc_time", True, bool)))
+ self.show_time.setChecked((self.settings.value(*options.rpc_time)))
self.show_time.stateChanged.connect(
- lambda: self.settings.setValue("rpc_time", self.show_time.isChecked())
+ lambda: self.settings.setValue(options.rpc_time.key, self.show_time.isChecked())
)
try:
@@ -37,6 +38,6 @@ class RPCSettings(QGroupBox, Ui_RPCSettings):
self.setDisabled(True)
self.setToolTip(self.tr("Pypresence is not installed"))
- def changed(self, i):
- self.settings.setValue("rpc_enable", i)
+ def __enable_changed(self, i):
+ self.settings.setValue(options.rpc_enable.key, i)
self.signals.discord_rpc.apply_settings.emit()
diff --git a/rare/components/tabs/settings/widgets/unix.py b/rare/components/tabs/settings/widgets/unix.py
new file mode 100644
index 00000000..28c9f21a
--- /dev/null
+++ b/rare/components/tabs/settings/widgets/unix.py
@@ -0,0 +1,15 @@
+from components.tabs.settings import LinuxSettings
+
+
+class LinuxAppSettings(LinuxSettings):
+ def __init__(self, parent=None):
+ super(LinuxAppSettings, self).__init__(parent=parent)
+
+ 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.dxvk.load_settings(self.name)
+
+ self.mangohud.load_settings(self.name)
\ No newline at end of file
diff --git a/rare/components/tabs/settings/widgets/linux.py b/rare/components/tabs/settings/widgets/wine.py
similarity index 58%
rename from rare/components/tabs/settings/widgets/linux.py
rename to rare/components/tabs/settings/widgets/wine.py
index 7df32196..cf7a8d27 100644
--- a/rare/components/tabs/settings/widgets/linux.py
+++ b/rare/components/tabs/settings/widgets/wine.py
@@ -3,31 +3,29 @@ import shutil
from logging import getLogger
from PyQt5.QtCore import pyqtSignal
-from PyQt5.QtWidgets import QFileDialog, QWidget
+from PyQt5.QtWidgets import QFileDialog, QWidget, QFormLayout, QGroupBox
-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.ui.components.tabs.settings.linux import Ui_LinuxSettings
+from rare.ui.components.tabs.settings.widgets.wine import Ui_WineSettings
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
from rare.utils import config_helper
logger = getLogger("LinuxSettings")
-class LinuxSettings(QWidget):
+class WineSettings(QGroupBox):
# str: option key
environ_changed = pyqtSignal(str)
def __init__(self, name=None, parent=None):
- super(LinuxSettings, self).__init__(parent=parent)
- self.ui = Ui_LinuxSettings()
+ super(WineSettings, self).__init__(parent=parent)
+ self.ui = Ui_WineSettings()
self.ui.setupUi(self)
self.core = LegendaryCoreSingleton()
self.signals = GlobalSignalsSingleton()
- self.name = name if name is not None else "default"
+ self.app_name: str = "default"
# Wine prefix
self.wine_prefix = PathEdit(
@@ -36,49 +34,46 @@ class LinuxSettings(QWidget):
edit_func=lambda path: (os.path.isdir(path) or not path, path, IndicatorReasonsCommon.DIR_NOT_EXISTS),
save_func=self.save_prefix,
)
- self.ui.prefix_layout.addWidget(self.wine_prefix)
+ self.ui.main_layout.setWidget(
+ self.ui.main_layout.getWidgetPosition(self.ui.prefix_label)[0],
+ QFormLayout.FieldRole,
+ self.wine_prefix
+ )
# 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)
-
- # dxvk
- 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.mangohud = MangoHudSettings()
- self.mangohud.environ_changed.connect(self.environ_changed)
- self.ui.linux_layout.addWidget(self.mangohud)
- self.mangohud.load_settings(self.name)
-
+ self.ui.main_layout.setWidget(
+ self.ui.main_layout.getWidgetPosition(self.ui.exec_label)[0],
+ QFormLayout.FieldRole,
+ self.wine_exec
+ )
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)
logger.debug(f"Set {setting} in {f'[{section}]'} to {text}")
diff --git a/rare/components/tabs/settings/widgets/wrapper.py b/rare/components/tabs/settings/widgets/wrapper.py
index b8b35c43..def0af39 100644
--- a/rare/components/tabs/settings/widgets/wrapper.py
+++ b/rare/components/tabs/settings/widgets/wrapper.py
@@ -1,9 +1,9 @@
-import re
+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 +16,41 @@ from PyQt5.QtWidgets import (
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 +67,8 @@ class WrapperWidget(QFrame):
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 +80,39 @@ class WrapperWidget(QFrame):
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")
- @pyqtSlot()
- def __delete(self):
- self.delete_wrapper.emit(self.text)
+ def data(self) -> Wrapper:
+ return self.wrapper
+ @pyqtSlot()
+ 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)
@@ -114,30 +120,22 @@ class WrapperWidget(QFrame):
class WrapperSettings(QWidget):
- def __init__(self):
- super(WrapperSettings, self).__init__()
+ def __init__(self, parent=None):
+ super(WrapperSettings, self).__init__(parent=parent)
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 +147,24 @@ class WrapperSettings(QWidget):
self.wrapper_scroll.verticalScrollBar().setObjectName(
f"{self.wrapper_scroll.objectName()}Bar")
+ self.ui.wrapper_settings_layout.setAlignment(Qt.AlignTop)
+
+ 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 +174,183 @@ class WrapperSettings(QWidget):
- self.wrapper_scroll.contentsRect().height()
)
- def get_wrapper_string(self):
- return " ".join(self.get_wrapper_list())
+ @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 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]
-
- 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)
+ widget.update_wrapper.connect(self.__update_wrapper)
+ widget.delete_wrapper.connect(self.__delete_wrapper)
- self.wrappers[show_text] = widget
+ 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)
- if not from_load:
- self.save()
+ def add_user_wrapper(self, wrapper: Wrapper, position: int = -1):
+ if not wrapper:
+ return
- @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()
+ 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
- if not self.wrappers:
+ # 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
+
+ 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
+
+ self.add_wrapper(wrapper, position)
+
+ @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)
- self.save()
+ @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)
- @pyqtSlot(str, str)
- def update_wrapper(self, old: str, new: str):
- key = old.split()[0]
- idx = self.scroll_content.layout().indexOf(self.wrappers[key])
- self.delete_wrapper(key)
- self.add_wrapper(new, position=idx)
-
- def save(self):
- # save wrappers twice, to support wrappers with spaces
- if len(self.wrappers) == 0:
- 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())
-
- def load_settings(self, app_name: str):
- self.app_name = app_name
- for i in self.wrappers.values():
- i.deleteLater()
- self.wrappers.clear()
-
- wrappers = self.settings.value(f"{self.app_name}/wrapper", [], str)
-
- 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]
-
- for wrapper in wrappers:
- self.add_wrapper(wrapper, from_load=True)
-
- 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/components/tray_icon.py b/rare/components/tray_icon.py
index 8b40cca1..e9d56185 100644
--- a/rare/components/tray_icon.py
+++ b/rare/components/tray_icon.py
@@ -5,6 +5,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, QSettings
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QSystemTrayIcon, QMenu, QAction, QApplication
+from rare.models.options import options
from rare.shared import RareCore
logger = getLogger("TrayIcon")
@@ -61,7 +62,7 @@ class TrayIcon(QSystemTrayIcon):
@pyqtSlot(str, str)
def notify(self, title: str, body: str):
- if self.settings.value("notification", True, bool):
+ if self.settings.value(*options.notification):
self.showMessage(f"{QApplication.applicationName()} - {title}", body, QSystemTrayIcon.Information, 4000)
@pyqtSlot()
diff --git a/rare/launcher/__init__.py b/rare/launcher/__init__.py
index a0439924..978b7854 100644
--- a/rare/launcher/__init__.py
+++ b/rare/launcher/__init__.py
@@ -17,6 +17,7 @@ from legendary.models.game import SaveGameStatus
from rare.lgndr.core import LegendaryCore
from rare.models.base_game import RareGameSlim
from rare.models.launcher import ErrorModel, Actions, FinishedModel, BaseModel, StateChangedModel
+from rare.models.options import options
from rare.widgets.rare_app import RareApp, RareAppException
from .cloud_sync_dialog import CloudSyncDialog, CloudSyncDialogResult
from .console_dialog import ConsoleDialog
@@ -145,7 +146,7 @@ class RareLauncher(RareApp):
lang = self.settings.value("language", self.core.language_code, type=str)
self.load_translator(lang)
- if QSettings().value("show_console", False, bool):
+ if QSettings(self).value(*options.log_games):
self.console = ConsoleDialog()
self.console.show()
diff --git a/rare/main.py b/rare/main.py
index ac173cfc..4d324d20 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"
if "LEGENDARY_CONFIG_PATH" in os.environ:
os.environ["LEGENDARY_CONFIG_PATH"] = os.path.expanduser(os.environ["LEGENDARY_CONFIG_PATH"])
diff --git a/rare/models/base_game.py b/rare/models/base_game.py
index 0033530a..8b65295f 100644
--- a/rare/models/base_game.py
+++ b/rare/models/base_game.py
@@ -10,6 +10,7 @@ from legendary.lfs import eos
from legendary.models.game import SaveGameFile, SaveGameStatus, Game, InstalledGame
from legendary.utils.selective_dl import get_sdl_appname
+from rare.models.options import options
from rare.lgndr.core import LegendaryCore
from rare.models.install import UninstallOptionsModel, InstallOptionsModel
@@ -222,11 +223,13 @@ class RareGameSlim(RareGameBase):
@property
def auto_sync_saves(self):
- return self.supports_cloud_saves and QSettings().value(
- f"{self.app_name}/auto_sync_cloud",
- QSettings().value("auto_sync_cloud", False, bool),
- bool
+ auto_sync_cloud = QSettings(self).value(
+ f"{self.app_name}/{options.auto_sync_cloud.key}",
+ options.auto_sync_cloud.default,
+ options.auto_sync_cloud.dtype
)
+ auto_sync_cloud = auto_sync_cloud or QSettings(self).value(*options.auto_sync_cloud)
+ return self.supports_cloud_saves and auto_sync_cloud
@property
def save_path(self) -> Optional[str]:
diff --git a/rare/models/game.py b/rare/models/game.py
index 8cc509d4..94b31f08 100644
--- a/rare/models/game.py
+++ b/rare/models/game.py
@@ -2,7 +2,7 @@ import json
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
@@ -19,7 +19,7 @@ from rare.shared.game_process import GameProcess
from rare.shared.image_manager import ImageManager
from rare.utils.paths import data_dir, get_rare_executable
from rare.utils.steam_grades import get_rating
-from rare.utils.config_helper import add_envvar, remove_envvar
+from rare.utils.config_helper import add_envvar
logger = getLogger("RareGame")
@@ -27,11 +27,10 @@ logger = getLogger("RareGame")
class RareGame(RareGameSlim):
@dataclass
class Metadata:
- auto_update: bool = False
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
@@ -40,24 +39,23 @@ class RareGame(RareGameSlim):
@classmethod
def from_dict(cls, data: Dict):
return cls(
- auto_update=data.get("auto_update", False),
queued=data.get("queued", False),
queue_pos=data.get("queue_pos", None),
- last_played=datetime.fromisoformat(data["last_played"]) if data.get("last_played", None) else datetime.min,
- grant_date=datetime.fromisoformat(data["grant_date"]) if data.get("grant_date", None) else 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 datetime.min,
steam_appid=data.get("steam_appid", None),
steam_grade=data.get("steam_grade", None),
- steam_date=datetime.fromisoformat(data["steam_date"]) if data.get("steam_date", None) else datetime.min,
+ steam_date=datetime.fromisoformat(x) if (x := data.get("steam_date", None)) else datetime.min,
tags=data.get("tags", []),
)
- def as_dict(self):
+ @property
+ def __dict__(self):
return dict(
- auto_update=self.auto_update,
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,
@@ -81,8 +79,7 @@ class RareGame(RareGameSlim):
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()
@@ -143,13 +140,14 @@ class RareGame(RareGameSlim):
def __load_metadata_json() -> Dict:
if RareGame.__metadata_json is None:
metadata = {}
+ file = os.path.join(data_dir(), "game_meta.json")
try:
- with open(os.path.join(data_dir(), "game_meta.json"), "r") as metadata_fh:
- metadata = json.load(metadata_fh)
+ with open(file, "r") as f:
+ metadata = json.load(f)
except FileNotFoundError:
- logger.info("Game metadata json file does not exist.")
+ logger.info("%s does not exist", file)
except json.JSONDecodeError:
- logger.warning("Game metadata json file is corrupt.")
+ logger.warning("%s is corrupt", file)
finally:
RareGame.__metadata_json = metadata
return RareGame.__metadata_json
@@ -166,9 +164,9 @@ class RareGame(RareGameSlim):
with RareGame.__metadata_lock:
metadata: Dict = self.__load_metadata_json()
# pylint: disable=unsupported-assignment-operation
- metadata[self.app_name] = self.metadata.as_dict()
- with open(os.path.join(data_dir(), "game_meta.json"), "w") as metadata_json:
- json.dump(metadata, metadata_json, indent=2)
+ metadata[self.app_name] = vars(self.metadata)
+ with open(os.path.join(data_dir(), "game_meta.json"), "w+") as file:
+ json.dump(metadata, file, indent=2)
def update_game(self):
self.game = self.core.get_game(
@@ -432,29 +430,27 @@ class RareGame(RareGameSlim):
def steam_grade(self) -> str:
if platform.system() == "Windows" or self.is_unreal:
return "na"
- elapsed_time = abs(datetime.utcnow() - self.metadata.steam_date)
- if (
- self.metadata.steam_grade is not None
- and self.metadata.steam_appid is not None
- and elapsed_time.days < 3
- ):
- return self.metadata.steam_grade
+ if self.metadata.steam_grade != "pending":
+ elapsed_time = abs(datetime.utcnow() - self.metadata.steam_date)
- def _set_steam_grade():
- appid, rating = get_rating(self.core, self.app_name)
- self.set_steam_grade(appid, rating)
-
- worker = QRunnable.create(_set_steam_grade)
- QThreadPool.globalInstance().start(worker)
- return "pending"
+ if elapsed_time.days > 3 and (self.metadata.steam_grade is None or self.metadata.steam_appid is None):
+ def _set_steam_grade():
+ appid, rating = get_rating(self.core, self.app_name)
+ self.set_steam_grade(appid, rating)
+ worker = QRunnable.create(_set_steam_grade)
+ QThreadPool.globalInstance().start(worker)
+ self.metadata.steam_grade = "pending"
+ return self.metadata.steam_grade
@property
def steam_appid(self) -> Optional[int]:
return self.metadata.steam_appid
def set_steam_grade(self, appid: int, grade: str) -> None:
- if appid or self.steam_appid is None:
+ if appid and self.steam_appid is None:
add_envvar(self.app_name, "SteamAppId", str(appid))
+ add_envvar(self.app_name, "SteamGameId", str(appid))
+ add_envvar(self.app_name, "STEAM_COMPAT_APP_ID", str(appid))
self.metadata.steam_appid = appid
self.metadata.steam_grade = grade
self.metadata.steam_date = datetime.utcnow()
@@ -463,17 +459,17 @@ class RareGame(RareGameSlim):
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/library.py b/rare/models/library.py
new file mode 100644
index 00000000..9318a2fe
--- /dev/null
+++ b/rare/models/library.py
@@ -0,0 +1,19 @@
+from enum import IntEnum
+
+
+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
diff --git a/rare/models/options.py b/rare/models/options.py
new file mode 100644
index 00000000..4a062d09
--- /dev/null
+++ b/rare/models/options.py
@@ -0,0 +1,53 @@
+import platform as pf
+from argparse import Namespace
+from typing import Any, Type
+from .library import LibraryFilter, LibraryOrder
+
+
+class Value(Namespace):
+ key: str
+ default: Any
+ dtype: Type
+
+ def __len__(self):
+ return len(self.__dict__)
+
+ def __iter__(self):
+ for v in self.__dict__.values():
+ yield v
+
+
+# They key names are set to the existing option name
+class Defaults(Namespace):
+ win32_meta = Value(key="win32_meta", default=False, dtype=bool)
+ macos_meta = Value(key="macos_meta", default=pf.system() == "Darwin", dtype=bool)
+ unreal_meta = Value(key="unreal_meta", default=False, dtype=bool)
+ exclude_non_asset = Value(key="exclude_non_asset", default=False, dtype=bool)
+ exclude_entitlements = Value(key="exclude_entitlements", default=False, dtype=bool)
+
+ sys_tray = Value(key="sys_tray", default=True, dtype=bool)
+ auto_update = Value(key="auto_update", default=False, dtype=bool)
+ auto_sync_cloud = Value(key="auto_sync_cloud", default=False, dtype=bool)
+ confirm_start = Value(key="confirm_start", default=False, dtype=bool)
+ save_size = Value(key="save_size", default=False, dtype=bool)
+ window_size = Value(key="window_size", default=(1280, 720), dtype=tuple)
+ notification = Value(key="notification", default=True, dtype=bool)
+ log_games = Value(key="show_console", default=False, dtype=bool)
+
+ library_filter = Value(
+ key="library_filter",
+ default=int(LibraryFilter.MAC if pf.system() == "Darwin" else LibraryFilter.ALL), dtype=int
+ )
+ library_order = Value(
+ key="library_order", default=int(LibraryOrder.TITLE), dtype=int
+ )
+
+ rpc_enable = Value(key="rpc_enable", default=0, dtype=int)
+ rpc_name = Value(key="rpc_game", default=True, dtype=bool)
+ rpc_time = Value(key="rpc_time", default=True, dtype=bool)
+ rpc_os = Value(key="rpc_os", default=True, dtype=bool)
+
+
+options = Defaults()
+
+__all__ = ['options']
diff --git a/rare/models/pathspec.py b/rare/models/pathspec.py
index 24c9d1a5..5249476e 100644
--- a/rare/models/pathspec.py
+++ b/rare/models/pathspec.py
@@ -2,46 +2,72 @@ import os
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
+ @staticmethod
+ def egl_appdata() -> str:
+ return r"%LOCALAPPDATA%\EpicGamesLauncher\Saved\Config\Windows"
- 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_programdata() -> str:
+ return r"%PROGRAMDATA%\Epic\EpicGamesLauncher\Data\Manifests"
- @property
- def wine_egl_programdata(self):
- return self.egl_programdata.replace("\\", "/").replace("%PROGRAMDATA%", self.wine_programdata)
+ @staticmethod
+ def wine_programdata() -> str:
+ return r"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_programdata() -> str:
+ return PathSpec.egl_programdata(
+ ).replace(
+ "\\", "/"
+ ).replace(
+ "%PROGRAMDATA%", PathSpec.wine_programdata()
+ )
+
+ @staticmethod
+ def prefix_egl_programdata(prefix: str) -> str:
+ return os.path.join(prefix, "dosdevices/c:", PathSpec.wine_egl_programdata())
+
+ @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 00000000..09a0fc9e
--- /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/rare_core.py b/rare/shared/rare_core.py
index 9afcaac2..d812addf 100644
--- a/rare/shared/rare_core.py
+++ b/rare/shared/rare_core.py
@@ -29,7 +29,7 @@ from .workers import (
)
from .workers.uninstall import uninstall_game
from .workers.worker import QueueWorkerInfo, QueueWorkerState
-from rare.utils import config_helper
+from .wrappers import Wrappers
logger = getLogger("RareCore")
@@ -53,6 +53,8 @@ class RareCore(QObject):
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()
@@ -61,8 +63,8 @@ class RareCore(QObject):
self.core(init=True)
config_helper.init_config_handler(self.__core)
self.image_manager(init=True)
-
- self.settings = QSettings(self)
+ self.__settings = QSettings(self)
+ self.__wrappers = Wrappers()
self.queue_workers: List[QueueWorker] = []
self.queue_threadpool = QThreadPool()
@@ -205,6 +207,12 @@ class RareCore(QObject):
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
@@ -328,10 +336,13 @@ class RareCore(QObject):
self.__core.lgd.entitlements = result
self.__fetched_entitlements = True
- logger.info(f"Acquired data for {FetchWorker.Result(result_type).name}")
+ logger.info("Acquired data from %s worker", FetchWorker.Result(result_type).name)
if all([self.__fetched_games_dlcs, self.__fetched_entitlements]):
- logger.debug(f"Fetch time {time.perf_counter() - self.__start_time} seconds")
+ logger.debug("Fetch time %s seconds", time.perf_counter() - self.__start_time)
+ 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)
@@ -366,7 +377,7 @@ class RareCore(QObject):
continue
self.__library[app_name].load_saves(saves)
except (HTTPError, ConnectionError) as e:
- logger.error(f"Exception while fetching saves from EGS.")
+ logger.error("Exception while fetching saves from EGS.")
logger.error(e)
return
logger.info(f"Saves: {len(saves_dict)}")
diff --git a/rare/shared/workers/fetch.py b/rare/shared/workers/fetch.py
index 1d4fe761..cac6a82e 100644
--- a/rare/shared/workers/fetch.py
+++ b/rare/shared/workers/fetch.py
@@ -8,6 +8,7 @@ from requests.exceptions import HTTPError, ConnectionError
from rare.lgndr.core import LegendaryCore
from rare.utils.metrics import timelogger
+from rare.models.options import options
from .worker import Worker
logger = getLogger("FetchWorker")
@@ -32,12 +33,12 @@ class FetchWorker(Worker):
class EntitlementsWorker(FetchWorker):
- def __init__(self, core: LegendaryCore, args: Namespace):
- super(EntitlementsWorker, self).__init__(core, args)
def run_real(self):
+
+ want_entitlements = not self.settings.value(*options.exclude_entitlements)
+
entitlements = ()
- want_entitlements = not self.settings.value("exclude_entitlements", False, bool)
if want_entitlements:
# Get entitlements, Ubisoft integration also uses them
self.signals.progress.emit(0, self.signals.tr("Updating entitlements"))
@@ -51,20 +52,20 @@ class EntitlementsWorker(FetchWorker):
class GamesDlcsWorker(FetchWorker):
- def __init__(self, core: LegendaryCore, args: Namespace):
- super(GamesDlcsWorker, self).__init__(core, args)
- self.exclude_non_asset = QSettings().value("exclude_non_asset", False, bool)
-
def run_real(self):
# Fetch regular EGL games with assets
- want_unreal = self.settings.value("unreal_meta", False, bool) or self.args.debug
- want_win32 = self.settings.value("win32_meta", False, bool)
- want_macos = self.settings.value("macos_meta", False, bool)
+ # want_unreal = self.settings.value(*options.unreal_meta) or self.args.debug
+ # want_win32 = self.settings.value(*options.win32_meta) or self.args.debug
+ # want_macos = self.settings.value(*options.macos_meta) or self.args.debug
+ want_unreal = self.settings.value(*options.unreal_meta)
+ want_win32 = self.settings.value(*options.win32_meta)
+ want_macos = self.settings.value(*options.macos_meta)
+ want_non_asset = not self.settings.value(*options.exclude_non_asset)
need_macos = platform.system() == "Darwin"
- need_windows = not any([want_win32, want_macos, need_macos, self.args.debug]) and not self.args.offline
+ need_windows = not any([want_win32, want_macos, need_macos]) and not self.args.offline
- if want_win32 or self.args.debug:
+ if want_win32:
logger.info(
"Requesting Win32 metadata due to %s, %s Unreal engine",
"settings" if want_win32 else "debug",
@@ -76,7 +77,7 @@ class GamesDlcsWorker(FetchWorker):
update_assets=not self.args.offline, platform="Win32", skip_ue=not want_unreal
)
- if need_macos or want_macos or self.args.debug:
+ if need_macos or want_macos:
logger.info(
"Requesting macOS metadata due to %s, %s Unreal engine",
"platform" if need_macos else "settings" if want_macos else "debug",
@@ -101,7 +102,6 @@ class GamesDlcsWorker(FetchWorker):
logger.info(f"Games: %s. Games with DLCs: %s", len(games), len(dlc_dict))
# Fetch non-asset games
- want_non_asset = not self.settings.value("exclude_non_asset", False, bool)
if want_non_asset:
self.signals.progress.emit(30, self.signals.tr("Updating non-asset game metadata"))
try:
diff --git a/rare/shared/workers/uninstall.py b/rare/shared/workers/uninstall.py
index 98fe21b7..23566cee 100644
--- a/rare/shared/workers/uninstall.py
+++ b/rare/shared/workers/uninstall.py
@@ -11,7 +11,7 @@ from rare.lgndr.glue.arguments import LgndrUninstallGameArgs
from rare.lgndr.glue.monkeys import LgndrIndirectStatus
from rare.models.game import RareGame
from rare.models.install import UninstallOptionsModel
-from rare.utils import config_helper
+from rare.utils import config_helper as config
from rare.utils.paths import desktop_links_supported, desktop_link_types, desktop_link_path
from .worker import Worker
@@ -31,7 +31,7 @@ def uninstall_game(
logger.info('Removing registry entries...')
if platform.system() != "Window":
- prefixes = config_helper.get_wine_prefixes()
+ prefixes = config.get_prefixes()
if platform.system() == "Darwin":
# TODO: add crossover support
pass
@@ -65,10 +65,10 @@ def uninstall_game(
)
if not keep_config:
logger.info("Removing sections in config file")
- config_helper.remove_section(rgame.app_name)
- config_helper.remove_section(f"{rgame.app_name}.env")
+ config.remove_section(rgame.app_name)
+ config.remove_section(f"{rgame.app_name}.env")
- config_helper.save_config()
+ config.save_config()
return status.success, status.message
diff --git a/rare/shared/workers/wine_resolver.py b/rare/shared/workers/wine_resolver.py
index fe63ea4e..ee8bce1a 100644
--- a/rare/shared/workers/wine_resolver.py
+++ b/rare/shared/workers/wine_resolver.py
@@ -3,50 +3,71 @@ import platform
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, 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)
+ 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):
- 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)
+ path = self._resolve_unix_path(self.command, self.environ, self.path)
+ self.signals.result_ready.emit(path, "default")
+ return
+
+
+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):
+ 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 @@ class OriginWineWorker(QRunnable):
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 @@ class OriginWineWorker(QRunnable):
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 getting 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
+ 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 @@ class OriginWineWorker(QRunnable):
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 00000000..3d146919
--- /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/ui/components/tabs/settings/game.py b/rare/ui/components/tabs/settings/game.py
new file mode 100644
index 00000000..ee43ea14
--- /dev/null
+++ b/rare/ui/components/tabs/settings/game.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+
+# Form implementation generated from reading ui file 'rare/ui/components/tabs/settings/game_settings.ui'
+#
+# Created by: PyQt5 UI code generator 5.15.10
+#
+# WARNING: Any manual changes made to this file will be lost when pyuic5 is
+# run again. Do not edit this file unless you know what you are doing.
+
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+
+class Ui_GameSettings(object):
+ def setupUi(self, GameSettings):
+ GameSettings.setObjectName("GameSettings")
+ GameSettings.resize(393, 202)
+ self.main_layout = QtWidgets.QVBoxLayout(GameSettings)
+ self.main_layout.setObjectName("main_layout")
+ self.launch_settings_group = QtWidgets.QGroupBox(GameSettings)
+ sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.launch_settings_group.sizePolicy().hasHeightForWidth())
+ self.launch_settings_group.setSizePolicy(sizePolicy)
+ self.launch_settings_group.setObjectName("launch_settings_group")
+ self.launch_layout = QtWidgets.QFormLayout(self.launch_settings_group)
+ self.launch_layout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
+ self.launch_layout.setObjectName("launch_layout")
+ self.skip_update_label = QtWidgets.QLabel(self.launch_settings_group)
+ self.skip_update_label.setObjectName("skip_update_label")
+ self.launch_layout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.skip_update_label)
+ self.skip_update_combo = QtWidgets.QComboBox(self.launch_settings_group)
+ sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.skip_update_combo.sizePolicy().hasHeightForWidth())
+ self.skip_update_combo.setSizePolicy(sizePolicy)
+ self.skip_update_combo.setObjectName("skip_update_combo")
+ self.skip_update_combo.addItem("")
+ self.skip_update_combo.addItem("")
+ self.skip_update_combo.addItem("")
+ self.launch_layout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.skip_update_combo)
+ self.offline_label = QtWidgets.QLabel(self.launch_settings_group)
+ self.offline_label.setObjectName("offline_label")
+ self.launch_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.offline_label)
+ self.offline_combo = QtWidgets.QComboBox(self.launch_settings_group)
+ sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.offline_combo.sizePolicy().hasHeightForWidth())
+ self.offline_combo.setSizePolicy(sizePolicy)
+ self.offline_combo.setObjectName("offline_combo")
+ self.offline_combo.addItem("")
+ self.offline_combo.addItem("")
+ self.offline_combo.addItem("")
+ self.launch_layout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.offline_combo)
+ self.launch_params_label = QtWidgets.QLabel(self.launch_settings_group)
+ self.launch_params_label.setObjectName("launch_params_label")
+ self.launch_layout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.launch_params_label)
+ self.launch_params_edit = QtWidgets.QLineEdit(self.launch_settings_group)
+ self.launch_params_edit.setObjectName("launch_params_edit")
+ self.launch_layout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.launch_params_edit)
+ self.pre_launch_label = QtWidgets.QLabel(self.launch_settings_group)
+ self.pre_launch_label.setObjectName("pre_launch_label")
+ self.launch_layout.setWidget(5, QtWidgets.QFormLayout.LabelRole, self.pre_launch_label)
+ self.override_exe_label = QtWidgets.QLabel(self.launch_settings_group)
+ self.override_exe_label.setObjectName("override_exe_label")
+ self.launch_layout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.override_exe_label)
+ self.wrapper_label = QtWidgets.QLabel(self.launch_settings_group)
+ self.wrapper_label.setObjectName("wrapper_label")
+ self.launch_layout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.wrapper_label)
+ self.main_layout.addWidget(self.launch_settings_group)
+
+ self.retranslateUi(GameSettings)
+
+ def retranslateUi(self, GameSettings):
+ _translate = QtCore.QCoreApplication.translate
+ GameSettings.setWindowTitle(_translate("GameSettings", "GameSettings"))
+ self.launch_settings_group.setTitle(_translate("GameSettings", "Launch Settings"))
+ self.skip_update_label.setText(_translate("GameSettings", "Skip update check"))
+ self.skip_update_combo.setItemText(0, _translate("GameSettings", "Default"))
+ self.skip_update_combo.setItemText(1, _translate("GameSettings", "Yes"))
+ self.skip_update_combo.setItemText(2, _translate("GameSettings", "No"))
+ self.offline_label.setText(_translate("GameSettings", "Offline mode"))
+ self.offline_combo.setItemText(0, _translate("GameSettings", "Default"))
+ self.offline_combo.setItemText(1, _translate("GameSettings", "Yes"))
+ self.offline_combo.setItemText(2, _translate("GameSettings", "No"))
+ self.launch_params_label.setText(_translate("GameSettings", "Launch parameters"))
+ self.launch_params_edit.setPlaceholderText(_translate("GameSettings", "parameters"))
+ self.pre_launch_label.setText(_translate("GameSettings", "Pre-launch command"))
+ self.override_exe_label.setText(_translate("GameSettings", "Override executable"))
+ self.wrapper_label.setText(_translate("GameSettings", "Wrappers"))
+
+
+if __name__ == "__main__":
+ import sys
+ app = QtWidgets.QApplication(sys.argv)
+ GameSettings = QtWidgets.QWidget()
+ ui = Ui_GameSettings()
+ ui.setupUi(GameSettings)
+ GameSettings.show()
+ sys.exit(app.exec_())
diff --git a/rare/ui/components/tabs/settings/game_settings.ui b/rare/ui/components/tabs/settings/game.ui
similarity index 75%
rename from rare/ui/components/tabs/settings/game_settings.ui
rename to rare/ui/components/tabs/settings/game.ui
index 2927d7e9..88c26201 100644
--- a/rare/ui/components/tabs/settings/game_settings.ui
+++ b/rare/ui/components/tabs/settings/game.ui
@@ -6,14 +6,14 @@
0
0
- 483
- 154
+ 393
+ 202
GameSettings
-
+
-
@@ -25,7 +25,7 @@
Launch Settings
-
+
Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
@@ -37,7 +37,7 @@
-
-
+
0
@@ -69,7 +69,7 @@
-
-
+
0
@@ -101,12 +101,33 @@
-
-
+
parameters
+ -
+
+
+ Pre-launch command
+
+
+
+ -
+
+
+ Override executable
+
+
+
+ -
+
+
+ Wrappers
+
+
+
diff --git a/rare/ui/components/tabs/settings/game_settings.py b/rare/ui/components/tabs/settings/game_settings.py
deleted file mode 100644
index 0a1280be..00000000
--- a/rare/ui/components/tabs/settings/game_settings.py
+++ /dev/null
@@ -1,100 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Form implementation generated from reading ui file 'rare/ui/components/tabs/settings/game_settings.ui'
-#
-# Created by: PyQt5 UI code generator 5.15.9
-#
-# WARNING: Any manual changes made to this file will be lost when pyuic5 is
-# run again. Do not edit this file unless you know what you are doing.
-
-
-from PyQt5 import QtCore, QtGui, QtWidgets
-
-
-class Ui_GameSettings(object):
- def setupUi(self, GameSettings):
- GameSettings.setObjectName("GameSettings")
- GameSettings.resize(505, 261)
- self.game_settings_layout = QtWidgets.QVBoxLayout(GameSettings)
- self.game_settings_layout.setObjectName("game_settings_layout")
- self.launch_settings_group = QtWidgets.QGroupBox(GameSettings)
- sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
- sizePolicy.setHorizontalStretch(0)
- sizePolicy.setVerticalStretch(0)
- sizePolicy.setHeightForWidth(self.launch_settings_group.sizePolicy().hasHeightForWidth())
- self.launch_settings_group.setSizePolicy(sizePolicy)
- self.launch_settings_group.setObjectName("launch_settings_group")
- self.launch_settings_layout = QtWidgets.QFormLayout(self.launch_settings_group)
- self.launch_settings_layout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
- self.launch_settings_layout.setObjectName("launch_settings_layout")
- self.skip_update_label = QtWidgets.QLabel(self.launch_settings_group)
- self.skip_update_label.setObjectName("skip_update_label")
- self.launch_settings_layout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.skip_update_label)
- self.skip_update = QtWidgets.QComboBox(self.launch_settings_group)
- sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed)
- sizePolicy.setHorizontalStretch(0)
- sizePolicy.setVerticalStretch(0)
- sizePolicy.setHeightForWidth(self.skip_update.sizePolicy().hasHeightForWidth())
- self.skip_update.setSizePolicy(sizePolicy)
- self.skip_update.setObjectName("skip_update")
- self.skip_update.addItem("")
- self.skip_update.addItem("")
- self.skip_update.addItem("")
- self.launch_settings_layout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.skip_update)
- self.offline_label = QtWidgets.QLabel(self.launch_settings_group)
- self.offline_label.setObjectName("offline_label")
- self.launch_settings_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.offline_label)
- self.offline = QtWidgets.QComboBox(self.launch_settings_group)
- sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed)
- sizePolicy.setHorizontalStretch(0)
- sizePolicy.setVerticalStretch(0)
- sizePolicy.setHeightForWidth(self.offline.sizePolicy().hasHeightForWidth())
- self.offline.setSizePolicy(sizePolicy)
- self.offline.setObjectName("offline")
- self.offline.addItem("")
- self.offline.addItem("")
- self.offline.addItem("")
- self.launch_settings_layout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.offline)
- self.launch_params_label = QtWidgets.QLabel(self.launch_settings_group)
- self.launch_params_label.setObjectName("launch_params_label")
- self.launch_settings_layout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.launch_params_label)
- self.launch_params = QtWidgets.QLineEdit(self.launch_settings_group)
- self.launch_params.setObjectName("launch_params")
- self.launch_settings_layout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.launch_params)
- self.game_settings_layout.addWidget(self.launch_settings_group)
- self.proton_layout = QtWidgets.QVBoxLayout()
- self.proton_layout.setObjectName("proton_layout")
- self.game_settings_layout.addLayout(self.proton_layout)
- self.linux_settings_widget = QtWidgets.QWidget(GameSettings)
- self.linux_settings_widget.setObjectName("linux_settings_widget")
- self.linux_settings_layout = QtWidgets.QVBoxLayout(self.linux_settings_widget)
- self.linux_settings_layout.setContentsMargins(0, 0, 0, 0)
- self.linux_settings_layout.setObjectName("linux_settings_layout")
- self.game_settings_layout.addWidget(self.linux_settings_widget)
-
- self.retranslateUi(GameSettings)
-
- def retranslateUi(self, GameSettings):
- _translate = QtCore.QCoreApplication.translate
- GameSettings.setWindowTitle(_translate("GameSettings", "GameSettings"))
- self.launch_settings_group.setTitle(_translate("GameSettings", "Launch Settings"))
- self.skip_update_label.setText(_translate("GameSettings", "Skip update check"))
- self.skip_update.setItemText(0, _translate("GameSettings", "Default"))
- self.skip_update.setItemText(1, _translate("GameSettings", "Yes"))
- self.skip_update.setItemText(2, _translate("GameSettings", "No"))
- self.offline_label.setText(_translate("GameSettings", "Offline mode"))
- self.offline.setItemText(0, _translate("GameSettings", "Default"))
- self.offline.setItemText(1, _translate("GameSettings", "Yes"))
- self.offline.setItemText(2, _translate("GameSettings", "No"))
- self.launch_params_label.setText(_translate("GameSettings", "Launch parameters"))
- self.launch_params.setPlaceholderText(_translate("GameSettings", "parameters"))
-
-
-if __name__ == "__main__":
- import sys
- app = QtWidgets.QApplication(sys.argv)
- GameSettings = QtWidgets.QWidget()
- ui = Ui_GameSettings()
- ui.setupUi(GameSettings)
- GameSettings.show()
- sys.exit(app.exec_())
diff --git a/rare/ui/components/tabs/settings/linux.py b/rare/ui/components/tabs/settings/linux.py
deleted file mode 100644
index 63996350..00000000
--- a/rare/ui/components/tabs/settings/linux.py
+++ /dev/null
@@ -1,63 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Form implementation generated from reading ui file 'rare/ui/components/tabs/settings/linux.ui'
-#
-# Created by: PyQt5 UI code generator 5.15.9
-#
-# WARNING: Any manual changes made to this file will be lost when pyuic5 is
-# run again. Do not edit this file unless you know what you are doing.
-
-
-from PyQt5 import QtCore, QtGui, QtWidgets
-
-
-class Ui_LinuxSettings(object):
- def setupUi(self, LinuxSettings):
- LinuxSettings.setObjectName("LinuxSettings")
- LinuxSettings.resize(394, 84)
- self.linux_layout = QtWidgets.QVBoxLayout(LinuxSettings)
- self.linux_layout.setObjectName("linux_layout")
- self.wine_groupbox = QtWidgets.QGroupBox(LinuxSettings)
- sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
- sizePolicy.setHorizontalStretch(0)
- sizePolicy.setVerticalStretch(0)
- sizePolicy.setHeightForWidth(self.wine_groupbox.sizePolicy().hasHeightForWidth())
- self.wine_groupbox.setSizePolicy(sizePolicy)
- self.wine_groupbox.setObjectName("wine_groupbox")
- self.wine_layout = QtWidgets.QFormLayout(self.wine_groupbox)
- self.wine_layout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
- self.wine_layout.setObjectName("wine_layout")
- self.prefix_label = QtWidgets.QLabel(self.wine_groupbox)
- self.prefix_label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
- self.prefix_label.setObjectName("prefix_label")
- self.wine_layout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.prefix_label)
- self.prefix_layout = QtWidgets.QVBoxLayout()
- self.prefix_layout.setObjectName("prefix_layout")
- self.wine_layout.setLayout(0, QtWidgets.QFormLayout.FieldRole, self.prefix_layout)
- self.exec_label = QtWidgets.QLabel(self.wine_groupbox)
- self.exec_label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
- self.exec_label.setObjectName("exec_label")
- self.wine_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.exec_label)
- self.exec_layout = QtWidgets.QVBoxLayout()
- self.exec_layout.setObjectName("exec_layout")
- self.wine_layout.setLayout(1, QtWidgets.QFormLayout.FieldRole, self.exec_layout)
- self.linux_layout.addWidget(self.wine_groupbox)
-
- self.retranslateUi(LinuxSettings)
-
- def retranslateUi(self, LinuxSettings):
- _translate = QtCore.QCoreApplication.translate
- LinuxSettings.setWindowTitle(_translate("LinuxSettings", "LinuxSettings"))
- self.wine_groupbox.setTitle(_translate("LinuxSettings", "Wine Settings"))
- self.prefix_label.setText(_translate("LinuxSettings", "Prefix"))
- self.exec_label.setText(_translate("LinuxSettings", "Executable"))
-
-
-if __name__ == "__main__":
- import sys
- app = QtWidgets.QApplication(sys.argv)
- LinuxSettings = QtWidgets.QWidget()
- ui = Ui_LinuxSettings()
- ui.setupUi(LinuxSettings)
- LinuxSettings.show()
- sys.exit(app.exec_())
diff --git a/rare/ui/components/tabs/settings/linux.ui b/rare/ui/components/tabs/settings/linux.ui
deleted file mode 100644
index 26e3721e..00000000
--- a/rare/ui/components/tabs/settings/linux.ui
+++ /dev/null
@@ -1,65 +0,0 @@
-
-
- LinuxSettings
-
-
-
- 0
- 0
- 394
- 84
-
-
-
- LinuxSettings
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Wine Settings
-
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
-
-
-
- Prefix
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
-
- -
-
-
- -
-
-
- Executable
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
diff --git a/rare/ui/components/tabs/settings/proton.py b/rare/ui/components/tabs/settings/widgets/proton.py
similarity index 69%
rename from rare/ui/components/tabs/settings/proton.py
rename to rare/ui/components/tabs/settings/widgets/proton.py
index 4672bb5c..d40bfd2a 100644
--- a/rare/ui/components/tabs/settings/proton.py
+++ b/rare/ui/components/tabs/settings/widgets/proton.py
@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file 'rare/ui/components/tabs/settings/proton.ui'
#
-# Created by: PyQt5 UI code generator 5.15.6
+# Created by: PyQt5 UI code generator 5.15.10
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
@@ -14,14 +14,14 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_ProtonSettings(object):
def setupUi(self, ProtonSettings):
ProtonSettings.setObjectName("ProtonSettings")
- ProtonSettings.resize(190, 86)
+ ProtonSettings.resize(180, 84)
ProtonSettings.setWindowTitle("ProtonSettings")
- self.proton_settings_layout = QtWidgets.QFormLayout(ProtonSettings)
- self.proton_settings_layout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
- self.proton_settings_layout.setObjectName("proton_settings_layout")
+ self.main_layout = QtWidgets.QFormLayout(ProtonSettings)
+ self.main_layout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
+ self.main_layout.setObjectName("main_layout")
self.proton_wrapper_label = QtWidgets.QLabel(ProtonSettings)
self.proton_wrapper_label.setObjectName("proton_wrapper_label")
- self.proton_settings_layout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.proton_wrapper_label)
+ self.main_layout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.proton_wrapper_label)
self.proton_combo = QtWidgets.QComboBox(ProtonSettings)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
@@ -30,13 +30,10 @@ class Ui_ProtonSettings(object):
self.proton_combo.setSizePolicy(sizePolicy)
self.proton_combo.setObjectName("proton_combo")
self.proton_combo.addItem("")
- self.proton_settings_layout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.proton_combo)
+ self.main_layout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.proton_combo)
self.proton_prefix_label = QtWidgets.QLabel(ProtonSettings)
self.proton_prefix_label.setObjectName("proton_prefix_label")
- self.proton_settings_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.proton_prefix_label)
- self.prefix_layout = QtWidgets.QHBoxLayout()
- self.prefix_layout.setObjectName("prefix_layout")
- self.proton_settings_layout.setLayout(1, QtWidgets.QFormLayout.FieldRole, self.prefix_layout)
+ self.main_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.proton_prefix_label)
self.retranslateUi(ProtonSettings)
diff --git a/rare/ui/components/tabs/settings/proton.ui b/rare/ui/components/tabs/settings/widgets/proton.ui
similarity index 86%
rename from rare/ui/components/tabs/settings/proton.ui
rename to rare/ui/components/tabs/settings/widgets/proton.ui
index 6b850d17..d25ac65e 100644
--- a/rare/ui/components/tabs/settings/proton.ui
+++ b/rare/ui/components/tabs/settings/widgets/proton.ui
@@ -6,8 +6,8 @@
0
0
- 190
- 86
+ 180
+ 84
@@ -16,7 +16,7 @@
Proton Settings
-
+
Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
@@ -49,9 +49,6 @@
- -
-
-
diff --git a/rare/ui/components/tabs/settings/widgets/wine.py b/rare/ui/components/tabs/settings/widgets/wine.py
new file mode 100644
index 00000000..6696e5f4
--- /dev/null
+++ b/rare/ui/components/tabs/settings/widgets/wine.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+
+# Form implementation generated from reading ui file 'rare/ui/components/tabs/settings/wine.ui'
+#
+# Created by: PyQt5 UI code generator 5.15.10
+#
+# WARNING: Any manual changes made to this file will be lost when pyuic5 is
+# run again. Do not edit this file unless you know what you are doing.
+
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+
+class Ui_WineSettings(object):
+ def setupUi(self, WineSettings):
+ WineSettings.setObjectName("WineSettings")
+ WineSettings.resize(104, 72)
+ WineSettings.setWindowTitle("WIneSettings")
+ self.main_layout = QtWidgets.QFormLayout(WineSettings)
+ self.main_layout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
+ self.main_layout.setObjectName("main_layout")
+ self.prefix_label = QtWidgets.QLabel(WineSettings)
+ self.prefix_label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
+ self.prefix_label.setObjectName("prefix_label")
+ self.main_layout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.prefix_label)
+ self.exec_label = QtWidgets.QLabel(WineSettings)
+ self.exec_label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
+ self.exec_label.setObjectName("exec_label")
+ self.main_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.exec_label)
+
+ self.retranslateUi(WineSettings)
+
+ def retranslateUi(self, WineSettings):
+ _translate = QtCore.QCoreApplication.translate
+ WineSettings.setTitle(_translate("WineSettings", "Wine Settings"))
+ self.prefix_label.setText(_translate("WineSettings", "Prefix"))
+ self.exec_label.setText(_translate("WineSettings", "Executable"))
+
+
+if __name__ == "__main__":
+ import sys
+ app = QtWidgets.QApplication(sys.argv)
+ WineSettings = QtWidgets.QGroupBox()
+ ui = Ui_WineSettings()
+ ui.setupUi(WineSettings)
+ WineSettings.show()
+ sys.exit(app.exec_())
diff --git a/rare/ui/components/tabs/settings/widgets/wine.ui b/rare/ui/components/tabs/settings/widgets/wine.ui
new file mode 100644
index 00000000..ce3d5044
--- /dev/null
+++ b/rare/ui/components/tabs/settings/widgets/wine.ui
@@ -0,0 +1,47 @@
+
+
+ WineSettings
+
+
+
+ 0
+ 0
+ 104
+ 72
+
+
+
+ WIneSettings
+
+
+ Wine Settings
+
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+ -
+
+
+ Prefix
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+ Executable
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+
+
+
+
+
diff --git a/rare/utils/rpc.py b/rare/utils/rpc.py
index 30ad9e1d..761f7d2a 100644
--- a/rare/utils/rpc.py
+++ b/rare/utils/rpc.py
@@ -7,6 +7,7 @@ from PyQt5.QtCore import QObject, QSettings
from pypresence import Presence
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
+from rare.models.options import options
client_id = "830732538225360908"
logger = getLogger("RPC")
@@ -21,7 +22,7 @@ class DiscordRPC(QObject):
self.signals = GlobalSignalsSingleton()
self.settings = QSettings()
- if self.settings.value("rpc_enable", 0, int) == 1: # show always
+ if self.settings.value(*options.rpc_enable) == 1: # show always
self.state = 2
self.set_discord_rpc()
@@ -32,7 +33,7 @@ class DiscordRPC(QObject):
self.set_discord_rpc(app_name)
def changed_settings(self, game_running: list = None):
- value = self.settings.value("rpc_enable", 0, int)
+ value = self.settings.value(*options.rpc_enable)
if value == 2:
self.remove_rpc()
return
@@ -44,7 +45,7 @@ class DiscordRPC(QObject):
self.set_discord_rpc(game_running[0])
def remove_rpc(self):
- if self.settings.value("rpc_enable", 0, int) != 1:
+ if self.settings.value(*options.rpc_enable) != 1:
if not self.RPC:
return
try:
@@ -85,8 +86,8 @@ class DiscordRPC(QObject):
self.update_rpc(app_name)
def update_rpc(self, app_name=None):
- if self.settings.value("rpc_enable", 0, int) == 2 or (
- not app_name and self.settings.value("rpc_enable", 0, int) == 0
+ if self.settings.value(*options.rpc_enable) == 2 or (
+ not app_name and self.settings.value(*options.rpc_enable) == 0
):
self.remove_rpc()
return
@@ -96,17 +97,17 @@ class DiscordRPC(QObject):
large_image="logo", details="https://github.com/RareDevs/Rare"
)
return
- if self.settings.value("rpc_name", True, bool):
+ if self.settings.value(*options.rpc_name):
try:
title = self.core.get_installed_game(app_name).title
except AttributeError:
logger.error(f"Could not get title of game: {app_name}")
title = app_name
start = None
- if self.settings.value("rpc_time", True, bool):
+ if self.settings.value(*options.rpc_time):
start = str(time.time()).split(".")[0]
os = None
- if self.settings.value("rpc_os", True, bool):
+ if self.settings.value(*options.rpc_os):
os = f"via Rare on {platform.system()}"
self.RPC.update(
diff --git a/rare/utils/wine.py b/rare/utils/runners/__init__.py
similarity index 60%
rename from rare/utils/wine.py
rename to rare/utils/runners/__init__.py
index 306192f0..9b9c9b0d 100644
--- a/rare/utils/wine.py
+++ b/rare/utils/runners/__init__.py
@@ -1,31 +1,32 @@
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(command: List, wine_env: Mapping) -> Tuple[str, str]:
+def execute(command: List[str], environment: Mapping) -> Tuple[str, str]:
if os.environ.get("container") == "flatpak":
flatpak_command = ["flatpak-spawn", "--host"]
- for name, value in wine_env.items():
+ for name, value in environment.items():
flatpak_command.append(f"--env={name}={value}")
command = flatpak_command + command
try:
@@ -35,7 +36,7 @@ def execute(command: List, wine_env: Mapping) -> Tuple[str, str]:
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(command: 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 717a8f14..2f646796 100644
--- a/rare/utils/proton.py
+++ b/rare/utils/runners/proton.py
@@ -1,6 +1,8 @@
import platform as pf
import os
+import shlex
from dataclasses import dataclass
+from hashlib import md5
from logging import getLogger
from typing import Optional, Union, List, Dict
@@ -28,11 +30,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
@@ -40,23 +53,36 @@ class SteamBase:
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"]
@@ -64,33 +90,36 @@ class SteamRuntime(SteamBase):
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]:
@@ -187,16 +216,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"] = ""
@@ -221,7 +252,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:
@@ -236,6 +268,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
@@ -247,7 +281,9 @@ if __name__ == "__main__":
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 00000000..2b87332b
--- /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 54e5e599..69913196 100644
--- a/rare/utils/steam_grades.py
+++ b/rare/utils/steam_grades.py
@@ -1,6 +1,7 @@
import difflib
import os
from datetime import datetime
+from enum import StrEnum, Enum
from typing import Tuple
import orjson
@@ -18,6 +19,31 @@ __grades_json = None
__active_download = False
+class ProtondbRatings(int, Enum):
+ # internal
+ PENDING = ("pending", -2)
+ FAIL = ("fail", -1)
+ # protondb
+ NA = ("na", 0)
+ BORKED = ("borked", 1)
+ BRONZE = ("bronze", 2)
+ SILVER = ("silver", 3)
+ GOLD = ("gold", 4)
+ PLATINUM = ("platinum", 5)
+
+ def __new__(cls, name: str, value: int):
+ obj = int.__new__(cls, value)
+ obj._value_ = value
+ obj._name_ = name
+ return obj
+
+ def __str__(self):
+ return self._name_
+
+ def __int__(self):
+ return self._value_
+
+
def get_rating(core: LegendaryCore, app_name: str) -> Tuple[int, str]:
game = core.get_game(app_name)
try:
@@ -31,7 +57,7 @@ def get_rating(core: LegendaryCore, app_name: str) -> Tuple[int, str]:
return steam_id, grade
-# you should iniciate the module with the game's steam code
+# you should initiate the module with the game's steam code
def get_grade(steam_code):
if steam_code == 0:
return "fail"