From 2431e1f355278ce83a146406b98e855f9b5dfdc1 Mon Sep 17 00:00:00 2001 From: Kraken Date: Fri, 10 Apr 2026 17:42:15 +0100 Subject: [PATCH 1/2] cyberpunk fix cache crash --- games/game_cyberpunk2077.py | 95 ++++++++++++++----------------------- 1 file changed, 35 insertions(+), 60 deletions(-) diff --git a/games/game_cyberpunk2077.py b/games/game_cyberpunk2077.py index 8358796..9805b89 100644 --- a/games/game_cyberpunk2077.py +++ b/games/game_cyberpunk2077.py @@ -10,6 +10,7 @@ from pathlib import Path from typing import Any, Literal, TypeVar +import mobase from PyQt6.QtCore import QDateTime, QDir, Qt, qCritical, qInfo, qWarning from PyQt6.QtWidgets import ( QCheckBox, @@ -19,8 +20,6 @@ QWidget, ) -import mobase - from ..basic_features import BasicLocalSavegames, BasicModDataChecker, GlobPatterns from ..basic_features.basic_save_game_info import ( BasicGameSaveGame, @@ -34,16 +33,6 @@ class CyberpunkModDataChecker(BasicModDataChecker): def __init__(self): super().__init__( GlobPatterns( - delete=[ - "*.gif", - "*.jpg", - "*.jpeg", - "*.jxl", - "*.md", - "*.png", - "*.txt", - "*.webp", - ], move={ # archive and ArchiveXL "*.archive": "archive/pc/mod/", @@ -85,7 +74,7 @@ def parse_cyberpunk_save_metadata(save_path: Path, save: mobase.ISaveGame): "Street Cred": int(meta_data["streetCred"]), "Life Path": meta_data["lifePath"], "Difficulty": meta_data["difficulty"], - "Gender": f"{meta_data['bodyGender']} / {meta_data['brainGender']}", + "Gender": f'{meta_data["bodyGender"]} / {meta_data["brainGender"]}', "Game version": meta_data["buildPatch"], } except (FileNotFoundError, json.JSONDecodeError): @@ -200,7 +189,7 @@ def active_mod_paths(self, reverse: bool = False) -> Iterable[Path]: class Cyberpunk2077Game(BasicGame): Name = "Cyberpunk 2077 Support Plugin" Author = "6788, Zash" - Version = "3.0.1" + Version = "3.0.0" GameName = "Cyberpunk 2077" GameShortName = "cyberpunk2077" @@ -233,7 +222,7 @@ class Cyberpunk2077Game(BasicGame): def init(self, organizer: mobase.IOrganizer) -> bool: super().init(organizer) - self._register_feature(BasicLocalSavegames(self)) + self._register_feature(BasicLocalSavegames(self.savesDirectory())) self._register_feature( BasicGameSaveGameInfo( lambda p: Path(p or "", "screenshot.png"), @@ -256,7 +245,6 @@ def init(self, organizer: mobase.IOrganizer) -> bool: ), ) organizer.onAboutToRun(self._onAboutToRun) - organizer.onFinishedRun(self._onFinishedRun) organizer.onPluginSettingChanged(self._on_settings_changed) organizer.modList().onModInstalled(self._check_disable_crashreporter) organizer.onUserInterfaceInitialized(self._on_user_interface_initialized) @@ -429,11 +417,6 @@ def settings(self) -> list[mobase.PluginSetting]: ), True, ), - mobase.PluginSetting( - "crash_message", - ("Show a crash message as replacement of disabled CrashReporter"), - True, - ), mobase.PluginSetting( "show_rootbuilder_conversion", ( @@ -455,22 +438,22 @@ def executables(self) -> list[mobase.ExecutableInfo]: game_dir = self.gameDirectory() bin_path = game_dir.absoluteFilePath(self.binaryName()) skip_start_screen = ( - "-skipStartScreen" if self._get_setting("skipStartScreen") else "" + " -skipStartScreen" if self._get_setting("skipStartScreen") else "" ) return [ # Default, runs REDmod deploy if necessary mobase.ExecutableInfo( - f"{game_name} (REDmod)", + f"{game_name}", bin_path, - ).withArgument(f"--launcher-skip -modded {skip_start_screen}"), + ).withArgument(f"--launcher-skip -modded{skip_start_screen}"), # Start game without REDmod mobase.ExecutableInfo( - f"{game_name}", + f"{game_name} - skip REDmod deploy", bin_path, ).withArgument(f"--launcher-skip {skip_start_screen}"), # Deploy REDmods only mobase.ExecutableInfo( - "REDmod", + "Manually deploy REDmod", self._get_redmod_binary(), ).withArgument("deploy -reportProgress -force %modlist%"), # Launcher @@ -504,7 +487,7 @@ def _onAboutToRun(self, app_path_str: str, wd: QDir, args: str) -> bool: _, ) = self._modlist_files.update_modlist("redmod") modlist_param = f'-modlist="{modlist_path}"' if modlist else "" - args = f"{args[: m.start()]}{modlist_param}{args[m.end() :]}" + args = f"{args[:m.start()]}{modlist_param}{args[m.end():]}" qInfo(f"Manual modlist deployment: replacing {m[0]}, new args = {args}") self._check_redmod_result( self._organizer.waitForApplication( @@ -527,35 +510,6 @@ def _onAboutToRun(self, app_path_str: str, wd: QDir, args: str) -> bool: self._modlist_files.update_modlist("archive") return True - def _onFinishedRun(self, path: str, exit_code: int) -> None: - if not self._get_setting("crash_message"): - return - if path.endswith(self.binaryName()) and exit_code > 0: - crash_message = QMessageBox( - QMessageBox.Icon.Critical, - "Cyberpunk Crashed", - textwrap.dedent( - f""" - Cyberpunk crashed. Tips: - - disable mods (create backup of modlist or use new profile) - - clear overwrite or delete at least overwrite/r6/cache (to keep mod settings) - - check log files of CET/redscript/RED4ext (in overwrite) - - read [FAQ & Troubleshooting]({self.GameSupportURL}#faq--troubleshooting) - """ - ), - QMessageBox.StandardButton.Ok, - self._parentWidget, - ) - crash_message.setTextFormat(Qt.TextFormat.MarkdownText) - hide_cb = QCheckBox("&Do not show again*", crash_message) - hide_cb.setToolTip(f"Settings/Plugins/{self.name()}/crash_message") - crash_message.setCheckBox(hide_cb) - crash_message.open( # type: ignore - lambda: ( - hide_cb.isChecked() and self._set_setting("crash_message", False) - ) - ) - def _check_redmod_result(self, result: tuple[bool, int]) -> bool: if result == (True, 0): return True @@ -649,25 +603,46 @@ def _is_cache_file_updated(self, file: Path, data_path: Path) -> bool: Args: file: Relative to data dir. """ + game_file = data_path.absolute() / file + mapped_files = self._organizer.findFiles(file.parent, file.name) + + # guard against missing mapped files early to avoid index/access issues + if not mapped_files: + return False + + mapped_file = Path(mapped_files[0]) + + # ensure both paths exist before any filesystem comparisons + # (prevents pathlib.samefile / filecmp from raising FileNotFoundError) + if not game_file.exists() or not mapped_file.exists(): + return False + return bool( - mapped_files - and (mapped_file := mapped_files[0]) + mapped_file and not ( + # samefile is only safe after existence checks game_file.samefile(mapped_file) + + # file comparison only executed when both files exist or filecmp.cmp(game_file, mapped_file) + or ( # different backup file ( backup_files := self._organizer.findFiles( file.parent, f"{file.name}.bk" ) ) - and filecmp.cmp(game_file, backup_files[0]) + and ( + # validate backup exists before comparison + Path(backup_files[0]).exists() + and game_file.exists() + and filecmp.cmp(game_file, backup_files[0]) + ) ) ) ) - def _unmapped_cache_files(self, data_path: Path) -> Iterable[Path]: """Yields unmapped cache files relative to `data_path`.""" for file in self._organizer.findFiles("r6/cache", "*"): From 69a476ed2ddc916a5bbacaf355f9487b4506545a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:53:58 +0000 Subject: [PATCH 2/2] [pre-commit.ci] Auto fixes from pre-commit.com hooks. --- games/game_cyberpunk2077.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/games/game_cyberpunk2077.py b/games/game_cyberpunk2077.py index 9805b89..8eb0151 100644 --- a/games/game_cyberpunk2077.py +++ b/games/game_cyberpunk2077.py @@ -10,7 +10,6 @@ from pathlib import Path from typing import Any, Literal, TypeVar -import mobase from PyQt6.QtCore import QDateTime, QDir, Qt, qCritical, qInfo, qWarning from PyQt6.QtWidgets import ( QCheckBox, @@ -20,6 +19,8 @@ QWidget, ) +import mobase + from ..basic_features import BasicLocalSavegames, BasicModDataChecker, GlobPatterns from ..basic_features.basic_save_game_info import ( BasicGameSaveGame, @@ -74,7 +75,7 @@ def parse_cyberpunk_save_metadata(save_path: Path, save: mobase.ISaveGame): "Street Cred": int(meta_data["streetCred"]), "Life Path": meta_data["lifePath"], "Difficulty": meta_data["difficulty"], - "Gender": f'{meta_data["bodyGender"]} / {meta_data["brainGender"]}', + "Gender": f"{meta_data['bodyGender']} / {meta_data['brainGender']}", "Game version": meta_data["buildPatch"], } except (FileNotFoundError, json.JSONDecodeError): @@ -487,7 +488,7 @@ def _onAboutToRun(self, app_path_str: str, wd: QDir, args: str) -> bool: _, ) = self._modlist_files.update_modlist("redmod") modlist_param = f'-modlist="{modlist_path}"' if modlist else "" - args = f"{args[:m.start()]}{modlist_param}{args[m.end():]}" + args = f"{args[: m.start()]}{modlist_param}{args[m.end() :]}" qInfo(f"Manual modlist deployment: replacing {m[0]}, new args = {args}") self._check_redmod_result( self._organizer.waitForApplication( @@ -624,10 +625,8 @@ def _is_cache_file_updated(self, file: Path, data_path: Path) -> bool: and not ( # samefile is only safe after existence checks game_file.samefile(mapped_file) - # file comparison only executed when both files exist or filecmp.cmp(game_file, mapped_file) - or ( # different backup file ( backup_files := self._organizer.findFiles( @@ -643,6 +642,7 @@ def _is_cache_file_updated(self, file: Path, data_path: Path) -> bool: ) ) ) + def _unmapped_cache_files(self, data_path: Path) -> Iterable[Path]: """Yields unmapped cache files relative to `data_path`.""" for file in self._organizer.findFiles("r6/cache", "*"):