1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
applyWorkaroundChange,
parseWorkaroundOptions,
readSteamLaunchOptions,
subscribeSteamLaunchOptions,
updateSteamLaunchOptions,
type ParsedWorkaroundOptions,
type SteamLaunchOptionsSnapshot,
type WorkaroundField,
} from "../utils/steamLaunchOptions";
import { showErrorToast } from "../utils/toastUtils";
export type WorkaroundLoadStatus = "loading" | "ready" | "error";
const SLIDER_DEBOUNCE_MS = 250;
interface PendingSliderUpdate {
timer: number;
value: number;
waiters: Array<(success: boolean) => void>;
}
interface WorkaroundSnapshot {
steam: SteamLaunchOptionsSnapshot;
parsed: ParsedWorkaroundOptions;
}
interface PerAppWorkarounds {
status: WorkaroundLoadStatus;
snapshot: WorkaroundSnapshot | null;
refresh: () => Promise<void>;
update: (field: WorkaroundField, value: boolean | number) => Promise<boolean>;
error: string | null;
}
function asError(error: unknown): Error {
return error instanceof Error ? error : new Error(String(error));
}
function makeSnapshot(steam: SteamLaunchOptionsSnapshot): WorkaroundSnapshot {
return { steam, parsed: parseWorkaroundOptions(steam.options) };
}
export function usePerAppWorkarounds(appId: string, nonSteam: boolean): PerAppWorkarounds {
const [status, setStatus] = useState<WorkaroundLoadStatus>("loading");
const [snapshot, setSnapshot] = useState<WorkaroundSnapshot | null>(null);
const [error, setError] = useState<string | null>(null);
const pendingSliderUpdate = useRef<PendingSliderUpdate | null>(null);
const numericAppId = Number(appId);
const applySnapshot = useCallback((steam: SteamLaunchOptionsSnapshot) => {
setSnapshot(makeSnapshot(steam));
setStatus("ready");
setError(null);
}, []);
const refresh = useCallback(async () => {
setStatus("loading");
setError(null);
try {
applySnapshot(await readSteamLaunchOptions(numericAppId, nonSteam));
} catch (refreshError) {
const nextError = asError(refreshError);
setStatus("error");
setError(nextError.message);
}
}, [applySnapshot, nonSteam, numericAppId]);
useEffect(() => {
let active = true;
setStatus("loading");
setSnapshot(null);
setError(null);
const handleSnapshot = (nextSnapshot: SteamLaunchOptionsSnapshot) => {
if (!active) return;
applySnapshot(nextSnapshot);
};
const handleSubscriptionError = (subscriptionError: Error) => {
if (!active) return;
setStatus("error");
setError(subscriptionError.message);
};
let unsubscribe = () => {};
try {
unsubscribe = subscribeSteamLaunchOptions(
numericAppId,
nonSteam,
handleSnapshot,
handleSubscriptionError,
);
} catch (subscriptionError) {
handleSubscriptionError(asError(subscriptionError));
}
void readSteamLaunchOptions(numericAppId, nonSteam)
.then((nextSnapshot) => {
if (active) applySnapshot(nextSnapshot);
})
.catch((readError) => {
if (active) handleSubscriptionError(asError(readError));
});
return () => {
active = false;
unsubscribe();
};
}, [applySnapshot, nonSteam, numericAppId]);
const persistUpdate = useCallback(async (field: WorkaroundField, value: boolean | number): Promise<boolean> => {
setError(null);
try {
const nextSnapshot = await updateSteamLaunchOptions(
numericAppId,
nonSteam,
(options) => applyWorkaroundChange(options, field, value),
);
applySnapshot(nextSnapshot);
return true;
} catch (updateError) {
const nextError = asError(updateError);
setStatus("error");
setError(nextError.message);
showErrorToast("Workaround update failed", nextError.message);
return false;
}
}, [applySnapshot, nonSteam, numericAppId]);
const flushSliderUpdate = useCallback(async (): Promise<boolean> => {
const pending = pendingSliderUpdate.current;
if (!pending) return true;
pendingSliderUpdate.current = null;
window.clearTimeout(pending.timer);
const success = await persistUpdate("dxvkFrameRate", pending.value);
pending.waiters.forEach((resolve) => resolve(success));
return success;
}, [persistUpdate]);
const update = useCallback(async (field: WorkaroundField, value: boolean | number): Promise<boolean> => {
if (field === "dxvkFrameRate") {
setError(null);
return new Promise<boolean>((resolve) => {
const pending = pendingSliderUpdate.current ?? { timer: 0, value: 0, waiters: [] };
window.clearTimeout(pending.timer);
pending.value = Number(value);
pending.waiters.push(resolve);
pending.timer = window.setTimeout(() => {
void flushSliderUpdate();
}, SLIDER_DEBOUNCE_MS);
pendingSliderUpdate.current = pending;
});
}
const sliderSuccess = await flushSliderUpdate();
if (!sliderSuccess) return false;
return persistUpdate(field, value);
}, [flushSliderUpdate, persistUpdate]);
useEffect(() => {
return () => {
const pending = pendingSliderUpdate.current;
if (!pending) return;
window.clearTimeout(pending.timer);
pendingSliderUpdate.current = null;
pending.waiters.forEach((resolve) => resolve(false));
};
}, [numericAppId, nonSteam]);
return useMemo(() => ({ status, snapshot, refresh, update, error }), [error, refresh, snapshot, status, update]);
}
|