summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorxXJsonDeruloXx <danielhimebauch@gmail.com>2026-03-20 17:32:52 -0400
committerxXJsonDeruloXx <danielhimebauch@gmail.com>2026-03-20 17:32:52 -0400
commitd81bb130385114389728f849d0ab8cccf62b90d1 (patch)
treee4bdd986c3b18c62cae7311b92fb5ad5f9be86e6
parentca5db2231b8554d1377dd449f6fb9c736e3d6386 (diff)
downloadDecky-Framegen-d81bb130385114389728f849d0ab8cccf62b90d1.tar.gz
Decky-Framegen-d81bb130385114389728f849d0ab8cccf62b90d1.zip
Add Steam UI for prefix-managed integrationfeature/prefix-managed-optiscaler
-rw-r--r--README.md184
-rw-r--r--plugin.json2
-rw-r--r--src/api/index.ts5
-rw-r--r--src/components/ClipboardCommands.tsx12
-rw-r--r--src/components/InstalledGamesSection.tsx158
-rw-r--r--src/components/InstructionCard.tsx6
-rw-r--r--src/components/OptiScalerControls.tsx56
-rw-r--r--src/components/SmartClipboardButton.tsx123
-rw-r--r--src/components/index.ts3
-rw-r--r--src/utils/constants.ts48
10 files changed, 294 insertions, 303 deletions
diff --git a/README.md b/README.md
index 1b08742..d6a276b 100644
--- a/README.md
+++ b/README.md
@@ -1,98 +1,142 @@
-# Decky Framegen Plugin
+# Decky Framegen
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/B0B71HZTAX)
-A Steam Deck plugin that enables DLSS upscaling and Frame Generation on handhelds by utilizing the latest OptiScaler and supporting modification software. This plugin automatically installs and manages all necessary components for FSR-based frame generation in games that support DLSS, or OptiFG for adding FG to games that do not have any existing FG pathway (highly experimental)
+Decky Framegen now treats OptiScaler as a **prefix-managed per-game runtime** instead of a game-folder mod installer.
-## What This Plugin Does
+## What changed
-This plugin uses OptiScaler to replace DLSS calls with FSR3/FSR3.1, giving you:
+The plugin no longer needs to copy OptiScaler into the game install directory.
-- **Frame Generation**: Smooth out your frame rate using AMD's FSR3 pathways
-- **Upscaling**: Improves performance while maintaining visual quality using FSR and XESS using DLSS FSR or XESS inputs. Upgrade FSR 2 games to FSR 3.1.4 or XESS for better visual quality.
-- **Easy Management**: One-click installation and game patching/unpatching through the Steam Deck interface. No going into desktop mode every time you want to add or remove OptiScaler from a game!
+Instead, it:
-## Features
+- installs a shared OptiScaler runtime under `~/fgmod`
+- stages OptiScaler into `compatdata/<appid>/pfx/drive_c/windows/system32` at launch time
+- keeps a writable per-game config under `compatdata/<appid>/optiscaler-managed`
+- restores the original Wine/Proton proxy DLL on cleanup
-### Core Functionality
-- **One-Click Setup**: Automatically downloads and installs OptiScaler into a "fgmod" directory
-- **Smart Installation**: Handles all required dependencies and library files
-- **Game Patching**: Easy copy-paste launch commands for enabling/disabling the mod per game
-- **OptiScaler Wiki**: Direct access to OptiScaler documentation and settings via a webpage launch button right inside the plugin.
+That makes the integration:
-## How to Use
+- non-invasive
+- reversible
+- per-game
+- compatible with Steam launch options and future launcher/runtime style integration
-1. **Install the Plugin**: Download and install through Decky Loader "install from zip" option in developer settings
-2. **Setup OptiScaler**: Open the plugin and click "Setup OptiScaler Mod"
-3. **Configure Games**: For each game you want to enhance:
- - Click "Copy Patch Command" in the plugin
- - Go to your game's Properties → Launch Options in Steam
- - Paste the command: `~/fgmod/fgmod %command%`
-4. **Enable Features**: Launch your game and enable DLSS in the graphics settings
-5. **Advanced Options**: Press the Insert key in-game for additional OptiScaler settings
+## Current default behavior
-### Removing the Mod from Games
-- Click "Copy Unpatch Command" and replace the launch options with: `~/fgmod/fgmod-uninstaller.sh %command%`
-- Run the game at least once to make the uninstaller script run. After you can leave the launch option or remove it
+The default proxy is:
-### Configuring OptiScaler via Environment Variables
-As of v0.15.1, you can update OptiScaler settings before a game launches by adding environment variables.
-This is useful if you plan to use the same settings across multiple games so they are pre-configured by the time you launch them.
+- `winmm.dll`
-For example, considering the following sample from the OptiScaler.ini config file:
+The default launch command is:
+
+```bash
+OPTISCALER_PROXY=winmm ~/fgmod/fgmod %command%
```
-[Upscalers]
-Dx11Upscaler=auto
-Dx12Upscaler=auto
-VulkanUpscaler=auto
-
-[FrameGen]
-Enabled=auto
-FGInput=auto
-FGOutput=auto
-DebugView=auto
-DrawUIOverFG=auto
+
+To clean a game's managed prefix manually:
+
+```bash
+~/fgmod/fgmod-uninstaller.sh %command%
```
-We can decide to set `Dx12Upscaler=fsr31` to enable FSR4 in DX12 games by default. This works because the option name `Dx12Upscaler` is unique throughout the file but for options that appear multiple times like `Enabled`, you can prefix the option name with the section name like `FrameGen_Enabled=true`.
-You can provide section names for all options if you want to be explicit. You can also prefix `Section_Option` with `OptiScaler` to ensure no conflict with other commands.
-Here's the breakdown of supported formats:
-- `OptiScaler_Section_Option=value` - Full format (foolproof)
-- `Section_Option=value` - Short format (recommended)
-- `Option=value` - Minimal format (only works if the option name appears once in OptiScaler.ini)
+## How to use
+
+1. Install the plugin zip through Decky Loader.
+2. Open Decky Framegen.
+3. Press **Install Prefix-Managed Runtime**.
+4. Enable a game from the **Steam game integration** section, or copy the launch command manually.
+5. Launch the game.
+6. Press **Insert** in-game to open the OptiScaler menu.
+
+## Steam game integration
+
+The plugin can now manage Steam launch options for a selected installed game.
+
+Enable:
+
+- writes `OPTISCALER_PROXY=winmm ~/fgmod/fgmod %COMMAND%` into Steam launch options
+
+Disable:
+
+- clears Steam launch options
+- cleans the managed OptiScaler files from the game's compatdata prefix
+
+## Advanced notes
+
+### Config persistence
+
+`OptiScaler.ini` is stored per game under:
+
+```text
+compatdata/<appid>/optiscaler-managed/OptiScaler.ini
+```
+
+The runtime copies that INI into `system32` before launch and syncs it back after the game exits, so in-game menu saves persist.
+
+### Proxy override
+
+You can test a different proxy by changing the launch option manually:
-**Example:**
```bash
-# Enable frame generation with XeFG output
-FrameGen_Enabled=true FGInput=fsrfg FGOutput=xefg ~/fgmod/fgmod %command%
+OPTISCALER_PROXY=dxgi ~/fgmod/fgmod %command%
+OPTISCALER_PROXY=version ~/fgmod/fgmod %command%
+```
+
+Supported values currently include:
+
+- `winmm`
+- `dxgi`
+- `version`
+- `dbghelp`
+- `winhttp`
+- `wininet`
+- `d3d12`
+
+### Environment-driven INI updates
-# Set DX12 upscaler to FSR 3.1 (Upgrades to FSR4)
-Dx12Upscaler=fsr31 ~/fgmod/fgmod %command%
+The existing OptiScaler env var patching still works. For example:
+
+```bash
+Dx12Upscaler=fsr31 FrameGen_Enabled=true OPTISCALER_PROXY=winmm ~/fgmod/fgmod %command%
```
-**Notes:**
-- Environment variables override the OptiScaler.ini file on each game launch
-- Hyphenated section names like `[V-Sync]` can be accessed like `VSync_Option=value`
-- If an option name appears in multiple sections of the OptiScaler.ini file, use the `Section_Option` or `OptiScaler_Section_Option` format
+## Technical summary
-## Technical Details
+At launch the runtime:
-### What's Included
-- **[OptiScaler 0.9.0-pre11](https://github.com/xXJSONDeruloXx/OptiScaler-Bleeding-Edge/releases/tag/opti-9-pre-11)**: Bleeding-edge OptiScaler bundle used by this plugin, paired with the RDNA2-optimized `amd_fidelityfx_upscaler_dx12.dll` override for Steam Deck compatibility
-- **Nukem9's DLSSG to FSR3 mod**: Allows use of DLSS inputs for FSR frame gen outputs, and xess or FSR upscaling outputs
-- **FakeNVAPI**: NVIDIA API emulation for AMD/Intel GPUs, to make DLSS options selectable in game
-- **Supporting Libraries**: All required DX12/Vulkan libraries (libxess.dll, amd_fidelityfx, etc.)
+1. resolves `STEAM_COMPAT_DATA_PATH`
+2. creates `compatdata/<appid>/optiscaler-managed`
+3. preserves the original selected proxy DLL as `<proxy>-original.dll`
+4. stages OptiScaler and helper DLLs into prefix `system32`
+5. sets `WINEDLLOVERRIDES=<proxy>=n,b`
+6. launches the game
+7. syncs `OptiScaler.ini` back to the managed directory on exit
+## Build notes
-## Credits
+Local frontend build:
-### Core Technologies
-- **[Nukem9](https://github.com/Nukem9/dlssg-to-fsr3)** - Creator of the DLSS to FSR3 mod that makes frame generation possible
-- **[Cdozdil/OptiScaler Team](https://github.com/optiscaler/OptiScaler)** - OptiScaler mod that provides the core functionality and bleeding-edge improvements
-- **[Artur Graniszewski](https://github.com/artur-graniszewski/DLSS-Enabler)** - DLSS Enabler that allows DLSS features on non-RTX hardware
-- **[FakeMichau](https://github.com/FakeMichau)** - Various essential tools including fgmod scripts, innoextract, and fakenvapi for AMD/Intel GPU support
+```bash
+pnpm build
+```
-### Community & Documentation
-- **[Deck Wizard](https://www.youtube.com/watch?v=o_TkF-Eiq3M)** - Extensive community support including comprehensive guides, promotional content, thorough testing and feedback, custom artworks, and tutorial videos. His passionate advocacy and continuous support have been instrumental in Decky Framegen's success.
+Decky zip build:
+
+```bash
+bash .vscode/build.sh
+```
+
+If Decky CLI is missing, run:
+
+```bash
+bash .vscode/setup.sh
+```
+
+## Credits
-- **The DLSS2FSR Community** - Ongoing support and guidance for understanding the various mods and tools
+- [Nukem9](https://github.com/Nukem9/dlssg-to-fsr3)
+- [OptiScaler / cdozdil](https://github.com/optiscaler/OptiScaler)
+- [Artur Graniszewski / DLSS Enabler](https://github.com/artur-graniszewski/DLSS-Enabler)
+- [FakeMichau](https://github.com/FakeMichau)
+- Deck Wizard and the DLSS2FSR community
diff --git a/plugin.json b/plugin.json
index 220fe0b..4905e89 100644
--- a/plugin.json
+++ b/plugin.json
@@ -5,7 +5,7 @@
"api_version": 1,
"publish": {
"tags": ["DLSS", "Framegen","upscaling","FSR"],
- "description": "This plugin installs and manages OptiScaler, a tool that enhances upscaling and enables frame generation in a range of DirectX 12 games.",
+ "description": "Prefix-managed OptiScaler integration for Decky that stages per-game payloads inside Proton compatdata instead of the game directory.",
"image": "https://raw.githubusercontent.com/xXJSONDeruloXx/Decky-Framegen/refs/heads/main/assets/optiscaler_final.png"
}
}
diff --git a/src/api/index.ts b/src/api/index.ts
index df52fee..f2e0ea1 100644
--- a/src/api/index.ts
+++ b/src/api/index.ts
@@ -20,6 +20,11 @@ export const listInstalledGames = callable<
{ status: string; games: { appid: string; name: string }[] }
>("list_installed_games");
+export const cleanupManagedGame = callable<
+ [string],
+ { status: string; message?: string; output?: string }
+>("cleanup_managed_game");
+
export const logError = callable<[string], void>("log_error");
export const getPathDefaults = callable<
diff --git a/src/components/ClipboardCommands.tsx b/src/components/ClipboardCommands.tsx
index 5a6f38f..5344a6b 100644
--- a/src/components/ClipboardCommands.tsx
+++ b/src/components/ClipboardCommands.tsx
@@ -9,14 +9,14 @@ export function ClipboardCommands({ pathExists }: ClipboardCommandsProps) {
return (
<>
- <SmartClipboardButton
- command="~/fgmod/fgmod %command%"
- buttonText="Copy Patch Command"
+ <SmartClipboardButton
+ command='OPTISCALER_PROXY=winmm ~/fgmod/fgmod %command%'
+ buttonText="Copy enable launch command"
/>
-
- <SmartClipboardButton
+
+ <SmartClipboardButton
command="~/fgmod/fgmod-uninstaller.sh %command%"
- buttonText="Copy Unpatch Command"
+ buttonText="Copy cleanup launch command"
/>
</>
);
diff --git a/src/components/InstalledGamesSection.tsx b/src/components/InstalledGamesSection.tsx
index 71278d7..eb750c8 100644
--- a/src/components/InstalledGamesSection.tsx
+++ b/src/components/InstalledGamesSection.tsx
@@ -1,122 +1,166 @@
-import { useState, useEffect } from "react";
-import { PanelSection, PanelSectionRow, ButtonItem, DropdownItem, ConfirmModal, showModal } from "@decky/ui";
-import { listInstalledGames, logError } from "../api";
+import { useEffect, useState } from "react";
+import {
+ ButtonItem,
+ ConfirmModal,
+ DropdownItem,
+ PanelSection,
+ PanelSectionRow,
+ showModal,
+} from "@decky/ui";
+import { cleanupManagedGame, listInstalledGames, logError } from "../api";
import { safeAsyncOperation } from "../utils";
-import { STYLES } from "../utils/constants";
import { GameInfo } from "../types/index";
+import { STYLES } from "../utils/constants";
+
+const DEFAULT_LAUNCH_COMMAND = 'OPTISCALER_PROXY=winmm ~/fgmod/fgmod %COMMAND%';
-export function InstalledGamesSection() {
+interface InstalledGamesSectionProps {
+ isAvailable: boolean;
+}
+
+export function InstalledGamesSection({ isAvailable }: InstalledGamesSectionProps) {
const [games, setGames] = useState<GameInfo[]>([]);
const [selectedGame, setSelectedGame] = useState<GameInfo | null>(null);
- const [result, setResult] = useState<string>('');
+ const [result, setResult] = useState<string>("");
+ const [loadingGames, setLoadingGames] = useState(false);
+ const [enabling, setEnabling] = useState(false);
+ const [disabling, setDisabling] = useState(false);
useEffect(() => {
+ if (!isAvailable) return;
+
+ let cancelled = false;
+
const fetchGames = async () => {
- const response = await safeAsyncOperation(
- async () => await listInstalledGames(),
- 'fetchGames'
- );
-
- if (response?.status === "success") {
+ setLoadingGames(true);
+ const response = await safeAsyncOperation(async () => await listInstalledGames(), "InstalledGamesSection.fetchGames");
+
+ if (cancelled || !response) {
+ setLoadingGames(false);
+ return;
+ }
+
+ if (response.status === "success") {
const sortedGames = [...response.games]
- .map(game => ({
+ .map((game) => ({
...game,
- appid: parseInt(game.appid, 10),
+ appid: parseInt(String(game.appid), 10),
}))
.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
setGames(sortedGames);
- } else if (response) {
- logError('fetchGames: ' + JSON.stringify(response));
- console.error('fetchGames: ' + JSON.stringify(response));
+ } else {
+ logError(`InstalledGamesSection.fetchGames: ${JSON.stringify(response)}`);
}
+
+ setLoadingGames(false);
};
-
+
fetchGames();
- }, []);
- const handlePatchClick = async () => {
+ return () => {
+ cancelled = true;
+ };
+ }, [isAvailable]);
+
+ const handleEnable = async () => {
if (!selectedGame) return;
- // Show confirmation modal
showModal(
- <ConfirmModal
- strTitle={`Enable Frame Generation for ${selectedGame.name}?`}
+ <ConfirmModal
+ strTitle={`Enable prefix-managed OptiScaler 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."
+ "This only changes the Steam launch option for the selected game. OptiScaler itself is staged into compatdata/pfx/system32 at launch time and does not write into the game install directory."
}
- strOKButtonText="Enable Frame Generation"
+ strOKButtonText="Enable"
strCancelButtonText="Cancel"
onOK={async () => {
+ setEnabling(true);
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.`);
+ await SteamClient.Apps.SetAppLaunchOptions(selectedGame.appid, DEFAULT_LAUNCH_COMMAND);
+ setResult(`✓ Enabled prefix-managed OptiScaler for ${selectedGame.name}. Launch the game, enable DLSS if needed, then press Insert for the OptiScaler menu.`);
} catch (error) {
- logError('handlePatchClick: ' + String(error));
- setResult(error instanceof Error ? `Error: ${error.message}` : 'Error enabling frame generation');
+ logError(`InstalledGamesSection.handleEnable: ${String(error)}`);
+ setResult(error instanceof Error ? `Error: ${error.message}` : "Error enabling prefix-managed OptiScaler");
+ } finally {
+ setEnabling(false);
}
}}
/>
);
};
- const handleUnpatchClick = async () => {
+ const handleDisable = async () => {
if (!selectedGame) return;
+ setDisabling(true);
try {
- await SteamClient.Apps.SetAppLaunchOptions(selectedGame.appid, '~/fgmod/fgmod-uninstaller.sh %COMMAND%');
- setResult(`✓ Frame generation will be disabled on next launch of ${selectedGame.name}.`);
+ const cleanupResult = await cleanupManagedGame(String(selectedGame.appid));
+ if (cleanupResult?.status !== "success") {
+ setResult(`Error: ${cleanupResult?.message || cleanupResult?.output || "Failed to clean managed compatdata prefix"}`);
+ return;
+ }
+
+ await SteamClient.Apps.SetAppLaunchOptions(selectedGame.appid, "");
+ setResult(`✓ Cleared launch options and cleaned the managed compatdata prefix for ${selectedGame.name}.`);
} catch (error) {
- logError('handleUnpatchClick: ' + String(error));
- setResult(error instanceof Error ? `Error: ${error.message}` : 'Error disabling frame generation');
+ logError(`InstalledGamesSection.handleDisable: ${String(error)}`);
+ setResult(error instanceof Error ? `Error: ${error.message}` : "Error disabling prefix-managed OptiScaler");
+ } finally {
+ setDisabling(false);
}
};
+ if (!isAvailable) return null;
+
return (
- <PanelSection title="Select a Game to Patch:">
+ <PanelSection title="Steam game integration">
<PanelSectionRow>
<DropdownItem
- rgOptions={games.map(game => ({
+ rgOptions={games.map((game) => ({
data: game.appid,
- label: game.name
+ label: game.name,
}))}
selectedOption={selectedGame?.appid}
onChange={(option) => {
- const game = games.find(g => g.appid === option.data);
+ const game = games.find((entry) => entry.appid === option.data);
setSelectedGame(game || null);
- setResult('');
+ setResult("");
}}
- strDefaultLabel="Choose a game"
- menuLabel="Installed Games"
+ strDefaultLabel={loadingGames ? "Loading installed games..." : "Choose a game"}
+ menuLabel="Installed Steam games"
+ disabled={loadingGames || games.length === 0}
/>
</PanelSectionRow>
+ <PanelSectionRow>
+ <div style={STYLES.instructionCard}>
+ Enable writes the launch option automatically. Disable clears launch options and removes staged files from the selected game's compatdata prefix.
+ </div>
+ </PanelSectionRow>
+
{result ? (
<PanelSectionRow>
- <div style={{
- ...STYLES.preWrap,
- ...(result.includes('Error') ? STYLES.statusNotInstalled : STYLES.statusInstalled)
- }}>
- {result.includes('Error') ? '❌' : '✅'} {result}
+ <div
+ style={{
+ ...STYLES.preWrap,
+ ...(result.startsWith("Error") ? STYLES.statusNotInstalled : STYLES.statusInstalled),
+ }}
+ >
+ {result.startsWith("Error") ? "❌" : "✅"} {result}
</div>
</PanelSectionRow>
) : null}
-
+
{selectedGame ? (
<>
<PanelSectionRow>
- <ButtonItem
- layout="below"
- onClick={handlePatchClick}
- >
- Enable Frame Generation
+ <ButtonItem layout="below" onClick={handleEnable} disabled={enabling || disabling}>
+ {enabling ? "Enabling..." : "Enable for selected game"}
</ButtonItem>
</PanelSectionRow>
<PanelSectionRow>
- <ButtonItem
- layout="below"
- onClick={handleUnpatchClick}
- >
- Disable Frame Generation
+ <ButtonItem layout="below" onClick={handleDisable} disabled={enabling || disabling}>
+ {disabling ? "Cleaning..." : "Disable and clean selected game"}
</ButtonItem>
</PanelSectionRow>
</>
diff --git a/src/components/InstructionCard.tsx b/src/components/InstructionCard.tsx
index fdf6755..392c782 100644
--- a/src/components/InstructionCard.tsx
+++ b/src/components/InstructionCard.tsx
@@ -11,12 +11,10 @@ export function InstructionCard({ pathExists }: InstructionCardProps) {
return (
<PanelSectionRow>
<div style={STYLES.instructionCard}>
- <div style={{ fontWeight: 'bold', marginBottom: '8px', color: 'var(--decky-accent-text)' }}>
+ <div style={{ fontWeight: "bold", marginBottom: "8px", color: "var(--decky-accent-text)" }}>
{MESSAGES.instructionTitle}
</div>
- <div style={{ whiteSpace: 'pre-line' }}>
- {MESSAGES.instructionText}
- </div>
+ <div style={{ whiteSpace: "pre-line" }}>{MESSAGES.instructionText}</div>
</div>
</PanelSectionRow>
);
diff --git a/src/components/OptiScalerControls.tsx b/src/components/OptiScalerControls.tsx
index 468683c..4a33b58 100644
--- a/src/components/OptiScalerControls.tsx
+++ b/src/components/OptiScalerControls.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect } from "react";
+import { useEffect, useState } from "react";
import { PanelSection } from "@decky/ui";
import { runInstallFGMod, runUninstallFGMod } from "../api";
import { OperationResult } from "./ResultDisplay";
@@ -10,7 +10,7 @@ import { ClipboardCommands } from "./ClipboardCommands";
import { InstructionCard } from "./InstructionCard";
import { OptiScalerWiki } from "./OptiScalerWiki";
import { UninstallButton } from "./UninstallButton";
-import { ManualPatchControls } from "./CustomPathOverride";
+import { InstalledGamesSection } from "./InstalledGamesSection";
interface OptiScalerControlsProps {
pathExists: boolean | null;
@@ -22,19 +22,15 @@ export function OptiScalerControls({ pathExists, setPathExists }: OptiScalerCont
const [uninstalling, setUninstalling] = useState(false);
const [installResult, setInstallResult] = useState<OperationResult | null>(null);
const [uninstallResult, setUninstallResult] = useState<OperationResult | null>(null);
- const [manualModeEnabled, setManualModeEnabled] = useState(false);
+
useEffect(() => {
- if (installResult) {
- return createAutoCleanupTimer(() => setInstallResult(null), TIMEOUTS.resultDisplay);
- }
- return () => {}; // Ensure a cleanup function is always returned
+ if (!installResult) return () => {};
+ return createAutoCleanupTimer(() => setInstallResult(null), TIMEOUTS.resultDisplay);
}, [installResult]);
useEffect(() => {
- if (uninstallResult) {
- return createAutoCleanupTimer(() => setUninstallResult(null), TIMEOUTS.resultDisplay);
- }
- return () => {}; // Ensure a cleanup function is always returned
+ if (!uninstallResult) return () => {};
+ return createAutoCleanupTimer(() => setUninstallResult(null), TIMEOUTS.resultDisplay);
}, [uninstallResult]);
const handleInstallClick = async () => {
@@ -45,8 +41,8 @@ export function OptiScalerControls({ pathExists, setPathExists }: OptiScalerCont
if (result.status === "success") {
setPathExists(true);
}
- } catch (e) {
- console.error(e);
+ } catch (error) {
+ console.error(error);
} finally {
setInstalling(false);
}
@@ -60,8 +56,8 @@ export function OptiScalerControls({ pathExists, setPathExists }: OptiScalerCont
if (result.status === "success") {
setPathExists(false);
}
- } catch (e) {
- console.error(e);
+ } catch (error) {
+ console.error(error);
} finally {
setUninstalling(false);
}
@@ -69,33 +65,13 @@ export function OptiScalerControls({ pathExists, setPathExists }: OptiScalerCont
return (
<PanelSection>
- <InstallationStatus
- pathExists={pathExists}
- installing={installing}
- onInstallClick={handleInstallClick}
- />
-
+ <InstallationStatus pathExists={pathExists} installing={installing} onInstallClick={handleInstallClick} />
<OptiScalerHeader pathExists={pathExists} />
-
- <ManualPatchControls
- isAvailable={pathExists === true}
- onManualModeChange={setManualModeEnabled}
- />
-
- {!manualModeEnabled && (
- <>
- <ClipboardCommands pathExists={pathExists} />
-
- <InstructionCard pathExists={pathExists} />
- </>
- )}
+ <InstalledGamesSection isAvailable={pathExists === true} />
+ <ClipboardCommands pathExists={pathExists} />
+ <InstructionCard pathExists={pathExists} />
<OptiScalerWiki pathExists={pathExists} />
-
- <UninstallButton
- pathExists={pathExists}
- uninstalling={uninstalling}
- onUninstallClick={handleUninstallClick}
- />
+ <UninstallButton pathExists={pathExists} uninstalling={uninstalling} onUninstallClick={handleUninstallClick} />
</PanelSection>
);
}
diff --git a/src/components/SmartClipboardButton.tsx b/src/components/SmartClipboardButton.tsx
index d88a58a..8cc52b1 100644
--- a/src/components/SmartClipboardButton.tsx
+++ b/src/components/SmartClipboardButton.tsx
@@ -1,119 +1,68 @@
-import { useState, useEffect } from "react";
-import { PanelSectionRow, ButtonItem, ConfirmModal, showModal } from "@decky/ui";
-import { FaClipboard, FaCheck } from "react-icons/fa";
+import { useEffect, useState } from "react";
+import { ButtonItem, PanelSectionRow } from "@decky/ui";
import { toaster } from "@decky/api";
+import { FaCheck, FaClipboard } from "react-icons/fa";
interface SmartClipboardButtonProps {
command?: string;
buttonText?: string;
}
-export function SmartClipboardButton({
- command = "~/fgmod/fgmod %command%",
- buttonText = "Copy Launch Command"
+export function SmartClipboardButton({
+ command = 'OPTISCALER_PROXY=winmm ~/fgmod/fgmod %command%',
+ buttonText = 'Copy Launch Command',
}: SmartClipboardButtonProps) {
const [isLoading, setIsLoading] = useState(false);
const [showSuccess, setShowSuccess] = useState(false);
- // Reset success state after 3 seconds
useEffect(() => {
- if (showSuccess) {
- const timer = setTimeout(() => {
- setShowSuccess(false);
- }, 3000);
- return () => clearTimeout(timer);
- }
- return undefined;
+ if (!showSuccess) return undefined;
+ const timer = setTimeout(() => setShowSuccess(false), 3000);
+ return () => clearTimeout(timer);
}, [showSuccess]);
- const copyToClipboard = async () => {
- if (isLoading || showSuccess) return;
-
- const isPatchCommand = command.includes("fgmod %command%") && !command.includes("uninstaller");
-
- if (isPatchCommand) {
- showModal(
- <ConfirmModal
- strTitle={`Patch Game with OptiScaler?`}
- strDescription={
- "WARNING: Decky Framegen does not unpatch games when uninstalled. Be sure to unpatch the game or run the OptiScaler uninstall script inside the game files if you choose to uninstall the plugin or the game has issues."
- }
- strOKButtonText="Copy Patch Command"
- strCancelButtonText="Cancel"
- onOK={async () => {
- await performCopy();
- }}
- />
- );
- return;
- }
-
- // For non-patch commands, copy directly
- await performCopy();
- };
-
const performCopy = async () => {
if (isLoading || showSuccess) return;
-
+
setIsLoading(true);
try {
- const text = command;
-
- // Use the proven input simulation method
- const tempInput = document.createElement('input');
- tempInput.value = text;
- tempInput.style.position = 'absolute';
- tempInput.style.left = '-9999px';
+ const tempInput = document.createElement("input");
+ tempInput.value = command;
+ tempInput.style.position = "absolute";
+ tempInput.style.left = "-9999px";
document.body.appendChild(tempInput);
-
- // Focus and select the text
tempInput.focus();
tempInput.select();
-
- // Try copying using execCommand first (most reliable in gaming mode)
+
let copySuccess = false;
try {
- if (document.execCommand('copy')) {
+ if (document.execCommand("copy")) {
copySuccess = true;
}
- } catch (e) {
- // If execCommand fails, try navigator.clipboard as fallback
+ } catch (execError) {
try {
- await navigator.clipboard.writeText(text);
+ await navigator.clipboard.writeText(command);
copySuccess = true;
} catch (clipboardError) {
- console.error('Both copy methods failed:', e, clipboardError);
+ console.error("Clipboard copy failed", execError, clipboardError);
}
}
-
- // Clean up
+
document.body.removeChild(tempInput);
-
- if (copySuccess) {
- // Show success feedback in the button instead of toast
- setShowSuccess(true);
- // Verify the copy worked by reading back
- try {
- const readBack = await navigator.clipboard.readText();
- if (readBack !== text) {
- // Copy worked but verification failed - still show success
- console.log('Copy verification failed but copy likely worked');
- }
- } catch (e) {
- // Verification failed but copy likely worked
- console.log('Copy verification unavailable but copy likely worked');
- }
- } else {
+
+ if (!copySuccess) {
toaster.toast({
title: "Copy Failed",
- body: "Unable to copy to clipboard"
+ body: "Unable to copy to clipboard",
});
+ return;
}
+ setShowSuccess(true);
} catch (error) {
toaster.toast({
title: "Copy Failed",
- body: `Error: ${String(error)}`
+ body: `Error: ${String(error)}`,
});
} finally {
setIsLoading(false);
@@ -122,28 +71,16 @@ export function SmartClipboardButton({
return (
<PanelSectionRow>
- <ButtonItem
- layout="below"
- onClick={copyToClipboard}
- disabled={isLoading || showSuccess}
- >
+ <ButtonItem layout="below" onClick={performCopy} disabled={isLoading || showSuccess}>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
{showSuccess ? (
- <FaCheck style={{
- color: "#4CAF50" // Green color for success
- }} />
+ <FaCheck style={{ color: "#4CAF50" }} />
) : isLoading ? (
- <FaClipboard style={{
- animation: "pulse 1s ease-in-out infinite",
- opacity: 0.7
- }} />
+ <FaClipboard style={{ animation: "pulse 1s ease-in-out infinite", opacity: 0.7 }} />
) : (
<FaClipboard />
)}
- <div style={{
- color: showSuccess ? "#4CAF50" : "inherit",
- fontWeight: showSuccess ? "bold" : "normal"
- }}>
+ <div style={{ color: showSuccess ? "#4CAF50" : "inherit", fontWeight: showSuccess ? "bold" : "normal" }}>
{showSuccess ? "Copied to clipboard" : isLoading ? "Copying..." : buttonText}
</div>
</div>
diff --git a/src/components/index.ts b/src/components/index.ts
index cd599ba..18ca2b4 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -1,4 +1,3 @@
-// Component exports for cleaner imports
export { OptiScalerControls } from './OptiScalerControls';
export { InstallationStatus } from './InstallationStatus';
export { OptiScalerHeader } from './OptiScalerHeader';
@@ -8,4 +7,4 @@ export { OptiScalerWiki } from './OptiScalerWiki';
export { UninstallButton } from './UninstallButton';
export { SmartClipboardButton } from './SmartClipboardButton';
export { ResultDisplay } from './ResultDisplay';
-export { ManualPatchControls } from './CustomPathOverride';
+export { InstalledGamesSection } from './InstalledGamesSection';
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index 1f583c0..74ffd7f 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -1,16 +1,3 @@
-// Common types for the application
-
-export interface ResultType {
- status: string;
- message?: string;
- output?: string;
-}
-
-export interface GameType {
- appid: number;
- name: string;
-}
-
// Common style definitions
export const STYLES = {
resultBox: {
@@ -21,19 +8,19 @@ export const STYLES = {
border: '1px solid var(--decky-border-color)',
fontSize: '14px'
},
- statusInstalled: {
+ statusInstalled: {
color: '#22c55e',
fontWeight: 'bold',
fontSize: '14px'
},
- statusNotInstalled: {
+ statusNotInstalled: {
color: '#f97316',
fontWeight: 'bold',
fontSize: '14px'
},
- statusSuccess: { color: "#22c55e" },
- statusError: { color: "#ef4444" },
- preWrap: { whiteSpace: "pre-wrap" as const },
+ statusSuccess: { color: '#22c55e' },
+ statusError: { color: '#ef4444' },
+ preWrap: { whiteSpace: 'pre-wrap' as const },
instructionCard: {
padding: '14px',
backgroundColor: 'var(--decky-selected-ui-bg)',
@@ -47,20 +34,21 @@ export const STYLES = {
// Common timeout values
export const TIMEOUTS = {
- resultDisplay: 5000, // 5 seconds
- pathCheck: 3000 // 3 seconds
+ resultDisplay: 5000,
+ pathCheck: 3000
};
// Message strings
export const MESSAGES = {
- 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.",
- 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."
+ modInstalled: '✅ Prefix-managed OptiScaler runtime installed',
+ modNotInstalled: '❌ Prefix-managed OptiScaler runtime not installed',
+ installing: 'Installing prefix-managed runtime...',
+ installButton: 'Install Prefix-Managed Runtime',
+ uninstalling: 'Removing runtime and cleaning prefixes...',
+ uninstallButton: 'Remove Runtime + Clean Prefixes',
+ installSuccess: '✅ Prefix-managed OptiScaler runtime installed successfully!',
+ uninstallSuccess: '✅ Prefix-managed OptiScaler runtime removed successfully.',
+ instructionTitle: 'How it works:',
+ instructionText:
+ 'Use the Steam game integration section to enable OptiScaler for a specific game, or copy the launch command manually.\n\nOn launch, the plugin stages OptiScaler into compatdata/<appid>/pfx/drive_c/windows/system32 and keeps its writable INI under compatdata/<appid>/optiscaler-managed. The game install directory is left untouched.\n\nDefault proxy: winmm.dll. For advanced testing you can override it in launch options, e.g. OPTISCALER_PROXY=dxgi ~/fgmod/fgmod %command%.\n\nIn-game: press Insert to open the OptiScaler menu.'
};