use std::env; use std::fs; use std::io::Write; use std::path::Path; use std::process::{Command, Stdio}; use reqwest::Client; use directories::ProjectDirs; const POSTGRES_URL: &str = "https://sbp.enterprisedb.com/getfile.jsp?fileid=1259603"; const GIT_URL: &str = "https://github.com/git-for-windows/git/releases/download/v2.50.1.windows.1/Git-2.50.1-64-bit.exe"; const PROTOC_RELEASES_API: &str = "https://api.github.com/repos/protocolbuffers/protobuf/releases/latest"; const PROTOC_ASSET_KEYWORD: &str = "protoc-31.1-win64.zip"; const WICKED_WAIFUS_REPO: &str = "https://git.xeondev.com/wickedwaifus/wicked-waifus-rs.git"; const RUSTUP_URL: &str = "https://static.rust-lang.org/rustup/dist/x86_64-pc-windows-msvc/rustup-init.exe"; /// Steps: /// 0. Download and install Rust /// 1. Download and install `PostgreSQL` 16.9 (user handles admin prompt) /// 2. Download and install Protoc, add bin to PATH /// 3. Download and install Git /// 4. Clone wicked-waifus-rs repo recursively /// 5. Build all required binaries in sequence pub async fn install_wuthering_waves() -> Result<(), String> { let client = Client::new(); let proj_dirs = ProjectDirs::from("com", "ReversedRooms", "RoomsLauncher").ok_or("Could not get ProjectDirs")?; let deps_dir = proj_dirs.data_dir().join("kurogames\\wuwa\\dependencies"); std::fs::create_dir_all(&deps_dir).map_err(|e| e.to_string())?; // 0. Download and install Rust let rustup_installer = deps_dir.join("rustup-init.exe"); download_file(&client, RUSTUP_URL, &rustup_installer).await?; let mut rustup_cmd = Command::new(&rustup_installer); rustup_cmd.arg("-y"); let mut child = rustup_cmd.spawn().map_err(|e| format!("Failed to run rustup-init: {e}"))?; let status = child.wait().map_err(|e| format!("Failed to wait for rustup-init: {e}"))?; if !status.success() { return Err("rustup-init failed".into()); } println!("Rust installed"); std::fs::remove_file(&rustup_installer).ok(); // 1. Download and install PostgreSQL (user handles admin prompt) let postgres_installer = deps_dir.join("postgresql-installer.exe"); download_file(&client, POSTGRES_URL, &postgres_installer).await?; run_installer(&postgres_installer, false)?; // not silent println!("PostgreSQL installed"); std::fs::remove_file(&postgres_installer).ok(); // 2. Download and install Protoc, add bin to PATH let protoc_zip = deps_dir.join("protoc.zip"); let protoc_dir = deps_dir.join("protoc"); download_latest_protoc(&client, &protoc_zip).await?; unzip_file(&protoc_zip, &protoc_dir)?; std::fs::remove_file(&protoc_zip).ok(); add_bin_to_path(&protoc_dir.join("bin"))?; println!("Protoc installed"); // 3. Download and install Git let git_installer = deps_dir.join("git-installer.exe"); download_file(&client, GIT_URL, &git_installer).await?; run_installer(&git_installer, true)?; // silent println!("Git installed"); std::fs::remove_file(&git_installer).ok(); // 4. Clone wicked-waifus-rs repo recursively let repo_dir = deps_dir.join("wicked-waifus-rs"); if !repo_dir.exists() { let status = Command::new("git") .arg("clone") .arg("--recursive") .arg(WICKED_WAIFUS_REPO) .arg(&repo_dir) .stdout(Stdio::null()) .stderr(Stdio::null()) .status() .map_err(|e| format!("Failed to run git clone: {e}"))?; if !status.success() { return Err("git clone failed".into()); } println!("Wicked Waifus repo cloned"); } // 5. Build all required binaries in sequence let bins = [ "wicked-waifus-config-server", "wicked-waifus-login-server", "wicked-waifus-gateway-server", "wicked-waifus-game-server", ]; for bin in &bins { let status = Command::new("cargo") .arg("build") .arg("--bin") .arg(bin) .current_dir(&repo_dir) .stdout(Stdio::null()) .stderr(Stdio::null()) .status() .map_err(|e| format!("Failed to run cargo build for {bin}: {e}"))?; if !status.success() { return Err(format!("cargo build failed for {bin}")); } println!("{bin} built"); } println!("All binaries built"); Ok(()) } async fn download_file(client: &Client, url: &str, dest: &Path) -> Result<(), String> { let resp = client.get(url) .header("User-Agent", "rooms-launcher") .send().await .map_err(|e| format!("Failed to download {url}: {e}"))?; let bytes = resp.bytes().await.map_err(|e| format!("Failed to read bytes: {e}"))?; let mut file = fs::File::create(dest).map_err(|e| format!("Failed to create file: {e}"))?; file.write_all(&bytes).map_err(|e| format!("Failed to write file: {e}"))?; println!("Downloaded {url} to {}", dest.to_string_lossy()); Ok(()) } fn run_installer(installer: &Path, silent: bool) -> Result<(), String> { let mut cmd = Command::new(installer); if silent { cmd.arg("/VERYSILENT"); } let mut child = cmd.spawn().map_err(|e| format!("Failed to run installer: {e}"))?; let status = child.wait().map_err(|e| format!("Failed to wait for installer: {e}"))?; if !status.success() { return Err(format!("Installer failed: {installer:?}")); } println!("{installer:?} ran successfully"); Ok(()) } async fn download_latest_protoc(client: &Client, dest: &Path) -> Result<(), String> { let resp = client.get(PROTOC_RELEASES_API) .header("User-Agent", "rooms-launcher") .send().await .map_err(|e| format!("Failed to fetch Protoc releases: {e}"))?; let json: serde_json::Value = resp.json().await.map_err(|e| format!("Failed to parse Protoc release JSON: {e}"))?; let assets = json["assets"].as_array().ok_or("No assets in Protoc release JSON")?; let asset = assets.iter().find(|a| a["name"].as_str().is_some_and(|n| n.contains(PROTOC_ASSET_KEYWORD))) .ok_or("No zip asset found for Protoc")?; let url = asset["browser_download_url"].as_str().ok_or("No download URL for Protoc asset")?; download_file(client, url, dest).await?; println!("Downloaded Protoc to {}", dest.to_string_lossy()); Ok(()) } fn unzip_file(zip_path: &Path, dest_dir: &Path) -> Result<(), String> { let file = fs::File::open(zip_path).map_err(|e| format!("Failed to open zip: {e}"))?; let mut archive = zip::ZipArchive::new(file).map_err(|e| format!("Failed to read zip: {e}"))?; fs::create_dir_all(dest_dir).map_err(|e| format!("Failed to create unzip dir: {e}"))?; for i in 0..archive.len() { let mut file = archive.by_index(i).map_err(|e| format!("Failed to get file in zip: {e}"))?; let outpath = dest_dir.join(file.name()); if file.is_dir() { fs::create_dir_all(&outpath).map_err(|e| format!("Failed to create dir in zip: {e}"))?; } else { if let Some(p) = outpath.parent() { fs::create_dir_all(p).map_err(|e| format!("Failed to create parent dir: {e}"))?; } let mut outfile = fs::File::create(&outpath).map_err(|e| format!("Failed to create file in zip: {e}"))?; std::io::copy(&mut file, &mut outfile).map_err(|e| format!("Failed to extract file: {e}"))?; } } println!("Unzipped {} to {}", zip_path.to_string_lossy(), dest_dir.to_string_lossy()); Ok(()) } fn add_bin_to_path(bin_dir: &Path) -> Result<(), String> { let bin_str = bin_dir.to_str().ok_or("Invalid bin path")?; let path = env::var("Path").unwrap_or_default(); let new_path = format!("{bin_str};{path}"); unsafe { env::set_var("Path", &new_path); } println!("Added {} to Path", bin_dir.to_string_lossy()); Ok(()) }