mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-01-17 19:47:00 +01:00
Sync from development - prepare for v0.1.4
This commit is contained in:
@@ -23,6 +23,44 @@ from jackify.backend.handlers.config_handler import ConfigHandler
|
||||
|
||||
# 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
|
||||
READLINE_AVAILABLE = False
|
||||
try:
|
||||
@@ -1248,13 +1286,16 @@ class ModlistInstallCLI:
|
||||
from jackify.backend.services.native_steam_service import 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(
|
||||
app_name=config_context['name'],
|
||||
exe_path=config_context['mo2_exe_path'],
|
||||
start_dir=os.path.dirname(config_context['mo2_exe_path']),
|
||||
launch_options="%command%",
|
||||
tags=["Jackify"],
|
||||
proton_version="proton_experimental"
|
||||
proton_version=proton_version
|
||||
)
|
||||
|
||||
if not success or not app_id:
|
||||
|
||||
@@ -47,8 +47,10 @@ class ConfigHandler:
|
||||
# If steam_path is not set, detect it
|
||||
if not self.settings["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 not self.settings.get("jackify_data_dir"):
|
||||
@@ -494,4 +496,28 @@ class ConfigHandler:
|
||||
logger.error(f"Error saving modlist downloads base directory: {e}")
|
||||
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"
|
||||
|
||||
|
||||
@@ -1163,7 +1163,7 @@ class ModlistHandler:
|
||||
# Determine game type
|
||||
game = (game_var_full or modlist_name or "").lower().replace(" ", "")
|
||||
# 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"]
|
||||
elif "falloutnewvegas" in game or "fnv" in game or "oblivion" in game:
|
||||
extras += ["d3dx9_43", "d3dx9"]
|
||||
@@ -1238,6 +1238,12 @@ class ModlistHandler:
|
||||
# Check ModOrganizer.ini for indicators (nvse/enderal) as an early, robust signal
|
||||
try:
|
||||
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():
|
||||
try:
|
||||
content = mo2_ini.read_text(errors='ignore').lower()
|
||||
|
||||
@@ -988,8 +988,8 @@ class ShortcutHandler:
|
||||
shortcuts_data = VDFHandler.load(shortcuts_vdf_path, binary=True)
|
||||
if shortcuts_data and 'shortcuts' in shortcuts_data:
|
||||
for idx, shortcut in shortcuts_data['shortcuts'].items():
|
||||
app_name = shortcut.get('AppName', '').strip()
|
||||
exe = shortcut.get('Exe', '').strip('"').strip()
|
||||
app_name = shortcut.get('AppName', shortcut.get('appname', '')).strip()
|
||||
exe = shortcut.get('Exe', shortcut.get('exe', '')).strip('"').strip()
|
||||
vdf_shortcuts.append((app_name, exe, idx))
|
||||
except Exception as 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}")
|
||||
continue
|
||||
|
||||
app_name = shortcut.get('AppName')
|
||||
exe_path = shortcut.get('Exe', '').strip('"')
|
||||
start_dir = shortcut.get('StartDir', '').strip('"')
|
||||
app_name = shortcut.get('AppName', shortcut.get('appname'))
|
||||
exe_path = shortcut.get('Exe', shortcut.get('exe', '')).strip('"')
|
||||
start_dir = shortcut.get('StartDir', shortcut.get('startdir', '')).strip('"')
|
||||
|
||||
# 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:
|
||||
|
||||
@@ -132,7 +132,8 @@ class WabbajackParser:
|
||||
'falloutnv': 'Fallout New Vegas',
|
||||
'oblivion': 'Oblivion',
|
||||
'starfield': 'Starfield',
|
||||
'oblivion_remastered': 'Oblivion Remastered'
|
||||
'oblivion_remastered': 'Oblivion Remastered',
|
||||
'enderal': 'Enderal'
|
||||
}
|
||||
return [display_names.get(game, game) for game in self.supported_games]
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import shutil
|
||||
import time
|
||||
from pathlib import Path
|
||||
import glob
|
||||
from typing import Optional, Tuple
|
||||
from typing import Optional, Tuple, List, Dict
|
||||
from .subprocess_utils import get_clean_subprocess_env
|
||||
|
||||
# Initialize logger
|
||||
@@ -643,7 +643,20 @@ class WineUtils:
|
||||
wine_bin = subdir / "files/bin/wine"
|
||||
if wine_bin.is_file():
|
||||
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:
|
||||
wine_bin = base_path / "Proton - Experimental" / "files/bin/wine"
|
||||
if wine_bin.is_file():
|
||||
@@ -698,4 +711,276 @@ class WineUtils:
|
||||
proton_path = str(Path(wine_bin).parent.parent)
|
||||
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
|
||||
@@ -38,6 +38,44 @@ class AutomatedPrefixService:
|
||||
"""Get consistent progress timestamp"""
|
||||
from jackify.shared.timing import get_timestamp
|
||||
return get_timestamp()
|
||||
|
||||
def _get_user_proton_version(self):
|
||||
"""Get user's preferred Proton version from config, with fallback to auto-detection"""
|
||||
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,
|
||||
@@ -87,6 +125,9 @@ class AutomatedPrefixService:
|
||||
logger.warning(f"Could not generate STEAM_COMPAT_MOUNTS, using default: {e}")
|
||||
launch_options = "%command%"
|
||||
|
||||
# Get user's preferred Proton version
|
||||
proton_version = self._get_user_proton_version()
|
||||
|
||||
# Create shortcut with Proton using native service
|
||||
success, app_id = steam_service.create_shortcut_with_proton(
|
||||
app_name=shortcut_name,
|
||||
@@ -94,7 +135,7 @@ class AutomatedPrefixService:
|
||||
start_dir=modlist_install_dir,
|
||||
launch_options=launch_options,
|
||||
tags=["Jackify"],
|
||||
proton_version="proton_experimental"
|
||||
proton_version=proton_version
|
||||
)
|
||||
|
||||
if success and app_id:
|
||||
@@ -292,13 +333,13 @@ class AutomatedPrefixService:
|
||||
logger.error(f"Steam userdata directory not found: {userdata_dir}")
|
||||
return None
|
||||
|
||||
# Find the first user directory (most systems have only one user)
|
||||
user_dirs = [d for d in userdata_dir.iterdir() if d.is_dir() and d.name.isdigit()]
|
||||
# Find user directories (excluding user 0 which is a system account)
|
||||
user_dirs = [d for d in userdata_dir.iterdir() if d.is_dir() and d.name.isdigit() and d.name != "0"]
|
||||
if not user_dirs:
|
||||
logger.error("No Steam user directories found in userdata")
|
||||
logger.error("No valid Steam user directories found in userdata (user 0 is not valid)")
|
||||
return None
|
||||
|
||||
# Use the first user directory found
|
||||
# Use the first valid user directory found
|
||||
user_dir = user_dirs[0]
|
||||
shortcuts_path = user_dir / "config" / "shortcuts.vdf"
|
||||
|
||||
@@ -2499,8 +2540,8 @@ echo Prefix creation complete.
|
||||
# Try the standard Steam userdata path
|
||||
steam_userdata_path = Path.home() / ".steam" / "steam" / "userdata"
|
||||
if steam_userdata_path.exists():
|
||||
# Find the first user directory (usually only one on Steam Deck)
|
||||
user_dirs = [d for d in steam_userdata_path.iterdir() if d.is_dir() and d.name.isdigit()]
|
||||
# Find user directories (excluding user 0 which is a system account)
|
||||
user_dirs = [d for d in steam_userdata_path.iterdir() if d.is_dir() and d.name.isdigit() and d.name != "0"]
|
||||
if user_dirs:
|
||||
localconfig_path = user_dirs[0] / "config" / "localconfig.vdf"
|
||||
if localconfig_path.exists():
|
||||
@@ -2601,8 +2642,11 @@ echo Prefix creation complete.
|
||||
env = os.environ.copy()
|
||||
env['STEAM_COMPAT_CLIENT_INSTALL_PATH'] = str(steam_root)
|
||||
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['WAYLAND_DISPLAY'] = ''
|
||||
env['WINEDEBUG'] = '-all'
|
||||
env['WINEDLLOVERRIDES'] = 'msdia80.dll=n;conhost.exe=d;cmd.exe=d'
|
||||
|
||||
# Create the compatdata directory
|
||||
compat_dir = compatdata_dir / str(abs(appid))
|
||||
@@ -2616,7 +2660,9 @@ echo Prefix creation complete.
|
||||
cmd = [str(proton_path), 'run', 'wineboot', '-u']
|
||||
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}")
|
||||
|
||||
if result.stdout:
|
||||
@@ -2718,26 +2764,39 @@ echo Prefix creation complete.
|
||||
|
||||
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:
|
||||
appid: The AppID to check
|
||||
|
||||
|
||||
Returns:
|
||||
True if compatibility tool is set, False otherwise
|
||||
True if compatibility tool is correctly set, False otherwise
|
||||
"""
|
||||
try:
|
||||
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()
|
||||
|
||||
|
||||
# Check if AppID exists and has a Proton version set
|
||||
if f'"{appid}"' in content:
|
||||
logger.info(" Compatibility tool persists")
|
||||
return True
|
||||
# Get the expected Proton version
|
||||
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:
|
||||
logger.warning("Compatibility tool not found")
|
||||
return False
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error verifying compatibility tool: {e}")
|
||||
return False
|
||||
|
||||
@@ -40,13 +40,13 @@ class NativeSteamService:
|
||||
logger.error("Steam userdata directory not found")
|
||||
return False
|
||||
|
||||
# Find the first user directory (usually there's only one)
|
||||
user_dirs = [d for d in self.userdata_path.iterdir() if d.is_dir() and d.name.isdigit()]
|
||||
# Find user directories (excluding user 0 which is a system account)
|
||||
user_dirs = [d for d in self.userdata_path.iterdir() if d.is_dir() and d.name.isdigit() and d.name != "0"]
|
||||
if not user_dirs:
|
||||
logger.error("No Steam user directories found")
|
||||
logger.error("No valid Steam user directories found (user 0 is not valid for shortcuts)")
|
||||
return False
|
||||
|
||||
# Use the first user directory
|
||||
# Use the first valid user directory
|
||||
user_dir = user_dirs[0]
|
||||
self.user_id = user_dir.name
|
||||
self.user_config_path = user_dir / "config"
|
||||
@@ -327,17 +327,27 @@ class NativeSteamService:
|
||||
logger.error(f"Error setting Proton version: {e}")
|
||||
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,
|
||||
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.
|
||||
|
||||
|
||||
This is the main method that replaces STL entirely.
|
||||
|
||||
|
||||
Returns:
|
||||
(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}'")
|
||||
|
||||
# Step 1: Create the shortcut
|
||||
|
||||
Reference in New Issue
Block a user