use crate::AppState; use log::{error, info, warn}; use reqwest; use serde::{Deserialize, Serialize}; use serde_json::json; use std::fs; use std::io; use std::path::Path; use std::sync::atomic::Ordering; use std::time::Duration; use tauri::Manager; use tauri::{AppHandle, Emitter}; use tempfile::tempdir; use zip::ZipArchive; // Constants for API endpoints and downloads const CREAMLINUX_RELEASE_URL: &str = "https://github.com/anticitizn/creamlinux/releases/latest/download/creamlinux.zip"; const SMOKEAPI_REPO: &str = "acidicoala/SmokeAPI"; // Type of installer #[derive(Debug, Clone, Copy)] pub enum InstallerType { Cream, Smoke, } // Action to perform #[derive(Debug, Clone, Copy)] pub enum InstallerAction { Install, Uninstall, } // Error type combining all possible errors #[derive(Debug)] pub enum InstallerError { IoError(io::Error), ReqwestError(reqwest::Error), ZipError(zip::result::ZipError), InstallationError(String), } impl From for InstallerError { fn from(err: io::Error) -> Self { InstallerError::IoError(err) } } impl From for InstallerError { fn from(err: reqwest::Error) -> Self { InstallerError::ReqwestError(err) } } impl From for InstallerError { fn from(err: zip::result::ZipError) -> Self { InstallerError::ZipError(err) } } impl std::fmt::Display for InstallerError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { InstallerError::IoError(e) => write!(f, "IO error: {}", e), InstallerError::ReqwestError(e) => write!(f, "Network error: {}", e), InstallerError::ZipError(e) => write!(f, "Zip extraction error: {}", e), InstallerError::InstallationError(e) => write!(f, "Installation error: {}", e), } } } impl std::error::Error for InstallerError {} // DLC Information structure #[derive(Serialize, Deserialize, Debug, Clone)] pub struct DlcInfo { pub appid: String, pub name: String, } // Struct to hold installation instructions for the frontend #[derive(Serialize, Debug, Clone)] pub struct InstallationInstructions { #[serde(rename = "type")] pub type_: String, pub command: String, pub game_title: String, pub dlc_count: Option, } // Game information structure from searcher module #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Game { pub id: String, pub title: String, pub path: String, pub native: bool, pub api_files: Vec, pub cream_installed: bool, pub smoke_installed: bool, pub installing: bool, } // Emit a progress update to the frontend pub fn emit_progress( app_handle: &AppHandle, title: &str, message: &str, progress: f32, complete: bool, show_instructions: bool, instructions: Option, ) { let mut payload = json!({ "title": title, "message": message, "progress": progress, "complete": complete, "show_instructions": show_instructions }); if let Some(inst) = instructions { payload["instructions"] = serde_json::to_value(inst).unwrap_or_default(); } if let Err(e) = app_handle.emit("installation-progress", payload) { warn!("Failed to emit progress event: {}", e); } } // Process a single game action (install/uninstall Cream/Smoke) pub async fn process_action( _game_id: String, installer_type: InstallerType, action: InstallerAction, game: Game, app_handle: AppHandle, ) -> Result<(), String> { match (installer_type, action) { (InstallerType::Cream, InstallerAction::Install) => { // We only allow CreamLinux for native games if !game.native { return Err("CreamLinux can only be installed on native Linux games".to_string()); } info!("Installing CreamLinux for game: {}", game.title); let game_title = game.title.clone(); emit_progress( &app_handle, &format!("Installing CreamLinux for {}", game_title), "Fetching DLC list...", 10.0, false, false, None, ); // Fetch DLC list let dlcs = match fetch_dlc_details(&game.id).await { Ok(dlcs) => dlcs, Err(e) => { error!("Failed to fetch DLC details: {}", e); return Err(format!("Failed to fetch DLC details: {}", e)); } }; let dlc_count = dlcs.len(); info!("Found {} DLCs for {}", dlc_count, game_title); emit_progress( &app_handle, &format!("Installing CreamLinux for {}", game_title), "Downloading CreamLinux...", 30.0, false, false, None, ); // Install CreamLinux let app_handle_clone = app_handle.clone(); let game_title_clone = game_title.clone(); match install_creamlinux(&game.path, &game.id, dlcs, move |progress, message| { // Emit progress updates during installation emit_progress( &app_handle_clone, &format!("Installing CreamLinux for {}", game_title_clone), message, 30.0 + (progress * 60.0), // Scale progress from 30% to 90% false, false, None, ); }) .await { Ok(_) => { // Emit completion with instructions let instructions = InstallationInstructions { type_: "cream_install".to_string(), command: "sh ./cream.sh %command%".to_string(), game_title: game_title.clone(), dlc_count: Some(dlc_count), }; emit_progress( &app_handle, &format!("Installation Completed: {}", game_title), "CreamLinux has been installed successfully!", 100.0, true, true, Some(instructions), ); info!("CreamLinux installation completed for: {}", game_title); Ok(()) } Err(e) => { error!("Failed to install CreamLinux: {}", e); Err(format!("Failed to install CreamLinux: {}", e)) } } } (InstallerType::Cream, InstallerAction::Uninstall) => { // Ensure this is a native game if !game.native { return Err( "CreamLinux can only be uninstalled from native Linux games".to_string() ); } let game_title = game.title.clone(); info!("Uninstalling CreamLinux from game: {}", game_title); emit_progress( &app_handle, &format!("Uninstalling CreamLinux from {}", game_title), "Removing CreamLinux files...", 30.0, false, false, None, ); // Uninstall CreamLinux match uninstall_creamlinux(&game.path) { Ok(_) => { // Emit completion with instructions let instructions = InstallationInstructions { type_: "cream_uninstall".to_string(), command: "sh ./cream.sh %command%".to_string(), game_title: game_title.clone(), dlc_count: None, }; emit_progress( &app_handle, &format!("Uninstallation Completed: {}", game_title), "CreamLinux has been uninstalled successfully!", 100.0, true, true, Some(instructions), ); info!("CreamLinux uninstallation completed for: {}", game_title); Ok(()) } Err(e) => { error!("Failed to uninstall CreamLinux: {}", e); Err(format!("Failed to uninstall CreamLinux: {}", e)) } } } (InstallerType::Smoke, InstallerAction::Install) => { // We only allow SmokeAPI for Proton/Windows games if game.native { return Err("SmokeAPI can only be installed on Proton/Windows games".to_string()); } // Check if we have any Steam API DLLs to patch if game.api_files.is_empty() { return Err( "No Steam API DLLs found to patch. SmokeAPI cannot be installed.".to_string(), ); } let game_title = game.title.clone(); info!("Installing SmokeAPI for game: {}", game_title); emit_progress( &app_handle, &format!("Installing SmokeAPI for {}", game_title), "Fetching SmokeAPI release information...", 10.0, false, false, None, ); // Create clones for the closure let app_handle_clone = app_handle.clone(); let game_title_clone = game_title.clone(); let api_files = game.api_files.clone(); // Call the SmokeAPI installation with progress updates match install_smokeapi(&game.path, &api_files, move |progress, message| { // Emit progress updates during installation emit_progress( &app_handle_clone, &format!("Installing SmokeAPI for {}", game_title_clone), message, 10.0 + (progress * 90.0), // Scale progress from 10% to 100% false, false, None, ); }) .await { Ok(_) => { // Emit completion with instructions let instructions = InstallationInstructions { type_: "smoke_install".to_string(), command: "No additional steps needed. SmokeAPI will work automatically." .to_string(), game_title: game_title.clone(), dlc_count: Some(game.api_files.len()), }; emit_progress( &app_handle, &format!("Installation Completed: {}", game_title), "SmokeAPI has been installed successfully!", 100.0, true, true, Some(instructions), ); info!("SmokeAPI installation completed for: {}", game_title); Ok(()) } Err(e) => { error!("Failed to install SmokeAPI: {}", e); Err(format!("Failed to install SmokeAPI: {}", e)) } } } (InstallerType::Smoke, InstallerAction::Uninstall) => { // Ensure this is a non-native game if game.native { return Err( "SmokeAPI can only be uninstalled from Proton/Windows games".to_string() ); } let game_title = game.title.clone(); info!("Uninstalling SmokeAPI from game: {}", game_title); emit_progress( &app_handle, &format!("Uninstalling SmokeAPI from {}", game_title), "Restoring original files...", 30.0, false, false, None, ); // Uninstall SmokeAPI match uninstall_smokeapi(&game.path, &game.api_files) { Ok(_) => { // Emit completion with instructions let instructions = InstallationInstructions { type_: "smoke_uninstall".to_string(), command: "Original Steam API files have been restored.".to_string(), game_title: game_title.clone(), dlc_count: None, }; emit_progress( &app_handle, &format!("Uninstallation Completed: {}", game_title), "SmokeAPI has been uninstalled successfully!", 100.0, true, true, Some(instructions), ); info!("SmokeAPI uninstallation completed for: {}", game_title); Ok(()) } Err(e) => { error!("Failed to uninstall SmokeAPI: {}", e); Err(format!("Failed to uninstall SmokeAPI: {}", e)) } } } } } // Install CreamLinux for a game async fn install_creamlinux( game_path: &str, app_id: &str, dlcs: Vec, progress_callback: F, ) -> Result<(), InstallerError> where F: Fn(f32, &str) + Send + 'static, { // Progress update progress_callback(0.1, "Preparing to download CreamLinux..."); // Download CreamLinux zip let client = reqwest::Client::new(); progress_callback(0.2, "Downloading CreamLinux..."); let response = client .get(CREAMLINUX_RELEASE_URL) .timeout(Duration::from_secs(30)) .send() .await?; if !response.status().is_success() { return Err(InstallerError::InstallationError(format!( "Failed to download CreamLinux: HTTP {}", response.status() ))); } // Save to temporary file progress_callback(0.4, "Saving downloaded files..."); let temp_dir = tempdir()?; let zip_path = temp_dir.path().join("creamlinux.zip"); let content = response.bytes().await?; fs::write(&zip_path, &content)?; // Extract the zip progress_callback(0.5, "Extracting CreamLinux files..."); let file = fs::File::open(&zip_path)?; let mut archive = ZipArchive::new(file)?; for i in 0..archive.len() { let mut file = archive.by_index(i)?; let outpath = Path::new(game_path).join(file.name()); if file.name().ends_with('/') { fs::create_dir_all(&outpath)?; } else { if let Some(p) = outpath.parent() { if !p.exists() { fs::create_dir_all(p)?; } } let mut outfile = fs::File::create(&outpath)?; io::copy(&mut file, &mut outfile)?; } // Set executable permissions for cream.sh if file.name() == "cream.sh" { progress_callback(0.6, "Setting executable permissions..."); #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let mut perms = fs::metadata(&outpath)?.permissions(); perms.set_mode(0o755); fs::set_permissions(&outpath, perms)?; } } } // Create cream_api.ini with DLC info progress_callback(0.8, "Creating configuration file..."); let cream_api_path = Path::new(game_path).join("cream_api.ini"); let mut config = String::new(); config.push_str(&format!("APPID = {}\n[config]\n", app_id)); config.push_str("issubscribedapp_on_false_use_real = true\n"); config.push_str("[methods]\n"); config.push_str("disable_steamapps_issubscribedapp = false\n"); config.push_str("[dlc]\n"); for dlc in dlcs { config.push_str(&format!("{} = {}\n", dlc.appid, dlc.name)); } fs::write(cream_api_path, config)?; progress_callback(1.0, "Installation completed successfully!"); Ok(()) } // Install CreamLinux for a game with pre-fetched DLC list pub async fn install_creamlinux_with_dlcs( game_path: &str, app_id: &str, dlcs: Vec, progress_callback: F, ) -> Result<(), InstallerError> where F: Fn(f32, &str) + Send + 'static, { // Progress update progress_callback(0.1, "Preparing to download CreamLinux..."); // Download CreamLinux zip let client = reqwest::Client::new(); progress_callback(0.2, "Downloading CreamLinux..."); let response = client .get(CREAMLINUX_RELEASE_URL) .timeout(Duration::from_secs(30)) .send() .await?; if !response.status().is_success() { return Err(InstallerError::InstallationError(format!( "Failed to download CreamLinux: HTTP {}", response.status() ))); } // Save to temporary file progress_callback(0.4, "Saving downloaded files..."); let temp_dir = tempdir()?; let zip_path = temp_dir.path().join("creamlinux.zip"); let content = response.bytes().await?; fs::write(&zip_path, &content)?; // Extract the zip progress_callback(0.5, "Extracting CreamLinux files..."); let file = fs::File::open(&zip_path)?; let mut archive = ZipArchive::new(file)?; for i in 0..archive.len() { let mut file = archive.by_index(i)?; let outpath = Path::new(game_path).join(file.name()); if file.name().ends_with('/') { fs::create_dir_all(&outpath)?; } else { if let Some(p) = outpath.parent() { if !p.exists() { fs::create_dir_all(p)?; } } let mut outfile = fs::File::create(&outpath)?; io::copy(&mut file, &mut outfile)?; } // Set executable permissions for cream.sh if file.name() == "cream.sh" { progress_callback(0.6, "Setting executable permissions..."); #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let mut perms = fs::metadata(&outpath)?.permissions(); perms.set_mode(0o755); fs::set_permissions(&outpath, perms)?; } } } // Create cream_api.ini with DLC info - using the provided DLCs directly progress_callback(0.8, "Creating configuration file..."); let cream_api_path = Path::new(game_path).join("cream_api.ini"); let mut config = String::new(); config.push_str(&format!("APPID = {}\n[config]\n", app_id)); config.push_str("issubscribedapp_on_false_use_real = true\n"); config.push_str("[methods]\n"); config.push_str("disable_steamapps_issubscribedapp = false\n"); config.push_str("[dlc]\n"); for dlc in dlcs { config.push_str(&format!("{} = {}\n", dlc.appid, dlc.name)); } fs::write(cream_api_path, config)?; progress_callback(1.0, "Installation completed successfully!"); Ok(()) } // Uninstall CreamLinux from a game fn uninstall_creamlinux(game_path: &str) -> Result<(), InstallerError> { info!("Uninstalling CreamLinux from: {}", game_path); // Files to remove during uninstallation let files_to_remove = [ "cream.sh", "cream_api.ini", "cream_api.so", "lib32Creamlinux.so", "lib64Creamlinux.so", ]; for file in &files_to_remove { let file_path = Path::new(game_path).join(file); if file_path.exists() { match fs::remove_file(&file_path) { Ok(_) => info!("Removed file: {}", file_path.display()), Err(e) => { error!("Failed to remove {}: {}", file_path.display(), e); // Continue with other files even if one fails } } } } info!("CreamLinux uninstallation completed for: {}", game_path); Ok(()) } // Fetch DLC details from Steam API pub async fn fetch_dlc_details(app_id: &str) -> Result, InstallerError> { let client = reqwest::Client::new(); let base_url = format!( "https://store.steampowered.com/api/appdetails?appids={}", app_id ); let response = client .get(&base_url) .timeout(Duration::from_secs(10)) .send() .await?; if !response.status().is_success() { return Err(InstallerError::InstallationError(format!( "Failed to fetch game details: HTTP {}", response.status() ))); } let data: serde_json::Value = response.json().await?; let dlc_ids = match data .get(app_id) .and_then(|app| app.get("data")) .and_then(|data| data.get("dlc")) { Some(dlc_array) => match dlc_array.as_array() { Some(array) => array .iter() .filter_map(|id| id.as_u64().map(|n| n.to_string())) .collect::>(), _ => Vec::new(), }, _ => Vec::new(), }; info!("Found {} DLCs for game ID {}", dlc_ids.len(), app_id); let mut dlc_details = Vec::new(); for dlc_id in dlc_ids { let dlc_url = format!( "https://store.steampowered.com/api/appdetails?appids={}", dlc_id ); // Add a small delay to avoid rate limiting tokio::time::sleep(Duration::from_millis(300)).await; let dlc_response = client .get(&dlc_url) .timeout(Duration::from_secs(10)) .send() .await?; if dlc_response.status().is_success() { let dlc_data: serde_json::Value = dlc_response.json().await?; let dlc_name = match dlc_data .get(&dlc_id) .and_then(|app| app.get("data")) .and_then(|data| data.get("name")) { Some(name) => match name.as_str() { Some(s) => s.to_string(), _ => "Unknown DLC".to_string(), }, _ => "Unknown DLC".to_string(), }; info!("Found DLC: {} ({})", dlc_name, dlc_id); dlc_details.push(DlcInfo { appid: dlc_id, name: dlc_name, }); } else if dlc_response.status() == reqwest::StatusCode::TOO_MANY_REQUESTS { // If rate limited, wait longer error!("Rate limited by Steam API, waiting 10 seconds"); tokio::time::sleep(Duration::from_secs(10)).await; } } info!( "Successfully retrieved details for {} DLCs", dlc_details.len() ); Ok(dlc_details) } // Fetch DLC details from Steam API with progress updates pub async fn fetch_dlc_details_with_progress( app_id: &str, app_handle: &tauri::AppHandle, ) -> Result, InstallerError> { info!( "Starting DLC details fetch with progress for game ID: {}", app_id ); // Get a reference to a cancellation flag from app state let state = app_handle.state::(); let should_cancel = state.fetch_cancellation.clone(); let client = reqwest::Client::new(); let base_url = format!( "https://store.steampowered.com/api/appdetails?appids={}", app_id ); // Emit initial progress emit_dlc_progress(app_handle, "Looking up game details...", 5, None); info!("Emitted initial DLC progress: 5%"); let response = client .get(&base_url) .timeout(Duration::from_secs(10)) .send() .await?; if !response.status().is_success() { let error_msg = format!("Failed to fetch game details: HTTP {}", response.status()); error!("{}", error_msg); return Err(InstallerError::InstallationError(error_msg)); } let data: serde_json::Value = response.json().await?; let dlc_ids = match data .get(app_id) .and_then(|app| app.get("data")) .and_then(|data| data.get("dlc")) { Some(dlc_array) => match dlc_array.as_array() { Some(array) => array .iter() .filter_map(|id| id.as_u64().map(|n| n.to_string())) .collect::>(), _ => Vec::new(), }, _ => Vec::new(), }; info!("Found {} DLCs for game ID {}", dlc_ids.len(), app_id); emit_dlc_progress( app_handle, &format!("Found {} DLCs. Fetching details...", dlc_ids.len()), 10, None, ); info!("Emitted DLC progress: 10%, found {} DLCs", dlc_ids.len()); let mut dlc_details = Vec::new(); let total_dlcs = dlc_ids.len(); for (index, dlc_id) in dlc_ids.iter().enumerate() { // Check if cancellation was requested if should_cancel.load(Ordering::SeqCst) { info!("DLC fetch cancelled for game {}", app_id); return Err(InstallerError::InstallationError( "Operation cancelled by user".to_string(), )); } let progress_percent = 10.0 + (index as f32 / total_dlcs as f32) * 90.0; let progress_rounded = progress_percent as u32; let remaining_dlcs = total_dlcs - index; // Estimate time remaining (rough calculation - 300ms per DLC) let est_time_left = if remaining_dlcs > 0 { let seconds = (remaining_dlcs as f32 * 0.3).ceil() as u32; if seconds < 60 { format!("~{} seconds", seconds) } else { format!("~{} minute(s)", (seconds as f32 / 60.0).ceil() as u32) } } else { "almost done".to_string() }; info!( "Processing DLC {}/{} - Progress: {}%", index + 1, total_dlcs, progress_rounded ); emit_dlc_progress( app_handle, &format!("Processing DLC {}/{}", index + 1, total_dlcs), progress_rounded, Some(&est_time_left), ); let dlc_url = format!( "https://store.steampowered.com/api/appdetails?appids={}", dlc_id ); // Add a small delay to avoid rate limiting tokio::time::sleep(Duration::from_millis(300)).await; let dlc_response = client .get(&dlc_url) .timeout(Duration::from_secs(10)) .send() .await?; if dlc_response.status().is_success() { let dlc_data: serde_json::Value = dlc_response.json().await?; let dlc_name = match dlc_data .get(&dlc_id) .and_then(|app| app.get("data")) .and_then(|data| data.get("name")) { Some(name) => match name.as_str() { Some(s) => s.to_string(), _ => "Unknown DLC".to_string(), }, _ => "Unknown DLC".to_string(), }; info!("Found DLC: {} ({})", dlc_name, dlc_id); let dlc_info = DlcInfo { appid: dlc_id.clone(), name: dlc_name, }; // Emit each DLC as we find it if let Ok(json) = serde_json::to_string(&dlc_info) { if let Err(e) = app_handle.emit("dlc-found", json) { warn!("Failed to emit dlc-found event: {}", e); } else { info!("Emitted dlc-found event for DLC: {}", dlc_id); } } dlc_details.push(dlc_info); } else if dlc_response.status() == reqwest::StatusCode::TOO_MANY_REQUESTS { // If rate limited, wait longer error!("Rate limited by Steam API, waiting 10 seconds"); emit_dlc_progress( app_handle, "Rate limited by Steam. Waiting...", progress_rounded, None, ); tokio::time::sleep(Duration::from_secs(10)).await; } } // Final progress update info!( "Completed DLC fetch. Found {} DLCs in total", dlc_details.len() ); emit_dlc_progress( app_handle, &format!("Completed! Found {} DLCs", dlc_details.len()), 100, None, ); info!("Emitted final DLC progress: 100%"); Ok(dlc_details) } // Emit DLC progress updates to the frontend fn emit_dlc_progress( app_handle: &tauri::AppHandle, message: &str, progress: u32, time_left: Option<&str>, ) { let mut payload = json!({ "message": message, "progress": progress }); if let Some(time) = time_left { payload["timeLeft"] = json!(time); } if let Err(e) = app_handle.emit("dlc-progress", payload) { warn!("Failed to emit dlc-progress event: {}", e); } } // Install SmokeAPI for a game async fn install_smokeapi( game_path: &str, api_files: &[String], progress_callback: F, ) -> Result<(), InstallerError> where F: Fn(f32, &str) + Send + 'static, { // Get the latest SmokeAPI release progress_callback(0.1, "Fetching latest SmokeAPI release..."); let client = reqwest::Client::new(); let releases_url = format!( "https://api.github.com/repos/{}/releases/latest", SMOKEAPI_REPO ); let response = client .get(&releases_url) .header("User-Agent", "CreamLinux") .timeout(Duration::from_secs(10)) .send() .await?; if !response.status().is_success() { return Err(InstallerError::InstallationError(format!( "Failed to fetch SmokeAPI releases: HTTP {}", response.status() ))); } let release_info: serde_json::Value = response.json().await?; let latest_version = match release_info.get("tag_name") { Some(tag) => tag.as_str().unwrap_or("latest"), _ => "latest", }; info!("Latest SmokeAPI version: {}", latest_version); // Construct download URL let zip_url = format!( "https://github.com/{}/releases/download/{}/SmokeAPI-{}.zip", SMOKEAPI_REPO, latest_version, latest_version ); // Download the zip progress_callback(0.3, "Downloading SmokeAPI..."); let response = client .get(&zip_url) .timeout(Duration::from_secs(30)) .send() .await?; if !response.status().is_success() { return Err(InstallerError::InstallationError(format!( "Failed to download SmokeAPI: HTTP {}", response.status() ))); } // Save to temporary file progress_callback(0.5, "Saving downloaded files..."); let temp_dir = tempdir()?; let zip_path = temp_dir.path().join("smokeapi.zip"); let content = response.bytes().await?; fs::write(&zip_path, &content)?; // Extract and install for each API file progress_callback(0.6, "Extracting SmokeAPI files..."); let file = fs::File::open(&zip_path)?; let mut archive = ZipArchive::new(file)?; for (i, api_file) in api_files.iter().enumerate() { let progress = 0.6 + (i as f32 / api_files.len() as f32) * 0.3; progress_callback(progress, &format!("Installing SmokeAPI for {}", api_file)); let api_dir = Path::new(game_path).join( Path::new(api_file) .parent() .unwrap_or_else(|| Path::new("")), ); let api_name = Path::new(api_file).file_name().unwrap_or_default(); // Backup original file let original_path = api_dir.join(api_name); let backup_path = api_dir.join(api_name.to_string_lossy().replace(".dll", "_o.dll")); info!("Processing: {}", original_path.display()); info!("Backup path: {}", backup_path.display()); // Only backup if not already backed up if !backup_path.exists() && original_path.exists() { fs::copy(&original_path, &backup_path)?; info!("Created backup: {}", backup_path.display()); } // Extract the appropriate DLL directly to the game directory if let Ok(mut file) = archive.by_name(&api_name.to_string_lossy()) { let mut outfile = fs::File::create(&original_path)?; io::copy(&mut file, &mut outfile)?; info!("Installed SmokeAPI as: {}", original_path.display()); } else { return Err(InstallerError::InstallationError(format!( "Could not find {} in the SmokeAPI zip file", api_name.to_string_lossy() ))); } } progress_callback(1.0, "SmokeAPI installation completed!"); info!("SmokeAPI installation completed for: {}", game_path); Ok(()) } // Uninstall SmokeAPI from a game fn uninstall_smokeapi(game_path: &str, api_files: &[String]) -> Result<(), InstallerError> { info!("Uninstalling SmokeAPI from: {}", game_path); for api_file in api_files { let api_path = Path::new(game_path).join(api_file); let api_dir = api_path.parent().unwrap_or_else(|| Path::new(game_path)); let api_name = api_path.file_name().unwrap_or_default(); let original_path = api_dir.join(api_name); let backup_path = api_dir.join(api_name.to_string_lossy().replace(".dll", "_o.dll")); info!("Processing: {}", original_path.display()); info!("Backup path: {}", backup_path.display()); if backup_path.exists() { // Remove the SmokeAPI version if original_path.exists() { match fs::remove_file(&original_path) { Ok(_) => info!("Removed SmokeAPI file: {}", original_path.display()), Err(e) => error!( "Failed to remove SmokeAPI file: {}, error: {}", original_path.display(), e ), } } // Restore the original file match fs::rename(&backup_path, &original_path) { Ok(_) => info!("Restored original file: {}", original_path.display()), Err(e) => { error!( "Failed to restore original file: {}, error: {}", original_path.display(), e ); // Try to copy instead if rename fails if let Err(copy_err) = fs::copy(&backup_path, &original_path) .and_then(|_| fs::remove_file(&backup_path)) { error!("Failed to copy backup file: {}", copy_err); } } } } else { info!("No backup found for: {}", api_file); } } info!("SmokeAPI uninstallation completed for: {}", game_path); Ok(()) }