mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-01-18 03:56:59 +01:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91ac08afb2 | ||
|
|
06bd94d119 | ||
|
|
52806f4116 | ||
|
|
956ea24465 | ||
|
|
f039cf9c24 | ||
|
|
d9ea1be347 | ||
|
|
a8862475d4 | ||
|
|
430d085287 | ||
|
|
7212a58480 |
83
CHANGELOG.md
83
CHANGELOG.md
@@ -1,5 +1,85 @@
|
||||
# Jackify Changelog
|
||||
|
||||
## v0.1.6.6 - AppImage Bundling Fix
|
||||
**Release Date:** October 29, 2025
|
||||
|
||||
### Bug Fixes
|
||||
- **Fixed AppImage bundling issue** causing legacy code to be retained in rare circumstances
|
||||
|
||||
---
|
||||
|
||||
## v0.1.6.5 - Steam Deck SD Card Path Fix
|
||||
**Release Date:** October 27, 2025
|
||||
|
||||
### Bug Fixes
|
||||
- **Fixed Steam Deck SD card path manipulation** when jackify-engine installed
|
||||
- **Fixed Ubuntu Qt platform plugin errors** by bundling XCB libraries
|
||||
- **Added Flatpak GE-Proton detection** and protontricks installation choices
|
||||
- **Extended Steam Deck SD card timeouts** for slower I/O operations
|
||||
|
||||
---
|
||||
|
||||
## v0.1.6.4 - Flatpak Steam Detection Hotfix
|
||||
**Release Date:** October 24, 2025
|
||||
|
||||
### Critical Bug Fixes
|
||||
- **FIXED: Flatpak Steam Detection**: Added support for `/data/Steam/` directory structure used by some Flatpak Steam installations
|
||||
- **IMPROVED: Steam Path Detection**: Now checks all known Flatpak Steam directory structures for maximum compatibility
|
||||
|
||||
---
|
||||
|
||||
## v0.1.6.3 - Emergency Hotfix
|
||||
**Release Date:** October 23, 2025
|
||||
|
||||
### Critical Bug Fixes
|
||||
- **FIXED: Proton Detection for Custom Steam Libraries**: Now properly reads all Steam libraries from libraryfolders.vdf
|
||||
- **IMPROVED: Registry Wine Binary Detection**: Uses user's configured Proton for better compatibility
|
||||
- **IMPROVED: Error Handling**: Registry fixes now provide clear warnings if they fail instead of breaking entire workflow
|
||||
|
||||
---
|
||||
|
||||
## v0.1.6.2 - Minor Bug Fixes
|
||||
**Release Date:** October 23, 2025
|
||||
|
||||
### Bug Fixes
|
||||
- **Improved dotnet4.x Compatibility**: Universal registry fixes for better modlist compatibility
|
||||
- **Fixed Proton 9 Override**: A bug meant that modlists with spaces in the name weren't being overridden correctly
|
||||
- **Removed PageFileManager Plugin**: Eliminates Linux PageFile warnings
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
@@ -54,7 +134,8 @@
|
||||
- **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
|
||||
- **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
|
||||
- **Winetricks Reliability**: Fixed vcrun2022 installation failures and wine app crashes
|
||||
- **Enderal Registry Injection**: Switched from launch options to registry injection approach
|
||||
|
||||
@@ -5,4 +5,4 @@ This package provides both CLI and GUI interfaces for managing
|
||||
Wabbajack modlists natively on Linux systems.
|
||||
"""
|
||||
|
||||
__version__ = "0.1.5.2"
|
||||
__version__ = "0.1.6.6"
|
||||
|
||||
@@ -156,7 +156,7 @@ class ModlistInstallCLI:
|
||||
from ..models.configuration import SystemInfo
|
||||
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.context = {}
|
||||
# Use standard logging (no file handler)
|
||||
|
||||
@@ -38,7 +38,9 @@ class ConfigHandler:
|
||||
"default_download_parent_dir": None, # Parent directory for downloads
|
||||
"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
|
||||
"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
|
||||
@@ -498,20 +500,44 @@ class ConfigHandler:
|
||||
|
||||
def get_proton_path(self):
|
||||
"""
|
||||
Retrieve the saved Proton path from configuration
|
||||
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 Proton path or 'auto' if not saved
|
||||
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 proton_path from config: {proton_path}")
|
||||
logger.debug(f"Retrieved fresh install proton_path from config: {proton_path}")
|
||||
return proton_path
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving proton_path: {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):
|
||||
|
||||
@@ -152,8 +152,10 @@ class ModlistMenuHandler:
|
||||
self.path_handler = PathHandler()
|
||||
self.vdf_handler = VDFHandler()
|
||||
|
||||
# Determine Steam Deck status (already done by ConfigHandler, use it)
|
||||
self.steamdeck = config_handler.settings.get('steamdeck', False)
|
||||
# Determine Steam Deck status using centralized detection
|
||||
from ..services.platform_detection_service import PlatformDetectionService
|
||||
platform_service = PlatformDetectionService.get_instance()
|
||||
self.steamdeck = platform_service.is_steamdeck
|
||||
|
||||
# Create the resolution handler
|
||||
self.resolution_handler = ResolutionHandler()
|
||||
@@ -178,7 +180,13 @@ class ModlistMenuHandler:
|
||||
self.logger.error(f"Error initializing ModlistMenuHandler: {e}")
|
||||
# Initialize with defaults/empty to prevent errors
|
||||
self.filesystem_handler = FileSystemHandler()
|
||||
self.steamdeck = False
|
||||
# Use centralized detection even in fallback
|
||||
try:
|
||||
from ..services.platform_detection_service import PlatformDetectionService
|
||||
platform_service = PlatformDetectionService.get_instance()
|
||||
self.steamdeck = platform_service.is_steamdeck
|
||||
except:
|
||||
self.steamdeck = False # Final fallback
|
||||
self.modlist_handler = None
|
||||
|
||||
def show_modlist_menu(self):
|
||||
|
||||
@@ -71,15 +71,19 @@ class ModlistHandler:
|
||||
}
|
||||
|
||||
# Canonical mapping of modlist-specific Wine components (from omni-guides.sh)
|
||||
# NOTE: dotnet4.x components disabled in v0.1.6.2 - replaced with universal registry fixes
|
||||
MODLIST_WINE_COMPONENTS = {
|
||||
"wildlander": ["dotnet472"],
|
||||
"librum": ["dotnet40", "dotnet8"],
|
||||
"apostasy": ["dotnet40", "dotnet8"],
|
||||
"nordicsouls": ["dotnet40"],
|
||||
"livingskyrim": ["dotnet40"],
|
||||
"lsiv": ["dotnet40"],
|
||||
"ls4": ["dotnet40"],
|
||||
"lostlegacy": ["dotnet48"],
|
||||
# "wildlander": ["dotnet472"], # DISABLED: Universal registry fixes replace dotnet472 installation
|
||||
# "librum": ["dotnet40", "dotnet8"], # PARTIAL DISABLE: Keep dotnet8, remove dotnet40
|
||||
"librum": ["dotnet8"], # dotnet40 replaced with universal registry fixes
|
||||
# "apostasy": ["dotnet40", "dotnet8"], # PARTIAL DISABLE: Keep dotnet8, remove dotnet40
|
||||
"apostasy": ["dotnet8"], # dotnet40 replaced with universal registry fixes
|
||||
# "nordicsouls": ["dotnet40"], # DISABLED: Universal registry fixes replace dotnet40 installation
|
||||
# "livingskyrim": ["dotnet40"], # DISABLED: Universal registry fixes replace dotnet40 installation
|
||||
# "lsiv": ["dotnet40"], # DISABLED: Universal registry fixes replace dotnet40 installation
|
||||
# "ls4": ["dotnet40"], # DISABLED: Universal registry fixes replace dotnet40 installation
|
||||
# "lorerim": ["dotnet40"], # DISABLED: Universal registry fixes replace dotnet40 installation
|
||||
# "lostlegacy": ["dotnet40"], # DISABLED: Universal registry fixes replace dotnet40 installation
|
||||
}
|
||||
|
||||
def __init__(self, steam_path_or_config: Union[Dict, str, Path, None] = None,
|
||||
@@ -105,6 +109,12 @@ class ModlistHandler:
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.logger.propagate = False
|
||||
self.steamdeck = steamdeck
|
||||
|
||||
# DEBUG: Log ModlistHandler instantiation details for SD card path debugging
|
||||
import traceback
|
||||
caller_info = traceback.extract_stack()[-2] # Get caller info
|
||||
self.logger.debug(f"[SD_CARD_DEBUG] ModlistHandler created: id={id(self)}, steamdeck={steamdeck}")
|
||||
self.logger.debug(f"[SD_CARD_DEBUG] Created from: {caller_info.filename}:{caller_info.lineno} in {caller_info.name}()")
|
||||
self.steam_path: Optional[Path] = None
|
||||
self.verbose = verbose # Store verbose flag
|
||||
self.mo2_path: Optional[Path] = None
|
||||
@@ -158,7 +168,7 @@ class ModlistHandler:
|
||||
self.stock_game_path = None
|
||||
|
||||
# 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
|
||||
from .winetricks_handler import WinetricksHandler
|
||||
self.winetricks_handler = WinetricksHandler(logger=self.logger)
|
||||
@@ -315,13 +325,26 @@ class ModlistHandler:
|
||||
self.modlist_dir = Path(modlist_dir_path_str)
|
||||
self.modlist_ini = modlist_ini_path
|
||||
|
||||
# Determine if modlist is on SD card
|
||||
# Use str() for startswith check
|
||||
if str(self.modlist_dir).startswith("/run/media") or str(self.modlist_dir).startswith("/media"):
|
||||
# Determine if modlist is on SD card (Steam Deck only)
|
||||
# On non-Steam Deck systems, /media mounts should use Z: drive, not D: drive
|
||||
is_on_sdcard_path = str(self.modlist_dir).startswith("/run/media") or str(self.modlist_dir).startswith("/media")
|
||||
|
||||
# DEBUG: Log SD card detection logic
|
||||
self.logger.debug(f"[SD_CARD_DEBUG] SD card detection for instance id={id(self)}:")
|
||||
self.logger.debug(f"[SD_CARD_DEBUG] modlist_dir: {self.modlist_dir}")
|
||||
self.logger.debug(f"[SD_CARD_DEBUG] is_on_sdcard_path: {is_on_sdcard_path}")
|
||||
self.logger.debug(f"[SD_CARD_DEBUG] self.steamdeck: {self.steamdeck}")
|
||||
|
||||
if is_on_sdcard_path and self.steamdeck:
|
||||
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).")
|
||||
self.logger.debug(f"[SD_CARD_DEBUG] Set modlist_sdcard=True")
|
||||
else:
|
||||
self.modlist_sdcard = False
|
||||
self.logger.debug(f"[SD_CARD_DEBUG] Set modlist_sdcard=False because: is_on_sdcard_path={is_on_sdcard_path} AND steamdeck={self.steamdeck}")
|
||||
if is_on_sdcard_path and not self.steamdeck:
|
||||
self.logger.info("Modlist on /media mount detected on non-Steam Deck system - using Z: drive mapping.")
|
||||
self.logger.debug("[SD_CARD_DEBUG] This is the ROOT CAUSE - SD card path but steamdeck=False!")
|
||||
|
||||
# Find and set compatdata path now that we have appid
|
||||
# Ensure PathHandler is available (should be initialized in __init__)
|
||||
@@ -345,7 +368,8 @@ class ModlistHandler:
|
||||
# Store engine_installed flag for conditional path manipulation
|
||||
self.engine_installed = modlist_info.get('engine_installed', False)
|
||||
self.logger.debug(f" Engine Installed: {self.engine_installed}")
|
||||
|
||||
|
||||
|
||||
# Call internal detection methods to populate more state
|
||||
if not self._detect_game_variables():
|
||||
self.logger.warning("Failed to auto-detect game type after setting context.")
|
||||
@@ -665,6 +689,25 @@ class ModlistHandler:
|
||||
return False
|
||||
self.logger.info("Step 3: Curated user.reg.modlist and system.reg.modlist applied successfully.")
|
||||
|
||||
# Step 3.5: Apply universal dotnet4.x compatibility registry fixes
|
||||
if status_callback:
|
||||
status_callback(f"{self._get_progress_timestamp()} Applying universal dotnet4.x compatibility fixes")
|
||||
self.logger.info("Step 3.5: Applying universal dotnet4.x compatibility registry fixes...")
|
||||
registry_success = False
|
||||
try:
|
||||
registry_success = self._apply_universal_dotnet_fixes()
|
||||
except Exception as e:
|
||||
self.logger.error(f"CRITICAL: Registry fixes failed - modlist may have .NET compatibility issues: {e}")
|
||||
registry_success = False
|
||||
|
||||
if not registry_success:
|
||||
self.logger.error("=" * 80)
|
||||
self.logger.error("WARNING: Universal dotnet4.x registry fixes FAILED!")
|
||||
self.logger.error("This modlist may experience .NET Framework compatibility issues.")
|
||||
self.logger.error("Consider manually setting mscoree=native in winecfg if problems occur.")
|
||||
self.logger.error("=" * 80)
|
||||
# Continue but user should be aware of potential issues
|
||||
|
||||
# Step 4: Install Wine Components
|
||||
if status_callback:
|
||||
status_callback(f"{self._get_progress_timestamp()} Installing Wine components (this may take a while)")
|
||||
@@ -685,39 +728,27 @@ class ModlistHandler:
|
||||
# All modlists now use their own AppID for wine components
|
||||
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)
|
||||
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.")
|
||||
return False
|
||||
|
||||
# Try winetricks first (preferred method with current fix)
|
||||
winetricks_success = False
|
||||
# Use the winetricks handler which respects the user's toggle setting
|
||||
try:
|
||||
self.logger.info("Attempting Wine component installation using winetricks...")
|
||||
winetricks_success = self.winetricks_handler.install_wine_components(wineprefix, self.game_var_full, specific_components=components)
|
||||
if winetricks_success:
|
||||
self.logger.info("Winetricks installation completed successfully")
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Winetricks installation failed with exception: {e}")
|
||||
winetricks_success = False
|
||||
|
||||
# Fallback to protontricks if winetricks failed
|
||||
if not winetricks_success:
|
||||
self.logger.warning("Winetricks failed, falling back to protontricks for Wine component installation...")
|
||||
try:
|
||||
protontricks_success = self.protontricks_handler.install_wine_components(target_appid, self.game_var_full, specific_components=components)
|
||||
if protontricks_success:
|
||||
self.logger.info("Protontricks fallback installation completed successfully")
|
||||
else:
|
||||
self.logger.error("Both winetricks and protontricks failed to install Wine components.")
|
||||
print("Error: Failed to install necessary Wine components using both winetricks and protontricks.")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"Protontricks fallback also failed with exception: {e}")
|
||||
print("Error: Failed to install necessary Wine components using both winetricks and protontricks.")
|
||||
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.")
|
||||
return False
|
||||
self.logger.info("Step 4: Installing Wine components... Done")
|
||||
|
||||
# Step 5: Ensure permissions of Modlist directory
|
||||
@@ -745,6 +776,14 @@ class ModlistHandler:
|
||||
self.logger.info(f"ModOrganizer.ini backed up to: {backup_path}")
|
||||
self.logger.info("Step 6: Backing up ModOrganizer.ini... Done")
|
||||
|
||||
# Step 6.5: Handle symlinked downloads directory
|
||||
if status_callback:
|
||||
status_callback(f"{self._get_progress_timestamp()} Checking for symlinked downloads directory")
|
||||
self.logger.info("Step 6.5: Checking for symlinked downloads directory...")
|
||||
if not self._handle_symlinked_downloads():
|
||||
self.logger.warning("Warning during symlink handling (non-critical)")
|
||||
self.logger.info("Step 6.5: Checking for symlinked downloads directory... Done")
|
||||
|
||||
# Step 7a: Detect Stock Game/Game Root path
|
||||
if status_callback:
|
||||
status_callback(f"{self._get_progress_timestamp()} Detecting stock game path")
|
||||
@@ -790,6 +829,15 @@ class ModlistHandler:
|
||||
# Conditionally update binary and working directory paths
|
||||
# Skip for jackify-engine workflows since paths are already correct
|
||||
# Exception: Always run for SD card installs to fix Z:/run/media/... to D:/... paths
|
||||
|
||||
# DEBUG: Add comprehensive logging to identify Steam Deck SD card path manipulation issues
|
||||
engine_installed = getattr(self, 'engine_installed', False)
|
||||
self.logger.debug(f"[SD_CARD_DEBUG] ModlistHandler instance: id={id(self)}")
|
||||
self.logger.debug(f"[SD_CARD_DEBUG] engine_installed: {engine_installed}")
|
||||
self.logger.debug(f"[SD_CARD_DEBUG] modlist_sdcard: {self.modlist_sdcard}")
|
||||
self.logger.debug(f"[SD_CARD_DEBUG] steamdeck parameter passed to constructor: {getattr(self, 'steamdeck', 'NOT_SET')}")
|
||||
self.logger.debug(f"[SD_CARD_DEBUG] Path manipulation condition: not {engine_installed} or {self.modlist_sdcard} = {not engine_installed or self.modlist_sdcard}")
|
||||
|
||||
if not getattr(self, 'engine_installed', False) or self.modlist_sdcard:
|
||||
# Convert steamapps/common path to library root path
|
||||
steam_libraries = None
|
||||
@@ -809,7 +857,8 @@ class ModlistHandler:
|
||||
print("Error: Failed to update binary and working directory paths in ModOrganizer.ini.")
|
||||
return False # Abort on failure
|
||||
else:
|
||||
self.logger.debug("Skipping path manipulation - jackify-engine already set correct paths in ModOrganizer.ini")
|
||||
self.logger.debug("[SD_CARD_DEBUG] Skipping path manipulation - jackify-engine already set correct paths in ModOrganizer.ini")
|
||||
self.logger.debug(f"[SD_CARD_DEBUG] SKIPPED because: engine_installed={engine_installed} and modlist_sdcard={self.modlist_sdcard}")
|
||||
self.logger.info("Step 8: Updating ModOrganizer.ini paths... Done")
|
||||
|
||||
# Step 9: Update Resolution Settings (if applicable)
|
||||
@@ -822,10 +871,10 @@ class ModlistHandler:
|
||||
vanilla_game_dir = None
|
||||
if self.steam_library and 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(
|
||||
modlist_dir=self.modlist_dir,
|
||||
game_var=self.game_var_full,
|
||||
|
||||
if not ResolutionHandler.update_ini_resolution(
|
||||
modlist_dir=self.modlist_dir,
|
||||
game_var=self.game_var_full,
|
||||
set_res=self.selected_resolution,
|
||||
vanilla_game_dir=vanilla_game_dir
|
||||
):
|
||||
@@ -871,21 +920,38 @@ class ModlistHandler:
|
||||
print("Warning: Failed to create dxvk.conf file.")
|
||||
self.logger.info("Step 10: Creating dxvk.conf... Done")
|
||||
|
||||
# Step 11a: Small Tasks - Delete Plugin
|
||||
# Step 11a: Small Tasks - Delete Incompatible Plugins
|
||||
if status_callback:
|
||||
status_callback(f"{self._get_progress_timestamp()} Deleting incompatible MO2 plugin")
|
||||
self.logger.info("Step 11a: Deleting incompatible MO2 plugin (FixGameRegKey.py)...")
|
||||
plugin_path = Path(self.modlist_dir) / "plugins" / "FixGameRegKey.py"
|
||||
if plugin_path.exists():
|
||||
status_callback(f"{self._get_progress_timestamp()} Deleting incompatible MO2 plugins")
|
||||
self.logger.info("Step 11a: Deleting incompatible MO2 plugins...")
|
||||
|
||||
# Delete FixGameRegKey.py plugin
|
||||
fixgamereg_path = Path(self.modlist_dir) / "plugins" / "FixGameRegKey.py"
|
||||
if fixgamereg_path.exists():
|
||||
try:
|
||||
plugin_path.unlink()
|
||||
fixgamereg_path.unlink()
|
||||
self.logger.info("FixGameRegKey.py plugin deleted successfully.")
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Failed to delete FixGameRegKey.py plugin: {e}")
|
||||
print("Warning: Failed to delete incompatible plugin file.")
|
||||
print("Warning: Failed to delete FixGameRegKey.py plugin file.")
|
||||
else:
|
||||
self.logger.debug("FixGameRegKey.py plugin not found (this is normal).")
|
||||
self.logger.info("Step 11a: Plugin deletion check complete.")
|
||||
|
||||
# Delete PageFileManager plugin directory (Linux has no PageFile)
|
||||
pagefilemgr_path = Path(self.modlist_dir) / "plugins" / "PageFileManager"
|
||||
if pagefilemgr_path.exists():
|
||||
try:
|
||||
import shutil
|
||||
shutil.rmtree(pagefilemgr_path)
|
||||
self.logger.info("PageFileManager plugin directory deleted successfully.")
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Failed to delete PageFileManager plugin directory: {e}")
|
||||
print("Warning: Failed to delete PageFileManager plugin directory.")
|
||||
else:
|
||||
self.logger.debug("PageFileManager plugin not found (this is normal).")
|
||||
|
||||
self.logger.info("Step 11a: Incompatible plugin deletion check complete.")
|
||||
|
||||
|
||||
# Step 11b: Download Font
|
||||
if status_callback:
|
||||
@@ -928,6 +994,10 @@ class ModlistHandler:
|
||||
# status_callback("Configuration 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
|
||||
|
||||
def _detect_steam_library_info(self) -> bool:
|
||||
@@ -1330,4 +1400,236 @@ class ModlistHandler:
|
||||
self.logger.debug("No special game type detected - standard workflow will be used")
|
||||
return None
|
||||
|
||||
# (Ensure EOF is clean and no extra incorrect methods exist below)
|
||||
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
|
||||
|
||||
def _apply_universal_dotnet_fixes(self):
|
||||
"""Apply universal dotnet4.x compatibility registry fixes to ALL modlists"""
|
||||
try:
|
||||
prefix_path = os.path.join(str(self.compat_data_path), "pfx")
|
||||
if not os.path.exists(prefix_path):
|
||||
self.logger.warning(f"Prefix path not found: {prefix_path}")
|
||||
return False
|
||||
|
||||
self.logger.info("Applying universal dotnet4.x compatibility registry fixes...")
|
||||
|
||||
# Find the appropriate Wine binary to use for registry operations
|
||||
wine_binary = self._find_wine_binary_for_registry()
|
||||
if not wine_binary:
|
||||
self.logger.error("Could not find Wine binary for registry operations")
|
||||
return False
|
||||
|
||||
# Set environment for Wine registry operations
|
||||
env = os.environ.copy()
|
||||
env['WINEPREFIX'] = prefix_path
|
||||
env['WINEDEBUG'] = '-all' # Suppress Wine debug output
|
||||
|
||||
# Registry fix 1: Set mscoree=native DLL override
|
||||
# This tells Wine to use native .NET runtime instead of Wine's implementation
|
||||
self.logger.debug("Setting mscoree=native DLL override...")
|
||||
cmd1 = [
|
||||
wine_binary, 'reg', 'add',
|
||||
'HKEY_CURRENT_USER\\Software\\Wine\\DllOverrides',
|
||||
'/v', 'mscoree', '/t', 'REG_SZ', '/d', 'native', '/f'
|
||||
]
|
||||
|
||||
result1 = subprocess.run(cmd1, env=env, capture_output=True, text=True)
|
||||
if result1.returncode == 0:
|
||||
self.logger.info("Successfully applied mscoree=native DLL override")
|
||||
else:
|
||||
self.logger.warning(f"Failed to set mscoree DLL override: {result1.stderr}")
|
||||
|
||||
# Registry fix 2: Set OnlyUseLatestCLR=1
|
||||
# This prevents .NET version conflicts by using the latest CLR
|
||||
self.logger.debug("Setting OnlyUseLatestCLR=1 registry entry...")
|
||||
cmd2 = [
|
||||
wine_binary, 'reg', 'add',
|
||||
'HKEY_LOCAL_MACHINE\\Software\\Microsoft\\.NETFramework',
|
||||
'/v', 'OnlyUseLatestCLR', '/t', 'REG_DWORD', '/d', '1', '/f'
|
||||
]
|
||||
|
||||
result2 = subprocess.run(cmd2, env=env, capture_output=True, text=True)
|
||||
if result2.returncode == 0:
|
||||
self.logger.info("Successfully applied OnlyUseLatestCLR=1 registry entry")
|
||||
else:
|
||||
self.logger.warning(f"Failed to set OnlyUseLatestCLR: {result2.stderr}")
|
||||
|
||||
# Both fixes applied - this should eliminate dotnet4.x installation requirements
|
||||
if result1.returncode == 0 and result2.returncode == 0:
|
||||
self.logger.info("Universal dotnet4.x compatibility fixes applied successfully")
|
||||
return True
|
||||
else:
|
||||
self.logger.warning("Some dotnet4.x registry fixes failed, but continuing...")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to apply universal dotnet4.x fixes: {e}")
|
||||
return False
|
||||
|
||||
def _find_wine_binary_for_registry(self) -> Optional[str]:
|
||||
"""Find the appropriate Wine binary for registry operations using user's configured Proton"""
|
||||
try:
|
||||
# Use the user's configured Proton version from settings
|
||||
from ..handlers.config_handler import ConfigHandler
|
||||
config_handler = ConfigHandler()
|
||||
user_proton_path = config_handler.get_game_proton_path()
|
||||
|
||||
if user_proton_path and user_proton_path != 'auto':
|
||||
# User has selected a specific Proton version
|
||||
proton_path = Path(user_proton_path).expanduser()
|
||||
|
||||
# Check for wine binary in both GE-Proton and Valve Proton structures
|
||||
wine_candidates = [
|
||||
proton_path / "files" / "bin" / "wine", # GE-Proton structure
|
||||
proton_path / "dist" / "bin" / "wine" # Valve Proton structure
|
||||
]
|
||||
|
||||
for wine_path in wine_candidates:
|
||||
if wine_path.exists():
|
||||
self.logger.info(f"Using Wine binary from user's configured Proton: {wine_path}")
|
||||
return str(wine_path)
|
||||
|
||||
self.logger.warning(f"User's configured Proton path has no wine binary: {user_proton_path}")
|
||||
|
||||
# Fallback: Try to use same Steam library detection as main Proton detection
|
||||
from ..handlers.wine_utils import WineUtils
|
||||
best_proton = WineUtils.select_best_proton()
|
||||
if best_proton:
|
||||
wine_binary = WineUtils.find_proton_binary(best_proton['name'])
|
||||
if wine_binary:
|
||||
self.logger.info(f"Using Wine binary from detected Proton: {wine_binary}")
|
||||
return wine_binary
|
||||
|
||||
# NEVER fall back to system wine - it will break Proton prefixes with architecture mismatches
|
||||
self.logger.error("No suitable Proton Wine binary found for registry operations")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error finding Wine binary: {e}")
|
||||
return None
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ class ModlistInstallCLI:
|
||||
def __init__(self, menu_handler: MenuHandler, steamdeck: bool = False):
|
||||
self.menu_handler = menu_handler
|
||||
self.steamdeck = steamdeck
|
||||
self.protontricks_handler = ProtontricksHandler(steamdeck=steamdeck)
|
||||
self.protontricks_handler = ProtontricksHandler(steamdeck)
|
||||
self.shortcut_handler = ShortcutHandler(steamdeck=steamdeck)
|
||||
self.context = {}
|
||||
# Use standard logging (no file handler)
|
||||
|
||||
@@ -653,41 +653,7 @@ class PathHandler:
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error using multi-user detection for shortcuts.vdf: {e}")
|
||||
|
||||
# Fallback to legacy behavior if multi-user detection fails
|
||||
logger.warning("Falling back to legacy shortcuts.vdf detection (first-found user)")
|
||||
userdata_base_paths = [
|
||||
os.path.expanduser("~/.steam/steam/userdata"),
|
||||
os.path.expanduser("~/.local/share/Steam/userdata"),
|
||||
os.path.expanduser("~/.var/app/com.valvesoftware.Steam/.local/share/Steam/userdata")
|
||||
]
|
||||
found_vdf_path = None
|
||||
for base_path in userdata_base_paths:
|
||||
if not os.path.isdir(base_path):
|
||||
logger.debug(f"Userdata base path not found or not a directory: {base_path}")
|
||||
continue
|
||||
logger.debug(f"Searching for user IDs in: {base_path}")
|
||||
try:
|
||||
for item in os.listdir(base_path):
|
||||
user_path = os.path.join(base_path, item)
|
||||
if os.path.isdir(user_path) and item.isdigit():
|
||||
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
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def find_game_install_paths(target_appids: Dict[str, str]) -> Dict[str, Path]:
|
||||
@@ -811,17 +777,35 @@ class PathHandler:
|
||||
|
||||
# Extract existing gamePath to use as source of truth for vanilla game location
|
||||
existing_game_path = None
|
||||
for line in lines:
|
||||
gamepath_line_index = -1
|
||||
for i, line in enumerate(lines):
|
||||
if re.match(r'^\s*gamepath\s*=.*@ByteArray\(([^)]+)\)', line, re.IGNORECASE):
|
||||
match = re.search(r'@ByteArray\(([^)]+)\)', line)
|
||||
if match:
|
||||
raw_path = match.group(1)
|
||||
gamepath_line_index = i
|
||||
# Convert Windows path back to Linux path
|
||||
if raw_path.startswith(('Z:', 'D:')):
|
||||
linux_path = raw_path[2:].replace('\\\\', '/').replace('\\', '/')
|
||||
existing_game_path = linux_path
|
||||
logger.debug(f"Extracted existing gamePath: {existing_game_path}")
|
||||
break
|
||||
|
||||
# Special handling for gamePath in three-true scenario (engine_installed + steamdeck + sdcard)
|
||||
if modlist_sdcard and existing_game_path and existing_game_path.startswith('/run/media') and gamepath_line_index != -1:
|
||||
# Simple manual stripping of /run/media/deck/UUID pattern for SD card paths
|
||||
# Match /run/media/deck/[UUID]/Games/... and extract just /Games/...
|
||||
sdcard_pattern = r'^/run/media/deck/[^/]+(/Games/.*)$'
|
||||
match = re.match(sdcard_pattern, existing_game_path)
|
||||
if match:
|
||||
stripped_path = match.group(1) # Just the /Games/... part
|
||||
new_gamepath_value = f"D:\\\\{stripped_path.replace('/', '\\\\')}"
|
||||
new_gamepath_line = f"gamePath = @ByteArray({new_gamepath_value})\n"
|
||||
|
||||
logger.info(f"Updating gamePath for SD card: {lines[gamepath_line_index].strip()} -> {new_gamepath_line.strip()}")
|
||||
lines[gamepath_line_index] = new_gamepath_line
|
||||
else:
|
||||
logger.warning(f"SD card path doesn't match expected pattern: {existing_game_path}")
|
||||
|
||||
game_path_updated = False
|
||||
binary_paths_updated = 0
|
||||
|
||||
@@ -149,9 +149,29 @@ class ProtontricksHandler:
|
||||
should_install = True
|
||||
else:
|
||||
try:
|
||||
response = input("Protontricks not found. Install the Flatpak version? (Y/n): ").lower()
|
||||
if response == 'y' or response == '':
|
||||
print("\nProtontricks not found. Choose installation method:")
|
||||
print("1. Install via Flatpak (automatic)")
|
||||
print("2. Install via native package manager (manual)")
|
||||
print("3. Skip (Use bundled winetricks instead)")
|
||||
choice = input("Enter choice (1/2/3): ").strip()
|
||||
|
||||
if choice == '1' or choice == '':
|
||||
should_install = True
|
||||
elif choice == '2':
|
||||
print("\nTo install protontricks via your system package manager:")
|
||||
print("• Ubuntu/Debian: sudo apt install protontricks")
|
||||
print("• Fedora: sudo dnf install protontricks")
|
||||
print("• Arch Linux: sudo pacman -S protontricks")
|
||||
print("• openSUSE: sudo zypper install protontricks")
|
||||
print("\nAfter installation, please rerun Jackify.")
|
||||
return False
|
||||
elif choice == '3':
|
||||
print("Skipping protontricks installation. Will use bundled winetricks for component installation.")
|
||||
logger.info("User chose to skip protontricks and use winetricks fallback")
|
||||
return False
|
||||
else:
|
||||
print("Invalid choice. Installation cancelled.")
|
||||
return False
|
||||
except KeyboardInterrupt:
|
||||
print("\nInstallation cancelled.")
|
||||
return False
|
||||
@@ -488,7 +508,7 @@ class ProtontricksHandler:
|
||||
if "ShowDotFiles" not in content:
|
||||
logger.debug(f"Adding ShowDotFiles entry to {user_reg_path}")
|
||||
with open(user_reg_path, 'a', encoding='utf-8') as f:
|
||||
f.write('\n[Software\\Wine] 1603891765\n')
|
||||
f.write('\n[Software\\Wine] 1603891765\n')
|
||||
f.write('"ShowDotFiles"="Y"\n')
|
||||
dotfiles_set_success = True # Count file write as success too
|
||||
else:
|
||||
@@ -497,7 +517,7 @@ class ProtontricksHandler:
|
||||
else:
|
||||
logger.warning(f"user.reg not found at {user_reg_path}, creating it.")
|
||||
with open(user_reg_path, 'w', encoding='utf-8') as f:
|
||||
f.write('[Software\\Wine] 1603891765\n')
|
||||
f.write('[Software\\Wine] 1603891765\n')
|
||||
f.write('"ShowDotFiles"="Y"\n')
|
||||
dotfiles_set_success = True # Creating file counts as success
|
||||
except Exception as e:
|
||||
|
||||
@@ -41,7 +41,7 @@ class ShortcutHandler:
|
||||
self._last_shortcuts_backup = None # Track the last backup path
|
||||
self._safe_shortcuts_backup = None # Track backup made just before restart
|
||||
# Initialize ProtontricksHandler here, passing steamdeck status
|
||||
self.protontricks_handler = ProtontricksHandler(steamdeck=self.steamdeck)
|
||||
self.protontricks_handler = ProtontricksHandler(self.steamdeck)
|
||||
|
||||
def _enable_tab_completion(self):
|
||||
"""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}')")
|
||||
try:
|
||||
from .protontricks_handler import ProtontricksHandler # Local import
|
||||
pt_handler = ProtontricksHandler(steamdeck=self.steamdeck)
|
||||
pt_handler = ProtontricksHandler(self.steamdeck)
|
||||
if not pt_handler.detect_protontricks():
|
||||
self.logger.error("Protontricks not detected")
|
||||
return None
|
||||
@@ -1036,7 +1036,7 @@ class ShortcutHandler:
|
||||
matched_shortcuts = []
|
||||
|
||||
if not self.shortcuts_path or not os.path.isfile(self.shortcuts_path):
|
||||
self.logger.error(f"shortcuts.vdf path not found or invalid: {self.shortcuts_path}")
|
||||
self.logger.info(f"No shortcuts.vdf file found at {self.shortcuts_path} - this is normal for new Steam installations")
|
||||
return []
|
||||
|
||||
# Directly process the single shortcuts.vdf file found during init
|
||||
@@ -1159,7 +1159,7 @@ class ShortcutHandler:
|
||||
|
||||
# --- Use the single shortcuts.vdf path found during init ---
|
||||
if not self.shortcuts_path or not os.path.isfile(self.shortcuts_path):
|
||||
self.logger.error(f"shortcuts.vdf path not found or invalid: {self.shortcuts_path}")
|
||||
self.logger.info(f"No shortcuts.vdf file found at {self.shortcuts_path} - this is normal for new Steam installations")
|
||||
return []
|
||||
|
||||
vdf_path = self.shortcuts_path
|
||||
|
||||
@@ -537,10 +537,7 @@ class WineUtils:
|
||||
if "mods" in binary_path:
|
||||
# mods path type found
|
||||
if modlist_sdcard:
|
||||
path_middle = modlist_dir.split('mmcblk0p1', 1)[1] if 'mmcblk0p1' in modlist_dir else 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]
|
||||
path_middle = WineUtils._strip_sdcard_path(modlist_dir)
|
||||
else:
|
||||
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"]):
|
||||
# Stock/Game Root found
|
||||
if modlist_sdcard:
|
||||
path_middle = modlist_dir.split('mmcblk0p1', 1)[1] if 'mmcblk0p1' in modlist_dir else 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]
|
||||
path_middle = WineUtils._strip_sdcard_path(modlist_dir)
|
||||
else:
|
||||
path_middle = modlist_dir
|
||||
|
||||
@@ -589,7 +583,7 @@ class WineUtils:
|
||||
elif "steamapps" in binary_path:
|
||||
# Steamapps found
|
||||
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:"
|
||||
else:
|
||||
path_middle = steam_library.split('steamapps', 1)[0] if 'steamapps' in steam_library else steam_library
|
||||
@@ -674,7 +668,10 @@ class WineUtils:
|
||||
# Add standard compatibility tool locations (covers edge cases like Flatpak)
|
||||
compatibility_paths.extend([
|
||||
Path.home() / ".steam/root/compatibilitytools.d",
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam/.local/share/Steam/compatibilitytools.d"
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam/.local/share/Steam/compatibilitytools.d",
|
||||
# Flatpak GE-Proton extension paths
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam.CompatibilityTool.Proton-GE/.local/share/Steam/compatibilitytools.d",
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam/.local/share/Steam/compatibilitytools.d/GE-Proton"
|
||||
])
|
||||
# Special handling for Proton 9: try all possible directory names
|
||||
if proton_version.strip().startswith("Proton 9"):
|
||||
@@ -783,27 +780,40 @@ class WineUtils:
|
||||
Returns:
|
||||
List of Path objects for Steam library directories
|
||||
"""
|
||||
steam_common_paths = []
|
||||
|
||||
try:
|
||||
from .path_handler import PathHandler
|
||||
# Use existing PathHandler that reads libraryfolders.vdf
|
||||
library_paths = PathHandler.get_all_steam_library_paths()
|
||||
logger.info(f"PathHandler found Steam libraries: {library_paths}")
|
||||
|
||||
# Convert to steamapps/common paths for Proton scanning
|
||||
steam_common_paths = []
|
||||
for lib_path in library_paths:
|
||||
common_path = lib_path / "steamapps" / "common"
|
||||
if common_path.exists():
|
||||
steam_common_paths.append(common_path)
|
||||
logger.debug(f"Found Steam library paths: {steam_common_paths}")
|
||||
return steam_common_paths
|
||||
logger.debug(f"Added Steam library: {common_path}")
|
||||
else:
|
||||
logger.debug(f"Steam library path doesn't exist: {common_path}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get Steam library paths from libraryfolders.vdf: {e}")
|
||||
# Fallback to hardcoded paths if PathHandler fails
|
||||
fallback_paths = [
|
||||
Path.home() / ".steam/steam/steamapps/common",
|
||||
Path.home() / ".local/share/Steam/steamapps/common",
|
||||
Path.home() / ".steam/root/steamapps/common"
|
||||
]
|
||||
return [path for path in fallback_paths if path.exists()]
|
||||
logger.error(f"PathHandler failed to read libraryfolders.vdf: {e}")
|
||||
|
||||
# Always add fallback paths in case PathHandler missed something
|
||||
fallback_paths = [
|
||||
Path.home() / ".steam/steam/steamapps/common",
|
||||
Path.home() / ".local/share/Steam/steamapps/common",
|
||||
Path.home() / ".steam/root/steamapps/common"
|
||||
]
|
||||
|
||||
for fallback_path in fallback_paths:
|
||||
if fallback_path.exists() and fallback_path not in steam_common_paths:
|
||||
steam_common_paths.append(fallback_path)
|
||||
logger.debug(f"Added fallback Steam library: {fallback_path}")
|
||||
|
||||
logger.info(f"Final Steam library paths for Proton scanning: {steam_common_paths}")
|
||||
return steam_common_paths
|
||||
|
||||
@staticmethod
|
||||
def get_compatibility_tool_paths() -> List[Path]:
|
||||
@@ -815,7 +825,12 @@ class WineUtils:
|
||||
"""
|
||||
compat_paths = [
|
||||
Path.home() / ".steam/steam/compatibilitytools.d",
|
||||
Path.home() / ".local/share/Steam/compatibilitytools.d"
|
||||
Path.home() / ".local/share/Steam/compatibilitytools.d",
|
||||
Path.home() / ".steam/root/compatibilitytools.d",
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam/.local/share/Steam/compatibilitytools.d",
|
||||
# Flatpak GE-Proton extension paths
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam.CompatibilityTool.Proton-GE/.local/share/Steam/compatibilitytools.d",
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam/.local/share/Steam/compatibilitytools.d/GE-Proton"
|
||||
]
|
||||
|
||||
# Return only existing paths
|
||||
|
||||
@@ -169,9 +169,18 @@ class WinetricksHandler:
|
||||
if best_proton:
|
||||
wine_binary = WineUtils.find_proton_binary(best_proton['name'])
|
||||
self.logger.info(f"Auto-selected Proton: {best_proton['name']} at {best_proton['path']}")
|
||||
else:
|
||||
# Enhanced debugging for Proton detection failure
|
||||
self.logger.error("Auto-detection failed - no Proton versions found")
|
||||
available_versions = WineUtils.scan_all_proton_versions()
|
||||
if available_versions:
|
||||
self.logger.error(f"Available Proton versions: {[v['name'] for v in available_versions]}")
|
||||
else:
|
||||
self.logger.error("No Proton versions detected in standard Steam locations")
|
||||
|
||||
if not wine_binary:
|
||||
self.logger.error("Cannot run winetricks: No compatible Proton version found")
|
||||
self.logger.error("Please ensure you have Proton 9+ or GE-Proton installed through Steam")
|
||||
return False
|
||||
|
||||
if not (os.path.exists(wine_binary) and os.access(wine_binary, os.X_OK)):
|
||||
@@ -257,10 +266,28 @@ class WinetricksHandler:
|
||||
components_to_install = self._reorder_components_for_installation(all_components)
|
||||
self.logger.info(f"WINEPREFIX: {wineprefix}, Game: {game_var}, Ordered Components: {components_to_install}")
|
||||
|
||||
# Install components separately if dotnet40 is present (mimics protontricks behavior)
|
||||
if "dotnet40" in components_to_install:
|
||||
self.logger.info("dotnet40 detected - installing components separately like protontricks")
|
||||
return self._install_components_separately(components_to_install, wineprefix, wine_binary, env)
|
||||
# Check user preference for component installation method
|
||||
from ..handlers.config_handler import ConfigHandler
|
||||
config_handler = ConfigHandler()
|
||||
use_winetricks = config_handler.get('use_winetricks_for_components', True)
|
||||
|
||||
# Legacy .NET Framework versions that are problematic in Wine/Proton
|
||||
# DISABLED in v0.1.6.2: Universal registry fixes replace dotnet4.x installation
|
||||
# legacy_dotnet_versions = ['dotnet40', 'dotnet472', 'dotnet48']
|
||||
legacy_dotnet_versions = [] # ALL dotnet4.x versions disabled - universal registry fixes handle compatibility
|
||||
|
||||
# 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
|
||||
# HYBRID APPROACH MOSTLY DISABLED: dotnet40/dotnet472 replaced with universal registry fixes
|
||||
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)
|
||||
max_attempts = 3
|
||||
@@ -289,6 +316,8 @@ class WinetricksHandler:
|
||||
self.logger.debug(f"Winetricks output: {result.stdout}")
|
||||
if result.returncode == 0:
|
||||
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
|
||||
else:
|
||||
# Special handling for dotnet40 verification issue (mimics protontricks behavior)
|
||||
@@ -415,14 +444,8 @@ class WinetricksHandler:
|
||||
self.logger.error("Failed to prepare prefix for dotnet40")
|
||||
return False
|
||||
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")
|
||||
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
|
||||
max_attempts = 3
|
||||
@@ -449,7 +472,7 @@ class WinetricksHandler:
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
self.logger.info(f"✓ {component} installed successfully")
|
||||
self.logger.info(f"{component} installed successfully")
|
||||
component_success = True
|
||||
break
|
||||
else:
|
||||
@@ -463,13 +486,13 @@ class WinetricksHandler:
|
||||
try:
|
||||
with open(log_path, 'r') as f:
|
||||
if 'dotnet40' in f.read():
|
||||
self.logger.info("✓ dotnet40 confirmed in winetricks.log")
|
||||
self.logger.info("dotnet40 confirmed in winetricks.log")
|
||||
component_success = True
|
||||
break
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Could not read winetricks.log: {e}")
|
||||
|
||||
self.logger.error(f"✗ {component} failed (attempt {attempt}): {result.stderr.strip()}")
|
||||
self.logger.error(f"{component} failed (attempt {attempt}): {result.stderr.strip()}")
|
||||
self.logger.debug(f"Full stdout for {component}: {result.stdout.strip()}")
|
||||
|
||||
except Exception as e:
|
||||
@@ -479,9 +502,384 @@ class WinetricksHandler:
|
||||
self.logger.error(f"Failed to install {component} after {max_attempts} attempts")
|
||||
return False
|
||||
|
||||
self.logger.info("✓ All components installed successfully using separate sessions")
|
||||
self.logger.info("All components installed successfully using separate sessions")
|
||||
# Set Windows 10 mode after all component installation (matches legacy script timing)
|
||||
self._set_windows_10_mode(wineprefix, env.get('WINE', ''))
|
||||
return True
|
||||
|
||||
def _install_components_hybrid_approach(self, components: list, wineprefix: str, game_var: str, 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):
|
||||
"""
|
||||
Internal method to clean up wine processes during component installation
|
||||
|
||||
@@ -39,14 +39,35 @@ class AutomatedPrefixService:
|
||||
from jackify.shared.timing import get_timestamp
|
||||
return get_timestamp()
|
||||
|
||||
def _get_user_proton_version(self):
|
||||
"""Get user's preferred Proton version from config, with fallback to auto-detection"""
|
||||
def _get_user_proton_version(self, modlist_name: str = None):
|
||||
"""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:
|
||||
from jackify.backend.handlers.config_handler import ConfigHandler
|
||||
from jackify.backend.handlers.wine_utils import WineUtils
|
||||
|
||||
# Check for Lorerim-specific Proton override first
|
||||
modlist_normalized = modlist_name.lower().replace(" ", "") if modlist_name else ""
|
||||
if modlist_normalized == '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_normalized == 'lostlegacy':
|
||||
lostlegacy_proton = self._get_lorerim_preferred_proton() # Use same logic as Lorerim
|
||||
if lostlegacy_proton:
|
||||
logger.info(f"Lost Legacy detected: Using {lostlegacy_proton} instead of user settings (ENB compatibility)")
|
||||
self._store_proton_override_notification("Lost Legacy", lostlegacy_proton)
|
||||
return lostlegacy_proton
|
||||
|
||||
config_handler = ConfigHandler()
|
||||
user_proton_path = config_handler.get_proton_path()
|
||||
user_proton_path = config_handler.get_game_proton_path()
|
||||
|
||||
if user_proton_path == 'auto':
|
||||
# Use enhanced fallback logic with GE-Proton preference
|
||||
@@ -125,8 +146,8 @@ class AutomatedPrefixService:
|
||||
logger.warning(f"Could not generate STEAM_COMPAT_MOUNTS, using default: {e}")
|
||||
launch_options = "%command%"
|
||||
|
||||
# Get user's preferred Proton version
|
||||
proton_version = self._get_user_proton_version()
|
||||
# Get user's preferred Proton version (with Lorerim-specific override)
|
||||
proton_version = self._get_user_proton_version(shortcut_name)
|
||||
|
||||
# Create shortcut with Proton using native service
|
||||
success, app_id = steam_service.create_shortcut_with_proton(
|
||||
@@ -487,7 +508,7 @@ exit"""
|
||||
try:
|
||||
# Use the existing protontricks handler
|
||||
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')
|
||||
|
||||
if result.returncode == 0:
|
||||
@@ -1556,6 +1577,9 @@ echo Prefix creation complete.
|
||||
|
||||
if progress_callback:
|
||||
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")
|
||||
return True, prefix_path, actual_appid
|
||||
|
||||
@@ -1869,6 +1893,11 @@ echo Prefix creation complete.
|
||||
if progress_callback:
|
||||
progress_callback(f"{last_timestamp} 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("") # And one more to create space before Prefix Configuration
|
||||
|
||||
@@ -2668,9 +2697,18 @@ echo Prefix creation complete.
|
||||
# Run proton run wineboot -u to initialize the prefix
|
||||
cmd = [str(proton_path), 'run', 'wineboot', '-u']
|
||||
logger.info(f"Running: {' '.join(cmd)}")
|
||||
|
||||
|
||||
# Adjust timeout for SD card installations on Steam Deck (slower I/O)
|
||||
from ..services.platform_detection_service import PlatformDetectionService
|
||||
platform_service = PlatformDetectionService.get_instance()
|
||||
is_steamdeck_sdcard = (platform_service.is_steamdeck and
|
||||
str(proton_path).startswith('/run/media/'))
|
||||
timeout = 180 if is_steamdeck_sdcard else 60
|
||||
if is_steamdeck_sdcard:
|
||||
logger.info(f"Using extended timeout ({timeout}s) for Steam Deck SD card Proton installation")
|
||||
|
||||
# Use jackify-engine's approach: UseShellExecute=false, CreateNoWindow=true equivalent
|
||||
result = subprocess.run(cmd, env=env, capture_output=True, text=True, timeout=60,
|
||||
result = subprocess.run(cmd, env=env, capture_output=True, text=True, timeout=timeout,
|
||||
shell=False, creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0))
|
||||
logger.info(f"Proton exit code: {result.returncode}")
|
||||
|
||||
@@ -2705,7 +2743,7 @@ echo Prefix creation complete.
|
||||
from jackify.backend.handlers.wine_utils import WineUtils
|
||||
|
||||
config = ConfigHandler()
|
||||
user_proton_path = config.get_proton_path()
|
||||
user_proton_path = config.get_game_proton_path()
|
||||
|
||||
# If user selected a specific Proton, try that first
|
||||
if user_proton_path != 'auto':
|
||||
@@ -2952,14 +2990,115 @@ echo Prefix creation complete.
|
||||
logger.error(f"Failed to update registry path: {e}")
|
||||
return False
|
||||
|
||||
def _apply_universal_dotnet_fixes(self, modlist_compatdata_path: str):
|
||||
"""Apply universal dotnet4.x compatibility registry fixes to ALL modlists"""
|
||||
try:
|
||||
prefix_path = os.path.join(modlist_compatdata_path, "pfx")
|
||||
if not os.path.exists(prefix_path):
|
||||
logger.warning(f"Prefix path not found: {prefix_path}")
|
||||
return False
|
||||
|
||||
logger.info("Applying universal dotnet4.x compatibility registry fixes...")
|
||||
|
||||
# Find the appropriate Wine binary to use for registry operations
|
||||
wine_binary = self._find_wine_binary_for_registry(modlist_compatdata_path)
|
||||
if not wine_binary:
|
||||
logger.error("Could not find Wine binary for registry operations")
|
||||
return False
|
||||
|
||||
# Set environment for Wine registry operations
|
||||
env = os.environ.copy()
|
||||
env['WINEPREFIX'] = prefix_path
|
||||
env['WINEDEBUG'] = '-all' # Suppress Wine debug output
|
||||
|
||||
# Registry fix 1: Set mscoree=native DLL override
|
||||
# This tells Wine to use native .NET runtime instead of Wine's implementation
|
||||
logger.debug("Setting mscoree=native DLL override...")
|
||||
cmd1 = [
|
||||
wine_binary, 'reg', 'add',
|
||||
'HKEY_CURRENT_USER\\Software\\Wine\\DllOverrides',
|
||||
'/v', 'mscoree', '/t', 'REG_SZ', '/d', 'native', '/f'
|
||||
]
|
||||
|
||||
result1 = subprocess.run(cmd1, env=env, capture_output=True, text=True)
|
||||
if result1.returncode == 0:
|
||||
logger.info("Successfully applied mscoree=native DLL override")
|
||||
else:
|
||||
logger.warning(f"Failed to set mscoree DLL override: {result1.stderr}")
|
||||
|
||||
# Registry fix 2: Set OnlyUseLatestCLR=1
|
||||
# This prevents .NET version conflicts by using the latest CLR
|
||||
logger.debug("Setting OnlyUseLatestCLR=1 registry entry...")
|
||||
cmd2 = [
|
||||
wine_binary, 'reg', 'add',
|
||||
'HKEY_LOCAL_MACHINE\\Software\\Microsoft\\.NETFramework',
|
||||
'/v', 'OnlyUseLatestCLR', '/t', 'REG_DWORD', '/d', '1', '/f'
|
||||
]
|
||||
|
||||
result2 = subprocess.run(cmd2, env=env, capture_output=True, text=True)
|
||||
if result2.returncode == 0:
|
||||
logger.info("Successfully applied OnlyUseLatestCLR=1 registry entry")
|
||||
else:
|
||||
logger.warning(f"Failed to set OnlyUseLatestCLR: {result2.stderr}")
|
||||
|
||||
# Both fixes applied - this should eliminate dotnet4.x installation requirements
|
||||
if result1.returncode == 0 and result2.returncode == 0:
|
||||
logger.info("Universal dotnet4.x compatibility fixes applied successfully")
|
||||
return True
|
||||
else:
|
||||
logger.warning("Some dotnet4.x registry fixes failed, but continuing...")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to apply universal dotnet4.x fixes: {e}")
|
||||
return False
|
||||
|
||||
def _find_wine_binary_for_registry(self, modlist_compatdata_path: str) -> Optional[str]:
|
||||
"""Find the appropriate Wine binary for registry operations"""
|
||||
try:
|
||||
# Method 1: Try to detect from Steam's config or use Proton from compat data
|
||||
# Look for wine binary in common Proton locations
|
||||
proton_paths = [
|
||||
os.path.expanduser("~/.local/share/Steam/compatibilitytools.d"),
|
||||
os.path.expanduser("~/.steam/steam/steamapps/common")
|
||||
]
|
||||
|
||||
for base_path in proton_paths:
|
||||
if os.path.exists(base_path):
|
||||
for item in os.listdir(base_path):
|
||||
if 'proton' in item.lower():
|
||||
wine_path = os.path.join(base_path, item, 'files', 'bin', 'wine')
|
||||
if os.path.exists(wine_path):
|
||||
logger.debug(f"Found Wine binary: {wine_path}")
|
||||
return wine_path
|
||||
|
||||
# Method 2: Fallback to system wine if available
|
||||
try:
|
||||
result = subprocess.run(['which', 'wine'], capture_output=True, text=True)
|
||||
if result.returncode == 0:
|
||||
wine_path = result.stdout.strip()
|
||||
logger.debug(f"Using system Wine binary: {wine_path}")
|
||||
return wine_path
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
logger.error("No suitable Wine binary found for registry operations")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error finding Wine binary: {e}")
|
||||
return None
|
||||
|
||||
def _inject_game_registry_entries(self, modlist_compatdata_path: str):
|
||||
"""Detect and inject FNV/Enderal game paths into modlist's system.reg"""
|
||||
"""Detect and inject FNV/Enderal game paths and apply universal dotnet4.x compatibility fixes"""
|
||||
system_reg_path = os.path.join(modlist_compatdata_path, "pfx", "system.reg")
|
||||
if not os.path.exists(system_reg_path):
|
||||
logger.warning("system.reg not found, skipping game path injection")
|
||||
return
|
||||
|
||||
logger.info("Detecting and injecting game registry entries...")
|
||||
|
||||
logger.info("Detecting game registry entries...")
|
||||
|
||||
# NOTE: Universal dotnet4.x registry fixes now applied in modlist_handler.py after .reg downloads
|
||||
|
||||
# Game configurations
|
||||
games_config = {
|
||||
@@ -3005,4 +3144,92 @@ echo Prefix creation complete.
|
||||
logger.debug(f"{config['name']} not found in Steam libraries")
|
||||
|
||||
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}")
|
||||
|
||||
|
||||
|
||||
@@ -34,8 +34,10 @@ class ModlistService:
|
||||
"""Lazy initialization of modlist handler."""
|
||||
if self._modlist_handler is None:
|
||||
from ..handlers.modlist_handler import ModlistHandler
|
||||
# Initialize with proper dependencies
|
||||
self._modlist_handler = ModlistHandler()
|
||||
from ..services.platform_detection_service import PlatformDetectionService
|
||||
# Initialize with proper dependencies and centralized Steam Deck detection
|
||||
platform_service = PlatformDetectionService.get_instance()
|
||||
self._modlist_handler = ModlistHandler(steamdeck=platform_service.is_steamdeck)
|
||||
return self._modlist_handler
|
||||
|
||||
def _get_wabbajack_handler(self):
|
||||
@@ -629,8 +631,13 @@ class ModlistService:
|
||||
'mo2_exe_path': str(context.install_dir / 'ModOrganizer.exe'),
|
||||
'resolution': getattr(context, 'resolution', None),
|
||||
'skip_confirmation': True, # Service layer should be non-interactive
|
||||
'manual_steps_completed': False
|
||||
'manual_steps_completed': False,
|
||||
'appid': getattr(context, 'app_id', None) # Fix: Include appid like other configuration paths
|
||||
}
|
||||
|
||||
# DEBUG: Log what resolution we're passing
|
||||
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
|
||||
success = modlist_menu.run_modlist_configuration_phase(config_context)
|
||||
|
||||
@@ -30,100 +30,97 @@ class NativeSteamService:
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.steam_path = Path.home() / ".steam" / "steam"
|
||||
self.userdata_path = self.steam_path / "userdata"
|
||||
self.steam_paths = [
|
||||
Path.home() / ".steam" / "steam",
|
||||
Path.home() / ".local" / "share" / "Steam",
|
||||
Path.home() / ".var" / "app" / "com.valvesoftware.Steam" / "data" / "Steam",
|
||||
Path.home() / ".var" / "app" / "com.valvesoftware.Steam" / ".local" / "share" / "Steam",
|
||||
Path.home() / ".var" / "app" / "com.valvesoftware.Steam" / "home" / ".local" / "share" / "Steam"
|
||||
]
|
||||
self.steam_path = None
|
||||
self.userdata_path = None
|
||||
self.user_id = None
|
||||
self.user_config_path = None
|
||||
|
||||
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:
|
||||
if not self.userdata_path.exists():
|
||||
logger.error("Steam userdata directory not found")
|
||||
# Step 1: Find Steam installation using Steam's own file structure
|
||||
if not self._find_steam_installation():
|
||||
logger.error("No Steam installation found")
|
||||
return False
|
||||
|
||||
# Find user directories (excluding user 0 which is a system account)
|
||||
user_dirs = [d for d in self.userdata_path.iterdir() if d.is_dir() and d.name.isdigit() and d.name != "0"]
|
||||
if not user_dirs:
|
||||
logger.error("No valid Steam user directories found (user 0 is not valid for shortcuts)")
|
||||
# Step 2: Parse loginusers.vdf to get the most recent user (SteamID64)
|
||||
steamid64 = self._get_most_recent_user_from_loginusers()
|
||||
if not steamid64:
|
||||
logger.warning("Could not determine most recent Steam user from loginusers.vdf, trying fallback method")
|
||||
# Fallback: Look for existing user directories in userdata
|
||||
steamid3 = self._find_user_from_userdata_directory()
|
||||
if steamid3:
|
||||
logger.info(f"Found Steam user using userdata directory fallback: SteamID3={steamid3}")
|
||||
# Skip the conversion step since we already have SteamID3
|
||||
self.user_id = str(steamid3)
|
||||
self.user_config_path = self.userdata_path / str(steamid3) / "config"
|
||||
logger.info(f"Steam user set up via fallback: {self.user_id}")
|
||||
logger.info(f"User config path: {self.user_config_path}")
|
||||
return True
|
||||
else:
|
||||
logger.error("Could not determine Steam user using any method")
|
||||
return False
|
||||
|
||||
# Step 3: Convert SteamID64 to SteamID3 (userdata directory format)
|
||||
steamid3 = self._convert_steamid64_to_steamid3(steamid64)
|
||||
logger.info(f"Most recent Steam user: SteamID64={steamid64}, SteamID3={steamid3}")
|
||||
|
||||
# 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
|
||||
|
||||
# Detect the correct Steam user
|
||||
user_dir = self._detect_active_steam_user(user_dirs)
|
||||
if not user_dir:
|
||||
logger.error("Could not determine active Steam user")
|
||||
config_dir = user_dir / "config"
|
||||
if not config_dir.exists():
|
||||
logger.error(f"User config directory does not exist: {config_dir}")
|
||||
return False
|
||||
|
||||
self.user_id = user_dir.name
|
||||
self.user_config_path = user_dir / "config"
|
||||
# Step 5: Set up the service state
|
||||
self.user_id = str(steamid3)
|
||||
self.user_config_path = config_dir
|
||||
|
||||
logger.info(f"Found Steam user: {self.user_id}")
|
||||
logger.info(f"VERIFIED Steam user: {self.user_id}")
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
Parse loginusers.vdf to get the SteamID64 of the most recent user.
|
||||
Uses Steam's own MostRecent flag and Timestamp.
|
||||
"""
|
||||
try:
|
||||
if not loginusers_path.exists():
|
||||
logger.debug(f"loginusers.vdf not found at {loginusers_path}")
|
||||
return None
|
||||
loginusers_path = self.steam_path / "config" / "loginusers.vdf"
|
||||
|
||||
# Load VDF data
|
||||
vdf_data = VDFHandler.load(str(loginusers_path), binary=False)
|
||||
@@ -133,27 +130,52 @@ class NativeSteamService:
|
||||
|
||||
users_section = vdf_data.get("users", {})
|
||||
if not users_section:
|
||||
logger.debug("No users section found in loginusers.vdf")
|
||||
logger.error("No users section found in loginusers.vdf")
|
||||
return None
|
||||
|
||||
# Find user marked as logged in
|
||||
for user_id, user_data in users_section.items():
|
||||
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
|
||||
most_recent_user = None
|
||||
most_recent_timestamp = 0
|
||||
|
||||
# If no specific user found, try to get the first valid user
|
||||
if users_section:
|
||||
first_user = next(iter(users_section.keys()))
|
||||
logger.debug(f"No specific logged-in user found, using first user: {first_user}")
|
||||
return first_user
|
||||
# Find user with MostRecent=1 or highest timestamp
|
||||
for steamid64, user_data in users_section.items():
|
||||
if isinstance(user_data, dict):
|
||||
# Check for MostRecent flag first
|
||||
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:
|
||||
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]:
|
||||
"""Get the path to shortcuts.vdf"""
|
||||
@@ -337,16 +359,22 @@ class NativeSteamService:
|
||||
def set_proton_version(self, app_id: int, proton_version: str = "proton_experimental") -> bool:
|
||||
"""
|
||||
Set the Proton version for a specific app using ONLY config.vdf like steam-conductor does.
|
||||
|
||||
|
||||
Args:
|
||||
app_id: The unsigned AppID
|
||||
app_id: The unsigned AppID
|
||||
proton_version: The Proton version to set
|
||||
|
||||
|
||||
Returns:
|
||||
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")
|
||||
|
||||
|
||||
try:
|
||||
# Step 1: Write to the main config.vdf for CompatToolMapping
|
||||
config_path = self.steam_path / "config" / "config.vdf"
|
||||
@@ -368,8 +396,27 @@ class NativeSteamService:
|
||||
# Find the CompatToolMapping section
|
||||
compat_start = config_text.find('"CompatToolMapping"')
|
||||
if compat_start == -1:
|
||||
logger.error("CompatToolMapping section not found in config.vdf")
|
||||
return False
|
||||
logger.warning("CompatToolMapping section not found in config.vdf, creating it")
|
||||
# Find the Steam section to add CompatToolMapping to
|
||||
steam_section = config_text.find('"Steam"')
|
||||
if steam_section == -1:
|
||||
logger.error("Steam section not found in config.vdf")
|
||||
return False
|
||||
|
||||
# Find the opening brace for Steam section
|
||||
steam_brace = config_text.find('{', steam_section)
|
||||
if steam_brace == -1:
|
||||
logger.error("Steam section opening brace not found")
|
||||
return False
|
||||
|
||||
# Insert CompatToolMapping section right after Steam opening brace
|
||||
insert_pos = steam_brace + 1
|
||||
compat_section = '\n\t\t"CompatToolMapping"\n\t\t{\n\t\t}\n'
|
||||
config_text = config_text[:insert_pos] + compat_section + config_text[insert_pos:]
|
||||
|
||||
# Update compat_start position after insertion
|
||||
compat_start = config_text.find('"CompatToolMapping"')
|
||||
logger.info("Created CompatToolMapping section in config.vdf")
|
||||
|
||||
# Find the closing brace for CompatToolMapping
|
||||
# Look for the opening brace after CompatToolMapping
|
||||
|
||||
67
jackify/backend/services/platform_detection_service.py
Normal file
67
jackify/backend/services/platform_detection_service.py
Normal file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Platform Detection Service
|
||||
|
||||
Centralizes platform detection logic (Steam Deck, etc.) to be performed once at application startup
|
||||
and shared across all components.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PlatformDetectionService:
|
||||
"""
|
||||
Service for detecting platform-specific information once at startup
|
||||
"""
|
||||
|
||||
_instance = None
|
||||
_is_steamdeck = None
|
||||
|
||||
def __new__(cls):
|
||||
"""Singleton pattern to ensure only one instance"""
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize platform detection if not already done"""
|
||||
if self._is_steamdeck is None:
|
||||
self._detect_platform()
|
||||
|
||||
def _detect_platform(self):
|
||||
"""Perform platform detection once"""
|
||||
logger.debug("Performing platform detection...")
|
||||
|
||||
# Steam Deck detection
|
||||
self._is_steamdeck = False
|
||||
try:
|
||||
if os.path.exists('/etc/os-release'):
|
||||
with open('/etc/os-release', 'r') as f:
|
||||
content = f.read().lower()
|
||||
if 'steamdeck' in content:
|
||||
self._is_steamdeck = True
|
||||
logger.info("Steam Deck platform detected")
|
||||
else:
|
||||
logger.debug("Non-Steam Deck Linux platform detected")
|
||||
else:
|
||||
logger.debug("No /etc/os-release found - assuming non-Steam Deck platform")
|
||||
except Exception as e:
|
||||
logger.warning(f"Error detecting Steam Deck platform: {e}")
|
||||
self._is_steamdeck = False
|
||||
|
||||
logger.debug(f"Platform detection complete: is_steamdeck={self._is_steamdeck}")
|
||||
|
||||
@property
|
||||
def is_steamdeck(self) -> bool:
|
||||
"""Get Steam Deck detection result"""
|
||||
if self._is_steamdeck is None:
|
||||
self._detect_platform()
|
||||
return self._is_steamdeck
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
"""Get the singleton instance"""
|
||||
return cls()
|
||||
@@ -39,7 +39,7 @@ class ProtontricksDetectionService:
|
||||
def _get_protontricks_handler(self) -> ProtontricksHandler:
|
||||
"""Get or create ProtontricksHandler instance"""
|
||||
if self._protontricks_handler is None:
|
||||
self._protontricks_handler = ProtontricksHandler(steamdeck=self.steamdeck)
|
||||
self._protontricks_handler = ProtontricksHandler(self.steamdeck)
|
||||
return self._protontricks_handler
|
||||
|
||||
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -7,7 +7,7 @@
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v8.0": {},
|
||||
".NETCoreApp,Version=v8.0/linux-x64": {
|
||||
"jackify-engine/0.3.16": {
|
||||
"jackify-engine/0.3.17": {
|
||||
"dependencies": {
|
||||
"Markdig": "0.40.0",
|
||||
"Microsoft.Extensions.Configuration.Json": "9.0.1",
|
||||
@@ -22,16 +22,16 @@
|
||||
"SixLabors.ImageSharp": "3.1.6",
|
||||
"System.CommandLine": "2.0.0-beta4.22272.1",
|
||||
"System.CommandLine.NamingConventionBinder": "2.0.0-beta4.22272.1",
|
||||
"Wabbajack.CLI.Builder": "0.3.16",
|
||||
"Wabbajack.Downloaders.Bethesda": "0.3.16",
|
||||
"Wabbajack.Downloaders.Dispatcher": "0.3.16",
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.16",
|
||||
"Wabbajack.Networking.Discord": "0.3.16",
|
||||
"Wabbajack.Networking.GitHub": "0.3.16",
|
||||
"Wabbajack.Paths.IO": "0.3.16",
|
||||
"Wabbajack.Server.Lib": "0.3.16",
|
||||
"Wabbajack.Services.OSIntegrated": "0.3.16",
|
||||
"Wabbajack.VFS": "0.3.16",
|
||||
"Wabbajack.CLI.Builder": "0.3.17",
|
||||
"Wabbajack.Downloaders.Bethesda": "0.3.17",
|
||||
"Wabbajack.Downloaders.Dispatcher": "0.3.17",
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.17",
|
||||
"Wabbajack.Networking.Discord": "0.3.17",
|
||||
"Wabbajack.Networking.GitHub": "0.3.17",
|
||||
"Wabbajack.Paths.IO": "0.3.17",
|
||||
"Wabbajack.Server.Lib": "0.3.17",
|
||||
"Wabbajack.Services.OSIntegrated": "0.3.17",
|
||||
"Wabbajack.VFS": "0.3.17",
|
||||
"MegaApiClient": "1.0.0.0",
|
||||
"runtimepack.Microsoft.NETCore.App.Runtime.linux-x64": "8.0.19"
|
||||
},
|
||||
@@ -1781,7 +1781,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Wabbajack.CLI.Builder/0.3.16": {
|
||||
"Wabbajack.CLI.Builder/0.3.17": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Json": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
@@ -1791,109 +1791,109 @@
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"System.CommandLine": "2.0.0-beta4.22272.1",
|
||||
"System.CommandLine.NamingConventionBinder": "2.0.0-beta4.22272.1",
|
||||
"Wabbajack.Paths": "0.3.16"
|
||||
"Wabbajack.Paths": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.CLI.Builder.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Common/0.3.16": {
|
||||
"Wabbajack.Common/0.3.17": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"System.Reactive": "6.0.1",
|
||||
"Wabbajack.DTOs": "0.3.16",
|
||||
"Wabbajack.Networking.Http": "0.3.16",
|
||||
"Wabbajack.Paths.IO": "0.3.16"
|
||||
"Wabbajack.DTOs": "0.3.17",
|
||||
"Wabbajack.Networking.Http": "0.3.17",
|
||||
"Wabbajack.Paths.IO": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Common.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Compiler/0.3.16": {
|
||||
"Wabbajack.Compiler/0.3.17": {
|
||||
"dependencies": {
|
||||
"F23.StringSimilarity": "6.0.0",
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Newtonsoft.Json": "13.0.3",
|
||||
"SixLabors.ImageSharp": "3.1.6",
|
||||
"Wabbajack.Downloaders.Dispatcher": "0.3.16",
|
||||
"Wabbajack.Installer": "0.3.16",
|
||||
"Wabbajack.VFS": "0.3.16",
|
||||
"Wabbajack.Downloaders.Dispatcher": "0.3.17",
|
||||
"Wabbajack.Installer": "0.3.17",
|
||||
"Wabbajack.VFS": "0.3.17",
|
||||
"ini-parser-netstandard": "2.5.2"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Compiler.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Compression.BSA/0.3.16": {
|
||||
"Wabbajack.Compression.BSA/0.3.17": {
|
||||
"dependencies": {
|
||||
"K4os.Compression.LZ4.Streams": "1.3.8",
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"SharpZipLib": "1.4.2",
|
||||
"Wabbajack.Common": "0.3.16",
|
||||
"Wabbajack.DTOs": "0.3.16"
|
||||
"Wabbajack.Common": "0.3.17",
|
||||
"Wabbajack.DTOs": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Compression.BSA.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Compression.Zip/0.3.16": {
|
||||
"Wabbajack.Compression.Zip/0.3.17": {
|
||||
"dependencies": {
|
||||
"Wabbajack.IO.Async": "0.3.16"
|
||||
"Wabbajack.IO.Async": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Compression.Zip.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Configuration/0.3.16": {
|
||||
"Wabbajack.Configuration/0.3.17": {
|
||||
"runtime": {
|
||||
"Wabbajack.Configuration.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.Bethesda/0.3.16": {
|
||||
"Wabbajack.Downloaders.Bethesda/0.3.17": {
|
||||
"dependencies": {
|
||||
"LibAES-CTR": "1.1.0",
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"SharpZipLib": "1.4.2",
|
||||
"Wabbajack.Common": "0.3.16",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.16",
|
||||
"Wabbajack.Networking.BethesdaNet": "0.3.16"
|
||||
"Wabbajack.Common": "0.3.17",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.17",
|
||||
"Wabbajack.Networking.BethesdaNet": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.Bethesda.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.Dispatcher/0.3.16": {
|
||||
"Wabbajack.Downloaders.Dispatcher/0.3.17": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"Newtonsoft.Json": "13.0.3",
|
||||
"SixLabors.ImageSharp": "3.1.6",
|
||||
"Wabbajack.Downloaders.Bethesda": "0.3.16",
|
||||
"Wabbajack.Downloaders.GameFile": "0.3.16",
|
||||
"Wabbajack.Downloaders.GoogleDrive": "0.3.16",
|
||||
"Wabbajack.Downloaders.Http": "0.3.16",
|
||||
"Wabbajack.Downloaders.IPS4OAuth2Downloader": "0.3.16",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.16",
|
||||
"Wabbajack.Downloaders.Manual": "0.3.16",
|
||||
"Wabbajack.Downloaders.MediaFire": "0.3.16",
|
||||
"Wabbajack.Downloaders.Mega": "0.3.16",
|
||||
"Wabbajack.Downloaders.ModDB": "0.3.16",
|
||||
"Wabbajack.Downloaders.Nexus": "0.3.16",
|
||||
"Wabbajack.Downloaders.VerificationCache": "0.3.16",
|
||||
"Wabbajack.Downloaders.WabbajackCDN": "0.3.16",
|
||||
"Wabbajack.Networking.WabbajackClientApi": "0.3.16"
|
||||
"Wabbajack.Downloaders.Bethesda": "0.3.17",
|
||||
"Wabbajack.Downloaders.GameFile": "0.3.17",
|
||||
"Wabbajack.Downloaders.GoogleDrive": "0.3.17",
|
||||
"Wabbajack.Downloaders.Http": "0.3.17",
|
||||
"Wabbajack.Downloaders.IPS4OAuth2Downloader": "0.3.17",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.17",
|
||||
"Wabbajack.Downloaders.Manual": "0.3.17",
|
||||
"Wabbajack.Downloaders.MediaFire": "0.3.17",
|
||||
"Wabbajack.Downloaders.Mega": "0.3.17",
|
||||
"Wabbajack.Downloaders.ModDB": "0.3.17",
|
||||
"Wabbajack.Downloaders.Nexus": "0.3.17",
|
||||
"Wabbajack.Downloaders.VerificationCache": "0.3.17",
|
||||
"Wabbajack.Downloaders.WabbajackCDN": "0.3.17",
|
||||
"Wabbajack.Networking.WabbajackClientApi": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.Dispatcher.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.GameFile/0.3.16": {
|
||||
"Wabbajack.Downloaders.GameFile/0.3.17": {
|
||||
"dependencies": {
|
||||
"GameFinder.StoreHandlers.EADesktop": "4.5.0",
|
||||
"GameFinder.StoreHandlers.EGS": "4.5.0",
|
||||
@@ -1903,360 +1903,360 @@
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"SixLabors.ImageSharp": "3.1.6",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.16",
|
||||
"Wabbajack.VFS": "0.3.16"
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.17",
|
||||
"Wabbajack.VFS": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.GameFile.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.GoogleDrive/0.3.16": {
|
||||
"Wabbajack.Downloaders.GoogleDrive/0.3.17": {
|
||||
"dependencies": {
|
||||
"HtmlAgilityPack": "1.11.72",
|
||||
"Microsoft.AspNetCore.Http.Extensions": "2.3.0",
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"Wabbajack.Common": "0.3.16",
|
||||
"Wabbajack.DTOs": "0.3.16",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.16",
|
||||
"Wabbajack.Networking.Http": "0.3.16",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.16"
|
||||
"Wabbajack.Common": "0.3.17",
|
||||
"Wabbajack.DTOs": "0.3.17",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.17",
|
||||
"Wabbajack.Networking.Http": "0.3.17",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.GoogleDrive.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.Http/0.3.16": {
|
||||
"Wabbajack.Downloaders.Http/0.3.17": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"Wabbajack.Common": "0.3.16",
|
||||
"Wabbajack.DTOs": "0.3.16",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.16",
|
||||
"Wabbajack.Networking.BethesdaNet": "0.3.16",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.16",
|
||||
"Wabbajack.Paths.IO": "0.3.16"
|
||||
"Wabbajack.Common": "0.3.17",
|
||||
"Wabbajack.DTOs": "0.3.17",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.17",
|
||||
"Wabbajack.Networking.BethesdaNet": "0.3.17",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.17",
|
||||
"Wabbajack.Paths.IO": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.Http.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.Interfaces/0.3.16": {
|
||||
"Wabbajack.Downloaders.Interfaces/0.3.17": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Wabbajack.Compression.Zip": "0.3.16",
|
||||
"Wabbajack.DTOs": "0.3.16",
|
||||
"Wabbajack.Paths.IO": "0.3.16"
|
||||
"Wabbajack.Compression.Zip": "0.3.17",
|
||||
"Wabbajack.DTOs": "0.3.17",
|
||||
"Wabbajack.Paths.IO": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.Interfaces.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.16": {
|
||||
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.17": {
|
||||
"dependencies": {
|
||||
"F23.StringSimilarity": "6.0.0",
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"Wabbajack.Common": "0.3.16",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.16",
|
||||
"Wabbajack.Networking.Http": "0.3.16",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.16"
|
||||
"Wabbajack.Common": "0.3.17",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.17",
|
||||
"Wabbajack.Networking.Http": "0.3.17",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.IPS4OAuth2Downloader.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.Manual/0.3.16": {
|
||||
"Wabbajack.Downloaders.Manual/0.3.17": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"Wabbajack.Common": "0.3.16",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.16"
|
||||
"Wabbajack.Common": "0.3.17",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.Manual.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.MediaFire/0.3.16": {
|
||||
"Wabbajack.Downloaders.MediaFire/0.3.17": {
|
||||
"dependencies": {
|
||||
"HtmlAgilityPack": "1.11.72",
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"Wabbajack.Common": "0.3.16",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.16",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.16"
|
||||
"Wabbajack.Common": "0.3.17",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.17",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.MediaFire.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.Mega/0.3.16": {
|
||||
"Wabbajack.Downloaders.Mega/0.3.17": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"Newtonsoft.Json": "13.0.3",
|
||||
"Wabbajack.Common": "0.3.16",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.16",
|
||||
"Wabbajack.Paths.IO": "0.3.16"
|
||||
"Wabbajack.Common": "0.3.17",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.17",
|
||||
"Wabbajack.Paths.IO": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.Mega.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.ModDB/0.3.16": {
|
||||
"Wabbajack.Downloaders.ModDB/0.3.17": {
|
||||
"dependencies": {
|
||||
"HtmlAgilityPack": "1.11.72",
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"Newtonsoft.Json": "13.0.3",
|
||||
"Wabbajack.Common": "0.3.16",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.16",
|
||||
"Wabbajack.Networking.Http": "0.3.16",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.16"
|
||||
"Wabbajack.Common": "0.3.17",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.17",
|
||||
"Wabbajack.Networking.Http": "0.3.17",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.ModDB.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.Nexus/0.3.16": {
|
||||
"Wabbajack.Downloaders.Nexus/0.3.17": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Wabbajack.DTOs": "0.3.16",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.16",
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.16",
|
||||
"Wabbajack.Networking.Http": "0.3.16",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.16",
|
||||
"Wabbajack.Networking.NexusApi": "0.3.16",
|
||||
"Wabbajack.Paths": "0.3.16"
|
||||
"Wabbajack.DTOs": "0.3.17",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.17",
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.17",
|
||||
"Wabbajack.Networking.Http": "0.3.17",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.17",
|
||||
"Wabbajack.Networking.NexusApi": "0.3.17",
|
||||
"Wabbajack.Paths": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.Nexus.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.VerificationCache/0.3.16": {
|
||||
"Wabbajack.Downloaders.VerificationCache/0.3.17": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"Stub.System.Data.SQLite.Core.NetStandard": "1.0.119",
|
||||
"Wabbajack.DTOs": "0.3.16",
|
||||
"Wabbajack.Paths.IO": "0.3.16"
|
||||
"Wabbajack.DTOs": "0.3.17",
|
||||
"Wabbajack.Paths.IO": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.VerificationCache.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.WabbajackCDN/0.3.16": {
|
||||
"Wabbajack.Downloaders.WabbajackCDN/0.3.17": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"Microsoft.Toolkit.HighPerformance": "7.1.2",
|
||||
"Wabbajack.Common": "0.3.16",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.16",
|
||||
"Wabbajack.Networking.Http": "0.3.16",
|
||||
"Wabbajack.RateLimiter": "0.3.16"
|
||||
"Wabbajack.Common": "0.3.17",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.17",
|
||||
"Wabbajack.Networking.Http": "0.3.17",
|
||||
"Wabbajack.RateLimiter": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.WabbajackCDN.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.DTOs/0.3.16": {
|
||||
"Wabbajack.DTOs/0.3.17": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.16",
|
||||
"Wabbajack.Paths": "0.3.16"
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.17",
|
||||
"Wabbajack.Paths": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.DTOs.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.FileExtractor/0.3.16": {
|
||||
"Wabbajack.FileExtractor/0.3.17": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"OMODFramework": "3.0.1",
|
||||
"Wabbajack.Common": "0.3.16",
|
||||
"Wabbajack.Compression.BSA": "0.3.16",
|
||||
"Wabbajack.Hashing.PHash": "0.3.16",
|
||||
"Wabbajack.Paths": "0.3.16"
|
||||
"Wabbajack.Common": "0.3.17",
|
||||
"Wabbajack.Compression.BSA": "0.3.17",
|
||||
"Wabbajack.Hashing.PHash": "0.3.17",
|
||||
"Wabbajack.Paths": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.FileExtractor.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Hashing.PHash/0.3.16": {
|
||||
"Wabbajack.Hashing.PHash/0.3.17": {
|
||||
"dependencies": {
|
||||
"BCnEncoder.Net.ImageSharp": "1.1.1",
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Shipwreck.Phash": "0.5.0",
|
||||
"SixLabors.ImageSharp": "3.1.6",
|
||||
"Wabbajack.Common": "0.3.16",
|
||||
"Wabbajack.DTOs": "0.3.16",
|
||||
"Wabbajack.Paths": "0.3.16",
|
||||
"Wabbajack.Paths.IO": "0.3.16"
|
||||
"Wabbajack.Common": "0.3.17",
|
||||
"Wabbajack.DTOs": "0.3.17",
|
||||
"Wabbajack.Paths": "0.3.17",
|
||||
"Wabbajack.Paths.IO": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Hashing.PHash.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Hashing.xxHash64/0.3.16": {
|
||||
"Wabbajack.Hashing.xxHash64/0.3.17": {
|
||||
"dependencies": {
|
||||
"Wabbajack.Paths": "0.3.16",
|
||||
"Wabbajack.RateLimiter": "0.3.16"
|
||||
"Wabbajack.Paths": "0.3.17",
|
||||
"Wabbajack.RateLimiter": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Hashing.xxHash64.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Installer/0.3.16": {
|
||||
"Wabbajack.Installer/0.3.17": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Newtonsoft.Json": "13.0.3",
|
||||
"Octopus.Octodiff": "2.0.548",
|
||||
"SixLabors.ImageSharp": "3.1.6",
|
||||
"Wabbajack.DTOs": "0.3.16",
|
||||
"Wabbajack.Downloaders.Dispatcher": "0.3.16",
|
||||
"Wabbajack.Downloaders.GameFile": "0.3.16",
|
||||
"Wabbajack.FileExtractor": "0.3.16",
|
||||
"Wabbajack.Networking.WabbajackClientApi": "0.3.16",
|
||||
"Wabbajack.Paths": "0.3.16",
|
||||
"Wabbajack.Paths.IO": "0.3.16",
|
||||
"Wabbajack.VFS": "0.3.16",
|
||||
"Wabbajack.DTOs": "0.3.17",
|
||||
"Wabbajack.Downloaders.Dispatcher": "0.3.17",
|
||||
"Wabbajack.Downloaders.GameFile": "0.3.17",
|
||||
"Wabbajack.FileExtractor": "0.3.17",
|
||||
"Wabbajack.Networking.WabbajackClientApi": "0.3.17",
|
||||
"Wabbajack.Paths": "0.3.17",
|
||||
"Wabbajack.Paths.IO": "0.3.17",
|
||||
"Wabbajack.VFS": "0.3.17",
|
||||
"ini-parser-netstandard": "2.5.2"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Installer.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.IO.Async/0.3.16": {
|
||||
"Wabbajack.IO.Async/0.3.17": {
|
||||
"runtime": {
|
||||
"Wabbajack.IO.Async.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Networking.BethesdaNet/0.3.16": {
|
||||
"Wabbajack.Networking.BethesdaNet/0.3.17": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Wabbajack.DTOs": "0.3.16",
|
||||
"Wabbajack.Networking.Http": "0.3.16",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.16"
|
||||
"Wabbajack.DTOs": "0.3.17",
|
||||
"Wabbajack.Networking.Http": "0.3.17",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Networking.BethesdaNet.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Networking.Discord/0.3.16": {
|
||||
"Wabbajack.Networking.Discord/0.3.17": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.16"
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Networking.Discord.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Networking.GitHub/0.3.16": {
|
||||
"Wabbajack.Networking.GitHub/0.3.17": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"Octokit": "14.0.0",
|
||||
"Wabbajack.DTOs": "0.3.16",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.16"
|
||||
"Wabbajack.DTOs": "0.3.17",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Networking.GitHub.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Networking.Http/0.3.16": {
|
||||
"Wabbajack.Networking.Http/0.3.17": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Http": "9.0.1",
|
||||
"Microsoft.Extensions.Logging": "9.0.1",
|
||||
"Wabbajack.Configuration": "0.3.16",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.16",
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.16",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.16",
|
||||
"Wabbajack.Paths": "0.3.16",
|
||||
"Wabbajack.Paths.IO": "0.3.16"
|
||||
"Wabbajack.Configuration": "0.3.17",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.17",
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.17",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.17",
|
||||
"Wabbajack.Paths": "0.3.17",
|
||||
"Wabbajack.Paths.IO": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Networking.Http.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Networking.Http.Interfaces/0.3.16": {
|
||||
"Wabbajack.Networking.Http.Interfaces/0.3.17": {
|
||||
"dependencies": {
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.16"
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Networking.Http.Interfaces.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Networking.NexusApi/0.3.16": {
|
||||
"Wabbajack.Networking.NexusApi/0.3.17": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"Wabbajack.DTOs": "0.3.16",
|
||||
"Wabbajack.Networking.Http": "0.3.16",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.16",
|
||||
"Wabbajack.Networking.WabbajackClientApi": "0.3.16"
|
||||
"Wabbajack.DTOs": "0.3.17",
|
||||
"Wabbajack.Networking.Http": "0.3.17",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.17",
|
||||
"Wabbajack.Networking.WabbajackClientApi": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Networking.NexusApi.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Networking.WabbajackClientApi/0.3.16": {
|
||||
"Wabbajack.Networking.WabbajackClientApi/0.3.17": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"Octokit": "14.0.0",
|
||||
"Wabbajack.Common": "0.3.16",
|
||||
"Wabbajack.DTOs": "0.3.16",
|
||||
"Wabbajack.Paths.IO": "0.3.16",
|
||||
"Wabbajack.VFS.Interfaces": "0.3.16",
|
||||
"Wabbajack.Common": "0.3.17",
|
||||
"Wabbajack.DTOs": "0.3.17",
|
||||
"Wabbajack.Paths.IO": "0.3.17",
|
||||
"Wabbajack.VFS.Interfaces": "0.3.17",
|
||||
"YamlDotNet": "16.3.0"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Networking.WabbajackClientApi.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Paths/0.3.16": {
|
||||
"Wabbajack.Paths/0.3.17": {
|
||||
"runtime": {
|
||||
"Wabbajack.Paths.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Paths.IO/0.3.16": {
|
||||
"Wabbajack.Paths.IO/0.3.17": {
|
||||
"dependencies": {
|
||||
"Wabbajack.Paths": "0.3.16",
|
||||
"Wabbajack.Paths": "0.3.17",
|
||||
"shortid": "4.0.0"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Paths.IO.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.RateLimiter/0.3.16": {
|
||||
"Wabbajack.RateLimiter/0.3.17": {
|
||||
"runtime": {
|
||||
"Wabbajack.RateLimiter.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Server.Lib/0.3.16": {
|
||||
"Wabbajack.Server.Lib/0.3.17": {
|
||||
"dependencies": {
|
||||
"FluentFTP": "52.0.0",
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
@@ -2264,58 +2264,58 @@
|
||||
"Nettle": "3.0.0",
|
||||
"Newtonsoft.Json": "13.0.3",
|
||||
"SixLabors.ImageSharp": "3.1.6",
|
||||
"Wabbajack.Common": "0.3.16",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.16",
|
||||
"Wabbajack.Services.OSIntegrated": "0.3.16"
|
||||
"Wabbajack.Common": "0.3.17",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.17",
|
||||
"Wabbajack.Services.OSIntegrated": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Server.Lib.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Services.OSIntegrated/0.3.16": {
|
||||
"Wabbajack.Services.OSIntegrated/0.3.17": {
|
||||
"dependencies": {
|
||||
"DeviceId": "6.8.0",
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Newtonsoft.Json": "13.0.3",
|
||||
"SixLabors.ImageSharp": "3.1.6",
|
||||
"Wabbajack.Compiler": "0.3.16",
|
||||
"Wabbajack.Downloaders.Dispatcher": "0.3.16",
|
||||
"Wabbajack.Installer": "0.3.16",
|
||||
"Wabbajack.Networking.BethesdaNet": "0.3.16",
|
||||
"Wabbajack.Networking.Discord": "0.3.16",
|
||||
"Wabbajack.VFS": "0.3.16"
|
||||
"Wabbajack.Compiler": "0.3.17",
|
||||
"Wabbajack.Downloaders.Dispatcher": "0.3.17",
|
||||
"Wabbajack.Installer": "0.3.17",
|
||||
"Wabbajack.Networking.BethesdaNet": "0.3.17",
|
||||
"Wabbajack.Networking.Discord": "0.3.17",
|
||||
"Wabbajack.VFS": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Services.OSIntegrated.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.VFS/0.3.16": {
|
||||
"Wabbajack.VFS/0.3.17": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||
"SixLabors.ImageSharp": "3.1.6",
|
||||
"System.Data.SQLite.Core": "1.0.119",
|
||||
"Wabbajack.Common": "0.3.16",
|
||||
"Wabbajack.FileExtractor": "0.3.16",
|
||||
"Wabbajack.Hashing.PHash": "0.3.16",
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.16",
|
||||
"Wabbajack.Paths": "0.3.16",
|
||||
"Wabbajack.Paths.IO": "0.3.16",
|
||||
"Wabbajack.VFS.Interfaces": "0.3.16"
|
||||
"Wabbajack.Common": "0.3.17",
|
||||
"Wabbajack.FileExtractor": "0.3.17",
|
||||
"Wabbajack.Hashing.PHash": "0.3.17",
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.17",
|
||||
"Wabbajack.Paths": "0.3.17",
|
||||
"Wabbajack.Paths.IO": "0.3.17",
|
||||
"Wabbajack.VFS.Interfaces": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.VFS.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.VFS.Interfaces/0.3.16": {
|
||||
"Wabbajack.VFS.Interfaces/0.3.17": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Wabbajack.DTOs": "0.3.16",
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.16",
|
||||
"Wabbajack.Paths": "0.3.16"
|
||||
"Wabbajack.DTOs": "0.3.17",
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.17",
|
||||
"Wabbajack.Paths": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.VFS.Interfaces.dll": {}
|
||||
@@ -2332,7 +2332,7 @@
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"jackify-engine/0.3.16": {
|
||||
"jackify-engine/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
@@ -3021,202 +3021,202 @@
|
||||
"path": "yamldotnet/16.3.0",
|
||||
"hashPath": "yamldotnet.16.3.0.nupkg.sha512"
|
||||
},
|
||||
"Wabbajack.CLI.Builder/0.3.16": {
|
||||
"Wabbajack.CLI.Builder/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Common/0.3.16": {
|
||||
"Wabbajack.Common/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Compiler/0.3.16": {
|
||||
"Wabbajack.Compiler/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Compression.BSA/0.3.16": {
|
||||
"Wabbajack.Compression.BSA/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Compression.Zip/0.3.16": {
|
||||
"Wabbajack.Compression.Zip/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Configuration/0.3.16": {
|
||||
"Wabbajack.Configuration/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.Bethesda/0.3.16": {
|
||||
"Wabbajack.Downloaders.Bethesda/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.Dispatcher/0.3.16": {
|
||||
"Wabbajack.Downloaders.Dispatcher/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.GameFile/0.3.16": {
|
||||
"Wabbajack.Downloaders.GameFile/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.GoogleDrive/0.3.16": {
|
||||
"Wabbajack.Downloaders.GoogleDrive/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.Http/0.3.16": {
|
||||
"Wabbajack.Downloaders.Http/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.Interfaces/0.3.16": {
|
||||
"Wabbajack.Downloaders.Interfaces/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.16": {
|
||||
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.Manual/0.3.16": {
|
||||
"Wabbajack.Downloaders.Manual/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.MediaFire/0.3.16": {
|
||||
"Wabbajack.Downloaders.MediaFire/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.Mega/0.3.16": {
|
||||
"Wabbajack.Downloaders.Mega/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.ModDB/0.3.16": {
|
||||
"Wabbajack.Downloaders.ModDB/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.Nexus/0.3.16": {
|
||||
"Wabbajack.Downloaders.Nexus/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.VerificationCache/0.3.16": {
|
||||
"Wabbajack.Downloaders.VerificationCache/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.WabbajackCDN/0.3.16": {
|
||||
"Wabbajack.Downloaders.WabbajackCDN/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.DTOs/0.3.16": {
|
||||
"Wabbajack.DTOs/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.FileExtractor/0.3.16": {
|
||||
"Wabbajack.FileExtractor/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Hashing.PHash/0.3.16": {
|
||||
"Wabbajack.Hashing.PHash/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Hashing.xxHash64/0.3.16": {
|
||||
"Wabbajack.Hashing.xxHash64/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Installer/0.3.16": {
|
||||
"Wabbajack.Installer/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.IO.Async/0.3.16": {
|
||||
"Wabbajack.IO.Async/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Networking.BethesdaNet/0.3.16": {
|
||||
"Wabbajack.Networking.BethesdaNet/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Networking.Discord/0.3.16": {
|
||||
"Wabbajack.Networking.Discord/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Networking.GitHub/0.3.16": {
|
||||
"Wabbajack.Networking.GitHub/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Networking.Http/0.3.16": {
|
||||
"Wabbajack.Networking.Http/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Networking.Http.Interfaces/0.3.16": {
|
||||
"Wabbajack.Networking.Http.Interfaces/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Networking.NexusApi/0.3.16": {
|
||||
"Wabbajack.Networking.NexusApi/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Networking.WabbajackClientApi/0.3.16": {
|
||||
"Wabbajack.Networking.WabbajackClientApi/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Paths/0.3.16": {
|
||||
"Wabbajack.Paths/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Paths.IO/0.3.16": {
|
||||
"Wabbajack.Paths.IO/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.RateLimiter/0.3.16": {
|
||||
"Wabbajack.RateLimiter/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Server.Lib/0.3.16": {
|
||||
"Wabbajack.Server.Lib/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Services.OSIntegrated/0.3.16": {
|
||||
"Wabbajack.Services.OSIntegrated/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.VFS/0.3.16": {
|
||||
"Wabbajack.VFS/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.VFS.Interfaces/0.3.16": {
|
||||
"Wabbajack.VFS.Interfaces/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
|
||||
Binary file not shown.
@@ -256,18 +256,24 @@ class UpdateDialog(QDialog):
|
||||
self.downloaded_path = downloaded_path
|
||||
self.progress_label.setText("Download completed successfully!")
|
||||
self.progress_bar.setValue(100)
|
||||
|
||||
# Show install button
|
||||
self.download_button.setVisible(False)
|
||||
self.install_button.setVisible(True)
|
||||
|
||||
# Re-enable other buttons
|
||||
self.later_button.setEnabled(True)
|
||||
self.skip_button.setEnabled(True)
|
||||
|
||||
|
||||
# Check if auto-restart is enabled
|
||||
if self.auto_restart_checkbox.isChecked():
|
||||
# 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
|
||||
self.later_button.setEnabled(True)
|
||||
self.skip_button.setEnabled(True)
|
||||
|
||||
else:
|
||||
self.show_error("Download Failed", "Failed to download the update. Please try again later.")
|
||||
|
||||
|
||||
# Reset UI
|
||||
self.progress_group.setVisible(False)
|
||||
self.download_button.setEnabled(True)
|
||||
|
||||
@@ -303,29 +303,50 @@ class SettingsDialog(QDialog):
|
||||
general_layout.addWidget(api_group)
|
||||
general_layout.addSpacing(12)
|
||||
|
||||
# --- Default Proton Version Section ---
|
||||
proton_group = QGroupBox("Default Proton Version")
|
||||
# --- 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 = QHBoxLayout()
|
||||
proton_layout = QVBoxLayout()
|
||||
proton_group.setLayout(proton_layout)
|
||||
|
||||
self.proton_dropdown = QComboBox()
|
||||
self.proton_dropdown.setToolTip("Select default Proton version for shortcut creation and texture processing")
|
||||
self.proton_dropdown.setMinimumWidth(200)
|
||||
# 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)
|
||||
|
||||
# Populate Proton dropdown
|
||||
self._populate_proton_dropdown()
|
||||
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)
|
||||
|
||||
# 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)
|
||||
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()
|
||||
|
||||
proton_layout.addWidget(QLabel("Proton Version:"))
|
||||
proton_layout.addWidget(self.proton_dropdown)
|
||||
proton_layout.addWidget(refresh_btn)
|
||||
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)
|
||||
@@ -416,6 +437,40 @@ class SettingsDialog(QDialog):
|
||||
self.bandwidth_spin = None # No bandwidth UI if Downloads resource doesn't exist
|
||||
|
||||
advanced_layout.addWidget(resource_group)
|
||||
|
||||
# Advanced Tool Options Section
|
||||
component_group = QGroupBox("Advanced Tool Options")
|
||||
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)
|
||||
|
||||
# Label for the toggle button
|
||||
method_label = QLabel("Wine Components Installation:")
|
||||
component_layout.addWidget(method_label)
|
||||
|
||||
# Toggle button for winetricks/protontricks selection
|
||||
self.component_toggle = QPushButton("Winetricks")
|
||||
self.component_toggle.setCheckable(True)
|
||||
use_winetricks = self.config_handler.get('use_winetricks_for_components', True)
|
||||
self.component_toggle.setChecked(use_winetricks)
|
||||
|
||||
# Function to update button text based on state
|
||||
def update_button_text():
|
||||
if self.component_toggle.isChecked():
|
||||
self.component_toggle.setText("Winetricks")
|
||||
else:
|
||||
self.component_toggle.setText("Protontricks")
|
||||
|
||||
self.component_toggle.toggled.connect(update_button_text)
|
||||
update_button_text() # Set initial text
|
||||
|
||||
self.component_toggle.setToolTip(
|
||||
"Winetricks: Faster, uses bundled tools (Default)\n"
|
||||
"Protontricks: Legacy mode, slower but system-compatible"
|
||||
)
|
||||
component_layout.addWidget(self.component_toggle)
|
||||
|
||||
advanced_layout.addWidget(component_group)
|
||||
advanced_layout.addStretch() # Add stretch to push content to top
|
||||
|
||||
self.tab_widget.addTab(advanced_tab, "Advanced")
|
||||
@@ -488,166 +543,250 @@ class SettingsDialog(QDialog):
|
||||
except:
|
||||
return 'auto'
|
||||
|
||||
def _populate_proton_dropdown(self):
|
||||
"""Populate Proton version dropdown with detected versions (includes GE-Proton and Valve Proton)"""
|
||||
def _populate_install_proton_dropdown(self):
|
||||
"""Populate Install Proton dropdown (Experimental/GE-Proton 10+ only for fast texture processing)"""
|
||||
try:
|
||||
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()
|
||||
|
||||
# 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:
|
||||
proton_name = proton.get('name', 'Unknown Proton')
|
||||
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':
|
||||
display_name = f"{proton_name} (GE)"
|
||||
elif proton_type == 'Valve-Proton':
|
||||
display_name = f"{proton_name}"
|
||||
else:
|
||||
display_name = proton_name
|
||||
|
||||
self.proton_dropdown.addItem(display_name, str(proton['path']))
|
||||
self.game_proton_dropdown.addItem(display_name, str(proton['path']))
|
||||
|
||||
# Load saved preference and determine UI selection
|
||||
saved_proton = self.config_handler.get('proton_path', self._get_proton_10_path())
|
||||
|
||||
# Check if saved path matches any specific Proton in dropdown
|
||||
found_match = False
|
||||
for i in range(self.proton_dropdown.count()):
|
||||
if self.proton_dropdown.itemData(i) == saved_proton:
|
||||
self.proton_dropdown.setCurrentIndex(i)
|
||||
found_match = True
|
||||
break
|
||||
|
||||
# If no exact match found, check if it's a resolved auto-selection
|
||||
if not found_match and saved_proton != "auto":
|
||||
# This means config has a resolved path from previous "Auto" selection
|
||||
# Show "Auto" in UI since user chose auto-detection
|
||||
for i in range(self.proton_dropdown.count()):
|
||||
if self.proton_dropdown.itemData(i) == "auto":
|
||||
self.proton_dropdown.setCurrentIndex(i)
|
||||
break
|
||||
# Load saved preference
|
||||
saved_game_proton = self.config_handler.get('game_proton_path', 'same_as_install')
|
||||
self._set_dropdown_selection(self.game_proton_dropdown, saved_game_proton)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to populate Proton dropdown: {e}")
|
||||
# Fallback: just show auto
|
||||
self.proton_dropdown.addItem("Auto", "auto")
|
||||
logger.error(f"Failed to populate game Proton dropdown: {e}")
|
||||
self.game_proton_dropdown.addItem("Same as Install Proton", "same_as_install")
|
||||
|
||||
def _refresh_proton_dropdown(self):
|
||||
"""Refresh Proton dropdown with latest detected versions"""
|
||||
current_selection = self.proton_dropdown.currentData()
|
||||
self.proton_dropdown.clear()
|
||||
self._populate_proton_dropdown()
|
||||
|
||||
# Restore selection if still available
|
||||
for i in range(self.proton_dropdown.count()):
|
||||
if self.proton_dropdown.itemData(i) == current_selection:
|
||||
self.proton_dropdown.setCurrentIndex(i)
|
||||
def _set_dropdown_selection(self, dropdown, saved_value):
|
||||
"""Helper to set dropdown selection based on saved value"""
|
||||
found_match = False
|
||||
for i in range(dropdown.count()):
|
||||
if dropdown.itemData(i) == saved_value:
|
||||
dropdown.setCurrentIndex(i)
|
||||
found_match = True
|
||||
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):
|
||||
# Validate values
|
||||
for k, (multithreading_checkbox, max_tasks_spin) in self.resource_edits.items():
|
||||
if max_tasks_spin.value() > 128:
|
||||
self.error_label.setText(f"Invalid value for {k}: Max Tasks must be <= 128.")
|
||||
try:
|
||||
# Validate values (only if resource_edits exist)
|
||||
for k, (multithreading_checkbox, max_tasks_spin) in self.resource_edits.items():
|
||||
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
|
||||
if self.bandwidth_spin and self.bandwidth_spin.value() > 1000000:
|
||||
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()
|
||||
self.resource_settings[k] = resource_data
|
||||
|
||||
# Save bandwidth limit to Downloads resource MaxThroughput (only if bandwidth UI exists)
|
||||
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 all resource settings (including bandwidth) in one operation
|
||||
self._save_json(self.resource_settings_path, self.resource_settings)
|
||||
|
||||
# Save debug mode to config
|
||||
self.config_handler.set('debug_mode', self.debug_checkbox.isChecked())
|
||||
# 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)
|
||||
self.error_label.setText("")
|
||||
|
||||
# Save Proton selection - resolve "auto" to actual path
|
||||
selected_proton_path = self.proton_dropdown.currentData()
|
||||
if selected_proton_path == "auto":
|
||||
# Resolve "auto" to actual best Proton path using unified detection
|
||||
try:
|
||||
from jackify.backend.handlers.wine_utils import WineUtils
|
||||
best_proton = WineUtils.select_best_proton()
|
||||
# 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()
|
||||
self.resource_settings[k] = resource_data
|
||||
|
||||
if best_proton:
|
||||
resolved_path = str(best_proton['path'])
|
||||
resolved_version = best_proton['name']
|
||||
else:
|
||||
resolved_path = "auto"
|
||||
resolved_version = "auto"
|
||||
except:
|
||||
resolved_path = "auto"
|
||||
resolved_version = "auto"
|
||||
else:
|
||||
# User selected specific Proton version
|
||||
resolved_path = selected_proton_path
|
||||
# Extract version from dropdown text
|
||||
resolved_version = self.proton_dropdown.currentText()
|
||||
# Save bandwidth limit to Downloads resource MaxThroughput (only if bandwidth UI exists)
|
||||
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()
|
||||
|
||||
self.config_handler.set("proton_path", resolved_path)
|
||||
self.config_handler.set("proton_version", resolved_version)
|
||||
# Save all resource settings (including bandwidth) in one operation
|
||||
self._save_json(self.resource_settings_path, self.resource_settings)
|
||||
|
||||
# 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: path={resolved_path}, version={resolved_version}")
|
||||
# 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}")
|
||||
# Save debug mode to config
|
||||
self.config_handler.set('debug_mode', self.debug_checkbox.isChecked())
|
||||
# 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 Install Proton selection - resolve "auto" to actual path
|
||||
selected_install_proton_path = self.install_proton_dropdown.currentData()
|
||||
if selected_install_proton_path == "auto":
|
||||
# Resolve "auto" to actual best Proton path using unified detection
|
||||
try:
|
||||
from jackify.backend.handlers.wine_utils import WineUtils
|
||||
best_proton = WineUtils.select_best_proton()
|
||||
|
||||
if best_proton:
|
||||
resolved_install_path = str(best_proton['path'])
|
||||
resolved_install_version = best_proton['name']
|
||||
else:
|
||||
resolved_install_path = "auto"
|
||||
resolved_install_version = "auto"
|
||||
except:
|
||||
resolved_install_path = "auto"
|
||||
resolved_install_version = "auto"
|
||||
else:
|
||||
self.logger.debug("Config save verified successfully")
|
||||
|
||||
# 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
|
||||
if getattr(sys, 'frozen', False):
|
||||
# PyInstaller bundle: safe to restart
|
||||
self.accept()
|
||||
os.execv(sys.executable, [sys.executable] + sys.argv)
|
||||
return
|
||||
# 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()
|
||||
|
||||
self.config_handler.set("proton_path", resolved_install_path)
|
||||
self.config_handler.set("proton_version", resolved_install_version)
|
||||
|
||||
# Save Game Proton selection
|
||||
selected_game_proton_path = self.game_proton_dropdown.currentData()
|
||||
if selected_game_proton_path == "same_as_install":
|
||||
# Use same as install proton
|
||||
resolved_game_path = resolved_install_path
|
||||
resolved_game_version = resolved_install_version
|
||||
else:
|
||||
# User selected specific game Proton version
|
||||
resolved_game_path = selected_game_proton_path
|
||||
resolved_game_version = self.game_proton_dropdown.currentText()
|
||||
|
||||
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.component_toggle.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:
|
||||
# Dev mode: show message instead of auto-restart
|
||||
MessageService.information(self, "Manual Restart Required", "Please restart Jackify manually to apply debug mode changes.", safety_level="low")
|
||||
self.logger.debug("Config save verified successfully")
|
||||
|
||||
# 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()
|
||||
|
||||
# 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
|
||||
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):
|
||||
"""Refresh cached paths in all GUI screens."""
|
||||
|
||||
@@ -37,7 +37,9 @@ class ConfigureExistingModlistScreen(QWidget):
|
||||
self.refresh_paths()
|
||||
|
||||
# --- Detect Steam Deck ---
|
||||
steamdeck = os.path.exists('/etc/os-release') and 'steamdeck' in open('/etc/os-release').read().lower()
|
||||
from jackify.backend.services.platform_detection_service import PlatformDetectionService
|
||||
platform_service = PlatformDetectionService.get_instance()
|
||||
steamdeck = platform_service.is_steamdeck
|
||||
self.shortcut_handler = ShortcutHandler(steamdeck=steamdeck)
|
||||
|
||||
# Initialize services early
|
||||
@@ -499,6 +501,7 @@ class ConfigureExistingModlistScreen(QWidget):
|
||||
# For existing modlists, add resolution if specified
|
||||
if self.resolution != "Leave unchanged":
|
||||
modlist_context.resolution = self.resolution.split()[0]
|
||||
# Note: If "Leave unchanged" is selected, resolution stays None (no fallback needed)
|
||||
|
||||
# Define callbacks
|
||||
def progress_callback(message):
|
||||
|
||||
@@ -591,7 +591,9 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
return
|
||||
# --- Shortcut creation will be handled by automated workflow ---
|
||||
from jackify.backend.handlers.shortcut_handler import ShortcutHandler
|
||||
steamdeck = os.path.exists('/etc/os-release') and 'steamdeck' in open('/etc/os-release').read().lower()
|
||||
from jackify.backend.services.platform_detection_service import PlatformDetectionService
|
||||
platform_service = PlatformDetectionService.get_instance()
|
||||
steamdeck = platform_service.is_steamdeck
|
||||
shortcut_handler = ShortcutHandler(steamdeck=steamdeck) # Still needed for Steam restart
|
||||
|
||||
# Check if auto-restart is enabled
|
||||
@@ -723,16 +725,10 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
except Exception as e:
|
||||
self.error_occurred.emit(str(e))
|
||||
|
||||
# Detect Steam Deck once
|
||||
try:
|
||||
import os
|
||||
_is_steamdeck = False
|
||||
if os.path.exists('/etc/os-release'):
|
||||
with open('/etc/os-release') as f:
|
||||
if 'steamdeck' in f.read().lower():
|
||||
_is_steamdeck = True
|
||||
except Exception:
|
||||
_is_steamdeck = False
|
||||
# Detect Steam Deck once using centralized service
|
||||
from jackify.backend.services.platform_detection_service import PlatformDetectionService
|
||||
platform_service = PlatformDetectionService.get_instance()
|
||||
_is_steamdeck = platform_service.is_steamdeck
|
||||
|
||||
# Create and start the thread
|
||||
self.automated_prefix_thread = AutomatedPrefixThread(modlist_name, install_dir, mo2_exe_path, _is_steamdeck)
|
||||
@@ -928,7 +924,10 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
# Steam assigns a NEW AppID during restart, different from the one we initially created
|
||||
self._safe_append_text(f"Re-detecting AppID for shortcut '{modlist_name}' after Steam restart...")
|
||||
from jackify.backend.handlers.shortcut_handler import ShortcutHandler
|
||||
shortcut_handler = ShortcutHandler(steamdeck=False)
|
||||
from jackify.backend.services.platform_detection_service import PlatformDetectionService
|
||||
|
||||
platform_service = PlatformDetectionService.get_instance()
|
||||
shortcut_handler = ShortcutHandler(steamdeck=platform_service.is_steamdeck)
|
||||
current_appid = shortcut_handler.get_appid_for_shortcut(modlist_name, mo2_exe_path)
|
||||
|
||||
if not current_appid or not current_appid.isdigit():
|
||||
@@ -952,7 +951,12 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
|
||||
# Initialize ModlistHandler with correct parameters
|
||||
path_handler = PathHandler()
|
||||
modlist_handler = ModlistHandler(steamdeck=False, verbose=False)
|
||||
|
||||
# Use centralized Steam Deck detection
|
||||
from jackify.backend.services.platform_detection_service import PlatformDetectionService
|
||||
platform_service = PlatformDetectionService.get_instance()
|
||||
|
||||
modlist_handler = ModlistHandler(steamdeck=platform_service.is_steamdeck, verbose=False)
|
||||
|
||||
# Set required properties manually after initialization
|
||||
modlist_handler.modlist_dir = install_dir
|
||||
@@ -1184,7 +1188,7 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
nexus_api_key='', # Not needed for configuration
|
||||
modlist_value='', # Not needed for existing modlist
|
||||
modlist_source='existing',
|
||||
resolution=self.context.get('resolution'),
|
||||
resolution=self.context.get('resolution') or get_resolution_fallback(None),
|
||||
skip_confirmation=True
|
||||
)
|
||||
|
||||
|
||||
@@ -1746,7 +1746,14 @@ class InstallModlistScreen(QWidget):
|
||||
|
||||
# Save resolution for later use in configuration
|
||||
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
|
||||
debug_print("DEBUG: Starting automated prefix creation workflow")
|
||||
@@ -1760,7 +1767,14 @@ class InstallModlistScreen(QWidget):
|
||||
# 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
|
||||
self._current_resolution = resolution.split()[0] if resolution and resolution != "Leave unchanged" 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"""
|
||||
try:
|
||||
# Disable controls during installation
|
||||
@@ -2098,7 +2112,10 @@ class InstallModlistScreen(QWidget):
|
||||
# Steam assigns a NEW AppID during restart, different from the one we initially created
|
||||
self._safe_append_text(f"Re-detecting AppID for shortcut '{modlist_name}' after Steam restart...")
|
||||
from jackify.backend.handlers.shortcut_handler import ShortcutHandler
|
||||
shortcut_handler = ShortcutHandler(steamdeck=False)
|
||||
from jackify.backend.services.platform_detection_service import PlatformDetectionService
|
||||
|
||||
platform_service = PlatformDetectionService.get_instance()
|
||||
shortcut_handler = ShortcutHandler(steamdeck=platform_service.is_steamdeck)
|
||||
current_appid = shortcut_handler.get_appid_for_shortcut(modlist_name, mo2_exe_path)
|
||||
|
||||
if not current_appid or not current_appid.isdigit():
|
||||
@@ -2119,7 +2136,12 @@ class InstallModlistScreen(QWidget):
|
||||
|
||||
# Initialize ModlistHandler with correct parameters
|
||||
path_handler = PathHandler()
|
||||
modlist_handler = ModlistHandler(steamdeck=False, verbose=False)
|
||||
|
||||
# Use centralized Steam Deck detection
|
||||
from jackify.backend.services.platform_detection_service import PlatformDetectionService
|
||||
platform_service = PlatformDetectionService.get_instance()
|
||||
|
||||
modlist_handler = ModlistHandler(steamdeck=platform_service.is_steamdeck, verbose=False)
|
||||
|
||||
# Set required properties manually after initialization
|
||||
modlist_handler.modlist_dir = install_dir
|
||||
@@ -2337,15 +2359,21 @@ class InstallModlistScreen(QWidget):
|
||||
self.context = updated_context # Ensure context is always set
|
||||
debug_print(f"Updated context with new AppID: {new_appid}")
|
||||
|
||||
# Get Steam Deck detection once and pass to ConfigThread
|
||||
from jackify.backend.services.platform_detection_service import PlatformDetectionService
|
||||
platform_service = PlatformDetectionService.get_instance()
|
||||
is_steamdeck = platform_service.is_steamdeck
|
||||
|
||||
# Create new config thread with updated context
|
||||
class ConfigThread(QThread):
|
||||
progress_update = Signal(str)
|
||||
configuration_complete = Signal(bool, str, str)
|
||||
error_occurred = Signal(str)
|
||||
|
||||
def __init__(self, context):
|
||||
|
||||
def __init__(self, context, is_steamdeck):
|
||||
super().__init__()
|
||||
self.context = context
|
||||
self.is_steamdeck = is_steamdeck
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
@@ -2354,8 +2382,8 @@ class InstallModlistScreen(QWidget):
|
||||
from jackify.backend.models.modlist import ModlistContext
|
||||
from pathlib import Path
|
||||
|
||||
# Initialize backend service
|
||||
system_info = SystemInfo(is_steamdeck=False)
|
||||
# Initialize backend service with passed Steam Deck detection
|
||||
system_info = SystemInfo(is_steamdeck=self.is_steamdeck)
|
||||
modlist_service = ModlistService(system_info)
|
||||
|
||||
# Convert context to ModlistContext for service
|
||||
@@ -2402,7 +2430,7 @@ class InstallModlistScreen(QWidget):
|
||||
self.error_occurred.emit(str(e))
|
||||
|
||||
# Start configuration thread
|
||||
self.config_thread = ConfigThread(updated_context)
|
||||
self.config_thread = ConfigThread(updated_context, is_steamdeck)
|
||||
self.config_thread.progress_update.connect(self.on_configuration_progress)
|
||||
self.config_thread.configuration_complete.connect(self.on_configuration_complete)
|
||||
self.config_thread.error_occurred.connect(self.on_configuration_error)
|
||||
@@ -2463,15 +2491,21 @@ class InstallModlistScreen(QWidget):
|
||||
def _create_config_thread(self, context):
|
||||
"""Create a new ConfigThread with proper lifecycle management"""
|
||||
from PySide6.QtCore import QThread, Signal
|
||||
|
||||
|
||||
# Get Steam Deck detection once
|
||||
from jackify.backend.services.platform_detection_service import PlatformDetectionService
|
||||
platform_service = PlatformDetectionService.get_instance()
|
||||
is_steamdeck = platform_service.is_steamdeck
|
||||
|
||||
class ConfigThread(QThread):
|
||||
progress_update = Signal(str)
|
||||
configuration_complete = Signal(bool, str, str)
|
||||
error_occurred = Signal(str)
|
||||
|
||||
def __init__(self, context, parent=None):
|
||||
|
||||
def __init__(self, context, is_steamdeck, parent=None):
|
||||
super().__init__(parent)
|
||||
self.context = context
|
||||
self.is_steamdeck = is_steamdeck
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
@@ -2480,8 +2514,8 @@ class InstallModlistScreen(QWidget):
|
||||
from jackify.backend.models.modlist import ModlistContext
|
||||
from pathlib import Path
|
||||
|
||||
# Initialize backend service
|
||||
system_info = SystemInfo(is_steamdeck=False)
|
||||
# Initialize backend service with passed Steam Deck detection
|
||||
system_info = SystemInfo(is_steamdeck=self.is_steamdeck)
|
||||
modlist_service = ModlistService(system_info)
|
||||
|
||||
# Convert context to ModlistContext for service
|
||||
@@ -2530,7 +2564,7 @@ class InstallModlistScreen(QWidget):
|
||||
self.progress_update.emit(f"DEBUG: {error_details}")
|
||||
self.error_occurred.emit(str(e))
|
||||
|
||||
return ConfigThread(context, parent=self)
|
||||
return ConfigThread(context, is_steamdeck, parent=self)
|
||||
|
||||
def handle_validation_failure(self, missing_text):
|
||||
"""Handle failed validation with retry logic"""
|
||||
|
||||
Reference in New Issue
Block a user