From 73ddf24b04bf94ee7fa76974e1af55eb94112b93 Mon Sep 17 00:00:00 2001 From: HsiangNianian Date: Fri, 16 Jan 2026 14:18:04 +0800 Subject: feat: integrate AI assistant functionality and configuration management Implemented new commands for managing the AI assistant, including health checks, chat interactions, and model listings for both Ollama and OpenAI. Enhanced the configuration system to support raw JSON editing and added a dedicated AssistantConfig structure for better management of assistant settings. This update significantly improves the user experience by providing comprehensive control over AI interactions and configurations. --- src-tauri/src/core/java.rs | 53 +++++++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 20 deletions(-) (limited to 'src-tauri/src/core/java.rs') diff --git a/src-tauri/src/core/java.rs b/src-tauri/src/core/java.rs index 8341138..ac52da6 100644 --- a/src-tauri/src/core/java.rs +++ b/src-tauri/src/core/java.rs @@ -5,7 +5,7 @@ use tauri::AppHandle; use tauri::Emitter; use tauri::Manager; -use crate::core::downloader::{self, JavaDownloadProgress, DownloadQueue, PendingJavaDownload}; +use crate::core::downloader::{self, DownloadQueue, JavaDownloadProgress, PendingJavaDownload}; use crate::utils::zip; const ADOPTIUM_API_BASE: &str = "https://api.adoptium.net/v3"; @@ -58,7 +58,7 @@ pub struct JavaReleaseInfo { } /// Java catalog containing all available versions -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct JavaCatalog { pub releases: Vec, pub available_major_versions: Vec, @@ -66,17 +66,6 @@ pub struct JavaCatalog { pub cached_at: u64, } -impl Default for JavaCatalog { - fn default() -> Self { - Self { - releases: Vec::new(), - available_major_versions: Vec::new(), - lts_versions: Vec::new(), - cached_at: 0, - } - } -} - /// Adoptium `/v3/assets/latest/{version}/hotspot` API response structures #[derive(Debug, Clone, Deserialize)] pub struct AdoptiumAsset { @@ -86,6 +75,7 @@ pub struct AdoptiumAsset { } #[derive(Debug, Clone, Deserialize)] +#[allow(dead_code)] pub struct AdoptiumBinary { pub os: String, pub architecture: String, @@ -104,6 +94,7 @@ pub struct AdoptiumPackage { } #[derive(Debug, Clone, Deserialize)] +#[allow(dead_code)] pub struct AdoptiumVersionData { pub major: u32, pub minor: u32, @@ -114,6 +105,7 @@ pub struct AdoptiumVersionData { /// Adoptium available releases response #[derive(Debug, Clone, Deserialize)] +#[allow(dead_code)] pub struct AvailableReleases { pub available_releases: Vec, pub available_lts_releases: Vec, @@ -231,6 +223,7 @@ pub fn save_catalog_cache(app_handle: &AppHandle, catalog: &JavaCatalog) -> Resu } /// Clear Java catalog cache +#[allow(dead_code)] pub fn clear_catalog_cache(app_handle: &AppHandle) -> Result<(), String> { let cache_path = get_catalog_cache_path(app_handle); if cache_path.exists() { @@ -240,7 +233,10 @@ pub fn clear_catalog_cache(app_handle: &AppHandle) -> Result<(), String> { } /// Fetch complete Java catalog from Adoptium API with platform availability check -pub async fn fetch_java_catalog(app_handle: &AppHandle, force_refresh: bool) -> Result { +pub async fn fetch_java_catalog( + app_handle: &AppHandle, + force_refresh: bool, +) -> Result { // Check cache first unless force refresh if !force_refresh { if let Some(cached) = load_cached_catalog(app_handle) { @@ -294,7 +290,9 @@ pub async fn fetch_java_catalog(app_handle: &AppHandle, force_refresh: bool) -> file_size: asset.binary.package.size, checksum: asset.binary.package.checksum, download_url: asset.binary.package.link, - is_lts: available.available_lts_releases.contains(major_version), + is_lts: available + .available_lts_releases + .contains(major_version), is_available: true, architecture: asset.binary.architecture.clone(), }); @@ -547,7 +545,11 @@ pub async fn download_and_install_java( // Linux/Windows: jdk-xxx/bin/java let java_home = version_dir.join(&top_level_dir); let java_bin = if cfg!(target_os = "macos") { - java_home.join("Contents").join("Home").join("bin").join("java") + java_home + .join("Contents") + .join("Home") + .join("bin") + .join("java") } else if cfg!(windows) { java_home.join("bin").join("java.exe") } else { @@ -885,7 +887,7 @@ pub fn detect_all_java_installations(app_handle: &AppHandle) -> Vec Option { let bin_name = if cfg!(windows) { "java.exe" } else { "java" }; @@ -918,7 +920,11 @@ fn find_java_executable(dir: &PathBuf) -> Option { // macOS: nested/Contents/Home/bin/java #[cfg(target_os = "macos")] { - let macos_nested = path.join("Contents").join("Home").join("bin").join(bin_name); + let macos_nested = path + .join("Contents") + .join("Home") + .join("bin") + .join(bin_name); if macos_nested.exists() { return Some(macos_nested); } @@ -931,7 +937,9 @@ fn find_java_executable(dir: &PathBuf) -> Option { } /// Resume pending Java downloads from queue -pub async fn resume_pending_downloads(app_handle: &AppHandle) -> Result, String> { +pub async fn resume_pending_downloads( + app_handle: &AppHandle, +) -> Result, String> { let queue = DownloadQueue::load(app_handle); let mut installed = Vec::new(); @@ -978,7 +986,12 @@ pub fn get_pending_downloads(app_handle: &AppHandle) -> Vec } /// Clear a specific pending download -pub fn clear_pending_download(app_handle: &AppHandle, major_version: u32, image_type: &str) -> Result<(), String> { +#[allow(dead_code)] +pub fn clear_pending_download( + app_handle: &AppHandle, + major_version: u32, + image_type: &str, +) -> Result<(), String> { let mut queue = DownloadQueue::load(app_handle); queue.remove(major_version, image_type); queue.save(app_handle) -- cgit v1.2.3-70-g09d2 From 9c366b8d5c3f68d19bb87ed354b42b48c99b66c5 Mon Sep 17 00:00:00 2001 From: HsiangNianian Date: Fri, 16 Jan 2026 14:40:02 +0800 Subject: feat: improve Java command execution on Windows Enhanced the Java command execution by adding support for Windows-specific creation flags in both the installer and candidate checking functions. This change ensures better compatibility and performance when running Java commands on Windows systems. --- src-tauri/src/core/forge.rs | 15 ++++++++++----- src-tauri/src/core/java.rs | 19 ++++++++++++++----- 2 files changed, 24 insertions(+), 10 deletions(-) (limited to 'src-tauri/src/core/java.rs') diff --git a/src-tauri/src/core/forge.rs b/src-tauri/src/core/forge.rs index c8bd6e4..0528b76 100644 --- a/src-tauri/src/core/forge.rs +++ b/src-tauri/src/core/forge.rs @@ -9,6 +9,8 @@ use serde::{Deserialize, Serialize}; use std::error::Error; +#[cfg(target_os = "windows")] +use std::os::windows::process::CommandExt; use std::path::PathBuf; const FORGE_PROMOTIONS_URL: &str = @@ -293,13 +295,16 @@ pub async fn run_forge_installer( // Run the installer in headless mode // The installer accepts --installClient to install to a specific directory - let output = tokio::process::Command::new(java_path) - .arg("-jar") + let mut cmd = tokio::process::Command::new(java_path); + cmd.arg("-jar") .arg(&installer_path) .arg("--installClient") - .arg(game_dir) - .output() - .await?; + .arg(game_dir); + + #[cfg(target_os = "windows")] + cmd.creation_flags(0x08000000); + + let output = cmd.output().await?; // Clean up installer let _ = tokio::fs::remove_file(&installer_path).await; diff --git a/src-tauri/src/core/java.rs b/src-tauri/src/core/java.rs index ac52da6..7aa43b8 100644 --- a/src-tauri/src/core/java.rs +++ b/src-tauri/src/core/java.rs @@ -1,4 +1,6 @@ use serde::{Deserialize, Serialize}; +#[cfg(target_os = "windows")] +use std::os::windows::process::CommandExt; use std::path::PathBuf; use std::process::Command; use tauri::AppHandle; @@ -636,10 +638,12 @@ fn get_java_candidates() -> Vec { let mut candidates = Vec::new(); // Check PATH first - if let Ok(output) = Command::new(if cfg!(windows) { "where" } else { "which" }) - .arg("java") - .output() - { + let mut cmd = Command::new(if cfg!(windows) { "where" } else { "which" }); + cmd.arg("java"); + #[cfg(target_os = "windows")] + cmd.creation_flags(0x08000000); + + if let Ok(output) = cmd.output() { if output.status.success() { let paths = String::from_utf8_lossy(&output.stdout); for line in paths.lines() { @@ -788,7 +792,12 @@ fn get_java_candidates() -> Vec { /// Check a specific Java installation and get its version info fn check_java_installation(path: &PathBuf) -> Option { - let output = Command::new(path).arg("-version").output().ok()?; + let mut cmd = Command::new(path); + cmd.arg("-version"); + #[cfg(target_os = "windows")] + cmd.creation_flags(0x08000000); + + let output = cmd.output().ok()?; // Java outputs version info to stderr let version_output = String::from_utf8_lossy(&output.stderr); -- cgit v1.2.3-70-g09d2 From 1e0905613a7b7b98daf6dfecffa87fc0096a8e55 Mon Sep 17 00:00:00 2001 From: HsiangNianian Date: Fri, 16 Jan 2026 14:49:05 +0800 Subject: feat: add UNC prefix stripping for Windows paths in Java handling Implemented a helper function to strip the UNC prefix from file paths on Windows, ensuring cleaner path handling. Updated the Java candidate retrieval process to resolve symlinks and apply the new prefix stripping function, enhancing compatibility and usability on Windows systems. --- src-tauri/src/core/java.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) (limited to 'src-tauri/src/core/java.rs') diff --git a/src-tauri/src/core/java.rs b/src-tauri/src/core/java.rs index 7aa43b8..3f7a1e4 100644 --- a/src-tauri/src/core/java.rs +++ b/src-tauri/src/core/java.rs @@ -13,6 +13,18 @@ use crate::utils::zip; const ADOPTIUM_API_BASE: &str = "https://api.adoptium.net/v3"; const CACHE_DURATION_SECS: u64 = 24 * 60 * 60; // 24 hours +/// Helper to strip UNC prefix on Windows (\\?\) +fn strip_unc_prefix(path: PathBuf) -> PathBuf { + #[cfg(target_os = "windows")] + { + let s = path.to_string_lossy().to_string(); + if s.starts_with(r"\\?\") { + return PathBuf::from(&s[4..]); + } + } + path +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct JavaInstallation { pub path: String, @@ -649,7 +661,11 @@ fn get_java_candidates() -> Vec { for line in paths.lines() { let path = PathBuf::from(line.trim()); if path.exists() { - candidates.push(path); + // Resolve symlinks (important for Windows javapath wrapper) + let resolved = std::fs::canonicalize(&path).unwrap_or(path); + // Strip UNC prefix if present to keep paths clean + let final_path = strip_unc_prefix(resolved); + candidates.push(final_path); } } } -- cgit v1.2.3-70-g09d2 From 4d6ed7b93ed57a2f397e4f8060b2b183b7c86774 Mon Sep 17 00:00:00 2001 From: HsiangNianian Date: Fri, 16 Jan 2026 14:49:50 +0800 Subject: feat: enhance Java path handling by resolving symlinks and stripping UNC prefixes Updated the Java installation and executable retrieval functions to resolve symlinks and strip UNC prefixes from paths. This improvement ensures cleaner and more reliable path handling on Windows systems, enhancing compatibility and usability. --- src-tauri/src/core/java.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'src-tauri/src/core/java.rs') diff --git a/src-tauri/src/core/java.rs b/src-tauri/src/core/java.rs index 3f7a1e4..1d57d21 100644 --- a/src-tauri/src/core/java.rs +++ b/src-tauri/src/core/java.rs @@ -577,6 +577,10 @@ pub async fn download_and_install_java( )); } + // Resolve symlinks and strip UNC prefix to ensure clean path + let java_bin = std::fs::canonicalize(&java_bin).map_err(|e| e.to_string())?; + let java_bin = strip_unc_prefix(java_bin); + // 9. Verify installation let installation = check_java_installation(&java_bin) .ok_or_else(|| "Failed to verify Java installation".to_string())?; @@ -919,7 +923,8 @@ fn find_java_executable(dir: &PathBuf) -> Option { // Directly look in the bin directory let direct_bin = dir.join("bin").join(bin_name); if direct_bin.exists() { - return Some(direct_bin); + let resolved = std::fs::canonicalize(&direct_bin).unwrap_or(direct_bin); + return Some(strip_unc_prefix(resolved)); } // macOS: Contents/Home/bin/java @@ -939,7 +944,8 @@ fn find_java_executable(dir: &PathBuf) -> Option { // Try direct bin path let nested_bin = path.join("bin").join(bin_name); if nested_bin.exists() { - return Some(nested_bin); + let resolved = std::fs::canonicalize(&nested_bin).unwrap_or(nested_bin); + return Some(strip_unc_prefix(resolved)); } // macOS: nested/Contents/Home/bin/java -- cgit v1.2.3-70-g09d2 From 1119f6c3cf421da2f2db92873efae8135c76b678 Mon Sep 17 00:00:00 2001 From: HsiangNianian Date: Fri, 16 Jan 2026 18:38:47 +0800 Subject: feat: enhance Java version management for Minecraft versions Added functionality to determine and validate the required Java version for Minecraft versions, including checks for compatibility with older versions. Implemented event emissions for version installation and deletion, and updated the UI to reflect Java version requirements and installation status. Improved version metadata handling and added support for deleting versions. --- src-tauri/Cargo.toml | 1 + src-tauri/src/core/forge.rs | 116 ++++++--- src-tauri/src/core/java.rs | 58 +++++ src-tauri/src/core/manifest.rs | 7 + src-tauri/src/main.rs | 383 +++++++++++++++++++++++++++-- ui/src/components/BottomBar.svelte | 56 ++--- ui/src/components/ModLoaderSelector.svelte | 38 +-- ui/src/components/VersionsView.svelte | 325 +++++++++++++++++++++--- ui/src/stores/game.svelte.ts | 6 +- ui/src/types/index.ts | 2 + 10 files changed, 854 insertions(+), 138 deletions(-) (limited to 'src-tauri/src/core/java.rs') diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 9fe91b7..407da5a 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -29,6 +29,7 @@ dirs = "5.0" serde_urlencoded = "0.7.1" tauri-plugin-dialog = "2.6.0" tauri-plugin-fs = "2.4.5" +bytes = "1.11.0" [build-dependencies] tauri-build = { version = "2.0", features = [] } diff --git a/src-tauri/src/core/forge.rs b/src-tauri/src/core/forge.rs index 0528b76..65bf413 100644 --- a/src-tauri/src/core/forge.rs +++ b/src-tauri/src/core/forge.rs @@ -16,6 +16,7 @@ use std::path::PathBuf; const FORGE_PROMOTIONS_URL: &str = "https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json"; const FORGE_MAVEN_URL: &str = "https://maven.minecraftforge.net/"; +const FORGE_FILES_URL: &str = "https://files.minecraftforge.net/"; /// Represents a Forge version entry. #[derive(Debug, Deserialize, Serialize, Clone)] @@ -180,27 +181,93 @@ pub fn generate_version_id(game_version: &str, forge_version: &str) -> String { format!("{}-forge-{}", game_version, forge_version) } -/// Fetch the Forge installer manifest to get the library list -async fn fetch_forge_installer_manifest( +/// Try to download the Forge installer from multiple possible URL formats. +/// This is necessary because older Forge versions use different URL patterns. +async fn try_download_forge_installer( game_version: &str, forge_version: &str, -) -> Result> { +) -> Result> { let forge_full = format!("{}-{}", game_version, forge_version); - - // Download the installer JAR to extract version.json - let installer_url = format!( - "{}net/minecraftforge/forge/{}/forge-{}-installer.jar", - FORGE_MAVEN_URL, forge_full, forge_full - ); - - println!("Fetching Forge installer from: {}", installer_url); - - let response = reqwest::get(&installer_url).await?; - if !response.status().is_success() { - return Err(format!("Failed to download Forge installer: {}", response.status()).into()); + // For older versions (like 1.7.10), the URL needs an additional -{game_version} suffix + let forge_full_with_suffix = format!("{}-{}", forge_full, game_version); + + // Try different URL formats for different Forge versions + // Order matters: try most common formats first, then fallback to alternatives + let url_patterns = vec![ + // Standard Maven format (for modern versions): forge/{game_version}-{forge_version}/forge-{game_version}-{forge_version}-installer.jar + format!( + "{}net/minecraftforge/forge/{}/forge-{}-installer.jar", + FORGE_MAVEN_URL, forge_full, forge_full + ), + // Old version format with suffix (for versions like 1.7.10): forge/{game_version}-{forge_version}-{game_version}/forge-{game_version}-{forge_version}-{game_version}-installer.jar + // This is the correct format for 1.7.10 and similar old versions + format!( + "{}net/minecraftforge/forge/{}/forge-{}-installer.jar", + FORGE_MAVEN_URL, forge_full_with_suffix, forge_full_with_suffix + ), + // Files.minecraftforge.net format with suffix (for old versions like 1.7.10) + format!( + "{}maven/net/minecraftforge/forge/{}/forge-{}-installer.jar", + FORGE_FILES_URL, forge_full_with_suffix, forge_full_with_suffix + ), + // Files.minecraftforge.net standard format (for older versions) + format!( + "{}maven/net/minecraftforge/forge/{}/forge-{}-installer.jar", + FORGE_FILES_URL, forge_full, forge_full + ), + // Alternative Maven format + format!( + "{}net/minecraftforge/forge/{}-{}/forge-{}-{}-installer.jar", + FORGE_MAVEN_URL, game_version, forge_version, game_version, forge_version + ), + // Alternative files format + format!( + "{}maven/net/minecraftforge/forge/{}-{}/forge-{}-{}-installer.jar", + FORGE_FILES_URL, game_version, forge_version, game_version, forge_version + ), + ]; + + let mut last_error = None; + for url in url_patterns { + println!("Trying Forge installer URL: {}", url); + match reqwest::get(&url).await { + Ok(response) => { + if response.status().is_success() { + match response.bytes().await { + Ok(bytes) => { + println!("Successfully downloaded Forge installer from: {}", url); + return Ok(bytes); + } + Err(e) => { + last_error = Some(format!("Failed to read response body: {}", e)); + continue; + } + } + } else { + last_error = Some(format!("HTTP {}: {}", response.status(), url)); + continue; + } + } + Err(e) => { + last_error = Some(format!("Request failed: {}", e)); + continue; + } + } } - let bytes = response.bytes().await?; + Err(format!( + "Failed to download Forge installer from any URL. Last error: {}", + last_error.unwrap_or_else(|| "Unknown error".to_string()) + ) + .into()) +} + +/// Fetch the Forge installer manifest to get the library list +async fn fetch_forge_installer_manifest( + game_version: &str, + forge_version: &str, +) -> Result> { + let bytes = try_download_forge_installer(game_version, forge_version).await?; // Extract version.json from the JAR (which is a ZIP file) let cursor = std::io::Cursor::new(bytes.as_ref()); @@ -274,23 +341,10 @@ pub async fn run_forge_installer( forge_version: &str, java_path: &PathBuf, ) -> Result<(), Box> { - // Download the installer JAR - let installer_url = format!( - "{}net/minecraftforge/forge/{}-{}/forge-{}-{}-installer.jar", - FORGE_MAVEN_URL, game_version, forge_version, game_version, forge_version - ); - let installer_path = game_dir.join("forge-installer.jar"); - // Download installer - let client = reqwest::Client::new(); - let response = client.get(&installer_url).send().await?; - - if !response.status().is_success() { - return Err(format!("Failed to download Forge installer: {}", response.status()).into()); - } - - let bytes = response.bytes().await?; + // Download installer using the same multi-URL approach + let bytes = try_download_forge_installer(game_version, forge_version).await?; tokio::fs::write(&installer_path, &bytes).await?; // Run the installer in headless mode diff --git a/src-tauri/src/core/java.rs b/src-tauri/src/core/java.rs index 1d57d21..0c7769b 100644 --- a/src-tauri/src/core/java.rs +++ b/src-tauri/src/core/java.rs @@ -881,6 +881,64 @@ pub fn get_recommended_java(required_major_version: Option) -> Option, + max_major_version: Option, +) -> Option { + let installations = detect_all_java_installations(app_handle); + + if let Some(max_version) = max_major_version { + // Find Java version within the acceptable range + installations.into_iter().find(|java| { + let major = parse_java_version(&java.version); + let meets_min = if let Some(required) = required_major_version { + major >= required as u32 + } else { + true + }; + meets_min && major <= max_version + }) + } else if let Some(required) = required_major_version { + // Find exact match or higher (no upper bound) + installations.into_iter().find(|java| { + let major = parse_java_version(&java.version); + major >= required as u32 + }) + } else { + // Return newest + installations.into_iter().next() + } +} + +/// Check if a Java installation is compatible with the required version range +pub fn is_java_compatible( + java_path: &str, + required_major_version: Option, + max_major_version: Option, +) -> bool { + let java_path_buf = PathBuf::from(java_path); + if let Some(java) = check_java_installation(&java_path_buf) { + let major = parse_java_version(&java.version); + let meets_min = if let Some(required) = required_major_version { + major >= required as u32 + } else { + true + }; + let meets_max = if let Some(max_version) = max_major_version { + major <= max_version + } else { + true + }; + meets_min && meets_max + } else { + false + } +} + /// Detect all installed Java versions (including system installations and DropOut downloads) pub fn detect_all_java_installations(app_handle: &AppHandle) -> Vec { let mut installations = detect_java_installations(); diff --git a/src-tauri/src/core/manifest.rs b/src-tauri/src/core/manifest.rs index d92ae58..637b935 100644 --- a/src-tauri/src/core/manifest.rs +++ b/src-tauri/src/core/manifest.rs @@ -25,6 +25,13 @@ pub struct Version { pub time: String, #[serde(rename = "releaseTime")] pub release_time: String, + /// Java version requirement (major version number) + /// This is populated from the version JSON file if the version is installed locally + #[serde(skip_serializing_if = "Option::is_none")] + pub java_version: Option, + /// Whether this version is installed locally + #[serde(rename = "isInstalled", skip_serializing_if = "Option::is_none")] + pub is_installed: Option, } pub async fn fetch_version_manifest() -> Result> { diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 463bd5d..661309a 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -139,6 +139,121 @@ async fn start_game( // (for modded versions, this is the parent vanilla version) 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 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()); let mut download_tasks = Vec::new(); @@ -521,33 +636,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!("Java Args: {:?}", &args)); - } - // Get Java path from config or detect - 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); - if let Some(java) = javas.first() { - java.path.clone() - } else { - return Err( - "No Java installation found. Please configure Java in settings.".to_string(), - ); - } - }; - let java_path = utils::path::normalize_java_path(&java_path_str)?; + // Format Java command with sensitive information masked + let masked_args: Vec = 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 = 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: {}", java_path.display()) + format!("Starting Java process: {}", java_path_to_use) ); - let mut command = Command::new(&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()); @@ -567,7 +716,7 @@ async fn start_game( // Spawn and handle output let mut child = command .spawn() - .map_err(|e| format!("Failed to launch Java at '{}': {}\nPlease check your Java installation and path configuration in Settings.", java_path.display(), 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()); @@ -699,9 +848,42 @@ fn parse_jvm_arguments( } #[tauri::command] -async fn get_versions() -> Result, String> { +async fn get_versions(window: Window) -> Result, 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()), } } @@ -1008,6 +1190,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(()) } @@ -1371,6 +1556,9 @@ async fn install_fabric( format!("Fabric installed successfully: {}", result.id) ); + // Emit event to notify frontend + let _ = window.emit("fabric-installed", &result.id); + Ok(result) } @@ -1388,6 +1576,147 @@ async fn list_installed_fabric_versions(window: Window) -> Result, S .map_err(|e| e.to_string()) } +/// Get Java version requirement for a specific version +#[tauri::command] +async fn get_version_java_version( + window: Window, + version_id: String, +) -> Result, 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))?; + + // 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, + #[serde(rename = "isInstalled")] + is_installed: bool, +} + +/// Delete a version (remove version directory) +#[tauri::command] +async fn delete_version(window: Window, version_id: String) -> Result<(), 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 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, + version_id: String, +) -> Result { + 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))?; + + // 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 { @@ -1580,6 +1909,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) } @@ -1802,6 +2134,9 @@ 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, diff --git a/ui/src/components/BottomBar.svelte b/ui/src/components/BottomBar.svelte index b7bbf71..8a6b7ff 100644 --- a/ui/src/components/BottomBar.svelte +++ b/ui/src/components/BottomBar.svelte @@ -4,7 +4,7 @@ import { authState } from "../stores/auth.svelte"; import { gameState } from "../stores/game.svelte"; import { uiState } from "../stores/ui.svelte"; - import { Terminal, ChevronDown, Play, User, Check, RefreshCw } from 'lucide-svelte'; + import { Terminal, ChevronDown, Play, User, Check } from 'lucide-svelte'; interface InstalledVersion { id: string; @@ -16,23 +16,31 @@ let installedVersions = $state([]); let isLoadingVersions = $state(true); let downloadCompleteUnlisten: UnlistenFn | null = null; + let versionDeletedUnlisten: UnlistenFn | null = null; // Load installed versions on mount $effect(() => { loadInstalledVersions(); - setupDownloadListener(); + setupEventListeners(); return () => { if (downloadCompleteUnlisten) { downloadCompleteUnlisten(); } + if (versionDeletedUnlisten) { + versionDeletedUnlisten(); + } }; }); - async function setupDownloadListener() { + async function setupEventListeners() { // Refresh list when a download completes downloadCompleteUnlisten = await listen("download-complete", () => { loadInstalledVersions(); }); + // Refresh list when a version is deleted + versionDeletedUnlisten = await listen("version-deleted", () => { + loadInstalledVersions(); + }); } async function loadInstalledVersions() { @@ -160,18 +168,7 @@
-
- - -
+ + {#if isLoadingVersions} + Loading... + {:else if installedVersions.length === 0} + No versions installed + {:else} + {gameState.selectedVersion || "Select version"} + {/if} + + + {#if isVersionDropdownOpen && installedVersions.length > 0}
-
+ {#if fabricLoaders.length === 0} +
+ No Fabric versions available for {selectedGameVersion} +
+ {:else} +
@@ -339,21 +344,22 @@
{/if}
-
- - +
+ + + {/if}
{:else if selectedLoader === "forge"} diff --git a/ui/src/components/VersionsView.svelte b/ui/src/components/VersionsView.svelte index 063c28d..2e8b028 100644 --- a/ui/src/components/VersionsView.svelte +++ b/ui/src/components/VersionsView.svelte @@ -1,5 +1,6 @@