mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-01-17 19:47:00 +01:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
430d085287 | ||
|
|
7212a58480 | ||
|
|
80914bc76f | ||
|
|
8661f8963e | ||
|
|
f46ed2c0fe |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -35,7 +35,7 @@ Thumbs.db
|
||||
docs/
|
||||
testing/
|
||||
|
||||
# PyInstaller build files (development only)
|
||||
# Build files (development only)
|
||||
*.spec
|
||||
hook-*.py
|
||||
requirements-packaging.txt
|
||||
|
||||
105
CHANGELOG.md
105
CHANGELOG.md
@@ -1,5 +1,110 @@
|
||||
# Jackify Changelog
|
||||
|
||||
## v0.1.6 - Advanced Proton Management & Lorerim Support
|
||||
**Release Date:** October 16, 2025
|
||||
|
||||
### Major New Features
|
||||
- **Dual Proton Configuration**: Separate Install Proton and Game Proton version selection in Settings
|
||||
- **Install Proton**: Optimized for modlist installation and texture processing (Experimental/GE-Proton 10+ recommended for performance)
|
||||
- **Game Proton**: For game shortcuts (supports any Proton 9+ version)
|
||||
- Independent configuration allows users to optimize for both installation speed and game compatibility
|
||||
|
||||
- **Lorerim Proton Override**: Automatic Proton 9 selection for Lorerim modlist installations
|
||||
- Priority system: GE-Proton9-27 → Other GE-Proton 9 versions → Valve Proton 9 → user settings fallback
|
||||
- User notification when override is applied
|
||||
- Case-insensitive detection for Lorerim modlists
|
||||
|
||||
- **Configurable Component Installation Method**: User-selectable toggle in Settings
|
||||
- **Optimized Mode** (default): Protontricks for dotnet40 (reliable), winetricks for other components (fast)
|
||||
- **Legacy Mode**: Protontricks for all components (slower but maximum compatibility)
|
||||
|
||||
### Engine & Technical Improvements
|
||||
- **jackify-engine v0.3.17**: Latest engine version with performance improvements
|
||||
- **Windows 10 Prefix Timing**: Improved timing to match legacy script behavior
|
||||
- **Self-Updater Enhancement**: Fixed auto-restart checkbox functionality
|
||||
- **ProtontricksHandler**: Updated constructor calls across codebase for consistency
|
||||
|
||||
---
|
||||
|
||||
## v0.1.5.3 - Critical Bug Fixes
|
||||
**Release Date:** October 2, 2025
|
||||
|
||||
### Critical Bug Fixes
|
||||
- **Fixed Multi-User Steam Detection**: Properly reads loginusers.vdf and converts SteamID64 to SteamID3 for accurate user identification
|
||||
- **Fixed dotnet40 Installation Failures**: Hybrid approach uses protontricks for dotnet40 (reliable), winetricks for other components (fast)
|
||||
- **Fixed dotnet8 Installation**: Now properly handled by winetricks instead of unimplemented pass statement
|
||||
- **Fixed D: Drive Detection**: SD card detection now only applies to Steam Deck systems, not regular Linux systems
|
||||
- **Fixed SD Card Mount Patterns**: Replaced hardcoded mmcblk0p1 references with dynamic path detection
|
||||
- **Fixed Debug Restart UX**: Replaced PyInstaller detection with AppImage detection for proper restart behavior
|
||||
|
||||
---
|
||||
|
||||
## v0.1.5.2 - Proton Configuration & Engine Updates
|
||||
**Release Date:** September 30, 2025
|
||||
|
||||
### Critical Bug Fixes
|
||||
- **Fixed Proton Version Selection**: Wine component installation now properly honors user-selected Proton version from Settings dialog
|
||||
- Previously, changing from GE-Proton to Proton Experimental in settings would still use the old version for component installation
|
||||
- Fixed ConfigHandler to reload fresh configuration from disk instead of using stale cache
|
||||
- Updated all Proton path retrieval across codebase to use fresh-reading methods
|
||||
|
||||
### Engine Updates
|
||||
- **jackify-engine v0.3.16**: Updated to latest engine version with important reliability improvements
|
||||
- **Sanity Check Fallback**: Added Proton 7z.exe fallback for case sensitivity extraction failures
|
||||
- **Enhanced Error Messages**: Improved texconv/texdiag error messages to include original texture file names and conversion parameters
|
||||
|
||||
### Technical Improvements
|
||||
- Enhanced configuration system reliability for multi-instance scenarios
|
||||
- Improved error diagnostics for texture processing operations
|
||||
- Fix Qt platform plugin discovery in AppImage distribution for improved compatibility
|
||||
|
||||
---
|
||||
|
||||
## v0.1.5.1 - Bug Fixes
|
||||
**Release Date:** September 28, 2025
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed Steam user detection in multi-user environments
|
||||
- Fixed controls not re-enabling after workflow errors
|
||||
- Fixed screen state persistence between workflows
|
||||
|
||||
---
|
||||
|
||||
## v0.1.5 - Winetricks Integration & Enhanced Compatibility
|
||||
**Release Date:** September 26, 2025
|
||||
|
||||
### Major Improvements
|
||||
- **Winetricks Integration**: Replaced protontricks with bundled winetricks for faster, more reliable wine component installation
|
||||
- **Enhanced SD Card Detection**: Dynamic detection of SD card mount points supports both `/run/media/mmcblk0p1` and `/run/media/deck/UUID` patterns
|
||||
- **Smart Proton Detection**: Comprehensive GE-Proton support with detection in both steamapps/common and compatibilitytools.d directories
|
||||
- **Steam Deck SD Card Support**: Fixed path handling for SD card installations on Steam Deck
|
||||
|
||||
### User Experience
|
||||
- **No Focus Stealing**: Wine component installation runs in background without disrupting user workflow
|
||||
- **Popup Suppression**: Eliminated wine GUI popups while maintaining functionality
|
||||
- **GUI Navigation**: Fixed navigation issues after Tuxborn workflow removal
|
||||
|
||||
### Bug Fixes
|
||||
- **CLI Configure Existing**: Fixed AppID detection with signed-to-unsigned conversion, removing protontricks dependency
|
||||
- **GE-Proton Validation**: Fixed validation to support both Valve Proton and GE-Proton directory structures
|
||||
- **Resolution Override**: Eliminated hardcoded 2560x1600 fallbacks that overrode user Steam Deck settings
|
||||
- **VDF Case-Sensitivity**: Added case-insensitive parsing for Steam shortcuts fields
|
||||
- **Cabextract Bundling**: Bundled cabextract binary to resolve winetricks dependency issues
|
||||
- **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
|
||||
|
||||
#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
|
||||
- **Proton Path Detection**: Uses actual Steam libraries from libraryfolders.vdf instead of hardcoded paths
|
||||
|
||||
### Technical Improvements
|
||||
- **Self-contained Cache**: Relocated winetricks cache to jackify_data_dir for better isolation
|
||||
|
||||
---
|
||||
|
||||
## v0.1.4 - GE-Proton Support and Performance Optimization
|
||||
**Release Date:** September 22, 2025
|
||||
|
||||
|
||||
@@ -5,4 +5,4 @@ This package provides both CLI and GUI interfaces for managing
|
||||
Wabbajack modlists natively on Linux systems.
|
||||
"""
|
||||
|
||||
__version__ = "0.1.4"
|
||||
__version__ = "0.1.6"
|
||||
|
||||
@@ -30,7 +30,7 @@ def _get_user_proton_version():
|
||||
from jackify.backend.handlers.wine_utils import WineUtils
|
||||
|
||||
config_handler = ConfigHandler()
|
||||
user_proton_path = config_handler.get('proton_path', 'auto')
|
||||
user_proton_path = config_handler.get_proton_path()
|
||||
|
||||
if user_proton_path == 'auto':
|
||||
# Use enhanced fallback logic with GE-Proton preference
|
||||
@@ -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)
|
||||
@@ -730,6 +730,14 @@ class ModlistInstallCLI:
|
||||
cmd += ['-m', self.context['machineid']]
|
||||
cmd += ['-o', install_dir_str, '-d', download_dir_str]
|
||||
|
||||
# Add debug flag if debug mode is enabled
|
||||
from jackify.backend.handlers.config_handler import ConfigHandler
|
||||
config_handler = ConfigHandler()
|
||||
debug_mode = config_handler.get('debug_mode', False)
|
||||
if debug_mode:
|
||||
cmd.append('--debug')
|
||||
self.logger.info("Adding --debug flag to jackify-engine")
|
||||
|
||||
# Store original environment values to restore later
|
||||
original_env_values = {
|
||||
'NEXUS_API_KEY': os.environ.get('NEXUS_API_KEY'),
|
||||
|
||||
@@ -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
|
||||
@@ -496,6 +498,66 @@ class ConfigHandler:
|
||||
logger.error(f"Error saving modlist downloads base directory: {e}")
|
||||
return False
|
||||
|
||||
def get_proton_path(self):
|
||||
"""
|
||||
Retrieve the saved Install Proton path from configuration (for jackify-engine)
|
||||
Always reads fresh from disk to pick up changes from Settings dialog
|
||||
|
||||
Returns:
|
||||
str: Saved Install Proton path or 'auto' if not saved
|
||||
"""
|
||||
try:
|
||||
# Reload config from disk to pick up changes from Settings dialog
|
||||
self._load_config()
|
||||
proton_path = self.settings.get("proton_path", "auto")
|
||||
logger.debug(f"Retrieved fresh install proton_path from config: {proton_path}")
|
||||
return proton_path
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving install proton_path: {e}")
|
||||
return "auto"
|
||||
|
||||
def get_game_proton_path(self):
|
||||
"""
|
||||
Retrieve the saved Game Proton path from configuration (for game shortcuts)
|
||||
Falls back to install Proton path if game Proton not set
|
||||
Always reads fresh from disk to pick up changes from Settings dialog
|
||||
|
||||
Returns:
|
||||
str: Saved Game Proton path, Install Proton path, or 'auto' if not saved
|
||||
"""
|
||||
try:
|
||||
# Reload config from disk to pick up changes from Settings dialog
|
||||
self._load_config()
|
||||
game_proton_path = self.settings.get("game_proton_path")
|
||||
|
||||
# If game proton not set or set to same_as_install, use install proton
|
||||
if not game_proton_path or game_proton_path == "same_as_install":
|
||||
game_proton_path = self.settings.get("proton_path", "auto")
|
||||
|
||||
logger.debug(f"Retrieved fresh game proton_path from config: {game_proton_path}")
|
||||
return game_proton_path
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving game proton_path: {e}")
|
||||
return "auto"
|
||||
|
||||
def get_proton_version(self):
|
||||
"""
|
||||
Retrieve the saved Proton version from configuration
|
||||
Always reads fresh from disk to pick up changes from Settings dialog
|
||||
|
||||
Returns:
|
||||
str: Saved Proton version or 'auto' if not saved
|
||||
"""
|
||||
try:
|
||||
# Reload config from disk to pick up changes from Settings dialog
|
||||
self._load_config()
|
||||
proton_version = self.settings.get("proton_version", "auto")
|
||||
logger.debug(f"Retrieved fresh proton_version from config: {proton_version}")
|
||||
return proton_version
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving proton_version: {e}")
|
||||
return "auto"
|
||||
|
||||
def _auto_detect_proton(self):
|
||||
"""Auto-detect and set best Proton version (includes GE-Proton and Valve Proton)"""
|
||||
try:
|
||||
|
||||
@@ -79,6 +79,7 @@ class ModlistHandler:
|
||||
"livingskyrim": ["dotnet40"],
|
||||
"lsiv": ["dotnet40"],
|
||||
"ls4": ["dotnet40"],
|
||||
"lorerim": ["dotnet40"],
|
||||
"lostlegacy": ["dotnet48"],
|
||||
}
|
||||
|
||||
@@ -158,7 +159,10 @@ 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)
|
||||
self.shortcut_handler = ShortcutHandler(steamdeck=self.steamdeck, verbose=self.verbose)
|
||||
self.filesystem_handler = filesystem_handler if filesystem_handler else FileSystemHandler()
|
||||
self.resolution_handler = ResolutionHandler()
|
||||
@@ -224,44 +228,41 @@ class ModlistHandler:
|
||||
discovered_modlists_info = []
|
||||
|
||||
try:
|
||||
# 1. Get ALL non-Steam shortcuts from Protontricks
|
||||
# Now calls the renamed method without filtering
|
||||
protontricks_shortcuts = self.protontricks_handler.list_non_steam_shortcuts()
|
||||
if not protontricks_shortcuts:
|
||||
self.logger.warning("Protontricks did not list any non-Steam shortcuts.")
|
||||
return []
|
||||
self.logger.debug(f"Protontricks non-Steam shortcuts found: {protontricks_shortcuts}")
|
||||
|
||||
# 2. Get shortcuts pointing to the executable from shortcuts.vdf
|
||||
# Get shortcuts pointing to the executable from shortcuts.vdf
|
||||
matching_vdf_shortcuts = self.shortcut_handler.find_shortcuts_by_exe(executable_name)
|
||||
if not matching_vdf_shortcuts:
|
||||
self.logger.debug(f"No shortcuts found pointing to '{executable_name}' in shortcuts.vdf.")
|
||||
return []
|
||||
self.logger.debug(f"Shortcuts matching executable '{executable_name}' in VDF: {matching_vdf_shortcuts}")
|
||||
|
||||
# 3. Correlate the two lists and extract required info
|
||||
# Process each matching shortcut and convert signed AppID to unsigned
|
||||
for vdf_shortcut in matching_vdf_shortcuts:
|
||||
app_name = vdf_shortcut.get('AppName')
|
||||
start_dir = vdf_shortcut.get('StartDir')
|
||||
|
||||
signed_appid = vdf_shortcut.get('appid')
|
||||
|
||||
if not app_name or not start_dir:
|
||||
self.logger.warning(f"Skipping VDF shortcut due to missing AppName or StartDir: {vdf_shortcut}")
|
||||
continue
|
||||
|
||||
if app_name in protontricks_shortcuts:
|
||||
app_id = protontricks_shortcuts[app_name]
|
||||
|
||||
# Append dictionary with all necessary info
|
||||
modlist_info = {
|
||||
'name': app_name,
|
||||
'appid': app_id,
|
||||
'path': start_dir
|
||||
}
|
||||
discovered_modlists_info.append(modlist_info)
|
||||
self.logger.info(f"Validated shortcut: '{app_name}' (AppID: {app_id}, Path: {start_dir})")
|
||||
if signed_appid is None:
|
||||
self.logger.warning(f"Skipping VDF shortcut due to missing appid: {vdf_shortcut}")
|
||||
continue
|
||||
|
||||
# Convert signed AppID to unsigned AppID (the format used by Steam prefixes)
|
||||
if signed_appid < 0:
|
||||
unsigned_appid = signed_appid + (2**32)
|
||||
else:
|
||||
# Downgraded from WARNING to INFO
|
||||
self.logger.info(f"Shortcut '{app_name}' found in VDF but not listed by protontricks. Skipping.")
|
||||
unsigned_appid = signed_appid
|
||||
|
||||
# Append dictionary with all necessary info using unsigned AppID
|
||||
modlist_info = {
|
||||
'name': app_name,
|
||||
'appid': unsigned_appid,
|
||||
'path': start_dir
|
||||
}
|
||||
discovered_modlists_info.append(modlist_info)
|
||||
self.logger.info(f"Discovered shortcut: '{app_name}' (Signed: {signed_appid} → Unsigned: {unsigned_appid}, Path: {start_dir})")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error discovering executable shortcuts: {e}", exc_info=True)
|
||||
@@ -315,13 +316,15 @@ 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
|
||||
if (str(self.modlist_dir).startswith("/run/media") or str(self.modlist_dir).startswith("/media")) 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).")
|
||||
else:
|
||||
self.modlist_sdcard = False
|
||||
if (str(self.modlist_dir).startswith("/run/media") or str(self.modlist_dir).startswith("/media")) and not self.steamdeck:
|
||||
self.logger.info("Modlist on /media mount detected on non-Steam Deck system - using Z: drive mapping.")
|
||||
|
||||
# Find and set compatdata path now that we have appid
|
||||
# Ensure PathHandler is available (should be initialized in __init__)
|
||||
@@ -345,7 +348,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.")
|
||||
@@ -685,10 +689,27 @@ class ModlistHandler:
|
||||
# All modlists now use their own AppID for wine components
|
||||
target_appid = self.appid
|
||||
|
||||
if not self.protontricks_handler.install_wine_components(target_appid, self.game_var_full, specific_components=components):
|
||||
self.logger.error("Failed to install Wine components. Configuration aborted.")
|
||||
# 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 component installation.")
|
||||
print("Error: Could not determine wine prefix location.")
|
||||
return False
|
||||
|
||||
# Use the winetricks handler which respects the user's toggle setting
|
||||
try:
|
||||
self.logger.info("Installing Wine components using user's preferred method...")
|
||||
success = self.winetricks_handler.install_wine_components(wineprefix, self.game_var_full, specific_components=components)
|
||||
if success:
|
||||
self.logger.info("Wine component installation completed successfully")
|
||||
else:
|
||||
self.logger.error("Wine component installation failed")
|
||||
print("Error: Failed to install necessary Wine components.")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"Wine component installation failed with exception: {e}")
|
||||
print("Error: Failed to install necessary Wine components.")
|
||||
return False # Abort on failure
|
||||
return False
|
||||
self.logger.info("Step 4: Installing Wine components... Done")
|
||||
|
||||
# Step 5: Ensure permissions of Modlist directory
|
||||
@@ -758,9 +779,10 @@ class ModlistHandler:
|
||||
self.logger.info("No stock game path found, skipping gamePath update - edit_binary_working_paths will handle all path updates.")
|
||||
self.logger.info("Using unified path manipulation to avoid duplicate processing.")
|
||||
|
||||
# Conditionally update binary and working directory paths
|
||||
# Conditionally update binary and working directory paths
|
||||
# Skip for jackify-engine workflows since paths are already correct
|
||||
if not getattr(self, 'engine_installed', False):
|
||||
# Exception: Always run for SD card installs to fix Z:/run/media/... to D:/... paths
|
||||
if not getattr(self, 'engine_installed', False) or self.modlist_sdcard:
|
||||
# Convert steamapps/common path to library root path
|
||||
steam_libraries = None
|
||||
if self.steam_library:
|
||||
@@ -792,10 +814,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
|
||||
):
|
||||
@@ -863,7 +885,7 @@ class ModlistHandler:
|
||||
prefix_path_str = self.path_handler.find_compat_data(str(self.appid))
|
||||
if prefix_path_str:
|
||||
prefix_path = Path(prefix_path_str)
|
||||
fonts_dir = prefix_path / "drive_c" / "windows" / "Fonts"
|
||||
fonts_dir = prefix_path / "pfx" / "drive_c" / "windows" / "Fonts"
|
||||
font_url = "https://github.com/mrbvrz/segoe-ui-linux/raw/refs/heads/master/font/seguisym.ttf"
|
||||
font_dest_path = fonts_dir / "seguisym.ttf"
|
||||
|
||||
@@ -898,6 +920,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:
|
||||
@@ -1300,4 +1326,39 @@ class ModlistHandler:
|
||||
self.logger.debug("No special game type detected - standard workflow will be used")
|
||||
return None
|
||||
|
||||
def _re_enforce_windows_10_mode(self):
|
||||
"""
|
||||
Re-enforce Windows 10 mode after modlist-specific configurations.
|
||||
This matches the legacy script behavior (line 1333) where Windows 10 mode
|
||||
is re-applied after modlist-specific steps to ensure consistency.
|
||||
"""
|
||||
try:
|
||||
if not hasattr(self, 'appid') or not self.appid:
|
||||
self.logger.warning("Cannot re-enforce Windows 10 mode - no AppID available")
|
||||
return
|
||||
|
||||
from ..handlers.winetricks_handler import WinetricksHandler
|
||||
from ..handlers.path_handler import PathHandler
|
||||
|
||||
# Get prefix path for the AppID
|
||||
prefix_path = PathHandler.find_compat_data(str(self.appid))
|
||||
if not prefix_path:
|
||||
self.logger.warning("Cannot re-enforce Windows 10 mode - prefix path not found")
|
||||
return
|
||||
|
||||
# Get wine binary path
|
||||
wine_binary = PathHandler.get_wine_binary_for_appid(str(self.appid))
|
||||
if not wine_binary:
|
||||
self.logger.warning("Cannot re-enforce Windows 10 mode - wine binary not found")
|
||||
return
|
||||
|
||||
# Use winetricks handler to set Windows 10 mode
|
||||
winetricks_handler = WinetricksHandler()
|
||||
winetricks_handler._set_windows_10_mode(str(prefix_path), wine_binary)
|
||||
|
||||
self.logger.info("✓ Windows 10 mode re-enforced after modlist-specific configurations")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Error re-enforcing Windows 10 mode: {e}")
|
||||
|
||||
# (Ensure EOF is clean and no extra incorrect methods exist below)
|
||||
@@ -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)
|
||||
|
||||
@@ -32,14 +32,21 @@ class PathHandler:
|
||||
@staticmethod
|
||||
def _strip_sdcard_path_prefix(path_obj: Path) -> str:
|
||||
"""
|
||||
Removes the '/run/media/mmcblk0p1/' prefix if present.
|
||||
Removes any detected SD card mount prefix dynamically.
|
||||
Handles both /run/media/mmcblk0p1 and /run/media/deck/UUID patterns.
|
||||
Returns the path as a POSIX-style string (using /).
|
||||
"""
|
||||
path_str = path_obj.as_posix() # Work with consistent forward slashes
|
||||
if path_str.lower().startswith(SDCARD_PREFIX.lower()):
|
||||
# Return the part *after* the prefix, ensuring no leading slash remains unless root
|
||||
relative_part = path_str[len(SDCARD_PREFIX):]
|
||||
return relative_part if relative_part else "." # Return '.' if it was exactly the prefix
|
||||
from .wine_utils import WineUtils
|
||||
|
||||
path_str = path_obj.as_posix() # Work with consistent forward slashes
|
||||
|
||||
# Use dynamic SD card detection from WineUtils
|
||||
stripped_path = WineUtils._strip_sdcard_path(path_str)
|
||||
|
||||
if stripped_path != path_str:
|
||||
# Path was stripped, remove leading slash for relative path
|
||||
return stripped_path.lstrip('/') if stripped_path != '/' else '.'
|
||||
|
||||
return path_str
|
||||
|
||||
@staticmethod
|
||||
@@ -623,47 +630,30 @@ class PathHandler:
|
||||
|
||||
# Moved _find_shortcuts_vdf here from ShortcutHandler
|
||||
def _find_shortcuts_vdf(self) -> Optional[str]:
|
||||
"""Helper to find the active shortcuts.vdf file for a user.
|
||||
"""Helper to find the active shortcuts.vdf file for the current Steam user.
|
||||
|
||||
Iterates through userdata directories and returns the path to the
|
||||
first found shortcuts.vdf file.
|
||||
Uses proper multi-user detection to find the correct Steam user instead
|
||||
of just taking the first found user directory.
|
||||
|
||||
Returns:
|
||||
Optional[str]: The full path to the shortcuts.vdf file, or None if not found.
|
||||
"""
|
||||
# This implementation was moved from ShortcutHandler
|
||||
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
|
||||
try:
|
||||
# Use native Steam service for proper multi-user detection
|
||||
from jackify.backend.services.native_steam_service import NativeSteamService
|
||||
steam_service = NativeSteamService()
|
||||
shortcuts_path = steam_service.get_shortcuts_vdf_path()
|
||||
|
||||
if shortcuts_path:
|
||||
logger.info(f"Found shortcuts.vdf using multi-user detection: {shortcuts_path}")
|
||||
return str(shortcuts_path)
|
||||
else:
|
||||
logger.error("Could not determine shortcuts.vdf path using multi-user detection")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error using multi-user detection for shortcuts.vdf: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def find_game_install_paths(target_appids: Dict[str, str]) -> Dict[str, Path]:
|
||||
@@ -737,7 +727,7 @@ class PathHandler:
|
||||
try:
|
||||
with open(modlist_ini_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
lines = f.readlines()
|
||||
drive_letter = "D:" if modlist_sdcard else "Z:"
|
||||
drive_letter = "D:\\\\" if modlist_sdcard else "Z:\\\\"
|
||||
processed_path = self._strip_sdcard_path_prefix(new_game_path)
|
||||
windows_style = processed_path.replace('/', '\\')
|
||||
windows_style_double = windows_style.replace('\\', '\\\\')
|
||||
@@ -876,9 +866,10 @@ class PathHandler:
|
||||
rel_path = value_part[idx:].lstrip('/')
|
||||
else:
|
||||
rel_path = exe_name
|
||||
new_binary_path = f"{drive_prefix}/{modlist_dir_path}/{rel_path}".replace('\\', '/').replace('//', '/')
|
||||
processed_modlist_path = PathHandler._strip_sdcard_path_prefix(modlist_dir_path) if modlist_sdcard else str(modlist_dir_path)
|
||||
new_binary_path = f"{drive_prefix}/{processed_modlist_path}/{rel_path}".replace('\\', '/').replace('//', '/')
|
||||
formatted_binary_path = PathHandler._format_binary_for_mo2(new_binary_path)
|
||||
new_binary_line = f"{index}{backslash_style}binary={formatted_binary_path}"
|
||||
new_binary_line = f"{index}{backslash_style}binary = {formatted_binary_path}"
|
||||
logger.debug(f"Updating binary path: {line.strip()} -> {new_binary_line}")
|
||||
lines[i] = new_binary_line + "\n"
|
||||
binary_paths_updated += 1
|
||||
@@ -893,7 +884,7 @@ class PathHandler:
|
||||
wd_path = drive_prefix + wd_path
|
||||
formatted_wd_path = PathHandler._format_workingdir_for_mo2(wd_path)
|
||||
key_part = f"{index}{backslash_style}workingDirectory"
|
||||
new_wd_line = f"{key_part}={formatted_wd_path}"
|
||||
new_wd_line = f"{key_part} = {formatted_wd_path}"
|
||||
logger.debug(f"Updating working directory: {wd_line.strip()} -> {new_wd_line}")
|
||||
lines[j] = new_wd_line + "\n"
|
||||
working_dirs_updated += 1
|
||||
|
||||
@@ -21,14 +21,19 @@ logger = logging.getLogger(__name__)
|
||||
class ProtontricksHandler:
|
||||
"""
|
||||
Handles operations related to Protontricks detection and usage
|
||||
|
||||
This handler now supports native Steam operations as a fallback/replacement
|
||||
for protontricks functionality.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, steamdeck: bool, logger=None):
|
||||
self.logger = logger or logging.getLogger(__name__)
|
||||
self.which_protontricks = None # 'flatpak' or 'native'
|
||||
self.protontricks_version = None
|
||||
self.protontricks_path = None
|
||||
self.steamdeck = steamdeck # Store steamdeck status
|
||||
self._native_steam_service = None
|
||||
self.use_native_operations = True # Enable native Steam operations by default
|
||||
|
||||
def _get_clean_subprocess_env(self):
|
||||
"""
|
||||
@@ -69,7 +74,14 @@ class ProtontricksHandler:
|
||||
env.pop('DYLD_LIBRARY_PATH', None)
|
||||
|
||||
return env
|
||||
|
||||
|
||||
def _get_native_steam_service(self):
|
||||
"""Get native Steam operations service instance"""
|
||||
if self._native_steam_service is None:
|
||||
from ..services.native_steam_operations_service import NativeSteamOperationsService
|
||||
self._native_steam_service = NativeSteamOperationsService(steamdeck=self.steamdeck)
|
||||
return self._native_steam_service
|
||||
|
||||
def detect_protontricks(self):
|
||||
"""
|
||||
Detect if protontricks is installed and whether it's flatpak or native.
|
||||
@@ -255,9 +267,19 @@ class ProtontricksHandler:
|
||||
|
||||
def set_protontricks_permissions(self, modlist_dir, steamdeck=False):
|
||||
"""
|
||||
Set permissions for Protontricks to access the modlist directory
|
||||
Set permissions for Steam operations to access the modlist directory.
|
||||
|
||||
Uses native operations when enabled, falls back to protontricks permissions.
|
||||
Returns True on success, False on failure
|
||||
"""
|
||||
# Use native operations if enabled
|
||||
if self.use_native_operations:
|
||||
logger.debug("Using native Steam operations, permissions handled natively")
|
||||
try:
|
||||
return self._get_native_steam_service().set_steam_permissions(modlist_dir, steamdeck)
|
||||
except Exception as e:
|
||||
logger.warning(f"Native permissions failed, falling back to protontricks: {e}")
|
||||
|
||||
if self.which_protontricks != 'flatpak':
|
||||
logger.debug("Using Native protontricks, skip setting permissions")
|
||||
return True
|
||||
@@ -338,15 +360,22 @@ class ProtontricksHandler:
|
||||
|
||||
# Renamed from list_non_steam_games for clarity and purpose
|
||||
def list_non_steam_shortcuts(self) -> Dict[str, str]:
|
||||
"""List ALL non-Steam shortcuts recognized by Protontricks.
|
||||
"""List ALL non-Steam shortcuts.
|
||||
|
||||
Runs 'protontricks -l' and parses the output for lines matching
|
||||
"Non-Steam shortcut: [Name] ([AppID])".
|
||||
Uses native VDF parsing when enabled, falls back to protontricks -l parsing.
|
||||
|
||||
Returns:
|
||||
A dictionary mapping the shortcut name (AppName) to its AppID.
|
||||
Returns an empty dictionary if none are found or an error occurs.
|
||||
"""
|
||||
# Use native operations if enabled
|
||||
if self.use_native_operations:
|
||||
logger.info("Listing non-Steam shortcuts via native VDF parsing...")
|
||||
try:
|
||||
return self._get_native_steam_service().list_non_steam_shortcuts()
|
||||
except Exception as e:
|
||||
logger.warning(f"Native shortcut listing failed, falling back to protontricks: {e}")
|
||||
|
||||
logger.info("Listing ALL non-Steam shortcuts via protontricks...")
|
||||
non_steam_shortcuts = {}
|
||||
# --- Ensure protontricks is detected before proceeding ---
|
||||
@@ -577,12 +606,22 @@ class ProtontricksHandler:
|
||||
def get_wine_prefix_path(self, appid) -> Optional[str]:
|
||||
"""Gets the WINEPREFIX path for a given AppID.
|
||||
|
||||
Uses native path discovery when enabled, falls back to protontricks detection.
|
||||
|
||||
Args:
|
||||
appid (str): The Steam AppID.
|
||||
|
||||
Returns:
|
||||
The WINEPREFIX path as a string, or None if detection fails.
|
||||
"""
|
||||
# Use native operations if enabled
|
||||
if self.use_native_operations:
|
||||
logger.debug(f"Getting WINEPREFIX for AppID {appid} via native path discovery")
|
||||
try:
|
||||
return self._get_native_steam_service().get_wine_prefix_path(appid)
|
||||
except Exception as e:
|
||||
logger.warning(f"Native WINEPREFIX detection failed, falling back to protontricks: {e}")
|
||||
|
||||
logger.debug(f"Getting WINEPREFIX for AppID {appid}")
|
||||
result = self.run_protontricks("-c", "echo $WINEPREFIX", appid)
|
||||
if result and result.returncode == 0 and result.stdout.strip():
|
||||
|
||||
@@ -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
|
||||
@@ -1180,18 +1180,21 @@ class ShortcutHandler:
|
||||
self.logger.warning(f"Skipping invalid shortcut entry at index {index} in {vdf_path}")
|
||||
continue
|
||||
|
||||
exe_path = shortcut_details.get('Exe', '').strip('"') # Get Exe path, remove quotes
|
||||
app_name = shortcut_details.get('AppName', 'Unknown Shortcut')
|
||||
exe_path = shortcut_details.get('Exe', shortcut_details.get('exe', '')).strip('"') # Get Exe path, remove quotes
|
||||
app_name = shortcut_details.get('AppName', shortcut_details.get('appname', 'Unknown Shortcut'))
|
||||
|
||||
# Check if the executable_name is present in the Exe path
|
||||
if executable_name in os.path.basename(exe_path):
|
||||
self.logger.info(f"Found matching shortcut '{app_name}' in {vdf_path}")
|
||||
# Extract relevant details
|
||||
# Extract relevant details with case-insensitive fallbacks
|
||||
app_id = shortcut_details.get('appid', shortcut_details.get('AppID', shortcut_details.get('appId', None)))
|
||||
start_dir = shortcut_details.get('StartDir', shortcut_details.get('startdir', '')).strip('"')
|
||||
|
||||
match = {
|
||||
'AppName': app_name,
|
||||
'Exe': exe_path, # Store unquoted path
|
||||
'StartDir': shortcut_details.get('StartDir', '').strip('"') # Unquoted
|
||||
# Add other useful fields if needed, e.g., 'ShortcutPath'
|
||||
'StartDir': start_dir,
|
||||
'appid': app_id # Include the AppID for conversion to unsigned
|
||||
}
|
||||
matching_shortcuts.append(match)
|
||||
else:
|
||||
|
||||
@@ -222,15 +222,21 @@ class ValidationHandler:
|
||||
def validate_steam_shortcut(self, app_id: str) -> Tuple[bool, str]:
|
||||
"""Validate a Steam shortcut."""
|
||||
try:
|
||||
# Check if shortcuts.vdf exists
|
||||
shortcuts_path = Path.home() / '.steam' / 'steam' / 'userdata' / '75424832' / 'config' / 'shortcuts.vdf'
|
||||
# Use native Steam service to get proper shortcuts.vdf path with multi-user support
|
||||
from jackify.backend.services.native_steam_service import NativeSteamService
|
||||
steam_service = NativeSteamService()
|
||||
shortcuts_path = steam_service.get_shortcuts_vdf_path()
|
||||
|
||||
if not shortcuts_path:
|
||||
return False, "Could not determine shortcuts.vdf path (no active Steam user found)"
|
||||
|
||||
if not shortcuts_path.exists():
|
||||
return False, "shortcuts.vdf not found"
|
||||
|
||||
|
||||
# Check if shortcuts.vdf is accessible
|
||||
if not os.access(shortcuts_path, os.R_OK | os.W_OK):
|
||||
return False, "shortcuts.vdf is not accessible"
|
||||
|
||||
|
||||
# Parse shortcuts.vdf using VDFHandler
|
||||
shortcuts_data = VDFHandler.load(str(shortcuts_path), binary=True)
|
||||
|
||||
|
||||
@@ -197,16 +197,43 @@ class WineUtils:
|
||||
logger.error(f"Error editing binary working paths: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _get_sd_card_mounts():
|
||||
"""
|
||||
Dynamically detect all current SD card mount points
|
||||
Returns list of mount point paths
|
||||
"""
|
||||
try:
|
||||
import subprocess
|
||||
result = subprocess.run(['df', '-h'], capture_output=True, text=True, timeout=5)
|
||||
sd_mounts = []
|
||||
for line in result.stdout.split('\n'):
|
||||
# Look for common SD card mount patterns
|
||||
if '/run/media' in line or ('/mnt' in line and 'sdcard' in line.lower()):
|
||||
parts = line.split()
|
||||
if len(parts) >= 6: # df output has 6+ columns
|
||||
mount_point = parts[-1] # Last column is mount point
|
||||
if mount_point.startswith(('/run/media', '/mnt')):
|
||||
sd_mounts.append(mount_point)
|
||||
return sd_mounts
|
||||
except Exception:
|
||||
# Fallback to common patterns if df fails
|
||||
return ['/run/media/mmcblk0p1', '/run/media/deck']
|
||||
|
||||
@staticmethod
|
||||
def _strip_sdcard_path(path):
|
||||
"""
|
||||
Strip /run/media/deck/UUID from SD card paths
|
||||
Internal helper method
|
||||
Strip any detected SD card mount prefix from paths
|
||||
Handles both /run/media/mmcblk0p1 and /run/media/deck/UUID patterns
|
||||
"""
|
||||
if path.startswith("/run/media/deck/"):
|
||||
parts = path.split("/", 5)
|
||||
if len(parts) >= 6:
|
||||
return "/" + parts[5]
|
||||
sd_mounts = WineUtils._get_sd_card_mounts()
|
||||
|
||||
for mount in sd_mounts:
|
||||
if path.startswith(mount):
|
||||
# Strip the mount prefix and ensure proper leading slash
|
||||
relative_path = path[len(mount):].lstrip('/')
|
||||
return "/" + relative_path if relative_path else "/"
|
||||
|
||||
return path
|
||||
|
||||
@staticmethod
|
||||
@@ -510,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
|
||||
|
||||
@@ -523,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
|
||||
|
||||
@@ -562,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
|
||||
@@ -609,12 +630,46 @@ class WineUtils:
|
||||
"""
|
||||
# Clean up the version string for directory matching
|
||||
version_patterns = [proton_version, proton_version.replace(' ', '_'), proton_version.replace(' ', '')]
|
||||
# Standard Steam library locations
|
||||
steam_common_paths = [
|
||||
Path.home() / ".steam/steam/steamapps/common",
|
||||
Path.home() / ".local/share/Steam/steamapps/common",
|
||||
Path.home() / ".steam/root/steamapps/common"
|
||||
]
|
||||
|
||||
# Get actual Steam library paths from libraryfolders.vdf (smart detection)
|
||||
steam_common_paths = []
|
||||
compatibility_paths = []
|
||||
|
||||
try:
|
||||
from .path_handler import PathHandler
|
||||
# Get root Steam library paths (without /steamapps/common suffix)
|
||||
root_steam_libs = PathHandler.get_all_steam_library_paths()
|
||||
for lib_path in root_steam_libs:
|
||||
lib = Path(lib_path)
|
||||
if lib.exists():
|
||||
# Valve Proton: {library}/steamapps/common
|
||||
common_path = lib / "steamapps/common"
|
||||
if common_path.exists():
|
||||
steam_common_paths.append(common_path)
|
||||
# GE-Proton: same Steam installation root + compatibilitytools.d
|
||||
compatibility_paths.append(lib / "compatibilitytools.d")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not detect Steam libraries from libraryfolders.vdf: {e}")
|
||||
|
||||
# Fallback locations if dynamic detection fails
|
||||
if not steam_common_paths:
|
||||
steam_common_paths = [
|
||||
Path.home() / ".steam/steam/steamapps/common",
|
||||
Path.home() / ".local/share/Steam/steamapps/common",
|
||||
Path.home() / ".steam/root/steamapps/common"
|
||||
]
|
||||
|
||||
if not compatibility_paths:
|
||||
compatibility_paths = [
|
||||
Path.home() / ".steam/steam/compatibilitytools.d",
|
||||
Path.home() / ".local/share/Steam/compatibilitytools.d"
|
||||
]
|
||||
|
||||
# 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"
|
||||
])
|
||||
# Special handling for Proton 9: try all possible directory names
|
||||
if proton_version.strip().startswith("Proton 9"):
|
||||
proton9_candidates = ["Proton 9.0", "Proton 9.0 (Beta)"]
|
||||
@@ -628,8 +683,9 @@ class WineUtils:
|
||||
wine_bin = subdir / "files/bin/wine"
|
||||
if wine_bin.is_file():
|
||||
return str(wine_bin)
|
||||
# General case: try version patterns
|
||||
for base_path in steam_common_paths:
|
||||
# General case: try version patterns in both steamapps and compatibilitytools.d
|
||||
all_paths = steam_common_paths + compatibility_paths
|
||||
for base_path in all_paths:
|
||||
if not base_path.is_dir():
|
||||
continue
|
||||
for pattern in version_patterns:
|
||||
@@ -647,7 +703,7 @@ class WineUtils:
|
||||
try:
|
||||
from .config_handler import ConfigHandler
|
||||
config = ConfigHandler()
|
||||
fallback_path = config.get('proton_path', 'auto')
|
||||
fallback_path = config.get_proton_path()
|
||||
if fallback_path != 'auto':
|
||||
fallback_wine_bin = Path(fallback_path) / "files/bin/wine"
|
||||
if fallback_wine_bin.is_file():
|
||||
@@ -716,19 +772,32 @@ class WineUtils:
|
||||
@staticmethod
|
||||
def get_steam_library_paths() -> List[Path]:
|
||||
"""
|
||||
Get all Steam library paths including standard locations.
|
||||
Get all Steam library paths from libraryfolders.vdf (handles Flatpak, custom locations, etc.).
|
||||
|
||||
Returns:
|
||||
List of Path objects for Steam library directories
|
||||
"""
|
||||
steam_paths = [
|
||||
Path.home() / ".steam/steam/steamapps/common",
|
||||
Path.home() / ".local/share/Steam/steamapps/common",
|
||||
Path.home() / ".steam/root/steamapps/common"
|
||||
]
|
||||
|
||||
# Return only existing paths
|
||||
return [path for path in steam_paths if path.exists()]
|
||||
try:
|
||||
from .path_handler import PathHandler
|
||||
# Use existing PathHandler that reads libraryfolders.vdf
|
||||
library_paths = PathHandler.get_all_steam_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
|
||||
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()]
|
||||
|
||||
@staticmethod
|
||||
def get_compatibility_tool_paths() -> List[Path]:
|
||||
|
||||
860
jackify/backend/handlers/winetricks_handler.py
Normal file
860
jackify/backend/handlers/winetricks_handler.py
Normal file
@@ -0,0 +1,860 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Winetricks Handler Module
|
||||
Handles wine component installation using bundled winetricks
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional, List
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WinetricksHandler:
|
||||
"""
|
||||
Handles wine component installation using bundled winetricks
|
||||
"""
|
||||
|
||||
def __init__(self, logger=None):
|
||||
self.logger = logger or logging.getLogger(__name__)
|
||||
self.winetricks_path = self._get_bundled_winetricks_path()
|
||||
|
||||
def _get_bundled_winetricks_path(self) -> Optional[str]:
|
||||
"""
|
||||
Get the path to the bundled winetricks script following AppImage best practices
|
||||
"""
|
||||
possible_paths = []
|
||||
|
||||
# AppImage environment - use APPDIR (standard AppImage best practice)
|
||||
if os.environ.get('APPDIR'):
|
||||
appdir_path = os.path.join(os.environ['APPDIR'], 'opt', 'jackify', 'tools', 'winetricks')
|
||||
possible_paths.append(appdir_path)
|
||||
|
||||
# Development environment - relative to module location
|
||||
module_dir = Path(__file__).parent.parent.parent # Go from handlers/ up to jackify/
|
||||
dev_path = module_dir / 'tools' / 'winetricks'
|
||||
possible_paths.append(str(dev_path))
|
||||
|
||||
# Try each path until we find one that works
|
||||
for path in possible_paths:
|
||||
if os.path.exists(path) and os.access(path, os.X_OK):
|
||||
self.logger.debug(f"Found bundled winetricks at: {path}")
|
||||
return str(path)
|
||||
|
||||
self.logger.error(f"Bundled winetricks not found. Tried paths: {possible_paths}")
|
||||
return None
|
||||
|
||||
def _get_bundled_cabextract(self) -> Optional[str]:
|
||||
"""
|
||||
Get the path to the bundled cabextract binary, checking same locations as winetricks
|
||||
"""
|
||||
possible_paths = []
|
||||
|
||||
# AppImage environment - same pattern as winetricks detection
|
||||
if os.environ.get('APPDIR'):
|
||||
appdir_path = os.path.join(os.environ['APPDIR'], 'opt', 'jackify', 'tools', 'cabextract')
|
||||
possible_paths.append(appdir_path)
|
||||
|
||||
# Development environment - relative to module location, same as winetricks
|
||||
module_dir = Path(__file__).parent.parent.parent # Go from handlers/ up to jackify/
|
||||
dev_path = module_dir / 'tools' / 'cabextract'
|
||||
possible_paths.append(str(dev_path))
|
||||
|
||||
# Try each path until we find one that works
|
||||
for path in possible_paths:
|
||||
if os.path.exists(path) and os.access(path, os.X_OK):
|
||||
self.logger.debug(f"Found bundled cabextract at: {path}")
|
||||
return str(path)
|
||||
|
||||
# Fallback to system PATH
|
||||
try:
|
||||
import shutil
|
||||
system_cabextract = shutil.which('cabextract')
|
||||
if system_cabextract:
|
||||
self.logger.debug(f"Using system cabextract: {system_cabextract}")
|
||||
return system_cabextract
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.logger.warning("Bundled cabextract not found in tools directory")
|
||||
return None
|
||||
|
||||
def is_available(self) -> bool:
|
||||
"""
|
||||
Check if winetricks is available and ready to use
|
||||
"""
|
||||
if not self.winetricks_path:
|
||||
self.logger.error("Bundled winetricks not found")
|
||||
return False
|
||||
|
||||
try:
|
||||
env = os.environ.copy()
|
||||
result = subprocess.run(
|
||||
[self.winetricks_path, '--version'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=env,
|
||||
timeout=10
|
||||
)
|
||||
if result.returncode == 0:
|
||||
self.logger.debug(f"Winetricks version: {result.stdout.strip()}")
|
||||
return True
|
||||
else:
|
||||
self.logger.error(f"Winetricks --version failed: {result.stderr}")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error testing winetricks: {e}")
|
||||
return False
|
||||
|
||||
def install_wine_components(self, wineprefix: str, game_var: str, specific_components: Optional[List[str]] = None) -> bool:
|
||||
"""
|
||||
Install the specified Wine components into the given prefix using winetricks.
|
||||
If specific_components is None, use the default set (fontsmooth=rgb, xact, xact_x64, vcrun2022).
|
||||
"""
|
||||
if not self.is_available():
|
||||
self.logger.error("Winetricks is not available")
|
||||
return False
|
||||
|
||||
env = os.environ.copy()
|
||||
env['WINEDEBUG'] = '-all' # Suppress Wine debug output
|
||||
env['WINEPREFIX'] = wineprefix
|
||||
env['WINETRICKS_GUI'] = 'none' # Suppress GUI popups
|
||||
# Less aggressive popup suppression - don't completely disable display
|
||||
if 'DISPLAY' in env:
|
||||
# Keep DISPLAY but add window manager hints to prevent focus stealing
|
||||
env['WINEDLLOVERRIDES'] = 'winemenubuilder.exe=d' # Disable Wine menu integration
|
||||
else:
|
||||
# No display available anyway
|
||||
env['DISPLAY'] = ''
|
||||
|
||||
# Force winetricks to use Proton wine binary - NEVER fall back to system wine
|
||||
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':
|
||||
# Check if user-selected Proton still exists
|
||||
if os.path.exists(user_proton_path):
|
||||
# Resolve symlinks to handle ~/.steam/steam -> ~/.local/share/Steam
|
||||
resolved_proton_path = os.path.realpath(user_proton_path)
|
||||
|
||||
# Check for wine binary in different Proton structures
|
||||
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
|
||||
self.logger.info(f"Using user-selected Proton: {user_proton_path}")
|
||||
elif os.path.exists(ge_proton_wine):
|
||||
wine_binary = ge_proton_wine
|
||||
self.logger.info(f"Using user-selected GE-Proton: {user_proton_path}")
|
||||
else:
|
||||
self.logger.warning(f"User-selected Proton path invalid: {user_proton_path}")
|
||||
else:
|
||||
self.logger.warning(f"User-selected Proton no longer exists: {user_proton_path}")
|
||||
|
||||
# Fall back to auto-detection if user selection failed or is 'auto'
|
||||
if not wine_binary:
|
||||
self.logger.info("Falling back to automatic Proton detection")
|
||||
best_proton = WineUtils.select_best_proton()
|
||||
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']}")
|
||||
|
||||
if not wine_binary:
|
||||
self.logger.error("Cannot run winetricks: No compatible Proton version found")
|
||||
return False
|
||||
|
||||
if not (os.path.exists(wine_binary) and os.access(wine_binary, os.X_OK)):
|
||||
self.logger.error(f"Cannot run winetricks: Wine binary not found or not executable: {wine_binary}")
|
||||
return False
|
||||
|
||||
env['WINE'] = str(wine_binary)
|
||||
self.logger.info(f"Using Proton wine binary for winetricks: {wine_binary}")
|
||||
|
||||
# CRITICAL: Set up protontricks-compatible environment
|
||||
proton_dist_path = os.path.dirname(os.path.dirname(wine_binary)) # e.g., /path/to/proton/dist/bin/wine -> /path/to/proton/dist
|
||||
self.logger.debug(f"Proton dist path: {proton_dist_path}")
|
||||
|
||||
# Set WINEDLLPATH like protontricks does
|
||||
env['WINEDLLPATH'] = f"{proton_dist_path}/lib64/wine:{proton_dist_path}/lib/wine"
|
||||
|
||||
# Ensure Proton bin directory is first in PATH
|
||||
env['PATH'] = f"{proton_dist_path}/bin:{env.get('PATH', '')}"
|
||||
|
||||
# Set DLL overrides exactly like protontricks
|
||||
dll_overrides = {
|
||||
"beclient": "b,n",
|
||||
"beclient_x64": "b,n",
|
||||
"dxgi": "n",
|
||||
"d3d9": "n",
|
||||
"d3d10core": "n",
|
||||
"d3d11": "n",
|
||||
"d3d12": "n",
|
||||
"d3d12core": "n",
|
||||
"nvapi": "n",
|
||||
"nvapi64": "n",
|
||||
"nvofapi64": "n",
|
||||
"nvcuda": "b"
|
||||
}
|
||||
|
||||
# Merge with existing overrides
|
||||
existing_overrides = env.get('WINEDLLOVERRIDES', '')
|
||||
if existing_overrides:
|
||||
# Parse existing overrides
|
||||
for override in existing_overrides.split(';'):
|
||||
if '=' in override:
|
||||
name, value = override.split('=', 1)
|
||||
dll_overrides[name] = value
|
||||
|
||||
env['WINEDLLOVERRIDES'] = ';'.join(f"{name}={setting}" for name, setting in dll_overrides.items())
|
||||
|
||||
# Set Wine defaults from protontricks
|
||||
env['WINE_LARGE_ADDRESS_AWARE'] = '1'
|
||||
env['DXVK_ENABLE_NVAPI'] = '1'
|
||||
|
||||
self.logger.debug(f"Set protontricks environment: WINEDLLPATH={env['WINEDLLPATH']}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Cannot run winetricks: Failed to get Proton wine binary: {e}")
|
||||
return False
|
||||
|
||||
# Set up bundled cabextract for winetricks
|
||||
bundled_cabextract = self._get_bundled_cabextract()
|
||||
if bundled_cabextract:
|
||||
env['PATH'] = f"{os.path.dirname(bundled_cabextract)}:{env.get('PATH', '')}"
|
||||
self.logger.info(f"Using bundled cabextract: {bundled_cabextract}")
|
||||
else:
|
||||
self.logger.warning("Bundled cabextract not found, relying on system PATH")
|
||||
|
||||
# Set winetricks cache to jackify_data_dir for self-containment
|
||||
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)
|
||||
|
||||
if specific_components is not None:
|
||||
all_components = specific_components
|
||||
self.logger.info(f"Installing specific components: {all_components}")
|
||||
else:
|
||||
all_components = ["fontsmooth=rgb", "xact", "xact_x64", "vcrun2022"]
|
||||
self.logger.info(f"Installing default components: {all_components}")
|
||||
|
||||
if not all_components:
|
||||
self.logger.info("No Wine components to install.")
|
||||
return True
|
||||
|
||||
# Reorder components for proper installation sequence
|
||||
components_to_install = self._reorder_components_for_installation(all_components)
|
||||
self.logger.info(f"WINEPREFIX: {wineprefix}, Game: {game_var}, Ordered Components: {components_to_install}")
|
||||
|
||||
# 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)
|
||||
|
||||
# Choose installation method based on user preference and components
|
||||
if use_winetricks and "dotnet40" in components_to_install:
|
||||
self.logger.info("Using optimized approach: protontricks for dotnet40 (reliable), winetricks for other components (fast)")
|
||||
return self._install_components_hybrid_approach(components_to_install, wineprefix, game_var)
|
||||
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
|
||||
for attempt in range(1, max_attempts + 1):
|
||||
if attempt > 1:
|
||||
self.logger.warning(f"Retrying component installation (attempt {attempt}/{max_attempts})...")
|
||||
self._cleanup_wine_processes()
|
||||
|
||||
try:
|
||||
# Build winetricks command - using --unattended for silent installation
|
||||
cmd = [self.winetricks_path, '--unattended'] + components_to_install
|
||||
|
||||
self.logger.debug(f"Running: {' '.join(cmd)}")
|
||||
self.logger.debug(f"Environment WINE={env.get('WINE', 'NOT SET')}")
|
||||
self.logger.debug(f"Environment DISPLAY={env.get('DISPLAY', 'NOT SET')}")
|
||||
self.logger.debug(f"Environment WINEPREFIX={env.get('WINEPREFIX', 'NOT SET')}")
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
env=env,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
timeout=600
|
||||
)
|
||||
|
||||
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)
|
||||
if "dotnet40" in components_to_install and "ngen.exe not found" in result.stderr:
|
||||
self.logger.warning("dotnet40 verification warning (common in Steam Proton prefixes)")
|
||||
self.logger.info("Checking if dotnet40 was actually installed...")
|
||||
|
||||
# Check if dotnet40 appears in winetricks.log (indicates successful installation)
|
||||
log_path = os.path.join(wineprefix, 'winetricks.log')
|
||||
if os.path.exists(log_path):
|
||||
try:
|
||||
with open(log_path, 'r') as f:
|
||||
log_content = f.read()
|
||||
if 'dotnet40' in log_content:
|
||||
self.logger.info("dotnet40 found in winetricks.log - installation succeeded despite verification warning")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Could not read winetricks.log: {e}")
|
||||
|
||||
self.logger.error(f"Winetricks command failed (Attempt {attempt}/{max_attempts}). Return Code: {result.returncode}")
|
||||
self.logger.error(f"Stdout: {result.stdout.strip()}")
|
||||
self.logger.error(f"Stderr: {result.stderr.strip()}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error during winetricks run (Attempt {attempt}/{max_attempts}): {e}", exc_info=True)
|
||||
|
||||
self.logger.error(f"Failed to install Wine components after {max_attempts} attempts.")
|
||||
return False
|
||||
|
||||
def _reorder_components_for_installation(self, components: list) -> list:
|
||||
"""
|
||||
Reorder components for proper installation sequence.
|
||||
Critical: dotnet40 must be installed before dotnet6/dotnet7 to avoid conflicts.
|
||||
"""
|
||||
# Simple reordering: dotnet40 first, then everything else
|
||||
reordered = []
|
||||
|
||||
# Add dotnet40 first if it exists
|
||||
if "dotnet40" in components:
|
||||
reordered.append("dotnet40")
|
||||
|
||||
# Add all other components in original order
|
||||
for component in components:
|
||||
if component != "dotnet40":
|
||||
reordered.append(component)
|
||||
|
||||
if reordered != components:
|
||||
self.logger.info(f"Reordered for dotnet40 compatibility: {reordered}")
|
||||
|
||||
return reordered
|
||||
|
||||
def _prepare_prefix_for_dotnet(self, wineprefix: str, wine_binary: str) -> bool:
|
||||
"""
|
||||
Prepare the Wine prefix for .NET installation by mimicking protontricks preprocessing.
|
||||
This removes mono components and specific symlinks that interfere with .NET installation.
|
||||
"""
|
||||
try:
|
||||
env = os.environ.copy()
|
||||
env['WINEDEBUG'] = '-all'
|
||||
env['WINEPREFIX'] = wineprefix
|
||||
|
||||
# Step 1: Remove mono components (mimics protontricks behavior)
|
||||
self.logger.info("Preparing prefix for .NET installation: removing mono")
|
||||
mono_result = subprocess.run([
|
||||
self.winetricks_path,
|
||||
'-q',
|
||||
'remove_mono'
|
||||
], env=env, capture_output=True, text=True, timeout=300)
|
||||
|
||||
if mono_result.returncode != 0:
|
||||
self.logger.warning(f"Mono removal warning (non-critical): {mono_result.stderr}")
|
||||
|
||||
# Step 2: Set Windows version to XP (protontricks uses winxp for dotnet40)
|
||||
self.logger.info("Setting Windows version to XP for .NET compatibility")
|
||||
winxp_result = subprocess.run([
|
||||
self.winetricks_path,
|
||||
'-q',
|
||||
'winxp'
|
||||
], env=env, capture_output=True, text=True, timeout=300)
|
||||
|
||||
if winxp_result.returncode != 0:
|
||||
self.logger.warning(f"Windows XP setting warning: {winxp_result.stderr}")
|
||||
|
||||
# Step 3: Remove mscoree.dll symlinks (critical for .NET installation)
|
||||
self.logger.info("Removing problematic mscoree.dll symlinks")
|
||||
dosdevices_path = os.path.join(wineprefix, 'dosdevices', 'c:')
|
||||
mscoree_paths = [
|
||||
os.path.join(dosdevices_path, 'windows', 'syswow64', 'mscoree.dll'),
|
||||
os.path.join(dosdevices_path, 'windows', 'system32', 'mscoree.dll')
|
||||
]
|
||||
|
||||
for dll_path in mscoree_paths:
|
||||
if os.path.exists(dll_path) or os.path.islink(dll_path):
|
||||
try:
|
||||
os.remove(dll_path)
|
||||
self.logger.debug(f"Removed symlink: {dll_path}")
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Could not remove {dll_path}: {e}")
|
||||
|
||||
self.logger.info("Prefix preparation complete for .NET installation")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error preparing prefix for .NET: {e}")
|
||||
return False
|
||||
|
||||
def _install_components_separately(self, components: list, wineprefix: str, wine_binary: str, base_env: dict) -> bool:
|
||||
"""
|
||||
Install components separately like protontricks does.
|
||||
This is necessary when dotnet40 is present to avoid component conflicts.
|
||||
"""
|
||||
self.logger.info(f"Installing {len(components)} components separately (protontricks style)")
|
||||
|
||||
for i, component in enumerate(components, 1):
|
||||
self.logger.info(f"Installing component {i}/{len(components)}: {component}")
|
||||
|
||||
# Prepare environment for this component
|
||||
env = base_env.copy()
|
||||
|
||||
# Special preprocessing for dotnet40 only
|
||||
if component == "dotnet40":
|
||||
self.logger.info("Applying dotnet40 preprocessing")
|
||||
if not self._prepare_prefix_for_dotnet(wineprefix, wine_binary):
|
||||
self.logger.error("Failed to prepare prefix for dotnet40")
|
||||
return False
|
||||
else:
|
||||
# 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")
|
||||
|
||||
# Install this component
|
||||
max_attempts = 3
|
||||
component_success = False
|
||||
|
||||
for attempt in range(1, max_attempts + 1):
|
||||
if attempt > 1:
|
||||
self.logger.warning(f"Retrying {component} installation (attempt {attempt}/{max_attempts})")
|
||||
self._cleanup_wine_processes()
|
||||
|
||||
try:
|
||||
cmd = [self.winetricks_path, '--unattended', component]
|
||||
env['WINEPREFIX'] = wineprefix
|
||||
env['WINE'] = wine_binary
|
||||
|
||||
self.logger.debug(f"Running: {' '.join(cmd)}")
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=600
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
self.logger.info(f"✓ {component} installed successfully")
|
||||
component_success = True
|
||||
break
|
||||
else:
|
||||
# Special handling for dotnet40 verification issue
|
||||
if component == "dotnet40" and "ngen.exe not found" in result.stderr:
|
||||
self.logger.warning("dotnet40 verification warning (expected in Steam Proton)")
|
||||
|
||||
# Check winetricks.log for actual success
|
||||
log_path = os.path.join(wineprefix, 'winetricks.log')
|
||||
if os.path.exists(log_path):
|
||||
try:
|
||||
with open(log_path, 'r') as f:
|
||||
if 'dotnet40' in f.read():
|
||||
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.debug(f"Full stdout for {component}: {result.stdout.strip()}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error installing {component} (attempt {attempt}): {e}")
|
||||
|
||||
if not component_success:
|
||||
self.logger.error(f"Failed to install {component} after {max_attempts} attempts")
|
||||
return False
|
||||
|
||||
self.logger.info("✓ All components installed successfully using separate sessions")
|
||||
# Set Windows 10 mode after all component installation (matches legacy script timing)
|
||||
self._set_windows_10_mode(wineprefix, env.get('WINE', ''))
|
||||
return True
|
||||
|
||||
def _install_components_hybrid_approach(self, components: list, wineprefix: str, game_var: str) -> bool:
|
||||
"""
|
||||
Hybrid approach: Install dotnet40 with protontricks (known to work),
|
||||
then install remaining components with winetricks (faster for other components).
|
||||
|
||||
Args:
|
||||
components: List of all components to install
|
||||
wineprefix: Wine prefix path
|
||||
game_var: Game variable for AppID detection
|
||||
|
||||
Returns:
|
||||
bool: True if all installations succeeded, False otherwise
|
||||
"""
|
||||
self.logger.info("Starting hybrid installation approach")
|
||||
|
||||
# Separate dotnet40 (protontricks) from other components (winetricks)
|
||||
protontricks_components = [comp for comp in components if comp == "dotnet40"]
|
||||
other_components = [comp for comp in components if comp != "dotnet40"]
|
||||
|
||||
self.logger.info(f"Protontricks components: {protontricks_components}")
|
||||
self.logger.info(f"Other components: {other_components}")
|
||||
|
||||
# Step 1: Install dotnet40 with protontricks if present
|
||||
if protontricks_components:
|
||||
self.logger.info(f"Installing {protontricks_components} using protontricks...")
|
||||
if not self._install_dotnet40_with_protontricks(wineprefix, game_var):
|
||||
self.logger.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 with winetricks if any
|
||||
if other_components:
|
||||
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)
|
||||
|
||||
self.logger.info("✓ Hybrid component installation completed successfully")
|
||||
# Set Windows 10 mode after all component installation (matches legacy script timing)
|
||||
wine_binary = self._get_wine_binary_for_prefix(wineprefix)
|
||||
self._set_windows_10_mode(wineprefix, wine_binary)
|
||||
return True
|
||||
|
||||
def _install_dotnet40_with_protontricks(self, wineprefix: str, game_var: str) -> bool:
|
||||
"""
|
||||
Install dotnet40 using protontricks (known to work reliably).
|
||||
|
||||
Args:
|
||||
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("Protontricks not available for dotnet40 installation")
|
||||
return False
|
||||
|
||||
# Install dotnet40 using protontricks
|
||||
success = protontricks_handler.install_wine_components(appid, game_var, ["dotnet40"])
|
||||
|
||||
if success:
|
||||
self.logger.info("✓ dotnet40 installed successfully with protontricks")
|
||||
return True
|
||||
else:
|
||||
self.logger.error("✗ dotnet40 installation failed with protontricks")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error installing dotnet40 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
|
||||
Only cleanup winetricks processes - NEVER kill all wine processes
|
||||
"""
|
||||
try:
|
||||
# Only cleanup winetricks processes - do NOT kill other wine apps
|
||||
subprocess.run("pkill -f winetricks", shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
self.logger.debug("Cleaned up winetricks processes only")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error cleaning up winetricks processes: {e}")
|
||||
@@ -39,14 +39,26 @@ 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
|
||||
if modlist_name and modlist_name.lower() == 'lorerim':
|
||||
lorerim_proton = self._get_lorerim_preferred_proton()
|
||||
if lorerim_proton:
|
||||
logger.info(f"Lorerim detected: Using {lorerim_proton} instead of user settings")
|
||||
self._store_proton_override_notification("Lorerim", lorerim_proton)
|
||||
return lorerim_proton
|
||||
|
||||
config_handler = ConfigHandler()
|
||||
user_proton_path = config_handler.get('proton_path', 'auto')
|
||||
user_proton_path = config_handler.get_game_proton_path()
|
||||
|
||||
if user_proton_path == 'auto':
|
||||
# Use enhanced fallback logic with GE-Proton preference
|
||||
@@ -125,8 +137,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(
|
||||
@@ -333,15 +345,18 @@ class AutomatedPrefixService:
|
||||
logger.error(f"Steam userdata directory not found: {userdata_dir}")
|
||||
return None
|
||||
|
||||
# Find user directories (excluding user 0 which is a system account)
|
||||
user_dirs = [d for d in userdata_dir.iterdir() if d.is_dir() and d.name.isdigit() and d.name != "0"]
|
||||
if not user_dirs:
|
||||
logger.error("No valid Steam user directories found in userdata (user 0 is not valid)")
|
||||
# Use NativeSteamService for proper user detection
|
||||
from ..services.native_steam_service import NativeSteamService
|
||||
steam_service = NativeSteamService()
|
||||
|
||||
if not steam_service.find_steam_user():
|
||||
logger.error("Could not detect Steam user for shortcuts")
|
||||
return None
|
||||
|
||||
shortcuts_path = steam_service.get_shortcuts_vdf_path()
|
||||
if not shortcuts_path:
|
||||
logger.error("Could not get shortcuts.vdf path from Steam service")
|
||||
return None
|
||||
|
||||
# Use the first valid user directory found
|
||||
user_dir = user_dirs[0]
|
||||
shortcuts_path = user_dir / "config" / "shortcuts.vdf"
|
||||
|
||||
logger.debug(f"Looking for shortcuts.vdf at: {shortcuts_path}")
|
||||
if not shortcuts_path.exists():
|
||||
@@ -448,7 +463,7 @@ exit"""
|
||||
|
||||
if shortcut_name in name:
|
||||
appid = shortcut.get('appid')
|
||||
exe_path = shortcut.get('Exe', '')
|
||||
exe_path = shortcut.get('Exe', '').strip('"')
|
||||
|
||||
logger.info(f"Found shortcut: {name}")
|
||||
logger.info(f" AppID: {appid}")
|
||||
@@ -484,7 +499,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:
|
||||
@@ -1553,6 +1568,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
|
||||
|
||||
@@ -1759,21 +1777,15 @@ echo Prefix creation complete.
|
||||
progress_callback("=== Steam Integration ===")
|
||||
progress_callback(f"{self._get_progress_timestamp()} Creating Steam shortcut with native service")
|
||||
|
||||
# Dual approach: Registry injection for FNV, launch options for Enderal
|
||||
# Registry injection approach for both FNV and Enderal
|
||||
from ..handlers.modlist_handler import ModlistHandler
|
||||
modlist_handler = ModlistHandler()
|
||||
special_game_type = modlist_handler.detect_special_game_type(modlist_install_dir)
|
||||
|
||||
# Generate launch options only for Enderal (FNV uses registry injection)
|
||||
|
||||
# No launch options needed - both FNV and Enderal use registry injection
|
||||
custom_launch_options = None
|
||||
if special_game_type == "enderal":
|
||||
custom_launch_options = self._generate_special_game_launch_options(special_game_type, modlist_install_dir)
|
||||
if not custom_launch_options:
|
||||
logger.error(f"Failed to generate launch options for Enderal modlist")
|
||||
return False, None, None, None
|
||||
logger.info("Using launch options approach for Enderal modlist")
|
||||
elif special_game_type == "fnv":
|
||||
logger.info("Using registry injection approach for FNV modlist")
|
||||
if special_game_type in ["fnv", "enderal"]:
|
||||
logger.info(f"Using registry injection approach for {special_game_type.upper()} modlist")
|
||||
else:
|
||||
logger.debug("Standard modlist - no special game handling needed")
|
||||
|
||||
@@ -1849,23 +1861,19 @@ echo Prefix creation complete.
|
||||
if progress_callback:
|
||||
progress_callback(f"{self._get_progress_timestamp()} Setup verification completed")
|
||||
|
||||
# Step 5: Inject game registry entries for FNV modlists (Enderal uses launch options)
|
||||
# Step 5: Inject game registry entries for FNV and Enderal modlists
|
||||
# Get prefix path (needed for logging regardless of game type)
|
||||
prefix_path = self.get_prefix_path(appid)
|
||||
|
||||
if special_game_type == "fnv":
|
||||
logger.info("Step 5: Injecting FNV game registry entries")
|
||||
|
||||
if special_game_type in ["fnv", "enderal"]:
|
||||
logger.info(f"Step 5: Injecting {special_game_type.upper()} game registry entries")
|
||||
if progress_callback:
|
||||
progress_callback(f"{self._get_progress_timestamp()} Injecting FNV game registry entries...")
|
||||
|
||||
progress_callback(f"{self._get_progress_timestamp()} Injecting {special_game_type.upper()} game registry entries...")
|
||||
|
||||
if prefix_path:
|
||||
self._inject_game_registry_entries(str(prefix_path))
|
||||
else:
|
||||
logger.warning("Could not find prefix path for registry injection")
|
||||
elif special_game_type == "enderal":
|
||||
logger.info("Step 5: Skipping registry injection for Enderal (using launch options)")
|
||||
if progress_callback:
|
||||
progress_callback(f"{self._get_progress_timestamp()} Skipping registry injection for Enderal")
|
||||
else:
|
||||
logger.info("Step 5: Skipping registry injection for standard modlist")
|
||||
if progress_callback:
|
||||
@@ -1876,6 +1884,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
|
||||
|
||||
@@ -2537,15 +2550,31 @@ echo Prefix creation complete.
|
||||
Returns:
|
||||
Path to localconfig.vdf or None if not found
|
||||
"""
|
||||
# Try the standard Steam userdata path
|
||||
steam_userdata_path = Path.home() / ".steam" / "steam" / "userdata"
|
||||
if steam_userdata_path.exists():
|
||||
# Find user directories (excluding user 0 which is a system account)
|
||||
user_dirs = [d for d in steam_userdata_path.iterdir() if d.is_dir() and d.name.isdigit() and d.name != "0"]
|
||||
if user_dirs:
|
||||
localconfig_path = user_dirs[0] / "config" / "localconfig.vdf"
|
||||
# Use NativeSteamService for proper user detection
|
||||
try:
|
||||
from ..services.native_steam_service import NativeSteamService
|
||||
steam_service = NativeSteamService()
|
||||
|
||||
if steam_service.find_steam_user():
|
||||
localconfig_path = steam_service.user_config_path / "localconfig.vdf"
|
||||
if localconfig_path.exists():
|
||||
return str(localconfig_path)
|
||||
except Exception as e:
|
||||
logger.error(f"Error using Steam service for localconfig.vdf detection: {e}")
|
||||
|
||||
# Fallback to manual detection
|
||||
steam_userdata_path = Path.home() / ".steam" / "steam" / "userdata"
|
||||
if steam_userdata_path.exists():
|
||||
user_dirs = [d for d in steam_userdata_path.iterdir() if d.is_dir() and d.name.isdigit() and d.name != "0"]
|
||||
if user_dirs:
|
||||
# Use most recently modified directory as fallback
|
||||
try:
|
||||
most_recent = max(user_dirs, key=lambda d: d.stat().st_mtime)
|
||||
localconfig_path = most_recent / "config" / "localconfig.vdf"
|
||||
if localconfig_path.exists():
|
||||
return str(localconfig_path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
logger.error("Could not find localconfig.vdf")
|
||||
return None
|
||||
@@ -2690,31 +2719,64 @@ echo Prefix creation complete.
|
||||
return False
|
||||
|
||||
def _find_proton_binary(self, proton_common_dir: Path) -> Optional[Path]:
|
||||
"""Locate a Proton wrapper script to use (prefer Experimental)."""
|
||||
candidates = []
|
||||
preferred = [
|
||||
"Proton - Experimental",
|
||||
"Proton 9.0",
|
||||
"Proton 8.0",
|
||||
"Proton Hotfix",
|
||||
]
|
||||
|
||||
for name in preferred:
|
||||
p = proton_common_dir / name / "proton"
|
||||
if p.exists():
|
||||
candidates.append(p)
|
||||
|
||||
# As a fallback, scan all Proton* dirs
|
||||
if not candidates and proton_common_dir.exists():
|
||||
for p in proton_common_dir.glob("Proton*/proton"):
|
||||
candidates.append(p)
|
||||
|
||||
if not candidates:
|
||||
logger.error("No Proton wrapper found under steamapps/common")
|
||||
"""Locate a Proton wrapper script to use, respecting user's configuration."""
|
||||
try:
|
||||
from jackify.backend.handlers.config_handler import ConfigHandler
|
||||
from jackify.backend.handlers.wine_utils import WineUtils
|
||||
|
||||
config = ConfigHandler()
|
||||
user_proton_path = config.get_game_proton_path()
|
||||
|
||||
# If user selected a specific Proton, try that first
|
||||
if user_proton_path != 'auto':
|
||||
# Resolve symlinks to handle ~/.steam/steam -> ~/.local/share/Steam
|
||||
resolved_proton_path = os.path.realpath(user_proton_path)
|
||||
|
||||
# Check for wine binary in different Proton structures
|
||||
valve_proton_wine = Path(resolved_proton_path) / "dist" / "bin" / "wine"
|
||||
ge_proton_wine = Path(resolved_proton_path) / "files" / "bin" / "wine"
|
||||
|
||||
if valve_proton_wine.exists() or ge_proton_wine.exists():
|
||||
# Found user's Proton, now find the proton wrapper script
|
||||
proton_wrapper = Path(resolved_proton_path) / "proton"
|
||||
if proton_wrapper.exists():
|
||||
logger.info(f"Using user-selected Proton wrapper: {proton_wrapper}")
|
||||
return proton_wrapper
|
||||
else:
|
||||
logger.warning(f"User-selected Proton missing wrapper script: {proton_wrapper}")
|
||||
else:
|
||||
logger.warning(f"User-selected Proton path invalid: {user_proton_path}")
|
||||
|
||||
# Fall back to auto-detection
|
||||
logger.info("Falling back to automatic Proton detection")
|
||||
candidates = []
|
||||
preferred = [
|
||||
"Proton - Experimental",
|
||||
"Proton 9.0",
|
||||
"Proton 8.0",
|
||||
"Proton Hotfix",
|
||||
]
|
||||
|
||||
for name in preferred:
|
||||
p = proton_common_dir / name / "proton"
|
||||
if p.exists():
|
||||
candidates.append(p)
|
||||
|
||||
# As a fallback, scan all Proton* dirs
|
||||
if not candidates and proton_common_dir.exists():
|
||||
for p in proton_common_dir.glob("Proton*/proton"):
|
||||
candidates.append(p)
|
||||
|
||||
if not candidates:
|
||||
logger.error("No Proton wrapper found under steamapps/common")
|
||||
return None
|
||||
|
||||
logger.info(f"Using auto-detected Proton wrapper: {candidates[0]}")
|
||||
return candidates[0]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error finding Proton binary: {e}")
|
||||
return None
|
||||
|
||||
logger.info(f"Using Proton wrapper: {candidates[0]}")
|
||||
return candidates[0]
|
||||
|
||||
def replace_existing_shortcut(self, shortcut_name: str, exe_path: str, modlist_install_dir: str) -> Tuple[bool, Optional[int]]:
|
||||
"""
|
||||
@@ -2948,10 +3010,107 @@ echo Prefix creation complete.
|
||||
)
|
||||
if success:
|
||||
logger.info(f"Updated registry entry for {config['name']}")
|
||||
|
||||
# Special handling for Enderal: Create required user directory
|
||||
if app_id == "976620": # Enderal Special Edition
|
||||
try:
|
||||
enderal_docs_path = os.path.join(modlist_compatdata_path, "pfx", "drive_c", "users", "steamuser", "Documents", "My Games", "Enderal Special Edition")
|
||||
os.makedirs(enderal_docs_path, exist_ok=True)
|
||||
logger.info(f"Created Enderal user directory: {enderal_docs_path}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to create Enderal user directory: {e}")
|
||||
else:
|
||||
logger.warning(f"Failed to update registry entry for {config['name']}")
|
||||
else:
|
||||
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}")
|
||||
|
||||
|
||||
|
||||
@@ -293,15 +293,7 @@ class ModlistService:
|
||||
elif context.get('machineid'):
|
||||
cmd += ['-m', context['machineid']]
|
||||
cmd += ['-o', install_dir_str, '-d', download_dir_str]
|
||||
|
||||
# Check for debug mode and add --debug flag
|
||||
from ..handlers.config_handler import ConfigHandler
|
||||
config_handler = ConfigHandler()
|
||||
debug_mode = config_handler.get('debug_mode', False)
|
||||
if debug_mode:
|
||||
cmd.append('--debug')
|
||||
logger.debug("DEBUG: Added --debug flag to jackify-engine command")
|
||||
|
||||
|
||||
# NOTE: API key is passed via environment variable only, not as command line argument
|
||||
|
||||
# Store original environment values (copied from working code)
|
||||
@@ -639,6 +631,10 @@ class ModlistService:
|
||||
'skip_confirmation': True, # Service layer should be non-interactive
|
||||
'manual_steps_completed': False
|
||||
}
|
||||
|
||||
# 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)
|
||||
|
||||
185
jackify/backend/services/native_steam_operations_service.py
Normal file
185
jackify/backend/services/native_steam_operations_service.py
Normal file
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Native Steam Operations Service
|
||||
|
||||
This service provides direct Steam operations using VDF parsing and path discovery.
|
||||
Replaces protontricks dependencies with native Steam functionality.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
import vdf
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, List
|
||||
import subprocess
|
||||
import shutil
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NativeSteamOperationsService:
|
||||
"""
|
||||
Service providing native Steam operations for shortcut discovery and prefix management.
|
||||
|
||||
Replaces protontricks functionality with:
|
||||
- Direct VDF parsing for shortcut discovery
|
||||
- Native compatdata path construction
|
||||
- Direct Steam library detection
|
||||
"""
|
||||
|
||||
def __init__(self, steamdeck: bool = False):
|
||||
self.steamdeck = steamdeck
|
||||
self.logger = logger
|
||||
|
||||
def list_non_steam_shortcuts(self) -> Dict[str, str]:
|
||||
"""
|
||||
List non-Steam shortcuts via direct VDF parsing.
|
||||
|
||||
Returns:
|
||||
Dict mapping shortcut name to AppID string
|
||||
"""
|
||||
logger.info("Listing non-Steam shortcuts via native VDF parsing...")
|
||||
shortcuts = {}
|
||||
|
||||
try:
|
||||
# Find all possible shortcuts.vdf locations
|
||||
shortcuts_paths = self._find_shortcuts_vdf_paths()
|
||||
|
||||
for shortcuts_path in shortcuts_paths:
|
||||
logger.debug(f"Checking shortcuts.vdf at: {shortcuts_path}")
|
||||
|
||||
if not shortcuts_path.exists():
|
||||
continue
|
||||
|
||||
try:
|
||||
with open(shortcuts_path, 'rb') as f:
|
||||
data = vdf.binary_load(f)
|
||||
|
||||
shortcuts_data = data.get('shortcuts', {})
|
||||
for shortcut_key, shortcut_data in shortcuts_data.items():
|
||||
if isinstance(shortcut_data, dict):
|
||||
app_name = shortcut_data.get('AppName', '').strip()
|
||||
app_id = shortcut_data.get('appid', '')
|
||||
|
||||
if app_name and app_id:
|
||||
# Convert to positive AppID string (compatible format)
|
||||
positive_appid = str(abs(int(app_id)))
|
||||
shortcuts[app_name] = positive_appid
|
||||
logger.debug(f"Found non-Steam shortcut: '{app_name}' with AppID {positive_appid}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error reading shortcuts.vdf at {shortcuts_path}: {e}")
|
||||
continue
|
||||
|
||||
if not shortcuts:
|
||||
logger.warning("No non-Steam shortcuts found in any shortcuts.vdf")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing non-Steam shortcuts: {e}")
|
||||
|
||||
return shortcuts
|
||||
|
||||
def set_steam_permissions(self, modlist_dir: str, steamdeck: bool = False) -> bool:
|
||||
"""
|
||||
Handle Steam access permissions for native operations.
|
||||
|
||||
Since we're using direct file access, no special permissions needed.
|
||||
|
||||
Args:
|
||||
modlist_dir: Modlist directory path (for future use)
|
||||
steamdeck: Steam Deck flag (for future use)
|
||||
|
||||
Returns:
|
||||
Always True (no permissions needed for native operations)
|
||||
"""
|
||||
logger.debug("Using native Steam operations, no permission setting needed")
|
||||
return True
|
||||
|
||||
def get_wine_prefix_path(self, appid: str) -> Optional[str]:
|
||||
"""
|
||||
Get WINEPREFIX path via direct compatdata discovery.
|
||||
|
||||
Args:
|
||||
appid: Steam AppID string
|
||||
|
||||
Returns:
|
||||
WINEPREFIX path string or None if not found
|
||||
"""
|
||||
logger.debug(f"Getting WINEPREFIX for AppID {appid} using native path discovery")
|
||||
|
||||
try:
|
||||
# Find all possible compatdata locations
|
||||
compatdata_paths = self._find_compatdata_paths()
|
||||
|
||||
for compatdata_base in compatdata_paths:
|
||||
prefix_path = compatdata_base / appid / "pfx"
|
||||
logger.debug(f"Checking prefix path: {prefix_path}")
|
||||
|
||||
if prefix_path.exists():
|
||||
logger.debug(f"Found WINEPREFIX: {prefix_path}")
|
||||
return str(prefix_path)
|
||||
|
||||
logger.error(f"WINEPREFIX not found for AppID {appid} in any compatdata location")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting WINEPREFIX for AppID {appid}: {e}")
|
||||
return None
|
||||
|
||||
def _find_shortcuts_vdf_paths(self) -> List[Path]:
|
||||
"""Find all possible shortcuts.vdf file locations"""
|
||||
paths = []
|
||||
|
||||
# Standard Steam locations
|
||||
steam_locations = [
|
||||
Path.home() / ".steam/steam",
|
||||
Path.home() / ".local/share/Steam",
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam/home/.steam/steam",
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam/home/.local/share/Steam"
|
||||
]
|
||||
|
||||
for steam_root in steam_locations:
|
||||
if not steam_root.exists():
|
||||
continue
|
||||
|
||||
# Find userdata directories
|
||||
userdata_path = steam_root / "userdata"
|
||||
if userdata_path.exists():
|
||||
for user_dir in userdata_path.iterdir():
|
||||
if user_dir.is_dir() and user_dir.name.isdigit():
|
||||
shortcuts_path = user_dir / "config" / "shortcuts.vdf"
|
||||
paths.append(shortcuts_path)
|
||||
|
||||
return paths
|
||||
|
||||
def _find_compatdata_paths(self) -> List[Path]:
|
||||
"""Find all possible compatdata directory locations"""
|
||||
paths = []
|
||||
|
||||
# Standard compatdata locations
|
||||
standard_locations = [
|
||||
Path.home() / ".steam/steam/steamapps/compatdata",
|
||||
Path.home() / ".local/share/Steam/steamapps/compatdata",
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam/home/.steam/steam/steamapps/compatdata",
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam/home/.local/share/Steam/steamapps/compatdata"
|
||||
]
|
||||
|
||||
for path in standard_locations:
|
||||
if path.exists():
|
||||
paths.append(path)
|
||||
|
||||
# Also check additional Steam libraries via libraryfolders.vdf
|
||||
try:
|
||||
from jackify.shared.paths import PathHandler
|
||||
all_steam_libs = PathHandler.get_all_steam_library_paths()
|
||||
|
||||
for lib_path in all_steam_libs:
|
||||
compatdata_path = lib_path / "steamapps" / "compatdata"
|
||||
if compatdata_path.exists():
|
||||
paths.append(compatdata_path)
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not get additional Steam library paths: {e}")
|
||||
|
||||
return paths
|
||||
@@ -15,6 +15,8 @@ import vdf
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple, Dict, Any, List
|
||||
|
||||
from ..handlers.vdf_handler import VDFHandler
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class NativeSteamService:
|
||||
@@ -28,37 +30,139 @@ 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" / ".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.error("Could not determine most recent Steam user from loginusers.vdf")
|
||||
return False
|
||||
|
||||
# Use the first valid user directory
|
||||
user_dir = user_dirs[0]
|
||||
self.user_id = user_dir.name
|
||||
self.user_config_path = user_dir / "config"
|
||||
|
||||
logger.info(f"Found Steam user: {self.user_id}")
|
||||
|
||||
# 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
|
||||
|
||||
config_dir = user_dir / "config"
|
||||
if not config_dir.exists():
|
||||
logger.error(f"User config directory does not exist: {config_dir}")
|
||||
return False
|
||||
|
||||
# Step 5: Set up the service state
|
||||
self.user_id = str(steamid3)
|
||||
self.user_config_path = config_dir
|
||||
|
||||
logger.info(f"VERIFIED Steam user: {self.user_id}")
|
||||
logger.info(f"User config path: {self.user_config_path}")
|
||||
logger.info(f"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 _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]:
|
||||
"""
|
||||
Parse loginusers.vdf to get the SteamID64 of the most recent user.
|
||||
Uses Steam's own MostRecent flag and Timestamp.
|
||||
"""
|
||||
try:
|
||||
loginusers_path = self.steam_path / "config" / "loginusers.vdf"
|
||||
|
||||
# Load VDF data
|
||||
vdf_data = VDFHandler.load(str(loginusers_path), binary=False)
|
||||
if not vdf_data:
|
||||
logger.error("Failed to parse loginusers.vdf")
|
||||
return None
|
||||
|
||||
users_section = vdf_data.get("users", {})
|
||||
if not users_section:
|
||||
logger.error("No users section found in loginusers.vdf")
|
||||
return None
|
||||
|
||||
most_recent_user = None
|
||||
most_recent_timestamp = 0
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
def get_shortcuts_vdf_path(self) -> Optional[Path]:
|
||||
"""Get the path to shortcuts.vdf"""
|
||||
if not self.user_config_path:
|
||||
@@ -241,16 +345,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"
|
||||
|
||||
@@ -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.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Downloaders.Bethesda": "0.3.15",
|
||||
"Wabbajack.Downloaders.Dispatcher": "0.3.15",
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.15",
|
||||
"Wabbajack.Networking.Discord": "0.3.15",
|
||||
"Wabbajack.Networking.GitHub": "0.3.15",
|
||||
"Wabbajack.Paths.IO": "0.3.15",
|
||||
"Wabbajack.Server.Lib": "0.3.15",
|
||||
"Wabbajack.Services.OSIntegrated": "0.3.15",
|
||||
"Wabbajack.VFS": "0.3.15",
|
||||
"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.15": {
|
||||
"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.15"
|
||||
"Wabbajack.Paths": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.CLI.Builder.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Common/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Networking.Http": "0.3.15",
|
||||
"Wabbajack.Paths.IO": "0.3.15"
|
||||
"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.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Installer": "0.3.15",
|
||||
"Wabbajack.VFS": "0.3.15",
|
||||
"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.15": {
|
||||
"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.15",
|
||||
"Wabbajack.DTOs": "0.3.15"
|
||||
"Wabbajack.Common": "0.3.17",
|
||||
"Wabbajack.DTOs": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Compression.BSA.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Compression.Zip/0.3.15": {
|
||||
"Wabbajack.Compression.Zip/0.3.17": {
|
||||
"dependencies": {
|
||||
"Wabbajack.IO.Async": "0.3.15"
|
||||
"Wabbajack.IO.Async": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Compression.Zip.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Configuration/0.3.15": {
|
||||
"Wabbajack.Configuration/0.3.17": {
|
||||
"runtime": {
|
||||
"Wabbajack.Configuration.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.Bethesda/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||
"Wabbajack.Networking.BethesdaNet": "0.3.15"
|
||||
"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.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Downloaders.GameFile": "0.3.15",
|
||||
"Wabbajack.Downloaders.GoogleDrive": "0.3.15",
|
||||
"Wabbajack.Downloaders.Http": "0.3.15",
|
||||
"Wabbajack.Downloaders.IPS4OAuth2Downloader": "0.3.15",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||
"Wabbajack.Downloaders.Manual": "0.3.15",
|
||||
"Wabbajack.Downloaders.MediaFire": "0.3.15",
|
||||
"Wabbajack.Downloaders.Mega": "0.3.15",
|
||||
"Wabbajack.Downloaders.ModDB": "0.3.15",
|
||||
"Wabbajack.Downloaders.Nexus": "0.3.15",
|
||||
"Wabbajack.Downloaders.VerificationCache": "0.3.15",
|
||||
"Wabbajack.Downloaders.WabbajackCDN": "0.3.15",
|
||||
"Wabbajack.Networking.WabbajackClientApi": "0.3.15"
|
||||
"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.15": {
|
||||
"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.15",
|
||||
"Wabbajack.VFS": "0.3.15"
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.17",
|
||||
"Wabbajack.VFS": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.GameFile.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.GoogleDrive/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.DTOs": "0.3.15",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||
"Wabbajack.Networking.Http": "0.3.15",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.15"
|
||||
"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.15": {
|
||||
"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.15",
|
||||
"Wabbajack.DTOs": "0.3.15",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||
"Wabbajack.Networking.BethesdaNet": "0.3.15",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.15",
|
||||
"Wabbajack.Paths.IO": "0.3.15"
|
||||
"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.15": {
|
||||
"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.15",
|
||||
"Wabbajack.DTOs": "0.3.15",
|
||||
"Wabbajack.Paths.IO": "0.3.15"
|
||||
"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.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||
"Wabbajack.Networking.Http": "0.3.15",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.15"
|
||||
"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.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.15"
|
||||
"Wabbajack.Common": "0.3.17",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.Manual.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.MediaFire/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.15"
|
||||
"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.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||
"Wabbajack.Paths.IO": "0.3.15"
|
||||
"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.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||
"Wabbajack.Networking.Http": "0.3.15",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.15"
|
||||
"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.15": {
|
||||
"Wabbajack.Downloaders.Nexus/0.3.17": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Wabbajack.DTOs": "0.3.15",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.15",
|
||||
"Wabbajack.Networking.Http": "0.3.15",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.15",
|
||||
"Wabbajack.Networking.NexusApi": "0.3.15",
|
||||
"Wabbajack.Paths": "0.3.15"
|
||||
"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.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Paths.IO": "0.3.15"
|
||||
"Wabbajack.DTOs": "0.3.17",
|
||||
"Wabbajack.Paths.IO": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.VerificationCache.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.WabbajackCDN/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||
"Wabbajack.Networking.Http": "0.3.15",
|
||||
"Wabbajack.RateLimiter": "0.3.15"
|
||||
"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.15": {
|
||||
"Wabbajack.DTOs/0.3.17": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.15",
|
||||
"Wabbajack.Paths": "0.3.15"
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.17",
|
||||
"Wabbajack.Paths": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.DTOs.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.FileExtractor/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Compression.BSA": "0.3.15",
|
||||
"Wabbajack.Hashing.PHash": "0.3.15",
|
||||
"Wabbajack.Paths": "0.3.15"
|
||||
"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.15": {
|
||||
"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.15",
|
||||
"Wabbajack.DTOs": "0.3.15",
|
||||
"Wabbajack.Paths": "0.3.15",
|
||||
"Wabbajack.Paths.IO": "0.3.15"
|
||||
"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.15": {
|
||||
"Wabbajack.Hashing.xxHash64/0.3.17": {
|
||||
"dependencies": {
|
||||
"Wabbajack.Paths": "0.3.15",
|
||||
"Wabbajack.RateLimiter": "0.3.15"
|
||||
"Wabbajack.Paths": "0.3.17",
|
||||
"Wabbajack.RateLimiter": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Hashing.xxHash64.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Installer/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Downloaders.Dispatcher": "0.3.15",
|
||||
"Wabbajack.Downloaders.GameFile": "0.3.15",
|
||||
"Wabbajack.FileExtractor": "0.3.15",
|
||||
"Wabbajack.Networking.WabbajackClientApi": "0.3.15",
|
||||
"Wabbajack.Paths": "0.3.15",
|
||||
"Wabbajack.Paths.IO": "0.3.15",
|
||||
"Wabbajack.VFS": "0.3.15",
|
||||
"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.15": {
|
||||
"Wabbajack.IO.Async/0.3.17": {
|
||||
"runtime": {
|
||||
"Wabbajack.IO.Async.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Networking.BethesdaNet/0.3.15": {
|
||||
"Wabbajack.Networking.BethesdaNet/0.3.17": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Wabbajack.DTOs": "0.3.15",
|
||||
"Wabbajack.Networking.Http": "0.3.15",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.15"
|
||||
"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.15": {
|
||||
"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.15"
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Networking.Discord.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Networking.GitHub/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.15"
|
||||
"Wabbajack.DTOs": "0.3.17",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Networking.GitHub.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Networking.Http/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.15",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.15",
|
||||
"Wabbajack.Paths": "0.3.15",
|
||||
"Wabbajack.Paths.IO": "0.3.15"
|
||||
"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.15": {
|
||||
"Wabbajack.Networking.Http.Interfaces/0.3.17": {
|
||||
"dependencies": {
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.15"
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.17"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Networking.Http.Interfaces.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Networking.NexusApi/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Networking.Http": "0.3.15",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.15",
|
||||
"Wabbajack.Networking.WabbajackClientApi": "0.3.15"
|
||||
"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.15": {
|
||||
"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.15",
|
||||
"Wabbajack.DTOs": "0.3.15",
|
||||
"Wabbajack.Paths.IO": "0.3.15",
|
||||
"Wabbajack.VFS.Interfaces": "0.3.15",
|
||||
"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.15": {
|
||||
"Wabbajack.Paths/0.3.17": {
|
||||
"runtime": {
|
||||
"Wabbajack.Paths.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Paths.IO/0.3.15": {
|
||||
"Wabbajack.Paths.IO/0.3.17": {
|
||||
"dependencies": {
|
||||
"Wabbajack.Paths": "0.3.15",
|
||||
"Wabbajack.Paths": "0.3.17",
|
||||
"shortid": "4.0.0"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Paths.IO.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.RateLimiter/0.3.15": {
|
||||
"Wabbajack.RateLimiter/0.3.17": {
|
||||
"runtime": {
|
||||
"Wabbajack.RateLimiter.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Server.Lib/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.15",
|
||||
"Wabbajack.Services.OSIntegrated": "0.3.15"
|
||||
"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.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Downloaders.Dispatcher": "0.3.15",
|
||||
"Wabbajack.Installer": "0.3.15",
|
||||
"Wabbajack.Networking.BethesdaNet": "0.3.15",
|
||||
"Wabbajack.Networking.Discord": "0.3.15",
|
||||
"Wabbajack.VFS": "0.3.15"
|
||||
"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.15": {
|
||||
"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.15",
|
||||
"Wabbajack.FileExtractor": "0.3.15",
|
||||
"Wabbajack.Hashing.PHash": "0.3.15",
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.15",
|
||||
"Wabbajack.Paths": "0.3.15",
|
||||
"Wabbajack.Paths.IO": "0.3.15",
|
||||
"Wabbajack.VFS.Interfaces": "0.3.15"
|
||||
"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.15": {
|
||||
"Wabbajack.VFS.Interfaces/0.3.17": {
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||
"Wabbajack.DTOs": "0.3.15",
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.15",
|
||||
"Wabbajack.Paths": "0.3.15"
|
||||
"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.15": {
|
||||
"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.15": {
|
||||
"Wabbajack.CLI.Builder/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Common/0.3.15": {
|
||||
"Wabbajack.Common/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Compiler/0.3.15": {
|
||||
"Wabbajack.Compiler/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Compression.BSA/0.3.15": {
|
||||
"Wabbajack.Compression.BSA/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Compression.Zip/0.3.15": {
|
||||
"Wabbajack.Compression.Zip/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Configuration/0.3.15": {
|
||||
"Wabbajack.Configuration/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.Bethesda/0.3.15": {
|
||||
"Wabbajack.Downloaders.Bethesda/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.Dispatcher/0.3.15": {
|
||||
"Wabbajack.Downloaders.Dispatcher/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.GameFile/0.3.15": {
|
||||
"Wabbajack.Downloaders.GameFile/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.GoogleDrive/0.3.15": {
|
||||
"Wabbajack.Downloaders.GoogleDrive/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.Http/0.3.15": {
|
||||
"Wabbajack.Downloaders.Http/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.Interfaces/0.3.15": {
|
||||
"Wabbajack.Downloaders.Interfaces/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.15": {
|
||||
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.Manual/0.3.15": {
|
||||
"Wabbajack.Downloaders.Manual/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.MediaFire/0.3.15": {
|
||||
"Wabbajack.Downloaders.MediaFire/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.Mega/0.3.15": {
|
||||
"Wabbajack.Downloaders.Mega/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.ModDB/0.3.15": {
|
||||
"Wabbajack.Downloaders.ModDB/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.Nexus/0.3.15": {
|
||||
"Wabbajack.Downloaders.Nexus/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.VerificationCache/0.3.15": {
|
||||
"Wabbajack.Downloaders.VerificationCache/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.WabbajackCDN/0.3.15": {
|
||||
"Wabbajack.Downloaders.WabbajackCDN/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.DTOs/0.3.15": {
|
||||
"Wabbajack.DTOs/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.FileExtractor/0.3.15": {
|
||||
"Wabbajack.FileExtractor/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Hashing.PHash/0.3.15": {
|
||||
"Wabbajack.Hashing.PHash/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Hashing.xxHash64/0.3.15": {
|
||||
"Wabbajack.Hashing.xxHash64/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Installer/0.3.15": {
|
||||
"Wabbajack.Installer/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.IO.Async/0.3.15": {
|
||||
"Wabbajack.IO.Async/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Networking.BethesdaNet/0.3.15": {
|
||||
"Wabbajack.Networking.BethesdaNet/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Networking.Discord/0.3.15": {
|
||||
"Wabbajack.Networking.Discord/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Networking.GitHub/0.3.15": {
|
||||
"Wabbajack.Networking.GitHub/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Networking.Http/0.3.15": {
|
||||
"Wabbajack.Networking.Http/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Networking.Http.Interfaces/0.3.15": {
|
||||
"Wabbajack.Networking.Http.Interfaces/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Networking.NexusApi/0.3.15": {
|
||||
"Wabbajack.Networking.NexusApi/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Networking.WabbajackClientApi/0.3.15": {
|
||||
"Wabbajack.Networking.WabbajackClientApi/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Paths/0.3.15": {
|
||||
"Wabbajack.Paths/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Paths.IO/0.3.15": {
|
||||
"Wabbajack.Paths.IO/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.RateLimiter/0.3.15": {
|
||||
"Wabbajack.RateLimiter/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Server.Lib/0.3.15": {
|
||||
"Wabbajack.Server.Lib/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Services.OSIntegrated/0.3.15": {
|
||||
"Wabbajack.Services.OSIntegrated/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.VFS/0.3.15": {
|
||||
"Wabbajack.VFS/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.VFS.Interfaces/0.3.15": {
|
||||
"Wabbajack.VFS.Interfaces/0.3.17": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
|
||||
Binary file not shown.
@@ -1,247 +0,0 @@
|
||||
"""
|
||||
Tuxborn Command
|
||||
|
||||
CLI command for the Tuxborn Automatic Installer.
|
||||
Extracted from the original jackify-cli.py.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
# Import the backend services we'll need
|
||||
from jackify.backend.models.modlist import ModlistContext
|
||||
from jackify.shared.colors import COLOR_INFO, COLOR_ERROR, COLOR_RESET
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TuxbornCommand:
|
||||
"""Handler for the tuxborn-auto CLI command."""
|
||||
|
||||
def __init__(self, backend_services, system_info):
|
||||
"""Initialize with backend services.
|
||||
|
||||
Args:
|
||||
backend_services: Dictionary of backend service instances
|
||||
system_info: System information (steamdeck flag, etc.)
|
||||
"""
|
||||
self.backend_services = backend_services
|
||||
self.system_info = system_info
|
||||
|
||||
def add_args(self, parser):
|
||||
"""Add tuxborn-auto arguments to the main parser.
|
||||
|
||||
Args:
|
||||
parser: The main ArgumentParser
|
||||
"""
|
||||
parser.add_argument(
|
||||
"--tuxborn-auto",
|
||||
action="store_true",
|
||||
help="Run the Tuxborn Automatic Installer non-interactively (for GUI integration)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--install-dir",
|
||||
type=str,
|
||||
help="Install directory for Tuxborn (required with --tuxborn-auto)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--download-dir",
|
||||
type=str,
|
||||
help="Downloads directory for Tuxborn (required with --tuxborn-auto)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--modlist-name",
|
||||
type=str,
|
||||
default="Tuxborn",
|
||||
help="Modlist name (optional, defaults to 'Tuxborn')"
|
||||
)
|
||||
|
||||
def execute(self, args) -> int:
|
||||
"""Execute the tuxborn-auto command.
|
||||
|
||||
Args:
|
||||
args: Parsed command-line arguments
|
||||
|
||||
Returns:
|
||||
Exit code (0 for success, 1 for failure)
|
||||
"""
|
||||
logger.info("Starting Tuxborn Automatic Installer (GUI integration mode)")
|
||||
|
||||
try:
|
||||
# Set up logging redirection (copied from original)
|
||||
self._setup_tee_logging()
|
||||
|
||||
# Build context from args
|
||||
context = self._build_context_from_args(args)
|
||||
|
||||
# Validate required fields
|
||||
if not self._validate_context(context):
|
||||
return 1
|
||||
|
||||
# Use legacy implementation for now - will migrate to backend services later
|
||||
result = self._execute_legacy_tuxborn(context)
|
||||
|
||||
logger.info("Finished Tuxborn Automatic Installer")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to run Tuxborn installer: {e}")
|
||||
print(f"{COLOR_ERROR}Tuxborn installation failed: {e}{COLOR_RESET}")
|
||||
return 1
|
||||
finally:
|
||||
# Restore stdout/stderr
|
||||
self._restore_stdout_stderr()
|
||||
|
||||
def _build_context_from_args(self, args) -> dict:
|
||||
"""Build context dictionary from command arguments.
|
||||
|
||||
Args:
|
||||
args: Parsed command-line arguments
|
||||
|
||||
Returns:
|
||||
Context dictionary
|
||||
"""
|
||||
install_dir = getattr(args, 'install_dir', None)
|
||||
download_dir = getattr(args, 'download_dir', None)
|
||||
modlist_name = getattr(args, 'modlist_name', 'Tuxborn')
|
||||
machineid = 'Tuxborn/Tuxborn'
|
||||
|
||||
# Try to get API key from saved config first, then environment variable
|
||||
from jackify.backend.services.api_key_service import APIKeyService
|
||||
api_key_service = APIKeyService()
|
||||
api_key = api_key_service.get_saved_api_key()
|
||||
if not api_key:
|
||||
api_key = os.environ.get('NEXUS_API_KEY')
|
||||
|
||||
resolution = getattr(args, 'resolution', None)
|
||||
mo2_exe_path = getattr(args, 'mo2_exe_path', None)
|
||||
skip_confirmation = True # Always true in GUI mode
|
||||
|
||||
context = {
|
||||
'machineid': machineid,
|
||||
'modlist_name': modlist_name,
|
||||
'install_dir': install_dir,
|
||||
'download_dir': download_dir,
|
||||
'nexus_api_key': api_key,
|
||||
'skip_confirmation': skip_confirmation,
|
||||
'resolution': resolution,
|
||||
'mo2_exe_path': mo2_exe_path,
|
||||
}
|
||||
|
||||
# PATCH: Always set modlist_value and modlist_source for Tuxborn workflow
|
||||
context['modlist_value'] = 'Tuxborn/Tuxborn'
|
||||
context['modlist_source'] = 'identifier'
|
||||
|
||||
return context
|
||||
|
||||
def _validate_context(self, context: dict) -> bool:
|
||||
"""Validate Tuxborn context.
|
||||
|
||||
Args:
|
||||
context: Tuxborn context dictionary
|
||||
|
||||
Returns:
|
||||
True if valid, False otherwise
|
||||
"""
|
||||
required_keys = ['modlist_name', 'install_dir', 'download_dir', 'nexus_api_key']
|
||||
missing = [k for k in required_keys if not context.get(k)]
|
||||
|
||||
if missing:
|
||||
print(f"{COLOR_ERROR}Missing required arguments for --tuxborn-auto.\\n"
|
||||
f"--install-dir, --download-dir, and NEXUS_API_KEY (env, 32+ chars) are required.{COLOR_RESET}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _setup_tee_logging(self):
|
||||
"""Set up TEE logging (copied from original implementation)."""
|
||||
import shutil
|
||||
|
||||
# TEE logging setup & log rotation (copied from original)
|
||||
class TeeStdout:
|
||||
def __init__(self, *files):
|
||||
self.files = files
|
||||
def write(self, data):
|
||||
for f in self.files:
|
||||
f.write(data)
|
||||
f.flush()
|
||||
def flush(self):
|
||||
for f in self.files:
|
||||
f.flush()
|
||||
|
||||
log_dir = Path.home() / "Jackify" / "logs"
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
workflow_log_path = log_dir / "tuxborn_workflow.log"
|
||||
|
||||
# Log rotation: keep last 3 logs, 1KB each (for testing)
|
||||
max_logs = 3
|
||||
max_size = 1024 # 1KB for testing
|
||||
if workflow_log_path.exists() and workflow_log_path.stat().st_size > max_size:
|
||||
for i in range(max_logs, 0, -1):
|
||||
prev = log_dir / f"tuxborn_workflow.log.{i-1}" if i > 1 else workflow_log_path
|
||||
dest = log_dir / f"tuxborn_workflow.log.{i}"
|
||||
if prev.exists():
|
||||
if dest.exists():
|
||||
dest.unlink()
|
||||
prev.rename(dest)
|
||||
|
||||
self.workflow_log = open(workflow_log_path, 'a')
|
||||
self.orig_stdout, self.orig_stderr = sys.stdout, sys.stderr
|
||||
sys.stdout = TeeStdout(sys.stdout, self.workflow_log)
|
||||
sys.stderr = TeeStdout(sys.stderr, self.workflow_log)
|
||||
|
||||
def _restore_stdout_stderr(self):
|
||||
"""Restore original stdout/stderr."""
|
||||
if hasattr(self, 'orig_stdout'):
|
||||
sys.stdout = self.orig_stdout
|
||||
sys.stderr = self.orig_stderr
|
||||
if hasattr(self, 'workflow_log'):
|
||||
self.workflow_log.close()
|
||||
|
||||
def _execute_legacy_tuxborn(self, context: dict) -> int:
|
||||
"""Execute Tuxborn using legacy implementation.
|
||||
|
||||
Args:
|
||||
context: Tuxborn context dictionary
|
||||
|
||||
Returns:
|
||||
Exit code
|
||||
"""
|
||||
# Import backend services
|
||||
from jackify.backend.core.modlist_operations import ModlistInstallCLI
|
||||
from jackify.backend.handlers.menu_handler import MenuHandler
|
||||
|
||||
# Create legacy handler instances
|
||||
menu_handler = MenuHandler()
|
||||
modlist_cli = ModlistInstallCLI(
|
||||
menu_handler=menu_handler,
|
||||
steamdeck=self.system_info.get('is_steamdeck', False)
|
||||
)
|
||||
|
||||
confirmed_context = modlist_cli.run_discovery_phase(context_override=context)
|
||||
if confirmed_context:
|
||||
menu_handler.logger.info("Tuxborn discovery confirmed by GUI. Proceeding to configuration/installation.")
|
||||
modlist_cli.configuration_phase()
|
||||
|
||||
# Handle GUI integration prompts (copied from original)
|
||||
print('[PROMPT:RESTART_STEAM]')
|
||||
if os.environ.get('JACKIFY_GUI_MODE'):
|
||||
input() # Wait for GUI to send confirmation, no CLI prompt
|
||||
else:
|
||||
answer = input('Restart Steam automatically now? (Y/n): ')
|
||||
# ... handle answer as before ...
|
||||
|
||||
print('[PROMPT:MANUAL_STEPS]')
|
||||
if os.environ.get('JACKIFY_GUI_MODE'):
|
||||
input() # Wait for GUI to send confirmation, no CLI prompt
|
||||
else:
|
||||
input('Once you have completed ALL the steps above, press Enter to continue...')
|
||||
|
||||
return 0
|
||||
else:
|
||||
menu_handler.logger.info("Tuxborn discovery/confirmation cancelled or failed (GUI mode).")
|
||||
print(f"{COLOR_INFO}Tuxborn installation cancelled or not confirmed.{COLOR_RESET}")
|
||||
return 1
|
||||
@@ -21,11 +21,9 @@ from jackify import __version__ as jackify_version
|
||||
# Import our command handlers
|
||||
from .commands.configure_modlist import ConfigureModlistCommand
|
||||
from .commands.install_modlist import InstallModlistCommand
|
||||
from .commands.tuxborn import TuxbornCommand
|
||||
|
||||
# Import our menu handlers
|
||||
from .menus.main_menu import MainMenuHandler
|
||||
from .menus.tuxborn_menu import TuxbornMenuHandler
|
||||
from .menus.wabbajack_menu import WabbajackMenuHandler
|
||||
from .menus.hoolamike_menu import HoolamikeMenuHandler
|
||||
from .menus.additional_menu import AdditionalMenuHandler
|
||||
@@ -280,7 +278,6 @@ class JackifyCLI:
|
||||
commands = {
|
||||
'configure_modlist': ConfigureModlistCommand(self.backend_services),
|
||||
'install_modlist': InstallModlistCommand(self.backend_services, self.system_info),
|
||||
'tuxborn': TuxbornCommand(self.backend_services, self.system_info)
|
||||
}
|
||||
return commands
|
||||
|
||||
@@ -292,7 +289,6 @@ class JackifyCLI:
|
||||
"""
|
||||
menus = {
|
||||
'main': MainMenuHandler(dev_mode=getattr(self, 'dev_mode', False)),
|
||||
'tuxborn': TuxbornMenuHandler(),
|
||||
'wabbajack': WabbajackMenuHandler(),
|
||||
'hoolamike': HoolamikeMenuHandler(),
|
||||
'additional': AdditionalMenuHandler()
|
||||
@@ -371,10 +367,6 @@ class JackifyCLI:
|
||||
self._debug_print('Entering restart_steam workflow')
|
||||
return self._handle_restart_steam()
|
||||
|
||||
# Handle Tuxborn auto mode
|
||||
if getattr(self.args, 'tuxborn_auto', False):
|
||||
self._debug_print('Entering Tuxborn workflow')
|
||||
return self.commands['tuxborn'].execute(self.args)
|
||||
|
||||
# Handle install-modlist top-level functionality
|
||||
if getattr(self.args, 'install_modlist', False):
|
||||
@@ -404,7 +396,6 @@ class JackifyCLI:
|
||||
parser.add_argument('--update', action='store_true', help='Check for and install updates')
|
||||
|
||||
# Add command-specific arguments
|
||||
self.commands['tuxborn'].add_args(parser)
|
||||
self.commands['install_modlist'].add_top_level_args(parser)
|
||||
|
||||
# Add subcommands
|
||||
@@ -459,8 +450,6 @@ class JackifyCLI:
|
||||
return 0
|
||||
elif choice == "wabbajack":
|
||||
self.menus['wabbajack'].show_wabbajack_tasks_menu(self)
|
||||
elif choice == "tuxborn":
|
||||
self.menus['tuxborn'].show_tuxborn_installer_menu(self)
|
||||
# HIDDEN FOR FIRST RELEASE - UNCOMMENT WHEN READY
|
||||
# elif choice == "hoolamike":
|
||||
# self.menus['hoolamike'].show_hoolamike_menu(self)
|
||||
|
||||
@@ -4,7 +4,6 @@ Extracted from the legacy monolithic CLI system
|
||||
"""
|
||||
|
||||
from .main_menu import MainMenuHandler
|
||||
from .tuxborn_menu import TuxbornMenuHandler
|
||||
from .wabbajack_menu import WabbajackMenuHandler
|
||||
from .hoolamike_menu import HoolamikeMenuHandler
|
||||
from .additional_menu import AdditionalMenuHandler
|
||||
@@ -12,7 +11,6 @@ from .recovery_menu import RecoveryMenuHandler
|
||||
|
||||
__all__ = [
|
||||
'MainMenuHandler',
|
||||
'TuxbornMenuHandler',
|
||||
'WabbajackMenuHandler',
|
||||
'HoolamikeMenuHandler',
|
||||
'AdditionalMenuHandler',
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
"""
|
||||
Tuxborn Menu Handler for Jackify CLI Frontend
|
||||
Extracted from src.modules.menu_handler.MenuHandler.show_tuxborn_installer_menu()
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from jackify.shared.colors import (
|
||||
COLOR_SELECTION, COLOR_RESET, COLOR_INFO, COLOR_PROMPT, COLOR_WARNING
|
||||
)
|
||||
from jackify.shared.ui_utils import print_jackify_banner
|
||||
from jackify.backend.handlers.config_handler import ConfigHandler
|
||||
|
||||
class TuxbornMenuHandler:
|
||||
"""
|
||||
Handles the Tuxborn Automatic Installer workflow
|
||||
Extracted from legacy MenuHandler class
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = None # Will be set by CLI when needed
|
||||
|
||||
def show_tuxborn_installer_menu(self, cli_instance):
|
||||
"""
|
||||
Implements the Tuxborn Automatic Installer workflow.
|
||||
Prompts for install path, downloads path, and Nexus API key, then runs the one-shot install from start to finish
|
||||
|
||||
Args:
|
||||
cli_instance: Reference to main CLI instance for access to handlers
|
||||
"""
|
||||
# Import backend service
|
||||
from jackify.backend.core.modlist_operations import ModlistInstallCLI
|
||||
|
||||
print_jackify_banner()
|
||||
print(f"{COLOR_SELECTION}Tuxborn Automatic Installer{COLOR_RESET}")
|
||||
print(f"{COLOR_SELECTION}{'-'*32}{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}This will install the Tuxborn modlist using the custom Jackify Install Engine in one automated flow.{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}You will be prompted for the install location, downloads directory, and your Nexus API key.{COLOR_RESET}\n")
|
||||
|
||||
tuxborn_machineid = "Tuxborn/Tuxborn"
|
||||
tuxborn_modlist_name = "Tuxborn"
|
||||
|
||||
# Prompt for install directory
|
||||
print("----------------------------")
|
||||
config_handler = ConfigHandler()
|
||||
base_install_dir = Path(config_handler.get_modlist_install_base_dir())
|
||||
default_install_dir = base_install_dir / "Skyrim" / "Tuxborn"
|
||||
print(f"{COLOR_PROMPT}Please enter the directory you wish to use for Tuxborn installation.{COLOR_RESET}")
|
||||
print(f"(Default: {default_install_dir})")
|
||||
install_dir_result = self._get_directory_path_legacy(
|
||||
cli_instance,
|
||||
prompt_message=f"{COLOR_PROMPT}Install directory (Enter for default, 'q' to cancel): {COLOR_RESET}",
|
||||
default_path=default_install_dir,
|
||||
create_if_missing=True,
|
||||
no_header=True
|
||||
)
|
||||
if not install_dir_result:
|
||||
print(f"{COLOR_INFO}Cancelled by user.{COLOR_RESET}")
|
||||
input("Press Enter to return to the main menu...")
|
||||
return
|
||||
if isinstance(install_dir_result, tuple):
|
||||
install_dir, _ = install_dir_result # We'll use the path, creation handled by engine or later
|
||||
else:
|
||||
install_dir = install_dir_result
|
||||
|
||||
# Prompt for download directory
|
||||
print("----------------------------")
|
||||
base_download_dir = Path(config_handler.get_modlist_downloads_base_dir())
|
||||
default_download_dir = base_download_dir / "Tuxborn"
|
||||
print(f"{COLOR_PROMPT}Please enter the directory you wish to use for Tuxborn downloads.{COLOR_RESET}")
|
||||
print(f"(Default: {default_download_dir})")
|
||||
download_dir_result = self._get_directory_path_legacy(
|
||||
cli_instance,
|
||||
prompt_message=f"{COLOR_PROMPT}Download directory (Enter for default, 'q' to cancel): {COLOR_RESET}",
|
||||
default_path=default_download_dir,
|
||||
create_if_missing=True,
|
||||
no_header=True
|
||||
)
|
||||
if not download_dir_result:
|
||||
print(f"{COLOR_INFO}Cancelled by user.{COLOR_RESET}")
|
||||
input("Press Enter to return to the main menu...")
|
||||
return
|
||||
if isinstance(download_dir_result, tuple):
|
||||
download_dir, _ = download_dir_result # We'll use the path, creation handled by engine or later
|
||||
else:
|
||||
download_dir = download_dir_result
|
||||
|
||||
# Prompt for Nexus API key
|
||||
print("----------------------------")
|
||||
from jackify.backend.services.api_key_service import APIKeyService
|
||||
api_key_service = APIKeyService()
|
||||
saved_key = api_key_service.get_saved_api_key()
|
||||
api_key = None
|
||||
|
||||
if saved_key:
|
||||
print(f"{COLOR_INFO}A Nexus API Key is already saved.{COLOR_RESET}")
|
||||
use_saved = input(f"{COLOR_PROMPT}Use the saved API key? [Y/n]: {COLOR_RESET}").strip().lower()
|
||||
if use_saved in ('', 'y', 'yes'):
|
||||
api_key = saved_key
|
||||
else:
|
||||
new_key = input(f"{COLOR_PROMPT}Enter a new Nexus API Key (or press Enter to keep the saved one): {COLOR_RESET}").strip()
|
||||
if new_key:
|
||||
api_key = new_key
|
||||
replace = input(f"{COLOR_PROMPT}Replace the saved key with this one? [y/N]: {COLOR_RESET}").strip().lower()
|
||||
if replace == 'y':
|
||||
if api_key_service.save_api_key(api_key):
|
||||
print(f"{COLOR_INFO}API key saved successfully.{COLOR_RESET}")
|
||||
else:
|
||||
print(f"{COLOR_WARNING}Failed to save API key. Using for this session only.{COLOR_RESET}")
|
||||
else:
|
||||
print(f"{COLOR_INFO}Using new key for this session only. Saved key unchanged.{COLOR_RESET}")
|
||||
else:
|
||||
api_key = saved_key
|
||||
else:
|
||||
print(f"{COLOR_PROMPT}A Nexus Mods API key is required for downloading mods.{COLOR_RESET}")
|
||||
print(f"{COLOR_INFO}You can get your personal key at: {COLOR_SELECTION}https://www.nexusmods.com/users/myaccount?tab=api{COLOR_RESET}")
|
||||
print(f"{COLOR_WARNING}Your API Key is NOT saved locally. It is used only for this session unless you choose to save it.{COLOR_RESET}")
|
||||
api_key = input(f"{COLOR_PROMPT}Enter Nexus API Key (or 'q' to cancel): {COLOR_RESET}").strip()
|
||||
if not api_key or api_key.lower() == 'q':
|
||||
print(f"{COLOR_INFO}Cancelled by user.{COLOR_RESET}")
|
||||
input("Press Enter to return to the main menu...")
|
||||
return
|
||||
save = input(f"{COLOR_PROMPT}Would you like to save this API key for future use? [y/N]: {COLOR_RESET}").strip().lower()
|
||||
if save == 'y':
|
||||
if api_key_service.save_api_key(api_key):
|
||||
print(f"{COLOR_INFO}API key saved successfully.{COLOR_RESET}")
|
||||
else:
|
||||
print(f"{COLOR_WARNING}Failed to save API key. Using for this session only.{COLOR_RESET}")
|
||||
else:
|
||||
print(f"{COLOR_INFO}Using API key for this session only. It will not be saved.{COLOR_RESET}")
|
||||
|
||||
# Context for ModlistInstallCLI
|
||||
context = {
|
||||
'machineid': tuxborn_machineid,
|
||||
'modlist_name': tuxborn_modlist_name, # Will be used for shortcut name
|
||||
'install_dir': install_dir_result, # Pass tuple (path, create_flag) or path
|
||||
'download_dir': download_dir_result, # Pass tuple (path, create_flag) or path
|
||||
'nexus_api_key': api_key,
|
||||
'resolution': None
|
||||
}
|
||||
|
||||
modlist_cli = ModlistInstallCLI(self, getattr(cli_instance, 'steamdeck', False))
|
||||
|
||||
# run_discovery_phase will use context_override, display summary, and ask for confirmation.
|
||||
# If user confirms, it returns the context, otherwise None.
|
||||
confirmed_context = modlist_cli.run_discovery_phase(context_override=context)
|
||||
|
||||
if confirmed_context:
|
||||
if self.logger:
|
||||
self.logger.info("Tuxborn discovery confirmed by user. Proceeding to configuration/installation.")
|
||||
# The modlist_cli instance now holds the confirmed context.
|
||||
# configuration_phase will use modlist_cli.context
|
||||
modlist_cli.configuration_phase()
|
||||
# After configuration_phase, messages about success or next steps are handled within it or by _configure_new_modlist
|
||||
else:
|
||||
if self.logger:
|
||||
self.logger.info("Tuxborn discovery/confirmation cancelled or failed.")
|
||||
print(f"{COLOR_INFO}Tuxborn installation cancelled or not confirmed.{COLOR_RESET}")
|
||||
input(f"{COLOR_PROMPT}Press Enter to return to the main menu...{COLOR_RESET}")
|
||||
return
|
||||
|
||||
def _get_directory_path_legacy(self, cli_instance, prompt_message: str, default_path: Optional[Path],
|
||||
create_if_missing: bool = True, no_header: bool = False) -> Optional[Path]:
|
||||
"""
|
||||
LEGACY BRIDGE: Delegate to legacy menu handler until full backend migration
|
||||
|
||||
Args:
|
||||
cli_instance: Reference to main CLI instance
|
||||
prompt_message: The prompt to show user
|
||||
default_path: Default path if user presses Enter
|
||||
create_if_missing: Whether to create directory if it doesn't exist
|
||||
no_header: Whether to skip header display
|
||||
|
||||
Returns:
|
||||
Path object or None if cancelled
|
||||
"""
|
||||
# LEGACY BRIDGE: Use the original menu handler's method
|
||||
if hasattr(cli_instance, 'menu') and hasattr(cli_instance.menu, 'get_directory_path'):
|
||||
return cli_instance.menu.get_directory_path(
|
||||
prompt_message=prompt_message,
|
||||
default_path=default_path,
|
||||
create_if_missing=create_if_missing,
|
||||
no_header=no_header
|
||||
)
|
||||
else:
|
||||
# Fallback: simple input for now (will be replaced in future phases)
|
||||
response = input(prompt_message).strip()
|
||||
if response.lower() == 'q':
|
||||
return None
|
||||
elif response == '':
|
||||
return default_path
|
||||
else:
|
||||
return Path(response)
|
||||
@@ -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)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
10
jackify/frontends/gui/mixins/__init__.py
Normal file
10
jackify/frontends/gui/mixins/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
GUI Mixins Package
|
||||
Reusable mixins for GUI functionality
|
||||
"""
|
||||
|
||||
from .operation_lock_mixin import OperationLockMixin
|
||||
|
||||
__all__ = ['OperationLockMixin']
|
||||
66
jackify/frontends/gui/mixins/operation_lock_mixin.py
Normal file
66
jackify/frontends/gui/mixins/operation_lock_mixin.py
Normal file
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Operation Lock Mixin
|
||||
Provides reliable button state management for GUI operations
|
||||
"""
|
||||
|
||||
from contextlib import contextmanager
|
||||
|
||||
|
||||
class OperationLockMixin:
|
||||
"""
|
||||
Mixin that provides reliable button state management.
|
||||
Ensures controls are always re-enabled after operations, even if exceptions occur.
|
||||
"""
|
||||
|
||||
def operation_lock(self):
|
||||
"""
|
||||
Context manager that ensures controls are always re-enabled after operations.
|
||||
|
||||
Usage:
|
||||
with self.operation_lock():
|
||||
# Perform operation that might fail
|
||||
risky_operation()
|
||||
# Controls are guaranteed to be re-enabled here
|
||||
"""
|
||||
@contextmanager
|
||||
def lock_manager():
|
||||
try:
|
||||
if hasattr(self, '_disable_controls_during_operation'):
|
||||
self._disable_controls_during_operation()
|
||||
yield
|
||||
finally:
|
||||
# Ensure controls are re-enabled even if exceptions occur
|
||||
if hasattr(self, '_enable_controls_after_operation'):
|
||||
self._enable_controls_after_operation()
|
||||
|
||||
return lock_manager()
|
||||
|
||||
def safe_operation(self, operation_func, *args, **kwargs):
|
||||
"""
|
||||
Execute an operation with automatic button state management.
|
||||
|
||||
Args:
|
||||
operation_func: Function to execute
|
||||
*args, **kwargs: Arguments to pass to operation_func
|
||||
|
||||
Returns:
|
||||
Result of operation_func or None if exception occurred
|
||||
"""
|
||||
try:
|
||||
with self.operation_lock():
|
||||
return operation_func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
# Log the error but don't re-raise - controls are already re-enabled
|
||||
if hasattr(self, 'logger'):
|
||||
self.logger.error(f"Operation failed: {e}", exc_info=True)
|
||||
# Could also show user error dialog here if needed
|
||||
return None
|
||||
|
||||
def reset_screen_to_defaults(self):
|
||||
"""
|
||||
Reset the screen to default state when navigating back from main menu.
|
||||
Override this method in subclasses to implement screen-specific reset logic.
|
||||
"""
|
||||
pass # Default implementation does nothing - subclasses should override
|
||||
@@ -5,7 +5,6 @@ Contains all the GUI screen components for Jackify.
|
||||
"""
|
||||
|
||||
from .main_menu import MainMenu
|
||||
from .tuxborn_installer import TuxbornInstallerScreen
|
||||
from .modlist_tasks import ModlistTasksScreen
|
||||
from .install_modlist import InstallModlistScreen
|
||||
from .configure_new_modlist import ConfigureNewModlistScreen
|
||||
@@ -13,7 +12,6 @@ from .configure_existing_modlist import ConfigureExistingModlistScreen
|
||||
|
||||
__all__ = [
|
||||
'MainMenu',
|
||||
'TuxbornInstallerScreen',
|
||||
'ModlistTasksScreen',
|
||||
'InstallModlistScreen',
|
||||
'ConfigureNewModlistScreen',
|
||||
|
||||
@@ -119,7 +119,7 @@ class ConfigureExistingModlistScreen(QWidget):
|
||||
self.shortcut_combo.addItem("Please Select...")
|
||||
self.shortcut_map = []
|
||||
for shortcut in self.mo2_shortcuts:
|
||||
display = f"{shortcut.get('AppName', 'Unknown')} ({shortcut.get('StartDir', '')})"
|
||||
display = f"{shortcut.get('AppName', shortcut.get('appname', 'Unknown'))} ({shortcut.get('StartDir', shortcut.get('startdir', ''))})"
|
||||
self.shortcut_combo.addItem(display)
|
||||
self.shortcut_map.append(shortcut)
|
||||
|
||||
@@ -427,8 +427,8 @@ class ConfigureExistingModlistScreen(QWidget):
|
||||
self._enable_controls_after_operation()
|
||||
return
|
||||
shortcut = self.shortcut_map[idx]
|
||||
modlist_name = shortcut.get('AppName', '')
|
||||
install_dir = shortcut.get('StartDir', '')
|
||||
modlist_name = shortcut.get('AppName', shortcut.get('appname', ''))
|
||||
install_dir = shortcut.get('StartDir', shortcut.get('startdir', ''))
|
||||
if not modlist_name or not install_dir:
|
||||
MessageService.critical(self, "Invalid Shortcut", "The selected shortcut is missing required information.", safety_level="medium")
|
||||
self._enable_controls_after_operation()
|
||||
@@ -499,6 +499,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):
|
||||
@@ -710,7 +711,7 @@ class ConfigureExistingModlistScreen(QWidget):
|
||||
self.shortcut_map.clear()
|
||||
|
||||
for shortcut in self.mo2_shortcuts:
|
||||
display = f"{shortcut.get('AppName', 'Unknown')} ({shortcut.get('StartDir', '')})"
|
||||
display = f"{shortcut.get('AppName', shortcut.get('appname', 'Unknown'))} ({shortcut.get('StartDir', shortcut.get('startdir', ''))})"
|
||||
self.shortcut_combo.addItem(display)
|
||||
self.shortcut_map.append(shortcut)
|
||||
|
||||
@@ -738,6 +739,30 @@ class ConfigureExistingModlistScreen(QWidget):
|
||||
else:
|
||||
return f"{elapsed_seconds_remainder} seconds"
|
||||
|
||||
def reset_screen_to_defaults(self):
|
||||
"""Reset the screen to default state when navigating back from main menu"""
|
||||
# Clear the shortcut selection
|
||||
self.shortcut_combo.clear()
|
||||
self.shortcut_map.clear()
|
||||
# Auto-refresh modlist list when screen is entered
|
||||
self.refresh_modlist_list()
|
||||
|
||||
# Clear console and process monitor
|
||||
self.console.clear()
|
||||
self.process_monitor.clear()
|
||||
|
||||
# Reset resolution combo to saved config preference
|
||||
saved_resolution = self.resolution_service.get_saved_resolution()
|
||||
if saved_resolution:
|
||||
combo_items = [self.resolution_combo.itemText(i) for i in range(self.resolution_combo.count())]
|
||||
resolution_index = self.resolution_service.get_resolution_index(saved_resolution, combo_items)
|
||||
self.resolution_combo.setCurrentIndex(resolution_index)
|
||||
elif self.resolution_combo.count() > 0:
|
||||
self.resolution_combo.setCurrentIndex(0) # Fallback to "Leave unchanged"
|
||||
|
||||
# Re-enable controls (in case they were disabled from previous errors)
|
||||
self._enable_controls_after_operation()
|
||||
|
||||
def cleanup(self):
|
||||
"""Clean up any running threads when the screen is closed"""
|
||||
debug_print("DEBUG: cleanup called - cleaning up ConfigurationThread")
|
||||
|
||||
@@ -481,7 +481,7 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
|
||||
def go_back(self):
|
||||
if self.stacked_widget:
|
||||
self.stacked_widget.setCurrentIndex(3) # Return to Modlist Tasks menu
|
||||
self.stacked_widget.setCurrentIndex(self.main_menu_index)
|
||||
|
||||
def update_top_panel(self):
|
||||
try:
|
||||
@@ -1184,7 +1184,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
|
||||
)
|
||||
|
||||
@@ -1326,6 +1326,27 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
btn_exit.clicked.connect(on_exit)
|
||||
dlg.exec()
|
||||
|
||||
def reset_screen_to_defaults(self):
|
||||
"""Reset the screen to default state when navigating back from main menu"""
|
||||
# Reset form fields
|
||||
self.install_dir_edit.setText("/path/to/Modlist/ModOrganizer.exe")
|
||||
|
||||
# Clear console and process monitor
|
||||
self.console.clear()
|
||||
self.process_monitor.clear()
|
||||
|
||||
# Reset resolution combo to saved config preference
|
||||
saved_resolution = self.resolution_service.get_saved_resolution()
|
||||
if saved_resolution:
|
||||
combo_items = [self.resolution_combo.itemText(i) for i in range(self.resolution_combo.count())]
|
||||
resolution_index = self.resolution_service.get_resolution_index(saved_resolution, combo_items)
|
||||
self.resolution_combo.setCurrentIndex(resolution_index)
|
||||
elif self.resolution_combo.count() > 0:
|
||||
self.resolution_combo.setCurrentIndex(0) # Fallback to "Leave unchanged"
|
||||
|
||||
# Re-enable controls (in case they were disabled from previous errors)
|
||||
self._enable_controls_after_operation()
|
||||
|
||||
def cleanup(self):
|
||||
"""Clean up any running threads when the screen is closed"""
|
||||
debug_print("DEBUG: cleanup called - cleaning up threads")
|
||||
|
||||
@@ -1057,7 +1057,7 @@ class InstallModlistScreen(QWidget):
|
||||
|
||||
def go_back(self):
|
||||
if self.stacked_widget:
|
||||
self.stacked_widget.setCurrentIndex(3) # Return to Modlist Tasks menu
|
||||
self.stacked_widget.setCurrentIndex(self.main_menu_index)
|
||||
|
||||
def update_top_panel(self):
|
||||
try:
|
||||
@@ -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 "2560x1600"
|
||||
# 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")
|
||||
@@ -1757,6 +1764,17 @@ class InstallModlistScreen(QWidget):
|
||||
MessageService.critical(self, "Steam Restart Failed", "Failed to restart Steam automatically. Please restart Steam manually, then try again.")
|
||||
|
||||
def start_automated_prefix_workflow(self):
|
||||
# Ensure _current_resolution is always set before starting workflow
|
||||
if not hasattr(self, '_current_resolution') or self._current_resolution is None:
|
||||
resolution = self.resolution_combo.currentText() if hasattr(self, 'resolution_combo') else None
|
||||
# Extract resolution properly (e.g., "1280x800" from "1280x800 (Steam Deck)")
|
||||
if resolution and resolution != "Leave unchanged":
|
||||
if " (" in resolution:
|
||||
self._current_resolution = resolution.split(" (")[0]
|
||||
else:
|
||||
self._current_resolution = resolution
|
||||
else:
|
||||
self._current_resolution = None
|
||||
"""Start the automated prefix creation workflow"""
|
||||
try:
|
||||
# Disable controls during installation
|
||||
@@ -2023,7 +2041,10 @@ class InstallModlistScreen(QWidget):
|
||||
"""Handle configuration error on main thread"""
|
||||
self._safe_append_text(f"Configuration failed with error: {error_message}")
|
||||
MessageService.critical(self, "Configuration Error", f"Configuration failed: {error_message}")
|
||||
|
||||
|
||||
# Re-enable all controls on error
|
||||
self._enable_controls_after_operation()
|
||||
|
||||
# Clean up thread
|
||||
if hasattr(self, 'config_thread') and self.config_thread is not None:
|
||||
# Disconnect all signals to prevent "Internal C++ object already deleted" errors
|
||||
@@ -2321,7 +2342,7 @@ class InstallModlistScreen(QWidget):
|
||||
'mo2_exe_path': self._get_mo2_path(install_dir, modlist_name),
|
||||
'modlist_value': None,
|
||||
'modlist_source': None,
|
||||
'resolution': getattr(self, '_current_resolution', '2560x1600'),
|
||||
'resolution': getattr(self, '_current_resolution', None),
|
||||
'skip_confirmation': True,
|
||||
'manual_steps_completed': True, # Mark as completed since automated prefix is done
|
||||
'appid': new_appid, # Use the NEW AppID from automated prefix creation
|
||||
@@ -2360,7 +2381,7 @@ class InstallModlistScreen(QWidget):
|
||||
nexus_api_key='', # Not needed for configuration
|
||||
modlist_value=self.context.get('modlist_value'),
|
||||
modlist_source=self.context.get('modlist_source', 'identifier'),
|
||||
resolution=self.context.get('resolution', '2560x1600'),
|
||||
resolution=self.context.get('resolution'),
|
||||
skip_confirmation=True,
|
||||
engine_installed=True # Skip path manipulation for engine workflows
|
||||
)
|
||||
@@ -2379,7 +2400,7 @@ class InstallModlistScreen(QWidget):
|
||||
# This shouldn't happen since automated prefix creation is complete
|
||||
self.progress_update.emit(f"Unexpected manual steps callback for {modlist_name}")
|
||||
|
||||
# Call the service method for post-Steam configuration
|
||||
# Call the service method for post-Steam configuration
|
||||
result = modlist_service.configure_modlist_post_steam(
|
||||
context=modlist_context,
|
||||
progress_callback=progress_callback,
|
||||
@@ -2419,7 +2440,7 @@ class InstallModlistScreen(QWidget):
|
||||
'mo2_exe_path': self._get_mo2_path(install_dir, modlist_name),
|
||||
'modlist_value': None,
|
||||
'modlist_source': None,
|
||||
'resolution': getattr(self, '_current_resolution', '2560x1600'),
|
||||
'resolution': getattr(self, '_current_resolution', None),
|
||||
'skip_confirmation': True,
|
||||
'manual_steps_completed': True, # Mark as completed
|
||||
'appid': new_appid # Use the NEW AppID from Steam
|
||||
@@ -2676,6 +2697,36 @@ https://wiki.scenicroute.games/Somnium/1_Installation.html</i>"""
|
||||
self.cleanup_processes()
|
||||
self.go_back()
|
||||
|
||||
def reset_screen_to_defaults(self):
|
||||
"""Reset the screen to default state when navigating back from main menu"""
|
||||
# Reset form fields
|
||||
self.modlist_btn.setText("Select Modlist")
|
||||
self.modlist_btn.setEnabled(False)
|
||||
self.file_edit.setText("")
|
||||
self.modlist_name_edit.setText("")
|
||||
self.install_dir_edit.setText(self.config_handler.get_modlist_install_base_dir())
|
||||
# Reset game type button
|
||||
self.game_type_btn.setText("Please Select...")
|
||||
|
||||
# Clear console and process monitor
|
||||
self.console.clear()
|
||||
self.process_monitor.clear()
|
||||
|
||||
# Reset tabs to first tab (Online)
|
||||
self.source_tabs.setCurrentIndex(0)
|
||||
|
||||
# Reset resolution combo to saved config preference
|
||||
saved_resolution = self.resolution_service.get_saved_resolution()
|
||||
if saved_resolution:
|
||||
combo_items = [self.resolution_combo.itemText(i) for i in range(self.resolution_combo.count())]
|
||||
resolution_index = self.resolution_service.get_resolution_index(saved_resolution, combo_items)
|
||||
self.resolution_combo.setCurrentIndex(resolution_index)
|
||||
elif self.resolution_combo.count() > 0:
|
||||
self.resolution_combo.setCurrentIndex(0) # Fallback to "Leave unchanged"
|
||||
|
||||
# Re-enable controls (in case they were disabled from previous errors)
|
||||
self._enable_controls_after_operation()
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Handle window close event - clean up processes"""
|
||||
self.cleanup_processes()
|
||||
|
||||
@@ -120,7 +120,7 @@ class MainMenu(QWidget):
|
||||
msg.setIcon(QMessageBox.Information)
|
||||
msg.exec()
|
||||
elif action_id == "modlist_tasks" and self.stacked_widget:
|
||||
self.stacked_widget.setCurrentIndex(3)
|
||||
self.stacked_widget.setCurrentIndex(2)
|
||||
elif action_id == "return_main_menu":
|
||||
# This is the main menu, so do nothing
|
||||
pass
|
||||
|
||||
@@ -198,11 +198,11 @@ class ModlistTasksScreen(QWidget):
|
||||
if action_id == "return_main_menu":
|
||||
self.stacked_widget.setCurrentIndex(0)
|
||||
elif action_id == "install_modlist":
|
||||
self.stacked_widget.setCurrentIndex(4)
|
||||
self.stacked_widget.setCurrentIndex(3)
|
||||
elif action_id == "configure_new_modlist":
|
||||
self.stacked_widget.setCurrentIndex(5)
|
||||
self.stacked_widget.setCurrentIndex(4)
|
||||
elif action_id == "configure_existing_modlist":
|
||||
self.stacked_widget.setCurrentIndex(6)
|
||||
self.stacked_widget.setCurrentIndex(5)
|
||||
|
||||
def go_back(self):
|
||||
"""Return to main menu"""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -222,15 +222,21 @@ class ValidationHandler:
|
||||
def validate_steam_shortcut(self, app_id: str) -> Tuple[bool, str]:
|
||||
"""Validate a Steam shortcut."""
|
||||
try:
|
||||
# Check if shortcuts.vdf exists
|
||||
shortcuts_path = Path.home() / '.steam' / 'steam' / 'userdata' / '75424832' / 'config' / 'shortcuts.vdf'
|
||||
# Use native Steam service to get proper shortcuts.vdf path with multi-user support
|
||||
from jackify.backend.services.native_steam_service import NativeSteamService
|
||||
steam_service = NativeSteamService()
|
||||
shortcuts_path = steam_service.get_shortcuts_vdf_path()
|
||||
|
||||
if not shortcuts_path:
|
||||
return False, "Could not determine shortcuts.vdf path (no active Steam user found)"
|
||||
|
||||
if not shortcuts_path.exists():
|
||||
return False, "shortcuts.vdf not found"
|
||||
|
||||
|
||||
# Check if shortcuts.vdf is accessible
|
||||
if not os.access(shortcuts_path, os.R_OK | os.W_OK):
|
||||
return False, "shortcuts.vdf is not accessible"
|
||||
|
||||
|
||||
# Parse shortcuts.vdf using VDFHandler
|
||||
shortcuts_data = VDFHandler.load(str(shortcuts_path), binary=True)
|
||||
|
||||
|
||||
BIN
jackify/tools/cabextract
Executable file
BIN
jackify/tools/cabextract
Executable file
Binary file not shown.
19627
jackify/tools/winetricks
Executable file
19627
jackify/tools/winetricks
Executable file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user