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
## 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
---

View File

@@ -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"

View File

@@ -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)

View File

@@ -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:
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)
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:

View File

@@ -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()

View File

@@ -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

View File

@@ -382,9 +382,28 @@ 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")
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
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.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)