diff --git a/CHANGELOG.md b/CHANGELOG.md index 95a87e9..7889df0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,50 @@ # Jackify Changelog +## v0.1.4 - GE-Proton Support and Performance Optimization +**Release Date:** September 22, 2025 + +### New Features +- **GE-Proton Detection**: Automatic detection and prioritization of GE-Proton versions +- **User-selectable Proton version**: Settings dialog displays all available Proton versions with type indicators + +### Engine Updates +- **jackify-engine v0.3.15**: Reads Proton configuration from config.json, adds degree symbol handling for special characters, removes Wine fallback (Proton now required) + +### Technical Improvements +- **Smart Priority**: GE-Proton 10+ → Proton Experimental → Proton 10 → Proton 9 +- **Auto-Configuration**: Fresh installations automatically select optimal Proton version + +### Bug Fixes +- **Steam VDF Compatibility**: Fixed case-sensitivity issues with Steam shortcuts.vdf parsing for Configure Existing Modlist workflows + +--- + +## v0.1.3 - Enhanced Proton Support and System Compatibility +**Release Date:** September 21, 2025 + +### New Features +- **Enhanced Proton Detection**: Automatic fallback system with priority: Experimental → Proton 10 → Proton 9 +- **Guided Proton Installation**: Professional auto-install dialog with Steam protocol integration for missing Proton versions +- **Enderal Game Support**: Added Enderal to supported games list with special handling for Somnium modlist structure +- **Proton Version Leniency**: Accept any Proton version 9+ instead of requiring Experimental + +### UX Improvements +- **Resolution System Overhaul**: Eliminated hardcoded 2560x1600 fallbacks across all screens +- **Steam Deck Detection**: Proper 1280x800 default resolution with 1920x1080 fallback for desktop +- **Leave Unchanged Logic**: Fixed resolution setting to actually preserve existing user configurations + +### Technical Improvements +- **Resolution Utilities**: New `shared/resolution_utils.py` with centralized resolution management +- **Protontricks Detection**: Enhanced detection for both native and Flatpak protontricks installations +- **Real-time Monitoring**: Progress tracking for Proton installation with directory stability detection + +### Bug Fixes +- **Somnium Support**: Automatic detection of `files/ModOrganizer.exe` structure in edge-case modlists +- **Steam Protocol Integration**: Reliable triggering of Proton installation via `steam://install/` URLs +- **Manual Fallback**: Clear instructions and recheck functionality when auto-install fails + +--- + ## v0.1.2 - About Dialog and System Information **Release Date:** September 16, 2025 diff --git a/jackify/__init__.py b/jackify/__init__.py index d5f1880..17e3840 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.2" +__version__ = "0.1.4" diff --git a/jackify/backend/core/modlist_operations.py b/jackify/backend/core/modlist_operations.py index e3923ec..23d0f1a 100644 --- a/jackify/backend/core/modlist_operations.py +++ b/jackify/backend/core/modlist_operations.py @@ -23,6 +23,44 @@ from jackify.backend.handlers.config_handler import ConfigHandler # UI Colors already imported above +def _get_user_proton_version(): + """Get user's preferred Proton version from config, with fallback to auto-detection""" + try: + from jackify.backend.handlers.config_handler import ConfigHandler + from jackify.backend.handlers.wine_utils import WineUtils + + config_handler = ConfigHandler() + user_proton_path = config_handler.get('proton_path', 'auto') + + if user_proton_path == 'auto': + # Use enhanced fallback logic with GE-Proton preference + logging.info("User selected auto-detect, using GE-Proton → Experimental → Proton precedence") + return WineUtils.select_best_proton() + else: + # User has selected a specific Proton version + # Use the exact directory name for Steam config.vdf + try: + proton_version = os.path.basename(user_proton_path) + # GE-Proton uses exact directory name, Valve Proton needs lowercase conversion + if proton_version.startswith('GE-Proton'): + # Keep GE-Proton name exactly as-is + steam_proton_name = proton_version + else: + # Convert Valve Proton names to Steam's format + steam_proton_name = proton_version.lower().replace(' - ', '_').replace(' ', '_').replace('-', '_') + if not steam_proton_name.startswith('proton'): + steam_proton_name = f"proton_{steam_proton_name}" + + logging.info(f"Using user-selected Proton: {steam_proton_name}") + return steam_proton_name + except Exception as e: + logging.warning(f"Invalid user Proton path '{user_proton_path}', falling back to auto: {e}") + return WineUtils.select_best_proton() + + except Exception as e: + logging.error(f"Failed to get user Proton preference, using default: {e}") + return "proton_experimental" + # Attempt to import readline for tab completion READLINE_AVAILABLE = False try: @@ -1248,13 +1286,16 @@ class ModlistInstallCLI: from jackify.backend.services.native_steam_service import NativeSteamService steam_service = NativeSteamService() + # Get user's preferred Proton version + proton_version = _get_user_proton_version() + success, app_id = steam_service.create_shortcut_with_proton( app_name=config_context['name'], exe_path=config_context['mo2_exe_path'], start_dir=os.path.dirname(config_context['mo2_exe_path']), launch_options="%command%", tags=["Jackify"], - proton_version="proton_experimental" + proton_version=proton_version ) if not success or not app_id: diff --git a/jackify/backend/handlers/config_handler.py b/jackify/backend/handlers/config_handler.py index 65425b1..d7aea67 100644 --- a/jackify/backend/handlers/config_handler.py +++ b/jackify/backend/handlers/config_handler.py @@ -47,8 +47,10 @@ class ConfigHandler: # If steam_path is not set, detect it if not self.settings["steam_path"]: self.settings["steam_path"] = self._detect_steam_path() - # Save the updated settings - self.save_config() + + # Auto-detect and set Proton version on first run + if not self.settings.get("proton_path"): + self._auto_detect_proton() # If jackify_data_dir is not set, initialize it to default if not self.settings.get("jackify_data_dir"): @@ -494,4 +496,28 @@ class ConfigHandler: logger.error(f"Error saving modlist downloads base directory: {e}") return False + def _auto_detect_proton(self): + """Auto-detect and set best Proton version (includes GE-Proton and Valve Proton)""" + try: + from .wine_utils import WineUtils + best_proton = WineUtils.select_best_proton() + + if best_proton: + self.settings["proton_path"] = str(best_proton['path']) + self.settings["proton_version"] = best_proton['name'] + proton_type = best_proton.get('type', 'Unknown') + logger.info(f"Auto-detected Proton: {best_proton['name']} ({proton_type})") + self.save_config() + else: + # Fallback to auto-detect mode + self.settings["proton_path"] = "auto" + self.settings["proton_version"] = "auto" + logger.info("No compatible Proton versions found, using auto-detect mode") + self.save_config() + + except Exception as e: + logger.error(f"Failed to auto-detect Proton: {e}") + self.settings["proton_path"] = "auto" + self.settings["proton_version"] = "auto" + \ No newline at end of file diff --git a/jackify/backend/handlers/modlist_handler.py b/jackify/backend/handlers/modlist_handler.py index dc88219..ef67ee3 100644 --- a/jackify/backend/handlers/modlist_handler.py +++ b/jackify/backend/handlers/modlist_handler.py @@ -1163,7 +1163,7 @@ class ModlistHandler: # Determine game type game = (game_var_full or modlist_name or "").lower().replace(" ", "") # Add game-specific extras - if "skyrim" in game or "fallout4" in game or "starfield" in game or "oblivion_remastered" in game: + if "skyrim" in game or "fallout4" in game or "starfield" in game or "oblivion_remastered" in game or "enderal" in game: extras += ["d3dcompiler_47", "d3dx11_43", "d3dcompiler_43", "dotnet6", "dotnet7"] elif "falloutnewvegas" in game or "fnv" in game or "oblivion" in game: extras += ["d3dx9_43", "d3dx9"] @@ -1238,6 +1238,12 @@ class ModlistHandler: # Check ModOrganizer.ini for indicators (nvse/enderal) as an early, robust signal try: mo2_ini = modlist_path / "ModOrganizer.ini" + # Also check Somnium's non-standard location + if not mo2_ini.exists(): + somnium_mo2_ini = modlist_path / "files" / "ModOrganizer.ini" + if somnium_mo2_ini.exists(): + mo2_ini = somnium_mo2_ini + if mo2_ini.exists(): try: content = mo2_ini.read_text(errors='ignore').lower() diff --git a/jackify/backend/handlers/shortcut_handler.py b/jackify/backend/handlers/shortcut_handler.py index ef29ebd..0757988 100644 --- a/jackify/backend/handlers/shortcut_handler.py +++ b/jackify/backend/handlers/shortcut_handler.py @@ -988,8 +988,8 @@ class ShortcutHandler: shortcuts_data = VDFHandler.load(shortcuts_vdf_path, binary=True) if shortcuts_data and 'shortcuts' in shortcuts_data: for idx, shortcut in shortcuts_data['shortcuts'].items(): - app_name = shortcut.get('AppName', '').strip() - exe = shortcut.get('Exe', '').strip('"').strip() + app_name = shortcut.get('AppName', shortcut.get('appname', '')).strip() + exe = shortcut.get('Exe', shortcut.get('exe', '')).strip('"').strip() vdf_shortcuts.append((app_name, exe, idx)) except Exception as e: self.logger.error(f"Error parsing shortcuts.vdf for exe path matching: {e}") @@ -1054,9 +1054,9 @@ class ShortcutHandler: self.logger.warning(f"Skipping invalid shortcut entry (not a dict) at index {shortcut_id} in {shortcuts_file}") continue - app_name = shortcut.get('AppName') - exe_path = shortcut.get('Exe', '').strip('"') - start_dir = shortcut.get('StartDir', '').strip('"') + app_name = shortcut.get('AppName', shortcut.get('appname')) + exe_path = shortcut.get('Exe', shortcut.get('exe', '')).strip('"') + start_dir = shortcut.get('StartDir', shortcut.get('startdir', '')).strip('"') # Check if the base name of the exe_path matches the target if app_name and start_dir and os.path.basename(exe_path) == executable_name: diff --git a/jackify/backend/handlers/wabbajack_parser.py b/jackify/backend/handlers/wabbajack_parser.py index 9b3f396..951540c 100644 --- a/jackify/backend/handlers/wabbajack_parser.py +++ b/jackify/backend/handlers/wabbajack_parser.py @@ -132,7 +132,8 @@ class WabbajackParser: 'falloutnv': 'Fallout New Vegas', 'oblivion': 'Oblivion', 'starfield': 'Starfield', - 'oblivion_remastered': 'Oblivion Remastered' + 'oblivion_remastered': 'Oblivion Remastered', + 'enderal': 'Enderal' } return [display_names.get(game, game) for game in self.supported_games] diff --git a/jackify/backend/handlers/wine_utils.py b/jackify/backend/handlers/wine_utils.py index d5ec8ae..ffde4c0 100644 --- a/jackify/backend/handlers/wine_utils.py +++ b/jackify/backend/handlers/wine_utils.py @@ -13,7 +13,7 @@ import shutil import time from pathlib import Path import glob -from typing import Optional, Tuple +from typing import Optional, Tuple, List, Dict from .subprocess_utils import get_clean_subprocess_env # Initialize logger @@ -643,7 +643,20 @@ class WineUtils: wine_bin = subdir / "files/bin/wine" if wine_bin.is_file(): return str(wine_bin) - # Fallback: Try 'Proton - Experimental' if present + # Fallback: Try user's configured Proton version + try: + from .config_handler import ConfigHandler + config = ConfigHandler() + fallback_path = config.get('proton_path', 'auto') + if fallback_path != 'auto': + fallback_wine_bin = Path(fallback_path) / "files/bin/wine" + if fallback_wine_bin.is_file(): + logger.warning(f"Requested Proton version '{proton_version}' not found. Falling back to user's configured version.") + return str(fallback_wine_bin) + except Exception: + pass + + # Final fallback: Try 'Proton - Experimental' if present for base_path in steam_common_paths: wine_bin = base_path / "Proton - Experimental" / "files/bin/wine" if wine_bin.is_file(): @@ -698,4 +711,276 @@ class WineUtils: proton_path = str(Path(wine_bin).parent.parent) logger.debug(f"Found Proton path: {proton_path}") - return compatdata_path, proton_path, wine_bin \ No newline at end of file + return compatdata_path, proton_path, wine_bin + + @staticmethod + def get_steam_library_paths() -> List[Path]: + """ + Get all Steam library paths including standard locations. + + Returns: + List of Path objects for Steam library directories + """ + steam_paths = [ + Path.home() / ".steam/steam/steamapps/common", + Path.home() / ".local/share/Steam/steamapps/common", + Path.home() / ".steam/root/steamapps/common" + ] + + # Return only existing paths + return [path for path in steam_paths if path.exists()] + + @staticmethod + def get_compatibility_tool_paths() -> List[Path]: + """ + Get all compatibility tool paths for GE-Proton and other custom Proton versions. + + Returns: + List of Path objects for compatibility tool directories + """ + compat_paths = [ + Path.home() / ".steam/steam/compatibilitytools.d", + Path.home() / ".local/share/Steam/compatibilitytools.d" + ] + + # Return only existing paths + return [path for path in compat_paths if path.exists()] + + @staticmethod + def scan_ge_proton_versions() -> List[Dict[str, any]]: + """ + Scan for available GE-Proton versions in compatibilitytools.d directories. + + Returns: + List of dicts with version info, sorted by priority (newest first) + """ + logger.info("Scanning for available GE-Proton versions...") + + found_versions = [] + compat_paths = WineUtils.get_compatibility_tool_paths() + + if not compat_paths: + logger.warning("No compatibility tool paths found") + return [] + + for compat_path in compat_paths: + logger.debug(f"Scanning compatibility tools: {compat_path}") + + try: + # Look for GE-Proton directories + for proton_dir in compat_path.iterdir(): + if not proton_dir.is_dir(): + continue + + dir_name = proton_dir.name + if not dir_name.startswith("GE-Proton"): + continue + + # Check for wine binary + wine_bin = proton_dir / "files" / "bin" / "wine" + if not wine_bin.exists() or not wine_bin.is_file(): + logger.debug(f"Skipping {dir_name} - no wine binary found") + continue + + # Parse version from directory name (e.g., "GE-Proton10-16") + version_match = re.match(r'GE-Proton(\d+)-(\d+)', dir_name) + if version_match: + major_ver = int(version_match.group(1)) + minor_ver = int(version_match.group(2)) + + # Calculate priority: GE-Proton gets highest priority + # Priority format: 200 (base) + major*10 + minor (e.g., 200 + 100 + 16 = 316) + priority = 200 + (major_ver * 10) + minor_ver + + found_versions.append({ + 'name': dir_name, + 'path': proton_dir, + 'wine_bin': wine_bin, + 'priority': priority, + 'major_version': major_ver, + 'minor_version': minor_ver, + 'type': 'GE-Proton' + }) + logger.debug(f"Found {dir_name} at {proton_dir} (priority: {priority})") + else: + logger.debug(f"Skipping {dir_name} - unknown GE-Proton version format") + + except Exception as e: + logger.warning(f"Error scanning {compat_path}: {e}") + + # Sort by priority (highest first, so newest GE-Proton versions come first) + found_versions.sort(key=lambda x: x['priority'], reverse=True) + + logger.info(f"Found {len(found_versions)} GE-Proton version(s)") + return found_versions + + @staticmethod + def scan_valve_proton_versions() -> List[Dict[str, any]]: + """ + Scan for available Valve Proton versions with fallback priority. + + Returns: + List of dicts with version info, sorted by priority (best first) + """ + logger.info("Scanning for available Valve Proton versions...") + + found_versions = [] + steam_libs = WineUtils.get_steam_library_paths() + + if not steam_libs: + logger.warning("No Steam library paths found") + return [] + + # Priority order for Valve Proton versions + # Note: GE-Proton uses 200+ range, so Valve Proton gets 100+ range + preferred_versions = [ + ("Proton - Experimental", 150), # Higher priority than regular Valve Proton + ("Proton 10.0", 140), + ("Proton 9.0", 130), + ("Proton 9.0 (Beta)", 125) + ] + + for steam_path in steam_libs: + logger.debug(f"Scanning Steam library: {steam_path}") + + for version_name, priority in preferred_versions: + proton_path = steam_path / version_name + wine_bin = proton_path / "files" / "bin" / "wine" + + if wine_bin.exists() and wine_bin.is_file(): + found_versions.append({ + 'name': version_name, + 'path': proton_path, + 'wine_bin': wine_bin, + 'priority': priority, + 'type': 'Valve-Proton' + }) + logger.debug(f"Found {version_name} at {proton_path}") + + # Sort by priority (highest first) + found_versions.sort(key=lambda x: x['priority'], reverse=True) + + # Remove duplicates while preserving order + unique_versions = [] + seen_names = set() + for version in found_versions: + if version['name'] not in seen_names: + unique_versions.append(version) + seen_names.add(version['name']) + + logger.info(f"Found {len(unique_versions)} unique Valve Proton version(s)") + return unique_versions + + @staticmethod + def scan_all_proton_versions() -> List[Dict[str, any]]: + """ + Scan for all available Proton versions (GE-Proton + Valve Proton) with unified priority. + + Priority Chain (highest to lowest): + 1. GE-Proton10-16+ (priority 316+) + 2. GE-Proton10-* (priority 200+) + 3. Proton - Experimental (priority 150) + 4. Proton 10.0 (priority 140) + 5. Proton 9.0 (priority 130) + 6. Proton 9.0 (Beta) (priority 125) + + Returns: + List of dicts with version info, sorted by priority (best first) + """ + logger.info("Scanning for all available Proton versions...") + + all_versions = [] + + # Scan GE-Proton versions (highest priority) + ge_versions = WineUtils.scan_ge_proton_versions() + all_versions.extend(ge_versions) + + # Scan Valve Proton versions + valve_versions = WineUtils.scan_valve_proton_versions() + all_versions.extend(valve_versions) + + # Sort by priority (highest first) + all_versions.sort(key=lambda x: x['priority'], reverse=True) + + # Remove duplicates while preserving order + unique_versions = [] + seen_names = set() + for version in all_versions: + if version['name'] not in seen_names: + unique_versions.append(version) + seen_names.add(version['name']) + + if unique_versions: + logger.info(f"Found {len(unique_versions)} total Proton version(s)") + logger.info(f"Best available: {unique_versions[0]['name']} ({unique_versions[0]['type']})") + else: + logger.warning("No Proton versions found") + + return unique_versions + + @staticmethod + def select_best_proton() -> Optional[Dict[str, any]]: + """ + Select the best available Proton version (GE-Proton or Valve Proton) using unified precedence. + + Returns: + Dict with version info for the best Proton, or None if none found + """ + available_versions = WineUtils.scan_all_proton_versions() + + if not available_versions: + logger.warning("No compatible Proton versions found") + return None + + # Return the highest priority version (first in sorted list) + best_version = available_versions[0] + logger.info(f"Selected best Proton version: {best_version['name']} ({best_version['type']})") + return best_version + + @staticmethod + def select_best_valve_proton() -> Optional[Dict[str, any]]: + """ + Select the best available Valve Proton version using fallback precedence. + Note: This method is kept for backward compatibility. Consider using select_best_proton() instead. + + Returns: + Dict with version info for the best Proton, or None if none found + """ + available_versions = WineUtils.scan_valve_proton_versions() + + if not available_versions: + logger.warning("No compatible Valve Proton versions found") + return None + + # Return the highest priority version (first in sorted list) + best_version = available_versions[0] + logger.info(f"Selected Valve Proton version: {best_version['name']}") + return best_version + + @staticmethod + def check_proton_requirements() -> Tuple[bool, str, Optional[Dict[str, any]]]: + """ + Check if compatible Proton version is available for workflows. + + Returns: + tuple: (requirements_met, status_message, proton_info) + - requirements_met: True if compatible Proton found + - status_message: Human-readable status for display to user + - proton_info: Dict with Proton details if found, None otherwise + """ + logger.info("Checking Proton requirements for workflow...") + + # Scan for available Proton versions (includes GE-Proton + Valve Proton) + best_proton = WineUtils.select_best_proton() + + if best_proton: + # Compatible Proton found + proton_type = best_proton.get('type', 'Unknown') + status_msg = f"✓ Using {best_proton['name']} ({proton_type}) for this workflow" + logger.info(f"Proton requirements satisfied: {best_proton['name']} ({proton_type})") + return True, status_msg, best_proton + else: + # No compatible Proton found + status_msg = "✗ No compatible Proton version found (GE-Proton 10+, Proton 9+, 10, or Experimental required)" + logger.warning("Proton requirements not met - no compatible version found") + return False, status_msg, None \ No newline at end of file diff --git a/jackify/backend/services/automated_prefix_service.py b/jackify/backend/services/automated_prefix_service.py index 6c0f3f0..490dff4 100644 --- a/jackify/backend/services/automated_prefix_service.py +++ b/jackify/backend/services/automated_prefix_service.py @@ -38,6 +38,44 @@ class AutomatedPrefixService: """Get consistent progress timestamp""" from jackify.shared.timing import get_timestamp return get_timestamp() + + def _get_user_proton_version(self): + """Get user's preferred Proton version from config, with fallback to auto-detection""" + try: + from jackify.backend.handlers.config_handler import ConfigHandler + from jackify.backend.handlers.wine_utils import WineUtils + + config_handler = ConfigHandler() + user_proton_path = config_handler.get('proton_path', 'auto') + + if user_proton_path == 'auto': + # Use enhanced fallback logic with GE-Proton preference + logger.info("User selected auto-detect, using GE-Proton → Experimental → Proton precedence") + return WineUtils.select_best_proton() + else: + # User has selected a specific Proton version + # Use the exact directory name for Steam config.vdf + try: + proton_version = os.path.basename(user_proton_path) + # GE-Proton uses exact directory name, Valve Proton needs lowercase conversion + if proton_version.startswith('GE-Proton'): + # Keep GE-Proton name exactly as-is + steam_proton_name = proton_version + else: + # Convert Valve Proton names to Steam's format + steam_proton_name = proton_version.lower().replace(' - ', '_').replace(' ', '_').replace('-', '_') + if not steam_proton_name.startswith('proton'): + steam_proton_name = f"proton_{steam_proton_name}" + + logger.info(f"Using user-selected Proton: {steam_proton_name}") + return steam_proton_name + except Exception as e: + logger.warning(f"Invalid user Proton path '{user_proton_path}', falling back to auto: {e}") + return WineUtils.select_best_proton() + + except Exception as e: + logger.error(f"Failed to get user Proton preference, using default: {e}") + return "proton_experimental" def create_shortcut_with_native_service(self, shortcut_name: str, exe_path: str, @@ -87,6 +125,9 @@ class AutomatedPrefixService: logger.warning(f"Could not generate STEAM_COMPAT_MOUNTS, using default: {e}") launch_options = "%command%" + # Get user's preferred Proton version + proton_version = self._get_user_proton_version() + # Create shortcut with Proton using native service success, app_id = steam_service.create_shortcut_with_proton( app_name=shortcut_name, @@ -94,7 +135,7 @@ class AutomatedPrefixService: start_dir=modlist_install_dir, launch_options=launch_options, tags=["Jackify"], - proton_version="proton_experimental" + proton_version=proton_version ) if success and app_id: @@ -292,13 +333,13 @@ class AutomatedPrefixService: logger.error(f"Steam userdata directory not found: {userdata_dir}") return None - # Find the first user directory (most systems have only one user) - user_dirs = [d for d in userdata_dir.iterdir() if d.is_dir() and d.name.isdigit()] + # Find user directories (excluding user 0 which is a system account) + user_dirs = [d for d in userdata_dir.iterdir() if d.is_dir() and d.name.isdigit() and d.name != "0"] if not user_dirs: - logger.error("No Steam user directories found in userdata") + logger.error("No valid Steam user directories found in userdata (user 0 is not valid)") return None - # Use the first user directory found + # Use the first valid user directory found user_dir = user_dirs[0] shortcuts_path = user_dir / "config" / "shortcuts.vdf" @@ -2499,8 +2540,8 @@ echo Prefix creation complete. # Try the standard Steam userdata path steam_userdata_path = Path.home() / ".steam" / "steam" / "userdata" if steam_userdata_path.exists(): - # Find the first user directory (usually only one on Steam Deck) - user_dirs = [d for d in steam_userdata_path.iterdir() if d.is_dir() and d.name.isdigit()] + # Find user directories (excluding user 0 which is a system account) + user_dirs = [d for d in steam_userdata_path.iterdir() if d.is_dir() and d.name.isdigit() and d.name != "0"] if user_dirs: localconfig_path = user_dirs[0] / "config" / "localconfig.vdf" if localconfig_path.exists(): @@ -2601,8 +2642,11 @@ echo Prefix creation complete. env = os.environ.copy() env['STEAM_COMPAT_CLIENT_INSTALL_PATH'] = str(steam_root) env['STEAM_COMPAT_DATA_PATH'] = str(compatdata_dir / str(abs(appid))) - # Suppress GUI windows by unsetting DISPLAY + # Suppress GUI windows using jackify-engine's proven approach env['DISPLAY'] = '' + env['WAYLAND_DISPLAY'] = '' + env['WINEDEBUG'] = '-all' + env['WINEDLLOVERRIDES'] = 'msdia80.dll=n;conhost.exe=d;cmd.exe=d' # Create the compatdata directory compat_dir = compatdata_dir / str(abs(appid)) @@ -2616,7 +2660,9 @@ echo Prefix creation complete. cmd = [str(proton_path), 'run', 'wineboot', '-u'] logger.info(f"Running: {' '.join(cmd)}") - result = subprocess.run(cmd, env=env, capture_output=True, text=True, timeout=60) + # Use jackify-engine's approach: UseShellExecute=false, CreateNoWindow=true equivalent + result = subprocess.run(cmd, env=env, capture_output=True, text=True, timeout=60, + shell=False, creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0)) logger.info(f"Proton exit code: {result.returncode}") if result.stdout: @@ -2718,26 +2764,39 @@ echo Prefix creation complete. def verify_compatibility_tool_persists(self, appid: int) -> bool: """ - Verify that the compatibility tool setting persists. - + Verify that the compatibility tool setting persists with correct Proton version. + Args: appid: The AppID to check - + Returns: - True if compatibility tool is set, False otherwise + True if compatibility tool is correctly set, False otherwise """ try: config_path = Path.home() / ".steam/steam/config/config.vdf" - with open(config_path, 'r') as f: + if not config_path.exists(): + logger.warning("Steam config.vdf not found") + return False + + with open(config_path, 'r', encoding='utf-8') as f: content = f.read() - + + # Check if AppID exists and has a Proton version set if f'"{appid}"' in content: - logger.info(" Compatibility tool persists") - return True + # Get the expected Proton version + expected_proton = self._get_user_proton_version() + + # Look for the Proton version in the compatibility tool mapping + if expected_proton in content: + logger.info(f" Compatibility tool persists: {expected_proton}") + return True + else: + logger.warning(f"AppID {appid} found but Proton version '{expected_proton}' not set") + return False else: logger.warning("Compatibility tool not found") return False - + except Exception as e: logger.error(f"Error verifying compatibility tool: {e}") return False diff --git a/jackify/backend/services/native_steam_service.py b/jackify/backend/services/native_steam_service.py index 3b4e101..eddb44d 100644 --- a/jackify/backend/services/native_steam_service.py +++ b/jackify/backend/services/native_steam_service.py @@ -40,13 +40,13 @@ class NativeSteamService: logger.error("Steam userdata directory not found") return False - # Find the first user directory (usually there's only one) - user_dirs = [d for d in self.userdata_path.iterdir() if d.is_dir() and d.name.isdigit()] + # Find user directories (excluding user 0 which is a system account) + user_dirs = [d for d in self.userdata_path.iterdir() if d.is_dir() and d.name.isdigit() and d.name != "0"] if not user_dirs: - logger.error("No Steam user directories found") + logger.error("No valid Steam user directories found (user 0 is not valid for shortcuts)") return False - # Use the first user directory + # Use the first valid user directory user_dir = user_dirs[0] self.user_id = user_dir.name self.user_config_path = user_dir / "config" @@ -327,17 +327,27 @@ class NativeSteamService: logger.error(f"Error setting Proton version: {e}") return False - def create_shortcut_with_proton(self, app_name: str, exe_path: str, start_dir: str = None, + def create_shortcut_with_proton(self, app_name: str, exe_path: str, start_dir: str = None, launch_options: str = "%command%", tags: List[str] = None, - proton_version: str = "proton_experimental") -> Tuple[bool, Optional[int]]: + proton_version: str = None) -> Tuple[bool, Optional[int]]: """ Complete workflow: Create shortcut and set Proton version. - + This is the main method that replaces STL entirely. - + Returns: (success, app_id) - Success status and the AppID """ + # Auto-detect best Proton version if none provided + if proton_version is None: + try: + from jackify.backend.core.modlist_operations import _get_user_proton_version + proton_version = _get_user_proton_version() + logger.info(f"Auto-detected Proton version: {proton_version}") + except Exception as e: + logger.warning(f"Failed to auto-detect Proton, falling back to experimental: {e}") + proton_version = "proton_experimental" + logger.info(f"Creating shortcut with Proton: '{app_name}' -> '{proton_version}'") # Step 1: Create the shortcut diff --git a/jackify/engine/Wabbajack.CLI.Builder.dll b/jackify/engine/Wabbajack.CLI.Builder.dll index e40be4c..069ed6e 100644 Binary files a/jackify/engine/Wabbajack.CLI.Builder.dll and b/jackify/engine/Wabbajack.CLI.Builder.dll differ diff --git a/jackify/engine/Wabbajack.Common.dll b/jackify/engine/Wabbajack.Common.dll index cc54e48..351e404 100644 Binary files a/jackify/engine/Wabbajack.Common.dll and b/jackify/engine/Wabbajack.Common.dll differ diff --git a/jackify/engine/Wabbajack.Compiler.dll b/jackify/engine/Wabbajack.Compiler.dll index 9a74999..8c45d85 100644 Binary files a/jackify/engine/Wabbajack.Compiler.dll and b/jackify/engine/Wabbajack.Compiler.dll differ diff --git a/jackify/engine/Wabbajack.Compression.BSA.dll b/jackify/engine/Wabbajack.Compression.BSA.dll index 6c9be8a..98205eb 100644 Binary files a/jackify/engine/Wabbajack.Compression.BSA.dll and b/jackify/engine/Wabbajack.Compression.BSA.dll differ diff --git a/jackify/engine/Wabbajack.Compression.Zip.dll b/jackify/engine/Wabbajack.Compression.Zip.dll index a2d75a2..c8a6b0c 100644 Binary files a/jackify/engine/Wabbajack.Compression.Zip.dll and b/jackify/engine/Wabbajack.Compression.Zip.dll differ diff --git a/jackify/engine/Wabbajack.Configuration.dll b/jackify/engine/Wabbajack.Configuration.dll index 0855672..2031231 100644 Binary files a/jackify/engine/Wabbajack.Configuration.dll and b/jackify/engine/Wabbajack.Configuration.dll differ diff --git a/jackify/engine/Wabbajack.DTOs.dll b/jackify/engine/Wabbajack.DTOs.dll index 63a953a..e5d7367 100644 Binary files a/jackify/engine/Wabbajack.DTOs.dll and b/jackify/engine/Wabbajack.DTOs.dll differ diff --git a/jackify/engine/Wabbajack.Downloaders.Bethesda.dll b/jackify/engine/Wabbajack.Downloaders.Bethesda.dll index 80b0eb3..28df717 100644 Binary files a/jackify/engine/Wabbajack.Downloaders.Bethesda.dll and b/jackify/engine/Wabbajack.Downloaders.Bethesda.dll differ diff --git a/jackify/engine/Wabbajack.Downloaders.Dispatcher.dll b/jackify/engine/Wabbajack.Downloaders.Dispatcher.dll index 915bcb7..3416aed 100644 Binary files a/jackify/engine/Wabbajack.Downloaders.Dispatcher.dll and b/jackify/engine/Wabbajack.Downloaders.Dispatcher.dll differ diff --git a/jackify/engine/Wabbajack.Downloaders.GameFile.dll b/jackify/engine/Wabbajack.Downloaders.GameFile.dll index 84faeee..2db7ded 100644 Binary files a/jackify/engine/Wabbajack.Downloaders.GameFile.dll and b/jackify/engine/Wabbajack.Downloaders.GameFile.dll differ diff --git a/jackify/engine/Wabbajack.Downloaders.GoogleDrive.dll b/jackify/engine/Wabbajack.Downloaders.GoogleDrive.dll index 1f09c62..766afb9 100644 Binary files a/jackify/engine/Wabbajack.Downloaders.GoogleDrive.dll and b/jackify/engine/Wabbajack.Downloaders.GoogleDrive.dll differ diff --git a/jackify/engine/Wabbajack.Downloaders.Http.dll b/jackify/engine/Wabbajack.Downloaders.Http.dll index b9f70ac..ddbdb45 100644 Binary files a/jackify/engine/Wabbajack.Downloaders.Http.dll and b/jackify/engine/Wabbajack.Downloaders.Http.dll differ diff --git a/jackify/engine/Wabbajack.Downloaders.IPS4OAuth2Downloader.dll b/jackify/engine/Wabbajack.Downloaders.IPS4OAuth2Downloader.dll index f6853a7..17f8b9a 100644 Binary files a/jackify/engine/Wabbajack.Downloaders.IPS4OAuth2Downloader.dll and b/jackify/engine/Wabbajack.Downloaders.IPS4OAuth2Downloader.dll differ diff --git a/jackify/engine/Wabbajack.Downloaders.Interfaces.dll b/jackify/engine/Wabbajack.Downloaders.Interfaces.dll index 41cedad..5a5680c 100644 Binary files a/jackify/engine/Wabbajack.Downloaders.Interfaces.dll and b/jackify/engine/Wabbajack.Downloaders.Interfaces.dll differ diff --git a/jackify/engine/Wabbajack.Downloaders.Manual.dll b/jackify/engine/Wabbajack.Downloaders.Manual.dll index dc2c504..1816990 100644 Binary files a/jackify/engine/Wabbajack.Downloaders.Manual.dll and b/jackify/engine/Wabbajack.Downloaders.Manual.dll differ diff --git a/jackify/engine/Wabbajack.Downloaders.MediaFire.dll b/jackify/engine/Wabbajack.Downloaders.MediaFire.dll index 963c733..4394662 100644 Binary files a/jackify/engine/Wabbajack.Downloaders.MediaFire.dll and b/jackify/engine/Wabbajack.Downloaders.MediaFire.dll differ diff --git a/jackify/engine/Wabbajack.Downloaders.Mega.dll b/jackify/engine/Wabbajack.Downloaders.Mega.dll index b6c3388..9a079c2 100644 Binary files a/jackify/engine/Wabbajack.Downloaders.Mega.dll and b/jackify/engine/Wabbajack.Downloaders.Mega.dll differ diff --git a/jackify/engine/Wabbajack.Downloaders.ModDB.dll b/jackify/engine/Wabbajack.Downloaders.ModDB.dll index f1d4c93..8c42eb1 100644 Binary files a/jackify/engine/Wabbajack.Downloaders.ModDB.dll and b/jackify/engine/Wabbajack.Downloaders.ModDB.dll differ diff --git a/jackify/engine/Wabbajack.Downloaders.Nexus.dll b/jackify/engine/Wabbajack.Downloaders.Nexus.dll index 0f1aa3c..6bf0cfd 100644 Binary files a/jackify/engine/Wabbajack.Downloaders.Nexus.dll and b/jackify/engine/Wabbajack.Downloaders.Nexus.dll differ diff --git a/jackify/engine/Wabbajack.Downloaders.VerificationCache.dll b/jackify/engine/Wabbajack.Downloaders.VerificationCache.dll index 0d32d9a..0df14ef 100644 Binary files a/jackify/engine/Wabbajack.Downloaders.VerificationCache.dll and b/jackify/engine/Wabbajack.Downloaders.VerificationCache.dll differ diff --git a/jackify/engine/Wabbajack.Downloaders.WabbajackCDN.dll b/jackify/engine/Wabbajack.Downloaders.WabbajackCDN.dll index 7203a01..758d973 100644 Binary files a/jackify/engine/Wabbajack.Downloaders.WabbajackCDN.dll and b/jackify/engine/Wabbajack.Downloaders.WabbajackCDN.dll differ diff --git a/jackify/engine/Wabbajack.FileExtractor.dll b/jackify/engine/Wabbajack.FileExtractor.dll index eab09a9..6797aa1 100644 Binary files a/jackify/engine/Wabbajack.FileExtractor.dll and b/jackify/engine/Wabbajack.FileExtractor.dll differ diff --git a/jackify/engine/Wabbajack.Hashing.PHash.dll b/jackify/engine/Wabbajack.Hashing.PHash.dll index 6746836..cc358fa 100644 Binary files a/jackify/engine/Wabbajack.Hashing.PHash.dll and b/jackify/engine/Wabbajack.Hashing.PHash.dll differ diff --git a/jackify/engine/Wabbajack.Hashing.xxHash64.dll b/jackify/engine/Wabbajack.Hashing.xxHash64.dll index 2c1459f..5407d92 100644 Binary files a/jackify/engine/Wabbajack.Hashing.xxHash64.dll and b/jackify/engine/Wabbajack.Hashing.xxHash64.dll differ diff --git a/jackify/engine/Wabbajack.IO.Async.dll b/jackify/engine/Wabbajack.IO.Async.dll index 94d05b6..3b619be 100644 Binary files a/jackify/engine/Wabbajack.IO.Async.dll and b/jackify/engine/Wabbajack.IO.Async.dll differ diff --git a/jackify/engine/Wabbajack.Installer.dll b/jackify/engine/Wabbajack.Installer.dll index 1dd7ba0..7593881 100644 Binary files a/jackify/engine/Wabbajack.Installer.dll and b/jackify/engine/Wabbajack.Installer.dll differ diff --git a/jackify/engine/Wabbajack.Networking.BethesdaNet.dll b/jackify/engine/Wabbajack.Networking.BethesdaNet.dll index 282661a..a4d69c9 100644 Binary files a/jackify/engine/Wabbajack.Networking.BethesdaNet.dll and b/jackify/engine/Wabbajack.Networking.BethesdaNet.dll differ diff --git a/jackify/engine/Wabbajack.Networking.Discord.dll b/jackify/engine/Wabbajack.Networking.Discord.dll index eb6ca84..ebbd796 100644 Binary files a/jackify/engine/Wabbajack.Networking.Discord.dll and b/jackify/engine/Wabbajack.Networking.Discord.dll differ diff --git a/jackify/engine/Wabbajack.Networking.GitHub.dll b/jackify/engine/Wabbajack.Networking.GitHub.dll index ff9c9c6..6124a60 100644 Binary files a/jackify/engine/Wabbajack.Networking.GitHub.dll and b/jackify/engine/Wabbajack.Networking.GitHub.dll differ diff --git a/jackify/engine/Wabbajack.Networking.Http.Interfaces.dll b/jackify/engine/Wabbajack.Networking.Http.Interfaces.dll index 76708eb..e06e883 100644 Binary files a/jackify/engine/Wabbajack.Networking.Http.Interfaces.dll and b/jackify/engine/Wabbajack.Networking.Http.Interfaces.dll differ diff --git a/jackify/engine/Wabbajack.Networking.Http.dll b/jackify/engine/Wabbajack.Networking.Http.dll index e9b54aa..c523dd2 100644 Binary files a/jackify/engine/Wabbajack.Networking.Http.dll and b/jackify/engine/Wabbajack.Networking.Http.dll differ diff --git a/jackify/engine/Wabbajack.Networking.NexusApi.dll b/jackify/engine/Wabbajack.Networking.NexusApi.dll index 863cf3b..cf6260e 100644 Binary files a/jackify/engine/Wabbajack.Networking.NexusApi.dll and b/jackify/engine/Wabbajack.Networking.NexusApi.dll differ diff --git a/jackify/engine/Wabbajack.Networking.WabbajackClientApi.dll b/jackify/engine/Wabbajack.Networking.WabbajackClientApi.dll index 14a5ec6..899578d 100644 Binary files a/jackify/engine/Wabbajack.Networking.WabbajackClientApi.dll and b/jackify/engine/Wabbajack.Networking.WabbajackClientApi.dll differ diff --git a/jackify/engine/Wabbajack.Paths.IO.dll b/jackify/engine/Wabbajack.Paths.IO.dll index f25da12..8091dc7 100644 Binary files a/jackify/engine/Wabbajack.Paths.IO.dll and b/jackify/engine/Wabbajack.Paths.IO.dll differ diff --git a/jackify/engine/Wabbajack.Paths.dll b/jackify/engine/Wabbajack.Paths.dll index debdb38..28c8eb2 100644 Binary files a/jackify/engine/Wabbajack.Paths.dll and b/jackify/engine/Wabbajack.Paths.dll differ diff --git a/jackify/engine/Wabbajack.RateLimiter.dll b/jackify/engine/Wabbajack.RateLimiter.dll index f77cd11..901749f 100644 Binary files a/jackify/engine/Wabbajack.RateLimiter.dll and b/jackify/engine/Wabbajack.RateLimiter.dll differ diff --git a/jackify/engine/Wabbajack.Server.Lib.dll b/jackify/engine/Wabbajack.Server.Lib.dll index 3eab3e0..9f974de 100644 Binary files a/jackify/engine/Wabbajack.Server.Lib.dll and b/jackify/engine/Wabbajack.Server.Lib.dll differ diff --git a/jackify/engine/Wabbajack.Services.OSIntegrated.dll b/jackify/engine/Wabbajack.Services.OSIntegrated.dll index 86f5a2b..b57cce1 100644 Binary files a/jackify/engine/Wabbajack.Services.OSIntegrated.dll and b/jackify/engine/Wabbajack.Services.OSIntegrated.dll differ diff --git a/jackify/engine/Wabbajack.VFS.Interfaces.dll b/jackify/engine/Wabbajack.VFS.Interfaces.dll index 9eb2c70..627f5f4 100644 Binary files a/jackify/engine/Wabbajack.VFS.Interfaces.dll and b/jackify/engine/Wabbajack.VFS.Interfaces.dll differ diff --git a/jackify/engine/Wabbajack.VFS.dll b/jackify/engine/Wabbajack.VFS.dll index 44c1077..42abcf4 100644 Binary files a/jackify/engine/Wabbajack.VFS.dll and b/jackify/engine/Wabbajack.VFS.dll differ diff --git a/jackify/engine/jackify-engine.deps.json b/jackify/engine/jackify-engine.deps.json index 0e07f79..3a447af 100644 --- a/jackify/engine/jackify-engine.deps.json +++ b/jackify/engine/jackify-engine.deps.json @@ -7,7 +7,7 @@ "targets": { ".NETCoreApp,Version=v8.0": {}, ".NETCoreApp,Version=v8.0/linux-x64": { - "jackify-engine/0.3.14": { + "jackify-engine/0.3.15": { "dependencies": { "Markdig": "0.40.0", "Microsoft.Extensions.Configuration.Json": "9.0.1", @@ -22,16 +22,16 @@ "SixLabors.ImageSharp": "3.1.6", "System.CommandLine": "2.0.0-beta4.22272.1", "System.CommandLine.NamingConventionBinder": "2.0.0-beta4.22272.1", - "Wabbajack.CLI.Builder": "0.3.14", - "Wabbajack.Downloaders.Bethesda": "0.3.14", - "Wabbajack.Downloaders.Dispatcher": "0.3.14", - "Wabbajack.Hashing.xxHash64": "0.3.14", - "Wabbajack.Networking.Discord": "0.3.14", - "Wabbajack.Networking.GitHub": "0.3.14", - "Wabbajack.Paths.IO": "0.3.14", - "Wabbajack.Server.Lib": "0.3.14", - "Wabbajack.Services.OSIntegrated": "0.3.14", - "Wabbajack.VFS": "0.3.14", + "Wabbajack.CLI.Builder": "0.3.15", + "Wabbajack.Downloaders.Bethesda": "0.3.15", + "Wabbajack.Downloaders.Dispatcher": "0.3.15", + "Wabbajack.Hashing.xxHash64": "0.3.15", + "Wabbajack.Networking.Discord": "0.3.15", + "Wabbajack.Networking.GitHub": "0.3.15", + "Wabbajack.Paths.IO": "0.3.15", + "Wabbajack.Server.Lib": "0.3.15", + "Wabbajack.Services.OSIntegrated": "0.3.15", + "Wabbajack.VFS": "0.3.15", "MegaApiClient": "1.0.0.0", "runtimepack.Microsoft.NETCore.App.Runtime.linux-x64": "8.0.19" }, @@ -1781,7 +1781,7 @@ } } }, - "Wabbajack.CLI.Builder/0.3.14": { + "Wabbajack.CLI.Builder/0.3.15": { "dependencies": { "Microsoft.Extensions.Configuration.Json": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1", @@ -1791,109 +1791,109 @@ "Microsoft.Extensions.Logging.Abstractions": "9.0.1", "System.CommandLine": "2.0.0-beta4.22272.1", "System.CommandLine.NamingConventionBinder": "2.0.0-beta4.22272.1", - "Wabbajack.Paths": "0.3.14" + "Wabbajack.Paths": "0.3.15" }, "runtime": { "Wabbajack.CLI.Builder.dll": {} } }, - "Wabbajack.Common/0.3.14": { + "Wabbajack.Common/0.3.15": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", "System.Reactive": "6.0.1", - "Wabbajack.DTOs": "0.3.14", - "Wabbajack.Networking.Http": "0.3.14", - "Wabbajack.Paths.IO": "0.3.14" + "Wabbajack.DTOs": "0.3.15", + "Wabbajack.Networking.Http": "0.3.15", + "Wabbajack.Paths.IO": "0.3.15" }, "runtime": { "Wabbajack.Common.dll": {} } }, - "Wabbajack.Compiler/0.3.14": { + "Wabbajack.Compiler/0.3.15": { "dependencies": { "F23.StringSimilarity": "6.0.0", "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Newtonsoft.Json": "13.0.3", "SixLabors.ImageSharp": "3.1.6", - "Wabbajack.Downloaders.Dispatcher": "0.3.14", - "Wabbajack.Installer": "0.3.14", - "Wabbajack.VFS": "0.3.14", + "Wabbajack.Downloaders.Dispatcher": "0.3.15", + "Wabbajack.Installer": "0.3.15", + "Wabbajack.VFS": "0.3.15", "ini-parser-netstandard": "2.5.2" }, "runtime": { "Wabbajack.Compiler.dll": {} } }, - "Wabbajack.Compression.BSA/0.3.14": { + "Wabbajack.Compression.BSA/0.3.15": { "dependencies": { "K4os.Compression.LZ4.Streams": "1.3.8", "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "SharpZipLib": "1.4.2", - "Wabbajack.Common": "0.3.14", - "Wabbajack.DTOs": "0.3.14" + "Wabbajack.Common": "0.3.15", + "Wabbajack.DTOs": "0.3.15" }, "runtime": { "Wabbajack.Compression.BSA.dll": {} } }, - "Wabbajack.Compression.Zip/0.3.14": { + "Wabbajack.Compression.Zip/0.3.15": { "dependencies": { - "Wabbajack.IO.Async": "0.3.14" + "Wabbajack.IO.Async": "0.3.15" }, "runtime": { "Wabbajack.Compression.Zip.dll": {} } }, - "Wabbajack.Configuration/0.3.14": { + "Wabbajack.Configuration/0.3.15": { "runtime": { "Wabbajack.Configuration.dll": {} } }, - "Wabbajack.Downloaders.Bethesda/0.3.14": { + "Wabbajack.Downloaders.Bethesda/0.3.15": { "dependencies": { "LibAES-CTR": "1.1.0", "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "SharpZipLib": "1.4.2", - "Wabbajack.Common": "0.3.14", - "Wabbajack.Downloaders.Interfaces": "0.3.14", - "Wabbajack.Networking.BethesdaNet": "0.3.14" + "Wabbajack.Common": "0.3.15", + "Wabbajack.Downloaders.Interfaces": "0.3.15", + "Wabbajack.Networking.BethesdaNet": "0.3.15" }, "runtime": { "Wabbajack.Downloaders.Bethesda.dll": {} } }, - "Wabbajack.Downloaders.Dispatcher/0.3.14": { + "Wabbajack.Downloaders.Dispatcher/0.3.15": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Newtonsoft.Json": "13.0.3", "SixLabors.ImageSharp": "3.1.6", - "Wabbajack.Downloaders.Bethesda": "0.3.14", - "Wabbajack.Downloaders.GameFile": "0.3.14", - "Wabbajack.Downloaders.GoogleDrive": "0.3.14", - "Wabbajack.Downloaders.Http": "0.3.14", - "Wabbajack.Downloaders.IPS4OAuth2Downloader": "0.3.14", - "Wabbajack.Downloaders.Interfaces": "0.3.14", - "Wabbajack.Downloaders.Manual": "0.3.14", - "Wabbajack.Downloaders.MediaFire": "0.3.14", - "Wabbajack.Downloaders.Mega": "0.3.14", - "Wabbajack.Downloaders.ModDB": "0.3.14", - "Wabbajack.Downloaders.Nexus": "0.3.14", - "Wabbajack.Downloaders.VerificationCache": "0.3.14", - "Wabbajack.Downloaders.WabbajackCDN": "0.3.14", - "Wabbajack.Networking.WabbajackClientApi": "0.3.14" + "Wabbajack.Downloaders.Bethesda": "0.3.15", + "Wabbajack.Downloaders.GameFile": "0.3.15", + "Wabbajack.Downloaders.GoogleDrive": "0.3.15", + "Wabbajack.Downloaders.Http": "0.3.15", + "Wabbajack.Downloaders.IPS4OAuth2Downloader": "0.3.15", + "Wabbajack.Downloaders.Interfaces": "0.3.15", + "Wabbajack.Downloaders.Manual": "0.3.15", + "Wabbajack.Downloaders.MediaFire": "0.3.15", + "Wabbajack.Downloaders.Mega": "0.3.15", + "Wabbajack.Downloaders.ModDB": "0.3.15", + "Wabbajack.Downloaders.Nexus": "0.3.15", + "Wabbajack.Downloaders.VerificationCache": "0.3.15", + "Wabbajack.Downloaders.WabbajackCDN": "0.3.15", + "Wabbajack.Networking.WabbajackClientApi": "0.3.15" }, "runtime": { "Wabbajack.Downloaders.Dispatcher.dll": {} } }, - "Wabbajack.Downloaders.GameFile/0.3.14": { + "Wabbajack.Downloaders.GameFile/0.3.15": { "dependencies": { "GameFinder.StoreHandlers.EADesktop": "4.5.0", "GameFinder.StoreHandlers.EGS": "4.5.0", @@ -1903,360 +1903,360 @@ "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "SixLabors.ImageSharp": "3.1.6", - "Wabbajack.Downloaders.Interfaces": "0.3.14", - "Wabbajack.VFS": "0.3.14" + "Wabbajack.Downloaders.Interfaces": "0.3.15", + "Wabbajack.VFS": "0.3.15" }, "runtime": { "Wabbajack.Downloaders.GameFile.dll": {} } }, - "Wabbajack.Downloaders.GoogleDrive/0.3.14": { + "Wabbajack.Downloaders.GoogleDrive/0.3.15": { "dependencies": { "HtmlAgilityPack": "1.11.72", "Microsoft.AspNetCore.Http.Extensions": "2.3.0", "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", - "Wabbajack.Common": "0.3.14", - "Wabbajack.DTOs": "0.3.14", - "Wabbajack.Downloaders.Interfaces": "0.3.14", - "Wabbajack.Networking.Http": "0.3.14", - "Wabbajack.Networking.Http.Interfaces": "0.3.14" + "Wabbajack.Common": "0.3.15", + "Wabbajack.DTOs": "0.3.15", + "Wabbajack.Downloaders.Interfaces": "0.3.15", + "Wabbajack.Networking.Http": "0.3.15", + "Wabbajack.Networking.Http.Interfaces": "0.3.15" }, "runtime": { "Wabbajack.Downloaders.GoogleDrive.dll": {} } }, - "Wabbajack.Downloaders.Http/0.3.14": { + "Wabbajack.Downloaders.Http/0.3.15": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", - "Wabbajack.Common": "0.3.14", - "Wabbajack.DTOs": "0.3.14", - "Wabbajack.Downloaders.Interfaces": "0.3.14", - "Wabbajack.Networking.BethesdaNet": "0.3.14", - "Wabbajack.Networking.Http.Interfaces": "0.3.14", - "Wabbajack.Paths.IO": "0.3.14" + "Wabbajack.Common": "0.3.15", + "Wabbajack.DTOs": "0.3.15", + "Wabbajack.Downloaders.Interfaces": "0.3.15", + "Wabbajack.Networking.BethesdaNet": "0.3.15", + "Wabbajack.Networking.Http.Interfaces": "0.3.15", + "Wabbajack.Paths.IO": "0.3.15" }, "runtime": { "Wabbajack.Downloaders.Http.dll": {} } }, - "Wabbajack.Downloaders.Interfaces/0.3.14": { + "Wabbajack.Downloaders.Interfaces/0.3.15": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", - "Wabbajack.Compression.Zip": "0.3.14", - "Wabbajack.DTOs": "0.3.14", - "Wabbajack.Paths.IO": "0.3.14" + "Wabbajack.Compression.Zip": "0.3.15", + "Wabbajack.DTOs": "0.3.15", + "Wabbajack.Paths.IO": "0.3.15" }, "runtime": { "Wabbajack.Downloaders.Interfaces.dll": {} } }, - "Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.14": { + "Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.15": { "dependencies": { "F23.StringSimilarity": "6.0.0", "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", - "Wabbajack.Common": "0.3.14", - "Wabbajack.Downloaders.Interfaces": "0.3.14", - "Wabbajack.Networking.Http": "0.3.14", - "Wabbajack.Networking.Http.Interfaces": "0.3.14" + "Wabbajack.Common": "0.3.15", + "Wabbajack.Downloaders.Interfaces": "0.3.15", + "Wabbajack.Networking.Http": "0.3.15", + "Wabbajack.Networking.Http.Interfaces": "0.3.15" }, "runtime": { "Wabbajack.Downloaders.IPS4OAuth2Downloader.dll": {} } }, - "Wabbajack.Downloaders.Manual/0.3.14": { + "Wabbajack.Downloaders.Manual/0.3.15": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", - "Wabbajack.Common": "0.3.14", - "Wabbajack.Downloaders.Interfaces": "0.3.14" + "Wabbajack.Common": "0.3.15", + "Wabbajack.Downloaders.Interfaces": "0.3.15" }, "runtime": { "Wabbajack.Downloaders.Manual.dll": {} } }, - "Wabbajack.Downloaders.MediaFire/0.3.14": { + "Wabbajack.Downloaders.MediaFire/0.3.15": { "dependencies": { "HtmlAgilityPack": "1.11.72", "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", - "Wabbajack.Common": "0.3.14", - "Wabbajack.Downloaders.Interfaces": "0.3.14", - "Wabbajack.Networking.Http.Interfaces": "0.3.14" + "Wabbajack.Common": "0.3.15", + "Wabbajack.Downloaders.Interfaces": "0.3.15", + "Wabbajack.Networking.Http.Interfaces": "0.3.15" }, "runtime": { "Wabbajack.Downloaders.MediaFire.dll": {} } }, - "Wabbajack.Downloaders.Mega/0.3.14": { + "Wabbajack.Downloaders.Mega/0.3.15": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Newtonsoft.Json": "13.0.3", - "Wabbajack.Common": "0.3.14", - "Wabbajack.Downloaders.Interfaces": "0.3.14", - "Wabbajack.Paths.IO": "0.3.14" + "Wabbajack.Common": "0.3.15", + "Wabbajack.Downloaders.Interfaces": "0.3.15", + "Wabbajack.Paths.IO": "0.3.15" }, "runtime": { "Wabbajack.Downloaders.Mega.dll": {} } }, - "Wabbajack.Downloaders.ModDB/0.3.14": { + "Wabbajack.Downloaders.ModDB/0.3.15": { "dependencies": { "HtmlAgilityPack": "1.11.72", "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Newtonsoft.Json": "13.0.3", - "Wabbajack.Common": "0.3.14", - "Wabbajack.Downloaders.Interfaces": "0.3.14", - "Wabbajack.Networking.Http": "0.3.14", - "Wabbajack.Networking.Http.Interfaces": "0.3.14" + "Wabbajack.Common": "0.3.15", + "Wabbajack.Downloaders.Interfaces": "0.3.15", + "Wabbajack.Networking.Http": "0.3.15", + "Wabbajack.Networking.Http.Interfaces": "0.3.15" }, "runtime": { "Wabbajack.Downloaders.ModDB.dll": {} } }, - "Wabbajack.Downloaders.Nexus/0.3.14": { + "Wabbajack.Downloaders.Nexus/0.3.15": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", - "Wabbajack.DTOs": "0.3.14", - "Wabbajack.Downloaders.Interfaces": "0.3.14", - "Wabbajack.Hashing.xxHash64": "0.3.14", - "Wabbajack.Networking.Http": "0.3.14", - "Wabbajack.Networking.Http.Interfaces": "0.3.14", - "Wabbajack.Networking.NexusApi": "0.3.14", - "Wabbajack.Paths": "0.3.14" + "Wabbajack.DTOs": "0.3.15", + "Wabbajack.Downloaders.Interfaces": "0.3.15", + "Wabbajack.Hashing.xxHash64": "0.3.15", + "Wabbajack.Networking.Http": "0.3.15", + "Wabbajack.Networking.Http.Interfaces": "0.3.15", + "Wabbajack.Networking.NexusApi": "0.3.15", + "Wabbajack.Paths": "0.3.15" }, "runtime": { "Wabbajack.Downloaders.Nexus.dll": {} } }, - "Wabbajack.Downloaders.VerificationCache/0.3.14": { + "Wabbajack.Downloaders.VerificationCache/0.3.15": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Stub.System.Data.SQLite.Core.NetStandard": "1.0.119", - "Wabbajack.DTOs": "0.3.14", - "Wabbajack.Paths.IO": "0.3.14" + "Wabbajack.DTOs": "0.3.15", + "Wabbajack.Paths.IO": "0.3.15" }, "runtime": { "Wabbajack.Downloaders.VerificationCache.dll": {} } }, - "Wabbajack.Downloaders.WabbajackCDN/0.3.14": { + "Wabbajack.Downloaders.WabbajackCDN/0.3.15": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Toolkit.HighPerformance": "7.1.2", - "Wabbajack.Common": "0.3.14", - "Wabbajack.Downloaders.Interfaces": "0.3.14", - "Wabbajack.Networking.Http": "0.3.14", - "Wabbajack.RateLimiter": "0.3.14" + "Wabbajack.Common": "0.3.15", + "Wabbajack.Downloaders.Interfaces": "0.3.15", + "Wabbajack.Networking.Http": "0.3.15", + "Wabbajack.RateLimiter": "0.3.15" }, "runtime": { "Wabbajack.Downloaders.WabbajackCDN.dll": {} } }, - "Wabbajack.DTOs/0.3.14": { + "Wabbajack.DTOs/0.3.15": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", - "Wabbajack.Hashing.xxHash64": "0.3.14", - "Wabbajack.Paths": "0.3.14" + "Wabbajack.Hashing.xxHash64": "0.3.15", + "Wabbajack.Paths": "0.3.15" }, "runtime": { "Wabbajack.DTOs.dll": {} } }, - "Wabbajack.FileExtractor/0.3.14": { + "Wabbajack.FileExtractor/0.3.15": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", "OMODFramework": "3.0.1", - "Wabbajack.Common": "0.3.14", - "Wabbajack.Compression.BSA": "0.3.14", - "Wabbajack.Hashing.PHash": "0.3.14", - "Wabbajack.Paths": "0.3.14" + "Wabbajack.Common": "0.3.15", + "Wabbajack.Compression.BSA": "0.3.15", + "Wabbajack.Hashing.PHash": "0.3.15", + "Wabbajack.Paths": "0.3.15" }, "runtime": { "Wabbajack.FileExtractor.dll": {} } }, - "Wabbajack.Hashing.PHash/0.3.14": { + "Wabbajack.Hashing.PHash/0.3.15": { "dependencies": { "BCnEncoder.Net.ImageSharp": "1.1.1", "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Shipwreck.Phash": "0.5.0", "SixLabors.ImageSharp": "3.1.6", - "Wabbajack.Common": "0.3.14", - "Wabbajack.DTOs": "0.3.14", - "Wabbajack.Paths": "0.3.14", - "Wabbajack.Paths.IO": "0.3.14" + "Wabbajack.Common": "0.3.15", + "Wabbajack.DTOs": "0.3.15", + "Wabbajack.Paths": "0.3.15", + "Wabbajack.Paths.IO": "0.3.15" }, "runtime": { "Wabbajack.Hashing.PHash.dll": {} } }, - "Wabbajack.Hashing.xxHash64/0.3.14": { + "Wabbajack.Hashing.xxHash64/0.3.15": { "dependencies": { - "Wabbajack.Paths": "0.3.14", - "Wabbajack.RateLimiter": "0.3.14" + "Wabbajack.Paths": "0.3.15", + "Wabbajack.RateLimiter": "0.3.15" }, "runtime": { "Wabbajack.Hashing.xxHash64.dll": {} } }, - "Wabbajack.Installer/0.3.14": { + "Wabbajack.Installer/0.3.15": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Newtonsoft.Json": "13.0.3", "Octopus.Octodiff": "2.0.548", "SixLabors.ImageSharp": "3.1.6", - "Wabbajack.DTOs": "0.3.14", - "Wabbajack.Downloaders.Dispatcher": "0.3.14", - "Wabbajack.Downloaders.GameFile": "0.3.14", - "Wabbajack.FileExtractor": "0.3.14", - "Wabbajack.Networking.WabbajackClientApi": "0.3.14", - "Wabbajack.Paths": "0.3.14", - "Wabbajack.Paths.IO": "0.3.14", - "Wabbajack.VFS": "0.3.14", + "Wabbajack.DTOs": "0.3.15", + "Wabbajack.Downloaders.Dispatcher": "0.3.15", + "Wabbajack.Downloaders.GameFile": "0.3.15", + "Wabbajack.FileExtractor": "0.3.15", + "Wabbajack.Networking.WabbajackClientApi": "0.3.15", + "Wabbajack.Paths": "0.3.15", + "Wabbajack.Paths.IO": "0.3.15", + "Wabbajack.VFS": "0.3.15", "ini-parser-netstandard": "2.5.2" }, "runtime": { "Wabbajack.Installer.dll": {} } }, - "Wabbajack.IO.Async/0.3.14": { + "Wabbajack.IO.Async/0.3.15": { "runtime": { "Wabbajack.IO.Async.dll": {} } }, - "Wabbajack.Networking.BethesdaNet/0.3.14": { + "Wabbajack.Networking.BethesdaNet/0.3.15": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", - "Wabbajack.DTOs": "0.3.14", - "Wabbajack.Networking.Http": "0.3.14", - "Wabbajack.Networking.Http.Interfaces": "0.3.14" + "Wabbajack.DTOs": "0.3.15", + "Wabbajack.Networking.Http": "0.3.15", + "Wabbajack.Networking.Http.Interfaces": "0.3.15" }, "runtime": { "Wabbajack.Networking.BethesdaNet.dll": {} } }, - "Wabbajack.Networking.Discord/0.3.14": { + "Wabbajack.Networking.Discord/0.3.15": { "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", - "Wabbajack.Networking.Http.Interfaces": "0.3.14" + "Wabbajack.Networking.Http.Interfaces": "0.3.15" }, "runtime": { "Wabbajack.Networking.Discord.dll": {} } }, - "Wabbajack.Networking.GitHub/0.3.14": { + "Wabbajack.Networking.GitHub/0.3.15": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Octokit": "14.0.0", - "Wabbajack.DTOs": "0.3.14", - "Wabbajack.Networking.Http.Interfaces": "0.3.14" + "Wabbajack.DTOs": "0.3.15", + "Wabbajack.Networking.Http.Interfaces": "0.3.15" }, "runtime": { "Wabbajack.Networking.GitHub.dll": {} } }, - "Wabbajack.Networking.Http/0.3.14": { + "Wabbajack.Networking.Http/0.3.15": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Http": "9.0.1", "Microsoft.Extensions.Logging": "9.0.1", - "Wabbajack.Configuration": "0.3.14", - "Wabbajack.Downloaders.Interfaces": "0.3.14", - "Wabbajack.Hashing.xxHash64": "0.3.14", - "Wabbajack.Networking.Http.Interfaces": "0.3.14", - "Wabbajack.Paths": "0.3.14", - "Wabbajack.Paths.IO": "0.3.14" + "Wabbajack.Configuration": "0.3.15", + "Wabbajack.Downloaders.Interfaces": "0.3.15", + "Wabbajack.Hashing.xxHash64": "0.3.15", + "Wabbajack.Networking.Http.Interfaces": "0.3.15", + "Wabbajack.Paths": "0.3.15", + "Wabbajack.Paths.IO": "0.3.15" }, "runtime": { "Wabbajack.Networking.Http.dll": {} } }, - "Wabbajack.Networking.Http.Interfaces/0.3.14": { + "Wabbajack.Networking.Http.Interfaces/0.3.15": { "dependencies": { - "Wabbajack.Hashing.xxHash64": "0.3.14" + "Wabbajack.Hashing.xxHash64": "0.3.15" }, "runtime": { "Wabbajack.Networking.Http.Interfaces.dll": {} } }, - "Wabbajack.Networking.NexusApi/0.3.14": { + "Wabbajack.Networking.NexusApi/0.3.15": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", - "Wabbajack.DTOs": "0.3.14", - "Wabbajack.Networking.Http": "0.3.14", - "Wabbajack.Networking.Http.Interfaces": "0.3.14", - "Wabbajack.Networking.WabbajackClientApi": "0.3.14" + "Wabbajack.DTOs": "0.3.15", + "Wabbajack.Networking.Http": "0.3.15", + "Wabbajack.Networking.Http.Interfaces": "0.3.15", + "Wabbajack.Networking.WabbajackClientApi": "0.3.15" }, "runtime": { "Wabbajack.Networking.NexusApi.dll": {} } }, - "Wabbajack.Networking.WabbajackClientApi/0.3.14": { + "Wabbajack.Networking.WabbajackClientApi/0.3.15": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Octokit": "14.0.0", - "Wabbajack.Common": "0.3.14", - "Wabbajack.DTOs": "0.3.14", - "Wabbajack.Paths.IO": "0.3.14", - "Wabbajack.VFS.Interfaces": "0.3.14", + "Wabbajack.Common": "0.3.15", + "Wabbajack.DTOs": "0.3.15", + "Wabbajack.Paths.IO": "0.3.15", + "Wabbajack.VFS.Interfaces": "0.3.15", "YamlDotNet": "16.3.0" }, "runtime": { "Wabbajack.Networking.WabbajackClientApi.dll": {} } }, - "Wabbajack.Paths/0.3.14": { + "Wabbajack.Paths/0.3.15": { "runtime": { "Wabbajack.Paths.dll": {} } }, - "Wabbajack.Paths.IO/0.3.14": { + "Wabbajack.Paths.IO/0.3.15": { "dependencies": { - "Wabbajack.Paths": "0.3.14", + "Wabbajack.Paths": "0.3.15", "shortid": "4.0.0" }, "runtime": { "Wabbajack.Paths.IO.dll": {} } }, - "Wabbajack.RateLimiter/0.3.14": { + "Wabbajack.RateLimiter/0.3.15": { "runtime": { "Wabbajack.RateLimiter.dll": {} } }, - "Wabbajack.Server.Lib/0.3.14": { + "Wabbajack.Server.Lib/0.3.15": { "dependencies": { "FluentFTP": "52.0.0", "Microsoft.Extensions.DependencyInjection": "9.0.1", @@ -2264,58 +2264,58 @@ "Nettle": "3.0.0", "Newtonsoft.Json": "13.0.3", "SixLabors.ImageSharp": "3.1.6", - "Wabbajack.Common": "0.3.14", - "Wabbajack.Networking.Http.Interfaces": "0.3.14", - "Wabbajack.Services.OSIntegrated": "0.3.14" + "Wabbajack.Common": "0.3.15", + "Wabbajack.Networking.Http.Interfaces": "0.3.15", + "Wabbajack.Services.OSIntegrated": "0.3.15" }, "runtime": { "Wabbajack.Server.Lib.dll": {} } }, - "Wabbajack.Services.OSIntegrated/0.3.14": { + "Wabbajack.Services.OSIntegrated/0.3.15": { "dependencies": { "DeviceId": "6.8.0", "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Newtonsoft.Json": "13.0.3", "SixLabors.ImageSharp": "3.1.6", - "Wabbajack.Compiler": "0.3.14", - "Wabbajack.Downloaders.Dispatcher": "0.3.14", - "Wabbajack.Installer": "0.3.14", - "Wabbajack.Networking.BethesdaNet": "0.3.14", - "Wabbajack.Networking.Discord": "0.3.14", - "Wabbajack.VFS": "0.3.14" + "Wabbajack.Compiler": "0.3.15", + "Wabbajack.Downloaders.Dispatcher": "0.3.15", + "Wabbajack.Installer": "0.3.15", + "Wabbajack.Networking.BethesdaNet": "0.3.15", + "Wabbajack.Networking.Discord": "0.3.15", + "Wabbajack.VFS": "0.3.15" }, "runtime": { "Wabbajack.Services.OSIntegrated.dll": {} } }, - "Wabbajack.VFS/0.3.14": { + "Wabbajack.VFS/0.3.15": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", "SixLabors.ImageSharp": "3.1.6", "System.Data.SQLite.Core": "1.0.119", - "Wabbajack.Common": "0.3.14", - "Wabbajack.FileExtractor": "0.3.14", - "Wabbajack.Hashing.PHash": "0.3.14", - "Wabbajack.Hashing.xxHash64": "0.3.14", - "Wabbajack.Paths": "0.3.14", - "Wabbajack.Paths.IO": "0.3.14", - "Wabbajack.VFS.Interfaces": "0.3.14" + "Wabbajack.Common": "0.3.15", + "Wabbajack.FileExtractor": "0.3.15", + "Wabbajack.Hashing.PHash": "0.3.15", + "Wabbajack.Hashing.xxHash64": "0.3.15", + "Wabbajack.Paths": "0.3.15", + "Wabbajack.Paths.IO": "0.3.15", + "Wabbajack.VFS.Interfaces": "0.3.15" }, "runtime": { "Wabbajack.VFS.dll": {} } }, - "Wabbajack.VFS.Interfaces/0.3.14": { + "Wabbajack.VFS.Interfaces/0.3.15": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", - "Wabbajack.DTOs": "0.3.14", - "Wabbajack.Hashing.xxHash64": "0.3.14", - "Wabbajack.Paths": "0.3.14" + "Wabbajack.DTOs": "0.3.15", + "Wabbajack.Hashing.xxHash64": "0.3.15", + "Wabbajack.Paths": "0.3.15" }, "runtime": { "Wabbajack.VFS.Interfaces.dll": {} @@ -2332,7 +2332,7 @@ } }, "libraries": { - "jackify-engine/0.3.14": { + "jackify-engine/0.3.15": { "type": "project", "serviceable": false, "sha512": "" @@ -3021,202 +3021,202 @@ "path": "yamldotnet/16.3.0", "hashPath": "yamldotnet.16.3.0.nupkg.sha512" }, - "Wabbajack.CLI.Builder/0.3.14": { + "Wabbajack.CLI.Builder/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Common/0.3.14": { + "Wabbajack.Common/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Compiler/0.3.14": { + "Wabbajack.Compiler/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Compression.BSA/0.3.14": { + "Wabbajack.Compression.BSA/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Compression.Zip/0.3.14": { + "Wabbajack.Compression.Zip/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Configuration/0.3.14": { + "Wabbajack.Configuration/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.Bethesda/0.3.14": { + "Wabbajack.Downloaders.Bethesda/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.Dispatcher/0.3.14": { + "Wabbajack.Downloaders.Dispatcher/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.GameFile/0.3.14": { + "Wabbajack.Downloaders.GameFile/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.GoogleDrive/0.3.14": { + "Wabbajack.Downloaders.GoogleDrive/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.Http/0.3.14": { + "Wabbajack.Downloaders.Http/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.Interfaces/0.3.14": { + "Wabbajack.Downloaders.Interfaces/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.14": { + "Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.Manual/0.3.14": { + "Wabbajack.Downloaders.Manual/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.MediaFire/0.3.14": { + "Wabbajack.Downloaders.MediaFire/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.Mega/0.3.14": { + "Wabbajack.Downloaders.Mega/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.ModDB/0.3.14": { + "Wabbajack.Downloaders.ModDB/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.Nexus/0.3.14": { + "Wabbajack.Downloaders.Nexus/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.VerificationCache/0.3.14": { + "Wabbajack.Downloaders.VerificationCache/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.WabbajackCDN/0.3.14": { + "Wabbajack.Downloaders.WabbajackCDN/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.DTOs/0.3.14": { + "Wabbajack.DTOs/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.FileExtractor/0.3.14": { + "Wabbajack.FileExtractor/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Hashing.PHash/0.3.14": { + "Wabbajack.Hashing.PHash/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Hashing.xxHash64/0.3.14": { + "Wabbajack.Hashing.xxHash64/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Installer/0.3.14": { + "Wabbajack.Installer/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.IO.Async/0.3.14": { + "Wabbajack.IO.Async/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Networking.BethesdaNet/0.3.14": { + "Wabbajack.Networking.BethesdaNet/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Networking.Discord/0.3.14": { + "Wabbajack.Networking.Discord/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Networking.GitHub/0.3.14": { + "Wabbajack.Networking.GitHub/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Networking.Http/0.3.14": { + "Wabbajack.Networking.Http/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Networking.Http.Interfaces/0.3.14": { + "Wabbajack.Networking.Http.Interfaces/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Networking.NexusApi/0.3.14": { + "Wabbajack.Networking.NexusApi/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Networking.WabbajackClientApi/0.3.14": { + "Wabbajack.Networking.WabbajackClientApi/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Paths/0.3.14": { + "Wabbajack.Paths/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Paths.IO/0.3.14": { + "Wabbajack.Paths.IO/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.RateLimiter/0.3.14": { + "Wabbajack.RateLimiter/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Server.Lib/0.3.14": { + "Wabbajack.Server.Lib/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Services.OSIntegrated/0.3.14": { + "Wabbajack.Services.OSIntegrated/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.VFS/0.3.14": { + "Wabbajack.VFS/0.3.15": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.VFS.Interfaces/0.3.14": { + "Wabbajack.VFS.Interfaces/0.3.15": { "type": "project", "serviceable": false, "sha512": "" diff --git a/jackify/engine/jackify-engine.dll b/jackify/engine/jackify-engine.dll index 14aa184..fee0c2c 100644 Binary files a/jackify/engine/jackify-engine.dll and b/jackify/engine/jackify-engine.dll differ diff --git a/jackify/frontends/gui/main.py b/jackify/frontends/gui/main.py index cc6c3a5..9432cd9 100644 --- a/jackify/frontends/gui/main.py +++ b/jackify/frontends/gui/main.py @@ -7,6 +7,7 @@ This replaces the legacy jackify_gui implementation with a refactored architectu import sys import os +import logging from pathlib import Path # Suppress xkbcommon locale errors (harmless but annoying) @@ -81,6 +82,9 @@ if '--env-diagnostic' in sys.argv: from jackify import __version__ as jackify_version +# Initialize logger +logger = logging.getLogger(__name__) + if '--help' in sys.argv or '-h' in sys.argv: print("""Jackify - Native Linux Modlist Manager\n\nUsage:\n jackify [--cli] [--debug] [--version] [--help]\n\nOptions:\n --cli Launch CLI frontend\n --debug Enable debug logging\n --version Show version and exit\n --help, -h Show this help message and exit\n\nIf no options are given, the GUI will launch by default.\n""") sys.exit(0) @@ -98,7 +102,7 @@ sys.path.insert(0, str(src_dir)) from PySide6.QtWidgets import ( QApplication, QMainWindow, QWidget, QLabel, QVBoxLayout, QPushButton, - QStackedWidget, QHBoxLayout, QDialog, QFormLayout, QLineEdit, QCheckBox, QSpinBox, QMessageBox, QGroupBox, QGridLayout, QFileDialog, QToolButton, QStyle + QStackedWidget, QHBoxLayout, QDialog, QFormLayout, QLineEdit, QCheckBox, QSpinBox, QMessageBox, QGroupBox, QGridLayout, QFileDialog, QToolButton, QStyle, QComboBox ) from PySide6.QtCore import Qt, QEvent from PySide6.QtGui import QIcon @@ -298,6 +302,33 @@ class SettingsDialog(QDialog): main_layout.addWidget(api_group) main_layout.addSpacing(12) + # --- Proton Version Section --- + proton_group = QGroupBox("Proton Version") + proton_group.setStyleSheet("QGroupBox { border: 1px solid #555; border-radius: 6px; margin-top: 8px; padding: 8px; background: #23282d; } QGroupBox:title { subcontrol-origin: margin; left: 10px; padding: 0 3px 0 3px; font-weight: bold; color: #fff; }") + proton_layout = QHBoxLayout() + proton_group.setLayout(proton_layout) + + self.proton_dropdown = QComboBox() + self.proton_dropdown.setToolTip("Select Proton version for shortcut creation and texture processing") + self.proton_dropdown.setMinimumWidth(200) + + # Populate Proton dropdown + self._populate_proton_dropdown() + + # Refresh button for Proton detection + refresh_btn = QPushButton("↻") + refresh_btn.setFixedSize(30, 30) + refresh_btn.setToolTip("Refresh Proton version list") + refresh_btn.clicked.connect(self._refresh_proton_dropdown) + + proton_layout.addWidget(QLabel("Proton Version:")) + proton_layout.addWidget(self.proton_dropdown) + proton_layout.addWidget(refresh_btn) + proton_layout.addStretch() + + main_layout.addWidget(proton_group) + main_layout.addSpacing(12) + # --- Directories & Paths Section --- dir_group = QGroupBox("Directories & Paths") dir_group.setStyleSheet("QGroupBox { border: 1px solid #555; border-radius: 6px; margin-top: 8px; padding: 8px; background: #23282d; } QGroupBox:title { subcontrol-origin: margin; left: 10px; padding: 0 3px 0 3px; font-weight: bold; color: #fff; }") @@ -447,6 +478,85 @@ class SettingsDialog(QDialog): api_key = text.strip() self.config_handler.save_api_key(api_key) + def _get_proton_10_path(self): + """Get Proton 10 path if available, fallback to auto""" + try: + from jackify.backend.handlers.wine_utils import WineUtils + available_protons = WineUtils.scan_valve_proton_versions() + + # Look for Proton 10.x + for proton in available_protons: + if proton['version'].startswith('10.'): + return proton['path'] + + # Fallback to auto if no Proton 10 found + return 'auto' + except: + return 'auto' + + def _populate_proton_dropdown(self): + """Populate Proton version dropdown with detected versions (includes GE-Proton and Valve Proton)""" + try: + from jackify.backend.handlers.wine_utils import WineUtils + + # Get all available Proton versions (GE-Proton + Valve Proton) + available_protons = WineUtils.scan_all_proton_versions() + + # Add "Auto" option first + self.proton_dropdown.addItem("Auto", "auto") + + # Add detected Proton versions with type indicators + for proton in available_protons: + proton_name = proton.get('name', 'Unknown Proton') + proton_type = proton.get('type', 'Unknown') + + # Format display name to show type for clarity + if proton_type == 'GE-Proton': + display_name = f"{proton_name} (GE)" + elif proton_type == 'Valve-Proton': + display_name = f"{proton_name}" + else: + display_name = proton_name + + self.proton_dropdown.addItem(display_name, str(proton['path'])) + + # Load saved preference and determine UI selection + saved_proton = self.config_handler.get('proton_path', self._get_proton_10_path()) + + # Check if saved path matches any specific Proton in dropdown + found_match = False + for i in range(self.proton_dropdown.count()): + if self.proton_dropdown.itemData(i) == saved_proton: + self.proton_dropdown.setCurrentIndex(i) + found_match = True + break + + # If no exact match found, check if it's a resolved auto-selection + if not found_match and saved_proton != "auto": + # This means config has a resolved path from previous "Auto" selection + # Show "Auto" in UI since user chose auto-detection + for i in range(self.proton_dropdown.count()): + if self.proton_dropdown.itemData(i) == "auto": + self.proton_dropdown.setCurrentIndex(i) + break + + except Exception as e: + logger.error(f"Failed to populate Proton dropdown: {e}") + # Fallback: just show auto + self.proton_dropdown.addItem("Auto", "auto") + + def _refresh_proton_dropdown(self): + """Refresh Proton dropdown with latest detected versions""" + current_selection = self.proton_dropdown.currentData() + self.proton_dropdown.clear() + self._populate_proton_dropdown() + + # Restore selection if still available + for i in range(self.proton_dropdown.count()): + if self.proton_dropdown.itemData(i) == current_selection: + self.proton_dropdown.setCurrentIndex(i) + break + def _save(self): # Validate values for k, (multithreading_checkbox, max_tasks_spin) in self.resource_edits.items(): @@ -490,6 +600,33 @@ class SettingsDialog(QDialog): # Save jackify data directory (always store actual path, never None) jackify_data_dir = self.jackify_data_dir_edit.text().strip() self.config_handler.set("jackify_data_dir", jackify_data_dir) + + # Save Proton selection - resolve "auto" to actual path + selected_proton_path = self.proton_dropdown.currentData() + if selected_proton_path == "auto": + # Resolve "auto" to actual best Proton path using unified detection + try: + from jackify.backend.handlers.wine_utils import WineUtils + best_proton = WineUtils.select_best_proton() + + if best_proton: + resolved_path = str(best_proton['path']) + resolved_version = best_proton['name'] + else: + resolved_path = "auto" + resolved_version = "auto" + except: + resolved_path = "auto" + resolved_version = "auto" + else: + # User selected specific Proton version + resolved_path = selected_proton_path + # Extract version from dropdown text + resolved_version = self.proton_dropdown.currentText() + + self.config_handler.set("proton_path", resolved_path) + self.config_handler.set("proton_version", resolved_version) + self.config_handler.save_config() # Refresh cached paths in GUI screens if Jackify directory changed diff --git a/jackify/frontends/gui/screens/configure_new_modlist.py b/jackify/frontends/gui/screens/configure_new_modlist.py index 43e2395..b20860e 100644 --- a/jackify/frontends/gui/screens/configure_new_modlist.py +++ b/jackify/frontends/gui/screens/configure_new_modlist.py @@ -22,6 +22,7 @@ from jackify.backend.handlers.config_handler import ConfigHandler from ..dialogs import SuccessDialog from PySide6.QtWidgets import QApplication from jackify.frontends.gui.services.message_service import MessageService +from jackify.shared.resolution_utils import get_resolution_fallback def debug_print(message): """Print debug message only if debug mode is enabled""" @@ -1033,7 +1034,7 @@ class ConfigureNewModlistScreen(QWidget): try: # Get resolution from UI resolution = self.resolution_combo.currentText() - resolution_value = resolution.split()[0] if resolution != "Leave unchanged" else '2560x1600' + resolution_value = resolution.split()[0] if resolution != "Leave unchanged" else None # Update the context with the new AppID (same format as manual steps) mo2_exe_path = self.install_dir_edit.text().strip() @@ -1082,7 +1083,7 @@ class ConfigureNewModlistScreen(QWidget): nexus_api_key='', # Not needed for configuration modlist_value=self.context.get('modlist_value'), modlist_source=self.context.get('modlist_source', 'identifier'), - resolution=self.context.get('resolution', '2560x1600'), + resolution=self.context.get('resolution') or get_resolution_fallback(None), skip_confirmation=True ) diff --git a/jackify/frontends/gui/screens/install_modlist.py b/jackify/frontends/gui/screens/install_modlist.py index c140ed5..97f12d5 100644 --- a/jackify/frontends/gui/screens/install_modlist.py +++ b/jackify/frontends/gui/screens/install_modlist.py @@ -367,6 +367,10 @@ class InstallModlistScreen(QWidget): self.resolution_service = ResolutionService() self.config_handler = ConfigHandler() self.protontricks_service = ProtontricksDetectionService() + + # Somnium guidance tracking + self._show_somnium_guidance = False + self._somnium_install_dir = None # Scroll tracking for professional auto-scroll behavior self._user_manually_scrolled = False @@ -1356,7 +1360,8 @@ class InstallModlistScreen(QWidget): 'oblivion': 'oblivion', 'starfield': 'starfield', 'oblivion_remastered': 'oblivion_remastered', - 'enderal': 'enderal' + 'enderal': 'enderal', + 'enderal special edition': 'enderal' } game_type = game_mapping.get(game_name.lower()) debug_print(f"DEBUG: Mapped game_name '{game_name}' to game_type: '{game_type}'") @@ -1373,6 +1378,7 @@ class InstallModlistScreen(QWidget): # Check if game is supported debug_print(f"DEBUG: Checking if game_type '{game_type}' is supported") + debug_print(f"DEBUG: game_type='{game_type}', game_name='{game_name}'") is_supported = self.wabbajack_parser.is_supported_game(game_type) if game_type else False debug_print(f"DEBUG: is_supported_game('{game_type}') returned: {is_supported}") @@ -1760,10 +1766,26 @@ class InstallModlistScreen(QWidget): final_exe_path = os.path.join(install_dir, "ModOrganizer.exe") if not os.path.exists(final_exe_path): - self._safe_append_text(f"ERROR: ModOrganizer.exe not found at {final_exe_path}") - MessageService.critical(self, "ModOrganizer.exe Not Found", - f"ModOrganizer.exe not found at:\n{final_exe_path}\n\nCannot proceed with automated setup.") - return + # Check if this is Somnium specifically (uses files/ subdirectory) + modlist_name_lower = modlist_name.lower() + if "somnium" in modlist_name_lower: + somnium_exe_path = os.path.join(install_dir, "files", "ModOrganizer.exe") + if os.path.exists(somnium_exe_path): + final_exe_path = somnium_exe_path + self._safe_append_text(f"Detected Somnium modlist - will proceed with automated setup") + # Show Somnium guidance popup after automated workflow completes + self._show_somnium_guidance = True + self._somnium_install_dir = install_dir + else: + self._safe_append_text(f"ERROR: Somnium ModOrganizer.exe not found at {somnium_exe_path}") + MessageService.critical(self, "Somnium ModOrganizer.exe Not Found", + f"Expected Somnium ModOrganizer.exe not found at:\n{somnium_exe_path}\n\nCannot proceed with automated setup.") + return + else: + self._safe_append_text(f"ERROR: ModOrganizer.exe not found at {final_exe_path}") + MessageService.critical(self, "ModOrganizer.exe Not Found", + f"ModOrganizer.exe not found at:\n{final_exe_path}\n\nCannot proceed with automated setup.") + return # Run automated prefix creation in separate thread from PySide6.QtCore import QThread, Signal @@ -1940,6 +1962,10 @@ class InstallModlistScreen(QWidget): self._enable_controls_after_operation() if success: + # Check if we need to show Somnium guidance + if self._show_somnium_guidance: + self._show_somnium_post_install_guidance() + # Show celebration SuccessDialog after the entire workflow from ..dialogs import SuccessDialog import time @@ -2041,11 +2067,20 @@ class InstallModlistScreen(QWidget): self.cancel_btn.setVisible(True) self.cancel_install_btn.setVisible(False) + def _get_mo2_path(self, install_dir, modlist_name): + """Get ModOrganizer.exe path, handling Somnium's non-standard structure""" + mo2_exe_path = os.path.join(install_dir, "ModOrganizer.exe") + if not os.path.exists(mo2_exe_path) and "somnium" in modlist_name.lower(): + somnium_path = os.path.join(install_dir, "files", "ModOrganizer.exe") + if os.path.exists(somnium_path): + mo2_exe_path = somnium_path + return mo2_exe_path + def validate_manual_steps_completion(self): """Validate that manual steps were actually completed and handle retry logic""" modlist_name = self.modlist_name_edit.text().strip() install_dir = self.install_dir_edit.text().strip() - mo2_exe_path = os.path.join(install_dir, "ModOrganizer.exe") + mo2_exe_path = self._get_mo2_path(install_dir, modlist_name) # Add delay to allow Steam filesystem updates to complete self._safe_append_text("Waiting for Steam filesystem updates to complete...") @@ -2283,7 +2318,7 @@ class InstallModlistScreen(QWidget): updated_context = { 'name': modlist_name, 'path': install_dir, - 'mo2_exe_path': os.path.join(install_dir, "ModOrganizer.exe"), + 'mo2_exe_path': self._get_mo2_path(install_dir, modlist_name), 'modlist_value': None, 'modlist_source': None, 'resolution': getattr(self, '_current_resolution', '2560x1600'), @@ -2381,7 +2416,7 @@ class InstallModlistScreen(QWidget): updated_context = { 'name': modlist_name, 'path': install_dir, - 'mo2_exe_path': os.path.join(install_dir, "ModOrganizer.exe"), + 'mo2_exe_path': self._get_mo2_path(install_dir, modlist_name), 'modlist_value': None, 'modlist_source': None, 'resolution': getattr(self, '_current_resolution', '2560x1600'), @@ -2616,6 +2651,26 @@ class InstallModlistScreen(QWidget): self._safe_append_text("Installation cancelled by user.") + def _show_somnium_post_install_guidance(self): + """Show guidance popup for Somnium post-installation steps""" + from ..widgets.message_service import MessageService + + guidance_text = f"""Somnium Post-Installation Required

+Due to Somnium's non-standard folder structure, you need to manually update the binary paths in ModOrganizer:

+1. Launch the Steam shortcut created for Somnium
+2. In ModOrganizer, go to Settings → Executables
+3. For each executable entry (SKSE64, etc.), update the binary path to point to:
+{self._somnium_install_dir}/files/root/Enderal Special Edition/skse64_loader.exe

+Note: Full Somnium support will be added in a future Jackify update.

+You can also refer to the Somnium installation guide at:
+https://wiki.scenicroute.games/Somnium/1_Installation.html
""" + + MessageService.information(self, "Somnium Setup Required", guidance_text) + + # Reset the guidance flag + self._show_somnium_guidance = False + self._somnium_install_dir = None + def cancel_and_cleanup(self): """Handle Cancel button - clean up processes and go back""" self.cleanup_processes() diff --git a/jackify/frontends/gui/widgets/unsupported_game_dialog.py b/jackify/frontends/gui/widgets/unsupported_game_dialog.py index 48b8570..1df6686 100644 --- a/jackify/frontends/gui/widgets/unsupported_game_dialog.py +++ b/jackify/frontends/gui/widgets/unsupported_game_dialog.py @@ -94,6 +94,7 @@ class UnsupportedGameDialog(QDialog):
  • Oblivion
  • Starfield
  • Oblivion Remastered
  • +
  • Enderal
  • For unsupported games, you will need to manually configure Steam shortcuts and other post-install steps.

    @@ -113,6 +114,7 @@ class UnsupportedGameDialog(QDialog):
  • Oblivion
  • Starfield
  • Oblivion Remastered
  • +
  • Enderal
  • For unsupported games, you will need to manually configure Steam shortcuts and other post-install steps.

    diff --git a/jackify/shared/resolution_utils.py b/jackify/shared/resolution_utils.py new file mode 100644 index 0000000..a7aea84 --- /dev/null +++ b/jackify/shared/resolution_utils.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Resolution Utilities Module +Provides utility functions for handling resolution across GUI and CLI frontends +""" + +import logging +import os +from typing import Optional + +logger = logging.getLogger(__name__) + + +def get_default_resolution() -> str: + """ + Get the appropriate default resolution based on system detection and user preferences. + + Returns: + str: Resolution string (e.g., '1920x1080', '1280x800') + """ + try: + # First try to get saved resolution from config + from ..backend.services.resolution_service import ResolutionService + resolution_service = ResolutionService() + + saved_resolution = resolution_service.get_saved_resolution() + if saved_resolution and saved_resolution != 'Leave unchanged': + logger.debug(f"Using saved resolution: {saved_resolution}") + return saved_resolution + + except Exception as e: + logger.warning(f"Could not load ResolutionService: {e}") + + try: + # Check for Steam Deck + if _is_steam_deck(): + logger.debug("Steam Deck detected, using 1280x800") + return "1280x800" + + except Exception as e: + logger.warning(f"Error detecting Steam Deck: {e}") + + # Fallback to common 1080p instead of arbitrary resolution + logger.debug("Using fallback resolution: 1920x1080") + return "1920x1080" + + +def _is_steam_deck() -> bool: + """ + Detect if running on Steam Deck + + Returns: + bool: True if Steam Deck detected + """ + try: + if os.path.exists("/etc/os-release"): + with open("/etc/os-release", "r") as f: + content = f.read().lower() + return "steamdeck" in content or "steamos" in content + except Exception as e: + logger.debug(f"Error reading /etc/os-release: {e}") + + return False + + +def get_resolution_fallback(current_resolution: Optional[str]) -> str: + """ + Get appropriate resolution fallback when current resolution is invalid or None + + Args: + current_resolution: Current resolution value that might be None/invalid + + Returns: + str: Valid resolution string + """ + if current_resolution and current_resolution != 'Leave unchanged': + # Validate format + if _validate_resolution_format(current_resolution): + return current_resolution + + # Use proper default resolution logic + return get_default_resolution() + + +def _validate_resolution_format(resolution: str) -> bool: + """ + Validate resolution format + + Args: + resolution: Resolution string to validate + + Returns: + bool: True if valid WxH format + """ + import re + + if not resolution: + return False + + # Handle Steam Deck format + clean_resolution = resolution.replace(' (Steam Deck)', '') + + # Check WxH format + if re.match(r'^[0-9]+x[0-9]+$', clean_resolution): + try: + width, height = clean_resolution.split('x') + width_int, height_int = int(width), int(height) + return 0 < width_int <= 10000 and 0 < height_int <= 10000 + except ValueError: + return False + + return False \ No newline at end of file