diff --git a/CHANGELOG.md b/CHANGELOG.md index 318e91d..b946dbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Jackify Changelog +## v0.1.6.2 - Minor Bug Fixes +**Release Date:** October 23, 2025 + +### Bug Fixes +- **Improved dotnet4.x Compatibility**: Universal registry fixes for better modlist compatibility +- **Fixed Proton 9 Override**: A bug meant that modlists with spaces in the name weren't being overridden correctly +- **Removed PageFileManager Plugin**: Eliminates Linux PageFile warnings + +--- + ## v0.1.6.1 - Fix dotnet40 install and expand Game Proton override **Release Date:** October 21, 2025 diff --git a/jackify/__init__.py b/jackify/__init__.py index 249e5f3..394fb26 100644 --- a/jackify/__init__.py +++ b/jackify/__init__.py @@ -5,4 +5,4 @@ This package provides both CLI and GUI interfaces for managing Wabbajack modlists natively on Linux systems. """ -__version__ = "0.1.6.1" +__version__ = "0.1.6.2" diff --git a/jackify/backend/handlers/modlist_handler.py b/jackify/backend/handlers/modlist_handler.py index 3ee149f..9ad60a1 100644 --- a/jackify/backend/handlers/modlist_handler.py +++ b/jackify/backend/handlers/modlist_handler.py @@ -71,16 +71,19 @@ class ModlistHandler: } # Canonical mapping of modlist-specific Wine components (from omni-guides.sh) + # NOTE: dotnet4.x components disabled in v0.1.6.2 - replaced with universal registry fixes MODLIST_WINE_COMPONENTS = { - "wildlander": ["dotnet472"], - "librum": ["dotnet40", "dotnet8"], - "apostasy": ["dotnet40", "dotnet8"], - "nordicsouls": ["dotnet40"], - "livingskyrim": ["dotnet40"], - "lsiv": ["dotnet40"], - "ls4": ["dotnet40"], - "lorerim": ["dotnet40"], - "lostlegacy": ["dotnet40"], + # "wildlander": ["dotnet472"], # DISABLED: Universal registry fixes replace dotnet472 installation + # "librum": ["dotnet40", "dotnet8"], # PARTIAL DISABLE: Keep dotnet8, remove dotnet40 + "librum": ["dotnet8"], # dotnet40 replaced with universal registry fixes + # "apostasy": ["dotnet40", "dotnet8"], # PARTIAL DISABLE: Keep dotnet8, remove dotnet40 + "apostasy": ["dotnet8"], # dotnet40 replaced with universal registry fixes + # "nordicsouls": ["dotnet40"], # DISABLED: Universal registry fixes replace dotnet40 installation + # "livingskyrim": ["dotnet40"], # DISABLED: Universal registry fixes replace dotnet40 installation + # "lsiv": ["dotnet40"], # DISABLED: Universal registry fixes replace dotnet40 installation + # "ls4": ["dotnet40"], # DISABLED: Universal registry fixes replace dotnet40 installation + # "lorerim": ["dotnet40"], # DISABLED: Universal registry fixes replace dotnet40 installation + # "lostlegacy": ["dotnet40"], # DISABLED: Universal registry fixes replace dotnet40 installation } def __init__(self, steam_path_or_config: Union[Dict, str, Path, None] = None, @@ -669,6 +672,12 @@ class ModlistHandler: return False self.logger.info("Step 3: Curated user.reg.modlist and system.reg.modlist applied successfully.") + # Step 3.5: Apply universal dotnet4.x compatibility registry fixes + if status_callback: + status_callback(f"{self._get_progress_timestamp()} Applying universal dotnet4.x compatibility fixes") + self.logger.info("Step 3.5: Applying universal dotnet4.x compatibility registry fixes...") + self._apply_universal_dotnet_fixes() + # Step 4: Install Wine Components if status_callback: status_callback(f"{self._get_progress_timestamp()} Installing Wine components (this may take a while)") @@ -871,21 +880,38 @@ class ModlistHandler: print("Warning: Failed to create dxvk.conf file.") self.logger.info("Step 10: Creating dxvk.conf... Done") - # Step 11a: Small Tasks - Delete Plugin + # Step 11a: Small Tasks - Delete Incompatible Plugins if status_callback: - status_callback(f"{self._get_progress_timestamp()} Deleting incompatible MO2 plugin") - self.logger.info("Step 11a: Deleting incompatible MO2 plugin (FixGameRegKey.py)...") - plugin_path = Path(self.modlist_dir) / "plugins" / "FixGameRegKey.py" - if plugin_path.exists(): + status_callback(f"{self._get_progress_timestamp()} Deleting incompatible MO2 plugins") + self.logger.info("Step 11a: Deleting incompatible MO2 plugins...") + + # Delete FixGameRegKey.py plugin + fixgamereg_path = Path(self.modlist_dir) / "plugins" / "FixGameRegKey.py" + if fixgamereg_path.exists(): try: - plugin_path.unlink() + fixgamereg_path.unlink() self.logger.info("FixGameRegKey.py plugin deleted successfully.") except Exception as e: self.logger.warning(f"Failed to delete FixGameRegKey.py plugin: {e}") - print("Warning: Failed to delete incompatible plugin file.") + print("Warning: Failed to delete FixGameRegKey.py plugin file.") else: self.logger.debug("FixGameRegKey.py plugin not found (this is normal).") - self.logger.info("Step 11a: Plugin deletion check complete.") + + # Delete PageFileManager plugin directory (Linux has no PageFile) + pagefilemgr_path = Path(self.modlist_dir) / "plugins" / "PageFileManager" + if pagefilemgr_path.exists(): + try: + import shutil + shutil.rmtree(pagefilemgr_path) + self.logger.info("PageFileManager plugin directory deleted successfully.") + except Exception as e: + self.logger.warning(f"Failed to delete PageFileManager plugin directory: {e}") + print("Warning: Failed to delete PageFileManager plugin directory.") + else: + self.logger.debug("PageFileManager plugin not found (this is normal).") + + self.logger.info("Step 11a: Incompatible plugin deletion check complete.") + # Step 11b: Download Font if status_callback: @@ -1461,4 +1487,103 @@ class ModlistHandler: self.logger.error(f"Error handling symlinked downloads: {e}", exc_info=True) return False -# (Ensure EOF is clean and no extra incorrect methods exist below) \ No newline at end of file + def _apply_universal_dotnet_fixes(self): + """Apply universal dotnet4.x compatibility registry fixes to ALL modlists""" + try: + prefix_path = os.path.join(str(self.compat_data_path), "pfx") + if not os.path.exists(prefix_path): + self.logger.warning(f"Prefix path not found: {prefix_path}") + return False + + self.logger.info("Applying universal dotnet4.x compatibility registry fixes...") + + # Find the appropriate Wine binary to use for registry operations + wine_binary = self._find_wine_binary_for_registry() + if not wine_binary: + self.logger.error("Could not find Wine binary for registry operations") + return False + + # Set environment for Wine registry operations + env = os.environ.copy() + env['WINEPREFIX'] = prefix_path + env['WINEDEBUG'] = '-all' # Suppress Wine debug output + + # Registry fix 1: Set mscoree=native DLL override + # This tells Wine to use native .NET runtime instead of Wine's implementation + self.logger.debug("Setting mscoree=native DLL override...") + cmd1 = [ + wine_binary, 'reg', 'add', + 'HKEY_CURRENT_USER\\Software\\Wine\\DllOverrides', + '/v', 'mscoree', '/t', 'REG_SZ', '/d', 'native', '/f' + ] + + result1 = subprocess.run(cmd1, env=env, capture_output=True, text=True) + if result1.returncode == 0: + self.logger.info("Successfully applied mscoree=native DLL override") + else: + self.logger.warning(f"Failed to set mscoree DLL override: {result1.stderr}") + + # Registry fix 2: Set OnlyUseLatestCLR=1 + # This prevents .NET version conflicts by using the latest CLR + self.logger.debug("Setting OnlyUseLatestCLR=1 registry entry...") + cmd2 = [ + wine_binary, 'reg', 'add', + 'HKEY_LOCAL_MACHINE\\Software\\Microsoft\\.NETFramework', + '/v', 'OnlyUseLatestCLR', '/t', 'REG_DWORD', '/d', '1', '/f' + ] + + result2 = subprocess.run(cmd2, env=env, capture_output=True, text=True) + if result2.returncode == 0: + self.logger.info("Successfully applied OnlyUseLatestCLR=1 registry entry") + else: + self.logger.warning(f"Failed to set OnlyUseLatestCLR: {result2.stderr}") + + # Both fixes applied - this should eliminate dotnet4.x installation requirements + if result1.returncode == 0 and result2.returncode == 0: + self.logger.info("Universal dotnet4.x compatibility fixes applied successfully") + return True + else: + self.logger.warning("Some dotnet4.x registry fixes failed, but continuing...") + return False + + except Exception as e: + self.logger.error(f"Failed to apply universal dotnet4.x fixes: {e}") + return False + + def _find_wine_binary_for_registry(self) -> Optional[str]: + """Find the appropriate Wine binary for registry operations""" + try: + # Method 1: Try to detect from Steam's config or use Proton from compat data + # Look for wine binary in common Proton locations + proton_paths = [ + os.path.expanduser("~/.local/share/Steam/compatibilitytools.d"), + os.path.expanduser("~/.steam/steam/steamapps/common") + ] + + for base_path in proton_paths: + if os.path.exists(base_path): + for item in os.listdir(base_path): + if 'proton' in item.lower(): + wine_path = os.path.join(base_path, item, 'files', 'bin', 'wine') + if os.path.exists(wine_path): + self.logger.debug(f"Found Wine binary: {wine_path}") + return wine_path + + # Method 2: Fallback to system wine if available + try: + result = subprocess.run(['which', 'wine'], capture_output=True, text=True) + if result.returncode == 0: + wine_path = result.stdout.strip() + self.logger.debug(f"Using system Wine binary: {wine_path}") + return wine_path + except Exception: + pass + + self.logger.error("No suitable Wine binary found for registry operations") + return None + + except Exception as e: + self.logger.error(f"Error finding Wine binary: {e}") + return None + + \ No newline at end of file diff --git a/jackify/backend/handlers/winetricks_handler.py b/jackify/backend/handlers/winetricks_handler.py index f5b9ab9..0400140 100644 --- a/jackify/backend/handlers/winetricks_handler.py +++ b/jackify/backend/handlers/winetricks_handler.py @@ -263,13 +263,15 @@ class WinetricksHandler: use_winetricks = config_handler.get('use_winetricks_for_components', True) # Legacy .NET Framework versions that are problematic in Wine/Proton - legacy_dotnet_versions = ['dotnet40', 'dotnet472', 'dotnet48'] + # DISABLED in v0.1.6.2: Universal registry fixes replace dotnet4.x installation + # legacy_dotnet_versions = ['dotnet40', 'dotnet472', 'dotnet48'] + legacy_dotnet_versions = [] # ALL dotnet4.x versions disabled - universal registry fixes handle compatibility # Check if any legacy .NET Framework versions are present has_legacy_dotnet = any(comp in components_to_install for comp in legacy_dotnet_versions) # Choose installation method based on user preference and components - # ALWAYS use hybrid approach when legacy .NET Framework versions are present + # HYBRID APPROACH MOSTLY DISABLED: dotnet40/dotnet472 replaced with universal registry fixes if has_legacy_dotnet: legacy_found = [comp for comp in legacy_dotnet_versions if comp in components_to_install] self.logger.info(f"Using hybrid approach: protontricks for legacy .NET versions {legacy_found} (reliable), {'winetricks' if use_winetricks else 'protontricks'} for other components") diff --git a/jackify/backend/services/automated_prefix_service.py b/jackify/backend/services/automated_prefix_service.py index cfc2111..dd29e49 100644 --- a/jackify/backend/services/automated_prefix_service.py +++ b/jackify/backend/services/automated_prefix_service.py @@ -50,7 +50,8 @@ class AutomatedPrefixService: from jackify.backend.handlers.wine_utils import WineUtils # Check for Lorerim-specific Proton override first - if modlist_name and modlist_name.lower() == 'lorerim': + modlist_normalized = modlist_name.lower().replace(" ", "") if modlist_name else "" + if modlist_normalized == 'lorerim': lorerim_proton = self._get_lorerim_preferred_proton() if lorerim_proton: logger.info(f"Lorerim detected: Using {lorerim_proton} instead of user settings") @@ -58,7 +59,7 @@ class AutomatedPrefixService: return lorerim_proton # Check for Lost Legacy-specific Proton override (needs Proton 9 for ENB compatibility) - if modlist_name and modlist_name.lower() == 'lostlegacy': + if modlist_normalized == 'lostlegacy': lostlegacy_proton = self._get_lorerim_preferred_proton() # Use same logic as Lorerim if lostlegacy_proton: logger.info(f"Lost Legacy detected: Using {lostlegacy_proton} instead of user settings (ENB compatibility)") @@ -2980,14 +2981,115 @@ echo Prefix creation complete. logger.error(f"Failed to update registry path: {e}") return False + def _apply_universal_dotnet_fixes(self, modlist_compatdata_path: str): + """Apply universal dotnet4.x compatibility registry fixes to ALL modlists""" + try: + prefix_path = os.path.join(modlist_compatdata_path, "pfx") + if not os.path.exists(prefix_path): + logger.warning(f"Prefix path not found: {prefix_path}") + return False + + logger.info("Applying universal dotnet4.x compatibility registry fixes...") + + # Find the appropriate Wine binary to use for registry operations + wine_binary = self._find_wine_binary_for_registry(modlist_compatdata_path) + if not wine_binary: + logger.error("Could not find Wine binary for registry operations") + return False + + # Set environment for Wine registry operations + env = os.environ.copy() + env['WINEPREFIX'] = prefix_path + env['WINEDEBUG'] = '-all' # Suppress Wine debug output + + # Registry fix 1: Set mscoree=native DLL override + # This tells Wine to use native .NET runtime instead of Wine's implementation + logger.debug("Setting mscoree=native DLL override...") + cmd1 = [ + wine_binary, 'reg', 'add', + 'HKEY_CURRENT_USER\\Software\\Wine\\DllOverrides', + '/v', 'mscoree', '/t', 'REG_SZ', '/d', 'native', '/f' + ] + + result1 = subprocess.run(cmd1, env=env, capture_output=True, text=True) + if result1.returncode == 0: + logger.info("Successfully applied mscoree=native DLL override") + else: + logger.warning(f"Failed to set mscoree DLL override: {result1.stderr}") + + # Registry fix 2: Set OnlyUseLatestCLR=1 + # This prevents .NET version conflicts by using the latest CLR + logger.debug("Setting OnlyUseLatestCLR=1 registry entry...") + cmd2 = [ + wine_binary, 'reg', 'add', + 'HKEY_LOCAL_MACHINE\\Software\\Microsoft\\.NETFramework', + '/v', 'OnlyUseLatestCLR', '/t', 'REG_DWORD', '/d', '1', '/f' + ] + + result2 = subprocess.run(cmd2, env=env, capture_output=True, text=True) + if result2.returncode == 0: + logger.info("Successfully applied OnlyUseLatestCLR=1 registry entry") + else: + logger.warning(f"Failed to set OnlyUseLatestCLR: {result2.stderr}") + + # Both fixes applied - this should eliminate dotnet4.x installation requirements + if result1.returncode == 0 and result2.returncode == 0: + logger.info("Universal dotnet4.x compatibility fixes applied successfully") + return True + else: + logger.warning("Some dotnet4.x registry fixes failed, but continuing...") + return False + + except Exception as e: + logger.error(f"Failed to apply universal dotnet4.x fixes: {e}") + return False + + def _find_wine_binary_for_registry(self, modlist_compatdata_path: str) -> Optional[str]: + """Find the appropriate Wine binary for registry operations""" + try: + # Method 1: Try to detect from Steam's config or use Proton from compat data + # Look for wine binary in common Proton locations + proton_paths = [ + os.path.expanduser("~/.local/share/Steam/compatibilitytools.d"), + os.path.expanduser("~/.steam/steam/steamapps/common") + ] + + for base_path in proton_paths: + if os.path.exists(base_path): + for item in os.listdir(base_path): + if 'proton' in item.lower(): + wine_path = os.path.join(base_path, item, 'files', 'bin', 'wine') + if os.path.exists(wine_path): + logger.debug(f"Found Wine binary: {wine_path}") + return wine_path + + # Method 2: Fallback to system wine if available + try: + result = subprocess.run(['which', 'wine'], capture_output=True, text=True) + if result.returncode == 0: + wine_path = result.stdout.strip() + logger.debug(f"Using system Wine binary: {wine_path}") + return wine_path + except Exception: + pass + + logger.error("No suitable Wine binary found for registry operations") + return None + + except Exception as e: + logger.error(f"Error finding Wine binary: {e}") + return None + def _inject_game_registry_entries(self, modlist_compatdata_path: str): - """Detect and inject FNV/Enderal game paths into modlist's system.reg""" + """Detect and inject FNV/Enderal game paths and apply universal dotnet4.x compatibility fixes""" system_reg_path = os.path.join(modlist_compatdata_path, "pfx", "system.reg") if not os.path.exists(system_reg_path): logger.warning("system.reg not found, skipping game path injection") return - - logger.info("Detecting and injecting game registry entries...") + + logger.info("Detecting game registry entries...") + + # NOTE: Universal dotnet4.x registry fixes now applied in modlist_handler.py after .reg downloads # Game configurations games_config = {