Add files via upload

This commit is contained in:
Nighthawk
2025-02-18 22:03:59 -05:00
committed by GitHub
parent 280207a718
commit c564dd49a4
3 changed files with 2718 additions and 0 deletions
Generated
+2062
View File
File diff suppressed because it is too large Load Diff
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "desktopmate_installer"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.11", features = ["json", "gzip"] }
colored = "2"
chrono = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
zip = "0.6"
crossterm = "0.24"
dirs = "4"
uuid = { version = "1", features = ["v4"] }
winapi = { version = "0.3", features = ["wincon"] }
+639
View File
@@ -0,0 +1,639 @@
// main.rs
#![allow(clippy::needless_return)]
#![cfg(target_os = "windows")]
use chrono::Local;
use colored::*;
use crossterm::{
event::{self, Event, KeyCode},
terminal::{disable_raw_mode, enable_raw_mode},
};
use reqwest::Client;
use serde::Deserialize;
use std::env;
use std::error::Error;
use std::ffi::OsStr;
use std::fs::{self, File, OpenOptions};
use std::io::{self, Write};
use std::path::Path;
use std::process::Stdio;
use tokio::io::{AsyncBufReadExt, BufReader as AsyncBufReader};
use tokio::process::Command;
use zip::ZipArchive;
use winapi::um::wincon::SetConsoleTitleW;
use winapi::um::winnt::LPCWSTR;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
// Determine our base directory (where the exe is located)
let exe_path = env::current_exe()?;
let base_dir = exe_path.parent().unwrap_or(Path::new("."));
let log_file = base_dir.join("DesktopMate_Install.log");
// Ensure log file directory exists
if let Some(parent) = log_file.parent() {
fs::create_dir_all(parent)?;
}
write_log(&log_file, "------------------------------------------------------------")?;
write_log(
&log_file,
&format!("{} - Starting DesktopMate Installer", Local::now()),
)?;
// Set console title.
set_console_title("DesktopMate Installer");
// Display symmetrical banner
const BANNER_WIDTH: usize = 45;
let banner_line = "=".repeat(BANNER_WIDTH);
let title = "DesktopMate Installer";
let padding = (BANNER_WIDTH.saturating_sub(title.len())) / 2;
let banner_title = format!("{:padding$}{}{:padding$}", "", title, "", padding = padding);
color_echo(ConsoleColor::Cyan, &banner_line);
color_echo(ConsoleColor::Cyan, &banner_title);
color_echo(ConsoleColor::Cyan, &banner_line);
println!();
// Prompt for installation path (default: C:\Games\DesktopMate)
let default_path = r"C:\Games\DesktopMate";
print!("Enter installation path (default: {}): ", default_path);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim();
let target_path = if input.is_empty() {
default_path.to_owned()
} else {
input.to_owned()
};
color_echo(ConsoleColor::Green, &format!("Installation directory: {}", target_path));
write_log(
&log_file,
&format!("Installation directory set to: {}", target_path),
)?;
// Ensure target directory exists.
fs::create_dir_all(&target_path)?;
// Ensure DepotDownloader.exe is available.
let depot_downloader_dir = base_dir.join("DepotDownloader");
let depot_downloader_exe = depot_downloader_dir.join("DepotDownloader.exe");
let depot_downloader_zip = env::temp_dir().join("DepotDownloader.zip");
let depot_downloader_url = "https://github.com/SteamRE/DepotDownloader/releases/latest/download/DepotDownloader-windows-x64.zip";
if !depot_downloader_exe.exists() {
color_echo(ConsoleColor::Yellow, "DepotDownloader.exe not found! Downloading now...");
write_log(&log_file, "DepotDownloader not found. Initiating download.")?;
if let Err(ex) = download_file(depot_downloader_url, &depot_downloader_zip).await {
color_echo(
ConsoleColor::Red,
&format!("ERROR: Failed to download DepotDownloader! {}", ex),
);
write_log(&log_file, "ERROR: DepotDownloader download failed.")?;
pause_and_exit().await;
return Ok(());
}
color_echo(ConsoleColor::Green, "Extracting DepotDownloader...");
write_log(&log_file, "Extracting DepotDownloader.")?;
if let Err(ex) = extract_zip(&depot_downloader_zip, &depot_downloader_dir) {
color_echo(
ConsoleColor::Red,
&format!("ERROR: Failed to extract DepotDownloader! {}", ex),
);
write_log(&log_file, "ERROR: DepotDownloader extraction failed.")?;
pause_and_exit().await;
return Ok(());
}
fs::remove_file(&depot_downloader_zip)?;
if !depot_downloader_exe.exists() {
color_echo(
ConsoleColor::Red,
"ERROR: DepotDownloader.exe still not found after extraction!",
);
write_log(&log_file, "ERROR: DepotDownloader.exe still missing.")?;
pause_and_exit().await;
return Ok(());
} else {
color_echo(ConsoleColor::Green, "DepotDownloader downloaded and extracted successfully.");
write_log(&log_file, "DepotDownloader ready.")?;
}
}
// STEP 1: Download the DesktopMate depot if needed.
let desktop_mate_data_path = Path::new(&target_path).join("DesktopMate_Data");
if !desktop_mate_data_path.exists() {
// Prompt for Steam credentials.
let steam_user = loop {
print!("Enter your Steam username: ");
io::stdout().flush()?;
let mut user_input = String::new();
io::stdin().read_line(&mut user_input)?;
let trimmed = user_input.trim().to_string();
if !trimmed.is_empty() {
break trimmed;
}
println!("Steam username is required.");
};
let steam_pass = read_password("Enter your Steam password: ")?;
write_log(&log_file, "Steam credentials collected.")?;
// Build DepotDownloader arguments.
let app_id = "3301060";
let depot_id = "3301061";
let manifest_id = "2467897585300615012";
let dd_args = vec![
"-app", app_id,
"-depot", depot_id,
"-manifest", manifest_id,
"-username", &steam_user,
"-password", &steam_pass,
"-dir", &target_path,
];
let dd_arg_string = dd_args.join(" ");
color_echo(ConsoleColor::Blue, "Downloading DesktopMate depot (via DepotDownloader)...");
write_log(&log_file, &format!("Running DepotDownloader with arguments: {}", dd_arg_string))?;
let dd_exit = run_depot_downloader(&depot_downloader_exe, &dd_args).await?;
if dd_exit != 0 {
color_echo(
ConsoleColor::Red,
&format!("ERROR: DepotDownloader encountered an error. Exit code = {}", dd_exit),
);
write_log(&log_file, &format!("ERROR: DepotDownloader failed (exit code {}).", dd_exit))?;
pause_and_exit().await;
return Ok(());
}
color_echo(ConsoleColor::Green, "Depot download complete.");
write_log(&log_file, "Depot download complete.")?;
} else {
color_echo(ConsoleColor::Yellow, "DesktopMate files already exist. Skipping depot download.");
write_log(&log_file, "DesktopMate files already exist; skipping download.")?;
}
// STEP 2: Apply Goldberg Offline Patch.
let goldberg_url = "https://gitlab.com/Mr_Goldberg/goldberg_emulator/-/jobs/4247811310/artifacts/download";
let goldberg_zip = env::temp_dir().join(format!("goldberg_{}.zip", uuid::Uuid::new_v4()));
let extract_path = env::temp_dir().join("goldberg_extracted");
let patch_dll = extract_path.join("experimental").join("steam_api64.dll");
let target_dll = Path::new(&target_path)
.join("DesktopMate_Data")
.join("Plugins")
.join("x86_64")
.join("steam_api64.dll");
color_echo(ConsoleColor::Blue, "Downloading Goldberg patch...");
write_log(&log_file, "Downloading Goldberg emulator patch from GitLab.")?;
download_file(goldberg_url, &goldberg_zip).await?;
if extract_path.exists() {
fs::remove_dir_all(&extract_path)?;
}
fs::create_dir_all(&extract_path)?;
extract_zip(&goldberg_zip, &extract_path)?;
fs::remove_file(&goldberg_zip)?;
if patch_dll.exists() {
if let Some(target_dll_dir) = target_dll.parent() {
fs::create_dir_all(target_dll_dir)?;
fs::copy(&patch_dll, &target_dll)?;
color_echo(ConsoleColor::Green, "Goldberg patch applied successfully.");
write_log(&log_file, "Goldberg patch applied.")?;
} else {
color_echo(
ConsoleColor::Red,
"ERROR: Unable to determine target directory for Goldberg patch DLL.",
);
write_log(&log_file, "ERROR: target directory is null or empty.")?;
pause_and_exit().await;
return Ok(());
}
} else {
color_echo(ConsoleColor::Red, "ERROR: steam_api64.dll not found in the patch archive!");
write_log(&log_file, "ERROR: steam_api64.dll missing in goldberg archive.")?;
pause_and_exit().await;
return Ok(());
}
// STEP 3: Install MelonLoader v0.6.6 by downloading and extracting its ZIP.
update_melonloader_if_needed(&target_path, &log_file).await?;
// STEP 4: Install or update Custom Avatar Loader mod.
install_or_update_custom_avatar_loader(&target_path, &log_file).await?;
// STEP 5: Create Desktop Shortcuts.
color_echo(ConsoleColor::Blue, "Creating desktop shortcuts...");
write_log(&log_file, "Creating desktop shortcuts.")?;
let desktop = match dirs::desktop_dir() {
Some(d) => d,
None => {
color_echo(ConsoleColor::Red, "ERROR: Cannot determine Desktop directory.");
pause_and_exit().await;
return Ok(());
}
};
let exe_path = Path::new(&target_path).join("DesktopMate.exe");
let shortcut_console = desktop.join("DesktopMate_Console.lnk");
let shortcut_no_console = desktop.join("DesktopMate_NoConsole.lnk");
// Use PowerShell to create shortcuts.
create_shortcut(&shortcut_console, &exe_path, &target_path, "")?;
create_shortcut(
&shortcut_no_console,
&exe_path,
&target_path,
"melonloader.hideconsole",
)?;
color_echo(ConsoleColor::Green, "Desktop shortcuts created successfully.");
write_log(&log_file, "Shortcuts created.")?;
println!("Installation complete. Press any key to exit.");
pause_and_exit().await;
Ok(())
}
/// Sets the console title using the Windows API.
fn set_console_title(title: &str) {
use std::os::windows::ffi::OsStrExt;
let wide: Vec<u16> = OsStr::new(title).encode_wide().chain(std::iter::once(0)).collect();
unsafe {
SetConsoleTitleW(wide.as_ptr() as LPCWSTR);
}
}
/// Writes a colored message to the console.
enum ConsoleColor {
Cyan,
Green,
Yellow,
Blue,
Red,
}
fn color_echo(color: ConsoleColor, message: &str) {
match color {
ConsoleColor::Cyan => println!("{}", message.cyan()),
ConsoleColor::Green => println!("{}", message.green()),
ConsoleColor::Yellow => println!("{}", message.yellow()),
ConsoleColor::Blue => println!("{}", message.blue()),
ConsoleColor::Red => println!("{}", message.red()),
}
}
/// Appends a message to the log file.
fn write_log(log_file: &Path, message: &str) -> io::Result<()> {
let mut file = OpenOptions::new().create(true).append(true).open(log_file)?;
writeln!(file, "{} - {}", Local::now(), message)?;
Ok(())
}
/// Downloads a file from the given URL and writes it to the specified path.
async fn download_file(url: &str, output_path: &Path) -> Result<(), Box<dyn Error>> {
let client = Client::builder().user_agent("DesktopMateInstaller").build()?;
let resp = client.get(url).send().await?;
if !resp.status().is_success() {
return Err(format!("HTTP error: {}", resp.status()).into());
}
let bytes = resp.bytes().await?;
fs::write(output_path, &bytes)?;
Ok(())
}
/// Extracts a zip file (at zip_path) to the specified destination directory.
fn extract_zip(zip_path: &Path, destination: &Path) -> Result<(), Box<dyn Error>> {
let file = File::open(zip_path)?;
let mut archive = ZipArchive::new(file)?;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
// Use `mangled_name()` instead of the deprecated `sanitized_name()`
let outpath = destination.join(file.mangled_name());
if file.name().ends_with('/') {
fs::create_dir_all(&outpath)?;
} else {
if let Some(p) = outpath.parent() {
fs::create_dir_all(p)?;
}
let mut outfile = File::create(&outpath)?;
std::io::copy(&mut file, &mut outfile)?;
}
}
Ok(())
}
/// Runs DepotDownloader.exe with the provided arguments and logs output.
async fn run_depot_downloader(exe_path: &Path, args: &[&str]) -> Result<i32, Box<dyn Error>> {
let mut cmd = Command::new(exe_path);
cmd.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = cmd.spawn()?;
let stdout = child.stdout.take().unwrap();
let stderr = child.stderr.take().unwrap();
let mut stdout_reader = AsyncBufReader::new(stdout).lines();
let mut stderr_reader = AsyncBufReader::new(stderr).lines();
let log_file = env::current_exe()?.parent().unwrap().join("DesktopMate_Install.log");
let stdout_log = log_file.clone();
let stdout_handle = tokio::spawn(async move {
while let Ok(Some(line)) = stdout_reader.next_line().await {
println!("{}", line);
let _ = write_log(&stdout_log, &format!("[DD] {}", line));
}
});
let stderr_log = log_file.clone();
let stderr_handle = tokio::spawn(async move {
while let Ok(Some(line)) = stderr_reader.next_line().await {
println!("{}", line.red());
let _ = write_log(&stderr_log, &format!("[DD-ERR] {}", line));
}
});
let status = child.wait().await?;
let _ = stdout_handle.await;
let _ = stderr_handle.await;
Ok(status.code().unwrap_or(-1))
}
/// Uses PowerShell to create a Windows shortcut.
fn create_shortcut(
shortcut_path: &Path,
target_path: &Path,
working_directory: &str,
arguments: &str,
) -> Result<(), Box<dyn Error>> {
// Build a PowerShell command to create the shortcut via WScript.Shell.
let script = format!(
r#"
$WshShell = New-Object -ComObject WScript.Shell;
$Shortcut = $WshShell.CreateShortcut("{0}");
$Shortcut.TargetPath = "{1}";
$Shortcut.WorkingDirectory = "{2}";
{3}
$Shortcut.Save();
"#,
shortcut_path.display(),
target_path.display(),
working_directory,
if arguments.trim().is_empty() {
"".to_string()
} else {
format!(r#"$Shortcut.Arguments = "{}";"#, arguments)
}
);
// Spawn PowerShell to run the script.
let status = std::process::Command::new("powershell")
.args(&["-NoProfile", "-Command", &script])
.status()?;
if !status.success() {
return Err("Failed to create shortcut".into());
}
Ok(())
}
/// Waits for any key press and then exits.
async fn pause_and_exit() {
println!("Press any key to exit...");
enable_raw_mode().unwrap();
loop {
if event::poll(std::time::Duration::from_millis(500)).unwrap() {
if let Event::Key(_) = event::read().unwrap() {
break;
}
}
}
disable_raw_mode().unwrap();
std::process::exit(0);
}
/// Reads a password from the console while masking input with asterisks.
fn read_password(prompt: &str) -> io::Result<String> {
print!("{}", prompt);
io::stdout().flush()?;
let mut password = String::new();
enable_raw_mode()?;
loop {
if let Event::Key(key_event) = event::read()? {
match key_event.code {
KeyCode::Enter => {
println!();
break;
}
KeyCode::Backspace => {
if !password.is_empty() {
password.pop();
print!("\r{} \r", "*".repeat(password.len()));
io::stdout().flush()?;
}
}
KeyCode::Char(c) => {
password.push(c);
print!("*");
io::stdout().flush()?;
}
_ => {}
}
}
}
disable_raw_mode()?;
Ok(password)
}
/// Structure to store GitHub release info.
#[derive(Debug, Deserialize)]
struct GitHubRelease {
tag_name: String,
assets: Vec<GitHubAsset>,
}
#[derive(Debug, Deserialize)]
struct GitHubAsset {
name: String,
browser_download_url: String,
}
/// Helper structure for release info.
struct ReleaseInfo {
tag_name: String,
download_url: String,
}
/// Retrieves the latest release info from GitHub.
async fn get_latest_release(
owner: &str,
repo: &str,
asset_name_filter: Option<&str>,
) -> Option<ReleaseInfo> {
let url = format!("https://api.github.com/repos/{}/{}/releases/latest", owner, repo);
let client = Client::builder().user_agent("DesktopMateInstaller").build().ok()?;
let resp = client.get(&url).send().await.ok()?;
if !resp.status().is_success() {
return None;
}
let release: GitHubRelease = resp.json().await.ok()?;
let mut download_url = String::new();
for asset in release.assets {
if let Some(filter) = asset_name_filter {
if asset.name.eq_ignore_ascii_case(filter) {
download_url = asset.browser_download_url;
break;
}
} else if asset.name.to_lowercase().ends_with(".zip") {
download_url = asset.browser_download_url;
break;
}
}
// Fallback for MelonLoader.
if download_url.is_empty() && repo.eq_ignore_ascii_case("MelonLoader") {
download_url = "https://github.com/LavaGang/MelonLoader/releases/latest/download/MelonLoader.x64.zip".to_owned();
}
Some(ReleaseInfo { tag_name: release.tag_name, download_url })
}
/// Installs MelonLoader version 0.6.6 by downloading and extracting its ZIP into the game directory.
async fn update_melonloader_if_needed(target_path: &str, log_file: &Path) -> Result<(), Box<dyn Error>> {
let version_file = Path::new(target_path).join("MelonLoader.version");
let installed_version = if version_file.exists() {
fs::read_to_string(&version_file)?.trim().to_string()
} else {
String::new()
};
let desired_version = "v0.6.6";
if installed_version == desired_version {
color_echo(ConsoleColor::Green, &format!("MelonLoader is up-to-date (version {}).", installed_version));
write_log(log_file, &format!("MelonLoader up-to-date (version {}).", installed_version))?;
return Ok(());
}
color_echo(ConsoleColor::Yellow, &format!("Installing MelonLoader {}...", desired_version));
write_log(log_file, &format!("Downloading MelonLoader {} zip.", desired_version))?;
let melon_zip_url = "https://github.com/LavaGang/MelonLoader/releases/download/v0.6.6/MelonLoader.x64.zip";
let melon_zip_path = env::temp_dir().join("MelonLoader.x64.zip");
download_file(melon_zip_url, &melon_zip_path).await?;
color_echo(ConsoleColor::Blue, "Extracting MelonLoader contents to game directory...");
write_log(log_file, "Extracting MelonLoader contents to game directory.")?;
extract_zip(&melon_zip_path, Path::new(target_path))?;
fs::remove_file(&melon_zip_path)?;
fs::write(&version_file, desired_version)?;
color_echo(ConsoleColor::Green, "MelonLoader installed successfully.");
write_log(log_file, "MelonLoader installed successfully.")?;
Ok(())
}
/// Installs or updates the Custom Avatar Loader mod.
/// It now checks for both the "Mods" and "UserLibs" folders and copies them into the game directory.
async fn install_or_update_custom_avatar_loader(target_path: &str, log_file: &Path) -> Result<(), Box<dyn Error>> {
let version_file = Path::new(target_path).join("CustomAvatarLoader.version");
let installed_version = if version_file.exists() {
fs::read_to_string(&version_file)?.trim().to_string()
} else {
String::new()
};
color_echo(ConsoleColor::Blue, "Checking for Custom Avatar Loader mod updates...");
write_log(log_file, "Checking for Custom Avatar Loader mod updates.")?;
if let Some(latest_release) = get_latest_release("YusufOzmen01", "desktopmate-custom-avatar-loader", Some("CustomAvatarLoader.zip")).await {
if installed_version == latest_release.tag_name {
color_echo(ConsoleColor::Green, &format!("Custom Avatar Loader mod is up-to-date (version {}).", installed_version));
write_log(log_file, &format!("Custom Avatar Loader mod up-to-date (version {}).", installed_version))?;
} else {
if installed_version.is_empty() {
color_echo(ConsoleColor::Yellow, "Custom Avatar Loader mod not installed. Installing now...");
write_log(log_file, "Custom Avatar Loader mod not installed. Installing.")?;
} else {
color_echo(ConsoleColor::Yellow, &format!(
"Custom Avatar Loader mod update available: Installed version: {}, Latest version: {}",
installed_version, latest_release.tag_name
));
write_log(log_file, &format!(
"Custom Avatar Loader mod update available: Installed version: {}, Latest version: {}",
installed_version, latest_release.tag_name
))?;
print!("Do you want to update Custom Avatar Loader mod? (Y/N): ");
io::stdout().flush()?;
let mut response = String::new();
io::stdin().read_line(&mut response)?;
if response.trim().to_uppercase() != "Y" {
color_echo(ConsoleColor::Yellow, "Skipping Custom Avatar Loader mod update.");
write_log(log_file, "User opted to skip Custom Avatar Loader mod update.")?;
return Ok(());
}
}
let mod_zip = env::temp_dir().join(format!("custom_avatar_{}.zip", uuid::Uuid::new_v4()));
color_echo(ConsoleColor::Blue, "Downloading Custom Avatar Loader mod...");
write_log(log_file, &format!("Downloading Custom Avatar Loader mod from {}", latest_release.download_url))?;
download_file(&latest_release.download_url, &mod_zip).await.map_err(|e| {
color_echo(ConsoleColor::Red, &format!("ERROR: Failed to download Custom Avatar Loader mod: {}", e));
write_log(log_file, "ERROR: Custom Avatar Loader mod download failed.").unwrap();
e
})?;
let extract_path = env::temp_dir().join("custom_avatar_loader_extracted");
if extract_path.exists() {
fs::remove_dir_all(&extract_path)?;
}
fs::create_dir_all(&extract_path)?;
extract_zip(&mod_zip, &extract_path)?;
fs::remove_file(&mod_zip)?;
// If the ZIP contains a single folder, use it as the root.
let root_extracted = {
let dirs: Vec<_> = fs::read_dir(&extract_path)?
.filter_map(Result::ok)
.filter(|entry| entry.path().is_dir())
.collect();
if dirs.len() == 1 {
dirs[0].path()
} else {
extract_path.clone()
}
};
let mut copied_something = false;
let mods_source = root_extracted.join("Mods");
if mods_source.exists() {
copy_directory(&mods_source, &Path::new(target_path).join("Mods"))?;
copied_something = true;
}
let userlibs_source = root_extracted.join("UserLibs");
if userlibs_source.exists() {
copy_directory(&userlibs_source, &Path::new(target_path).join("UserLibs"))?;
copied_something = true;
}
fs::remove_dir_all(&extract_path)?;
if !copied_something {
color_echo(ConsoleColor::Red, "ERROR: Neither 'Mods' nor 'UserLibs' directory found in the extracted archive!");
write_log(log_file, "ERROR: Extracted mod archive does not contain expected 'Mods' or 'UserLibs' directories.")?;
pause_and_exit().await;
}
fs::write(&version_file, &latest_release.tag_name)?;
color_echo(ConsoleColor::Green, "Custom Avatar Loader mod installed/updated successfully.");
write_log(log_file, "Custom Avatar Loader mod installed/updated.")?;
}
} else {
color_echo(ConsoleColor::Yellow, "Could not retrieve latest Custom Avatar Loader mod release info. Skipping update check.");
write_log(log_file, "Failed to get latest Custom Avatar Loader mod release info.")?;
}
Ok(())
}
/// Recursively copies a directory from source to destination.
fn copy_directory(source: &Path, destination: &Path) -> io::Result<()> {
fs::create_dir_all(destination)?;
for entry in fs::read_dir(source)? {
let entry = entry?;
let path = entry.path();
let dest_path = destination.join(entry.file_name());
if path.is_dir() {
copy_directory(&path, &dest_path)?;
} else {
fs::copy(&path, &dest_path)?;
}
}
Ok(())
}