summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com>2026-04-05 20:29:56 -0400
committerGitHub <noreply@github.com>2026-04-05 20:29:56 -0400
commit47e7e2610b44809e13bea6bad7f1345fe6f58fa3 (patch)
tree669108810d1e8cae084dc658f74c740366bd464d
parentef469a8036e3b3f129a753dad4cf04fad3ca92f7 (diff)
parentb8eed9a4f3d98d887a9cc8f18b821d6a2af4598d (diff)
downloadDecky-Framegen-47e7e2610b44809e13bea6bad7f1345fe6f58fa3.tar.gz
Decky-Framegen-47e7e2610b44809e13bea6bad7f1345fe6f58fa3.zip
Merge pull request #186 from xXJSONDeruloXx/opti-090-finalHEADmain
Opti 090 final
-rwxr-xr-xdefaults/assets/fgmod-uninstaller.sh41
-rwxr-xr-xdefaults/assets/fgmod.sh92
-rw-r--r--main.py94
-rw-r--r--package.json10
-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
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
diff --git a/main.py b/main.py
index ddee8ba..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",
@@ -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."
};