1use std::path::{Path, PathBuf};
8
9use anyhow::{Context, Result};
10use smallvec::{SmallVec, smallvec};
11use tracing::info;
12
13use super::{
14 AppliedFiles, GameTool, ToolApplyPreview, ToolAvailability, ToolCategory, ToolConfig,
15 ToolGameContext,
16};
17
18pub static RESHADE: ReShade = ReShade;
19
20pub struct ReShade;
21
22impl GameTool for ReShade {
23 fn tool_id(&self) -> &'static str {
24 "reshade"
25 }
26
27 fn display_name(&self) -> &'static str {
28 "ReShade"
29 }
30
31 fn category(&self) -> ToolCategory {
32 ToolCategory::PostProcess
33 }
34
35 fn description(&self) -> &'static str {
36 "Wine/Proton ReShade deployment through proxy DLLs and shader directories."
37 }
38
39 fn settings_schema(&self) -> Vec<super::ToolSettingSpec> {
40 vec![
41 super::ToolSettingSpec::path(
42 "source_dir",
43 "Source directory",
44 "Directory containing ReShade DLLs, ReShade.ini, and shader folders.",
45 )
46 .section("Source"),
47 super::ToolSettingSpec::select(
48 "dll_name",
49 "Proxy DLL",
50 "DLL name copied into the executable directory.",
51 &["dxgi.dll", "d3d11.dll", "dinput8.dll"],
52 )
53 .section("Deployment"),
54 super::ToolSettingSpec::read_only(
55 "derived_executable_dir",
56 "Executable directory",
57 "Derived from the selected game's metadata.",
58 )
59 .section("Detected Game"),
60 ]
61 }
62
63 fn detect_available(&self) -> ToolAvailability {
64 ToolAvailability::Available {
67 version: Some("user-provided".into()),
68 }
69 }
70
71 fn env_vars(&self, _config: &ToolConfig) -> SmallVec<[(String, String); 4]> {
72 SmallVec::new()
73 }
74
75 fn wine_dll_overrides(&self, config: &ToolConfig) -> SmallVec<[String; 4]> {
76 let dll_name = config.get_str("dll_name").unwrap_or("dxgi");
77 smallvec![dll_name.to_string()]
78 }
79
80 fn apply(&self, game_dir: &Path, config: &ToolConfig) -> Result<AppliedFiles> {
81 self.apply_for(game_dir, None, config)
82 }
83
84 fn apply_for(
85 &self,
86 game_dir: &Path,
87 context: Option<&ToolGameContext>,
88 config: &ToolConfig,
89 ) -> Result<AppliedFiles> {
90 let source_dir = config
91 .get_str("source_dir")
92 .map(PathBuf::from)
93 .context("reshade: 'source_dir' setting is required (path to ReShade DLLs)")?;
94
95 let dll_name = config.get_str("dll_name").unwrap_or("dxgi.dll");
96 let target_dir = context
97 .and_then(|context| context.executable_dir.clone())
98 .unwrap_or_else(|| {
99 let exe_subdir = config.get_str("exe_subdir").unwrap_or("");
100 if exe_subdir.is_empty() {
101 game_dir.to_path_buf()
102 } else {
103 game_dir.join(exe_subdir)
104 }
105 });
106
107 std::fs::create_dir_all(&target_dir)
108 .with_context(|| format!("failed to create {}", target_dir.display()))?;
109
110 let mut applied = AppliedFiles::default();
111
112 let src_dll = source_dir.join(dll_name);
114 if src_dll.exists() {
115 let dest = target_dir.join(dll_name);
116 std::fs::copy(&src_dll, &dest)
117 .with_context(|| format!("failed to copy ReShade DLL to {}", dest.display()))?;
118 let rel = dest.strip_prefix(game_dir).unwrap_or(&dest).to_path_buf();
119 applied.files.push(rel);
120 info!(dll = %dll_name, "applied ReShade DLL");
121 }
122
123 let ini = source_dir.join("ReShade.ini");
125 if ini.exists() {
126 let dest = target_dir.join("ReShade.ini");
127 std::fs::copy(&ini, &dest)
128 .with_context(|| format!("failed to copy ReShade.ini to {}", dest.display()))?;
129 let rel = dest.strip_prefix(game_dir).unwrap_or(&dest).to_path_buf();
130 applied.files.push(rel);
131 }
132
133 for subdir in &["reshade-shaders", "reshade-presets"] {
135 let src = source_dir.join(subdir);
136 if src.is_dir() {
137 let dest = target_dir.join(subdir);
138 copy_dir_recursive(&src, &dest, game_dir, &mut applied)?;
139 }
140 }
141
142 Ok(applied)
143 }
144
145 fn preview_apply_for(
146 &self,
147 game_dir: &Path,
148 context: Option<&ToolGameContext>,
149 config: &ToolConfig,
150 ) -> Result<ToolApplyPreview> {
151 let Some(source_dir) = config
152 .get_str("source_dir")
153 .filter(|value| !value.trim().is_empty())
154 .map(PathBuf::from)
155 else {
156 return Ok(missing_preview(
157 "reshade: source directory is not configured",
158 ));
159 };
160 if !source_dir.is_dir() {
161 return Ok(missing_preview(format!(
162 "reshade: source directory does not exist: {}",
163 source_dir.display()
164 )));
165 }
166
167 let dll_name = config.get_str("dll_name").unwrap_or("dxgi.dll");
168 let target_dir = reshade_target_dir(game_dir, context, config);
169 let mut preview = ToolApplyPreview::default();
170
171 let src_dll = source_dir.join(dll_name);
172 if src_dll.is_file() {
173 preview_source_file(game_dir, &src_dll, &target_dir.join(dll_name), &mut preview)?;
174 } else {
175 preview.missing_inputs.push(format!(
176 "reshade: source DLL not found: {}",
177 src_dll.display()
178 ));
179 }
180
181 let ini = source_dir.join("ReShade.ini");
182 if ini.is_file() {
183 preview_source_file(
184 game_dir,
185 &ini,
186 &target_dir.join("ReShade.ini"),
187 &mut preview,
188 )?;
189 }
190
191 for subdir in &["reshade-shaders", "reshade-presets"] {
192 let src = source_dir.join(subdir);
193 if src.is_dir() {
194 preview_dir_recursive(game_dir, &src, &target_dir.join(subdir), &mut preview)?;
195 }
196 }
197
198 Ok(preview)
199 }
200
201 fn default_config(&self) -> ToolConfig {
202 let mut config = ToolConfig::new("reshade");
203 config.set("dll_name", serde_json::json!("dxgi.dll"));
204 config
205 }
206}
207
208fn reshade_target_dir(
209 game_dir: &Path,
210 context: Option<&ToolGameContext>,
211 config: &ToolConfig,
212) -> PathBuf {
213 context
214 .and_then(|context| context.executable_dir.clone())
215 .unwrap_or_else(|| {
216 let exe_subdir = config.get_str("exe_subdir").unwrap_or("");
217 if exe_subdir.is_empty() {
218 game_dir.to_path_buf()
219 } else {
220 game_dir.join(exe_subdir)
221 }
222 })
223}
224
225fn missing_preview(message: impl Into<String>) -> ToolApplyPreview {
226 ToolApplyPreview {
227 missing_inputs: vec![message.into()],
228 ..ToolApplyPreview::default()
229 }
230}
231
232fn preview_source_file(
233 game_dir: &Path,
234 src: &Path,
235 dest: &Path,
236 preview: &mut ToolApplyPreview,
237) -> Result<()> {
238 let expected =
239 std::fs::read(src).with_context(|| format!("failed to read {}", src.display()))?;
240 preview_bytes(game_dir, dest, &expected, preview);
241 Ok(())
242}
243
244fn preview_bytes(game_dir: &Path, dest: &Path, expected: &[u8], preview: &mut ToolApplyPreview) {
245 let changed = std::fs::read(dest).map_or(true, |current| current != expected);
246 let rel = dest.strip_prefix(game_dir).unwrap_or(dest).to_path_buf();
247 preview.record_file(rel, changed);
248}
249
250fn preview_dir_recursive(
251 game_dir: &Path,
252 src: &Path,
253 dest: &Path,
254 preview: &mut ToolApplyPreview,
255) -> Result<()> {
256 for entry in std::fs::read_dir(src)
257 .with_context(|| format!("failed to read directory: {}", src.display()))?
258 .flatten()
259 {
260 let ty = entry.file_type()?;
261 let src_path = entry.path();
262 let dest_path = dest.join(entry.file_name());
263
264 if ty.is_dir() {
265 preview_dir_recursive(game_dir, &src_path, &dest_path, preview)?;
266 } else {
267 preview_source_file(game_dir, &src_path, &dest_path, preview)?;
268 }
269 }
270 Ok(())
271}
272
273fn copy_dir_recursive(
275 src: &Path,
276 dest: &Path,
277 game_dir: &Path,
278 applied: &mut AppliedFiles,
279) -> Result<()> {
280 std::fs::create_dir_all(dest)
281 .with_context(|| format!("failed to create {}", dest.display()))?;
282
283 for entry in std::fs::read_dir(src)
284 .with_context(|| format!("failed to read directory: {}", src.display()))?
285 .flatten()
286 {
287 let ty = entry.file_type()?;
288 let src_path = entry.path();
289 let dest_path = dest.join(entry.file_name());
290
291 if ty.is_dir() {
292 copy_dir_recursive(&src_path, &dest_path, game_dir, applied)?;
293 } else {
294 std::fs::copy(&src_path, &dest_path)
295 .with_context(|| format!("failed to copy {}", dest_path.display()))?;
296 let rel = dest_path
297 .strip_prefix(game_dir)
298 .unwrap_or(&dest_path)
299 .to_path_buf();
300 applied.files.push(rel);
301 }
302 }
303
304 Ok(())
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 #[test]
312 fn preview_reports_changed_when_destination_is_missing() {
313 let source = tempfile::tempdir().expect("source");
314 let game = tempfile::tempdir().expect("game");
315 std::fs::write(source.path().join("dxgi.dll"), b"reshade").expect("source dll");
316 let mut config = ReShade.default_config();
317 config.set(
318 "source_dir",
319 serde_json::json!(source.path().display().to_string()),
320 );
321
322 let preview = ReShade
323 .preview_apply_for(game.path(), None, &config)
324 .expect("preview");
325
326 assert_eq!(preview.changed_files, vec![PathBuf::from("dxgi.dll")]);
327 assert!(preview.unchanged_files.is_empty());
328 assert!(preview.missing_inputs.is_empty());
329 }
330
331 #[test]
332 fn preview_reports_unchanged_when_destination_matches() {
333 let source = tempfile::tempdir().expect("source");
334 let game = tempfile::tempdir().expect("game");
335 std::fs::write(source.path().join("dxgi.dll"), b"reshade").expect("source dll");
336 std::fs::write(game.path().join("dxgi.dll"), b"reshade").expect("dest dll");
337 let mut config = ReShade.default_config();
338 config.set(
339 "source_dir",
340 serde_json::json!(source.path().display().to_string()),
341 );
342
343 let preview = ReShade
344 .preview_apply_for(game.path(), None, &config)
345 .expect("preview");
346
347 assert!(preview.changed_files.is_empty());
348 assert_eq!(preview.unchanged_files, vec![PathBuf::from("dxgi.dll")]);
349 assert!(preview.missing_inputs.is_empty());
350 }
351}