aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/src-tauri/src
diff options
context:
space:
mode:
authorHsiangNianian <i@jyunko.cn>2026-01-15 19:33:03 +0800
committerHsiangNianian <i@jyunko.cn>2026-01-15 19:33:03 +0800
commita9c5ed1550a4270dd34b62aedbf9ecf43a86de51 (patch)
tree6e714dae36f52f5a1c6ce25b64d70076b451c901 /src-tauri/src
parentf584425ca78eab398a150aa9ffc3ed74f0cc8a70 (diff)
downloadDropOut-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.rs302
-rw-r--r--src-tauri/src/main.rs44
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())?;