mirror of
https://github.com/Novattz/creamlinux-installer.git
synced 2026-01-24 12:22:49 -05:00
1075 lines
35 KiB
Rust
1075 lines
35 KiB
Rust
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<io::Error> for InstallerError {
|
|
fn from(err: io::Error) -> Self {
|
|
InstallerError::IoError(err)
|
|
}
|
|
}
|
|
|
|
impl From<reqwest::Error> for InstallerError {
|
|
fn from(err: reqwest::Error) -> Self {
|
|
InstallerError::ReqwestError(err)
|
|
}
|
|
}
|
|
|
|
impl From<zip::result::ZipError> 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<usize>,
|
|
}
|
|
|
|
// 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<String>,
|
|
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<InstallationInstructions>,
|
|
) {
|
|
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<F>(
|
|
game_path: &str,
|
|
app_id: &str,
|
|
dlcs: Vec<DlcInfo>,
|
|
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<F>(
|
|
game_path: &str,
|
|
app_id: &str,
|
|
dlcs: Vec<DlcInfo>,
|
|
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<Vec<DlcInfo>, 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<String>>(),
|
|
_ => 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<Vec<DlcInfo>, 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::<AppState>();
|
|
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<String>>(),
|
|
_ => 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<F>(
|
|
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(())
|
|
}
|