From a6955e828b1dee7b14f8021a8a470dd51d77e33e Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Fri, 3 Apr 2026 09:52:39 -0400 Subject: feat: proxy DLL name picker Expose the proxy DLL rename as a user-selectable option across all injection paths. Previously hardcoded to dxgi.dll with no way to change it short of manually prepending DLL= to the Steam launch option. src/utils/constants.ts - Add PROXY_DLL_OPTIONS (7 entries matching _create_renamed_copies) each with a label and one-line hint - Add DEFAULT_PROXY_DLL constant (dxgi.dll) and ProxyDllValue type src/api/index.ts - runManualPatch now takes [directory, dll_name] so the chosen name reaches the backend src/components/OptiScalerControls.tsx - Own dllName state (default: dxgi.dll) - Render a DropdownItem (visible when installed) showing the 7 options with the selected option's hint as the description - Pass dllName down to both ClipboardCommands and ManualPatchControls src/components/ClipboardCommands.tsx - Accept dllName prop - Patch command is plain ~/fgmod/fgmod %command% for the default; prefixed DLL= ~/fgmod/fgmod %command% for any other choice src/components/CustomPathOverride.tsx - Accept dllName prop - Pass it to runManualPatch - Manual launch cmd clipboard button builds WINEDLLOVERRIDES="=n,b" dynamically; emits bare SteamDeck=0 %command% for OptiScaler.asi (ASI loader path needs no Wine DLL override) main.py - Add VALID_DLL_NAMES set (whitelist matching the renames dir) - manual_patch_directory validates dll_name against the whitelist and returns an error for unknown values - _manual_patch_directory_impl accepts dll_name param; removes the hardcoded "dxgi.dll" line defaults/assets/fgmod.sh - Fix longstanding bug: WINEDLLOVERRIDES was hardcoded to dxgi=n,b regardless of the DLL= env var selection. Now derives the stem from $dll_name and skips the override entirely for .asi files. --- src/components/ClipboardCommands.tsx | 17 ++++++++++++----- src/components/CustomPathOverride.tsx | 13 +++++++++---- src/components/OptiScalerControls.tsx | 23 +++++++++++++++++++---- 3 files changed, 40 insertions(+), 13 deletions(-) (limited to 'src/components') diff --git a/src/components/ClipboardCommands.tsx b/src/components/ClipboardCommands.tsx index 5a6f38f..b8cf6bf 100644 --- a/src/components/ClipboardCommands.tsx +++ b/src/components/ClipboardCommands.tsx @@ -1,20 +1,27 @@ import { SmartClipboardButton } from "./SmartClipboardButton"; +import { DEFAULT_PROXY_DLL } from "../utils/constants"; interface ClipboardCommandsProps { pathExists: boolean | null; + dllName: string; } -export function ClipboardCommands({ pathExists }: ClipboardCommandsProps) { +export function ClipboardCommands({ pathExists, dllName }: ClipboardCommandsProps) { if (pathExists !== true) return null; + const launchCommand = + dllName === DEFAULT_PROXY_DLL + ? "~/fgmod/fgmod %command%" + : `DLL=${dllName} ~/fgmod/fgmod %command%`; + return ( <> - - - diff --git a/src/components/CustomPathOverride.tsx b/src/components/CustomPathOverride.tsx index ffc4b1f..14a0905 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(INITIAL_DEFAULTS); const [pickerState, setPickerState] = useState(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 && ( <> diff --git a/src/components/OptiScalerControls.tsx b/src/components/OptiScalerControls.tsx index 468683c..fb5d2f8 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"; @@ -23,6 +23,7 @@ export function OptiScalerControls({ pathExists, setPathExists }: OptiScalerCont const [installResult, setInstallResult] = useState(null); const [uninstallResult, setUninstallResult] = useState(null); const [manualModeEnabled, setManualModeEnabled] = useState(false); + const [dllName, setDllName] = useState(DEFAULT_PROXY_DLL); useEffect(() => { if (installResult) { return createAutoCleanupTimer(() => setInstallResult(null), TIMEOUTS.resultDisplay); @@ -76,15 +77,29 @@ export function OptiScalerControls({ pathExists, setPathExists }: OptiScalerCont /> - + + {pathExists === true && ( + + 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))} + /> + + )} + {!manualModeEnabled && ( <> - + -- cgit v1.2.3 From 089c858feb1b6f4756bc241b14edcb2ad55dc0a0 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Fri, 3 Apr 2026 09:55:41 -0400 Subject: fix: always emit DLL= in patch command, even for default dxgi.dll --- src/components/ClipboardCommands.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) (limited to 'src/components') diff --git a/src/components/ClipboardCommands.tsx b/src/components/ClipboardCommands.tsx index b8cf6bf..7bbd12d 100644 --- a/src/components/ClipboardCommands.tsx +++ b/src/components/ClipboardCommands.tsx @@ -1,5 +1,4 @@ import { SmartClipboardButton } from "./SmartClipboardButton"; -import { DEFAULT_PROXY_DLL } from "../utils/constants"; interface ClipboardCommandsProps { pathExists: boolean | null; @@ -9,15 +8,10 @@ interface ClipboardCommandsProps { export function ClipboardCommands({ pathExists, dllName }: ClipboardCommandsProps) { if (pathExists !== true) return null; - const launchCommand = - dllName === DEFAULT_PROXY_DLL - ? "~/fgmod/fgmod %command%" - : `DLL=${dllName} ~/fgmod/fgmod %command%`; - return ( <> -- cgit v1.2.3 From 3d813ea87335298be5a47de3441f410651851b71 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Fri, 3 Apr 2026 10:06:44 -0400 Subject: feat: add Steam game picker with one-click launch option setter --- src/components/OptiScalerControls.tsx | 13 +- src/components/SteamGamePatcher.tsx | 277 ++++++++++++++++++++++++++++++++++ src/components/index.ts | 1 + 3 files changed, 286 insertions(+), 5 deletions(-) create mode 100644 src/components/SteamGamePatcher.tsx (limited to 'src/components') diff --git a/src/components/OptiScalerControls.tsx b/src/components/OptiScalerControls.tsx index fb5d2f8..f88e8f9 100644 --- a/src/components/OptiScalerControls.tsx +++ b/src/components/OptiScalerControls.tsx @@ -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; @@ -91,6 +92,12 @@ export function OptiScalerControls({ pathExists, setPathExists }: OptiScalerCont )} + {pathExists === true && ( + + )} + + + {!manualModeEnabled && ( - <> - - - - + )} diff --git a/src/components/SteamGamePatcher.tsx b/src/components/SteamGamePatcher.tsx new file mode 100644 index 0000000..5947f01 --- /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 => + 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([]); + const [gamesLoading, setGamesLoading] = useState(true); + const [selectedAppId, setSelectedAppId] = useState(""); + const [launchOptions, setLaunchOptions] = useState(""); + const [launchOptionsLoading, setLaunchOptionsLoading] = useState(false); + const [busy, setBusy] = useState(false); + const [resultMessage, setResultMessage] = useState(""); + + // 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(`❌ ${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(`❌ ${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 ( + <> + + ({ data: g.appid, label: g.name }))} + onChange={(option) => { + setSelectedAppId(String(option.data)); + setResultMessage(""); + }} + /> + + + {selectedGame && ( + <> + + + {statusColor ? ( + + {statusText} + + ) : ( + statusText + )} + + + + + + {setButtonLabel} + + + + {isManaged && ( + + + {busy ? "Removing..." : "Remove from launch options"} + + + )} + + {resultMessage && ( + + + {resultMessage} + + + )} + + )} + + ); +} 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'; -- cgit v1.2.3 From d845c74d039fd08449ce723d25958cd96a72ee06 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Fri, 3 Apr 2026 10:10:36 -0400 Subject: chore: remove all emojis from source files --- src/components/CustomPathOverride.tsx | 4 +--- src/components/InstalledGamesSection.tsx | 8 ++++---- src/components/ResultDisplay.tsx | 4 ++-- src/components/SteamGamePatcher.tsx | 8 ++++---- 4 files changed, 11 insertions(+), 13 deletions(-) (limited to 'src/components') diff --git a/src/components/CustomPathOverride.tsx b/src/components/CustomPathOverride.tsx index 14a0905..4effc6c 100644 --- a/src/components/CustomPathOverride.tsx +++ b/src/components/CustomPathOverride.tsx @@ -239,9 +239,7 @@ export const ManualPatchControls = ({ isAvailable, onManualModeChange, dllName } - ⚠️ - + /> )} 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() { { 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} ) : null} 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 = ({ result }) => {
{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"} ) : ( <> - ❌ Error: {result.message || "Operation failed"} + Error: {result.message || "Operation failed"} )} {result.output && !isSuccess && ( diff --git a/src/components/SteamGamePatcher.tsx b/src/components/SteamGamePatcher.tsx index 5947f01..06c373c 100644 --- a/src/components/SteamGamePatcher.tsx +++ b/src/components/SteamGamePatcher.tsx @@ -159,10 +159,10 @@ export function SteamGamePatcher({ dllName }: SteamGamePatcherProps) { setSteamLaunchOptions(Number(selectedAppId), targetCommand); setLaunchOptions(targetCommand); setResultMessage( - `✅ Launch options set for ${selectedGame?.name ?? selectedAppId}` + `Launch options set for ${selectedGame?.name ?? selectedAppId}` ); } catch (e) { - setResultMessage(`❌ ${e instanceof Error ? e.message : String(e)}`); + setResultMessage(`Error: ${e instanceof Error ? e.message : String(e)}`); } finally { setBusy(false); } @@ -176,10 +176,10 @@ export function SteamGamePatcher({ dllName }: SteamGamePatcherProps) { setSteamLaunchOptions(Number(selectedAppId), stripped); setLaunchOptions(stripped); setResultMessage( - `✅ Removed fgmod from ${selectedGame?.name ?? selectedAppId}` + `Removed fgmod from ${selectedGame?.name ?? selectedAppId}` ); } catch (e) { - setResultMessage(`❌ ${e instanceof Error ? e.message : String(e)}`); + setResultMessage(`Error: ${e instanceof Error ? e.message : String(e)}`); } finally { setBusy(false); } -- cgit v1.2.3