diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/api/index.ts | 25 | ||||
| -rw-r--r-- | src/components/CustomPathOverride.tsx | 11 | ||||
| -rw-r--r-- | src/components/OptiScalerControls.tsx | 82 | ||||
| -rw-r--r-- | src/components/SteamGamePatcher.tsx | 78 | ||||
| -rw-r--r-- | src/index.tsx | 8 | ||||
| -rw-r--r-- | src/utils/constants.ts | 16 |
6 files changed, 169 insertions, 51 deletions
diff --git a/src/api/index.ts b/src/api/index.ts index b1bf0b8..4dda43c 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,7 +1,7 @@ import { callable } from "@decky/api"; export const runInstallFGMod = callable< - [selected_default_variant?: string], + [selected_default_variant?: string, framegen_backend?: string], { status: string; message?: string; @@ -9,6 +9,8 @@ export const runInstallFGMod = callable< version?: string; selected_default_variant?: string; selected_default_variant_label?: string; + framegen_backend?: string; + framegen_backend_label?: string; } >("run_install_fgmod"); @@ -29,6 +31,17 @@ export const setDefaultFsr4Variant = callable< } >("set_default_fsr4_variant"); +export const setFramegenBackend = callable< + [framegen_backend?: string], + { + status: string; + message?: string; + output?: string; + framegen_backend?: string; + framegen_backend_label?: string; + } +>("set_framegen_backend"); + export const checkFGModPath = callable< [], { @@ -36,6 +49,8 @@ export const checkFGModPath = callable< version?: string | null; selected_fsr4_variant?: string | null; selected_fsr4_variant_label?: string | null; + framegen_backend?: string | null; + framegen_backend_label?: string | null; install_manifest_present?: boolean; } >("check_fgmod_path"); @@ -53,7 +68,7 @@ export const getPathDefaults = callable< >("get_path_defaults"); export const runManualPatch = callable< - [string, string, string], + [string, string, string, string], { status: string; message?: string; @@ -62,6 +77,7 @@ export const runManualPatch = callable< fsr4_variant_label?: string; fsr4_upscaler_sha256?: string; optiscaler_version?: string | null; + framegen_backend?: string; } >("manual_patch_directory"); @@ -86,11 +102,13 @@ export const getGameStatus = callable< fsr4_variant?: string | null; fsr4_variant_label?: string | null; fsr4_upscaler_sha256?: string | null; + framegen_backend?: string | null; + framegen_backend_label?: string | null; } >("get_game_status"); export const patchGame = callable< - [appid: string, dll_name: string, current_launch_options: string, fsr4_variant: string], + [appid: string, dll_name: string, current_launch_options: string, fsr4_variant: string, framegen_backend: string], { status: string; message?: string; @@ -104,6 +122,7 @@ export const patchGame = callable< fsr4_variant?: string; fsr4_variant_label?: string; fsr4_upscaler_sha256?: string; + framegen_backend?: string; } >("patch_game"); diff --git a/src/components/CustomPathOverride.tsx b/src/components/CustomPathOverride.tsx index af47735..fb7cd05 100644 --- a/src/components/CustomPathOverride.tsx +++ b/src/components/CustomPathOverride.tsx @@ -38,6 +38,7 @@ interface ManualPatchControlsProps { onManualModeChange?: (enabled: boolean) => void; dllName: string; fsr4Variant: string; + framegenBackend: string; } interface PickerState { @@ -58,7 +59,7 @@ const formatResultMessage = (result: ApiResponse | null) => { return result.message || result.output || "Operation failed."; }; -export const ManualPatchControls = ({ isAvailable, onManualModeChange, dllName, fsr4Variant }: ManualPatchControlsProps) => { +export const ManualPatchControls = ({ isAvailable, onManualModeChange, dllName, fsr4Variant, framegenBackend }: ManualPatchControlsProps) => { const [isEnabled, setEnabled] = useState(false); const [defaults, setDefaults] = useState<PathDefaults>(INITIAL_DEFAULTS); const [pickerState, setPickerState] = useState<PickerState>(INITIAL_PICKER_STATE); @@ -160,14 +161,14 @@ export const ManualPatchControls = ({ isAvailable, onManualModeChange, dllName, if (!selectedPath) return; const setBusy = action === "patch" ? setIsPatching : setIsUnpatching; - setLastOperation(action); + setLastOperation(action); setBusy(true); setOperationResult(null); try { const response = action === "patch" - ? await runManualPatch(selectedPath, dllName, fsr4Variant) + ? await runManualPatch(selectedPath, dllName, fsr4Variant, framegenBackend) : await runManualUnpatch(selectedPath); setOperationResult(response ?? { status: "error", message: "No response from backend." }); } catch (err) { @@ -179,7 +180,7 @@ export const ManualPatchControls = ({ isAvailable, onManualModeChange, dllName, setBusy(false); } }, - [selectedPath, dllName, fsr4Variant] + [selectedPath, dllName, fsr4Variant, framegenBackend] ); const handleToggle = (value: boolean) => { @@ -304,4 +305,4 @@ export const ManualPatchControls = ({ isAvailable, onManualModeChange, dllName, )} </> ); -};
\ No newline at end of file +}; diff --git a/src/components/OptiScalerControls.tsx b/src/components/OptiScalerControls.tsx index a8b0863..d6c4c77 100644 --- a/src/components/OptiScalerControls.tsx +++ b/src/components/OptiScalerControls.tsx @@ -1,9 +1,17 @@ import { useState, useEffect } from "react"; import { DropdownItem, Field, PanelSection, PanelSectionRow, ToggleField } from "@decky/ui"; -import { runInstallFGMod, runUninstallFGMod, setDefaultFsr4Variant } from "../api"; -import { OperationResult } from "./ResultDisplay"; +import { runInstallFGMod, runUninstallFGMod, setDefaultFsr4Variant, setFramegenBackend } from "../api"; +import { ResultDisplay, type OperationResult } from "./ResultDisplay"; import { createAutoCleanupTimer } from "../utils"; -import { TIMEOUTS, PROXY_DLL_OPTIONS, DEFAULT_PROXY_DLL, FSR4_VARIANT_OPTIONS, DEFAULT_FSR4_VARIANT } from "../utils/constants"; +import { + TIMEOUTS, + PROXY_DLL_OPTIONS, + DEFAULT_PROXY_DLL, + FSR4_VARIANT_OPTIONS, + DEFAULT_FSR4_VARIANT, + FRAMEGEN_BACKEND_OPTIONS, + DEFAULT_FRAMEGEN_BACKEND, +} from "../utils/constants"; import { InstallationStatus } from "./InstallationStatus"; import { OptiScalerHeader } from "./OptiScalerHeader"; import { ClipboardCommands } from "./ClipboardCommands"; @@ -18,6 +26,8 @@ interface FgmodInfo { version?: string | null; selected_fsr4_variant?: string | null; selected_fsr4_variant_label?: string | null; + framegen_backend?: string | null; + framegen_backend_label?: string | null; install_manifest_present?: boolean; } @@ -37,19 +47,22 @@ export function OptiScalerControls({ pathExists, setPathExists, fgmodInfo }: Opt const [dllName, setDllName] = useState<string>(DEFAULT_PROXY_DLL); const [fsr4Variant, setFsr4Variant] = useState<string>(DEFAULT_FSR4_VARIANT); const [fsr4VariantTouched, setFsr4VariantTouched] = useState(false); + const [framegenBackend, setFramegenBackendValue] = useState<string>(DEFAULT_FRAMEGEN_BACKEND); + const [framegenBackendTouched, setFramegenBackendTouched] = useState(false); const [switchingVariant, setSwitchingVariant] = useState(false); + const [switchingBackend, setSwitchingBackend] = useState(false); useEffect(() => { if (installResult) { return createAutoCleanupTimer(() => setInstallResult(null), TIMEOUTS.resultDisplay); } - return () => {}; // Ensure a cleanup function is always returned + return undefined; }, [installResult]); useEffect(() => { if (uninstallResult) { return createAutoCleanupTimer(() => setUninstallResult(null), TIMEOUTS.resultDisplay); } - return () => {}; // Ensure a cleanup function is always returned + return undefined; }, [uninstallResult]); useEffect(() => { @@ -59,16 +72,26 @@ export function OptiScalerControls({ pathExists, setPathExists, fgmodInfo }: Opt } }, [fgmodInfo?.selected_fsr4_variant, fsr4VariantTouched]); + useEffect(() => { + const installedBackend = fgmodInfo?.framegen_backend; + if (!framegenBackendTouched && installedBackend && FRAMEGEN_BACKEND_OPTIONS.some((option) => option.value === installedBackend)) { + setFramegenBackendValue(installedBackend); + } + }, [fgmodInfo?.framegen_backend, framegenBackendTouched]); + const handleInstallClick = async () => { try { setInstalling(true); - const result = await runInstallFGMod(fsr4Variant); + const result = await runInstallFGMod(fsr4Variant, framegenBackend); setInstallResult(result); if (result.status === "success") { setPathExists(true); + setFsr4VariantTouched(false); + setFramegenBackendTouched(false); } } catch (e) { console.error(e); + setInstallResult({ status: "error", message: e instanceof Error ? e.message : "Installation failed." }); } finally { setInstalling(false); } @@ -84,11 +107,35 @@ export function OptiScalerControls({ pathExists, setPathExists, fgmodInfo }: Opt } } catch (e) { console.error(e); + setUninstallResult({ status: "error", message: e instanceof Error ? e.message : "Uninstall failed." }); } finally { setUninstalling(false); } }; + const handleFramegenBackendChange = async (nextBackend: string) => { + const previousBackend = framegenBackend; + setFramegenBackendValue(nextBackend); + setFramegenBackendTouched(true); + if (pathExists !== true) return; + + try { + setSwitchingBackend(true); + const result = await setFramegenBackend(nextBackend); + if (result.status !== "success") { + throw new Error(result.message || result.output || "Failed to update the frame-generation backend."); + } + setFramegenBackendValue(result.framegen_backend || nextBackend); + setFramegenBackendTouched(false); + } catch (error) { + console.error(error); + setFramegenBackendValue(previousBackend); + setFramegenBackendTouched(false); + } finally { + setSwitchingBackend(false); + } + }; + const handleFsr4VariantChange = async (nextVariant: string) => { const previousVariant = fsr4Variant; setFsr4Variant(nextVariant); @@ -132,13 +179,28 @@ export function OptiScalerControls({ pathExists, setPathExists, fgmodInfo }: Opt menuLabel="Default FSR4 runtime" selectedOption={fsr4Variant} rgOptions={FSR4_VARIANT_OPTIONS.map((option) => ({ data: option.value, label: option.label }))} - disabled={installing || uninstalling || switchingVariant} + disabled={installing || uninstalling || switchingVariant || switchingBackend} onChange={(option) => { void handleFsr4VariantChange(String(option.data)); }} /> </PanelSectionRow> + <PanelSectionRow> + <DropdownItem + layout="below" + label="Frame generation backend" + description={FRAMEGEN_BACKEND_OPTIONS.find((option) => option.value === framegenBackend)?.hint} + menuLabel="Frame generation backend" + selectedOption={framegenBackend} + rgOptions={FRAMEGEN_BACKEND_OPTIONS.map((option) => ({ data: option.value, label: option.label }))} + disabled={installing || uninstalling || switchingBackend} + onChange={(option) => { + void handleFramegenBackendChange(String(option.data)); + }} + /> + </PanelSectionRow> + {pathExists === true && fgmodInfo?.version && installedVariantLabel && ( <PanelSectionRow> <Field label="Installed bundle" description={`OptiScaler ${fgmodInfo.version}`}> @@ -162,7 +224,7 @@ export function OptiScalerControls({ pathExists, setPathExists, fgmodInfo }: Opt )} {pathExists === true && ( - <SteamGamePatcher dllName={dllName} fsr4Variant={fsr4Variant} /> + <SteamGamePatcher dllName={dllName} fsr4Variant={fsr4Variant} framegenBackend={framegenBackend} /> )} <ClipboardCommands pathExists={pathExists} dllName={dllName} /> @@ -192,8 +254,12 @@ export function OptiScalerControls({ pathExists, setPathExists, fgmodInfo }: Opt onManualModeChange={setAdvancedModeEnabled} dllName={dllName} fsr4Variant={fsr4Variant} + framegenBackend={framegenBackend} /> + <ResultDisplay result={installResult} /> + <ResultDisplay result={uninstallResult} /> + {!advancedModeEnabled && ( <InstructionCard pathExists={pathExists} /> )} diff --git a/src/components/SteamGamePatcher.tsx b/src/components/SteamGamePatcher.tsx index e8ac3d2..cd8fe3c 100644 --- a/src/components/SteamGamePatcher.tsx +++ b/src/components/SteamGamePatcher.tsx @@ -4,12 +4,24 @@ import { toaster } from "@decky/api"; import { listInstalledGames, getGameStatus, patchGame, unpatchGame } from "../api"; import { FSR4_VARIANT_OPTIONS } from "../utils/constants"; -// ─── SteamClient helpers ───────────────────────────────────────────────────── +type SteamAppsApi = { + RegisterForAppDetails?: ( + appId: number, + callback: (details: { strLaunchOptions?: string }) => void + ) => { unregister: () => void }; + SetAppLaunchOptions?: (appId: number, options: string) => void; +}; + +const getSteamApps = (): SteamAppsApi | undefined => { + if (typeof SteamClient === "undefined") return undefined; + return SteamClient.Apps as unknown as SteamAppsApi; +}; const getAppLaunchOptions = (appId: number): Promise<string> => new Promise((resolve, reject) => { - if (typeof SteamClient === "undefined" || !SteamClient?.Apps?.RegisterForAppDetails) { - resolve(""); + const apps = getSteamApps(); + if (!apps || typeof apps.RegisterForAppDetails !== "function" || typeof apps.SetAppLaunchOptions !== "function") { + reject(new Error("Steam launch-options API is unavailable; patching was not started.")); return; } let settled = false; @@ -20,7 +32,7 @@ const getAppLaunchOptions = (appId: number): Promise<string> => unregister(); reject(new Error("Timed out reading launch options.")); }, 5000); - const registration = SteamClient.Apps.RegisterForAppDetails( + const registration = apps.RegisterForAppDetails( appId, (details: { strLaunchOptions?: string }) => { if (settled) return; @@ -31,15 +43,25 @@ const getAppLaunchOptions = (appId: number): Promise<string> => } ); unregister = registration.unregister; + if (settled) unregister(); }); const setAppLaunchOptions = (appId: number, options: string): void => { - if (typeof SteamClient !== "undefined" && SteamClient?.Apps?.SetAppLaunchOptions) { - SteamClient.Apps.SetAppLaunchOptions(appId, options); + const apps = getSteamApps(); + if (!apps || typeof apps.SetAppLaunchOptions !== "function") { + throw new Error("Steam launch-options API is unavailable; launch options were not changed."); } + apps.SetAppLaunchOptions(appId, options); }; -// ─── Types ─────────────────────────────────────────────────────────────────── +const canManageLaunchOptions = (): boolean => { + const apps = getSteamApps(); + return Boolean( + apps && + typeof apps.RegisterForAppDetails === "function" && + typeof apps.SetAppLaunchOptions === "function" + ); +}; type GameEntry = { appid: string; name: string; install_found?: boolean }; @@ -55,20 +77,19 @@ type GameStatus = { fsr4_variant?: string | null; fsr4_variant_label?: string | null; fsr4_upscaler_sha256?: string | null; + framegen_backend?: string | null; + framegen_backend_label?: string | null; }; -// ─── Module-level state persistence ────────────────────────────────────────── - let lastSelectedAppId = ""; -// ─── Component ─────────────────────────────────────────────────────────────── - interface SteamGamePatcherProps { dllName: string; fsr4Variant: string; + framegenBackend: string; } -export function SteamGamePatcher({ dllName, fsr4Variant }: SteamGamePatcherProps) { +export function SteamGamePatcher({ dllName, fsr4Variant, framegenBackend }: SteamGamePatcherProps) { const [games, setGames] = useState<GameEntry[]>([]); const [gamesLoading, setGamesLoading] = useState(true); const [selectedAppId, setSelectedAppId] = useState<string>(() => lastSelectedAppId); @@ -77,8 +98,6 @@ export function SteamGamePatcher({ dllName, fsr4Variant }: SteamGamePatcherProps const [busyAction, setBusyAction] = useState<"patch" | "unpatch" | null>(null); const [resultMessage, setResultMessage] = useState<string>(""); - // ── Data loaders ─────────────────────────────────────────────────────────── - const loadGames = useCallback(async () => { setGamesLoading(true); try { @@ -136,8 +155,6 @@ export function SteamGamePatcher({ dllName, fsr4Variant }: SteamGamePatcherProps void loadStatus(selectedAppId); }, [selectedAppId, loadStatus]); - // ── Derived state ────────────────────────────────────────────────────────── - const selectedGame = useMemo( () => games.find((g) => g.appid === selectedAppId) ?? null, [games, selectedAppId] @@ -163,20 +180,16 @@ export function SteamGamePatcher({ dllName, fsr4Variant }: SteamGamePatcherProps return `Patch with ${dllName}`; }, [busyAction, dllName, gameStatus, isPatchedWithDifferentDll, selectedGame]); - // ── Actions ──────────────────────────────────────────────────────────────── - const handlePatch = useCallback(async () => { if (!selectedGame || !selectedAppId || busyAction) return; setBusyAction("patch"); setResultMessage(""); try { - let currentLaunchOptions = ""; - try { - currentLaunchOptions = await getAppLaunchOptions(Number(selectedAppId)); - } catch { - // non-fatal: proceed without current launch options + if (!canManageLaunchOptions()) { + throw new Error("Steam launch-options API is unavailable; patching was not started."); } - const result = await patchGame(selectedAppId, dllName, currentLaunchOptions, fsr4Variant); + const currentLaunchOptions = await getAppLaunchOptions(Number(selectedAppId)); + const result = await patchGame(selectedAppId, dllName, currentLaunchOptions, fsr4Variant, framegenBackend); if (result.status !== "success") throw new Error(result.message || "Patch failed."); setAppLaunchOptions(Number(selectedAppId), result.launch_options || ""); const msg = result.message || `Patched ${selectedGame.name}.`; @@ -190,13 +203,16 @@ export function SteamGamePatcher({ dllName, fsr4Variant }: SteamGamePatcherProps } finally { setBusyAction(null); } - }, [busyAction, dllName, fsr4Variant, loadStatus, selectedAppId, selectedGame]); + }, [busyAction, dllName, framegenBackend, fsr4Variant, loadStatus, selectedAppId, selectedGame]); const handleUnpatch = useCallback(async () => { if (!selectedGame || !selectedAppId || busyAction) return; setBusyAction("unpatch"); setResultMessage(""); try { + if (!canManageLaunchOptions()) { + throw new Error("Steam launch-options API is unavailable; unpatching was not started."); + } const result = await unpatchGame(selectedAppId); if (result.status !== "success") throw new Error(result.message || "Unpatch failed."); setAppLaunchOptions(Number(selectedAppId), result.launch_options || ""); @@ -213,8 +229,6 @@ export function SteamGamePatcher({ dllName, fsr4Variant }: SteamGamePatcherProps } }, [busyAction, loadStatus, selectedAppId, selectedGame]); - // ── Status display ───────────────────────────────────────────────────────── - const statusDisplay = useMemo(() => { if (!selectedGame) return { text: "—", color: undefined as string | undefined }; if (statusLoading) return { text: "Loading...", color: undefined }; @@ -230,8 +244,6 @@ export function SteamGamePatcher({ dllName, fsr4Variant }: SteamGamePatcherProps const focusableFieldProps = { focusable: true, highlightOnFocus: true } as const; - // ── Render ───────────────────────────────────────────────────────────────── - return ( <> <PanelSectionRow> @@ -278,6 +290,14 @@ export function SteamGamePatcher({ dllName, fsr4Variant }: SteamGamePatcherProps </PanelSectionRow> <PanelSectionRow> + <Field {...focusableFieldProps} label="Frame generation backend"> + {gameStatus?.patched + ? (gameStatus?.framegen_backend_label || gameStatus?.framegen_backend || "Unknown") + : `Will patch with ${framegenBackend}`} + </Field> + </PanelSectionRow> + + <PanelSectionRow> <ButtonItem layout="below" disabled={!canPatch} onClick={handlePatch}> {patchButtonLabel} </ButtonItem> diff --git a/src/index.tsx b/src/index.tsx index 4a9a9f6..6d598fc 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,7 +2,6 @@ import { definePlugin } from "@decky/api"; import { MdOutlineAutoAwesomeMotion } from "react-icons/md"; import { useState, useEffect } from "react"; import { OptiScalerControls } from "./components"; -// import { InstalledGamesSection } from "./components/InstalledGamesSection"; import { checkFGModPath } from "./api"; import { safeAsyncOperation } from "./utils"; import { TIMEOUTS } from "./utils/constants"; @@ -12,6 +11,8 @@ type FgmodInfo = { version?: string | null; selected_fsr4_variant?: string | null; selected_fsr4_variant_label?: string | null; + framegen_backend?: string | null; + framegen_backend_label?: string | null; install_manifest_present?: boolean; }; @@ -43,11 +44,6 @@ function MainContent() { setPathExists={setPathExists} fgmodInfo={fgmodInfo} /> - {pathExists === true ? ( - <> - {/* <InstalledGamesSection /> */} - </> - ) : null} </> ); } diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 3af9874..2568dd4 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -85,6 +85,22 @@ export const FSR4_VARIANT_OPTIONS = [ export type Fsr4VariantValue = typeof FSR4_VARIANT_OPTIONS[number]["value"]; export const DEFAULT_FSR4_VARIANT: Fsr4VariantValue = "rdna23-int8"; +export const FRAMEGEN_BACKEND_OPTIONS = [ + { + value: "auto", + label: "OptiScaler automatic selection", + hint: "Let OptiScaler select the compatible frame-generation path.", + }, + { + value: "nukems", + label: "Nukem's DLSSG → FSR3", + hint: "Use Nukem's DLSSG-to-FSR3 path for games that need it.", + }, +] as const; + +export type FramegenBackendValue = typeof FRAMEGEN_BACKEND_OPTIONS[number]["value"]; +export const DEFAULT_FRAMEGEN_BACKEND: FramegenBackendValue = "auto"; + // Common timeout values export const TIMEOUTS = { resultDisplay: 5000, // 5 seconds |
