summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorxXJSONDeruloXx <danielhimebauch@gmail.com>2025-09-26 20:14:44 -0400
committerxXJSONDeruloXx <danielhimebauch@gmail.com>2025-09-26 20:14:44 -0400
commit1cc6a781fe7d6c053b2eb59aa79766a859c66209 (patch)
tree0946a308568ff7a144a6cec29719334a4e6d754b
parent7d2322e637faae5ccfab58c54f7a13e6a5f7ea88 (diff)
downloadDecky-Framegen-1cc6a781fe7d6c053b2eb59aa79766a859c66209.tar.gz
Decky-Framegen-1cc6a781fe7d6c053b2eb59aa79766a859c66209.zip
revised approach, pick dir then patch in plugin ui itself
-rw-r--r--main.py208
-rw-r--r--src/api/index.ts10
-rw-r--r--src/components/ClipboardCommands.tsx10
-rw-r--r--src/components/CustomPathOverride.tsx445
-rw-r--r--src/components/OptiScalerControls.tsx17
-rw-r--r--src/components/index.ts1
-rw-r--r--src/types/index.ts11
7 files changed, 388 insertions, 314 deletions
diff --git a/main.py b/main.py
index a22acca..881298a 100644
--- a/main.py
+++ b/main.py
@@ -10,6 +10,56 @@ from pathlib import Path
# Set to False or comment out this constant to skip the overwrite by default.
UPSCALER_OVERWRITE_ENABLED = True
+INJECTOR_FILENAMES = [
+ "dxgi.dll",
+ "winmm.dll",
+ "nvngx.dll",
+ "_nvngx.dll",
+ "nvngx-wrapper.dll",
+ "dlss-enabler.dll",
+ "OptiScaler.dll",
+]
+
+ORIGINAL_DLL_BACKUPS = [
+ "d3dcompiler_47.dll",
+ "amd_fidelityfx_dx12.dll",
+ "amd_fidelityfx_framegeneration_dx12.dll",
+ "amd_fidelityfx_upscaler_dx12.dll",
+ "amd_fidelityfx_vk.dll",
+]
+
+SUPPORT_FILES = [
+ "libxess.dll",
+ "libxess_dx11.dll",
+ "libxess_fg.dll",
+ "libxell.dll",
+ "amd_fidelityfx_dx12.dll",
+ "amd_fidelityfx_framegeneration_dx12.dll",
+ "amd_fidelityfx_upscaler_dx12.dll",
+ "amd_fidelityfx_vk.dll",
+ "nvngx.dll",
+ "dlssg_to_fsr3_amd_is_better.dll",
+ "fakenvapi.dll",
+ "fakenvapi.ini",
+]
+
+LEGACY_FILES = [
+ "dlssg_to_fsr3.ini",
+ "dlssg_to_fsr3.log",
+ "nvapi64.dll",
+ "nvapi64.dll.b",
+ "fakenvapi.log",
+ "dlss-enabler.dll",
+ "dlss-enabler-upscaler.dll",
+ "dlss-enabler.log",
+ "nvngx-wrapper.dll",
+ "_nvngx.dll",
+ "dlssg_to_fsr3_amd_is_better-3.0.dll",
+ "OptiScaler.asi",
+ "OptiScaler.ini",
+ "OptiScaler.log",
+]
+
class Plugin:
async def _main(self):
decky.logger.info("Framegen plugin loaded")
@@ -380,6 +430,146 @@ class Plugin:
else:
return {"exists": False}
+ def _resolve_target_directory(self, directory: str) -> Path:
+ target = Path(directory).expanduser()
+ if not target.exists():
+ raise FileNotFoundError(f"Target directory does not exist: {directory}")
+ if not target.is_dir():
+ raise NotADirectoryError(f"Target path is not a directory: {directory}")
+ if not os.access(target, os.W_OK | os.X_OK):
+ raise PermissionError(f"Insufficient permissions for {directory}")
+ return target
+
+ def _manual_patch_directory_impl(self, directory: Path) -> dict:
+ fgmod_path = Path(decky.HOME) / "fgmod"
+ if not fgmod_path.exists():
+ return {
+ "status": "error",
+ "message": "OptiScaler bundle not installed. Run Install first.",
+ }
+
+ optiscaler_dll = fgmod_path / "OptiScaler.dll"
+ if not optiscaler_dll.exists():
+ return {
+ "status": "error",
+ "message": "OptiScaler.dll not found in ~/fgmod. Reinstall OptiScaler.",
+ }
+
+ dll_name = "dxgi.dll"
+ preserve_ini = True
+
+ try:
+ decky.logger.info(f"Manual patch started for {directory}")
+
+ for filename in INJECTOR_FILENAMES:
+ path = directory / filename
+ if path.exists():
+ path.unlink()
+
+ for dll in ORIGINAL_DLL_BACKUPS:
+ source = directory / dll
+ backup = directory / f"{dll}.b"
+ if source.exists() and not backup.exists():
+ shutil.move(source, backup)
+
+ for legacy in ["nvapi64.dll", "nvapi64.dll.b"]:
+ legacy_path = directory / legacy
+ if legacy_path.exists():
+ legacy_path.unlink()
+
+ renamed = fgmod_path / "renames" / dll_name
+ destination_dll = directory / dll_name
+ source_for_copy = renamed if renamed.exists() else optiscaler_dll
+ shutil.copy2(source_for_copy, destination_dll)
+
+ target_ini = directory / "OptiScaler.ini"
+ source_ini = fgmod_path / "OptiScaler.ini"
+ if preserve_ini and target_ini.exists():
+ decky.logger.info(f"Preserving existing OptiScaler.ini at {target_ini}")
+ elif source_ini.exists():
+ shutil.copy2(source_ini, target_ini)
+
+ plugins_src = fgmod_path / "plugins"
+ plugins_dest = directory / "plugins"
+ if plugins_src.exists():
+ shutil.copytree(plugins_src, plugins_dest, dirs_exist_ok=True)
+
+ for filename in SUPPORT_FILES:
+ source = fgmod_path / filename
+ if source.exists():
+ shutil.copy2(source, directory / filename)
+
+ decky.logger.info(f"Manual patch complete for {directory}")
+ return {
+ "status": "success",
+ "message": f"OptiScaler files copied to {directory}",
+ }
+
+ except PermissionError as exc:
+ decky.logger.error(f"Manual patch permission error: {exc}")
+ return {
+ "status": "error",
+ "message": f"Permission error while patching: {exc}",
+ }
+ except Exception as exc:
+ decky.logger.error(f"Manual patch failed: {exc}")
+ return {
+ "status": "error",
+ "message": f"Manual patch failed: {exc}",
+ }
+
+ def _manual_unpatch_directory_impl(self, directory: Path) -> dict:
+ try:
+ decky.logger.info(f"Manual unpatch started for {directory}")
+
+ for filename in set(INJECTOR_FILENAMES + SUPPORT_FILES):
+ path = directory / filename
+ if path.exists():
+ path.unlink()
+
+ for legacy in LEGACY_FILES:
+ path = directory / legacy
+ if path.exists():
+ try:
+ path.unlink()
+ except IsADirectoryError:
+ shutil.rmtree(path, ignore_errors=True)
+
+ plugins_dir = directory / "plugins"
+ if plugins_dir.exists():
+ shutil.rmtree(plugins_dir, ignore_errors=True)
+
+ for dll in ORIGINAL_DLL_BACKUPS:
+ backup = directory / f"{dll}.b"
+ original = directory / dll
+ if backup.exists():
+ if original.exists():
+ original.unlink()
+ shutil.move(backup, original)
+
+ uninstaller = directory / "fgmod-uninstaller.sh"
+ if uninstaller.exists():
+ uninstaller.unlink()
+
+ decky.logger.info(f"Manual unpatch complete for {directory}")
+ return {
+ "status": "success",
+ "message": f"OptiScaler files removed from {directory}",
+ }
+
+ except PermissionError as exc:
+ decky.logger.error(f"Manual unpatch permission error: {exc}")
+ return {
+ "status": "error",
+ "message": f"Permission error while unpatching: {exc}",
+ }
+ except Exception as exc:
+ decky.logger.error(f"Manual unpatch failed: {exc}")
+ return {
+ "status": "error",
+ "message": f"Manual unpatch failed: {exc}",
+ }
+
async def list_installed_games(self) -> dict:
try:
steam_root = Path(decky.HOME) / ".steam" / "steam"
@@ -444,3 +634,21 @@ class Plugin:
async def log_error(self, error: str) -> None:
decky.logger.error(f"FRONTEND: {error}")
+
+ async def manual_patch_directory(self, directory: str) -> dict:
+ try:
+ target_dir = self._resolve_target_directory(directory)
+ except (FileNotFoundError, NotADirectoryError, PermissionError) as exc:
+ decky.logger.error(f"Manual patch validation failed: {exc}")
+ return {"status": "error", "message": str(exc)}
+
+ return self._manual_patch_directory_impl(target_dir)
+
+ async def manual_unpatch_directory(self, directory: str) -> dict:
+ try:
+ target_dir = self._resolve_target_directory(directory)
+ except (FileNotFoundError, NotADirectoryError, PermissionError) as exc:
+ decky.logger.error(f"Manual unpatch validation failed: {exc}")
+ return {"status": "error", "message": str(exc)}
+
+ return self._manual_unpatch_directory_impl(target_dir)
diff --git a/src/api/index.ts b/src/api/index.ts
index 1cc3285..df52fee 100644
--- a/src/api/index.ts
+++ b/src/api/index.ts
@@ -26,3 +26,13 @@ export const getPathDefaults = callable<
[],
{ home: string; steam_common?: string }
>("get_path_defaults");
+
+export const runManualPatch = callable<
+ [string],
+ { status: string; message?: string; output?: string }
+>("manual_patch_directory");
+
+export const runManualUnpatch = callable<
+ [string],
+ { status: string; message?: string; output?: string }
+>("manual_unpatch_directory");
diff --git a/src/components/ClipboardCommands.tsx b/src/components/ClipboardCommands.tsx
index f96b460..5a6f38f 100644
--- a/src/components/ClipboardCommands.tsx
+++ b/src/components/ClipboardCommands.tsx
@@ -1,22 +1,16 @@
import { SmartClipboardButton } from "./SmartClipboardButton";
-import type { CustomOverrideConfig } from "../types/index";
interface ClipboardCommandsProps {
pathExists: boolean | null;
- overrideConfig?: CustomOverrideConfig | null;
}
-export function ClipboardCommands({ pathExists, overrideConfig }: ClipboardCommandsProps) {
+export function ClipboardCommands({ pathExists }: ClipboardCommandsProps) {
if (pathExists !== true) return null;
- const patchCommand = overrideConfig
- ? `${overrideConfig.envAssignment} ~/fgmod/fgmod %command%`
- : "~/fgmod/fgmod %command%";
-
return (
<>
<SmartClipboardButton
- command={patchCommand}
+ command="~/fgmod/fgmod %command%"
buttonText="Copy Patch Command"
/>
diff --git a/src/components/CustomPathOverride.tsx b/src/components/CustomPathOverride.tsx
index 9915efc..b28a8de 100644
--- a/src/components/CustomPathOverride.tsx
+++ b/src/components/CustomPathOverride.tsx
@@ -1,388 +1,271 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { ButtonItem, Field, PanelSectionRow, ToggleField } from "@decky/ui";
import { FileSelectionType, openFilePicker } from "@decky/api";
-import { getPathDefaults } from "../api";
-import type { CustomOverrideConfig } from "../types/index";
-
-interface CustomPathOverrideProps {
- onOverrideChange: (override: CustomOverrideConfig | null) => void;
-}
-
-const DEFAULT_START_PATH = "/home";
-const DEFAULT_STEAM_LIBRARY_PATH = "/home/deck/.local/share/Steam/steamapps/common";
+import { getPathDefaults, runManualPatch, runManualUnpatch } from "../api";
+import type { ApiResponse } from "../types/index";
interface PathDefaults {
home: string;
steamCommon: string;
}
-const INITIAL_PATH_DEFAULTS: PathDefaults = {
- home: DEFAULT_START_PATH,
- steamCommon: DEFAULT_STEAM_LIBRARY_PATH,
+const DEFAULT_HOME = "/home";
+const DEFAULT_STEAM_COMMON = "/home/deck/.local/share/Steam/steamapps/common";
+
+const INITIAL_DEFAULTS: PathDefaults = {
+ home: DEFAULT_HOME,
+ steamCommon: DEFAULT_STEAM_COMMON,
};
-const normalizePath = (path: string) => path.replace(/\\/g, "/");
+const normalizePath = (value: string) => value.replace(/\\/g, "/");
const stripTrailingSlash = (value: string) =>
- value.endsWith("/") ? value.slice(0, -1) : value;
-
-const escapeForDoubleQuotes = (value: string) =>
- value.replace(/[`"\\$]/g, (match) => `\\${match}`);
-
-const escapeForPattern = (value: string) =>
- value
- .replace(/\\/g, "\\\\")
- .replace(/\//g, "\\/")
- .replace(/\[/g, "\\[")
- .replace(/\]/g, "\\]")
- .replace(/\*/g, "\\*")
- .replace(/\?/g, "\\?");
-
-const escapeForReplacement = (value: string) =>
- value
- .replace(/\\/g, "\\\\")
- .replace(/\//g, "\\/")
- .replace(/\$/g, "\\$");
-
-const quoteForShell = (value: string) => `'${value.replace(/'/g, "'\\''")}'`;
-
-const dirname = (path: string) => {
- const normalized = normalizePath(path);
- const parts = normalized.split("/");
- parts.pop();
- const dir = parts.join("/");
- return dir.length > 0 ? dir : "/";
-};
+ value.length > 1 && value.endsWith("/") ? value.slice(0, -1) : value;
-const longestCommonPrefix = (left: string[], right: string[]) => {
- const length = Math.min(left.length, right.length);
- let idx = 0;
- while (idx < length && left[idx] === right[idx]) {
- idx++;
+const ensureDirectory = (value: string) => {
+ const normalized = normalizePath(value);
+ const lastSegment = normalized.substring(normalized.lastIndexOf("/") + 1);
+ if (!lastSegment || !lastSegment.includes(".")) {
+ return stripTrailingSlash(normalized);
}
- return idx;
+ const parent = normalized.slice(0, normalized.lastIndexOf("/"));
+ return parent || "/";
};
-interface ComputedOverride {
- config: CustomOverrideConfig | null;
- error: string | null;
+interface ManualPatchControlsProps {
+ isAvailable: boolean;
}
-const buildOverride = (
- rawDefault: string | null,
- rawOverride: string | null
-): ComputedOverride => {
- if (!rawDefault || !rawOverride) {
- return { config: null, error: null };
- }
-
- const defaultPath = normalizePath(rawDefault.trim());
- const overridePath = normalizePath(rawOverride.trim());
-
- if (defaultPath === overridePath) {
- return {
- config: null,
- error: "Paths are identical. Choose a different target executable.",
- };
- }
-
- const defaultParts = defaultPath.split("/").filter(Boolean);
- const overrideParts = overridePath.split("/").filter(Boolean);
-
- if (!defaultParts.length || !overrideParts.length) {
- return {
- config: null,
- error: "Unable to parse selected paths. Pick them again.",
- };
- }
-
- const prefixLength = longestCommonPrefix(defaultParts, overrideParts);
-
- if (prefixLength < 2) {
- return {
- config: null,
- error: "Selections do not share a common game folder.",
- };
- }
-
- const searchSuffixParts = defaultParts.slice(prefixLength);
- const replaceSuffixParts = overrideParts.slice(prefixLength);
-
- if (!searchSuffixParts.length || !replaceSuffixParts.length) {
- return {
- config: null,
- error: "Could not determine differing portion of the paths.",
- };
- }
+interface PickerState {
+ selectedPath: string | null;
+ lastError: string | null;
+}
- const searchSuffix = searchSuffixParts.join("/");
- const replaceSuffix = replaceSuffixParts.join("/");
- const pattern = defaultParts[prefixLength - 1] ?? defaultParts[defaultParts.length - 1];
+const INITIAL_PICKER_STATE: PickerState = {
+ selectedPath: null,
+ lastError: null,
+};
- if (!pattern) {
- return {
- config: null,
- error: "Unable to infer game identifier from path.",
- };
+const formatResultMessage = (result: ApiResponse | null) => {
+ if (!result) return null;
+ if (result.status === "success") {
+ return result.message || result.output || "Operation completed successfully.";
}
-
- const escapedPattern = escapeForDoubleQuotes(pattern);
- const escapedSearch = escapeForPattern(searchSuffix);
- const escapedReplace = escapeForReplacement(replaceSuffix);
-
- const expression = `[[ "$arg" == *"${escapedPattern}"* ]] && arg=\${arg//${escapedSearch}/${escapedReplace}}`;
- const snippet = `[[ "$arg" == *"${pattern}"* ]] && arg=\${arg//${searchSuffix}/${replaceSuffix}}`;
- const envAssignment = `FGMOD_OVERRIDE_EXPRESSION=${quoteForShell(expression)}`;
-
- const config: CustomOverrideConfig = {
- defaultPath,
- overridePath,
- pattern,
- searchSuffix,
- replaceSuffix,
- expression,
- snippet,
- envAssignment,
- };
-
- return { config, error: null };
+ return result.message || result.output || "Operation failed.";
};
-export const CustomPathOverride = ({ onOverrideChange }: CustomPathOverrideProps) => {
- const [launcherPath, setLauncherPath] = useState<string | null>(null);
- const [overridePath, setOverridePath] = useState<string | null>(null);
+export const ManualPatchControls = ({ isAvailable }: ManualPatchControlsProps) => {
const [isEnabled, setEnabled] = useState(false);
- const [pathDefaults, setPathDefaults] = useState<PathDefaults>(INITIAL_PATH_DEFAULTS);
+ const [defaults, setDefaults] = useState<PathDefaults>(INITIAL_DEFAULTS);
+ const [pickerState, setPickerState] = useState<PickerState>(INITIAL_PICKER_STATE);
+ const [isPatching, setIsPatching] = useState(false);
+ const [isUnpatching, setIsUnpatching] = useState(false);
+ const [operationResult, setOperationResult] = useState<ApiResponse | null>(null);
useEffect(() => {
let cancelled = false;
- const fetchDefaults = async () => {
+ (async () => {
try {
- const result = await getPathDefaults();
- if (!result) {
- return;
- }
+ const response = await getPathDefaults();
+ if (!response || cancelled) return;
- const home = result.home ? normalizePath(result.home) : INITIAL_PATH_DEFAULTS.home;
- const steamCommonSource = result.steam_common
- ? normalizePath(result.steam_common)
+ const home = response.home ? normalizePath(response.home) : DEFAULT_HOME;
+ const steamCommon = response.steam_common
+ ? normalizePath(response.steam_common)
: normalizePath(`${stripTrailingSlash(home)}/.local/share/Steam/steamapps/common`);
- if (!cancelled) {
- setPathDefaults({
- home,
- steamCommon: steamCommonSource || INITIAL_PATH_DEFAULTS.steamCommon,
- });
- }
+ setDefaults({
+ home,
+ steamCommon: steamCommon || DEFAULT_STEAM_COMMON,
+ });
} catch (err) {
- console.error("CustomPathOverride -> getPathDefaults", err);
+ console.error("ManualPatchControls -> getPathDefaults", err);
}
- };
-
- fetchDefaults();
+ })();
return () => {
cancelled = true;
};
}, []);
- const { config, error } = useMemo(
- () => buildOverride(launcherPath, overridePath),
- [launcherPath, overridePath]
- );
-
useEffect(() => {
- if (isEnabled && config) {
- onOverrideChange(config);
- } else {
- onOverrideChange(null);
+ if (!isAvailable) {
+ setEnabled(false);
+ setPickerState(INITIAL_PICKER_STATE);
+ setOperationResult(null);
}
- }, [config, isEnabled, onOverrideChange]);
+ }, [isAvailable]);
- interface PickerArgs {
- existing: string | null;
- setter: (value: string) => void;
- fallbackStart?: string | null;
- }
+ const canInteract = isAvailable && isEnabled;
+ const selectedPath = pickerState.selectedPath;
+ const statusMessage = useMemo(() => formatResultMessage(operationResult), [operationResult]);
+ const wasSuccessful = operationResult?.status === "success";
- const openPicker = useCallback(
- async ({ existing, setter, fallbackStart }: PickerArgs) => {
- const candidates = new Set<string>();
+ const openDirectoryPicker = useCallback(async () => {
+ const candidates = [
+ selectedPath,
+ defaults.steamCommon,
+ defaults.home,
+ ];
- if (existing) {
- candidates.add(normalizePath(existing));
- } else {
- if (fallbackStart) {
- candidates.add(normalizePath(fallbackStart));
- }
- candidates.add(pathDefaults.steamCommon);
- candidates.add(pathDefaults.home);
- }
+ let lastError: string | null = null;
- let lastError: unknown = null;
+ for (const candidate of candidates) {
+ if (!candidate) continue;
- for (const candidate of candidates) {
- if (!candidate) {
- continue;
- }
+ const startPath = ensureDirectory(candidate);
- try {
- const result = await openFilePicker(
- FileSelectionType.FILE,
- candidate,
- true,
- true,
- undefined,
- undefined,
- true
- );
-
- if (result?.path) {
- setter(normalizePath(result.path));
- return;
- }
- } catch (err) {
- lastError = err;
+ try {
+ const result = await openFilePicker(
+ FileSelectionType.FOLDER,
+ startPath,
+ true,
+ true,
+ undefined,
+ undefined,
+ true
+ );
+
+ if (result?.path) {
+ setPickerState({ selectedPath: normalizePath(result.path), lastError: null });
+ setOperationResult(null);
+ return;
}
+ } catch (err) {
+ console.error("ManualPatchControls -> openDirectoryPicker", err);
+ lastError = err instanceof Error ? err.message : String(err);
}
+ }
- if (lastError) {
- console.error("CustomPathOverride -> openPicker", lastError);
+ setPickerState((prev) => ({ ...prev, lastError }));
+ }, [defaults.home, defaults.steamCommon, selectedPath]);
+
+ const runOperation = useCallback(
+ async (action: "patch" | "unpatch") => {
+ if (!selectedPath) return;
+
+ const setBusy = action === "patch" ? setIsPatching : setIsUnpatching;
+ setBusy(true);
+ setOperationResult(null);
+
+ try {
+ const response =
+ action === "patch"
+ ? await runManualPatch(selectedPath)
+ : await runManualUnpatch(selectedPath);
+ setOperationResult(response ?? { status: "error", message: "No response from backend." });
+ } catch (err) {
+ setOperationResult({
+ status: "error",
+ message: err instanceof Error ? err.message : String(err),
+ });
+ } finally {
+ setBusy(false);
}
},
- [pathDefaults]
+ [selectedPath]
);
const handleToggle = (value: boolean) => {
+ if (!isAvailable) {
+ setEnabled(false);
+ return;
+ }
+
setEnabled(value);
if (!value) {
- setLauncherPath(null);
- setOverridePath(null);
- onOverrideChange(null);
- } else if (config) {
- onOverrideChange(config);
+ setPickerState(INITIAL_PICKER_STATE);
+ setOperationResult(null);
}
};
+ const busy = isPatching || isUnpatching;
+
return (
<>
<PanelSectionRow>
<ToggleField
- label="Custom Launcher Override"
- description="Select launcher and target executables from the Deck's file browser."
- checked={isEnabled}
+ label="Manual Patch Controls"
+ description={
+ isAvailable
+ ? "Manually apply OptiScaler to a specific game directory."
+ : "Install OptiScaler first to enable manual patching."
+ }
+ checked={isEnabled && isAvailable}
+ disabled={!isAvailable}
onChange={handleToggle}
/>
</PanelSectionRow>
- {isEnabled && (
+ {canInteract && (
<>
<PanelSectionRow>
<ButtonItem
layout="below"
- onClick={() =>
- openPicker({
- existing: launcherPath,
- setter: setLauncherPath,
- fallbackStart: pathDefaults.steamCommon,
- })
- }
- description={launcherPath || "Pick the EXE Steam currently uses."}
- >
- Select Steam-provided EXE
- </ButtonItem>
- </PanelSectionRow>
-
- <PanelSectionRow>
- <ButtonItem
- layout="below"
- disabled={!launcherPath}
- onClick={() =>
- launcherPath &&
- openPicker({
- existing: overridePath,
- setter: setOverridePath,
- fallbackStart: launcherPath ? dirname(launcherPath) : pathDefaults.steamCommon,
- })
- }
+ onClick={openDirectoryPicker}
description={
- launcherPath
- ? overridePath || "Pick the executable that should run instead."
- : "Select the Steam-provided executable first."
+ selectedPath || "Choose the game's installation directory (where the EXE lives)."
}
>
- Select Override EXE
+ Select directory
</ButtonItem>
</PanelSectionRow>
- {(launcherPath || overridePath) && (
- <PanelSectionRow>
- <ButtonItem
- layout="below"
- onClick={() => {
- setLauncherPath(null);
- setOverridePath(null);
- }}
- >
- Clear selections
- </ButtonItem>
- </PanelSectionRow>
- )}
-
- {error && (
+ {pickerState.lastError && (
<PanelSectionRow>
<Field
- label="Override status"
- description={error}
+ label="Picker error"
+ description={pickerState.lastError}
>
⚠️
</Field>
</PanelSectionRow>
)}
- {!error && config && (
+ {selectedPath && (
<>
<PanelSectionRow>
<Field
- label="Detected game"
- description="Based on the shared folder between both selections."
+ label="Target directory"
+ description="OptiScaler files will be copied here."
>
- {config.pattern}
+ {selectedPath}
</Field>
</PanelSectionRow>
<PanelSectionRow>
- <Field
- label="Code snippet"
- description="This is added to fgmod before resolving the install path."
- bottomSeparator="none"
+ <ButtonItem
+ layout="below"
+ disabled={busy}
+ onClick={() => runOperation("patch")}
>
- <div
- style={{
- backgroundColor: "rgba(255,255,255,0.05)",
- borderRadius: "6px",
- padding: "12px",
- fontFamily: "monospace",
- whiteSpace: "pre-wrap",
- wordBreak: "break-word",
- }}
- >
- {config.snippet}
- </div>
- </Field>
+ {isPatching ? "Patching..." : "Patch directory"}
+ </ButtonItem>
</PanelSectionRow>
<PanelSectionRow>
- <Field
- label="Environment preview"
- description="Automatically appended to the Patch command."
+ <ButtonItem
+ layout="below"
+ disabled={busy}
+ onClick={() => runOperation("unpatch")}
>
- {config.envAssignment}
- </Field>
+ {isUnpatching ? "Reverting..." : "Unpatch directory"}
+ </ButtonItem>
</PanelSectionRow>
</>
)}
+
+ {operationResult && (
+ <PanelSectionRow>
+ <Field
+ label={wasSuccessful ? "Last action succeeded" : "Last action failed"}
+ description={wasSuccessful ? undefined : operationResult.output?.slice(0, 200)}
+ >
+ {statusMessage}
+ </Field>
+ </PanelSectionRow>
+ )}
</>
)}
</>
diff --git a/src/components/OptiScalerControls.tsx b/src/components/OptiScalerControls.tsx
index 5dac6d0..3a288e8 100644
--- a/src/components/OptiScalerControls.tsx
+++ b/src/components/OptiScalerControls.tsx
@@ -10,8 +10,7 @@ import { ClipboardCommands } from "./ClipboardCommands";
import { InstructionCard } from "./InstructionCard";
import { OptiScalerWiki } from "./OptiScalerWiki";
import { UninstallButton } from "./UninstallButton";
-import { CustomPathOverride } from "./CustomPathOverride";
-import type { CustomOverrideConfig } from "../types/index";
+import { ManualPatchControls } from "./CustomPathOverride";
interface OptiScalerControlsProps {
pathExists: boolean | null;
@@ -23,14 +22,6 @@ 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 [overrideConfig, setOverrideConfig] = useState<CustomOverrideConfig | null>(null);
-
- useEffect(() => {
- if (pathExists !== true && overrideConfig) {
- setOverrideConfig(null);
- }
- }, [pathExists, overrideConfig]);
-
useEffect(() => {
if (installResult) {
return createAutoCleanupTimer(() => setInstallResult(null), TIMEOUTS.resultDisplay);
@@ -85,11 +76,9 @@ export function OptiScalerControls({ pathExists, setPathExists }: OptiScalerCont
<OptiScalerHeader pathExists={pathExists} />
- {pathExists === true && (
- <CustomPathOverride onOverrideChange={setOverrideConfig} />
- )}
+ <ManualPatchControls isAvailable={pathExists === true} />
- <ClipboardCommands pathExists={pathExists} overrideConfig={overrideConfig} />
+ <ClipboardCommands pathExists={pathExists} />
<InstructionCard pathExists={pathExists} />
diff --git a/src/components/index.ts b/src/components/index.ts
index 3f67341..cd599ba 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -8,3 +8,4 @@ export { OptiScalerWiki } from './OptiScalerWiki';
export { UninstallButton } from './UninstallButton';
export { SmartClipboardButton } from './SmartClipboardButton';
export { ResultDisplay } from './ResultDisplay';
+export { ManualPatchControls } from './CustomPathOverride';
diff --git a/src/types/index.ts b/src/types/index.ts
index 7b5b890..c810754 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -24,14 +24,3 @@ export interface ModInstallationConfig {
bin: string;
};
}
-
-export interface CustomOverrideConfig {
- defaultPath: string;
- overridePath: string;
- pattern: string;
- searchSuffix: string;
- replaceSuffix: string;
- expression: string;
- snippet: string;
- envAssignment: string;
-}