1
0
Fork 0
mirror of synced 2024-06-26 18:20:50 +12:00

Various WIP

* Use `vars()` instead of directly accessing `__dict__`
* Remove `auto_update` from RareGame's metadata
* Correct check for updating the Steam App ID (We want to keep any changes from the user)
* Collect both Wine and Proton prefixes when removing overlay registry keys.
* Add few convenience functions in config_helper and paths.
This commit is contained in:
loathingKernel 2023-09-20 01:39:15 +03:00
parent 7a5bb0b732
commit af6d7c5055
51 changed files with 1626 additions and 1065 deletions

View file

@ -16,6 +16,7 @@ from PyQt5.QtWidgets import (
QHBoxLayout, QHBoxLayout,
) )
from rare.models.options import options
from rare.components.tabs import MainTabWidget from rare.components.tabs import MainTabWidget
from rare.components.tray_icon import TrayIcon from rare.components.tray_icon import TrayIcon
from rare.shared import RareCore from rare.shared import RareCore
@ -93,8 +94,8 @@ class MainWindow(QMainWindow):
# self.status_timer.start() # self.status_timer.start()
width, height = 1280, 720 width, height = 1280, 720
if self.settings.value("save_size", False, bool): if self.settings.value(*options.save_size):
width, height = self.settings.value("window_size", (width, height), tuple) width, height = self.settings.value(*options.window_size)
self.resize(width, height) self.resize(width, height)
@ -151,9 +152,9 @@ class MainWindow(QMainWindow):
self._window_launched = True self._window_launched = True
def hide(self) -> None: 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() 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() super(MainWindow, self).hide()
def toggle(self): 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: `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 # lk: ensures exiting instead of hiding when `close()` is called programmatically
if not self.__accept_close: if not self.__accept_close:
if self.settings.value("sys_tray", True, bool): if self.settings.value(*options.sys_tray):
self.hide() self.hide()
e.ignore() e.ignore()
return return

View file

@ -15,6 +15,7 @@ from rare.components.dialogs.uninstall_dialog import UninstallDialog
from rare.lgndr.models.downloading import UIUpdate from rare.lgndr.models.downloading import UIUpdate
from rare.models.game import RareGame from rare.models.game import RareGame
from rare.models.install import InstallOptionsModel, InstallQueueItemModel, UninstallOptionsModel from rare.models.install import InstallOptionsModel, InstallQueueItemModel, UninstallOptionsModel
from rare.models.options import options
from rare.shared import RareCore from rare.shared import RareCore
from rare.shared.workers.install_info import InstallInfoWorker from rare.shared.workers.install_info import InstallInfoWorker
from rare.shared.workers.uninstall import UninstallWorker from rare.shared.workers.uninstall import UninstallWorker
@ -105,9 +106,13 @@ class DownloadsTab(QWidget):
def __add_update(self, update: Union[str, RareGame]): def __add_update(self, update: Union[str, RareGame]):
if isinstance(update, str): if isinstance(update, str):
update = self.rcore.get_game(update) update = self.rcore.get_game(update)
if QSettings().value(
f"{update.app_name}/auto_update", False, bool auto_update = QSettings(self).value(
) or QSettings().value("auto_update", False, bool): 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( self.__get_install_options(
InstallOptionsModel(app_name=update.app_name, update=True, silent=True) InstallOptionsModel(app_name=update.app_name, update=True, silent=True)
) )

View file

@ -15,7 +15,7 @@ from rare.shared import RareCore
from rare.widgets.library_layout import LibraryLayout from rare.widgets.library_layout import LibraryLayout
from rare.widgets.sliding_stack import SlidingStackedWidget from rare.widgets.sliding_stack import SlidingStackedWidget
from .game_info import GameInfoTabs 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.icon_game_widget import IconGameWidget
from .game_widgets.list_game_widget import ListGameWidget from .game_widgets.list_game_widget import ListGameWidget
from .head_bar import GameListHeadBar 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.search_bar.textChanged.connect(self.scroll_to_top)
self.head_bar.filterChanged.connect(self.filter_games) self.head_bar.filterChanged.connect(self.filter_games)
self.head_bar.filterChanged.connect(self.scroll_to_top) 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.head_bar.view.toggled.connect(self.toggle_view)
self.active_filter: str = self.head_bar.filter.currentData(Qt.UserRole)
# signals # signals
self.signals.game.installed.connect(self.update_count_games_label) self.signals.game.installed.connect(self.update_count_games_label)
self.signals.game.uninstalled.connect(self.update_count_games_label) self.signals.game.uninstalled.connect(self.update_count_games_label)
@ -157,7 +157,7 @@ class GamesTab(QStackedWidget):
continue continue
self.icon_view.layout().addWidget(icon_widget) self.icon_view.layout().addWidget(icon_widget)
self.list_view.layout().addWidget(list_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() self.update_count_games_label()
def add_library_widget(self, rgame: RareGame): def add_library_widget(self, rgame: RareGame):
@ -170,18 +170,26 @@ class GamesTab(QStackedWidget):
list_widget.show_info.connect(self.show_game_info) list_widget.show_info.connect(self.show_game_info)
return icon_widget, list_widget return icon_widget, list_widget
@pyqtSlot(str) @pyqtSlot(int)
@pyqtSlot(str, str) @pyqtSlot(int, str)
def filter_games(self, filter_name="all", search_text: 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()): if not search_text and (t := self.head_bar.search_bar.text()):
search_text = t search_text = t
if filter_name: # if library_filter:
self.active_filter = filter_name # self.active_filter = filter_type
if not filter_name and (t := self.active_filter): # if not library_filter and (t := self.active_filter):
filter_name = t # 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): def toggle_view(self):
self.settings.setValue("icon_view", not self.head_bar.view.isChecked()) self.settings.setValue("icon_view", not self.head_bar.view.isChecked())

View file

@ -3,7 +3,7 @@ import platform
from logging import getLogger from logging import getLogger
from typing import Tuple from typing import Tuple
from PyQt5.QtCore import QThreadPool, QSettings from PyQt5.QtCore import QThreadPool, QSettings, pyqtSlot
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QWidget, QWidget,
QFileDialog, QFileDialog,
@ -19,10 +19,11 @@ from legendary.models.game import SaveGameStatus
from rare.models.game import RareGame from rare.models.game import RareGame
from rare.shared import RareCore 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_settings_widget import Ui_CloudSettingsWidget
from rare.ui.components.tabs.games.game_info.cloud_sync_widget import Ui_CloudSyncWidget from rare.ui.components.tabs.games.game_info.cloud_sync_widget import Ui_CloudSyncWidget
from rare.utils.misc import icon from rare.utils.misc import icon
from rare.utils.metrics import timelogger
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
from rare.widgets.loading_widget import LoadingWidget from rare.widgets.loading_widget import LoadingWidget
from rare.widgets.side_tab import SideTabContents from rare.widgets.side_tab import SideTabContents
@ -114,13 +115,14 @@ class CloudSaves(QWidget, SideTabContents):
def compute_save_path(self): def compute_save_path(self):
if self.rgame.is_installed and self.rgame.game.supports_cloud_saves: if self.rgame.is_installed and self.rgame.game.supports_cloud_saves:
try: try:
with timelogger(logger, "Detecting save path"):
new_path = self.core.get_save_path(self.rgame.app_name) new_path = self.core.get_save_path(self.rgame.app_name)
if platform.system() != "Windows" and not os.path.exists(new_path): if platform.system() != "Windows" and not os.path.exists(new_path):
raise ValueError(f'Path "{new_path}" does not exist.') raise ValueError(f'Path "{new_path}" does not exist.')
except Exception as e: except Exception as e:
logger.warning(str(e)) logger.warning(str(e))
resolver = WineResolver(self.core, self.rgame.raw_save_path, self.rgame.app_name) resolver = WineSavePathResolver(self.core, self.rgame)
if not resolver.wine_env.get("WINEPREFIX"): if not resolver.environment.get("WINEPREFIX"):
del resolver del resolver
self.cloud_save_path_edit.setText("") self.cloud_save_path_edit.setText("")
QMessageBox.warning(self, "Warning", "No wine prefix selected. Please set it in settings") 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.cloud_save_path_edit.setDisabled(True)
self.compute_save_path_button.setDisabled(True) self.compute_save_path_button.setDisabled(True)
app_name = self.rgame.app_name resolver.signals.result_ready.connect(self.__on_wine_resolver_result)
resolver.signals.result_ready.connect(lambda x: self.wine_resolver_finished(x, app_name))
QThreadPool.globalInstance().start(resolver) QThreadPool.globalInstance().start(resolver)
return return
else: else:
self.cloud_save_path_edit.setText(new_path) 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}") logger.info(f"Wine resolver finished for {app_name}. Computed save path: {path}")
if app_name == self.rgame.app_name: if app_name == self.rgame.app_name:
self.cloud_save_path_edit.setDisabled(False) self.cloud_save_path_edit.setDisabled(False)
@ -158,8 +160,6 @@ class CloudSaves(QWidget, SideTabContents):
self.cloud_save_path_edit.setText("") self.cloud_save_path_edit.setText("")
return return
self.cloud_save_path_edit.setText(path) self.cloud_save_path_edit.setText(path)
elif path:
self.rcore.get_game(app_name).save_path = path
def __update_widget(self): def __update_widget(self):
supports_saves = self.rgame.igame is not None and ( supports_saves = self.rgame.igame is not None and (

View file

@ -4,7 +4,7 @@ from logging import getLogger
from typing import Tuple from typing import Tuple
from PyQt5.QtCore import Qt 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 legendary.models.game import Game, InstalledGame
from rare.components.tabs.settings import DefaultGameSettings from rare.components.tabs.settings import DefaultGameSettings
@ -20,10 +20,6 @@ logger = getLogger("GameSettings")
class GameSettings(DefaultGameSettings, SideTabContents): class GameSettings(DefaultGameSettings, SideTabContents):
def __init__(self, parent=None): def __init__(self, parent=None):
super(GameSettings, self).__init__(False, parent=parent) 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( self.ui.skip_update.currentIndexChanged.connect(
lambda x: self.update_combobox("skip_update_check", x) lambda x: self.update_combobox("skip_update_check", x)
@ -43,9 +39,17 @@ class GameSettings(DefaultGameSettings, SideTabContents):
save_func=self.override_exe_save_callback, save_func=self.override_exe_save_callback,
parent=self parent=self
) )
self.ui.launch_settings_layout.insertRow( self.ui.launch_settings_layout.setWidget(
self.ui.launch_settings_layout.getWidgetPosition(self.ui.launch_params)[0] + 1, self.ui.launch_settings_layout.getWidgetPosition(self.ui.override_exe_label)[0],
QLabel(self.tr("Override executable"), self), self.override_exe_edit 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) self.ui.game_settings_layout.setAlignment(Qt.AlignTop)
@ -126,9 +130,9 @@ class GameSettings(DefaultGameSettings, SideTabContents):
self.set_title.emit(self.game.app_title) self.set_title.emit(self.game.app_title)
if platform.system() != "Windows": if platform.system() != "Windows":
if self.igame and self.igame.platform == "Mac": if self.igame and self.igame.platform == "Mac":
self.ui.linux_settings_widget.setVisible(False) self.linux_settings.setVisible(False)
else: 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.ui.launch_params.setText(self.core.lgd.config.get(self.game.app_name, "start_params", fallback=""))
self.override_exe_edit.setText( self.override_exe_edit.setText(

View file

@ -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.QtCore import QObject, pyqtSlot
from PyQt5.QtWidgets import QWidget from PyQt5.QtWidgets import QWidget
@ -6,6 +6,7 @@ from PyQt5.QtWidgets import QWidget
from rare.lgndr.core import LegendaryCore from rare.lgndr.core import LegendaryCore
from rare.models.game import RareGame from rare.models.game import RareGame
from rare.models.signals import GlobalSignals from rare.models.signals import GlobalSignals
from rare.models.library import LibraryFilter, LibraryOrder
from rare.shared import RareCore from rare.shared import RareCore
from .icon_game_widget import IconGameWidget from .icon_game_widget import IconGameWidget
from .list_game_widget import ListGameWidget from .list_game_widget import ListGameWidget
@ -20,8 +21,8 @@ class LibraryWidgetController(QObject):
self.core: LegendaryCore = self.rcore.core() self.core: LegendaryCore = self.rcore.core()
self.signals: GlobalSignals = self.rcore.signals() self.signals: GlobalSignals = self.rcore.signals()
self.signals.game.installed.connect(self.sort_list) self.signals.game.installed.connect(self.order_game_views)
self.signals.game.uninstalled.connect(self.sort_list) self.signals.game.uninstalled.connect(self.order_game_views)
def add_game(self, rgame: RareGame): def add_game(self, rgame: RareGame):
return self.add_widgets(rgame) return self.add_widgets(rgame)
@ -32,24 +33,26 @@ class LibraryWidgetController(QObject):
return icon_widget, list_widget return icon_widget, list_widget
@staticmethod @staticmethod
def __visibility(widget: Union[IconGameWidget,ListGameWidget], filter_name, search_text) -> Tuple[bool, float]: def __visibility(
if filter_name == "hidden": widget: Union[IconGameWidget, ListGameWidget], library_filter, search_text
) -> Tuple[bool, float]:
if library_filter == LibraryFilter.HIDDEN:
visible = "hidden" in widget.rgame.metadata.tags visible = "hidden" in widget.rgame.metadata.tags
elif "hidden" in widget.rgame.metadata.tags: elif "hidden" in widget.rgame.metadata.tags:
visible = False visible = False
elif filter_name == "installed": elif library_filter == LibraryFilter.INSTALLED:
visible = widget.rgame.is_installed and not widget.rgame.is_unreal 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 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 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 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 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 visible = True
elif filter_name == "all": elif library_filter == LibraryFilter.ALL:
visible = not widget.rgame.is_unreal visible = not widget.rgame.is_unreal
else: else:
visible = True visible = True
@ -64,7 +67,7 @@ class LibraryWidgetController(QObject):
return visible, opacity 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) icon_widgets = self._icon_container.findChildren(IconGameWidget)
list_widgets = self._list_container.findChildren(ListGameWidget) list_widgets = self._list_container.findChildren(ListGameWidget)
for iw in icon_widgets: for iw in icon_widgets:
@ -75,42 +78,52 @@ class LibraryWidgetController(QObject):
visibility, opacity = self.__visibility(lw, filter_name, search_text) visibility, opacity = self.__visibility(lw, filter_name, search_text)
lw.setOpacity(opacity) lw.setOpacity(opacity)
lw.setVisible(visibility) lw.setVisible(visibility)
self.sort_list(search_text) self.order_game_views(search_text=search_text)
@pyqtSlot() @pyqtSlot()
def sort_list(self, sort_by: str = ""): def order_game_views(self, order_by: LibraryOrder = LibraryOrder.TITLE, search_text: 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,
)
)
list_widgets = self._list_container.findChildren(ListGameWidget) list_widgets = self._list_container.findChildren(ListGameWidget)
if sort_by: if search_text:
list_widgets.sort(key=lambda x: (sort_by not in x.rgame.app_title.lower(),)) 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: else:
list_widgets.sort( if (newest := order_by == LibraryOrder.NEWEST) or order_by == LibraryOrder.OLDEST:
# Sort by grant date # Sort by grant date
# key=lambda x: (x.rgame.is_installed, not x.rgame.is_non_asset, x.rgame.grant_date()), reverse=True 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) key=lambda x: (not x.rgame.is_installed, x.rgame.is_non_asset, x.rgame.app_title)
) )
for idx, wl in enumerate(list_widgets): for idx, wl in enumerate(list_widgets):
self._list_container.layout().insertWidget(idx, wl) self._list_container.layout().insertWidget(idx, wl)
@pyqtSlot() @pyqtSlot()
@pyqtSlot(list) @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: if not app_names:
# lk: base it on icon widgets, the two lists should be identical # lk: base it on icon widgets, the two lists should be identical
icon_widgets = self._icon_container.findChildren(IconGameWidget) icon_widgets = self._icon_container.findChildren(IconGameWidget)
@ -129,7 +142,7 @@ class LibraryWidgetController(QObject):
game = self.rcore.get_game(app_name) game = self.rcore.get_game(app_name)
lw = ListGameWidget(game) lw = ListGameWidget(game)
self._list_container.layout().addWidget(lw) 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]]: def __find_widget(self, app_name: str) -> Tuple[Union[IconGameWidget, None], Union[ListGameWidget, None]]:
iw = self._icon_container.findChild(IconGameWidget, app_name) iw = self._icon_container.findChild(IconGameWidget, app_name)

View file

@ -1,22 +1,25 @@
import platform as pf from PyQt5.QtCore import QSettings, pyqtSignal, pyqtSlot, QSize, Qt
from PyQt5.QtCore import QSettings, pyqtSignal, pyqtSlot, Qt
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QLabel, QLabel,
QPushButton, QPushButton,
QWidget, QWidget,
QHBoxLayout, QHBoxLayout,
QComboBox, QToolButton, QMenu, QAction, QComboBox,
QToolButton,
QMenu,
QAction,
) )
from qtawesome import IconWidget
from rare.shared import RareCore from rare.shared import RareCore
from rare.models.options import options
from rare.utils.extra_widgets import SelectViewWidget, ButtonLineEdit from rare.utils.extra_widgets import SelectViewWidget, ButtonLineEdit
from rare.utils.misc import icon from rare.utils.misc import icon
from .game_widgets import LibraryFilter, LibraryOrder
class GameListHeadBar(QWidget): class GameListHeadBar(QWidget):
filterChanged: pyqtSignal = pyqtSignal(str) filterChanged: pyqtSignal = pyqtSignal(int)
orderChanged: pyqtSignal = pyqtSignal(int)
goto_import: pyqtSignal = pyqtSignal() goto_import: pyqtSignal = pyqtSignal()
goto_egl_sync: pyqtSignal = pyqtSignal() goto_egl_sync: pyqtSignal = pyqtSignal()
goto_eos_ubisoft: pyqtSignal = pyqtSignal() goto_eos_ubisoft: pyqtSignal = pyqtSignal()
@ -27,43 +30,63 @@ class GameListHeadBar(QWidget):
self.settings = QSettings(self) self.settings = QSettings(self)
self.filter = QComboBox(self) self.filter = QComboBox(self)
self.filter.addItem(self.tr("All games"), "all") self.filter.addItem(self.tr("All games"), LibraryFilter.ALL)
self.filter.addItem(self.tr("Installed"), "installed") self.filter.addItem(self.tr("Installed"), LibraryFilter.INSTALLED)
self.filter.addItem(self.tr("Offline"), "offline") self.filter.addItem(self.tr("Offline"), LibraryFilter.OFFLINE)
# self.filter.addItem(self.tr("Hidden"), "hidden") # self.filter.addItem(self.tr("Hidden"), LibraryFilter.HIDDEN)
if self.rcore.bit32_games: 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: 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: if self.rcore.origin_games:
self.filter.addItem(self.tr("Exclude Origin"), "installable") self.filter.addItem(self.tr("Exclude Origin"), LibraryFilter.INSTALLABLE)
self.filter.addItem(self.tr("Include Unreal"), "include_ue") self.filter.addItem(self.tr("Include Unreal"), LibraryFilter.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
try: try:
self.filter.setCurrentIndex(self.settings.value("library_filter", filter_index, int)) self.filter.setCurrentIndex(self.filter.findData(
except TypeError: LibraryFilter(self.settings.value(*options.library_filter))
self.settings.setValue("library_filter", filter_index) ))
self.filter.setCurrentIndex(filter_index) except (TypeError, ValueError):
self.filter.currentIndexChanged.connect(self.filter_changed) 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) self.order = QComboBox(parent=self)
import_action = QAction(icon("mdi.import", "fa.arrow-down"), self.tr("Import Game"), integrations_menu) 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) 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 = QAction(icon("mdi.sync", "fa.refresh"), self.tr("Sync with EGL"), integrations_menu)
egl_sync_action.triggered.connect(self.goto_egl_sync) egl_sync_action.triggered.connect(self.goto_egl_sync)
eos_ubisoft_action = QAction(icon("mdi.rocket", "fa.rocket"), self.tr("Epic Overlay and Ubisoft"), eos_ubisoft_action = QAction(
integrations_menu) icon("mdi.rocket", "fa.rocket"), self.tr("Epic Overlay and Ubisoft"), integrations_menu
)
eos_ubisoft_action.triggered.connect(self.goto_eos_ubisoft) eos_ubisoft_action.triggered.connect(self.goto_eos_ubisoft)
integrations_menu.addAction(import_action) integrations_menu.addAction(import_action)
integrations_menu.addAction(egl_sync_action) integrations_menu.addAction(egl_sync_action)
integrations_menu.addAction(eos_ubisoft_action) integrations_menu.addAction(eos_ubisoft_action)
integrations = QToolButton(self) integrations = QToolButton(parent=self)
integrations.setText(self.tr("Integrations")) integrations.setText(self.tr("Integrations"))
integrations.setMenu(integrations_menu) integrations.setMenu(integrations_menu)
integrations.setPopupMode(QToolButton.InstantPopup) integrations.setPopupMode(QToolButton.InstantPopup)
@ -76,8 +99,8 @@ class GameListHeadBar(QWidget):
checked = QSettings().value("icon_view", True, bool) checked = QSettings().value("icon_view", True, bool)
installed_tooltip = self.tr("Installed games") installed_tooltip = self.tr("Installed games")
self.installed_icon = IconWidget(parent=self) self.installed_icon = QLabel(parent=self)
self.installed_icon.setIcon(icon("ph.floppy-disk-back-fill")) self.installed_icon.setPixmap(icon("ph.floppy-disk-back-fill").pixmap(QSize(16, 16)))
self.installed_icon.setToolTip(installed_tooltip) self.installed_icon.setToolTip(installed_tooltip)
self.installed_label = QLabel(parent=self) self.installed_label = QLabel(parent=self)
font = self.installed_label.font() font = self.installed_label.font()
@ -85,24 +108,25 @@ class GameListHeadBar(QWidget):
self.installed_label.setFont(font) self.installed_label.setFont(font)
self.installed_label.setToolTip(installed_tooltip) self.installed_label.setToolTip(installed_tooltip)
available_tooltip = self.tr("Available games") available_tooltip = self.tr("Available games")
self.available_icon = IconWidget(parent=self) self.available_icon = QLabel(parent=self)
self.available_icon.setIcon(icon("ph.floppy-disk-back-light")) self.available_icon.setPixmap(icon("ph.floppy-disk-back-light").pixmap(QSize(16, 16)))
self.available_icon.setToolTip(available_tooltip) self.available_icon.setToolTip(available_tooltip)
self.available_label = QLabel(parent=self) self.available_label = QLabel(parent=self)
self.available_label.setToolTip(available_tooltip) self.available_label.setToolTip(available_tooltip)
self.view = SelectViewWidget(checked) 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.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.setContentsMargins(0, 5, 0, 5)
layout.addWidget(self.filter) layout.addWidget(self.filter)
layout.addWidget(self.order)
layout.addStretch(0) layout.addStretch(0)
layout.addWidget(integrations) layout.addWidget(integrations)
layout.addStretch(5) layout.addStretch(2)
layout.addWidget(self.search_bar) layout.addWidget(self.search_bar)
layout.addStretch(2) layout.addStretch(2)
layout.addWidget(self.installed_icon) layout.addWidget(self.installed_icon)
@ -113,17 +137,29 @@ class GameListHeadBar(QWidget):
layout.addWidget(self.view) layout.addWidget(self.view)
layout.addStretch(2) layout.addStretch(2)
layout.addWidget(self.refresh_list) layout.addWidget(self.refresh_list)
self.setLayout(layout)
def set_games_count(self, inst: int, avail: int) -> None: def set_games_count(self, inst: int, avail: int) -> None:
self.installed_label.setText(str(inst)) self.installed_label.setText(str(inst))
self.available_label.setText(str(avail)) self.available_label.setText(str(avail))
@pyqtSlot() @pyqtSlot()
def refresh_clicked(self): def __refresh_clicked(self):
self.rcore.fetch() self.rcore.fetch()
def current_filter(self) -> int:
return int(self.filter.currentData(Qt.UserRole))
@pyqtSlot(int) @pyqtSlot(int)
def filter_changed(self, index: int): def __filter_changed(self, index: int):
self.filterChanged.emit(self.filter.itemData(index, Qt.UserRole)) data = int(self.filter.itemData(index, Qt.UserRole))
self.settings.setValue("library_filter", index) 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)

View file

@ -13,9 +13,10 @@ from legendary.models.game import InstalledGame
from rare.lgndr.glue.exception import LgndrException from rare.lgndr.glue.exception import LgndrException
from rare.models.pathspec import PathSpec from rare.models.pathspec import PathSpec
from rare.shared import RareCore 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_group import Ui_EGLSyncGroup
from rare.ui.components.tabs.games.integrations.egl_sync_list_group import Ui_EGLSyncListGroup 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.elide_label import ElideLabel
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
@ -87,10 +88,10 @@ class EGLSyncGroup(QGroupBox):
def __run_wine_resolver(self): def __run_wine_resolver(self):
self.egl_path_info.setText(self.tr("Updating...")) self.egl_path_info.setText(self.tr("Updating..."))
wine_resolver = WineResolver( wine_resolver = WinePathResolver(
self.core, self.core.get_app_launch_command("default"),
PathSpec.egl_programdata, runners.get_environment(self.core.get_app_environment("default")),
"default" PathSpec.egl_programdata()
) )
wine_resolver.signals.result_ready.connect(self.__on_wine_resolver_result) wine_resolver.signals.result_ready.connect(self.__on_wine_resolver_result)
QThreadPool.globalInstance().start(wine_resolver) QThreadPool.globalInstance().start(wine_resolver)
@ -122,14 +123,8 @@ class EGLSyncGroup(QGroupBox):
os.path.join(path, "dosdevices/c:") os.path.join(path, "dosdevices/c:")
): ):
# path is a wine prefix # path is a wine prefix
path = os.path.join( path = PathSpec.prefix_egl_programdata(path)
path, elif not path.rstrip("/").endswith(PathSpec.wine_egl_programdata()):
"dosdevices/c:",
"ProgramData/Epic/EpicGamesLauncher/Data/Manifests",
)
elif not path.rstrip("/").endswith(
"ProgramData/Epic/EpicGamesLauncher/Data/Manifests"
):
# lower() might or might not be needed in the check # lower() might or might not be needed in the check
return False, path, IndicatorReasonsCommon.WRONG_FORMAT return False, path, IndicatorReasonsCommon.WRONG_FORMAT
if os.path.exists(path): if os.path.exists(path):

View file

@ -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.shared import ArgumentsSingleton
from rare.widgets.side_tab import SideTabWidget from rare.widgets.side_tab import SideTabWidget
from .about import About from .about import About
from .debug import DebugSettings from .debug import DebugSettings
from .game_settings import DefaultGameSettings from .game import DefaultGameSettings
from .legendary import LegendarySettings from .legendary import LegendarySettings
from .rare import RareSettings from .rare import RareSettings
@ -13,9 +13,14 @@ class SettingsTab(SideTabWidget):
super(SettingsTab, self).__init__(parent=parent) super(SettingsTab, self).__init__(parent=parent)
self.args = ArgumentsSingleton() self.args = ArgumentsSingleton()
self.rare_index = self.addTab(RareSettings(self), "Rare") rare_settings = RareSettings(self)
self.legendary_index = self.addTab(LegendarySettings(self), "Legendary") self.rare_index = self.addTab(rare_settings, "Rare")
self.settings_index = self.addTab(DefaultGameSettings(True, self), self.tr("Default Settings"))
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 = About(self)
self.about_index = self.addTab(self.about, "About", "About") self.about_index = self.addTab(self.about, "About", "About")

View file

@ -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)

View file

@ -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)

View file

@ -6,6 +6,7 @@ from typing import Tuple, List
from PyQt5.QtCore import QObject, pyqtSignal, QThreadPool, QSettings from PyQt5.QtCore import QObject, pyqtSignal, QThreadPool, QSettings
from PyQt5.QtWidgets import QSizePolicy, QWidget, QFileDialog, QMessageBox from PyQt5.QtWidgets import QSizePolicy, QWidget, QFileDialog, QMessageBox
from rare.models.options import options
from rare.shared import LegendaryCoreSingleton from rare.shared import LegendaryCoreSingleton
from rare.shared.workers.worker import Worker from rare.shared.workers.worker import Worker
from rare.ui.components.tabs.settings.legendary import Ui_LegendarySettings 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.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( 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( 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_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( 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.exclude_non_asset_check.setChecked(self.settings.value(*options.exclude_non_asset))
self.settings.value("exclude_non_asset", False, bool)
)
self.exclude_non_asset_check.stateChanged.connect( self.exclude_non_asset_check.stateChanged.connect(
lambda: self.settings.setValue("exclude_non_asset", self.exclude_non_asset_check.isChecked()) lambda: self.settings.setValue(options.exclude_non_asset.key, self.exclude_non_asset_check.isChecked())
)
self.exclude_entitlements_check.setChecked(
self.settings.value("exclude_entitlements", False, bool)
) )
self.exclude_entitlements_check.setChecked(self.settings.value(*options.exclude_entitlements))
self.exclude_entitlements_check.stateChanged.connect( 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) self.refresh_metadata_button.clicked.connect(self.refresh_metadata)

View file

@ -8,6 +8,7 @@ from PyQt5.QtCore import QSettings, Qt
from PyQt5.QtWidgets import QWidget, QMessageBox from PyQt5.QtWidgets import QWidget, QMessageBox
from rare.components.tabs.settings.widgets.rpc import RPCSettings from rare.components.tabs.settings.widgets.rpc import RPCSettings
from rare.models.options import options
from rare.shared import LegendaryCoreSingleton from rare.shared import LegendaryCoreSingleton
from rare.ui.components.tabs.settings.rare import Ui_RareSettings from rare.ui.components.tabs.settings.rare import Ui_RareSettings
from rare.utils.misc import ( from rare.utils.misc import (
@ -39,18 +40,8 @@ class RareSettings(QWidget, Ui_RareSettings):
super(RareSettings, self).__init__(parent=parent) super(RareSettings, self).__init__(parent=parent)
self.setupUi(self) self.setupUi(self)
self.core = LegendaryCoreSingleton() self.core = LegendaryCoreSingleton()
# (widget_name, option_name, default) self.settings = QSettings(self)
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()
language = self.settings.value("language", self.core.language_code, type=str) language = self.settings.value("language", self.core.language_code, type=str)
# Select lang # Select lang
@ -85,29 +76,37 @@ class RareSettings(QWidget, Ui_RareSettings):
self.rpc = RPCSettings(self) self.rpc = RPCSettings(self)
self.right_layout.insertWidget(1, self.rpc, alignment=Qt.AlignTop) 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( 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( 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( self.confirm_start.stateChanged.connect(
lambda: self.settings.setValue( lambda: self.settings.setValue(options.confirm_start.key, self.confirm_start.isChecked())
"confirm_start", self.confirm_start.isChecked()
)
) )
self.auto_sync_cloud.setChecked(self.settings.value(*options.auto_sync_cloud))
self.auto_sync_cloud.stateChanged.connect( self.auto_sync_cloud.stateChanged.connect(
lambda: self.settings.setValue( lambda: self.settings.setValue(options.auto_sync_cloud.key, self.auto_sync_cloud.isChecked())
"auto_sync_cloud", self.auto_sync_cloud.isChecked()
)
) )
self.notification.setChecked(self.settings.value(*options.notification))
self.notification.stateChanged.connect( 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.save_size.stateChanged.connect(self.save_window_size)
self.log_games.setChecked(self.settings.value(*options.log_games))
self.log_games.stateChanged.connect( 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(): if desktop_links_supported():
@ -221,13 +220,8 @@ class RareSettings(QWidget, Ui_RareSettings):
subprocess.Popen([opener, log_dir()]) subprocess.Popen([opener, log_dir()])
def save_window_size(self): def save_window_size(self):
self.settings.setValue("save_size", self.save_size.isChecked()) self.settings.setValue(options.save_size.key, self.save_size.isChecked())
self.settings.remove("window_size") self.settings.remove(options.window_size.key)
def update_lang(self, i: int): def update_lang(self, i: int):
self.settings.setValue("language", languages[i][0]) 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))

View file

@ -1,10 +1,10 @@
from PyQt5.QtCore import QCoreApplication from PyQt5.QtCore import QCoreApplication
from .overlay_settings import OverlaySettings, CustomOption from .overlays import OverlaySettings, CustomOption
class DxvkSettings(OverlaySettings): class DxvkSettings(OverlaySettings):
def __init__(self): def __init__(self, parent=None):
super(DxvkSettings, self).__init__( super(DxvkSettings, self).__init__(
[ [
("fps", QCoreApplication.translate("DxvkSettings", "FPS")), ("fps", QCoreApplication.translate("DxvkSettings", "FPS")),
@ -19,7 +19,8 @@ class DxvkSettings(OverlaySettings):
[ [
(CustomOption.number_input("scale", 1, True), QCoreApplication.translate("DxvkSettings", "Scale")) (CustomOption.number_input("scale", 1, True), QCoreApplication.translate("DxvkSettings", "Scale"))
], ],
"DXVK_HUD", "0" "DXVK_HUD", "0",
parent=parent
) )
self.setTitle(self.tr("DXVK Settings")) self.setTitle(self.tr("DXVK Settings"))

View file

@ -11,8 +11,10 @@ from rare.lgndr.core import LegendaryCore
from rare.utils.misc import icon from rare.utils.misc import icon
if platform.system() != "Windows": if platform.system() != "Windows":
from rare.utils.runners.wine import get_wine_environment
if platform.system() != "Darwin": if platform.system() != "Darwin":
from rare.utils import proton from rare.utils.runners.proton import get_steam_environment
class EnvVarsTableModel(QAbstractTableModel): class EnvVarsTableModel(QAbstractTableModel):
@ -27,14 +29,13 @@ class EnvVarsTableModel(QAbstractTableModel):
self.__data_map: ChainMap = ChainMap() self.__data_map: ChainMap = ChainMap()
self.__readonly = [ self.__readonly = [
"STEAM_COMPAT_DATA_PATH",
"WINEPREFIX",
"DXVK_HUD", "DXVK_HUD",
"MANGOHUD_CONFIG", "MANGOHUD_CONFIG",
] ]
if platform.system() != "Windows": if platform.system() != "Windows":
self.__readonly.extend(get_wine_environment().keys())
if platform.system() != "Darwin": 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.__default: str = "default"
self.__appname: str = None self.__appname: str = None
@ -256,8 +257,6 @@ class EnvVarsTableModel(QAbstractTableModel):
if __name__ == "__main__": if __name__ == "__main__":
from PyQt5.QtWidgets import QApplication, QDialog, QVBoxLayout, QTableView, QHeaderView 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 rare.utils.misc import set_style_sheet
from legendary.core import LegendaryCore from legendary.core import LegendaryCore

View file

@ -5,18 +5,17 @@ from PyQt5.QtCore import QCoreApplication, pyqtSignal
from PyQt5.QtWidgets import QMessageBox from PyQt5.QtWidgets import QMessageBox
from rare.shared import LegendaryCoreSingleton from rare.shared import LegendaryCoreSingleton
from .overlay_settings import OverlaySettings, CustomOption, ActivationStates
from rare.utils import config_helper 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", position_values = ["default", "top-left", "top-right", "middle-left", "middle-right", "bottom-left",
"bottom-right", "top-center"] "bottom-right", "top-center"]
class MangoHudSettings(OverlaySettings): class MangoHudSettings(OverlaySettings):
set_wrapper_activated = pyqtSignal(bool) set_wrapper_activated = pyqtSignal(bool)
def __init__(self): def __init__(self, parent=None):
super(MangoHudSettings, self).__init__( super(MangoHudSettings, self).__init__(
[ [
("fps", QCoreApplication.translate("MangoSettings", "FPS")), ("fps", QCoreApplication.translate("MangoSettings", "FPS")),
@ -45,7 +44,8 @@ class MangoHudSettings(OverlaySettings):
QCoreApplication.translate("MangoSettings", "Position") 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.core = LegendaryCoreSingleton()
self.setTitle(self.tr("MangoHud Settings")) self.setTitle(self.tr("MangoHud Settings"))

View file

@ -82,8 +82,8 @@ class OverlaySettings(QGroupBox, Ui_OverlaySettings):
def __init__(self, checkboxes_map: List[Tuple[str, str]], value_map: List[Tuple[CustomOption, str]], def __init__(self, checkboxes_map: List[Tuple[str, str]], value_map: List[Tuple[CustomOption, str]],
config_env_var_name: str, no_display_value: str, config_env_var_name: str, no_display_value: str,
set_activation_state: Callable[[Enum], None] = lambda x: None): set_activation_state: Callable[[Enum], None] = lambda x: None, parent=None):
super(OverlaySettings, self).__init__() super(OverlaySettings, self).__init__(parent=parent)
self.setupUi(self) self.setupUi(self)
self.core = LegendaryCoreSingleton() self.core = LegendaryCoreSingleton()
self.config_env_var_name = config_env_var_name self.config_env_var_name = config_env_var_name

View file

@ -2,18 +2,18 @@ import os
import shutil import shutil
from typing import Tuple 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.shared import LegendaryCoreSingleton
from rare.utils import config_helper from rare.utils import config_helper
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
class PreLaunchSettings(QHBoxLayout): class PreLaunchSettings(QWidget):
app_name: str app_name: str
def __init__(self): def __init__(self, parent=None):
super(PreLaunchSettings, self).__init__() super(PreLaunchSettings, self).__init__(parent=parent)
self.core = LegendaryCoreSingleton() self.core = LegendaryCoreSingleton()
self.edit = PathEdit( self.edit = PathEdit(
path="", path="",
@ -22,12 +22,15 @@ class PreLaunchSettings(QHBoxLayout):
edit_func=self.edit_command, edit_func=self.edit_command,
save_func=self.save_pre_launch_command, save_func=self.save_pre_launch_command,
) )
self.layout().addWidget(self.edit)
self.wait_check = QCheckBox(self.tr("Wait for finish")) self.wait_check = QCheckBox(self.tr("Wait for finish"))
self.layout().addWidget(self.wait_check)
self.wait_check.stateChanged.connect(self.save_wait_finish) 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]: def edit_command(self, text: str) -> Tuple[bool, str, int]:
if not text.strip(): if not text.strip():
return True, text, IndicatorReasonsCommon.VALID return True, text, IndicatorReasonsCommon.VALID

View file

@ -1,85 +1,106 @@
import os import os
from logging import getLogger from logging import getLogger
from pathlib import Path from typing import Tuple, Union, Optional
from typing import Tuple
from PyQt5.QtCore import pyqtSignal from PyQt5.QtCore import pyqtSignal
from PyQt5.QtGui import QShowEvent
from PyQt5.QtWidgets import QGroupBox, QFileDialog from PyQt5.QtWidgets import QGroupBox, QFileDialog
from rare.components.tabs.settings import LinuxSettings from rare.models.wrapper import Wrapper, WrapperType
from rare.shared import LegendaryCoreSingleton from rare.shared import RareCore
from rare.shared.wrappers import Wrappers
from rare.ui.components.tabs.settings.proton import Ui_ProtonSettings 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 rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
from .wrapper import WrapperSettings
logger = getLogger("Proton") logger = getLogger("ProtonSettings")
class ProtonSettings(QGroupBox): class ProtonSettings(QGroupBox):
# str: option key # str: option key
environ_changed = pyqtSignal(str) environ_changed: pyqtSignal = pyqtSignal(str)
app_name: str # bool: state
changeable = True tool_enabled: pyqtSignal = pyqtSignal(bool)
def __init__(self, linux_settings: LinuxSettings, wrapper_settings: WrapperSettings): def __init__(self, parent=None):
super(ProtonSettings, self).__init__() super(ProtonSettings, self).__init__(parent=parent)
self.ui = Ui_ProtonSettings() self.ui = Ui_ProtonSettings()
self.ui.setupUi(self) 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( self.proton_prefix = PathEdit(
file_mode=QFileDialog.DirectoryOnly, file_mode=QFileDialog.DirectoryOnly,
edit_func=self.proton_prefix_edit, edit_func=self.proton_prefix_edit,
save_func=self.proton_prefix_save, 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) self.ui.prefix_layout.addWidget(self.proton_prefix)
def change_proton(self, i): self.app_name: str = "default"
if not self.changeable: self.core = RareCore.instance().core()
return self.wrappers: Wrappers = RareCore.instance().wrappers()
# First combo box entry: Don't use Proton self.tool_wrapper: Optional[Wrapper] = None
if i == 0:
self._wrapper_settings.delete_wrapper("proton")
config_helper.remove_option(self.app_name, "no_wine")
config_helper.remove_option(f"{self.app_name}.env", "STEAM_COMPAT_DATA_PATH")
self.environ_changed.emit("STEAM_COMPAT_DATA_PATH")
config_helper.remove_option(f"{self.app_name}.env", "STEAM_COMPAT_CLIENT_INSTALL_PATH")
self.environ_changed.emit("STEAM_COMPAT_CLIENT_INSTALL_PATH")
self.proton_prefix.setEnabled(False) def showEvent(self, a0: QShowEvent) -> None:
self.proton_prefix.setText("") if a0.spontaneous():
return super().showEvent(a0)
self._linux_settings.ui.wine_groupbox.setEnabled(True) self.ui.proton_combo.blockSignals(True)
else: self.ui.proton_combo.clear()
self.proton_prefix.setEnabled(True) self.ui.proton_combo.addItem(self.tr("Don't use a compatibility tool"), None)
self._linux_settings.ui.wine_groupbox.setEnabled(False) tools = proton.find_tools()
wrapper = self.possible_proton_combos[i - 1] for tool in tools:
self._wrapper_settings.add_wrapper(wrapper) self.ui.proton_combo.addItem(tool.name, tool)
config_helper.add_option(self.app_name, "no_wine", "true") try:
config_helper.add_option( wrapper = next(
f"{self.app_name}.env", filter(lambda w: w.is_compat_tool, self.wrappers.get_game_wrapper_list(self.app_name))
"STEAM_COMPAT_CLIENT_INSTALL_PATH",
str(Path.home().joinpath(".steam", "steam"))
) )
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 steam_environ = proton.get_steam_environment(steam_tool)
self._linux_settings.wine_exec.setText("") for key, value in steam_environ.items():
self._linux_settings.wine_prefix.setText("") 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: if not text:
return False, text, IndicatorReasonsCommon.EMPTY return False, text, IndicatorReasonsCommon.EMPTY
parent_dir = os.path.dirname(text) parent_dir = os.path.dirname(text)
@ -88,28 +109,9 @@ class ProtonSettings(QGroupBox):
def proton_prefix_save(self, text: str): def proton_prefix_save(self, text: str):
if not text: if not text:
return return
config_helper.add_option( config.add_envvar(self.app_name, "STEAM_COMPAT_DATA_PATH", text)
f"{self.app_name}.env", "STEAM_COMPAT_DATA_PATH", text
)
self.environ_changed.emit("STEAM_COMPAT_DATA_PATH") self.environ_changed.emit("STEAM_COMPAT_DATA_PATH")
config_helper.save_config() config.save_config()
def load_settings(self, app_name: str, proton: str): def load_settings(self, app_name: str):
self.changeable = False
self.app_name = app_name 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

View file

@ -2,6 +2,7 @@ from PyQt5.QtCore import QSettings
from PyQt5.QtWidgets import QGroupBox from PyQt5.QtWidgets import QGroupBox
from rare.shared import GlobalSignalsSingleton from rare.shared import GlobalSignalsSingleton
from rare.models.options import options
from rare.ui.components.tabs.settings.widgets.rpc import Ui_RPCSettings from rare.ui.components.tabs.settings.widgets.rpc import Ui_RPCSettings
@ -13,22 +14,22 @@ class RPCSettings(QGroupBox, Ui_RPCSettings):
self.settings = QSettings() self.settings = QSettings()
self.enable.setCurrentIndex(self.settings.value("rpc_enable", 0, int)) self.enable.setCurrentIndex(self.settings.value(*options.rpc_enable))
self.enable.currentIndexChanged.connect(self.changed) 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( 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( 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( 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: try:
@ -37,6 +38,6 @@ class RPCSettings(QGroupBox, Ui_RPCSettings):
self.setDisabled(True) self.setDisabled(True)
self.setToolTip(self.tr("Pypresence is not installed")) self.setToolTip(self.tr("Pypresence is not installed"))
def changed(self, i): def __enable_changed(self, i):
self.settings.setValue("rpc_enable", i) self.settings.setValue(options.rpc_enable.key, i)
self.signals.discord_rpc.apply_settings.emit() self.signals.discord_rpc.apply_settings.emit()

View file

@ -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)

View file

@ -3,31 +3,29 @@ import shutil
from logging import getLogger from logging import getLogger
from PyQt5.QtCore import pyqtSignal 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.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.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
from rare.utils import config_helper from rare.utils import config_helper
logger = getLogger("LinuxSettings") logger = getLogger("LinuxSettings")
class LinuxSettings(QWidget): class WineSettings(QGroupBox):
# str: option key # str: option key
environ_changed = pyqtSignal(str) environ_changed = pyqtSignal(str)
def __init__(self, name=None, parent=None): def __init__(self, name=None, parent=None):
super(LinuxSettings, self).__init__(parent=parent) super(WineSettings, self).__init__(parent=parent)
self.ui = Ui_LinuxSettings() self.ui = Ui_WineSettings()
self.ui.setupUi(self) self.ui.setupUi(self)
self.core = LegendaryCoreSingleton() self.core = LegendaryCoreSingleton()
self.signals = GlobalSignalsSingleton() self.signals = GlobalSignalsSingleton()
self.name = name if name is not None else "default" self.app_name: str = "default"
# Wine prefix # Wine prefix
self.wine_prefix = PathEdit( 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), edit_func=lambda path: (os.path.isdir(path) or not path, path, IndicatorReasonsCommon.DIR_NOT_EXISTS),
save_func=self.save_prefix, 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 # Wine executable
self.wine_exec = PathEdit( self.wine_exec = PathEdit(
self.load_setting(self.name, "wine_executable"), self.load_setting(self.app_name, "wine_executable"),
file_mode=QFileDialog.ExistingFile, file_mode=QFileDialog.ExistingFile,
name_filters=["wine", "wine64"], name_filters=["wine", "wine64"],
edit_func=lambda text: (os.path.exists(text) or not text, text, IndicatorReasonsCommon.DIR_NOT_EXISTS), edit_func=lambda text: (os.path.exists(text) or not text, text, IndicatorReasonsCommon.DIR_NOT_EXISTS),
save_func=lambda text: self.save_setting( 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) self.ui.main_layout.setWidget(
self.ui.main_layout.getWidgetPosition(self.ui.exec_label)[0],
# dxvk QFormLayout.FieldRole,
self.dxvk = DxvkSettings() self.wine_exec
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)
def load_prefix(self) -> str: def load_prefix(self) -> str:
return self.load_setting( return self.load_setting(
f"{self.name}.env", f"{self.app_name}.env",
"WINEPREFIX", "WINEPREFIX",
fallback=self.load_setting(self.name, "wine_prefix"), fallback=self.load_setting(self.app_name, "wine_prefix"),
) )
def save_prefix(self, text: str): 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.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() self.signals.application.prefix_updated.emit()
def load_setting(self, section: str, setting: str, fallback: str = ""): def load_setting(self, section: str, setting: str, fallback: str = ""):
return self.core.lgd.config.get(section, setting, fallback=fallback) 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: if text:
config_helper.add_option(section, setting, text) config_helper.add_option(section, setting, text)
logger.debug(f"Set {setting} in {f'[{section}]'} to {text}") logger.debug(f"Set {setting} in {f'[{section}]'} to {text}")

View file

@ -1,9 +1,9 @@
import re import shlex
import shutil import shutil
from logging import getLogger 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.QtGui import QDrag, QDropEvent, QDragEnterEvent, QDragMoveEvent, QFont, QMouseEvent
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QHBoxLayout, QHBoxLayout,
@ -16,46 +16,41 @@ from PyQt5.QtWidgets import (
QScrollArea, QScrollArea,
QAction, QAction,
QToolButton, QToolButton,
QMenu, QMenu, QDialog,
) )
from rare.models.wrapper import Wrapper
from rare.shared import RareCore from rare.shared import RareCore
from rare.ui.components.tabs.settings.widgets.wrapper import Ui_WrapperSettings 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.misc import icon
from rare.utils.runners import proton
logger = getLogger("WrapperSettings") logger = getLogger("WrapperSettings")
extra_wrapper_regex = { # extra_wrapper_regex = {
"proton": "\".*proton\" run", # proton # "proton": "\".*proton\" run", # proton
"mangohud": "mangohud" # mangohud # }
}
class Wrapper: class WrapperDialog(QDialog):
pass pass
class WrapperWidget(QFrame): class WrapperWidget(QFrame):
update_wrapper = pyqtSignal(str, str) # object: current, object: new
delete_wrapper = pyqtSignal(str) 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) super(WrapperWidget, self).__init__(parent=parent)
if not show_text:
show_text = text.split()[0]
self.setFrameShape(QFrame.StyledPanel) self.setFrameShape(QFrame.StyledPanel)
self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed) self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
self.setToolTip(wrapper.command)
self.text = text text_lbl = QLabel(wrapper.name, parent=self)
self.setToolTip(text)
unmanaged = show_text in extra_wrapper_regex.keys()
text_lbl = QLabel(show_text, parent=self)
text_lbl.setFont(QFont("monospace")) text_lbl.setFont(QFont("monospace"))
text_lbl.setDisabled(unmanaged) text_lbl.setEnabled(wrapper.is_editable)
image_lbl = QLabel(parent=self) image_lbl = QLabel(parent=self)
image_lbl.setPixmap(icon("mdi.drag-vertical").pixmap(QSize(20, 20))) 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.setIcon(icon("mdi.menu"))
manage_button.setMenu(manage_menu) manage_button.setMenu(manage_menu)
manage_button.setPopupMode(QToolButton.InstantPopup) manage_button.setPopupMode(QToolButton.InstantPopup)
manage_button.setDisabled(unmanaged) manage_button.setEnabled(wrapper.is_editable)
if unmanaged: if not wrapper.is_editable:
manage_button.setToolTip(self.tr("Manage through settings")) manage_button.setToolTip(self.tr("Manage through settings"))
else: else:
manage_button.setToolTip(self.tr("Manage")) manage_button.setToolTip(self.tr("Manage"))
@ -85,28 +80,39 @@ class WrapperWidget(QFrame):
layout.addWidget(manage_button) layout.addWidget(manage_button)
self.setLayout(layout) self.setLayout(layout)
self.wrapper = wrapper
# lk: set object names for the stylesheet # lk: set object names for the stylesheet
self.setObjectName(type(self).__name__) self.setObjectName(type(self).__name__)
manage_button.setObjectName(f"{self.objectName()}Button") manage_button.setObjectName(f"{self.objectName()}Button")
@pyqtSlot() def data(self) -> Wrapper:
def __delete(self): return self.wrapper
self.delete_wrapper.emit(self.text)
@pyqtSlot()
def __delete(self) -> None:
self.delete_wrapper.emit(self.wrapper)
self.deleteLater()
@pyqtSlot()
def __edit(self) -> None: def __edit(self) -> None:
dialog = QInputDialog(self) dialog = QInputDialog(self)
dialog.setWindowTitle(f"{self.tr('Edit wrapper')} - {QCoreApplication.instance().applicationName()}") dialog.setWindowTitle(f"{self.tr('Edit wrapper')} - {QCoreApplication.instance().applicationName()}")
dialog.setLabelText(self.tr("Edit wrapper command")) dialog.setLabelText(self.tr("Edit wrapper command"))
dialog.setTextValue(self.text) dialog.setTextValue(self.wrapper.command)
accepted = dialog.exec() accepted = dialog.exec()
wrapper = dialog.textValue() command = dialog.textValue()
dialog.deleteLater() dialog.deleteLater()
if accepted and wrapper: if accepted and command:
self.update_wrapper.emit(self.text, wrapper) new_wrapper = Wrapper(command=shlex.split(command))
self.update_wrapper.emit(self.wrapper, new_wrapper)
self.deleteLater()
def mouseMoveEvent(self, a0: QMouseEvent) -> None: def mouseMoveEvent(self, a0: QMouseEvent) -> None:
if a0.buttons() == Qt.LeftButton: if a0.buttons() == Qt.LeftButton:
a0.accept() a0.accept()
if self.wrapper.is_compat_tool:
return
drag = QDrag(self) drag = QDrag(self)
mime = QMimeData() mime = QMimeData()
drag.setMimeData(mime) drag.setMimeData(mime)
@ -114,30 +120,22 @@ class WrapperWidget(QFrame):
class WrapperSettings(QWidget): class WrapperSettings(QWidget):
def __init__(self): def __init__(self, parent=None):
super(WrapperSettings, self).__init__() super(WrapperSettings, self).__init__(parent=parent)
self.ui = Ui_WrapperSettings() self.ui = Ui_WrapperSettings()
self.ui.setupUi(self) 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 = QScrollArea(self.ui.widget_stack)
self.wrapper_scroll.setWidgetResizable(True) self.wrapper_scroll.setWidgetResizable(True)
self.wrapper_scroll.setSizeAdjustPolicy(QScrollArea.AdjustToContents) self.wrapper_scroll.setSizeAdjustPolicy(QScrollArea.AdjustToContents)
self.wrapper_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.wrapper_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.wrapper_scroll.setProperty("no_kinetic_scroll", True) self.wrapper_scroll.setProperty("no_kinetic_scroll", True)
self.scroll_content = WrapperContainer( self.wrapper_container = WrapperContainer(parent=self.wrapper_scroll)
save_cb=self.save, parent=self.wrapper_scroll self.wrapper_container.orderChanged.connect(self.__on_order_changed)
) self.wrapper_scroll.setWidget(self.wrapper_container)
self.wrapper_scroll.setWidget(self.scroll_content)
self.ui.widget_stack.insertWidget(0, self.wrapper_scroll) self.ui.widget_stack.insertWidget(0, self.wrapper_scroll)
self.core = RareCore.instance().core() self.ui.add_button.clicked.connect(self.__on_add_button_pressed)
self.ui.add_button.clicked.connect(self.add_button_pressed)
self.settings = QSettings()
self.wrapper_scroll.horizontalScrollBar().rangeChanged.connect(self.adjust_scrollarea) self.wrapper_scroll.horizontalScrollBar().rangeChanged.connect(self.adjust_scrollarea)
# lk: set object names for the stylesheet # lk: set object names for the stylesheet
@ -149,18 +147,24 @@ class WrapperSettings(QWidget):
self.wrapper_scroll.verticalScrollBar().setObjectName( self.wrapper_scroll.verticalScrollBar().setObjectName(
f"{self.wrapper_scroll.objectName()}Bar") 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) @pyqtSlot(int, int)
def adjust_scrollarea(self, min: int, max: int): def adjust_scrollarea(self, minh: int, maxh: int):
wrapper_widget = self.scroll_content.findChild(WrapperWidget) wrapper_widget = self.wrapper_container.findChild(WrapperWidget)
if not wrapper_widget: if not wrapper_widget:
return return
# lk: when the scrollbar is not visible, min and max are 0 # lk: when the scrollbar is not visible, min and max are 0
if max > min: if maxh > minh:
self.wrapper_scroll.setMaximumHeight( self.wrapper_scroll.setMaximumHeight(
wrapper_widget.sizeHint().height() wrapper_widget.sizeHint().height()
+ self.wrapper_scroll.rect().height() // 2 + self.wrapper_scroll.rect().height() // 2
- self.wrapper_scroll.contentsRect().height() // 2 - self.wrapper_scroll.contentsRect().height() // 2
+ self.scroll_content.layout().spacing() + self.wrapper_container.layout().spacing()
+ self.wrapper_scroll.horizontalScrollBar().sizeHint().height() + self.wrapper_scroll.horizontalScrollBar().sizeHint().height()
) )
else: else:
@ -170,187 +174,183 @@ class WrapperSettings(QWidget):
- self.wrapper_scroll.contentsRect().height() - self.wrapper_scroll.contentsRect().height()
) )
def get_wrapper_string(self): @pyqtSlot(QWidget, int)
return " ".join(self.get_wrapper_list()) 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): @pyqtSlot()
wrappers = list(self.wrappers.values()) def __on_add_button_pressed(self):
wrappers.sort(key=lambda x: self.scroll_content.layout().indexOf(x))
return [w.text for w in wrappers]
def add_button_pressed(self):
dialog = QInputDialog(self) dialog = QInputDialog(self)
dialog.setWindowTitle(f"{self.tr('Add wrapper')} - {QCoreApplication.instance().applicationName()}") dialog.setWindowTitle(f"{self.tr('Add wrapper')} - {QCoreApplication.instance().applicationName()}")
dialog.setLabelText(self.tr("Enter wrapper command")) dialog.setLabelText(self.tr("Enter wrapper command"))
accepted = dialog.exec() accepted = dialog.exec()
wrapper = dialog.textValue() command = dialog.textValue()
dialog.deleteLater() dialog.deleteLater()
if accepted: if accepted:
self.add_wrapper(wrapper) wrapper = Wrapper(shlex.split(command))
self.add_user_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 <b>{0}</b> is already in the list").format(text)
)
return
if show_text != "proton" and not shutil.which(text.split()[0]):
if (
QMessageBox.question(
self,
self.tr("Warning"),
self.tr("Wrapper <b>{0}</b> is not in $PATH. Add it anyway?").format(show_text),
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No,
)
== QMessageBox.No
):
return
if text == "proton":
QMessageBox.warning(
self,
self.tr("Warning"),
self.tr("Do not insert <b>proton</b> manually. Add it through Proton settings"),
)
return
def __add_wrapper(self, wrapper: Wrapper, position: int = -1):
self.ui.widget_stack.setCurrentIndex(0) self.ui.widget_stack.setCurrentIndex(0)
widget = WrapperWidget(wrapper, self.wrapper_container)
if widget := self.wrappers.get(show_text, None):
widget.deleteLater()
widget = WrapperWidget(text, show_text, self.scroll_content)
if position < 0: if position < 0:
self.scroll_content.layout().addWidget(widget) self.wrapper_container.addWidget(widget)
else: else:
self.scroll_content.layout().insertWidget(position, widget) self.wrapper_container.insertWidget(position, widget)
self.adjust_scrollarea( self.adjust_scrollarea(
self.wrapper_scroll.horizontalScrollBar().minimum(), self.wrapper_scroll.horizontalScrollBar().minimum(),
self.wrapper_scroll.horizontalScrollBar().maximum(), self.wrapper_scroll.horizontalScrollBar().maximum(),
) )
widget.update_wrapper.connect(self.update_wrapper) widget.update_wrapper.connect(self.__update_wrapper)
widget.delete_wrapper.connect(self.delete_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: def add_user_wrapper(self, wrapper: Wrapper, position: int = -1):
self.save() if not wrapper:
return
@pyqtSlot(str) compat_cmds = [tool.command() for tool in proton.find_tools()]
def delete_wrapper(self, text: str): if wrapper.command in compat_cmds:
text = text.split()[0] QMessageBox.warning(
widget = self.wrappers.get(text, None) self,
if widget: self.tr("Warning"),
self.wrappers.pop(text) self.tr("Do not insert compatibility tools manually. Add them through Proton settings"),
widget.deleteLater() )
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 <b>{0}</b> 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 <b>{0}</b> 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.wrapper_scroll.setMaximumHeight(self.ui.label_page.sizeHint().height())
self.ui.widget_stack.setCurrentIndex(1) 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) @pyqtSlot()
def update_wrapper(self, old: str, new: str): def update_state(self):
key = old.split()[0] for w in self.wrapper_container.findChildren(WrapperWidget, options=Qt.FindDirectChildrenOnly):
idx = self.scroll_content.layout().indexOf(self.wrappers[key]) w.deleteLater()
self.delete_wrapper(key) wrappers = self.wrappers.get_game_wrapper_list(self.app_name)
self.add_wrapper(new, position=idx) if not wrappers:
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:
self.wrapper_scroll.setMaximumHeight(self.ui.label_page.sizeHint().height()) self.wrapper_scroll.setMaximumHeight(self.ui.label_page.sizeHint().height())
self.ui.widget_stack.setCurrentIndex(1) self.ui.widget_stack.setCurrentIndex(1)
else: else:
self.ui.widget_stack.setCurrentIndex(0) 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): 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) super(WrapperContainer, self).__init__(parent=parent)
self.setAcceptDrops(True) self.setAcceptDrops(True)
self.save = save_cb self.__layout = QHBoxLayout(self)
layout = QHBoxLayout() self.__layout.setContentsMargins(0, 0, 0, 0)
layout.setContentsMargins(0, 0, 0, 0) self.__layout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
layout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
self.setLayout(layout)
self.drag_widget: Optional[QWidget] = None self.__drag_widget: Optional[QWidget] = None
# lk: set object names for the stylesheet # lk: set object names for the stylesheet
self.setObjectName(type(self).__name__) 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): def dragEnterEvent(self, e: QDragEnterEvent):
widget = e.source() widget = e.source()
self.drag_widget = widget self.__drag_widget = widget
e.accept() e.accept()
def _get_drop_index(self, x): def __get_drop_index(self, x) -> int:
drag_idx = self.layout().indexOf(self.drag_widget) drag_idx = self.__layout.indexOf(self.__drag_widget)
if drag_idx > 0: if drag_idx > 0:
prev_widget = self.layout().itemAt(drag_idx - 1).widget() prev_widget = self.__layout.itemAt(drag_idx - 1).widget()
if x < self.drag_widget.x() - prev_widget.width() // 2: if x < self.__drag_widget.x() - prev_widget.width() // 2:
return drag_idx - 1 return drag_idx - 1
if drag_idx < self.layout().count() - 1: if drag_idx < self.__layout.count() - 1:
next_widget = self.layout().itemAt(drag_idx + 1).widget() next_widget = self.__layout.itemAt(drag_idx + 1).widget()
if x > self.drag_widget.x() + self.drag_widget.width() + next_widget.width() // 2: if x > self.__drag_widget.x() + self.__drag_widget.width() + next_widget.width() // 2:
return drag_idx + 1 return drag_idx + 1
return drag_idx return drag_idx
def dragMoveEvent(self, e: QDragMoveEvent) -> None: def dragMoveEvent(self, e: QDragMoveEvent) -> None:
i = self._get_drop_index(e.pos().x()) new_x = self.__get_drop_index(e.pos().x())
self.layout().insertWidget(i, self.drag_widget) self.__layout.insertWidget(new_x, self.__drag_widget)
def dropEvent(self, e: QDropEvent): def dropEvent(self, e: QDropEvent):
pos = e.pos() pos = e.pos()
widget = e.source() widget = e.source()
index = self._get_drop_index(pos.x()) new_x = self.__get_drop_index(pos.x())
self.layout().insertWidget(index, widget) self.__layout.insertWidget(new_x, widget)
self.drag_widget = None self.__drag_widget = None
self.orderChanged.emit(widget, new_x)
e.accept() e.accept()
self.save()

View file

@ -5,6 +5,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, QSettings
from PyQt5.QtGui import QIcon from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QSystemTrayIcon, QMenu, QAction, QApplication from PyQt5.QtWidgets import QSystemTrayIcon, QMenu, QAction, QApplication
from rare.models.options import options
from rare.shared import RareCore from rare.shared import RareCore
logger = getLogger("TrayIcon") logger = getLogger("TrayIcon")
@ -61,7 +62,7 @@ class TrayIcon(QSystemTrayIcon):
@pyqtSlot(str, str) @pyqtSlot(str, str)
def notify(self, title: str, body: 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) self.showMessage(f"{QApplication.applicationName()} - {title}", body, QSystemTrayIcon.Information, 4000)
@pyqtSlot() @pyqtSlot()

View file

@ -17,6 +17,7 @@ from legendary.models.game import SaveGameStatus
from rare.lgndr.core import LegendaryCore from rare.lgndr.core import LegendaryCore
from rare.models.base_game import RareGameSlim from rare.models.base_game import RareGameSlim
from rare.models.launcher import ErrorModel, Actions, FinishedModel, BaseModel, StateChangedModel 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 rare.widgets.rare_app import RareApp, RareAppException
from .cloud_sync_dialog import CloudSyncDialog, CloudSyncDialogResult from .cloud_sync_dialog import CloudSyncDialog, CloudSyncDialogResult
from .console_dialog import ConsoleDialog from .console_dialog import ConsoleDialog
@ -145,7 +146,7 @@ class RareLauncher(RareApp):
lang = self.settings.value("language", self.core.language_code, type=str) lang = self.settings.value("language", self.core.language_code, type=str)
self.load_translator(lang) self.load_translator(lang)
if QSettings().value("show_console", False, bool): if QSettings(self).value(*options.log_games):
self.console = ConsoleDialog() self.console = ConsoleDialog()
self.console.show() self.console.show()

View file

@ -18,6 +18,8 @@ def main() -> int:
sys.stderr = open(os.devnull, 'w') sys.stderr = open(os.devnull, 'w')
os.environ["QT_QPA_PLATFORMTHEME"] = "" 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: if "LEGENDARY_CONFIG_PATH" in os.environ:
os.environ["LEGENDARY_CONFIG_PATH"] = os.path.expanduser(os.environ["LEGENDARY_CONFIG_PATH"]) os.environ["LEGENDARY_CONFIG_PATH"] = os.path.expanduser(os.environ["LEGENDARY_CONFIG_PATH"])

View file

@ -10,6 +10,7 @@ from legendary.lfs import eos
from legendary.models.game import SaveGameFile, SaveGameStatus, Game, InstalledGame from legendary.models.game import SaveGameFile, SaveGameStatus, Game, InstalledGame
from legendary.utils.selective_dl import get_sdl_appname from legendary.utils.selective_dl import get_sdl_appname
from rare.models.options import options
from rare.lgndr.core import LegendaryCore from rare.lgndr.core import LegendaryCore
from rare.models.install import UninstallOptionsModel, InstallOptionsModel from rare.models.install import UninstallOptionsModel, InstallOptionsModel
@ -222,11 +223,13 @@ class RareGameSlim(RareGameBase):
@property @property
def auto_sync_saves(self): def auto_sync_saves(self):
return self.supports_cloud_saves and QSettings().value( auto_sync_cloud = QSettings(self).value(
f"{self.app_name}/auto_sync_cloud", f"{self.app_name}/{options.auto_sync_cloud.key}",
QSettings().value("auto_sync_cloud", False, bool), options.auto_sync_cloud.default,
bool 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 @property
def save_path(self) -> Optional[str]: def save_path(self) -> Optional[str]:

View file

@ -2,7 +2,7 @@ import json
import os import os
import platform import platform
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import datetime from datetime import datetime, UTC
from logging import getLogger from logging import getLogger
from threading import Lock from threading import Lock
from typing import List, Optional, Dict, Set 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.shared.image_manager import ImageManager
from rare.utils.paths import data_dir, get_rare_executable from rare.utils.paths import data_dir, get_rare_executable
from rare.utils.steam_grades import get_rating 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") logger = getLogger("RareGame")
@ -27,11 +27,10 @@ logger = getLogger("RareGame")
class RareGame(RareGameSlim): class RareGame(RareGameSlim):
@dataclass @dataclass
class Metadata: class Metadata:
auto_update: bool = False
queued: bool = False queued: bool = False
queue_pos: Optional[int] = None queue_pos: Optional[int] = None
last_played: datetime = datetime.min last_played: datetime = datetime.min
grant_date: Optional[datetime] = None grant_date: datetime = datetime.min
steam_appid: Optional[int] = None steam_appid: Optional[int] = None
steam_grade: Optional[str] = None steam_grade: Optional[str] = None
steam_date: datetime = datetime.min steam_date: datetime = datetime.min
@ -40,24 +39,23 @@ class RareGame(RareGameSlim):
@classmethod @classmethod
def from_dict(cls, data: Dict): def from_dict(cls, data: Dict):
return cls( return cls(
auto_update=data.get("auto_update", False),
queued=data.get("queued", False), queued=data.get("queued", False),
queue_pos=data.get("queue_pos", None), queue_pos=data.get("queue_pos", None),
last_played=datetime.fromisoformat(data["last_played"]) if data.get("last_played", None) else datetime.min, last_played=datetime.fromisoformat(x) if (x := data.get("last_played", None)) else datetime.min,
grant_date=datetime.fromisoformat(data["grant_date"]) if data.get("grant_date", None) else None, grant_date=datetime.fromisoformat(x) if (x := data.get("grant_date", None)) else datetime.min,
steam_appid=data.get("steam_appid", None), steam_appid=data.get("steam_appid", None),
steam_grade=data.get("steam_grade", 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", []), tags=data.get("tags", []),
) )
def as_dict(self): @property
def __dict__(self):
return dict( return dict(
auto_update=self.auto_update,
queued=self.queued, queued=self.queued,
queue_pos=self.queue_pos, queue_pos=self.queue_pos,
last_played=self.last_played.isoformat() if self.last_played else datetime.min, 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_appid=self.steam_appid,
steam_grade=self.steam_grade, steam_grade=self.steam_grade,
steam_date=self.steam_date.isoformat() if self.steam_date else datetime.min, steam_date=self.steam_date.isoformat() if self.steam_date else datetime.min,
@ -81,7 +79,6 @@ class RareGame(RareGameSlim):
self.pixmap: QPixmap = QPixmap() self.pixmap: QPixmap = QPixmap()
self.metadata: RareGame.Metadata = RareGame.Metadata() self.metadata: RareGame.Metadata = RareGame.Metadata()
self.__load_metadata() self.__load_metadata()
if self.metadata.grant_date is None:
self.grant_date() self.grant_date()
self.owned_dlcs: Set[RareGame] = set() self.owned_dlcs: Set[RareGame] = set()
@ -143,13 +140,14 @@ class RareGame(RareGameSlim):
def __load_metadata_json() -> Dict: def __load_metadata_json() -> Dict:
if RareGame.__metadata_json is None: if RareGame.__metadata_json is None:
metadata = {} metadata = {}
file = os.path.join(data_dir(), "game_meta.json")
try: try:
with open(os.path.join(data_dir(), "game_meta.json"), "r") as metadata_fh: with open(file, "r") as f:
metadata = json.load(metadata_fh) metadata = json.load(f)
except FileNotFoundError: except FileNotFoundError:
logger.info("Game metadata json file does not exist.") logger.info("%s does not exist", file)
except json.JSONDecodeError: except json.JSONDecodeError:
logger.warning("Game metadata json file is corrupt.") logger.warning("%s is corrupt", file)
finally: finally:
RareGame.__metadata_json = metadata RareGame.__metadata_json = metadata
return RareGame.__metadata_json return RareGame.__metadata_json
@ -166,9 +164,9 @@ class RareGame(RareGameSlim):
with RareGame.__metadata_lock: with RareGame.__metadata_lock:
metadata: Dict = self.__load_metadata_json() metadata: Dict = self.__load_metadata_json()
# pylint: disable=unsupported-assignment-operation # pylint: disable=unsupported-assignment-operation
metadata[self.app_name] = self.metadata.as_dict() metadata[self.app_name] = vars(self.metadata)
with open(os.path.join(data_dir(), "game_meta.json"), "w") as metadata_json: with open(os.path.join(data_dir(), "game_meta.json"), "w+") as file:
json.dump(metadata, metadata_json, indent=2) json.dump(metadata, file, indent=2)
def update_game(self): def update_game(self):
self.game = self.core.get_game( self.game = self.core.get_game(
@ -432,29 +430,27 @@ class RareGame(RareGameSlim):
def steam_grade(self) -> str: def steam_grade(self) -> str:
if platform.system() == "Windows" or self.is_unreal: if platform.system() == "Windows" or self.is_unreal:
return "na" return "na"
if self.metadata.steam_grade != "pending":
elapsed_time = abs(datetime.utcnow() - self.metadata.steam_date) 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 elapsed_time.days > 3 and (self.metadata.steam_grade is None or self.metadata.steam_appid is None):
def _set_steam_grade(): def _set_steam_grade():
appid, rating = get_rating(self.core, self.app_name) appid, rating = get_rating(self.core, self.app_name)
self.set_steam_grade(appid, rating) self.set_steam_grade(appid, rating)
worker = QRunnable.create(_set_steam_grade) worker = QRunnable.create(_set_steam_grade)
QThreadPool.globalInstance().start(worker) QThreadPool.globalInstance().start(worker)
return "pending" self.metadata.steam_grade = "pending"
return self.metadata.steam_grade
@property @property
def steam_appid(self) -> Optional[int]: def steam_appid(self) -> Optional[int]:
return self.metadata.steam_appid return self.metadata.steam_appid
def set_steam_grade(self, appid: int, grade: str) -> None: 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, "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_appid = appid
self.metadata.steam_grade = grade self.metadata.steam_grade = grade
self.metadata.steam_date = datetime.utcnow() self.metadata.steam_date = datetime.utcnow()
@ -463,17 +459,17 @@ class RareGame(RareGameSlim):
def grant_date(self, force=False) -> datetime: def grant_date(self, force=False) -> datetime:
if (entitlements := self.core.lgd.entitlements) is None: if (entitlements := self.core.lgd.entitlements) is None:
return self.metadata.grant_date return self.metadata.grant_date.replace(tzinfo=UTC)
if self.metadata.grant_date is None or force: 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) logger.debug("Grant date for %s not found in metadata, resolving", self.app_name)
matching = filter(lambda ent: ent["namespace"] == self.game.namespace, entitlements) matching = filter(lambda ent: ent["namespace"] == self.game.namespace, entitlements)
entitlement = next(matching, None) entitlement = next(matching, None)
grant_date = datetime.fromisoformat( grant_date = datetime.fromisoformat(
entitlement["grantDate"].replace("Z", "+00:00") 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.metadata.grant_date = grant_date
self.__save_metadata() 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: def set_origin_attributes(self, path: str, size: int = 0) -> None:
self.__origin_install_path = path self.__origin_install_path = path

19
rare/models/library.py Normal file
View file

@ -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

53
rare/models/options.py Normal file
View file

@ -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']

View file

@ -2,46 +2,72 @@ import os
from typing import Union, List from typing import Union, List
from legendary.core import LegendaryCore from legendary.core import LegendaryCore
from legendary.models.game import InstalledGame
from rare.utils.config_helper import get_prefixes
class PathSpec: 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"): @staticmethod
if core is not None: def egl_appdata() -> str:
self.__egl_path_vars.update({"{epicid}": core.lgd.userdata["account_id"]}) return r"%LOCALAPPDATA%\EpicGamesLauncher\Saved\Config\Windows"
self.app_name = app_name
def cook(self, path: str) -> str: @staticmethod
cooked_path = [self.__egl_path_vars.get(p.lower(), p) for p in path.split("/")] def egl_programdata() -> str:
return os.path.join(*cooked_path) return r"%PROGRAMDATA%\Epic\EpicGamesLauncher\Data\Manifests"
@property @staticmethod
def wine_egl_programdata(self): def wine_programdata() -> str:
return self.egl_programdata.replace("\\", "/").replace("%PROGRAMDATA%", self.wine_programdata) return r"ProgramData"
def wine_egl_prefixes(self, results: int = 0) -> Union[List[str], str]: @staticmethod
possible_prefixes = [ def wine_egl_programdata() -> str:
os.path.expanduser("~/.wine"), return PathSpec.egl_programdata(
os.path.expanduser("~/Games/epic-games-store"), ).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 = [] prefixes = []
for prefix in possible_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) prefixes.append(prefix)
if not prefixes: if not prefixes:
return str() return ""
if not results: if not results:
return prefixes return prefixes
elif results == 1: elif results == 1:
return prefixes[0] return prefixes[0]
else: else:
return prefixes[:results] 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)

74
rare/models/wrapper.py Normal file
View file

@ -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)
)

View file

@ -29,7 +29,7 @@ from .workers import (
) )
from .workers.uninstall import uninstall_game from .workers.uninstall import uninstall_game
from .workers.worker import QueueWorkerInfo, QueueWorkerState from .workers.worker import QueueWorkerInfo, QueueWorkerState
from rare.utils import config_helper from .wrappers import Wrappers
logger = getLogger("RareCore") logger = getLogger("RareCore")
@ -53,6 +53,8 @@ class RareCore(QObject):
self.__signals: Optional[GlobalSignals] = None self.__signals: Optional[GlobalSignals] = None
self.__core: Optional[LegendaryCore] = None self.__core: Optional[LegendaryCore] = None
self.__image_manager: Optional[ImageManager] = None self.__image_manager: Optional[ImageManager] = None
self.__settings: Optional[QSettings] = None
self.__wrappers: Optional[Wrappers] = None
self.__start_time = time.perf_counter() self.__start_time = time.perf_counter()
@ -61,8 +63,8 @@ class RareCore(QObject):
self.core(init=True) self.core(init=True)
config_helper.init_config_handler(self.__core) config_helper.init_config_handler(self.__core)
self.image_manager(init=True) self.image_manager(init=True)
self.__settings = QSettings(self)
self.settings = QSettings(self) self.__wrappers = Wrappers()
self.queue_workers: List[QueueWorker] = [] self.queue_workers: List[QueueWorker] = []
self.queue_threadpool = QThreadPool() self.queue_threadpool = QThreadPool()
@ -205,6 +207,12 @@ class RareCore(QObject):
self.__image_manager = ImageManager(self.signals(), self.core()) self.__image_manager = ImageManager(self.signals(), self.core())
return self.__image_manager return self.__image_manager
def wrappers(self) -> Wrappers:
return self.__wrappers
def settings(self) -> QSettings:
return self.__settings
def deleteLater(self) -> None: def deleteLater(self) -> None:
self.__image_manager.deleteLater() self.__image_manager.deleteLater()
del self.__image_manager del self.__image_manager
@ -328,10 +336,13 @@ class RareCore(QObject):
self.__core.lgd.entitlements = result self.__core.lgd.entitlements = result
self.__fetched_entitlements = True 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]): 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.progress.emit(100, self.tr("Launching Rare"))
self.completed.emit() self.completed.emit()
QTimer.singleShot(100, self.__post_init) QTimer.singleShot(100, self.__post_init)
@ -366,7 +377,7 @@ class RareCore(QObject):
continue continue
self.__library[app_name].load_saves(saves) self.__library[app_name].load_saves(saves)
except (HTTPError, ConnectionError) as e: 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) logger.error(e)
return return
logger.info(f"Saves: {len(saves_dict)}") logger.info(f"Saves: {len(saves_dict)}")

View file

@ -8,6 +8,7 @@ from requests.exceptions import HTTPError, ConnectionError
from rare.lgndr.core import LegendaryCore from rare.lgndr.core import LegendaryCore
from rare.utils.metrics import timelogger from rare.utils.metrics import timelogger
from rare.models.options import options
from .worker import Worker from .worker import Worker
logger = getLogger("FetchWorker") logger = getLogger("FetchWorker")
@ -32,12 +33,12 @@ class FetchWorker(Worker):
class EntitlementsWorker(FetchWorker): class EntitlementsWorker(FetchWorker):
def __init__(self, core: LegendaryCore, args: Namespace):
super(EntitlementsWorker, self).__init__(core, args)
def run_real(self): def run_real(self):
want_entitlements = not self.settings.value(*options.exclude_entitlements)
entitlements = () entitlements = ()
want_entitlements = not self.settings.value("exclude_entitlements", False, bool)
if want_entitlements: if want_entitlements:
# Get entitlements, Ubisoft integration also uses them # Get entitlements, Ubisoft integration also uses them
self.signals.progress.emit(0, self.signals.tr("Updating entitlements")) self.signals.progress.emit(0, self.signals.tr("Updating entitlements"))
@ -51,20 +52,20 @@ class EntitlementsWorker(FetchWorker):
class GamesDlcsWorker(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): def run_real(self):
# Fetch regular EGL games with assets # Fetch regular EGL games with assets
want_unreal = self.settings.value("unreal_meta", False, bool) or self.args.debug # want_unreal = self.settings.value(*options.unreal_meta) or self.args.debug
want_win32 = self.settings.value("win32_meta", False, bool) # want_win32 = self.settings.value(*options.win32_meta) or self.args.debug
want_macos = self.settings.value("macos_meta", False, bool) # 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_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( logger.info(
"Requesting Win32 metadata due to %s, %s Unreal engine", "Requesting Win32 metadata due to %s, %s Unreal engine",
"settings" if want_win32 else "debug", "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 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( logger.info(
"Requesting macOS metadata due to %s, %s Unreal engine", "Requesting macOS metadata due to %s, %s Unreal engine",
"platform" if need_macos else "settings" if want_macos else "debug", "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)) logger.info(f"Games: %s. Games with DLCs: %s", len(games), len(dlc_dict))
# Fetch non-asset games # Fetch non-asset games
want_non_asset = not self.settings.value("exclude_non_asset", False, bool)
if want_non_asset: if want_non_asset:
self.signals.progress.emit(30, self.signals.tr("Updating non-asset game metadata")) self.signals.progress.emit(30, self.signals.tr("Updating non-asset game metadata"))
try: try:

View file

@ -11,7 +11,7 @@ from rare.lgndr.glue.arguments import LgndrUninstallGameArgs
from rare.lgndr.glue.monkeys import LgndrIndirectStatus from rare.lgndr.glue.monkeys import LgndrIndirectStatus
from rare.models.game import RareGame from rare.models.game import RareGame
from rare.models.install import UninstallOptionsModel 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 rare.utils.paths import desktop_links_supported, desktop_link_types, desktop_link_path
from .worker import Worker from .worker import Worker
@ -31,7 +31,7 @@ def uninstall_game(
logger.info('Removing registry entries...') logger.info('Removing registry entries...')
if platform.system() != "Window": if platform.system() != "Window":
prefixes = config_helper.get_wine_prefixes() prefixes = config.get_prefixes()
if platform.system() == "Darwin": if platform.system() == "Darwin":
# TODO: add crossover support # TODO: add crossover support
pass pass
@ -65,10 +65,10 @@ def uninstall_game(
) )
if not keep_config: if not keep_config:
logger.info("Removing sections in config file") logger.info("Removing sections in config file")
config_helper.remove_section(rgame.app_name) config.remove_section(rgame.app_name)
config_helper.remove_section(f"{rgame.app_name}.env") config.remove_section(f"{rgame.app_name}.env")
config_helper.save_config() config.save_config()
return status.success, status.message return status.success, status.message

View file

@ -3,14 +3,14 @@ import platform
import time import time
from configparser import ConfigParser from configparser import ConfigParser
from logging import getLogger from logging import getLogger
from typing import Union, Iterable from typing import Union, Iterable, Mapping, List
from PyQt5.QtCore import pyqtSignal, QObject, QRunnable from PyQt5.QtCore import pyqtSignal, QObject, QRunnable
import rare.utils.wine as wine
from rare.lgndr.core import LegendaryCore from rare.lgndr.core import LegendaryCore
from rare.models.game import RareGame from rare.models.game import RareGame
from rare.models.pathspec import PathSpec 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 rare.utils.misc import path_size, format_size
from .worker import Worker from .worker import Worker
@ -22,31 +22,52 @@ if platform.system() == "Windows":
logger = getLogger("WineResolver") logger = getLogger("WineResolver")
class WineResolver(Worker): class WinePathResolver(Worker):
class Signals(QObject): class Signals(QObject):
result_ready = pyqtSignal(str) result_ready = pyqtSignal(str, str)
def __init__(self, core: LegendaryCore, path: str, app_name: str): def __init__(self, command: List[str], environ: Mapping, path: str):
super(WineResolver, self).__init__() super(WinePathResolver, self). __init__()
self.signals = WineResolver.Signals() self.signals = WinePathResolver.Signals()
self.wine_env = wine.environ(core, app_name) self.command = command
self.wine_exec = wine.wine(core, app_name) self.environ = environ
self.path = PathSpec(core, app_name).cook(path) 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): def run_real(self):
if "WINEPREFIX" not in self.wine_env or not os.path.exists(self.wine_env["WINEPREFIX"]): path = self._resolve_unix_path(self.command, self.environ, self.path)
# pylint: disable=E1136 self.signals.result_ready.emit(path, "default")
self.signals.result_ready[str].emit("")
return return
if not os.path.exists(self.wine_exec):
# pylint: disable=E1136
self.signals.result_ready[str].emit("") class WineSavePathResolver(WinePathResolver):
return
path = wine.resolve_path(self.wine_exec, self.wine_env, self.path) 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 # Clean wine output
real_path = wine.convert_to_unix_path(self.wine_exec, self.wine_env, path)
# pylint: disable=E1136 # 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 return
@ -55,9 +76,7 @@ class OriginWineWorker(QRunnable):
super(OriginWineWorker, self).__init__() super(OriginWineWorker, self).__init__()
self.__cache: dict[str, ConfigParser] = {} self.__cache: dict[str, ConfigParser] = {}
self.core = core self.core = core
if isinstance(games, RareGame): self.games = [games] if isinstance(games, RareGame) else games
games = [games]
self.games = games
def run(self) -> None: def run(self) -> None:
t = time.time() t = time.time()
@ -79,15 +98,19 @@ class OriginWineWorker(QRunnable):
if platform.system() == "Windows": if platform.system() == "Windows":
install_dir = windows_helpers.query_registry_value(winreg.HKEY_LOCAL_MACHINE, reg_path, reg_key) install_dir = windows_helpers.query_registry_value(winreg.HKEY_LOCAL_MACHINE, reg_path, reg_key)
else: else:
wine_env = wine.environ(self.core, rgame.app_name) command = self.core.get_app_launch_command(rgame.app_name)
wine_exec = wine.wine(self.core, 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 use_wine = False
if not use_wine: if not use_wine:
# lk: this is the original way of getting the path by parsing "system.reg" # 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(prefix, None) or runners.read_registry("system.reg", prefix)
reg = self.__cache.get(wine_prefix, None) or wine.read_registry("system.reg", wine_prefix) self.__cache[prefix] = reg
self.__cache[wine_prefix] = reg
reg_path = reg_path.replace("SOFTWARE", "Software").replace("WOW6432Node", "Wow6432Node") reg_path = reg_path.replace("SOFTWARE", "Software").replace("WOW6432Node", "Wow6432Node")
# lk: split and rejoin the registry path to avoid slash expansion # 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) install_dir = reg.get(reg_path, f'"{reg_key}"', fallback=None)
else: else:
# lk: this is the alternative way of getting the path by using wine itself # 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: if install_dir:
logger.debug("Found Wine install directory %s", 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: if install_dir:
logger.debug("Found Unix install directory %s", install_dir) logger.debug("Found Unix install directory %s", install_dir)
else: else:

169
rare/shared/wrappers.py Normal file
View file

@ -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)

View file

@ -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_())

View file

@ -6,14 +6,14 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>483</width> <width>393</width>
<height>154</height> <height>202</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
<string>GameSettings</string> <string>GameSettings</string>
</property> </property>
<layout class="QVBoxLayout" name="game_settings_layout"> <layout class="QVBoxLayout" name="main_layout">
<item> <item>
<widget class="QGroupBox" name="launch_settings_group"> <widget class="QGroupBox" name="launch_settings_group">
<property name="sizePolicy"> <property name="sizePolicy">
@ -25,7 +25,7 @@
<property name="title"> <property name="title">
<string>Launch Settings</string> <string>Launch Settings</string>
</property> </property>
<layout class="QFormLayout" name="launch_settings_layout"> <layout class="QFormLayout" name="launch_layout">
<property name="labelAlignment"> <property name="labelAlignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property> </property>
@ -37,7 +37,7 @@
</widget> </widget>
</item> </item>
<item row="0" column="1"> <item row="0" column="1">
<widget class="QComboBox" name="skip_update"> <widget class="QComboBox" name="skip_update_combo">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed"> <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch> <horstretch>0</horstretch>
@ -69,7 +69,7 @@
</widget> </widget>
</item> </item>
<item row="1" column="1"> <item row="1" column="1">
<widget class="QComboBox" name="offline"> <widget class="QComboBox" name="offline_combo">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed"> <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch> <horstretch>0</horstretch>
@ -101,12 +101,33 @@
</widget> </widget>
</item> </item>
<item row="2" column="1"> <item row="2" column="1">
<widget class="QLineEdit" name="launch_params"> <widget class="QLineEdit" name="launch_params_edit">
<property name="placeholderText"> <property name="placeholderText">
<string>parameters</string> <string>parameters</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="0">
<widget class="QLabel" name="pre_launch_label">
<property name="text">
<string>Pre-launch command</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="override_exe_label">
<property name="text">
<string>Override executable</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="wrapper_label">
<property name="text">
<string>Wrappers</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>

View file

@ -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_())

View file

@ -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_())

View file

@ -1,65 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>LinuxSettings</class>
<widget class="QWidget" name="LinuxSettings">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>394</width>
<height>84</height>
</rect>
</property>
<property name="windowTitle">
<string>LinuxSettings</string>
</property>
<layout class="QVBoxLayout" name="linux_layout">
<item>
<widget class="QGroupBox" name="wine_groupbox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Wine Settings</string>
</property>
<layout class="QFormLayout" name="wine_layout">
<property name="labelAlignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<item row="0" column="0">
<widget class="QLabel" name="prefix_label">
<property name="text">
<string>Prefix</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QVBoxLayout" name="prefix_layout"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="exec_label">
<property name="text">
<string>Executable</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QVBoxLayout" name="exec_layout"/>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -2,7 +2,7 @@
# Form implementation generated from reading ui file 'rare/ui/components/tabs/settings/proton.ui' # 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 # 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. # 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): class Ui_ProtonSettings(object):
def setupUi(self, ProtonSettings): def setupUi(self, ProtonSettings):
ProtonSettings.setObjectName("ProtonSettings") ProtonSettings.setObjectName("ProtonSettings")
ProtonSettings.resize(190, 86) ProtonSettings.resize(180, 84)
ProtonSettings.setWindowTitle("ProtonSettings") ProtonSettings.setWindowTitle("ProtonSettings")
self.proton_settings_layout = QtWidgets.QFormLayout(ProtonSettings) self.main_layout = QtWidgets.QFormLayout(ProtonSettings)
self.proton_settings_layout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) self.main_layout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.proton_settings_layout.setObjectName("proton_settings_layout") self.main_layout.setObjectName("main_layout")
self.proton_wrapper_label = QtWidgets.QLabel(ProtonSettings) self.proton_wrapper_label = QtWidgets.QLabel(ProtonSettings)
self.proton_wrapper_label.setObjectName("proton_wrapper_label") 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) self.proton_combo = QtWidgets.QComboBox(ProtonSettings)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0) sizePolicy.setHorizontalStretch(0)
@ -30,13 +30,10 @@ class Ui_ProtonSettings(object):
self.proton_combo.setSizePolicy(sizePolicy) self.proton_combo.setSizePolicy(sizePolicy)
self.proton_combo.setObjectName("proton_combo") self.proton_combo.setObjectName("proton_combo")
self.proton_combo.addItem("") 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 = QtWidgets.QLabel(ProtonSettings)
self.proton_prefix_label.setObjectName("proton_prefix_label") self.proton_prefix_label.setObjectName("proton_prefix_label")
self.proton_settings_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.proton_prefix_label) self.main_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.retranslateUi(ProtonSettings) self.retranslateUi(ProtonSettings)

View file

@ -6,8 +6,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>190</width> <width>180</width>
<height>86</height> <height>84</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@ -16,7 +16,7 @@
<property name="title"> <property name="title">
<string>Proton Settings</string> <string>Proton Settings</string>
</property> </property>
<layout class="QFormLayout" name="proton_settings_layout"> <layout class="QFormLayout" name="main_layout">
<property name="labelAlignment"> <property name="labelAlignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property> </property>
@ -49,9 +49,6 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="prefix_layout"/>
</item>
</layout> </layout>
</widget> </widget>
<resources/> <resources/>

View file

@ -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_())

View file

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>WineSettings</class>
<widget class="QGroupBox" name="WineSettings">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>104</width>
<height>72</height>
</rect>
</property>
<property name="windowTitle">
<string notr="true">WIneSettings</string>
</property>
<property name="title">
<string>Wine Settings</string>
</property>
<layout class="QFormLayout" name="main_layout">
<property name="labelAlignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<item row="0" column="0">
<widget class="QLabel" name="prefix_label">
<property name="text">
<string>Prefix</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="exec_label">
<property name="text">
<string>Executable</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -7,6 +7,7 @@ from PyQt5.QtCore import QObject, QSettings
from pypresence import Presence from pypresence import Presence
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
from rare.models.options import options
client_id = "830732538225360908" client_id = "830732538225360908"
logger = getLogger("RPC") logger = getLogger("RPC")
@ -21,7 +22,7 @@ class DiscordRPC(QObject):
self.signals = GlobalSignalsSingleton() self.signals = GlobalSignalsSingleton()
self.settings = QSettings() 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.state = 2
self.set_discord_rpc() self.set_discord_rpc()
@ -32,7 +33,7 @@ class DiscordRPC(QObject):
self.set_discord_rpc(app_name) self.set_discord_rpc(app_name)
def changed_settings(self, game_running: list = None): 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: if value == 2:
self.remove_rpc() self.remove_rpc()
return return
@ -44,7 +45,7 @@ class DiscordRPC(QObject):
self.set_discord_rpc(game_running[0]) self.set_discord_rpc(game_running[0])
def remove_rpc(self): 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: if not self.RPC:
return return
try: try:
@ -85,8 +86,8 @@ class DiscordRPC(QObject):
self.update_rpc(app_name) self.update_rpc(app_name)
def update_rpc(self, app_name=None): def update_rpc(self, app_name=None):
if self.settings.value("rpc_enable", 0, int) == 2 or ( if self.settings.value(*options.rpc_enable) == 2 or (
not app_name and self.settings.value("rpc_enable", 0, int) == 0 not app_name and self.settings.value(*options.rpc_enable) == 0
): ):
self.remove_rpc() self.remove_rpc()
return return
@ -96,17 +97,17 @@ class DiscordRPC(QObject):
large_image="logo", details="https://github.com/RareDevs/Rare" large_image="logo", details="https://github.com/RareDevs/Rare"
) )
return return
if self.settings.value("rpc_name", True, bool): if self.settings.value(*options.rpc_name):
try: try:
title = self.core.get_installed_game(app_name).title title = self.core.get_installed_game(app_name).title
except AttributeError: except AttributeError:
logger.error(f"Could not get title of game: {app_name}") logger.error(f"Could not get title of game: {app_name}")
title = app_name title = app_name
start = None start = None
if self.settings.value("rpc_time", True, bool): if self.settings.value(*options.rpc_time):
start = str(time.time()).split(".")[0] start = str(time.time()).split(".")[0]
os = None os = None
if self.settings.value("rpc_os", True, bool): if self.settings.value(*options.rpc_os):
os = f"via Rare on {platform.system()}" os = f"via Rare on {platform.system()}"
self.RPC.update( self.RPC.update(

View file

@ -1,31 +1,32 @@
import os import os
import shutil
import subprocess import subprocess
from configparser import ConfigParser from configparser import ConfigParser
from logging import getLogger 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 # 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"] accepted = ["system.reg", "user.reg"]
if registry not in accepted: if registry not in accepted:
raise RuntimeError(f'Unknown target "{registry}" not in {accepted}') raise RuntimeError(f'Unknown target "{registry}" not in {accepted}')
reg = ConfigParser(comment_prefixes=(';', '#', '/', 'WINE'), allow_no_value=True, reg = ConfigParser(comment_prefixes=(';', '#', '/', 'WINE'), allow_no_value=True,
strict=False) strict=False)
reg.optionxform = str reg.optionxform = str
reg.read(os.path.join(wine_pfx, 'system.reg')) reg.read(os.path.join(prefix, 'system.reg'))
return 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": if os.environ.get("container") == "flatpak":
flatpak_command = ["flatpak-spawn", "--host"] 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}") flatpak_command.append(f"--env={name}={value}")
command = flatpak_command + command command = flatpak_command + command
try: try:
@ -35,7 +36,7 @@ def execute(command: List, wine_env: Mapping) -> Tuple[str, str]:
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
# Use the current environment if we are in flatpak or our own if we are on host # 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 # 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, shell=False,
text=True, text=True,
) )
@ -45,13 +46,13 @@ def execute(command: List, wine_env: Mapping) -> Tuple[str, str]:
return res 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("/", "\\") path = path.strip().replace("/", "\\")
# lk: if path does not exist form # 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 # lk: if path exists and needs a case-sensitive interpretation form
# cmd = [wine_cmd, 'cmd', '/c', f'cd {path} & cd'] # 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() out, err = out.strip(), err.strip()
if not out: if not out:
logger.error("Failed to resolve wine path due to \"%s\"", err) 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 raise NotImplementedError
def query_reg_key(wine_exec: str, wine_env: Mapping, reg_path: str, reg_key) -> str: def query_reg_key(command: List[str], environment: Mapping, reg_path: str, reg_key) -> str:
cmd = [wine_exec, "reg", "query", reg_path, "/v", reg_key] cmd = command + ["reg", "query", reg_path, "/v", reg_key]
out, err = execute(cmd, wine_env) out, err = execute(cmd, environment)
out, err = out.strip(), err.strip() out, err = out.strip(), err.strip()
if not out: if not out:
logger.error("Failed to query registry key due to \"%s\"", err) 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 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('"') path = path.strip().strip('"')
cmd = [wine_exec, "winepath.exe", "-u", path] cmd = command + ["winepath.exe", "-u", path]
out, err = execute(cmd, wine_env) out, err = execute(cmd, environment)
out, err = out.strip(), err.strip() out, err = out.strip(), err.strip()
if not out: if not out:
logger.error("Failed to convert to unix path due to \"%s\"", err) logger.error("Failed to convert to unix path due to \"%s\"", err)
return os.path.realpath(out) if (out := out.strip()) else out return os.path.realpath(out) if (out := out.strip()) else out
def wine(core: LegendaryCore, app_name: str = "default") -> str: def get_environment(app_environment: Dict, silent: bool = True) -> Dict:
_wine = core.lgd.config.get( # Get a clean environment if we are in flatpak, this environment will be passed
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
# to `flatpak-spawn`, otherwise use the system's. # to `flatpak-spawn`, otherwise use the system's.
_environ = {} if os.environ.get("container") == "flatpak" else os.environ.copy() _environ = {} if os.environ.get("container") == "flatpak" else os.environ.copy()
_environ.update(core.get_app_environment(app_name)) _environ.update(app_environment)
if silent:
_environ["WINEDEBUG"] = "-all" _environ["WINEDEBUG"] = "-all"
_environ["WINEDLLOVERRIDES"] = "winemenubuilder=d;mscoree=d;mshtml=d;" _environ["WINEDLLOVERRIDES"] = "winemenubuilder=d;mscoree=d;mshtml=d;"
_environ["DISPLAY"] = "" _environ["DISPLAY"] = ""
return _environ 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 ""

View file

@ -1,6 +1,8 @@
import platform as pf import platform as pf
import os import os
import shlex
from dataclasses import dataclass from dataclasses import dataclass
from hashlib import md5
from logging import getLogger from logging import getLogger
from typing import Optional, Union, List, Dict from typing import Optional, Union, List, Dict
@ -28,11 +30,22 @@ def find_libraries(steam_path: str) -> List[str]:
return libraries 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 @dataclass
class SteamBase: class SteamBase:
steam_path: str steam_path: str
tool_path: str tool_path: str
toolmanifest: dict toolmanifest: Dict
def __eq__(self, other): def __eq__(self, other):
return self.tool_path == other.tool_path return self.tool_path == other.tool_path
@ -40,23 +53,36 @@ class SteamBase:
def __hash__(self): def __hash__(self):
return hash(self.tool_path) return hash(self.tool_path)
def commandline(self): @property
cmd = "".join([f"'{self.tool_path}'", self.toolmanifest["manifest"]["commandline"]]) def required_tool(self) -> Optional[str]:
cmd = os.path.normpath(cmd) 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 # NOTE: "waitforexitandrun" seems to be the verb used in by steam to execute stuff
cmd = cmd.replace("%verb%", "waitforexitandrun") # `run` is used when setting up the environment, so use that if we are setting up the prefix.
return cmd 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 @dataclass
class SteamRuntime(SteamBase): class SteamRuntime(SteamBase):
steam_library: str steam_library: str
appmanifest: dict appmanifest: Dict
def name(self): @property
def name(self) -> str:
return self.appmanifest["AppState"]["name"] return self.appmanifest["AppState"]["name"]
def appid(self): @property
def appid(self) -> str:
return self.appmanifest["AppState"]["appid"] return self.appmanifest["AppState"]["appid"]
@ -64,33 +90,36 @@ class SteamRuntime(SteamBase):
class ProtonTool(SteamRuntime): class ProtonTool(SteamRuntime):
runtime: SteamRuntime = None runtime: SteamRuntime = None
def __bool__(self): def __bool__(self) -> bool:
if appid := self.toolmanifest.get("require_tool_appid", False): if appid := self.required_tool:
return self.runtime is not None and self.runtime.appid() == appid return self.runtime is not None and self.runtime.appid == appid
return True
def commandline(self): def command(self, setup: bool = False) -> List[str]:
runtime_cmd = self.runtime.commandline() cmd = self.runtime.command(setup)
cmd = super().commandline() cmd.extend(super().command(setup))
return " ".join([runtime_cmd, cmd]) return cmd
@dataclass @dataclass
class CompatibilityTool(SteamBase): class CompatibilityTool(SteamBase):
compatibilitytool: dict compatibilitytool: Dict
runtime: SteamRuntime = None runtime: SteamRuntime = None
def __bool__(self): def __bool__(self) -> bool:
if appid := self.toolmanifest.get("require_tool_appid", False): if appid := self.required_tool:
return self.runtime is not None and self.runtime.appid() == appid 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] name, data = list(self.compatibilitytool["compatibilitytools"]["compat_tools"].items())[0]
return data["display_name"] return data["display_name"]
def commandline(self): def command(self, setup: bool = False) -> List[str]:
runtime_cmd = self.runtime.commandline() if self.runtime is not None else "" cmd = self.runtime.command(setup) if self.runtime is not None else []
cmd = super().commandline() cmd.extend(super().command(setup))
return " ".join([runtime_cmd, cmd]) return cmd
def find_appmanifests(library: str) -> List[dict]: 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( def find_runtime(
tool: Union[ProtonTool, CompatibilityTool], runtimes: Dict[str, SteamRuntime] tool: Union[ProtonTool, CompatibilityTool], runtimes: Dict[str, SteamRuntime]
) -> Optional[SteamRuntime]: ) -> Optional[SteamRuntime]:
required_tool = tool.toolmanifest["manifest"].get("require_tool_appid") required_tool = tool.required_tool
if required_tool is None: if required_tool is None:
return 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: def get_steam_environment(
environ = {} tool: Optional[Union[ProtonTool, CompatibilityTool]] = None, compat_path: Optional[str] = None
) -> Dict:
# If the tool is unset, return all affected env variable names # If the tool is unset, return all affected env variable names
# IMPORTANT: keep this in sync with the code below # IMPORTANT: keep this in sync with the code below
environ = {"STEAM_COMPAT_DATA_PATH": compat_path if compat_path else ""}
if tool is None: if tool is None:
environ["STEAM_COMPAT_CLIENT_INSTALL_PATH"] = "" environ["STEAM_COMPAT_CLIENT_INSTALL_PATH"] = ""
environ["STEAM_COMPAT_LIBRARY_PATHS"] = "" environ["STEAM_COMPAT_LIBRARY_PATHS"] = ""
@ -221,7 +252,8 @@ def find_tools() -> List[Union[ProtonTool, CompatibilityTool]]:
steam_path = find_steam() steam_path = find_steam()
logger.debug("Using Steam install in %s", steam_path) logger.debug("Using Steam install in %s", steam_path)
steam_libraries = find_libraries(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 = {} runtimes = {}
for library in steam_libraries: for library in steam_libraries:
@ -236,6 +268,8 @@ def find_tools() -> List[Union[ProtonTool, CompatibilityTool]]:
runtime = find_runtime(tool, runtimes) runtime = find_runtime(tool, runtimes)
tool.runtime = runtime tool.runtime = runtime
tools = list(filter(lambda t: bool(t), tools))
return tools return tools
@ -247,7 +281,9 @@ if __name__ == "__main__":
for tool in _tools: for tool in _tools:
print(get_steam_environment(tool)) print(get_steam_environment(tool))
print(tool.name(), tool.commandline()) print(tool.name)
print(tool.command())
print(" ".join(tool.command()))
def find_proton_combos(): def find_proton_combos():

View file

@ -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))

View file

@ -1,6 +1,7 @@
import difflib import difflib
import os import os
from datetime import datetime from datetime import datetime
from enum import StrEnum, Enum
from typing import Tuple from typing import Tuple
import orjson import orjson
@ -18,6 +19,31 @@ __grades_json = None
__active_download = False __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]: def get_rating(core: LegendaryCore, app_name: str) -> Tuple[int, str]:
game = core.get_game(app_name) game = core.get_game(app_name)
try: try:
@ -31,7 +57,7 @@ def get_rating(core: LegendaryCore, app_name: str) -> Tuple[int, str]:
return steam_id, grade 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): def get_grade(steam_code):
if steam_code == 0: if steam_code == 0:
return "fail" return "fail"