#![deny(missing_docs)]
use dircpy::CopyBuilder;
use lazy_static::lazy_static;
use std::{
io::{Read, Seek},
path::{Path, PathBuf},
};
use crate::prelude::*;
pub mod prelude;
mod git;
static LIB_VERSION: &str = env!("CARGO_PKG_VERSION");
static DEFAULT_INI: &str = include_str!("../../reshade.example.ini");
lazy_static! {
static ref SHADER_REPOSITORIES: Vec<Shader> = vec![
Shader::new("SweetFX", "https://github.com/CeeJayDK/SweetFX", true, None),
Shader::new("PD80", "https://github.com/prod80/prod80-ReShade-Repository", false, None),
Shader::new("ReShade","https://github.com/crosire/reshade-shaders", true, Some("master")),
Shader::new("qUINT", "https://github.com/martymcmodding/qUINT", false, None),
Shader::new("AstrayFX", "https://github.com/BlueSkyDefender/AstrayFX", false, None),
];
}
pub struct Shader {
pub name: String,
pub repository: String,
pub branch: Option<String>,
pub essential: bool,
}
impl Shader {
pub fn new(name: &str, repository: &str, essential: bool, branch: Option<&str>) -> Self {
Self {
name: name.to_string(),
repository: repository.to_string(),
branch: branch.map(|b| b.to_string()),
essential,
}
}
pub fn pull(&self, directory: &Path) -> ReShaderResult<()> {
let target_directory = directory.join(&self.name);
git::pull(&target_directory, self.branch.as_deref())?;
Ok(())
}
pub fn clone_repo(&self, target_directory: &Path) -> ReShaderResult<git2::Repository> {
let target_directory = target_directory.join(&self.name);
if target_directory.exists() {
return Ok(git2::Repository::open(&target_directory)?);
}
let fetch_options = git2::FetchOptions::new();
let mut builder = git2::build::RepoBuilder::new();
builder.fetch_options(fetch_options);
if let Some(branch) = &self.branch {
builder.branch(branch);
}
Ok(builder.clone(&self.repository, &target_directory)?)
}
}
pub async fn download_file(client: &reqwest::Client, url: &str, path: &str) -> ReShaderResult<()> {
let resp = client
.get(url)
.header(
reqwest::header::USER_AGENT,
format!("reshader/{LIB_VERSION}"),
)
.send()
.await
.map_err(|e| ReShaderError::Download(url.to_string(), e.to_string()))?
.bytes()
.await
.map_err(|e| ReShaderError::Download(url.to_string(), e.to_string()))?;
let mut out = tokio::fs::File::create(path).await?;
let mut reader = tokio::io::BufReader::new(resp.as_ref());
tokio::io::copy(&mut reader, &mut out).await?;
Ok(())
}
pub fn clone_reshade_shaders(directory: &Path) -> ReShaderResult<()> {
let merge_directory = directory.join("Merged");
if !merge_directory.exists() {
std::fs::create_dir(&merge_directory)?;
}
let shader_directory = merge_directory.join("Shaders");
if !shader_directory.exists() {
std::fs::create_dir(&shader_directory)?;
}
let texture_directory = merge_directory.join("Textures");
if !texture_directory.exists() {
std::fs::create_dir(&texture_directory)?;
}
let intermediate_directory = merge_directory.join("Intermediate");
if !intermediate_directory.exists() {
std::fs::create_dir(&intermediate_directory)?;
}
for shader in SHADER_REPOSITORIES.iter() {
shader.clone_repo(directory)?;
shader.pull(directory)?;
let repo_directory = directory.join(&shader.name);
let repo_shader_directory = repo_directory.join("Shaders");
let repo_texture_directory = repo_directory.join("Textures");
let target_directory = if shader.essential {
shader_directory.clone()
} else {
shader_directory.join(&shader.name)
};
if !target_directory.exists() {
std::fs::create_dir(&target_directory)?;
}
if repo_shader_directory.exists() {
let builder = CopyBuilder::new(&repo_shader_directory, &target_directory);
builder.overwrite(true).run()?;
}
if repo_texture_directory.exists() {
let builder = CopyBuilder::new(&repo_texture_directory, &texture_directory);
builder.overwrite(true).run()?;
}
}
Ok(())
}
pub fn install_reshade_shaders(directory: &Path, game_path: &Path) -> ReShaderResult<()> {
let target_path = game_path.join("reshade-shaders");
if target_path.exists() && std::fs::read_link(&target_path).is_err() {
return Err(ReShaderError::Symlink(
directory.to_str().unwrap().to_string(),
target_path.to_str().unwrap().to_string(),
"Directory already exists".to_string(),
));
} else if target_path.exists() && std::fs::read_link(&target_path).is_ok() {
return Ok(());
}
std::os::unix::fs::symlink(directory, &target_path)?;
Ok(())
}
pub async fn get_latest_reshade_version(
client: &reqwest::Client,
version: Option<String>,
vanilla: bool,
) -> ReShaderResult<String> {
let version = if let Some(version) = version {
version
} else {
let tags = client
.get("https://api.github.com/repos/crosire/reshade/tags")
.header(
reqwest::header::USER_AGENT,
format!("reshader/{LIB_VERSION}"),
)
.send()
.await
.map_err(|_| {
ReShaderError::FetchLatestVersion("error while fetching tags".to_string())
})?
.json::<Vec<serde_json::Value>>()
.await
.map_err(|_| {
ReShaderError::FetchLatestVersion("invalid json returned by github".to_string())
})?;
let mut tags = tags
.iter()
.map(|tag| tag["name"].as_str().unwrap().trim_start_matches('v'))
.collect::<Vec<_>>();
tags.sort_by(|a, b| {
let a = semver::Version::parse(a).unwrap();
let b = semver::Version::parse(b).unwrap();
a.cmp(&b)
});
let latest = tags
.last()
.ok_or(ReShaderError::FetchLatestVersion(
"no tags available".to_string(),
))?
.trim_start_matches('v');
latest.to_string()
};
if vanilla {
Ok(format!(
"https://reshade.me/downloads/ReShade_Setup_{version}.exe"
))
} else {
Ok(format!(
"https://reshade.me/downloads/ReShade_Setup_{version}_Addon.exe"
))
}
}
pub async fn download_reshade(
client: &reqwest::Client,
target_directory: &Path,
vanilla: bool,
version: Option<String>,
specific_installer: &Option<String>,
) -> ReShaderResult<()> {
let tmp = tempdir::TempDir::new("reshader_downloads")?;
let reshade_path = if let Some(specific_installer) = specific_installer {
PathBuf::from(specific_installer)
} else {
let reshade_url = get_latest_reshade_version(client, version, vanilla)
.await
.expect("Could not get latest ReShade version");
let reshade_path = tmp.path().join("reshade.exe");
download_file(client, &reshade_url, reshade_path.to_str().unwrap()).await?;
reshade_path
};
let d3dcompiler_path = tmp.path().join("d3dcompiler_47.dll");
download_file(
client,
"https://lutris.net/files/tools/dll/d3dcompiler_47.dll",
d3dcompiler_path.to_str().unwrap(),
)
.await?;
let exe = std::fs::File::open(&reshade_path).expect("Could not open ReShade installer");
let mut exe = std::io::BufReader::new(exe);
let mut buf = [0u8; 4];
let mut offset = 0;
loop {
exe.read_exact(&mut buf)?;
if buf == [0x50, 0x4b, 0x03, 0x04] {
break;
}
offset += 1;
exe.seek(std::io::SeekFrom::Start(offset))?;
}
let mut contents = zip::ZipArchive::new(exe).map_err(|_| ReShaderError::NoZipFile)?;
let mut buf = Vec::new();
contents
.by_name("ReShade64.dll")
.map_err(|_| ReShaderError::NoReShade64Dll)?
.read_to_end(&mut buf)?;
let reshade_dll = if vanilla {
target_directory.join("ReShade64.Vanilla.dll")
} else {
target_directory.join("ReShade64.Addon.dll")
};
std::fs::write(reshade_dll, buf)?;
std::fs::copy(
d3dcompiler_path,
target_directory.join("d3dcompiler_47.dll"),
)?;
Ok(())
}
pub async fn install_reshade(
data_dir: &Path,
game_path: &Path,
vanilla: bool,
) -> ReShaderResult<()> {
if game_path.join("dxgi.dll").exists() {
std::fs::remove_file(game_path.join("dxgi.dll"))?;
}
if game_path.join("d3dcompiler_47.dll").exists() {
std::fs::remove_file(game_path.join("d3dcompiler_47.dll"))?;
}
if vanilla {
std::os::unix::fs::symlink(
data_dir.join("ReShade64.Vanilla.dll"),
game_path.join("dxgi.dll"),
)?;
} else {
std::os::unix::fs::symlink(
data_dir.join("ReShade64.Addon.dll"),
game_path.join("dxgi.dll"),
)?;
}
std::os::unix::fs::symlink(
data_dir.join("d3dcompiler_47.dll"),
game_path.join("d3dcompiler_47.dll"),
)?;
let ini_path = game_path.join("ReShade.ini");
if !ini_path.exists() {
std::fs::write(ini_path, DEFAULT_INI)?;
}
Ok(())
}
pub async fn install_presets(
directory: &PathBuf,
presets_path: &PathBuf,
shaders_path: &PathBuf,
) -> ReShaderResult<()> {
let file = std::fs::File::open(presets_path)?;
let mut presets_zip =
zip::read::ZipArchive::new(file).map_err(|_| ReShaderError::ReadZipFile)?;
presets_zip
.extract(directory)
.map_err(|_| ReShaderError::ExtractZipFile)?;
CopyBuilder::new(
directory.join("GShade-Presets-master"),
directory.join("reshade-presets"),
)
.overwrite(true)
.run()?;
std::fs::remove_dir_all(directory.join("GShade-Presets-master"))?;
let file = std::fs::File::open(shaders_path).expect("unable to open shaders file");
let mut shaders_zip =
zip::read::ZipArchive::new(file).map_err(|_| ReShaderError::ReadZipFile)?;
shaders_zip
.extract(directory)
.map_err(|_| ReShaderError::ExtractZipFile)?;
CopyBuilder::new(
directory.join("gshade-shaders"),
directory.join("reshade-shaders"),
)
.overwrite(true)
.run()?;
std::fs::remove_dir_all(directory.join("gshade-shaders"))?;
let intermediate_path = directory.join("reshade-shaders").join("Intermediate");
if !intermediate_path.exists() {
std::fs::create_dir(intermediate_path)?;
}
Ok(())
}
pub fn uninstall(game_path: &Path) -> ReShaderResult<()> {
let dxgi_path = PathBuf::from(&game_path).join("dxgi.dll");
let d3dcompiler_path = PathBuf::from(&game_path).join("d3dcompiler_47.dll");
let presets_path = PathBuf::from(&game_path).join("reshade-presets");
let shaders_path = PathBuf::from(&game_path).join("reshade-shaders");
if dxgi_path.exists() {
std::fs::remove_file(dxgi_path)?;
}
if d3dcompiler_path.exists() {
std::fs::remove_file(d3dcompiler_path)?;
}
if presets_path.exists() {
std::fs::remove_dir_all(presets_path)?;
}
if shaders_path.exists() {
std::fs::remove_dir_all(shaders_path)?;
}
Ok(())
}
pub fn install_preset_for_game(data_dir: &Path, game_path: &Path) -> ReShaderResult<()> {
let target_preset_path = PathBuf::from(game_path).join("gshade-presets");
let target_shaders_path = PathBuf::from(game_path).join("gshade-shaders");
if std::fs::read_link(&target_preset_path).is_ok()
|| std::fs::read_link(&target_shaders_path).is_ok()
{
return Ok(());
}
std::os::unix::fs::symlink(data_dir.join("reshade-presets"), target_preset_path)?;
std::os::unix::fs::symlink(data_dir.join("reshade-shaders"), target_shaders_path)?;
Ok(())
}