pub mod options;
pub use options::*;
use crate::error::Result;
use tokio::sync::broadcast;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct DownloadFile {
pub url: String,
pub path: String,
pub size: u64,
pub sha1: Option<String>,
}
#[derive(Debug, Clone)]
pub struct JavaDownloadResult {
pub path: String,
pub files: Vec<DownloadFile>,
}
#[derive(Debug, Clone)]
pub struct MinecraftArguments {
pub jvm: Vec<String>,
pub classpath: Vec<String>,
pub main_class: String,
pub game: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct LoaderArguments {
pub jvm: Vec<String>,
pub game: Vec<String>,
}
#[derive(Debug, Clone)]
pub enum LaunchEvent {
Progress { downloaded: u64, total: u64, element: String },
Speed(f64),
Estimated(f64),
Extract(String),
Data(String),
Close(String),
Error(String),
}
pub struct Launch {
event_sender: broadcast::Sender<LaunchEvent>,
}
impl Launch {
pub fn new() -> Self {
let (event_sender, _) = broadcast::channel(100);
Self { 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, mut options: LaunchOptions) -> Result<()> {
let default_options = LaunchOptions::default();
self.apply_defaults(&mut options, &default_options);
options.path = std::fs::canonicalize(&options.path)
.unwrap_or_else(|_| std::path::PathBuf::from(&options.path))
.to_string_lossy()
.replace('\\', "/");
if let Some(ref mut mcp) = options.mcp {
if let Some(ref instance) = options.instance {
*mcp = format!("{}/instances/{}/{}", options.path, instance, mcp);
} else {
*mcp = format!("{}/{}", options.path, mcp);
}
}
if let Some(ref mut loader_type) = options.loader.r#type {
*loader_type = loader_type.to_lowercase();
}
if let Some(ref mut build) = options.loader.build {
*build = build.to_lowercase();
}
self.validate_options(&options)?;
if let Some(ref mut multiple) = options.download_file_multiple {
if *multiple < 1 {
*multiple = 1;
} else if *multiple > 30 {
*multiple = 30;
}
}
if options.loader.path.is_none() {
if let Some(ref loader_type) = options.loader.r#type {
options.loader.path = Some(format!("./loader/{}", loader_type));
}
}
self.start(options).await
}
fn apply_defaults(&self, options: &mut LaunchOptions, defaults: &LaunchOptions) {
if options.url.is_none() { options.url = defaults.url.clone(); }
if options.headers.is_none() { options.headers = defaults.headers.clone(); }
if options.token.is_none() { options.token = defaults.token.clone(); }
if options.timeout.is_none() { options.timeout = defaults.timeout; }
if options.instance.is_none() { options.instance = defaults.instance.clone(); }
if options.detached.is_none() { options.detached = defaults.detached; }
if options.download_file_multiple.is_none() { options.download_file_multiple = defaults.download_file_multiple; }
if options.bypass_offline.is_none() { options.bypass_offline = defaults.bypass_offline; }
if options.intel_enabled_mac.is_none() { options.intel_enabled_mac = defaults.intel_enabled_mac; }
if options.mcp.is_none() { options.mcp = defaults.mcp.clone(); }
if options.verify.is_none() { options.verify = defaults.verify; }
if options.ignored.is_empty() { options.ignored = defaults.ignored.clone(); }
if options.jvm_args.is_empty() { options.jvm_args = defaults.jvm_args.clone(); }
if options.game_args.is_empty() { options.game_args = defaults.game_args.clone(); }
}
async fn start(&mut self, options: LaunchOptions) -> Result<()> {
let download_result = self.download_game(&options).await?;
let (minecraft_json, minecraft_loader, minecraft_version, minecraft_java) = download_result;
let minecraft_arguments = self.get_minecraft_arguments(&options, &minecraft_json, &minecraft_loader).await?;
let loader_arguments = self.get_loader_arguments(&options, &minecraft_loader, &minecraft_version).await?;
let mut final_arguments = Vec::new();
final_arguments.extend(minecraft_arguments.jvm);
final_arguments.extend(minecraft_arguments.classpath);
final_arguments.extend(loader_arguments.jvm);
final_arguments.push(minecraft_arguments.main_class);
final_arguments.extend(minecraft_arguments.game);
final_arguments.extend(loader_arguments.game);
let java_path = if let Some(ref java_path) = options.java.path {
java_path.clone()
} else {
minecraft_java.path
};
let working_dir = if let Some(ref instance) = options.instance {
format!("{}/instances/{}", options.path, instance)
} else {
options.path.clone()
};
if !std::path::Path::new(&working_dir).exists() {
std::fs::create_dir_all(&working_dir)?;
}
let mut arguments_logs = final_arguments.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(ref 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)));
self.execute_minecraft(&options, final_arguments, &java_path, &working_dir).await?;
Ok(())
}
fn validate_options(&self, options: &LaunchOptions) -> Result<()> {
if options.authenticator.access_token.is_empty() {
return Err(crate::error::LateJavaCoreError::Validation("Authenticator not found".to_string()));
}
Ok(())
}
async fn download_game(&self, options: &LaunchOptions) -> Result<(serde_json::Value, Option<serde_json::Value>, String, JavaDownloadResult)> {
let version_info = self.get_info_version(options).await?;
let (json, version) = version_info;
let game_libraries = self.get_libraries(&json).await?;
let game_assets = self.get_assets(&json).await?;
let game_java = if options.java.path.is_some() {
JavaDownloadResult { path: options.java.path.as_ref().unwrap().clone(), files: vec![] }
} else {
self.get_java_files(&json).await?
};
let mut all_files = Vec::new();
all_files.extend(game_libraries);
all_files.extend(game_assets);
all_files.extend(game_java.files.clone());
let files_list = self.check_bundle(&mut all_files).await?;
if !files_list.is_empty() {
self.download_files(options, &files_list).await?;
}
let loader_json = if options.loader.enable {
Some(self.install_loader(options, &version).await?)
} else {
None
};
Ok((json, loader_json, version, game_java))
}
async fn get_info_version(&self, options: &LaunchOptions) -> Result<(serde_json::Value, String)> {
let version = options.version.clone();
let json = serde_json::json!({
"id": version,
"type": "release",
"mainClass": "net.minecraft.client.main.Main"
});
Ok((json, version))
}
async fn get_libraries(&self, _json: &serde_json::Value) -> Result<Vec<DownloadFile>> {
Ok(vec![])
}
async fn get_assets(&self, _json: &serde_json::Value) -> Result<Vec<DownloadFile>> {
Ok(vec![])
}
async fn get_java_files(&self, _json: &serde_json::Value) -> Result<JavaDownloadResult> {
Ok(JavaDownloadResult {
path: "java".to_string(),
files: vec![]
})
}
async fn check_bundle(&self, _files: &mut Vec<DownloadFile>) -> Result<Vec<DownloadFile>> {
Ok(vec![])
}
async fn download_files(&self, options: &LaunchOptions, _files: &[DownloadFile]) -> Result<()> {
self.emit(LaunchEvent::Progress { downloaded: 0, total: 100, element: "Downloading files".to_string() });
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
self.emit(LaunchEvent::Progress { downloaded: 100, total: 100, element: "Download complete".to_string() });
Ok(())
}
async fn install_loader(&self, _options: &LaunchOptions, _version: &str) -> Result<serde_json::Value> {
Ok(serde_json::json!({}))
}
async fn get_minecraft_arguments(&self, _options: &LaunchOptions, _json: &serde_json::Value, _loader: &Option<serde_json::Value>) -> Result<MinecraftArguments> {
Ok(MinecraftArguments {
jvm: vec!["-Xmx2G".to_string()],
classpath: vec!["-cp".to_string(), "minecraft.jar".to_string()],
main_class: "net.minecraft.client.main.Main".to_string(),
game: vec!["--version".to_string(), "1.20.4".to_string()]
})
}
async fn get_loader_arguments(&self, _options: &LaunchOptions, _loader: &Option<serde_json::Value>, _version: &str) -> Result<LoaderArguments> {
Ok(LoaderArguments {
jvm: vec![],
game: vec![]
})
}
async fn execute_minecraft(&self, options: &LaunchOptions, args: Vec<String>, java_path: &str, working_dir: &str) -> Result<()> {
use tokio::process::Command;
let mut command = Command::new(java_path);
command.args(&args);
command.current_dir(working_dir);
if options.detached.unwrap_or(false) {
command.stdout(std::process::Stdio::null());
command.stderr(std::process::Stdio::null());
}
let mut child = command.spawn()?;
if !options.detached.unwrap_or(false) {
if let Some(stdout) = child.stdout.take() {
let event_sender = self.event_sender.clone();
tokio::spawn(async move {
use tokio::io::{AsyncBufReadExt, BufReader};
let reader = BufReader::new(stdout);
let mut lines = reader.lines();
while let Ok(Some(line)) = lines.next_line().await {
let _ = event_sender.send(LaunchEvent::Data(line));
}
});
}
if let Some(stderr) = child.stderr.take() {
let event_sender = self.event_sender.clone();
tokio::spawn(async move {
use tokio::io::{AsyncBufReadExt, BufReader};
let reader = BufReader::new(stderr);
let mut lines = reader.lines();
while let Ok(Some(line)) = lines.next_line().await {
let _ = event_sender.send(LaunchEvent::Data(line));
}
});
}
let status = child.wait().await?;
if !status.success() {
return Err(crate::error::LateJavaCoreError::Minecraft("Minecraft process failed".to_string()));
}
self.emit(LaunchEvent::Close("Minecraft closed".to_string()));
}
Ok(())
}
}