diff --git a/src-tauri/src/installer/mod.rs b/src-tauri/src/installer/mod.rs index 6255185..7fcfe86 100644 --- a/src-tauri/src/installer/mod.rs +++ b/src-tauri/src/installer/mod.rs @@ -241,8 +241,26 @@ async fn uninstall_creamlinux(game: Game, app_handle: AppHandle) -> Result<(), S Ok(()) } -// Install SmokeAPI to a game async fn install_smokeapi(game: Game, app_handle: AppHandle) -> Result<(), String> { + // Check if native or proton and route accordingly + if game.native { + install_smokeapi_native(game, app_handle).await + } else { + install_smokeapi_proton(game, app_handle).await + } +} + +async fn uninstall_smokeapi(game: Game, app_handle: AppHandle) -> Result<(), String> { + // Check if native or proton and route accordingly + if game.native { + uninstall_smokeapi_native(game, app_handle).await + } else { + uninstall_smokeapi_proton(game, app_handle).await + } +} + +// Install SmokeAPI to a proton game +async fn install_smokeapi_proton(game: Game, app_handle: AppHandle) -> Result<(), String> { if game.native { return Err("SmokeAPI can only be installed on Proton/Windows games".to_string()); } @@ -286,8 +304,8 @@ async fn install_smokeapi(game: Game, app_handle: AppHandle) -> Result<(), Strin Ok(()) } -// Uninstall SmokeAPI from a game -async fn uninstall_smokeapi(game: Game, app_handle: AppHandle) -> Result<(), String> { +// Uninstall SmokeAPI from a proton game +async fn uninstall_smokeapi_proton(game: Game, app_handle: AppHandle) -> Result<(), String> { if game.native { return Err("SmokeAPI can only be uninstalled from Proton/Windows games".to_string()); } @@ -329,6 +347,99 @@ async fn uninstall_smokeapi(game: Game, app_handle: AppHandle) -> Result<(), Str Ok(()) } +// Install SmokeAPI to a native Linux game +async fn install_smokeapi_native( + game: Game, + app_handle: AppHandle, +) -> Result<(), String> { + + info!("Installing SmokeAPI (native) for game: {}", game.title); + let game_title = game.title.clone(); + + emit_progress( + &app_handle, + &format!("Installing SmokeAPI for {}", game_title), + "Detecting game architecture...", + 20.0, + false, + false, + None, + ); + + emit_progress( + &app_handle, + &format!("Installing SmokeAPI for {}", game_title), + "Installing from cache...", + 50.0, + false, + false, + None, + ); + + // Install SmokeAPI for native Linux (empty string for api_files_str) + SmokeAPI::install_to_game(&game.path, "") + .await + .map_err(|e| format!("Failed to install SmokeAPI: {}", e))?; + + // Update version manifest + let cached_versions = crate::cache::read_versions()?; + update_game_smokeapi_version(&game.path, cached_versions.smokeapi.latest)?; + + emit_progress( + &app_handle, + &format!("Installation Completed: {}", game_title), + "SmokeAPI has been installed successfully!", + 100.0, + true, + false, + None, + ); + + info!("SmokeAPI (native) installation completed for: {}", game_title); + Ok(()) +} + +// Uninstall SmokeAPI from a native Linux game +async fn uninstall_smokeapi_native(game: Game, app_handle: AppHandle) -> Result<(), String> { + if !game.native { + return Err("This function is only for native Linux games".to_string()); + } + + let game_title = game.title.clone(); + info!("Uninstalling SmokeAPI (native) from game: {}", game_title); + + emit_progress( + &app_handle, + &format!("Uninstalling SmokeAPI from {}", game_title), + "Removing SmokeAPI files...", + 50.0, + false, + false, + None, + ); + + // Uninstall SmokeAPI (empty string for api_files_str) + SmokeAPI::uninstall_from_game(&game.path, "") + .await + .map_err(|e| format!("Failed to uninstall SmokeAPI: {}", e))?; + + // Remove version from manifest + remove_smokeapi_version(&game.path)?; + + emit_progress( + &app_handle, + &format!("Uninstallation Completed: {}", game_title), + "SmokeAPI has been removed successfully!", + 100.0, + true, + false, + None, + ); + + info!("SmokeAPI (native) uninstallation completed for: {}", game_title); + Ok(()) +} + // Fetch DLC details from Steam API (simple version without progress) pub async fn fetch_dlc_details(app_id: &str) -> Result, String> { let client = reqwest::Client::new(); diff --git a/src-tauri/src/unlockers/smokeapi.rs b/src-tauri/src/unlockers/smokeapi.rs index 2fb73dd..7a24bbf 100644 --- a/src-tauri/src/unlockers/smokeapi.rs +++ b/src-tauri/src/unlockers/smokeapi.rs @@ -94,7 +94,7 @@ impl Unlocker for SmokeAPI { let mut archive = ZipArchive::new(file).map_err(|e| format!("Failed to read zip archive: {}", e))?; - // Extract all DLL files + // Extract both DLL files (for Proton) and .so files (for native Linux) for i in 0..archive.len() { let mut file = archive .by_index(i) @@ -102,8 +102,11 @@ impl Unlocker for SmokeAPI { let file_name = file.name(); - // Only extract DLL files - if file_name.to_lowercase().ends_with(".dll") { + // Extract DLL files for Proton and .so files for native Linux + let should_extract = file_name.to_lowercase().ends_with(".dll") + || file_name.to_lowercase().ends_with(".so"); + + if should_extract { let output_path = version_dir.join( Path::new(file_name) .file_name() @@ -127,17 +130,56 @@ impl Unlocker for SmokeAPI { } async fn install_to_game(game_path: &str, api_files_str: &str) -> Result<(), String> { + // Check if this is a native Linux game or Proton game + // Native games have empty api_files_str, Proton games have DLL paths + let is_native = api_files_str.is_empty(); + + if is_native { + Self::install_to_native_game(game_path).await + } else { + Self::install_to_proton_game(game_path, api_files_str).await + } + } + + async fn uninstall_from_game(game_path: &str, api_files_str: &str) -> Result<(), String> { + // Check if this is a native Linux game or Proton game + let is_native = api_files_str.is_empty(); + + if is_native { + Self::uninstall_from_native_game(game_path).await + } else { + Self::uninstall_from_proton_game(game_path, api_files_str).await + } + } + + fn name() -> &'static str { + "SmokeAPI" + } +} + +impl SmokeAPI { + /// Install SmokeAPI to a Proton/Windows game + async fn install_to_proton_game(game_path: &str, api_files_str: &str) -> Result<(), String> { // Parse api_files from the context string (comma-separated) let api_files: Vec = api_files_str.split(',').map(|s| s.to_string()).collect(); info!( - "Installing SmokeAPI to {} for {} API files", + "Installing SmokeAPI (Proton) to {} for {} API files", game_path, api_files.len() ); // Get the cached SmokeAPI DLLs - let cached_dlls = crate::cache::list_smokeapi_dlls()?; + let cached_files = crate::cache::list_smokeapi_files()?; + if cached_files.is_empty() { + return Err("No SmokeAPI files found in cache".to_string()); + } + + let cached_dlls: Vec<_> = cached_files + .iter() + .filter(|f| f.extension().and_then(|e| e.to_str()) == Some("dll")) + .collect(); + if cached_dlls.is_empty() { return Err("No SmokeAPI DLLs found in cache".to_string()); } @@ -195,15 +237,77 @@ impl Unlocker for SmokeAPI { ); } - info!("SmokeAPI installation completed for: {}", game_path); + info!("SmokeAPI (Proton) installation completed for: {}", game_path); Ok(()) } - async fn uninstall_from_game(game_path: &str, api_files_str: &str) -> Result<(), String> { + /// Install SmokeAPI to a native Linux game + async fn install_to_native_game(game_path: &str) -> Result<(), String> { + info!("Installing SmokeAPI (native) to {}", game_path); + + // Detect game bitness + let bitness = crate::utils::bitness::detect_game_bitness(game_path)?; + info!("Detected game bitness: {:?}", bitness); + + // Get the cached SmokeAPI files + let cached_files = crate::cache::list_smokeapi_files()?; + if cached_files.is_empty() { + return Err("No SmokeAPI files found in cache".to_string()); + } + + // Determine which .so file to use based on bitness + let target_so = match bitness { + crate::utils::bitness::Bitness::Bit32 => "libsmoke_api32.so", + crate::utils::bitness::Bitness::Bit64 => "libsmoke_api64.so", + }; + + // Find the matching .so file in cache + let matching_so = cached_files + .iter() + .find(|file| { + file.file_name() + .unwrap_or_default() + .to_string_lossy() + == target_so + }) + .ok_or_else(|| format!("No matching {} found in cache", target_so))?; + + let game_path_obj = Path::new(game_path); + + // Look for libsteam_api.so in the game directory (scan up to depth 3) + let libsteam_path = Self::find_libsteam_api(game_path_obj)?; + + info!("Found libsteam_api.so at: {}", libsteam_path.display()); + + // Create backup of original libsteam_api.so + let backup_path = libsteam_path.with_file_name("libsteam_api_o.so"); + + // Only backup if not already backed up + if !backup_path.exists() && libsteam_path.exists() { + fs::copy(&libsteam_path, &backup_path) + .map_err(|e| format!("Failed to backup libsteam_api.so: {}", e))?; + info!("Created backup: {}", backup_path.display()); + } + + // Replace libsteam_api.so with SmokeAPI's libsmoke_api.so + fs::copy(matching_so, &libsteam_path) + .map_err(|e| format!("Failed to replace libsteam_api.so: {}", e))?; + + info!( + "Replaced libsteam_api.so with {} (hook mode)", + target_so + ); + + info!("SmokeAPI (native) installation completed for: {}", game_path); + Ok(()) + } + + /// Uninstall SmokeAPI from a Proton/Windows game + async fn uninstall_from_proton_game(game_path: &str, api_files_str: &str) -> Result<(), String> { // Parse api_files from the context string (comma-separated) let api_files: Vec = api_files_str.split(',').map(|s| s.to_string()).collect(); - info!("Uninstalling SmokeAPI from: {}", game_path); + info!("Uninstalling SmokeAPI (Proton) from: {}", game_path); for api_file in &api_files { let api_path = Path::new(game_path).join(api_file); @@ -250,11 +354,79 @@ impl Unlocker for SmokeAPI { } } - info!("SmokeAPI uninstallation completed for: {}", game_path); + info!("SmokeAPI (Proton) uninstallation completed for: {}", game_path); Ok(()) } - fn name() -> &'static str { - "SmokeAPI" + /// Uninstall SmokeAPI from a native Linux game + async fn uninstall_from_native_game(game_path: &str) -> Result<(), String> { + info!("Uninstalling SmokeAPI (native) from: {}", game_path); + + let game_path_obj = Path::new(game_path); + + // Look for libsteam_api.so (which is actually our SmokeAPI now) + let libsteam_path = match Self::find_libsteam_api(game_path_obj) { + Ok(path) => path, + Err(_) => { + warn!("libsteam_api.so not found, nothing to uninstall"); + return Ok(()); + } + }; + + // Look for backup + let backup_path = libsteam_path.with_file_name("libsteam_api_o.so"); + + if backup_path.exists() { + // Remove the SmokeAPI version + if libsteam_path.exists() { + match fs::remove_file(&libsteam_path) { + Ok(_) => info!("Removed SmokeAPI version: {}", libsteam_path.display()), + Err(e) => warn!("Failed to remove SmokeAPI file: {}", e), + } + } + + // Restore the original file + match fs::rename(&backup_path, &libsteam_path) { + Ok(_) => info!("Restored original libsteam_api.so"), + Err(e) => { + warn!("Failed to restore original file: {}", e); + // Try to copy instead if rename fails + if let Err(copy_err) = fs::copy(&backup_path, &libsteam_path) + .and_then(|_| fs::remove_file(&backup_path)) + { + error!("Failed to copy backup file: {}", copy_err); + } + } + } + } else { + warn!("No backup found (libsteam_api_o.so), cannot restore original"); + } + + info!("SmokeAPI (native) uninstallation completed for: {}", game_path); + Ok(()) + } + + /// Find libsteam_api.so in the game directory + fn find_libsteam_api(game_path: &Path) -> Result { + use walkdir::WalkDir; + + // Scan for libsteam_api.so (not too deep to keep it fast) + for entry in WalkDir::new(game_path) + .max_depth(3) + .into_iter() + .filter_map(Result::ok) + { + let path = entry.path(); + if !path.is_file() { + continue; + } + + let filename = path.file_name().unwrap_or_default().to_string_lossy(); + if filename == "libsteam_api.so" { + return Ok(path.to_path_buf()); + } + } + + Err("libsteam_api.so not found in game directory".to_string()) } } \ No newline at end of file