diff options
| author | xXJsonDeruloXx <danielhimebauch@gmail.com> | 2026-03-20 17:32:05 -0400 |
|---|---|---|
| committer | xXJsonDeruloXx <danielhimebauch@gmail.com> | 2026-03-20 17:32:05 -0400 |
| commit | ca5db2231b8554d1377dd449f6fb9c736e3d6386 (patch) | |
| tree | 0c3a52bebfa5602a6499b1fcaa1fea5539632c1e | |
| parent | ef469a8036e3b3f129a753dad4cf04fad3ca92f7 (diff) | |
| download | Decky-Framegen-ca5db2231b8554d1377dd449f6fb9c736e3d6386.tar.gz Decky-Framegen-ca5db2231b8554d1377dd449f6fb9c736e3d6386.zip | |
Implement prefix-managed OptiScaler runtime
| -rwxr-xr-x | .vscode/build.sh | 28 | ||||
| -rwxr-xr-x | defaults/assets/fgmod-uninstaller.sh | 235 | ||||
| -rwxr-xr-x | defaults/assets/fgmod.sh | 335 | ||||
| -rw-r--r-- | main.py | 904 | ||||
| -rw-r--r-- | package.json | 33 | ||||
| -rw-r--r-- | pnpm-lock.yaml | 18 |
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" @@ -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': |
