diff options
| author | Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> | 2026-04-05 20:29:56 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-04-05 20:29:56 -0400 |
| commit | 47e7e2610b44809e13bea6bad7f1345fe6f58fa3 (patch) | |
| tree | 669108810d1e8cae084dc658f74c740366bd464d /src | |
| parent | ef469a8036e3b3f129a753dad4cf04fad3ca92f7 (diff) | |
| parent | b8eed9a4f3d98d887a9cc8f18b821d6a2af4598d (diff) | |
| download | Decky-Framegen-47e7e2610b44809e13bea6bad7f1345fe6f58fa3.tar.gz Decky-Framegen-47e7e2610b44809e13bea6bad7f1345fe6f58fa3.zip | |
Opti 090 final
Diffstat (limited to 'src')
| -rw-r--r-- | src/api/index.ts | 2 | ||||
| -rw-r--r-- | src/components/ClipboardCommands.tsx | 11 | ||||
| -rw-r--r-- | src/components/CustomPathOverride.tsx | 17 | ||||
| -rw-r--r-- | src/components/InstalledGamesSection.tsx | 8 | ||||
| -rw-r--r-- | src/components/OptiScalerControls.tsx | 34 | ||||
| -rw-r--r-- | src/components/ResultDisplay.tsx | 4 | ||||
| -rw-r--r-- | src/components/SteamGamePatcher.tsx | 277 | ||||
| -rw-r--r-- | src/components/index.ts | 1 | ||||
| -rw-r--r-- | src/types.d.ts | 10 | ||||
| -rw-r--r-- | src/utils/constants.ts | 22 |
10 files changed, 355 insertions, 31 deletions
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." }; |
