diff options
Diffstat (limited to 'backend/steam.py')
| -rw-r--r-- | backend/steam.py | 187 |
1 files changed, 0 insertions, 187 deletions
diff --git a/backend/steam.py b/backend/steam.py deleted file mode 100644 index c2e90b0..0000000 --- a/backend/steam.py +++ /dev/null @@ -1,187 +0,0 @@ -"""Steam library discovery and conservative executable selection.""" - -import re -import subprocess -from pathlib import Path -from typing import Callable - - -Logger = Callable[[str], None] - -BAD_EXE_SUBSTRINGS = [ - "crashreport", - "crashreportclient", - "eac", - "easyanticheat", - "beclient", - "eosbootstrap", - "benchmark", - "uninstall", - "setup", - "launcher", - "updater", - "bootstrap", - "_redist", - "prereq", -] - - -class SteamLibrary: - def __init__(self, home: Path, logger: Logger): - self.home = home - self.logger = logger - - def steam_common_path(self) -> Path: - return self.home / ".local" / "share" / "Steam" / "steamapps" / "common" - - def _steam_root_candidates(self) -> list[Path]: - candidates = [ - self.home / ".local" / "share" / "Steam", - self.home / ".steam" / "steam", - self.home / ".steam" / "root", - self.home / ".var" / "app" / "com.valvesoftware.Steam" / "home" / ".local" / "share" / "Steam", - self.home / ".var" / "app" / "com.valvesoftware.Steam" / "home" / ".steam" / "steam", - ] - unique: list[Path] = [] - seen: set[str] = set() - for candidate in candidates: - key = str(candidate) - if key not in seen: - unique.append(candidate) - seen.add(key) - return unique - - def _steam_library_paths(self) -> list[Path]: - library_paths: list[Path] = [] - seen: set[str] = set() - for steam_root in self._steam_root_candidates(): - if steam_root.exists() and str(steam_root) not in seen: - library_paths.append(steam_root) - seen.add(str(steam_root)) - library_file = steam_root / "steamapps" / "libraryfolders.vdf" - if not library_file.exists(): - continue - try: - for line in library_file.read_text(encoding="utf-8", errors="replace").splitlines(): - if '"path"' not in line: - continue - path = line.split('"path"', 1)[1].strip().strip('"').replace("\\\\", "/") - candidate = Path(path) - if str(candidate) not in seen: - library_paths.append(candidate) - seen.add(str(candidate)) - except OSError as exc: - self.logger(f"[Framegen] failed to parse libraryfolders: {library_file}: {exc}") - return library_paths - - def find_installed_games(self, appid: str | None = None) -> list[dict]: - games: list[dict] = [] - for library_path in self._steam_library_paths(): - steamapps_path = library_path / "steamapps" - if not steamapps_path.exists(): - continue - for appmanifest in steamapps_path.glob("appmanifest_*.acf"): - game_info: dict = { - "appid": "", - "name": "", - "library_path": str(library_path), - "install_path": "", - } - install_dir = "" - try: - for line in appmanifest.read_text(encoding="utf-8", errors="replace").splitlines(): - if '"appid"' in line: - game_info["appid"] = line.split('"appid"', 1)[1].strip().strip('"') - elif '"name"' in line: - game_info["name"] = line.split('"name"', 1)[1].strip().strip('"') - elif '"installdir"' in line: - install_dir = line.split('"installdir"', 1)[1].strip().strip('"') - except OSError as exc: - self.logger(f"[Framegen] skipping manifest {appmanifest}: {exc}") - continue - if not game_info["appid"] or not game_info["name"]: - continue - if "Proton" in game_info["name"] or "Steam Linux Runtime" in game_info["name"]: - continue - game_info["install_path"] = str(steamapps_path / "common" / install_dir) if install_dir else str(Path()) - if appid is None or str(game_info["appid"]) == str(appid): - games.append(game_info) - deduped = {str(game["appid"]): game for game in games} - return sorted(deduped.values(), key=lambda game: game["name"].lower()) - - def game_record(self, appid: str) -> dict | None: - matches = self.find_installed_games(appid) - return matches[0] if matches else None - - @staticmethod - def normalized_path_string(value: str) -> str: - normalized = value.lower().replace("\\", "/") - normalized = normalized.replace("z:/", "/") - return normalized.replace("//", "/") - - def candidate_executables(self, install_root: Path) -> list[Path]: - if not install_root.exists(): - return [] - try: - return [exe for exe in install_root.rglob("*.exe") if exe.is_file()] - except OSError as exc: - self.logger(f"[Framegen] exe scan failed for {install_root}: {exc}") - return [] - - def _exe_score(self, exe: Path, install_root: Path, game_name: str) -> int: - normalized = self.normalized_path_string(str(exe)) - name = exe.name.lower() - score = 0 - if normalized.endswith("-win64-shipping.exe"): - score += 300 - if "shipping.exe" in name: - score += 220 - if "/binaries/win64/" in normalized: - score += 200 - if "/win64/" in normalized: - score += 80 - if exe.parent == install_root: - score += 20 - sanitized_game = re.sub(r"[^a-z0-9]", "", game_name.lower()) - sanitized_name = re.sub(r"[^a-z0-9]", "", exe.stem.lower()) - sanitized_root = re.sub(r"[^a-z0-9]", "", install_root.name.lower()) - if sanitized_game and sanitized_game in sanitized_name: - score += 120 - if sanitized_root and sanitized_root in sanitized_name: - score += 90 - for bad in BAD_EXE_SUBSTRINGS: - if bad in normalized: - score -= 200 - return score - len(exe.parts) - - def best_running_executable(self, candidates: list[Path]) -> Path | None: - if not candidates: - return None - try: - result = subprocess.run(["ps", "-eo", "args="], capture_output=True, text=True, check=False) - process_lines = result.stdout.splitlines() - except OSError as exc: - self.logger(f"[Framegen] running exe scan failed: {exc}") - return None - normalized_candidates = [(exe, self.normalized_path_string(str(exe))) for exe in candidates] - matches: list[tuple[int, Path]] = [] - for line in process_lines: - normalized_line = self.normalized_path_string(line) - for exe, normalized_exe in normalized_candidates: - if normalized_exe in normalized_line: - matches.append((len(normalized_exe), exe)) - return max(matches, key=lambda item: item[0])[1] if matches else None - - def guess_patch_target(self, game_info: dict) -> tuple[Path, Path | None]: - install_root = Path(game_info["install_path"]) - candidates = self.candidate_executables(install_root) - if not candidates: - return install_root, None - running_exe = self.best_running_executable(candidates) - if running_exe: - return running_exe.parent, running_exe - best = max(candidates, key=lambda exe: self._exe_score(exe, install_root, game_info["name"])) - return best.parent, best - - def is_game_running(self, game_info: dict) -> bool: - return self.best_running_executable(self.candidate_executables(Path(game_info["install_path"]))) is not None |
