use super::*;
use crate::error::{Result, LateJavaCoreError};
use crate::minecraft::{
VersionManifest, VersionInfo, MinecraftArguments, MinecraftAssets,
MinecraftLibraries, JavaDownloader, MinecraftBundle, MinecraftLoader
};
use crate::downloader::Downloader;
use crate::utils::is_old;
use std::path::Path;
use std::process::{Command, Stdio};
use tokio::process::Command as AsyncCommand;
use std::sync::Arc;
use tokio::sync::Mutex;
use tokio::sync::broadcast;
pub struct Launch {
downloader: Arc<Mutex<Downloader>>,
event_sender: broadcast::Sender<LaunchEvent>,
}
#[derive(Debug, Clone)]
pub enum LaunchEvent {
Progress { downloaded: u64, total: u64, element: Option<String> },
Speed(f64),
Estimated(f64),
Extract(String),
Check { progress: u64, size: u64, element: Option<String> },
Patch(String),
Data(String),
Close(String),
Error(String),
}
impl Launch {
pub fn new() -> Self {
let (event_sender, _) = broadcast::channel(100);
Self {
downloader: Arc::new(Mutex::new(Downloader::new())),
event_sender,
}
}
pub fn subscribe(&self) -> broadcast::Receiver<LaunchEvent> {
self.event_sender.subscribe()
}
fn emit(&self, event: LaunchEvent) {
let _ = self.event_sender.send(event);
}
pub async fn launch(&mut self, options: LaunchOptions) -> Result<()> {
self.validate_options(&options)?;
let version_info = self.get_version_info(&options).await?;
let download_result = self.download_game(&options, &version_info).await?;
let (minecraft_json, minecraft_loader, minecraft_version, minecraft_java) = download_result;
let minecraft_arguments = self.get_minecraft_arguments(&options, &minecraft_json, minecraft_loader.as_ref()).await?;
let loader_arguments = self.get_loader_arguments(&options, minecraft_loader.as_ref(), &minecraft_version).await?;
let mut launch_args = Vec::new();
launch_args.extend(minecraft_arguments.jvm);
launch_args.extend(minecraft_arguments.classpath);
launch_args.extend(loader_arguments.jvm);
if let Some(main_class) = minecraft_arguments.main_class {
launch_args.push(main_class);
}
launch_args.extend(minecraft_arguments.game);
launch_args.extend(loader_arguments.game);
self.execute_minecraft(&options, launch_args, &minecraft_java).await?;
Ok(())
}
fn validate_options(&self, options: &LaunchOptions) -> Result<()> {
if options.authenticator.access_token.is_empty() {
return Err(LateJavaCoreError::Validation("Authenticator is required".to_string()));
}
if options.path.is_empty() {
return Err(LateJavaCoreError::Validation("Path is required".to_string()));
}
if options.version.is_empty() {
return Err(LateJavaCoreError::Validation("Version is required".to_string()));
}
if options.memory.min.is_empty() || options.memory.max.is_empty() {
return Err(LateJavaCoreError::Validation("Memory options are required".to_string()));
}
if let Some(limit) = options.download_file_multiple {
if limit < 1 {
return Err(LateJavaCoreError::Validation("downloadFileMultiple must be at least 1".to_string()));
}
if limit > 30 {
return Err(LateJavaCoreError::Validation("downloadFileMultiple cannot exceed 30".to_string()));
}
}
Ok(())
}
async fn download_game(&mut self, options: &LaunchOptions, version_info: &VersionInfo) -> Result<(serde_json::Value, Option<serde_json::Value>, String, JavaDownloadResult)> {
let info_version = self.get_info_version(options).await?;
let (json, version) = info_version;
let mut loader_json: Option<serde_json::Value> = None;
let libraries = MinecraftLibraries::new(crate::minecraft::libraries::LibrariesOptions {
path: options.path.clone(),
instance: options.instance.clone(),
headers: options.headers.clone(),
token: options.token.clone(),
});
let bundle = MinecraftBundle::new(crate::minecraft::bundle::MinecraftBundleOptions {
path: options.path.clone(),
instance: options.instance.clone(),
ignored: options.ignored.clone(),
});
let java = JavaDownloader::new(crate::minecraft::java::JavaDownloaderOptions {
path: options.path.clone(),
java: crate::minecraft::java::JavaOptions {
version: options.java.version.clone(),
java_type: options.java.java_type.clone(),
},
intel_enabled_mac: Some(false),
});
let game_libraries = libraries.get_libraries(json.clone()).await?;
let game_assets_other = libraries.get_assets_others(options.url.as_deref()).await?;
let mut assets = MinecraftAssets::new(crate::minecraft::assets::MinecraftAssetsOptions {
path: options.path.clone(),
instance: options.instance.clone(),
});
let game_assets = assets.get_assets(&json).await?;
let game_java = if options.java.path.is_some() {
crate::minecraft::java::JavaDownloadResult {
files: Vec::new(),
path: options.java.path.as_ref().unwrap().clone(),
error: None,
message: None,
}
} else {
java.get_java_files(&json).await?
};
if let Some(true) = game_java.error {
return Err(LateJavaCoreError::Java("Java download failed".to_string()));
}
let mut all_files = Vec::new();
all_files.extend(game_libraries);
all_files.extend(game_assets_other);
all_files.extend(game_assets);
all_files.extend(game_java.files);
let files_list = bundle.check_bundle(&mut all_files).await?;
if !files_list.is_empty() {
let mut headers = options.headers.clone();
if options.token.is_some() && headers.is_none() {
let mut auth_headers = std::collections::HashMap::new();
auth_headers.insert("Authorization".to_string(), options.token.as_ref().unwrap().clone());
headers = Some(auth_headers);
}
let downloader = if let Some(headers) = headers {
Downloader::with_headers(headers)
} else {
Downloader::new()
};
let total_size = bundle.get_total_size(&files_list).await?;
let download_options: Vec<crate::downloader::DownloadOptions> = files_list.into_iter().map(|file| {
crate::downloader::DownloadOptions {
url: file.url.unwrap_or_default(),
path: file.path.clone(),
length: file.size,
folder: file.folder.unwrap_or_default(),
r#type: file.r#type,
}
}).collect();
downloader.download_file_multiple(
&download_options,
total_size,
options.download_file_multiple.unwrap_or(5),
options.timeout.unwrap_or(10000),
).await?;
}
if options.loader.enable {
let loader_install = MinecraftLoader::new(crate::minecraft::loader::LoaderOptions {
path: options.path.clone(),
loader: crate::minecraft::loader::LoaderConfig {
path: options.loader.path.clone(),
loader_type: options.loader.loader_type.clone(),
build: options.loader.build.clone(),
},
download_file_multiple: options.download_file_multiple,
});
let java_path = if options.java.path.is_some() {
options.java.path.as_ref().unwrap().clone()
} else {
game_java.path.clone()
};
match loader_install.get_loader(&version, &java_path).await {
Ok(json_loader) => {
loader_json = Some(serde_json::to_value(json_loader)?);
}
Err(err) => {
return Err(err);
}
}
}
if options.verify {
let mut verify_files = Vec::new();
verify_files.extend(game_assets_other);
verify_files.extend(game_assets);
verify_files.extend(game_java.files);
bundle.check_files(&verify_files).await?;
}
let natives = libraries.natives(&game_libraries).await?;
if natives.is_empty() {
}
if is_old(&json) {
assets.copy_assets(&json)?;
}
Ok((json, loader_json, version, game_java))
}
async fn get_info_version(&self, options: &LaunchOptions) -> Result<(serde_json::Value, String)> {
let manifest = VersionManifest::fetch().await?;
let version = self.resolve_version(&manifest, &options.version)?;
let response = reqwest::get(&version.url).await?;
let json: serde_json::Value = response.json().await?;
Ok((json, version.id))
}
async fn get_minecraft_arguments(&self, options: &LaunchOptions, json: &serde_json::Value, loader_json: Option<&serde_json::Value>) -> Result<crate::minecraft::arguments::LaunchArguments> {
let arguments = MinecraftArguments::new(options.clone(), options.authenticator.clone());
arguments.get_arguments(json, loader_json).await
}
async fn get_loader_arguments(&self, options: &LaunchOptions, loader_json: Option<&serde_json::Value>, version: &str) -> Result<crate::minecraft::loader::LoaderArgumentsResult> {
if let Some(loader) = loader_json {
let loader_install = MinecraftLoader::new(crate::minecraft::loader::LoaderOptions {
path: options.path.clone(),
loader: crate::minecraft::loader::LoaderConfig {
path: options.loader.path.clone(),
loader_type: options.loader.loader_type.clone(),
build: options.loader.build.clone(),
},
download_file_multiple: options.download_file_multiple,
});
let loader_data: crate::minecraft::loader::LoaderJson = serde_json::from_value(loader.clone())?;
loader_install.get_arguments(Some(&loader_data), version).await
} else {
Ok(crate::minecraft::loader::LoaderArgumentsResult {
game: Vec::new(),
jvm: Vec::new(),
main_class: None,
})
}
}
async fn get_version_info(&self, options: &LaunchOptions) -> Result<VersionInfo> {
let manifest = VersionManifest::fetch().await?;
let version = self.resolve_version(&manifest, &options.version)?;
Ok(version)
}
fn resolve_version(&self, manifest: &VersionManifest, version: &str) -> Result<VersionInfo> {
let resolved_version = match version {
"latest_release" | "r" | "lr" => &manifest.latest.release,
"latest_snapshot" | "s" | "ls" => &manifest.latest.snapshot,
_ => version,
};
manifest.versions
.iter()
.find(|v| v.id == resolved_version)
.cloned()
.ok_or_else(|| LateJavaCoreError::Minecraft(format!("Version {} not found", version)))
}
async fn execute_minecraft(&self, options: &LaunchOptions, args: Vec<String>, java_result: &crate::minecraft::java::JavaDownloadResult) -> Result<()> {
let java_path = if options.java.path.is_some() {
options.java.path.as_ref().unwrap()
} else {
&java_result.path
};
let working_dir = if let Some(instance) = &options.instance {
Path::new(&options.path).join("instances").join(instance)
} else {
Path::new(&options.path).to_path_buf()
};
if !working_dir.exists() {
std::fs::create_dir_all(&working_dir)?;
}
let mut arguments_logs = args.join(" ");
arguments_logs = arguments_logs.replace(&options.authenticator.access_token, "????????");
arguments_logs = arguments_logs.replace(&options.authenticator.client_token, "????????");
arguments_logs = arguments_logs.replace(&options.authenticator.uuid, "????????");
if let Some(xbox_account) = &options.authenticator.xbox_account {
arguments_logs = arguments_logs.replace(&xbox_account.xuid, "????????");
}
arguments_logs = arguments_logs.replace(&format!("{}/", options.path), "");
self.emit(LaunchEvent::Data(format!("Launching with arguments {}", arguments_logs)));
log::info!("Launching Minecraft with args: {:?}", args);
let mut command = AsyncCommand::new(java_path);
command.args(&args);
command.current_dir(&working_dir);
if options.detached.unwrap_or(false) {
command.stdout(Stdio::null());
command.stderr(Stdio::null());
}
let mut child = command.spawn()?;
if !options.detached.unwrap_or(false) {
let status = child.wait().await?;
if !status.success() {
return Err(LateJavaCoreError::Minecraft("Minecraft process failed".to_string()));
}
self.emit(LaunchEvent::Close("Minecraft closed".to_string()));
}
Ok(())
}
pub async fn get_download_progress(&self) -> crate::downloader::DownloadProgress {
crate::downloader::DownloadProgress {
downloaded: 0,
total: 0,
speed: 0.0,
percentage: 0.0,
}
}
}