summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md42
-rwxr-xr-xdefaults/assets/fgmod-uninstaller.sh54
-rwxr-xr-xdefaults/assets/fgmod.sh112
-rw-r--r--defaults/assets/update-optiscaler-config.py110
-rw-r--r--main.py137
-rw-r--r--package.json12
-rw-r--r--src/api/index.ts2
-rw-r--r--src/components/ClipboardCommands.tsx11
-rw-r--r--src/components/CustomPathOverride.tsx17
-rw-r--r--src/components/InstalledGamesSection.tsx8
-rw-r--r--src/components/OptiScalerControls.tsx34
-rw-r--r--src/components/ResultDisplay.tsx4
-rw-r--r--src/components/SteamGamePatcher.tsx277
-rw-r--r--src/components/index.ts1
-rw-r--r--src/types.d.ts10
-rw-r--r--src/utils/constants.ts22
16 files changed, 749 insertions, 104 deletions
diff --git a/README.md b/README.md
index 70b8131..1b08742 100644
--- a/README.md
+++ b/README.md
@@ -35,10 +35,50 @@ This plugin uses OptiScaler to replace DLSS calls with FSR3/FSR3.1, giving you:
- Click "Copy Unpatch Command" and replace the launch options with: `~/fgmod/fgmod-uninstaller.sh %command%`
- Run the game at least once to make the uninstaller script run. After you can leave the launch option or remove it
+### Configuring OptiScaler via Environment Variables
+As of v0.15.1, you can update OptiScaler settings before a game launches by adding environment variables.
+This is useful if you plan to use the same settings across multiple games so they are pre-configured by the time you launch them.
+
+For example, considering the following sample from the OptiScaler.ini config file:
+```
+[Upscalers]
+Dx11Upscaler=auto
+Dx12Upscaler=auto
+VulkanUpscaler=auto
+
+[FrameGen]
+Enabled=auto
+FGInput=auto
+FGOutput=auto
+DebugView=auto
+DrawUIOverFG=auto
+```
+We can decide to set `Dx12Upscaler=fsr31` to enable FSR4 in DX12 games by default. This works because the option name `Dx12Upscaler` is unique throughout the file but for options that appear multiple times like `Enabled`, you can prefix the option name with the section name like `FrameGen_Enabled=true`.
+You can provide section names for all options if you want to be explicit. You can also prefix `Section_Option` with `OptiScaler` to ensure no conflict with other commands.
+
+Here's the breakdown of supported formats:
+- `OptiScaler_Section_Option=value` - Full format (foolproof)
+- `Section_Option=value` - Short format (recommended)
+- `Option=value` - Minimal format (only works if the option name appears once in OptiScaler.ini)
+
+**Example:**
+```bash
+# Enable frame generation with XeFG output
+FrameGen_Enabled=true FGInput=fsrfg FGOutput=xefg ~/fgmod/fgmod %command%
+
+# Set DX12 upscaler to FSR 3.1 (Upgrades to FSR4)
+Dx12Upscaler=fsr31 ~/fgmod/fgmod %command%
+```
+
+**Notes:**
+- Environment variables override the OptiScaler.ini file on each game launch
+- Hyphenated section names like `[V-Sync]` can be accessed like `VSync_Option=value`
+- If an option name appears in multiple sections of the OptiScaler.ini file, use the `Section_Option` or `OptiScaler_Section_Option` format
+
## Technical Details
### What's Included
-- **[OptiScaler_v0.7.9](https://github.com/optiscaler/OptiScaler/releases/tag/v0.7.9)**: Latest bleeding-edge build (as of writing), with new features such as OptiFG for adding FG to games without any FG (highly experimental)
+- **[OptiScaler 0.9.0-pre11](https://github.com/xXJSONDeruloXx/OptiScaler-Bleeding-Edge/releases/tag/opti-9-pre-11)**: Bleeding-edge OptiScaler bundle used by this plugin, paired with the RDNA2-optimized `amd_fidelityfx_upscaler_dx12.dll` override for Steam Deck compatibility
- **Nukem9's DLSSG to FSR3 mod**: Allows use of DLSS inputs for FSR frame gen outputs, and xess or FSR upscaling outputs
- **FakeNVAPI**: NVIDIA API emulation for AMD/Intel GPUs, to make DLSS options selectable in game
- **Supporting Libraries**: All required DX12/Vulkan libraries (libxess.dll, amd_fidelityfx, etc.)
diff --git a/defaults/assets/fgmod-uninstaller.sh b/defaults/assets/fgmod-uninstaller.sh
index 38abcf4..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,66 +101,78 @@ 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..."
-rm -f "libxess.dll" "libxess_dx11.dll" "libxess_fg.dll" "libxell.dll" "nvngx.dll" "nvngx.ini"
-rm -f "amd_fidelityfx_dx12.dll" "amd_fidelityfx_framegeneration_dx12.dll" "amd_fidelityfx_upscaler_dx12.dll" "amd_fidelityfx_vk.dll"
+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..."
+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 d48856d..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
}
@@ -68,7 +68,7 @@ for arg in "$@"; do
fi
# Extract executable path from YAML
- exe_path=$(grep -E '^\s*exe:' "$config_file" | sed 's/.*exe:[[:space:]]*//')
+ exe_path=$(grep -E '^\s*exe:' "$config_file" | sed 's/.*exe:[[:space:]]*//' )
if [[ -n "$exe_path" ]]; then
exe_folder_path=$(dirname "$exe_path")
@@ -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,34 +107,71 @@ 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 ===
+if [[ -f "$fgmod_path/update-optiscaler-config.py" ]]; then
+ python "$fgmod_path/update-optiscaler-config.py" "$exe_folder_path/OptiScaler.ini"
+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
+
+# === 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 ===
@@ -150,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
@@ -186,8 +223,21 @@ if [[ $# -gt 1 ]]; then
# Execute the original command
export SteamDeck=0
- export WINEDLLOVERRIDES="$WINEDLLOVERRIDES,dxgi=n,b"
- exec "$@"
+ # 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
+ shift
+ done
+
+ exec >/dev/null 2>&1
+ "$@"
else
echo "Done!"
echo "----------------------------------------"
diff --git a/defaults/assets/update-optiscaler-config.py b/defaults/assets/update-optiscaler-config.py
new file mode 100644
index 0000000..f4a65de
--- /dev/null
+++ b/defaults/assets/update-optiscaler-config.py
@@ -0,0 +1,110 @@
+import os
+import sys
+import re
+from configparser import ConfigParser
+
+def update_optiscaler_config(file_path):
+ if not os.path.exists(file_path):
+ print(f"Error: File '{file_path}' not found.")
+ return
+
+ with open(file_path, 'r') as f:
+ lines = f.readlines()
+
+ config = ConfigParser()
+ config.optionxform = str # Preserve case for keys (otherwise PATH could match Path)
+ config.read(file_path)
+
+ # Because we want to support unprefixed env variables, we need to count key occurrences across all sections of the ini file
+ # Keys that appear multiple times should be prefixed like Section_Key by the user for them to be targeted properly
+
+ # Normalize section names: strip - and . so V-Sync becomes VSync
+ # This allows env vars like VSync_Key to match INI section [V-Sync]
+ def normalize_section(section_name):
+ return section_name.replace('-', '').replace('.', '')
+
+ key_occurrences = {}
+ key_to_sections = {}
+ section_normalized_to_actual = {} # Maps normalized section name to actual section name
+
+ for section in config.sections():
+ normalized = normalize_section(section)
+ section_normalized_to_actual[normalized] = section
+
+ for key in config.options(section):
+ key_occurrences[key] = key_occurrences.get(key, 0) + 1
+ if key not in key_to_sections:
+ key_to_sections[key] = []
+ key_to_sections[key].append(section)
+
+ env_updates = []
+
+ # Handle OptiScaler_Section_Key format
+ optiscaler_vars = {k: v for k, v in os.environ.items() if k.startswith("OptiScaler_")}
+ for env_name, env_value in optiscaler_vars.items():
+ parts = env_name.split('_', 2)
+ if len(parts) >= 3:
+ env_updates.append(('optiscaler', parts[1], parts[2], env_value, env_name))
+
+ # Handle Section_Key and Key formats
+ other_vars = {k: v for k, v in os.environ.items() if not k.startswith("OptiScaler_")}
+ for env_name, env_value in other_vars.items():
+ # Try Section_Key format
+ if '_' in env_name:
+ parts = env_name.split('_', 1)
+ section_from_env = parts[0]
+ key = parts[1]
+
+ # Try exact section match first
+ if config.has_section(section_from_env) and config.has_option(section_from_env, key):
+ env_updates.append(('section_key', section_from_env, key, env_value, env_name))
+ continue
+
+ # Try section match with normalized section names
+ if section_from_env in section_normalized_to_actual:
+ actual_section = section_normalized_to_actual[section_from_env]
+ if config.has_option(actual_section, key):
+ env_updates.append(('section_key', actual_section, key, env_value, env_name))
+ continue
+
+ # Try Key format (only if key appears exactly once across all sections)
+ if env_name in key_occurrences and key_occurrences[env_name] == 1:
+ section = key_to_sections[env_name][0]
+ env_updates.append(('key', section, env_name, env_value, env_name))
+
+ print(f"Found {len(env_updates)} updates to apply")
+ for entry in env_updates:
+ print(f"> {entry}")
+
+ for update_type, section_target, key_target, env_value, env_name in env_updates:
+ found_section = False
+
+ # Regex to match [Section] and Key=Value (case-sensitive)
+ section_pattern = re.compile(rf'^\s*\[{re.escape(section_target)}]\s*')
+ key_pattern = re.compile(rf'^(\s*{re.escape(key_target)}\s*)=.*')
+
+ for i, line in enumerate(lines):
+ # Track if we are inside the correct section
+ if section_pattern.match(line):
+ found_section = True
+ continue
+
+ # If we hit a new section before finding the key, the key doesn't exist in the target section
+ if found_section and line.strip().startswith('[') and not section_pattern.match(line):
+ break
+
+ # Replace the value if the key is found within the correct section
+ if found_section and key_pattern.match(line):
+ lines[i] = key_pattern.sub(r'\1=' + env_value, line)
+ print(f"Updated: [{section_target}] {key_target} = {env_value} (from {env_name})")
+ break
+
+ # Write the modified content back
+ with open(file_path, 'w') as f:
+ f.writelines(lines)
+
+if __name__ == "__main__":
+ if len(sys.argv) < 2:
+ print("Usage: python update-optiscaler-config.py <path_to_ini>")
+ else:
+ update_optiscaler_config(sys.argv[1]) \ No newline at end of file
diff --git a/main.py b/main.py
index 5b6b8e8..ea80561 100644
--- a/main.py
+++ b/main.py
@@ -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",
@@ -114,21 +124,97 @@ class Plugin:
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 _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:
+ 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:
+ content = f.read()
+
+ updated_content = re.sub(r'UseHQFont\s*=\s*auto', 'UseHQFont=false', content)
+ if updated_content != content:
+ with open(ini_file, 'w') 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}")
+ return False
+
def _modify_optiscaler_ini(self, ini_file):
- """Modify OptiScaler.ini to set FGType=nukems, Fsr4Update=true, and ASI plugin settings"""
+ """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 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)
@@ -138,11 +224,14 @@ class Plugin:
# 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")
+ 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}")
@@ -235,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
@@ -289,7 +378,8 @@ class Plugin:
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 a newer static binary
+ # 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")
@@ -400,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",
@@ -409,10 +499,11 @@ 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"
+ "fgmod-uninstaller.sh",
+ "update-optiscaler-config.py"
]
if path.exists():
@@ -442,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 {
@@ -457,7 +548,6 @@ class Plugin:
"message": "OptiScaler.dll not found in ~/fgmod. Reinstall OptiScaler.",
}
- dll_name = "dxgi.dll"
preserve_ini = True
try:
@@ -504,6 +594,10 @@ class Plugin:
else:
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"
plugins_dest = directory / "plugins"
if plugins_src.exists():
@@ -512,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:
@@ -574,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"
@@ -674,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 eaee760..6448a0d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "decky-framegen",
- "version": "0.13.2",
+ "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.",
"type": "module",
"scripts": {
@@ -53,13 +53,13 @@
"remote_binary":
[
{
- "sha256hash": "1ebf2473cd8879b63fb773b76f169a75f35a7db1021820be1049a48c8b88694b",
- "url": "https://github.com/xXJSONDeruloXx/OptiScaler-Bleeding-Edge/releases/download/OptiScaler_v0.9.0_pre_6/Optiscaler_0.9.0-pre6.20251205.7z",
- "name": "Optiscaler_0.9.0-pre6.20251205.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": "9123f83739e7bb39fcb135cafc339606dec78a74c650338ad275ee45c2d59d02",
- "url": "https://github.com/xXJSONDeruloXx/OptiScaler-Bleeding-Edge/releases/download/amd-fsr-r-int8/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."
};