1010from pathlib import Path
1111from typing import Any , Literal , TypeVar
1212
13+ import mobase
1314from PyQt6 .QtCore import QDateTime , QDir , Qt , qCritical , qInfo , qWarning
1415from PyQt6 .QtWidgets import (
1516 QCheckBox ,
1920 QWidget ,
2021)
2122
22- import mobase
23-
2423from ..basic_features import BasicLocalSavegames , BasicModDataChecker , GlobPatterns
2524from ..basic_features .basic_save_game_info import (
2625 BasicGameSaveGame ,
@@ -34,16 +33,6 @@ class CyberpunkModDataChecker(BasicModDataChecker):
3433 def __init__ (self ):
3534 super ().__init__ (
3635 GlobPatterns (
37- delete = [
38- "*.gif" ,
39- "*.jpg" ,
40- "*.jpeg" ,
41- "*.jxl" ,
42- "*.md" ,
43- "*.png" ,
44- "*.txt" ,
45- "*.webp" ,
46- ],
4736 move = {
4837 # archive and ArchiveXL
4938 "*.archive" : "archive/pc/mod/" ,
@@ -85,7 +74,7 @@ def parse_cyberpunk_save_metadata(save_path: Path, save: mobase.ISaveGame):
8574 "Street Cred" : int (meta_data ["streetCred" ]),
8675 "Life Path" : meta_data ["lifePath" ],
8776 "Difficulty" : meta_data ["difficulty" ],
88- "Gender" : f" { meta_data [' bodyGender' ]} / { meta_data [' brainGender' ] } " ,
77+ "Gender" : f' { meta_data [" bodyGender" ]} / { meta_data [" brainGender" ] } ' ,
8978 "Game version" : meta_data ["buildPatch" ],
9079 }
9180 except (FileNotFoundError , json .JSONDecodeError ):
@@ -200,7 +189,7 @@ def active_mod_paths(self, reverse: bool = False) -> Iterable[Path]:
200189class Cyberpunk2077Game (BasicGame ):
201190 Name = "Cyberpunk 2077 Support Plugin"
202191 Author = "6788, Zash"
203- Version = "3.0.1 "
192+ Version = "3.0.0 "
204193
205194 GameName = "Cyberpunk 2077"
206195 GameShortName = "cyberpunk2077"
@@ -233,7 +222,7 @@ class Cyberpunk2077Game(BasicGame):
233222
234223 def init (self , organizer : mobase .IOrganizer ) -> bool :
235224 super ().init (organizer )
236- self ._register_feature (BasicLocalSavegames (self ))
225+ self ._register_feature (BasicLocalSavegames (self . savesDirectory () ))
237226 self ._register_feature (
238227 BasicGameSaveGameInfo (
239228 lambda p : Path (p or "" , "screenshot.png" ),
@@ -256,7 +245,6 @@ def init(self, organizer: mobase.IOrganizer) -> bool:
256245 ),
257246 )
258247 organizer .onAboutToRun (self ._onAboutToRun )
259- organizer .onFinishedRun (self ._onFinishedRun )
260248 organizer .onPluginSettingChanged (self ._on_settings_changed )
261249 organizer .modList ().onModInstalled (self ._check_disable_crashreporter )
262250 organizer .onUserInterfaceInitialized (self ._on_user_interface_initialized )
@@ -429,11 +417,6 @@ def settings(self) -> list[mobase.PluginSetting]:
429417 ),
430418 True ,
431419 ),
432- mobase .PluginSetting (
433- "crash_message" ,
434- ("Show a crash message as replacement of disabled CrashReporter" ),
435- True ,
436- ),
437420 mobase .PluginSetting (
438421 "show_rootbuilder_conversion" ,
439422 (
@@ -455,22 +438,22 @@ def executables(self) -> list[mobase.ExecutableInfo]:
455438 game_dir = self .gameDirectory ()
456439 bin_path = game_dir .absoluteFilePath (self .binaryName ())
457440 skip_start_screen = (
458- "-skipStartScreen" if self ._get_setting ("skipStartScreen" ) else ""
441+ " -skipStartScreen" if self ._get_setting ("skipStartScreen" ) else ""
459442 )
460443 return [
461444 # Default, runs REDmod deploy if necessary
462445 mobase .ExecutableInfo (
463- f"{ game_name } (REDmod) " ,
446+ f"{ game_name } " ,
464447 bin_path ,
465- ).withArgument (f"--launcher-skip -modded { skip_start_screen } " ),
448+ ).withArgument (f"--launcher-skip -modded{ skip_start_screen } " ),
466449 # Start game without REDmod
467450 mobase .ExecutableInfo (
468- f"{ game_name } " ,
451+ f"{ game_name } - skip REDmod deploy " ,
469452 bin_path ,
470453 ).withArgument (f"--launcher-skip { skip_start_screen } " ),
471454 # Deploy REDmods only
472455 mobase .ExecutableInfo (
473- "REDmod" ,
456+ "Manually deploy REDmod" ,
474457 self ._get_redmod_binary (),
475458 ).withArgument ("deploy -reportProgress -force %modlist%" ),
476459 # Launcher
@@ -504,7 +487,7 @@ def _onAboutToRun(self, app_path_str: str, wd: QDir, args: str) -> bool:
504487 _ ,
505488 ) = self ._modlist_files .update_modlist ("redmod" )
506489 modlist_param = f'-modlist="{ modlist_path } "' if modlist else ""
507- args = f"{ args [: m .start ()]} { modlist_param } { args [m .end () :]} "
490+ args = f"{ args [:m .start ()]} { modlist_param } { args [m .end ():]} "
508491 qInfo (f"Manual modlist deployment: replacing { m [0 ]} , new args = { args } " )
509492 self ._check_redmod_result (
510493 self ._organizer .waitForApplication (
@@ -527,35 +510,6 @@ def _onAboutToRun(self, app_path_str: str, wd: QDir, args: str) -> bool:
527510 self ._modlist_files .update_modlist ("archive" )
528511 return True
529512
530- def _onFinishedRun (self , path : str , exit_code : int ) -> None :
531- if not self ._get_setting ("crash_message" ):
532- return
533- if path .endswith (self .binaryName ()) and exit_code > 0 :
534- crash_message = QMessageBox (
535- QMessageBox .Icon .Critical ,
536- "Cyberpunk Crashed" ,
537- textwrap .dedent (
538- f"""
539- Cyberpunk crashed. Tips:
540- - disable mods (create backup of modlist or use new profile)
541- - clear overwrite or delete at least overwrite/r6/cache (to keep mod settings)
542- - check log files of CET/redscript/RED4ext (in overwrite)
543- - read [FAQ & Troubleshooting]({ self .GameSupportURL } #faq--troubleshooting)
544- """
545- ),
546- QMessageBox .StandardButton .Ok ,
547- self ._parentWidget ,
548- )
549- crash_message .setTextFormat (Qt .TextFormat .MarkdownText )
550- hide_cb = QCheckBox ("&Do not show again*" , crash_message )
551- hide_cb .setToolTip (f"Settings/Plugins/{ self .name ()} /crash_message" )
552- crash_message .setCheckBox (hide_cb )
553- crash_message .open ( # type: ignore
554- lambda : (
555- hide_cb .isChecked () and self ._set_setting ("crash_message" , False )
556- )
557- )
558-
559513 def _check_redmod_result (self , result : tuple [bool , int ]) -> bool :
560514 if result == (True , 0 ):
561515 return True
@@ -649,25 +603,46 @@ def _is_cache_file_updated(self, file: Path, data_path: Path) -> bool:
649603 Args:
650604 file: Relative to data dir.
651605 """
606+
652607 game_file = data_path .absolute () / file
608+
653609 mapped_files = self ._organizer .findFiles (file .parent , file .name )
610+
611+ # guard against missing mapped files early to avoid index/access issues
612+ if not mapped_files :
613+ return False
614+
615+ mapped_file = Path (mapped_files [0 ])
616+
617+ # ensure both paths exist before any filesystem comparisons
618+ # (prevents pathlib.samefile / filecmp from raising FileNotFoundError)
619+ if not game_file .exists () or not mapped_file .exists ():
620+ return False
621+
654622 return bool (
655- mapped_files
656- and (mapped_file := mapped_files [0 ])
623+ mapped_file
657624 and not (
625+ # samefile is only safe after existence checks
658626 game_file .samefile (mapped_file )
627+
628+ # file comparison only executed when both files exist
659629 or filecmp .cmp (game_file , mapped_file )
630+
660631 or ( # different backup file
661632 (
662633 backup_files := self ._organizer .findFiles (
663634 file .parent , f"{ file .name } .bk"
664635 )
665636 )
666- and filecmp .cmp (game_file , backup_files [0 ])
637+ and (
638+ # validate backup exists before comparison
639+ Path (backup_files [0 ]).exists ()
640+ and game_file .exists ()
641+ and filecmp .cmp (game_file , backup_files [0 ])
642+ )
667643 )
668644 )
669645 )
670-
671646 def _unmapped_cache_files (self , data_path : Path ) -> Iterable [Path ]:
672647 """Yields unmapped cache files relative to `data_path`."""
673648 for file in self ._organizer .findFiles ("r6/cache" , "*" ):
0 commit comments