Files
Jackify/jackify/backend/services/automated_prefix_game_utils.py
2026-02-07 18:26:54 +00:00

273 lines
11 KiB
Python

#!/usr/bin/env python3
"""
Game utilities mixin for AutomatedPrefixService.
Handles game-specific operations:
- Launch options generation
- Game detection
- User directory creation
- Proton version preferences
"""
import os
import logging
from pathlib import Path
from typing import Optional, List
logger = logging.getLogger(__name__)
class GameUtilsMixin:
"""Mixin for game-related utility operations"""
def _generate_special_game_launch_options(self, special_game_type: str, modlist_install_dir: str) -> Optional[str]:
"""
Generate launch options for FNV/Enderal games that require vanilla compatdata.
Args:
special_game_type: "fnv" or "enderal"
modlist_install_dir: Directory where the modlist is installed
Returns:
Complete launch options string with STEAM_COMPAT_DATA_PATH, or None if failed
"""
if not special_game_type or special_game_type not in ["fnv", "enderal"]:
return None
logger.info(f"Generating {special_game_type.upper()} launch options")
# Map game types to AppIDs
appid_map = {"fnv": "22380", "enderal": "976620"}
appid = appid_map[special_game_type]
# Find vanilla game compatdata
from ..handlers.path_handler import PathHandler
compatdata_path = PathHandler.find_compat_data(appid)
if not compatdata_path:
logger.error(f"Could not find vanilla {special_game_type.upper()} compatdata directory (AppID {appid})")
return None
# Create STEAM_COMPAT_DATA_PATH string
compat_data_str = f'STEAM_COMPAT_DATA_PATH="{compatdata_path}"'
# Generate STEAM_COMPAT_MOUNTS if multiple libraries exist
compat_mounts_str = ""
try:
all_libs = PathHandler.get_all_steam_library_paths()
main_steam_lib_path_obj = PathHandler.find_steam_library()
if main_steam_lib_path_obj and main_steam_lib_path_obj.name == "common":
main_steam_lib_path = main_steam_lib_path_obj.parent.parent
else:
main_steam_lib_path = main_steam_lib_path_obj
mount_paths = []
if main_steam_lib_path:
main_resolved = main_steam_lib_path.resolve()
for lib_path in all_libs:
if lib_path.resolve() != main_resolved:
mount_paths.append(str(lib_path.resolve()))
if mount_paths:
mount_paths_str = ':'.join(mount_paths)
compat_mounts_str = f'STEAM_COMPAT_MOUNTS="{mount_paths_str}"'
logger.info(f"Added STEAM_COMPAT_MOUNTS for {special_game_type.upper()}")
except Exception as e:
logger.warning(f"Error generating STEAM_COMPAT_MOUNTS for {special_game_type}: {e}")
# Combine all launch options
launch_options = f"{compat_mounts_str} {compat_data_str} %command%".strip()
launch_options = ' '.join(launch_options.split()) # Clean up spacing
logger.info(f"Generated {special_game_type.upper()} launch options: {launch_options}")
return launch_options
def _find_steam_game(self, app_id: str, common_names: list) -> Optional[str]:
"""Find a Steam game installation path by AppID and common names"""
import os
from pathlib import Path
# Get Steam libraries from libraryfolders.vdf - check multiple possible locations
possible_config_paths = [
Path.home() / ".steam/steam/config/libraryfolders.vdf",
Path.home() / ".local/share/Steam/config/libraryfolders.vdf",
Path.home() / ".var/app/com.valvesoftware.Steam/.local/share/Steam/config/libraryfolders.vdf" # Flatpak
]
steam_config_path = None
for path in possible_config_paths:
if path.exists():
steam_config_path = path
break
if not steam_config_path:
return None
steam_libraries = []
try:
with open(steam_config_path, 'r') as f:
content = f.read()
# Parse library paths from VDF
import re
library_matches = re.findall(r'"path"\s+"([^"]+)"', content)
steam_libraries = [Path(path) / "steamapps" / "common" for path in library_matches]
except Exception as e:
logger.warning(f"Failed to parse Steam library folders: {e}")
return None
# Search for game in each library
for library_path in steam_libraries:
if not library_path.exists():
continue
# Check manifest file first (more reliable)
manifest_path = library_path.parent / "appmanifest_{}.acf".format(app_id)
if manifest_path.exists():
try:
with open(manifest_path, 'r') as f:
content = f.read()
install_dir_match = re.search(r'"installdir"\s+"([^"]+)"', content)
if install_dir_match:
game_path = library_path / install_dir_match.group(1)
if game_path.exists():
return str(game_path)
except Exception:
pass
# Fallback: check common folder names
for name in common_names:
game_path = library_path / name
if game_path.exists():
return str(game_path)
return None
def _create_game_user_directories(self, modlist_compatdata_path: str, special_game_type: str):
"""
Pre-create game-specific user directories to prevent first-launch issues.
Creates both My Documents/My Games and AppData/Local directories for the game.
This prevents issues where games fail to create these on first launch under Proton.
"""
# Map game types to their directory names
game_dir_names = {
"skyrim": "Skyrim Special Edition",
"fnv": "FalloutNV",
"fo4": "Fallout4",
"oblivion": "Oblivion",
"oblivion_remastered": "Oblivion Remastered",
"enderal": "Enderal Special Edition",
"starfield": "Starfield"
}
# Get the directory name for this game type
game_dir_name = game_dir_names.get(special_game_type)
if not game_dir_name:
logger.debug(f"No user directory mapping for game type: {special_game_type}")
return
base_path = os.path.join(modlist_compatdata_path, "pfx", "drive_c", "users", "steamuser")
directories_to_create = [
os.path.join(base_path, "Documents", "My Games", game_dir_name),
os.path.join(base_path, "AppData", "Local", game_dir_name)
]
created_count = 0
for directory in directories_to_create:
try:
os.makedirs(directory, exist_ok=True)
logger.info(f"Created user directory: {directory}")
created_count += 1
except Exception as e:
logger.warning(f"Failed to create directory {directory}: {e}")
if created_count > 0:
logger.info(f"Created {created_count} user directories for {game_dir_name}")
def _get_lorerim_preferred_proton(self):
"""Get Lorerim's preferred Proton 9 version with specific priority order"""
try:
from jackify.backend.handlers.wine_utils import WineUtils
# Get all available Proton versions
available_versions = WineUtils.scan_all_proton_versions()
if not available_versions:
logger.warning("No Proton versions found for Lorerim override")
return None
# Priority order for Lorerim:
# 1. GEProton9-27 (specific version)
# 2. Other GEProton-9 versions (latest first)
# 3. Valve Proton 9 (any version)
preferred_candidates = []
for version in available_versions:
version_name = version['name']
# Priority 1: GEProton9-27 specifically
if version_name == 'GE-Proton9-27':
logger.info(f"Lorerim: Found preferred GE-Proton9-27")
return version_name
# Priority 2: Other GE-Proton 9 versions
elif version_name.startswith('GE-Proton9-'):
preferred_candidates.append(('ge_proton_9', version_name, version))
# Priority 3: Valve Proton 9
elif 'Proton 9' in version_name:
preferred_candidates.append(('valve_proton_9', version_name, version))
# Return best candidate if any found
if preferred_candidates:
# Sort by priority (GE-Proton first, then by name for latest)
preferred_candidates.sort(key=lambda x: (x[0], x[1]), reverse=True)
best_candidate = preferred_candidates[0]
logger.info(f"Lorerim: Selected {best_candidate[1]} as best Proton 9 option")
return best_candidate[1]
logger.warning("Lorerim: No suitable Proton 9 versions found, will use user settings")
return None
except Exception as e:
logger.error(f"Error detecting Lorerim Proton preference: {e}")
return None
def _store_proton_override_notification(self, modlist_name: str, proton_version: str):
"""Store Proton override information for end-of-install notification"""
try:
# Store override info for later display
if not hasattr(self, '_proton_overrides'):
self._proton_overrides = []
self._proton_overrides.append({
'modlist': modlist_name,
'proton_version': proton_version,
'reason': f'{modlist_name} requires Proton 9 for optimal compatibility'
})
logger.debug(f"Stored Proton override notification: {modlist_name}{proton_version}")
except Exception as e:
logger.error(f"Failed to store Proton override notification: {e}")
def _show_proton_override_notification(self, progress_callback=None):
"""Display any Proton override notifications to the user"""
try:
if hasattr(self, '_proton_overrides') and self._proton_overrides:
for override in self._proton_overrides:
notification_msg = f"PROTON OVERRIDE: {override['modlist']} configured to use {override['proton_version']} for optimal compatibility"
if progress_callback:
progress_callback("")
progress_callback(f"{self._get_progress_timestamp()} {notification_msg}")
logger.info(notification_msg)
# Clear notifications after display
self._proton_overrides = []
except Exception as e:
logger.error(f"Failed to show Proton override notification: {e}")