diff options
| author | 2026-01-14 15:54:39 +0800 | |
|---|---|---|
| committer | 2026-01-14 15:54:39 +0800 | |
| commit | ce4b0c2053d5d16f7091d74840d4a502401f1a4e (patch) | |
| tree | 170667359ecb773800cd334e4176341cf09306b2 | |
| parent | fd6d7ffef4a1c6b093ad1d5b83579ab27ef5327e (diff) | |
| parent | 505e3485f3dfa31969651f7f281fde33e9843fe8 (diff) | |
| download | DropOut-ce4b0c2053d5d16f7091d74840d4a502401f1a4e.tar.gz DropOut-ce4b0c2053d5d16f7091d74840d4a502401f1a4e.zip | |
Merge pull request #25 from HsiangNianian/main
| -rw-r--r-- | .github/workflows/release.yml | 2 | ||||
| -rw-r--r-- | src-tauri/src/core/account_storage.rs | 29 | ||||
| -rw-r--r-- | src-tauri/src/core/auth.rs | 36 | ||||
| -rw-r--r-- | src-tauri/src/core/downloader.rs | 40 | ||||
| -rw-r--r-- | src-tauri/src/core/java.rs | 24 | ||||
| -rw-r--r-- | src-tauri/src/main.rs | 187 |
6 files changed, 209 insertions, 109 deletions
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 23d4181..774e547 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,6 +38,8 @@ jobs: token: ${{ github.token }} tag: ${{ github.event_name == 'workflow_dispatch' && inputs.tag_name || github.ref_name }} changelogFilePath: CHANGELOG.md + includeInvalidCommits: true + useGitmojis: false - name: Create Release id: create_release diff --git a/src-tauri/src/core/account_storage.rs b/src-tauri/src/core/account_storage.rs index b8e15e1..569df7b 100644 --- a/src-tauri/src/core/account_storage.rs +++ b/src-tauri/src/core/account_storage.rs @@ -4,21 +4,12 @@ use std::fs; use std::path::PathBuf; /// Stored account data for persistence -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct AccountStore { pub accounts: Vec<StoredAccount>, pub active_account_id: Option<String>, } -impl Default for AccountStore { - fn default() -> Self { - Self { - accounts: Vec::new(), - active_account_id: None, - } - } -} - #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type")] pub enum StoredAccount { @@ -131,13 +122,17 @@ impl AccountStorage { pub fn get_active_account(&self) -> Option<(StoredAccount, Option<String>)> { let store = self.load(); if let Some(active_id) = &store.active_account_id { - store.accounts.iter().find(|a| &a.id() == active_id).map(|a| { - let ms_token = match a { - StoredAccount::Microsoft(m) => m.ms_refresh_token.clone(), - _ => None, - }; - (a.clone(), ms_token) - }) + store + .accounts + .iter() + .find(|a| &a.id() == active_id) + .map(|a| { + let ms_token = match a { + StoredAccount::Microsoft(m) => m.ms_refresh_token.clone(), + _ => None, + }; + (a.clone(), ms_token) + }) } else { None } diff --git a/src-tauri/src/core/auth.rs b/src-tauri/src/core/auth.rs index 624f1de..5f01a58 100644 --- a/src-tauri/src/core/auth.rs +++ b/src-tauri/src/core/auth.rs @@ -2,7 +2,6 @@ use serde::{Deserialize, Serialize}; use std::sync::Mutex; use uuid::Uuid; - // Helper to create a client with a custom User-Agent // This is critical because Microsoft's WAF often blocks requests without a valid UA fn get_client() -> reqwest::Client { @@ -116,7 +115,7 @@ pub async fn refresh_microsoft_token(refresh_token: &str) -> Result<TokenRespons let resp = client .post(url) .header("Content-Type", "application/x-www-form-urlencoded") - .body(serde_urlencoded::to_string(¶ms).map_err(|e| e.to_string())?) + .body(serde_urlencoded::to_string(params).map_err(|e| e.to_string())?) .send() .await .map_err(|e| e.to_string())?; @@ -142,30 +141,32 @@ pub fn is_token_expired(expires_at: i64) -> bool { .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs() as i64; - + // Consider expired if less than 5 minutes remaining expires_at - now < 300 } /// Full refresh flow: refresh MS token -> Xbox -> XSTS -> Minecraft -pub async fn refresh_full_auth(ms_refresh_token: &str) -> Result<(MicrosoftAccount, String), String> { +pub async fn refresh_full_auth( + ms_refresh_token: &str, +) -> Result<(MicrosoftAccount, String), String> { println!("[Auth] Starting full token refresh..."); - + // 1. Refresh Microsoft token let token_resp = refresh_microsoft_token(ms_refresh_token).await?; - + // 2. Xbox Live Auth let (xbl_token, uhs) = method_xbox_live(&token_resp.access_token).await?; - + // 3. XSTS Auth let xsts_token = method_xsts(&xbl_token).await?; - + // 4. Minecraft Auth let mc_token = login_minecraft(&xsts_token, &uhs).await?; - + // 5. Get Profile let profile = fetch_profile(&mc_token).await?; - + // 6. Create Account let account = MicrosoftAccount { username: profile.name, @@ -175,12 +176,15 @@ pub async fn refresh_full_auth(ms_refresh_token: &str) -> Result<(MicrosoftAccou expires_at: (std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() - .as_secs() + token_resp.expires_in) as i64, + .as_secs() + + token_resp.expires_in) as i64, }; - + // Return new MS refresh token for storage - let new_ms_refresh = token_resp.refresh_token.unwrap_or_else(|| ms_refresh_token.to_string()); - + let new_ms_refresh = token_resp + .refresh_token + .unwrap_or_else(|| ms_refresh_token.to_string()); + Ok((account, new_ms_refresh)) } @@ -221,7 +225,7 @@ pub async fn start_device_flow() -> Result<DeviceCodeResponse, String> { let resp = client .post(url) .header("Content-Type", "application/x-www-form-urlencoded") - .body(serde_urlencoded::to_string(¶ms).map_err(|e| e.to_string())?) + .body(serde_urlencoded::to_string(params).map_err(|e| e.to_string())?) .send() .await .map_err(|e| e.to_string())?; @@ -257,7 +261,7 @@ pub async fn exchange_code_for_token(device_code: &str) -> Result<TokenResponse, let resp = client .post(url) .header("Content-Type", "application/x-www-form-urlencoded") - .body(serde_urlencoded::to_string(¶ms).map_err(|e| e.to_string())?) + .body(serde_urlencoded::to_string(params).map_err(|e| e.to_string())?) .send() .await .map_err(|e| e.to_string())?; diff --git a/src-tauri/src/core/downloader.rs b/src-tauri/src/core/downloader.rs index 7ff81ad..3add9b7 100644 --- a/src-tauri/src/core/downloader.rs +++ b/src-tauri/src/core/downloader.rs @@ -69,7 +69,10 @@ impl GlobalProgress { /// Add downloaded bytes and return updated snapshot fn add_bytes(&self, delta: u64) -> ProgressSnapshot { - let total_bytes = self.total_downloaded_bytes.fetch_add(delta, Ordering::Relaxed) + delta; + let total_bytes = self + .total_downloaded_bytes + .fetch_add(delta, Ordering::Relaxed) + + delta; ProgressSnapshot { completed_files: self.completed_files.load(Ordering::Relaxed), total_files: self.total_files, @@ -101,10 +104,14 @@ fn emit_progress( ); } -pub async fn download_files(window: Window, tasks: Vec<DownloadTask>, max_concurrent: usize) -> Result<(), String> { +pub async fn download_files( + window: Window, + tasks: Vec<DownloadTask>, + max_concurrent: usize, +) -> Result<(), String> { // Clamp max_concurrent to a valid range (1-128) to prevent edge cases let max_concurrent = max_concurrent.clamp(1, 128); - + let client = reqwest::Client::new(); let semaphore = Arc::new(Semaphore::new(max_concurrent)); let progress = Arc::new(GlobalProgress::new(tasks.len())); @@ -141,7 +148,14 @@ pub async fn download_files(window: Window, tasks: Vec<DownloadTask>, max_concur if skipped_size > 0 { let _ = progress.add_bytes(skipped_size); } - emit_progress(&window, &file_name, "Skipped", 0, 0, &progress.inc_completed()); + emit_progress( + &window, + &file_name, + "Skipped", + 0, + 0, + &progress.inc_completed(), + ); return Ok(()); } } @@ -170,7 +184,14 @@ pub async fn download_files(window: Window, tasks: Vec<DownloadTask>, max_concur } downloaded += chunk.len() as u64; let snapshot = progress.add_bytes(chunk.len() as u64); - emit_progress(&window, &file_name, "Downloading", downloaded, total_size, &snapshot); + emit_progress( + &window, + &file_name, + "Downloading", + downloaded, + total_size, + &snapshot, + ); } Ok(None) => break, Err(e) => return Err(format!("Download error: {}", e)), @@ -180,7 +201,14 @@ pub async fn download_files(window: Window, tasks: Vec<DownloadTask>, max_concur Err(e) => return Err(format!("Request error: {}", e)), } - emit_progress(&window, &file_name, "Finished", 0, 0, &progress.inc_completed()); + emit_progress( + &window, + &file_name, + "Finished", + 0, + 0, + &progress.inc_completed(), + ); Ok(()) } }); diff --git a/src-tauri/src/core/java.rs b/src-tauri/src/core/java.rs index e0962fa..9cf3053 100644 --- a/src-tauri/src/core/java.rs +++ b/src-tauri/src/core/java.rs @@ -17,7 +17,10 @@ pub fn detect_java_installations() -> Vec<JavaInstallation> { for candidate in candidates { if let Some(java) = check_java_installation(&candidate) { // Avoid duplicates - if !installations.iter().any(|j: &JavaInstallation| j.path == java.path) { + if !installations + .iter() + .any(|j: &JavaInstallation| j.path == java.path) + { installations.push(java); } } @@ -121,7 +124,9 @@ fn get_java_candidates() -> Vec<PathBuf> { if homebrew_arm.exists() { if let Ok(entries) = std::fs::read_dir(&homebrew_arm) { for entry in entries.flatten() { - let java_path = entry.path().join("libexec/openjdk.jdk/Contents/Home/bin/java"); + let java_path = entry + .path() + .join("libexec/openjdk.jdk/Contents/Home/bin/java"); if java_path.exists() { candidates.push(java_path); } @@ -133,8 +138,10 @@ fn get_java_candidates() -> Vec<PathBuf> { #[cfg(target_os = "windows")] { // Windows Java paths - let program_files = std::env::var("ProgramFiles").unwrap_or_else(|_| "C:\\Program Files".to_string()); - let program_files_x86 = std::env::var("ProgramFiles(x86)").unwrap_or_else(|_| "C:\\Program Files (x86)".to_string()); + let program_files = + std::env::var("ProgramFiles").unwrap_or_else(|_| "C:\\Program Files".to_string()); + let program_files_x86 = std::env::var("ProgramFiles(x86)") + .unwrap_or_else(|_| "C:\\Program Files (x86)".to_string()); let local_app_data = std::env::var("LOCALAPPDATA").unwrap_or_default(); let win_paths = [ @@ -186,14 +193,11 @@ fn get_java_candidates() -> Vec<PathBuf> { /// Check a specific Java installation and get its version info fn check_java_installation(path: &PathBuf) -> Option<JavaInstallation> { - let output = Command::new(path) - .arg("-version") - .output() - .ok()?; + let output = Command::new(path).arg("-version").output().ok()?; // Java outputs version info to stderr let version_output = String::from_utf8_lossy(&output.stderr); - + // Parse version string (e.g., "openjdk version \"17.0.1\"" or "java version \"1.8.0_301\"") let version = parse_version_string(&version_output)?; let is_64bit = version_output.contains("64-Bit"); @@ -240,7 +244,7 @@ fn parse_java_version(version: &str) -> u32 { /// Get the best Java for a specific Minecraft version pub fn get_recommended_java(required_major_version: Option<u64>) -> Option<JavaInstallation> { let installations = detect_java_installations(); - + if let Some(required) = required_major_version { // Find exact match or higher installations.into_iter().find(|java| { diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 73310d5..ae74a03 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -26,6 +26,12 @@ pub struct MsRefreshTokenState { pub token: Mutex<Option<String>>, } +impl Default for MsRefreshTokenState { + fn default() -> Self { + Self::new() + } +} + impl MsRefreshTokenState { pub fn new() -> Self { Self { @@ -41,7 +47,10 @@ async fn start_game( config_state: State<'_, core::config::ConfigState>, version_id: String, ) -> Result<String, String> { - emit_log!(window, format!("Starting game launch for version: {}", version_id)); + emit_log!( + window, + format!("Starting game launch for version: {}", version_id) + ); // Check for active account emit_log!(window, "Checking for active account...".to_string()); @@ -51,16 +60,22 @@ async fn start_game( .unwrap() .clone() .ok_or("No active account found. Please login first.")?; - + let account_type = match &account { core::auth::Account::Offline(_) => "Offline", core::auth::Account::Microsoft(_) => "Microsoft", }; - emit_log!(window, format!("Account found: {} ({})", account.username(), account_type)); + emit_log!( + window, + format!("Account found: {} ({})", account.username(), account_type) + ); let config = config_state.config.lock().unwrap().clone(); emit_log!(window, format!("Java path: {}", config.java_path)); - emit_log!(window, format!("Memory: {}MB - {}MB", config.min_memory, config.max_memory)); + emit_log!( + window, + format!("Memory: {}MB - {}MB", config.min_memory, config.max_memory) + ); // Get App Data Directory (e.g., ~/.local/share/com.dropout.launcher or similar) // The identifier is set in tauri.conf.json. @@ -83,7 +98,10 @@ async fn start_game( let manifest = core::manifest::fetch_version_manifest() .await .map_err(|e| e.to_string())?; - emit_log!(window, format!("Found {} versions in manifest", manifest.versions.len())); + emit_log!( + window, + format!("Found {} versions in manifest", manifest.versions.len()) + ); // Find the version info let version_info = manifest @@ -93,7 +111,10 @@ async fn start_game( .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)); + 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 @@ -101,7 +122,13 @@ async fn start_game( .json() .await .map_err(|e| e.to_string())?; - emit_log!(window, format!("Version details loaded: main class = {}", version_details.main_class)); + emit_log!( + window, + format!( + "Version details loaded: main class = {}", + version_details.main_class + ) + ); // 3. Prepare download tasks emit_log!(window, "Preparing download tasks...".to_string()); @@ -256,16 +283,29 @@ async fn start_game( }); } - emit_log!(window, format!( - "Total download tasks: {} (Client + Libraries + Assets)", - download_tasks.len() - )); + emit_log!( + window, + format!( + "Total download tasks: {} (Client + Libraries + Assets)", + download_tasks.len() + ) + ); // 4. Start Download - emit_log!(window, format!("Starting downloads with {} concurrent threads...", config.download_threads)); - core::downloader::download_files(window.clone(), download_tasks, config.download_threads as usize) - .await - .map_err(|e| e.to_string())?; + emit_log!( + window, + format!( + "Starting downloads with {} concurrent threads...", + config.download_threads + ) + ); + core::downloader::download_files( + window.clone(), + download_tasks, + config.download_threads as usize, + ) + .await + .map_err(|e| e.to_string())?; emit_log!(window, "All downloads completed successfully".to_string()); // 5. Extract Natives @@ -328,16 +368,16 @@ async fn start_game( parse_jvm_arguments(jvm_args, &mut args, &natives_path, &classpath); } } - + // Add memory settings (these override any defaults) args.push(format!("-Xmx{}M", config.max_memory)); args.push(format!("-Xms{}M", config.min_memory)); - + // Ensure natives path is set if not already in jvm args if !args.iter().any(|a| a.contains("-Djava.library.path")) { args.push(format!("-Djava.library.path={}", natives_path)); } - + // Ensure classpath is set if not already if !args.iter().any(|a| a == "-cp" || a == "-classpath") { args.push("-cp".to_string()); @@ -429,14 +469,20 @@ async fn start_game( } } - emit_log!(window, format!("Preparing to launch game with {} arguments...", args.len())); + emit_log!( + window, + format!("Preparing to launch game with {} arguments...", args.len()) + ); // Debug: Log arguments (only first few to avoid spam) if args.len() > 10 { emit_log!(window, format!("First 10 args: {:?}", &args[..10])); } // Spawn the process - emit_log!(window, format!("Starting Java process: {}", config.java_path)); + emit_log!( + window, + format!("Starting Java process: {}", config.java_path) + ); let mut command = Command::new(&config.java_path); command.args(&args); command.current_dir(&game_dir); // Run in game directory @@ -448,7 +494,10 @@ async fn start_game( { const CREATE_NO_WINDOW: u32 = 0x08000000; command.creation_flags(CREATE_NO_WINDOW); - emit_log!(window, "Applied CREATE_NO_WINDOW flag for Windows".to_string()); + emit_log!( + window, + "Applied CREATE_NO_WINDOW flag for Windows".to_string() + ); } // Spawn and handle output @@ -468,7 +517,10 @@ async fn start_game( .expect("child did not have a handle to stderr"); // Emit launcher log that game is running - emit_log!(window, "Game is now running, capturing output...".to_string()); + emit_log!( + window, + "Game is now running, capturing output...".to_string() + ); let window_rx = window.clone(); tokio::spawn(async move { @@ -537,9 +589,9 @@ fn parse_jvm_arguments( } else if let Some(obj) = item.as_object() { // Conditional argument with rules let allow = if let Some(rules_val) = obj.get("rules") { - if let Ok(rules) = serde_json::from_value::<Vec<core::game_version::Rule>>( - rules_val.clone(), - ) { + if let Ok(rules) = + serde_json::from_value::<Vec<core::game_version::Rule>>(rules_val.clone()) + { core::rules::is_library_allowed(&Some(rules)) } else { false @@ -596,13 +648,16 @@ async fn login_offline( let account = core::auth::Account::Offline(core::auth::OfflineAccount { username, uuid }); *state.active_account.lock().unwrap() = Some(account.clone()); - + // Save to storage let app_handle = window.app_handle(); - let app_dir = app_handle.path().app_data_dir().map_err(|e| e.to_string())?; + let app_dir = app_handle + .path() + .app_data_dir() + .map_err(|e| e.to_string())?; let storage = core::account_storage::AccountStorage::new(app_dir); storage.add_or_update_account(&account, None)?; - + Ok(account) } @@ -614,23 +669,28 @@ async fn get_active_account( } #[tauri::command] -async fn logout( - window: Window, - state: State<'_, core::auth::AccountState>, -) -> Result<(), String> { +async fn logout(window: Window, state: State<'_, core::auth::AccountState>) -> Result<(), String> { // Get current account UUID before clearing - let uuid = state.active_account.lock().unwrap().as_ref().map(|a| a.uuid()); - + let uuid = state + .active_account + .lock() + .unwrap() + .as_ref() + .map(|a| a.uuid()); + *state.active_account.lock().unwrap() = None; - + // Remove from storage if let Some(uuid) = uuid { let app_handle = window.app_handle(); - let app_dir = app_handle.path().app_data_dir().map_err(|e| e.to_string())?; + let app_dir = app_handle + .path() + .app_data_dir() + .map_err(|e| e.to_string())?; let storage = core::account_storage::AccountStorage::new(app_dir); storage.remove_account(&uuid)?; } - + Ok(()) } @@ -665,23 +725,23 @@ async fn complete_microsoft_login( ) -> Result<core::auth::Account, String> { // 1. Poll (once) for token let token_resp = core::auth::exchange_code_for_token(&device_code).await?; - + // Store MS refresh token let ms_refresh_token = token_resp.refresh_token.clone(); *ms_refresh_state.token.lock().unwrap() = ms_refresh_token.clone(); - + // 2. Xbox Live Auth let (xbl_token, uhs) = core::auth::method_xbox_live(&token_resp.access_token).await?; - + // 3. XSTS Auth let xsts_token = core::auth::method_xsts(&xbl_token).await?; - + // 4. Minecraft Auth let mc_token = core::auth::login_minecraft(&xsts_token, &uhs).await?; - + // 5. Get Profile let profile = core::auth::fetch_profile(&mc_token).await?; - + // 6. Create Account let account = core::auth::Account::Microsoft(core::auth::MicrosoftAccount { username: profile.name, @@ -691,18 +751,22 @@ async fn complete_microsoft_login( expires_at: (std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() - .as_secs() + token_resp.expires_in) as i64, + .as_secs() + + token_resp.expires_in) as i64, }); - + // 7. Save to state *state.active_account.lock().unwrap() = Some(account.clone()); - + // 8. Save to storage let app_handle = window.app_handle(); - let app_dir = app_handle.path().app_data_dir().map_err(|e| e.to_string())?; + let app_dir = app_handle + .path() + .app_data_dir() + .map_err(|e| e.to_string())?; let storage = core::account_storage::AccountStorage::new(app_dir); storage.add_or_update_account(&account, ms_refresh_token)?; - + Ok(account) } @@ -715,26 +779,29 @@ async fn refresh_account( ) -> Result<core::auth::Account, String> { // Get stored MS refresh token let app_handle = window.app_handle(); - let app_dir = app_handle.path().app_data_dir().map_err(|e| e.to_string())?; + let app_dir = app_handle + .path() + .app_data_dir() + .map_err(|e| e.to_string())?; let storage = core::account_storage::AccountStorage::new(app_dir.clone()); - + let (stored_account, ms_refresh) = storage .get_active_account() .ok_or("No active account found")?; - + let ms_refresh_token = ms_refresh.ok_or("No refresh token available")?; - + // Perform full refresh let (new_account, new_ms_refresh) = core::auth::refresh_full_auth(&ms_refresh_token).await?; let account = core::auth::Account::Microsoft(new_account); - + // Update state *state.active_account.lock().unwrap() = Some(account.clone()); *ms_refresh_state.token.lock().unwrap() = Some(new_ms_refresh.clone()); - + // Update storage storage.add_or_update_account(&account, Some(new_ms_refresh))?; - + Ok(account) } @@ -760,25 +827,25 @@ fn main() { .setup(|app| { let config_state = core::config::ConfigState::new(app.handle()); app.manage(config_state); - + // Load saved account on startup let app_dir = app.path().app_data_dir().unwrap(); let storage = core::account_storage::AccountStorage::new(app_dir); - + if let Some((stored_account, ms_refresh)) = storage.get_active_account() { let account = stored_account.to_account(); let auth_state: State<core::auth::AccountState> = app.state(); *auth_state.active_account.lock().unwrap() = Some(account); - + // Store MS refresh token if let Some(token) = ms_refresh { let ms_state: State<MsRefreshTokenState> = app.state(); *ms_state.token.lock().unwrap() = Some(token); } - + println!("[Startup] Loaded saved account"); } - + Ok(()) }) .invoke_handler(tauri::generate_handler![ |