import decky import os import re import shutil import subprocess from configparser import ConfigParser from pathlib import Path # Toggle to enable overwriting the upscaler DLL from the static remote binary. UPSCALER_OVERWRITE_ENABLED = True BUNDLE_DIRNAME = "fgmod" MANAGED_DIRNAME = "optiscaler-managed" MANIFEST_FILENAME = "manifest.env" SUPPORTED_PROXIES = [ "dxgi", "winmm", "dbghelp", "version", "wininet", "winhttp", "d3d12", ] SUPPORT_FILES = [ "libxess.dll", "libxess_dx11.dll", "libxess_fg.dll", "libxell.dll", "amd_fidelityfx_dx12.dll", "amd_fidelityfx_framegeneration_dx12.dll", "amd_fidelityfx_upscaler_dx12.dll", "amd_fidelityfx_vk.dll", "nvngx.dll", "dlssg_to_fsr3_amd_is_better.dll", "fakenvapi.dll", "fakenvapi.ini", ] REQUIRED_BUNDLE_FILES = [ "OptiScaler.dll", "OptiScaler.ini", *SUPPORT_FILES, "fgmod", "fgmod-uninstaller.sh", "update-optiscaler-config.py", ] LEGACY_FILES = [ "dlssg_to_fsr3.ini", "dlssg_to_fsr3.log", "nvapi64.dll", "nvapi64.dll.b", "fakenvapi.log", "dlss-enabler.dll", "dlss-enabler-upscaler.dll", "dlss-enabler.log", "nvngx-wrapper.dll", "_nvngx.dll", "dlssg_to_fsr3_amd_is_better-3.0.dll", "OptiScaler.asi", "OptiScaler.log", ] QUICK_SETTING_DEFINITIONS = [ {"id": "Dx12Upscaler", "section": None, "key": "Dx12Upscaler", "default": "auto"}, {"id": "FrameGen.Enabled", "section": "FrameGen", "key": "Enabled", "default": "auto"}, {"id": "FGInput", "section": None, "key": "FGInput", "default": "auto"}, {"id": "FGOutput", "section": None, "key": "FGOutput", "default": "auto"}, {"id": "Fsr4ForceCapable", "section": None, "key": "Fsr4ForceCapable", "default": "false"}, {"id": "Fsr4EnableWatermark", "section": None, "key": "Fsr4EnableWatermark", "default": "false"}, {"id": "UseHQFont", "section": None, "key": "UseHQFont", "default": "false"}, {"id": "Menu.Scale", "section": "Menu", "key": "Scale", "default": "1.000000"}, ] class Plugin: async def _main(self): decky.logger.info("Framegen plugin loaded") async def _unload(self): decky.logger.info("Framegen plugin unloaded.") def _home_path(self) -> Path: try: return Path(decky.HOME) except TypeError: return Path(str(decky.HOME)) def _bundle_path(self) -> Path: return self._home_path() / BUNDLE_DIRNAME def _steam_root_candidates(self) -> list[Path]: home = self._home_path() candidates = [ home / ".local" / "share" / "Steam", home / ".steam" / "steam", ] unique = [] seen = 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() for steam_root in self._steam_root_candidates(): if steam_root.exists(): key = str(steam_root) if key not in seen: library_paths.append(steam_root) seen.add(key) library_file = steam_root / "steamapps" / "libraryfolders.vdf" if not library_file.exists(): continue try: with open(library_file, "r", encoding="utf-8", errors="replace") as file: for line in file: if '"path"' not in line: continue path = line.split('"path"', 1)[1].strip().strip('"').replace("\\\\", "/") candidate = Path(path) key = str(candidate) if key not in seen: library_paths.append(candidate) seen.add(key) except Exception as exc: decky.logger.error(f"Failed to parse {library_file}: {exc}") return library_paths def _compatdata_dirs_for_appid(self, appid: str) -> list[Path]: matches = [] for library in self._steam_library_paths(): compatdata_dir = library / "steamapps" / "compatdata" / str(appid) if compatdata_dir.exists(): matches.append(compatdata_dir) return matches def _parse_manifest_env(self, manifest_path: Path) -> dict: data = {} if not manifest_path.exists(): return data try: with open(manifest_path, "r", encoding="utf-8", errors="replace") as manifest: for raw_line in manifest: line = raw_line.strip() if not line or line.startswith("#") or "=" not in line: continue key, value = line.split("=", 1) data[key.strip()] = value.strip().strip('"') except Exception as exc: decky.logger.error(f"Failed to parse manifest {manifest_path}: {exc}") return data def _write_manifest_env(self, manifest_path: Path, updates: dict) -> dict: data = self._parse_manifest_env(manifest_path) for key, value in updates.items(): if value is None: continue data[str(key)] = str(value) manifest_path.parent.mkdir(parents=True, exist_ok=True) with open(manifest_path, "w", encoding="utf-8") as manifest: for key in sorted(data.keys()): manifest.write(f'{key}="{data[key]}"\n') return data def _normalize_proxy(self, proxy: str | None) -> str: normalized = (proxy or "winmm").replace(".dll", "").strip().lower() if normalized not in SUPPORTED_PROXIES: raise ValueError(f"Unsupported proxy '{proxy}'") return normalized def _find_installed_game(self, appid: str | None = None) -> list[dict]: games = [] 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 = {"appid": "", "name": "", "library_path": str(library_path)} try: with open(appmanifest, "r", encoding="utf-8", errors="replace") as file: for line in file: if '"appid"' in line: game_info["appid"] = line.split('"appid"', 1)[1].strip().strip('"') if '"name"' in line: game_info["name"] = line.split('"name"', 1)[1].strip().strip('"') except Exception as exc: decky.logger.error(f"Skipping {appmanifest}: {exc}") 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 if appid is None or str(game_info["appid"]) == str(appid): games.append(game_info) deduped = {} for game in games: deduped[str(game["appid"])] = game return list(deduped.values()) def _compatdata_dir_for_appid(self, appid: str, create: bool = False) -> Path | None: compatdata_dirs = self._compatdata_dirs_for_appid(str(appid)) if compatdata_dirs: return compatdata_dirs[0] if not create: return None installed_games = self._find_installed_game(appid) if not installed_games: return None library_path = Path(installed_games[0]["library_path"]) compatdata_dir = library_path / "steamapps" / "compatdata" / str(appid) compatdata_dir.mkdir(parents=True, exist_ok=True) return compatdata_dir def _managed_paths_for_appid(self, appid: str, create: bool = False) -> dict | None: compatdata_dir = self._compatdata_dir_for_appid(str(appid), create=create) if not compatdata_dir: return None managed_root = compatdata_dir / MANAGED_DIRNAME system32 = compatdata_dir / "pfx" / "drive_c" / "windows" / "system32" if create: managed_root.mkdir(parents=True, exist_ok=True) return { "compatdata_dir": compatdata_dir, "managed_root": managed_root, "manifest_path": managed_root / MANIFEST_FILENAME, "managed_ini": managed_root / "OptiScaler.ini", "system32": system32, "live_ini": system32 / "OptiScaler.ini", } def _ensure_managed_ini(self, appid: str) -> dict: paths = self._managed_paths_for_appid(appid, create=True) if not paths: raise FileNotFoundError(f"Unable to resolve compatdata path for app {appid}") managed_ini = paths["managed_ini"] if managed_ini.exists(): return paths bundle_ini = self._bundle_path() / "OptiScaler.ini" if not bundle_ini.exists(): raise FileNotFoundError("OptiScaler runtime is not installed; missing bundled OptiScaler.ini") shutil.copy2(bundle_ini, managed_ini) self._modify_optiscaler_ini(managed_ini) return paths def _read_ini_config(self, ini_path: Path) -> ConfigParser: config = ConfigParser(interpolation=None) config.optionxform = str if ini_path.exists(): config.read(ini_path, encoding="utf-8") return config def _find_sections_for_key(self, config: ConfigParser, key: str) -> list[str]: return [section for section in config.sections() if config.has_option(section, key)] def _resolve_setting_definition(self, setting_id: str) -> dict | None: for definition in QUICK_SETTING_DEFINITIONS: if definition["id"] == setting_id: return definition return None def _resolve_setting_section(self, config: ConfigParser, definition: dict) -> str | None: hinted_section = definition.get("section") key = definition["key"] if hinted_section and config.has_option(hinted_section, key): return hinted_section matching_sections = self._find_sections_for_key(config, key) if len(matching_sections) == 1: return matching_sections[0] if hinted_section: return hinted_section if matching_sections: return matching_sections[0] return None def _extract_quick_settings(self, ini_path: Path) -> dict: config = self._read_ini_config(ini_path) settings = {} for definition in QUICK_SETTING_DEFINITIONS: resolved_section = self._resolve_setting_section(config, definition) value = definition["default"] if resolved_section and config.has_option(resolved_section, definition["key"]): value = config.get(resolved_section, definition["key"]) settings[definition["id"]] = value return settings def _replace_ini_values(self, ini_path: Path, resolved_updates: list[tuple[str, str, str]]) -> None: lines = [] if ini_path.exists(): with open(ini_path, "r", encoding="utf-8", errors="replace") as ini_file: lines = ini_file.readlines() for section, key, value in resolved_updates: section_pattern = re.compile(rf"^\s*\[{re.escape(section)}]\s*$") any_section_pattern = re.compile(r"^\s*\[.*]\s*$") key_pattern = re.compile(rf"^\s*{re.escape(key)}\s*=.*$") in_target_section = False section_found = False inserted_or_updated = False insert_index = len(lines) for index, line in enumerate(lines): if section_pattern.match(line): in_target_section = True section_found = True insert_index = index + 1 continue if in_target_section and any_section_pattern.match(line): insert_index = index break if in_target_section: if key_pattern.match(line): newline = "\n" if line.endswith("\n") else "" lines[index] = f"{key}={value}{newline}" inserted_or_updated = True break insert_index = index + 1 if inserted_or_updated: continue if not section_found: if lines and lines[-1].strip(): lines.append("\n") lines.append(f"[{section}]\n") lines.append(f"{key}={value}\n") continue lines[insert_index:insert_index] = [f"{key}={value}\n"] with open(ini_path, "w", encoding="utf-8") as ini_file: ini_file.writelines(lines) def _disable_hq_font_auto(self, ini_file: Path) -> bool: try: if not ini_file.exists(): decky.logger.warning(f"OptiScaler.ini not found at {ini_file}") return False with open(ini_file, "r", encoding="utf-8", errors="replace") as f: content = f.read() updated_content = re.sub(r"UseHQFont\s*=\s*auto", "UseHQFont=false", content) if updated_content != content: with open(ini_file, "w", encoding="utf-8") as f: f.write(updated_content) decky.logger.info("Set UseHQFont=false to avoid missing font assertions") return True except Exception as exc: decky.logger.error(f"Failed to update HQ font setting in OptiScaler.ini: {exc}") return False def _modify_optiscaler_ini(self, ini_file: Path) -> bool: """Modify OptiScaler.ini to set FG defaults, ASI plugin settings, and safe font defaults.""" try: if not ini_file.exists(): decky.logger.warning(f"OptiScaler.ini not found at {ini_file}") return False with open(ini_file, "r", encoding="utf-8", errors="replace") as f: content = f.read() updated_content = re.sub(r"FGType\s*=\s*auto", "FGType=nukems", content) updated_content = re.sub(r"Fsr4Update\s*=\s*auto", "Fsr4Update=true", updated_content) updated_content = re.sub(r"LoadAsiPlugins\s*=\s*auto", "LoadAsiPlugins=true", updated_content) updated_content = re.sub(r"Path\s*=\s*auto", "Path=plugins", updated_content) updated_content = re.sub(r"UseHQFont\s*=\s*auto", "UseHQFont=false", updated_content) with open(ini_file, "w", encoding="utf-8") as f: f.write(updated_content) decky.logger.info( "Modified OptiScaler.ini to set FGType=nukems, Fsr4Update=true, LoadAsiPlugins=true, Path=plugins, UseHQFont=false" ) return True except Exception as exc: decky.logger.error(f"Failed to modify OptiScaler.ini: {exc}") return False def _create_renamed_copies(self, source_file: Path, renames_dir: Path) -> bool: try: renames_dir.mkdir(exist_ok=True) rename_files = [f"{proxy}.dll" for proxy in SUPPORTED_PROXIES] + ["OptiScaler.asi"] if not source_file.exists(): decky.logger.error(f"Source file {source_file} does not exist") return False for rename_file in rename_files: dest_file = renames_dir / rename_file shutil.copy2(source_file, dest_file) decky.logger.info(f"Created renamed copy: {dest_file}") return True except Exception as exc: decky.logger.error(f"Failed to create renamed copies: {exc}") return False def _copy_launcher_scripts(self, assets_dir: Path, extract_path: Path) -> bool: try: launcher_assets = { "fgmod.sh": "fgmod", "fgmod-uninstaller.sh": "fgmod-uninstaller.sh", "update-optiscaler-config.py": "update-optiscaler-config.py", } for asset_name, dest_name in launcher_assets.items(): source = assets_dir / asset_name dest = extract_path / dest_name if not source.exists(): decky.logger.error(f"Launcher asset missing: {source}") return False shutil.copy2(source, dest) dest.chmod(0o755) decky.logger.info(f"Copied launcher asset {source} to {dest}") return True except Exception as exc: decky.logger.error(f"Failed to copy launcher scripts: {exc}") return False def _cleanup_prefix(self, compatdata_dir: Path, proxy: str | None = None, remove_managed_root: bool = True) -> dict: managed_root = compatdata_dir / MANAGED_DIRNAME manifest_path = managed_root / MANIFEST_FILENAME manifest = self._parse_manifest_env(manifest_path) selected_proxy = (proxy or manifest.get("MANAGED_PROXY") or "winmm").replace(".dll", "") system32 = compatdata_dir / "pfx" / "drive_c" / "windows" / "system32" if not system32.exists() and not managed_root.exists(): return {"status": "success", "message": f"No managed OptiScaler state found for {compatdata_dir.name}"} removed = [] for filename in ["OptiScaler.ini", *SUPPORT_FILES, *LEGACY_FILES]: target = system32 / filename if target.exists(): try: if target.is_dir(): shutil.rmtree(target, ignore_errors=True) else: target.unlink() removed.append(filename) except Exception as exc: decky.logger.error(f"Failed removing {target}: {exc}") plugins_dir = system32 / "plugins" if plugins_dir.exists(): shutil.rmtree(plugins_dir, ignore_errors=True) removed.append("plugins/") proxy_path = system32 / f"{selected_proxy}.dll" backup_path = system32 / f"{selected_proxy}-original.dll" if proxy_path.exists(): try: proxy_path.unlink() removed.append(proxy_path.name) except Exception as exc: decky.logger.error(f"Failed removing proxy {proxy_path}: {exc}") if backup_path.exists(): try: shutil.move(backup_path, proxy_path) removed.append(backup_path.name) decky.logger.info(f"Restored original proxy {proxy_path.name} in {system32}") except Exception as exc: decky.logger.error(f"Failed restoring backup {backup_path}: {exc}") if remove_managed_root and managed_root.exists(): shutil.rmtree(managed_root, ignore_errors=True) removed.append(str(managed_root)) message = f"Cleaned prefix-managed OptiScaler for app {compatdata_dir.name}" decky.logger.info(f"{message}; removed entries: {removed}") return {"status": "success", "message": message, "removed": removed} def _cleanup_all_managed_prefixes(self) -> list[dict]: cleanup_results = [] seen = set() for library in self._steam_library_paths(): compatdata_root = library / "steamapps" / "compatdata" if not compatdata_root.exists(): continue for managed_root in compatdata_root.glob(f"*/{MANAGED_DIRNAME}"): compatdata_dir = managed_root.parent key = str(compatdata_dir) if key in seen: continue seen.add(key) cleanup_results.append(self._cleanup_prefix(compatdata_dir)) return cleanup_results async def extract_static_optiscaler(self) -> dict: """Extract OptiScaler from the plugin's bin directory and copy runtime assets.""" try: decky.logger.info("Starting extract_static_optiscaler method") bin_path = Path(decky.DECKY_PLUGIN_DIR) / "bin" extract_path = self._bundle_path() if not bin_path.exists(): decky.logger.error(f"Bin directory does not exist: {bin_path}") return {"status": "error", "message": f"Bin directory not found: {bin_path}"} optiscaler_archive = None for file in bin_path.glob("*.7z"): if ("OptiScaler" in file.name or "Optiscaler" in file.name) and "BUNDLE" not in file.name: optiscaler_archive = file break if not optiscaler_archive: decky.logger.error("OptiScaler archive not found in plugin bin directory") return {"status": "error", "message": "OptiScaler archive not found in plugin bin directory"} if extract_path.exists(): shutil.rmtree(extract_path) extract_path.mkdir(parents=True, exist_ok=True) extract_cmd = ["7z", "x", "-y", "-o" + str(extract_path), str(optiscaler_archive)] clean_env = os.environ.copy() clean_env["LD_LIBRARY_PATH"] = "" extract_result = subprocess.run( extract_cmd, capture_output=True, text=True, check=False, env=clean_env, ) if extract_result.returncode != 0: decky.logger.error(f"Extraction failed: {extract_result.stderr}") return { "status": "error", "message": f"Failed to extract OptiScaler archive: {extract_result.stderr}", } additional_files = [ "nvngx.dll", "OptiPatcher_v0.30.asi", ] for file_name in additional_files: src_file = bin_path / file_name dest_file = extract_path / file_name if not src_file.exists(): return { "status": "error", "message": f"Required file {file_name} not found in plugin bin directory", } shutil.copy2(src_file, dest_file) source_file = extract_path / "OptiScaler.dll" renames_dir = extract_path / "renames" self._create_renamed_copies(source_file, renames_dir) assets_dir = Path(decky.DECKY_PLUGIN_DIR) / "assets" if not self._copy_launcher_scripts(assets_dir, extract_path): return {"status": "error", "message": "Failed to install runtime launcher scripts"} plugins_dir = extract_path / "plugins" plugins_dir.mkdir(exist_ok=True) asi_src = bin_path / "OptiPatcher_v0.30.asi" if asi_src.exists(): shutil.copy2(asi_src, plugins_dir / "OptiPatcher.asi") try: skip_overwrite = os.environ.get("DECKY_SKIP_UPSCALER_OVERWRITE", "false").lower() in ("1", "true", "yes") if UPSCALER_OVERWRITE_ENABLED and not skip_overwrite: upscaler_src = bin_path / "amd_fidelityfx_upscaler_dx12.dll" upscaler_dst = extract_path / "amd_fidelityfx_upscaler_dx12.dll" if upscaler_src.exists(): shutil.copy2(upscaler_src, upscaler_dst) decky.logger.info("Overwrote amd_fidelityfx_upscaler_dx12.dll with static remote binary") else: decky.logger.info("Skipping upscaler DLL overwrite due to DECKY_SKIP_UPSCALER_OVERWRITE") except Exception as exc: decky.logger.error(f"Failed upscaler overwrite step: {exc}") version_match = optiscaler_archive.name.replace(".7z", "") if "OptiScaler_" in version_match: version = "v" + version_match.split("OptiScaler_")[1] elif "Optiscaler_" in version_match: version = "v" + version_match.split("Optiscaler_")[1] else: version = version_match with open(extract_path / "version.txt", "w", encoding="utf-8") as f: f.write(version) ini_file = extract_path / "OptiScaler.ini" self._modify_optiscaler_ini(ini_file) return { "status": "success", "message": f"Installed prefix-managed OptiScaler runtime {version} to {extract_path}", "version": version, } except Exception as exc: decky.logger.error(f"Extract failed with exception: {str(exc)}") import traceback decky.logger.error(f"Traceback: {traceback.format_exc()}") return {"status": "error", "message": f"Extract failed: {str(exc)}"} async def run_uninstall_fgmod(self) -> dict: try: cleanup_results = self._cleanup_all_managed_prefixes() bundle_path = self._bundle_path() if bundle_path.exists(): shutil.rmtree(bundle_path) decky.logger.info(f"Removed directory: {bundle_path}") cleaned_prefixes = len([result for result in cleanup_results if result.get("status") == "success"]) return { "status": "success", "output": f"Removed OptiScaler runtime and cleaned {cleaned_prefixes} managed compatdata prefixes.", } except Exception as exc: decky.logger.error(f"Uninstall error: {str(exc)}") return { "status": "error", "message": f"Uninstall failed: {str(exc)}", "output": str(exc), } async def run_install_fgmod(self) -> dict: try: decky.logger.info("Starting OptiScaler installation from static bundle") extract_result = await self.extract_static_optiscaler() if extract_result["status"] != "success": return { "status": "error", "message": f"OptiScaler extraction failed: {extract_result.get('message', 'Unknown error')}", } return { "status": "success", "output": "Installed the prefix-managed OptiScaler runtime. Use the game selector or launch command to stage it inside a Proton prefix at launch time.", } except Exception as exc: decky.logger.error(f"Unexpected error during installation: {str(exc)}") return {"status": "error", "message": f"Installation failed: {str(exc)}"} async def check_fgmod_path(self) -> dict: path = self._bundle_path() if not path.exists(): return {"exists": False} for file_name in REQUIRED_BUNDLE_FILES: if not path.joinpath(file_name).exists(): return {"exists": False} plugins_dir = path / "plugins" if not plugins_dir.exists() or not (plugins_dir / "OptiPatcher.asi").exists(): return {"exists": False} return {"exists": True} async def cleanup_managed_game(self, appid: str) -> dict: compatdata_dirs = self._compatdata_dirs_for_appid(str(appid)) if not compatdata_dirs: return {"status": "success", "message": f"No compatdata prefix found for app {appid}; launch options can still be cleared."} cleanup_messages = [] for compatdata_dir in compatdata_dirs: result = self._cleanup_prefix(compatdata_dir) cleanup_messages.append(result.get("message", f"Cleaned {compatdata_dir}")) return {"status": "success", "message": "\n".join(cleanup_messages)} async def get_game_config(self, appid: str) -> dict: try: paths = self._ensure_managed_ini(str(appid)) manifest = self._parse_manifest_env(paths["manifest_path"]) installed_game = self._find_installed_game(str(appid)) game_name = installed_game[0]["name"] if installed_game else str(appid) source_ini = paths["live_ini"] if paths["live_ini"].exists() else paths["managed_ini"] with open(source_ini, "r", encoding="utf-8", errors="replace") as ini_file: raw_ini = ini_file.read() preferred_proxy = manifest.get("PREFERRED_PROXY") or manifest.get("MANAGED_PROXY") or "winmm" proxy = self._normalize_proxy(preferred_proxy) settings = self._extract_quick_settings(source_ini) return { "status": "success", "appid": str(appid), "name": game_name, "proxy": proxy, "settings": settings, "raw_ini": raw_ini, "managed_exists": paths["managed_ini"].exists(), "live_available": paths["live_ini"].exists(), "paths": { "compatdata": str(paths["compatdata_dir"]), "managed_root": str(paths["managed_root"]), "managed_ini": str(paths["managed_ini"]), "system32": str(paths["system32"]), "live_ini": str(paths["live_ini"]), }, } except Exception as exc: decky.logger.error(f"get_game_config failed for {appid}: {exc}") return {"status": "error", "message": str(exc)} async def save_game_config( self, appid: str, settings: dict | None = None, proxy: str | None = None, apply_live: bool = False, raw_ini: str | None = None, ) -> dict: try: paths = self._ensure_managed_ini(str(appid)) manifest_updates = {} if proxy: manifest_updates["PREFERRED_PROXY"] = self._normalize_proxy(proxy) if manifest_updates: self._write_manifest_env(paths["manifest_path"], manifest_updates) managed_ini = paths["managed_ini"] resolved_updates = [] settings = settings or {} if raw_ini is not None: with open(managed_ini, "w", encoding="utf-8") as ini_file: ini_file.write(raw_ini) elif settings: config = self._read_ini_config(managed_ini) for setting_id, value in settings.items(): definition = self._resolve_setting_definition(setting_id) if not definition: continue section = self._resolve_setting_section(config, definition) or definition.get("section") or "OptiScaler" resolved_updates.append((section, definition["key"], str(value))) if resolved_updates: self._replace_ini_values(managed_ini, resolved_updates) self._disable_hq_font_auto(managed_ini) live_applied = False if apply_live and paths["live_ini"].exists(): if raw_ini is not None: with open(paths["live_ini"], "w", encoding="utf-8") as ini_file: ini_file.write(raw_ini) elif resolved_updates: self._replace_ini_values(paths["live_ini"], resolved_updates) self._disable_hq_font_auto(paths["live_ini"]) live_applied = True message = "Saved OptiScaler config" if apply_live: message = ( "Saved OptiScaler config and applied it to the running game's live prefix copy" if live_applied else "Saved OptiScaler config, but no live prefix copy was available to update" ) return { "status": "success", "message": message, "live_applied": live_applied, "proxy": self._parse_manifest_env(paths["manifest_path"]).get("PREFERRED_PROXY", proxy or "winmm"), } except Exception as exc: decky.logger.error(f"save_game_config failed for {appid}: {exc}") return {"status": "error", "message": str(exc)} async def list_installed_games(self) -> dict: try: games = self._find_installed_game() sanitized_games = [{"appid": game["appid"], "name": game["name"]} for game in games] return {"status": "success", "games": sanitized_games} except Exception as exc: decky.logger.error(str(exc)) return {"status": "error", "message": str(exc)} async def get_path_defaults(self) -> dict: home_path = self._home_path() steam_common = home_path / ".local" / "share" / "Steam" / "steamapps" / "common" return { "home": str(home_path), "steam_common": str(steam_common), } async def log_error(self, error: str) -> None: decky.logger.error(f"FRONTEND: {error}") async def manual_patch_directory(self, directory: str) -> dict: return { "status": "error", "message": "Direct game-directory patching has been removed. Use the prefix-managed launch command instead.", } async def manual_unpatch_directory(self, directory: str) -> dict: return { "status": "error", "message": "Direct game-directory patching has been removed. Use the prefix-managed launch command or per-game cleanup instead.", }