From 1ca2a7ccb0a86a67c5e1f182ced4e42be036a37c Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Sat, 14 Feb 2026 20:50:33 +0200 Subject: [PATCH 1/9] static_css: make install button green darker --- rare/resources/static_css/__init__.py | 119 ++++++++++++----------- rare/resources/static_css/stylesheet.py | 8 +- rare/resources/static_css/stylesheet.qss | 8 +- 3 files changed, 68 insertions(+), 67 deletions(-) diff --git a/rare/resources/static_css/__init__.py b/rare/resources/static_css/__init__.py index 0d68eed80b..5b6d4b6d35 100644 --- a/rare/resources/static_css/__init__.py +++ b/rare/resources/static_css/__init__.py @@ -1,69 +1,70 @@ # Resource object code (Python 3) # Created by: object code -# Created by: The Resource Compiler for Qt version 6.9.1 +# Created by: The Resource Compiler for Qt version 6.10.1 # WARNING! All changes made in this file will be lost! from PySide6 import QtCore qt_resource_data = b"\ -\x00\x00\x03\x80\ +\x00\x00\x03\x82\ \x00\ -\x00\x0f\xa5x\x9c\xbdW\xdf\x8f\xd2@\x10~\xe7\xaf\xd8\ -\x83\x90\xe8E\xb0?h\x81\x1a_N\x8d\xb9\xe4N\xbd\ -\x80\xfa`|\xd8v\x87vse\xb7\xd9n\x054\xfe\ -\xefni\x81\x16\xda\x05\x8dJ\x08]vg\xbf\x99\xf9\ -f:\xb3\xfb\xfc\x1a\xcd#\x9a\xa2\x05\x8d\x01\xa9'\xce\ -$\x1f\x84\xc0@`\x09\x04-\x04_\xa2n*71\ -\xa4\x11\x80\x1c&\x9b\xee\x10\xbd~\x8f\xde\xbd\x9f\xa37\ -\xafo\xe7WWW\xe8\xfay\xa7\xf3\xf0!K\xa3\x9b\ -LJ\xce\xd0\x8f\x0eR\x9f%e\x83\x08h\x18I\x0f\ -Y\xc3\xb1\x03\xeb\x17\x9d\x9fJN\xf0P@\x9a\xde`\ -\xa1\x17\x9c%\x94\xdd\xf0\xb5^h\xcey|\x89\xd6;\ -\xca\xe0\x0d\xa1R/\xf5\x8a/}~V\xe7\xc1\xd3/\ -\x8b\x18\xcb\x97])2\xe8~-7\xf9\x5c\x10\x10\x1e\ -2\x12%}\x98\x18\x08Lh\x96z\xc8\xd9\xcf\xe3\xe0\ -1\x14\x18\x1f\xa6\x9dK;\xd92\xd3L\xa7\x02\x1a\xff\ +\xbbSZ\xa0\x85v@\xa3n\x08t\xe7\xe3\x9c{\xcf\ +\xbd\xbdw\xe6\xd9\x0d\x9aG4E\x0b\x1a\x03R\xbf8\ +\x93|\x10\x02\x03\x81%\x10\xb4\x10|\x89\xba\xa9\xdc\xc4\ +\x90F\x00r\x98l\xbaC\xf4\xea\x1e\xbd\xbf\x9f\xa3\xd7\ +\xaf\xde\xce\xaf\xae\xae\xd0\xcd\xb3N\xe7\xe1C\x96F\xb7\ +\x99\x94\x9c\xa1\x1f\x1d\xa4\xfe\x96\x94\x0d\x22\xa0a$=\ +d\x0d\xc7\x0e\xac\x9fw~\xaau\x82\x87\x02\xd2\xf4\x16\ +\x0b\xfd\xc2YB\xd9-_\xeb\x17\xcd9\x8f/a}\ +G\x19\xbc&T\xeaW\xbd\xe4K\x9f\x9f\xe5\x19\ +k\xd1G\xee9tBS\xec\xc7*o[\x08\xec@\ +K`MN\x09>2z^\x98\x5c\x16\x0d\xb2[N\ +k\x91u\xd2\xe4\xc2h\xf0sa\xce\xe3\xeb\xc5\xc9\xa5\ +\xd1P\xe4\xd2\x9cR|\x02A\x17\x1b\x9d2\xae\xef\x8c\ +\xad\xa0\x15\xd6v\xac\xc0tu\xb0:YFc\x1b\x9b\ +\xa4\xddf\xdf\xb2MS\x0b~F\x93\xd2\xba6\xfc\x91\ +I\x0c8\xc6\xbfS\xf6\xea\x14\xb1\x0c\xd7q\xdb\x151\ +\x0d\xdb\xb2O\x149\x80\xea\xf40\xdd\x91=\x9a\xb4'\ +8\xb1&\x96\xaf\x81\xd6\xab\xb1\xb3\xac\x0d\xdd\xb7,k\ +T)V\x0f\x19d\xf0\x99\x8bG\x10\xd5\x9aU\xa2\xae\ +(\x91\x91\x87\xcc\xa32Y\xd6\xae\x94\xc7\x944\xd6O\ +;Yk8\xbe\xac\xb6\xcfr\x93\xc0\x8bn\x11\xe9b\ +\xf6\xa8>\xffA~^@\x98K\xa9\xa7\xbb<\xf8\x17\ +\xd0\xbd\x8cyFf\x1b\x16\x9cq\x91\xb8S\xb7=+\ +\xec\xb1\xed\xd8\xd5\xb8\xed:\xf4\xe5A\xdb\xf7\xb6~C\ +\xb7\xf1yL\xaa\xcd\x89~W\xf15\xddDV\xfbZ\ +\xc0\xd9gJB\x903\x89eV\xa3n\x01+\xcd_\ +ET\x82\xb6\xa1\x1aOQ\xf1q\xb7\xbdTcv9\ +,y2\x88a!\xf7\xf3\xa6y\xba@\xe4\xf6\xd4W\ +(o\x0a\x1f*\xee\xdcQF\x8b\xa7z\xc7V\x96=\ +\xb1\xec\x89j\xf2\xe5\xd7\xf5\xdf\xf5\xc1\xe7\xea\x85^\xd6\ +\xdd\x986.\xa9;2\xed7FeNe\x0c\xff$\ +(F\xfd|s`T\x07;I\x93\x86\xc3\xce\x1f\x80\ +W\xcf\x0b{\x86\x86\x1a\xbd\xcd\xec\xa2\xf6 ?V\xe0\ +\x8dR\x9bF\xffw\x82\xa5\xe7\xd7\x94\xf3P\xe0ME\ +\x9bw4\x95\x97G\xa3a\xdb?|\xb5\xc6Gi\xb9\ +=\xfe\x9e\x15\xd2\xde\x95\x91\x04\x13BY\xb8MW\xb5\ +uh\xc3\xb2>!\x0a\x13\xcb\x99&I\xfeF\xba\xfc\ +w/*yqp\xa5\x96\x97\x12\xd6r\xa0\x8e\xf1!\ +\xf3P\x8e\xdb\xe8\xbc\xf6V\x90_\x92\xb0\xafn[\xbd\ +;LY\xf1\xe8y\x12\xfb\xcd\xdd\xbezq9\x91K\ +\x0a\xcc\xd2\x04\x0b`\xf2\xd8\xfe\x19\xc4\x10\xc8O\x14V\ +\x9a\xb7\x8bq\xd6\x1a\x0a\x0dv\xf1\xb3\xbb\xc2\xd5\x91\xf5\ +8\x8d~\x95\xe1(G\xf2k_\x04\xc1\xa3\xba\xf6\xf5\ +\xde\xe0%\xccq\xb8\xfb\x7fw\x0d\xc4\x22\xa4\xec\xb4\xeb\ +\xfd\xee\x09\xa64\xf0\xf2L\xda\x1fv~\x01\xd3a\xec\ +\xde\ " qt_resource_name = b"\ @@ -83,7 +84,7 @@ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x1a\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x01\x9a\xa3;L\x07\ +\x00\x00\x01\x9c]]\x00Y\ " def qInitResources(): diff --git a/rare/resources/static_css/stylesheet.py b/rare/resources/static_css/stylesheet.py index c229e9e5cc..82f56713ff 100644 --- a/rare/resources/static_css/stylesheet.py +++ b/rare/resources/static_css/stylesheet.py @@ -56,19 +56,19 @@ def css_name(widget: Union[wrappertype, QObject, Type], subwidget: str = ""): # [Un]InstallButton style.QPushButton["#InstallButton"].setValues( - borderColor=QColor(0, 180, 0).name(), backgroundColor=QColor(0, 120, 0).name() + borderColor=QColor(0, 180, 0).name(), backgroundColor=QColor(0, 100, 0).name() ) style.QPushButton["#InstallButton"].hover.setValues( - borderColor=QColor(0, 135, 0).name(), backgroundColor=QColor(0, 90, 0).name() + borderColor=QColor(0, 135, 0).name(), backgroundColor=QColor(0, 70, 0).name() ) style.QPushButton["#InstallButton"].disabled.setValues( borderColor=QColor(0, 60, 0).name(), backgroundColor=QColor(0, 40, 0).name() ) style.QPushButton["#UninstallButton"].setValues( - borderColor=QColor(180, 0, 0).name(), backgroundColor=QColor(120, 0, 0).name() + borderColor=QColor(180, 0, 0).name(), backgroundColor=QColor(100, 0, 0).name() ) style.QPushButton["#UninstallButton"].hover.setValues( - borderColor=QColor(135, 0, 0).name(), backgroundColor=QColor(90, 0, 0).name() + borderColor=QColor(135, 0, 0).name(), backgroundColor=QColor(70, 0, 0).name() ) style.QPushButton["#UninstallButton"].disabled.setValues( borderColor=QColor(60, 0, 0).name(), backgroundColor=QColor(40, 0, 0).name() diff --git a/rare/resources/static_css/stylesheet.qss b/rare/resources/static_css/stylesheet.qss index a304b43600..f9f70906e5 100644 --- a/rare/resources/static_css/stylesheet.qss +++ b/rare/resources/static_css/stylesheet.qss @@ -30,11 +30,11 @@ QLabel#InfoLabel { } QPushButton#InstallButton { border-color: #00b400; - background-color: #007800; + background-color: #006400; } QPushButton#InstallButton:hover { border-color: #008700; - background-color: #005a00; + background-color: #004600; } QPushButton#InstallButton:disabled { border-color: #003c00; @@ -42,11 +42,11 @@ QPushButton#InstallButton:disabled { } QPushButton#UninstallButton { border-color: #b40000; - background-color: #780000; + background-color: #640000; } QPushButton#UninstallButton:hover { border-color: #870000; - background-color: #5a0000; + background-color: #460000; } QPushButton#UninstallButton:disabled { border-color: #3c0000; From a5690360ce5cc8a15c7a994825561c443a793527 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Sat, 14 Feb 2026 20:51:03 +0200 Subject: [PATCH 2/9] PathEdit: make line edit read-only --- rare/widgets/indicator_edit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rare/widgets/indicator_edit.py b/rare/widgets/indicator_edit.py index 69b3b07f8f..e01c878668 100644 --- a/rare/widgets/indicator_edit.py +++ b/rare/widgets/indicator_edit.py @@ -371,6 +371,7 @@ def __init__( ) self.setObjectName(type(self).__name__) self.line_edit.setMinimumSize(QSize(250, 0)) + self.line_edit.setReadOnly(True) self.path_select = QPushButton(self) self.path_select.setObjectName(f"{type(self).__name__}Button") layout = self.layout() From e0adb21d61a7bc58729a929c45e131eed0f98456 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Sat, 14 Feb 2026 22:07:09 +0200 Subject: [PATCH 3/9] launcher: create steamuser -> $USER or $USER -> steamuser symlinks in the prefix --- rare/commands/launcher/lgd_helper.py | 19 ++++++++++--------- rare/utils/compat/utils.py | 17 +++++++++++++++++ 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/rare/commands/launcher/lgd_helper.py b/rare/commands/launcher/lgd_helper.py index 48ee17888e..595800a6be 100644 --- a/rare/commands/launcher/lgd_helper.py +++ b/rare/commands/launcher/lgd_helper.py @@ -11,6 +11,7 @@ from PySide6.QtCore import QProcess, QProcessEnvironment from rare.models.game_slim import RareGameSlim +from rare.utils.compat.utils import create_compat_users from rare.utils.paths import setup_compat_shaders_dir logger = getLogger("RareLauncherUtils") @@ -155,21 +156,21 @@ def prepare_process(command: List[str], environment: Dict) -> Tuple[str, List[st environ = environment.copy() # Sanity check environment (mostly for Linux) - command_line = shlex.join(command) - if os.environ.get("XDG_CURRENT_DESKTOP", None) == "gamescope" or "gamescope" in command_line: - # disable mangohud in gamescope - environ["MANGOHUD"] = "0" # ensure shader compat dirs exist if platform.system() in {"Linux", "FreeBSD"}: - environ["UMU_USE_STEAM"] = "1" + command_line = shlex.join(command) + if os.environ.get("XDG_CURRENT_DESKTOP", None) == "gamescope" or "gamescope" in command_line: + # disable mangohud in gamescope + environ["MANGOHUD"] = "0" if "STEAM_COMPAT_CLIENT_INSTALL_PATH" not in environ: environ["STEAM_COMPAT_CLIENT_INSTALL_PATH"] = "" - if "WINEPREFIX" in environ and not os.path.isdir(environ["WINEPREFIX"]): - os.makedirs(environ["WINEPREFIX"], exist_ok=True) if "STEAM_COMPAT_DATA_PATH" in environ: compat_pfx = os.path.join(environ["STEAM_COMPAT_DATA_PATH"], "pfx") - if not os.path.isdir(compat_pfx): - os.makedirs(compat_pfx, exist_ok=True) + os.makedirs(compat_pfx, exist_ok=True) + create_compat_users(compat_pfx) + if "WINEPREFIX" in environ and not os.path.isdir(environ["WINEPREFIX"]): + os.makedirs(environ["WINEPREFIX"], exist_ok=True) + create_compat_users(environ["WINEPREFIX"]) if "STEAM_COMPAT_SHADER_PATH" in environ: environ.update(setup_compat_shaders_dir(environ["STEAM_COMPAT_SHADER_PATH"])) environ["WINEDLLOVERRIDES"] = environ.get("WINEDLLOVERRIDES", "") + ";lsteamclient=d;" diff --git a/rare/utils/compat/utils.py b/rare/utils/compat/utils.py index 414f155e30..1c7a692e2d 100644 --- a/rare/utils/compat/utils.py +++ b/rare/utils/compat/utils.py @@ -2,6 +2,7 @@ import platform as pf import subprocess from configparser import ConfigParser +from getpass import getuser from logging import getLogger from typing import Dict, List, Mapping, Tuple @@ -174,3 +175,19 @@ def get_host_environment(app_environment: Dict, silent: bool = False) -> Dict: # lk: pressure-vessel complains about this but it doesn't fail due to it # _environ["DISPLAY"] = "" return _environ + + +def create_compat_users(pfx: str): + pfx_users = os.path.join(pfx, "drive_c", "users") + os.makedirs(pfx_users, exist_ok=True) + steam_user = os.path.join(pfx_users, "steamuser") + unix_user = os.path.join(pfx_users, getuser()) + if not os.path.exists(steam_user) and not os.path.exists(unix_user): + os.makedirs(steam_user, exist_ok=True) + pwd = os.getcwd() + os.chdir(pfx_users) + if os.path.exists(steam_user) and not os.path.exists(unix_user): + os.symlink("steamuser", getuser()) + if not os.path.exists(steam_user) and os.path.exists(unix_user): + os.symlink(getuser(), "steamuser") + os.chdir(pwd) From 5d0a3aeff6353e7b0c1c0d8e50e1ef30626307c6 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Sat, 14 Feb 2026 22:07:36 +0200 Subject: [PATCH 4/9] compat: search own `tools` folder for compatibility tools --- rare/utils/compat/steam.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rare/utils/compat/steam.py b/rare/utils/compat/steam.py index 9258c7ecd9..9e0d694353 100644 --- a/rare/utils/compat/steam.py +++ b/rare/utils/compat/steam.py @@ -256,7 +256,7 @@ def find_steam_tools(steam_path: str, library: str) -> List[ProtonTool]: def find_compatibility_tools(steam_path: str) -> List[CompatibilityTool]: compatibilitytools_paths = { - data_dir().joinpath("compatibilitytools").as_posix(), + data_dir().joinpath("tools").as_posix(), os.path.expanduser("~/.local/share/umu/compatibilitytools"), os.path.expanduser(os.path.join(steam_path, "compatibilitytools.d")), os.path.expanduser("~/.steam/compatibilitytools.d"), From 0c853ab506cd5b74da2dfae4328871705b54e75e Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Sat, 14 Feb 2026 22:08:28 +0200 Subject: [PATCH 5/9] chore: return before `finally:` block --- rare/components/tabs/downloads/thread.py | 1 + rare/lgndr/cli.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/rare/components/tabs/downloads/thread.py b/rare/components/tabs/downloads/thread.py index 3a4386c40b..acaaae4c88 100644 --- a/rare/components/tabs/downloads/thread.py +++ b/rare/components/tabs/downloads/thread.py @@ -190,6 +190,7 @@ def chunk_url_sign_thread(): result.shortcut = not self.item.options.update and self.item.options.create_shortcut result.folder_name = self.rgame.folder_name + return finally: ticket_thread.stop = True sign_thread.stop = True diff --git a/rare/lgndr/cli.py b/rare/lgndr/cli.py index b259ea204c..7a6c2c2d5a 100644 --- a/rare/lgndr/cli.py +++ b/rare/lgndr/cli.py @@ -348,12 +348,14 @@ def chunk_url_sign_thread(): self.install_game_cleanup(game, igame, args.repair_mode, args.repair_file) logger.info(f'Finished installation process in {end_t - start_t:.02f} seconds.') + + return ret finally: ticket_thread.stop = True sign_thread.stop = True ticket_thread.join() sign_thread.join() - return ret + @unlock_installed.__func__ def install_game_cleanup(self, game: Game, igame: InstalledGame, repair_mode: bool = False, repair_file: str = '') -> None: From 47535b749a87381d22bbb151ff020be77c9c2535 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Sat, 14 Feb 2026 22:14:34 +0200 Subject: [PATCH 6/9] RunnerSettings: add buttons to open the prefix and user folder. Winetricks button still unimplemented --- .../tabs/settings/widgets/runner.py | 57 +++++++++++++------ 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/rare/components/tabs/settings/widgets/runner.py b/rare/components/tabs/settings/widgets/runner.py index 7ebe18cff1..3f2c50b575 100644 --- a/rare/components/tabs/settings/widgets/runner.py +++ b/rare/components/tabs/settings/widgets/runner.py @@ -1,11 +1,15 @@ +import os.path import platform as pf +from getpass import getuser from typing import Type, TypeVar -from PySide6.QtCore import Qt, Signal, Slot -from PySide6.QtWidgets import QCheckBox, QFormLayout, QGroupBox, QVBoxLayout +from PySide6.QtCore import Qt, QUrl, Signal, Slot +from PySide6.QtGui import QDesktopServices +from PySide6.QtWidgets import QCheckBox, QFormLayout, QGroupBox, QHBoxLayout, QPushButton, QVBoxLayout from rare.models.settings import RareAppSettings, app_settings from rare.shared import RareCore +from rare.utils import config_helper as lgd_config from .wine import WineSettings @@ -33,20 +37,11 @@ def __init__( self.setTitle(self.tr("Compatibility")) - # self.compat_label = QLabel(self.tr("Runner")) - # self.compat_combo = QComboBox(self) - # self.compat_stack = QStackedWidget(self) - self.main_layout = QVBoxLayout(self) self.wine = wine_widget(settings, rcore, self) self.wine.environ_changed.connect(self.environ_changed) self.main_layout.addWidget(self.wine) - # self.compat_layout = QFormLayout(self.compat) - # self.compat_layout.setWidget(0, QFormLayout.ItemRole.LabelRole, self.compat_label) - # self.compat_layout.setWidget(0, QFormLayout.ItemRole.FieldRole, self.compat_combo) - # self.compat_layout.setWidget(1, QFormLayout.ItemRole.SpanningRole, self.compat_stack) - # self.compat_layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.FieldsStayAtSizeHint) self.ctool = False if proton_widget is not None: @@ -55,7 +50,18 @@ def __init__( self.ctool.compat_tool_enabled.connect(self.wine.compat_tool_enabled) self.ctool.compat_path_changed.connect(self.wine.compat_path_changed) self.main_layout.addWidget(self.ctool) - # self.ctool.compat_tool_enabled.connect(self.compat_tool_enabled) + + self.pfx_folder_button = QPushButton(self.tr("Open prefix folder"), self) + self.pfx_folder_button.clicked.connect(self._open_pfx_folder) + self.usr_folder_button = QPushButton(self.tr("Open user folder"), self) + self.usr_folder_button.clicked.connect(self._open_usr_folder) + self.winetricks_button = QPushButton(self.tr("Run winetricks"), self) + + self.button_layout = QHBoxLayout() + self.button_layout.addWidget(self.pfx_folder_button) + self.button_layout.addWidget(self.usr_folder_button) + self.button_layout.addWidget(self.winetricks_button) + self.button_layout.setAlignment(Qt.AlignmentFlag.AlignRight) font = self.font() font.setItalic(True) @@ -64,12 +70,8 @@ def __init__( self.shader_cache_check.setChecked(self.settings.get_value(app_settings.local_shader_cache)) self.shader_cache_check.checkStateChanged.connect(self._shader_cache_check_changed) - # wine_index = self.compat_stack.addWidget(self.wine) - # self.compat_combo.addItem("Wine", wine_index) - # proton_index = self.compat_stack.addWidget(self.proton_tool) - # self.compat_combo.addItem("Proton", proton_index) - self.form_layout = QFormLayout() + self.form_layout.addRow(self.tr(""), self.button_layout) self.form_layout.addRow(self.tr("Shader cache"), self.shader_cache_check) self.form_layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) self.form_layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) @@ -80,5 +82,26 @@ def __init__( def _shader_cache_check_changed(self, state: Qt.CheckState): self.settings.set_value(app_settings.local_shader_cache, state != Qt.CheckState.Unchecked) + @Slot() + def _open_pfx_folder(self): + QDesktopServices.openUrl( + QUrl.fromLocalFile(lgd_config.get_wine_prefix_with_global(self.app_name)) + ) + + @Slot() + def _open_usr_folder(self): + path = os.path.join( + lgd_config.get_wine_prefix_with_global(self.app_name), "drive_c", "users", getuser() + ) + if not os.path.exists(path): + path = os.path.join( + lgd_config.get_wine_prefix_with_global(self.app_name), "drive_c", "users", "steamuser" + ) + QDesktopServices.openUrl(path) + + @Slot() + def _run_winetricks(self): + pass + RunnerSettingsType = TypeVar("RunnerSettingsType", bound=RunnerSettingsBase) From 71addb22086081685ac040833a77f960d79a56c3 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Sat, 14 Feb 2026 22:27:28 +0200 Subject: [PATCH 7/9] chore: various minor code refactors --- rare/components/__init__.py | 14 +++---- .../tabs/library/details/cloud_saves.py | 2 +- .../tabs/settings/widgets/overlay.py | 19 +++++---- .../tabs/settings/widgets/proton.py | 40 +++++++++++-------- rare/components/tabs/settings/widgets/wine.py | 25 ++++++------ rare/models/game.py | 22 +++++----- 6 files changed, 64 insertions(+), 58 deletions(-) diff --git a/rare/components/__init__.py b/rare/components/__init__.py index e34f31cc70..9c460997c3 100644 --- a/rare/components/__init__.py +++ b/rare/components/__init__.py @@ -73,22 +73,22 @@ def relogin(self): @Slot() def launch_app(self): self.launch_dialog = LaunchDialog(self.rcore, parent=None) - self.launch_dialog.rejected.connect(self.__on_exit_app) + self.launch_dialog.rejected.connect(self._on_exit_app) # lk: the reason we use the `start_app` signal here instead of accepted, is to keep the dialog # until the main window has been created, then we call `accept()` to close the dialog - self.launch_dialog.start_app.connect(self.__on_start_app) + self.launch_dialog.start_app.connect(self._on_start_app) self.launch_dialog.start_app.connect(self.launch_dialog.accept) self.launch_dialog.login() @Slot() - def __on_start_app(self): + def _on_start_app(self): self.relogin_timer = QTimer(self) self.relogin_timer.setTimerType(Qt.TimerType.VeryCoarseTimer) self.relogin_timer.timeout.connect(self.relogin) self.poke_timer() self.main_window = RareWindow(self.settings, self.rcore) - self.main_window.exit_app.connect(self.__on_exit_app) + self.main_window.exit_app.connect(self._on_exit_app) if (not self.args.silent) and (not self.settings.get_value(app_settings.sys_tray_start)): self.main_window.show() @@ -96,11 +96,11 @@ def __on_start_app(self): if self.args.test_start: self.main_window.close() self.main_window = None - self.__on_exit_app(0) + self._on_exit_app(0) @Slot() @Slot(int) - def __on_exit_app(self, exit_code=0): + def _on_exit_app(self, exit_code=0): threadpool = QThreadPool.globalInstance() threadpool.waitForDone() if self.relogin_timer is not None: @@ -118,7 +118,7 @@ def __on_exit_app(self, exit_code=0): self.exit(exit_code) -def start(args) -> int: +def start(args: Namespace) -> int: while True: QApplication.setAttribute(Qt.ApplicationAttribute.AA_EnableHighDpiScaling, True) QApplication.setAttribute(Qt.ApplicationAttribute.AA_UseHighDpiPixmaps, True) diff --git a/rare/components/tabs/library/details/cloud_saves.py b/rare/components/tabs/library/details/cloud_saves.py index 2dfe6d326a..2fa3431490 100644 --- a/rare/components/tabs/library/details/cloud_saves.py +++ b/rare/components/tabs/library/details/cloud_saves.py @@ -68,7 +68,7 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): self.cloud_save_path_edit = PathEdit( path="", file_mode=QFileDialog.FileMode.Directory, - placeholder=self.tr('Use "Calculate path" or "Browse" ...'), + placeholder=self.tr('Use "Resolve path" or "Browse" ...'), edit_func=self.edit_save_path, save_func=self.save_save_path, ) diff --git a/rare/components/tabs/settings/widgets/overlay.py b/rare/components/tabs/settings/widgets/overlay.py index b8e87b0a3b..dc68dd49f7 100644 --- a/rare/components/tabs/settings/widgets/overlay.py +++ b/rare/components/tabs/settings/widgets/overlay.py @@ -151,9 +151,9 @@ def __init__(self, parent=None): # self.values: Dict[str, Union[OverlayLineEdit, OverlayComboBox]] = {} self.ui.options_group.setTitle(self.tr("Custom options")) - self.ui.overlay_state_combo.currentIndexChanged.connect(self.__update_settings) + self.ui.overlay_state_combo.currentIndexChanged.connect(self._update_settings) - self.environ_changed.connect(self.__update_current_value) + self.environ_changed.connect(self._update_current_value) def setupWidget( self, @@ -174,26 +174,26 @@ def setupWidget( self.ui.overlay_state_label.setText(label) - for i, widget in enumerate(grid_map): + for idx, widget in enumerate(grid_map): widget.setParent(self.ui.options_group) - self.ui.options_grid.addWidget(widget, i // 4, i % 4) + self.ui.options_grid.addWidget(widget, idx // 4, idx % 4) # self.checkboxes[widget.option] = widget self.option_widgets.append(widget) - widget.stateChanged.connect(self.__update_settings) + widget.stateChanged.connect(self._update_settings) for widget, label in form_map: widget.setParent(self.ui.options_group) self.ui.options_form.addRow(label, widget) # self.values[widget.option] = widget self.option_widgets.append(widget) - widget.valueChanged.connect(self.__update_settings) + widget.valueChanged.connect(self._update_settings) @abstractmethod def update_settings_override(self, state: ActivationStates): raise NotImplementedError @Slot() - def __update_settings(self): + def _update_settings(self): current_state = self.ui.overlay_state_combo.currentData(Qt.ItemDataRole.UserRole) self.ui.options_group.setEnabled(current_state == ActivationStates.CUSTOM) @@ -212,14 +212,13 @@ def __update_settings(self): self.ui.options_group.setDisabled(False) # custom options options = (name for widget in self.option_widgets if (name := widget.getValue()) is not None) - config.set_envvar(self.app_name, self.config_key, self.separator.join(options)) self.environ_changed.emit(self.config_key) self.update_settings_override(current_state) @Slot() - def __update_current_value(self): + def _update_current_value(self): self.ui.current_value_info.setText(config.get_envvar_with_global(self.app_name, self.config_key, "")) def setCurrentState(self, state: ActivationStates): @@ -262,7 +261,7 @@ def showEvent(self, a0: QShowEvent): ) self.ui.options_group.blockSignals(False) - self.__update_current_value() + self._update_current_value() return super().showEvent(a0) diff --git a/rare/components/tabs/settings/widgets/proton.py b/rare/components/tabs/settings/widgets/proton.py index 0ba250f098..4e73a75e43 100644 --- a/rare/components/tabs/settings/widgets/proton.py +++ b/rare/components/tabs/settings/widgets/proton.py @@ -16,7 +16,7 @@ from rare.models.wrapper import Wrapper, WrapperType from rare.shared import RareCore from rare.shared.wrappers import Wrappers -from rare.utils import config_helper as lgd_conf +from rare.utils import config_helper as lgd_config from rare.utils.compat import steam from rare.utils.paths import proton_compat_dir from rare.widgets.indicator_edit import IndicatorReasonsCommon, PathEdit @@ -49,18 +49,18 @@ def __init__(self, rcore: RareCore, parent=None): self.setTitle(self.tr("Proton")) self.tool_combo = QComboBox(self) - self.tool_combo.currentIndexChanged.connect(self.__on_tool_changed) + self.tool_combo.currentIndexChanged.connect(self._on_tool_changed) self.compat_combo = QComboBox(self) self.compat_combo.addItem(self.tr("Shared"), ProtonSettings.CompatLocation.SHARED) self.compat_combo.addItem(self.tr("Isolated"), ProtonSettings.CompatLocation.ISOLATED) self.compat_combo.addItem(self.tr("Custom"), ProtonSettings.CompatLocation.CUSTOM) - self.compat_combo.currentIndexChanged.connect(self.__on_compat_changed) + self.compat_combo.currentIndexChanged.connect(self._on_compat_changed) self.compat_edit = PathEdit( file_mode=QFileDialog.FileMode.Directory, - edit_func=self.proton_prefix_edit, - save_func=self.proton_prefix_save, + edit_func=self._proton_prefix_edit, + save_func=self._proton_prefix_save, placeholder=self.tr("Please select path for proton prefix"), parent=self, ) @@ -127,7 +127,7 @@ def showEvent(self, a0: QShowEvent) -> None: self.tool_combo.blockSignals(False) enabled = bool(self.tool_combo.currentData(Qt.ItemDataRole.UserRole)) - compat_path = lgd_conf.get_compat_data_path(self.app_name, fallback="") + compat_path = lgd_config.get_compat_data_path(self.app_name, fallback="") self.compat_combo.blockSignals(True) compat_location = self._update_compat_folder(compat_path) @@ -142,7 +142,7 @@ def showEvent(self, a0: QShowEvent) -> None: return super().showEvent(a0) @Slot(int) - def __on_tool_changed(self, index): + def _on_tool_changed(self, index): steam_tool: Union[steam.ProtonTool, steam.CompatibilityTool] = self.tool_combo.itemData(index, Qt.ItemDataRole.UserRole) steam_environ = steam.get_steam_environment(steam_tool, self.compat_edit.text()) @@ -156,7 +156,7 @@ def __on_tool_changed(self, index): steam_environ["STEAM_COMPAT_INSTALL_PATH"] = install_path steam_environ["STEAM_COMPAT_LIBRARY_PATHS"] = library_paths for key, value in steam_environ.items(): - lgd_conf.adjust_envvar(self.app_name, key, value) + lgd_config.adjust_envvar(self.app_name, key, value) self.environ_changed.emit(key) wrappers = self.wrappers.get_wrappers(self.app_name) @@ -178,7 +178,7 @@ def __on_tool_changed(self, index): self.compat_edit.setEnabled(steam_tool is not None) compat_path = "" if steam_tool: - compat_path = lgd_conf.get_compat_data_path(self.app_name, fallback="") + compat_path = lgd_config.get_compat_data_path(self.app_name, fallback="") if not compat_path: compat_path = str(self._get_compat_path(ProtonSettings.CompatLocation.NONE)) self._update_compat_folder(compat_path) @@ -186,10 +186,10 @@ def __on_tool_changed(self, index): self.compat_tool_enabled.emit(steam_tool is not None, compat_path) @Slot(int) - def __on_compat_changed(self, index): + def _on_compat_changed(self, index): compat_location: ProtonSettings.CompatLocation = self.compat_combo.itemData(index, Qt.ItemDataRole.UserRole) compat_path = str(self._get_compat_path(compat_location)) - lgd_conf.adjust_compat_data_path(self.app_name, compat_path) + lgd_config.adjust_compat_data_path(self.app_name, compat_path) self.compat_edit.setText(compat_path) self.compat_edit.setEnabled( compat_location @@ -200,17 +200,23 @@ def __on_compat_changed(self, index): ) @staticmethod - def proton_prefix_edit(text: str) -> Tuple[bool, str, int]: + def _proton_prefix_edit(text: str) -> Tuple[bool, str, int]: if not text: return False, text, IndicatorReasonsCommon.IS_EMPTY if os.path.isdir(text): - if os.listdir(text) and not os.path.exists(os.path.join(text, "pfx")): - return False, text, IndicatorReasonsCommon.DIR_NOT_EMPTY - return True, text, IndicatorReasonsCommon.VALID + dir_list = os.listdir(text) + if not dir_list: + return True, text, IndicatorReasonsCommon.VALID + if any( + (x in dir_list) for x in + ("pfx", "shadercache", "dosdevices", "drive_c", "system.reg", "user.reg", "userdef.reg") + ): + return True, text, IndicatorReasonsCommon.VALID + return False, text, IndicatorReasonsCommon.DIR_NOT_EMPTY else: return False, text, IndicatorReasonsCommon.DIR_NOT_EXISTS - def proton_prefix_save(self, text: str): - lgd_conf.adjust_compat_data_path(self.app_name, text) + def _proton_prefix_save(self, text: str): + lgd_config.adjust_compat_data_path(self.app_name, text) self.environ_changed.emit("STEAM_COMPAT_DATA_PATH") self.compat_path_changed.emit(text) diff --git a/rare/components/tabs/settings/widgets/wine.py b/rare/components/tabs/settings/widgets/wine.py index 8fcb2c022b..8b9077503c 100644 --- a/rare/components/tabs/settings/widgets/wine.py +++ b/rare/components/tabs/settings/widgets/wine.py @@ -8,7 +8,7 @@ from rare.models.settings import RareAppSettings from rare.shared import RareCore -from rare.utils import config_helper as lgd_conf +from rare.utils import config_helper as lgd_config from rare.widgets.indicator_edit import IndicatorReasonsCommon, PathEdit logger = getLogger("WineSettings") @@ -54,7 +54,7 @@ def __init__(self, settings: RareAppSettings, rcore: RareCore, parent=None): layout = QFormLayout(self) layout.addRow(self.tr("Executable"), self.wine_execut_edit) - layout.addRow(self.tr("Prefix"), self.wine_prefix_edit) + layout.addRow(self.tr("Prefix folder"), self.wine_prefix_edit) layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow) layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) layout.setFormAlignment(Qt.AlignmentFlag.AlignLeading | Qt.AlignmentFlag.AlignVCenter) @@ -64,7 +64,7 @@ def __update_widget(self): self.wine_prefix_edit.setText(self.load_prefix()) _ = QSignalBlocker(self.wine_execut_edit) self.wine_execut_edit.setText(self.load_execut()) - self.setDisabled(lgd_conf.get_boolean(self.app_name, "no_wine", fallback=False)) + self.setDisabled(lgd_config.get_boolean(self.app_name, "no_wine", fallback=False)) def showEvent(self, a0: QShowEvent): if a0.spontaneous(): @@ -74,47 +74,48 @@ def showEvent(self, a0: QShowEvent): @Slot(str) def compat_path_changed(self, text: str): - self.wine_prefix_edit.setText(os.path.join(text, "pfx") if text else text) + path = os.path.join(text, "pfx") if text else text + self.wine_prefix_edit.setText(path) @Slot(bool) def compat_tool_enabled(self, enabled: bool, path: str): old_wine_execut = self.settings.value(f"{self.app_name}/wine_execut", defaultValue=None) old_wine_prefix = self.settings.value(f"{self.app_name}/wine_prefix", defaultValue=None) if enabled: - wine_execut = lgd_conf.get_option(self.app_name, "wine_executable", "") - wine_prefix = lgd_conf.get_wine_prefix(self.app_name, "") + wine_execut = lgd_config.get_option(self.app_name, "wine_executable", "") + wine_prefix = lgd_config.get_wine_prefix(self.app_name, "") if old_wine_execut is None: self.settings.setValue(f"{self.app_name}/wine_execut", wine_execut) if old_wine_prefix is None: self.settings.setValue(f"{self.app_name}/wine_prefix", wine_prefix) self.wine_execut_edit.setText("") self.wine_prefix_edit.setText(os.path.join(path, "pfx")) - lgd_conf.set_boolean(self.app_name, "no_wine", True) + lgd_config.set_boolean(self.app_name, "no_wine", True) else: self.settings.remove(f"{self.app_name}/wine_execut") self.settings.remove(f"{self.app_name}/wine_prefix") self.wine_execut_edit.setText(old_wine_execut) self.wine_prefix_edit.setText(old_wine_prefix) - lgd_conf.remove_option(self.app_name, "no_wine") + lgd_config.remove_option(self.app_name, "no_wine") self.setDisabled(enabled) def load_prefix(self) -> str: if self.app_name is None: raise RuntimeError - return lgd_conf.get_wine_prefix(self.app_name, "") + return lgd_config.get_wine_prefix(self.app_name, "") def save_prefix(self, path: str) -> None: if self.app_name is None: raise RuntimeError - lgd_conf.adjust_wine_prefix(self.app_name, path) + lgd_config.adjust_wine_prefix(self.app_name, path) self.environ_changed.emit("WINEPREFIX") def load_execut(self) -> str: if self.app_name is None: raise RuntimeError - return lgd_conf.get_option(self.app_name, "wine_executable", "") + return lgd_config.get_option(self.app_name, "wine_executable", "") def save_execut(self, text: str) -> None: if self.app_name is None: raise RuntimeError - lgd_conf.adjust_option(self.app_name, "wine_executable", text) + lgd_config.adjust_option(self.app_name, "wine_executable", text) diff --git a/rare/models/game.py b/rare/models/game.py index 06012d7266..c3fbdbf5c8 100644 --- a/rare/models/game.py +++ b/rare/models/game.py @@ -117,8 +117,8 @@ def __init__( self.__steam_grade_pending: bool = False self.game_process = GameProcess(self.game) - self.game_process.launched.connect(self.__game_launched) - self.game_process.finished.connect(self.__game_finished) + self.game_process.launched.connect(self._game_launched) + self.game_process.finished.connect(self._game_finished) if self.is_installed and not self.is_dlc: self.game_process.connect_to_server(on_startup=True) @@ -165,7 +165,7 @@ def del_worker(self, worker: QRunnable): self.state = RareGame.State.IDLE @Slot(int) - def __game_launched(self, code: int): + def _game_launched(self, code: int): self.state = RareGame.State.RUNNING self.metadata.last_played = datetime.now(timezone.utc) if code == GameProcess.Code.ON_STARTUP: @@ -174,7 +174,7 @@ def __game_launched(self, code: int): self.signals.game.launched.emit(self.app_name) @Slot(int) - def __game_finished(self, exit_code: int): + def _game_finished(self, exit_code: int): if exit_code == GameProcess.Code.ON_STARTUP: return if self.supports_cloud_saves: @@ -341,7 +341,7 @@ def set_installed(self, installed: bool) -> None: self.core.egstore_delete(self.igame) self.igame = None self.signals.game.uninstalled.emit(self.app_name) - self.__update_pixmap() + self._update_pixmap() @property def can_run_offline(self) -> bool: @@ -598,7 +598,7 @@ def set_origin_attributes(self, path: str, size: int = 0) -> None: self.signals.game.installed.emit(self.app_name) else: self.signals.game.uninstalled.emit(self.app_name) - self.__update_pixmap() + self._update_pixmap() @property def can_launch(self) -> bool: @@ -614,7 +614,7 @@ def get_pixmap(self, preset: ImageSize.Preset, color=True) -> QPixmap: return self.image_manager.get_pixmap(self.app_name, preset, color) @Slot() - def __update_pixmap(self): + def _update_pixmap(self): self.has_pixmap = self.image_manager.has_pixmaps(self.app_name) if self.has_pixmap: self.signals.widget.refresh.emit() @@ -622,10 +622,10 @@ def __update_pixmap(self): def load_pixmaps(self): """Do not call this function, call set_pixmap instead. This is only used for initial image loading""" if not self.has_pixmap: - self.image_manager.download_image(self.game, self.__update_pixmap, 0, False) + self.image_manager.download_image(self.game, self._update_pixmap, 0, False) def refresh_pixmap(self): - self.image_manager.download_image(self.game, self.__update_pixmap, 0, True) + self.image_manager.download_image(self.game, self._update_pixmap, 0, True) @property def __install_base_path(self) -> str: @@ -763,10 +763,10 @@ def __init__( self.image_manager = image_manager self.igame: Optional[InstalledGame] = self.core.lgd.get_overlay_install_info() - self.image_manager.download_image(game, self.__update_pixmap, 0, False) + self.image_manager.download_image(game, self._update_pixmap, 0, False) @Slot() - def __update_pixmap(self): + def _update_pixmap(self): self.has_pixmap = self.image_manager.has_pixmaps(self.app_name) if self.has_pixmap: self.signals.widget.refresh.emit() From 3adcf304c9dd7fa3b662e7a7e78d0d040d389a84 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Sat, 14 Feb 2026 23:27:28 +0200 Subject: [PATCH 8/9] refactor: use data present in the egs manifests to populate SDLs The SDL data legendary provides is missing a lot of games which makes games unable to download optional language packs etc. In the case of HITMAN 3 (Eider) the executable needed to run the game is also in a separate "optional" download which caused the game to not install or work properly. To fix this situation, for games that legendary doesn't provide the needed data, fetch the manifest and get the optional download tags from there. In the case of games such as Fortnite, that legendary provides an incomplete list of tags, combine the tags from the manifest with legendary's data. Partially fixes: https://github.com/RareDevs/Rare/issues/681 --- rare/components/dialogs/install/dialog.py | 2 +- rare/components/dialogs/install/selective.py | 49 ++++++++++--------- .../tabs/library/details/details.py | 4 +- rare/models/game.py | 41 +++++++++++++++- rare/models/game_slim.py | 7 +-- 5 files changed, 71 insertions(+), 32 deletions(-) diff --git a/rare/components/dialogs/install/dialog.py b/rare/components/dialogs/install/dialog.py index 592b5c1684..82fe6c8eee 100644 --- a/rare/components/dialogs/install/dialog.py +++ b/rare/components/dialogs/install/dialog.py @@ -232,7 +232,7 @@ def get_options(self): self.__options.ignore_space = self.advanced.ui.ignore_space_check.isChecked() self.__options.no_install = self.advanced.ui.download_only_check.isChecked() self.__options.install_prereqs = self.advanced.ui.install_prereqs_check.isChecked() - self.__options.install_tag = self.selectable.install_tags() + self.__options.install_tag = self.selectable.enabled_tags() self.__options.reset_sdl = True def get_download_info(self): diff --git a/rare/components/dialogs/install/selective.py b/rare/components/dialogs/install/selective.py index cf79b4e4f0..4f016f11c1 100644 --- a/rare/components/dialogs/install/selective.py +++ b/rare/components/dialogs/install/selective.py @@ -26,40 +26,44 @@ class SelectiveWidget(QWidget): def __init__(self, rgame: RareGame, platform: str, parent=None): super().__init__(parent=parent) + self._has_tags = False main_layout = QVBoxLayout(self) - main_layout.setSpacing(0) + main_layout.setSpacing(2) core = rgame.core config_tags = core.lgd.config.get(rgame.app_name, "install_tags", fallback=None) config_disable_sdl = core.lgd.config.getboolean(rgame.app_name, "disable_sdl", fallback=False) - sdl_name = get_sdl_appname(rgame.app_name) - if not config_disable_sdl and sdl_name is not None: - sdl_data = core.get_sdl_data(sdl_name, platform=platform) - if sdl_data: - for tag, info in sdl_data.items(): - cb = InstallTagCheckBox(info["name"].strip(), info["description"].strip(), info["tags"]) - if tag == "__required": + + sdl_data = rgame.sdl_data(platform) + if not config_disable_sdl and sdl_data: + for group, info in sdl_data.items(): + cb = InstallTagCheckBox(info["name"].strip(), info["description"].strip(), info["tags"]) + if group == "__required": + cb.setChecked(True) + cb.setDisabled(True) + if config_tags is not None: + if all(tag in config_tags for tag in info["tags"]): cb.setChecked(True) - cb.setDisabled(True) - if config_tags is not None: - if all(elem in config_tags for elem in info["tags"]): - cb.setChecked(True) - cb.stateChanged.connect(self.stateChanged) - main_layout.addWidget(cb) - self.parent().setDisabled(False) + cb.stateChanged.connect(self.stateChanged) + main_layout.addWidget(cb) + self._has_tags = True else: - self.parent().setDisabled(True) + self._has_tags = False - def install_tags(self): - install_tags = [""] + def enabled_tags(self) -> List[str]: + install_tags = set() for cb in self.findChildren(InstallTagCheckBox, options=Qt.FindChildOption.FindDirectChildrenOnly): if data := cb.isChecked(): # noinspection PyTypeChecker - install_tags.extend(data) + install_tags.update(data) + install_tags = ["", *install_tags] return install_tags + def supports_tags(self) -> bool: + return self._has_tags + class InstallDialogSelective(CollapsibleFrame): stateChanged: Signal = Signal() @@ -68,7 +72,7 @@ def __init__(self, rgame: RareGame, parent=None): super(InstallDialogSelective, self).__init__(parent=parent) title = self.tr("Optional downloads") self.setTitle(title) - self.setEnabled(bool(rgame.sdl_name)) + self.setEnabled(False) self.widget: SelectiveWidget = None self.rgame = rgame @@ -78,11 +82,12 @@ def update_list(self, platform: str): self.widget.disconnect(self.widget) self.widget.deleteLater() self.widget = SelectiveWidget(self.rgame, platform, parent=self) + self.setEnabled(self.widget.supports_tags()) self.widget.stateChanged.connect(self.stateChanged) self.setWidget(self.widget) - def install_tags(self): - return self.widget.install_tags() + def enabled_tags(self) -> List[str]: + return self.widget.enabled_tags() __all__ = ["InstallDialogSelective", "SelectiveWidget"] diff --git a/rare/components/tabs/library/details/details.py b/rare/components/tabs/library/details/details.py index 1626e9a120..7d28108ec6 100644 --- a/rare/components/tabs/library/details/details.py +++ b/rare/components/tabs/library/details/details.py @@ -184,7 +184,7 @@ def __on_verify(self): self.tr("Installation path for {} does not exist. Cannot continue.").format(self.rgame.app_title), ) return - if self.rgame.sdl_name is not None: + if self.rgame.sdl_available: selective_dialog = SelectiveDialog(self.rgame, parent=self) selective_dialog.result_ready.connect(self.verify_game) selective_dialog.open() @@ -362,7 +362,7 @@ def __update_widget(self): self.ui.import_button.setEnabled((not self.rgame.is_installed or self.rgame.is_non_asset) and self.rgame.is_idle) self.ui.modify_button.setEnabled( - self.rgame.is_installed and (not self.rgame.is_non_asset) and self.rgame.is_idle and self.rgame.sdl_name is not None + self.rgame.is_installed and (not self.rgame.is_non_asset) and self.rgame.is_idle and self.rgame.sdl_available ) self.ui.verify_button.setEnabled(self.rgame.is_installed and (not self.rgame.is_non_asset) and self.rgame.is_idle) diff --git a/rare/models/game.py b/rare/models/game.py index c3fbdbf5c8..7f5cb2855f 100644 --- a/rare/models/game.py +++ b/rare/models/game.py @@ -10,6 +10,7 @@ from legendary.lfs import eos from legendary.models.game import Game, InstalledGame +from legendary.utils.selective_dl import get_sdl_appname from PySide6.QtCore import QProcess, QRunnable, QThreadPool, Slot from PySide6.QtGui import QPixmap @@ -524,6 +525,44 @@ def eulas(self) -> List: return not_accepted_eulas + def sdl_data(self, platform: str) -> Optional[Dict[str, Dict]]: + sdl_data = {} + + sdl_name = get_sdl_appname(self.app_name) + if data := self.core.get_sdl_data(sdl_name, platform=platform): + sdl_data.update(data) + known_install_tags = set() + if sdl_data: + known_install_tags = set(tag for _, info in sdl_data.items() for tag in info["tags"]) + + if self.igame is not None and not self.has_update: + manifest_data = self.core.lgd.load_manifest(self.app_name, self.igame.version, self.igame.platform) + else: + manifest_data, _, _ = self.core.get_cdn_manifest(self.game, platform) + manifest = self.core.load_manifest(manifest_data) + manifest_install_tags = set() + for fm in manifest.file_manifest_list.elements: + for tag in fm.install_tags: + manifest_install_tags.add(tag) + + extra_install_tags = manifest_install_tags.difference(known_install_tags) + for extra_tag in extra_install_tags: + sdl_data[extra_tag] = {"name": extra_tag, "description": "", "tags": [extra_tag]} + + return sdl_data + + @property + def sdl_available(self) -> bool: + if self.igame is not None: + manifest_data = self.core.lgd.load_manifest(self.app_name, self.igame.version, self.igame.platform) + manifest = self.core.load_manifest(manifest_data) + manifest_install_tags = set() + for fm in manifest.file_manifest_list.elements: + for tag in fm.install_tags: + manifest_install_tags.add(tag) + return bool(manifest_install_tags) + return get_sdl_appname(self.app_name) is not None + def reset_steam_date(self): self.metadata.steam_date = datetime.min.replace(tzinfo=timezone.utc) self.signals.widget.refresh.emit() @@ -690,7 +729,7 @@ def uninstall(self) -> bool: UninstallOptionsModel( app_name=self.app_name, keep_folder=self.is_dlc, - keep_config=self.sdl_name is not None or self.is_dlc or platform.system() not in {"Windows"}, + keep_config=self.sdl_available or self.is_dlc or platform.system() not in {"Windows"}, )) return True diff --git a/rare/models/game_slim.py b/rare/models/game_slim.py index 98e4e27e48..270f0ed0cd 100644 --- a/rare/models/game_slim.py +++ b/rare/models/game_slim.py @@ -4,11 +4,10 @@ from datetime import datetime from enum import IntEnum from logging import getLogger -from typing import List, Optional, Tuple +from typing import Dict, List, Optional, Tuple from legendary.lfs import eos from legendary.models.game import Game, InstalledGame, SaveGameFile, SaveGameStatus -from legendary.utils.selective_dl import get_sdl_appname from PySide6.QtCore import QObject, Signal from rare.lgndr.core import LegendaryCore @@ -203,10 +202,6 @@ def is_launchable_addon(self) -> bool: except AttributeError: return False - @property - def sdl_name(self) -> Optional[str]: - return get_sdl_appname(self.app_name) - @property def version(self) -> str: """! From 610fe4f49a6030f1e1a22e86c03482d86eaf1740 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Sun, 15 Feb 2026 01:47:15 +0200 Subject: [PATCH 9/9] chore: refactor install dialog variables --- rare/components/dialogs/install/dialog.py | 106 +++++++++++----------- 1 file changed, 54 insertions(+), 52 deletions(-) diff --git a/rare/components/dialogs/install/dialog.py b/rare/components/dialogs/install/dialog.py index 82fe6c8eee..70d261760a 100644 --- a/rare/components/dialogs/install/dialog.py +++ b/rare/components/dialogs/install/dialog.py @@ -52,9 +52,9 @@ def __init__(self, settings: RareAppSettings, rgame: "RareGame", options: Instal self.core = rgame.core self.rgame = rgame - self.__options: InstallOptionsModel = options - self.__download: Optional[InstallDownloadModel] = None - self.__queue_item: Optional[InstallQueueItemModel] = None + self._options: InstallOptionsModel = options + self._download: Optional[InstallDownloadModel] = None + self._queue_item: Optional[InstallQueueItemModel] = None self.selectable = InstallDialogSelective(rgame, parent=self) self.selectable.stateChanged.connect(self._on_option_changed) @@ -86,11 +86,11 @@ def __init__(self, settings: RareAppSettings, rgame: "RareGame", options: Instal self.install_dir_edit = PathEdit( path=options.base_path, file_mode=QFileDialog.FileMode.Directory, - edit_func=self.__install_dir_edit_callback, - save_func=self.__install_dir_save_callback, + edit_func=self._install_dir_edit_callback, + save_func=self._install_dir_save_callback, parent=self, ) - self.install_dir_edit.validationFinished.connect(self.__on_install_dir_validation) + self.install_dir_edit.validationFinished.connect(self._on_install_dir_validation) self.ui.main_layout.setWidget( self.ui.main_layout.getWidgetPosition(self.ui.install_dir_label)[0], QFormLayout.ItemRole.FieldRole, @@ -145,13 +145,9 @@ def __init__(self, settings: RareAppSettings, rgame: "RareGame", options: Instal self.advanced.ui.download_only_check.setChecked(options.no_install) self.advanced.ui.download_only_check.checkStateChanged.connect(self._on_option_changed_no_reload) - self.reset_install_dir(self.ui.platform_combo.currentIndex()) - self.selectable.update_list(self.ui.platform_combo.currentText()) - self.check_incompatible_platform(self.ui.platform_combo.currentIndex()) - self.accept_button.setEnabled(False) - if self.__options.overlay: + if self._options.overlay: self.ui.platform_label.setEnabled(False) self.ui.platform_combo.setEnabled(False) self.advanced.ui.ignore_space_label.setEnabled(False) @@ -171,7 +167,7 @@ def __init__(self, settings: RareAppSettings, rgame: "RareGame", options: Instal self.advanced.ui.install_prereqs_label.setEnabled(False) self.advanced.ui.install_prereqs_check.setEnabled(False) self.advanced.ui.install_prereqs_check.checkStateChanged.connect(self._on_option_changed_no_reload) - self.advanced.ui.install_prereqs_check.setChecked(self.__options.install_prereqs) + self.advanced.ui.install_prereqs_check.setChecked(self._options.install_prereqs) # lk: set object names for CSS properties self.accept_button.setText(header) @@ -190,7 +186,11 @@ def showEvent(self, a0: QShowEvent) -> None: return super().showEvent(a0) def execute(self): - if self.__options.silent: + self.reset_install_dir(self.ui.platform_combo.currentIndex()) + self.check_incompatible_platform(self.ui.platform_combo.currentIndex()) + self.selectable.update_list(self.ui.platform_combo.currentText()) + + if self._options.silent: self.get_download_info() else: self.action_handler() @@ -215,31 +215,31 @@ def check_incompatible_platform(self, index: int): self.set_error_labels() def get_options(self): - base_path = os.path.join(self.install_dir_edit.text(), ".overlay" if self.__options.overlay else "") + base_path = os.path.join(self.install_dir_edit.text(), ".overlay" if self._options.overlay else "") # TODO: investigate if this check is needed if self.rgame.is_installed or self.rgame.is_dlc: - self.__options.base_path = "" + self._options.base_path = "" else: - self.__options.base_path = base_path - self.__options.platform = self.ui.platform_combo.currentText() - self.__options.create_shortcut = self.ui.shortcut_check.isChecked() - self.__options.max_workers = self.advanced.ui.max_workers_spin.value() - self.__options.shared_memory = self.advanced.ui.max_memory_spin.value() - self.__options.read_files = self.advanced.ui.read_files_check.isChecked() - self.__options.always_use_signed_urls = self.advanced.ui.use_signed_urls_check.isChecked() - self.__options.order_opt = self.advanced.ui.dl_optimizations_check.isChecked() - self.__options.force = self.advanced.ui.force_download_check.isChecked() - self.__options.ignore_space = self.advanced.ui.ignore_space_check.isChecked() - self.__options.no_install = self.advanced.ui.download_only_check.isChecked() - self.__options.install_prereqs = self.advanced.ui.install_prereqs_check.isChecked() - self.__options.install_tag = self.selectable.enabled_tags() - self.__options.reset_sdl = True + self._options.base_path = base_path + self._options.platform = self.ui.platform_combo.currentText() + self._options.create_shortcut = self.ui.shortcut_check.isChecked() + self._options.max_workers = self.advanced.ui.max_workers_spin.value() + self._options.shared_memory = self.advanced.ui.max_memory_spin.value() + self._options.read_files = self.advanced.ui.read_files_check.isChecked() + self._options.always_use_signed_urls = self.advanced.ui.use_signed_urls_check.isChecked() + self._options.order_opt = self.advanced.ui.dl_optimizations_check.isChecked() + self._options.force = self.advanced.ui.force_download_check.isChecked() + self._options.ignore_space = self.advanced.ui.ignore_space_check.isChecked() + self._options.no_install = self.advanced.ui.download_only_check.isChecked() + self._options.install_prereqs = self.advanced.ui.install_prereqs_check.isChecked() + self._options.install_tag = self.selectable.enabled_tags() + self._options.reset_sdl = True def get_download_info(self): - self.__download = None - info_worker = InstallInfoWorker(self.core, self.__options) - info_worker.signals.result.connect(self.__on_worker_result) - info_worker.signals.failed.connect(self.__on_worker_failed) + self._download = None + info_worker = InstallInfoWorker(self.core, self._options) + info_worker.signals.result.connect(self._on_worker_result) + info_worker.signals.failed.connect(self._on_worker_failed) self.threadpool.start(info_worker) def action_handler(self): @@ -260,15 +260,15 @@ def _on_option_changed(self): @Slot(Qt.CheckState) def _on_option_changed_no_reload(self, state: Qt.CheckState): if self.sender() is self.advanced.ui.download_only_check: - self.__options.no_install = state != Qt.CheckState.Unchecked + self._options.no_install = state != Qt.CheckState.Unchecked elif self.sender() is self.ui.shortcut_check: self.settings.set_value(app_settings.create_shortcut, state != Qt.CheckState.Unchecked) - self.__options.create_shortcut = state != Qt.CheckState.Unchecked + self._options.create_shortcut = state != Qt.CheckState.Unchecked elif self.sender() is self.advanced.ui.install_prereqs_check: - self.__options.install_prereqs = state != Qt.CheckState.Unchecked + self._options.install_prereqs = state != Qt.CheckState.Unchecked @staticmethod - def __install_dir_edit_callback(path: str) -> Tuple[bool, str, int]: + def _install_dir_edit_callback(path: str) -> Tuple[bool, str, int]: if not path: return False, path, IndicatorReasonsCommon.IS_EMPTY try: @@ -281,14 +281,14 @@ def __install_dir_edit_callback(path: str) -> Tuple[bool, str, int]: return False, path, IndicatorReasonsCommon.DIR_NOT_EXISTS return True, path, IndicatorReasonsCommon.VALID - def __install_dir_save_callback(self, path: str): + def _install_dir_save_callback(self, path: str): if not os.path.exists(path): return _, _, free_space = shutil.disk_usage(path) self.ui.available_space_text.setText(format_size(free_space)) @Slot(bool, str) - def __on_install_dir_validation(self, is_valid: bool, reason: str): + def _on_install_dir_validation(self, is_valid: bool, reason: str): self.accept_button.setEnabled(False) self.action_button.setEnabled(is_valid and not self.active()) if not is_valid: @@ -307,9 +307,9 @@ def same_platform(download: InstallDownloadModel) -> bool: return False @Slot(InstallDownloadModel) - def __on_worker_result(self, download: InstallDownloadModel): + def _on_worker_result(self, download: InstallDownloadModel): self.setActive(False) - self.__download = download + self._download = download download_size = download.analysis.dl_size install_size = download.analysis.install_size # install_size = self.dl_item.download.analysis.disk_space_delta @@ -330,28 +330,30 @@ def __on_worker_result(self, download: InstallDownloadModel): self.advanced.ui.install_prereqs_check.setEnabled(has_prereqs) self.advanced.ui.install_prereqs_check.setChecked(has_prereqs and self.same_platform(download)) - # new_manifest_data, _, _ = self.core.get_cdn_manifest(download.game, download.igame.platform, self.__options.disable_https) + # TODO: there is information about the files to be downloaded in analres, don't fetch the manifest again + # TODO: see if you can re-use the one from selective downloads + # new_manifest_data, _, _ = self.core.get_cdn_manifest(download.game, download.igame.platform, self._options.disable_https) # new_manifest = self.core.load_manifest(new_manifest_data) # self.file_filters.clear() # for e in new_manifest.file_manifest_list.elements: # self.file_filters.add_item(e.filename.lower()) - if self.__options.silent: + if self._options.silent: self.accept() @Slot(str) - def __on_worker_failed(self, message: str): + def _on_worker_failed(self, message: str): self.setActive(False) error_text = self.tr("Error") self.set_size_labels(error_text, error_text) self.set_error_labels(error_text, message) self.action_button.setEnabled(self.options_changed) self.accept_button.setEnabled(False) - if self.__options.silent: + if self._options.silent: self.open() @staticmethod - def __set_size_label(label: QLabel, value: Union[int, float, str]): + def _set_size_label(label: QLabel, value: Union[int, float, str]): is_numeric = isinstance(value, (int, float)) font = label.font() font.setBold(is_numeric) @@ -361,8 +363,8 @@ def __set_size_label(label: QLabel, value: Union[int, float, str]): label.setText(text) def set_size_labels(self, download: Union[int, float, str], install: Union[int, float, str]): - self.__set_size_label(self.ui.download_size_text, download) - self.__set_size_label(self.ui.install_size_text, install) + self._set_size_label(self.ui.download_size_text, download) + self._set_size_label(self.ui.install_size_text, install) def set_error_labels(self, label: str = "", message: str = ""): self.ui.warning_label.setVisible(bool(label)) @@ -373,11 +375,11 @@ def set_error_labels(self, label: str = "", message: str = ""): def done_handler(self): self.threadpool.clear() self.threadpool.waitForDone() - self.result_ready.emit(self.__queue_item) + self.result_ready.emit(self._queue_item) - # lk: __download is already set at this point so just do nothing. + # lk: _download is already set at this point so just do nothing. def accept_handler(self): - self.__queue_item = InstallQueueItemModel(options=self.__options, download=self.__download) + self._queue_item = InstallQueueItemModel(options=self._options, download=self._download) def reject_handler(self): - self.__queue_item = InstallQueueItemModel(options=self.__options, download=None) + self._queue_item = InstallQueueItemModel(options=self._options, download=None)