diff options
| author | 2026-01-18 12:24:29 +0800 | |
|---|---|---|
| committer | 2026-01-18 12:24:29 +0800 | |
| commit | fd00ac6878b2cee9337b9e92d0c990ecdce9a346 (patch) | |
| tree | bb5540f763dc0061877c9d9ac53747d79193eecc /src-tauri/src/main.rs | |
| parent | ad36e0ce82770f9b3509ddb1cf96bc3422969806 (diff) | |
| parent | 6d82ab2275130f3bafdb7ec664297eb700321526 (diff) | |
| download | DropOut-fd00ac6878b2cee9337b9e92d0c990ecdce9a346.tar.gz DropOut-fd00ac6878b2cee9337b9e92d0c990ecdce9a346.zip | |
Merge pull request #58 from HsiangNianian/main
Diffstat (limited to 'src-tauri/src/main.rs')
| -rw-r--r-- | src-tauri/src/main.rs | 765 |
1 files changed, 671 insertions, 94 deletions
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 3671166..2871b03 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,12 +1,12 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +use serde::Serialize; use std::process::Stdio; 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 +use tokio::process::Command; // Added Serialize #[cfg(target_os = "windows")] use std::os::windows::process::CommandExt; @@ -67,11 +67,17 @@ async fn start_game( window: Window, auth_state: State<'_, core::auth::AccountState>, config_state: State<'_, core::config::ConfigState>, + assistant_state: State<'_, core::assistant::AssistantState>, + instance_state: State<'_, core::instance::InstanceState>, + instance_id: String, version_id: String, ) -> Result<String, String> { emit_log!( window, - format!("Starting game launch for version: {}", version_id) + format!( + "Starting game launch for version: {} in instance: {}", + version_id, instance_id + ) ); // Check for active account @@ -83,14 +89,7 @@ async fn start_game( .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())); let config = config_state.config.lock().unwrap().clone(); emit_log!(window, format!("Java path: {}", config.java_path)); @@ -99,14 +98,10 @@ async fn start_game( 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. - // If not accessible, use a specific logic. - 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))?; + // Get game directory from instance + let game_dir = instance_state + .get_instance_game_dir(&instance_id) + .ok_or_else(|| format!("Instance {} not found", instance_id))?; // Ensure game directory exists tokio::fs::create_dir_all(&game_dir) @@ -123,10 +118,11 @@ async fn start_game( // First, load the local version to get the original inheritsFrom value // (before merge clears it) - let original_inherits_from = match core::manifest::load_local_version(&game_dir, &version_id).await { - Ok(local_version) => local_version.inherits_from.clone(), - Err(_) => None, - }; + let original_inherits_from = + match core::manifest::load_local_version(&game_dir, &version_id).await { + Ok(local_version) => local_version.inherits_from.clone(), + Err(_) => None, + }; let version_details = core::manifest::load_version(&game_dir, &version_id) .await @@ -142,8 +138,123 @@ async fn start_game( // Determine the actual minecraft version for client.jar // (for modded versions, this is the parent vanilla version) - let minecraft_version = original_inherits_from - .unwrap_or_else(|| version_id.clone()); + let minecraft_version = original_inherits_from.unwrap_or_else(|| version_id.clone()); + + // Get required Java version from version file's javaVersion field + // The version file (after merging with parent) should contain the correct javaVersion + let required_java_major = version_details + .java_version + .as_ref() + .map(|jv| jv.major_version); + + // For older Minecraft versions (1.13.x and below), if javaVersion specifies Java 8, + // we should only allow Java 8 (not higher) due to compatibility issues with old Forge + // For newer versions, javaVersion.majorVersion is the minimum required version + let max_java_major = if let Some(required) = required_java_major { + // If version file specifies Java 8, enforce it as maximum (old versions need exactly Java 8) + // For Java 9+, allow that version or higher + if required <= 8 { + Some(8) + } else { + None // No upper bound for Java 9+ + } + } else { + // If version file doesn't specify javaVersion, this shouldn't happen for modern versions + // But if it does, we can't determine compatibility - log a warning + emit_log!( + window, + "Warning: Version file does not specify javaVersion. Using system default Java." + .to_string() + ); + None + }; + + // Check if configured Java is compatible + let app_handle = window.app_handle(); + let mut java_path_to_use = config.java_path.clone(); + if !java_path_to_use.is_empty() && java_path_to_use != "java" { + let is_compatible = + core::java::is_java_compatible(&java_path_to_use, required_java_major, max_java_major); + + if !is_compatible { + emit_log!( + window, + format!( + "Configured Java version may not be compatible. Looking for compatible Java..." + ) + ); + + // Try to find a compatible Java version + if let Some(compatible_java) = + core::java::get_compatible_java(app_handle, required_java_major, max_java_major) + { + emit_log!( + window, + format!( + "Found compatible Java {} at: {}", + compatible_java.version, compatible_java.path + ) + ); + java_path_to_use = compatible_java.path; + } else { + let version_constraint = if let Some(max) = max_java_major { + if let Some(min) = required_java_major { + if min == max as u64 { + format!("Java {}", min) + } else { + format!("Java {} to {}", min, max) + } + } else { + format!("Java {} (or lower)", max) + } + } else if let Some(min) = required_java_major { + format!("Java {} or higher", min) + } else { + "any Java version".to_string() + }; + + return Err(format!( + "No compatible Java installation found. This version requires {}. Please install a compatible Java version in settings.", + version_constraint + )); + } + } + } else { + // No Java configured, try to find a compatible one + if let Some(compatible_java) = + core::java::get_compatible_java(app_handle, required_java_major, max_java_major) + { + emit_log!( + window, + format!( + "Using Java {} at: {}", + compatible_java.version, compatible_java.path + ) + ); + java_path_to_use = compatible_java.path; + } else { + let version_constraint = if let Some(max) = max_java_major { + if let Some(min) = required_java_major { + if min == max as u64 { + format!("Java {}", min) + } else { + format!("Java {} to {}", min, max) + } + } else { + format!("Java {} (or lower)", max) + } + } else if let Some(min) = required_java_major { + format!("Java {} or higher", min) + } else { + "any Java version".to_string() + }; + + return Err(format!( + "No compatible Java installation found. This version requires {}. Please install a compatible Java version in settings.", + version_constraint + )); + } + } // 2. Prepare download tasks emit_log!(window, "Preparing download tasks...".to_string()); @@ -527,17 +638,67 @@ async fn start_game( 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])); - } + + // Format Java command with sensitive information masked + let masked_args: Vec<String> = args + .iter() + .enumerate() + .map(|(i, arg)| { + // Check if previous argument was a sensitive flag + if i > 0 { + let prev_arg = &args[i - 1]; + if prev_arg == "--accessToken" || prev_arg == "--uuid" { + return "***".to_string(); + } + } + + // Mask sensitive argument values + if arg == "--accessToken" || arg == "--uuid" { + arg.clone() + } else if arg.starts_with("token:") { + // Mask token: prefix tokens (Session ID format) + "token:***".to_string() + } else if arg.len() > 100 + && arg.contains('.') + && !arg.contains('/') + && !arg.contains('\\') + && !arg.contains(':') + { + // Likely a JWT token (very long string with dots, no paths) + "***".to_string() + } else if arg.len() == 36 + && arg.contains('-') + && arg.chars().all(|c| c.is_ascii_hexdigit() || c == '-') + { + // Likely a UUID (36 chars with dashes) + "***".to_string() + } else { + arg.clone() + } + }) + .collect(); + + // Format as actual Java command (properly quote arguments with spaces) + let masked_args_str: Vec<String> = masked_args + .iter() + .map(|arg| { + if arg.contains(' ') { + format!("\"{}\"", arg) + } else { + arg.clone() + } + }) + .collect(); + + let java_command = format!("{} {}", java_path_to_use, masked_args_str.join(" ")); + emit_log!(window, format!("Java Command: {}", java_command)); // Spawn the process emit_log!( window, - format!("Starting Java process: {}", config.java_path) + format!("Starting Java process: {}", java_path_to_use) ); - let mut command = Command::new(&config.java_path); + let mut command = Command::new(&java_path_to_use); command.args(&args); command.current_dir(&game_dir); // Run in game directory command.stdout(Stdio::piped()); @@ -557,7 +718,7 @@ async fn start_game( // Spawn and handle output let mut child = command .spawn() - .map_err(|e| format!("Failed to launch java: {}", e))?; + .map_err(|e| format!("Failed to launch Java at '{}': {}\nPlease check your Java installation and path configuration in Settings.", java_path_to_use, e))?; emit_log!(window, "Java process started successfully".to_string()); @@ -577,9 +738,11 @@ async fn start_game( ); let window_rx = window.clone(); + let assistant_arc = assistant_state.assistant.clone(); tokio::spawn(async move { let mut reader = BufReader::new(stdout).lines(); while let Ok(Some(line)) = reader.next_line().await { + assistant_arc.lock().unwrap().add_log(line.clone()); let _ = window_rx.emit("game-stdout", line); } // Emit log when stdout stream ends (game closing) @@ -587,10 +750,12 @@ async fn start_game( }); let window_rx_err = window.clone(); + let assistant_arc_err = assistant_state.assistant.clone(); let window_exit = window.clone(); tokio::spawn(async move { let mut reader = BufReader::new(stderr).lines(); while let Ok(Some(line)) = reader.next_line().await { + assistant_arc_err.lock().unwrap().add_log(line.clone()); let _ = window_rx_err.emit("game-stderr", line); } // Emit log when stderr stream ends @@ -685,29 +850,73 @@ fn parse_jvm_arguments( } #[tauri::command] -async fn get_versions() -> Result<Vec<core::manifest::Version>, String> { +async fn get_versions(window: Window) -> Result<Vec<core::manifest::Version>, 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))?; + match core::manifest::fetch_version_manifest().await { - Ok(manifest) => Ok(manifest.versions), + Ok(manifest) => { + let mut versions = manifest.versions; + + // For each version, try to load Java version info and check installation status + for version in &mut versions { + // Check if version is installed + let version_dir = game_dir.join("versions").join(&version.id); + let json_path = version_dir.join(format!("{}.json", version.id)); + let client_jar_path = version_dir.join(format!("{}.jar", version.id)); + + // Version is installed if both JSON and client jar exist + let is_installed = json_path.exists() && client_jar_path.exists(); + version.is_installed = Some(is_installed); + + // If installed, try to load the version JSON to get javaVersion + if is_installed { + if let Ok(game_version) = + core::manifest::load_local_version(&game_dir, &version.id).await + { + if let Some(java_ver) = game_version.java_version { + version.java_version = Some(java_ver.major_version); + } + } + } + } + + Ok(versions) + } Err(e) => Err(e.to_string()), } } /// Check if a version is installed (has client.jar) #[tauri::command] -async fn check_version_installed(window: Window, version_id: 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))?; +async fn check_version_installed( + _window: Window, + instance_state: State<'_, core::instance::InstanceState>, + instance_id: String, + version_id: String, +) -> Result<bool, String> { + let game_dir = instance_state + .get_instance_game_dir(&instance_id) + .ok_or_else(|| format!("Instance {} not found", instance_id))?; // For modded versions, check the parent vanilla version let minecraft_version = if version_id.starts_with("fabric-loader-") { // Format: fabric-loader-X.X.X-1.20.4 - version_id.split('-').last().unwrap_or(&version_id).to_string() + version_id + .split('-') + .next_back() + .unwrap_or(&version_id) + .to_string() } else if version_id.contains("-forge-") { // Format: 1.20.4-forge-49.0.38 - version_id.split("-forge-").next().unwrap_or(&version_id).to_string() + version_id + .split("-forge-") + .next() + .unwrap_or(&version_id) + .to_string() } else { version_id.clone() }; @@ -725,19 +934,24 @@ async fn check_version_installed(window: Window, version_id: String) -> Result<b async fn install_version( window: Window, config_state: State<'_, core::config::ConfigState>, + instance_state: State<'_, core::instance::InstanceState>, + instance_id: String, version_id: String, ) -> Result<(), String> { emit_log!( window, - format!("Starting installation for version: {}", version_id) + format!( + "Starting installation for version: {} in instance: {}", + version_id, instance_id + ) ); let config = config_state.config.lock().unwrap().clone(); - 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))?; + + // Get game directory from instance + let game_dir = instance_state + .get_instance_game_dir(&instance_id) + .ok_or_else(|| format!("Instance {} not found", instance_id))?; // Ensure game directory exists tokio::fs::create_dir_all(&game_dir) @@ -753,21 +967,24 @@ async fn install_version( ); // First, try to fetch the vanilla version from Mojang and save it locally - let version_details = match core::manifest::load_local_version(&game_dir, &version_id).await { + let _version_details = match core::manifest::load_local_version(&game_dir, &version_id).await { Ok(v) => v, Err(_) => { // Not found locally, fetch from Mojang - emit_log!(window, format!("Fetching version {} from Mojang...", version_id)); + emit_log!( + window, + format!("Fetching version {} from Mojang...", version_id) + ); let fetched = core::manifest::fetch_vanilla_version(&version_id) .await .map_err(|e| e.to_string())?; - + // Save the version JSON locally emit_log!(window, format!("Saving version JSON...")); core::manifest::save_local_version(&game_dir, &fetched) .await .map_err(|e| e.to_string())?; - + fetched } }; @@ -983,6 +1200,9 @@ async fn install_version( format!("Installation of {} completed successfully!", version_id) ); + // Emit event to notify frontend that version installation is complete + let _ = window.emit("version-installed", &version_id); + Ok(()) } @@ -1060,6 +1280,38 @@ async fn save_settings( } #[tauri::command] +async fn get_config_path(state: State<'_, core::config::ConfigState>) -> Result<String, String> { + Ok(state.file_path.to_string_lossy().to_string()) +} + +#[tauri::command] +async fn read_raw_config(state: State<'_, core::config::ConfigState>) -> Result<String, String> { + tokio::fs::read_to_string(&state.file_path) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn save_raw_config( + state: State<'_, core::config::ConfigState>, + content: String, +) -> Result<(), String> { + // Validate JSON + let new_config: core::config::LauncherConfig = + serde_json::from_str(&content).map_err(|e| format!("Invalid JSON: {}", e))?; + + // Save to file + tokio::fs::write(&state.file_path, &content) + .await + .map_err(|e| e.to_string())?; + + // Update in-memory state + *state.config.lock().unwrap() = new_config; + + Ok(()) +} + +#[tauri::command] async fn start_microsoft_login() -> Result<core::auth::DeviceCodeResponse, String> { core::auth::start_device_flow().await } @@ -1170,7 +1422,9 @@ async fn refresh_account( /// Detect Java installations on the system #[tauri::command] -async fn detect_java(app_handle: tauri::AppHandle) -> Result<Vec<core::java::JavaInstallation>, String> { +async fn detect_java( + app_handle: tauri::AppHandle, +) -> Result<Vec<core::java::JavaInstallation>, String> { Ok(core::java::detect_all_java_installations(&app_handle)) } @@ -1286,22 +1540,22 @@ async fn get_fabric_loaders_for_version( #[tauri::command] async fn install_fabric( window: Window, + instance_state: State<'_, core::instance::InstanceState>, + instance_id: String, game_version: String, loader_version: String, ) -> Result<core::fabric::InstalledFabricVersion, String> { emit_log!( window, format!( - "Installing Fabric {} for Minecraft {}...", - loader_version, game_version + "Installing Fabric {} for Minecraft {} in instance {}...", + loader_version, game_version, instance_id ) ); - 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 game_dir = instance_state + .get_instance_game_dir(&instance_id) + .ok_or_else(|| format!("Instance {} not found", instance_id))?; let result = core::fabric::install_fabric(&game_dir, &game_version, &loader_version) .await @@ -1312,23 +1566,172 @@ async fn install_fabric( format!("Fabric installed successfully: {}", result.id) ); + // Emit event to notify frontend + let _ = window.emit("fabric-installed", &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))?; +async fn list_installed_fabric_versions( + _window: Window, + instance_state: State<'_, core::instance::InstanceState>, + instance_id: String, +) -> Result<Vec<String>, String> { + let game_dir = instance_state + .get_instance_game_dir(&instance_id) + .ok_or_else(|| format!("Instance {} not found", instance_id))?; core::fabric::list_installed_fabric_versions(&game_dir) .await .map_err(|e| e.to_string()) } +/// Get Java version requirement for a specific version +#[tauri::command] +async fn get_version_java_version( + _window: Window, + instance_state: State<'_, core::instance::InstanceState>, + instance_id: String, + version_id: String, +) -> Result<Option<u64>, String> { + let game_dir = instance_state + .get_instance_game_dir(&instance_id) + .ok_or_else(|| format!("Instance {} not found", instance_id))?; + + // Try to load the version JSON to get javaVersion + match core::manifest::load_version(&game_dir, &version_id).await { + Ok(game_version) => Ok(game_version.java_version.map(|jv| jv.major_version)), + Err(_) => Ok(None), // Version not found or can't be loaded + } +} + +/// Version metadata for display in the UI +#[derive(serde::Serialize)] +struct VersionMetadata { + id: String, + #[serde(rename = "javaVersion")] + java_version: Option<u64>, + #[serde(rename = "isInstalled")] + is_installed: bool, +} + +/// Delete a version (remove version directory) +#[tauri::command] +async fn delete_version( + window: Window, + instance_state: State<'_, core::instance::InstanceState>, + instance_id: String, + version_id: String, +) -> Result<(), String> { + let game_dir = instance_state + .get_instance_game_dir(&instance_id) + .ok_or_else(|| format!("Instance {} not found", instance_id))?; + + let version_dir = game_dir.join("versions").join(&version_id); + + if !version_dir.exists() { + return Err(format!("Version {} not found", version_id)); + } + + // Remove the entire version directory + tokio::fs::remove_dir_all(&version_dir) + .await + .map_err(|e| format!("Failed to delete version: {}", e))?; + + // Emit event to notify frontend + let _ = window.emit("version-deleted", &version_id); + + Ok(()) +} + +/// Get detailed metadata for a specific version +#[tauri::command] +async fn get_version_metadata( + _window: Window, + instance_state: State<'_, core::instance::InstanceState>, + instance_id: String, + version_id: String, +) -> Result<VersionMetadata, String> { + let game_dir = instance_state + .get_instance_game_dir(&instance_id) + .ok_or_else(|| format!("Instance {} not found", instance_id))?; + + // Initialize metadata + let mut metadata = VersionMetadata { + id: version_id.clone(), + java_version: None, + is_installed: false, + }; + + // Check if version is in manifest and get Java version if available + if let Ok(manifest) = core::manifest::fetch_version_manifest().await { + if let Some(version_entry) = manifest.versions.iter().find(|v| v.id == version_id) { + // Note: version_entry.java_version is only set if version is installed locally + // For uninstalled versions, we'll fetch from remote below + if let Some(java_ver) = version_entry.java_version { + metadata.java_version = Some(java_ver); + } + } + } + + // Check if version is installed (both JSON and client jar must exist) + let version_dir = game_dir.join("versions").join(&version_id); + let json_path = version_dir.join(format!("{}.json", version_id)); + + // For modded versions, check the parent vanilla version's client jar + let client_jar_path = if version_id.starts_with("fabric-loader-") { + // Format: fabric-loader-X.X.X-1.20.4 + let minecraft_version = version_id + .split('-') + .next_back() + .unwrap_or(&version_id) + .to_string(); + game_dir + .join("versions") + .join(&minecraft_version) + .join(format!("{}.jar", minecraft_version)) + } else if version_id.contains("-forge-") { + // Format: 1.20.4-forge-49.0.38 + let minecraft_version = version_id + .split("-forge-") + .next() + .unwrap_or(&version_id) + .to_string(); + game_dir + .join("versions") + .join(&minecraft_version) + .join(format!("{}.jar", minecraft_version)) + } else { + version_dir.join(format!("{}.jar", version_id)) + }; + + metadata.is_installed = json_path.exists() && client_jar_path.exists(); + + // Try to get Java version - from local if installed, or from remote if not + if metadata.is_installed { + // If installed, load from local version JSON + if let Ok(game_version) = core::manifest::load_version(&game_dir, &version_id).await { + if let Some(java_ver) = game_version.java_version { + metadata.java_version = Some(java_ver.major_version); + } + } + } else if metadata.java_version.is_none() { + // If not installed and we don't have Java version yet, try to fetch from remote + // This is for vanilla versions that are not installed + if !version_id.starts_with("fabric-loader-") && !version_id.contains("-forge-") { + if let Ok(game_version) = core::manifest::fetch_vanilla_version(&version_id).await { + if let Some(java_ver) = game_version.java_version { + metadata.java_version = Some(java_ver.major_version); + } + } + } + } + + Ok(metadata) +} + /// Installed version info #[derive(serde::Serialize)] struct InstalledVersion { @@ -1340,12 +1743,14 @@ struct InstalledVersion { /// List all installed versions from the data directory /// Simply lists all folders in the versions directory without validation #[tauri::command] -async fn list_installed_versions(window: Window) -> Result<Vec<InstalledVersion>, 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))?; +async fn list_installed_versions( + _window: Window, + instance_state: State<'_, core::instance::InstanceState>, + instance_id: String, +) -> Result<Vec<InstalledVersion>, String> { + let game_dir = instance_state + .get_instance_game_dir(&instance_id) + .ok_or_else(|| format!("Instance {} not found", instance_id))?; let versions_dir = game_dir.join("versions"); let mut installed = Vec::new(); @@ -1425,15 +1830,15 @@ async fn list_installed_versions(window: Window) -> Result<Vec<InstalledVersion> /// Check if Fabric is installed for a specific version #[tauri::command] async fn is_fabric_installed( - window: Window, + _window: Window, + instance_state: State<'_, core::instance::InstanceState>, + instance_id: String, 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))?; + let game_dir = instance_state + .get_instance_game_dir(&instance_id) + .ok_or_else(|| format!("Instance {} not found", instance_id))?; Ok(core::fabric::is_fabric_installed( &game_dir, @@ -1465,37 +1870,40 @@ async fn get_forge_versions_for_game( async fn install_forge( window: Window, config_state: State<'_, core::config::ConfigState>, + instance_state: State<'_, core::instance::InstanceState>, + instance_id: String, game_version: String, forge_version: String, ) -> Result<core::forge::InstalledForgeVersion, String> { emit_log!( window, format!( - "Installing Forge {} for Minecraft {}...", - forge_version, game_version + "Installing Forge {} for Minecraft {} in instance {}...", + forge_version, game_version, instance_id ) ); - 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 game_dir = instance_state + .get_instance_game_dir(&instance_id) + .ok_or_else(|| format!("Instance {} not found", instance_id))?; // Get Java path from config or detect let config = config_state.config.lock().unwrap().clone(); + let app_handle = window.app_handle(); let java_path_str = if !config.java_path.is_empty() && config.java_path != "java" { config.java_path.clone() } else { // Try to find a suitable Java installation - let javas = core::java::detect_all_java_installations(&app_handle); + let javas = core::java::detect_all_java_installations(app_handle); if let Some(java) = javas.first() { java.path.clone() } else { - return Err("No Java installation found. Please configure Java in settings.".to_string()); + return Err( + "No Java installation found. Please configure Java in settings.".to_string(), + ); } }; - let java_path = std::path::PathBuf::from(&java_path_str); + let java_path = utils::path::normalize_java_path(&java_path_str)?; emit_log!(window, "Running Forge installer...".to_string()); @@ -1504,7 +1912,10 @@ async fn install_forge( .await .map_err(|e| format!("Forge installer failed: {}", e))?; - emit_log!(window, "Forge installer completed, creating version profile...".to_string()); + emit_log!( + window, + "Forge installer completed, creating version profile...".to_string() + ); // Now create the version JSON let result = core::forge::install_forge(&game_dir, &game_version, &forge_version) @@ -1516,6 +1927,9 @@ async fn install_forge( format!("Forge installed successfully: {}", result.id) ); + // Emit event to notify frontend + let _ = window.emit("forge-installed", &result.id); + Ok(result) } @@ -1551,7 +1965,7 @@ async fn get_github_releases() -> Result<Vec<GithubRelease>, String> { r["name"].as_str(), r["published_at"].as_str(), r["body"].as_str(), - r["html_url"].as_str() + r["html_url"].as_str(), ) { result.push(GithubRelease { tag_name: tag.to_string(), @@ -1593,8 +2007,7 @@ async fn upload_to_pastebin( match service.as_str() { "pastebin.com" => { - let api_key = api_key - .ok_or("Pastebin API Key not configured in settings")?; + 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") @@ -1640,6 +2053,139 @@ async fn upload_to_pastebin( } } +#[tauri::command] +async fn assistant_check_health( + assistant_state: State<'_, core::assistant::AssistantState>, + config_state: State<'_, core::config::ConfigState>, +) -> Result<bool, String> { + let assistant = assistant_state.assistant.lock().unwrap().clone(); + let config = config_state.config.lock().unwrap().clone(); + Ok(assistant.check_health(&config.assistant).await) +} + +#[tauri::command] +async fn assistant_chat( + assistant_state: State<'_, core::assistant::AssistantState>, + config_state: State<'_, core::config::ConfigState>, + messages: Vec<core::assistant::Message>, +) -> Result<core::assistant::Message, String> { + let assistant = assistant_state.assistant.lock().unwrap().clone(); + let config = config_state.config.lock().unwrap().clone(); + assistant.chat(messages, &config.assistant).await +} + +#[tauri::command] +async fn list_ollama_models( + assistant_state: State<'_, core::assistant::AssistantState>, + endpoint: String, +) -> Result<Vec<core::assistant::ModelInfo>, String> { + let assistant = assistant_state.assistant.lock().unwrap().clone(); + assistant.list_ollama_models(&endpoint).await +} + +#[tauri::command] +async fn list_openai_models( + assistant_state: State<'_, core::assistant::AssistantState>, + config_state: State<'_, core::config::ConfigState>, +) -> Result<Vec<core::assistant::ModelInfo>, String> { + let assistant = assistant_state.assistant.lock().unwrap().clone(); + let config = config_state.config.lock().unwrap().clone(); + assistant.list_openai_models(&config.assistant).await +} + +// ==================== Instance Management Commands ==================== + +/// Create a new instance +#[tauri::command] +async fn create_instance( + window: Window, + state: State<'_, core::instance::InstanceState>, + name: String, +) -> Result<core::instance::Instance, String> { + let app_handle = window.app_handle(); + state.create_instance(name, app_handle) +} + +/// Delete an instance +#[tauri::command] +async fn delete_instance( + state: State<'_, core::instance::InstanceState>, + instance_id: String, +) -> Result<(), String> { + state.delete_instance(&instance_id) +} + +/// Update an instance +#[tauri::command] +async fn update_instance( + state: State<'_, core::instance::InstanceState>, + instance: core::instance::Instance, +) -> Result<(), String> { + state.update_instance(instance) +} + +/// Get all instances +#[tauri::command] +async fn list_instances( + state: State<'_, core::instance::InstanceState>, +) -> Result<Vec<core::instance::Instance>, String> { + Ok(state.list_instances()) +} + +/// Get a single instance by ID +#[tauri::command] +async fn get_instance( + state: State<'_, core::instance::InstanceState>, + instance_id: String, +) -> Result<core::instance::Instance, String> { + state + .get_instance(&instance_id) + .ok_or_else(|| format!("Instance {} not found", instance_id)) +} + +/// Set the active instance +#[tauri::command] +async fn set_active_instance( + state: State<'_, core::instance::InstanceState>, + instance_id: String, +) -> Result<(), String> { + state.set_active_instance(&instance_id) +} + +/// Get the active instance +#[tauri::command] +async fn get_active_instance( + state: State<'_, core::instance::InstanceState>, +) -> Result<Option<core::instance::Instance>, String> { + Ok(state.get_active_instance()) +} + +/// Duplicate an instance +#[tauri::command] +async fn duplicate_instance( + window: Window, + state: State<'_, core::instance::InstanceState>, + instance_id: String, + new_name: String, +) -> Result<core::instance::Instance, String> { + let app_handle = window.app_handle(); + state.duplicate_instance(&instance_id, new_name, app_handle) +} + +#[tauri::command] +async fn assistant_chat_stream( + window: tauri::Window, + assistant_state: State<'_, core::assistant::AssistantState>, + config_state: State<'_, core::config::ConfigState>, + messages: Vec<core::assistant::Message>, +) -> Result<String, String> { + let assistant = assistant_state.assistant.lock().unwrap().clone(); + let config = config_state.config.lock().unwrap().clone(); + assistant + .chat_stream(messages, &config.assistant, &window) + .await +} + fn main() { tauri::Builder::default() .plugin(tauri_plugin_fs::init()) @@ -1647,10 +2193,21 @@ fn main() { .plugin(tauri_plugin_shell::init()) .manage(core::auth::AccountState::new()) .manage(MsRefreshTokenState::new()) + .manage(core::assistant::AssistantState::new()) .setup(|app| { let config_state = core::config::ConfigState::new(app.handle()); app.manage(config_state); + // Initialize instance state + let instance_state = core::instance::InstanceState::new(app.handle()); + + // Migrate legacy data if needed + if let Err(e) = core::instance::migrate_legacy_data(app.handle(), &instance_state) { + eprintln!("[Startup] Warning: Failed to migrate legacy data: {}", e); + } + + app.manage(instance_state); + // Load saved account on startup let app_dir = app.path().app_data_dir().unwrap(); let storage = core::account_storage::AccountStorage::new(app_dir); @@ -1670,7 +2227,7 @@ fn main() { } // Check for pending Java downloads and notify frontend - let pending = core::java::get_pending_downloads(&app.app_handle()); + 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()); @@ -1684,11 +2241,17 @@ fn main() { check_version_installed, install_version, list_installed_versions, + get_version_java_version, + get_version_metadata, + delete_version, login_offline, get_active_account, logout, get_settings, save_settings, + get_config_path, + read_raw_config, + save_raw_config, start_microsoft_login, complete_microsoft_login, refresh_account, @@ -1715,7 +2278,21 @@ fn main() { get_forge_versions_for_game, install_forge, get_github_releases, - upload_to_pastebin + upload_to_pastebin, + assistant_check_health, + assistant_chat, + assistant_chat_stream, + list_ollama_models, + list_openai_models, + // Instance management commands + create_instance, + delete_instance, + update_instance, + list_instances, + get_instance, + set_active_instance, + get_active_instance, + duplicate_instance ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); |