1#![deny(missing_docs)]
2
3use dircpy::CopyBuilder;
14use lazy_static::lazy_static;
15use std::{
16 io::{Read, Seek},
17 path::{Path, PathBuf},
18};
19
20use crate::prelude::*;
21
22pub mod prelude;
24
25mod git;
26
27static LIB_VERSION: &str = env!("CARGO_PKG_VERSION");
28static DEFAULT_INI: &str = include_str!("../../reshade.example.ini");
29
30lazy_static! {
31 static ref SHADER_REPOSITORIES: Vec<Shader> = vec![
32 Shader::new("SweetFX", "https://github.com/CeeJayDK/SweetFX", true, None),
33 Shader::new("PD80", "https://github.com/prod80/prod80-ReShade-Repository", false, None),
34 Shader::new("ReShade","https://github.com/crosire/reshade-shaders", true, Some("master")),
36 Shader::new("qUINT", "https://github.com/martymcmodding/qUINT", false, None),
37 Shader::new("AstrayFX", "https://github.com/BlueSkyDefender/AstrayFX", false, None),
38 ];
39}
40
41pub struct Shader {
43 pub name: String,
45 pub repository: String,
47 pub branch: Option<String>,
49 pub essential: bool,
51}
52
53impl Shader {
54 pub fn new(name: &str, repository: &str, essential: bool, branch: Option<&str>) -> Self {
56 Self {
57 name: name.to_string(),
58 repository: repository.to_string(),
59 branch: branch.map(|b| b.to_string()),
60 essential,
61 }
62 }
63
64 pub fn pull(&self, directory: &Path) -> ReShaderResult<()> {
66 let target_directory = directory.join(&self.name);
67 git::pull(&target_directory, self.branch.as_deref())?;
68
69 Ok(())
70 }
71
72 pub fn clone_repo(&self, target_directory: &Path) -> ReShaderResult<git2::Repository> {
74 let target_directory = target_directory.join(&self.name);
75 if target_directory.exists() {
76 return Ok(git2::Repository::open(&target_directory)?);
77 }
78
79 let fetch_options = git2::FetchOptions::new();
80 let mut builder = git2::build::RepoBuilder::new();
81 builder.fetch_options(fetch_options);
82 if let Some(branch) = &self.branch {
83 builder.branch(branch);
84 }
85
86 Ok(builder.clone(&self.repository, &target_directory)?)
87 }
88}
89
90pub async fn download_file(client: &reqwest::Client, url: &str, path: &str) -> ReShaderResult<()> {
92 let resp = client
93 .get(url)
94 .header(
95 reqwest::header::USER_AGENT,
96 format!("reshader/{LIB_VERSION}"),
97 )
98 .send()
99 .await
100 .map_err(|e| ReShaderError::Download(url.to_string(), e.to_string()))?
101 .bytes()
102 .await
103 .map_err(|e| ReShaderError::Download(url.to_string(), e.to_string()))?;
104 let mut out = tokio::fs::File::create(path).await?;
105 let mut reader = tokio::io::BufReader::new(resp.as_ref());
106 tokio::io::copy(&mut reader, &mut out).await?;
107 Ok(())
108}
109
110pub fn clone_reshade_shaders(directory: &Path) -> ReShaderResult<()> {
112 let merge_directory = directory.join("Merged");
113 if !merge_directory.exists() {
114 std::fs::create_dir(&merge_directory)?;
115 }
116 let shader_directory = merge_directory.join("Shaders");
117 if !shader_directory.exists() {
118 std::fs::create_dir(&shader_directory)?;
119 }
120 let texture_directory = merge_directory.join("Textures");
121 if !texture_directory.exists() {
122 std::fs::create_dir(&texture_directory)?;
123 }
124 let intermediate_directory = merge_directory.join("Intermediate");
125 if !intermediate_directory.exists() {
126 std::fs::create_dir(&intermediate_directory)?;
127 }
128
129 for shader in SHADER_REPOSITORIES.iter() {
130 shader.clone_repo(directory)?;
131 shader.pull(directory)?;
132
133 let repo_directory = directory.join(&shader.name);
134 let repo_shader_directory = repo_directory.join("Shaders");
135 let repo_texture_directory = repo_directory.join("Textures");
136 let target_directory = if shader.essential {
137 shader_directory.clone()
138 } else {
139 shader_directory.join(&shader.name)
140 };
141 if !target_directory.exists() {
142 std::fs::create_dir(&target_directory)?;
143 }
144
145 if repo_shader_directory.exists() {
146 let builder = CopyBuilder::new(&repo_shader_directory, &target_directory);
147 builder.overwrite(true).run()?;
148 }
149
150 if repo_texture_directory.exists() {
151 let builder = CopyBuilder::new(&repo_texture_directory, &texture_directory);
152 builder.overwrite(true).run()?;
153 }
154 }
155
156 Ok(())
157}
158
159pub fn install_reshade_shaders(directory: &Path, game_path: &Path) -> ReShaderResult<()> {
163 let target_path = game_path.join("reshade-shaders");
164 if target_path.exists() && std::fs::read_link(&target_path).is_err() {
166 return Err(ReShaderError::Symlink(
167 directory.to_str().unwrap().to_string(),
168 target_path.to_str().unwrap().to_string(),
169 "Directory already exists".to_string(),
170 ));
171 } else if target_path.exists() && std::fs::read_link(&target_path).is_ok() {
172 return Ok(());
173 }
174
175 std::os::unix::fs::symlink(directory, &target_path)?;
176
177 Ok(())
178}
179
180pub async fn get_latest_reshade_version(
185 client: &reqwest::Client,
186 version: Option<String>,
187 vanilla: bool,
188) -> ReShaderResult<String> {
189 let version = if let Some(version) = version {
190 version
191 } else {
192 let tags = client
193 .get("https://api.github.com/repos/crosire/reshade/tags")
194 .header(
195 reqwest::header::USER_AGENT,
196 format!("reshader/{LIB_VERSION}"),
197 )
198 .send()
199 .await
200 .map_err(|_| {
201 ReShaderError::FetchLatestVersion("error while fetching tags".to_string())
202 })?
203 .json::<Vec<serde_json::Value>>()
204 .await
205 .map_err(|_| {
206 ReShaderError::FetchLatestVersion("invalid json returned by github".to_string())
207 })?;
208 let mut tags = tags
209 .iter()
210 .map(|tag| tag["name"].as_str().unwrap().trim_start_matches('v'))
211 .collect::<Vec<_>>();
212 tags.sort_by(|a, b| {
213 let a = semver::Version::parse(a).unwrap();
214 let b = semver::Version::parse(b).unwrap();
215 a.cmp(&b)
216 });
217 let latest = tags
218 .last()
219 .ok_or(ReShaderError::FetchLatestVersion(
220 "no tags available".to_string(),
221 ))?
222 .trim_start_matches('v');
223
224 latest.to_string()
225 };
226
227 if vanilla {
230 Ok(format!(
231 "https://reshade.me/downloads/ReShade_Setup_{version}.exe"
232 ))
233 } else {
234 Ok(format!(
235 "https://reshade.me/downloads/ReShade_Setup_{version}_Addon.exe"
236 ))
237 }
238}
239
240pub async fn download_reshade(
248 client: &reqwest::Client,
249 target_directory: &Path,
250 vanilla: bool,
251 version: Option<String>,
252 specific_installer: &Option<String>,
253) -> ReShaderResult<()> {
254 let tmp = tempdir::TempDir::new("reshader_downloads")?;
255
256 let reshade_path = if let Some(specific_installer) = specific_installer {
257 PathBuf::from(specific_installer)
258 } else {
259 let reshade_url = get_latest_reshade_version(client, version, vanilla)
260 .await
261 .expect("Could not get latest ReShade version");
262 let reshade_path = tmp.path().join("reshade.exe");
263
264 download_file(client, &reshade_url, reshade_path.to_str().unwrap()).await?;
265 reshade_path
266 };
267
268 let d3dcompiler_path = tmp.path().join("d3dcompiler_47.dll");
269 download_file(
270 client,
271 "https://lutris.net/files/tools/dll/d3dcompiler_47.dll",
272 d3dcompiler_path.to_str().unwrap(),
273 )
274 .await?;
275
276 let exe = std::fs::File::open(&reshade_path).expect("Could not open ReShade installer");
277 let mut exe = std::io::BufReader::new(exe);
278 let mut buf = [0u8; 4];
279 let mut offset = 0;
280 loop {
282 exe.read_exact(&mut buf)?;
283 if buf == [0x50, 0x4b, 0x03, 0x04] {
284 break;
285 }
286 offset += 1;
287 exe.seek(std::io::SeekFrom::Start(offset))?;
288 }
289 let mut contents = zip::ZipArchive::new(exe).map_err(|_| ReShaderError::NoZipFile)?;
290
291 let mut buf = Vec::new();
292 contents
293 .by_name("ReShade64.dll")
294 .map_err(|_| ReShaderError::NoReShade64Dll)?
295 .read_to_end(&mut buf)?;
296 let reshade_dll = if vanilla {
297 target_directory.join("ReShade64.Vanilla.dll")
298 } else {
299 target_directory.join("ReShade64.Addon.dll")
300 };
301 std::fs::write(reshade_dll, buf)?;
302
303 std::fs::copy(
304 d3dcompiler_path,
305 target_directory.join("d3dcompiler_47.dll"),
306 )?;
307
308 Ok(())
309}
310
311pub async fn install_reshade(
316 data_dir: &Path,
317 game_path: &Path,
318 vanilla: bool,
319) -> ReShaderResult<()> {
320 if game_path.join("dxgi.dll").exists() {
321 std::fs::remove_file(game_path.join("dxgi.dll"))?;
322 }
323
324 if game_path.join("d3dcompiler_47.dll").exists() {
325 std::fs::remove_file(game_path.join("d3dcompiler_47.dll"))?;
326 }
327
328 if vanilla {
329 std::os::unix::fs::symlink(
330 data_dir.join("ReShade64.Vanilla.dll"),
331 game_path.join("dxgi.dll"),
332 )?;
333 } else {
334 std::os::unix::fs::symlink(
335 data_dir.join("ReShade64.Addon.dll"),
336 game_path.join("dxgi.dll"),
337 )?;
338 }
339 std::os::unix::fs::symlink(
340 data_dir.join("d3dcompiler_47.dll"),
341 game_path.join("d3dcompiler_47.dll"),
342 )?;
343
344 let ini_path = game_path.join("ReShade.ini");
345 if !ini_path.exists() {
346 std::fs::write(ini_path, DEFAULT_INI)?;
347 }
348
349 Ok(())
350}
351
352pub async fn install_presets(
357 directory: &PathBuf,
358 presets_path: &PathBuf,
359 shaders_path: &PathBuf,
360) -> ReShaderResult<()> {
361 let file = std::fs::File::open(presets_path)?;
362 let mut presets_zip =
363 zip::read::ZipArchive::new(file).map_err(|_| ReShaderError::ReadZipFile)?;
364 presets_zip
365 .extract(directory)
366 .map_err(|_| ReShaderError::ExtractZipFile)?;
367
368 CopyBuilder::new(
369 directory.join("GShade-Presets-master"),
370 directory.join("reshade-presets"),
371 )
372 .overwrite(true)
373 .run()?;
374 std::fs::remove_dir_all(directory.join("GShade-Presets-master"))?;
375
376 let file = std::fs::File::open(shaders_path).expect("unable to open shaders file");
377 let mut shaders_zip =
378 zip::read::ZipArchive::new(file).map_err(|_| ReShaderError::ReadZipFile)?;
379 shaders_zip
380 .extract(directory)
381 .map_err(|_| ReShaderError::ExtractZipFile)?;
382
383 CopyBuilder::new(
384 directory.join("gshade-shaders"),
385 directory.join("reshade-shaders"),
386 )
387 .overwrite(true)
388 .run()?;
389 std::fs::remove_dir_all(directory.join("gshade-shaders"))?;
390
391 let intermediate_path = directory.join("reshade-shaders").join("Intermediate");
392 if !intermediate_path.exists() {
393 std::fs::create_dir(intermediate_path)?;
394 }
395
396 Ok(())
397}
398
399pub fn uninstall(game_path: &Path) -> ReShaderResult<()> {
404 let dxgi_path = PathBuf::from(&game_path).join("dxgi.dll");
405 let d3dcompiler_path = PathBuf::from(&game_path).join("d3dcompiler_47.dll");
406 let presets_path = PathBuf::from(&game_path).join("reshade-presets");
407 let shaders_path = PathBuf::from(&game_path).join("reshade-shaders");
408
409 if dxgi_path.exists() {
410 std::fs::remove_file(dxgi_path)?;
411 }
412 if d3dcompiler_path.exists() {
413 std::fs::remove_file(d3dcompiler_path)?;
414 }
415 if presets_path.exists() {
416 std::fs::remove_dir_all(presets_path)?;
417 }
418 if shaders_path.exists() {
419 std::fs::remove_dir_all(shaders_path)?;
420 }
421
422 Ok(())
423}
424
425pub fn install_preset_for_game(data_dir: &Path, game_path: &Path) -> ReShaderResult<()> {
427 let target_preset_path = PathBuf::from(game_path).join("gshade-presets");
428 let target_shaders_path = PathBuf::from(game_path).join("gshade-shaders");
429
430 if std::fs::read_link(&target_preset_path).is_ok()
431 || std::fs::read_link(&target_shaders_path).is_ok()
432 {
433 return Ok(());
434 }
435
436 std::os::unix::fs::symlink(data_dir.join("reshade-presets"), target_preset_path)?;
437 std::os::unix::fs::symlink(data_dir.join("reshade-shaders"), target_shaders_path)?;
438 Ok(())
439}