reshaderlib/
lib.rs

1#![deny(missing_docs)]
2
3//! # reshaderlib
4//!
5//! This library contains the common code for the ReShader installer.
6//!
7//! You can use this crate as a base to create your own ReShade installer.
8//!
9//! ## Examples
10//!
11//! For examples, please look at the [ReShader installer](https://github.com/cozyGalvinism/reshader).
12
13use dircpy::CopyBuilder;
14use lazy_static::lazy_static;
15use std::{
16    io::{Read, Seek},
17    path::{Path, PathBuf},
18};
19
20use crate::prelude::*;
21
22/// Common ReShader types and functions
23pub 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        // default branch is slim, which doesn't include all shaders
35        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
41/// A shader repository
42pub struct Shader {
43    /// The name of the shader
44    pub name: String,
45    /// The URL to the shader repository
46    pub repository: String,
47    /// The branch to use
48    pub branch: Option<String>,
49    /// Is this shader an essential shader?
50    pub essential: bool,
51}
52
53impl Shader {
54    /// Creates a new shader repository
55    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    /// Pulls the latest changes from the shader repository
65    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    /// Clones the shader repository
73    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
90/// Downloads a file from the given URL to the given path
91pub 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
110/// Clones ReShade shaders from their repositories
111pub 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
159/// Installs ReShade shaders and textures to a game directory by symlinking them
160///
161/// This function will create a symlink called `reshade-shaders` in the game directory
162pub fn install_reshade_shaders(directory: &Path, game_path: &Path) -> ReShaderResult<()> {
163    let target_path = game_path.join("reshade-shaders");
164    // if target_path exists and is not a symlink, return an error
165    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
180/// Fetches the latest ReShade version from GitHub.
181///
182/// Alternatively, if `version` is provided, it will return that version.
183/// Please note that there is no check to see if the version is valid or not.
184pub 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    // we're going to ignore that serving content over http in 2023 is terrible
228    // just get a letsencrypt cert already
229    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
240/// Downloads ReShade and d3dcopmiler_47.dll to the given directory.
241///
242/// If `specific_installer` is provided, it will use that installer instead of downloading the latest version.
243///
244/// If `version` is provided, it will use that version instead of the latest version.
245///
246/// If `vanilla` is true, it will download the vanilla version of ReShade instead of the addon version.
247pub 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    // after 0x50, 0x4b, 0x03, 0x04, the zip archive starts
281    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
311/// Installs ReShade to the given game directory by symlinking the ReShade dll
312/// and d3dcompiler_47.dll to the game directory.
313///
314/// Depending on the `vanilla` parameter, it will symlink the vanilla or addon version of ReShade.
315pub 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
352/// Installs GShade presets and shaders to the given directory.
353///
354/// This does **not** download the presets and shaders, it just extracts them
355/// from the given zip files.
356pub 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
399/// Uninstalls ReShade from the given game directory by removing the ReShade dll
400/// (dxgi.dll) and d3dcompiler_47.dll.
401///
402/// INI files are not removed.
403pub 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
425/// Installs the GShade presets and shaders to the given game directory by symlinking
426pub 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}