3 Commits

Author SHA1 Message Date
Omni
f039cf9c24 Sync from development - prepare for v0.1.6.2 2025-10-23 21:50:28 +01:00
Omni
d9ea1be347 Sync from development - prepare for v0.1.6.1 2025-10-21 21:11:48 +01:00
Omni
a8862475d4 Sync from development - prepare for v0.1.6.1 2025-10-21 21:07:42 +01:00
9 changed files with 477 additions and 95 deletions

View File

@@ -1,28 +1,31 @@
# Jackify Changelog # Jackify Changelog
## v0.1.6 - Advanced Proton Management & Lorerim Support ## 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
### Bug Fixes
- **Fixed dotnet40 Installation Failures**: Resolved widespread .NET Framework installation issues affecting multiple modlists
- **Added Lost Legacy Proton 9 Override**: Automatic ENB compatibility for Lost Legacy modlist
- **Fixed Symlinked Downloads**: Automatically handles symlinked download directories to avoid Wine compatibility issues
---
## v0.1.6 - Lorerim Proton Support
**Release Date:** October 16, 2025 **Release Date:** October 16, 2025
### Major New Features ### New Features
- **Dual Proton Configuration**: Separate Install Proton and Game Proton version selection in Settings - **Lorerim Proton Override**: Automatically selects Proton 9 for Lorerim installations (GE-Proton9-27 preferred)
- **Install Proton**: Optimized for modlist installation and texture processing (Experimental/GE-Proton 10+ recommended for performance) - **Engine Update**: jackify-engine v0.3.17
- **Game Proton**: For game shortcuts (supports any Proton 9+ version)
- Independent configuration allows users to optimize for both installation speed and game compatibility
- **Lorerim Proton Override**: Automatic Proton 9 selection for Lorerim modlist installations
- Priority system: GE-Proton9-27 → Other GE-Proton 9 versions → Valve Proton 9 → user settings fallback
- User notification when override is applied
- Case-insensitive detection for Lorerim modlists
- **Configurable Component Installation Method**: User-selectable toggle in Settings
- **Optimized Mode** (default): Protontricks for dotnet40 (reliable), winetricks for other components (fast)
- **Legacy Mode**: Protontricks for all components (slower but maximum compatibility)
### Engine & Technical Improvements
- **jackify-engine v0.3.17**: Latest engine version with performance improvements
- **Windows 10 Prefix Timing**: Improved timing to match legacy script behavior
- **Self-Updater Enhancement**: Fixed auto-restart checkbox functionality
- **ProtontricksHandler**: Updated constructor calls across codebase for consistency
--- ---

View File

@@ -5,4 +5,4 @@ This package provides both CLI and GUI interfaces for managing
Wabbajack modlists natively on Linux systems. Wabbajack modlists natively on Linux systems.
""" """
__version__ = "0.1.6" __version__ = "0.1.6.2"

View File

@@ -71,16 +71,19 @@ class ModlistHandler:
} }
# Canonical mapping of modlist-specific Wine components (from omni-guides.sh) # 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 = { MODLIST_WINE_COMPONENTS = {
"wildlander": ["dotnet472"], # "wildlander": ["dotnet472"], # DISABLED: Universal registry fixes replace dotnet472 installation
"librum": ["dotnet40", "dotnet8"], # "librum": ["dotnet40", "dotnet8"], # PARTIAL DISABLE: Keep dotnet8, remove dotnet40
"apostasy": ["dotnet40", "dotnet8"], "librum": ["dotnet8"], # dotnet40 replaced with universal registry fixes
"nordicsouls": ["dotnet40"], # "apostasy": ["dotnet40", "dotnet8"], # PARTIAL DISABLE: Keep dotnet8, remove dotnet40
"livingskyrim": ["dotnet40"], "apostasy": ["dotnet8"], # dotnet40 replaced with universal registry fixes
"lsiv": ["dotnet40"], # "nordicsouls": ["dotnet40"], # DISABLED: Universal registry fixes replace dotnet40 installation
"ls4": ["dotnet40"], # "livingskyrim": ["dotnet40"], # DISABLED: Universal registry fixes replace dotnet40 installation
"lorerim": ["dotnet40"], # "lsiv": ["dotnet40"], # DISABLED: Universal registry fixes replace dotnet40 installation
"lostlegacy": ["dotnet48"], # "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, def __init__(self, steam_path_or_config: Union[Dict, str, Path, None] = None,
@@ -669,6 +672,12 @@ class ModlistHandler:
return False return False
self.logger.info("Step 3: Curated user.reg.modlist and system.reg.modlist applied successfully.") 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 # Step 4: Install Wine Components
if status_callback: if status_callback:
status_callback(f"{self._get_progress_timestamp()} Installing Wine components (this may take a while)") status_callback(f"{self._get_progress_timestamp()} Installing Wine components (this may take a while)")
@@ -737,6 +746,14 @@ class ModlistHandler:
self.logger.info(f"ModOrganizer.ini backed up to: {backup_path}") self.logger.info(f"ModOrganizer.ini backed up to: {backup_path}")
self.logger.info("Step 6: Backing up ModOrganizer.ini... Done") self.logger.info("Step 6: Backing up ModOrganizer.ini... Done")
# Step 6.5: Handle symlinked downloads directory
if status_callback:
status_callback(f"{self._get_progress_timestamp()} Checking for symlinked downloads directory")
self.logger.info("Step 6.5: Checking for symlinked downloads directory...")
if not self._handle_symlinked_downloads():
self.logger.warning("Warning during symlink handling (non-critical)")
self.logger.info("Step 6.5: Checking for symlinked downloads directory... Done")
# Step 7a: Detect Stock Game/Game Root path # Step 7a: Detect Stock Game/Game Root path
if status_callback: if status_callback:
status_callback(f"{self._get_progress_timestamp()} Detecting stock game path") status_callback(f"{self._get_progress_timestamp()} Detecting stock game path")
@@ -863,21 +880,38 @@ class ModlistHandler:
print("Warning: Failed to create dxvk.conf file.") print("Warning: Failed to create dxvk.conf file.")
self.logger.info("Step 10: Creating dxvk.conf... Done") 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: if status_callback:
status_callback(f"{self._get_progress_timestamp()} Deleting incompatible MO2 plugin") status_callback(f"{self._get_progress_timestamp()} Deleting incompatible MO2 plugins")
self.logger.info("Step 11a: Deleting incompatible MO2 plugin (FixGameRegKey.py)...") self.logger.info("Step 11a: Deleting incompatible MO2 plugins...")
plugin_path = Path(self.modlist_dir) / "plugins" / "FixGameRegKey.py"
if plugin_path.exists(): # Delete FixGameRegKey.py plugin
fixgamereg_path = Path(self.modlist_dir) / "plugins" / "FixGameRegKey.py"
if fixgamereg_path.exists():
try: try:
plugin_path.unlink() fixgamereg_path.unlink()
self.logger.info("FixGameRegKey.py plugin deleted successfully.") self.logger.info("FixGameRegKey.py plugin deleted successfully.")
except Exception as e: except Exception as e:
self.logger.warning(f"Failed to delete FixGameRegKey.py plugin: {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: else:
self.logger.debug("FixGameRegKey.py plugin not found (this is normal).") 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 # Step 11b: Download Font
if status_callback: if status_callback:
@@ -1346,19 +1380,210 @@ class ModlistHandler:
self.logger.warning("Cannot re-enforce Windows 10 mode - prefix path not found") self.logger.warning("Cannot re-enforce Windows 10 mode - prefix path not found")
return return
# Get wine binary path # Use winetricks handler to set Windows 10 mode
wine_binary = PathHandler.get_wine_binary_for_appid(str(self.appid)) winetricks_handler = WinetricksHandler()
wine_binary = winetricks_handler._get_wine_binary_for_prefix(str(prefix_path))
if not wine_binary: if not wine_binary:
self.logger.warning("Cannot re-enforce Windows 10 mode - wine binary not found") self.logger.warning("Cannot re-enforce Windows 10 mode - wine binary not found")
return return
# Use winetricks handler to set Windows 10 mode
winetricks_handler = WinetricksHandler()
winetricks_handler._set_windows_10_mode(str(prefix_path), wine_binary) winetricks_handler._set_windows_10_mode(str(prefix_path), wine_binary)
self.logger.info("Windows 10 mode re-enforced after modlist-specific configurations") self.logger.info("Windows 10 mode re-enforced after modlist-specific configurations")
except Exception as e: except Exception as e:
self.logger.warning(f"Error re-enforcing Windows 10 mode: {e}") self.logger.warning(f"Error re-enforcing Windows 10 mode: {e}")
# (Ensure EOF is clean and no extra incorrect methods exist below) def _handle_symlinked_downloads(self) -> bool:
"""
Check if downloads_directory in ModOrganizer.ini points to a symlink.
If it does, comment out the line to force MO2 to use default behavior.
Returns:
bool: True on success or no action needed, False on error
"""
try:
import configparser
import os
if not self.modlist_ini or not os.path.exists(self.modlist_ini):
self.logger.warning("ModOrganizer.ini not found for symlink check")
return True # Non-critical
# Read the INI file
config = configparser.ConfigParser(allow_no_value=True, delimiters=['='])
config.optionxform = str # Preserve case sensitivity
try:
# Read file manually to handle BOM
with open(self.modlist_ini, 'r', encoding='utf-8-sig') as f:
config.read_file(f)
except UnicodeDecodeError:
with open(self.modlist_ini, 'r', encoding='latin-1') as f:
config.read_file(f)
# Check if downloads_directory or download_directory exists and is a symlink
downloads_key = None
downloads_path = None
if 'General' in config:
# Check for both possible key names
if 'downloads_directory' in config['General']:
downloads_key = 'downloads_directory'
downloads_path = config['General']['downloads_directory']
elif 'download_directory' in config['General']:
downloads_key = 'download_directory'
downloads_path = config['General']['download_directory']
if downloads_path:
if downloads_path and os.path.exists(downloads_path):
# Check if the path or any parent directory contains symlinks
def has_symlink_in_path(path):
"""Check if path or any parent directory is a symlink"""
current_path = Path(path).resolve()
check_path = Path(path)
# Walk up the path checking each component
for parent in [check_path] + list(check_path.parents):
if parent.is_symlink():
return True, str(parent)
return False, None
has_symlink, symlink_path = has_symlink_in_path(downloads_path)
if has_symlink:
self.logger.info(f"Detected symlink in downloads directory path: {symlink_path} -> {downloads_path}")
self.logger.info("Commenting out downloads_directory to avoid Wine symlink issues")
# Read the file manually to preserve comments and formatting
with open(self.modlist_ini, 'r', encoding='utf-8') as f:
lines = f.readlines()
# Find and comment out the downloads directory line
modified = False
for i, line in enumerate(lines):
if line.strip().startswith(f'{downloads_key}='):
lines[i] = '#' + line # Comment out the line
modified = True
break
if modified:
# Write the modified file back
with open(self.modlist_ini, 'w', encoding='utf-8') as f:
f.writelines(lines)
self.logger.info(f"{downloads_key} line commented out successfully")
else:
self.logger.warning("downloads_directory line not found in file")
else:
self.logger.debug(f"downloads_directory is not a symlink: {downloads_path}")
else:
self.logger.debug("downloads_directory path does not exist or is empty")
else:
self.logger.debug("No downloads_directory found in ModOrganizer.ini")
return True
except Exception as e:
self.logger.error(f"Error handling symlinked downloads: {e}", exc_info=True)
return False
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

View File

@@ -488,7 +488,7 @@ class ProtontricksHandler:
if "ShowDotFiles" not in content: if "ShowDotFiles" not in content:
logger.debug(f"Adding ShowDotFiles entry to {user_reg_path}") logger.debug(f"Adding ShowDotFiles entry to {user_reg_path}")
with open(user_reg_path, 'a', encoding='utf-8') as f: with open(user_reg_path, 'a', encoding='utf-8') as f:
f.write('\n[Software\\Wine] 1603891765\n') f.write('\n[Software\\Wine] 1603891765\n')
f.write('"ShowDotFiles"="Y"\n') f.write('"ShowDotFiles"="Y"\n')
dotfiles_set_success = True # Count file write as success too dotfiles_set_success = True # Count file write as success too
else: else:
@@ -497,7 +497,7 @@ class ProtontricksHandler:
else: else:
logger.warning(f"user.reg not found at {user_reg_path}, creating it.") logger.warning(f"user.reg not found at {user_reg_path}, creating it.")
with open(user_reg_path, 'w', encoding='utf-8') as f: with open(user_reg_path, 'w', encoding='utf-8') as f:
f.write('[Software\\Wine] 1603891765\n') f.write('[Software\\Wine] 1603891765\n')
f.write('"ShowDotFiles"="Y"\n') f.write('"ShowDotFiles"="Y"\n')
dotfiles_set_success = True # Creating file counts as success dotfiles_set_success = True # Creating file counts as success
except Exception as e: except Exception as e:

View File

@@ -262,10 +262,20 @@ class WinetricksHandler:
config_handler = ConfigHandler() config_handler = ConfigHandler()
use_winetricks = config_handler.get('use_winetricks_for_components', True) use_winetricks = config_handler.get('use_winetricks_for_components', True)
# Legacy .NET Framework versions that are problematic in Wine/Proton
# 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 # Choose installation method based on user preference and components
if use_winetricks and "dotnet40" in components_to_install: # HYBRID APPROACH MOSTLY DISABLED: dotnet40/dotnet472 replaced with universal registry fixes
self.logger.info("Using optimized approach: protontricks for dotnet40 (reliable), winetricks for other components (fast)") if has_legacy_dotnet:
return self._install_components_hybrid_approach(components_to_install, wineprefix, game_var) 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")
return self._install_components_hybrid_approach(components_to_install, wineprefix, game_var, use_winetricks)
elif not use_winetricks: elif not use_winetricks:
self.logger.info("Using legacy approach: protontricks for all components") self.logger.info("Using legacy approach: protontricks for all components")
return self._install_components_protontricks_only(components_to_install, wineprefix, game_var) return self._install_components_protontricks_only(components_to_install, wineprefix, game_var)
@@ -453,7 +463,7 @@ class WinetricksHandler:
) )
if result.returncode == 0: if result.returncode == 0:
self.logger.info(f"{component} installed successfully") self.logger.info(f"{component} installed successfully")
component_success = True component_success = True
break break
else: else:
@@ -467,13 +477,13 @@ class WinetricksHandler:
try: try:
with open(log_path, 'r') as f: with open(log_path, 'r') as f:
if 'dotnet40' in f.read(): if 'dotnet40' in f.read():
self.logger.info("dotnet40 confirmed in winetricks.log") self.logger.info("dotnet40 confirmed in winetricks.log")
component_success = True component_success = True
break break
except Exception as e: except Exception as e:
self.logger.warning(f"Could not read winetricks.log: {e}") self.logger.warning(f"Could not read winetricks.log: {e}")
self.logger.error(f"{component} failed (attempt {attempt}): {result.stderr.strip()}") self.logger.error(f"{component} failed (attempt {attempt}): {result.stderr.strip()}")
self.logger.debug(f"Full stdout for {component}: {result.stdout.strip()}") self.logger.debug(f"Full stdout for {component}: {result.stdout.strip()}")
except Exception as e: except Exception as e:
@@ -483,63 +493,70 @@ class WinetricksHandler:
self.logger.error(f"Failed to install {component} after {max_attempts} attempts") self.logger.error(f"Failed to install {component} after {max_attempts} attempts")
return False return False
self.logger.info("All components installed successfully using separate sessions") self.logger.info("All components installed successfully using separate sessions")
# Set Windows 10 mode after all component installation (matches legacy script timing) # Set Windows 10 mode after all component installation (matches legacy script timing)
self._set_windows_10_mode(wineprefix, env.get('WINE', '')) self._set_windows_10_mode(wineprefix, env.get('WINE', ''))
return True return True
def _install_components_hybrid_approach(self, components: list, wineprefix: str, game_var: str) -> bool: def _install_components_hybrid_approach(self, components: list, wineprefix: str, game_var: str, use_winetricks: bool = True) -> bool:
""" """
Hybrid approach: Install dotnet40 with protontricks (known to work), Hybrid approach: Install legacy .NET Framework versions with protontricks (reliable),
then install remaining components with winetricks (faster for other components). then install remaining components with winetricks OR protontricks based on user preference.
Args: Args:
components: List of all components to install components: List of all components to install
wineprefix: Wine prefix path wineprefix: Wine prefix path
game_var: Game variable for AppID detection game_var: Game variable for AppID detection
use_winetricks: Whether to use winetricks for non-legacy components
Returns: Returns:
bool: True if all installations succeeded, False otherwise bool: True if all installations succeeded, False otherwise
""" """
self.logger.info("Starting hybrid installation approach") self.logger.info("Starting hybrid installation approach")
# Separate dotnet40 (protontricks) from other components (winetricks) # Legacy .NET Framework versions that need protontricks
protontricks_components = [comp for comp in components if comp == "dotnet40"] legacy_dotnet_versions = ['dotnet40', 'dotnet472', 'dotnet48']
other_components = [comp for comp in components if comp != "dotnet40"]
# Separate legacy .NET (protontricks) from other components (winetricks)
protontricks_components = [comp for comp in components if comp in legacy_dotnet_versions]
other_components = [comp for comp in components if comp not in legacy_dotnet_versions]
self.logger.info(f"Protontricks components: {protontricks_components}") self.logger.info(f"Protontricks components: {protontricks_components}")
self.logger.info(f"Other components: {other_components}") self.logger.info(f"Other components: {other_components}")
# Step 1: Install dotnet40 with protontricks if present # Step 1: Install legacy .NET Framework versions with protontricks if present
if protontricks_components: if protontricks_components:
self.logger.info(f"Installing {protontricks_components} using protontricks...") self.logger.info(f"Installing legacy .NET versions {protontricks_components} using protontricks...")
if not self._install_dotnet40_with_protontricks(wineprefix, game_var): if not self._install_legacy_dotnet_with_protontricks(protontricks_components, wineprefix, game_var):
self.logger.error(f"Failed to install {protontricks_components} with protontricks") self.logger.error(f"Failed to install {protontricks_components} with protontricks")
return False return False
self.logger.info(f"{protontricks_components} installation completed successfully with protontricks") self.logger.info(f"{protontricks_components} installation completed successfully with protontricks")
# Step 2: Install remaining components with winetricks if any # Step 2: Install remaining components if any
if other_components: if other_components:
self.logger.info(f"Installing remaining components with winetricks: {other_components}") if use_winetricks:
self.logger.info(f"Installing remaining components with winetricks: {other_components}")
# Use existing winetricks logic for other components
env = self._prepare_winetricks_environment(wineprefix)
if not env:
return False
return self._install_components_with_winetricks(other_components, wineprefix, env)
else:
self.logger.info(f"Installing remaining components with protontricks: {other_components}")
return self._install_components_protontricks_only(other_components, wineprefix, game_var)
# Use existing winetricks logic for other components self.logger.info("Hybrid component installation completed successfully")
env = self._prepare_winetricks_environment(wineprefix)
if not env:
return False
return self._install_components_with_winetricks(other_components, wineprefix, env)
self.logger.info("✓ Hybrid component installation completed successfully")
# Set Windows 10 mode after all component installation (matches legacy script timing) # Set Windows 10 mode after all component installation (matches legacy script timing)
wine_binary = self._get_wine_binary_for_prefix(wineprefix) wine_binary = self._get_wine_binary_for_prefix(wineprefix)
self._set_windows_10_mode(wineprefix, wine_binary) self._set_windows_10_mode(wineprefix, wine_binary)
return True return True
def _install_dotnet40_with_protontricks(self, wineprefix: str, game_var: str) -> bool: def _install_legacy_dotnet_with_protontricks(self, legacy_components: list, wineprefix: str, game_var: str) -> bool:
""" """
Install dotnet40 using protontricks (known to work reliably). Install legacy .NET Framework versions using protontricks (known to work more reliably).
Args: Args:
legacy_components: List of legacy .NET components to install (dotnet40, dotnet472, dotnet48)
wineprefix: Wine prefix path wineprefix: Wine prefix path
game_var: Game variable for AppID detection game_var: Game variable for AppID detection
@@ -575,21 +592,28 @@ class WinetricksHandler:
# Detect protontricks availability # Detect protontricks availability
if not protontricks_handler.detect_protontricks(): if not protontricks_handler.detect_protontricks():
self.logger.error("Protontricks not available for dotnet40 installation") self.logger.error(f"Protontricks not available for legacy .NET installation: {legacy_components}")
return False return False
# Install dotnet40 using protontricks # Install legacy .NET components using protontricks
success = protontricks_handler.install_wine_components(appid, game_var, ["dotnet40"]) success = protontricks_handler.install_wine_components(appid, game_var, legacy_components)
if success: if success:
self.logger.info("✓ dotnet40 installed successfully with protontricks") self.logger.info(f"Legacy .NET components {legacy_components} installed successfully with protontricks")
# Enable dotfiles and symlinks for the prefix
if protontricks_handler.enable_dotfiles(appid):
self.logger.info("Enabled dotfiles and symlinks support")
else:
self.logger.warning("Failed to enable dotfiles/symlinks (non-critical)")
return True return True
else: else:
self.logger.error("✗ dotnet40 installation failed with protontricks") self.logger.error(f"Legacy .NET components {legacy_components} installation failed with protontricks")
return False return False
except Exception as e: except Exception as e:
self.logger.error(f"Error installing dotnet40 with protontricks: {e}", exc_info=True) self.logger.error(f"Error installing legacy .NET components {legacy_components} with protontricks: {e}", exc_info=True)
return False return False
def _prepare_winetricks_environment(self, wineprefix: str) -> Optional[dict]: def _prepare_winetricks_environment(self, wineprefix: str) -> Optional[dict]:
@@ -699,13 +723,13 @@ class WinetricksHandler:
) )
if result.returncode == 0: if result.returncode == 0:
self.logger.info(f"Winetricks components installed successfully: {components}") self.logger.info(f"Winetricks components installed successfully: {components}")
# Set Windows 10 mode after component installation (matches legacy script timing) # Set Windows 10 mode after component installation (matches legacy script timing)
wine_binary = env.get('WINE', '') wine_binary = env.get('WINE', '')
self._set_windows_10_mode(env.get('WINEPREFIX', ''), wine_binary) self._set_windows_10_mode(env.get('WINEPREFIX', ''), wine_binary)
return True return True
else: else:
self.logger.error(f"Winetricks failed (attempt {attempt}): {result.stderr.strip()}") self.logger.error(f"Winetricks failed (attempt {attempt}): {result.stderr.strip()}")
except Exception as e: except Exception as e:
self.logger.error(f"Error during winetricks run (attempt {attempt}): {e}") self.logger.error(f"Error during winetricks run (attempt {attempt}): {e}")
@@ -729,7 +753,7 @@ class WinetricksHandler:
], env=env, capture_output=True, text=True, timeout=300) ], env=env, capture_output=True, text=True, timeout=300)
if result.returncode == 0: if result.returncode == 0:
self.logger.info("Windows 10 mode set successfully") self.logger.info("Windows 10 mode set successfully")
else: else:
self.logger.warning(f"Could not set Windows 10 mode: {result.stderr}") self.logger.warning(f"Could not set Windows 10 mode: {result.stderr}")
@@ -768,13 +792,13 @@ class WinetricksHandler:
success = protontricks_handler.install_wine_components(appid, game_var, components) success = protontricks_handler.install_wine_components(appid, game_var, components)
if success: if success:
self.logger.info("All components installed successfully with protontricks") self.logger.info("All components installed successfully with protontricks")
# Set Windows 10 mode after component installation # Set Windows 10 mode after component installation
wine_binary = self._get_wine_binary_for_prefix(wineprefix) wine_binary = self._get_wine_binary_for_prefix(wineprefix)
self._set_windows_10_mode(wineprefix, wine_binary) self._set_windows_10_mode(wineprefix, wine_binary)
return True return True
else: else:
self.logger.error("Component installation failed with protontricks") self.logger.error("Component installation failed with protontricks")
return False return False
except Exception as e: except Exception as e:

View File

@@ -50,13 +50,22 @@ class AutomatedPrefixService:
from jackify.backend.handlers.wine_utils import WineUtils from jackify.backend.handlers.wine_utils import WineUtils
# Check for Lorerim-specific Proton override first # 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() lorerim_proton = self._get_lorerim_preferred_proton()
if lorerim_proton: if lorerim_proton:
logger.info(f"Lorerim detected: Using {lorerim_proton} instead of user settings") logger.info(f"Lorerim detected: Using {lorerim_proton} instead of user settings")
self._store_proton_override_notification("Lorerim", lorerim_proton) self._store_proton_override_notification("Lorerim", lorerim_proton)
return lorerim_proton return lorerim_proton
# Check for Lost Legacy-specific Proton override (needs Proton 9 for ENB compatibility)
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)")
self._store_proton_override_notification("Lost Legacy", lostlegacy_proton)
return lostlegacy_proton
config_handler = ConfigHandler() config_handler = ConfigHandler()
user_proton_path = config_handler.get_game_proton_path() user_proton_path = config_handler.get_game_proton_path()
@@ -2972,14 +2981,115 @@ echo Prefix creation complete.
logger.error(f"Failed to update registry path: {e}") logger.error(f"Failed to update registry path: {e}")
return False 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): 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") system_reg_path = os.path.join(modlist_compatdata_path, "pfx", "system.reg")
if not os.path.exists(system_reg_path): if not os.path.exists(system_reg_path):
logger.warning("system.reg not found, skipping game path injection") logger.warning("system.reg not found, skipping game path injection")
return 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 # Game configurations
games_config = { games_config = {

View File

@@ -629,7 +629,8 @@ class ModlistService:
'mo2_exe_path': str(context.install_dir / 'ModOrganizer.exe'), 'mo2_exe_path': str(context.install_dir / 'ModOrganizer.exe'),
'resolution': getattr(context, 'resolution', None), 'resolution': getattr(context, 'resolution', None),
'skip_confirmation': True, # Service layer should be non-interactive 'skip_confirmation': True, # Service layer should be non-interactive
'manual_steps_completed': False 'manual_steps_completed': False,
'appid': getattr(context, 'app_id', None) # Fix: Include appid like other configuration paths
} }
# DEBUG: Log what resolution we're passing # DEBUG: Log what resolution we're passing

View File

@@ -382,8 +382,27 @@ class NativeSteamService:
# Find the CompatToolMapping section # Find the CompatToolMapping section
compat_start = config_text.find('"CompatToolMapping"') compat_start = config_text.find('"CompatToolMapping"')
if compat_start == -1: if compat_start == -1:
logger.error("CompatToolMapping section not found in config.vdf") logger.warning("CompatToolMapping section not found in config.vdf, creating it")
return False # Find the Steam section to add CompatToolMapping to
steam_section = config_text.find('"Steam"')
if steam_section == -1:
logger.error("Steam section not found in config.vdf")
return False
# Find the opening brace for Steam section
steam_brace = config_text.find('{', steam_section)
if steam_brace == -1:
logger.error("Steam section opening brace not found")
return False
# Insert CompatToolMapping section right after Steam opening brace
insert_pos = steam_brace + 1
compat_section = '\n\t\t"CompatToolMapping"\n\t\t{\n\t\t}\n'
config_text = config_text[:insert_pos] + compat_section + config_text[insert_pos:]
# Update compat_start position after insertion
compat_start = config_text.find('"CompatToolMapping"')
logger.info("Created CompatToolMapping section in config.vdf")
# Find the closing brace for CompatToolMapping # Find the closing brace for CompatToolMapping
# Look for the opening brace after CompatToolMapping # Look for the opening brace after CompatToolMapping

View File

@@ -447,7 +447,7 @@ class SettingsDialog(QDialog):
self.use_winetricks_checkbox = QCheckBox("Use winetricks for component installation (faster)") self.use_winetricks_checkbox = QCheckBox("Use winetricks for component installation (faster)")
self.use_winetricks_checkbox.setChecked(self.config_handler.get('use_winetricks_for_components', True)) self.use_winetricks_checkbox.setChecked(self.config_handler.get('use_winetricks_for_components', True))
self.use_winetricks_checkbox.setToolTip( self.use_winetricks_checkbox.setToolTip(
"When enabled: Uses winetricks for most components (faster) and protontricks only for dotnet40 (more reliable).\n" "When enabled: Uses winetricks for most components (faster) and protontricks for legacy .NET versions (dotnet40, dotnet472, dotnet48) which are more reliable.\n"
"When disabled: Uses protontricks for all components (legacy behavior, slower but more compatible)." "When disabled: Uses protontricks for all components (legacy behavior, slower but more compatible)."
) )
component_layout.addWidget(self.use_winetricks_checkbox) component_layout.addWidget(self.use_winetricks_checkbox)