summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md70
-rw-r--r--src/components/ClipboardCommands.tsx2
-rw-r--r--src/components/InstalledGamesSection.tsx430
-rw-r--r--src/components/SmartClipboardButton.tsx4
-rw-r--r--src/utils/constants.ts2
5 files changed, 454 insertions, 54 deletions
diff --git a/README.md b/README.md
index d6a276b..d6c4de8 100644
--- a/README.md
+++ b/README.md
@@ -14,13 +14,15 @@ Instead, it:
- 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
+- lets you edit per-game OptiScaler settings from the plugin UI
+- can mirror INI changes into the live prefix copy while the game is running
That makes the integration:
- non-invasive
- reversible
- per-game
-- compatible with Steam launch options and future launcher/runtime style integration
+- much closer to a launcher/runtime feature than a file-drop mod installer
## Current default behavior
@@ -31,7 +33,7 @@ The default proxy is:
The default launch command is:
```bash
-OPTISCALER_PROXY=winmm ~/fgmod/fgmod %command%
+~/fgmod/fgmod %command%
```
To clean a game's managed prefix manually:
@@ -45,45 +47,65 @@ To clean a game's managed prefix manually:
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.
+4. Enable a game from the **Steam game integration + live config** section, or copy the launch command manually.
5. Launch the game.
6. Press **Insert** in-game to open the OptiScaler menu.
-## Steam game integration
+## Steam game integration + live config
-The plugin can now manage Steam launch options for a selected installed game.
+The plugin can now:
-Enable:
+- detect the current running game from Steam UI
+- pick any installed Steam game from a dropdown
+- enable the generic prefix-managed launch option automatically
+- disable a game and clean its compatdata-managed OptiScaler state
+- read the selected game's managed or live `OptiScaler.ini`
+- persist per-game proxy preference
+- update quick OptiScaler settings from the plugin UI
+- save the full raw `OptiScaler.ini`
+- push config changes into the currently staged live prefix copy while the game is running
-- writes `OPTISCALER_PROXY=winmm ~/fgmod/fgmod %COMMAND%` into Steam launch options
+Enable writes this launch option:
-Disable:
+```bash
+~/fgmod/fgmod %COMMAND%
+```
-- clears Steam launch options
-- cleans the managed OptiScaler files from the game's compatdata prefix
+The wrapper then resolves the proxy using this order:
-## Advanced notes
+1. `OPTISCALER_PROXY` / `DLL` environment override if provided
+2. saved per-game preferred proxy from `manifest.env`
+3. fallback default `winmm`
-### Config persistence
+## Config persistence
-`OptiScaler.ini` is stored per game under:
+Per-game config lives here:
```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.
+During launch the wrapper copies that INI into:
-### Proxy override
+```text
+compatdata/<appid>/pfx/drive_c/windows/system32/OptiScaler.ini
+```
-You can test a different proxy by changing the launch option manually:
+When the game exits, the staged INI is synced back to the managed location.
-```bash
-OPTISCALER_PROXY=dxgi ~/fgmod/fgmod %command%
-OPTISCALER_PROXY=version ~/fgmod/fgmod %command%
-```
+## Live editing caveat
+
+The plugin can write directly into the live prefix INI while the game is running.
+
+That means:
+
+- the file is updated immediately
+- persistence is preserved
+- some settings may still require OptiScaler or the game to reload/relaunch before they fully take effect, depending on what OptiScaler hot-reloads internally
+
+In other words: **live file update is supported**, but **live behavioral reload depends on OptiScaler/game behavior**.
-Supported values currently include:
+## Supported proxy values
- `winmm`
- `dxgi`
@@ -93,12 +115,12 @@ Supported values currently include:
- `wininet`
- `d3d12`
-### Environment-driven INI updates
+## Environment-driven INI updates
-The existing OptiScaler env var patching still works. For example:
+The wrapper still supports environment-based INI overrides. Example:
```bash
-Dx12Upscaler=fsr31 FrameGen_Enabled=true OPTISCALER_PROXY=winmm ~/fgmod/fgmod %command%
+Dx12Upscaler=fsr31 FrameGen_Enabled=true ~/fgmod/fgmod %command%
```
## Technical summary
diff --git a/src/components/ClipboardCommands.tsx b/src/components/ClipboardCommands.tsx
index 5344a6b..d822929 100644
--- a/src/components/ClipboardCommands.tsx
+++ b/src/components/ClipboardCommands.tsx
@@ -10,7 +10,7 @@ export function ClipboardCommands({ pathExists }: ClipboardCommandsProps) {
return (
<>
<SmartClipboardButton
- command='OPTISCALER_PROXY=winmm ~/fgmod/fgmod %command%'
+ command="~/fgmod/fgmod %command%"
buttonText="Copy enable launch command"
/>
diff --git a/src/components/InstalledGamesSection.tsx b/src/components/InstalledGamesSection.tsx
index eb750c8..4ed345d 100644
--- a/src/components/InstalledGamesSection.tsx
+++ b/src/components/InstalledGamesSection.tsx
@@ -1,30 +1,144 @@
-import { useEffect, useState } from "react";
+import { useCallback, useEffect, useMemo, useState } from "react";
import {
ButtonItem,
ConfirmModal,
DropdownItem,
+ Field,
PanelSection,
PanelSectionRow,
+ SliderField,
+ Router,
showModal,
} from "@decky/ui";
-import { cleanupManagedGame, listInstalledGames, logError } from "../api";
+import {
+ cleanupManagedGame,
+ getGameConfig,
+ listInstalledGames,
+ logError,
+ saveGameConfig,
+} from "../api";
import { safeAsyncOperation } from "../utils";
-import { GameInfo } from "../types/index";
+import type { ApiResponse, GameConfigResponse, GameInfo } from "../types/index";
import { STYLES } from "../utils/constants";
-const DEFAULT_LAUNCH_COMMAND = 'OPTISCALER_PROXY=winmm ~/fgmod/fgmod %COMMAND%';
+const DEFAULT_LAUNCH_COMMAND = "~/fgmod/fgmod %COMMAND%";
+const POLL_INTERVAL_MS = 3000;
+
+type RunningApp = {
+ appid: string;
+ display_name: string;
+};
+
+const PROXY_OPTIONS = ["winmm", "dxgi", "version", "dbghelp", "winhttp", "wininet", "d3d12"];
+const UPSCALER_OPTIONS = ["auto", "fsr31", "xess", "dlss", "native"];
+const TRI_STATE_OPTIONS = ["auto", "true", "false"];
+const FG_INPUT_OPTIONS = ["auto", "fsrfg", "xefg", "dlssg"];
+const FG_OUTPUT_OPTIONS = ["auto", "fsrfg", "xefg"];
+
+const defaultQuickSettings = {
+ Dx12Upscaler: "auto",
+ "FrameGen.Enabled": "auto",
+ FGInput: "auto",
+ FGOutput: "auto",
+ Fsr4ForceCapable: "false",
+ Fsr4EnableWatermark: "false",
+ UseHQFont: "false",
+ "Menu.Scale": "1.000000",
+};
interface InstalledGamesSectionProps {
isAvailable: boolean;
}
+const normalizeSettings = (settings?: Record<string, string>) => ({
+ ...defaultQuickSettings,
+ ...(settings || {}),
+});
+
+const formatResult = (result: ApiResponse | null, fallbackSuccess: string) => {
+ if (!result) return "";
+ if (result.status === "success") {
+ return result.message || result.output || fallbackSuccess;
+ }
+ return `Error: ${result.message || result.output || "Operation failed"}`;
+};
+
export function InstalledGamesSection({ isAvailable }: InstalledGamesSectionProps) {
const [games, setGames] = useState<GameInfo[]>([]);
const [selectedGame, setSelectedGame] = useState<GameInfo | null>(null);
- const [result, setResult] = useState<string>("");
+ const [runningApps, setRunningApps] = useState<RunningApp[]>([]);
+ const [mainRunningApp, setMainRunningApp] = useState<RunningApp | null>(null);
const [loadingGames, setLoadingGames] = useState(false);
const [enabling, setEnabling] = useState(false);
const [disabling, setDisabling] = useState(false);
+ const [configLoading, setConfigLoading] = useState(false);
+ const [configResult, setConfigResult] = useState<string>("");
+ const [config, setConfig] = useState<GameConfigResponse | null>(null);
+ const [quickSettings, setQuickSettings] = useState<Record<string, string>>(defaultQuickSettings);
+ const [selectedProxy, setSelectedProxy] = useState<string>("winmm");
+ const [rawIni, setRawIni] = useState<string>("");
+ const [savingQuick, setSavingQuick] = useState(false);
+ const [savingQuickLive, setSavingQuickLive] = useState(false);
+ const [savingRaw, setSavingRaw] = useState(false);
+ const [savingRawLive, setSavingRawLive] = useState(false);
+
+ const selectedAppId = selectedGame ? String(selectedGame.appid) : null;
+ const selectedIsRunning = useMemo(
+ () => Boolean(selectedAppId && runningApps.some((app) => String(app.appid) === selectedAppId)),
+ [runningApps, selectedAppId]
+ );
+
+ const refreshRunningApps = useCallback(() => {
+ try {
+ const nextRunningApps = ((Router?.RunningApps || []) as RunningApp[])
+ .filter((app) => app?.appid && app?.display_name)
+ .map((app) => ({ appid: String(app.appid), display_name: app.display_name }));
+
+ const nextMainRunningApp = Router?.MainRunningApp
+ ? {
+ appid: String(Router.MainRunningApp.appid),
+ display_name: Router.MainRunningApp.display_name,
+ }
+ : nextRunningApps[0] || null;
+
+ setRunningApps(nextRunningApps);
+ setMainRunningApp(nextMainRunningApp);
+
+ if (!selectedGame && nextMainRunningApp) {
+ setSelectedGame({
+ appid: nextMainRunningApp.appid,
+ name: nextMainRunningApp.display_name,
+ });
+ }
+ } catch (error) {
+ console.error("InstalledGamesSection.refreshRunningApps", error);
+ }
+ }, [selectedGame]);
+
+ const loadConfig = useCallback(
+ async (appid: string) => {
+ setConfigLoading(true);
+ const response = await safeAsyncOperation(() => getGameConfig(appid), `InstalledGamesSection.loadConfig.${appid}`);
+ if (!response) {
+ setConfigLoading(false);
+ return;
+ }
+
+ if (response.status === "success") {
+ setConfig(response);
+ setQuickSettings(normalizeSettings(response.settings));
+ setSelectedProxy(response.proxy || "winmm");
+ setRawIni(response.raw_ini || "");
+ setConfigResult("");
+ } else {
+ setConfig(null);
+ setConfigResult(`Error: ${response.message || response.output || "Failed to load game config"}`);
+ }
+
+ setConfigLoading(false);
+ },
+ []
+ );
useEffect(() => {
if (!isAvailable) return;
@@ -34,7 +148,6 @@ export function InstalledGamesSection({ isAvailable }: InstalledGamesSectionProp
const fetchGames = async () => {
setLoadingGames(true);
const response = await safeAsyncOperation(async () => await listInstalledGames(), "InstalledGamesSection.fetchGames");
-
if (cancelled || !response) {
setLoadingGames(false);
return;
@@ -56,11 +169,19 @@ export function InstalledGamesSection({ isAvailable }: InstalledGamesSectionProp
};
fetchGames();
+ refreshRunningApps();
+ const interval = setInterval(refreshRunningApps, POLL_INTERVAL_MS);
return () => {
cancelled = true;
+ clearInterval(interval);
};
- }, [isAvailable]);
+ }, [isAvailable, refreshRunningApps]);
+
+ useEffect(() => {
+ if (!isAvailable || !selectedAppId) return;
+ void loadConfig(selectedAppId);
+ }, [isAvailable, loadConfig, selectedAppId]);
const handleEnable = async () => {
if (!selectedGame) return;
@@ -77,10 +198,10 @@ export function InstalledGamesSection({ isAvailable }: InstalledGamesSectionProp
setEnabling(true);
try {
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.`);
+ setConfigResult(`✓ Enabled prefix-managed OptiScaler for ${selectedGame.name}. Launch the game, enable DLSS if needed, then press Insert for the OptiScaler menu.`);
} catch (error) {
logError(`InstalledGamesSection.handleEnable: ${String(error)}`);
- setResult(error instanceof Error ? `Error: ${error.message}` : "Error enabling prefix-managed OptiScaler");
+ setConfigResult(error instanceof Error ? `Error: ${error.message}` : "Error enabling prefix-managed OptiScaler");
} finally {
setEnabling(false);
}
@@ -96,35 +217,104 @@ export function InstalledGamesSection({ isAvailable }: InstalledGamesSectionProp
try {
const cleanupResult = await cleanupManagedGame(String(selectedGame.appid));
if (cleanupResult?.status !== "success") {
- setResult(`Error: ${cleanupResult?.message || cleanupResult?.output || "Failed to clean managed compatdata prefix"}`);
+ setConfigResult(`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}.`);
+ setConfig(null);
+ setQuickSettings(defaultQuickSettings);
+ setSelectedProxy("winmm");
+ setRawIni("");
+ setConfigResult(`✓ Cleared launch options and cleaned the managed compatdata prefix for ${selectedGame.name}.`);
} catch (error) {
logError(`InstalledGamesSection.handleDisable: ${String(error)}`);
- setResult(error instanceof Error ? `Error: ${error.message}` : "Error disabling prefix-managed OptiScaler");
+ setConfigResult(error instanceof Error ? `Error: ${error.message}` : "Error disabling prefix-managed OptiScaler");
} finally {
setDisabling(false);
}
};
+ const saveQuickSettings = async (applyLive: boolean) => {
+ if (!selectedAppId) return;
+
+ const setBusy = applyLive ? setSavingQuickLive : setSavingQuick;
+ setBusy(true);
+ try {
+ const response = await saveGameConfig(selectedAppId, quickSettings, selectedProxy, applyLive, null);
+ setConfigResult(formatResult(response, applyLive ? "Applied config to the running game." : "Saved config."));
+ await loadConfig(selectedAppId);
+ } catch (error) {
+ setConfigResult(error instanceof Error ? `Error: ${error.message}` : "Error saving config");
+ } finally {
+ setBusy(false);
+ }
+ };
+
+ const saveRawEditor = async (applyLive: boolean) => {
+ if (!selectedAppId) return;
+
+ const setBusy = applyLive ? setSavingRawLive : setSavingRaw;
+ setBusy(true);
+ try {
+ const response = await saveGameConfig(selectedAppId, {}, selectedProxy, applyLive, rawIni);
+ setConfigResult(formatResult(response, applyLive ? "Applied raw INI to the running game." : "Saved raw INI."));
+ await loadConfig(selectedAppId);
+ } catch (error) {
+ setConfigResult(error instanceof Error ? `Error: ${error.message}` : "Error saving raw INI");
+ } finally {
+ setBusy(false);
+ }
+ };
+
if (!isAvailable) return null;
return (
- <PanelSection title="Steam game integration">
+ <PanelSection title="Steam game integration + live config">
+ <PanelSectionRow>
+ <Field
+ label="Running now"
+ description={
+ mainRunningApp
+ ? `Main running game: ${mainRunningApp.display_name}`
+ : "No running Steam game detected right now."
+ }
+ >
+ <div style={{ ...STYLES.preWrap, fontSize: "12px" }}>
+ {runningApps.length > 0
+ ? runningApps.map((app) => app.display_name).join("\n")
+ : "Idle"}
+ </div>
+ </Field>
+ </PanelSectionRow>
+
+ {mainRunningApp ? (
+ <PanelSectionRow>
+ <ButtonItem
+ layout="below"
+ onClick={() =>
+ setSelectedGame({
+ appid: mainRunningApp.appid,
+ name: mainRunningApp.display_name,
+ })
+ }
+ >
+ Use current running game
+ </ButtonItem>
+ </PanelSectionRow>
+ ) : null}
+
<PanelSectionRow>
<DropdownItem
rgOptions={games.map((game) => ({
- data: game.appid,
+ data: String(game.appid),
label: game.name,
}))}
- selectedOption={selectedGame?.appid}
+ selectedOption={selectedAppId}
onChange={(option) => {
- const game = games.find((entry) => entry.appid === option.data);
+ const game = games.find((entry) => String(entry.appid) === String(option.data));
setSelectedGame(game || null);
- setResult("");
+ setConfigResult("");
}}
strDefaultLabel={loadingGames ? "Loading installed games..." : "Choose a game"}
menuLabel="Installed Steam games"
@@ -134,33 +324,221 @@ export function InstalledGamesSection({ isAvailable }: InstalledGamesSectionProp
<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.
+ Enable writes the wrapper launch option automatically. Disable clears launch options and removes staged files from the selected game's compatdata prefix. The config editor below persists changes to the selected game and can also mirror them into the live prefix copy while the game is running.
</div>
</PanelSectionRow>
- {result ? (
+ {selectedGame ? (
+ <>
+ <PanelSectionRow>
+ <Field
+ label="Selected game"
+ description={selectedIsRunning ? "Detected as currently running." : "Not currently running."}
+ >
+ {selectedGame.name}
+ </Field>
+ </PanelSectionRow>
+
+ <PanelSectionRow>
+ <ButtonItem layout="below" onClick={handleEnable} disabled={enabling || disabling}>
+ {enabling ? "Enabling..." : "Enable for selected game"}
+ </ButtonItem>
+ </PanelSectionRow>
+
+ <PanelSectionRow>
+ <ButtonItem layout="below" onClick={handleDisable} disabled={enabling || disabling}>
+ {disabling ? "Cleaning..." : "Disable and clean selected game"}
+ </ButtonItem>
+ </PanelSectionRow>
+
+ <PanelSectionRow>
+ <ButtonItem layout="below" onClick={() => loadConfig(String(selectedGame.appid))} disabled={configLoading}>
+ {configLoading ? "Loading config..." : "Reload selected game config"}
+ </ButtonItem>
+ </PanelSectionRow>
+ </>
+ ) : null}
+
+ {configResult ? (
<PanelSectionRow>
<div
style={{
...STYLES.preWrap,
- ...(result.startsWith("Error") ? STYLES.statusNotInstalled : STYLES.statusInstalled),
+ ...(configResult.startsWith("Error") ? STYLES.statusNotInstalled : STYLES.statusInstalled),
}}
>
- {result.startsWith("Error") ? "❌" : "✅"} {result}
+ {configResult.startsWith("Error") ? "❌" : "✅"} {configResult}
</div>
</PanelSectionRow>
) : null}
- {selectedGame ? (
+ {selectedGame && config ? (
<>
<PanelSectionRow>
- <ButtonItem layout="below" onClick={handleEnable} disabled={enabling || disabling}>
- {enabling ? "Enabling..." : "Enable for selected game"}
+ <Field
+ label="Managed paths"
+ description={config.live_available ? "Live prefix copy is present." : "Live prefix copy is not staged right now."}
+ >
+ <div style={{ ...STYLES.preWrap, fontSize: "11px", wordBreak: "break-word" }}>
+ {config.paths?.managed_ini ? `Managed INI: ${config.paths.managed_ini}\n` : ""}
+ {config.paths?.live_ini ? `Live INI: ${config.paths.live_ini}` : ""}
+ </div>
+ </Field>
+ </PanelSectionRow>
+
+ <PanelSectionRow>
+ <DropdownItem
+ rgOptions={PROXY_OPTIONS.map((proxy) => ({ data: proxy, label: proxy }))}
+ selectedOption={selectedProxy}
+ onChange={(option) => setSelectedProxy(String(option.data))}
+ menuLabel="Proxy DLL"
+ strDefaultLabel="Proxy DLL"
+ description="Persisted per game and used by the wrapper on next launch."
+ />
+ </PanelSectionRow>
+
+ <PanelSectionRow>
+ <DropdownItem
+ rgOptions={UPSCALER_OPTIONS.map((value) => ({ data: value, label: value }))}
+ selectedOption={quickSettings.Dx12Upscaler}
+ onChange={(option) => setQuickSettings((prev) => ({ ...prev, Dx12Upscaler: String(option.data) }))}
+ menuLabel="DX12 upscaler"
+ strDefaultLabel="DX12 upscaler"
+ />
+ </PanelSectionRow>
+
+ <PanelSectionRow>
+ <DropdownItem
+ rgOptions={TRI_STATE_OPTIONS.map((value) => ({ data: value, label: value }))}
+ selectedOption={quickSettings["FrameGen.Enabled"]}
+ onChange={(option) => setQuickSettings((prev) => ({ ...prev, "FrameGen.Enabled": String(option.data) }))}
+ menuLabel="Frame generation enabled"
+ strDefaultLabel="Frame generation enabled"
+ />
+ </PanelSectionRow>
+
+ <PanelSectionRow>
+ <DropdownItem
+ rgOptions={FG_INPUT_OPTIONS.map((value) => ({ data: value, label: value }))}
+ selectedOption={quickSettings.FGInput}
+ onChange={(option) => setQuickSettings((prev) => ({ ...prev, FGInput: String(option.data) }))}
+ menuLabel="FG input"
+ strDefaultLabel="FG input"
+ />
+ </PanelSectionRow>
+
+ <PanelSectionRow>
+ <DropdownItem
+ rgOptions={FG_OUTPUT_OPTIONS.map((value) => ({ data: value, label: value }))}
+ selectedOption={quickSettings.FGOutput}
+ onChange={(option) => setQuickSettings((prev) => ({ ...prev, FGOutput: String(option.data) }))}
+ menuLabel="FG output"
+ strDefaultLabel="FG output"
+ />
+ </PanelSectionRow>
+
+ <PanelSectionRow>
+ <DropdownItem
+ rgOptions={TRI_STATE_OPTIONS.map((value) => ({ data: value, label: value }))}
+ selectedOption={quickSettings.Fsr4ForceCapable}
+ onChange={(option) => setQuickSettings((prev) => ({ ...prev, Fsr4ForceCapable: String(option.data) }))}
+ menuLabel="FSR4 force capable"
+ strDefaultLabel="FSR4 force capable"
+ />
+ </PanelSectionRow>
+
+ <PanelSectionRow>
+ <DropdownItem
+ rgOptions={TRI_STATE_OPTIONS.map((value) => ({ data: value, label: value }))}
+ selectedOption={quickSettings.Fsr4EnableWatermark}
+ onChange={(option) => setQuickSettings((prev) => ({ ...prev, Fsr4EnableWatermark: String(option.data) }))}
+ menuLabel="FSR4 watermark"
+ strDefaultLabel="FSR4 watermark"
+ />
+ </PanelSectionRow>
+
+ <PanelSectionRow>
+ <DropdownItem
+ rgOptions={TRI_STATE_OPTIONS.map((value) => ({ data: value, label: value }))}
+ selectedOption={quickSettings.UseHQFont}
+ onChange={(option) => setQuickSettings((prev) => ({ ...prev, UseHQFont: String(option.data) }))}
+ menuLabel="Use HQ font"
+ strDefaultLabel="Use HQ font"
+ />
+ </PanelSectionRow>
+
+ <PanelSectionRow>
+ <SliderField
+ label="Menu scale"
+ value={Number.parseFloat(quickSettings["Menu.Scale"] || "1") || 1}
+ min={0.5}
+ max={2.0}
+ step={0.05}
+ showValue
+ editableValue
+ onChange={(value) =>
+ setQuickSettings((prev) => ({
+ ...prev,
+ "Menu.Scale": value.toFixed(6),
+ }))
+ }
+ />
+ </PanelSectionRow>
+
+ <PanelSectionRow>
+ <ButtonItem layout="below" onClick={() => saveQuickSettings(false)} disabled={savingQuick || savingQuickLive}>
+ {savingQuick ? "Saving..." : "Apply + persist quick settings"}
</ButtonItem>
</PanelSectionRow>
+
<PanelSectionRow>
- <ButtonItem layout="below" onClick={handleDisable} disabled={enabling || disabling}>
- {disabling ? "Cleaning..." : "Disable and clean selected game"}
+ <ButtonItem
+ layout="below"
+ onClick={() => saveQuickSettings(true)}
+ disabled={!selectedIsRunning || savingQuick || savingQuickLive}
+ >
+ {savingQuickLive ? "Applying live..." : "Apply quick settings to running game now"}
+ </ButtonItem>
+ </PanelSectionRow>
+
+ <PanelSectionRow>
+ <Field
+ label="Advanced raw INI editor"
+ description="Edits the selected game's OptiScaler.ini directly. Use this for settings not exposed above."
+ >
+ <textarea
+ value={rawIni}
+ onChange={(event) => setRawIni(event.target.value)}
+ style={{
+ width: "100%",
+ minHeight: "280px",
+ resize: "vertical",
+ boxSizing: "border-box",
+ borderRadius: "8px",
+ border: "1px solid var(--decky-border-color)",
+ background: "rgba(255,255,255,0.05)",
+ color: "inherit",
+ padding: "10px",
+ fontFamily: "monospace",
+ fontSize: "11px",
+ }}
+ />
+ </Field>
+ </PanelSectionRow>
+
+ <PanelSectionRow>
+ <ButtonItem layout="below" onClick={() => saveRawEditor(false)} disabled={savingRaw || savingRawLive}>
+ {savingRaw ? "Saving raw INI..." : "Save raw INI"}
+ </ButtonItem>
+ </PanelSectionRow>
+
+ <PanelSectionRow>
+ <ButtonItem
+ layout="below"
+ onClick={() => saveRawEditor(true)}
+ disabled={!selectedIsRunning || savingRaw || savingRawLive}
+ >
+ {savingRawLive ? "Applying raw INI live..." : "Save raw INI + apply live"}
</ButtonItem>
</PanelSectionRow>
</>
diff --git a/src/components/SmartClipboardButton.tsx b/src/components/SmartClipboardButton.tsx
index 8cc52b1..78f76b5 100644
--- a/src/components/SmartClipboardButton.tsx
+++ b/src/components/SmartClipboardButton.tsx
@@ -9,8 +9,8 @@ interface SmartClipboardButtonProps {
}
export function SmartClipboardButton({
- command = 'OPTISCALER_PROXY=winmm ~/fgmod/fgmod %command%',
- buttonText = 'Copy Launch Command',
+ command = "~/fgmod/fgmod %command%",
+ buttonText = "Copy Launch Command",
}: SmartClipboardButtonProps) {
const [isLoading, setIsLoading] = useState(false);
const [showSuccess, setShowSuccess] = useState(false);
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index 74ffd7f..d764e8f 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -50,5 +50,5 @@ export const MESSAGES = {
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.'
+ '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\nYou can now detect the current running game from the plugin, persist per-game proxy/config changes, and mirror INI edits into the live prefix copy while the game is running.\n\nDefault proxy: winmm.dll unless you set a different per-game preference. In-game: press Insert to open the OptiScaler menu.'
};