diff options
| author | xXJsonDeruloXx <danielhimebauch@gmail.com> | 2026-08-01 14:47:18 -0400 |
|---|---|---|
| committer | xXJsonDeruloXx <danielhimebauch@gmail.com> | 2026-08-01 14:47:18 -0400 |
| commit | 02521a797e195b331af1778cd7bc854d3a396ead (patch) | |
| tree | 63c48b246d9db2f6ab0140259d7fb8b15f2e085b /src/components | |
| parent | 96eb17b0a9f2cfd2b00ad082bef893f4efc229f7 (diff) | |
| download | Decky-Framegen-02521a797e195b331af1778cd7bc854d3a396ead.tar.gz Decky-Framegen-02521a797e195b331af1778cd7bc854d3a396ead.zip | |
refactor patch management and add ownership safeguards
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/CustomPathOverride.tsx | 11 | ||||
| -rw-r--r-- | src/components/OptiScalerControls.tsx | 82 | ||||
| -rw-r--r-- | src/components/SteamGamePatcher.tsx | 78 |
3 files changed, 129 insertions, 42 deletions
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> |
