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

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

View File

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

View File

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

View File

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

View File

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