mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-01-17 19:47:00 +01:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f46ed2c0fe | ||
|
|
c9bd6f60e6 | ||
|
|
28cde64887 | ||
|
|
64c76046ce |
79
CHANGELOG.md
79
CHANGELOG.md
@@ -1,5 +1,84 @@
|
|||||||
# Jackify Changelog
|
# Jackify Changelog
|
||||||
|
|
||||||
|
## 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
|
||||||
|
- **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
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
- **GE-Proton Detection**: Automatic detection and prioritization of GE-Proton versions
|
||||||
|
- **User-selectable Proton version**: Settings dialog displays all available Proton versions with type indicators
|
||||||
|
|
||||||
|
### Engine Updates
|
||||||
|
- **jackify-engine v0.3.15**: Reads Proton configuration from config.json, adds degree symbol handling for special characters, removes Wine fallback (Proton now required)
|
||||||
|
|
||||||
|
### Technical Improvements
|
||||||
|
- **Smart Priority**: GE-Proton 10+ → Proton Experimental → Proton 10 → Proton 9
|
||||||
|
- **Auto-Configuration**: Fresh installations automatically select optimal Proton version
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
- **Steam VDF Compatibility**: Fixed case-sensitivity issues with Steam shortcuts.vdf parsing for Configure Existing Modlist workflows
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.1.3 - Enhanced Proton Support and System Compatibility
|
||||||
|
**Release Date:** September 21, 2025
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
- **Enhanced Proton Detection**: Automatic fallback system with priority: Experimental → Proton 10 → Proton 9
|
||||||
|
- **Guided Proton Installation**: Professional auto-install dialog with Steam protocol integration for missing Proton versions
|
||||||
|
- **Enderal Game Support**: Added Enderal to supported games list with special handling for Somnium modlist structure
|
||||||
|
- **Proton Version Leniency**: Accept any Proton version 9+ instead of requiring Experimental
|
||||||
|
|
||||||
|
### UX Improvements
|
||||||
|
- **Resolution System Overhaul**: Eliminated hardcoded 2560x1600 fallbacks across all screens
|
||||||
|
- **Steam Deck Detection**: Proper 1280x800 default resolution with 1920x1080 fallback for desktop
|
||||||
|
- **Leave Unchanged Logic**: Fixed resolution setting to actually preserve existing user configurations
|
||||||
|
|
||||||
|
### Technical Improvements
|
||||||
|
- **Resolution Utilities**: New `shared/resolution_utils.py` with centralized resolution management
|
||||||
|
- **Protontricks Detection**: Enhanced detection for both native and Flatpak protontricks installations
|
||||||
|
- **Real-time Monitoring**: Progress tracking for Proton installation with directory stability detection
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
- **Somnium Support**: Automatic detection of `files/ModOrganizer.exe` structure in edge-case modlists
|
||||||
|
- **Steam Protocol Integration**: Reliable triggering of Proton installation via `steam://install/` URLs
|
||||||
|
- **Manual Fallback**: Clear instructions and recheck functionality when auto-install fails
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v0.1.2 - About Dialog and System Information
|
## v0.1.2 - About Dialog and System Information
|
||||||
**Release Date:** September 16, 2025
|
**Release Date:** September 16, 2025
|
||||||
|
|
||||||
|
|||||||
@@ -5,4 +5,4 @@ This package provides both CLI and GUI interfaces for managing
|
|||||||
Wabbajack modlists natively on Linux systems.
|
Wabbajack modlists natively on Linux systems.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "0.1.2"
|
__version__ = "0.1.5"
|
||||||
|
|||||||
@@ -23,6 +23,44 @@ from jackify.backend.handlers.config_handler import ConfigHandler
|
|||||||
|
|
||||||
# UI Colors already imported above
|
# UI Colors already imported above
|
||||||
|
|
||||||
|
def _get_user_proton_version():
|
||||||
|
"""Get user's preferred Proton version from config, with fallback to auto-detection"""
|
||||||
|
try:
|
||||||
|
from jackify.backend.handlers.config_handler import ConfigHandler
|
||||||
|
from jackify.backend.handlers.wine_utils import WineUtils
|
||||||
|
|
||||||
|
config_handler = ConfigHandler()
|
||||||
|
user_proton_path = config_handler.get('proton_path', 'auto')
|
||||||
|
|
||||||
|
if user_proton_path == 'auto':
|
||||||
|
# Use enhanced fallback logic with GE-Proton preference
|
||||||
|
logging.info("User selected auto-detect, using GE-Proton → Experimental → Proton precedence")
|
||||||
|
return WineUtils.select_best_proton()
|
||||||
|
else:
|
||||||
|
# User has selected a specific Proton version
|
||||||
|
# Use the exact directory name for Steam config.vdf
|
||||||
|
try:
|
||||||
|
proton_version = os.path.basename(user_proton_path)
|
||||||
|
# GE-Proton uses exact directory name, Valve Proton needs lowercase conversion
|
||||||
|
if proton_version.startswith('GE-Proton'):
|
||||||
|
# Keep GE-Proton name exactly as-is
|
||||||
|
steam_proton_name = proton_version
|
||||||
|
else:
|
||||||
|
# Convert Valve Proton names to Steam's format
|
||||||
|
steam_proton_name = proton_version.lower().replace(' - ', '_').replace(' ', '_').replace('-', '_')
|
||||||
|
if not steam_proton_name.startswith('proton'):
|
||||||
|
steam_proton_name = f"proton_{steam_proton_name}"
|
||||||
|
|
||||||
|
logging.info(f"Using user-selected Proton: {steam_proton_name}")
|
||||||
|
return steam_proton_name
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(f"Invalid user Proton path '{user_proton_path}', falling back to auto: {e}")
|
||||||
|
return WineUtils.select_best_proton()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to get user Proton preference, using default: {e}")
|
||||||
|
return "proton_experimental"
|
||||||
|
|
||||||
# Attempt to import readline for tab completion
|
# Attempt to import readline for tab completion
|
||||||
READLINE_AVAILABLE = False
|
READLINE_AVAILABLE = False
|
||||||
try:
|
try:
|
||||||
@@ -692,6 +730,14 @@ class ModlistInstallCLI:
|
|||||||
cmd += ['-m', self.context['machineid']]
|
cmd += ['-m', self.context['machineid']]
|
||||||
cmd += ['-o', install_dir_str, '-d', download_dir_str]
|
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
|
# Store original environment values to restore later
|
||||||
original_env_values = {
|
original_env_values = {
|
||||||
'NEXUS_API_KEY': os.environ.get('NEXUS_API_KEY'),
|
'NEXUS_API_KEY': os.environ.get('NEXUS_API_KEY'),
|
||||||
@@ -1248,13 +1294,16 @@ class ModlistInstallCLI:
|
|||||||
from jackify.backend.services.native_steam_service import NativeSteamService
|
from jackify.backend.services.native_steam_service import NativeSteamService
|
||||||
steam_service = NativeSteamService()
|
steam_service = NativeSteamService()
|
||||||
|
|
||||||
|
# Get user's preferred Proton version
|
||||||
|
proton_version = _get_user_proton_version()
|
||||||
|
|
||||||
success, app_id = steam_service.create_shortcut_with_proton(
|
success, app_id = steam_service.create_shortcut_with_proton(
|
||||||
app_name=config_context['name'],
|
app_name=config_context['name'],
|
||||||
exe_path=config_context['mo2_exe_path'],
|
exe_path=config_context['mo2_exe_path'],
|
||||||
start_dir=os.path.dirname(config_context['mo2_exe_path']),
|
start_dir=os.path.dirname(config_context['mo2_exe_path']),
|
||||||
launch_options="%command%",
|
launch_options="%command%",
|
||||||
tags=["Jackify"],
|
tags=["Jackify"],
|
||||||
proton_version="proton_experimental"
|
proton_version=proton_version
|
||||||
)
|
)
|
||||||
|
|
||||||
if not success or not app_id:
|
if not success or not app_id:
|
||||||
|
|||||||
@@ -47,8 +47,10 @@ class ConfigHandler:
|
|||||||
# If steam_path is not set, detect it
|
# If steam_path is not set, detect it
|
||||||
if not self.settings["steam_path"]:
|
if not self.settings["steam_path"]:
|
||||||
self.settings["steam_path"] = self._detect_steam_path()
|
self.settings["steam_path"] = self._detect_steam_path()
|
||||||
# Save the updated settings
|
|
||||||
self.save_config()
|
# Auto-detect and set Proton version on first run
|
||||||
|
if not self.settings.get("proton_path"):
|
||||||
|
self._auto_detect_proton()
|
||||||
|
|
||||||
# If jackify_data_dir is not set, initialize it to default
|
# If jackify_data_dir is not set, initialize it to default
|
||||||
if not self.settings.get("jackify_data_dir"):
|
if not self.settings.get("jackify_data_dir"):
|
||||||
@@ -494,4 +496,28 @@ class ConfigHandler:
|
|||||||
logger.error(f"Error saving modlist downloads base directory: {e}")
|
logger.error(f"Error saving modlist downloads base directory: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _auto_detect_proton(self):
|
||||||
|
"""Auto-detect and set best Proton version (includes GE-Proton and Valve Proton)"""
|
||||||
|
try:
|
||||||
|
from .wine_utils import WineUtils
|
||||||
|
best_proton = WineUtils.select_best_proton()
|
||||||
|
|
||||||
|
if best_proton:
|
||||||
|
self.settings["proton_path"] = str(best_proton['path'])
|
||||||
|
self.settings["proton_version"] = best_proton['name']
|
||||||
|
proton_type = best_proton.get('type', 'Unknown')
|
||||||
|
logger.info(f"Auto-detected Proton: {best_proton['name']} ({proton_type})")
|
||||||
|
self.save_config()
|
||||||
|
else:
|
||||||
|
# Fallback to auto-detect mode
|
||||||
|
self.settings["proton_path"] = "auto"
|
||||||
|
self.settings["proton_version"] = "auto"
|
||||||
|
logger.info("No compatible Proton versions found, using auto-detect mode")
|
||||||
|
self.save_config()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to auto-detect Proton: {e}")
|
||||||
|
self.settings["proton_path"] = "auto"
|
||||||
|
self.settings["proton_version"] = "auto"
|
||||||
|
|
||||||
|
|
||||||
@@ -159,6 +159,9 @@ class ModlistHandler:
|
|||||||
|
|
||||||
# Initialize Handlers (should happen regardless of how paths were provided)
|
# Initialize Handlers (should happen regardless of how paths were provided)
|
||||||
self.protontricks_handler = ProtontricksHandler(steamdeck=self.steamdeck, logger=self.logger)
|
self.protontricks_handler = ProtontricksHandler(steamdeck=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.shortcut_handler = ShortcutHandler(steamdeck=self.steamdeck, verbose=self.verbose)
|
||||||
self.filesystem_handler = filesystem_handler if filesystem_handler else FileSystemHandler()
|
self.filesystem_handler = filesystem_handler if filesystem_handler else FileSystemHandler()
|
||||||
self.resolution_handler = ResolutionHandler()
|
self.resolution_handler = ResolutionHandler()
|
||||||
@@ -224,44 +227,41 @@ class ModlistHandler:
|
|||||||
discovered_modlists_info = []
|
discovered_modlists_info = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 1. Get ALL non-Steam shortcuts from Protontricks
|
# Get shortcuts pointing to the executable from shortcuts.vdf
|
||||||
# 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
|
|
||||||
matching_vdf_shortcuts = self.shortcut_handler.find_shortcuts_by_exe(executable_name)
|
matching_vdf_shortcuts = self.shortcut_handler.find_shortcuts_by_exe(executable_name)
|
||||||
if not matching_vdf_shortcuts:
|
if not matching_vdf_shortcuts:
|
||||||
self.logger.debug(f"No shortcuts found pointing to '{executable_name}' in shortcuts.vdf.")
|
self.logger.debug(f"No shortcuts found pointing to '{executable_name}' in shortcuts.vdf.")
|
||||||
return []
|
return []
|
||||||
self.logger.debug(f"Shortcuts matching executable '{executable_name}' in VDF: {matching_vdf_shortcuts}")
|
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:
|
for vdf_shortcut in matching_vdf_shortcuts:
|
||||||
app_name = vdf_shortcut.get('AppName')
|
app_name = vdf_shortcut.get('AppName')
|
||||||
start_dir = vdf_shortcut.get('StartDir')
|
start_dir = vdf_shortcut.get('StartDir')
|
||||||
|
signed_appid = vdf_shortcut.get('appid')
|
||||||
|
|
||||||
if not app_name or not start_dir:
|
if not app_name or not start_dir:
|
||||||
self.logger.warning(f"Skipping VDF shortcut due to missing AppName or StartDir: {vdf_shortcut}")
|
self.logger.warning(f"Skipping VDF shortcut due to missing AppName or StartDir: {vdf_shortcut}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if app_name in protontricks_shortcuts:
|
if signed_appid is None:
|
||||||
app_id = protontricks_shortcuts[app_name]
|
self.logger.warning(f"Skipping VDF shortcut due to missing appid: {vdf_shortcut}")
|
||||||
|
continue
|
||||||
# Append dictionary with all necessary info
|
|
||||||
modlist_info = {
|
# Convert signed AppID to unsigned AppID (the format used by Steam prefixes)
|
||||||
'name': app_name,
|
if signed_appid < 0:
|
||||||
'appid': app_id,
|
unsigned_appid = signed_appid + (2**32)
|
||||||
'path': start_dir
|
|
||||||
}
|
|
||||||
discovered_modlists_info.append(modlist_info)
|
|
||||||
self.logger.info(f"Validated shortcut: '{app_name}' (AppID: {app_id}, Path: {start_dir})")
|
|
||||||
else:
|
else:
|
||||||
# Downgraded from WARNING to INFO
|
unsigned_appid = signed_appid
|
||||||
self.logger.info(f"Shortcut '{app_name}' found in VDF but not listed by protontricks. Skipping.")
|
|
||||||
|
# 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:
|
except Exception as e:
|
||||||
self.logger.error(f"Error discovering executable shortcuts: {e}", exc_info=True)
|
self.logger.error(f"Error discovering executable shortcuts: {e}", exc_info=True)
|
||||||
@@ -685,7 +685,14 @@ class ModlistHandler:
|
|||||||
# All modlists now use their own AppID for wine components
|
# All modlists now use their own AppID for wine components
|
||||||
target_appid = self.appid
|
target_appid = self.appid
|
||||||
|
|
||||||
if not self.protontricks_handler.install_wine_components(target_appid, self.game_var_full, specific_components=components):
|
# Use winetricks for wine component installation (faster than protontricks)
|
||||||
|
wineprefix = self.protontricks_handler.get_wine_prefix_path(target_appid)
|
||||||
|
if not wineprefix:
|
||||||
|
self.logger.error("Failed to get WINEPREFIX path for winetricks.")
|
||||||
|
print("Error: Could not determine wine prefix location.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not self.winetricks_handler.install_wine_components(wineprefix, self.game_var_full, specific_components=components):
|
||||||
self.logger.error("Failed to install Wine components. Configuration aborted.")
|
self.logger.error("Failed to install Wine components. Configuration aborted.")
|
||||||
print("Error: Failed to install necessary Wine components.")
|
print("Error: Failed to install necessary Wine components.")
|
||||||
return False # Abort on failure
|
return False # Abort on failure
|
||||||
@@ -758,9 +765,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("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.")
|
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
|
# 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
|
# Convert steamapps/common path to library root path
|
||||||
steam_libraries = None
|
steam_libraries = None
|
||||||
if self.steam_library:
|
if self.steam_library:
|
||||||
@@ -863,7 +871,7 @@ class ModlistHandler:
|
|||||||
prefix_path_str = self.path_handler.find_compat_data(str(self.appid))
|
prefix_path_str = self.path_handler.find_compat_data(str(self.appid))
|
||||||
if prefix_path_str:
|
if prefix_path_str:
|
||||||
prefix_path = Path(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_url = "https://github.com/mrbvrz/segoe-ui-linux/raw/refs/heads/master/font/seguisym.ttf"
|
||||||
font_dest_path = fonts_dir / "seguisym.ttf"
|
font_dest_path = fonts_dir / "seguisym.ttf"
|
||||||
|
|
||||||
@@ -1163,7 +1171,7 @@ class ModlistHandler:
|
|||||||
# Determine game type
|
# Determine game type
|
||||||
game = (game_var_full or modlist_name or "").lower().replace(" ", "")
|
game = (game_var_full or modlist_name or "").lower().replace(" ", "")
|
||||||
# Add game-specific extras
|
# Add game-specific extras
|
||||||
if "skyrim" in game or "fallout4" in game or "starfield" in game or "oblivion_remastered" in game:
|
if "skyrim" in game or "fallout4" in game or "starfield" in game or "oblivion_remastered" in game or "enderal" in game:
|
||||||
extras += ["d3dcompiler_47", "d3dx11_43", "d3dcompiler_43", "dotnet6", "dotnet7"]
|
extras += ["d3dcompiler_47", "d3dx11_43", "d3dcompiler_43", "dotnet6", "dotnet7"]
|
||||||
elif "falloutnewvegas" in game or "fnv" in game or "oblivion" in game:
|
elif "falloutnewvegas" in game or "fnv" in game or "oblivion" in game:
|
||||||
extras += ["d3dx9_43", "d3dx9"]
|
extras += ["d3dx9_43", "d3dx9"]
|
||||||
@@ -1238,6 +1246,12 @@ class ModlistHandler:
|
|||||||
# Check ModOrganizer.ini for indicators (nvse/enderal) as an early, robust signal
|
# Check ModOrganizer.ini for indicators (nvse/enderal) as an early, robust signal
|
||||||
try:
|
try:
|
||||||
mo2_ini = modlist_path / "ModOrganizer.ini"
|
mo2_ini = modlist_path / "ModOrganizer.ini"
|
||||||
|
# Also check Somnium's non-standard location
|
||||||
|
if not mo2_ini.exists():
|
||||||
|
somnium_mo2_ini = modlist_path / "files" / "ModOrganizer.ini"
|
||||||
|
if somnium_mo2_ini.exists():
|
||||||
|
mo2_ini = somnium_mo2_ini
|
||||||
|
|
||||||
if mo2_ini.exists():
|
if mo2_ini.exists():
|
||||||
try:
|
try:
|
||||||
content = mo2_ini.read_text(errors='ignore').lower()
|
content = mo2_ini.read_text(errors='ignore').lower()
|
||||||
|
|||||||
@@ -32,14 +32,21 @@ class PathHandler:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _strip_sdcard_path_prefix(path_obj: Path) -> str:
|
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 /).
|
Returns the path as a POSIX-style string (using /).
|
||||||
"""
|
"""
|
||||||
path_str = path_obj.as_posix() # Work with consistent forward slashes
|
from .wine_utils import WineUtils
|
||||||
if path_str.lower().startswith(SDCARD_PREFIX.lower()):
|
|
||||||
# Return the part *after* the prefix, ensuring no leading slash remains unless root
|
path_str = path_obj.as_posix() # Work with consistent forward slashes
|
||||||
relative_part = path_str[len(SDCARD_PREFIX):]
|
|
||||||
return relative_part if relative_part else "." # Return '.' if it was exactly the prefix
|
# 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
|
return path_str
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -737,7 +744,7 @@ class PathHandler:
|
|||||||
try:
|
try:
|
||||||
with open(modlist_ini_path, 'r', encoding='utf-8', errors='ignore') as f:
|
with open(modlist_ini_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||||
lines = f.readlines()
|
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)
|
processed_path = self._strip_sdcard_path_prefix(new_game_path)
|
||||||
windows_style = processed_path.replace('/', '\\')
|
windows_style = processed_path.replace('/', '\\')
|
||||||
windows_style_double = windows_style.replace('\\', '\\\\')
|
windows_style_double = windows_style.replace('\\', '\\\\')
|
||||||
@@ -876,9 +883,10 @@ class PathHandler:
|
|||||||
rel_path = value_part[idx:].lstrip('/')
|
rel_path = value_part[idx:].lstrip('/')
|
||||||
else:
|
else:
|
||||||
rel_path = exe_name
|
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)
|
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}")
|
logger.debug(f"Updating binary path: {line.strip()} -> {new_binary_line}")
|
||||||
lines[i] = new_binary_line + "\n"
|
lines[i] = new_binary_line + "\n"
|
||||||
binary_paths_updated += 1
|
binary_paths_updated += 1
|
||||||
@@ -893,7 +901,7 @@ class PathHandler:
|
|||||||
wd_path = drive_prefix + wd_path
|
wd_path = drive_prefix + wd_path
|
||||||
formatted_wd_path = PathHandler._format_workingdir_for_mo2(wd_path)
|
formatted_wd_path = PathHandler._format_workingdir_for_mo2(wd_path)
|
||||||
key_part = f"{index}{backslash_style}workingDirectory"
|
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}")
|
logger.debug(f"Updating working directory: {wd_line.strip()} -> {new_wd_line}")
|
||||||
lines[j] = new_wd_line + "\n"
|
lines[j] = new_wd_line + "\n"
|
||||||
working_dirs_updated += 1
|
working_dirs_updated += 1
|
||||||
|
|||||||
@@ -21,14 +21,19 @@ logger = logging.getLogger(__name__)
|
|||||||
class ProtontricksHandler:
|
class ProtontricksHandler:
|
||||||
"""
|
"""
|
||||||
Handles operations related to Protontricks detection and usage
|
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):
|
def __init__(self, steamdeck: bool, logger=None):
|
||||||
self.logger = logger or logging.getLogger(__name__)
|
self.logger = logger or logging.getLogger(__name__)
|
||||||
self.which_protontricks = None # 'flatpak' or 'native'
|
self.which_protontricks = None # 'flatpak' or 'native'
|
||||||
self.protontricks_version = None
|
self.protontricks_version = None
|
||||||
self.protontricks_path = None
|
self.protontricks_path = None
|
||||||
self.steamdeck = steamdeck # Store steamdeck status
|
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):
|
def _get_clean_subprocess_env(self):
|
||||||
"""
|
"""
|
||||||
@@ -69,7 +74,14 @@ class ProtontricksHandler:
|
|||||||
env.pop('DYLD_LIBRARY_PATH', None)
|
env.pop('DYLD_LIBRARY_PATH', None)
|
||||||
|
|
||||||
return env
|
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):
|
def detect_protontricks(self):
|
||||||
"""
|
"""
|
||||||
Detect if protontricks is installed and whether it's flatpak or native.
|
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):
|
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
|
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':
|
if self.which_protontricks != 'flatpak':
|
||||||
logger.debug("Using Native protontricks, skip setting permissions")
|
logger.debug("Using Native protontricks, skip setting permissions")
|
||||||
return True
|
return True
|
||||||
@@ -338,15 +360,22 @@ class ProtontricksHandler:
|
|||||||
|
|
||||||
# Renamed from list_non_steam_games for clarity and purpose
|
# Renamed from list_non_steam_games for clarity and purpose
|
||||||
def list_non_steam_shortcuts(self) -> Dict[str, str]:
|
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
|
Uses native VDF parsing when enabled, falls back to protontricks -l parsing.
|
||||||
"Non-Steam shortcut: [Name] ([AppID])".
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A dictionary mapping the shortcut name (AppName) to its AppID.
|
A dictionary mapping the shortcut name (AppName) to its AppID.
|
||||||
Returns an empty dictionary if none are found or an error occurs.
|
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...")
|
logger.info("Listing ALL non-Steam shortcuts via protontricks...")
|
||||||
non_steam_shortcuts = {}
|
non_steam_shortcuts = {}
|
||||||
# --- Ensure protontricks is detected before proceeding ---
|
# --- Ensure protontricks is detected before proceeding ---
|
||||||
@@ -577,12 +606,22 @@ class ProtontricksHandler:
|
|||||||
def get_wine_prefix_path(self, appid) -> Optional[str]:
|
def get_wine_prefix_path(self, appid) -> Optional[str]:
|
||||||
"""Gets the WINEPREFIX path for a given AppID.
|
"""Gets the WINEPREFIX path for a given AppID.
|
||||||
|
|
||||||
|
Uses native path discovery when enabled, falls back to protontricks detection.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
appid (str): The Steam AppID.
|
appid (str): The Steam AppID.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The WINEPREFIX path as a string, or None if detection fails.
|
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}")
|
logger.debug(f"Getting WINEPREFIX for AppID {appid}")
|
||||||
result = self.run_protontricks("-c", "echo $WINEPREFIX", appid)
|
result = self.run_protontricks("-c", "echo $WINEPREFIX", appid)
|
||||||
if result and result.returncode == 0 and result.stdout.strip():
|
if result and result.returncode == 0 and result.stdout.strip():
|
||||||
|
|||||||
@@ -988,8 +988,8 @@ class ShortcutHandler:
|
|||||||
shortcuts_data = VDFHandler.load(shortcuts_vdf_path, binary=True)
|
shortcuts_data = VDFHandler.load(shortcuts_vdf_path, binary=True)
|
||||||
if shortcuts_data and 'shortcuts' in shortcuts_data:
|
if shortcuts_data and 'shortcuts' in shortcuts_data:
|
||||||
for idx, shortcut in shortcuts_data['shortcuts'].items():
|
for idx, shortcut in shortcuts_data['shortcuts'].items():
|
||||||
app_name = shortcut.get('AppName', '').strip()
|
app_name = shortcut.get('AppName', shortcut.get('appname', '')).strip()
|
||||||
exe = shortcut.get('Exe', '').strip('"').strip()
|
exe = shortcut.get('Exe', shortcut.get('exe', '')).strip('"').strip()
|
||||||
vdf_shortcuts.append((app_name, exe, idx))
|
vdf_shortcuts.append((app_name, exe, idx))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Error parsing shortcuts.vdf for exe path matching: {e}")
|
self.logger.error(f"Error parsing shortcuts.vdf for exe path matching: {e}")
|
||||||
@@ -1054,9 +1054,9 @@ class ShortcutHandler:
|
|||||||
self.logger.warning(f"Skipping invalid shortcut entry (not a dict) at index {shortcut_id} in {shortcuts_file}")
|
self.logger.warning(f"Skipping invalid shortcut entry (not a dict) at index {shortcut_id} in {shortcuts_file}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
app_name = shortcut.get('AppName')
|
app_name = shortcut.get('AppName', shortcut.get('appname'))
|
||||||
exe_path = shortcut.get('Exe', '').strip('"')
|
exe_path = shortcut.get('Exe', shortcut.get('exe', '')).strip('"')
|
||||||
start_dir = shortcut.get('StartDir', '').strip('"')
|
start_dir = shortcut.get('StartDir', shortcut.get('startdir', '')).strip('"')
|
||||||
|
|
||||||
# Check if the base name of the exe_path matches the target
|
# Check if the base name of the exe_path matches the target
|
||||||
if app_name and start_dir and os.path.basename(exe_path) == executable_name:
|
if app_name and start_dir and os.path.basename(exe_path) == executable_name:
|
||||||
@@ -1180,18 +1180,21 @@ class ShortcutHandler:
|
|||||||
self.logger.warning(f"Skipping invalid shortcut entry at index {index} in {vdf_path}")
|
self.logger.warning(f"Skipping invalid shortcut entry at index {index} in {vdf_path}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
exe_path = shortcut_details.get('Exe', '').strip('"') # Get Exe path, remove quotes
|
exe_path = shortcut_details.get('Exe', shortcut_details.get('exe', '')).strip('"') # Get Exe path, remove quotes
|
||||||
app_name = shortcut_details.get('AppName', 'Unknown Shortcut')
|
app_name = shortcut_details.get('AppName', shortcut_details.get('appname', 'Unknown Shortcut'))
|
||||||
|
|
||||||
# Check if the executable_name is present in the Exe path
|
# Check if the executable_name is present in the Exe path
|
||||||
if executable_name in os.path.basename(exe_path):
|
if executable_name in os.path.basename(exe_path):
|
||||||
self.logger.info(f"Found matching shortcut '{app_name}' in {vdf_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 = {
|
match = {
|
||||||
'AppName': app_name,
|
'AppName': app_name,
|
||||||
'Exe': exe_path, # Store unquoted path
|
'Exe': exe_path, # Store unquoted path
|
||||||
'StartDir': shortcut_details.get('StartDir', '').strip('"') # Unquoted
|
'StartDir': start_dir,
|
||||||
# Add other useful fields if needed, e.g., 'ShortcutPath'
|
'appid': app_id # Include the AppID for conversion to unsigned
|
||||||
}
|
}
|
||||||
matching_shortcuts.append(match)
|
matching_shortcuts.append(match)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -132,7 +132,8 @@ class WabbajackParser:
|
|||||||
'falloutnv': 'Fallout New Vegas',
|
'falloutnv': 'Fallout New Vegas',
|
||||||
'oblivion': 'Oblivion',
|
'oblivion': 'Oblivion',
|
||||||
'starfield': 'Starfield',
|
'starfield': 'Starfield',
|
||||||
'oblivion_remastered': 'Oblivion Remastered'
|
'oblivion_remastered': 'Oblivion Remastered',
|
||||||
|
'enderal': 'Enderal'
|
||||||
}
|
}
|
||||||
return [display_names.get(game, game) for game in self.supported_games]
|
return [display_names.get(game, game) for game in self.supported_games]
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import shutil
|
|||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import glob
|
import glob
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple, List, Dict
|
||||||
from .subprocess_utils import get_clean_subprocess_env
|
from .subprocess_utils import get_clean_subprocess_env
|
||||||
|
|
||||||
# Initialize logger
|
# Initialize logger
|
||||||
@@ -197,16 +197,43 @@ class WineUtils:
|
|||||||
logger.error(f"Error editing binary working paths: {e}")
|
logger.error(f"Error editing binary working paths: {e}")
|
||||||
return False
|
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
|
@staticmethod
|
||||||
def _strip_sdcard_path(path):
|
def _strip_sdcard_path(path):
|
||||||
"""
|
"""
|
||||||
Strip /run/media/deck/UUID from SD card paths
|
Strip any detected SD card mount prefix from paths
|
||||||
Internal helper method
|
Handles both /run/media/mmcblk0p1 and /run/media/deck/UUID patterns
|
||||||
"""
|
"""
|
||||||
if path.startswith("/run/media/deck/"):
|
sd_mounts = WineUtils._get_sd_card_mounts()
|
||||||
parts = path.split("/", 5)
|
|
||||||
if len(parts) >= 6:
|
for mount in sd_mounts:
|
||||||
return "/" + parts[5]
|
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
|
return path
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -609,12 +636,46 @@ class WineUtils:
|
|||||||
"""
|
"""
|
||||||
# Clean up the version string for directory matching
|
# Clean up the version string for directory matching
|
||||||
version_patterns = [proton_version, proton_version.replace(' ', '_'), proton_version.replace(' ', '')]
|
version_patterns = [proton_version, proton_version.replace(' ', '_'), proton_version.replace(' ', '')]
|
||||||
# Standard Steam library locations
|
|
||||||
steam_common_paths = [
|
# Get actual Steam library paths from libraryfolders.vdf (smart detection)
|
||||||
Path.home() / ".steam/steam/steamapps/common",
|
steam_common_paths = []
|
||||||
Path.home() / ".local/share/Steam/steamapps/common",
|
compatibility_paths = []
|
||||||
Path.home() / ".steam/root/steamapps/common"
|
|
||||||
]
|
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
|
# Special handling for Proton 9: try all possible directory names
|
||||||
if proton_version.strip().startswith("Proton 9"):
|
if proton_version.strip().startswith("Proton 9"):
|
||||||
proton9_candidates = ["Proton 9.0", "Proton 9.0 (Beta)"]
|
proton9_candidates = ["Proton 9.0", "Proton 9.0 (Beta)"]
|
||||||
@@ -628,8 +689,9 @@ class WineUtils:
|
|||||||
wine_bin = subdir / "files/bin/wine"
|
wine_bin = subdir / "files/bin/wine"
|
||||||
if wine_bin.is_file():
|
if wine_bin.is_file():
|
||||||
return str(wine_bin)
|
return str(wine_bin)
|
||||||
# General case: try version patterns
|
# General case: try version patterns in both steamapps and compatibilitytools.d
|
||||||
for base_path in steam_common_paths:
|
all_paths = steam_common_paths + compatibility_paths
|
||||||
|
for base_path in all_paths:
|
||||||
if not base_path.is_dir():
|
if not base_path.is_dir():
|
||||||
continue
|
continue
|
||||||
for pattern in version_patterns:
|
for pattern in version_patterns:
|
||||||
@@ -643,7 +705,20 @@ class WineUtils:
|
|||||||
wine_bin = subdir / "files/bin/wine"
|
wine_bin = subdir / "files/bin/wine"
|
||||||
if wine_bin.is_file():
|
if wine_bin.is_file():
|
||||||
return str(wine_bin)
|
return str(wine_bin)
|
||||||
# Fallback: Try 'Proton - Experimental' if present
|
# Fallback: Try user's configured Proton version
|
||||||
|
try:
|
||||||
|
from .config_handler import ConfigHandler
|
||||||
|
config = ConfigHandler()
|
||||||
|
fallback_path = config.get('proton_path', 'auto')
|
||||||
|
if fallback_path != 'auto':
|
||||||
|
fallback_wine_bin = Path(fallback_path) / "files/bin/wine"
|
||||||
|
if fallback_wine_bin.is_file():
|
||||||
|
logger.warning(f"Requested Proton version '{proton_version}' not found. Falling back to user's configured version.")
|
||||||
|
return str(fallback_wine_bin)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Final fallback: Try 'Proton - Experimental' if present
|
||||||
for base_path in steam_common_paths:
|
for base_path in steam_common_paths:
|
||||||
wine_bin = base_path / "Proton - Experimental" / "files/bin/wine"
|
wine_bin = base_path / "Proton - Experimental" / "files/bin/wine"
|
||||||
if wine_bin.is_file():
|
if wine_bin.is_file():
|
||||||
@@ -698,4 +773,289 @@ class WineUtils:
|
|||||||
proton_path = str(Path(wine_bin).parent.parent)
|
proton_path = str(Path(wine_bin).parent.parent)
|
||||||
logger.debug(f"Found Proton path: {proton_path}")
|
logger.debug(f"Found Proton path: {proton_path}")
|
||||||
|
|
||||||
return compatdata_path, proton_path, wine_bin
|
return compatdata_path, proton_path, wine_bin
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_steam_library_paths() -> List[Path]:
|
||||||
|
"""
|
||||||
|
Get all Steam library paths from libraryfolders.vdf (handles Flatpak, custom locations, etc.).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of Path objects for Steam library directories
|
||||||
|
"""
|
||||||
|
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]:
|
||||||
|
"""
|
||||||
|
Get all compatibility tool paths for GE-Proton and other custom Proton versions.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of Path objects for compatibility tool directories
|
||||||
|
"""
|
||||||
|
compat_paths = [
|
||||||
|
Path.home() / ".steam/steam/compatibilitytools.d",
|
||||||
|
Path.home() / ".local/share/Steam/compatibilitytools.d"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Return only existing paths
|
||||||
|
return [path for path in compat_paths if path.exists()]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def scan_ge_proton_versions() -> List[Dict[str, any]]:
|
||||||
|
"""
|
||||||
|
Scan for available GE-Proton versions in compatibilitytools.d directories.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of dicts with version info, sorted by priority (newest first)
|
||||||
|
"""
|
||||||
|
logger.info("Scanning for available GE-Proton versions...")
|
||||||
|
|
||||||
|
found_versions = []
|
||||||
|
compat_paths = WineUtils.get_compatibility_tool_paths()
|
||||||
|
|
||||||
|
if not compat_paths:
|
||||||
|
logger.warning("No compatibility tool paths found")
|
||||||
|
return []
|
||||||
|
|
||||||
|
for compat_path in compat_paths:
|
||||||
|
logger.debug(f"Scanning compatibility tools: {compat_path}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Look for GE-Proton directories
|
||||||
|
for proton_dir in compat_path.iterdir():
|
||||||
|
if not proton_dir.is_dir():
|
||||||
|
continue
|
||||||
|
|
||||||
|
dir_name = proton_dir.name
|
||||||
|
if not dir_name.startswith("GE-Proton"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check for wine binary
|
||||||
|
wine_bin = proton_dir / "files" / "bin" / "wine"
|
||||||
|
if not wine_bin.exists() or not wine_bin.is_file():
|
||||||
|
logger.debug(f"Skipping {dir_name} - no wine binary found")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Parse version from directory name (e.g., "GE-Proton10-16")
|
||||||
|
version_match = re.match(r'GE-Proton(\d+)-(\d+)', dir_name)
|
||||||
|
if version_match:
|
||||||
|
major_ver = int(version_match.group(1))
|
||||||
|
minor_ver = int(version_match.group(2))
|
||||||
|
|
||||||
|
# Calculate priority: GE-Proton gets highest priority
|
||||||
|
# Priority format: 200 (base) + major*10 + minor (e.g., 200 + 100 + 16 = 316)
|
||||||
|
priority = 200 + (major_ver * 10) + minor_ver
|
||||||
|
|
||||||
|
found_versions.append({
|
||||||
|
'name': dir_name,
|
||||||
|
'path': proton_dir,
|
||||||
|
'wine_bin': wine_bin,
|
||||||
|
'priority': priority,
|
||||||
|
'major_version': major_ver,
|
||||||
|
'minor_version': minor_ver,
|
||||||
|
'type': 'GE-Proton'
|
||||||
|
})
|
||||||
|
logger.debug(f"Found {dir_name} at {proton_dir} (priority: {priority})")
|
||||||
|
else:
|
||||||
|
logger.debug(f"Skipping {dir_name} - unknown GE-Proton version format")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error scanning {compat_path}: {e}")
|
||||||
|
|
||||||
|
# Sort by priority (highest first, so newest GE-Proton versions come first)
|
||||||
|
found_versions.sort(key=lambda x: x['priority'], reverse=True)
|
||||||
|
|
||||||
|
logger.info(f"Found {len(found_versions)} GE-Proton version(s)")
|
||||||
|
return found_versions
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def scan_valve_proton_versions() -> List[Dict[str, any]]:
|
||||||
|
"""
|
||||||
|
Scan for available Valve Proton versions with fallback priority.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of dicts with version info, sorted by priority (best first)
|
||||||
|
"""
|
||||||
|
logger.info("Scanning for available Valve Proton versions...")
|
||||||
|
|
||||||
|
found_versions = []
|
||||||
|
steam_libs = WineUtils.get_steam_library_paths()
|
||||||
|
|
||||||
|
if not steam_libs:
|
||||||
|
logger.warning("No Steam library paths found")
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Priority order for Valve Proton versions
|
||||||
|
# Note: GE-Proton uses 200+ range, so Valve Proton gets 100+ range
|
||||||
|
preferred_versions = [
|
||||||
|
("Proton - Experimental", 150), # Higher priority than regular Valve Proton
|
||||||
|
("Proton 10.0", 140),
|
||||||
|
("Proton 9.0", 130),
|
||||||
|
("Proton 9.0 (Beta)", 125)
|
||||||
|
]
|
||||||
|
|
||||||
|
for steam_path in steam_libs:
|
||||||
|
logger.debug(f"Scanning Steam library: {steam_path}")
|
||||||
|
|
||||||
|
for version_name, priority in preferred_versions:
|
||||||
|
proton_path = steam_path / version_name
|
||||||
|
wine_bin = proton_path / "files" / "bin" / "wine"
|
||||||
|
|
||||||
|
if wine_bin.exists() and wine_bin.is_file():
|
||||||
|
found_versions.append({
|
||||||
|
'name': version_name,
|
||||||
|
'path': proton_path,
|
||||||
|
'wine_bin': wine_bin,
|
||||||
|
'priority': priority,
|
||||||
|
'type': 'Valve-Proton'
|
||||||
|
})
|
||||||
|
logger.debug(f"Found {version_name} at {proton_path}")
|
||||||
|
|
||||||
|
# Sort by priority (highest first)
|
||||||
|
found_versions.sort(key=lambda x: x['priority'], reverse=True)
|
||||||
|
|
||||||
|
# Remove duplicates while preserving order
|
||||||
|
unique_versions = []
|
||||||
|
seen_names = set()
|
||||||
|
for version in found_versions:
|
||||||
|
if version['name'] not in seen_names:
|
||||||
|
unique_versions.append(version)
|
||||||
|
seen_names.add(version['name'])
|
||||||
|
|
||||||
|
logger.info(f"Found {len(unique_versions)} unique Valve Proton version(s)")
|
||||||
|
return unique_versions
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def scan_all_proton_versions() -> List[Dict[str, any]]:
|
||||||
|
"""
|
||||||
|
Scan for all available Proton versions (GE-Proton + Valve Proton) with unified priority.
|
||||||
|
|
||||||
|
Priority Chain (highest to lowest):
|
||||||
|
1. GE-Proton10-16+ (priority 316+)
|
||||||
|
2. GE-Proton10-* (priority 200+)
|
||||||
|
3. Proton - Experimental (priority 150)
|
||||||
|
4. Proton 10.0 (priority 140)
|
||||||
|
5. Proton 9.0 (priority 130)
|
||||||
|
6. Proton 9.0 (Beta) (priority 125)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of dicts with version info, sorted by priority (best first)
|
||||||
|
"""
|
||||||
|
logger.info("Scanning for all available Proton versions...")
|
||||||
|
|
||||||
|
all_versions = []
|
||||||
|
|
||||||
|
# Scan GE-Proton versions (highest priority)
|
||||||
|
ge_versions = WineUtils.scan_ge_proton_versions()
|
||||||
|
all_versions.extend(ge_versions)
|
||||||
|
|
||||||
|
# Scan Valve Proton versions
|
||||||
|
valve_versions = WineUtils.scan_valve_proton_versions()
|
||||||
|
all_versions.extend(valve_versions)
|
||||||
|
|
||||||
|
# Sort by priority (highest first)
|
||||||
|
all_versions.sort(key=lambda x: x['priority'], reverse=True)
|
||||||
|
|
||||||
|
# Remove duplicates while preserving order
|
||||||
|
unique_versions = []
|
||||||
|
seen_names = set()
|
||||||
|
for version in all_versions:
|
||||||
|
if version['name'] not in seen_names:
|
||||||
|
unique_versions.append(version)
|
||||||
|
seen_names.add(version['name'])
|
||||||
|
|
||||||
|
if unique_versions:
|
||||||
|
logger.info(f"Found {len(unique_versions)} total Proton version(s)")
|
||||||
|
logger.info(f"Best available: {unique_versions[0]['name']} ({unique_versions[0]['type']})")
|
||||||
|
else:
|
||||||
|
logger.warning("No Proton versions found")
|
||||||
|
|
||||||
|
return unique_versions
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def select_best_proton() -> Optional[Dict[str, any]]:
|
||||||
|
"""
|
||||||
|
Select the best available Proton version (GE-Proton or Valve Proton) using unified precedence.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with version info for the best Proton, or None if none found
|
||||||
|
"""
|
||||||
|
available_versions = WineUtils.scan_all_proton_versions()
|
||||||
|
|
||||||
|
if not available_versions:
|
||||||
|
logger.warning("No compatible Proton versions found")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Return the highest priority version (first in sorted list)
|
||||||
|
best_version = available_versions[0]
|
||||||
|
logger.info(f"Selected best Proton version: {best_version['name']} ({best_version['type']})")
|
||||||
|
return best_version
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def select_best_valve_proton() -> Optional[Dict[str, any]]:
|
||||||
|
"""
|
||||||
|
Select the best available Valve Proton version using fallback precedence.
|
||||||
|
Note: This method is kept for backward compatibility. Consider using select_best_proton() instead.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with version info for the best Proton, or None if none found
|
||||||
|
"""
|
||||||
|
available_versions = WineUtils.scan_valve_proton_versions()
|
||||||
|
|
||||||
|
if not available_versions:
|
||||||
|
logger.warning("No compatible Valve Proton versions found")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Return the highest priority version (first in sorted list)
|
||||||
|
best_version = available_versions[0]
|
||||||
|
logger.info(f"Selected Valve Proton version: {best_version['name']}")
|
||||||
|
return best_version
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_proton_requirements() -> Tuple[bool, str, Optional[Dict[str, any]]]:
|
||||||
|
"""
|
||||||
|
Check if compatible Proton version is available for workflows.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (requirements_met, status_message, proton_info)
|
||||||
|
- requirements_met: True if compatible Proton found
|
||||||
|
- status_message: Human-readable status for display to user
|
||||||
|
- proton_info: Dict with Proton details if found, None otherwise
|
||||||
|
"""
|
||||||
|
logger.info("Checking Proton requirements for workflow...")
|
||||||
|
|
||||||
|
# Scan for available Proton versions (includes GE-Proton + Valve Proton)
|
||||||
|
best_proton = WineUtils.select_best_proton()
|
||||||
|
|
||||||
|
if best_proton:
|
||||||
|
# Compatible Proton found
|
||||||
|
proton_type = best_proton.get('type', 'Unknown')
|
||||||
|
status_msg = f"✓ Using {best_proton['name']} ({proton_type}) for this workflow"
|
||||||
|
logger.info(f"Proton requirements satisfied: {best_proton['name']} ({proton_type})")
|
||||||
|
return True, status_msg, best_proton
|
||||||
|
else:
|
||||||
|
# No compatible Proton found
|
||||||
|
status_msg = "✗ No compatible Proton version found (GE-Proton 10+, Proton 9+, 10, or Experimental required)"
|
||||||
|
logger.warning("Proton requirements not met - no compatible version found")
|
||||||
|
return False, status_msg, None
|
||||||
263
jackify/backend/handlers/winetricks_handler.py
Normal file
263
jackify/backend/handlers/winetricks_handler.py
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
#!/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', 'auto')
|
||||||
|
|
||||||
|
# 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}")
|
||||||
|
|
||||||
|
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:
|
||||||
|
components_to_install = specific_components
|
||||||
|
self.logger.info(f"Installing specific components: {components_to_install}")
|
||||||
|
else:
|
||||||
|
components_to_install = ["fontsmooth=rgb", "xact", "xact_x64", "vcrun2022"]
|
||||||
|
self.logger.info(f"Installing default components: {components_to_install}")
|
||||||
|
|
||||||
|
if not components_to_install:
|
||||||
|
self.logger.info("No Wine components to install.")
|
||||||
|
return True
|
||||||
|
|
||||||
|
self.logger.info(f"WINEPREFIX: {wineprefix}, Game: {game_var}, Components: {components_to_install}")
|
||||||
|
|
||||||
|
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.")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
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 _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}")
|
||||||
@@ -38,6 +38,44 @@ class AutomatedPrefixService:
|
|||||||
"""Get consistent progress timestamp"""
|
"""Get consistent progress timestamp"""
|
||||||
from jackify.shared.timing import get_timestamp
|
from jackify.shared.timing import get_timestamp
|
||||||
return get_timestamp()
|
return get_timestamp()
|
||||||
|
|
||||||
|
def _get_user_proton_version(self):
|
||||||
|
"""Get user's preferred Proton version from config, with fallback to auto-detection"""
|
||||||
|
try:
|
||||||
|
from jackify.backend.handlers.config_handler import ConfigHandler
|
||||||
|
from jackify.backend.handlers.wine_utils import WineUtils
|
||||||
|
|
||||||
|
config_handler = ConfigHandler()
|
||||||
|
user_proton_path = config_handler.get('proton_path', 'auto')
|
||||||
|
|
||||||
|
if user_proton_path == 'auto':
|
||||||
|
# Use enhanced fallback logic with GE-Proton preference
|
||||||
|
logger.info("User selected auto-detect, using GE-Proton → Experimental → Proton precedence")
|
||||||
|
return WineUtils.select_best_proton()
|
||||||
|
else:
|
||||||
|
# User has selected a specific Proton version
|
||||||
|
# Use the exact directory name for Steam config.vdf
|
||||||
|
try:
|
||||||
|
proton_version = os.path.basename(user_proton_path)
|
||||||
|
# GE-Proton uses exact directory name, Valve Proton needs lowercase conversion
|
||||||
|
if proton_version.startswith('GE-Proton'):
|
||||||
|
# Keep GE-Proton name exactly as-is
|
||||||
|
steam_proton_name = proton_version
|
||||||
|
else:
|
||||||
|
# Convert Valve Proton names to Steam's format
|
||||||
|
steam_proton_name = proton_version.lower().replace(' - ', '_').replace(' ', '_').replace('-', '_')
|
||||||
|
if not steam_proton_name.startswith('proton'):
|
||||||
|
steam_proton_name = f"proton_{steam_proton_name}"
|
||||||
|
|
||||||
|
logger.info(f"Using user-selected Proton: {steam_proton_name}")
|
||||||
|
return steam_proton_name
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Invalid user Proton path '{user_proton_path}', falling back to auto: {e}")
|
||||||
|
return WineUtils.select_best_proton()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get user Proton preference, using default: {e}")
|
||||||
|
return "proton_experimental"
|
||||||
|
|
||||||
|
|
||||||
def create_shortcut_with_native_service(self, shortcut_name: str, exe_path: str,
|
def create_shortcut_with_native_service(self, shortcut_name: str, exe_path: str,
|
||||||
@@ -87,6 +125,9 @@ class AutomatedPrefixService:
|
|||||||
logger.warning(f"Could not generate STEAM_COMPAT_MOUNTS, using default: {e}")
|
logger.warning(f"Could not generate STEAM_COMPAT_MOUNTS, using default: {e}")
|
||||||
launch_options = "%command%"
|
launch_options = "%command%"
|
||||||
|
|
||||||
|
# Get user's preferred Proton version
|
||||||
|
proton_version = self._get_user_proton_version()
|
||||||
|
|
||||||
# Create shortcut with Proton using native service
|
# Create shortcut with Proton using native service
|
||||||
success, app_id = steam_service.create_shortcut_with_proton(
|
success, app_id = steam_service.create_shortcut_with_proton(
|
||||||
app_name=shortcut_name,
|
app_name=shortcut_name,
|
||||||
@@ -94,7 +135,7 @@ class AutomatedPrefixService:
|
|||||||
start_dir=modlist_install_dir,
|
start_dir=modlist_install_dir,
|
||||||
launch_options=launch_options,
|
launch_options=launch_options,
|
||||||
tags=["Jackify"],
|
tags=["Jackify"],
|
||||||
proton_version="proton_experimental"
|
proton_version=proton_version
|
||||||
)
|
)
|
||||||
|
|
||||||
if success and app_id:
|
if success and app_id:
|
||||||
@@ -292,13 +333,13 @@ class AutomatedPrefixService:
|
|||||||
logger.error(f"Steam userdata directory not found: {userdata_dir}")
|
logger.error(f"Steam userdata directory not found: {userdata_dir}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Find the first user directory (most systems have only one user)
|
# 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()]
|
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:
|
if not user_dirs:
|
||||||
logger.error("No Steam user directories found in userdata")
|
logger.error("No valid Steam user directories found in userdata (user 0 is not valid)")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Use the first user directory found
|
# Use the first valid user directory found
|
||||||
user_dir = user_dirs[0]
|
user_dir = user_dirs[0]
|
||||||
shortcuts_path = user_dir / "config" / "shortcuts.vdf"
|
shortcuts_path = user_dir / "config" / "shortcuts.vdf"
|
||||||
|
|
||||||
@@ -407,7 +448,7 @@ exit"""
|
|||||||
|
|
||||||
if shortcut_name in name:
|
if shortcut_name in name:
|
||||||
appid = shortcut.get('appid')
|
appid = shortcut.get('appid')
|
||||||
exe_path = shortcut.get('Exe', '')
|
exe_path = shortcut.get('Exe', '').strip('"')
|
||||||
|
|
||||||
logger.info(f"Found shortcut: {name}")
|
logger.info(f"Found shortcut: {name}")
|
||||||
logger.info(f" AppID: {appid}")
|
logger.info(f" AppID: {appid}")
|
||||||
@@ -1718,21 +1759,15 @@ echo Prefix creation complete.
|
|||||||
progress_callback("=== Steam Integration ===")
|
progress_callback("=== Steam Integration ===")
|
||||||
progress_callback(f"{self._get_progress_timestamp()} Creating Steam shortcut with native service")
|
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
|
from ..handlers.modlist_handler import ModlistHandler
|
||||||
modlist_handler = ModlistHandler()
|
modlist_handler = ModlistHandler()
|
||||||
special_game_type = modlist_handler.detect_special_game_type(modlist_install_dir)
|
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
|
custom_launch_options = None
|
||||||
if special_game_type == "enderal":
|
if special_game_type in ["fnv", "enderal"]:
|
||||||
custom_launch_options = self._generate_special_game_launch_options(special_game_type, modlist_install_dir)
|
logger.info(f"Using registry injection approach for {special_game_type.upper()} modlist")
|
||||||
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")
|
|
||||||
else:
|
else:
|
||||||
logger.debug("Standard modlist - no special game handling needed")
|
logger.debug("Standard modlist - no special game handling needed")
|
||||||
|
|
||||||
@@ -1808,23 +1843,19 @@ echo Prefix creation complete.
|
|||||||
if progress_callback:
|
if progress_callback:
|
||||||
progress_callback(f"{self._get_progress_timestamp()} Setup verification completed")
|
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)
|
# Get prefix path (needed for logging regardless of game type)
|
||||||
prefix_path = self.get_prefix_path(appid)
|
prefix_path = self.get_prefix_path(appid)
|
||||||
|
|
||||||
if special_game_type == "fnv":
|
if special_game_type in ["fnv", "enderal"]:
|
||||||
logger.info("Step 5: Injecting FNV game registry entries")
|
logger.info(f"Step 5: Injecting {special_game_type.upper()} game registry entries")
|
||||||
if progress_callback:
|
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:
|
if prefix_path:
|
||||||
self._inject_game_registry_entries(str(prefix_path))
|
self._inject_game_registry_entries(str(prefix_path))
|
||||||
else:
|
else:
|
||||||
logger.warning("Could not find prefix path for registry injection")
|
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:
|
else:
|
||||||
logger.info("Step 5: Skipping registry injection for standard modlist")
|
logger.info("Step 5: Skipping registry injection for standard modlist")
|
||||||
if progress_callback:
|
if progress_callback:
|
||||||
@@ -2499,8 +2530,8 @@ echo Prefix creation complete.
|
|||||||
# Try the standard Steam userdata path
|
# Try the standard Steam userdata path
|
||||||
steam_userdata_path = Path.home() / ".steam" / "steam" / "userdata"
|
steam_userdata_path = Path.home() / ".steam" / "steam" / "userdata"
|
||||||
if steam_userdata_path.exists():
|
if steam_userdata_path.exists():
|
||||||
# Find the first user directory (usually only one on Steam Deck)
|
# 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()]
|
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:
|
if user_dirs:
|
||||||
localconfig_path = user_dirs[0] / "config" / "localconfig.vdf"
|
localconfig_path = user_dirs[0] / "config" / "localconfig.vdf"
|
||||||
if localconfig_path.exists():
|
if localconfig_path.exists():
|
||||||
@@ -2601,8 +2632,11 @@ echo Prefix creation complete.
|
|||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env['STEAM_COMPAT_CLIENT_INSTALL_PATH'] = str(steam_root)
|
env['STEAM_COMPAT_CLIENT_INSTALL_PATH'] = str(steam_root)
|
||||||
env['STEAM_COMPAT_DATA_PATH'] = str(compatdata_dir / str(abs(appid)))
|
env['STEAM_COMPAT_DATA_PATH'] = str(compatdata_dir / str(abs(appid)))
|
||||||
# Suppress GUI windows by unsetting DISPLAY
|
# Suppress GUI windows using jackify-engine's proven approach
|
||||||
env['DISPLAY'] = ''
|
env['DISPLAY'] = ''
|
||||||
|
env['WAYLAND_DISPLAY'] = ''
|
||||||
|
env['WINEDEBUG'] = '-all'
|
||||||
|
env['WINEDLLOVERRIDES'] = 'msdia80.dll=n;conhost.exe=d;cmd.exe=d'
|
||||||
|
|
||||||
# Create the compatdata directory
|
# Create the compatdata directory
|
||||||
compat_dir = compatdata_dir / str(abs(appid))
|
compat_dir = compatdata_dir / str(abs(appid))
|
||||||
@@ -2616,7 +2650,9 @@ echo Prefix creation complete.
|
|||||||
cmd = [str(proton_path), 'run', 'wineboot', '-u']
|
cmd = [str(proton_path), 'run', 'wineboot', '-u']
|
||||||
logger.info(f"Running: {' '.join(cmd)}")
|
logger.info(f"Running: {' '.join(cmd)}")
|
||||||
|
|
||||||
result = subprocess.run(cmd, env=env, capture_output=True, text=True, timeout=60)
|
# Use jackify-engine's approach: UseShellExecute=false, CreateNoWindow=true equivalent
|
||||||
|
result = subprocess.run(cmd, env=env, capture_output=True, text=True, timeout=60,
|
||||||
|
shell=False, creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0))
|
||||||
logger.info(f"Proton exit code: {result.returncode}")
|
logger.info(f"Proton exit code: {result.returncode}")
|
||||||
|
|
||||||
if result.stdout:
|
if result.stdout:
|
||||||
@@ -2644,31 +2680,64 @@ echo Prefix creation complete.
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def _find_proton_binary(self, proton_common_dir: Path) -> Optional[Path]:
|
def _find_proton_binary(self, proton_common_dir: Path) -> Optional[Path]:
|
||||||
"""Locate a Proton wrapper script to use (prefer Experimental)."""
|
"""Locate a Proton wrapper script to use, respecting user's configuration."""
|
||||||
candidates = []
|
try:
|
||||||
preferred = [
|
from jackify.backend.handlers.config_handler import ConfigHandler
|
||||||
"Proton - Experimental",
|
from jackify.backend.handlers.wine_utils import WineUtils
|
||||||
"Proton 9.0",
|
|
||||||
"Proton 8.0",
|
config = ConfigHandler()
|
||||||
"Proton Hotfix",
|
user_proton_path = config.get('proton_path', 'auto')
|
||||||
]
|
|
||||||
|
# If user selected a specific Proton, try that first
|
||||||
for name in preferred:
|
if user_proton_path != 'auto':
|
||||||
p = proton_common_dir / name / "proton"
|
# Resolve symlinks to handle ~/.steam/steam -> ~/.local/share/Steam
|
||||||
if p.exists():
|
resolved_proton_path = os.path.realpath(user_proton_path)
|
||||||
candidates.append(p)
|
|
||||||
|
# Check for wine binary in different Proton structures
|
||||||
# As a fallback, scan all Proton* dirs
|
valve_proton_wine = Path(resolved_proton_path) / "dist" / "bin" / "wine"
|
||||||
if not candidates and proton_common_dir.exists():
|
ge_proton_wine = Path(resolved_proton_path) / "files" / "bin" / "wine"
|
||||||
for p in proton_common_dir.glob("Proton*/proton"):
|
|
||||||
candidates.append(p)
|
if valve_proton_wine.exists() or ge_proton_wine.exists():
|
||||||
|
# Found user's Proton, now find the proton wrapper script
|
||||||
if not candidates:
|
proton_wrapper = Path(resolved_proton_path) / "proton"
|
||||||
logger.error("No Proton wrapper found under steamapps/common")
|
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
|
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]]:
|
def replace_existing_shortcut(self, shortcut_name: str, exe_path: str, modlist_install_dir: str) -> Tuple[bool, Optional[int]]:
|
||||||
"""
|
"""
|
||||||
@@ -2718,26 +2787,39 @@ echo Prefix creation complete.
|
|||||||
|
|
||||||
def verify_compatibility_tool_persists(self, appid: int) -> bool:
|
def verify_compatibility_tool_persists(self, appid: int) -> bool:
|
||||||
"""
|
"""
|
||||||
Verify that the compatibility tool setting persists.
|
Verify that the compatibility tool setting persists with correct Proton version.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
appid: The AppID to check
|
appid: The AppID to check
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if compatibility tool is set, False otherwise
|
True if compatibility tool is correctly set, False otherwise
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
config_path = Path.home() / ".steam/steam/config/config.vdf"
|
config_path = Path.home() / ".steam/steam/config/config.vdf"
|
||||||
with open(config_path, 'r') as f:
|
if not config_path.exists():
|
||||||
|
logger.warning("Steam config.vdf not found")
|
||||||
|
return False
|
||||||
|
|
||||||
|
with open(config_path, 'r', encoding='utf-8') as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
|
|
||||||
|
# Check if AppID exists and has a Proton version set
|
||||||
if f'"{appid}"' in content:
|
if f'"{appid}"' in content:
|
||||||
logger.info(" Compatibility tool persists")
|
# Get the expected Proton version
|
||||||
return True
|
expected_proton = self._get_user_proton_version()
|
||||||
|
|
||||||
|
# Look for the Proton version in the compatibility tool mapping
|
||||||
|
if expected_proton in content:
|
||||||
|
logger.info(f" Compatibility tool persists: {expected_proton}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.warning(f"AppID {appid} found but Proton version '{expected_proton}' not set")
|
||||||
|
return False
|
||||||
else:
|
else:
|
||||||
logger.warning("Compatibility tool not found")
|
logger.warning("Compatibility tool not found")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error verifying compatibility tool: {e}")
|
logger.error(f"Error verifying compatibility tool: {e}")
|
||||||
return False
|
return False
|
||||||
@@ -2889,6 +2971,15 @@ echo Prefix creation complete.
|
|||||||
)
|
)
|
||||||
if success:
|
if success:
|
||||||
logger.info(f"Updated registry entry for {config['name']}")
|
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:
|
else:
|
||||||
logger.warning(f"Failed to update registry entry for {config['name']}")
|
logger.warning(f"Failed to update registry entry for {config['name']}")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -293,15 +293,7 @@ class ModlistService:
|
|||||||
elif context.get('machineid'):
|
elif context.get('machineid'):
|
||||||
cmd += ['-m', context['machineid']]
|
cmd += ['-m', context['machineid']]
|
||||||
cmd += ['-o', install_dir_str, '-d', download_dir_str]
|
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
|
# NOTE: API key is passed via environment variable only, not as command line argument
|
||||||
|
|
||||||
# Store original environment values (copied from working code)
|
# Store original environment values (copied from working code)
|
||||||
|
|||||||
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
|
||||||
@@ -40,13 +40,13 @@ class NativeSteamService:
|
|||||||
logger.error("Steam userdata directory not found")
|
logger.error("Steam userdata directory not found")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Find the first user directory (usually there's only one)
|
# 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()]
|
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:
|
if not user_dirs:
|
||||||
logger.error("No Steam user directories found")
|
logger.error("No valid Steam user directories found (user 0 is not valid for shortcuts)")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Use the first user directory
|
# Use the first valid user directory
|
||||||
user_dir = user_dirs[0]
|
user_dir = user_dirs[0]
|
||||||
self.user_id = user_dir.name
|
self.user_id = user_dir.name
|
||||||
self.user_config_path = user_dir / "config"
|
self.user_config_path = user_dir / "config"
|
||||||
@@ -327,17 +327,27 @@ class NativeSteamService:
|
|||||||
logger.error(f"Error setting Proton version: {e}")
|
logger.error(f"Error setting Proton version: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def create_shortcut_with_proton(self, app_name: str, exe_path: str, start_dir: str = None,
|
def create_shortcut_with_proton(self, app_name: str, exe_path: str, start_dir: str = None,
|
||||||
launch_options: str = "%command%", tags: List[str] = None,
|
launch_options: str = "%command%", tags: List[str] = None,
|
||||||
proton_version: str = "proton_experimental") -> Tuple[bool, Optional[int]]:
|
proton_version: str = None) -> Tuple[bool, Optional[int]]:
|
||||||
"""
|
"""
|
||||||
Complete workflow: Create shortcut and set Proton version.
|
Complete workflow: Create shortcut and set Proton version.
|
||||||
|
|
||||||
This is the main method that replaces STL entirely.
|
This is the main method that replaces STL entirely.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(success, app_id) - Success status and the AppID
|
(success, app_id) - Success status and the AppID
|
||||||
"""
|
"""
|
||||||
|
# Auto-detect best Proton version if none provided
|
||||||
|
if proton_version is None:
|
||||||
|
try:
|
||||||
|
from jackify.backend.core.modlist_operations import _get_user_proton_version
|
||||||
|
proton_version = _get_user_proton_version()
|
||||||
|
logger.info(f"Auto-detected Proton version: {proton_version}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to auto-detect Proton, falling back to experimental: {e}")
|
||||||
|
proton_version = "proton_experimental"
|
||||||
|
|
||||||
logger.info(f"Creating shortcut with Proton: '{app_name}' -> '{proton_version}'")
|
logger.info(f"Creating shortcut with Proton: '{app_name}' -> '{proton_version}'")
|
||||||
|
|
||||||
# Step 1: Create the shortcut
|
# Step 1: Create the shortcut
|
||||||
|
|||||||
@@ -103,15 +103,33 @@ class UpdateService:
|
|||||||
# Determine if this is a delta update
|
# Determine if this is a delta update
|
||||||
is_delta = '.delta' in download_url or 'delta' in download_url.lower()
|
is_delta = '.delta' in download_url or 'delta' in download_url.lower()
|
||||||
|
|
||||||
return UpdateInfo(
|
# Safety checks to prevent segfault
|
||||||
version=latest_version,
|
try:
|
||||||
tag_name=release_data['tag_name'],
|
# Sanitize string fields
|
||||||
release_date=release_data['published_at'],
|
safe_version = str(latest_version) if latest_version else ""
|
||||||
changelog=release_data.get('body', ''),
|
safe_tag = str(release_data.get('tag_name', ''))
|
||||||
download_url=download_url,
|
safe_date = str(release_data.get('published_at', ''))
|
||||||
file_size=file_size,
|
safe_changelog = str(release_data.get('body', ''))[:1000] # Limit size
|
||||||
is_delta_update=is_delta
|
safe_url = str(download_url)
|
||||||
)
|
|
||||||
|
logger.debug(f"Creating UpdateInfo for version {safe_version}")
|
||||||
|
|
||||||
|
update_info = UpdateInfo(
|
||||||
|
version=safe_version,
|
||||||
|
tag_name=safe_tag,
|
||||||
|
release_date=safe_date,
|
||||||
|
changelog=safe_changelog,
|
||||||
|
download_url=safe_url,
|
||||||
|
file_size=file_size,
|
||||||
|
is_delta_update=is_delta
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug(f"UpdateInfo created successfully")
|
||||||
|
return update_info
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to create UpdateInfo: {e}")
|
||||||
|
return None
|
||||||
else:
|
else:
|
||||||
logger.warning(f"No AppImage found in release {latest_version}")
|
logger.warning(f"No AppImage found in release {latest_version}")
|
||||||
|
|
||||||
@@ -173,9 +191,14 @@ class UpdateService:
|
|||||||
def check_worker():
|
def check_worker():
|
||||||
try:
|
try:
|
||||||
update_info = self.check_for_updates()
|
update_info = self.check_for_updates()
|
||||||
|
logger.debug(f"check_worker: Received update_info: {update_info}")
|
||||||
|
logger.debug(f"check_worker: About to call callback...")
|
||||||
callback(update_info)
|
callback(update_info)
|
||||||
|
logger.debug(f"check_worker: Callback completed")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in background update check: {e}")
|
logger.error(f"Error in background update check: {e}")
|
||||||
|
import traceback
|
||||||
|
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||||
callback(None)
|
callback(None)
|
||||||
|
|
||||||
thread = threading.Thread(target=check_worker, daemon=True)
|
thread = threading.Thread(target=check_worker, daemon=True)
|
||||||
@@ -331,42 +354,69 @@ class UpdateService:
|
|||||||
|
|
||||||
script_content = f'''#!/bin/bash
|
script_content = f'''#!/bin/bash
|
||||||
# Jackify Update Helper Script
|
# Jackify Update Helper Script
|
||||||
# This script replaces the current AppImage with the new version
|
# This script safely replaces the current AppImage with the new version
|
||||||
|
|
||||||
CURRENT_APPIMAGE="{current_appimage}"
|
CURRENT_APPIMAGE="{current_appimage}"
|
||||||
NEW_APPIMAGE="{new_appimage}"
|
NEW_APPIMAGE="{new_appimage}"
|
||||||
|
TEMP_NAME="$CURRENT_APPIMAGE.updating"
|
||||||
|
|
||||||
echo "Jackify Update Helper"
|
echo "Jackify Update Helper"
|
||||||
echo "Waiting for Jackify to exit..."
|
echo "Waiting for Jackify to exit..."
|
||||||
|
|
||||||
# Wait for Jackify to exit (give it a few seconds)
|
# Wait longer for Jackify to fully exit and unmount
|
||||||
sleep 3
|
sleep 5
|
||||||
|
|
||||||
echo "Replacing AppImage..."
|
echo "Validating new AppImage..."
|
||||||
|
|
||||||
# Backup current version (optional)
|
# Validate new AppImage exists and is executable
|
||||||
|
if [ ! -f "$NEW_APPIMAGE" ]; then
|
||||||
|
echo "ERROR: New AppImage not found: $NEW_APPIMAGE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test that new AppImage can execute --version
|
||||||
|
if ! timeout 10 "$NEW_APPIMAGE" --version >/dev/null 2>&1; then
|
||||||
|
echo "ERROR: New AppImage failed validation test"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "New AppImage validated successfully"
|
||||||
|
echo "Performing safe replacement..."
|
||||||
|
|
||||||
|
# Backup current version
|
||||||
if [ -f "$CURRENT_APPIMAGE" ]; then
|
if [ -f "$CURRENT_APPIMAGE" ]; then
|
||||||
cp "$CURRENT_APPIMAGE" "$CURRENT_APPIMAGE.backup"
|
cp "$CURRENT_APPIMAGE" "$CURRENT_APPIMAGE.backup"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Replace with new version
|
# Safe replacement: copy to temp name first, then atomic move
|
||||||
if cp "$NEW_APPIMAGE" "$CURRENT_APPIMAGE"; then
|
if cp "$NEW_APPIMAGE" "$TEMP_NAME"; then
|
||||||
chmod +x "$CURRENT_APPIMAGE"
|
chmod +x "$TEMP_NAME"
|
||||||
echo "Update completed successfully!"
|
|
||||||
|
|
||||||
# Clean up temporary file
|
# Atomic move to replace
|
||||||
rm -f "$NEW_APPIMAGE"
|
if mv "$TEMP_NAME" "$CURRENT_APPIMAGE"; then
|
||||||
|
echo "Update completed successfully!"
|
||||||
# Restart Jackify
|
|
||||||
echo "Restarting Jackify..."
|
# Clean up
|
||||||
exec "$CURRENT_APPIMAGE"
|
rm -f "$NEW_APPIMAGE"
|
||||||
else
|
rm -f "$CURRENT_APPIMAGE.backup"
|
||||||
echo "Update failed - could not replace AppImage"
|
|
||||||
# Restore backup if replacement failed
|
# Restart Jackify
|
||||||
if [ -f "$CURRENT_APPIMAGE.backup" ]; then
|
echo "Restarting Jackify..."
|
||||||
mv "$CURRENT_APPIMAGE.backup" "$CURRENT_APPIMAGE"
|
sleep 1
|
||||||
echo "Restored original AppImage"
|
exec "$CURRENT_APPIMAGE"
|
||||||
|
else
|
||||||
|
echo "ERROR: Failed to move updated AppImage"
|
||||||
|
rm -f "$TEMP_NAME"
|
||||||
|
# Restore backup
|
||||||
|
if [ -f "$CURRENT_APPIMAGE.backup" ]; then
|
||||||
|
mv "$CURRENT_APPIMAGE.backup" "$CURRENT_APPIMAGE"
|
||||||
|
echo "Restored original AppImage"
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
else
|
||||||
|
echo "ERROR: Failed to copy new AppImage"
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Clean up this script
|
# Clean up this script
|
||||||
|
|||||||
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": {
|
"targets": {
|
||||||
".NETCoreApp,Version=v8.0": {},
|
".NETCoreApp,Version=v8.0": {},
|
||||||
".NETCoreApp,Version=v8.0/linux-x64": {
|
".NETCoreApp,Version=v8.0/linux-x64": {
|
||||||
"jackify-engine/0.3.14": {
|
"jackify-engine/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Markdig": "0.40.0",
|
"Markdig": "0.40.0",
|
||||||
"Microsoft.Extensions.Configuration.Json": "9.0.1",
|
"Microsoft.Extensions.Configuration.Json": "9.0.1",
|
||||||
@@ -22,16 +22,16 @@
|
|||||||
"SixLabors.ImageSharp": "3.1.6",
|
"SixLabors.ImageSharp": "3.1.6",
|
||||||
"System.CommandLine": "2.0.0-beta4.22272.1",
|
"System.CommandLine": "2.0.0-beta4.22272.1",
|
||||||
"System.CommandLine.NamingConventionBinder": "2.0.0-beta4.22272.1",
|
"System.CommandLine.NamingConventionBinder": "2.0.0-beta4.22272.1",
|
||||||
"Wabbajack.CLI.Builder": "0.3.14",
|
"Wabbajack.CLI.Builder": "0.3.15",
|
||||||
"Wabbajack.Downloaders.Bethesda": "0.3.14",
|
"Wabbajack.Downloaders.Bethesda": "0.3.15",
|
||||||
"Wabbajack.Downloaders.Dispatcher": "0.3.14",
|
"Wabbajack.Downloaders.Dispatcher": "0.3.15",
|
||||||
"Wabbajack.Hashing.xxHash64": "0.3.14",
|
"Wabbajack.Hashing.xxHash64": "0.3.15",
|
||||||
"Wabbajack.Networking.Discord": "0.3.14",
|
"Wabbajack.Networking.Discord": "0.3.15",
|
||||||
"Wabbajack.Networking.GitHub": "0.3.14",
|
"Wabbajack.Networking.GitHub": "0.3.15",
|
||||||
"Wabbajack.Paths.IO": "0.3.14",
|
"Wabbajack.Paths.IO": "0.3.15",
|
||||||
"Wabbajack.Server.Lib": "0.3.14",
|
"Wabbajack.Server.Lib": "0.3.15",
|
||||||
"Wabbajack.Services.OSIntegrated": "0.3.14",
|
"Wabbajack.Services.OSIntegrated": "0.3.15",
|
||||||
"Wabbajack.VFS": "0.3.14",
|
"Wabbajack.VFS": "0.3.15",
|
||||||
"MegaApiClient": "1.0.0.0",
|
"MegaApiClient": "1.0.0.0",
|
||||||
"runtimepack.Microsoft.NETCore.App.Runtime.linux-x64": "8.0.19"
|
"runtimepack.Microsoft.NETCore.App.Runtime.linux-x64": "8.0.19"
|
||||||
},
|
},
|
||||||
@@ -1781,7 +1781,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.CLI.Builder/0.3.14": {
|
"Wabbajack.CLI.Builder/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.Configuration.Json": "9.0.1",
|
"Microsoft.Extensions.Configuration.Json": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
@@ -1791,109 +1791,109 @@
|
|||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"System.CommandLine": "2.0.0-beta4.22272.1",
|
"System.CommandLine": "2.0.0-beta4.22272.1",
|
||||||
"System.CommandLine.NamingConventionBinder": "2.0.0-beta4.22272.1",
|
"System.CommandLine.NamingConventionBinder": "2.0.0-beta4.22272.1",
|
||||||
"Wabbajack.Paths": "0.3.14"
|
"Wabbajack.Paths": "0.3.15"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.CLI.Builder.dll": {}
|
"Wabbajack.CLI.Builder.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Common/0.3.14": {
|
"Wabbajack.Common/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"System.Reactive": "6.0.1",
|
"System.Reactive": "6.0.1",
|
||||||
"Wabbajack.DTOs": "0.3.14",
|
"Wabbajack.DTOs": "0.3.15",
|
||||||
"Wabbajack.Networking.Http": "0.3.14",
|
"Wabbajack.Networking.Http": "0.3.15",
|
||||||
"Wabbajack.Paths.IO": "0.3.14"
|
"Wabbajack.Paths.IO": "0.3.15"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Common.dll": {}
|
"Wabbajack.Common.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Compiler/0.3.14": {
|
"Wabbajack.Compiler/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"F23.StringSimilarity": "6.0.0",
|
"F23.StringSimilarity": "6.0.0",
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Newtonsoft.Json": "13.0.3",
|
"Newtonsoft.Json": "13.0.3",
|
||||||
"SixLabors.ImageSharp": "3.1.6",
|
"SixLabors.ImageSharp": "3.1.6",
|
||||||
"Wabbajack.Downloaders.Dispatcher": "0.3.14",
|
"Wabbajack.Downloaders.Dispatcher": "0.3.15",
|
||||||
"Wabbajack.Installer": "0.3.14",
|
"Wabbajack.Installer": "0.3.15",
|
||||||
"Wabbajack.VFS": "0.3.14",
|
"Wabbajack.VFS": "0.3.15",
|
||||||
"ini-parser-netstandard": "2.5.2"
|
"ini-parser-netstandard": "2.5.2"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Compiler.dll": {}
|
"Wabbajack.Compiler.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Compression.BSA/0.3.14": {
|
"Wabbajack.Compression.BSA/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"K4os.Compression.LZ4.Streams": "1.3.8",
|
"K4os.Compression.LZ4.Streams": "1.3.8",
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"SharpZipLib": "1.4.2",
|
"SharpZipLib": "1.4.2",
|
||||||
"Wabbajack.Common": "0.3.14",
|
"Wabbajack.Common": "0.3.15",
|
||||||
"Wabbajack.DTOs": "0.3.14"
|
"Wabbajack.DTOs": "0.3.15"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Compression.BSA.dll": {}
|
"Wabbajack.Compression.BSA.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Compression.Zip/0.3.14": {
|
"Wabbajack.Compression.Zip/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Wabbajack.IO.Async": "0.3.14"
|
"Wabbajack.IO.Async": "0.3.15"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Compression.Zip.dll": {}
|
"Wabbajack.Compression.Zip.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Configuration/0.3.14": {
|
"Wabbajack.Configuration/0.3.15": {
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Configuration.dll": {}
|
"Wabbajack.Configuration.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.Bethesda/0.3.14": {
|
"Wabbajack.Downloaders.Bethesda/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"LibAES-CTR": "1.1.0",
|
"LibAES-CTR": "1.1.0",
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"SharpZipLib": "1.4.2",
|
"SharpZipLib": "1.4.2",
|
||||||
"Wabbajack.Common": "0.3.14",
|
"Wabbajack.Common": "0.3.15",
|
||||||
"Wabbajack.Downloaders.Interfaces": "0.3.14",
|
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||||
"Wabbajack.Networking.BethesdaNet": "0.3.14"
|
"Wabbajack.Networking.BethesdaNet": "0.3.15"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Downloaders.Bethesda.dll": {}
|
"Wabbajack.Downloaders.Bethesda.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.Dispatcher/0.3.14": {
|
"Wabbajack.Downloaders.Dispatcher/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"Newtonsoft.Json": "13.0.3",
|
"Newtonsoft.Json": "13.0.3",
|
||||||
"SixLabors.ImageSharp": "3.1.6",
|
"SixLabors.ImageSharp": "3.1.6",
|
||||||
"Wabbajack.Downloaders.Bethesda": "0.3.14",
|
"Wabbajack.Downloaders.Bethesda": "0.3.15",
|
||||||
"Wabbajack.Downloaders.GameFile": "0.3.14",
|
"Wabbajack.Downloaders.GameFile": "0.3.15",
|
||||||
"Wabbajack.Downloaders.GoogleDrive": "0.3.14",
|
"Wabbajack.Downloaders.GoogleDrive": "0.3.15",
|
||||||
"Wabbajack.Downloaders.Http": "0.3.14",
|
"Wabbajack.Downloaders.Http": "0.3.15",
|
||||||
"Wabbajack.Downloaders.IPS4OAuth2Downloader": "0.3.14",
|
"Wabbajack.Downloaders.IPS4OAuth2Downloader": "0.3.15",
|
||||||
"Wabbajack.Downloaders.Interfaces": "0.3.14",
|
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||||
"Wabbajack.Downloaders.Manual": "0.3.14",
|
"Wabbajack.Downloaders.Manual": "0.3.15",
|
||||||
"Wabbajack.Downloaders.MediaFire": "0.3.14",
|
"Wabbajack.Downloaders.MediaFire": "0.3.15",
|
||||||
"Wabbajack.Downloaders.Mega": "0.3.14",
|
"Wabbajack.Downloaders.Mega": "0.3.15",
|
||||||
"Wabbajack.Downloaders.ModDB": "0.3.14",
|
"Wabbajack.Downloaders.ModDB": "0.3.15",
|
||||||
"Wabbajack.Downloaders.Nexus": "0.3.14",
|
"Wabbajack.Downloaders.Nexus": "0.3.15",
|
||||||
"Wabbajack.Downloaders.VerificationCache": "0.3.14",
|
"Wabbajack.Downloaders.VerificationCache": "0.3.15",
|
||||||
"Wabbajack.Downloaders.WabbajackCDN": "0.3.14",
|
"Wabbajack.Downloaders.WabbajackCDN": "0.3.15",
|
||||||
"Wabbajack.Networking.WabbajackClientApi": "0.3.14"
|
"Wabbajack.Networking.WabbajackClientApi": "0.3.15"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Downloaders.Dispatcher.dll": {}
|
"Wabbajack.Downloaders.Dispatcher.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.GameFile/0.3.14": {
|
"Wabbajack.Downloaders.GameFile/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"GameFinder.StoreHandlers.EADesktop": "4.5.0",
|
"GameFinder.StoreHandlers.EADesktop": "4.5.0",
|
||||||
"GameFinder.StoreHandlers.EGS": "4.5.0",
|
"GameFinder.StoreHandlers.EGS": "4.5.0",
|
||||||
@@ -1903,360 +1903,360 @@
|
|||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"SixLabors.ImageSharp": "3.1.6",
|
"SixLabors.ImageSharp": "3.1.6",
|
||||||
"Wabbajack.Downloaders.Interfaces": "0.3.14",
|
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||||
"Wabbajack.VFS": "0.3.14"
|
"Wabbajack.VFS": "0.3.15"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Downloaders.GameFile.dll": {}
|
"Wabbajack.Downloaders.GameFile.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.GoogleDrive/0.3.14": {
|
"Wabbajack.Downloaders.GoogleDrive/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"HtmlAgilityPack": "1.11.72",
|
"HtmlAgilityPack": "1.11.72",
|
||||||
"Microsoft.AspNetCore.Http.Extensions": "2.3.0",
|
"Microsoft.AspNetCore.Http.Extensions": "2.3.0",
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"Wabbajack.Common": "0.3.14",
|
"Wabbajack.Common": "0.3.15",
|
||||||
"Wabbajack.DTOs": "0.3.14",
|
"Wabbajack.DTOs": "0.3.15",
|
||||||
"Wabbajack.Downloaders.Interfaces": "0.3.14",
|
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||||
"Wabbajack.Networking.Http": "0.3.14",
|
"Wabbajack.Networking.Http": "0.3.15",
|
||||||
"Wabbajack.Networking.Http.Interfaces": "0.3.14"
|
"Wabbajack.Networking.Http.Interfaces": "0.3.15"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Downloaders.GoogleDrive.dll": {}
|
"Wabbajack.Downloaders.GoogleDrive.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.Http/0.3.14": {
|
"Wabbajack.Downloaders.Http/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"Wabbajack.Common": "0.3.14",
|
"Wabbajack.Common": "0.3.15",
|
||||||
"Wabbajack.DTOs": "0.3.14",
|
"Wabbajack.DTOs": "0.3.15",
|
||||||
"Wabbajack.Downloaders.Interfaces": "0.3.14",
|
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||||
"Wabbajack.Networking.BethesdaNet": "0.3.14",
|
"Wabbajack.Networking.BethesdaNet": "0.3.15",
|
||||||
"Wabbajack.Networking.Http.Interfaces": "0.3.14",
|
"Wabbajack.Networking.Http.Interfaces": "0.3.15",
|
||||||
"Wabbajack.Paths.IO": "0.3.14"
|
"Wabbajack.Paths.IO": "0.3.15"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Downloaders.Http.dll": {}
|
"Wabbajack.Downloaders.Http.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.Interfaces/0.3.14": {
|
"Wabbajack.Downloaders.Interfaces/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Wabbajack.Compression.Zip": "0.3.14",
|
"Wabbajack.Compression.Zip": "0.3.15",
|
||||||
"Wabbajack.DTOs": "0.3.14",
|
"Wabbajack.DTOs": "0.3.15",
|
||||||
"Wabbajack.Paths.IO": "0.3.14"
|
"Wabbajack.Paths.IO": "0.3.15"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Downloaders.Interfaces.dll": {}
|
"Wabbajack.Downloaders.Interfaces.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.14": {
|
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"F23.StringSimilarity": "6.0.0",
|
"F23.StringSimilarity": "6.0.0",
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"Wabbajack.Common": "0.3.14",
|
"Wabbajack.Common": "0.3.15",
|
||||||
"Wabbajack.Downloaders.Interfaces": "0.3.14",
|
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||||
"Wabbajack.Networking.Http": "0.3.14",
|
"Wabbajack.Networking.Http": "0.3.15",
|
||||||
"Wabbajack.Networking.Http.Interfaces": "0.3.14"
|
"Wabbajack.Networking.Http.Interfaces": "0.3.15"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Downloaders.IPS4OAuth2Downloader.dll": {}
|
"Wabbajack.Downloaders.IPS4OAuth2Downloader.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.Manual/0.3.14": {
|
"Wabbajack.Downloaders.Manual/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"Wabbajack.Common": "0.3.14",
|
"Wabbajack.Common": "0.3.15",
|
||||||
"Wabbajack.Downloaders.Interfaces": "0.3.14"
|
"Wabbajack.Downloaders.Interfaces": "0.3.15"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Downloaders.Manual.dll": {}
|
"Wabbajack.Downloaders.Manual.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.MediaFire/0.3.14": {
|
"Wabbajack.Downloaders.MediaFire/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"HtmlAgilityPack": "1.11.72",
|
"HtmlAgilityPack": "1.11.72",
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"Wabbajack.Common": "0.3.14",
|
"Wabbajack.Common": "0.3.15",
|
||||||
"Wabbajack.Downloaders.Interfaces": "0.3.14",
|
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||||
"Wabbajack.Networking.Http.Interfaces": "0.3.14"
|
"Wabbajack.Networking.Http.Interfaces": "0.3.15"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Downloaders.MediaFire.dll": {}
|
"Wabbajack.Downloaders.MediaFire.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.Mega/0.3.14": {
|
"Wabbajack.Downloaders.Mega/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"Newtonsoft.Json": "13.0.3",
|
"Newtonsoft.Json": "13.0.3",
|
||||||
"Wabbajack.Common": "0.3.14",
|
"Wabbajack.Common": "0.3.15",
|
||||||
"Wabbajack.Downloaders.Interfaces": "0.3.14",
|
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||||
"Wabbajack.Paths.IO": "0.3.14"
|
"Wabbajack.Paths.IO": "0.3.15"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Downloaders.Mega.dll": {}
|
"Wabbajack.Downloaders.Mega.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.ModDB/0.3.14": {
|
"Wabbajack.Downloaders.ModDB/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"HtmlAgilityPack": "1.11.72",
|
"HtmlAgilityPack": "1.11.72",
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"Newtonsoft.Json": "13.0.3",
|
"Newtonsoft.Json": "13.0.3",
|
||||||
"Wabbajack.Common": "0.3.14",
|
"Wabbajack.Common": "0.3.15",
|
||||||
"Wabbajack.Downloaders.Interfaces": "0.3.14",
|
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||||
"Wabbajack.Networking.Http": "0.3.14",
|
"Wabbajack.Networking.Http": "0.3.15",
|
||||||
"Wabbajack.Networking.Http.Interfaces": "0.3.14"
|
"Wabbajack.Networking.Http.Interfaces": "0.3.15"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Downloaders.ModDB.dll": {}
|
"Wabbajack.Downloaders.ModDB.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.Nexus/0.3.14": {
|
"Wabbajack.Downloaders.Nexus/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Wabbajack.DTOs": "0.3.14",
|
"Wabbajack.DTOs": "0.3.15",
|
||||||
"Wabbajack.Downloaders.Interfaces": "0.3.14",
|
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||||
"Wabbajack.Hashing.xxHash64": "0.3.14",
|
"Wabbajack.Hashing.xxHash64": "0.3.15",
|
||||||
"Wabbajack.Networking.Http": "0.3.14",
|
"Wabbajack.Networking.Http": "0.3.15",
|
||||||
"Wabbajack.Networking.Http.Interfaces": "0.3.14",
|
"Wabbajack.Networking.Http.Interfaces": "0.3.15",
|
||||||
"Wabbajack.Networking.NexusApi": "0.3.14",
|
"Wabbajack.Networking.NexusApi": "0.3.15",
|
||||||
"Wabbajack.Paths": "0.3.14"
|
"Wabbajack.Paths": "0.3.15"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Downloaders.Nexus.dll": {}
|
"Wabbajack.Downloaders.Nexus.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.VerificationCache/0.3.14": {
|
"Wabbajack.Downloaders.VerificationCache/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"Stub.System.Data.SQLite.Core.NetStandard": "1.0.119",
|
"Stub.System.Data.SQLite.Core.NetStandard": "1.0.119",
|
||||||
"Wabbajack.DTOs": "0.3.14",
|
"Wabbajack.DTOs": "0.3.15",
|
||||||
"Wabbajack.Paths.IO": "0.3.14"
|
"Wabbajack.Paths.IO": "0.3.15"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Downloaders.VerificationCache.dll": {}
|
"Wabbajack.Downloaders.VerificationCache.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.WabbajackCDN/0.3.14": {
|
"Wabbajack.Downloaders.WabbajackCDN/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"Microsoft.Toolkit.HighPerformance": "7.1.2",
|
"Microsoft.Toolkit.HighPerformance": "7.1.2",
|
||||||
"Wabbajack.Common": "0.3.14",
|
"Wabbajack.Common": "0.3.15",
|
||||||
"Wabbajack.Downloaders.Interfaces": "0.3.14",
|
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||||
"Wabbajack.Networking.Http": "0.3.14",
|
"Wabbajack.Networking.Http": "0.3.15",
|
||||||
"Wabbajack.RateLimiter": "0.3.14"
|
"Wabbajack.RateLimiter": "0.3.15"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Downloaders.WabbajackCDN.dll": {}
|
"Wabbajack.Downloaders.WabbajackCDN.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.DTOs/0.3.14": {
|
"Wabbajack.DTOs/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Wabbajack.Hashing.xxHash64": "0.3.14",
|
"Wabbajack.Hashing.xxHash64": "0.3.15",
|
||||||
"Wabbajack.Paths": "0.3.14"
|
"Wabbajack.Paths": "0.3.15"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.DTOs.dll": {}
|
"Wabbajack.DTOs.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.FileExtractor/0.3.14": {
|
"Wabbajack.FileExtractor/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"OMODFramework": "3.0.1",
|
"OMODFramework": "3.0.1",
|
||||||
"Wabbajack.Common": "0.3.14",
|
"Wabbajack.Common": "0.3.15",
|
||||||
"Wabbajack.Compression.BSA": "0.3.14",
|
"Wabbajack.Compression.BSA": "0.3.15",
|
||||||
"Wabbajack.Hashing.PHash": "0.3.14",
|
"Wabbajack.Hashing.PHash": "0.3.15",
|
||||||
"Wabbajack.Paths": "0.3.14"
|
"Wabbajack.Paths": "0.3.15"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.FileExtractor.dll": {}
|
"Wabbajack.FileExtractor.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Hashing.PHash/0.3.14": {
|
"Wabbajack.Hashing.PHash/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"BCnEncoder.Net.ImageSharp": "1.1.1",
|
"BCnEncoder.Net.ImageSharp": "1.1.1",
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Shipwreck.Phash": "0.5.0",
|
"Shipwreck.Phash": "0.5.0",
|
||||||
"SixLabors.ImageSharp": "3.1.6",
|
"SixLabors.ImageSharp": "3.1.6",
|
||||||
"Wabbajack.Common": "0.3.14",
|
"Wabbajack.Common": "0.3.15",
|
||||||
"Wabbajack.DTOs": "0.3.14",
|
"Wabbajack.DTOs": "0.3.15",
|
||||||
"Wabbajack.Paths": "0.3.14",
|
"Wabbajack.Paths": "0.3.15",
|
||||||
"Wabbajack.Paths.IO": "0.3.14"
|
"Wabbajack.Paths.IO": "0.3.15"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Hashing.PHash.dll": {}
|
"Wabbajack.Hashing.PHash.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Hashing.xxHash64/0.3.14": {
|
"Wabbajack.Hashing.xxHash64/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Wabbajack.Paths": "0.3.14",
|
"Wabbajack.Paths": "0.3.15",
|
||||||
"Wabbajack.RateLimiter": "0.3.14"
|
"Wabbajack.RateLimiter": "0.3.15"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Hashing.xxHash64.dll": {}
|
"Wabbajack.Hashing.xxHash64.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Installer/0.3.14": {
|
"Wabbajack.Installer/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Newtonsoft.Json": "13.0.3",
|
"Newtonsoft.Json": "13.0.3",
|
||||||
"Octopus.Octodiff": "2.0.548",
|
"Octopus.Octodiff": "2.0.548",
|
||||||
"SixLabors.ImageSharp": "3.1.6",
|
"SixLabors.ImageSharp": "3.1.6",
|
||||||
"Wabbajack.DTOs": "0.3.14",
|
"Wabbajack.DTOs": "0.3.15",
|
||||||
"Wabbajack.Downloaders.Dispatcher": "0.3.14",
|
"Wabbajack.Downloaders.Dispatcher": "0.3.15",
|
||||||
"Wabbajack.Downloaders.GameFile": "0.3.14",
|
"Wabbajack.Downloaders.GameFile": "0.3.15",
|
||||||
"Wabbajack.FileExtractor": "0.3.14",
|
"Wabbajack.FileExtractor": "0.3.15",
|
||||||
"Wabbajack.Networking.WabbajackClientApi": "0.3.14",
|
"Wabbajack.Networking.WabbajackClientApi": "0.3.15",
|
||||||
"Wabbajack.Paths": "0.3.14",
|
"Wabbajack.Paths": "0.3.15",
|
||||||
"Wabbajack.Paths.IO": "0.3.14",
|
"Wabbajack.Paths.IO": "0.3.15",
|
||||||
"Wabbajack.VFS": "0.3.14",
|
"Wabbajack.VFS": "0.3.15",
|
||||||
"ini-parser-netstandard": "2.5.2"
|
"ini-parser-netstandard": "2.5.2"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Installer.dll": {}
|
"Wabbajack.Installer.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.IO.Async/0.3.14": {
|
"Wabbajack.IO.Async/0.3.15": {
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.IO.Async.dll": {}
|
"Wabbajack.IO.Async.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Networking.BethesdaNet/0.3.14": {
|
"Wabbajack.Networking.BethesdaNet/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Wabbajack.DTOs": "0.3.14",
|
"Wabbajack.DTOs": "0.3.15",
|
||||||
"Wabbajack.Networking.Http": "0.3.14",
|
"Wabbajack.Networking.Http": "0.3.15",
|
||||||
"Wabbajack.Networking.Http.Interfaces": "0.3.14"
|
"Wabbajack.Networking.Http.Interfaces": "0.3.15"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Networking.BethesdaNet.dll": {}
|
"Wabbajack.Networking.BethesdaNet.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Networking.Discord/0.3.14": {
|
"Wabbajack.Networking.Discord/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"Wabbajack.Networking.Http.Interfaces": "0.3.14"
|
"Wabbajack.Networking.Http.Interfaces": "0.3.15"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Networking.Discord.dll": {}
|
"Wabbajack.Networking.Discord.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Networking.GitHub/0.3.14": {
|
"Wabbajack.Networking.GitHub/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"Octokit": "14.0.0",
|
"Octokit": "14.0.0",
|
||||||
"Wabbajack.DTOs": "0.3.14",
|
"Wabbajack.DTOs": "0.3.15",
|
||||||
"Wabbajack.Networking.Http.Interfaces": "0.3.14"
|
"Wabbajack.Networking.Http.Interfaces": "0.3.15"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Networking.GitHub.dll": {}
|
"Wabbajack.Networking.GitHub.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Networking.Http/0.3.14": {
|
"Wabbajack.Networking.Http/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Http": "9.0.1",
|
"Microsoft.Extensions.Http": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging": "9.0.1",
|
"Microsoft.Extensions.Logging": "9.0.1",
|
||||||
"Wabbajack.Configuration": "0.3.14",
|
"Wabbajack.Configuration": "0.3.15",
|
||||||
"Wabbajack.Downloaders.Interfaces": "0.3.14",
|
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||||
"Wabbajack.Hashing.xxHash64": "0.3.14",
|
"Wabbajack.Hashing.xxHash64": "0.3.15",
|
||||||
"Wabbajack.Networking.Http.Interfaces": "0.3.14",
|
"Wabbajack.Networking.Http.Interfaces": "0.3.15",
|
||||||
"Wabbajack.Paths": "0.3.14",
|
"Wabbajack.Paths": "0.3.15",
|
||||||
"Wabbajack.Paths.IO": "0.3.14"
|
"Wabbajack.Paths.IO": "0.3.15"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Networking.Http.dll": {}
|
"Wabbajack.Networking.Http.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Networking.Http.Interfaces/0.3.14": {
|
"Wabbajack.Networking.Http.Interfaces/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Wabbajack.Hashing.xxHash64": "0.3.14"
|
"Wabbajack.Hashing.xxHash64": "0.3.15"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Networking.Http.Interfaces.dll": {}
|
"Wabbajack.Networking.Http.Interfaces.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Networking.NexusApi/0.3.14": {
|
"Wabbajack.Networking.NexusApi/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"Wabbajack.DTOs": "0.3.14",
|
"Wabbajack.DTOs": "0.3.15",
|
||||||
"Wabbajack.Networking.Http": "0.3.14",
|
"Wabbajack.Networking.Http": "0.3.15",
|
||||||
"Wabbajack.Networking.Http.Interfaces": "0.3.14",
|
"Wabbajack.Networking.Http.Interfaces": "0.3.15",
|
||||||
"Wabbajack.Networking.WabbajackClientApi": "0.3.14"
|
"Wabbajack.Networking.WabbajackClientApi": "0.3.15"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Networking.NexusApi.dll": {}
|
"Wabbajack.Networking.NexusApi.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Networking.WabbajackClientApi/0.3.14": {
|
"Wabbajack.Networking.WabbajackClientApi/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"Octokit": "14.0.0",
|
"Octokit": "14.0.0",
|
||||||
"Wabbajack.Common": "0.3.14",
|
"Wabbajack.Common": "0.3.15",
|
||||||
"Wabbajack.DTOs": "0.3.14",
|
"Wabbajack.DTOs": "0.3.15",
|
||||||
"Wabbajack.Paths.IO": "0.3.14",
|
"Wabbajack.Paths.IO": "0.3.15",
|
||||||
"Wabbajack.VFS.Interfaces": "0.3.14",
|
"Wabbajack.VFS.Interfaces": "0.3.15",
|
||||||
"YamlDotNet": "16.3.0"
|
"YamlDotNet": "16.3.0"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Networking.WabbajackClientApi.dll": {}
|
"Wabbajack.Networking.WabbajackClientApi.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Paths/0.3.14": {
|
"Wabbajack.Paths/0.3.15": {
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Paths.dll": {}
|
"Wabbajack.Paths.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Paths.IO/0.3.14": {
|
"Wabbajack.Paths.IO/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Wabbajack.Paths": "0.3.14",
|
"Wabbajack.Paths": "0.3.15",
|
||||||
"shortid": "4.0.0"
|
"shortid": "4.0.0"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Paths.IO.dll": {}
|
"Wabbajack.Paths.IO.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.RateLimiter/0.3.14": {
|
"Wabbajack.RateLimiter/0.3.15": {
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.RateLimiter.dll": {}
|
"Wabbajack.RateLimiter.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Server.Lib/0.3.14": {
|
"Wabbajack.Server.Lib/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"FluentFTP": "52.0.0",
|
"FluentFTP": "52.0.0",
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
@@ -2264,58 +2264,58 @@
|
|||||||
"Nettle": "3.0.0",
|
"Nettle": "3.0.0",
|
||||||
"Newtonsoft.Json": "13.0.3",
|
"Newtonsoft.Json": "13.0.3",
|
||||||
"SixLabors.ImageSharp": "3.1.6",
|
"SixLabors.ImageSharp": "3.1.6",
|
||||||
"Wabbajack.Common": "0.3.14",
|
"Wabbajack.Common": "0.3.15",
|
||||||
"Wabbajack.Networking.Http.Interfaces": "0.3.14",
|
"Wabbajack.Networking.Http.Interfaces": "0.3.15",
|
||||||
"Wabbajack.Services.OSIntegrated": "0.3.14"
|
"Wabbajack.Services.OSIntegrated": "0.3.15"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Server.Lib.dll": {}
|
"Wabbajack.Server.Lib.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Services.OSIntegrated/0.3.14": {
|
"Wabbajack.Services.OSIntegrated/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"DeviceId": "6.8.0",
|
"DeviceId": "6.8.0",
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Newtonsoft.Json": "13.0.3",
|
"Newtonsoft.Json": "13.0.3",
|
||||||
"SixLabors.ImageSharp": "3.1.6",
|
"SixLabors.ImageSharp": "3.1.6",
|
||||||
"Wabbajack.Compiler": "0.3.14",
|
"Wabbajack.Compiler": "0.3.15",
|
||||||
"Wabbajack.Downloaders.Dispatcher": "0.3.14",
|
"Wabbajack.Downloaders.Dispatcher": "0.3.15",
|
||||||
"Wabbajack.Installer": "0.3.14",
|
"Wabbajack.Installer": "0.3.15",
|
||||||
"Wabbajack.Networking.BethesdaNet": "0.3.14",
|
"Wabbajack.Networking.BethesdaNet": "0.3.15",
|
||||||
"Wabbajack.Networking.Discord": "0.3.14",
|
"Wabbajack.Networking.Discord": "0.3.15",
|
||||||
"Wabbajack.VFS": "0.3.14"
|
"Wabbajack.VFS": "0.3.15"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Services.OSIntegrated.dll": {}
|
"Wabbajack.Services.OSIntegrated.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.VFS/0.3.14": {
|
"Wabbajack.VFS/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"SixLabors.ImageSharp": "3.1.6",
|
"SixLabors.ImageSharp": "3.1.6",
|
||||||
"System.Data.SQLite.Core": "1.0.119",
|
"System.Data.SQLite.Core": "1.0.119",
|
||||||
"Wabbajack.Common": "0.3.14",
|
"Wabbajack.Common": "0.3.15",
|
||||||
"Wabbajack.FileExtractor": "0.3.14",
|
"Wabbajack.FileExtractor": "0.3.15",
|
||||||
"Wabbajack.Hashing.PHash": "0.3.14",
|
"Wabbajack.Hashing.PHash": "0.3.15",
|
||||||
"Wabbajack.Hashing.xxHash64": "0.3.14",
|
"Wabbajack.Hashing.xxHash64": "0.3.15",
|
||||||
"Wabbajack.Paths": "0.3.14",
|
"Wabbajack.Paths": "0.3.15",
|
||||||
"Wabbajack.Paths.IO": "0.3.14",
|
"Wabbajack.Paths.IO": "0.3.15",
|
||||||
"Wabbajack.VFS.Interfaces": "0.3.14"
|
"Wabbajack.VFS.Interfaces": "0.3.15"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.VFS.dll": {}
|
"Wabbajack.VFS.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.VFS.Interfaces/0.3.14": {
|
"Wabbajack.VFS.Interfaces/0.3.15": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Wabbajack.DTOs": "0.3.14",
|
"Wabbajack.DTOs": "0.3.15",
|
||||||
"Wabbajack.Hashing.xxHash64": "0.3.14",
|
"Wabbajack.Hashing.xxHash64": "0.3.15",
|
||||||
"Wabbajack.Paths": "0.3.14"
|
"Wabbajack.Paths": "0.3.15"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.VFS.Interfaces.dll": {}
|
"Wabbajack.VFS.Interfaces.dll": {}
|
||||||
@@ -2332,7 +2332,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"libraries": {
|
"libraries": {
|
||||||
"jackify-engine/0.3.14": {
|
"jackify-engine/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
@@ -3021,202 +3021,202 @@
|
|||||||
"path": "yamldotnet/16.3.0",
|
"path": "yamldotnet/16.3.0",
|
||||||
"hashPath": "yamldotnet.16.3.0.nupkg.sha512"
|
"hashPath": "yamldotnet.16.3.0.nupkg.sha512"
|
||||||
},
|
},
|
||||||
"Wabbajack.CLI.Builder/0.3.14": {
|
"Wabbajack.CLI.Builder/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Common/0.3.14": {
|
"Wabbajack.Common/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Compiler/0.3.14": {
|
"Wabbajack.Compiler/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Compression.BSA/0.3.14": {
|
"Wabbajack.Compression.BSA/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Compression.Zip/0.3.14": {
|
"Wabbajack.Compression.Zip/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Configuration/0.3.14": {
|
"Wabbajack.Configuration/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.Bethesda/0.3.14": {
|
"Wabbajack.Downloaders.Bethesda/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.Dispatcher/0.3.14": {
|
"Wabbajack.Downloaders.Dispatcher/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.GameFile/0.3.14": {
|
"Wabbajack.Downloaders.GameFile/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.GoogleDrive/0.3.14": {
|
"Wabbajack.Downloaders.GoogleDrive/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.Http/0.3.14": {
|
"Wabbajack.Downloaders.Http/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.Interfaces/0.3.14": {
|
"Wabbajack.Downloaders.Interfaces/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.14": {
|
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.Manual/0.3.14": {
|
"Wabbajack.Downloaders.Manual/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.MediaFire/0.3.14": {
|
"Wabbajack.Downloaders.MediaFire/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.Mega/0.3.14": {
|
"Wabbajack.Downloaders.Mega/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.ModDB/0.3.14": {
|
"Wabbajack.Downloaders.ModDB/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.Nexus/0.3.14": {
|
"Wabbajack.Downloaders.Nexus/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.VerificationCache/0.3.14": {
|
"Wabbajack.Downloaders.VerificationCache/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.WabbajackCDN/0.3.14": {
|
"Wabbajack.Downloaders.WabbajackCDN/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.DTOs/0.3.14": {
|
"Wabbajack.DTOs/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.FileExtractor/0.3.14": {
|
"Wabbajack.FileExtractor/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Hashing.PHash/0.3.14": {
|
"Wabbajack.Hashing.PHash/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Hashing.xxHash64/0.3.14": {
|
"Wabbajack.Hashing.xxHash64/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Installer/0.3.14": {
|
"Wabbajack.Installer/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.IO.Async/0.3.14": {
|
"Wabbajack.IO.Async/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Networking.BethesdaNet/0.3.14": {
|
"Wabbajack.Networking.BethesdaNet/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Networking.Discord/0.3.14": {
|
"Wabbajack.Networking.Discord/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Networking.GitHub/0.3.14": {
|
"Wabbajack.Networking.GitHub/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Networking.Http/0.3.14": {
|
"Wabbajack.Networking.Http/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Networking.Http.Interfaces/0.3.14": {
|
"Wabbajack.Networking.Http.Interfaces/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Networking.NexusApi/0.3.14": {
|
"Wabbajack.Networking.NexusApi/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Networking.WabbajackClientApi/0.3.14": {
|
"Wabbajack.Networking.WabbajackClientApi/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Paths/0.3.14": {
|
"Wabbajack.Paths/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Paths.IO/0.3.14": {
|
"Wabbajack.Paths.IO/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.RateLimiter/0.3.14": {
|
"Wabbajack.RateLimiter/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Server.Lib/0.3.14": {
|
"Wabbajack.Server.Lib/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Services.OSIntegrated/0.3.14": {
|
"Wabbajack.Services.OSIntegrated/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.VFS/0.3.14": {
|
"Wabbajack.VFS/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.VFS.Interfaces/0.3.14": {
|
"Wabbajack.VFS.Interfaces/0.3.15": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"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
|
# Import our command handlers
|
||||||
from .commands.configure_modlist import ConfigureModlistCommand
|
from .commands.configure_modlist import ConfigureModlistCommand
|
||||||
from .commands.install_modlist import InstallModlistCommand
|
from .commands.install_modlist import InstallModlistCommand
|
||||||
from .commands.tuxborn import TuxbornCommand
|
|
||||||
|
|
||||||
# Import our menu handlers
|
# Import our menu handlers
|
||||||
from .menus.main_menu import MainMenuHandler
|
from .menus.main_menu import MainMenuHandler
|
||||||
from .menus.tuxborn_menu import TuxbornMenuHandler
|
|
||||||
from .menus.wabbajack_menu import WabbajackMenuHandler
|
from .menus.wabbajack_menu import WabbajackMenuHandler
|
||||||
from .menus.hoolamike_menu import HoolamikeMenuHandler
|
from .menus.hoolamike_menu import HoolamikeMenuHandler
|
||||||
from .menus.additional_menu import AdditionalMenuHandler
|
from .menus.additional_menu import AdditionalMenuHandler
|
||||||
@@ -280,7 +278,6 @@ class JackifyCLI:
|
|||||||
commands = {
|
commands = {
|
||||||
'configure_modlist': ConfigureModlistCommand(self.backend_services),
|
'configure_modlist': ConfigureModlistCommand(self.backend_services),
|
||||||
'install_modlist': InstallModlistCommand(self.backend_services, self.system_info),
|
'install_modlist': InstallModlistCommand(self.backend_services, self.system_info),
|
||||||
'tuxborn': TuxbornCommand(self.backend_services, self.system_info)
|
|
||||||
}
|
}
|
||||||
return commands
|
return commands
|
||||||
|
|
||||||
@@ -292,7 +289,6 @@ class JackifyCLI:
|
|||||||
"""
|
"""
|
||||||
menus = {
|
menus = {
|
||||||
'main': MainMenuHandler(dev_mode=getattr(self, 'dev_mode', False)),
|
'main': MainMenuHandler(dev_mode=getattr(self, 'dev_mode', False)),
|
||||||
'tuxborn': TuxbornMenuHandler(),
|
|
||||||
'wabbajack': WabbajackMenuHandler(),
|
'wabbajack': WabbajackMenuHandler(),
|
||||||
'hoolamike': HoolamikeMenuHandler(),
|
'hoolamike': HoolamikeMenuHandler(),
|
||||||
'additional': AdditionalMenuHandler()
|
'additional': AdditionalMenuHandler()
|
||||||
@@ -371,10 +367,6 @@ class JackifyCLI:
|
|||||||
self._debug_print('Entering restart_steam workflow')
|
self._debug_print('Entering restart_steam workflow')
|
||||||
return self._handle_restart_steam()
|
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
|
# Handle install-modlist top-level functionality
|
||||||
if getattr(self.args, 'install_modlist', False):
|
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')
|
parser.add_argument('--update', action='store_true', help='Check for and install updates')
|
||||||
|
|
||||||
# Add command-specific arguments
|
# Add command-specific arguments
|
||||||
self.commands['tuxborn'].add_args(parser)
|
|
||||||
self.commands['install_modlist'].add_top_level_args(parser)
|
self.commands['install_modlist'].add_top_level_args(parser)
|
||||||
|
|
||||||
# Add subcommands
|
# Add subcommands
|
||||||
@@ -459,8 +450,6 @@ class JackifyCLI:
|
|||||||
return 0
|
return 0
|
||||||
elif choice == "wabbajack":
|
elif choice == "wabbajack":
|
||||||
self.menus['wabbajack'].show_wabbajack_tasks_menu(self)
|
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
|
# HIDDEN FOR FIRST RELEASE - UNCOMMENT WHEN READY
|
||||||
# elif choice == "hoolamike":
|
# elif choice == "hoolamike":
|
||||||
# self.menus['hoolamike'].show_hoolamike_menu(self)
|
# self.menus['hoolamike'].show_hoolamike_menu(self)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ Extracted from the legacy monolithic CLI system
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from .main_menu import MainMenuHandler
|
from .main_menu import MainMenuHandler
|
||||||
from .tuxborn_menu import TuxbornMenuHandler
|
|
||||||
from .wabbajack_menu import WabbajackMenuHandler
|
from .wabbajack_menu import WabbajackMenuHandler
|
||||||
from .hoolamike_menu import HoolamikeMenuHandler
|
from .hoolamike_menu import HoolamikeMenuHandler
|
||||||
from .additional_menu import AdditionalMenuHandler
|
from .additional_menu import AdditionalMenuHandler
|
||||||
@@ -12,7 +11,6 @@ from .recovery_menu import RecoveryMenuHandler
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'MainMenuHandler',
|
'MainMenuHandler',
|
||||||
'TuxbornMenuHandler',
|
|
||||||
'WabbajackMenuHandler',
|
'WabbajackMenuHandler',
|
||||||
'HoolamikeMenuHandler',
|
'HoolamikeMenuHandler',
|
||||||
'AdditionalMenuHandler',
|
'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)
|
|
||||||
@@ -7,6 +7,7 @@ This replaces the legacy jackify_gui implementation with a refactored architectu
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Suppress xkbcommon locale errors (harmless but annoying)
|
# Suppress xkbcommon locale errors (harmless but annoying)
|
||||||
@@ -81,6 +82,9 @@ if '--env-diagnostic' in sys.argv:
|
|||||||
|
|
||||||
from jackify import __version__ as jackify_version
|
from jackify import __version__ as jackify_version
|
||||||
|
|
||||||
|
# Initialize logger
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
if '--help' in sys.argv or '-h' in sys.argv:
|
if '--help' in sys.argv or '-h' in sys.argv:
|
||||||
print("""Jackify - Native Linux Modlist Manager\n\nUsage:\n jackify [--cli] [--debug] [--version] [--help]\n\nOptions:\n --cli Launch CLI frontend\n --debug Enable debug logging\n --version Show version and exit\n --help, -h Show this help message and exit\n\nIf no options are given, the GUI will launch by default.\n""")
|
print("""Jackify - Native Linux Modlist Manager\n\nUsage:\n jackify [--cli] [--debug] [--version] [--help]\n\nOptions:\n --cli Launch CLI frontend\n --debug Enable debug logging\n --version Show version and exit\n --help, -h Show this help message and exit\n\nIf no options are given, the GUI will launch by default.\n""")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
@@ -98,7 +102,7 @@ sys.path.insert(0, str(src_dir))
|
|||||||
|
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QApplication, QMainWindow, QWidget, QLabel, QVBoxLayout, QPushButton,
|
QApplication, QMainWindow, QWidget, QLabel, QVBoxLayout, QPushButton,
|
||||||
QStackedWidget, QHBoxLayout, QDialog, QFormLayout, QLineEdit, QCheckBox, QSpinBox, QMessageBox, QGroupBox, QGridLayout, QFileDialog, QToolButton, QStyle
|
QStackedWidget, QHBoxLayout, QDialog, QFormLayout, QLineEdit, QCheckBox, QSpinBox, QMessageBox, QGroupBox, QGridLayout, QFileDialog, QToolButton, QStyle, QComboBox
|
||||||
)
|
)
|
||||||
from PySide6.QtCore import Qt, QEvent
|
from PySide6.QtCore import Qt, QEvent
|
||||||
from PySide6.QtGui import QIcon
|
from PySide6.QtGui import QIcon
|
||||||
@@ -127,7 +131,6 @@ DISCLAIMER_TEXT = (
|
|||||||
|
|
||||||
MENU_ITEMS = [
|
MENU_ITEMS = [
|
||||||
("Modlist Tasks", "modlist_tasks"),
|
("Modlist Tasks", "modlist_tasks"),
|
||||||
("Tuxborn Automatic Installer", "tuxborn_installer"),
|
|
||||||
("Hoolamike Tasks", "hoolamike_tasks"),
|
("Hoolamike Tasks", "hoolamike_tasks"),
|
||||||
("Additional Tasks", "additional_tasks"),
|
("Additional Tasks", "additional_tasks"),
|
||||||
("Exit Jackify", "exit_jackify"),
|
("Exit Jackify", "exit_jackify"),
|
||||||
@@ -158,6 +161,8 @@ class SettingsDialog(QDialog):
|
|||||||
try:
|
try:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
from jackify.backend.handlers.config_handler import ConfigHandler
|
from jackify.backend.handlers.config_handler import ConfigHandler
|
||||||
|
import logging
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
self.config_handler = ConfigHandler()
|
self.config_handler = ConfigHandler()
|
||||||
self._original_debug_mode = self.config_handler.get('debug_mode', False)
|
self._original_debug_mode = self.config_handler.get('debug_mode', False)
|
||||||
self.setWindowTitle("Settings")
|
self.setWindowTitle("Settings")
|
||||||
@@ -298,6 +303,33 @@ class SettingsDialog(QDialog):
|
|||||||
main_layout.addWidget(api_group)
|
main_layout.addWidget(api_group)
|
||||||
main_layout.addSpacing(12)
|
main_layout.addSpacing(12)
|
||||||
|
|
||||||
|
# --- Proton Version Section ---
|
||||||
|
proton_group = QGroupBox("Proton Version")
|
||||||
|
proton_group.setStyleSheet("QGroupBox { border: 1px solid #555; border-radius: 6px; margin-top: 8px; padding: 8px; background: #23282d; } QGroupBox:title { subcontrol-origin: margin; left: 10px; padding: 0 3px 0 3px; font-weight: bold; color: #fff; }")
|
||||||
|
proton_layout = QHBoxLayout()
|
||||||
|
proton_group.setLayout(proton_layout)
|
||||||
|
|
||||||
|
self.proton_dropdown = QComboBox()
|
||||||
|
self.proton_dropdown.setToolTip("Select Proton version for shortcut creation and texture processing")
|
||||||
|
self.proton_dropdown.setMinimumWidth(200)
|
||||||
|
|
||||||
|
# Populate Proton dropdown
|
||||||
|
self._populate_proton_dropdown()
|
||||||
|
|
||||||
|
# Refresh button for Proton detection
|
||||||
|
refresh_btn = QPushButton("↻")
|
||||||
|
refresh_btn.setFixedSize(30, 30)
|
||||||
|
refresh_btn.setToolTip("Refresh Proton version list")
|
||||||
|
refresh_btn.clicked.connect(self._refresh_proton_dropdown)
|
||||||
|
|
||||||
|
proton_layout.addWidget(QLabel("Proton Version:"))
|
||||||
|
proton_layout.addWidget(self.proton_dropdown)
|
||||||
|
proton_layout.addWidget(refresh_btn)
|
||||||
|
proton_layout.addStretch()
|
||||||
|
|
||||||
|
main_layout.addWidget(proton_group)
|
||||||
|
main_layout.addSpacing(12)
|
||||||
|
|
||||||
# --- Directories & Paths Section ---
|
# --- Directories & Paths Section ---
|
||||||
dir_group = QGroupBox("Directories & Paths")
|
dir_group = QGroupBox("Directories & Paths")
|
||||||
dir_group.setStyleSheet("QGroupBox { border: 1px solid #555; border-radius: 6px; margin-top: 8px; padding: 8px; background: #23282d; } QGroupBox:title { subcontrol-origin: margin; left: 10px; padding: 0 3px 0 3px; font-weight: bold; color: #fff; }")
|
dir_group.setStyleSheet("QGroupBox { border: 1px solid #555; border-radius: 6px; margin-top: 8px; padding: 8px; background: #23282d; } QGroupBox:title { subcontrol-origin: margin; left: 10px; padding: 0 3px 0 3px; font-weight: bold; color: #fff; }")
|
||||||
@@ -447,6 +479,85 @@ class SettingsDialog(QDialog):
|
|||||||
api_key = text.strip()
|
api_key = text.strip()
|
||||||
self.config_handler.save_api_key(api_key)
|
self.config_handler.save_api_key(api_key)
|
||||||
|
|
||||||
|
def _get_proton_10_path(self):
|
||||||
|
"""Get Proton 10 path if available, fallback to auto"""
|
||||||
|
try:
|
||||||
|
from jackify.backend.handlers.wine_utils import WineUtils
|
||||||
|
available_protons = WineUtils.scan_valve_proton_versions()
|
||||||
|
|
||||||
|
# Look for Proton 10.x
|
||||||
|
for proton in available_protons:
|
||||||
|
if proton['version'].startswith('10.'):
|
||||||
|
return proton['path']
|
||||||
|
|
||||||
|
# Fallback to auto if no Proton 10 found
|
||||||
|
return 'auto'
|
||||||
|
except:
|
||||||
|
return 'auto'
|
||||||
|
|
||||||
|
def _populate_proton_dropdown(self):
|
||||||
|
"""Populate Proton version dropdown with detected versions (includes GE-Proton and Valve Proton)"""
|
||||||
|
try:
|
||||||
|
from jackify.backend.handlers.wine_utils import WineUtils
|
||||||
|
|
||||||
|
# Get all available Proton versions (GE-Proton + Valve Proton)
|
||||||
|
available_protons = WineUtils.scan_all_proton_versions()
|
||||||
|
|
||||||
|
# Add "Auto" option first
|
||||||
|
self.proton_dropdown.addItem("Auto", "auto")
|
||||||
|
|
||||||
|
# Add detected Proton versions with type indicators
|
||||||
|
for proton in available_protons:
|
||||||
|
proton_name = proton.get('name', 'Unknown Proton')
|
||||||
|
proton_type = proton.get('type', 'Unknown')
|
||||||
|
|
||||||
|
# Format display name to show type for clarity
|
||||||
|
if proton_type == 'GE-Proton':
|
||||||
|
display_name = f"{proton_name} (GE)"
|
||||||
|
elif proton_type == 'Valve-Proton':
|
||||||
|
display_name = f"{proton_name}"
|
||||||
|
else:
|
||||||
|
display_name = proton_name
|
||||||
|
|
||||||
|
self.proton_dropdown.addItem(display_name, str(proton['path']))
|
||||||
|
|
||||||
|
# Load saved preference and determine UI selection
|
||||||
|
saved_proton = self.config_handler.get('proton_path', self._get_proton_10_path())
|
||||||
|
|
||||||
|
# Check if saved path matches any specific Proton in dropdown
|
||||||
|
found_match = False
|
||||||
|
for i in range(self.proton_dropdown.count()):
|
||||||
|
if self.proton_dropdown.itemData(i) == saved_proton:
|
||||||
|
self.proton_dropdown.setCurrentIndex(i)
|
||||||
|
found_match = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# If no exact match found, check if it's a resolved auto-selection
|
||||||
|
if not found_match and saved_proton != "auto":
|
||||||
|
# This means config has a resolved path from previous "Auto" selection
|
||||||
|
# Show "Auto" in UI since user chose auto-detection
|
||||||
|
for i in range(self.proton_dropdown.count()):
|
||||||
|
if self.proton_dropdown.itemData(i) == "auto":
|
||||||
|
self.proton_dropdown.setCurrentIndex(i)
|
||||||
|
break
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to populate Proton dropdown: {e}")
|
||||||
|
# Fallback: just show auto
|
||||||
|
self.proton_dropdown.addItem("Auto", "auto")
|
||||||
|
|
||||||
|
def _refresh_proton_dropdown(self):
|
||||||
|
"""Refresh Proton dropdown with latest detected versions"""
|
||||||
|
current_selection = self.proton_dropdown.currentData()
|
||||||
|
self.proton_dropdown.clear()
|
||||||
|
self._populate_proton_dropdown()
|
||||||
|
|
||||||
|
# Restore selection if still available
|
||||||
|
for i in range(self.proton_dropdown.count()):
|
||||||
|
if self.proton_dropdown.itemData(i) == current_selection:
|
||||||
|
self.proton_dropdown.setCurrentIndex(i)
|
||||||
|
break
|
||||||
|
|
||||||
def _save(self):
|
def _save(self):
|
||||||
# Validate values
|
# Validate values
|
||||||
for k, (multithreading_checkbox, max_tasks_spin) in self.resource_edits.items():
|
for k, (multithreading_checkbox, max_tasks_spin) in self.resource_edits.items():
|
||||||
@@ -490,7 +601,45 @@ class SettingsDialog(QDialog):
|
|||||||
# Save jackify data directory (always store actual path, never None)
|
# Save jackify data directory (always store actual path, never None)
|
||||||
jackify_data_dir = self.jackify_data_dir_edit.text().strip()
|
jackify_data_dir = self.jackify_data_dir_edit.text().strip()
|
||||||
self.config_handler.set("jackify_data_dir", jackify_data_dir)
|
self.config_handler.set("jackify_data_dir", jackify_data_dir)
|
||||||
self.config_handler.save_config()
|
|
||||||
|
# Save Proton selection - resolve "auto" to actual path
|
||||||
|
selected_proton_path = self.proton_dropdown.currentData()
|
||||||
|
if selected_proton_path == "auto":
|
||||||
|
# Resolve "auto" to actual best Proton path using unified detection
|
||||||
|
try:
|
||||||
|
from jackify.backend.handlers.wine_utils import WineUtils
|
||||||
|
best_proton = WineUtils.select_best_proton()
|
||||||
|
|
||||||
|
if best_proton:
|
||||||
|
resolved_path = str(best_proton['path'])
|
||||||
|
resolved_version = best_proton['name']
|
||||||
|
else:
|
||||||
|
resolved_path = "auto"
|
||||||
|
resolved_version = "auto"
|
||||||
|
except:
|
||||||
|
resolved_path = "auto"
|
||||||
|
resolved_version = "auto"
|
||||||
|
else:
|
||||||
|
# User selected specific Proton version
|
||||||
|
resolved_path = selected_proton_path
|
||||||
|
# Extract version from dropdown text
|
||||||
|
resolved_version = self.proton_dropdown.currentText()
|
||||||
|
|
||||||
|
self.config_handler.set("proton_path", resolved_path)
|
||||||
|
self.config_handler.set("proton_version", resolved_version)
|
||||||
|
|
||||||
|
# Force immediate save and verify
|
||||||
|
save_result = self.config_handler.save_config()
|
||||||
|
if not save_result:
|
||||||
|
self.logger.error("Failed to save Proton configuration")
|
||||||
|
else:
|
||||||
|
self.logger.info(f"Saved Proton config: path={resolved_path}, version={resolved_version}")
|
||||||
|
# Verify the save worked by reading it back
|
||||||
|
saved_path = self.config_handler.get("proton_path")
|
||||||
|
if saved_path != resolved_path:
|
||||||
|
self.logger.error(f"Config save verification failed: expected {resolved_path}, got {saved_path}")
|
||||||
|
else:
|
||||||
|
self.logger.debug("Config save verified successfully")
|
||||||
|
|
||||||
# Refresh cached paths in GUI screens if Jackify directory changed
|
# Refresh cached paths in GUI screens if Jackify directory changed
|
||||||
self._refresh_gui_paths()
|
self._refresh_gui_paths()
|
||||||
@@ -527,7 +676,6 @@ class SettingsDialog(QDialog):
|
|||||||
getattr(main_window, 'install_modlist_screen', None),
|
getattr(main_window, 'install_modlist_screen', None),
|
||||||
getattr(main_window, 'configure_new_modlist_screen', None),
|
getattr(main_window, 'configure_new_modlist_screen', None),
|
||||||
getattr(main_window, 'configure_existing_modlist_screen', None),
|
getattr(main_window, 'configure_existing_modlist_screen', None),
|
||||||
getattr(main_window, 'tuxborn_screen', None),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
for screen in screens_to_refresh:
|
for screen in screens_to_refresh:
|
||||||
@@ -636,7 +784,7 @@ class JackifyMainWindow(QMainWindow):
|
|||||||
|
|
||||||
# Create screens using refactored codebase
|
# Create screens using refactored codebase
|
||||||
from jackify.frontends.gui.screens import (
|
from jackify.frontends.gui.screens import (
|
||||||
MainMenu, TuxbornInstallerScreen, ModlistTasksScreen,
|
MainMenu, ModlistTasksScreen,
|
||||||
InstallModlistScreen, ConfigureNewModlistScreen, ConfigureExistingModlistScreen
|
InstallModlistScreen, ConfigureNewModlistScreen, ConfigureExistingModlistScreen
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -648,31 +796,26 @@ class JackifyMainWindow(QMainWindow):
|
|||||||
main_menu_index=0,
|
main_menu_index=0,
|
||||||
dev_mode=dev_mode
|
dev_mode=dev_mode
|
||||||
)
|
)
|
||||||
self.tuxborn_screen = TuxbornInstallerScreen(
|
self.install_modlist_screen = InstallModlistScreen(
|
||||||
stacked_widget=self.stacked_widget,
|
stacked_widget=self.stacked_widget,
|
||||||
main_menu_index=0
|
main_menu_index=0
|
||||||
)
|
)
|
||||||
self.install_modlist_screen = InstallModlistScreen(
|
|
||||||
stacked_widget=self.stacked_widget,
|
|
||||||
main_menu_index=3
|
|
||||||
)
|
|
||||||
self.configure_new_modlist_screen = ConfigureNewModlistScreen(
|
self.configure_new_modlist_screen = ConfigureNewModlistScreen(
|
||||||
stacked_widget=self.stacked_widget,
|
stacked_widget=self.stacked_widget,
|
||||||
main_menu_index=3
|
main_menu_index=0
|
||||||
)
|
)
|
||||||
self.configure_existing_modlist_screen = ConfigureExistingModlistScreen(
|
self.configure_existing_modlist_screen = ConfigureExistingModlistScreen(
|
||||||
stacked_widget=self.stacked_widget,
|
stacked_widget=self.stacked_widget,
|
||||||
main_menu_index=3
|
main_menu_index=0
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add screens to stacked widget
|
# Add screens to stacked widget
|
||||||
self.stacked_widget.addWidget(self.main_menu) # Index 0: Main Menu
|
self.stacked_widget.addWidget(self.main_menu) # Index 0: Main Menu
|
||||||
self.stacked_widget.addWidget(self.tuxborn_screen) # Index 1: Tuxborn Installer
|
self.stacked_widget.addWidget(self.feature_placeholder) # Index 1: Placeholder
|
||||||
self.stacked_widget.addWidget(self.feature_placeholder) # Index 2: Placeholder
|
self.stacked_widget.addWidget(self.modlist_tasks_screen) # Index 2: Modlist Tasks
|
||||||
self.stacked_widget.addWidget(self.modlist_tasks_screen) # Index 3: Modlist Tasks
|
self.stacked_widget.addWidget(self.install_modlist_screen) # Index 3: Install Modlist
|
||||||
self.stacked_widget.addWidget(self.install_modlist_screen) # Index 4: Install Modlist
|
self.stacked_widget.addWidget(self.configure_new_modlist_screen) # Index 4: Configure New
|
||||||
self.stacked_widget.addWidget(self.configure_new_modlist_screen) # Index 5: Configure New
|
self.stacked_widget.addWidget(self.configure_existing_modlist_screen) # Index 5: Configure Existing
|
||||||
self.stacked_widget.addWidget(self.configure_existing_modlist_screen) # Index 6: Configure Existing
|
|
||||||
|
|
||||||
# Add debug tracking for screen changes
|
# Add debug tracking for screen changes
|
||||||
self.stacked_widget.currentChanged.connect(self._debug_screen_change)
|
self.stacked_widget.currentChanged.connect(self._debug_screen_change)
|
||||||
@@ -750,12 +893,11 @@ class JackifyMainWindow(QMainWindow):
|
|||||||
|
|
||||||
screen_names = {
|
screen_names = {
|
||||||
0: "Main Menu",
|
0: "Main Menu",
|
||||||
1: "Tuxborn Installer",
|
1: "Feature Placeholder",
|
||||||
2: "Feature Placeholder",
|
2: "Modlist Tasks Menu",
|
||||||
3: "Modlist Tasks Menu",
|
3: "Install Modlist Screen",
|
||||||
4: "Install Modlist Screen",
|
4: "Configure New Modlist",
|
||||||
5: "Configure New Modlist",
|
5: "Configure Existing Modlist"
|
||||||
6: "Configure Existing Modlist"
|
|
||||||
}
|
}
|
||||||
screen_name = screen_names.get(index, f"Unknown Screen (Index {index})")
|
screen_name = screen_names.get(index, f"Unknown Screen (Index {index})")
|
||||||
widget = self.stacked_widget.widget(index)
|
widget = self.stacked_widget.widget(index)
|
||||||
@@ -801,27 +943,56 @@ class JackifyMainWindow(QMainWindow):
|
|||||||
# Continue anyway - don't block startup on detection errors
|
# Continue anyway - don't block startup on detection errors
|
||||||
|
|
||||||
def _check_for_updates_on_startup(self):
|
def _check_for_updates_on_startup(self):
|
||||||
"""Check for updates on startup in background thread"""
|
"""Check for updates on startup - SIMPLE VERSION"""
|
||||||
try:
|
try:
|
||||||
debug_print("Checking for updates on startup...")
|
debug_print("Checking for updates on startup...")
|
||||||
|
|
||||||
def update_check_callback(update_info):
|
# Do it synchronously and simply
|
||||||
"""Handle update check results"""
|
update_info = self.update_service.check_for_updates()
|
||||||
try:
|
if update_info:
|
||||||
if update_info:
|
debug_print(f"Update available: v{update_info.version}")
|
||||||
debug_print(f"Update available: v{update_info.version}")
|
|
||||||
# Show update dialog
|
# Simple QMessageBox - no complex dialogs
|
||||||
|
from PySide6.QtWidgets import QMessageBox
|
||||||
|
from PySide6.QtCore import QTimer
|
||||||
|
|
||||||
|
def show_update_dialog():
|
||||||
|
try:
|
||||||
|
debug_print("Creating UpdateDialog...")
|
||||||
from jackify.frontends.gui.dialogs.update_dialog import UpdateDialog
|
from jackify.frontends.gui.dialogs.update_dialog import UpdateDialog
|
||||||
dialog = UpdateDialog(update_info, self.update_service, self)
|
dialog = UpdateDialog(update_info, self.update_service, self)
|
||||||
|
debug_print("UpdateDialog created, showing...")
|
||||||
dialog.show() # Non-blocking
|
dialog.show() # Non-blocking
|
||||||
else:
|
debug_print("UpdateDialog shown successfully")
|
||||||
debug_print("No updates available")
|
except Exception as e:
|
||||||
except Exception as e:
|
debug_print(f"UpdateDialog failed: {e}, falling back to simple dialog")
|
||||||
debug_print(f"Error showing update dialog: {e}")
|
# Fallback to simple dialog
|
||||||
|
reply = QMessageBox.question(
|
||||||
# Check for updates in background
|
self,
|
||||||
self.update_service.check_for_updates_async(update_check_callback)
|
"Update Available",
|
||||||
|
f"Jackify v{update_info.version} is available.\n\nDownload and install now?",
|
||||||
|
QMessageBox.Yes | QMessageBox.No,
|
||||||
|
QMessageBox.Yes
|
||||||
|
)
|
||||||
|
if reply == QMessageBox.Yes:
|
||||||
|
# Simple download and replace
|
||||||
|
try:
|
||||||
|
new_appimage = self.update_service.download_update(update_info)
|
||||||
|
if new_appimage:
|
||||||
|
if self.update_service.apply_update(new_appimage):
|
||||||
|
debug_print("Update applied successfully")
|
||||||
|
else:
|
||||||
|
QMessageBox.warning(self, "Update Failed", "Failed to apply update.")
|
||||||
|
else:
|
||||||
|
QMessageBox.warning(self, "Update Failed", "Failed to download update.")
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.warning(self, "Update Failed", f"Update failed: {e}")
|
||||||
|
|
||||||
|
# Use QTimer to show dialog after GUI is fully loaded
|
||||||
|
QTimer.singleShot(1000, show_update_dialog)
|
||||||
|
else:
|
||||||
|
debug_print("No updates available")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
debug_print(f"Error checking for updates on startup: {e}")
|
debug_print(f"Error checking for updates on startup: {e}")
|
||||||
# Continue anyway - don't block startup on update check errors
|
# Continue anyway - don't block startup on update check errors
|
||||||
@@ -836,7 +1007,7 @@ class JackifyMainWindow(QMainWindow):
|
|||||||
|
|
||||||
# Clean up screen processes
|
# Clean up screen processes
|
||||||
screens = [
|
screens = [
|
||||||
self.modlist_tasks_screen, self.tuxborn_screen, self.install_modlist_screen,
|
self.modlist_tasks_screen, self.install_modlist_screen,
|
||||||
self.configure_new_modlist_screen, self.configure_existing_modlist_screen
|
self.configure_new_modlist_screen, self.configure_existing_modlist_screen
|
||||||
]
|
]
|
||||||
for screen in screens:
|
for screen in screens:
|
||||||
@@ -906,7 +1077,18 @@ def main():
|
|||||||
# Command-line --debug always takes precedence
|
# Command-line --debug always takes precedence
|
||||||
if '--debug' in sys.argv or '-d' in sys.argv:
|
if '--debug' in sys.argv or '-d' in sys.argv:
|
||||||
debug_mode = True
|
debug_mode = True
|
||||||
|
# Temporarily save CLI debug flag to config so engine can see it
|
||||||
|
config_handler.set('debug_mode', True)
|
||||||
|
print("[DEBUG] CLI --debug flag detected, saved debug_mode=True to config")
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
# Initialize file logging on root logger so all modules inherit it
|
||||||
|
from jackify.shared.logging import LoggingHandler
|
||||||
|
logging_handler = LoggingHandler()
|
||||||
|
# Rotate log file before setting up new logger
|
||||||
|
logging_handler.rotate_log_for_logger('jackify_gui', 'jackify-gui.log')
|
||||||
|
root_logger = logging_handler.setup_logger('', 'jackify-gui.log', is_general=True) # Empty name = root logger
|
||||||
|
|
||||||
if debug_mode:
|
if debug_mode:
|
||||||
logging.getLogger().setLevel(logging.DEBUG)
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
print("[Jackify] Debug mode enabled (from config or CLI)")
|
print("[Jackify] Debug mode enabled (from config or CLI)")
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ Contains all the GUI screen components for Jackify.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from .main_menu import MainMenu
|
from .main_menu import MainMenu
|
||||||
from .tuxborn_installer import TuxbornInstallerScreen
|
|
||||||
from .modlist_tasks import ModlistTasksScreen
|
from .modlist_tasks import ModlistTasksScreen
|
||||||
from .install_modlist import InstallModlistScreen
|
from .install_modlist import InstallModlistScreen
|
||||||
from .configure_new_modlist import ConfigureNewModlistScreen
|
from .configure_new_modlist import ConfigureNewModlistScreen
|
||||||
@@ -13,7 +12,6 @@ from .configure_existing_modlist import ConfigureExistingModlistScreen
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'MainMenu',
|
'MainMenu',
|
||||||
'TuxbornInstallerScreen',
|
|
||||||
'ModlistTasksScreen',
|
'ModlistTasksScreen',
|
||||||
'InstallModlistScreen',
|
'InstallModlistScreen',
|
||||||
'ConfigureNewModlistScreen',
|
'ConfigureNewModlistScreen',
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ class ConfigureExistingModlistScreen(QWidget):
|
|||||||
self.shortcut_combo.addItem("Please Select...")
|
self.shortcut_combo.addItem("Please Select...")
|
||||||
self.shortcut_map = []
|
self.shortcut_map = []
|
||||||
for shortcut in self.mo2_shortcuts:
|
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_combo.addItem(display)
|
||||||
self.shortcut_map.append(shortcut)
|
self.shortcut_map.append(shortcut)
|
||||||
|
|
||||||
@@ -427,8 +427,8 @@ class ConfigureExistingModlistScreen(QWidget):
|
|||||||
self._enable_controls_after_operation()
|
self._enable_controls_after_operation()
|
||||||
return
|
return
|
||||||
shortcut = self.shortcut_map[idx]
|
shortcut = self.shortcut_map[idx]
|
||||||
modlist_name = shortcut.get('AppName', '')
|
modlist_name = shortcut.get('AppName', shortcut.get('appname', ''))
|
||||||
install_dir = shortcut.get('StartDir', '')
|
install_dir = shortcut.get('StartDir', shortcut.get('startdir', ''))
|
||||||
if not modlist_name or not install_dir:
|
if not modlist_name or not install_dir:
|
||||||
MessageService.critical(self, "Invalid Shortcut", "The selected shortcut is missing required information.", safety_level="medium")
|
MessageService.critical(self, "Invalid Shortcut", "The selected shortcut is missing required information.", safety_level="medium")
|
||||||
self._enable_controls_after_operation()
|
self._enable_controls_after_operation()
|
||||||
@@ -710,7 +710,7 @@ class ConfigureExistingModlistScreen(QWidget):
|
|||||||
self.shortcut_map.clear()
|
self.shortcut_map.clear()
|
||||||
|
|
||||||
for shortcut in self.mo2_shortcuts:
|
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_combo.addItem(display)
|
||||||
self.shortcut_map.append(shortcut)
|
self.shortcut_map.append(shortcut)
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from jackify.backend.handlers.config_handler import ConfigHandler
|
|||||||
from ..dialogs import SuccessDialog
|
from ..dialogs import SuccessDialog
|
||||||
from PySide6.QtWidgets import QApplication
|
from PySide6.QtWidgets import QApplication
|
||||||
from jackify.frontends.gui.services.message_service import MessageService
|
from jackify.frontends.gui.services.message_service import MessageService
|
||||||
|
from jackify.shared.resolution_utils import get_resolution_fallback
|
||||||
|
|
||||||
def debug_print(message):
|
def debug_print(message):
|
||||||
"""Print debug message only if debug mode is enabled"""
|
"""Print debug message only if debug mode is enabled"""
|
||||||
@@ -480,7 +481,7 @@ class ConfigureNewModlistScreen(QWidget):
|
|||||||
|
|
||||||
def go_back(self):
|
def go_back(self):
|
||||||
if self.stacked_widget:
|
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):
|
def update_top_panel(self):
|
||||||
try:
|
try:
|
||||||
@@ -1033,7 +1034,7 @@ class ConfigureNewModlistScreen(QWidget):
|
|||||||
try:
|
try:
|
||||||
# Get resolution from UI
|
# Get resolution from UI
|
||||||
resolution = self.resolution_combo.currentText()
|
resolution = self.resolution_combo.currentText()
|
||||||
resolution_value = resolution.split()[0] if resolution != "Leave unchanged" else '2560x1600'
|
resolution_value = resolution.split()[0] if resolution != "Leave unchanged" else None
|
||||||
|
|
||||||
# Update the context with the new AppID (same format as manual steps)
|
# Update the context with the new AppID (same format as manual steps)
|
||||||
mo2_exe_path = self.install_dir_edit.text().strip()
|
mo2_exe_path = self.install_dir_edit.text().strip()
|
||||||
@@ -1082,7 +1083,7 @@ class ConfigureNewModlistScreen(QWidget):
|
|||||||
nexus_api_key='', # Not needed for configuration
|
nexus_api_key='', # Not needed for configuration
|
||||||
modlist_value=self.context.get('modlist_value'),
|
modlist_value=self.context.get('modlist_value'),
|
||||||
modlist_source=self.context.get('modlist_source', 'identifier'),
|
modlist_source=self.context.get('modlist_source', 'identifier'),
|
||||||
resolution=self.context.get('resolution', '2560x1600'),
|
resolution=self.context.get('resolution') or get_resolution_fallback(None),
|
||||||
skip_confirmation=True
|
skip_confirmation=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -367,6 +367,10 @@ class InstallModlistScreen(QWidget):
|
|||||||
self.resolution_service = ResolutionService()
|
self.resolution_service = ResolutionService()
|
||||||
self.config_handler = ConfigHandler()
|
self.config_handler = ConfigHandler()
|
||||||
self.protontricks_service = ProtontricksDetectionService()
|
self.protontricks_service = ProtontricksDetectionService()
|
||||||
|
|
||||||
|
# Somnium guidance tracking
|
||||||
|
self._show_somnium_guidance = False
|
||||||
|
self._somnium_install_dir = None
|
||||||
|
|
||||||
# Scroll tracking for professional auto-scroll behavior
|
# Scroll tracking for professional auto-scroll behavior
|
||||||
self._user_manually_scrolled = False
|
self._user_manually_scrolled = False
|
||||||
@@ -1053,7 +1057,7 @@ class InstallModlistScreen(QWidget):
|
|||||||
|
|
||||||
def go_back(self):
|
def go_back(self):
|
||||||
if self.stacked_widget:
|
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):
|
def update_top_panel(self):
|
||||||
try:
|
try:
|
||||||
@@ -1356,7 +1360,8 @@ class InstallModlistScreen(QWidget):
|
|||||||
'oblivion': 'oblivion',
|
'oblivion': 'oblivion',
|
||||||
'starfield': 'starfield',
|
'starfield': 'starfield',
|
||||||
'oblivion_remastered': 'oblivion_remastered',
|
'oblivion_remastered': 'oblivion_remastered',
|
||||||
'enderal': 'enderal'
|
'enderal': 'enderal',
|
||||||
|
'enderal special edition': 'enderal'
|
||||||
}
|
}
|
||||||
game_type = game_mapping.get(game_name.lower())
|
game_type = game_mapping.get(game_name.lower())
|
||||||
debug_print(f"DEBUG: Mapped game_name '{game_name}' to game_type: '{game_type}'")
|
debug_print(f"DEBUG: Mapped game_name '{game_name}' to game_type: '{game_type}'")
|
||||||
@@ -1373,6 +1378,7 @@ class InstallModlistScreen(QWidget):
|
|||||||
|
|
||||||
# Check if game is supported
|
# Check if game is supported
|
||||||
debug_print(f"DEBUG: Checking if game_type '{game_type}' is supported")
|
debug_print(f"DEBUG: Checking if game_type '{game_type}' is supported")
|
||||||
|
debug_print(f"DEBUG: game_type='{game_type}', game_name='{game_name}'")
|
||||||
is_supported = self.wabbajack_parser.is_supported_game(game_type) if game_type else False
|
is_supported = self.wabbajack_parser.is_supported_game(game_type) if game_type else False
|
||||||
debug_print(f"DEBUG: is_supported_game('{game_type}') returned: {is_supported}")
|
debug_print(f"DEBUG: is_supported_game('{game_type}') returned: {is_supported}")
|
||||||
|
|
||||||
@@ -1740,7 +1746,7 @@ class InstallModlistScreen(QWidget):
|
|||||||
|
|
||||||
# Save resolution for later use in configuration
|
# Save resolution for later use in configuration
|
||||||
resolution = self.resolution_combo.currentText()
|
resolution = self.resolution_combo.currentText()
|
||||||
self._current_resolution = resolution.split()[0] if resolution != "Leave unchanged" else "2560x1600"
|
self._current_resolution = resolution.split()[0] if resolution != "Leave unchanged" else None
|
||||||
|
|
||||||
# Use automated prefix creation instead of manual steps
|
# Use automated prefix creation instead of manual steps
|
||||||
debug_print("DEBUG: Starting automated prefix creation workflow")
|
debug_print("DEBUG: Starting automated prefix creation workflow")
|
||||||
@@ -1760,10 +1766,26 @@ class InstallModlistScreen(QWidget):
|
|||||||
final_exe_path = os.path.join(install_dir, "ModOrganizer.exe")
|
final_exe_path = os.path.join(install_dir, "ModOrganizer.exe")
|
||||||
|
|
||||||
if not os.path.exists(final_exe_path):
|
if not os.path.exists(final_exe_path):
|
||||||
self._safe_append_text(f"ERROR: ModOrganizer.exe not found at {final_exe_path}")
|
# Check if this is Somnium specifically (uses files/ subdirectory)
|
||||||
MessageService.critical(self, "ModOrganizer.exe Not Found",
|
modlist_name_lower = modlist_name.lower()
|
||||||
f"ModOrganizer.exe not found at:\n{final_exe_path}\n\nCannot proceed with automated setup.")
|
if "somnium" in modlist_name_lower:
|
||||||
return
|
somnium_exe_path = os.path.join(install_dir, "files", "ModOrganizer.exe")
|
||||||
|
if os.path.exists(somnium_exe_path):
|
||||||
|
final_exe_path = somnium_exe_path
|
||||||
|
self._safe_append_text(f"Detected Somnium modlist - will proceed with automated setup")
|
||||||
|
# Show Somnium guidance popup after automated workflow completes
|
||||||
|
self._show_somnium_guidance = True
|
||||||
|
self._somnium_install_dir = install_dir
|
||||||
|
else:
|
||||||
|
self._safe_append_text(f"ERROR: Somnium ModOrganizer.exe not found at {somnium_exe_path}")
|
||||||
|
MessageService.critical(self, "Somnium ModOrganizer.exe Not Found",
|
||||||
|
f"Expected Somnium ModOrganizer.exe not found at:\n{somnium_exe_path}\n\nCannot proceed with automated setup.")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
self._safe_append_text(f"ERROR: ModOrganizer.exe not found at {final_exe_path}")
|
||||||
|
MessageService.critical(self, "ModOrganizer.exe Not Found",
|
||||||
|
f"ModOrganizer.exe not found at:\n{final_exe_path}\n\nCannot proceed with automated setup.")
|
||||||
|
return
|
||||||
|
|
||||||
# Run automated prefix creation in separate thread
|
# Run automated prefix creation in separate thread
|
||||||
from PySide6.QtCore import QThread, Signal
|
from PySide6.QtCore import QThread, Signal
|
||||||
@@ -1940,6 +1962,10 @@ class InstallModlistScreen(QWidget):
|
|||||||
self._enable_controls_after_operation()
|
self._enable_controls_after_operation()
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
|
# Check if we need to show Somnium guidance
|
||||||
|
if self._show_somnium_guidance:
|
||||||
|
self._show_somnium_post_install_guidance()
|
||||||
|
|
||||||
# Show celebration SuccessDialog after the entire workflow
|
# Show celebration SuccessDialog after the entire workflow
|
||||||
from ..dialogs import SuccessDialog
|
from ..dialogs import SuccessDialog
|
||||||
import time
|
import time
|
||||||
@@ -2041,11 +2067,20 @@ class InstallModlistScreen(QWidget):
|
|||||||
self.cancel_btn.setVisible(True)
|
self.cancel_btn.setVisible(True)
|
||||||
self.cancel_install_btn.setVisible(False)
|
self.cancel_install_btn.setVisible(False)
|
||||||
|
|
||||||
|
def _get_mo2_path(self, install_dir, modlist_name):
|
||||||
|
"""Get ModOrganizer.exe path, handling Somnium's non-standard structure"""
|
||||||
|
mo2_exe_path = os.path.join(install_dir, "ModOrganizer.exe")
|
||||||
|
if not os.path.exists(mo2_exe_path) and "somnium" in modlist_name.lower():
|
||||||
|
somnium_path = os.path.join(install_dir, "files", "ModOrganizer.exe")
|
||||||
|
if os.path.exists(somnium_path):
|
||||||
|
mo2_exe_path = somnium_path
|
||||||
|
return mo2_exe_path
|
||||||
|
|
||||||
def validate_manual_steps_completion(self):
|
def validate_manual_steps_completion(self):
|
||||||
"""Validate that manual steps were actually completed and handle retry logic"""
|
"""Validate that manual steps were actually completed and handle retry logic"""
|
||||||
modlist_name = self.modlist_name_edit.text().strip()
|
modlist_name = self.modlist_name_edit.text().strip()
|
||||||
install_dir = self.install_dir_edit.text().strip()
|
install_dir = self.install_dir_edit.text().strip()
|
||||||
mo2_exe_path = os.path.join(install_dir, "ModOrganizer.exe")
|
mo2_exe_path = self._get_mo2_path(install_dir, modlist_name)
|
||||||
|
|
||||||
# Add delay to allow Steam filesystem updates to complete
|
# Add delay to allow Steam filesystem updates to complete
|
||||||
self._safe_append_text("Waiting for Steam filesystem updates to complete...")
|
self._safe_append_text("Waiting for Steam filesystem updates to complete...")
|
||||||
@@ -2283,10 +2318,10 @@ class InstallModlistScreen(QWidget):
|
|||||||
updated_context = {
|
updated_context = {
|
||||||
'name': modlist_name,
|
'name': modlist_name,
|
||||||
'path': install_dir,
|
'path': install_dir,
|
||||||
'mo2_exe_path': os.path.join(install_dir, "ModOrganizer.exe"),
|
'mo2_exe_path': self._get_mo2_path(install_dir, modlist_name),
|
||||||
'modlist_value': None,
|
'modlist_value': None,
|
||||||
'modlist_source': None,
|
'modlist_source': None,
|
||||||
'resolution': getattr(self, '_current_resolution', '2560x1600'),
|
'resolution': getattr(self, '_current_resolution', None),
|
||||||
'skip_confirmation': True,
|
'skip_confirmation': True,
|
||||||
'manual_steps_completed': True, # Mark as completed since automated prefix is done
|
'manual_steps_completed': True, # Mark as completed since automated prefix is done
|
||||||
'appid': new_appid, # Use the NEW AppID from automated prefix creation
|
'appid': new_appid, # Use the NEW AppID from automated prefix creation
|
||||||
@@ -2325,7 +2360,7 @@ class InstallModlistScreen(QWidget):
|
|||||||
nexus_api_key='', # Not needed for configuration
|
nexus_api_key='', # Not needed for configuration
|
||||||
modlist_value=self.context.get('modlist_value'),
|
modlist_value=self.context.get('modlist_value'),
|
||||||
modlist_source=self.context.get('modlist_source', 'identifier'),
|
modlist_source=self.context.get('modlist_source', 'identifier'),
|
||||||
resolution=self.context.get('resolution', '2560x1600'),
|
resolution=self.context.get('resolution'),
|
||||||
skip_confirmation=True,
|
skip_confirmation=True,
|
||||||
engine_installed=True # Skip path manipulation for engine workflows
|
engine_installed=True # Skip path manipulation for engine workflows
|
||||||
)
|
)
|
||||||
@@ -2381,10 +2416,10 @@ class InstallModlistScreen(QWidget):
|
|||||||
updated_context = {
|
updated_context = {
|
||||||
'name': modlist_name,
|
'name': modlist_name,
|
||||||
'path': install_dir,
|
'path': install_dir,
|
||||||
'mo2_exe_path': os.path.join(install_dir, "ModOrganizer.exe"),
|
'mo2_exe_path': self._get_mo2_path(install_dir, modlist_name),
|
||||||
'modlist_value': None,
|
'modlist_value': None,
|
||||||
'modlist_source': None,
|
'modlist_source': None,
|
||||||
'resolution': getattr(self, '_current_resolution', '2560x1600'),
|
'resolution': getattr(self, '_current_resolution', None),
|
||||||
'skip_confirmation': True,
|
'skip_confirmation': True,
|
||||||
'manual_steps_completed': True, # Mark as completed
|
'manual_steps_completed': True, # Mark as completed
|
||||||
'appid': new_appid # Use the NEW AppID from Steam
|
'appid': new_appid # Use the NEW AppID from Steam
|
||||||
@@ -2616,6 +2651,26 @@ class InstallModlistScreen(QWidget):
|
|||||||
|
|
||||||
self._safe_append_text("Installation cancelled by user.")
|
self._safe_append_text("Installation cancelled by user.")
|
||||||
|
|
||||||
|
def _show_somnium_post_install_guidance(self):
|
||||||
|
"""Show guidance popup for Somnium post-installation steps"""
|
||||||
|
from ..widgets.message_service import MessageService
|
||||||
|
|
||||||
|
guidance_text = f"""<b>Somnium Post-Installation Required</b><br><br>
|
||||||
|
Due to Somnium's non-standard folder structure, you need to manually update the binary paths in ModOrganizer:<br><br>
|
||||||
|
<b>1.</b> Launch the Steam shortcut created for Somnium<br>
|
||||||
|
<b>2.</b> In ModOrganizer, go to Settings → Executables<br>
|
||||||
|
<b>3.</b> For each executable entry (SKSE64, etc.), update the binary path to point to:<br>
|
||||||
|
<code>{self._somnium_install_dir}/files/root/Enderal Special Edition/skse64_loader.exe</code><br><br>
|
||||||
|
<b>Note:</b> Full Somnium support will be added in a future Jackify update.<br><br>
|
||||||
|
<i>You can also refer to the Somnium installation guide at:<br>
|
||||||
|
https://wiki.scenicroute.games/Somnium/1_Installation.html</i>"""
|
||||||
|
|
||||||
|
MessageService.information(self, "Somnium Setup Required", guidance_text)
|
||||||
|
|
||||||
|
# Reset the guidance flag
|
||||||
|
self._show_somnium_guidance = False
|
||||||
|
self._somnium_install_dir = None
|
||||||
|
|
||||||
def cancel_and_cleanup(self):
|
def cancel_and_cleanup(self):
|
||||||
"""Handle Cancel button - clean up processes and go back"""
|
"""Handle Cancel button - clean up processes and go back"""
|
||||||
self.cleanup_processes()
|
self.cleanup_processes()
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ class MainMenu(QWidget):
|
|||||||
msg.setIcon(QMessageBox.Information)
|
msg.setIcon(QMessageBox.Information)
|
||||||
msg.exec()
|
msg.exec()
|
||||||
elif action_id == "modlist_tasks" and self.stacked_widget:
|
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":
|
elif action_id == "return_main_menu":
|
||||||
# This is the main menu, so do nothing
|
# This is the main menu, so do nothing
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -198,11 +198,11 @@ class ModlistTasksScreen(QWidget):
|
|||||||
if action_id == "return_main_menu":
|
if action_id == "return_main_menu":
|
||||||
self.stacked_widget.setCurrentIndex(0)
|
self.stacked_widget.setCurrentIndex(0)
|
||||||
elif action_id == "install_modlist":
|
elif action_id == "install_modlist":
|
||||||
self.stacked_widget.setCurrentIndex(4)
|
self.stacked_widget.setCurrentIndex(3)
|
||||||
elif action_id == "configure_new_modlist":
|
elif action_id == "configure_new_modlist":
|
||||||
self.stacked_widget.setCurrentIndex(5)
|
self.stacked_widget.setCurrentIndex(4)
|
||||||
elif action_id == "configure_existing_modlist":
|
elif action_id == "configure_existing_modlist":
|
||||||
self.stacked_widget.setCurrentIndex(6)
|
self.stacked_widget.setCurrentIndex(5)
|
||||||
|
|
||||||
def go_back(self):
|
def go_back(self):
|
||||||
"""Return to main menu"""
|
"""Return to main menu"""
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -94,6 +94,7 @@ class UnsupportedGameDialog(QDialog):
|
|||||||
<li><strong>Oblivion</strong></li>
|
<li><strong>Oblivion</strong></li>
|
||||||
<li><strong>Starfield</strong></li>
|
<li><strong>Starfield</strong></li>
|
||||||
<li><strong>Oblivion Remastered</strong></li>
|
<li><strong>Oblivion Remastered</strong></li>
|
||||||
|
<li><strong>Enderal</strong></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>For unsupported games, you will need to manually configure Steam shortcuts and other post-install steps.</p>
|
<p>For unsupported games, you will need to manually configure Steam shortcuts and other post-install steps.</p>
|
||||||
@@ -113,6 +114,7 @@ class UnsupportedGameDialog(QDialog):
|
|||||||
<li><strong>Oblivion</strong></li>
|
<li><strong>Oblivion</strong></li>
|
||||||
<li><strong>Starfield</strong></li>
|
<li><strong>Starfield</strong></li>
|
||||||
<li><strong>Oblivion Remastered</strong></li>
|
<li><strong>Oblivion Remastered</strong></li>
|
||||||
|
<li><strong>Enderal</strong></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>For unsupported games, you will need to manually configure Steam shortcuts and other post-install steps.</p>
|
<p>For unsupported games, you will need to manually configure Steam shortcuts and other post-install steps.</p>
|
||||||
|
|||||||
113
jackify/shared/resolution_utils.py
Normal file
113
jackify/shared/resolution_utils.py
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Resolution Utilities Module
|
||||||
|
Provides utility functions for handling resolution across GUI and CLI frontends
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_resolution() -> str:
|
||||||
|
"""
|
||||||
|
Get the appropriate default resolution based on system detection and user preferences.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Resolution string (e.g., '1920x1080', '1280x800')
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# First try to get saved resolution from config
|
||||||
|
from ..backend.services.resolution_service import ResolutionService
|
||||||
|
resolution_service = ResolutionService()
|
||||||
|
|
||||||
|
saved_resolution = resolution_service.get_saved_resolution()
|
||||||
|
if saved_resolution and saved_resolution != 'Leave unchanged':
|
||||||
|
logger.debug(f"Using saved resolution: {saved_resolution}")
|
||||||
|
return saved_resolution
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Could not load ResolutionService: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check for Steam Deck
|
||||||
|
if _is_steam_deck():
|
||||||
|
logger.debug("Steam Deck detected, using 1280x800")
|
||||||
|
return "1280x800"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error detecting Steam Deck: {e}")
|
||||||
|
|
||||||
|
# Fallback to common 1080p instead of arbitrary resolution
|
||||||
|
logger.debug("Using fallback resolution: 1920x1080")
|
||||||
|
return "1920x1080"
|
||||||
|
|
||||||
|
|
||||||
|
def _is_steam_deck() -> bool:
|
||||||
|
"""
|
||||||
|
Detect if running on Steam Deck
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if Steam Deck detected
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if os.path.exists("/etc/os-release"):
|
||||||
|
with open("/etc/os-release", "r") as f:
|
||||||
|
content = f.read().lower()
|
||||||
|
return "steamdeck" in content or "steamos" in content
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Error reading /etc/os-release: {e}")
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_resolution_fallback(current_resolution: Optional[str]) -> str:
|
||||||
|
"""
|
||||||
|
Get appropriate resolution fallback when current resolution is invalid or None
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current_resolution: Current resolution value that might be None/invalid
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Valid resolution string
|
||||||
|
"""
|
||||||
|
if current_resolution and current_resolution != 'Leave unchanged':
|
||||||
|
# Validate format
|
||||||
|
if _validate_resolution_format(current_resolution):
|
||||||
|
return current_resolution
|
||||||
|
|
||||||
|
# Use proper default resolution logic
|
||||||
|
return get_default_resolution()
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_resolution_format(resolution: str) -> bool:
|
||||||
|
"""
|
||||||
|
Validate resolution format
|
||||||
|
|
||||||
|
Args:
|
||||||
|
resolution: Resolution string to validate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if valid WxH format
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
|
||||||
|
if not resolution:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Handle Steam Deck format
|
||||||
|
clean_resolution = resolution.replace(' (Steam Deck)', '')
|
||||||
|
|
||||||
|
# Check WxH format
|
||||||
|
if re.match(r'^[0-9]+x[0-9]+$', clean_resolution):
|
||||||
|
try:
|
||||||
|
width, height = clean_resolution.split('x')
|
||||||
|
width_int, height_int = int(width), int(height)
|
||||||
|
return 0 < width_int <= 10000 and 0 < height_int <= 10000
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return False
|
||||||
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