5 Commits

Author SHA1 Message Date
Omni
d9ea1be347 Sync from development - prepare for v0.1.6.1 2025-10-21 21:11:48 +01:00
Omni
a8862475d4 Sync from development - prepare for v0.1.6.1 2025-10-21 21:07:42 +01:00
Omni
430d085287 Sync from development - prepare for v0.1.6 2025-10-16 14:44:49 +01:00
Omni
7212a58480 Sync from development - prepare for v0.1.5.3 2025-10-02 21:59:01 +01:00
Omni
80914bc76f Sync from development - prepare for v0.1.5.2 2025-10-01 22:11:14 +01:00
65 changed files with 1784 additions and 809 deletions

2
.gitignore vendored
View File

@@ -35,7 +35,7 @@ Thumbs.db
docs/ docs/
testing/ testing/
# PyInstaller build files (development only) # Build files (development only)
*.spec *.spec
hook-*.py hook-*.py
requirements-packaging.txt requirements-packaging.txt

View File

@@ -1,5 +1,58 @@
# Jackify Changelog # Jackify Changelog
## v0.1.6.1 - Fix dotnet40 install and expand Game Proton override
**Release Date:** October 21, 2025
### Bug Fixes
- **Fixed dotnet40 Installation Failures**: Resolved widespread .NET Framework installation issues affecting multiple modlists
- **Added Lost Legacy Proton 9 Override**: Automatic ENB compatibility for Lost Legacy modlist
- **Fixed Symlinked Downloads**: Automatically handles symlinked download directories to avoid Wine compatibility issues
---
## v0.1.6 - Lorerim Proton Support
**Release Date:** October 16, 2025
### New Features
- **Lorerim Proton Override**: Automatically selects Proton 9 for Lorerim installations (GE-Proton9-27 preferred)
- **Engine Update**: jackify-engine v0.3.17
---
## v0.1.5.3 - Critical Bug Fixes
**Release Date:** October 2, 2025
### Critical Bug Fixes
- **Fixed Multi-User Steam Detection**: Properly reads loginusers.vdf and converts SteamID64 to SteamID3 for accurate user identification
- **Fixed dotnet40 Installation Failures**: Hybrid approach uses protontricks for dotnet40 (reliable), winetricks for other components (fast)
- **Fixed dotnet8 Installation**: Now properly handled by winetricks instead of unimplemented pass statement
- **Fixed D: Drive Detection**: SD card detection now only applies to Steam Deck systems, not regular Linux systems
- **Fixed SD Card Mount Patterns**: Replaced hardcoded mmcblk0p1 references with dynamic path detection
- **Fixed Debug Restart UX**: Replaced PyInstaller detection with AppImage detection for proper restart behavior
---
## v0.1.5.2 - Proton Configuration & Engine Updates
**Release Date:** September 30, 2025
### Critical Bug Fixes
- **Fixed Proton Version Selection**: Wine component installation now properly honors user-selected Proton version from Settings dialog
- Previously, changing from GE-Proton to Proton Experimental in settings would still use the old version for component installation
- Fixed ConfigHandler to reload fresh configuration from disk instead of using stale cache
- Updated all Proton path retrieval across codebase to use fresh-reading methods
### Engine Updates
- **jackify-engine v0.3.16**: Updated to latest engine version with important reliability improvements
- **Sanity Check Fallback**: Added Proton 7z.exe fallback for case sensitivity extraction failures
- **Enhanced Error Messages**: Improved texconv/texdiag error messages to include original texture file names and conversion parameters
### Technical Improvements
- Enhanced configuration system reliability for multi-instance scenarios
- Improved error diagnostics for texture processing operations
- Fix Qt platform plugin discovery in AppImage distribution for improved compatibility
---
## v0.1.5.1 - Bug Fixes ## v0.1.5.1 - Bug Fixes
**Release Date:** September 28, 2025 **Release Date:** September 28, 2025
@@ -33,7 +86,8 @@
- **ModOrganizer.ini Path Format**: Fixed missing backslash in gamePath format for proper Windows path structure - **ModOrganizer.ini Path Format**: Fixed missing backslash in gamePath format for proper Windows path structure
- **SD Card Binary Paths**: Corrected binary paths to use D: drive mapping instead of raw Linux paths for SD card installs - **SD Card Binary Paths**: Corrected binary paths to use D: drive mapping instead of raw Linux paths for SD card installs
- **Proton Fallback Logic**: Enhanced fallback when user-selected Proton version is missing or invalid - **Proton Fallback Logic**: Enhanced fallback when user-selected Proton version is missing or invalid
- **Settings Persistence**: Improved configuration saving with verification and logging
#Y- **Settings Persistence**: Improved configuration saving with verification and logging
- **System Wine Elimination**: Comprehensive audit ensures Jackify never uses system wine installations - **System Wine Elimination**: Comprehensive audit ensures Jackify never uses system wine installations
- **Winetricks Reliability**: Fixed vcrun2022 installation failures and wine app crashes - **Winetricks Reliability**: Fixed vcrun2022 installation failures and wine app crashes
- **Enderal Registry Injection**: Switched from launch options to registry injection approach - **Enderal Registry Injection**: Switched from launch options to registry injection approach

View File

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

View File

@@ -30,7 +30,7 @@ def _get_user_proton_version():
from jackify.backend.handlers.wine_utils import WineUtils from jackify.backend.handlers.wine_utils import WineUtils
config_handler = ConfigHandler() config_handler = ConfigHandler()
user_proton_path = config_handler.get('proton_path', 'auto') user_proton_path = config_handler.get_proton_path()
if user_proton_path == 'auto': if user_proton_path == 'auto':
# Use enhanced fallback logic with GE-Proton preference # Use enhanced fallback logic with GE-Proton preference
@@ -156,7 +156,7 @@ class ModlistInstallCLI:
from ..models.configuration import SystemInfo from ..models.configuration import SystemInfo
self.system_info = SystemInfo(is_steamdeck=steamdeck) self.system_info = SystemInfo(is_steamdeck=steamdeck)
self.protontricks_handler = ProtontricksHandler(steamdeck=self.steamdeck) self.protontricks_handler = ProtontricksHandler(self.steamdeck)
self.shortcut_handler = ShortcutHandler(steamdeck=self.steamdeck) self.shortcut_handler = ShortcutHandler(steamdeck=self.steamdeck)
self.context = {} self.context = {}
# Use standard logging (no file handler) # Use standard logging (no file handler)

View File

@@ -38,7 +38,9 @@ class ConfigHandler:
"default_download_parent_dir": None, # Parent directory for downloads "default_download_parent_dir": None, # Parent directory for downloads
"modlist_install_base_dir": os.path.expanduser("~/Games"), # Configurable base directory for modlist installations "modlist_install_base_dir": os.path.expanduser("~/Games"), # Configurable base directory for modlist installations
"modlist_downloads_base_dir": os.path.expanduser("~/Games/Modlist_Downloads"), # Configurable base directory for downloads "modlist_downloads_base_dir": os.path.expanduser("~/Games/Modlist_Downloads"), # Configurable base directory for downloads
"jackify_data_dir": None # Configurable Jackify data directory (default: ~/Jackify) "jackify_data_dir": None, # Configurable Jackify data directory (default: ~/Jackify)
"use_winetricks_for_components": True, # True = use winetricks (faster), False = use protontricks for all (legacy)
"game_proton_path": None # Proton version for game shortcuts (can be any Proton 9+), separate from install proton
} }
# Load configuration if exists # Load configuration if exists
@@ -496,6 +498,66 @@ class ConfigHandler:
logger.error(f"Error saving modlist downloads base directory: {e}") logger.error(f"Error saving modlist downloads base directory: {e}")
return False return False
def get_proton_path(self):
"""
Retrieve the saved Install Proton path from configuration (for jackify-engine)
Always reads fresh from disk to pick up changes from Settings dialog
Returns:
str: Saved Install Proton path or 'auto' if not saved
"""
try:
# Reload config from disk to pick up changes from Settings dialog
self._load_config()
proton_path = self.settings.get("proton_path", "auto")
logger.debug(f"Retrieved fresh install proton_path from config: {proton_path}")
return proton_path
except Exception as e:
logger.error(f"Error retrieving install proton_path: {e}")
return "auto"
def get_game_proton_path(self):
"""
Retrieve the saved Game Proton path from configuration (for game shortcuts)
Falls back to install Proton path if game Proton not set
Always reads fresh from disk to pick up changes from Settings dialog
Returns:
str: Saved Game Proton path, Install Proton path, or 'auto' if not saved
"""
try:
# Reload config from disk to pick up changes from Settings dialog
self._load_config()
game_proton_path = self.settings.get("game_proton_path")
# If game proton not set or set to same_as_install, use install proton
if not game_proton_path or game_proton_path == "same_as_install":
game_proton_path = self.settings.get("proton_path", "auto")
logger.debug(f"Retrieved fresh game proton_path from config: {game_proton_path}")
return game_proton_path
except Exception as e:
logger.error(f"Error retrieving game proton_path: {e}")
return "auto"
def get_proton_version(self):
"""
Retrieve the saved Proton version from configuration
Always reads fresh from disk to pick up changes from Settings dialog
Returns:
str: Saved Proton version or 'auto' if not saved
"""
try:
# Reload config from disk to pick up changes from Settings dialog
self._load_config()
proton_version = self.settings.get("proton_version", "auto")
logger.debug(f"Retrieved fresh proton_version from config: {proton_version}")
return proton_version
except Exception as e:
logger.error(f"Error retrieving proton_version: {e}")
return "auto"
def _auto_detect_proton(self): def _auto_detect_proton(self):
"""Auto-detect and set best Proton version (includes GE-Proton and Valve Proton)""" """Auto-detect and set best Proton version (includes GE-Proton and Valve Proton)"""
try: try:

View File

@@ -79,7 +79,8 @@ class ModlistHandler:
"livingskyrim": ["dotnet40"], "livingskyrim": ["dotnet40"],
"lsiv": ["dotnet40"], "lsiv": ["dotnet40"],
"ls4": ["dotnet40"], "ls4": ["dotnet40"],
"lostlegacy": ["dotnet48"], "lorerim": ["dotnet40"],
"lostlegacy": ["dotnet40"],
} }
def __init__(self, steam_path_or_config: Union[Dict, str, Path, None] = None, def __init__(self, steam_path_or_config: Union[Dict, str, Path, None] = None,
@@ -158,7 +159,7 @@ class ModlistHandler:
self.stock_game_path = None self.stock_game_path = None
# Initialize Handlers (should happen regardless of how paths were provided) # Initialize Handlers (should happen regardless of how paths were provided)
self.protontricks_handler = ProtontricksHandler(steamdeck=self.steamdeck, logger=self.logger) self.protontricks_handler = ProtontricksHandler(self.steamdeck, logger=self.logger)
# Initialize winetricks handler for wine component installation # Initialize winetricks handler for wine component installation
from .winetricks_handler import WinetricksHandler from .winetricks_handler import WinetricksHandler
self.winetricks_handler = WinetricksHandler(logger=self.logger) self.winetricks_handler = WinetricksHandler(logger=self.logger)
@@ -315,13 +316,15 @@ class ModlistHandler:
self.modlist_dir = Path(modlist_dir_path_str) self.modlist_dir = Path(modlist_dir_path_str)
self.modlist_ini = modlist_ini_path self.modlist_ini = modlist_ini_path
# Determine if modlist is on SD card # Determine if modlist is on SD card (Steam Deck only)
# Use str() for startswith check # On non-Steam Deck systems, /media mounts should use Z: drive, not D: drive
if str(self.modlist_dir).startswith("/run/media") or str(self.modlist_dir).startswith("/media"): if (str(self.modlist_dir).startswith("/run/media") or str(self.modlist_dir).startswith("/media")) and self.steamdeck:
self.modlist_sdcard = True self.modlist_sdcard = True
self.logger.info("Modlist appears to be on an SD card.") self.logger.info("Modlist appears to be on an SD card (Steam Deck).")
else: else:
self.modlist_sdcard = False self.modlist_sdcard = False
if (str(self.modlist_dir).startswith("/run/media") or str(self.modlist_dir).startswith("/media")) and not self.steamdeck:
self.logger.info("Modlist on /media mount detected on non-Steam Deck system - using Z: drive mapping.")
# Find and set compatdata path now that we have appid # Find and set compatdata path now that we have appid
# Ensure PathHandler is available (should be initialized in __init__) # Ensure PathHandler is available (should be initialized in __init__)
@@ -346,6 +349,7 @@ class ModlistHandler:
self.engine_installed = modlist_info.get('engine_installed', False) self.engine_installed = modlist_info.get('engine_installed', False)
self.logger.debug(f" Engine Installed: {self.engine_installed}") self.logger.debug(f" Engine Installed: {self.engine_installed}")
# Call internal detection methods to populate more state # Call internal detection methods to populate more state
if not self._detect_game_variables(): if not self._detect_game_variables():
self.logger.warning("Failed to auto-detect game type after setting context.") self.logger.warning("Failed to auto-detect game type after setting context.")
@@ -685,17 +689,27 @@ class ModlistHandler:
# All modlists now use their own AppID for wine components # All modlists now use their own AppID for wine components
target_appid = self.appid target_appid = self.appid
# Use winetricks for wine component installation (faster than protontricks) # Use user's preferred component installation method (respects settings toggle)
wineprefix = self.protontricks_handler.get_wine_prefix_path(target_appid) wineprefix = self.protontricks_handler.get_wine_prefix_path(target_appid)
if not wineprefix: if not wineprefix:
self.logger.error("Failed to get WINEPREFIX path for winetricks.") self.logger.error("Failed to get WINEPREFIX path for component installation.")
print("Error: Could not determine wine prefix location.") print("Error: Could not determine wine prefix location.")
return False return False
if not self.winetricks_handler.install_wine_components(wineprefix, self.game_var_full, specific_components=components): # Use the winetricks handler which respects the user's toggle setting
self.logger.error("Failed to install Wine components. Configuration aborted.") try:
self.logger.info("Installing Wine components using user's preferred method...")
success = self.winetricks_handler.install_wine_components(wineprefix, self.game_var_full, specific_components=components)
if success:
self.logger.info("Wine component installation completed successfully")
else:
self.logger.error("Wine component installation failed")
print("Error: Failed to install necessary Wine components.")
return False
except Exception as e:
self.logger.error(f"Wine component installation failed with exception: {e}")
print("Error: Failed to install necessary Wine components.") print("Error: Failed to install necessary Wine components.")
return False # Abort on failure return False
self.logger.info("Step 4: Installing Wine components... Done") self.logger.info("Step 4: Installing Wine components... Done")
# Step 5: Ensure permissions of Modlist directory # Step 5: Ensure permissions of Modlist directory
@@ -723,6 +737,14 @@ class ModlistHandler:
self.logger.info(f"ModOrganizer.ini backed up to: {backup_path}") self.logger.info(f"ModOrganizer.ini backed up to: {backup_path}")
self.logger.info("Step 6: Backing up ModOrganizer.ini... Done") self.logger.info("Step 6: Backing up ModOrganizer.ini... Done")
# Step 6.5: Handle symlinked downloads directory
if status_callback:
status_callback(f"{self._get_progress_timestamp()} Checking for symlinked downloads directory")
self.logger.info("Step 6.5: Checking for symlinked downloads directory...")
if not self._handle_symlinked_downloads():
self.logger.warning("Warning during symlink handling (non-critical)")
self.logger.info("Step 6.5: Checking for symlinked downloads directory... Done")
# Step 7a: Detect Stock Game/Game Root path # Step 7a: Detect Stock Game/Game Root path
if status_callback: if status_callback:
status_callback(f"{self._get_progress_timestamp()} Detecting stock game path") status_callback(f"{self._get_progress_timestamp()} Detecting stock game path")
@@ -801,7 +823,7 @@ class ModlistHandler:
if self.steam_library and self.game_var_full: if self.steam_library and self.game_var_full:
vanilla_game_dir = str(Path(self.steam_library) / "steamapps" / "common" / self.game_var_full) vanilla_game_dir = str(Path(self.steam_library) / "steamapps" / "common" / self.game_var_full)
if not self.resolution_handler.update_ini_resolution( if not ResolutionHandler.update_ini_resolution(
modlist_dir=self.modlist_dir, modlist_dir=self.modlist_dir,
game_var=self.game_var_full, game_var=self.game_var_full,
set_res=self.selected_resolution, set_res=self.selected_resolution,
@@ -906,6 +928,10 @@ class ModlistHandler:
# status_callback("Configuration completed successfully!") # status_callback("Configuration completed successfully!")
self.logger.info("Configuration steps completed successfully.") self.logger.info("Configuration steps completed successfully.")
# Step 14: Re-enforce Windows 10 mode after modlist-specific configurations (matches legacy script line 1333)
self._re_enforce_windows_10_mode()
return True # Return True on success return True # Return True on success
def _detect_steam_library_info(self) -> bool: def _detect_steam_library_info(self) -> bool:
@@ -1308,4 +1334,131 @@ class ModlistHandler:
self.logger.debug("No special game type detected - standard workflow will be used") self.logger.debug("No special game type detected - standard workflow will be used")
return None return None
def _re_enforce_windows_10_mode(self):
"""
Re-enforce Windows 10 mode after modlist-specific configurations.
This matches the legacy script behavior (line 1333) where Windows 10 mode
is re-applied after modlist-specific steps to ensure consistency.
"""
try:
if not hasattr(self, 'appid') or not self.appid:
self.logger.warning("Cannot re-enforce Windows 10 mode - no AppID available")
return
from ..handlers.winetricks_handler import WinetricksHandler
from ..handlers.path_handler import PathHandler
# Get prefix path for the AppID
prefix_path = PathHandler.find_compat_data(str(self.appid))
if not prefix_path:
self.logger.warning("Cannot re-enforce Windows 10 mode - prefix path not found")
return
# 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
winetricks_handler._set_windows_10_mode(str(prefix_path), wine_binary)
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) # (Ensure EOF is clean and no extra incorrect methods exist below)

View File

@@ -68,7 +68,7 @@ class ModlistInstallCLI:
def __init__(self, menu_handler: MenuHandler, steamdeck: bool = False): def __init__(self, menu_handler: MenuHandler, steamdeck: bool = False):
self.menu_handler = menu_handler self.menu_handler = menu_handler
self.steamdeck = steamdeck self.steamdeck = steamdeck
self.protontricks_handler = ProtontricksHandler(steamdeck=steamdeck) self.protontricks_handler = ProtontricksHandler(steamdeck)
self.shortcut_handler = ShortcutHandler(steamdeck=steamdeck) self.shortcut_handler = ShortcutHandler(steamdeck=steamdeck)
self.context = {} self.context = {}
# Use standard logging (no file handler) # Use standard logging (no file handler)

View File

@@ -630,47 +630,30 @@ class PathHandler:
# Moved _find_shortcuts_vdf here from ShortcutHandler # Moved _find_shortcuts_vdf here from ShortcutHandler
def _find_shortcuts_vdf(self) -> Optional[str]: def _find_shortcuts_vdf(self) -> Optional[str]:
"""Helper to find the active shortcuts.vdf file for a user. """Helper to find the active shortcuts.vdf file for the current Steam user.
Iterates through userdata directories and returns the path to the Uses proper multi-user detection to find the correct Steam user instead
first found shortcuts.vdf file. of just taking the first found user directory.
Returns: Returns:
Optional[str]: The full path to the shortcuts.vdf file, or None if not found. Optional[str]: The full path to the shortcuts.vdf file, or None if not found.
""" """
# This implementation was moved from ShortcutHandler try:
userdata_base_paths = [ # Use native Steam service for proper multi-user detection
os.path.expanduser("~/.steam/steam/userdata"), from jackify.backend.services.native_steam_service import NativeSteamService
os.path.expanduser("~/.local/share/Steam/userdata"), steam_service = NativeSteamService()
os.path.expanduser("~/.var/app/com.valvesoftware.Steam/.local/share/Steam/userdata") shortcuts_path = steam_service.get_shortcuts_vdf_path()
]
found_vdf_path = None if shortcuts_path:
for base_path in userdata_base_paths: logger.info(f"Found shortcuts.vdf using multi-user detection: {shortcuts_path}")
if not os.path.isdir(base_path): return str(shortcuts_path)
logger.debug(f"Userdata base path not found or not a directory: {base_path}") else:
continue logger.error("Could not determine shortcuts.vdf path using multi-user detection")
logger.debug(f"Searching for user IDs in: {base_path}") return None
try:
for item in os.listdir(base_path): except Exception as e:
user_path = os.path.join(base_path, item) logger.error(f"Error using multi-user detection for shortcuts.vdf: {e}")
if os.path.isdir(user_path) and item.isdigit(): return None
logger.debug(f"Checking user directory: {user_path}")
config_path = os.path.join(user_path, "config")
shortcuts_file = os.path.join(config_path, "shortcuts.vdf")
if os.path.isfile(shortcuts_file):
logger.info(f"Found shortcuts.vdf at: {shortcuts_file}")
found_vdf_path = shortcuts_file
break # Found it for this base path
else:
logger.debug(f"shortcuts.vdf not found in {config_path}")
except OSError as e:
logger.warning(f"Could not access directory {base_path}: {e}")
continue # Try next base path
if found_vdf_path:
break # Found it in this base path
if not found_vdf_path:
logger.error("Could not find any shortcuts.vdf file in common Steam locations.")
return found_vdf_path
@staticmethod @staticmethod
def find_game_install_paths(target_appids: Dict[str, str]) -> Dict[str, Path]: def find_game_install_paths(target_appids: Dict[str, str]) -> Dict[str, Path]:

View File

@@ -41,7 +41,7 @@ class ShortcutHandler:
self._last_shortcuts_backup = None # Track the last backup path self._last_shortcuts_backup = None # Track the last backup path
self._safe_shortcuts_backup = None # Track backup made just before restart self._safe_shortcuts_backup = None # Track backup made just before restart
# Initialize ProtontricksHandler here, passing steamdeck status # Initialize ProtontricksHandler here, passing steamdeck status
self.protontricks_handler = ProtontricksHandler(steamdeck=self.steamdeck) self.protontricks_handler = ProtontricksHandler(self.steamdeck)
def _enable_tab_completion(self): def _enable_tab_completion(self):
"""Enable tab completion for file paths using the shared completer""" """Enable tab completion for file paths using the shared completer"""
@@ -964,7 +964,7 @@ class ShortcutHandler:
self.logger.info(f"Attempting to find current AppID for shortcut: '{shortcut_name}' (exe_path: '{exe_path}')") self.logger.info(f"Attempting to find current AppID for shortcut: '{shortcut_name}' (exe_path: '{exe_path}')")
try: try:
from .protontricks_handler import ProtontricksHandler # Local import from .protontricks_handler import ProtontricksHandler # Local import
pt_handler = ProtontricksHandler(steamdeck=self.steamdeck) pt_handler = ProtontricksHandler(self.steamdeck)
if not pt_handler.detect_protontricks(): if not pt_handler.detect_protontricks():
self.logger.error("Protontricks not detected") self.logger.error("Protontricks not detected")
return None return None

View File

@@ -222,8 +222,14 @@ class ValidationHandler:
def validate_steam_shortcut(self, app_id: str) -> Tuple[bool, str]: def validate_steam_shortcut(self, app_id: str) -> Tuple[bool, str]:
"""Validate a Steam shortcut.""" """Validate a Steam shortcut."""
try: try:
# Check if shortcuts.vdf exists # Use native Steam service to get proper shortcuts.vdf path with multi-user support
shortcuts_path = Path.home() / '.steam' / 'steam' / 'userdata' / '75424832' / 'config' / 'shortcuts.vdf' from jackify.backend.services.native_steam_service import NativeSteamService
steam_service = NativeSteamService()
shortcuts_path = steam_service.get_shortcuts_vdf_path()
if not shortcuts_path:
return False, "Could not determine shortcuts.vdf path (no active Steam user found)"
if not shortcuts_path.exists(): if not shortcuts_path.exists():
return False, "shortcuts.vdf not found" return False, "shortcuts.vdf not found"

View File

@@ -537,10 +537,7 @@ class WineUtils:
if "mods" in binary_path: if "mods" in binary_path:
# mods path type found # mods path type found
if modlist_sdcard: if modlist_sdcard:
path_middle = modlist_dir.split('mmcblk0p1', 1)[1] if 'mmcblk0p1' in modlist_dir else modlist_dir path_middle = WineUtils._strip_sdcard_path(modlist_dir)
# Strip /run/media/deck/UUID if present
if '/run/media/' in path_middle:
path_middle = '/' + path_middle.split('/run/media/', 1)[1].split('/', 2)[2]
else: else:
path_middle = modlist_dir path_middle = modlist_dir
@@ -550,10 +547,7 @@ class WineUtils:
elif any(x in binary_path for x in ["Stock Game", "Game Root", "STOCK GAME", "Stock Game Folder", "Stock Folder", "Skyrim Stock", "root/Skyrim Special Edition"]): elif any(x in binary_path for x in ["Stock Game", "Game Root", "STOCK GAME", "Stock Game Folder", "Stock Folder", "Skyrim Stock", "root/Skyrim Special Edition"]):
# Stock/Game Root found # Stock/Game Root found
if modlist_sdcard: if modlist_sdcard:
path_middle = modlist_dir.split('mmcblk0p1', 1)[1] if 'mmcblk0p1' in modlist_dir else modlist_dir path_middle = WineUtils._strip_sdcard_path(modlist_dir)
# Strip /run/media/deck/UUID if present
if '/run/media/' in path_middle:
path_middle = '/' + path_middle.split('/run/media/', 1)[1].split('/', 2)[2]
else: else:
path_middle = modlist_dir path_middle = modlist_dir
@@ -589,7 +583,7 @@ class WineUtils:
elif "steamapps" in binary_path: elif "steamapps" in binary_path:
# Steamapps found # Steamapps found
if basegame_sdcard: if basegame_sdcard:
path_middle = steam_library.split('mmcblk0p1', 1)[1] if 'mmcblk0p1' in steam_library else steam_library path_middle = WineUtils._strip_sdcard_path(steam_library)
drive_letter = "D:" drive_letter = "D:"
else: else:
path_middle = steam_library.split('steamapps', 1)[0] if 'steamapps' in steam_library else steam_library path_middle = steam_library.split('steamapps', 1)[0] if 'steamapps' in steam_library else steam_library
@@ -709,7 +703,7 @@ class WineUtils:
try: try:
from .config_handler import ConfigHandler from .config_handler import ConfigHandler
config = ConfigHandler() config = ConfigHandler()
fallback_path = config.get('proton_path', 'auto') fallback_path = config.get_proton_path()
if fallback_path != 'auto': if fallback_path != 'auto':
fallback_wine_bin = Path(fallback_path) / "files/bin/wine" fallback_wine_bin = Path(fallback_path) / "files/bin/wine"
if fallback_wine_bin.is_file(): if fallback_wine_bin.is_file():

View File

@@ -137,7 +137,7 @@ class WinetricksHandler:
from ..handlers.wine_utils import WineUtils from ..handlers.wine_utils import WineUtils
config = ConfigHandler() config = ConfigHandler()
user_proton_path = config.get('proton_path', 'auto') user_proton_path = config.get_proton_path()
# If user selected a specific Proton, try that first # If user selected a specific Proton, try that first
wine_binary = None wine_binary = None
@@ -181,6 +181,49 @@ class WinetricksHandler:
env['WINE'] = str(wine_binary) env['WINE'] = str(wine_binary)
self.logger.info(f"Using Proton wine binary for winetricks: {wine_binary}") self.logger.info(f"Using Proton wine binary for winetricks: {wine_binary}")
# CRITICAL: Set up protontricks-compatible environment
proton_dist_path = os.path.dirname(os.path.dirname(wine_binary)) # e.g., /path/to/proton/dist/bin/wine -> /path/to/proton/dist
self.logger.debug(f"Proton dist path: {proton_dist_path}")
# Set WINEDLLPATH like protontricks does
env['WINEDLLPATH'] = f"{proton_dist_path}/lib64/wine:{proton_dist_path}/lib/wine"
# Ensure Proton bin directory is first in PATH
env['PATH'] = f"{proton_dist_path}/bin:{env.get('PATH', '')}"
# Set DLL overrides exactly like protontricks
dll_overrides = {
"beclient": "b,n",
"beclient_x64": "b,n",
"dxgi": "n",
"d3d9": "n",
"d3d10core": "n",
"d3d11": "n",
"d3d12": "n",
"d3d12core": "n",
"nvapi": "n",
"nvapi64": "n",
"nvofapi64": "n",
"nvcuda": "b"
}
# Merge with existing overrides
existing_overrides = env.get('WINEDLLOVERRIDES', '')
if existing_overrides:
# Parse existing overrides
for override in existing_overrides.split(';'):
if '=' in override:
name, value = override.split('=', 1)
dll_overrides[name] = value
env['WINEDLLOVERRIDES'] = ';'.join(f"{name}={setting}" for name, setting in dll_overrides.items())
# Set Wine defaults from protontricks
env['WINE_LARGE_ADDRESS_AWARE'] = '1'
env['DXVK_ENABLE_NVAPI'] = '1'
self.logger.debug(f"Set protontricks environment: WINEDLLPATH={env['WINEDLLPATH']}")
except Exception as e: except Exception as e:
self.logger.error(f"Cannot run winetricks: Failed to get Proton wine binary: {e}") self.logger.error(f"Cannot run winetricks: Failed to get Proton wine binary: {e}")
return False return False
@@ -214,10 +257,26 @@ class WinetricksHandler:
components_to_install = self._reorder_components_for_installation(all_components) components_to_install = self._reorder_components_for_installation(all_components)
self.logger.info(f"WINEPREFIX: {wineprefix}, Game: {game_var}, Ordered Components: {components_to_install}") self.logger.info(f"WINEPREFIX: {wineprefix}, Game: {game_var}, Ordered Components: {components_to_install}")
# Install components separately if dotnet40 is present (mimics protontricks behavior) # Check user preference for component installation method
if "dotnet40" in components_to_install: from ..handlers.config_handler import ConfigHandler
self.logger.info("dotnet40 detected - installing components separately like protontricks") config_handler = ConfigHandler()
return self._install_components_separately(components_to_install, wineprefix, wine_binary, env) 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
# 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)
# For non-dotnet40 installations, install all components together (faster) # For non-dotnet40 installations, install all components together (faster)
max_attempts = 3 max_attempts = 3
@@ -246,6 +305,8 @@ class WinetricksHandler:
self.logger.debug(f"Winetricks output: {result.stdout}") self.logger.debug(f"Winetricks output: {result.stdout}")
if result.returncode == 0: if result.returncode == 0:
self.logger.info("Wine Component installation command completed successfully.") self.logger.info("Wine Component installation command completed successfully.")
# Set Windows 10 mode after component installation (matches legacy script timing)
self._set_windows_10_mode(wineprefix, env.get('WINE', ''))
return True return True
else: else:
# Special handling for dotnet40 verification issue (mimics protontricks behavior) # Special handling for dotnet40 verification issue (mimics protontricks behavior)
@@ -372,14 +433,8 @@ class WinetricksHandler:
self.logger.error("Failed to prepare prefix for dotnet40") self.logger.error("Failed to prepare prefix for dotnet40")
return False return False
else: else:
# For non-dotnet40 components, ensure we're in Windows 10 mode # For non-dotnet40 components, install in standard mode (Windows 10 will be set after all components)
self.logger.debug(f"Installing {component} in standard mode") self.logger.debug(f"Installing {component} in standard mode")
try:
subprocess.run([
self.winetricks_path, '-q', 'win10'
], env=env, capture_output=True, text=True, timeout=300)
except Exception as e:
self.logger.warning(f"Could not set win10 mode for {component}: {e}")
# Install this component # Install this component
max_attempts = 3 max_attempts = 3
@@ -406,7 +461,7 @@ class WinetricksHandler:
) )
if result.returncode == 0: if result.returncode == 0:
self.logger.info(f"{component} installed successfully") self.logger.info(f"{component} installed successfully")
component_success = True component_success = True
break break
else: else:
@@ -420,13 +475,14 @@ class WinetricksHandler:
try: try:
with open(log_path, 'r') as f: with open(log_path, 'r') as f:
if 'dotnet40' in f.read(): if 'dotnet40' in f.read():
self.logger.info("dotnet40 confirmed in winetricks.log") self.logger.info("dotnet40 confirmed in winetricks.log")
component_success = True component_success = True
break break
except Exception as e: except Exception as e:
self.logger.warning(f"Could not read winetricks.log: {e}") self.logger.warning(f"Could not read winetricks.log: {e}")
self.logger.error(f"{component} failed (attempt {attempt}): {result.stderr.strip()[:200]}") 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: except Exception as e:
self.logger.error(f"Error installing {component} (attempt {attempt}): {e}") self.logger.error(f"Error installing {component} (attempt {attempt}): {e}")
@@ -435,9 +491,384 @@ class WinetricksHandler:
self.logger.error(f"Failed to install {component} after {max_attempts} attempts") self.logger.error(f"Failed to install {component} after {max_attempts} attempts")
return False return False
self.logger.info("All components installed successfully using separate sessions") self.logger.info("All components installed successfully using separate sessions")
# Set Windows 10 mode after all component installation (matches legacy script timing)
self._set_windows_10_mode(wineprefix, env.get('WINE', ''))
return True return True
def _install_components_hybrid_approach(self, components: list, wineprefix: str, game_var: str, use_winetricks: bool = True) -> bool:
"""
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")
# 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 legacy .NET Framework versions with protontricks if present
if protontricks_components:
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")
# 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")
# 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_legacy_dotnet_with_protontricks(self, legacy_components: list, wineprefix: str, game_var: str) -> bool:
"""
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
Returns:
bool: True if installation succeeded, False otherwise
"""
try:
# Extract AppID from wineprefix path (e.g., /path/to/compatdata/123456789/pfx -> 123456789)
appid = None
if 'compatdata' in wineprefix:
# Standard Steam compatdata structure
path_parts = Path(wineprefix).parts
for i, part in enumerate(path_parts):
if part == 'compatdata' and i + 1 < len(path_parts):
potential_appid = path_parts[i + 1]
if potential_appid.isdigit():
appid = potential_appid
break
if not appid:
self.logger.error(f"Could not extract AppID from wineprefix path: {wineprefix}")
return False
self.logger.info(f"Using AppID {appid} for protontricks dotnet40 installation")
# Import and use protontricks handler
from .protontricks_handler import ProtontricksHandler
# Determine if we're on Steam Deck (for protontricks handler)
steamdeck = os.path.exists('/home/deck')
protontricks_handler = ProtontricksHandler(steamdeck, logger=self.logger)
# Detect protontricks availability
if not protontricks_handler.detect_protontricks():
self.logger.error(f"Protontricks not available for legacy .NET installation: {legacy_components}")
return False
# Install legacy .NET components using protontricks
success = protontricks_handler.install_wine_components(appid, game_var, legacy_components)
if success:
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(f"Legacy .NET components {legacy_components} installation failed with protontricks")
return False
except Exception as e:
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]:
"""
Prepare the environment for winetricks installation.
This reuses the existing environment setup logic.
Args:
wineprefix: Wine prefix path
Returns:
dict: Environment variables for winetricks, or None if failed
"""
try:
env = os.environ.copy()
env['WINEDEBUG'] = '-all'
env['WINEPREFIX'] = wineprefix
env['WINETRICKS_GUI'] = 'none'
# Existing Proton detection logic
from ..handlers.config_handler import ConfigHandler
from ..handlers.wine_utils import WineUtils
config = ConfigHandler()
user_proton_path = config.get_proton_path()
wine_binary = None
if user_proton_path != 'auto':
if os.path.exists(user_proton_path):
resolved_proton_path = os.path.realpath(user_proton_path)
valve_proton_wine = os.path.join(resolved_proton_path, 'dist', 'bin', 'wine')
ge_proton_wine = os.path.join(resolved_proton_path, 'files', 'bin', 'wine')
if os.path.exists(valve_proton_wine):
wine_binary = valve_proton_wine
elif os.path.exists(ge_proton_wine):
wine_binary = ge_proton_wine
if not wine_binary:
best_proton = WineUtils.select_best_proton()
if best_proton:
wine_binary = WineUtils.find_proton_binary(best_proton['name'])
if not wine_binary or not (os.path.exists(wine_binary) and os.access(wine_binary, os.X_OK)):
self.logger.error(f"Cannot prepare winetricks environment: No compatible Proton found")
return None
env['WINE'] = str(wine_binary)
# Set up protontricks-compatible environment (existing logic)
proton_dist_path = os.path.dirname(os.path.dirname(wine_binary))
env['WINEDLLPATH'] = f"{proton_dist_path}/lib64/wine:{proton_dist_path}/lib/wine"
env['PATH'] = f"{proton_dist_path}/bin:{env.get('PATH', '')}"
# Existing DLL overrides
dll_overrides = {
"beclient": "b,n", "beclient_x64": "b,n", "dxgi": "n", "d3d9": "n",
"d3d10core": "n", "d3d11": "n", "d3d12": "n", "d3d12core": "n",
"nvapi": "n", "nvapi64": "n", "nvofapi64": "n", "nvcuda": "b"
}
env['WINEDLLOVERRIDES'] = ';'.join(f"{name}={setting}" for name, setting in dll_overrides.items())
env['WINE_LARGE_ADDRESS_AWARE'] = '1'
env['DXVK_ENABLE_NVAPI'] = '1'
# Set up winetricks cache
from jackify.shared.paths import get_jackify_data_dir
jackify_cache_dir = get_jackify_data_dir() / 'winetricks_cache'
jackify_cache_dir.mkdir(parents=True, exist_ok=True)
env['WINETRICKS_CACHE'] = str(jackify_cache_dir)
return env
except Exception as e:
self.logger.error(f"Failed to prepare winetricks environment: {e}")
return None
def _install_components_with_winetricks(self, components: list, wineprefix: str, env: dict) -> bool:
"""
Install components using winetricks with the prepared environment.
Args:
components: List of components to install
wineprefix: Wine prefix path
env: Prepared environment variables
Returns:
bool: True if installation succeeded, False otherwise
"""
max_attempts = 3
for attempt in range(1, max_attempts + 1):
if attempt > 1:
self.logger.warning(f"Retrying winetricks installation (attempt {attempt}/{max_attempts})")
self._cleanup_wine_processes()
try:
cmd = [self.winetricks_path, '--unattended'] + components
self.logger.debug(f"Running winetricks: {' '.join(cmd)}")
result = subprocess.run(
cmd,
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=600
)
if result.returncode == 0:
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()}")
except Exception as e:
self.logger.error(f"Error during winetricks run (attempt {attempt}): {e}")
self.logger.error(f"Failed to install components with winetricks after {max_attempts} attempts")
return False
def _set_windows_10_mode(self, wineprefix: str, wine_binary: str):
"""
Set Windows 10 mode for the prefix after component installation (matches legacy script timing).
This should be called AFTER all Wine components are installed, not before.
"""
try:
env = os.environ.copy()
env['WINEPREFIX'] = wineprefix
env['WINE'] = wine_binary
self.logger.info("Setting Windows 10 mode after component installation (matching legacy script)")
result = subprocess.run([
self.winetricks_path, '-q', 'win10'
], env=env, capture_output=True, text=True, timeout=300)
if result.returncode == 0:
self.logger.info("Windows 10 mode set successfully")
else:
self.logger.warning(f"Could not set Windows 10 mode: {result.stderr}")
except Exception as e:
self.logger.warning(f"Error setting Windows 10 mode: {e}")
def _install_components_protontricks_only(self, components: list, wineprefix: str, game_var: str) -> bool:
"""
Legacy approach: Install all components using protontricks only.
This matches the behavior of the original bash script.
"""
try:
self.logger.info(f"Installing all components with protontricks (legacy method): {components}")
# Import protontricks handler
from ..handlers.protontricks_handler import ProtontricksHandler
# Determine if we're on Steam Deck (for protontricks handler)
steamdeck = os.path.exists('/home/deck')
protontricks_handler = ProtontricksHandler(steamdeck, logger=self.logger)
# Get AppID from wineprefix
appid = self._extract_appid_from_wineprefix(wineprefix)
if not appid:
self.logger.error("Could not extract AppID from wineprefix for protontricks installation")
return False
self.logger.info(f"Using AppID {appid} for protontricks installation")
# Detect protontricks availability
if not protontricks_handler.detect_protontricks():
self.logger.error("Protontricks not available for component installation")
return False
# Install all components using protontricks
success = protontricks_handler.install_wine_components(appid, game_var, components)
if success:
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")
return False
except Exception as e:
self.logger.error(f"Error installing components with protontricks: {e}", exc_info=True)
return False
def _extract_appid_from_wineprefix(self, wineprefix: str) -> Optional[str]:
"""
Extract AppID from wineprefix path.
Args:
wineprefix: Wine prefix path
Returns:
AppID as string, or None if extraction fails
"""
try:
if 'compatdata' in wineprefix:
# Standard Steam compatdata structure
path_parts = Path(wineprefix).parts
for i, part in enumerate(path_parts):
if part == 'compatdata' and i + 1 < len(path_parts):
potential_appid = path_parts[i + 1]
if potential_appid.isdigit():
return potential_appid
self.logger.error(f"Could not extract AppID from wineprefix path: {wineprefix}")
return None
except Exception as e:
self.logger.error(f"Error extracting AppID from wineprefix: {e}")
return None
def _get_wine_binary_for_prefix(self, wineprefix: str) -> str:
"""
Get the wine binary path for a given prefix.
Args:
wineprefix: Wine prefix path
Returns:
Wine binary path as string
"""
try:
from ..handlers.config_handler import ConfigHandler
from ..handlers.wine_utils import WineUtils
config = ConfigHandler()
user_proton_path = config.get_proton_path()
# If user selected a specific Proton, try that first
wine_binary = None
if user_proton_path != 'auto':
if os.path.exists(user_proton_path):
resolved_proton_path = os.path.realpath(user_proton_path)
valve_proton_wine = os.path.join(resolved_proton_path, 'dist', 'bin', 'wine')
ge_proton_wine = os.path.join(resolved_proton_path, 'files', 'bin', 'wine')
if os.path.exists(valve_proton_wine):
wine_binary = valve_proton_wine
elif os.path.exists(ge_proton_wine):
wine_binary = ge_proton_wine
# Fall back to auto-detection if user selection failed or is 'auto'
if not wine_binary:
best_proton = WineUtils.select_best_proton()
if best_proton:
wine_binary = WineUtils.find_proton_binary(best_proton['name'])
return wine_binary if wine_binary else ""
except Exception as e:
self.logger.error(f"Error getting wine binary for prefix: {e}")
return ""
def _cleanup_wine_processes(self): def _cleanup_wine_processes(self):
""" """
Internal method to clean up wine processes during component installation Internal method to clean up wine processes during component installation

View File

@@ -39,14 +39,34 @@ class AutomatedPrefixService:
from jackify.shared.timing import get_timestamp from jackify.shared.timing import get_timestamp
return get_timestamp() return get_timestamp()
def _get_user_proton_version(self): def _get_user_proton_version(self, modlist_name: str = None):
"""Get user's preferred Proton version from config, with fallback to auto-detection""" """Get user's preferred Proton version from config, with fallback to auto-detection
Args:
modlist_name: Optional modlist name for special handling (e.g., Lorerim)
"""
try: try:
from jackify.backend.handlers.config_handler import ConfigHandler from jackify.backend.handlers.config_handler import ConfigHandler
from jackify.backend.handlers.wine_utils import WineUtils from jackify.backend.handlers.wine_utils import WineUtils
# Check for Lorerim-specific Proton override first
if modlist_name and modlist_name.lower() == 'lorerim':
lorerim_proton = self._get_lorerim_preferred_proton()
if lorerim_proton:
logger.info(f"Lorerim detected: Using {lorerim_proton} instead of user settings")
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() config_handler = ConfigHandler()
user_proton_path = config_handler.get('proton_path', 'auto') user_proton_path = config_handler.get_game_proton_path()
if user_proton_path == 'auto': if user_proton_path == 'auto':
# Use enhanced fallback logic with GE-Proton preference # Use enhanced fallback logic with GE-Proton preference
@@ -125,8 +145,8 @@ class AutomatedPrefixService:
logger.warning(f"Could not generate STEAM_COMPAT_MOUNTS, using default: {e}") logger.warning(f"Could not generate STEAM_COMPAT_MOUNTS, using default: {e}")
launch_options = "%command%" launch_options = "%command%"
# Get user's preferred Proton version # Get user's preferred Proton version (with Lorerim-specific override)
proton_version = self._get_user_proton_version() proton_version = self._get_user_proton_version(shortcut_name)
# Create shortcut with Proton using native service # Create shortcut with Proton using native service
success, app_id = steam_service.create_shortcut_with_proton( success, app_id = steam_service.create_shortcut_with_proton(
@@ -487,7 +507,7 @@ exit"""
try: try:
# Use the existing protontricks handler # Use the existing protontricks handler
from jackify.backend.handlers.protontricks_handler import ProtontricksHandler from jackify.backend.handlers.protontricks_handler import ProtontricksHandler
protontricks_handler = ProtontricksHandler(steamdeck=steamdeck or False) protontricks_handler = ProtontricksHandler(steamdeck or False)
result = protontricks_handler.run_protontricks('-l') result = protontricks_handler.run_protontricks('-l')
if result.returncode == 0: if result.returncode == 0:
@@ -1556,6 +1576,9 @@ echo Prefix creation complete.
if progress_callback: if progress_callback:
progress_callback(f"{self._get_progress_timestamp()} Steam Configuration complete!") progress_callback(f"{self._get_progress_timestamp()} Steam Configuration complete!")
# Show Proton override notification if applicable
self._show_proton_override_notification(progress_callback)
logger.info(" Simple automated prefix creation workflow completed successfully") logger.info(" Simple automated prefix creation workflow completed successfully")
return True, prefix_path, actual_appid return True, prefix_path, actual_appid
@@ -1869,6 +1892,11 @@ echo Prefix creation complete.
if progress_callback: if progress_callback:
progress_callback(f"{last_timestamp} Steam integration complete") progress_callback(f"{last_timestamp} Steam integration complete")
progress_callback("") # Blank line after Steam integration complete progress_callback("") # Blank line after Steam integration complete
# Show Proton override notification if applicable
self._show_proton_override_notification(progress_callback)
if progress_callback:
progress_callback("") # Extra blank line to span across Configuration Summary progress_callback("") # Extra blank line to span across Configuration Summary
progress_callback("") # And one more to create space before Prefix Configuration progress_callback("") # And one more to create space before Prefix Configuration
@@ -2705,7 +2733,7 @@ echo Prefix creation complete.
from jackify.backend.handlers.wine_utils import WineUtils from jackify.backend.handlers.wine_utils import WineUtils
config = ConfigHandler() config = ConfigHandler()
user_proton_path = config.get('proton_path', 'auto') user_proton_path = config.get_game_proton_path()
# If user selected a specific Proton, try that first # If user selected a specific Proton, try that first
if user_proton_path != 'auto': if user_proton_path != 'auto':
@@ -3006,3 +3034,91 @@ echo Prefix creation complete.
logger.info("Game registry injection completed") logger.info("Game registry injection completed")
def _get_lorerim_preferred_proton(self):
"""Get Lorerim's preferred Proton 9 version with specific priority order"""
try:
from jackify.backend.handlers.wine_utils import WineUtils
# Get all available Proton versions
available_versions = WineUtils.scan_all_proton_versions()
if not available_versions:
logger.warning("No Proton versions found for Lorerim override")
return None
# Priority order for Lorerim:
# 1. GEProton9-27 (specific version)
# 2. Other GEProton-9 versions (latest first)
# 3. Valve Proton 9 (any version)
preferred_candidates = []
for version in available_versions:
version_name = version['name']
# Priority 1: GEProton9-27 specifically
if version_name == 'GE-Proton9-27':
logger.info(f"Lorerim: Found preferred GE-Proton9-27")
return version_name
# Priority 2: Other GE-Proton 9 versions
elif version_name.startswith('GE-Proton9-'):
preferred_candidates.append(('ge_proton_9', version_name, version))
# Priority 3: Valve Proton 9
elif 'Proton 9' in version_name:
preferred_candidates.append(('valve_proton_9', version_name, version))
# Return best candidate if any found
if preferred_candidates:
# Sort by priority (GE-Proton first, then by name for latest)
preferred_candidates.sort(key=lambda x: (x[0], x[1]), reverse=True)
best_candidate = preferred_candidates[0]
logger.info(f"Lorerim: Selected {best_candidate[1]} as best Proton 9 option")
return best_candidate[1]
logger.warning("Lorerim: No suitable Proton 9 versions found, will use user settings")
return None
except Exception as e:
logger.error(f"Error detecting Lorerim Proton preference: {e}")
return None
def _store_proton_override_notification(self, modlist_name: str, proton_version: str):
"""Store Proton override information for end-of-install notification"""
try:
# Store override info for later display
if not hasattr(self, '_proton_overrides'):
self._proton_overrides = []
self._proton_overrides.append({
'modlist': modlist_name,
'proton_version': proton_version,
'reason': f'{modlist_name} requires Proton 9 for optimal compatibility'
})
logger.debug(f"Stored Proton override notification: {modlist_name}{proton_version}")
except Exception as e:
logger.error(f"Failed to store Proton override notification: {e}")
def _show_proton_override_notification(self, progress_callback=None):
"""Display any Proton override notifications to the user"""
try:
if hasattr(self, '_proton_overrides') and self._proton_overrides:
for override in self._proton_overrides:
notification_msg = f"PROTON OVERRIDE: {override['modlist']} configured to use {override['proton_version']} for optimal compatibility"
if progress_callback:
progress_callback("")
progress_callback(f"{self._get_progress_timestamp()} {notification_msg}")
logger.info(notification_msg)
# Clear notifications after display
self._proton_overrides = []
except Exception as e:
logger.error(f"Failed to show Proton override notification: {e}")

View File

@@ -629,9 +629,14 @@ class ModlistService:
'mo2_exe_path': str(context.install_dir / 'ModOrganizer.exe'), 'mo2_exe_path': str(context.install_dir / 'ModOrganizer.exe'),
'resolution': getattr(context, 'resolution', None), 'resolution': getattr(context, 'resolution', None),
'skip_confirmation': True, # Service layer should be non-interactive 'skip_confirmation': True, # Service layer should be non-interactive
'manual_steps_completed': False 'manual_steps_completed': False,
'appid': getattr(context, 'app_id', None) # Fix: Include appid like other configuration paths
} }
# DEBUG: Log what resolution we're passing
logger.info(f"DEBUG: config_context resolution = {config_context['resolution']}")
logger.info(f"DEBUG: context.resolution = {getattr(context, 'resolution', 'NOT_SET')}")
# Run the complete configuration phase # Run the complete configuration phase
success = modlist_menu.run_modlist_configuration_phase(config_context) success = modlist_menu.run_modlist_configuration_phase(config_context)

View File

@@ -30,100 +30,83 @@ class NativeSteamService:
""" """
def __init__(self): def __init__(self):
self.steam_path = Path.home() / ".steam" / "steam" self.steam_paths = [
self.userdata_path = self.steam_path / "userdata" Path.home() / ".steam" / "steam",
Path.home() / ".local" / "share" / "Steam",
Path.home() / ".var" / "app" / "com.valvesoftware.Steam" / ".local" / "share" / "Steam"
]
self.steam_path = None
self.userdata_path = None
self.user_id = None self.user_id = None
self.user_config_path = None self.user_config_path = None
def find_steam_user(self) -> bool: def find_steam_user(self) -> bool:
"""Find the active Steam user directory""" """
Find the active Steam user directory using Steam's own configuration files.
No more guessing - uses loginusers.vdf to get the most recent user and converts SteamID64 to SteamID3.
"""
try: try:
if not self.userdata_path.exists(): # Step 1: Find Steam installation using Steam's own file structure
logger.error("Steam userdata directory not found") if not self._find_steam_installation():
logger.error("No Steam installation found")
return False return False
# Find user directories (excluding user 0 which is a system account) # Step 2: Parse loginusers.vdf to get the most recent user (SteamID64)
user_dirs = [d for d in self.userdata_path.iterdir() if d.is_dir() and d.name.isdigit() and d.name != "0"] steamid64 = self._get_most_recent_user_from_loginusers()
if not user_dirs: if not steamid64:
logger.error("No valid Steam user directories found (user 0 is not valid for shortcuts)") logger.error("Could not determine most recent Steam user from loginusers.vdf")
return False return False
# Detect the correct Steam user # Step 3: Convert SteamID64 to SteamID3 (userdata directory format)
user_dir = self._detect_active_steam_user(user_dirs) steamid3 = self._convert_steamid64_to_steamid3(steamid64)
if not user_dir: logger.info(f"Most recent Steam user: SteamID64={steamid64}, SteamID3={steamid3}")
logger.error("Could not determine active Steam user")
# Step 4: Verify the userdata directory exists
user_dir = self.userdata_path / str(steamid3)
if not user_dir.exists():
logger.error(f"Userdata directory does not exist: {user_dir}")
return False return False
self.user_id = user_dir.name config_dir = user_dir / "config"
self.user_config_path = user_dir / "config" if not config_dir.exists():
logger.error(f"User config directory does not exist: {config_dir}")
return False
logger.info(f"Found Steam user: {self.user_id}") # Step 5: Set up the service state
self.user_id = str(steamid3)
self.user_config_path = config_dir
logger.info(f"VERIFIED Steam user: {self.user_id}")
logger.info(f"User config path: {self.user_config_path}") logger.info(f"User config path: {self.user_config_path}")
logger.info(f"Shortcuts.vdf will be at: {self.user_config_path / 'shortcuts.vdf'}")
return True return True
except Exception as e: except Exception as e:
logger.error(f"Error finding Steam user: {e}") logger.error(f"Error finding Steam user: {e}", exc_info=True)
return False return False
def _detect_active_steam_user(self, user_dirs: List[Path]) -> Optional[Path]: def _find_steam_installation(self) -> bool:
"""Find Steam installation by checking for config/loginusers.vdf"""
for steam_path in self.steam_paths:
loginusers_path = steam_path / "config" / "loginusers.vdf"
userdata_path = steam_path / "userdata"
if loginusers_path.exists() and userdata_path.exists():
self.steam_path = steam_path
self.userdata_path = userdata_path
logger.info(f"Found Steam installation at: {steam_path}")
return True
return False
def _get_most_recent_user_from_loginusers(self) -> Optional[str]:
""" """
Detect the active Steam user from available user directories. Parse loginusers.vdf to get the SteamID64 of the most recent user.
Uses Steam's own MostRecent flag and Timestamp.
Priority:
1. Single non-0 user: Use automatically
2. Multiple users: Parse loginusers.vdf to find logged-in user
3. Fallback: Most recently active user directory
Args:
user_dirs: List of valid user directories
Returns:
Path to the active user directory, or None if detection fails
"""
if len(user_dirs) == 1:
logger.info(f"Single Steam user found: {user_dirs[0].name}")
return user_dirs[0]
logger.info(f"Multiple Steam users found: {[d.name for d in user_dirs]}")
# Try to parse loginusers.vdf to find logged-in user
loginusers_path = self.steam_path / "loginusers.vdf"
active_user = self._parse_loginusers_vdf(loginusers_path)
if active_user:
# Find matching user directory
for user_dir in user_dirs:
if user_dir.name == active_user:
logger.info(f"Found logged-in Steam user from loginusers.vdf: {active_user}")
return user_dir
logger.warning(f"Logged-in user {active_user} from loginusers.vdf not found in user directories")
# Fallback: Use most recently modified user directory
try:
most_recent = max(user_dirs, key=lambda d: d.stat().st_mtime)
logger.info(f"Using most recently active Steam user directory: {most_recent.name}")
return most_recent
except Exception as e:
logger.error(f"Error determining most recent user directory: {e}")
# Final fallback: Use first user
logger.warning(f"Using first user directory as final fallback: {user_dirs[0].name}")
return user_dirs[0]
def _parse_loginusers_vdf(self, loginusers_path: Path) -> Optional[str]:
"""
Parse loginusers.vdf to find the currently logged-in Steam user.
Args:
loginusers_path: Path to loginusers.vdf
Returns:
Steam user ID as string, or None if parsing fails
""" """
try: try:
if not loginusers_path.exists(): loginusers_path = self.steam_path / "config" / "loginusers.vdf"
logger.debug(f"loginusers.vdf not found at {loginusers_path}")
return None
# Load VDF data # Load VDF data
vdf_data = VDFHandler.load(str(loginusers_path), binary=False) vdf_data = VDFHandler.load(str(loginusers_path), binary=False)
@@ -133,27 +116,52 @@ class NativeSteamService:
users_section = vdf_data.get("users", {}) users_section = vdf_data.get("users", {})
if not users_section: if not users_section:
logger.debug("No users section found in loginusers.vdf") logger.error("No users section found in loginusers.vdf")
return None return None
# Find user marked as logged in most_recent_user = None
for user_id, user_data in users_section.items(): most_recent_timestamp = 0
if isinstance(user_data, dict):
# Check for indicators of logged-in status
if user_data.get("MostRecent") == "1" or user_data.get("WantsOfflineMode") == "0":
logger.debug(f"Found most recent/logged-in user: {user_id}")
return user_id
# If no specific user found, try to get the first valid user # Find user with MostRecent=1 or highest timestamp
if users_section: for steamid64, user_data in users_section.items():
first_user = next(iter(users_section.keys())) if isinstance(user_data, dict):
logger.debug(f"No specific logged-in user found, using first user: {first_user}") # Check for MostRecent flag first
return first_user if user_data.get("MostRecent") == "1":
logger.info(f"Found user marked as MostRecent: {steamid64}")
return steamid64
# Also track highest timestamp as fallback
timestamp = int(user_data.get("Timestamp", "0"))
if timestamp > most_recent_timestamp:
most_recent_timestamp = timestamp
most_recent_user = steamid64
# Return user with highest timestamp if no MostRecent flag found
if most_recent_user:
logger.info(f"Found most recent user by timestamp: {most_recent_user}")
return most_recent_user
logger.error("No valid users found in loginusers.vdf")
return None
except Exception as e: except Exception as e:
logger.error(f"Error parsing loginusers.vdf: {e}") logger.error(f"Error parsing loginusers.vdf: {e}")
return None
def _convert_steamid64_to_steamid3(self, steamid64: str) -> int:
"""
Convert SteamID64 to SteamID3 (used in userdata directory names).
Formula: SteamID3 = SteamID64 - 76561197960265728
"""
try:
steamid64_int = int(steamid64)
steamid3 = steamid64_int - 76561197960265728
logger.debug(f"Converted SteamID64 {steamid64} to SteamID3 {steamid3}")
return steamid3
except ValueError as e:
logger.error(f"Invalid SteamID64 format: {steamid64}")
raise
return None
def get_shortcuts_vdf_path(self) -> Optional[Path]: def get_shortcuts_vdf_path(self) -> Optional[Path]:
"""Get the path to shortcuts.vdf""" """Get the path to shortcuts.vdf"""
@@ -345,6 +353,12 @@ class NativeSteamService:
Returns: Returns:
True if successful True if successful
""" """
# Ensure Steam user detection is completed first
if not self.steam_path:
if not self.find_steam_user():
logger.error("Cannot set Proton version: Steam user detection failed")
return False
logger.info(f"Setting Proton version '{proton_version}' for AppID {app_id} using STL-compatible format") logger.info(f"Setting Proton version '{proton_version}' for AppID {app_id} using STL-compatible format")
try: try:
@@ -368,8 +382,27 @@ class NativeSteamService:
# Find the CompatToolMapping section # Find the CompatToolMapping section
compat_start = config_text.find('"CompatToolMapping"') compat_start = config_text.find('"CompatToolMapping"')
if compat_start == -1: if compat_start == -1:
logger.error("CompatToolMapping section not found in config.vdf") logger.warning("CompatToolMapping section not found in config.vdf, creating it")
return False # Find the Steam section to add CompatToolMapping to
steam_section = config_text.find('"Steam"')
if steam_section == -1:
logger.error("Steam section not found in config.vdf")
return False
# Find the opening brace for Steam section
steam_brace = config_text.find('{', steam_section)
if steam_brace == -1:
logger.error("Steam section opening brace not found")
return False
# Insert CompatToolMapping section right after Steam opening brace
insert_pos = steam_brace + 1
compat_section = '\n\t\t"CompatToolMapping"\n\t\t{\n\t\t}\n'
config_text = config_text[:insert_pos] + compat_section + config_text[insert_pos:]
# Update compat_start position after insertion
compat_start = config_text.find('"CompatToolMapping"')
logger.info("Created CompatToolMapping section in config.vdf")
# Find the closing brace for CompatToolMapping # Find the closing brace for CompatToolMapping
# Look for the opening brace after CompatToolMapping # Look for the opening brace after CompatToolMapping

View File

@@ -39,7 +39,7 @@ class ProtontricksDetectionService:
def _get_protontricks_handler(self) -> ProtontricksHandler: def _get_protontricks_handler(self) -> ProtontricksHandler:
"""Get or create ProtontricksHandler instance""" """Get or create ProtontricksHandler instance"""
if self._protontricks_handler is None: if self._protontricks_handler is None:
self._protontricks_handler = ProtontricksHandler(steamdeck=self.steamdeck) self._protontricks_handler = ProtontricksHandler(self.steamdeck)
return self._protontricks_handler return self._protontricks_handler
def detect_protontricks(self, use_cache: bool = True) -> Tuple[bool, str, str]: def detect_protontricks(self, use_cache: bool = True) -> Tuple[bool, str, str]:

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -7,7 +7,7 @@
"targets": { "targets": {
".NETCoreApp,Version=v8.0": {}, ".NETCoreApp,Version=v8.0": {},
".NETCoreApp,Version=v8.0/linux-x64": { ".NETCoreApp,Version=v8.0/linux-x64": {
"jackify-engine/0.3.15": { "jackify-engine/0.3.17": {
"dependencies": { "dependencies": {
"Markdig": "0.40.0", "Markdig": "0.40.0",
"Microsoft.Extensions.Configuration.Json": "9.0.1", "Microsoft.Extensions.Configuration.Json": "9.0.1",
@@ -22,16 +22,16 @@
"SixLabors.ImageSharp": "3.1.6", "SixLabors.ImageSharp": "3.1.6",
"System.CommandLine": "2.0.0-beta4.22272.1", "System.CommandLine": "2.0.0-beta4.22272.1",
"System.CommandLine.NamingConventionBinder": "2.0.0-beta4.22272.1", "System.CommandLine.NamingConventionBinder": "2.0.0-beta4.22272.1",
"Wabbajack.CLI.Builder": "0.3.15", "Wabbajack.CLI.Builder": "0.3.17",
"Wabbajack.Downloaders.Bethesda": "0.3.15", "Wabbajack.Downloaders.Bethesda": "0.3.17",
"Wabbajack.Downloaders.Dispatcher": "0.3.15", "Wabbajack.Downloaders.Dispatcher": "0.3.17",
"Wabbajack.Hashing.xxHash64": "0.3.15", "Wabbajack.Hashing.xxHash64": "0.3.17",
"Wabbajack.Networking.Discord": "0.3.15", "Wabbajack.Networking.Discord": "0.3.17",
"Wabbajack.Networking.GitHub": "0.3.15", "Wabbajack.Networking.GitHub": "0.3.17",
"Wabbajack.Paths.IO": "0.3.15", "Wabbajack.Paths.IO": "0.3.17",
"Wabbajack.Server.Lib": "0.3.15", "Wabbajack.Server.Lib": "0.3.17",
"Wabbajack.Services.OSIntegrated": "0.3.15", "Wabbajack.Services.OSIntegrated": "0.3.17",
"Wabbajack.VFS": "0.3.15", "Wabbajack.VFS": "0.3.17",
"MegaApiClient": "1.0.0.0", "MegaApiClient": "1.0.0.0",
"runtimepack.Microsoft.NETCore.App.Runtime.linux-x64": "8.0.19" "runtimepack.Microsoft.NETCore.App.Runtime.linux-x64": "8.0.19"
}, },
@@ -1781,7 +1781,7 @@
} }
} }
}, },
"Wabbajack.CLI.Builder/0.3.15": { "Wabbajack.CLI.Builder/0.3.17": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.Configuration.Json": "9.0.1", "Microsoft.Extensions.Configuration.Json": "9.0.1",
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
@@ -1791,109 +1791,109 @@
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"System.CommandLine": "2.0.0-beta4.22272.1", "System.CommandLine": "2.0.0-beta4.22272.1",
"System.CommandLine.NamingConventionBinder": "2.0.0-beta4.22272.1", "System.CommandLine.NamingConventionBinder": "2.0.0-beta4.22272.1",
"Wabbajack.Paths": "0.3.15" "Wabbajack.Paths": "0.3.17"
}, },
"runtime": { "runtime": {
"Wabbajack.CLI.Builder.dll": {} "Wabbajack.CLI.Builder.dll": {}
} }
}, },
"Wabbajack.Common/0.3.15": { "Wabbajack.Common/0.3.17": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"System.Reactive": "6.0.1", "System.Reactive": "6.0.1",
"Wabbajack.DTOs": "0.3.15", "Wabbajack.DTOs": "0.3.17",
"Wabbajack.Networking.Http": "0.3.15", "Wabbajack.Networking.Http": "0.3.17",
"Wabbajack.Paths.IO": "0.3.15" "Wabbajack.Paths.IO": "0.3.17"
}, },
"runtime": { "runtime": {
"Wabbajack.Common.dll": {} "Wabbajack.Common.dll": {}
} }
}, },
"Wabbajack.Compiler/0.3.15": { "Wabbajack.Compiler/0.3.17": {
"dependencies": { "dependencies": {
"F23.StringSimilarity": "6.0.0", "F23.StringSimilarity": "6.0.0",
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Newtonsoft.Json": "13.0.3", "Newtonsoft.Json": "13.0.3",
"SixLabors.ImageSharp": "3.1.6", "SixLabors.ImageSharp": "3.1.6",
"Wabbajack.Downloaders.Dispatcher": "0.3.15", "Wabbajack.Downloaders.Dispatcher": "0.3.17",
"Wabbajack.Installer": "0.3.15", "Wabbajack.Installer": "0.3.17",
"Wabbajack.VFS": "0.3.15", "Wabbajack.VFS": "0.3.17",
"ini-parser-netstandard": "2.5.2" "ini-parser-netstandard": "2.5.2"
}, },
"runtime": { "runtime": {
"Wabbajack.Compiler.dll": {} "Wabbajack.Compiler.dll": {}
} }
}, },
"Wabbajack.Compression.BSA/0.3.15": { "Wabbajack.Compression.BSA/0.3.17": {
"dependencies": { "dependencies": {
"K4os.Compression.LZ4.Streams": "1.3.8", "K4os.Compression.LZ4.Streams": "1.3.8",
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"SharpZipLib": "1.4.2", "SharpZipLib": "1.4.2",
"Wabbajack.Common": "0.3.15", "Wabbajack.Common": "0.3.17",
"Wabbajack.DTOs": "0.3.15" "Wabbajack.DTOs": "0.3.17"
}, },
"runtime": { "runtime": {
"Wabbajack.Compression.BSA.dll": {} "Wabbajack.Compression.BSA.dll": {}
} }
}, },
"Wabbajack.Compression.Zip/0.3.15": { "Wabbajack.Compression.Zip/0.3.17": {
"dependencies": { "dependencies": {
"Wabbajack.IO.Async": "0.3.15" "Wabbajack.IO.Async": "0.3.17"
}, },
"runtime": { "runtime": {
"Wabbajack.Compression.Zip.dll": {} "Wabbajack.Compression.Zip.dll": {}
} }
}, },
"Wabbajack.Configuration/0.3.15": { "Wabbajack.Configuration/0.3.17": {
"runtime": { "runtime": {
"Wabbajack.Configuration.dll": {} "Wabbajack.Configuration.dll": {}
} }
}, },
"Wabbajack.Downloaders.Bethesda/0.3.15": { "Wabbajack.Downloaders.Bethesda/0.3.17": {
"dependencies": { "dependencies": {
"LibAES-CTR": "1.1.0", "LibAES-CTR": "1.1.0",
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"SharpZipLib": "1.4.2", "SharpZipLib": "1.4.2",
"Wabbajack.Common": "0.3.15", "Wabbajack.Common": "0.3.17",
"Wabbajack.Downloaders.Interfaces": "0.3.15", "Wabbajack.Downloaders.Interfaces": "0.3.17",
"Wabbajack.Networking.BethesdaNet": "0.3.15" "Wabbajack.Networking.BethesdaNet": "0.3.17"
}, },
"runtime": { "runtime": {
"Wabbajack.Downloaders.Bethesda.dll": {} "Wabbajack.Downloaders.Bethesda.dll": {}
} }
}, },
"Wabbajack.Downloaders.Dispatcher/0.3.15": { "Wabbajack.Downloaders.Dispatcher/0.3.17": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Newtonsoft.Json": "13.0.3", "Newtonsoft.Json": "13.0.3",
"SixLabors.ImageSharp": "3.1.6", "SixLabors.ImageSharp": "3.1.6",
"Wabbajack.Downloaders.Bethesda": "0.3.15", "Wabbajack.Downloaders.Bethesda": "0.3.17",
"Wabbajack.Downloaders.GameFile": "0.3.15", "Wabbajack.Downloaders.GameFile": "0.3.17",
"Wabbajack.Downloaders.GoogleDrive": "0.3.15", "Wabbajack.Downloaders.GoogleDrive": "0.3.17",
"Wabbajack.Downloaders.Http": "0.3.15", "Wabbajack.Downloaders.Http": "0.3.17",
"Wabbajack.Downloaders.IPS4OAuth2Downloader": "0.3.15", "Wabbajack.Downloaders.IPS4OAuth2Downloader": "0.3.17",
"Wabbajack.Downloaders.Interfaces": "0.3.15", "Wabbajack.Downloaders.Interfaces": "0.3.17",
"Wabbajack.Downloaders.Manual": "0.3.15", "Wabbajack.Downloaders.Manual": "0.3.17",
"Wabbajack.Downloaders.MediaFire": "0.3.15", "Wabbajack.Downloaders.MediaFire": "0.3.17",
"Wabbajack.Downloaders.Mega": "0.3.15", "Wabbajack.Downloaders.Mega": "0.3.17",
"Wabbajack.Downloaders.ModDB": "0.3.15", "Wabbajack.Downloaders.ModDB": "0.3.17",
"Wabbajack.Downloaders.Nexus": "0.3.15", "Wabbajack.Downloaders.Nexus": "0.3.17",
"Wabbajack.Downloaders.VerificationCache": "0.3.15", "Wabbajack.Downloaders.VerificationCache": "0.3.17",
"Wabbajack.Downloaders.WabbajackCDN": "0.3.15", "Wabbajack.Downloaders.WabbajackCDN": "0.3.17",
"Wabbajack.Networking.WabbajackClientApi": "0.3.15" "Wabbajack.Networking.WabbajackClientApi": "0.3.17"
}, },
"runtime": { "runtime": {
"Wabbajack.Downloaders.Dispatcher.dll": {} "Wabbajack.Downloaders.Dispatcher.dll": {}
} }
}, },
"Wabbajack.Downloaders.GameFile/0.3.15": { "Wabbajack.Downloaders.GameFile/0.3.17": {
"dependencies": { "dependencies": {
"GameFinder.StoreHandlers.EADesktop": "4.5.0", "GameFinder.StoreHandlers.EADesktop": "4.5.0",
"GameFinder.StoreHandlers.EGS": "4.5.0", "GameFinder.StoreHandlers.EGS": "4.5.0",
@@ -1903,360 +1903,360 @@
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"SixLabors.ImageSharp": "3.1.6", "SixLabors.ImageSharp": "3.1.6",
"Wabbajack.Downloaders.Interfaces": "0.3.15", "Wabbajack.Downloaders.Interfaces": "0.3.17",
"Wabbajack.VFS": "0.3.15" "Wabbajack.VFS": "0.3.17"
}, },
"runtime": { "runtime": {
"Wabbajack.Downloaders.GameFile.dll": {} "Wabbajack.Downloaders.GameFile.dll": {}
} }
}, },
"Wabbajack.Downloaders.GoogleDrive/0.3.15": { "Wabbajack.Downloaders.GoogleDrive/0.3.17": {
"dependencies": { "dependencies": {
"HtmlAgilityPack": "1.11.72", "HtmlAgilityPack": "1.11.72",
"Microsoft.AspNetCore.Http.Extensions": "2.3.0", "Microsoft.AspNetCore.Http.Extensions": "2.3.0",
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Wabbajack.Common": "0.3.15", "Wabbajack.Common": "0.3.17",
"Wabbajack.DTOs": "0.3.15", "Wabbajack.DTOs": "0.3.17",
"Wabbajack.Downloaders.Interfaces": "0.3.15", "Wabbajack.Downloaders.Interfaces": "0.3.17",
"Wabbajack.Networking.Http": "0.3.15", "Wabbajack.Networking.Http": "0.3.17",
"Wabbajack.Networking.Http.Interfaces": "0.3.15" "Wabbajack.Networking.Http.Interfaces": "0.3.17"
}, },
"runtime": { "runtime": {
"Wabbajack.Downloaders.GoogleDrive.dll": {} "Wabbajack.Downloaders.GoogleDrive.dll": {}
} }
}, },
"Wabbajack.Downloaders.Http/0.3.15": { "Wabbajack.Downloaders.Http/0.3.17": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Wabbajack.Common": "0.3.15", "Wabbajack.Common": "0.3.17",
"Wabbajack.DTOs": "0.3.15", "Wabbajack.DTOs": "0.3.17",
"Wabbajack.Downloaders.Interfaces": "0.3.15", "Wabbajack.Downloaders.Interfaces": "0.3.17",
"Wabbajack.Networking.BethesdaNet": "0.3.15", "Wabbajack.Networking.BethesdaNet": "0.3.17",
"Wabbajack.Networking.Http.Interfaces": "0.3.15", "Wabbajack.Networking.Http.Interfaces": "0.3.17",
"Wabbajack.Paths.IO": "0.3.15" "Wabbajack.Paths.IO": "0.3.17"
}, },
"runtime": { "runtime": {
"Wabbajack.Downloaders.Http.dll": {} "Wabbajack.Downloaders.Http.dll": {}
} }
}, },
"Wabbajack.Downloaders.Interfaces/0.3.15": { "Wabbajack.Downloaders.Interfaces/0.3.17": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Wabbajack.Compression.Zip": "0.3.15", "Wabbajack.Compression.Zip": "0.3.17",
"Wabbajack.DTOs": "0.3.15", "Wabbajack.DTOs": "0.3.17",
"Wabbajack.Paths.IO": "0.3.15" "Wabbajack.Paths.IO": "0.3.17"
}, },
"runtime": { "runtime": {
"Wabbajack.Downloaders.Interfaces.dll": {} "Wabbajack.Downloaders.Interfaces.dll": {}
} }
}, },
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.15": { "Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.17": {
"dependencies": { "dependencies": {
"F23.StringSimilarity": "6.0.0", "F23.StringSimilarity": "6.0.0",
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Wabbajack.Common": "0.3.15", "Wabbajack.Common": "0.3.17",
"Wabbajack.Downloaders.Interfaces": "0.3.15", "Wabbajack.Downloaders.Interfaces": "0.3.17",
"Wabbajack.Networking.Http": "0.3.15", "Wabbajack.Networking.Http": "0.3.17",
"Wabbajack.Networking.Http.Interfaces": "0.3.15" "Wabbajack.Networking.Http.Interfaces": "0.3.17"
}, },
"runtime": { "runtime": {
"Wabbajack.Downloaders.IPS4OAuth2Downloader.dll": {} "Wabbajack.Downloaders.IPS4OAuth2Downloader.dll": {}
} }
}, },
"Wabbajack.Downloaders.Manual/0.3.15": { "Wabbajack.Downloaders.Manual/0.3.17": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Wabbajack.Common": "0.3.15", "Wabbajack.Common": "0.3.17",
"Wabbajack.Downloaders.Interfaces": "0.3.15" "Wabbajack.Downloaders.Interfaces": "0.3.17"
}, },
"runtime": { "runtime": {
"Wabbajack.Downloaders.Manual.dll": {} "Wabbajack.Downloaders.Manual.dll": {}
} }
}, },
"Wabbajack.Downloaders.MediaFire/0.3.15": { "Wabbajack.Downloaders.MediaFire/0.3.17": {
"dependencies": { "dependencies": {
"HtmlAgilityPack": "1.11.72", "HtmlAgilityPack": "1.11.72",
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Wabbajack.Common": "0.3.15", "Wabbajack.Common": "0.3.17",
"Wabbajack.Downloaders.Interfaces": "0.3.15", "Wabbajack.Downloaders.Interfaces": "0.3.17",
"Wabbajack.Networking.Http.Interfaces": "0.3.15" "Wabbajack.Networking.Http.Interfaces": "0.3.17"
}, },
"runtime": { "runtime": {
"Wabbajack.Downloaders.MediaFire.dll": {} "Wabbajack.Downloaders.MediaFire.dll": {}
} }
}, },
"Wabbajack.Downloaders.Mega/0.3.15": { "Wabbajack.Downloaders.Mega/0.3.17": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Newtonsoft.Json": "13.0.3", "Newtonsoft.Json": "13.0.3",
"Wabbajack.Common": "0.3.15", "Wabbajack.Common": "0.3.17",
"Wabbajack.Downloaders.Interfaces": "0.3.15", "Wabbajack.Downloaders.Interfaces": "0.3.17",
"Wabbajack.Paths.IO": "0.3.15" "Wabbajack.Paths.IO": "0.3.17"
}, },
"runtime": { "runtime": {
"Wabbajack.Downloaders.Mega.dll": {} "Wabbajack.Downloaders.Mega.dll": {}
} }
}, },
"Wabbajack.Downloaders.ModDB/0.3.15": { "Wabbajack.Downloaders.ModDB/0.3.17": {
"dependencies": { "dependencies": {
"HtmlAgilityPack": "1.11.72", "HtmlAgilityPack": "1.11.72",
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Newtonsoft.Json": "13.0.3", "Newtonsoft.Json": "13.0.3",
"Wabbajack.Common": "0.3.15", "Wabbajack.Common": "0.3.17",
"Wabbajack.Downloaders.Interfaces": "0.3.15", "Wabbajack.Downloaders.Interfaces": "0.3.17",
"Wabbajack.Networking.Http": "0.3.15", "Wabbajack.Networking.Http": "0.3.17",
"Wabbajack.Networking.Http.Interfaces": "0.3.15" "Wabbajack.Networking.Http.Interfaces": "0.3.17"
}, },
"runtime": { "runtime": {
"Wabbajack.Downloaders.ModDB.dll": {} "Wabbajack.Downloaders.ModDB.dll": {}
} }
}, },
"Wabbajack.Downloaders.Nexus/0.3.15": { "Wabbajack.Downloaders.Nexus/0.3.17": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Wabbajack.DTOs": "0.3.15", "Wabbajack.DTOs": "0.3.17",
"Wabbajack.Downloaders.Interfaces": "0.3.15", "Wabbajack.Downloaders.Interfaces": "0.3.17",
"Wabbajack.Hashing.xxHash64": "0.3.15", "Wabbajack.Hashing.xxHash64": "0.3.17",
"Wabbajack.Networking.Http": "0.3.15", "Wabbajack.Networking.Http": "0.3.17",
"Wabbajack.Networking.Http.Interfaces": "0.3.15", "Wabbajack.Networking.Http.Interfaces": "0.3.17",
"Wabbajack.Networking.NexusApi": "0.3.15", "Wabbajack.Networking.NexusApi": "0.3.17",
"Wabbajack.Paths": "0.3.15" "Wabbajack.Paths": "0.3.17"
}, },
"runtime": { "runtime": {
"Wabbajack.Downloaders.Nexus.dll": {} "Wabbajack.Downloaders.Nexus.dll": {}
} }
}, },
"Wabbajack.Downloaders.VerificationCache/0.3.15": { "Wabbajack.Downloaders.VerificationCache/0.3.17": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Stub.System.Data.SQLite.Core.NetStandard": "1.0.119", "Stub.System.Data.SQLite.Core.NetStandard": "1.0.119",
"Wabbajack.DTOs": "0.3.15", "Wabbajack.DTOs": "0.3.17",
"Wabbajack.Paths.IO": "0.3.15" "Wabbajack.Paths.IO": "0.3.17"
}, },
"runtime": { "runtime": {
"Wabbajack.Downloaders.VerificationCache.dll": {} "Wabbajack.Downloaders.VerificationCache.dll": {}
} }
}, },
"Wabbajack.Downloaders.WabbajackCDN/0.3.15": { "Wabbajack.Downloaders.WabbajackCDN/0.3.17": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Microsoft.Toolkit.HighPerformance": "7.1.2", "Microsoft.Toolkit.HighPerformance": "7.1.2",
"Wabbajack.Common": "0.3.15", "Wabbajack.Common": "0.3.17",
"Wabbajack.Downloaders.Interfaces": "0.3.15", "Wabbajack.Downloaders.Interfaces": "0.3.17",
"Wabbajack.Networking.Http": "0.3.15", "Wabbajack.Networking.Http": "0.3.17",
"Wabbajack.RateLimiter": "0.3.15" "Wabbajack.RateLimiter": "0.3.17"
}, },
"runtime": { "runtime": {
"Wabbajack.Downloaders.WabbajackCDN.dll": {} "Wabbajack.Downloaders.WabbajackCDN.dll": {}
} }
}, },
"Wabbajack.DTOs/0.3.15": { "Wabbajack.DTOs/0.3.17": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Wabbajack.Hashing.xxHash64": "0.3.15", "Wabbajack.Hashing.xxHash64": "0.3.17",
"Wabbajack.Paths": "0.3.15" "Wabbajack.Paths": "0.3.17"
}, },
"runtime": { "runtime": {
"Wabbajack.DTOs.dll": {} "Wabbajack.DTOs.dll": {}
} }
}, },
"Wabbajack.FileExtractor/0.3.15": { "Wabbajack.FileExtractor/0.3.17": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"OMODFramework": "3.0.1", "OMODFramework": "3.0.1",
"Wabbajack.Common": "0.3.15", "Wabbajack.Common": "0.3.17",
"Wabbajack.Compression.BSA": "0.3.15", "Wabbajack.Compression.BSA": "0.3.17",
"Wabbajack.Hashing.PHash": "0.3.15", "Wabbajack.Hashing.PHash": "0.3.17",
"Wabbajack.Paths": "0.3.15" "Wabbajack.Paths": "0.3.17"
}, },
"runtime": { "runtime": {
"Wabbajack.FileExtractor.dll": {} "Wabbajack.FileExtractor.dll": {}
} }
}, },
"Wabbajack.Hashing.PHash/0.3.15": { "Wabbajack.Hashing.PHash/0.3.17": {
"dependencies": { "dependencies": {
"BCnEncoder.Net.ImageSharp": "1.1.1", "BCnEncoder.Net.ImageSharp": "1.1.1",
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Shipwreck.Phash": "0.5.0", "Shipwreck.Phash": "0.5.0",
"SixLabors.ImageSharp": "3.1.6", "SixLabors.ImageSharp": "3.1.6",
"Wabbajack.Common": "0.3.15", "Wabbajack.Common": "0.3.17",
"Wabbajack.DTOs": "0.3.15", "Wabbajack.DTOs": "0.3.17",
"Wabbajack.Paths": "0.3.15", "Wabbajack.Paths": "0.3.17",
"Wabbajack.Paths.IO": "0.3.15" "Wabbajack.Paths.IO": "0.3.17"
}, },
"runtime": { "runtime": {
"Wabbajack.Hashing.PHash.dll": {} "Wabbajack.Hashing.PHash.dll": {}
} }
}, },
"Wabbajack.Hashing.xxHash64/0.3.15": { "Wabbajack.Hashing.xxHash64/0.3.17": {
"dependencies": { "dependencies": {
"Wabbajack.Paths": "0.3.15", "Wabbajack.Paths": "0.3.17",
"Wabbajack.RateLimiter": "0.3.15" "Wabbajack.RateLimiter": "0.3.17"
}, },
"runtime": { "runtime": {
"Wabbajack.Hashing.xxHash64.dll": {} "Wabbajack.Hashing.xxHash64.dll": {}
} }
}, },
"Wabbajack.Installer/0.3.15": { "Wabbajack.Installer/0.3.17": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Newtonsoft.Json": "13.0.3", "Newtonsoft.Json": "13.0.3",
"Octopus.Octodiff": "2.0.548", "Octopus.Octodiff": "2.0.548",
"SixLabors.ImageSharp": "3.1.6", "SixLabors.ImageSharp": "3.1.6",
"Wabbajack.DTOs": "0.3.15", "Wabbajack.DTOs": "0.3.17",
"Wabbajack.Downloaders.Dispatcher": "0.3.15", "Wabbajack.Downloaders.Dispatcher": "0.3.17",
"Wabbajack.Downloaders.GameFile": "0.3.15", "Wabbajack.Downloaders.GameFile": "0.3.17",
"Wabbajack.FileExtractor": "0.3.15", "Wabbajack.FileExtractor": "0.3.17",
"Wabbajack.Networking.WabbajackClientApi": "0.3.15", "Wabbajack.Networking.WabbajackClientApi": "0.3.17",
"Wabbajack.Paths": "0.3.15", "Wabbajack.Paths": "0.3.17",
"Wabbajack.Paths.IO": "0.3.15", "Wabbajack.Paths.IO": "0.3.17",
"Wabbajack.VFS": "0.3.15", "Wabbajack.VFS": "0.3.17",
"ini-parser-netstandard": "2.5.2" "ini-parser-netstandard": "2.5.2"
}, },
"runtime": { "runtime": {
"Wabbajack.Installer.dll": {} "Wabbajack.Installer.dll": {}
} }
}, },
"Wabbajack.IO.Async/0.3.15": { "Wabbajack.IO.Async/0.3.17": {
"runtime": { "runtime": {
"Wabbajack.IO.Async.dll": {} "Wabbajack.IO.Async.dll": {}
} }
}, },
"Wabbajack.Networking.BethesdaNet/0.3.15": { "Wabbajack.Networking.BethesdaNet/0.3.17": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Wabbajack.DTOs": "0.3.15", "Wabbajack.DTOs": "0.3.17",
"Wabbajack.Networking.Http": "0.3.15", "Wabbajack.Networking.Http": "0.3.17",
"Wabbajack.Networking.Http.Interfaces": "0.3.15" "Wabbajack.Networking.Http.Interfaces": "0.3.17"
}, },
"runtime": { "runtime": {
"Wabbajack.Networking.BethesdaNet.dll": {} "Wabbajack.Networking.BethesdaNet.dll": {}
} }
}, },
"Wabbajack.Networking.Discord/0.3.15": { "Wabbajack.Networking.Discord/0.3.17": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Wabbajack.Networking.Http.Interfaces": "0.3.15" "Wabbajack.Networking.Http.Interfaces": "0.3.17"
}, },
"runtime": { "runtime": {
"Wabbajack.Networking.Discord.dll": {} "Wabbajack.Networking.Discord.dll": {}
} }
}, },
"Wabbajack.Networking.GitHub/0.3.15": { "Wabbajack.Networking.GitHub/0.3.17": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Octokit": "14.0.0", "Octokit": "14.0.0",
"Wabbajack.DTOs": "0.3.15", "Wabbajack.DTOs": "0.3.17",
"Wabbajack.Networking.Http.Interfaces": "0.3.15" "Wabbajack.Networking.Http.Interfaces": "0.3.17"
}, },
"runtime": { "runtime": {
"Wabbajack.Networking.GitHub.dll": {} "Wabbajack.Networking.GitHub.dll": {}
} }
}, },
"Wabbajack.Networking.Http/0.3.15": { "Wabbajack.Networking.Http/0.3.17": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Http": "9.0.1", "Microsoft.Extensions.Http": "9.0.1",
"Microsoft.Extensions.Logging": "9.0.1", "Microsoft.Extensions.Logging": "9.0.1",
"Wabbajack.Configuration": "0.3.15", "Wabbajack.Configuration": "0.3.17",
"Wabbajack.Downloaders.Interfaces": "0.3.15", "Wabbajack.Downloaders.Interfaces": "0.3.17",
"Wabbajack.Hashing.xxHash64": "0.3.15", "Wabbajack.Hashing.xxHash64": "0.3.17",
"Wabbajack.Networking.Http.Interfaces": "0.3.15", "Wabbajack.Networking.Http.Interfaces": "0.3.17",
"Wabbajack.Paths": "0.3.15", "Wabbajack.Paths": "0.3.17",
"Wabbajack.Paths.IO": "0.3.15" "Wabbajack.Paths.IO": "0.3.17"
}, },
"runtime": { "runtime": {
"Wabbajack.Networking.Http.dll": {} "Wabbajack.Networking.Http.dll": {}
} }
}, },
"Wabbajack.Networking.Http.Interfaces/0.3.15": { "Wabbajack.Networking.Http.Interfaces/0.3.17": {
"dependencies": { "dependencies": {
"Wabbajack.Hashing.xxHash64": "0.3.15" "Wabbajack.Hashing.xxHash64": "0.3.17"
}, },
"runtime": { "runtime": {
"Wabbajack.Networking.Http.Interfaces.dll": {} "Wabbajack.Networking.Http.Interfaces.dll": {}
} }
}, },
"Wabbajack.Networking.NexusApi/0.3.15": { "Wabbajack.Networking.NexusApi/0.3.17": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Wabbajack.DTOs": "0.3.15", "Wabbajack.DTOs": "0.3.17",
"Wabbajack.Networking.Http": "0.3.15", "Wabbajack.Networking.Http": "0.3.17",
"Wabbajack.Networking.Http.Interfaces": "0.3.15", "Wabbajack.Networking.Http.Interfaces": "0.3.17",
"Wabbajack.Networking.WabbajackClientApi": "0.3.15" "Wabbajack.Networking.WabbajackClientApi": "0.3.17"
}, },
"runtime": { "runtime": {
"Wabbajack.Networking.NexusApi.dll": {} "Wabbajack.Networking.NexusApi.dll": {}
} }
}, },
"Wabbajack.Networking.WabbajackClientApi/0.3.15": { "Wabbajack.Networking.WabbajackClientApi/0.3.17": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Octokit": "14.0.0", "Octokit": "14.0.0",
"Wabbajack.Common": "0.3.15", "Wabbajack.Common": "0.3.17",
"Wabbajack.DTOs": "0.3.15", "Wabbajack.DTOs": "0.3.17",
"Wabbajack.Paths.IO": "0.3.15", "Wabbajack.Paths.IO": "0.3.17",
"Wabbajack.VFS.Interfaces": "0.3.15", "Wabbajack.VFS.Interfaces": "0.3.17",
"YamlDotNet": "16.3.0" "YamlDotNet": "16.3.0"
}, },
"runtime": { "runtime": {
"Wabbajack.Networking.WabbajackClientApi.dll": {} "Wabbajack.Networking.WabbajackClientApi.dll": {}
} }
}, },
"Wabbajack.Paths/0.3.15": { "Wabbajack.Paths/0.3.17": {
"runtime": { "runtime": {
"Wabbajack.Paths.dll": {} "Wabbajack.Paths.dll": {}
} }
}, },
"Wabbajack.Paths.IO/0.3.15": { "Wabbajack.Paths.IO/0.3.17": {
"dependencies": { "dependencies": {
"Wabbajack.Paths": "0.3.15", "Wabbajack.Paths": "0.3.17",
"shortid": "4.0.0" "shortid": "4.0.0"
}, },
"runtime": { "runtime": {
"Wabbajack.Paths.IO.dll": {} "Wabbajack.Paths.IO.dll": {}
} }
}, },
"Wabbajack.RateLimiter/0.3.15": { "Wabbajack.RateLimiter/0.3.17": {
"runtime": { "runtime": {
"Wabbajack.RateLimiter.dll": {} "Wabbajack.RateLimiter.dll": {}
} }
}, },
"Wabbajack.Server.Lib/0.3.15": { "Wabbajack.Server.Lib/0.3.17": {
"dependencies": { "dependencies": {
"FluentFTP": "52.0.0", "FluentFTP": "52.0.0",
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
@@ -2264,58 +2264,58 @@
"Nettle": "3.0.0", "Nettle": "3.0.0",
"Newtonsoft.Json": "13.0.3", "Newtonsoft.Json": "13.0.3",
"SixLabors.ImageSharp": "3.1.6", "SixLabors.ImageSharp": "3.1.6",
"Wabbajack.Common": "0.3.15", "Wabbajack.Common": "0.3.17",
"Wabbajack.Networking.Http.Interfaces": "0.3.15", "Wabbajack.Networking.Http.Interfaces": "0.3.17",
"Wabbajack.Services.OSIntegrated": "0.3.15" "Wabbajack.Services.OSIntegrated": "0.3.17"
}, },
"runtime": { "runtime": {
"Wabbajack.Server.Lib.dll": {} "Wabbajack.Server.Lib.dll": {}
} }
}, },
"Wabbajack.Services.OSIntegrated/0.3.15": { "Wabbajack.Services.OSIntegrated/0.3.17": {
"dependencies": { "dependencies": {
"DeviceId": "6.8.0", "DeviceId": "6.8.0",
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Newtonsoft.Json": "13.0.3", "Newtonsoft.Json": "13.0.3",
"SixLabors.ImageSharp": "3.1.6", "SixLabors.ImageSharp": "3.1.6",
"Wabbajack.Compiler": "0.3.15", "Wabbajack.Compiler": "0.3.17",
"Wabbajack.Downloaders.Dispatcher": "0.3.15", "Wabbajack.Downloaders.Dispatcher": "0.3.17",
"Wabbajack.Installer": "0.3.15", "Wabbajack.Installer": "0.3.17",
"Wabbajack.Networking.BethesdaNet": "0.3.15", "Wabbajack.Networking.BethesdaNet": "0.3.17",
"Wabbajack.Networking.Discord": "0.3.15", "Wabbajack.Networking.Discord": "0.3.17",
"Wabbajack.VFS": "0.3.15" "Wabbajack.VFS": "0.3.17"
}, },
"runtime": { "runtime": {
"Wabbajack.Services.OSIntegrated.dll": {} "Wabbajack.Services.OSIntegrated.dll": {}
} }
}, },
"Wabbajack.VFS/0.3.15": { "Wabbajack.VFS/0.3.17": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"SixLabors.ImageSharp": "3.1.6", "SixLabors.ImageSharp": "3.1.6",
"System.Data.SQLite.Core": "1.0.119", "System.Data.SQLite.Core": "1.0.119",
"Wabbajack.Common": "0.3.15", "Wabbajack.Common": "0.3.17",
"Wabbajack.FileExtractor": "0.3.15", "Wabbajack.FileExtractor": "0.3.17",
"Wabbajack.Hashing.PHash": "0.3.15", "Wabbajack.Hashing.PHash": "0.3.17",
"Wabbajack.Hashing.xxHash64": "0.3.15", "Wabbajack.Hashing.xxHash64": "0.3.17",
"Wabbajack.Paths": "0.3.15", "Wabbajack.Paths": "0.3.17",
"Wabbajack.Paths.IO": "0.3.15", "Wabbajack.Paths.IO": "0.3.17",
"Wabbajack.VFS.Interfaces": "0.3.15" "Wabbajack.VFS.Interfaces": "0.3.17"
}, },
"runtime": { "runtime": {
"Wabbajack.VFS.dll": {} "Wabbajack.VFS.dll": {}
} }
}, },
"Wabbajack.VFS.Interfaces/0.3.15": { "Wabbajack.VFS.Interfaces/0.3.17": {
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Wabbajack.DTOs": "0.3.15", "Wabbajack.DTOs": "0.3.17",
"Wabbajack.Hashing.xxHash64": "0.3.15", "Wabbajack.Hashing.xxHash64": "0.3.17",
"Wabbajack.Paths": "0.3.15" "Wabbajack.Paths": "0.3.17"
}, },
"runtime": { "runtime": {
"Wabbajack.VFS.Interfaces.dll": {} "Wabbajack.VFS.Interfaces.dll": {}
@@ -2332,7 +2332,7 @@
} }
}, },
"libraries": { "libraries": {
"jackify-engine/0.3.15": { "jackify-engine/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
@@ -3021,202 +3021,202 @@
"path": "yamldotnet/16.3.0", "path": "yamldotnet/16.3.0",
"hashPath": "yamldotnet.16.3.0.nupkg.sha512" "hashPath": "yamldotnet.16.3.0.nupkg.sha512"
}, },
"Wabbajack.CLI.Builder/0.3.15": { "Wabbajack.CLI.Builder/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Common/0.3.15": { "Wabbajack.Common/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Compiler/0.3.15": { "Wabbajack.Compiler/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Compression.BSA/0.3.15": { "Wabbajack.Compression.BSA/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Compression.Zip/0.3.15": { "Wabbajack.Compression.Zip/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Configuration/0.3.15": { "Wabbajack.Configuration/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Downloaders.Bethesda/0.3.15": { "Wabbajack.Downloaders.Bethesda/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Downloaders.Dispatcher/0.3.15": { "Wabbajack.Downloaders.Dispatcher/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Downloaders.GameFile/0.3.15": { "Wabbajack.Downloaders.GameFile/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Downloaders.GoogleDrive/0.3.15": { "Wabbajack.Downloaders.GoogleDrive/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Downloaders.Http/0.3.15": { "Wabbajack.Downloaders.Http/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Downloaders.Interfaces/0.3.15": { "Wabbajack.Downloaders.Interfaces/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.15": { "Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Downloaders.Manual/0.3.15": { "Wabbajack.Downloaders.Manual/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Downloaders.MediaFire/0.3.15": { "Wabbajack.Downloaders.MediaFire/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Downloaders.Mega/0.3.15": { "Wabbajack.Downloaders.Mega/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Downloaders.ModDB/0.3.15": { "Wabbajack.Downloaders.ModDB/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Downloaders.Nexus/0.3.15": { "Wabbajack.Downloaders.Nexus/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Downloaders.VerificationCache/0.3.15": { "Wabbajack.Downloaders.VerificationCache/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Downloaders.WabbajackCDN/0.3.15": { "Wabbajack.Downloaders.WabbajackCDN/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.DTOs/0.3.15": { "Wabbajack.DTOs/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.FileExtractor/0.3.15": { "Wabbajack.FileExtractor/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Hashing.PHash/0.3.15": { "Wabbajack.Hashing.PHash/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Hashing.xxHash64/0.3.15": { "Wabbajack.Hashing.xxHash64/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Installer/0.3.15": { "Wabbajack.Installer/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.IO.Async/0.3.15": { "Wabbajack.IO.Async/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Networking.BethesdaNet/0.3.15": { "Wabbajack.Networking.BethesdaNet/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Networking.Discord/0.3.15": { "Wabbajack.Networking.Discord/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Networking.GitHub/0.3.15": { "Wabbajack.Networking.GitHub/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Networking.Http/0.3.15": { "Wabbajack.Networking.Http/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Networking.Http.Interfaces/0.3.15": { "Wabbajack.Networking.Http.Interfaces/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Networking.NexusApi/0.3.15": { "Wabbajack.Networking.NexusApi/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Networking.WabbajackClientApi/0.3.15": { "Wabbajack.Networking.WabbajackClientApi/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Paths/0.3.15": { "Wabbajack.Paths/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Paths.IO/0.3.15": { "Wabbajack.Paths.IO/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.RateLimiter/0.3.15": { "Wabbajack.RateLimiter/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Server.Lib/0.3.15": { "Wabbajack.Server.Lib/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.Services.OSIntegrated/0.3.15": { "Wabbajack.Services.OSIntegrated/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.VFS/0.3.15": { "Wabbajack.VFS/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""
}, },
"Wabbajack.VFS.Interfaces/0.3.15": { "Wabbajack.VFS.Interfaces/0.3.17": {
"type": "project", "type": "project",
"serviceable": false, "serviceable": false,
"sha512": "" "sha512": ""

Binary file not shown.

View File

@@ -257,13 +257,19 @@ class UpdateDialog(QDialog):
self.progress_label.setText("Download completed successfully!") self.progress_label.setText("Download completed successfully!")
self.progress_bar.setValue(100) self.progress_bar.setValue(100)
# Show install button # Check if auto-restart is enabled
self.download_button.setVisible(False) if self.auto_restart_checkbox.isChecked():
self.install_button.setVisible(True) # Auto-install immediately
self.progress_label.setText("Auto-installing update...")
self.install_update()
else:
# Show install button for manual installation
self.download_button.setVisible(False)
self.install_button.setVisible(True)
# Re-enable other buttons # Re-enable other buttons
self.later_button.setEnabled(True) self.later_button.setEnabled(True)
self.skip_button.setEnabled(True) self.skip_button.setEnabled(True)
else: else:
self.show_error("Download Failed", "Failed to download the update. Please try again later.") self.show_error("Download Failed", "Failed to download the update. Please try again later.")

View File

@@ -102,7 +102,7 @@ sys.path.insert(0, str(src_dir))
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QLabel, QVBoxLayout, QPushButton, QApplication, QMainWindow, QWidget, QLabel, QVBoxLayout, QPushButton,
QStackedWidget, QHBoxLayout, QDialog, QFormLayout, QLineEdit, QCheckBox, QSpinBox, QMessageBox, QGroupBox, QGridLayout, QFileDialog, QToolButton, QStyle, QComboBox QStackedWidget, QHBoxLayout, QDialog, QFormLayout, QLineEdit, QCheckBox, QSpinBox, QMessageBox, QGroupBox, QGridLayout, QFileDialog, QToolButton, QStyle, QComboBox, QTabWidget
) )
from PySide6.QtCore import Qt, QEvent from PySide6.QtCore import Qt, QEvent
from PySide6.QtGui import QIcon from PySide6.QtGui import QIcon
@@ -167,221 +167,26 @@ class SettingsDialog(QDialog):
self._original_debug_mode = self.config_handler.get('debug_mode', False) self._original_debug_mode = self.config_handler.get('debug_mode', False)
self.setWindowTitle("Settings") self.setWindowTitle("Settings")
self.setModal(True) self.setModal(True)
self.setMinimumWidth(750) self.setMinimumWidth(650) # Reduced width for Steam Deck compatibility
self.setMaximumWidth(800) # Maximum width to prevent excessive stretching
self.setStyleSheet("QDialog { background-color: #232323; color: #eee; } QPushButton:hover { background-color: #333; }") self.setStyleSheet("QDialog { background-color: #232323; color: #eee; } QPushButton:hover { background-color: #333; }")
main_layout = QVBoxLayout() main_layout = QVBoxLayout()
self.setLayout(main_layout) self.setLayout(main_layout)
# --- Resource Limits Section --- # Create tab widget
resource_group = QGroupBox("Resource Limits") self.tab_widget = QTabWidget()
resource_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; }") self.tab_widget.setStyleSheet("""
resource_layout = QGridLayout() QTabWidget::pane { border: 1px solid #555; background: #232323; }
resource_group.setLayout(resource_layout) QTabBar::tab { background: #333; color: #eee; padding: 8px 16px; margin: 2px; }
resource_layout.setVerticalSpacing(4) QTabBar::tab:selected { background: #555; }
resource_layout.setHorizontalSpacing(8) QTabBar::tab:hover { background: #444; }
resource_layout.addWidget(self._bold_label("Resource"), 0, 0, 1, 1, Qt.AlignLeft) """)
resource_layout.addWidget(self._bold_label("Max Tasks"), 0, 1, 1, 1, Qt.AlignLeft) main_layout.addWidget(self.tab_widget)
self.resource_settings_path = os.path.expanduser("~/.config/jackify/resource_settings.json")
self.resource_settings = self._load_json(self.resource_settings_path)
self.resource_edits = {}
resource_row_index = 0
for resource_row_index, (k, v) in enumerate(self.resource_settings.items(), start=1):
try:
# Create resource label with optional inline checkbox for File Extractor
if k == "File Extractor":
# Create horizontal layout for File Extractor with inline checkbox
resource_row = QHBoxLayout()
resource_label = QLabel(f"{k}:", parent=self)
resource_row.addWidget(resource_label)
resource_row.addSpacing(10) # Add some spacing
multithreading_checkbox = QCheckBox("Multithreading (Experimental)") # Create tabs
multithreading_checkbox.setChecked(v.get('_7zzMultiThread', 'off') == 'on') self._create_general_tab()
multithreading_checkbox.setToolTip("Enables multithreaded file extraction using 7-Zip. May improve extraction speed on multi-core systems but could be less stable.") self._create_advanced_tab()
multithreading_checkbox.setStyleSheet("color: #fff;")
resource_row.addWidget(multithreading_checkbox)
resource_row.addStretch() # Push checkbox to the left
# Add the horizontal layout to the grid
resource_layout.addLayout(resource_row, resource_row_index, 0)
else:
resource_layout.addWidget(QLabel(f"{k}:", parent=self), resource_row_index, 0, 1, 1, Qt.AlignLeft)
max_tasks_spin = QSpinBox()
max_tasks_spin.setMinimum(1)
max_tasks_spin.setMaximum(128)
max_tasks_spin.setValue(v.get('MaxTasks', 16))
max_tasks_spin.setToolTip("Maximum number of concurrent tasks for this resource.")
max_tasks_spin.setFixedWidth(160)
resource_layout.addWidget(max_tasks_spin, resource_row_index, 1)
# Store the widgets (checkbox for File Extractor, None for others)
if k == "File Extractor":
self.resource_edits[k] = (multithreading_checkbox, max_tasks_spin)
else:
self.resource_edits[k] = (None, max_tasks_spin)
except Exception as e:
print(f"[ERROR] Failed to create widgets for resource '{k}': {e}")
continue
# If no resources exist, show helpful message
if not self.resource_edits:
info_label = QLabel("Resource Limit settings will be generated once a modlist install action is performed")
info_label.setStyleSheet("color: #aaa; font-style: italic; padding: 20px; font-size: 11pt;")
info_label.setWordWrap(True)
info_label.setAlignment(Qt.AlignCenter)
info_label.setMinimumHeight(60) # Ensure enough height to prevent cutoff
resource_layout.addWidget(info_label, 1, 0, 3, 2) # Span more rows for better space
# Bandwidth limiter row (only show if Downloads resource exists)
if "Downloads" in self.resource_settings:
downloads_throughput = self.resource_settings["Downloads"].get("MaxThroughput", 0)
self.bandwidth_spin = QSpinBox()
self.bandwidth_spin.setMinimum(0)
self.bandwidth_spin.setMaximum(1000000)
self.bandwidth_spin.setValue(downloads_throughput)
self.bandwidth_spin.setSuffix(" KB/s")
self.bandwidth_spin.setFixedWidth(160)
self.bandwidth_spin.setToolTip("Set the maximum download speed for modlist downloads. 0 = unlimited.")
bandwidth_note = QLabel("(0 = unlimited)")
bandwidth_note.setStyleSheet("color: #aaa; font-size: 10pt;")
# Create horizontal layout for bandwidth row
bandwidth_row = QHBoxLayout()
bandwidth_row.addWidget(self.bandwidth_spin)
bandwidth_row.addWidget(bandwidth_note)
bandwidth_row.addStretch() # Push to the left
resource_layout.addWidget(QLabel("Bandwidth Limit:", parent=self), resource_row_index+1, 0, 1, 1, Qt.AlignLeft)
resource_layout.addLayout(bandwidth_row, resource_row_index+1, 1)
else:
self.bandwidth_spin = None # No bandwidth UI if Downloads resource doesn't exist
main_layout.addWidget(resource_group)
main_layout.addSpacing(12)
# --- Debug & Diagnostics Section ---
debug_group = QGroupBox("Debug & Diagnostics")
debug_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; }")
debug_layout = QVBoxLayout()
debug_group.setLayout(debug_layout)
self.debug_checkbox = QCheckBox("Enable debug mode (requires restart)")
# Load debug_mode from config
self.debug_checkbox.setChecked(self.config_handler.get('debug_mode', False))
self.debug_checkbox.setToolTip("Enable verbose debug logging. Requires Jackify restart to take effect.")
self.debug_checkbox.setStyleSheet("color: #fff;")
debug_layout.addWidget(self.debug_checkbox)
main_layout.addWidget(debug_group)
main_layout.addSpacing(12)
# --- Nexus API Key Section ---
api_group = QGroupBox("Nexus API Key")
api_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; }")
api_layout = QHBoxLayout()
api_group.setLayout(api_layout)
self.api_key_edit = QLineEdit()
self.api_key_edit.setEchoMode(QLineEdit.Password)
api_key = self.config_handler.get_api_key()
if api_key:
self.api_key_edit.setText(api_key)
else:
self.api_key_edit.setText("")
self.api_key_edit.setToolTip("Your Nexus API Key (obfuscated by default, click Show to reveal)")
# Connect for immediate saving when text changes
self.api_key_edit.textChanged.connect(self._on_api_key_changed)
self.api_show_btn = QToolButton()
self.api_show_btn.setCheckable(True)
self.api_show_btn.setIcon(QIcon.fromTheme("view-visible"))
self.api_show_btn.setToolTip("Show or hide your API key")
self.api_show_btn.toggled.connect(self._toggle_api_key_visibility)
self.api_show_btn.setStyleSheet("")
clear_api_btn = QPushButton("Clear API Key")
clear_api_btn.clicked.connect(self._clear_api_key)
api_layout.addWidget(QLabel("Nexus API Key:"))
api_layout.addWidget(self.api_key_edit)
api_layout.addWidget(self.api_show_btn)
api_layout.addWidget(clear_api_btn)
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; }")
dir_layout = QFormLayout()
dir_group.setLayout(dir_layout)
self.install_dir_edit = QLineEdit(self.config_handler.get("modlist_install_base_dir", ""))
self.install_dir_edit.setToolTip("Default directory for modlist installations.")
self.install_dir_btn = QPushButton()
self.install_dir_btn.setIcon(QIcon.fromTheme("folder-open"))
self.install_dir_btn.setToolTip("Browse for directory")
self.install_dir_btn.setFixedWidth(32)
self.install_dir_btn.clicked.connect(lambda: self._pick_directory(self.install_dir_edit))
install_dir_row = QHBoxLayout()
install_dir_row.addWidget(self.install_dir_edit)
install_dir_row.addWidget(self.install_dir_btn)
dir_layout.addRow(QLabel("Install Base Dir:"), install_dir_row)
self.download_dir_edit = QLineEdit(self.config_handler.get("modlist_downloads_base_dir", ""))
self.download_dir_edit.setToolTip("Default directory for modlist downloads.")
self.download_dir_btn = QPushButton()
self.download_dir_btn.setIcon(QIcon.fromTheme("folder-open"))
self.download_dir_btn.setToolTip("Browse for directory")
self.download_dir_btn.setFixedWidth(32)
self.download_dir_btn.clicked.connect(lambda: self._pick_directory(self.download_dir_edit))
download_dir_row = QHBoxLayout()
download_dir_row.addWidget(self.download_dir_edit)
download_dir_row.addWidget(self.download_dir_btn)
dir_layout.addRow(QLabel("Downloads Base Dir:"), download_dir_row)
# Jackify Data Directory
from jackify.shared.paths import get_jackify_data_dir
current_jackify_dir = str(get_jackify_data_dir())
self.jackify_data_dir_edit = QLineEdit(current_jackify_dir)
self.jackify_data_dir_edit.setToolTip("Directory for Jackify data (logs, downloads, temp files). Default: ~/Jackify")
self.jackify_data_dir_btn = QPushButton()
self.jackify_data_dir_btn.setIcon(QIcon.fromTheme("folder-open"))
self.jackify_data_dir_btn.setToolTip("Browse for directory")
self.jackify_data_dir_btn.setFixedWidth(32)
self.jackify_data_dir_btn.clicked.connect(lambda: self._pick_directory(self.jackify_data_dir_edit))
jackify_data_dir_row = QHBoxLayout()
jackify_data_dir_row.addWidget(self.jackify_data_dir_edit)
jackify_data_dir_row.addWidget(self.jackify_data_dir_btn)
# Reset to default button
reset_jackify_dir_btn = QPushButton("Reset")
reset_jackify_dir_btn.setToolTip("Reset to default (~/ Jackify)")
reset_jackify_dir_btn.setFixedWidth(50)
reset_jackify_dir_btn.clicked.connect(lambda: self.jackify_data_dir_edit.setText(str(Path.home() / "Jackify")))
jackify_data_dir_row.addWidget(reset_jackify_dir_btn)
dir_layout.addRow(QLabel("Jackify Data Dir:"), jackify_data_dir_row)
main_layout.addWidget(dir_group)
main_layout.addSpacing(12)
# --- Save/Close/Help Buttons --- # --- Save/Close/Help Buttons ---
btn_layout = QHBoxLayout() btn_layout = QHBoxLayout()
@@ -396,36 +201,261 @@ class SettingsDialog(QDialog):
close_btn.clicked.connect(self.reject) close_btn.clicked.connect(self.reject)
btn_layout.addWidget(save_btn) btn_layout.addWidget(save_btn)
btn_layout.addWidget(close_btn) btn_layout.addWidget(close_btn)
# Add error label for validation messages
self.error_label = QLabel("")
self.error_label.setStyleSheet("QLabel { color: #ff6b6b; }")
main_layout.addWidget(self.error_label)
main_layout.addSpacing(10) main_layout.addSpacing(10)
main_layout.addLayout(btn_layout) main_layout.addLayout(btn_layout)
# Set tab order for accessibility
# Get the first resource's widgets if any exist
if self.resource_edits:
first_resource_key = list(self.resource_edits.keys())[0]
first_multithreading, first_max_tasks = self.resource_edits[first_resource_key]
# Set tab order starting with the first max tasks spinner
self.setTabOrder(first_max_tasks, self.bandwidth_spin)
# Continue with bandwidth spinner regardless of resources
self.setTabOrder(self.bandwidth_spin, self.debug_checkbox)
self.setTabOrder(self.debug_checkbox, self.api_key_edit)
self.setTabOrder(self.api_key_edit, self.api_show_btn)
self.setTabOrder(self.api_show_btn, clear_api_btn)
self.setTabOrder(clear_api_btn, self.install_dir_edit)
self.setTabOrder(self.install_dir_edit, self.install_dir_btn)
self.setTabOrder(self.install_dir_btn, self.download_dir_edit)
self.setTabOrder(self.download_dir_edit, self.download_dir_btn)
self.setTabOrder(self.download_dir_btn, save_btn)
self.setTabOrder(save_btn, close_btn)
self.error_label = QLabel("")
self.error_label.setStyleSheet("color: #f55; font-weight: bold;")
main_layout.insertWidget(0, self.error_label)
except Exception as e: except Exception as e:
print(f"[ERROR] Exception in SettingsDialog __init__: {e}") print(f"[ERROR] Exception in SettingsDialog.__init__: {e}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
raise
def _create_general_tab(self):
"""Create the General settings tab"""
general_tab = QWidget()
general_layout = QVBoxLayout(general_tab)
# --- Directory Paths Section (moved to top as most essential) ---
dir_group = QGroupBox("Directory 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; }")
dir_layout = QFormLayout()
dir_group.setLayout(dir_layout)
self.install_dir_edit = QLineEdit(self.config_handler.get("modlist_install_base_dir", ""))
self.install_dir_edit.setToolTip("Default directory for modlist installations.")
self.install_dir_btn = QPushButton()
self.install_dir_btn.setIcon(QIcon.fromTheme("folder-open"))
self.install_dir_btn.setToolTip("Browse for directory")
self.install_dir_btn.setFixedWidth(32)
self.install_dir_btn.clicked.connect(lambda: self._pick_directory(self.install_dir_edit))
install_dir_row = QHBoxLayout()
install_dir_row.addWidget(self.install_dir_edit)
install_dir_row.addWidget(self.install_dir_btn)
dir_layout.addRow(QLabel("Install Base Dir:"), install_dir_row)
self.download_dir_edit = QLineEdit(self.config_handler.get("modlist_downloads_base_dir", ""))
self.download_dir_edit.setToolTip("Default directory for modlist downloads.")
self.download_dir_btn = QPushButton()
self.download_dir_btn.setIcon(QIcon.fromTheme("folder-open"))
self.download_dir_btn.setToolTip("Browse for directory")
self.download_dir_btn.setFixedWidth(32)
self.download_dir_btn.clicked.connect(lambda: self._pick_directory(self.download_dir_edit))
download_dir_row = QHBoxLayout()
download_dir_row.addWidget(self.download_dir_edit)
download_dir_row.addWidget(self.download_dir_btn)
dir_layout.addRow(QLabel("Downloads Base Dir:"), download_dir_row)
# Jackify Data Directory
from jackify.shared.paths import get_jackify_data_dir
current_jackify_dir = str(get_jackify_data_dir())
self.jackify_data_dir_edit = QLineEdit(current_jackify_dir)
self.jackify_data_dir_edit.setToolTip("Directory for Jackify data (logs, downloads, temp files). Default: ~/Jackify")
self.jackify_data_dir_btn = QPushButton()
self.jackify_data_dir_btn.setIcon(QIcon.fromTheme("folder-open"))
self.jackify_data_dir_btn.setToolTip("Browse for directory")
self.jackify_data_dir_btn.setFixedWidth(32)
self.jackify_data_dir_btn.clicked.connect(lambda: self._pick_directory(self.jackify_data_dir_edit))
jackify_data_dir_row = QHBoxLayout()
jackify_data_dir_row.addWidget(self.jackify_data_dir_edit)
jackify_data_dir_row.addWidget(self.jackify_data_dir_btn)
# Reset to default button
reset_jackify_dir_btn = QPushButton("Reset")
reset_jackify_dir_btn.setToolTip("Reset to default (~/ Jackify)")
reset_jackify_dir_btn.setFixedWidth(50)
reset_jackify_dir_btn.clicked.connect(lambda: self.jackify_data_dir_edit.setText(str(Path.home() / "Jackify")))
jackify_data_dir_row.addWidget(reset_jackify_dir_btn)
dir_layout.addRow(QLabel("Jackify Data Dir:"), jackify_data_dir_row)
general_layout.addWidget(dir_group)
general_layout.addSpacing(12)
# --- Nexus API Key Section ---
api_group = QGroupBox("Nexus API Key")
api_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; }")
api_layout = QHBoxLayout()
api_group.setLayout(api_layout)
self.api_key_edit = QLineEdit()
self.api_key_edit.setEchoMode(QLineEdit.Password)
api_key = self.config_handler.get_api_key()
if api_key:
self.api_key_edit.setText(api_key)
else:
self.api_key_edit.setText("")
self.api_key_edit.setToolTip("Your Nexus API Key (obfuscated by default, click Show to reveal)")
# Connect for immediate saving when text changes
self.api_key_edit.textChanged.connect(self._on_api_key_changed)
self.api_show_btn = QToolButton()
self.api_show_btn.setCheckable(True)
self.api_show_btn.setIcon(QIcon.fromTheme("view-visible"))
self.api_show_btn.setToolTip("Show or hide your API key")
self.api_show_btn.toggled.connect(self._toggle_api_key_visibility)
self.api_show_btn.setStyleSheet("")
clear_api_btn = QPushButton("Clear API Key")
clear_api_btn.clicked.connect(self._clear_api_key)
api_layout.addWidget(QLabel("Nexus API Key:"))
api_layout.addWidget(self.api_key_edit)
api_layout.addWidget(self.api_show_btn)
api_layout.addWidget(clear_api_btn)
general_layout.addWidget(api_group)
general_layout.addSpacing(12)
# --- Proton Version Settings Section ---
proton_group = QGroupBox("Proton Version Settings")
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 = QVBoxLayout()
proton_group.setLayout(proton_layout)
# Install Proton Version (for jackify-engine texture processing)
install_proton_layout = QHBoxLayout()
self.install_proton_dropdown = QComboBox()
self.install_proton_dropdown.setToolTip("Proton version for modlist installation and texture processing (requires fast Proton)")
self.install_proton_dropdown.setMinimumWidth(200)
install_refresh_btn = QPushButton("")
install_refresh_btn.setFixedSize(30, 30)
install_refresh_btn.setToolTip("Refresh install Proton version list")
install_refresh_btn.clicked.connect(self._refresh_install_proton_dropdown)
install_proton_layout.addWidget(QLabel("Install Proton:"))
install_proton_layout.addWidget(self.install_proton_dropdown)
install_proton_layout.addWidget(install_refresh_btn)
install_proton_layout.addStretch()
# Game Proton Version (for game shortcuts)
game_proton_layout = QHBoxLayout()
self.game_proton_dropdown = QComboBox()
self.game_proton_dropdown.setToolTip("Proton version for game shortcuts (can be any Proton 9+)")
self.game_proton_dropdown.setMinimumWidth(200)
game_refresh_btn = QPushButton("")
game_refresh_btn.setFixedSize(30, 30)
game_refresh_btn.setToolTip("Refresh game Proton version list")
game_refresh_btn.clicked.connect(self._refresh_game_proton_dropdown)
game_proton_layout.addWidget(QLabel("Game Proton:"))
game_proton_layout.addWidget(self.game_proton_dropdown)
game_proton_layout.addWidget(game_refresh_btn)
game_proton_layout.addStretch()
proton_layout.addLayout(install_proton_layout)
proton_layout.addLayout(game_proton_layout)
# Populate both Proton dropdowns
self._populate_install_proton_dropdown()
self._populate_game_proton_dropdown()
general_layout.addWidget(proton_group)
general_layout.addSpacing(12)
# --- Enable Debug Section (moved to bottom as advanced option) ---
debug_group = QGroupBox("Enable Debug")
debug_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; }")
debug_layout = QVBoxLayout()
debug_group.setLayout(debug_layout)
self.debug_checkbox = QCheckBox("Enable debug mode (requires restart)")
# Load debug_mode from config
self.debug_checkbox.setChecked(self.config_handler.get('debug_mode', False))
self.debug_checkbox.setToolTip("Enable verbose debug logging. Requires Jackify restart to take effect.")
self.debug_checkbox.setStyleSheet("color: #fff;")
debug_layout.addWidget(self.debug_checkbox)
general_layout.addWidget(debug_group)
general_layout.addStretch() # Add stretch to push content to top
self.tab_widget.addTab(general_tab, "General")
def _create_advanced_tab(self):
"""Create the Advanced settings tab"""
advanced_tab = QWidget()
advanced_layout = QVBoxLayout(advanced_tab)
resource_group = QGroupBox("Resource Limits")
resource_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; }")
resource_layout = QGridLayout()
resource_group.setLayout(resource_layout)
resource_layout.setVerticalSpacing(4)
resource_layout.setHorizontalSpacing(8)
resource_layout.addWidget(self._bold_label("Resource"), 0, 0, 1, 1, Qt.AlignLeft)
resource_layout.addWidget(self._bold_label("Max Tasks"), 0, 1, 1, 1, Qt.AlignLeft)
self.resource_settings_path = os.path.expanduser("~/.config/jackify/resource_settings.json")
self.resource_settings = self._load_json(self.resource_settings_path)
self.resource_edits = {}
resource_row_index = 0
for resource_row_index, (k, v) in enumerate(self.resource_settings.items(), start=1):
try:
# Create resource label
resource_layout.addWidget(QLabel(f"{k}:", parent=self), resource_row_index, 0, 1, 1, Qt.AlignLeft)
max_tasks_spin = QSpinBox()
max_tasks_spin.setMinimum(1)
max_tasks_spin.setMaximum(128)
max_tasks_spin.setValue(v.get('MaxTasks', 16))
max_tasks_spin.setToolTip("Maximum number of concurrent tasks for this resource.")
max_tasks_spin.setFixedWidth(160)
resource_layout.addWidget(max_tasks_spin, resource_row_index, 1)
# Store the widgets
self.resource_edits[k] = (None, max_tasks_spin)
except Exception as e:
print(f"[ERROR] Failed to create widgets for resource '{k}': {e}")
continue
# If no resources exist, show helpful message
if not self.resource_edits:
info_label = QLabel("Resource Limit settings will be generated once a modlist install action is performed")
info_label.setStyleSheet("color: #aaa; font-style: italic; padding: 20px; font-size: 11pt;")
info_label.setWordWrap(True)
info_label.setAlignment(Qt.AlignCenter)
info_label.setMinimumHeight(60) # Ensure enough height to prevent cutoff
resource_layout.addWidget(info_label, 1, 0, 3, 2) # Span more rows for better space
# Bandwidth limiter row (only show if Downloads resource exists)
if "Downloads" in self.resource_settings:
downloads_throughput = self.resource_settings["Downloads"].get("MaxThroughput", 0)
self.bandwidth_spin = QSpinBox()
self.bandwidth_spin.setMinimum(0)
self.bandwidth_spin.setMaximum(1000000)
self.bandwidth_spin.setValue(downloads_throughput)
self.bandwidth_spin.setSuffix(" KB/s")
self.bandwidth_spin.setFixedWidth(160)
self.bandwidth_spin.setToolTip("Set the maximum download speed for modlist downloads. 0 = unlimited.")
bandwidth_note = QLabel("(0 = unlimited)")
bandwidth_note.setStyleSheet("color: #aaa; font-size: 10pt;")
# Create horizontal layout for bandwidth row
bandwidth_row = QHBoxLayout()
bandwidth_row.addWidget(self.bandwidth_spin)
bandwidth_row.addWidget(bandwidth_note)
bandwidth_row.addStretch() # Push to the left
resource_layout.addWidget(QLabel("Bandwidth Limit:", parent=self), resource_row_index+1, 0, 1, 1, Qt.AlignLeft)
resource_layout.addLayout(bandwidth_row, resource_row_index+1, 1)
else:
self.bandwidth_spin = None # No bandwidth UI if Downloads resource doesn't exist
advanced_layout.addWidget(resource_group)
# Component Installation Method Section
component_group = QGroupBox("Component Installation")
component_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; }")
component_layout = QVBoxLayout()
component_group.setLayout(component_layout)
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 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)
advanced_layout.addWidget(component_group)
advanced_layout.addStretch() # Add stretch to push content to top
self.tab_widget.addTab(advanced_tab, "Advanced")
def _toggle_api_key_visibility(self, checked): def _toggle_api_key_visibility(self, checked):
# Always use the same eyeball icon, only change color when toggled # Always use the same eyeball icon, only change color when toggled
@@ -495,173 +525,250 @@ class SettingsDialog(QDialog):
except: except:
return 'auto' return 'auto'
def _populate_proton_dropdown(self): def _populate_install_proton_dropdown(self):
"""Populate Proton version dropdown with detected versions (includes GE-Proton and Valve Proton)""" """Populate Install Proton dropdown (Experimental/GE-Proton 10+ only for fast texture processing)"""
try: try:
from jackify.backend.handlers.wine_utils import WineUtils from jackify.backend.handlers.wine_utils import WineUtils
# Get all available Proton versions (GE-Proton + Valve Proton) # Get all available Proton versions
available_protons = WineUtils.scan_all_proton_versions() available_protons = WineUtils.scan_all_proton_versions()
# Add "Auto" option first # Add "Auto" option first
self.proton_dropdown.addItem("Auto", "auto") self.install_proton_dropdown.addItem("Auto (Recommended)", "auto")
# Filter for fast Proton versions only
fast_protons = []
slow_protons = []
# Add detected Proton versions with type indicators
for proton in available_protons: for proton in available_protons:
proton_name = proton.get('name', 'Unknown Proton') proton_name = proton.get('name', 'Unknown Proton')
proton_type = proton.get('type', 'Unknown') proton_type = proton.get('type', 'Unknown')
# Format display name to show type for clarity is_fast_proton = False
# Fast Protons: Experimental, GE-Proton 10+
if proton_name == "Proton - Experimental":
is_fast_proton = True
elif proton_type == 'GE-Proton':
# For GE-Proton, check major_version field
major_version = proton.get('major_version', 0)
if major_version >= 10:
is_fast_proton = True
if is_fast_proton:
if proton_type == 'GE-Proton':
display_name = f"{proton_name} (GE)"
else:
display_name = proton_name
fast_protons.append((display_name, str(proton['path'])))
else:
# Slow Protons: Valve 9, 10 beta, older GE-Proton, etc.
if proton_type == 'GE-Proton':
display_name = f"{proton_name} (GE) (Slow texture processing)"
else:
display_name = f"{proton_name} (Slow texture processing)"
slow_protons.append((display_name, str(proton['path'])))
# Add fast Protons first
for display_name, path in fast_protons:
self.install_proton_dropdown.addItem(display_name, path)
# Add separator and slow Protons with warnings
if slow_protons:
self.install_proton_dropdown.insertSeparator(self.install_proton_dropdown.count())
for display_name, path in slow_protons:
self.install_proton_dropdown.addItem(display_name, path)
# Load saved preference
saved_proton = self.config_handler.get('proton_path', self._get_proton_10_path())
self._set_dropdown_selection(self.install_proton_dropdown, saved_proton)
except Exception as e:
logger.error(f"Failed to populate install Proton dropdown: {e}")
self.install_proton_dropdown.addItem("Auto (Recommended)", "auto")
def _populate_game_proton_dropdown(self):
"""Populate Game Proton dropdown (any Proton 9+ for game compatibility)"""
try:
from jackify.backend.handlers.wine_utils import WineUtils
# Get all available Proton versions
available_protons = WineUtils.scan_all_proton_versions()
# Add "Same as Install" option first
self.game_proton_dropdown.addItem("Same as Install Proton", "same_as_install")
# Add all Proton 9+ versions
for proton in available_protons:
proton_name = proton.get('name', 'Unknown Proton')
proton_type = proton.get('type', 'Unknown')
# Add type indicator for clarity
if proton_type == 'GE-Proton': if proton_type == 'GE-Proton':
display_name = f"{proton_name} (GE)" display_name = f"{proton_name} (GE)"
elif proton_type == 'Valve-Proton':
display_name = f"{proton_name}"
else: else:
display_name = proton_name display_name = proton_name
self.proton_dropdown.addItem(display_name, str(proton['path'])) self.game_proton_dropdown.addItem(display_name, str(proton['path']))
# Load saved preference and determine UI selection # Load saved preference
saved_proton = self.config_handler.get('proton_path', self._get_proton_10_path()) saved_game_proton = self.config_handler.get('game_proton_path', 'same_as_install')
self._set_dropdown_selection(self.game_proton_dropdown, saved_game_proton)
# 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: except Exception as e:
logger.error(f"Failed to populate Proton dropdown: {e}") logger.error(f"Failed to populate game Proton dropdown: {e}")
# Fallback: just show auto self.game_proton_dropdown.addItem("Same as Install Proton", "same_as_install")
self.proton_dropdown.addItem("Auto", "auto")
def _refresh_proton_dropdown(self): def _set_dropdown_selection(self, dropdown, saved_value):
"""Refresh Proton dropdown with latest detected versions""" """Helper to set dropdown selection based on saved value"""
current_selection = self.proton_dropdown.currentData() found_match = False
self.proton_dropdown.clear() for i in range(dropdown.count()):
self._populate_proton_dropdown() if dropdown.itemData(i) == saved_value:
dropdown.setCurrentIndex(i)
# Restore selection if still available found_match = True
for i in range(self.proton_dropdown.count()):
if self.proton_dropdown.itemData(i) == current_selection:
self.proton_dropdown.setCurrentIndex(i)
break break
# If no exact match and not auto/same_as_install, select first option
if not found_match and saved_value not in ["auto", "same_as_install"]:
dropdown.setCurrentIndex(0)
def _refresh_install_proton_dropdown(self):
"""Refresh Install Proton dropdown"""
current_selection = self.install_proton_dropdown.currentData()
self.install_proton_dropdown.clear()
self._populate_install_proton_dropdown()
self._set_dropdown_selection(self.install_proton_dropdown, current_selection)
def _refresh_game_proton_dropdown(self):
"""Refresh Game Proton dropdown"""
current_selection = self.game_proton_dropdown.currentData()
self.game_proton_dropdown.clear()
self._populate_game_proton_dropdown()
self._set_dropdown_selection(self.game_proton_dropdown, current_selection)
def _save(self): def _save(self):
# Validate values try:
for k, (multithreading_checkbox, max_tasks_spin) in self.resource_edits.items(): # Validate values (only if resource_edits exist)
if max_tasks_spin.value() > 128: for k, (multithreading_checkbox, max_tasks_spin) in self.resource_edits.items():
self.error_label.setText(f"Invalid value for {k}: Max Tasks must be <= 128.") if max_tasks_spin.value() > 128:
self.error_label.setText(f"Invalid value for {k}: Max Tasks must be <= 128.")
return
if self.bandwidth_spin and self.bandwidth_spin.value() > 1000000:
self.error_label.setText("Bandwidth limit must be <= 1,000,000 KB/s.")
return return
if self.bandwidth_spin and self.bandwidth_spin.value() > 1000000: self.error_label.setText("")
self.error_label.setText("Bandwidth limit must be <= 1,000,000 KB/s.")
return
self.error_label.setText("")
# Save resource settings
for k, (multithreading_checkbox, max_tasks_spin) in self.resource_edits.items():
resource_data = self.resource_settings.get(k, {})
resource_data['MaxTasks'] = max_tasks_spin.value()
# Only add multithreading setting for File Extractor
if k == "File Extractor" and multithreading_checkbox:
if multithreading_checkbox.isChecked():
resource_data['_7zzMultiThread'] = 'on'
else:
# Remove the setting if unchecked (don't add 'off')
resource_data.pop('_7zzMultiThread', None)
self.resource_settings[k] = resource_data
# Save bandwidth limit to Downloads resource MaxThroughput (only if bandwidth UI exists) # Save resource settings
if self.bandwidth_spin: for k, (multithreading_checkbox, max_tasks_spin) in self.resource_edits.items():
if "Downloads" not in self.resource_settings: resource_data = self.resource_settings.get(k, {})
self.resource_settings["Downloads"] = {"MaxTasks": 16} # Provide default MaxTasks resource_data['MaxTasks'] = max_tasks_spin.value()
self.resource_settings["Downloads"]["MaxThroughput"] = self.bandwidth_spin.value() self.resource_settings[k] = resource_data
# Save all resource settings (including bandwidth) in one operation # Save bandwidth limit to Downloads resource MaxThroughput (only if bandwidth UI exists)
self._save_json(self.resource_settings_path, self.resource_settings) if self.bandwidth_spin:
if "Downloads" not in self.resource_settings:
self.resource_settings["Downloads"] = {"MaxTasks": 16} # Provide default MaxTasks
self.resource_settings["Downloads"]["MaxThroughput"] = self.bandwidth_spin.value()
# Save debug mode to config # Save all resource settings (including bandwidth) in one operation
self.config_handler.set('debug_mode', self.debug_checkbox.isChecked()) self._save_json(self.resource_settings_path, self.resource_settings)
# Save API key
api_key = self.api_key_edit.text().strip()
self.config_handler.save_api_key(api_key)
# Save modlist base dirs
self.config_handler.set("modlist_install_base_dir", self.install_dir_edit.text().strip())
self.config_handler.set("modlist_downloads_base_dir", self.download_dir_edit.text().strip())
# 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 # Save debug mode to config
selected_proton_path = self.proton_dropdown.currentData() self.config_handler.set('debug_mode', self.debug_checkbox.isChecked())
if selected_proton_path == "auto": # Save API key
# Resolve "auto" to actual best Proton path using unified detection api_key = self.api_key_edit.text().strip()
try: self.config_handler.save_api_key(api_key)
from jackify.backend.handlers.wine_utils import WineUtils # Save modlist base dirs
best_proton = WineUtils.select_best_proton() self.config_handler.set("modlist_install_base_dir", self.install_dir_edit.text().strip())
self.config_handler.set("modlist_downloads_base_dir", self.download_dir_edit.text().strip())
# 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)
if best_proton: # Save Install Proton selection - resolve "auto" to actual path
resolved_path = str(best_proton['path']) selected_install_proton_path = self.install_proton_dropdown.currentData()
resolved_version = best_proton['name'] if selected_install_proton_path == "auto":
else: # Resolve "auto" to actual best Proton path using unified detection
resolved_path = "auto" try:
resolved_version = "auto" from jackify.backend.handlers.wine_utils import WineUtils
except: best_proton = WineUtils.select_best_proton()
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) if best_proton:
self.config_handler.set("proton_version", resolved_version) resolved_install_path = str(best_proton['path'])
resolved_install_version = best_proton['name']
# Force immediate save and verify else:
save_result = self.config_handler.save_config() resolved_install_path = "auto"
if not save_result: resolved_install_version = "auto"
self.logger.error("Failed to save Proton configuration") except:
else: resolved_install_path = "auto"
self.logger.info(f"Saved Proton config: path={resolved_path}, version={resolved_version}") resolved_install_version = "auto"
# Verify the save worked by reading it back
saved_path = self.config_handler.get("proton_path")
if saved_path != resolved_path:
self.logger.error(f"Config save verification failed: expected {resolved_path}, got {saved_path}")
else: else:
self.logger.debug("Config save verified successfully") # User selected specific Proton version
resolved_install_path = selected_install_proton_path
# Extract version from dropdown text
resolved_install_version = self.install_proton_dropdown.currentText()
# Refresh cached paths in GUI screens if Jackify directory changed self.config_handler.set("proton_path", resolved_install_path)
self._refresh_gui_paths() self.config_handler.set("proton_version", resolved_install_version)
# Check if debug mode changed and prompt for restart # Save Game Proton selection
new_debug_mode = self.debug_checkbox.isChecked() selected_game_proton_path = self.game_proton_dropdown.currentData()
if new_debug_mode != self._original_debug_mode: if selected_game_proton_path == "same_as_install":
reply = MessageService.question(self, "Restart Required", "Debug mode change requires a restart. Restart Jackify now?", safety_level="low") # Use same as install proton
if reply == QMessageBox.Yes: resolved_game_path = resolved_install_path
import os, sys resolved_game_version = resolved_install_version
if getattr(sys, 'frozen', False): else:
# PyInstaller bundle: safe to restart # User selected specific game Proton version
self.accept() resolved_game_path = selected_game_proton_path
os.execv(sys.executable, [sys.executable] + sys.argv) resolved_game_version = self.game_proton_dropdown.currentText()
return
self.config_handler.set("game_proton_path", resolved_game_path)
self.config_handler.set("game_proton_version", resolved_game_version)
# Save component installation method preference
self.config_handler.set("use_winetricks_for_components", self.use_winetricks_checkbox.isChecked())
# Force immediate save and verify
save_result = self.config_handler.save_config()
if not save_result:
self.logger.error("Failed to save Proton configuration")
else:
self.logger.info(f"Saved Proton config: install_path={resolved_install_path}, game_path={resolved_game_path}")
# Verify the save worked by reading it back
saved_path = self.config_handler.get("proton_path")
if saved_path != resolved_install_path:
self.logger.error(f"Config save verification failed: expected {resolved_install_path}, got {saved_path}")
else: else:
# Dev mode: show message instead of auto-restart self.logger.debug("Config save verified successfully")
MessageService.information(self, "Manual Restart Required", "Please restart Jackify manually to apply debug mode changes.", safety_level="low")
# Refresh cached paths in GUI screens if Jackify directory changed
self._refresh_gui_paths()
# Check if debug mode changed and prompt for restart
new_debug_mode = self.debug_checkbox.isChecked()
if new_debug_mode != self._original_debug_mode:
reply = MessageService.question(self, "Restart Required", "Debug mode change requires a restart. Restart Jackify now?", safety_level="low")
if reply == QMessageBox.Yes:
import os, sys
# User requested restart - do it regardless of execution environment
self.accept() self.accept()
# Check if running from AppImage
if os.environ.get('APPIMAGE'):
# AppImage: restart the AppImage
os.execv(os.environ['APPIMAGE'], [os.environ['APPIMAGE']] + sys.argv[1:])
else:
# Dev mode: restart the Python module
os.execv(sys.executable, [sys.executable, '-m', 'jackify.frontends.gui'] + sys.argv[1:])
return return
MessageService.information(self, "Settings Saved", "Settings have been saved successfully.", safety_level="low")
self.accept() # If we get here, no restart was needed
MessageService.information(self, "Settings Saved", "Settings have been saved successfully.", safety_level="low")
self.accept()
except Exception as e:
self.logger.error(f"Error saving settings: {e}")
MessageService.warning(self, "Save Error", f"Failed to save settings: {e}", safety_level="medium")
def _refresh_gui_paths(self): def _refresh_gui_paths(self):
"""Refresh cached paths in all GUI screens.""" """Refresh cached paths in all GUI screens."""

View File

@@ -499,6 +499,7 @@ class ConfigureExistingModlistScreen(QWidget):
# For existing modlists, add resolution if specified # For existing modlists, add resolution if specified
if self.resolution != "Leave unchanged": if self.resolution != "Leave unchanged":
modlist_context.resolution = self.resolution.split()[0] modlist_context.resolution = self.resolution.split()[0]
# Note: If "Leave unchanged" is selected, resolution stays None (no fallback needed)
# Define callbacks # Define callbacks
def progress_callback(message): def progress_callback(message):

View File

@@ -1184,7 +1184,7 @@ class ConfigureNewModlistScreen(QWidget):
nexus_api_key='', # Not needed for configuration nexus_api_key='', # Not needed for configuration
modlist_value='', # Not needed for existing modlist modlist_value='', # Not needed for existing modlist
modlist_source='existing', modlist_source='existing',
resolution=self.context.get('resolution'), resolution=self.context.get('resolution') or get_resolution_fallback(None),
skip_confirmation=True skip_confirmation=True
) )

View File

@@ -1746,7 +1746,14 @@ class InstallModlistScreen(QWidget):
# Save resolution for later use in configuration # Save resolution for later use in configuration
resolution = self.resolution_combo.currentText() resolution = self.resolution_combo.currentText()
self._current_resolution = resolution.split()[0] if resolution != "Leave unchanged" else None # Extract resolution properly (e.g., "1280x800" from "1280x800 (Steam Deck)")
if resolution != "Leave unchanged":
if " (" in resolution:
self._current_resolution = resolution.split(" (")[0]
else:
self._current_resolution = resolution
else:
self._current_resolution = None
# Use automated prefix creation instead of manual steps # Use automated prefix creation instead of manual steps
debug_print("DEBUG: Starting automated prefix creation workflow") debug_print("DEBUG: Starting automated prefix creation workflow")
@@ -1757,6 +1764,17 @@ class InstallModlistScreen(QWidget):
MessageService.critical(self, "Steam Restart Failed", "Failed to restart Steam automatically. Please restart Steam manually, then try again.") MessageService.critical(self, "Steam Restart Failed", "Failed to restart Steam automatically. Please restart Steam manually, then try again.")
def start_automated_prefix_workflow(self): def start_automated_prefix_workflow(self):
# Ensure _current_resolution is always set before starting workflow
if not hasattr(self, '_current_resolution') or self._current_resolution is None:
resolution = self.resolution_combo.currentText() if hasattr(self, 'resolution_combo') else None
# Extract resolution properly (e.g., "1280x800" from "1280x800 (Steam Deck)")
if resolution and resolution != "Leave unchanged":
if " (" in resolution:
self._current_resolution = resolution.split(" (")[0]
else:
self._current_resolution = resolution
else:
self._current_resolution = None
"""Start the automated prefix creation workflow""" """Start the automated prefix creation workflow"""
try: try:
# Disable controls during installation # Disable controls during installation

View File

@@ -222,8 +222,14 @@ class ValidationHandler:
def validate_steam_shortcut(self, app_id: str) -> Tuple[bool, str]: def validate_steam_shortcut(self, app_id: str) -> Tuple[bool, str]:
"""Validate a Steam shortcut.""" """Validate a Steam shortcut."""
try: try:
# Check if shortcuts.vdf exists # Use native Steam service to get proper shortcuts.vdf path with multi-user support
shortcuts_path = Path.home() / '.steam' / 'steam' / 'userdata' / '75424832' / 'config' / 'shortcuts.vdf' from jackify.backend.services.native_steam_service import NativeSteamService
steam_service = NativeSteamService()
shortcuts_path = steam_service.get_shortcuts_vdf_path()
if not shortcuts_path:
return False, "Could not determine shortcuts.vdf path (no active Steam user found)"
if not shortcuts_path.exists(): if not shortcuts_path.exists():
return False, "shortcuts.vdf not found" return False, "shortcuts.vdf not found"