//! Forge server installation: downloading the official installer and running it //! headlessly to materialize the server launcher (run script, `@args` files, and //! the mod loader) into a managed server directory. //! //! Forge ships a bootstrapper rather than a ready-to-run jar. The launcher jar is //! produced by `java -jar forge-installer.jar --installServer `, which lays //! down the loader, its libraries, and the `@args` launch files. The regular //! `servers.start` flow then boots that output (see `lifecycle::forge_launch_args`). use tokio::process::Command; use crate::event::ServerPayloadType; use crate::event::emit::emit_server; use crate::util::io::IOError; use crate::{ErrorKind, Result}; use super::files::{download_to_dir, safe_relative_join}; use super::manifest::{ InstallState, read_manifest, server_path, write_manifest, }; const FORGE_MAVEN: &str = "https://maven.minecraftforge.net/net/minecraftforge/forge"; /// Downloads the Forge installer for `mc_version`/`build` into the server dir /// and runs it headlessly (`--installServer`) to lay down the launcher. The /// server is left unstarted; the caller (frontend wizard) writes `eula.txt` and /// the regular `servers.start` flow boots it. pub async fn install_forge( server_id: &str, mc_version: &str, build: &str, java_path: Option, ) -> Result<()> { let dir = server_path(server_id).await?; let mut manifest = read_manifest(&dir).await?; manifest.install_state = Some(InstallState::Incomplete); manifest.install_error = None; write_manifest(&dir, &manifest).await?; drop(manifest); let installer_name = format!("forge-{mc_version}-{build}-installer.jar"); let installer_path = safe_relative_join(&dir, &installer_name)?; let installer_url = format!("{FORGE_MAVEN}/{mc_version}-{build}/{installer_name}"); log( server_id, &format!("Downloading Forge installer ({installer_name})"), ) .await?; download_to_dir(server_id, &dir, &installer_url, &installer_name, None) .await?; let java = java_path.clone().unwrap_or_else(|| "java".to_string()); log(server_id, "Running Forge installer (this may take a while)").await?; let output = Command::new(&java) .arg("-jar") .arg(&installer_path) .arg("--installServer") .arg(&dir) .current_dir(&dir) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .output() .await .map_err(|e| { ErrorKind::LauncherError(format!( "Failed to run Forge installer: {e}" )) .as_error() })?; // The installer is verbose; surface a condensed tail so failures are diagnosable. let stderr = String::from_utf8_lossy(&output.stderr); for line in stderr.lines().rev().take(20) { log(server_id, line).await.ok(); } if !output.status.success() { let mut manifest = read_manifest(&dir).await?; manifest.install_state = Some(InstallState::Failed); manifest.install_error = Some("Forge installer exited with an error".to_string()); write_manifest(&dir, &manifest).await?; return Err(ErrorKind::LauncherError( "Forge installer failed. Check that the selected Java version supports this game version." .to_string(), ) .as_error()); } // Ensure eula.txt exists (eula=false) so the manual-start gate can offer it // without booting the jar. let eula_path = dir.join("eula.txt"); if !eula_path.exists() { tokio::fs::write(&eula_path, "eula=false\n") .await .map_err(|e| IOError::with_path(e, &eula_path))?; } let mut manifest = read_manifest(&dir).await?; manifest.install_state = None; manifest.install_error = None; write_manifest(&dir, &manifest).await?; log(server_id, "Forge server files installed").await.ok(); Ok(()) } async fn log(server_id: &str, line: &str) -> Result<()> { emit_server( server_id, ServerPayloadType::Log { line: line.to_string(), }, ) .await }