summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-x.vscode/build.sh28
-rwxr-xr-xdefaults/assets/fgmod-uninstaller.sh235
-rwxr-xr-xdefaults/assets/fgmod.sh335
-rw-r--r--main.py904
-rw-r--r--package.json33
-rw-r--r--pnpm-lock.yaml18
6 files changed, 647 insertions, 906 deletions
diff --git a/.vscode/build.sh b/.vscode/build.sh
index 2310ff0..2c72e60 100755
--- a/.vscode/build.sh
+++ b/.vscode/build.sh
@@ -1,10 +1,26 @@
#!/usr/bin/env bash
-CLI_LOCATION="$(pwd)/cli"
-echo "Building plugin in $(pwd)"
-printf "Please input sudo password to proceed.\n"
-# read -s sudopass
+set -euo pipefail
-# printf "\n"
+SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]:-$0}")" &> /dev/null && pwd)"
+REPO_ROOT="$(cd -- "$SCRIPT_DIR/.." &> /dev/null && pwd)"
+LOCAL_CLI="$REPO_ROOT/cli/decky"
+SYSTEM_CLI="$(command -v decky || true)"
-echo $sudopass | sudo -E $CLI_LOCATION/decky plugin build $(pwd)
+if [[ -x "$LOCAL_CLI" ]]; then
+ CLI_LOCATION="$LOCAL_CLI"
+elif [[ -n "$SYSTEM_CLI" ]]; then
+ CLI_LOCATION="$SYSTEM_CLI"
+else
+ echo "Decky CLI not found. Run .vscode/setup.sh first or install the decky CLI manually."
+ exit 1
+fi
+
+echo "Building plugin in $REPO_ROOT"
+echo "Using Decky CLI: $CLI_LOCATION"
+
+if [[ "${DECKY_BUILD_USE_SUDO:-0}" == "1" ]]; then
+ sudo -E "$CLI_LOCATION" plugin build "$REPO_ROOT"
+else
+ "$CLI_LOCATION" plugin build "$REPO_ROOT"
+fi
diff --git a/defaults/assets/fgmod-uninstaller.sh b/defaults/assets/fgmod-uninstaller.sh
index 8c5e7b9..08e7113 100755
--- a/defaults/assets/fgmod-uninstaller.sh
+++ b/defaults/assets/fgmod-uninstaller.sh
@@ -1,175 +1,100 @@
#!/usr/bin/env bash
+set -euo pipefail
set -x
-exec > >(tee -i /tmp/fgmod-uninstaller.log) 2>&1
+exec > >(tee -i /tmp/fgmod-prefix-cleanup.log) 2>&1
+
+log() {
+ echo "$*"
+ logger -t fgmod-prefix-cleanup "$*"
+}
error_exit() {
- echo "โŒ $1"
- if [[ -n $STEAM_ZENITY ]]; then
- $STEAM_ZENITY --error --text "$1"
- else
- zenity --error --text "$1" || echo "Zenity failed to display error"
+ local message="$1"
+ echo "โŒ $message"
+ logger -t fgmod-prefix-cleanup "ERROR: $message"
+ if [[ -n "${STEAM_ZENITY:-}" ]]; then
+ "$STEAM_ZENITY" --error --text "$message" || true
+ elif command -v zenity >/dev/null 2>&1; then
+ zenity --error --text "$message" || true
fi
- logger -t fgmod-uninstaller "โŒ ERROR: $1"
exit 1
}
-if [ "$#" -lt 1 ]; then
- echo "Usage: $0 program [program_arguments...]"
- exit 1
+managed_dir_name="optiscaler-managed"
+manifest_name="manifest.env"
+default_proxy="${OPTISCALER_PROXY:-${DLL:-winmm}}"
+default_proxy="${default_proxy%.dll}"
+
+support_files=(
+ "OptiScaler.ini"
+ "OptiScaler.log"
+ "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"
+ "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"
+)
+
+[[ -n "${STEAM_COMPAT_DATA_PATH:-}" ]] || error_exit "STEAM_COMPAT_DATA_PATH is required. Use this wrapper from a Steam/Proton launch option."
+[[ $# -ge 1 ]] || error_exit "Usage: $0 program [program_arguments...]"
+
+compatdata_path="$STEAM_COMPAT_DATA_PATH"
+system32_path="$compatdata_path/pfx/drive_c/windows/system32"
+managed_root="$compatdata_path/$managed_dir_name"
+manifest_path="$managed_root/$manifest_name"
+proxy_name="$default_proxy"
+
+if [[ -f "$manifest_path" ]]; then
+ # shellcheck disable=SC1090
+ source "$manifest_path"
+ proxy_name="${MANAGED_PROXY:-$proxy_name}"
fi
-# === Resolve Game Path ===
-exe_folder_path=""
-if [[ "$1" == *.exe ]]; then
- exe_folder_path=$(dirname "$1")
-else
- for arg in "$@"; do
- if [[ "$arg" == *.exe ]]; then
- # Handle special cases for specific games
- [[ "$arg" == *"Cyberpunk 2077"* ]] && arg=${arg//REDprelauncher.exe/bin/x64/Cyberpunk2077.exe}
- [[ "$arg" == *"Witcher 3"* ]] && arg=${arg//REDprelauncher.exe/bin/x64_dx12/witcher3.exe}
- [[ "$arg" == *"Baldurs Gate 3"* ]] && arg=${arg//Launcher\/LariLauncher.exe/bin/bg3_dx11.exe}
- [[ "$arg" == *"HITMAN 3"* ]] && arg=${arg//Launcher.exe/Retail/HITMAN3.exe}
- [[ "$arg" == *"HITMAN World of Assassination"* ]] && arg=${arg//Launcher.exe/Retail/HITMAN3.exe}
- [[ "$arg" == *"SYNCED"* ]] && arg=${arg//Launcher\/sop_launcher.exe/SYNCED.exe}
- [[ "$arg" == *"2KLauncher"* ]] && arg=${arg//2KLauncher\/LauncherPatcher.exe/DoesntMatter.exe}
- [[ "$arg" == *"Warhammer 40,000 DARKTIDE"* ]] && arg=${arg//launcher\/Launcher.exe/binaries/Darktide.exe}
- [[ "$arg" == *"Warhammer Vermintide 2"* ]] && arg=${arg//launcher\/Launcher.exe/binaries_dx12/vermintide2_dx12.exe}
- [[ "$arg" == *"Satisfactory"* ]] && arg=${arg//FactoryGameSteam.exe/Engine/Binaries/Win64/FactoryGameSteam-Win64-Shipping.exe}
- [[ "$arg" == *"FINAL FANTASY XIV Online"* ]] && arg=${arg//boot\/ffxivboot.exe/game/ffxiv_dx11.exe}
- exe_folder_path=$(dirname "$arg")
- break
- fi
- done
-fi
+proxy_dll="${proxy_name}.dll"
+backup_dll="${proxy_name}-original.dll"
-for arg in "$@"; do
- if [[ "$arg" == lutris:rungameid/* ]]; then
- lutris_id="${arg#lutris:rungameid/}"
-
- # Get slug from Lutris JSON
- slug=$(lutris --list-games --json 2>/dev/null | jq -r ".[] | select(.id == $lutris_id) | .slug")
-
- if [[ -z "$slug" || "$slug" == "null" ]]; then
- echo "Could not find slug for Lutris ID $lutris_id"
- break
- fi
+for file_name in "${support_files[@]}"; do
+ rm -f "$system32_path/$file_name"
+done
- # Find matching YAML file using slug
- config_file=$(find ~/.config/lutris/games/ -iname "${slug}-*.yml" | head -1)
+rm -rf "$system32_path/plugins"
+rm -f "$system32_path/$proxy_dll"
- if [[ -z "$config_file" ]]; then
- echo "No config file found for slug '$slug'"
- break
- fi
+if [[ -f "$system32_path/$backup_dll" ]]; then
+ mv -f "$system32_path/$backup_dll" "$system32_path/$proxy_dll"
+fi
- # Extract executable path from YAML
- exe_path=$(grep -E '^\s*exe:' "$config_file" | sed 's/.*exe:[[:space:]]*//')
+rm -rf "$managed_root"
- if [[ -n "$exe_path" ]]; then
- exe_folder_path=$(dirname "$exe_path")
- echo "Resolved executable path: $exe_path"
- echo "Executable folder: $exe_folder_path"
- else
- echo "Executable path not found in $config_file"
- fi
+log "Cleaned prefix-managed OptiScaler files from $compatdata_path using proxy $proxy_name"
- break
- fi
+while [[ $# -gt 0 && "$1" == "--" ]]; do
+ shift
done
-# Fallback to STEAM_COMPAT_INSTALL_PATH when no path was found
-[[ -z "$exe_folder_path" && -n "$STEAM_COMPAT_INSTALL_PATH" ]] && exe_folder_path="$STEAM_COMPAT_INSTALL_PATH"
-
-# Check for Unreal Engine game paths
-if [[ -d "$exe_folder_path/Engine" ]]; then
- ue_exe_path=$(find "$exe_folder_path" -maxdepth 4 -mindepth 4 -path "*Binaries/Win64/*.exe" -not -path "*/Engine/*" | head -1)
- exe_folder_path=$(dirname "$ue_exe_path")
-fi
-
-# Verify the game folder exists
-[[ ! -d "$exe_folder_path" ]] && error_exit "Unable to locate the game folder: $exe_folder_path"
-
-# Avoid operating on the uninstaller's own directory
-script_dir=$(dirname "$(realpath "$0")")
-[[ "$(realpath "$exe_folder_path")" == "$script_dir" ]] && error_exit "The target directory matches the script's directory. Aborting to prevent accidental deletion."
-
-# Change to the game directory
-cd "$exe_folder_path" || error_exit "Failed to change directory to $exe_folder_path"
-
-# Verify current directory before proceeding
-[[ "$(pwd)" != "$exe_folder_path" ]] && error_exit "Unexpected working directory: $(pwd)"
-
-logger -t fgmod-uninstaller "๐ŸŸข Uninstalling from: $exe_folder_path"
-
-# === Remove OptiScaler Files ===
-echo "๐Ÿงน Removing OptiScaler files..."
-rm -f "OptiScaler.dll" "dxgi.dll" "winmm.dll" "dbghelp.dll" "version.dll" "wininet.dll" "winhttp.dll" "OptiScaler.asi"
-rm -f "OptiScaler.ini" "OptiScaler.log"
-
-# === Remove Nukem FG Mod Files ===
-echo "๐Ÿงน Removing Nukem FG Mod files..."
-rm -f "dlssg_to_fsr3_amd_is_better.dll" "dlssg_to_fsr3.ini" "dlssg_to_fsr3.log"
-rm -f "nvapi64.dll" "fakenvapi.ini" "fakenvapi.log"
-
-# === Remove Supporting Libraries ===
-echo "๐Ÿงน Removing supporting libraries..."
-rm -f "nvngx.dll" "nvngx.ini"
-# Only remove files if backups exist (to avoid removing restored originals)
-[[ -f "libxess.dll.b" ]] && rm -f "libxess.dll"
-[[ -f "libxess_dx11.dll.b" ]] && rm -f "libxess_dx11.dll"
-[[ -f "libxess_fg.dll.b" ]] && rm -f "libxess_fg.dll"
-[[ -f "libxell.dll.b" ]] && rm -f "libxell.dll"
-[[ -f "amd_fidelityfx_dx12.dll.b" ]] && rm -f "amd_fidelityfx_dx12.dll"
-[[ -f "amd_fidelityfx_framegeneration_dx12.dll.b" ]] && rm -f "amd_fidelityfx_framegeneration_dx12.dll"
-[[ -f "amd_fidelityfx_upscaler_dx12.dll.b" ]] && rm -f "amd_fidelityfx_upscaler_dx12.dll"
-[[ -f "amd_fidelityfx_vk.dll.b" ]] && rm -f "amd_fidelityfx_vk.dll"
-
-# === Remove FG Mod Files ===
-echo "๐Ÿงน Removing frame generation mod files..."
-rm -f "dlssg_to_fsr3_amd_is_better.dll" "dlssg_to_fsr3.ini"
-
-# === Remove NVAPI Files (Current and Legacy) ===
-echo "๐Ÿงน Removing NVAPI files..."
-rm -f "fakenvapi.dll" "fakenvapi.ini" # Current v0.9.0-pre4 approach
-rm -f "nvapi64.dll" "nvapi64.dll.b" # Legacy cleanup for older versions and backups
-
-# === Remove ASI Plugins ===
-echo "๐Ÿงน Removing ASI plugins directory..."
-rm -rf "plugins"
-
-# === Remove Legacy Files ===
-echo "๐Ÿงน Removing legacy files..."
-rm -f "dlss-enabler.dll" "dlss-enabler-upscaler.dll" "dlss-enabler.log"
-rm -f "nvngx-wrapper.dll" "_nvngx.dll"
-rm -f "dlssg_to_fsr3_amd_is_better-3.0.dll"
-
-# === Restore Original DLLs ===
-echo "๐Ÿ”„ Restoring original DLLs..."
-original_dlls=("d3dcompiler_47.dll" "amd_fidelityfx_dx12.dll" "amd_fidelityfx_framegeneration_dx12.dll" "amd_fidelityfx_upscaler_dx12.dll" "amd_fidelityfx_vk.dll" "libxess.dll" "libxess_dx11.dll" "libxess_fg.dll" "libxell.dll")
-for dll in "${original_dlls[@]}"; do
- if [[ -f "${dll}.b" ]]; then
- mv "${dll}.b" "$dll"
- echo "โœ… Restored original $dll"
- logger -t fgmod-uninstaller "โœ… Restored original $dll"
- fi
-done
+set +e
+"$@"
+exit_code=$?
+set -e
-# === Self-remove uninstaller ===
-echo "๐Ÿ—‘๏ธ Removing uninstaller..."
-rm -f "fgmod-uninstaller.sh"
-
-echo "โœ… fgmod removed from this game successfully!"
-logger -t fgmod-uninstaller "โœ… fgmod removed from $exe_folder_path"
-
-# === Execute original command if provided ===
-if [[ $# -gt 1 ]]; then
- echo "๐Ÿš€ Launching the game..."
- export SteamDeck=0
- export WINEDLLOVERRIDES="${WINEDLLOVERRIDES},dxgi=n,b"
- exec >/dev/null 2>&1
- exec "$@"
-else
- echo "โœ… Uninstallation complete. No game specified to run."
-fi \ No newline at end of file
+exit "$exit_code"
diff --git a/defaults/assets/fgmod.sh b/defaults/assets/fgmod.sh
index fa36558..7aa62bb 100755
--- a/defaults/assets/fgmod.sh
+++ b/defaults/assets/fgmod.sh
@@ -1,227 +1,182 @@
#!/usr/bin/env bash
+set -euo pipefail
set -x
-exec > >(tee -i /tmp/fgmod-install.log) 2>&1
+exec > >(tee -i /tmp/fgmod-prefix-managed.log) 2>&1
+
+log() {
+ echo "$*"
+ logger -t fgmod-prefix-managed "$*"
+}
error_exit() {
- echo "โŒ $1"
- if [[ -n $STEAM_ZENITY ]]; then
- $STEAM_ZENITY --error --text "$1"
- else
- zenity --error --text "$1" || echo "Zenity failed to display error"
+ local message="$1"
+ echo "โŒ $message"
+ logger -t fgmod-prefix-managed "ERROR: $message"
+ if [[ -n "${STEAM_ZENITY:-}" ]]; then
+ "$STEAM_ZENITY" --error --text "$message" || true
+ elif command -v zenity >/dev/null 2>&1; then
+ zenity --error --text "$message" || true
fi
- logger -t fgmod "โŒ ERROR: $1"
exit 1
}
-# === CONFIG ===
-fgmod_path="$HOME/fgmod"
-dll_name="${DLL:-dxgi.dll}"
-preserve_ini="${PRESERVE_INI:-true}"
-
-# === Resolve Game Path ===
-if [[ "$#" -lt 1 ]]; then
- error_exit "Usage: $0 program [program_arguments...]"
-fi
+bundle_root="${HOME}/fgmod"
+managed_dir_name="optiscaler-managed"
+manifest_name="manifest.env"
+proxy_name="${OPTISCALER_PROXY:-${DLL:-winmm}}"
+proxy_name="${proxy_name%.dll}"
+proxy_dll="${proxy_name}.dll"
+backup_dll="${proxy_name}-original.dll"
+
+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"
+)
+
+case "$proxy_name" in
+ winmm|dxgi|version|dbghelp|winhttp|wininet|d3d12) ;;
+ *) error_exit "Unsupported OPTISCALER_PROXY '$proxy_name'." ;;
+esac
+
+[[ -d "$bundle_root" ]] || error_exit "OptiScaler runtime not installed at $bundle_root"
+[[ -n "${STEAM_COMPAT_DATA_PATH:-}" ]] || error_exit "STEAM_COMPAT_DATA_PATH is required. Use this wrapper from a Steam/Proton launch option."
+[[ $# -ge 1 ]] || error_exit "Usage: $0 program [program_arguments...]"
+
+compatdata_path="$STEAM_COMPAT_DATA_PATH"
+system32_path="$compatdata_path/pfx/drive_c/windows/system32"
+managed_root="$compatdata_path/$managed_dir_name"
+manifest_path="$managed_root/$manifest_name"
+managed_ini="$managed_root/OptiScaler.ini"
+managed_plugins="$managed_root/plugins"
+
+cleanup_proxy_stage() {
+ local cleanup_proxy="$1"
+ local cleanup_proxy_dll="${cleanup_proxy}.dll"
+ local cleanup_backup_dll="${cleanup_proxy}-original.dll"
+
+ rm -f "$system32_path/$cleanup_proxy_dll"
+ if [[ -f "$system32_path/$cleanup_backup_dll" ]]; then
+ mv -f "$system32_path/$cleanup_backup_dll" "$system32_path/$cleanup_proxy_dll"
+ fi
+}
-exe_folder_path=""
-if [[ $# -eq 1 ]]; then
- [[ "$1" == *.exe ]] && exe_folder_path=$(dirname "$1") || exe_folder_path="$1"
-else
- for arg in "$@"; do
- if [[ "$arg" == *.exe ]]; then
- [[ "$arg" == *"Cyberpunk 2077"* ]] && arg=${arg//REDprelauncher.exe/bin/x64/Cyberpunk2077.exe}
- [[ "$arg" == *"Witcher 3"* ]] && arg=${arg//REDprelauncher.exe/bin/x64_dx12/witcher3.exe}
- [[ "$arg" == *"Baldurs Gate 3"* ]] && arg=${arg//Launcher\/LariLauncher.exe/bin/bg3_dx11.exe}
- [[ "$arg" == *"HITMAN 3"* ]] && arg=${arg//Launcher.exe/Retail/HITMAN3.exe}
- [[ "$arg" == *"HITMAN World of Assassination"* ]] && arg=${arg//Launcher.exe/Retail/HITMAN3.exe}
- [[ "$arg" == *"SYNCED"* ]] && arg=${arg//Launcher\/sop_launcher.exe/SYNCED.exe}
- [[ "$arg" == *"2KLauncher"* ]] && arg=${arg//2KLauncher\/LauncherPatcher.exe/DoesntMatter.exe}
- [[ "$arg" == *"Warhammer 40,000 DARKTIDE"* ]] && arg=${arg//launcher\/Launcher.exe/binaries/Darktide.exe}
- [[ "$arg" == *"Warhammer Vermintide 2"* ]] && arg=${arg//launcher\/Launcher.exe/binaries_dx12/vermintide2_dx12.exe}
- [[ "$arg" == *"Satisfactory"* ]] && arg=${arg//FactoryGameSteam.exe/Engine/Binaries/Win64/FactoryGameSteam-Win64-Shipping.exe}
- [[ "$arg" == *"FINAL FANTASY XIV Online"* ]] && arg=${arg//boot\/ffxivboot.exe/game/ffxiv_dx11.exe}
- exe_folder_path=$(dirname "$arg")
- break
- fi
+cleanup_stage_files() {
+ local cleanup_proxy="$1"
+ rm -f "$system32_path/OptiScaler.ini"
+ for file_name in "${support_files[@]}"; do
+ rm -f "$system32_path/$file_name"
done
-fi
-
-for arg in "$@"; do
- if [[ "$arg" == lutris:rungameid/* ]]; then
- lutris_id="${arg#lutris:rungameid/}"
-
- # Get slug from Lutris JSON
- slug=$(lutris --list-games --json 2>/dev/null | jq -r ".[] | select(.id == $lutris_id) | .slug")
-
- if [[ -z "$slug" || "$slug" == "null" ]]; then
- echo "Could not find slug for Lutris ID $lutris_id"
- break
- fi
-
- # Find matching YAML file using slug
- config_file=$(find ~/.config/lutris/games/ -iname "${slug}-*.yml" | head -1)
-
- if [[ -z "$config_file" ]]; then
- echo "No config file found for slug '$slug'"
- break
- fi
+ rm -f "$system32_path/OptiScaler.log"
+ rm -rf "$system32_path/plugins"
+ cleanup_proxy_stage "$cleanup_proxy"
+}
- # Extract executable path from YAML
- exe_path=$(grep -E '^\s*exe:' "$config_file" | sed 's/.*exe:[[:space:]]*//' )
+mkdir -p "$system32_path" "$managed_root" "$managed_plugins"
- if [[ -n "$exe_path" ]]; then
- exe_folder_path=$(dirname "$exe_path")
- echo "Resolved executable path: $exe_path"
- echo "Executable folder: $exe_folder_path"
- else
- echo "Executable path not found in $config_file"
- fi
+existing_proxy=""
+if [[ -f "$manifest_path" ]]; then
+ # shellcheck disable=SC1090
+ source "$manifest_path"
+ existing_proxy="${MANAGED_PROXY:-}"
+fi
- break
- fi
-done
+if [[ -n "$existing_proxy" && "$existing_proxy" != "$proxy_name" ]]; then
+ log "Switching managed proxy from $existing_proxy to $proxy_name"
+ cleanup_stage_files "$existing_proxy"
+fi
-[[ -z "$exe_folder_path" && -n "$STEAM_COMPAT_INSTALL_PATH" ]] && exe_folder_path="$STEAM_COMPAT_INSTALL_PATH"
+[[ -f "$bundle_root/OptiScaler.ini" ]] || error_exit "Missing OptiScaler.ini in runtime bundle"
+[[ -f "$bundle_root/update-optiscaler-config.py" ]] || error_exit "Missing update-optiscaler-config.py in runtime bundle"
-if [[ -d "$exe_folder_path/Engine" ]]; then
- ue_exe=$(find "$exe_folder_path" -maxdepth 4 -mindepth 4 -path "*Binaries/Win64/*.exe" -not -path "*/Engine/*" | head -1)
- exe_folder_path=$(dirname "$ue_exe")
+python_bin="python3"
+if ! command -v "$python_bin" >/dev/null 2>&1; then
+ python_bin="python"
fi
+command -v "$python_bin" >/dev/null 2>&1 || error_exit "Python interpreter not found"
-[[ ! -d "$exe_folder_path" ]] && error_exit "โŒ Could not resolve game directory!"
-[[ ! -w "$exe_folder_path" ]] && error_exit "๐Ÿ›‘ No write permission to the game folder!"
+if [[ ! -f "$managed_ini" ]]; then
+ cp -f "$bundle_root/OptiScaler.ini" "$managed_ini"
+fi
-logger -t fgmod "๐ŸŸข Target directory: $exe_folder_path"
-logger -t fgmod "๐Ÿงฉ Using DLL name: $dll_name"
-logger -t fgmod "๐Ÿ“„ Preserve INI: $preserve_ini"
+"$python_bin" "$bundle_root/update-optiscaler-config.py" "$managed_ini"
+sed -i 's/^UseHQFont[[:space:]]*=[[:space:]]*auto$/UseHQFont=false/' "$managed_ini" || true
-# === Cleanup Old Injectors ===
-rm -f "$exe_folder_path"/{dxgi.dll,winmm.dll,nvngx.dll,_nvngx.dll,nvngx-wrapper.dll,dlss-enabler.dll,OptiScaler.dll}
+if [[ -d "$bundle_root/plugins" ]]; then
+ rm -rf "$managed_plugins"
+ mkdir -p "$managed_plugins"
+ cp -f "$bundle_root/plugins"/* "$managed_plugins/" 2>/dev/null || true
+fi
-# === Optional: Backup Original DLLs ===
-original_dlls=("d3dcompiler_47.dll" "amd_fidelityfx_dx12.dll" "amd_fidelityfx_framegeneration_dx12.dll" "amd_fidelityfx_upscaler_dx12.dll" "amd_fidelityfx_vk.dll")
-for dll in "${original_dlls[@]}"; do
- [[ -f "$exe_folder_path/$dll" && ! -f "$exe_folder_path/$dll.b" ]] && mv -f "$exe_folder_path/$dll" "$exe_folder_path/$dll.b"
+for file_name in "${support_files[@]}"; do
+ if [[ -f "$bundle_root/$file_name" ]]; then
+ cp -f "$bundle_root/$file_name" "$system32_path/$file_name"
+ fi
done
-# === Remove nvapi64.dll and its backup (conflicts from previous fakenvapi versions) ===
-rm -f "$exe_folder_path/nvapi64.dll" "$exe_folder_path/nvapi64.dll.b"
-echo "๐Ÿงน Cleaned up nvapi64.dll and backup (legacy fakenvapi conflicts)"
+if [[ -d "$managed_plugins" ]]; then
+ rm -rf "$system32_path/plugins"
+ mkdir -p "$system32_path/plugins"
+ cp -f "$managed_plugins"/* "$system32_path/plugins/" 2>/dev/null || true
+fi
-# === Core Install ===
-if [[ -f "$fgmod_path/renames/$dll_name" ]]; then
- echo "โœ… Using pre-renamed $dll_name"
- cp "$fgmod_path/renames/$dll_name" "$exe_folder_path/$dll_name" || error_exit "โŒ Failed to copy $dll_name"
-else
- echo "โš ๏ธ Pre-renamed $dll_name not found, falling back to OptiScaler.dll"
- cp "$fgmod_path/OptiScaler.dll" "$exe_folder_path/$dll_name" || error_exit "โŒ Failed to copy OptiScaler.dll as $dll_name"
+if [[ -f "$system32_path/$proxy_dll" && ! -f "$system32_path/$backup_dll" ]]; then
+ mv -f "$system32_path/$proxy_dll" "$system32_path/$backup_dll"
fi
-# === OptiScaler.ini Handling ===
-if [[ "$preserve_ini" == "true" && -f "$exe_folder_path/OptiScaler.ini" ]]; then
- echo "๐Ÿ“„ Preserving existing OptiScaler.ini (user settings retained)"
- logger -t fgmod "๐Ÿ“„ Existing OptiScaler.ini preserved in $exe_folder_path"
+if [[ -f "$bundle_root/renames/$proxy_dll" ]]; then
+ cp -f "$bundle_root/renames/$proxy_dll" "$system32_path/$proxy_dll"
else
- echo "๐Ÿ“„ Installing OptiScaler.ini from plugin defaults"
- cp "$fgmod_path/OptiScaler.ini" "$exe_folder_path/OptiScaler.ini" || error_exit "โŒ Failed to copy OptiScaler.ini"
- logger -t fgmod "๐Ÿ“„ OptiScaler.ini installed to $exe_folder_path"
+ cp -f "$bundle_root/OptiScaler.dll" "$system32_path/$proxy_dll"
fi
-# === OptiScaler env variables Handling ===
-if [[ -f "$fgmod_path/update-optiscaler-config.py" ]]; then
- python "$fgmod_path/update-optiscaler-config.py" "$exe_folder_path/OptiScaler.ini"
+cp -f "$managed_ini" "$system32_path/OptiScaler.ini"
+
+runtime_version="unknown"
+if [[ -f "$bundle_root/version.txt" ]]; then
+ runtime_version="$(<"$bundle_root/version.txt")"
fi
-# OptiScaler 0.9.0-pre11 can assert on Proton when HQ font auto mode tries to load
-# an external TTF that is not present. Only normalize the default auto value.
-sed -i 's/^UseHQFont[[:space:]]*=[[:space:]]*auto$/UseHQFont=false/' "$exe_folder_path/OptiScaler.ini" || true
+cat > "$manifest_path" <<EOF
+MANAGED_PROXY="$proxy_name"
+BUNDLE_ROOT="$bundle_root"
+BUNDLE_VERSION="$runtime_version"
+SYSTEM32_PATH="$system32_path"
+EOF
-# === ASI Plugins Directory ===
-if [[ -d "$fgmod_path/plugins" ]]; then
- echo "๐Ÿ”Œ Installing ASI plugins directory"
- cp -r "$fgmod_path/plugins" "$exe_folder_path/" || true
- logger -t fgmod "๐Ÿ”Œ ASI plugins directory installed to $exe_folder_path"
+export SteamDeck=0
+if [[ -n "${WINEDLLOVERRIDES:-}" ]]; then
+ export WINEDLLOVERRIDES="${WINEDLLOVERRIDES},${proxy_name}=n,b"
else
- echo "โš ๏ธ No plugins directory found in fgmod"
+ export WINEDLLOVERRIDES="${proxy_name}=n,b"
fi
-# === Supporting Libraries ===
-cp -f "$fgmod_path/libxess.dll" "$exe_folder_path/" || true
-cp -f "$fgmod_path/libxess_dx11.dll" "$exe_folder_path/" || true
-cp -f "$fgmod_path/libxess_fg.dll" "$exe_folder_path/" || true
-cp -f "$fgmod_path/libxell.dll" "$exe_folder_path/" || true
-cp -f "$fgmod_path/amd_fidelityfx_dx12.dll" "$exe_folder_path/" || true
-cp -f "$fgmod_path/amd_fidelityfx_framegeneration_dx12.dll" "$exe_folder_path/" || true
-cp -f "$fgmod_path/amd_fidelityfx_upscaler_dx12.dll" "$exe_folder_path/" || true
-cp -f "$fgmod_path/amd_fidelityfx_vk.dll" "$exe_folder_path/" || true
-cp -f "$fgmod_path/nvngx.dll" "$exe_folder_path/" || true
-
-# === Nukem FG Mod Files (now in fgmod directory) ===
-cp -f "$fgmod_path/dlssg_to_fsr3_amd_is_better.dll" "$exe_folder_path/" || true
-# Note: dlssg_to_fsr3.ini is not included in v0.9.0-pre4 archive
-
-# === FakeNVAPI Files ===
-# Remove legacy nvapi64.dll to avoid conflicts
-# rm -f "$exe_folder_path/nvapi64.dll"
-# echo "๐Ÿงน Removed legacy nvapi64.dll"
-
-# Copy fakenvapi.dll with original name (v1.3.8.1)
-cp -f "$fgmod_path/fakenvapi.dll" "$exe_folder_path/" || true
-cp -f "$fgmod_path/fakenvapi.ini" "$exe_folder_path/" || true
-echo "๐Ÿ“ฆ Installed fakenvapi.dll and fakenvapi.ini"
-
-# === Additional Support Files ===
-# cp -f "$fgmod_path/d3dcompiler_47.dll" "$exe_folder_path/" || true
-
-# Note: d3dcompiler_47.dll is not included in v0.9.0-pre4 archive
-
-echo "โœ… Installation completed successfully!"
-echo "๐Ÿ“„ For Steam, add this to the launch options: \"$fgmod_path/fgmod\" %COMMAND%"
-echo "๐Ÿ“„ For Heroic, add this as a new wrapper: \"$fgmod_path/fgmod\""
-logger -t fgmod "๐ŸŸข Installation completed successfully for $exe_folder_path"
-
-# === Execute original command ===
-if [[ $# -gt 1 ]]; then
- # Log to both file and system journal
- logger -t fgmod "=================="
- logger -t fgmod "Debug Info (Launch Mode):"
- logger -t fgmod "Number of arguments: $#"
- for i in $(seq 1 $#); do
- logger -t fgmod "Arg $i: ${!i}"
- done
- logger -t fgmod "Final executable path: $exe_folder_path"
- logger -t fgmod "=================="
-
- # Execute the original command
- export SteamDeck=0
- export WINEDLLOVERRIDES="$WINEDLLOVERRIDES,dxgi=n,b"
-
- # Filter out leading -- separators (from Steam launch options)
- while [[ $# -gt 0 && "$1" == "--" ]]; do
- shift
- done
+while [[ $# -gt 0 && "$1" == "--" ]]; do
+ shift
+done
- exec >/dev/null 2>&1
- "$@"
-else
- echo "Done!"
- echo "----------------------------------------"
- echo "Debug Info (Standalone Mode):"
- echo "Number of arguments: $#"
- for i in $(seq 1 $#); do
- echo "Arg $i: ${!i}"
- done
- echo "Final executable path: $exe_folder_path"
- echo "----------------------------------------"
-
- # Also log standalone mode to journal
- logger -t fgmod "=================="
- logger -t fgmod "Debug Info (Standalone Mode):"
- logger -t fgmod "Number of arguments: $#"
- for i in $(seq 1 $#); do
- logger -t fgmod "Arg $i: ${!i}"
- done
- logger -t fgmod "Final executable path: $exe_folder_path"
- logger -t fgmod "=================="
+log "Using compatdata path: $compatdata_path"
+log "Using system32 path: $system32_path"
+log "Using prefix-managed proxy: $proxy_dll"
+log "Using WINEDLLOVERRIDES=$WINEDLLOVERRIDES"
+
+set +e
+"$@"
+exit_code=$?
+set -e
+
+if [[ -f "$system32_path/OptiScaler.ini" ]]; then
+ cp -f "$system32_path/OptiScaler.ini" "$managed_ini"
fi
+
+exit "$exit_code"
diff --git a/main.py b/main.py
index ddee8ba..4407e03 100644
--- a/main.py
+++ b/main.py
@@ -1,31 +1,25 @@
import decky
import os
-import subprocess
-import json
-import shutil
import re
+import shutil
+import subprocess
from pathlib import Path
# Toggle to enable overwriting the upscaler DLL from the static remote binary.
-# Set to False or comment out this constant to skip the overwrite by default.
UPSCALER_OVERWRITE_ENABLED = True
-INJECTOR_FILENAMES = [
- "dxgi.dll",
- "winmm.dll",
- "nvngx.dll",
- "_nvngx.dll",
- "nvngx-wrapper.dll",
- "dlss-enabler.dll",
- "OptiScaler.dll",
-]
-
-ORIGINAL_DLL_BACKUPS = [
- "d3dcompiler_47.dll",
- "amd_fidelityfx_dx12.dll",
- "amd_fidelityfx_framegeneration_dx12.dll",
- "amd_fidelityfx_upscaler_dx12.dll",
- "amd_fidelityfx_vk.dll",
+BUNDLE_DIRNAME = "fgmod"
+MANAGED_DIRNAME = "optiscaler-managed"
+MANIFEST_FILENAME = "manifest.env"
+
+SUPPORTED_PROXIES = [
+ "dxgi",
+ "winmm",
+ "dbghelp",
+ "version",
+ "wininet",
+ "winhttp",
+ "d3d12",
]
SUPPORT_FILES = [
@@ -43,6 +37,15 @@ SUPPORT_FILES = [
"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",
@@ -56,274 +59,334 @@ LEGACY_FILES = [
"_nvngx.dll",
"dlssg_to_fsr3_amd_is_better-3.0.dll",
"OptiScaler.asi",
- "OptiScaler.ini",
"OptiScaler.log",
]
+
class Plugin:
async def _main(self):
decky.logger.info("Framegen plugin loaded")
async def _unload(self):
decky.logger.info("Framegen plugin unloaded.")
-
- def _create_renamed_copies(self, source_file, renames_dir):
- """Create renamed copies of the OptiScaler.dll file"""
+
+ def _home_path(self) -> Path:
try:
- renames_dir.mkdir(exist_ok=True)
-
- rename_files = [
- "dxgi.dll",
- "winmm.dll",
- "dbghelp.dll",
- "version.dll",
- "wininet.dll",
- "winhttp.dll",
- "OptiScaler.asi"
- ]
-
- if source_file.exists():
- 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
- else:
- decky.logger.error(f"Source file {source_file} does not exist")
- return False
-
- except Exception as e:
- decky.logger.error(f"Failed to create renamed copies: {e}")
- return False
-
- def _copy_launcher_scripts(self, assets_dir, extract_path):
- """Copy launcher scripts from assets directory"""
+ 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:
- # Copy fgmod script
- fgmod_script_src = assets_dir / "fgmod.sh"
- fgmod_script_dest = extract_path / "fgmod"
- if fgmod_script_src.exists():
- shutil.copy2(fgmod_script_src, fgmod_script_dest)
- fgmod_script_dest.chmod(0o755)
- decky.logger.info(f"Copied fgmod script to {fgmod_script_dest}")
-
- # Copy uninstaller script
- uninstaller_src = assets_dir / "fgmod-uninstaller.sh"
- uninstaller_dest = extract_path / "fgmod-uninstaller.sh"
- if uninstaller_src.exists():
- shutil.copy2(uninstaller_src, uninstaller_dest)
- uninstaller_dest.chmod(0o755)
- decky.logger.info(f"Copied uninstaller script to {uninstaller_dest}")
-
- # Copy optiscaler config updater script
- optiscaler_config_updater_src = assets_dir / "update-optiscaler-config.py"
- optiscaler_config_updater_dest = extract_path / "update-optiscaler-config.py"
- if optiscaler_config_updater_src.exists():
- shutil.copy2(optiscaler_config_updater_src, optiscaler_config_updater_dest)
- optiscaler_config_updater_dest.chmod(0o755)
- decky.logger.info(f"Copied update-optiscaler-config.py script to {optiscaler_config_updater_dest}")
-
- return True
- except Exception as e:
- decky.logger.error(f"Failed to copy launcher scripts: {e}")
- return False
-
- def _disable_hq_font_auto(self, ini_file):
- """Disable the new HQ font auto mode to avoid missing font assertions on Wine/Proton."""
+ 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 _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') as f:
+ 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)
+ updated_content = re.sub(r"UseHQFont\s*=\s*auto", "UseHQFont=false", content)
if updated_content != content:
- with open(ini_file, 'w') as f:
+ 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 e:
- decky.logger.error(f"Failed to update HQ font setting in OptiScaler.ini: {e}")
+ 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):
+ 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 ini_file.exists():
- with open(ini_file, 'r') as f:
- content = f.read()
-
- # Replace FGType=auto with FGType=nukems
- updated_content = re.sub(r'FGType\s*=\s*auto', 'FGType=nukems', content)
-
- # Replace Fsr4Update=auto with Fsr4Update=true
- updated_content = re.sub(r'Fsr4Update\s*=\s*auto', 'Fsr4Update=true', updated_content)
-
- # Replace LoadAsiPlugins=auto with LoadAsiPlugins=true
- updated_content = re.sub(r'LoadAsiPlugins\s*=\s*auto', 'LoadAsiPlugins=true', updated_content)
-
- # Replace Path=auto with Path=plugins
- updated_content = re.sub(r'Path\s*=\s*auto', 'Path=plugins', updated_content)
-
- # Disable new HQ font auto mode to avoid missing font assertions on Proton
- updated_content = re.sub(r'UseHQFont\s*=\s*auto', 'UseHQFont=false', updated_content)
-
- with open(ini_file, 'w') 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
- else:
+ if not ini_file.exists():
decky.logger.warning(f"OptiScaler.ini not found at {ini_file}")
return False
- except Exception as e:
- decky.logger.error(f"Failed to modify OptiScaler.ini: {e}")
+
+ 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 additional files."""
+ """Extract OptiScaler from the plugin's bin directory and copy runtime assets."""
try:
decky.logger.info("Starting extract_static_optiscaler method")
-
- # Set up paths
+
bin_path = Path(decky.DECKY_PLUGIN_DIR) / "bin"
- extract_path = Path(decky.HOME) / "fgmod"
-
- decky.logger.info(f"Bin path: {bin_path}")
- decky.logger.info(f"Extract path: {extract_path}")
-
- # Check if bin directory exists
+ 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}"}
-
- # List files in bin directory for debugging
- bin_files = list(bin_path.glob("*"))
- decky.logger.info(f"Files in bin directory: {[f.name for f in bin_files]}")
-
- # Find the OptiScaler archive in the bin directory
+
optiscaler_archive = None
for file in bin_path.glob("*.7z"):
- decky.logger.info(f"Checking 7z file: {file.name}")
- # Check for both "OptiScaler" and "Optiscaler" (case variations) and exclude BUNDLE files
if ("OptiScaler" in file.name or "Optiscaler" in file.name) and "BUNDLE" not in file.name:
optiscaler_archive = file
- decky.logger.info(f"Found OptiScaler archive: {file.name}")
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"}
-
- decky.logger.info(f"Using archive: {optiscaler_archive}")
-
- # Clean up existing directory
+
if extract_path.exists():
- decky.logger.info(f"Removing existing directory: {extract_path}")
shutil.rmtree(extract_path)
-
- extract_path.mkdir(exist_ok=True)
- decky.logger.info(f"Created extract directory: {extract_path}")
-
- decky.logger.info(f"Extracting {optiscaler_archive.name} to {extract_path}")
-
- # Extract the 7z file
- extract_cmd = [
- "7z",
- "x",
- "-y",
- "-o" + str(extract_path),
- str(optiscaler_archive)
- ]
-
- decky.logger.info(f"Running extraction command: {' '.join(extract_cmd)}")
-
- # Create a clean environment to avoid PyInstaller issues
+ 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"] = ""
-
- decky.logger.info("Starting subprocess.run for extraction")
+
extract_result = subprocess.run(
extract_cmd,
capture_output=True,
text=True,
check=False,
- env=clean_env
+ env=clean_env,
)
-
- decky.logger.info(f"Extraction completed with return code: {extract_result.returncode}")
- decky.logger.info(f"Extraction stdout: {extract_result.stdout}")
- if extract_result.stderr:
- decky.logger.info(f"Extraction stderr: {extract_result.stderr}")
-
+
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}"
+ "message": f"Failed to extract OptiScaler archive: {extract_result.stderr}",
}
-
- # Copy additional individual files from bin directory
- # Note: v0.9.0-pre3+ includes dlssg_to_fsr3_amd_is_better.dll, fakenvapi.dll, and fakenvapi.ini in the 7z
- # Only copy files that aren't already in the archive (separate remote binaries)
+
additional_files = [
- "nvngx.dll", # nvidia dll from streamline sdk, not bundled in opti
- "OptiPatcher_v0.30.asi" # ASI plugin for OptiScaler spoofing
+ "nvngx.dll",
+ "OptiPatcher_v0.30.asi",
]
-
- decky.logger.info("Starting additional files copy")
+
for file_name in additional_files:
src_file = bin_path / file_name
dest_file = extract_path / file_name
-
- decky.logger.info(f"Checking for additional file: {file_name} at {src_file}")
- if src_file.exists():
- shutil.copy2(src_file, dest_file)
- decky.logger.info(f"Copied additional file: {file_name}")
- else:
- decky.logger.warning(f"Additional file not found: {file_name}")
+ if not src_file.exists():
return {
"status": "error",
- "message": f"Required file {file_name} not found in plugin bin directory"
+ "message": f"Required file {file_name} not found in plugin bin directory",
}
-
- decky.logger.info("Creating renamed copies of OptiScaler.dll")
- # Create renamed copies of OptiScaler.dll
+ 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)
-
- decky.logger.info("Copying launcher scripts")
- # Copy launcher scripts from assets
+
assets_dir = Path(decky.DECKY_PLUGIN_DIR) / "assets"
- self._copy_launcher_scripts(assets_dir, extract_path)
+ 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")
- decky.logger.info("Setting up ASI plugins directory")
- # Create plugins directory and copy OptiPatcher ASI file
- try:
- plugins_dir = extract_path / "plugins"
- plugins_dir.mkdir(exist_ok=True)
- decky.logger.info(f"Created plugins directory: {plugins_dir}")
-
- # Copy OptiPatcher ASI file to plugins directory
- asi_src = bin_path / "OptiPatcher_v0.30.asi"
- asi_dst = plugins_dir / "OptiPatcher.asi" # Rename to generic name
-
- if asi_src.exists():
- shutil.copy2(asi_src, asi_dst)
- decky.logger.info(f"Copied OptiPatcher ASI to plugins directory: {asi_dst}")
- else:
- decky.logger.warning("OptiPatcher ASI file not found in bin directory")
- except Exception as e:
- decky.logger.error(f"Failed to setup ASI plugins directory: {e}")
-
- decky.logger.info("Starting upscaler DLL overwrite check")
- # Optionally overwrite amd_fidelityfx_upscaler_dx12.dll with the separately bundled
- # RDNA2-optimized static binary used for Steam Deck compatibility.
- # Toggle via env DECKY_SKIP_UPSCALER_OVERWRITE=true to skip.
try:
skip_overwrite = os.environ.get("DECKY_SKIP_UPSCALER_OVERWRITE", "false").lower() in ("1", "true", "yes")
if UPSCALER_OVERWRITE_ENABLED and not skip_overwrite:
@@ -332,377 +395,146 @@ class Plugin:
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.warning("amd_fidelityfx_upscaler_dx12.dll not found in bin; skipping overwrite")
else:
decky.logger.info("Skipping upscaler DLL overwrite due to DECKY_SKIP_UPSCALER_OVERWRITE")
- except Exception as e:
- decky.logger.error(f"Failed upscaler overwrite step: {e}")
-
- # Extract version from filename (e.g., OptiScaler_0.7.9.7z -> v0.7.9)
- 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]
+ 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
-
- # Create version file
- version_file = extract_path / "version.txt"
- try:
- with open(version_file, 'w') as f:
- f.write(version)
- decky.logger.info(f"Created version file: {version}")
- except Exception as e:
- decky.logger.error(f"Failed to create version file: {e}")
-
- # Modify OptiScaler.ini to set FGType=nukems and Fsr4Update=true
- decky.logger.info("Modifying OptiScaler.ini")
+
+ 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)
-
- decky.logger.info(f"Successfully completed extraction to ~/fgmod with version {version}")
+
return {
"status": "success",
- "message": f"Successfully extracted OptiScaler {version} to ~/fgmod",
- "version": version
+ "message": f"Installed prefix-managed OptiScaler runtime {version} to {extract_path}",
+ "version": version,
}
-
- except Exception as e:
- decky.logger.error(f"Extract failed with exception: {str(e)}")
- decky.logger.error(f"Exception type: {type(e).__name__}")
+
+ 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(e)}"}
+ return {"status": "error", "message": f"Extract failed: {str(exc)}"}
async def run_uninstall_fgmod(self) -> dict:
try:
- # Remove fgmod directory
- fgmod_path = Path(decky.HOME) / "fgmod"
-
- if fgmod_path.exists():
- shutil.rmtree(fgmod_path)
- decky.logger.info(f"Removed directory: {fgmod_path}")
- return {
- "status": "success",
- "output": "Successfully removed fgmod directory"
- }
- else:
- return {
- "status": "success",
- "output": "No fgmod directory found to remove"
- }
-
- except Exception as e:
- decky.logger.error(f"Uninstall error: {str(e)}")
+ 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": "error",
- "message": f"Uninstall failed: {str(e)}",
- "output": str(e)
+ "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 the static OptiScaler 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')}"
+ "message": f"OptiScaler extraction failed: {extract_result.get('message', 'Unknown error')}",
}
-
- return {
- "status": "success",
- "output": "Successfully installed OptiScaler with all necessary components! You can now replace DLSS with FSR Frame Gen!"
- }
- except Exception as e:
- decky.logger.error(f"Unexpected error during installation: {str(e)}")
return {
- "status": "error",
- "message": f"Installation failed: {str(e)}"
+ "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 = Path(decky.HOME) / "fgmod"
- required_files = [
- "OptiScaler.dll",
- "OptiScaler.ini",
- "dlssg_to_fsr3_amd_is_better.dll",
- "fakenvapi.dll", # v0.9.0-pre3+ includes fakenvapi.dll in archive
- "fakenvapi.ini",
- "nvngx.dll",
- "amd_fidelityfx_dx12.dll",
- "amd_fidelityfx_framegeneration_dx12.dll",
- "amd_fidelityfx_upscaler_dx12.dll",
- "amd_fidelityfx_vk.dll",
- "libxess.dll",
- "libxess_dx11.dll",
- "libxess_fg.dll", # New in v0.9.0-pre4
- "libxell.dll", # New in v0.9.0-pre4
- "fgmod",
- "fgmod-uninstaller.sh",
- "update-optiscaler-config.py"
- ]
-
- if path.exists():
- # Check required files
- for file_name in required_files:
- if not path.joinpath(file_name).exists():
- return {"exists": False}
+ path = self._bundle_path()
+ if not path.exists():
+ return {"exists": False}
- # Check plugins directory and OptiPatcher ASI
- plugins_dir = path / "plugins"
- if not plugins_dir.exists() or not (plugins_dir / "OptiPatcher.asi").exists():
+ for file_name in REQUIRED_BUNDLE_FILES:
+ if not path.joinpath(file_name).exists():
return {"exists": False}
- return {"exists": True}
- else:
+ plugins_dir = path / "plugins"
+ if not plugins_dir.exists() or not (plugins_dir / "OptiPatcher.asi").exists():
return {"exists": False}
- def _resolve_target_directory(self, directory: str) -> Path:
- decky.logger.info(f"Resolving target directory: {directory}")
- target = Path(directory).expanduser()
- if not target.exists():
- raise FileNotFoundError(f"Target directory does not exist: {directory}")
- if not target.is_dir():
- raise NotADirectoryError(f"Target path is not a directory: {directory}")
- if not os.access(target, os.W_OK | os.X_OK):
- raise PermissionError(f"Insufficient permissions for {directory}")
- decky.logger.info(f"Resolved directory {directory} to absolute path {target}")
- return target
-
- def _manual_patch_directory_impl(self, directory: Path) -> dict:
- fgmod_path = Path(decky.HOME) / "fgmod"
- if not fgmod_path.exists():
- return {
- "status": "error",
- "message": "OptiScaler bundle not installed. Run Install first.",
- }
-
- optiscaler_dll = fgmod_path / "OptiScaler.dll"
- if not optiscaler_dll.exists():
- return {
- "status": "error",
- "message": "OptiScaler.dll not found in ~/fgmod. Reinstall OptiScaler.",
- }
-
- dll_name = "dxgi.dll"
- preserve_ini = True
-
- try:
- decky.logger.info(f"Manual patch started for {directory}")
-
- removed_injectors = []
- for filename in INJECTOR_FILENAMES:
- path = directory / filename
- if path.exists():
- path.unlink()
- removed_injectors.append(filename)
- decky.logger.info(f"Removed injector DLLs: {removed_injectors}" if removed_injectors else "No injector DLLs found to remove")
-
- backed_up_originals = []
- for dll in ORIGINAL_DLL_BACKUPS:
- source = directory / dll
- backup = directory / f"{dll}.b"
- if source.exists() and not backup.exists():
- shutil.move(source, backup)
- backed_up_originals.append(dll)
- decky.logger.info(f"Backed up original DLLs: {backed_up_originals}" if backed_up_originals else "No original DLLs required backup")
-
- removed_legacy = []
- for legacy in ["nvapi64.dll", "nvapi64.dll.b"]:
- legacy_path = directory / legacy
- if legacy_path.exists():
- legacy_path.unlink()
- removed_legacy.append(legacy)
- decky.logger.info(f"Removed legacy files: {removed_legacy}" if removed_legacy else "No legacy files to remove")
-
- renamed = fgmod_path / "renames" / dll_name
- destination_dll = directory / dll_name
- source_for_copy = renamed if renamed.exists() else optiscaler_dll
- shutil.copy2(source_for_copy, destination_dll)
- decky.logger.info(f"Copied injector DLL from {source_for_copy} to {destination_dll}")
-
- target_ini = directory / "OptiScaler.ini"
- source_ini = fgmod_path / "OptiScaler.ini"
- if preserve_ini and target_ini.exists():
- decky.logger.info(f"Preserving existing OptiScaler.ini at {target_ini}")
- elif source_ini.exists():
- shutil.copy2(source_ini, target_ini)
- decky.logger.info(f"Copied OptiScaler.ini from {source_ini} to {target_ini}")
- else:
- decky.logger.warning("No OptiScaler.ini found to copy")
-
- if target_ini.exists():
- self._disable_hq_font_auto(target_ini)
-
- plugins_src = fgmod_path / "plugins"
- plugins_dest = directory / "plugins"
- if plugins_src.exists():
- shutil.copytree(plugins_src, plugins_dest, dirs_exist_ok=True)
- decky.logger.info(f"Synced plugins directory from {plugins_src} to {plugins_dest}")
- else:
- decky.logger.warning("Plugins directory missing in fgmod bundle")
-
- copied_support = []
- missing_support = []
- for filename in SUPPORT_FILES:
- source = fgmod_path / filename
- dest = directory / filename
- if source.exists():
- shutil.copy2(source, dest)
- copied_support.append(filename)
- else:
- missing_support.append(filename)
- if copied_support:
- decky.logger.info(f"Copied support files: {copied_support}")
- if missing_support:
- decky.logger.warning(f"Support files missing from fgmod bundle: {missing_support}")
+ return {"exists": True}
- decky.logger.info(f"Manual patch complete for {directory}")
- return {
- "status": "success",
- "message": f"OptiScaler files copied to {directory}",
- }
+ 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."}
- except PermissionError as exc:
- decky.logger.error(f"Manual patch permission error: {exc}")
- return {
- "status": "error",
- "message": f"Permission error while patching: {exc}",
- }
- except Exception as exc:
- decky.logger.error(f"Manual patch failed: {exc}")
- return {
- "status": "error",
- "message": f"Manual patch failed: {exc}",
- }
+ cleanup_messages = []
+ for compatdata_dir in compatdata_dirs:
+ result = self._cleanup_prefix(compatdata_dir)
+ cleanup_messages.append(result.get("message", f"Cleaned {compatdata_dir}"))
- def _manual_unpatch_directory_impl(self, directory: Path) -> dict:
- try:
- decky.logger.info(f"Manual unpatch started for {directory}")
-
- removed_files = []
- for filename in set(INJECTOR_FILENAMES + SUPPORT_FILES):
- path = directory / filename
- if path.exists():
- path.unlink()
- removed_files.append(filename)
- decky.logger.info(f"Removed injector/support files: {removed_files}" if removed_files else "No injector/support files found to remove")
-
- legacy_removed = []
- for legacy in LEGACY_FILES:
- path = directory / legacy
- if path.exists():
- try:
- path.unlink()
- except IsADirectoryError:
- shutil.rmtree(path, ignore_errors=True)
- legacy_removed.append(legacy)
- decky.logger.info(f"Removed legacy artifacts: {legacy_removed}" if legacy_removed else "No legacy artifacts present")
-
- plugins_dir = directory / "plugins"
- if plugins_dir.exists():
- shutil.rmtree(plugins_dir, ignore_errors=True)
- decky.logger.info(f"Removed plugins directory at {plugins_dir}")
-
- restored_backups = []
- for dll in ORIGINAL_DLL_BACKUPS:
- backup = directory / f"{dll}.b"
- original = directory / dll
- if backup.exists():
- if original.exists():
- original.unlink()
- shutil.move(backup, original)
- restored_backups.append(dll)
- decky.logger.info(f"Restored backups: {restored_backups}" if restored_backups else "No backups found to restore")
-
- uninstaller = directory / "fgmod-uninstaller.sh"
- if uninstaller.exists():
- uninstaller.unlink()
- decky.logger.info(f"Removed fgmod uninstaller at {uninstaller}")
-
- decky.logger.info(f"Manual unpatch complete for {directory}")
- return {
- "status": "success",
- "message": f"OptiScaler files removed from {directory}",
- }
-
- except PermissionError as exc:
- decky.logger.error(f"Manual unpatch permission error: {exc}")
- return {
- "status": "error",
- "message": f"Permission error while unpatching: {exc}",
- }
- except Exception as exc:
- decky.logger.error(f"Manual unpatch failed: {exc}")
- return {
- "status": "error",
- "message": f"Manual unpatch failed: {exc}",
- }
+ return {"status": "success", "message": "\n".join(cleanup_messages)}
async def list_installed_games(self) -> dict:
try:
- steam_root = Path(decky.HOME) / ".steam" / "steam"
- library_file = Path(steam_root) / "steamapps" / "libraryfolders.vdf"
-
-
- if not library_file.exists():
- return {"status": "error", "message": "libraryfolders.vdf not found"}
-
- library_paths = []
- with open(library_file, "r", encoding="utf-8", errors="replace") as file:
- for line in file:
- if '"path"' in line:
- path = line.split('"path"')[1].strip().strip('"').replace("\\\\", "/")
- library_paths.append(path)
-
games = []
- for library_path in library_paths:
- steamapps_path = Path(library_path) / "steamapps"
+ 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": ""}
-
try:
- with open(appmanifest, "r", encoding="utf-8") as file:
+ 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].strip().strip('"')
+ game_info["appid"] = line.split('"appid"', 1)[1].strip().strip('"')
if '"name"' in line:
- game_info["name"] = line.split('"name"')[1].strip().strip('"')
- except UnicodeDecodeError as e:
- decky.logger.error(f"Skipping {appmanifest} due to encoding issue: {e}")
- finally:
- pass # Ensures loop continues even if an error occurs
+ game_info["name"] = line.split('"name"', 1)[1].strip().strip('"')
+ except Exception as exc:
+ decky.logger.error(f"Skipping {appmanifest}: {exc}")
if game_info["appid"] and game_info["name"]:
games.append(game_info)
- # Filter out games whose name contains "Proton" or "Steam Linux Runtime"
- filtered_games = [g for g in games if "Proton" not in g["name"] and "Steam Linux Runtime" not in g["name"]]
+ filtered_games = [
+ g
+ for g in games
+ if "Proton" not in g["name"] and "Steam Linux Runtime" not in g["name"]
+ ]
- return {"status": "success", "games": filtered_games}
+ deduped = {}
+ for game in filtered_games:
+ deduped[str(game["appid"])] = game
- except Exception as e:
- decky.logger.error(str(e))
- return {"status": "error", "message": str(e)}
+ return {"status": "success", "games": list(deduped.values())}
+ except Exception as exc:
+ decky.logger.error(str(exc))
+ return {"status": "error", "message": str(exc)}
async def get_path_defaults(self) -> dict:
- try:
- home_path = Path(decky.HOME)
- except TypeError:
- home_path = Path(str(decky.HOME))
-
+ home_path = self._home_path()
steam_common = home_path / ".local" / "share" / "Steam" / "steamapps" / "common"
-
return {
"home": str(home_path),
"steam_common": str(steam_common),
@@ -712,19 +544,13 @@ class Plugin:
decky.logger.error(f"FRONTEND: {error}")
async def manual_patch_directory(self, directory: str) -> dict:
- try:
- target_dir = self._resolve_target_directory(directory)
- except (FileNotFoundError, NotADirectoryError, PermissionError) as exc:
- decky.logger.error(f"Manual patch validation failed: {exc}")
- return {"status": "error", "message": str(exc)}
-
- return self._manual_patch_directory_impl(target_dir)
+ 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:
- try:
- target_dir = self._resolve_target_directory(directory)
- except (FileNotFoundError, NotADirectoryError, PermissionError) as exc:
- decky.logger.error(f"Manual unpatch validation failed: {exc}")
- return {"status": "error", "message": str(exc)}
-
- return self._manual_unpatch_directory_impl(target_dir)
+ return {
+ "status": "error",
+ "message": "Direct game-directory patching has been removed. Use the prefix-managed launch command or per-game cleanup instead.",
+ }
diff --git a/package.json b/package.json
index 9c95117..7c68e90 100644
--- a/package.json
+++ b/package.json
@@ -1,11 +1,11 @@
{
"name": "decky-framegen",
- "version": "0.15.1",
- "description": "This plugin installs and manages OptiScaler, a tool that enhances upscaling and enables frame generation in a range of DirectX 12 games.",
+ "version": "0.16.0",
+ "description": "Prefix-managed OptiScaler integration for Decky that stages per-game payloads inside Proton compatdata instead of the game directory.",
"type": "module",
"scripts": {
- "build": "rollup -c",
- "watch": "rollup -c -w",
+ "build": "node ./node_modules/@rollup/wasm-node/dist/bin/rollup -c",
+ "watch": "node ./node_modules/@rollup/wasm-node/dist/bin/rollup -c -w",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
@@ -30,6 +30,7 @@
"devDependencies": {
"@decky/rollup": "^1.0.1",
"@decky/ui": "^4.7.2",
+ "@rollup/wasm-node": "4.22.5",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"@types/webpack": "^5.28.5",
@@ -49,13 +50,12 @@
]
}
},
- "remote_binary_bundling" : true,
- "remote_binary":
- [
+ "remote_binary_bundling": true,
+ "remote_binary": [
{
- "sha256hash": "d30d98b9f58e340b8dcd5aa93fc59432e803e071cd6b207d55acc667685d79dc",
- "url": "https://github.com/xXJSONDeruloXx/OptiScaler-Bleeding-Edge/releases/download/opti-9-pre-11/Optiscaler_0.9.0-pre11.20260311._RC4.5.7z",
- "name": "Optiscaler_0.9.0-pre11.20260311._RC4.5.7z"
+ "sha256hash": "d30d98b9f58e340b8dcd5aa93fc59432e803e071cd6b207d55acc667685d79dc",
+ "url": "https://github.com/xXJSONDeruloXx/OptiScaler-Bleeding-Edge/releases/download/opti-9-pre-11/Optiscaler_0.9.0-pre11.20260311._RC4.5.7z",
+ "name": "Optiscaler_0.9.0-pre11.20260311._RC4.5.7z"
},
{
"sha256hash": "2604c0b392072d715b400b2f89434274de31995a4b6e68ce38250ebbd3f6c5fc",
@@ -68,11 +68,12 @@
"name": "OptiPatcher_v0.30.asi"
},
{
- "sha256hash": "1d75e7c1f37f966517f625aa3cc9602ff89d42ad2a7fcbdfa5fc91dab4674149",
- "url": "https://github.com/xXJSONDeruloXx/OptiScaler-Bleeding-Edge/releases/download/OptiScaler_v0.7.8-pre0_20250816_unsigned_dll-13b2b5d0/nvngx.dll",
- "name": "nvngx.dll"
+ "sha256hash": "1d75e7c1f37f966517f625aa3cc9602ff89d42ad2a7fcbdfa5fc91dab4674149",
+ "url": "https://github.com/xXJSONDeruloXx/OptiScaler-Bleeding-Edge/releases/download/OptiScaler_v0.7.8-pre0_20250816_unsigned_dll-13b2b5d0/nvngx.dll",
+ "name": "nvngx.dll"
}
- ]
+ ],
+ "optionalDependencies": {
+ "@rollup/rollup-linux-x64-musl": "4.22.5"
+ }
}
-
-
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8f954a4..8144543 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -17,6 +17,10 @@ importers:
tslib:
specifier: ^2.7.0
version: 2.7.0
+ optionalDependencies:
+ '@rollup/rollup-linux-x64-musl':
+ specifier: 4.22.5
+ version: 4.22.5
devDependencies:
'@decky/rollup':
specifier: ^1.0.1
@@ -24,6 +28,9 @@ importers:
'@decky/ui':
specifier: ^4.7.2
version: 4.7.2
+ '@rollup/wasm-node':
+ specifier: 4.22.5
+ version: 4.22.5
'@types/react':
specifier: 18.3.3
version: 18.3.3
@@ -230,6 +237,11 @@ packages:
cpu: [x64]
os: [win32]
+ '@rollup/wasm-node@4.22.5':
+ resolution: {integrity: sha512-42RTxk/g1NFaTRT7yo9T0K1OeJPS7xXAWQdUaajjmPPzZmfWjcms+tns8IQkwSlVrBB3EvE2y/FGwGwHmFAqEg==}
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+ hasBin: true
+
'@types/estree@1.0.6':
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
@@ -1067,6 +1079,12 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.22.5':
optional: true
+ '@rollup/wasm-node@4.22.5':
+ dependencies:
+ '@types/estree': 1.0.6
+ optionalDependencies:
+ fsevents: 2.3.3
+
'@types/estree@1.0.6': {}
'@types/glob@7.2.0':