33import re
44import shutil
55from collections import Counter
6- from collections .abc import Iterable , Mapping
6+ from collections .abc import Iterable
77from dataclasses import dataclass
88from pathlib import Path
9- from typing import Any , Literal , TypeVar
9+ from typing import Literal , TypeVar
1010
1111import mobase
1212from PyQt6 .QtCore import QDateTime , QDir , qCritical , qInfo , qWarning
1717 BasicGameSaveGameInfo ,
1818 format_date ,
1919)
20- from ..basic_features .utils import is_directory
2120from ..basic_game import BasicGame
2221
2322
@@ -36,103 +35,12 @@ def __init__(self):
3635 "engine" ,
3736 "r6" ,
3837 "mods" , # RedMod
39- "red4ext" , # red4ext/RED4ext.dll is moved to root in .fix()
38+ "red4ext" ,
4039 "bin" , # CET etc. gets handled below
41- "root" , # RootBuilder: hardlink / copy to game root
4240 ],
4341 )
4442 )
4543
46- _extra_files_to_move = {
47- # Red4ext: only .dll files
48- "red4ext/RED4ext.dll" : "root/red4ext/" ,
49- "bin/x64/winmm.dll" : "root/bin/x64/" ,
50- # CET: all files, folder gets handled in .fix()
51- "bin/x64/version.dll" : "root/bin/x64/" ,
52- "bin/x64/global.ini" : "root/bin/x64/" ,
53- "bin/x64/plugins/cyber_engine_tweaks.asi" : "root/bin/x64/plugins/" ,
54- }
55- _cet_path = "bin/x64/plugins/cyber_engine_tweaks/"
56-
57- def dataLooksValid (
58- self , filetree : mobase .IFileTree
59- ) -> mobase .ModDataChecker .CheckReturn :
60- # fix: single root folders get traversed by Simple Installer
61- parent = filetree .parent ()
62- if parent is not None and self .dataLooksValid (parent ) is self .FIXABLE :
63- return self .FIXABLE
64- status = mobase .ModDataChecker .INVALID
65- # Check extra fixes
66- if any (filetree .exists (p ) for p in self ._extra_files_to_move ):
67- return mobase .ModDataChecker .FIXABLE
68- rp = self ._regex_patterns
69- for entry in filetree :
70- name = entry .name ().casefold ()
71- if rp .move_match (name ) is not None :
72- status = mobase .ModDataChecker .FIXABLE
73- elif rp .valid .match (name ):
74- if status is mobase .ModDataChecker .INVALID :
75- status = mobase .ModDataChecker .VALID
76- elif self ._valid_redmod (entry ):
77- # Archive with REDmod folders, not in mods/
78- status = mobase .ModDataChecker .FIXABLE
79- # Accept any other entry
80- return status
81-
82- def _valid_redmod (self , filetree : mobase .IFileTree | mobase .FileTreeEntry ) -> bool :
83- return isinstance (filetree , mobase .IFileTree ) and bool (
84- filetree and filetree .find ("info.json" )
85- )
86-
87- def fix (self , filetree : mobase .IFileTree ) -> mobase .IFileTree :
88- for source , target in self ._extra_files_to_move .items ():
89- if file := filetree .find (source ):
90- parent = file .parent ()
91- filetree .move (file , target )
92- clear_empty_folder (parent )
93- if filetree := super ().fix (filetree ):
94- filetree = self ._fix_cet_framework (filetree )
95- # REDmod
96- for entry in list (filetree ):
97- if not self ._regex_patterns .valid .match (
98- entry .name ().casefold ()
99- ) and self ._valid_redmod (entry ):
100- filetree .move (entry , "mods/" )
101- return filetree
102-
103- def _fix_cet_framework (self , filetree : mobase .IFileTree ) -> mobase .IFileTree :
104- """Move CET framework to `root/`, except for `mods`.
105- Only CET >= v1.27.0 (Patch 2.01) works with USVFS.
106-
107- See: https://github.com/maximegmd/CyberEngineTweaks/pull/877
108- """
109- if cet_folder := filetree .find (
110- self ._cet_path , mobase .FileTreeEntry .FileTypes .DIRECTORY
111- ):
112- assert is_directory (cet_folder )
113- root_cet_path = f"root/{ self ._cet_path } "
114- if not cet_folder .exists ("mods" ):
115- parent = cet_folder .parent ()
116- filetree .move (cet_folder , root_cet_path .rstrip ("/\\ " ))
117- else :
118- parent = cet_folder
119- for entry in list (cet_folder ):
120- if entry .name () != "mods" :
121- filetree .move (entry , root_cet_path )
122- clear_empty_folder (parent )
123- return filetree
124-
125-
126- def clear_empty_folder (filetree : mobase .IFileTree | None ):
127- if filetree is None :
128- return
129- while not filetree :
130- parent = filetree .parent ()
131- filetree .detach ()
132- if parent is None :
133- break
134- filetree = parent
135-
13644
13745def time_from_seconds (s : int | float ) -> str :
13846 m , s = divmod (int (s ), 60 )
@@ -269,27 +177,10 @@ def active_mod_paths(self, reverse: bool = False) -> Iterable[Path]:
269177 yield mods_path / mod
270178
271179
272- @dataclass
273- class PluginDefaultSettings :
274- organizer : mobase .IOrganizer
275- plugin_name : str
276- settings : Mapping [str , mobase .MoVariant ]
277-
278- def is_plugin_enabled (self ) -> bool :
279- return self .organizer .isPluginEnabled (self .plugin_name )
280-
281- def apply (self ) -> bool :
282- if not self .is_plugin_enabled ():
283- return False
284- for setting , value in self .settings .items ():
285- self .organizer .setPluginSetting (self .plugin_name , setting , value )
286- return True
287-
288-
289180class Cyberpunk2077Game (BasicGame ):
290181 Name = "Cyberpunk 2077 Support Plugin"
291182 Author = "6788, Zash"
292- Version = "2.3.1 "
183+ Version = "3.0.0 "
293184
294185 GameName = "Cyberpunk 2077"
295186 GameShortName = "cyberpunk2077"
@@ -306,6 +197,9 @@ class Cyberpunk2077Game(BasicGame):
306197 "Game:-Cyberpunk-2077"
307198 )
308199
200+ # CET and RED4ext, relative to Cyberpunk2077.exe
201+ _forced_libraries = ["version.dll" , "winmm.dll" ]
202+
309203 _redmod_binary = Path ("tools/redmod/bin/redMod.exe" )
310204 _redmod_log = Path ("tools/redmod/bin/REDmodLog.txt" )
311205 _redmod_deploy_path = Path ("r6/cache/modded/" )
@@ -336,37 +230,7 @@ def init(self, organizer: mobase.IOrganizer) -> bool:
336230 reversed_priority = bool (self ._get_setting ("reverse_redmod_load_order" )),
337231 ),
338232 )
339- self ._rootbuilder_settings = PluginDefaultSettings (
340- organizer ,
341- "RootBuilder" ,
342- {
343- "usvfsmode" : False ,
344- "linkmode" : False ,
345- # Available with RootBuilder v4.5+
346- # Currently bugged / incompatible with MO 2.5.2 (Python 3.12)
347- # https://github.com/Kezyma/ModOrganizer-Plugins/issues/36
348- "linkonlymode" : False ,
349- "backup" : True ,
350- "cache" : True ,
351- "autobuild" : True ,
352- "redirect" : False ,
353- "installer" : False ,
354- "exclusions" : "archive,setup_redlauncher.exe,tools" ,
355- "linkextensions" : "dll,exe" ,
356- },
357- )
358-
359- def apply_rootbuilder_settings_once (* args : Any ):
360- if not self .isActive () or not self ._get_setting ("configure_RootBuilder" ):
361- return
362- if self ._rootbuilder_settings .apply ():
363- qInfo (f"RootBuilder configured for { self .gameName ()} " )
364- self ._set_setting ("configure_RootBuilder" , False )
365-
366- organizer .onUserInterfaceInitialized (apply_rootbuilder_settings_once )
367- organizer .onPluginEnabled ("RootBuilder" , apply_rootbuilder_settings_once )
368233 organizer .onAboutToRun (self ._onAboutToRun )
369-
370234 organizer .onPluginSettingChanged (self ._on_settings_changed )
371235 return True
372236
@@ -446,11 +310,6 @@ def settings(self) -> list[mobase.PluginSetting]:
446310 ),
447311 True ,
448312 ),
449- mobase .PluginSetting (
450- "configure_RootBuilder" ,
451- "Configures RootBuilder for Cyberpunk if installed and enabled" ,
452- True ,
453- ),
454313 ]
455314
456315 def _get_setting (self , key : str ) -> mobase .MoVariant :
@@ -489,6 +348,13 @@ def executables(self) -> list[mobase.ExecutableInfo]:
489348 ).withArgument (f"{ skip_start_screen } " ),
490349 ]
491350
351+ def executableForcedLoads (self ) -> list [mobase .ExecutableForcedLoadSetting ]:
352+ exe = Path (self .binaryName ()).name
353+ return [
354+ mobase .ExecutableForcedLoadSetting (exe , lib ).withEnabled (True )
355+ for lib in self ._forced_libraries
356+ ]
357+
492358 def _get_redmod_binary (self ) -> Path :
493359 """Absolute path to redmod binary"""
494360 return Path (self .gameDirectory ().absolutePath (), self ._redmod_binary )
0 commit comments