diff options
| author | 2026-01-15 17:49:26 +0800 | |
|---|---|---|
| committer | 2026-01-15 17:49:26 +0800 | |
| commit | 32a9aceee42a2261b64f9e6effda522639576a5e (patch) | |
| tree | 4cae8d216c3093421addaa0450bc8004c537e373 /src-tauri/src/main.rs | |
| parent | ce4b0c2053d5d16f7091d74840d4a502401f1a4e (diff) | |
| parent | 31077dbd39a25eecd24a1dca0f8c9d1879265277 (diff) | |
| download | DropOut-32a9aceee42a2261b64f9e6effda522639576a5e.tar.gz DropOut-32a9aceee42a2261b64f9e6effda522639576a5e.zip | |
Merge pull request #30 from HsiangNianian/main
Diffstat (limited to 'src-tauri/src/main.rs')
| -rw-r--r-- | src-tauri/src/main.rs | 508 |
1 files changed, 460 insertions, 48 deletions
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index ae74a03..b69912e 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -6,6 +6,7 @@ use std::sync::Mutex; use tauri::{Emitter, Manager, State, Window}; // Added Emitter use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::process::Command; +use serde::Serialize; // Added Serialize #[cfg(target_os = "windows")] use std::os::windows::process::CommandExt; @@ -40,6 +41,27 @@ impl MsRefreshTokenState { } } +/// Check if a string contains unresolved placeholders in the form ${...} +/// +/// After the replacement phase, if a string still contains ${...}, it means +/// that placeholder variable was not found in the replacements map and is +/// therefore unresolved. We should skip adding such arguments to avoid +/// passing malformed arguments to the game launcher. +fn has_unresolved_placeholder(s: &str) -> bool { + // Look for the opening sequence + if let Some(start_pos) = s.find("${") { + // Check if there's a closing brace after the opening sequence + if s[start_pos + 2..].find('}').is_some() { + // Found a complete ${...} pattern - this is an unresolved placeholder + return true; + } + // Found ${ but no closing } - also treat as unresolved/malformed + return true; + } + // No ${ found - the string is fully resolved + false +} + #[tauri::command] async fn start_game( window: Window, @@ -93,35 +115,16 @@ async fn start_game( emit_log!(window, format!("Game directory: {:?}", game_dir)); - // 1. Fetch manifest to find the version URL - emit_log!(window, "Fetching version manifest...".to_string()); - let manifest = core::manifest::fetch_version_manifest() - .await - .map_err(|e| e.to_string())?; + // 1. Load version (supports both vanilla and modded versions with inheritance) emit_log!( window, - format!("Found {} versions in manifest", manifest.versions.len()) + format!("Loading version details for {}...", version_id) ); - // Find the version info - let version_info = manifest - .versions - .iter() - .find(|v| v.id == version_id) - .ok_or_else(|| format!("Version {} not found in manifest", version_id))?; - - // 2. Fetch specific version JSON (client.jar info) - emit_log!( - window, - format!("Fetching version details for {}...", version_id) - ); - let version_url = &version_info.url; - let version_details: core::game_version::GameVersion = reqwest::get(version_url) - .await - .map_err(|e| e.to_string())? - .json() + let version_details = core::manifest::load_version(&game_dir, &version_id) .await .map_err(|e| e.to_string())?; + emit_log!( window, format!( @@ -130,20 +133,33 @@ async fn start_game( ) ); - // 3. Prepare download tasks + // Determine the actual minecraft version for client.jar + // (for modded versions, this is the parent vanilla version) + let minecraft_version = version_details + .inherits_from + .clone() + .unwrap_or_else(|| version_id.clone()); + + // 2. Prepare download tasks emit_log!(window, "Preparing download tasks...".to_string()); let mut download_tasks = Vec::new(); // --- Client Jar --- - let client_jar = version_details.downloads.client; + // Get downloads from version_details (may be inherited) + let downloads = version_details + .downloads + .as_ref() + .ok_or("Version has no downloads information")?; + let client_jar = &downloads.client; let mut client_path = game_dir.join("versions"); - client_path.push(&version_id); - client_path.push(format!("{}.jar", version_id)); + client_path.push(&minecraft_version); + client_path.push(format!("{}.jar", minecraft_version)); download_tasks.push(core::downloader::DownloadTask { - url: client_jar.url, + url: client_jar.url.clone(), path: client_path.clone(), - sha1: Some(client_jar.sha1), + sha1: client_jar.sha1.clone(), + sha256: None, }); // --- Libraries --- @@ -153,7 +169,7 @@ async fn start_game( for lib in &version_details.libraries { if core::rules::is_library_allowed(&lib.rules) { - // 1. Standard Library + // 1. Standard Library - check for explicit downloads first if let Some(downloads) = &lib.downloads { if let Some(artifact) = &downloads.artifact { let path_str = artifact @@ -167,7 +183,8 @@ async fn start_game( download_tasks.push(core::downloader::DownloadTask { url: artifact.url.clone(), path: lib_path, - sha1: Some(artifact.sha1.clone()), + sha1: artifact.sha1.clone(), + sha256: None, }); } @@ -200,13 +217,30 @@ async fn start_game( download_tasks.push(core::downloader::DownloadTask { url: native_artifact.url, path: native_path.clone(), - sha1: Some(native_artifact.sha1), + sha1: native_artifact.sha1, + sha256: None, }); native_libs_paths.push(native_path); } } } + } else { + // 3. Library without explicit downloads (mod loader libraries) + // Use Maven coordinate resolution + if let Some(url) = + core::maven::resolve_library_url(&lib.name, None, lib.url.as_deref()) + { + if let Some(lib_path) = core::maven::get_library_path(&lib.name, &libraries_dir) + { + download_tasks.push(core::downloader::DownloadTask { + url, + path: lib_path, + sha1: None, // Maven libraries often don't have SHA1 in the JSON + sha256: None, + }); + } + } } } } @@ -217,8 +251,14 @@ async fn start_game( let objects_dir = assets_dir.join("objects"); let indexes_dir = assets_dir.join("indexes"); + // Get asset index (may be inherited from parent) + let asset_index = version_details + .asset_index + .as_ref() + .ok_or("Version has no asset index information")?; + // Download Asset Index JSON - let asset_index_path = indexes_dir.join(format!("{}.json", version_details.asset_index.id)); + let asset_index_path = indexes_dir.join(format!("{}.json", asset_index.id)); // Check if index exists or download it // Note: We need the content of this file to parse it. @@ -230,11 +270,8 @@ async fn start_game( .await .map_err(|e| e.to_string())? } else { - println!( - "Downloading asset index from {}", - version_details.asset_index.url - ); - let content = reqwest::get(&version_details.asset_index.url) + println!("Downloading asset index from {}", asset_index.url); + let content = reqwest::get(&asset_index.url) .await .map_err(|e| e.to_string())? .text() @@ -254,6 +291,7 @@ async fn start_game( #[derive(serde::Deserialize, Debug)] struct AssetObject { hash: String, + #[allow(dead_code)] size: u64, } @@ -280,6 +318,7 @@ async fn start_game( url, path, sha1: Some(hash), + sha256: None, }); } @@ -394,10 +433,7 @@ async fn start_game( replacements.insert("${version_name}", version_id.clone()); replacements.insert("${game_directory}", game_dir.to_string_lossy().to_string()); replacements.insert("${assets_root}", assets_dir.to_string_lossy().to_string()); - replacements.insert( - "${assets_index_name}", - version_details.asset_index.id.clone(), - ); + replacements.insert("${assets_index_name}", asset_index.id.clone()); replacements.insert("${auth_uuid}", account.uuid()); replacements.insert("${auth_access_token}", account.access_token()); replacements.insert("${user_type}", "mojang".to_string()); @@ -449,7 +485,10 @@ async fn start_game( for (key, replacement) in &replacements { arg = arg.replace(key, replacement); } - args.push(arg); + // Skip arguments with unresolved placeholders + if !has_unresolved_placeholder(&arg) { + args.push(arg); + } } else if let Some(arr) = val.as_array() { for sub in arr { if let Some(s) = sub.as_str() { @@ -457,7 +496,10 @@ async fn start_game( for (key, replacement) in &replacements { arg = arg.replace(key, replacement); } - args.push(arg); + // Skip arguments with unresolved placeholders + if !has_unresolved_placeholder(&arg) { + args.push(arg); + } } } } @@ -785,7 +827,7 @@ async fn refresh_account( .map_err(|e| e.to_string())?; let storage = core::account_storage::AccountStorage::new(app_dir.clone()); - let (stored_account, ms_refresh) = storage + let (_stored_account, ms_refresh) = storage .get_active_account() .ok_or("No active account found")?; @@ -807,8 +849,8 @@ async fn refresh_account( /// Detect Java installations on the system #[tauri::command] -async fn detect_java() -> Result<Vec<core::java::JavaInstallation>, String> { - Ok(core::java::detect_java_installations()) +async fn detect_java(app_handle: tauri::AppHandle) -> Result<Vec<core::java::JavaInstallation>, String> { + Ok(core::java::detect_all_java_installations(&app_handle)) } /// Get recommended Java for a specific Minecraft version @@ -819,8 +861,349 @@ async fn get_recommended_java( Ok(core::java::get_recommended_java(required_major_version)) } +/// Get Adoptium Java download info +#[tauri::command] +async fn fetch_adoptium_java( + major_version: u32, + image_type: String, +) -> Result<core::java::JavaDownloadInfo, String> { + let img_type = match image_type.to_lowercase().as_str() { + "jdk" => core::java::ImageType::Jdk, + _ => core::java::ImageType::Jre, + }; + core::java::fetch_java_release(major_version, img_type).await +} + +/// Download and install Adoptium Java +#[tauri::command] +async fn download_adoptium_java( + app_handle: tauri::AppHandle, + major_version: u32, + image_type: String, + custom_path: Option<String>, +) -> Result<core::java::JavaInstallation, String> { + let img_type = match image_type.to_lowercase().as_str() { + "jdk" => core::java::ImageType::Jdk, + _ => core::java::ImageType::Jre, + }; + let path = custom_path.map(std::path::PathBuf::from); + core::java::download_and_install_java(&app_handle, major_version, img_type, path).await +} + +/// Get available Adoptium Java versions +#[tauri::command] +async fn fetch_available_java_versions() -> Result<Vec<u32>, String> { + core::java::fetch_available_versions().await +} + +/// Fetch Java catalog with platform availability (uses cache) +#[tauri::command] +async fn fetch_java_catalog( + app_handle: tauri::AppHandle, +) -> Result<core::java::JavaCatalog, String> { + core::java::fetch_java_catalog(&app_handle, false).await +} + +/// Refresh Java catalog (bypass cache) +#[tauri::command] +async fn refresh_java_catalog( + app_handle: tauri::AppHandle, +) -> Result<core::java::JavaCatalog, String> { + core::java::fetch_java_catalog(&app_handle, true).await +} + +/// Cancel current Java download +#[tauri::command] +async fn cancel_java_download() -> Result<(), String> { + core::java::cancel_current_download(); + Ok(()) +} + +/// Get pending Java downloads +#[tauri::command] +async fn get_pending_java_downloads( + app_handle: tauri::AppHandle, +) -> Result<Vec<core::downloader::PendingJavaDownload>, String> { + Ok(core::java::get_pending_downloads(&app_handle)) +} + +/// Resume pending Java downloads +#[tauri::command] +async fn resume_java_downloads( + app_handle: tauri::AppHandle, +) -> Result<Vec<core::java::JavaInstallation>, String> { + core::java::resume_pending_downloads(&app_handle).await +} + +/// Get Minecraft versions supported by Fabric +#[tauri::command] +async fn get_fabric_game_versions() -> Result<Vec<core::fabric::FabricGameVersion>, String> { + core::fabric::fetch_supported_game_versions() + .await + .map_err(|e| e.to_string()) +} + +/// Get available Fabric loader versions +#[tauri::command] +async fn get_fabric_loader_versions() -> Result<Vec<core::fabric::FabricLoaderVersion>, String> { + core::fabric::fetch_loader_versions() + .await + .map_err(|e| e.to_string()) +} + +/// Get Fabric loaders available for a specific Minecraft version +#[tauri::command] +async fn get_fabric_loaders_for_version( + game_version: String, +) -> Result<Vec<core::fabric::FabricLoaderEntry>, String> { + core::fabric::fetch_loaders_for_game_version(&game_version) + .await + .map_err(|e| e.to_string()) +} + +/// Install Fabric loader for a specific Minecraft version +#[tauri::command] +async fn install_fabric( + window: Window, + game_version: String, + loader_version: String, +) -> Result<core::fabric::InstalledFabricVersion, String> { + emit_log!( + window, + format!( + "Installing Fabric {} for Minecraft {}...", + loader_version, game_version + ) + ); + + let app_handle = window.app_handle(); + let game_dir = app_handle + .path() + .app_data_dir() + .map_err(|e| format!("Failed to get app data dir: {}", e))?; + + let result = core::fabric::install_fabric(&game_dir, &game_version, &loader_version) + .await + .map_err(|e| e.to_string())?; + + emit_log!( + window, + format!("Fabric installed successfully: {}", result.id) + ); + + Ok(result) +} + +/// List installed Fabric versions +#[tauri::command] +async fn list_installed_fabric_versions(window: Window) -> Result<Vec<String>, String> { + let app_handle = window.app_handle(); + let game_dir = app_handle + .path() + .app_data_dir() + .map_err(|e| format!("Failed to get app data dir: {}", e))?; + + core::fabric::list_installed_fabric_versions(&game_dir) + .await + .map_err(|e| e.to_string()) +} + +/// Check if Fabric is installed for a specific version +#[tauri::command] +async fn is_fabric_installed( + window: Window, + game_version: String, + loader_version: String, +) -> Result<bool, String> { + let app_handle = window.app_handle(); + let game_dir = app_handle + .path() + .app_data_dir() + .map_err(|e| format!("Failed to get app data dir: {}", e))?; + + Ok(core::fabric::is_fabric_installed( + &game_dir, + &game_version, + &loader_version, + )) +} + +/// Get Minecraft versions supported by Forge +#[tauri::command] +async fn get_forge_game_versions() -> Result<Vec<String>, String> { + core::forge::fetch_supported_game_versions() + .await + .map_err(|e| e.to_string()) +} + +/// Get available Forge versions for a specific Minecraft version +#[tauri::command] +async fn get_forge_versions_for_game( + game_version: String, +) -> Result<Vec<core::forge::ForgeVersion>, String> { + core::forge::fetch_forge_versions(&game_version) + .await + .map_err(|e| e.to_string()) +} + +/// Install Forge for a specific Minecraft version +#[tauri::command] +async fn install_forge( + window: Window, + game_version: String, + forge_version: String, +) -> Result<core::forge::InstalledForgeVersion, String> { + emit_log!( + window, + format!( + "Installing Forge {} for Minecraft {}...", + forge_version, game_version + ) + ); + + let app_handle = window.app_handle(); + let game_dir = app_handle + .path() + .app_data_dir() + .map_err(|e| format!("Failed to get app data dir: {}", e))?; + + let result = core::forge::install_forge(&game_dir, &game_version, &forge_version) + .await + .map_err(|e| e.to_string())?; + + emit_log!( + window, + format!("Forge installed successfully: {}", result.id) + ); + + Ok(result) +} + +#[derive(serde::Serialize)] +struct GithubRelease { + tag_name: String, + name: String, + published_at: String, + body: String, + html_url: String, +} + +#[tauri::command] +async fn get_github_releases() -> Result<Vec<GithubRelease>, String> { + let client = reqwest::Client::new(); + let res = client + .get("https://api.github.com/repos/HsiangNianian/DropOut/releases") + .header("User-Agent", "DropOut-Launcher") + .send() + .await + .map_err(|e| e.to_string())?; + + if !res.status().is_success() { + return Err(format!("GitHub API returned status: {}", res.status())); + } + + let releases: Vec<serde_json::Value> = res.json().await.map_err(|e| e.to_string())?; + + let mut result = Vec::new(); + for r in releases { + if let (Some(tag), Some(name), Some(date), Some(body), Some(url)) = ( + r["tag_name"].as_str(), + r["name"].as_str(), + r["published_at"].as_str(), + r["body"].as_str(), + r["html_url"].as_str() + ) { + result.push(GithubRelease { + tag_name: tag.to_string(), + name: name.to_string(), + published_at: date.to_string(), + body: body.to_string(), + html_url: url.to_string(), + }); + } + } + Ok(result) +} + +#[derive(Serialize)] +struct PastebinResponse { + url: String, +} + +#[tauri::command] +async fn upload_to_pastebin( + state: State<'_, core::config::ConfigState>, + content: String, +) -> Result<PastebinResponse, String> { + // Check content length limit + if content.len() > 500 * 1024 { + return Err("Log file too large (max 500KB)".to_string()); + } + + // Extract config values before any async calls to avoid holding MutexGuard across await + let (service, api_key) = { + let config = state.config.lock().unwrap(); + ( + config.log_upload_service.clone(), + config.pastebin_api_key.clone(), + ) + }; + + let client = reqwest::Client::new(); + + match service.as_str() { + "pastebin.com" => { + let api_key = api_key + .ok_or("Pastebin API Key not configured in settings")?; + + let res = client + .post("https://pastebin.com/api/api_post.php") + .form(&[ + ("api_dev_key", api_key.as_str()), + ("api_option", "paste"), + ("api_paste_code", content.as_str()), + ("api_paste_private", "1"), // Unlisted + ("api_paste_name", "DropOut Launcher Log"), + ("api_paste_expire_date", "1W"), + ]) + .send() + .await + .map_err(|e| e.to_string())?; + + if !res.status().is_success() { + return Err(format!("Pastebin upload failed: {}", res.status())); + } + + let url = res.text().await.map_err(|e| e.to_string())?; + if url.starts_with("Bad API Request") { + return Err(format!("Pastebin API error: {}", url)); + } + Ok(PastebinResponse { url }) + } + // Default to paste.rs + _ => { + let res = client + .post("https://paste.rs/") + .body(content) + .send() + .await + .map_err(|e| e.to_string())?; + + if !res.status().is_success() { + return Err(format!("paste.rs upload failed: {}", res.status())); + } + + let url = res.text().await.map_err(|e| e.to_string())?; + let url = url.trim().to_string(); + Ok(PastebinResponse { url }) + } + } +} + fn main() { tauri::Builder::default() + .plugin(tauri_plugin_fs::init()) + .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_shell::init()) .manage(core::auth::AccountState::new()) .manage(MsRefreshTokenState::new()) @@ -846,6 +1229,13 @@ fn main() { println!("[Startup] Loaded saved account"); } + // Check for pending Java downloads and notify frontend + let pending = core::java::get_pending_downloads(&app.app_handle()); + if !pending.is_empty() { + println!("[Startup] Found {} pending Java download(s)", pending.len()); + let _ = app.emit("pending-java-downloads", pending.len()); + } + Ok(()) }) .invoke_handler(tauri::generate_handler![ @@ -859,8 +1249,30 @@ fn main() { start_microsoft_login, complete_microsoft_login, refresh_account, + // Java commands detect_java, - get_recommended_java + get_recommended_java, + fetch_adoptium_java, + download_adoptium_java, + fetch_available_java_versions, + fetch_java_catalog, + refresh_java_catalog, + cancel_java_download, + get_pending_java_downloads, + resume_java_downloads, + // Fabric commands + get_fabric_game_versions, + get_fabric_loader_versions, + get_fabric_loaders_for_version, + install_fabric, + list_installed_fabric_versions, + is_fabric_installed, + // Forge commands + get_forge_game_versions, + get_forge_versions_for_game, + install_forge, + get_github_releases, + upload_to_pastebin ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); |