#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Game path and compatdata mixin for PathHandler. Extracted from path_handler for file-size and domain separation. """ import os import re import logging from pathlib import Path from typing import Optional, List, Dict logger = logging.getLogger(__name__) class PathHandlerGameMixin: """Mixin providing game install path and compatdata discovery.""" @classmethod def find_compat_data(cls, appid: str) -> Optional[Path]: """Find the compatdata directory for a given AppID.""" if not appid: logger.error(f"Invalid AppID provided for compatdata search: {appid}") return None appid_clean = appid.lstrip('-') if not appid_clean.isdigit(): logger.error(f"Invalid AppID provided for compatdata search: {appid}") return None logger.debug(f"Searching for compatdata directory for AppID: {appid}") library_paths = cls.get_all_steam_library_paths() if library_paths: logger.debug(f"Checking compatdata in {len(library_paths)} Steam libraries") for library_path in library_paths: compatdata_base = library_path / "steamapps" / "compatdata" if not compatdata_base.is_dir(): logger.debug(f"Compatdata directory does not exist: {compatdata_base}") continue potential_path = compatdata_base / appid if potential_path.is_dir(): logger.info(f"Found compatdata directory: {potential_path}") return potential_path logger.debug(f"Compatdata for AppID {appid} not found in {compatdata_base}") is_flatpak_steam = any('.var/app/com.valvesoftware.Steam' in str(lib) for lib in library_paths) if library_paths else False if not library_paths or is_flatpak_steam: logger.debug("Checking fallback compatdata locations...") if is_flatpak_steam: fallback_locations = [ Path.home() / ".var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/compatdata", Path.home() / ".var/app/com.valvesoftware.Steam/data/Steam/steamapps/compatdata", ] else: fallback_locations = [ Path.home() / ".local/share/Steam/steamapps/compatdata", Path.home() / ".steam/steam/steamapps/compatdata", ] for compatdata_base in fallback_locations: if compatdata_base.is_dir(): potential_path = compatdata_base / appid if potential_path.is_dir(): logger.warning(f"Found compatdata directory in fallback location: {potential_path}") return potential_path logger.warning(f"Compatdata directory for AppID {appid} not found in any Steam library or fallback location.") return None @staticmethod def detect_stock_game_path(game_type: str, steam_library: Path) -> Optional[Path]: """Detect the stock game path for a given game type and Steam library.""" try: game_app_ids = { 'skyrim': '489830', 'fallout4': '377160', 'fnv': '22380', 'oblivion': '22330' } if game_type not in game_app_ids: return None app_id = game_app_ids[game_type] game_path = steam_library / 'steamapps' / 'common' possible_names = { 'skyrim': ['Skyrim Special Edition', 'Skyrim'], 'fallout4': ['Fallout 4'], 'fnv': ['Fallout New Vegas', 'FalloutNV'], 'oblivion': ['Oblivion'] } if game_type not in possible_names: return None for name in possible_names[game_type]: potential_path = game_path / name if potential_path.exists(): return potential_path return None except Exception as e: logging.error(f"Error detecting stock game path: {e}") return None @classmethod def find_game_install_paths(cls, target_appids: Dict[str, str]) -> Dict[str, Path]: """Find installation paths for multiple specified games using Steam app IDs.""" library_paths = cls.get_all_steam_library_paths() if not library_paths: logger.warning("Failed to find any Steam library paths") return {} results = {} for library_path in library_paths: common_dir = library_path / "steamapps" / "common" if not common_dir.is_dir(): logger.debug(f"No 'steamapps/common' directory in library: {library_path}") continue try: game_dirs = [d for d in common_dir.iterdir() if d.is_dir()] except (PermissionError, OSError) as e: logger.warning(f"Cannot access directory {common_dir}: {e}") continue for game_name, app_id in target_appids.items(): if game_name in results: continue appmanifest_path = library_path / "steamapps" / f"appmanifest_{app_id}.acf" if appmanifest_path.is_file(): try: with open(appmanifest_path, 'r', encoding='utf-8') as f: content = f.read() match = re.search(r'"installdir"\s+"([^"]+)"', content) if match: install_dir_name = match.group(1) install_path = common_dir / install_dir_name if install_path.is_dir(): results[game_name] = install_path logger.info(f"Found {game_name} at {install_path}") continue except Exception as e: logger.warning(f"Error reading appmanifest for {game_name}: {e}") return results @classmethod def find_vanilla_game_paths(cls, game_names=None) -> Dict[str, Path]: """For each known game, iterate all Steam libraries and look for the canonical game directory in steamapps/common.""" GAME_DIR_NAMES = { "Skyrim Special Edition": ["Skyrim Special Edition"], "Fallout 4": ["Fallout 4"], "Fallout New Vegas": ["Fallout New Vegas"], "Oblivion": ["Oblivion"], "Fallout 3": ["Fallout 3", "Fallout 3 goty"] } if game_names is None: game_names = list(GAME_DIR_NAMES.keys()) all_steam_libraries = cls.get_all_steam_library_paths() logger.info(f"[DEBUG] Detected Steam libraries: {all_steam_libraries}") found_games = {} for game in game_names: possible_names = GAME_DIR_NAMES.get(game, [game]) for lib in all_steam_libraries: for name in possible_names: candidate = lib / "steamapps" / "common" / name logger.info(f"[DEBUG] Checking for vanilla game directory: {candidate}") if candidate.is_dir(): found_games[game] = candidate logger.info(f"Found vanilla game directory for {game}: {candidate}") break if game in found_games: break return found_games def _detect_stock_game_path(self) -> bool: """Detects common Stock Game or Game Root directories within the modlist path. Expects self.logger, self.modlist_dir, self.stock_game_path.""" self.logger.info("Step 7a: Detecting Stock Game/Game Root directory...") if not self.modlist_dir: self.logger.error("Modlist directory not set, cannot detect stock game path.") return False modlist_path = Path(self.modlist_dir) preferred_order = [ "Stock Game", "StockGame", "STOCK GAME", "Skyrim Stock", "Stock Game Folder", "Stock Folder", Path("root/Skyrim Special Edition"), "Game Root" ] found_path = None for name in preferred_order: potential_path = modlist_path / name if potential_path.is_dir(): found_path = str(potential_path) self.logger.info(f"Found potential stock game directory: {found_path}") break if found_path: self.stock_game_path = found_path return True self.stock_game_path = None searched = [str(modlist_path / n) for n in preferred_order] self.logger.info(f"No common Stock Game/Game Root directory found (searched: {searched}). Will assume vanilla game path is needed for some operations.") return True