diff options
| author | 2026-01-15 19:33:03 +0800 | |
|---|---|---|
| committer | 2026-01-15 19:33:03 +0800 | |
| commit | a9c5ed1550a4270dd34b62aedbf9ecf43a86de51 (patch) | |
| tree | 6e714dae36f52f5a1c6ce25b64d70076b451c901 /src-tauri/src | |
| parent | f584425ca78eab398a150aa9ffc3ed74f0cc8a70 (diff) | |
| download | DropOut-a9c5ed1550a4270dd34b62aedbf9ecf43a86de51.tar.gz DropOut-a9c5ed1550a4270dd34b62aedbf9ecf43a86de51.zip | |
feat: Enhance Forge installation process by fetching installer manifest and improving library management for better compatibility
Diffstat (limited to 'src-tauri/src')
| -rw-r--r-- | src-tauri/src/core/forge.rs | 302 | ||||
| -rw-r--r-- | src-tauri/src/main.rs | 44 |
2 files changed, 286 insertions, 60 deletions
diff --git a/src-tauri/src/core/forge.rs b/src-tauri/src/core/forge.rs index 0f17bcc..e69b296 100644 --- a/src-tauri/src/core/forge.rs +++ b/src-tauri/src/core/forge.rs @@ -5,8 +5,7 @@ //! - Install Forge loader for a specific Minecraft version //! //! Note: Forge installation is more complex than Fabric, especially for versions 1.13+. -//! This implementation focuses on the basic JSON generation approach. -//! For full Forge 1.13+ support, processor execution would need to be implemented. +//! This implementation fetches the installer manifest to get the correct library list. use serde::{Deserialize, Serialize}; use std::error::Error; @@ -42,6 +41,46 @@ pub struct InstalledForgeVersion { pub path: PathBuf, } +/// Forge installer manifest structure (from version.json inside installer JAR) +#[derive(Debug, Deserialize)] +struct ForgeInstallerManifest { + id: Option<String>, + #[serde(rename = "inheritsFrom")] + inherits_from: Option<String>, + #[serde(rename = "mainClass")] + main_class: Option<String>, + #[serde(default)] + libraries: Vec<ForgeLibrary>, + arguments: Option<ForgeArguments>, +} + +#[derive(Debug, Deserialize)] +struct ForgeArguments { + game: Option<Vec<serde_json::Value>>, + jvm: Option<Vec<serde_json::Value>>, +} + +#[derive(Debug, Deserialize, Clone)] +struct ForgeLibrary { + name: String, + #[serde(default)] + downloads: Option<ForgeLibraryDownloads>, + #[serde(default)] + url: Option<String>, +} + +#[derive(Debug, Deserialize, Clone)] +struct ForgeLibraryDownloads { + artifact: Option<ForgeArtifact>, +} + +#[derive(Debug, Deserialize, Clone)] +struct ForgeArtifact { + path: Option<String>, + url: Option<String>, + sha1: Option<String>, +} + /// Fetch all Minecraft versions supported by Forge. /// /// # Returns @@ -138,18 +177,49 @@ 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( + game_version: &str, + forge_version: &str, +) -> Result<ForgeInstallerManifest, Box<dyn Error + Send + Sync>> { + 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()); + } + + let bytes = response.bytes().await?; + + // Extract version.json from the JAR (which is a ZIP file) + let cursor = std::io::Cursor::new(bytes.as_ref()); + let mut archive = zip::ZipArchive::new(cursor)?; + + // Look for version.json in the archive + let version_json = archive.by_name("version.json")?; + let manifest: ForgeInstallerManifest = serde_json::from_reader(version_json)?; + + Ok(manifest) +} + /// Install Forge for a specific Minecraft version. /// -/// Note: This creates a basic version JSON. For Forge 1.13+, the full installation -/// requires running the Forge installer processors, which is not yet implemented. -/// This basic implementation works for legacy Forge versions (<1.13) and creates -/// the structure needed for modern Forge (libraries will need to be downloaded -/// separately). +/// This function downloads the Forge installer JAR and runs it in headless mode +/// to properly install Forge with all necessary patches. /// /// # Arguments /// * `game_dir` - The .minecraft directory path /// * `game_version` - The Minecraft version (e.g., "1.20.4") /// * `forge_version` - The Forge version (e.g., "49.0.38") +/// * `java_path` - Path to the Java executable /// /// # Returns /// Information about the installed version. @@ -160,10 +230,11 @@ pub async fn install_forge( ) -> Result<InstalledForgeVersion, Box<dyn Error + Send + Sync>> { let version_id = generate_version_id(game_version, forge_version); - // Create basic version JSON structure - // Note: This is a simplified version. Full Forge installation requires - // downloading the installer and running processors. - let version_json = create_forge_version_json(game_version, forge_version)?; + // Fetch the installer manifest to get the complete version.json + let manifest = fetch_forge_installer_manifest(game_version, forge_version).await?; + + // Create version JSON from the manifest + let version_json = create_forge_version_json_from_manifest(game_version, forge_version, &manifest)?; // Create the version directory let version_dir = game_dir.join("versions").join(&version_id); @@ -182,55 +253,185 @@ pub async fn install_forge( }) } -/// Create a basic Forge version JSON. +/// Install Forge using the official installer JAR. +/// This runs the Forge installer in headless mode to properly patch the client. +/// +/// # Arguments +/// * `game_dir` - The .minecraft directory path +/// * `game_version` - The Minecraft version +/// * `forge_version` - The Forge version +/// * `java_path` - Path to the Java executable /// -/// This creates a minimal version JSON that inherits from vanilla and adds -/// the Forge libraries. For full functionality with Forge 1.13+, the installer -/// would need to be run to patch the game. +/// # Returns +/// Result indicating success or failure +pub async fn run_forge_installer( + game_dir: &PathBuf, + game_version: &str, + forge_version: &str, + java_path: &PathBuf, +) -> Result<(), Box<dyn Error + Send + Sync>> { + // 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?; + tokio::fs::write(&installer_path, &bytes).await?; + + // Run the installer in headless mode + // The installer accepts --installClient <path> to install to a specific directory + let output = tokio::process::Command::new(java_path) + .arg("-jar") + .arg(&installer_path) + .arg("--installClient") + .arg(game_dir) + .output() + .await?; + + // Clean up installer + let _ = tokio::fs::remove_file(&installer_path).await; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + let stdout = String::from_utf8_lossy(&output.stdout); + return Err(format!( + "Forge installer failed:\nstdout: {}\nstderr: {}", + stdout, stderr + ).into()); + } + + Ok(()) +} + +/// Create a Forge version JSON from the installer manifest. +fn create_forge_version_json_from_manifest( + game_version: &str, + forge_version: &str, + manifest: &ForgeInstallerManifest, +) -> Result<serde_json::Value, Box<dyn Error + Send + Sync>> { + let version_id = generate_version_id(game_version, forge_version); + + // Use main class from manifest or default + let main_class = manifest.main_class.clone().unwrap_or_else(|| { + if is_modern_forge(game_version) { + "cpw.mods.bootstraplauncher.BootstrapLauncher".to_string() + } else { + "net.minecraft.launchwrapper.Launch".to_string() + } + }); + + // Convert libraries to JSON format, preserving download info + let lib_entries: Vec<serde_json::Value> = manifest.libraries + .iter() + .map(|lib| { + let mut entry = serde_json::json!({ + "name": lib.name + }); + + // Add URL if present + if let Some(url) = &lib.url { + entry["url"] = serde_json::Value::String(url.clone()); + } else { + // Default to Forge Maven for Forge libraries + entry["url"] = serde_json::Value::String(FORGE_MAVEN_URL.to_string()); + } + + // Add downloads if present + if let Some(downloads) = &lib.downloads { + if let Some(artifact) = &downloads.artifact { + let mut artifact_json = serde_json::Map::new(); + if let Some(path) = &artifact.path { + artifact_json.insert("path".to_string(), serde_json::Value::String(path.clone())); + } + if let Some(url) = &artifact.url { + artifact_json.insert("url".to_string(), serde_json::Value::String(url.clone())); + } + if let Some(sha1) = &artifact.sha1 { + artifact_json.insert("sha1".to_string(), serde_json::Value::String(sha1.clone())); + } + if !artifact_json.is_empty() { + entry["downloads"] = serde_json::json!({ + "artifact": artifact_json + }); + } + } + } + + entry + }) + .collect(); + + // Build arguments + let mut arguments = serde_json::json!({ + "game": [], + "jvm": [] + }); + + if let Some(args) = &manifest.arguments { + if let Some(game_args) = &args.game { + arguments["game"] = serde_json::Value::Array(game_args.clone()); + } + if let Some(jvm_args) = &args.jvm { + arguments["jvm"] = serde_json::Value::Array(jvm_args.clone()); + } + } + + let json = serde_json::json!({ + "id": version_id, + "inheritsFrom": manifest.inherits_from.clone().unwrap_or_else(|| game_version.to_string()), + "type": "release", + "mainClass": main_class, + "libraries": lib_entries, + "arguments": arguments + }); + + Ok(json) +} + +/// Create a Forge version JSON with the proper library list (fallback). +#[allow(dead_code)] fn create_forge_version_json( game_version: &str, forge_version: &str, + libraries: &[ForgeLibrary], ) -> Result<serde_json::Value, Box<dyn Error + Send + Sync>> { let version_id = generate_version_id(game_version, forge_version); - let forge_maven_coord = format!( - "net.minecraftforge:forge:{}-{}", - game_version, forge_version - ); // Determine main class based on version - // Forge 1.13+ uses different launchers - let (main_class, libraries) = if is_modern_forge(game_version) { - // Modern Forge (1.13+) uses cpw.mods.bootstraplauncher - ( - "cpw.mods.bootstraplauncher.BootstrapLauncher".to_string(), - vec![ - create_library_entry(&forge_maven_coord, Some(FORGE_MAVEN_URL)), - create_library_entry( - &format!( - "net.minecraftforge:forge:{}-{}:universal", - game_version, forge_version - ), - Some(FORGE_MAVEN_URL), - ), - ], - ) + let main_class = if is_modern_forge(game_version) { + "cpw.mods.bootstraplauncher.BootstrapLauncher" } else { - // Legacy Forge uses LaunchWrapper - ( - "net.minecraft.launchwrapper.Launch".to_string(), - vec![ - create_library_entry(&forge_maven_coord, Some(FORGE_MAVEN_URL)), - create_library_entry("net.minecraft:launchwrapper:1.12", None), - ], - ) + "net.minecraft.launchwrapper.Launch" }; + // Convert libraries to JSON format + let lib_entries: Vec<serde_json::Value> = libraries + .iter() + .map(|lib| { + serde_json::json!({ + "name": lib.name, + "url": FORGE_MAVEN_URL + }) + }) + .collect(); + let json = serde_json::json!({ "id": version_id, "inheritsFrom": game_version, "type": "release", "mainClass": main_class, - "libraries": libraries, + "libraries": lib_entries, "arguments": { "game": [], "jvm": [] @@ -240,19 +441,6 @@ fn create_forge_version_json( Ok(json) } -/// Create a library entry for the version JSON. -fn create_library_entry(name: &str, maven_url: Option<&str>) -> serde_json::Value { - let mut entry = serde_json::json!({ - "name": name - }); - - if let Some(url) = maven_url { - entry["url"] = serde_json::Value::String(url.to_string()); - } - - entry -} - /// Check if the Minecraft version uses modern Forge (1.13+). fn is_modern_forge(game_version: &str) -> bool { let parts: Vec<&str> = game_version.split('.').collect(); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 07e190e..3671166 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -121,6 +121,13 @@ async fn start_game( format!("Loading version details for {}...", version_id) ); + // 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 version_details = core::manifest::load_version(&game_dir, &version_id) .await .map_err(|e| e.to_string())?; @@ -135,9 +142,7 @@ async fn start_game( // Determine the actual minecraft version for client.jar // (for modded versions, this is the parent vanilla version) - let minecraft_version = version_details - .inherits_from - .clone() + let minecraft_version = original_inherits_from .unwrap_or_else(|| version_id.clone()); // 2. Prepare download tasks @@ -380,6 +385,7 @@ async fn start_game( for lib in &version_details.libraries { if core::rules::is_library_allowed(&lib.rules) { if let Some(downloads) = &lib.downloads { + // Standard library with explicit downloads if let Some(artifact) = &downloads.artifact { let path_str = artifact .path @@ -388,6 +394,12 @@ async fn start_game( let lib_path = libraries_dir.join(path_str); classpath_entries.push(lib_path.to_string_lossy().to_string()); } + } else { + // Library without explicit downloads (mod loader libraries) + // Use Maven coordinate resolution + if let Some(lib_path) = core::maven::get_library_path(&lib.name, &libraries_dir) { + classpath_entries.push(lib_path.to_string_lossy().to_string()); + } } } } @@ -1452,6 +1464,7 @@ async fn get_forge_versions_for_game( #[tauri::command] async fn install_forge( window: Window, + config_state: State<'_, core::config::ConfigState>, game_version: String, forge_version: String, ) -> Result<core::forge::InstalledForgeVersion, String> { @@ -1469,6 +1482,31 @@ async fn install_forge( .app_data_dir() .map_err(|e| format!("Failed to get app data dir: {}", e))?; + // Get Java path from config or detect + let config = config_state.config.lock().unwrap().clone(); + 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 = std::path::PathBuf::from(&java_path_str); + + emit_log!(window, "Running Forge installer...".to_string()); + + // Run the Forge installer to properly patch the client + core::forge::run_forge_installer(&game_dir, &game_version, &forge_version, &java_path) + .await + .map_err(|e| format!("Forge installer failed: {}", e))?; + + 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) .await .map_err(|e| e.to_string())?; |