use paths::{client_jar, library_file, natives_dir}; use serde::Deserialize; use std::path::Path; use std::process::Output; use std::{ path::PathBuf, process::{Command, ExitStatus}, }; use McError::Process; use crate::{ config::Config, errors::McError, minecraft::manifests::Version, platform::paths, util::fs::library_allowed, }; #[derive(Debug, Clone, Deserialize)] pub struct JavaRuntime { pub major: u8, pub path: PathBuf, } impl JavaRuntime { fn validate(&self) -> Result<(), McError> { let output: Output = Command::new(&self.path) .arg("-version") .output() .map_err(|e| { McError::Runtime(format!( "Failed to execute Java at {}: {}", self.path.display(), e )) })?; if !output.status.success() { return Err(McError::Runtime(format!( "Invalid Java binary: {}", self.path.display() ))); } Ok(()) } } fn required_java_major(java_major: Option) -> u8 { java_major.unwrap_or(8) } fn resolve_runtime( required: u8, runtimes: &[JavaRuntime], ) -> Result { let mut candidates: Vec<&JavaRuntime> = runtimes .iter() .filter(|r| r.major >= required) .collect(); candidates.sort_by_key(|r| r.major); let runtime = candidates.first().ok_or_else(|| { McError::Runtime(format!( "No suitable Java runtime found (required: Java {})", required )) })?; runtime.validate()?; Ok(runtime.path.clone()) } fn build_classpath(config: &Config, version: &Version) -> String { let system_separator: &str = if cfg!(windows) { ";" } else { ":" }; let mut entries: Vec = Vec::new(); for library in &version.libraries { if !library_allowed(library) { continue; } if let Some(artifact) = &library.downloads.artifact { let path = library_file(config, &artifact.path); entries.push(path.to_string_lossy().to_string()); } } let client: PathBuf = client_jar(config, &version.id); entries.push(client.to_string_lossy().to_string()); entries.join(system_separator) } fn build_command( java_path: PathBuf, config: &Config, version: &Version, natives_dir: &Path, classpath: String, asset_index_id: String, ) -> Command { let mut cmd: Command = Command::new(java_path); cmd.arg(format!("-Xmx{}M", config.max_memory_mb)) .arg(format!("-Djava.library.path={}", natives_dir.display())); for arg in &config.jvm_args { cmd.arg(arg); } cmd.arg("-cp") .arg(classpath) .arg(&version.main_class); cmd.arg("--username") .arg(&config.username) .arg("--version") .arg(&version.id) .arg("--gameDir") .arg(paths::game_directory(config)) .arg("--assetsDir") .arg(paths::assets_directory(config)) .arg("--assetIndex") .arg(asset_index_id) .arg("--uuid") .arg(&config.uuid) .arg("--userProperties") .arg("{}") .arg("--accessToken") .arg("0") .arg("--userType") .arg("legacy"); cmd } pub fn launch(config: &Config, version: &Version) -> Result<(), McError> { let required: u8 = required_java_major( version .java_version .as_ref() .map(|j| j.major_version), ); let java_path: PathBuf = resolve_runtime(required, &config.runtimes)?; let classpath: String = build_classpath(config, version); let natives_dir: PathBuf = natives_dir(config, &version.id); if !natives_dir.exists() { return Err(McError::Runtime(format!( "Natives folder does not exist: {}", natives_dir.display() ))); } let asset_index_id = version .asset_index .as_ref() .ok_or_else(|| { McError::Runtime("Missing assetIndex in version.json".into()) })? .id .clone(); let mut cmd: Command = build_command( java_path, config, version, &natives_dir, classpath, asset_index_id, ); let status: ExitStatus = cmd.status()?; if !status.success() { return Err(Process("Minecraft exited with error".into())); } Ok(()) } #[cfg(test)] mod tests { use super::*; use std::path::PathBuf; #[test] fn required_java_major_defaults_to_8() { assert_eq!(required_java_major(None), 8); } #[test] fn required_java_major_uses_given_value() { assert_eq!(required_java_major(Some(17)), 17); } #[test] fn resolve_runtime_errors_when_no_runtimes() { let runtimes: Vec = vec![]; assert!(resolve_runtime(17, &runtimes).is_err()); } #[test] fn resolve_runtime_picks_lowest_matching_major() { let runtimes: Vec = vec![ JavaRuntime { major: 21, path: PathBuf::from("/bin/true"), }, JavaRuntime { major: 17, path: PathBuf::from("/bin/true"), }, ]; let result: PathBuf = resolve_runtime(8, &runtimes).unwrap(); assert_eq!(result, PathBuf::from("/bin/true")); } #[test] fn resolve_runtime_respects_required_version() { let runtimes: Vec = vec![ JavaRuntime { major: 8, path: PathBuf::from("/bin/true"), }, JavaRuntime { major: 17, path: PathBuf::from("/bin/true"), }, ]; let result: PathBuf = resolve_runtime(17, &runtimes).unwrap(); assert_eq!(result, PathBuf::from("/bin/true")); } #[test] fn validate_succeeds_for_true_binary() { let runtime = JavaRuntime { major: 17, path: PathBuf::from("/bin/true"), }; assert!(runtime.validate().is_ok()); } #[test] fn validate_fails_for_false_binary() { let runtime = JavaRuntime { major: 17, path: PathBuf::from("/bin/false"), }; assert!(runtime.validate().is_err()); } }