diff --git a/CHANGELOG.md b/CHANGELOG.md index 3817d97..f07bb9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,28 +1,47 @@ # 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 -### Major New Features -- **Dual Proton Configuration**: Separate Install Proton and Game Proton version selection in Settings - - **Install Proton**: Optimized for modlist installation and texture processing (Experimental/GE-Proton 10+ recommended for performance) - - **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 +### New Features +- **Lorerim Proton Override**: Automatically selects Proton 9 for Lorerim installations (GE-Proton9-27 preferred) +- **Engine Update**: jackify-engine v0.3.17 --- diff --git a/jackify/__init__.py b/jackify/__init__.py index ab8b42a..249e5f3 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" +__version__ = "0.1.6.1" diff --git a/jackify/backend/handlers/modlist_handler.py b/jackify/backend/handlers/modlist_handler.py index 8f7930b..3ee149f 100644 --- a/jackify/backend/handlers/modlist_handler.py +++ b/jackify/backend/handlers/modlist_handler.py @@ -80,7 +80,7 @@ class ModlistHandler: "lsiv": ["dotnet40"], "ls4": ["dotnet40"], "lorerim": ["dotnet40"], - "lostlegacy": ["dotnet48"], + "lostlegacy": ["dotnet40"], } 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("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 if status_callback: 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") return - # Get wine binary path - wine_binary = PathHandler.get_wine_binary_for_appid(str(self.appid)) + # Use winetricks handler to set Windows 10 mode + winetricks_handler = WinetricksHandler() + wine_binary = winetricks_handler._get_wine_binary_for_prefix(str(prefix_path)) if not wine_binary: self.logger.warning("Cannot re-enforce Windows 10 mode - wine binary not found") return - # Use winetricks handler to set Windows 10 mode - winetricks_handler = WinetricksHandler() 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: 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) \ No newline at end of file diff --git a/jackify/backend/handlers/protontricks_handler.py b/jackify/backend/handlers/protontricks_handler.py index c278084..038921a 100644 --- a/jackify/backend/handlers/protontricks_handler.py +++ b/jackify/backend/handlers/protontricks_handler.py @@ -488,7 +488,7 @@ class ProtontricksHandler: if "ShowDotFiles" not in content: logger.debug(f"Adding ShowDotFiles entry to {user_reg_path}") 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') dotfiles_set_success = True # Count file write as success too else: @@ -497,7 +497,7 @@ class ProtontricksHandler: else: logger.warning(f"user.reg not found at {user_reg_path}, creating it.") 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') dotfiles_set_success = True # Creating file counts as success except Exception as e: diff --git a/jackify/backend/handlers/winetricks_handler.py b/jackify/backend/handlers/winetricks_handler.py index c1bff7a..f5b9ab9 100644 --- a/jackify/backend/handlers/winetricks_handler.py +++ b/jackify/backend/handlers/winetricks_handler.py @@ -262,10 +262,18 @@ class WinetricksHandler: config_handler = ConfigHandler() 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 - if use_winetricks and "dotnet40" in components_to_install: - self.logger.info("Using optimized approach: protontricks for dotnet40 (reliable), winetricks for other components (fast)") - return self._install_components_hybrid_approach(components_to_install, wineprefix, game_var) + # ALWAYS use hybrid approach when legacy .NET Framework versions are present + 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") + return self._install_components_hybrid_approach(components_to_install, wineprefix, game_var, use_winetricks) elif not use_winetricks: self.logger.info("Using legacy approach: protontricks for all components") return self._install_components_protontricks_only(components_to_install, wineprefix, game_var) @@ -453,7 +461,7 @@ class WinetricksHandler: ) if result.returncode == 0: - self.logger.info(f"✓ {component} installed successfully") + self.logger.info(f"{component} installed successfully") component_success = True break else: @@ -467,13 +475,13 @@ class WinetricksHandler: try: with open(log_path, 'r') as f: if 'dotnet40' in f.read(): - self.logger.info("✓ dotnet40 confirmed in winetricks.log") + self.logger.info("dotnet40 confirmed in winetricks.log") component_success = True break except Exception as 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()}") except Exception as e: @@ -483,63 +491,70 @@ class WinetricksHandler: self.logger.error(f"Failed to install {component} after {max_attempts} attempts") 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) self._set_windows_10_mode(wineprefix, env.get('WINE', '')) 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), - then install remaining components with winetricks (faster for other components). + Hybrid approach: Install legacy .NET Framework versions with protontricks (reliable), + then install remaining components with winetricks OR protontricks based on user preference. Args: components: List of all components to install wineprefix: Wine prefix path game_var: Game variable for AppID detection + use_winetricks: Whether to use winetricks for non-legacy components Returns: bool: True if all installations succeeded, False otherwise """ self.logger.info("Starting hybrid installation approach") - # Separate dotnet40 (protontricks) from other components (winetricks) - protontricks_components = [comp for comp in components if comp == "dotnet40"] - other_components = [comp for comp in components if comp != "dotnet40"] + # Legacy .NET Framework versions that need protontricks + legacy_dotnet_versions = ['dotnet40', 'dotnet472', 'dotnet48'] + + # 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"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: - self.logger.info(f"Installing {protontricks_components} using protontricks...") - if not self._install_dotnet40_with_protontricks(wineprefix, game_var): + self.logger.info(f"Installing legacy .NET versions {protontricks_components} using protontricks...") + if not self._install_legacy_dotnet_with_protontricks(protontricks_components, wineprefix, game_var): self.logger.error(f"Failed to install {protontricks_components} with protontricks") 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: - 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 - 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") + self.logger.info("Hybrid component installation completed successfully") # Set Windows 10 mode after all component installation (matches legacy script timing) wine_binary = self._get_wine_binary_for_prefix(wineprefix) self._set_windows_10_mode(wineprefix, wine_binary) 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: + legacy_components: List of legacy .NET components to install (dotnet40, dotnet472, dotnet48) wineprefix: Wine prefix path game_var: Game variable for AppID detection @@ -575,21 +590,28 @@ class WinetricksHandler: # Detect protontricks availability 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 - # Install dotnet40 using protontricks - success = protontricks_handler.install_wine_components(appid, game_var, ["dotnet40"]) + # Install legacy .NET components using protontricks + success = protontricks_handler.install_wine_components(appid, game_var, legacy_components) 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 else: - self.logger.error("✗ dotnet40 installation failed with protontricks") + self.logger.error(f"Legacy .NET components {legacy_components} installation failed with protontricks") return False 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 def _prepare_winetricks_environment(self, wineprefix: str) -> Optional[dict]: @@ -699,13 +721,13 @@ class WinetricksHandler: ) 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) wine_binary = env.get('WINE', '') self._set_windows_10_mode(env.get('WINEPREFIX', ''), wine_binary) return True 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: 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) if result.returncode == 0: - self.logger.info("✓ Windows 10 mode set successfully") + self.logger.info("Windows 10 mode set successfully") else: 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) 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 wine_binary = self._get_wine_binary_for_prefix(wineprefix) self._set_windows_10_mode(wineprefix, wine_binary) return True else: - self.logger.error("✗ Component installation failed with protontricks") + self.logger.error("Component installation failed with protontricks") return False except Exception as e: diff --git a/jackify/backend/services/automated_prefix_service.py b/jackify/backend/services/automated_prefix_service.py index 3b5714b..cfc2111 100644 --- a/jackify/backend/services/automated_prefix_service.py +++ b/jackify/backend/services/automated_prefix_service.py @@ -57,6 +57,14 @@ class AutomatedPrefixService: self._store_proton_override_notification("Lorerim", 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() user_proton_path = config_handler.get_game_proton_path() diff --git a/jackify/backend/services/modlist_service.py b/jackify/backend/services/modlist_service.py index f7c0381..6e85192 100644 --- a/jackify/backend/services/modlist_service.py +++ b/jackify/backend/services/modlist_service.py @@ -629,7 +629,8 @@ class ModlistService: 'mo2_exe_path': str(context.install_dir / 'ModOrganizer.exe'), 'resolution': getattr(context, 'resolution', None), '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 diff --git a/jackify/backend/services/native_steam_service.py b/jackify/backend/services/native_steam_service.py index 68b51e7..4c2f0c9 100644 --- a/jackify/backend/services/native_steam_service.py +++ b/jackify/backend/services/native_steam_service.py @@ -382,8 +382,27 @@ class NativeSteamService: # Find the CompatToolMapping section compat_start = config_text.find('"CompatToolMapping"') if compat_start == -1: - logger.error("CompatToolMapping section not found in config.vdf") - return False + 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 + + # 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 # Look for the opening brace after CompatToolMapping diff --git a/jackify/frontends/gui/main.py b/jackify/frontends/gui/main.py index 4cdb6ca..ef98b3a 100644 --- a/jackify/frontends/gui/main.py +++ b/jackify/frontends/gui/main.py @@ -447,7 +447,7 @@ class SettingsDialog(QDialog): 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.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)." ) component_layout.addWidget(self.use_winetricks_checkbox)