diff options
| -rwxr-xr-x | defaults/assets/fgmod-uninstaller.sh | 41 | ||||
| -rwxr-xr-x | defaults/assets/fgmod.sh | 92 | ||||
| -rw-r--r-- | main.py | 94 | ||||
| -rw-r--r-- | package.json | 10 | ||||
| -rw-r--r-- | src/api/index.ts | 2 | ||||
| -rw-r--r-- | src/components/ClipboardCommands.tsx | 11 | ||||
| -rw-r--r-- | src/components/CustomPathOverride.tsx | 17 | ||||
| -rw-r--r-- | src/components/InstalledGamesSection.tsx | 8 | ||||
| -rw-r--r-- | src/components/OptiScalerControls.tsx | 34 | ||||
| -rw-r--r-- | src/components/ResultDisplay.tsx | 4 | ||||
| -rw-r--r-- | src/components/SteamGamePatcher.tsx | 277 | ||||
| -rw-r--r-- | src/components/index.ts | 1 | ||||
| -rw-r--r-- | src/types.d.ts | 10 | ||||
| -rw-r--r-- | src/utils/constants.ts | 22 |
14 files changed, 528 insertions, 95 deletions
diff --git a/defaults/assets/fgmod-uninstaller.sh b/defaults/assets/fgmod-uninstaller.sh index 8c5e7b9..5e5143c 100755 --- a/defaults/assets/fgmod-uninstaller.sh +++ b/defaults/assets/fgmod-uninstaller.sh @@ -4,13 +4,13 @@ set -x exec > >(tee -i /tmp/fgmod-uninstaller.log) 2>&1 error_exit() { - echo "โ $1" + echo " $1" if [[ -n $STEAM_ZENITY ]]; then $STEAM_ZENITY --error --text "$1" else zenity --error --text "$1" || echo "Zenity failed to display error" fi - logger -t fgmod-uninstaller "โ ERROR: $1" + logger -t fgmod-uninstaller "ERROR: $1" exit 1 } @@ -101,20 +101,20 @@ cd "$exe_folder_path" || error_exit "Failed to change directory to $exe_folder_p # Verify current directory before proceeding [[ "$(pwd)" != "$exe_folder_path" ]] && error_exit "Unexpected working directory: $(pwd)" -logger -t fgmod-uninstaller "๐ข Uninstalling from: $exe_folder_path" +logger -t fgmod-uninstaller "Uninstalling from: $exe_folder_path" # === Remove OptiScaler Files === -echo "๐งน Removing 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..." +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..." +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" @@ -127,49 +127,52 @@ rm -f "nvngx.dll" "nvngx.ini" [[ -f "amd_fidelityfx_vk.dll.b" ]] && rm -f "amd_fidelityfx_vk.dll" # === Remove FG Mod Files === -echo "๐งน Removing frame generation 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 +echo " Removing NVAPI files..." +rm -f "fakenvapi.dll" "fakenvapi.ini" # v0.9.0-final rm -f "nvapi64.dll" "nvapi64.dll.b" # Legacy cleanup for older versions and backups # === Remove ASI Plugins === -echo "๐งน Removing ASI plugins directory..." +echo " Removing ASI plugins directory..." rm -rf "plugins" +# === Remove D3D12_Optiscaler directory (required by v0.9.0-final) === +rm -rf "D3D12_Optiscaler" + # === Remove Legacy Files === -echo "๐งน Removing 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..." +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" + echo " Restored original $dll" + logger -t fgmod-uninstaller "Restored original $dll" fi done # === Self-remove uninstaller === -echo "๐๏ธ Removing 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" +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..." + 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." + echo " Uninstallation complete. No game specified to run." fi
\ No newline at end of file diff --git a/defaults/assets/fgmod.sh b/defaults/assets/fgmod.sh index fa36558..decb981 100755 --- a/defaults/assets/fgmod.sh +++ b/defaults/assets/fgmod.sh @@ -4,13 +4,13 @@ set -x exec > >(tee -i /tmp/fgmod-install.log) 2>&1 error_exit() { - echo "โ $1" + echo " $1" if [[ -n $STEAM_ZENITY ]]; then $STEAM_ZENITY --error --text "$1" else zenity --error --text "$1" || echo "Zenity failed to display error" fi - logger -t fgmod "โ ERROR: $1" + logger -t fgmod "ERROR: $1" exit 1 } @@ -89,12 +89,12 @@ if [[ -d "$exe_folder_path/Engine" ]]; then exe_folder_path=$(dirname "$ue_exe") fi -[[ ! -d "$exe_folder_path" ]] && error_exit "โ Could not resolve game directory!" -[[ ! -w "$exe_folder_path" ]] && error_exit "๐ No write permission to the game folder!" +[[ ! -d "$exe_folder_path" ]] && error_exit " Could not resolve game directory!" +[[ ! -w "$exe_folder_path" ]] && error_exit " No write permission to the game folder!" -logger -t fgmod "๐ข Target directory: $exe_folder_path" -logger -t fgmod "๐งฉ Using DLL name: $dll_name" -logger -t fgmod "๐ Preserve INI: $preserve_ini" +logger -t fgmod "Target directory: $exe_folder_path" +logger -t fgmod "Using DLL name: $dll_name" +logger -t fgmod "Preserve INI: $preserve_ini" # === Cleanup Old Injectors === rm -f "$exe_folder_path"/{dxgi.dll,winmm.dll,nvngx.dll,_nvngx.dll,nvngx-wrapper.dll,dlss-enabler.dll,OptiScaler.dll} @@ -107,25 +107,25 @@ 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)" +echo " Cleaned up nvapi64.dll and backup (legacy fakenvapi conflicts)" # === 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" + 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" + 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" 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" + echo " Preserving existing OptiScaler.ini (user settings retained)" + logger -t fgmod "Existing OptiScaler.ini preserved in $exe_folder_path" 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" + 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" fi # === OptiScaler env variables Handling === @@ -137,13 +137,41 @@ fi # 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 +# === Migrate FGType โ FGInput/FGOutput (pre-v0.9-final INIs) === +# v0.9-final split the single FGType key into FGInput + FGOutput. Games that were +# patched with an older build will have FGType=<value> with no FGInput/FGOutput, +# causing the new DLL to silently use nofg. Fix that here on every launch. +_fgtype_ini="$exe_folder_path/OptiScaler.ini" +if grep -q '^FGType=' "$_fgtype_ini" 2>/dev/null; then + _fgtype_val=$(sed -n 's/^FGType=\(.*\)/\1/p' "$_fgtype_ini") + echo " Migrating FGType=$_fgtype_val โ FGInput/FGOutput in OptiScaler.ini" + logger -t fgmod "Migrating FGType=$_fgtype_val โ FGInput/FGOutput" + if grep -q '^FGInput=' "$_fgtype_ini"; then + # FGInput already present โ INI already in v0.9-final format; just drop FGType + sed -i '/^FGType=/d' "$_fgtype_ini" || true + else + # Replace FGType=X with FGInput=X + FGOutput=X + sed -i "s/^FGType=.*$/FGInput=$_fgtype_val\nFGOutput=$_fgtype_val/" "$_fgtype_ini" || true + fi +fi +unset _fgtype_ini _fgtype_val + # === ASI Plugins Directory === if [[ -d "$fgmod_path/plugins" ]]; then - echo "๐ Installing ASI plugins directory" + 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" + logger -t fgmod "ASI plugins directory installed to $exe_folder_path" else - echo "โ ๏ธ No plugins directory found in fgmod" + echo " No plugins directory found in fgmod" +fi + +# === D3D12_Optiscaler Directory (required for FSR4/FidelityFX DX12 path) === +if [[ -d "$fgmod_path/D3D12_Optiscaler" ]]; then + echo " Installing D3D12_Optiscaler directory" + cp -r "$fgmod_path/D3D12_Optiscaler" "$exe_folder_path/" || true + logger -t fgmod "D3D12_Optiscaler directory installed to $exe_folder_path" +else + echo " No D3D12_Optiscaler directory found in fgmod" fi # === Supporting Libraries === @@ -159,27 +187,27 @@ 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 +# Note: dlssg_to_fsr3.ini is not included in v0.9.0-final archive # === FakeNVAPI Files === # Remove legacy nvapi64.dll to avoid conflicts # rm -f "$exe_folder_path/nvapi64.dll" -# echo "๐งน Removed legacy 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" +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 +# Note: d3dcompiler_47.dll is not included in v0.9.0-final 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" +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 @@ -195,7 +223,13 @@ if [[ $# -gt 1 ]]; then # Execute the original command export SteamDeck=0 - export WINEDLLOVERRIDES="$WINEDLLOVERRIDES,dxgi=n,b" + # Build WINEDLLOVERRIDES from the actual proxy DLL name (strip extension to get the stem) + if [[ "$dll_name" == *.dll ]]; then + _wine_dll="${dll_name%.dll}" + export WINEDLLOVERRIDES="$WINEDLLOVERRIDES,${_wine_dll}=n,b" + unset _wine_dll + fi + # .asi files are loaded by an ASI loader โ no WINEDLLOVERRIDES entry needed # Filter out leading -- separators (from Steam launch options) while [[ $# -gt 0 && "$1" == "--" ]]; do @@ -10,6 +10,16 @@ from pathlib import Path # Set to False or comment out this constant to skip the overwrite by default. UPSCALER_OVERWRITE_ENABLED = True +VALID_DLL_NAMES = { + "dxgi.dll", + "winmm.dll", + "dbghelp.dll", + "version.dll", + "wininet.dll", + "winhttp.dll", + "OptiScaler.asi", +} + INJECTOR_FILENAMES = [ "dxgi.dll", "winmm.dll", @@ -128,6 +138,50 @@ class Plugin: decky.logger.error(f"Failed to copy launcher scripts: {e}") return False + def _migrate_optiscaler_ini(self, ini_file): + """Migrate pre-v0.9-final OptiScaler.ini: replace FGType with FGInput + FGOutput. + + v0.9-final split the single FGType key into separate FGInput and FGOutput keys. + Games already patched with an older build will have FGType=<value> in their + per-game INI but no FGInput/FGOutput entries, causing the new DLL to silently + fall back to nofg. This migration runs at patch-time and at every fgmod.sh + launch so users never have to manually touch their INI. + """ + try: + if not ini_file.exists(): + return False + + with open(ini_file, 'r') as f: + content = f.read() + + fg_type_match = re.search(r'^FGType\s*=\s*(\S+)', content, re.MULTILINE) + if not fg_type_match: + return True # Nothing to migrate + + fg_value = fg_type_match.group(1) + + if re.search(r'^FGInput\s*=', content, re.MULTILINE): + # FGInput already present (INI already in v0.9-final format); + # just remove the now-unknown FGType line. + content = re.sub(r'^FGType\s*=\s*\S+\n?', '', content, flags=re.MULTILINE) + decky.logger.info(f"Removed stale FGType from {ini_file} (FGInput already present)") + else: + # Replace the single FGType=X line with FGInput=X then FGOutput=X + content = re.sub( + r'^FGType\s*=\s*\S+', + f'FGInput={fg_value}\nFGOutput={fg_value}', + content, + flags=re.MULTILINE + ) + decky.logger.info(f"Migrated FGType={fg_value} โ FGInput={fg_value}, FGOutput={fg_value} in {ini_file}") + + with open(ini_file, 'w') as f: + f.write(content) + return True + except Exception as e: + decky.logger.error(f"Failed to migrate OptiScaler.ini: {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.""" try: @@ -156,8 +210,11 @@ class Plugin: 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 FGInput=auto with FGInput=nukems (final v0.9+ split FGType into FGInput/FGOutput) + updated_content = re.sub(r'FGInput\s*=\s*auto', 'FGInput=nukems', content) + + # Replace FGOutput=auto with FGOutput=nukems + updated_content = re.sub(r'FGOutput\s*=\s*auto', 'FGOutput=nukems', updated_content) # Replace Fsr4Update=auto with Fsr4Update=true updated_content = re.sub(r'Fsr4Update\s*=\s*auto', 'Fsr4Update=true', updated_content) @@ -174,7 +231,7 @@ class Plugin: 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") + decky.logger.info("Modified OptiScaler.ini to set FGInput=nukems, FGOutput=nukems, Fsr4Update=true, LoadAsiPlugins=true, Path=plugins, UseHQFont=false") return True else: decky.logger.warning(f"OptiScaler.ini not found at {ini_file}") @@ -267,7 +324,7 @@ class Plugin: } # 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 + # Note: v0.9.0-final 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 @@ -433,7 +490,7 @@ class Plugin: "OptiScaler.dll", "OptiScaler.ini", "dlssg_to_fsr3_amd_is_better.dll", - "fakenvapi.dll", # v0.9.0-pre3+ includes fakenvapi.dll in archive + "fakenvapi.dll", # v0.9.0-final includes fakenvapi.dll in archive "fakenvapi.ini", "nvngx.dll", "amd_fidelityfx_dx12.dll", @@ -442,8 +499,8 @@ class Plugin: "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 + "libxess_fg.dll", # added in v0.9.0 + "libxell.dll", # added in v0.9.0 "fgmod", "fgmod-uninstaller.sh", "update-optiscaler-config.py" @@ -476,7 +533,7 @@ class Plugin: decky.logger.info(f"Resolved directory {directory} to absolute path {target}") return target - def _manual_patch_directory_impl(self, directory: Path) -> dict: + def _manual_patch_directory_impl(self, directory: Path, dll_name: str = "dxgi.dll") -> dict: fgmod_path = Path(decky.HOME) / "fgmod" if not fgmod_path.exists(): return { @@ -491,7 +548,6 @@ class Plugin: "message": "OptiScaler.dll not found in ~/fgmod. Reinstall OptiScaler.", } - dll_name = "dxgi.dll" preserve_ini = True try: @@ -539,6 +595,7 @@ class Plugin: decky.logger.warning("No OptiScaler.ini found to copy") if target_ini.exists(): + self._migrate_optiscaler_ini(target_ini) self._disable_hq_font_auto(target_ini) plugins_src = fgmod_path / "plugins" @@ -549,6 +606,14 @@ class Plugin: else: decky.logger.warning("Plugins directory missing in fgmod bundle") + d3d12_src = fgmod_path / "D3D12_Optiscaler" + d3d12_dest = directory / "D3D12_Optiscaler" + if d3d12_src.exists(): + shutil.copytree(d3d12_src, d3d12_dest, dirs_exist_ok=True) + decky.logger.info(f"Copied D3D12_Optiscaler directory to {d3d12_dest}") + else: + decky.logger.warning("D3D12_Optiscaler directory missing in fgmod bundle") + copied_support = [] missing_support = [] for filename in SUPPORT_FILES: @@ -611,6 +676,11 @@ class Plugin: shutil.rmtree(plugins_dir, ignore_errors=True) decky.logger.info(f"Removed plugins directory at {plugins_dir}") + d3d12_dir = directory / "D3D12_Optiscaler" + if d3d12_dir.exists(): + shutil.rmtree(d3d12_dir, ignore_errors=True) + decky.logger.info(f"Removed D3D12_Optiscaler directory from {d3d12_dir}") + restored_backups = [] for dll in ORIGINAL_DLL_BACKUPS: backup = directory / f"{dll}.b" @@ -711,14 +781,16 @@ class Plugin: async def log_error(self, error: str) -> None: decky.logger.error(f"FRONTEND: {error}") - async def manual_patch_directory(self, directory: str) -> dict: + async def manual_patch_directory(self, directory: str, dll_name: str = "dxgi.dll") -> dict: + if dll_name not in VALID_DLL_NAMES: + return {"status": "error", "message": f"Invalid proxy DLL name: {dll_name}"} 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 self._manual_patch_directory_impl(target_dir, dll_name) async def manual_unpatch_directory(self, directory: str) -> dict: try: diff --git a/package.json b/package.json index 9c95117..6448a0d 100644 --- a/package.json +++ b/package.json @@ -53,13 +53,13 @@ "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": "a988ce2c0a86bba58a6313659d1ed2ab78f994dbdfab246394a2e4293ac68010", + "url": "https://github.com/optiscaler/OptiScaler/releases/download/v0.9/Optiscaler_0.9.0-final.20260401._AF.7z", + "name": "Optiscaler_0.9.0-final.20260401._AF.7z" }, { - "sha256hash": "2604c0b392072d715b400b2f89434274de31995a4b6e68ce38250ebbd3f6c5fc", - "url": "https://github.com/xXJSONDeruloXx/OptiScaler-Bleeding-Edge/releases/download/opti-9-pre-11/amd_fidelityfx_upscaler_dx12.dll", + "sha256hash": "c7720bc16bede334f59a1a32cd22edbcbbb159685ed5240e61350a5fb0bc8a94", + "url": "https://github.com/xXJSONDeruloXx/OptiScaler-Bleeding-Edge/releases/download/bins-for-4.0.2.c/amd_fidelityfx_upscaler_dx12.dll", "name": "amd_fidelityfx_upscaler_dx12.dll" }, { diff --git a/src/api/index.ts b/src/api/index.ts index df52fee..226f29f 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -28,7 +28,7 @@ export const getPathDefaults = callable< >("get_path_defaults"); export const runManualPatch = callable< - [string], + [string, string], { status: string; message?: string; output?: string } >("manual_patch_directory"); diff --git a/src/components/ClipboardCommands.tsx b/src/components/ClipboardCommands.tsx index 5a6f38f..7bbd12d 100644 --- a/src/components/ClipboardCommands.tsx +++ b/src/components/ClipboardCommands.tsx @@ -2,19 +2,20 @@ import { SmartClipboardButton } from "./SmartClipboardButton"; interface ClipboardCommandsProps { pathExists: boolean | null; + dllName: string; } -export function ClipboardCommands({ pathExists }: ClipboardCommandsProps) { +export function ClipboardCommands({ pathExists, dllName }: ClipboardCommandsProps) { if (pathExists !== true) return null; return ( <> - <SmartClipboardButton - command="~/fgmod/fgmod %command%" + <SmartClipboardButton + command={`DLL=${dllName} ~/fgmod/fgmod %command%`} buttonText="Copy Patch Command" /> - - <SmartClipboardButton + + <SmartClipboardButton command="~/fgmod/fgmod-uninstaller.sh %command%" buttonText="Copy Unpatch Command" /> diff --git a/src/components/CustomPathOverride.tsx b/src/components/CustomPathOverride.tsx index ffc4b1f..4effc6c 100644 --- a/src/components/CustomPathOverride.tsx +++ b/src/components/CustomPathOverride.tsx @@ -36,6 +36,7 @@ const ensureDirectory = (value: string) => { interface ManualPatchControlsProps { isAvailable: boolean; onManualModeChange?: (enabled: boolean) => void; + dllName: string; } interface PickerState { @@ -56,7 +57,7 @@ const formatResultMessage = (result: ApiResponse | null) => { return result.message || result.output || "Operation failed."; }; -export const ManualPatchControls = ({ isAvailable, onManualModeChange }: ManualPatchControlsProps) => { +export const ManualPatchControls = ({ isAvailable, onManualModeChange, dllName }: ManualPatchControlsProps) => { const [isEnabled, setEnabled] = useState(false); const [defaults, setDefaults] = useState<PathDefaults>(INITIAL_DEFAULTS); const [pickerState, setPickerState] = useState<PickerState>(INITIAL_PICKER_STATE); @@ -165,7 +166,7 @@ export const ManualPatchControls = ({ isAvailable, onManualModeChange }: ManualP try { const response = action === "patch" - ? await runManualPatch(selectedPath) + ? await runManualPatch(selectedPath, dllName) : await runManualUnpatch(selectedPath); setOperationResult(response ?? { status: "error", message: "No response from backend." }); } catch (err) { @@ -177,7 +178,7 @@ export const ManualPatchControls = ({ isAvailable, onManualModeChange }: ManualP setBusy(false); } }, - [selectedPath] + [selectedPath, dllName] ); const handleToggle = (value: boolean) => { @@ -216,7 +217,11 @@ export const ManualPatchControls = ({ isAvailable, onManualModeChange }: ManualP {canInteract && ( <> <SmartClipboardButton - command='WINEDLLOVERRIDES="dxgi=n,b" SteamDeck=0 %command%' + command={ + dllName === "OptiScaler.asi" + ? "SteamDeck=0 %command%" + : `WINEDLLOVERRIDES="${dllName.replace(".dll", "")}=n,b" SteamDeck=0 %command%` + } buttonText="Manual launch cmd" /> <PanelSectionRow> @@ -234,9 +239,7 @@ export const ManualPatchControls = ({ isAvailable, onManualModeChange }: ManualP <Field label="Picker error" description={pickerState.lastError} - > - โ ๏ธ - </Field> + /> </PanelSectionRow> )} diff --git a/src/components/InstalledGamesSection.tsx b/src/components/InstalledGamesSection.tsx index 71278d7..04d653b 100644 --- a/src/components/InstalledGamesSection.tsx +++ b/src/components/InstalledGamesSection.tsx @@ -42,14 +42,14 @@ export function InstalledGamesSection() { <ConfirmModal strTitle={`Enable Frame Generation for ${selectedGame.name}?`} strDescription={ - "โ ๏ธ Important: This plugin does not automatically unpatch games when uninstalled. If you uninstall this plugin or experience game issues, use the 'Disable Frame Generation' option or verify game file integrity through Steam." + "Important: This plugin does not automatically unpatch games when uninstalled. If you uninstall this plugin or experience game issues, use the 'Disable Frame Generation' option or verify game file integrity through Steam." } strOKButtonText="Enable Frame Generation" strCancelButtonText="Cancel" onOK={async () => { try { await SteamClient.Apps.SetAppLaunchOptions(selectedGame.appid, '~/fgmod/fgmod %COMMAND%'); - setResult(`โ Frame generation enabled for ${selectedGame.name}. Launch the game, enable DLSS in graphics settings, then press Insert to access OptiScaler options.`); + setResult(`Frame generation enabled for ${selectedGame.name}. Launch the game, enable DLSS in graphics settings, then press Insert to access OptiScaler options.`); } catch (error) { logError('handlePatchClick: ' + String(error)); setResult(error instanceof Error ? `Error: ${error.message}` : 'Error enabling frame generation'); @@ -64,7 +64,7 @@ export function InstalledGamesSection() { try { await SteamClient.Apps.SetAppLaunchOptions(selectedGame.appid, '~/fgmod/fgmod-uninstaller.sh %COMMAND%'); - setResult(`โ Frame generation will be disabled on next launch of ${selectedGame.name}.`); + setResult(`Frame generation will be disabled on next launch of ${selectedGame.name}.`); } catch (error) { logError('handleUnpatchClick: ' + String(error)); setResult(error instanceof Error ? `Error: ${error.message}` : 'Error disabling frame generation'); @@ -96,7 +96,7 @@ export function InstalledGamesSection() { ...STYLES.preWrap, ...(result.includes('Error') ? STYLES.statusNotInstalled : STYLES.statusInstalled) }}> - {result.includes('Error') ? 'โ' : 'โ
'} {result} + {result} </div> </PanelSectionRow> ) : null} diff --git a/src/components/OptiScalerControls.tsx b/src/components/OptiScalerControls.tsx index 468683c..f88e8f9 100644 --- a/src/components/OptiScalerControls.tsx +++ b/src/components/OptiScalerControls.tsx @@ -1,9 +1,9 @@ import { useState, useEffect } from "react"; -import { PanelSection } from "@decky/ui"; +import { DropdownItem, PanelSection, PanelSectionRow } from "@decky/ui"; import { runInstallFGMod, runUninstallFGMod } from "../api"; import { OperationResult } from "./ResultDisplay"; import { createAutoCleanupTimer } from "../utils"; -import { TIMEOUTS } from "../utils/constants"; +import { TIMEOUTS, PROXY_DLL_OPTIONS, DEFAULT_PROXY_DLL } from "../utils/constants"; import { InstallationStatus } from "./InstallationStatus"; import { OptiScalerHeader } from "./OptiScalerHeader"; import { ClipboardCommands } from "./ClipboardCommands"; @@ -11,6 +11,7 @@ import { InstructionCard } from "./InstructionCard"; import { OptiScalerWiki } from "./OptiScalerWiki"; import { UninstallButton } from "./UninstallButton"; import { ManualPatchControls } from "./CustomPathOverride"; +import { SteamGamePatcher } from "./SteamGamePatcher"; interface OptiScalerControlsProps { pathExists: boolean | null; @@ -23,6 +24,7 @@ export function OptiScalerControls({ pathExists, setPathExists }: OptiScalerCont const [installResult, setInstallResult] = useState<OperationResult | null>(null); const [uninstallResult, setUninstallResult] = useState<OperationResult | null>(null); const [manualModeEnabled, setManualModeEnabled] = useState(false); + const [dllName, setDllName] = useState<string>(DEFAULT_PROXY_DLL); useEffect(() => { if (installResult) { return createAutoCleanupTimer(() => setInstallResult(null), TIMEOUTS.resultDisplay); @@ -76,18 +78,34 @@ export function OptiScalerControls({ pathExists, setPathExists }: OptiScalerCont /> <OptiScalerHeader pathExists={pathExists} /> - + + {pathExists === true && ( + <PanelSectionRow> + <DropdownItem + label="Proxy DLL name" + description={PROXY_DLL_OPTIONS.find((o) => o.value === dllName)?.hint} + menuLabel="Proxy DLL name" + selectedOption={dllName} + rgOptions={PROXY_DLL_OPTIONS.map((o) => ({ data: o.value, label: o.label }))} + onChange={(option) => setDllName(String(option.data))} + /> + </PanelSectionRow> + )} + + {pathExists === true && ( + <SteamGamePatcher dllName={dllName} /> + )} + + <ClipboardCommands pathExists={pathExists} dllName={dllName} /> + <ManualPatchControls isAvailable={pathExists === true} onManualModeChange={setManualModeEnabled} + dllName={dllName} /> {!manualModeEnabled && ( - <> - <ClipboardCommands pathExists={pathExists} /> - - <InstructionCard pathExists={pathExists} /> - </> + <InstructionCard pathExists={pathExists} /> )} <OptiScalerWiki pathExists={pathExists} /> diff --git a/src/components/ResultDisplay.tsx b/src/components/ResultDisplay.tsx index bcd66c0..b54e41d 100644 --- a/src/components/ResultDisplay.tsx +++ b/src/components/ResultDisplay.tsx @@ -19,13 +19,13 @@ export const ResultDisplay: FC<ResultDisplayProps> = ({ result }) => { <div style={isSuccess ? STYLES.statusInstalled : STYLES.statusNotInstalled}> {isSuccess ? ( <> - โ
{result.output?.includes("uninstall") || result.output?.includes("remov") + {result.output?.includes("uninstall") || result.output?.includes("remov") ? "OptiScaler mod removed successfully" : "OptiScaler mod installed successfully"} </> ) : ( <> - โ <strong>Error:</strong> {result.message || "Operation failed"} + <strong>Error:</strong> {result.message || "Operation failed"} </> )} {result.output && !isSuccess && ( diff --git a/src/components/SteamGamePatcher.tsx b/src/components/SteamGamePatcher.tsx new file mode 100644 index 0000000..06c373c --- /dev/null +++ b/src/components/SteamGamePatcher.tsx @@ -0,0 +1,277 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { ButtonItem, DropdownItem, Field, PanelSectionRow } from "@decky/ui"; +import { listInstalledGames } from "../api"; +import { createAutoCleanupTimer } from "../utils"; +import { TIMEOUTS } from "../utils/constants"; + +// โโโ SteamClient helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ + +/** + * Wrap the callback-based RegisterForAppDetails in a Promise. + * Resolves with the current launch options string, or "" if SteamClient is + * unavailable (e.g. desktop / dev mode). Times out after 5 seconds. + */ +const getSteamLaunchOptions = (appId: number): Promise<string> => + new Promise((resolve, reject) => { + if ( + typeof SteamClient === "undefined" || + !SteamClient?.Apps?.RegisterForAppDetails + ) { + resolve(""); + return; + } + + let settled = false; + let unregister = () => {}; + + const timeout = window.setTimeout(() => { + if (settled) return; + settled = true; + unregister(); + reject(new Error("Timed out reading launch options.")); + }, 5000); + + const registration = SteamClient.Apps.RegisterForAppDetails( + appId, + (details: { strLaunchOptions?: string }) => { + if (settled) return; + settled = true; + window.clearTimeout(timeout); + unregister(); + resolve(details?.strLaunchOptions ?? ""); + } + ); + + unregister = registration.unregister; + }); + +const setSteamLaunchOptions = (appId: number, options: string): void => { + if ( + typeof SteamClient === "undefined" || + !SteamClient?.Apps?.SetAppLaunchOptions + ) { + throw new Error("SteamClient.Apps.SetAppLaunchOptions is not available."); + } + SteamClient.Apps.SetAppLaunchOptions(appId, options); +}; + +// โโโ Helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ + +/** Remove any fgmod invocation from a launch options string, keeping the rest. */ +const stripFgmod = (opts: string): string => + opts + .replace(/DLL=\S+\s+~\/fgmod\/fgmod\s+%command%/g, "") + .replace(/~\/fgmod\/fgmod\s+%command%/g, "") + .trim(); + +/** Extract the DLL= value from a launch options string, if present. */ +const extractDllName = (opts: string): string | null => { + const m = opts.match(/DLL=(\S+)\s+~\/fgmod\/fgmod/); + return m ? m[1] : null; +}; + +// โโโ Component โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ + +interface SteamGamePatcherProps { + dllName: string; +} + +type GameEntry = { appid: string; name: string }; + +export function SteamGamePatcher({ dllName }: SteamGamePatcherProps) { + const [games, setGames] = useState<GameEntry[]>([]); + const [gamesLoading, setGamesLoading] = useState(true); + const [selectedAppId, setSelectedAppId] = useState<string>(""); + const [launchOptions, setLaunchOptions] = useState<string>(""); + const [launchOptionsLoading, setLaunchOptionsLoading] = useState(false); + const [busy, setBusy] = useState(false); + const [resultMessage, setResultMessage] = useState<string>(""); + + // Auto-clear result message + useEffect(() => { + if (resultMessage) { + return createAutoCleanupTimer( + () => setResultMessage(""), + TIMEOUTS.resultDisplay + ); + } + return undefined; + }, [resultMessage]); + + // Load game list on mount + useEffect(() => { + let cancelled = false; + (async () => { + setGamesLoading(true); + try { + const result = await listInstalledGames(); + if (cancelled) return; + if (result.status === "success" && result.games.length > 0) { + setGames(result.games); + setSelectedAppId(result.games[0].appid); + } + } catch (e) { + console.error("SteamGamePatcher: failed to load games", e); + } finally { + if (!cancelled) setGamesLoading(false); + } + })(); + return () => { + cancelled = true; + }; + }, []); + + // Reload launch options when selected game changes + useEffect(() => { + if (!selectedAppId) { + setLaunchOptions(""); + return; + } + let cancelled = false; + (async () => { + setLaunchOptionsLoading(true); + try { + const opts = await getSteamLaunchOptions(Number(selectedAppId)); + if (!cancelled) setLaunchOptions(opts); + } catch { + if (!cancelled) setLaunchOptions(""); + } finally { + if (!cancelled) setLaunchOptionsLoading(false); + } + })(); + return () => { + cancelled = true; + }; + }, [selectedAppId]); + + const targetCommand = `DLL=${dllName} ~/fgmod/fgmod %command%`; + const isManaged = launchOptions.includes("fgmod/fgmod"); + const activeDll = useMemo(() => extractDllName(launchOptions), [launchOptions]); + const selectedGame = useMemo( + () => games.find((g) => g.appid === selectedAppId) ?? null, + [games, selectedAppId] + ); + + const handleSet = useCallback(() => { + if (!selectedAppId || busy) return; + setBusy(true); + try { + setSteamLaunchOptions(Number(selectedAppId), targetCommand); + setLaunchOptions(targetCommand); + setResultMessage( + `Launch options set for ${selectedGame?.name ?? selectedAppId}` + ); + } catch (e) { + setResultMessage(`Error: ${e instanceof Error ? e.message : String(e)}`); + } finally { + setBusy(false); + } + }, [selectedAppId, targetCommand, selectedGame, busy]); + + const handleRemove = useCallback(() => { + if (!selectedAppId || busy) return; + setBusy(true); + try { + const stripped = stripFgmod(launchOptions); + setSteamLaunchOptions(Number(selectedAppId), stripped); + setLaunchOptions(stripped); + setResultMessage( + `Removed fgmod from ${selectedGame?.name ?? selectedAppId}` + ); + } catch (e) { + setResultMessage(`Error: ${e instanceof Error ? e.message : String(e)}`); + } finally { + setBusy(false); + } + }, [selectedAppId, launchOptions, selectedGame, busy]); + + // โโ Status display โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ + const statusText = useMemo(() => { + if (!selectedGame) return "โ"; + if (launchOptionsLoading) return "Loading..."; + if (!isManaged) return "Not set"; + if (activeDll && activeDll !== dllName) + return `Active โ ${activeDll} ยท switch to apply ${dllName}`; + return `Active โ ${activeDll ?? dllName}`; + }, [selectedGame, launchOptionsLoading, isManaged, activeDll, dllName]); + + const statusColor = useMemo(() => { + if (!isManaged || launchOptionsLoading) return undefined; + if (activeDll && activeDll !== dllName) return "#ffd866"; // yellow โ different DLL selected + return "#3fb950"; // green โ active and matching + }, [isManaged, launchOptionsLoading, activeDll, dllName]); + + const setButtonLabel = useMemo(() => { + if (busy) return "Applying..."; + if (!isManaged) return "Enable for this game"; + if (activeDll && activeDll !== dllName) return `Switch to ${dllName}`; + return "Re-apply"; + }, [busy, isManaged, activeDll, dllName]); + + return ( + <> + <PanelSectionRow> + <DropdownItem + label="Steam game" + menuLabel="Select a Steam game" + strDefaultLabel={ + gamesLoading ? "Loading games..." : "Choose a game" + } + disabled={gamesLoading || games.length === 0} + selectedOption={selectedAppId} + rgOptions={games.map((g) => ({ data: g.appid, label: g.name }))} + onChange={(option) => { + setSelectedAppId(String(option.data)); + setResultMessage(""); + }} + /> + </PanelSectionRow> + + {selectedGame && ( + <> + <PanelSectionRow> + <Field focusable label="Launch options status"> + {statusColor ? ( + <span style={{ color: statusColor, fontWeight: 600 }}> + {statusText} + </span> + ) : ( + statusText + )} + </Field> + </PanelSectionRow> + + <PanelSectionRow> + <ButtonItem + layout="below" + disabled={busy || launchOptionsLoading} + onClick={handleSet} + > + {setButtonLabel} + </ButtonItem> + </PanelSectionRow> + + {isManaged && ( + <PanelSectionRow> + <ButtonItem + layout="below" + disabled={busy} + onClick={handleRemove} + > + {busy ? "Removing..." : "Remove from launch options"} + </ButtonItem> + </PanelSectionRow> + )} + + {resultMessage && ( + <PanelSectionRow> + <Field focusable label="Result"> + {resultMessage} + </Field> + </PanelSectionRow> + )} + </> + )} + </> + ); +} diff --git a/src/components/index.ts b/src/components/index.ts index cd599ba..ad47347 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -9,3 +9,4 @@ export { UninstallButton } from './UninstallButton'; export { SmartClipboardButton } from './SmartClipboardButton'; export { ResultDisplay } from './ResultDisplay'; export { ManualPatchControls } from './CustomPathOverride'; +export { SteamGamePatcher } from './SteamGamePatcher'; diff --git a/src/types.d.ts b/src/types.d.ts index dfc0472..4077a9e 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -12,3 +12,13 @@ declare module "*.jpg" { const content: string; export default content; } + +declare const SteamClient: { + Apps: { + RegisterForAppDetails( + appId: number, + callback: (details: { strLaunchOptions?: string }) => void + ): { unregister: () => void }; + SetAppLaunchOptions(appId: number, options: string): void; + }; +}; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 1f583c0..7fa6970 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -45,6 +45,20 @@ export const STYLES = { } }; +// Proxy DLL name options for OptiScaler injection +export const PROXY_DLL_OPTIONS = [ + { value: "dxgi.dll", label: "dxgi.dll (default)", hint: "Works for most DX12 games. Default." }, + { value: "winmm.dll", label: "winmm.dll", hint: "Use when dxgi.dll conflicts with an existing game file." }, + { value: "version.dll", label: "version.dll", hint: "Common fallback; works well with many launchers." }, + { value: "dbghelp.dll", label: "dbghelp.dll", hint: "Use for debug helper hook paths." }, + { value: "winhttp.dll", label: "winhttp.dll", hint: "Use when other DLL names conflict." }, + { value: "wininet.dll", label: "wininet.dll", hint: "Use when other DLL names conflict." }, + { value: "OptiScaler.asi", label: "OptiScaler.asi", hint: "For ASI loaders. Requires an ASI loader already installed in the game." }, +] as const; + +export type ProxyDllValue = typeof PROXY_DLL_OPTIONS[number]["value"]; +export const DEFAULT_PROXY_DLL: ProxyDllValue = "dxgi.dll"; + // Common timeout values export const TIMEOUTS = { resultDisplay: 5000, // 5 seconds @@ -53,14 +67,14 @@ export const TIMEOUTS = { // Message strings export const MESSAGES = { - modInstalled: "โ
OptiScaler Mod Installed", - modNotInstalled: "โ OptiScaler Mod Not Installed", + modInstalled: "OptiScaler Mod Installed", + modNotInstalled: "OptiScaler Mod Not Installed", installing: "Installing OptiScaler...", installButton: "Setup OptiScaler Mod", uninstalling: "Removing OptiScaler...", uninstallButton: "Remove OptiScaler Mod", - installSuccess: "โ
OptiScaler mod setup successfully!", - uninstallSuccess: "โ
OptiScaler mod removed successfully.", + installSuccess: "OptiScaler mod setup successfully!", + uninstallSuccess: "OptiScaler mod removed successfully.", instructionTitle: "How to Use:", instructionText: "Click 'Copy Patch Command' or 'Copy Unpatch Command', then go to your game's properties, and paste the command into the Launch Options field.\n\nIn-game: Enable DLSS in graphics settings to unlock FSR 3.1/XeSS 2.0 in DirectX12 Games.\n\nFor extended OptiScaler options, assign a back button to a keyboard's 'Insert' key." }; |
