Sync from development - prepare for v0.1.6.1

This commit is contained in:
Omni
2025-10-21 21:07:42 +01:00
parent 430d085287
commit a8862475d4
9 changed files with 243 additions and 74 deletions

View File

@@ -1,28 +1,47 @@
# Jackify Changelog # Jackify Changelog
## v0.1.6 - Advanced Proton Management & Lorerim Support ## v0.1.6.1 - Critical Configuration and Legacy .NET Fixes
**Release Date:** October 21, 2025
### Bug Fixes
- **Fixed FILFY Configuration Error**: Resolved "set_modlist failed" error in Configure New Modlist workflow
- Added missing `appid` field to configuration context in `configure_modlist` method
- Affects edge case where users configure modlists via specific GUI workflows
- Ensures consistent context handling across all configuration paths
- **Fixed Steam CompatToolMapping Creation**: Resolved Proton version setting failures on fresh Steam installations
- Native Steam service now creates missing CompatToolMapping section automatically
- Prevents "CompatToolMapping section not found" errors during shortcut creation
- Ensures consistent Proton configuration across all Steam setups
- **Fixed Lost Legacy .NET Requirements**: Corrected dotnet48 installation failures
- Changed Lost Legacy requirement from dotnet48 to dotnet40 (which actually works)
- Added Lost Legacy Proton 9 override for ENB compatibility
- Resolves widespread .NET Framework installation failures
- **Added Automatic Symlink Handling**: Wine symlink compatibility improvements
- Automatically detects symlinked downloads_directory in ModOrganizer.ini
- Comments out symlinked paths to avoid Wine symlink following issues
- Enables MO2 to use default download location instead of broken symlink paths
- **Enhanced Dotfiles and Symlinks Support**: Automatic Wine prefix configuration
- Automatically enables ShowDotFiles and symlink support during .NET component installation
- Improves compatibility with various file system configurations
- Applied to all modlists using legacy .NET components
### Code Quality
- **Removed Unprofessional Elements**: Cleaned up all emoji usage in logs and user output
- Maintains professional appearance in all user-facing messages
- Follows established coding standards
---
## 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.1"

View File

@@ -80,7 +80,7 @@ class ModlistHandler:
"lsiv": ["dotnet40"], "lsiv": ["dotnet40"],
"ls4": ["dotnet40"], "ls4": ["dotnet40"],
"lorerim": ["dotnet40"], "lorerim": ["dotnet40"],
"lostlegacy": ["dotnet48"], "lostlegacy": ["dotnet40"],
} }
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,
@@ -737,6 +737,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")
@@ -1346,19 +1354,111 @@ 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}")
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
# (Ensure EOF is clean and no extra incorrect methods exist below) # (Ensure EOF is clean and no extra incorrect methods exist below)

View File

@@ -262,10 +262,18 @@ 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
legacy_dotnet_versions = ['dotnet40', 'dotnet472', 'dotnet48']
# 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: # ALWAYS use hybrid approach when legacy .NET Framework versions are present
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 +461,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 +475,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 +491,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:
if use_winetricks:
self.logger.info(f"Installing remaining components with winetricks: {other_components}") self.logger.info(f"Installing remaining components with winetricks: {other_components}")
# Use existing winetricks logic for other components # Use existing winetricks logic for other components
env = self._prepare_winetricks_environment(wineprefix) env = self._prepare_winetricks_environment(wineprefix)
if not env: if not env:
return False return False
return self._install_components_with_winetricks(other_components, wineprefix, env) 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)
self.logger.info("Hybrid component installation completed successfully") 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 +590,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 +721,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 +751,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 +790,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

@@ -57,6 +57,14 @@ class AutomatedPrefixService:
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_name and modlist_name.lower() == '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()

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,9 +382,28 @@ 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")
# 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 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
brace_start = config_text.find('{', compat_start) brace_start = config_text.find('{', compat_start)

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)