Sync from development - prepare for v0.1.4

This commit is contained in:
Omni
2025-09-22 20:39:58 +01:00
parent 28cde64887
commit c9bd6f60e6
57 changed files with 1057 additions and 276 deletions

View File

@@ -1,5 +1,50 @@
# Jackify Changelog # Jackify Changelog
## v0.1.4 - GE-Proton Support and Performance Optimization
**Release Date:** September 22, 2025
### New Features
- **GE-Proton Detection**: Automatic detection and prioritization of GE-Proton versions
- **User-selectable Proton version**: Settings dialog displays all available Proton versions with type indicators
### Engine Updates
- **jackify-engine v0.3.15**: Reads Proton configuration from config.json, adds degree symbol handling for special characters, removes Wine fallback (Proton now required)
### Technical Improvements
- **Smart Priority**: GE-Proton 10+ → Proton Experimental → Proton 10 → Proton 9
- **Auto-Configuration**: Fresh installations automatically select optimal Proton version
### Bug Fixes
- **Steam VDF Compatibility**: Fixed case-sensitivity issues with Steam shortcuts.vdf parsing for Configure Existing Modlist workflows
---
## v0.1.3 - Enhanced Proton Support and System Compatibility
**Release Date:** September 21, 2025
### New Features
- **Enhanced Proton Detection**: Automatic fallback system with priority: Experimental → Proton 10 → Proton 9
- **Guided Proton Installation**: Professional auto-install dialog with Steam protocol integration for missing Proton versions
- **Enderal Game Support**: Added Enderal to supported games list with special handling for Somnium modlist structure
- **Proton Version Leniency**: Accept any Proton version 9+ instead of requiring Experimental
### UX Improvements
- **Resolution System Overhaul**: Eliminated hardcoded 2560x1600 fallbacks across all screens
- **Steam Deck Detection**: Proper 1280x800 default resolution with 1920x1080 fallback for desktop
- **Leave Unchanged Logic**: Fixed resolution setting to actually preserve existing user configurations
### Technical Improvements
- **Resolution Utilities**: New `shared/resolution_utils.py` with centralized resolution management
- **Protontricks Detection**: Enhanced detection for both native and Flatpak protontricks installations
- **Real-time Monitoring**: Progress tracking for Proton installation with directory stability detection
### Bug Fixes
- **Somnium Support**: Automatic detection of `files/ModOrganizer.exe` structure in edge-case modlists
- **Steam Protocol Integration**: Reliable triggering of Proton installation via `steam://install/` URLs
- **Manual Fallback**: Clear instructions and recheck functionality when auto-install fails
---
## v0.1.2 - About Dialog and System Information ## v0.1.2 - About Dialog and System Information
**Release Date:** September 16, 2025 **Release Date:** September 16, 2025

View File

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

View File

@@ -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:
@@ -1248,13 +1286,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:

View File

@@ -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"

View File

@@ -1163,7 +1163,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 +1238,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()

View File

@@ -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:

View File

@@ -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]

View File

@@ -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
@@ -643,7 +643,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 +711,276 @@ 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 including standard locations.
Returns:
List of Path objects for Steam library directories
"""
steam_paths = [
Path.home() / ".steam/steam/steamapps/common",
Path.home() / ".local/share/Steam/steamapps/common",
Path.home() / ".steam/root/steamapps/common"
]
# Return only existing paths
return [path for path in steam_paths if path.exists()]
@staticmethod
def get_compatibility_tool_paths() -> List[Path]:
"""
Get all compatibility tool paths for GE-Proton and other custom Proton versions.
Returns:
List of Path objects for compatibility tool directories
"""
compat_paths = [
Path.home() / ".steam/steam/compatibilitytools.d",
Path.home() / ".local/share/Steam/compatibilitytools.d"
]
# Return only existing paths
return [path for path in compat_paths if path.exists()]
@staticmethod
def scan_ge_proton_versions() -> List[Dict[str, any]]:
"""
Scan for available GE-Proton versions in compatibilitytools.d directories.
Returns:
List of dicts with version info, sorted by priority (newest first)
"""
logger.info("Scanning for available GE-Proton versions...")
found_versions = []
compat_paths = WineUtils.get_compatibility_tool_paths()
if not compat_paths:
logger.warning("No compatibility tool paths found")
return []
for compat_path in compat_paths:
logger.debug(f"Scanning compatibility tools: {compat_path}")
try:
# Look for GE-Proton directories
for proton_dir in compat_path.iterdir():
if not proton_dir.is_dir():
continue
dir_name = proton_dir.name
if not dir_name.startswith("GE-Proton"):
continue
# Check for wine binary
wine_bin = proton_dir / "files" / "bin" / "wine"
if not wine_bin.exists() or not wine_bin.is_file():
logger.debug(f"Skipping {dir_name} - no wine binary found")
continue
# Parse version from directory name (e.g., "GE-Proton10-16")
version_match = re.match(r'GE-Proton(\d+)-(\d+)', dir_name)
if version_match:
major_ver = int(version_match.group(1))
minor_ver = int(version_match.group(2))
# Calculate priority: GE-Proton gets highest priority
# Priority format: 200 (base) + major*10 + minor (e.g., 200 + 100 + 16 = 316)
priority = 200 + (major_ver * 10) + minor_ver
found_versions.append({
'name': dir_name,
'path': proton_dir,
'wine_bin': wine_bin,
'priority': priority,
'major_version': major_ver,
'minor_version': minor_ver,
'type': 'GE-Proton'
})
logger.debug(f"Found {dir_name} at {proton_dir} (priority: {priority})")
else:
logger.debug(f"Skipping {dir_name} - unknown GE-Proton version format")
except Exception as e:
logger.warning(f"Error scanning {compat_path}: {e}")
# Sort by priority (highest first, so newest GE-Proton versions come first)
found_versions.sort(key=lambda x: x['priority'], reverse=True)
logger.info(f"Found {len(found_versions)} GE-Proton version(s)")
return found_versions
@staticmethod
def scan_valve_proton_versions() -> List[Dict[str, any]]:
"""
Scan for available Valve Proton versions with fallback priority.
Returns:
List of dicts with version info, sorted by priority (best first)
"""
logger.info("Scanning for available Valve Proton versions...")
found_versions = []
steam_libs = WineUtils.get_steam_library_paths()
if not steam_libs:
logger.warning("No Steam library paths found")
return []
# Priority order for Valve Proton versions
# Note: GE-Proton uses 200+ range, so Valve Proton gets 100+ range
preferred_versions = [
("Proton - Experimental", 150), # Higher priority than regular Valve Proton
("Proton 10.0", 140),
("Proton 9.0", 130),
("Proton 9.0 (Beta)", 125)
]
for steam_path in steam_libs:
logger.debug(f"Scanning Steam library: {steam_path}")
for version_name, priority in preferred_versions:
proton_path = steam_path / version_name
wine_bin = proton_path / "files" / "bin" / "wine"
if wine_bin.exists() and wine_bin.is_file():
found_versions.append({
'name': version_name,
'path': proton_path,
'wine_bin': wine_bin,
'priority': priority,
'type': 'Valve-Proton'
})
logger.debug(f"Found {version_name} at {proton_path}")
# Sort by priority (highest first)
found_versions.sort(key=lambda x: x['priority'], reverse=True)
# Remove duplicates while preserving order
unique_versions = []
seen_names = set()
for version in found_versions:
if version['name'] not in seen_names:
unique_versions.append(version)
seen_names.add(version['name'])
logger.info(f"Found {len(unique_versions)} unique Valve Proton version(s)")
return unique_versions
@staticmethod
def scan_all_proton_versions() -> List[Dict[str, any]]:
"""
Scan for all available Proton versions (GE-Proton + Valve Proton) with unified priority.
Priority Chain (highest to lowest):
1. GE-Proton10-16+ (priority 316+)
2. GE-Proton10-* (priority 200+)
3. Proton - Experimental (priority 150)
4. Proton 10.0 (priority 140)
5. Proton 9.0 (priority 130)
6. Proton 9.0 (Beta) (priority 125)
Returns:
List of dicts with version info, sorted by priority (best first)
"""
logger.info("Scanning for all available Proton versions...")
all_versions = []
# Scan GE-Proton versions (highest priority)
ge_versions = WineUtils.scan_ge_proton_versions()
all_versions.extend(ge_versions)
# Scan Valve Proton versions
valve_versions = WineUtils.scan_valve_proton_versions()
all_versions.extend(valve_versions)
# Sort by priority (highest first)
all_versions.sort(key=lambda x: x['priority'], reverse=True)
# Remove duplicates while preserving order
unique_versions = []
seen_names = set()
for version in all_versions:
if version['name'] not in seen_names:
unique_versions.append(version)
seen_names.add(version['name'])
if unique_versions:
logger.info(f"Found {len(unique_versions)} total Proton version(s)")
logger.info(f"Best available: {unique_versions[0]['name']} ({unique_versions[0]['type']})")
else:
logger.warning("No Proton versions found")
return unique_versions
@staticmethod
def select_best_proton() -> Optional[Dict[str, any]]:
"""
Select the best available Proton version (GE-Proton or Valve Proton) using unified precedence.
Returns:
Dict with version info for the best Proton, or None if none found
"""
available_versions = WineUtils.scan_all_proton_versions()
if not available_versions:
logger.warning("No compatible Proton versions found")
return None
# Return the highest priority version (first in sorted list)
best_version = available_versions[0]
logger.info(f"Selected best Proton version: {best_version['name']} ({best_version['type']})")
return best_version
@staticmethod
def select_best_valve_proton() -> Optional[Dict[str, any]]:
"""
Select the best available Valve Proton version using fallback precedence.
Note: This method is kept for backward compatibility. Consider using select_best_proton() instead.
Returns:
Dict with version info for the best Proton, or None if none found
"""
available_versions = WineUtils.scan_valve_proton_versions()
if not available_versions:
logger.warning("No compatible Valve Proton versions found")
return None
# Return the highest priority version (first in sorted list)
best_version = available_versions[0]
logger.info(f"Selected Valve Proton version: {best_version['name']}")
return best_version
@staticmethod
def check_proton_requirements() -> Tuple[bool, str, Optional[Dict[str, any]]]:
"""
Check if compatible Proton version is available for workflows.
Returns:
tuple: (requirements_met, status_message, proton_info)
- requirements_met: True if compatible Proton found
- status_message: Human-readable status for display to user
- proton_info: Dict with Proton details if found, None otherwise
"""
logger.info("Checking Proton requirements for workflow...")
# Scan for available Proton versions (includes GE-Proton + Valve Proton)
best_proton = WineUtils.select_best_proton()
if best_proton:
# Compatible Proton found
proton_type = best_proton.get('type', 'Unknown')
status_msg = f"✓ Using {best_proton['name']} ({proton_type}) for this workflow"
logger.info(f"Proton requirements satisfied: {best_proton['name']} ({proton_type})")
return True, status_msg, best_proton
else:
# No compatible Proton found
status_msg = "✗ No compatible Proton version found (GE-Proton 10+, Proton 9+, 10, or Experimental required)"
logger.warning("Proton requirements not met - no compatible version found")
return False, status_msg, None

View File

@@ -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"
@@ -2499,8 +2540,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 +2642,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 +2660,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:
@@ -2718,26 +2764,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

View File

@@ -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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -7,7 +7,7 @@
"targets": { "targets": {
".NETCoreApp,Version=v8.0": {}, ".NETCoreApp,Version=v8.0": {},
".NETCoreApp,Version=v8.0/linux-x64": { ".NETCoreApp,Version=v8.0/linux-x64": {
"jackify-engine/0.3.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.

View File

@@ -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
@@ -298,6 +302,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 +478,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,6 +600,33 @@ 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)
# Save Proton selection - resolve "auto" to actual path
selected_proton_path = self.proton_dropdown.currentData()
if selected_proton_path == "auto":
# Resolve "auto" to actual best Proton path using unified detection
try:
from jackify.backend.handlers.wine_utils import WineUtils
best_proton = WineUtils.select_best_proton()
if best_proton:
resolved_path = str(best_proton['path'])
resolved_version = best_proton['name']
else:
resolved_path = "auto"
resolved_version = "auto"
except:
resolved_path = "auto"
resolved_version = "auto"
else:
# User selected specific Proton version
resolved_path = selected_proton_path
# Extract version from dropdown text
resolved_version = self.proton_dropdown.currentText()
self.config_handler.set("proton_path", resolved_path)
self.config_handler.set("proton_version", resolved_version)
self.config_handler.save_config() self.config_handler.save_config()
# Refresh cached paths in GUI screens if Jackify directory changed # Refresh cached paths in GUI screens if Jackify directory changed

View File

@@ -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"""
@@ -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
) )

View File

@@ -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
@@ -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}")
@@ -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,7 +2318,7 @@ 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', '2560x1600'),
@@ -2381,7 +2416,7 @@ 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', '2560x1600'),
@@ -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()

View File

@@ -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>

View 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