aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/src-tauri/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src-tauri/src/main.rs')
-rw-r--r--src-tauri/src/main.rs303
1 files changed, 292 insertions, 11 deletions
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
index 6623802..5b1c352 100644
--- a/src-tauri/src/main.rs
+++ b/src-tauri/src/main.rs
@@ -1,17 +1,35 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
-use tauri::{State, Window}; // Added Window
+use tauri::{Manager, State, Window}; // Added Manager
mod core;
mod launcher;
+mod utils;
#[tauri::command]
async fn start_game(
window: Window,
+ state: State<'_, core::auth::AccountState>,
version_id: String
) -> Result<String, String> {
println!("Backend received StartGame for {}", version_id);
+
+ // Check for active account
+ let account = state.active_account.lock().unwrap().clone()
+ .ok_or("No active account found. Please login first.")?;
+
+ // Get App Data Directory (e.g., ~/.local/share/com.dropout.launcher or similar)
+ // The identifier is set in tauri.conf.json.
+ // If not accessible, use a specific logic.
+ let app_handle = window.app_handle();
+ let game_dir = app_handle.path().app_data_dir()
+ .map_err(|e| format!("Failed to get app data dir: {}", e))?;
+
+ // Ensure game directory exists
+ tokio::fs::create_dir_all(&game_dir).await.map_err(|e| e.to_string())?;
+
+ println!("Game Directory: {:?}", game_dir);
// 1. Fetch manifest to find the version URL
let manifest = core::manifest::fetch_version_manifest().await.map_err(|e| e.to_string())?;
@@ -26,25 +44,288 @@ async fn start_game(
.await.map_err(|e| e.to_string())?
.json().await.map_err(|e| e.to_string())?;
- // 3. Prepare download task for Client Jar
+ // 3. Prepare download tasks
+ let mut download_tasks = Vec::new();
+
+ // --- Client Jar ---
let client_jar = version_details.downloads.client;
- // Where to save? Let's use ./versions/{version_id}/{version_id}.jar
- let mut path = std::path::PathBuf::from("versions");
- path.push(&version_id);
- path.push(format!("{}.jar", version_id));
+ let mut client_path = game_dir.join("versions");
+ client_path.push(&version_id);
+ client_path.push(format!("{}.jar", version_id));
- let task = core::downloader::DownloadTask {
+ download_tasks.push(core::downloader::DownloadTask {
url: client_jar.url,
- path,
+ path: client_path,
sha1: Some(client_jar.sha1),
+ });
+
+ // --- Libraries ---
+ println!("Processing libraries...");
+ let libraries_dir = game_dir.join("libraries");
+ let mut native_libs_paths = Vec::new(); // Store paths to native jars for extraction
+
+ for lib in &version_details.libraries {
+ if core::rules::is_library_allowed(&lib.rules) {
+ // 1. Standard Library
+ if let Some(downloads) = &lib.downloads {
+ if let Some(artifact) = &downloads.artifact {
+ let path_str = artifact.path.clone().unwrap_or_else(|| {
+ format!("{}.jar", lib.name)
+ });
+
+ let mut lib_path = libraries_dir.clone();
+ lib_path.push(path_str);
+
+ download_tasks.push(core::downloader::DownloadTask {
+ url: artifact.url.clone(),
+ path: lib_path,
+ sha1: Some(artifact.sha1.clone()),
+ });
+ }
+
+ // 2. Native Library (classifiers)
+ // e.g. "natives-linux": { ... }
+ if let Some(classifiers) = &downloads.classifiers {
+ // Determine the key based on OS
+ // Linux usually "natives-linux", Windows "natives-windows", Mac "natives-osx" (or macos)
+ let os_key = if cfg!(target_os = "linux") {
+ "natives-linux"
+ } else if cfg!(target_os = "windows") {
+ "natives-windows"
+ } else if cfg!(target_os = "macos") {
+ "natives-osx" // or natives-macos? check json
+ } else {
+ ""
+ };
+
+ if let Some(native_artifact_value) = classifiers.get(os_key) {
+ // Parse it as DownloadArtifact
+ if let Ok(native_artifact) = serde_json::from_value::<core::game_version::DownloadArtifact>(native_artifact_value.clone()) {
+ let path_str = native_artifact.path.clone().unwrap(); // Natives usually have path
+ let mut native_path = libraries_dir.clone();
+ native_path.push(&path_str);
+
+ download_tasks.push(core::downloader::DownloadTask {
+ url: native_artifact.url,
+ path: native_path.clone(),
+ sha1: Some(native_artifact.sha1),
+ });
+
+ native_libs_paths.push(native_path);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // --- Assets ---
+ println!("Fetching asset index...");
+ let assets_dir = game_dir.join("assets");
+ let objects_dir = assets_dir.join("objects");
+ let indexes_dir = assets_dir.join("indexes");
+
+ // Download Asset Index JSON
+ let asset_index_path = indexes_dir.join(format!("{}.json", version_details.asset_index.id));
+
+ // Check if index exists or download it
+ // Note: We need the content of this file to parse it.
+ // If we just add it to download_tasks, we can't parse it *now*.
+ // So we must download it immediately (await) before processing objects.
+
+ let asset_index_content: String = if asset_index_path.exists() {
+ tokio::fs::read_to_string(&asset_index_path).await.map_err(|e| e.to_string())?
+ } else {
+ println!("Downloading asset index from {}", version_details.asset_index.url);
+ let content = reqwest::get(&version_details.asset_index.url)
+ .await.map_err(|e| e.to_string())?
+ .text().await.map_err(|e| e.to_string())?;
+
+ // Save it for next time
+ tokio::fs::create_dir_all(&indexes_dir).await.map_err(|e| e.to_string())?;
+ tokio::fs::write(&asset_index_path, &content).await.map_err(|e| e.to_string())?;
+ content
};
- println!("Starting download of client jar...");
+ #[derive(serde::Deserialize, Debug)]
+ struct AssetObject {
+ hash: String,
+ size: u64,
+ }
+
+ #[derive(serde::Deserialize, Debug)]
+ struct AssetIndexJson {
+ objects: std::collections::HashMap<String, AssetObject>,
+ }
+
+ let asset_index_parsed: AssetIndexJson = serde_json::from_str(&asset_index_content).map_err(|e| e.to_string())?;
+
+ println!("Processing {} assets...", asset_index_parsed.objects.len());
+
+ for (_name, object) in asset_index_parsed.objects {
+ let hash = object.hash;
+ let prefix = &hash[0..2];
+ let path = objects_dir.join(prefix).join(&hash);
+ let url = format!("https://resources.download.minecraft.net/{}/{}", prefix, hash);
+
+ download_tasks.push(core::downloader::DownloadTask {
+ url,
+ path,
+ sha1: Some(hash),
+ });
+ }
+
+
+ println!("Total download tasks (Client + Libs + Assets): {}", download_tasks.len());
// 4. Start Download
- core::downloader::download_files(window, vec![task]).await.map_err(|e| e.to_string())?;
+ core::downloader::download_files(window, download_tasks).await.map_err(|e| e.to_string())?;
+
+ // 5. Extract Natives
+ println!("Extracting natives...");
+ let natives_dir = game_dir.join("versions").join(&version_id).join("natives");
+
+ // Clean old natives if they exist to prevent conflicts
+ if natives_dir.exists() {
+ tokio::fs::remove_dir_all(&natives_dir).await.map_err(|e| e.to_string())?;
+ }
+ tokio::fs::create_dir_all(&natives_dir).await.map_err(|e| e.to_string())?;
+
+ for path in native_libs_paths {
+ if path.exists() {
+ println!("Extracting native: {:?}", path);
+ utils::zip::extract_zip(&path, &natives_dir)?;
+ }
+ }
+
+ // 6. Construct Classpath
+ let cp_separator = if cfg!(target_os = "windows") { ";" } else { ":" };
+ let mut classpath_entries = Vec::new();
+
+ // Add libraries
+ for lib in &version_details.libraries {
+ if core::rules::is_library_allowed(&lib.rules) {
+ if let Some(downloads) = &lib.downloads {
+ if let Some(artifact) = &downloads.artifact {
+ let path_str = artifact.path.clone().unwrap_or_else(|| {
+ format!("{}.jar", lib.name)
+ });
+ let lib_path = libraries_dir.join(path_str);
+ classpath_entries.push(lib_path.to_string_lossy().to_string());
+ }
+ }
+ }
+ }
+ // Add client jar
+ classpath_entries.push(client_path.to_string_lossy().to_string());
+
+ let classpath = classpath_entries.join(cp_separator);
- Ok(format!("Download complete for {}", version_id))
+ // 7. Prepare Arguments
+ let mut args = Vec::new();
+ let natives_path = natives_dir.to_string_lossy().to_string();
+
+ // 7a. JVM Arguments (Simplified for now)
+ // We inject standard convenient defaults.
+ // TODO: Parse 'arguments.jvm' from version.json for full compatibility (Mac M1 support etc)
+ args.push(format!("-Djava.library.path={}", natives_path));
+ args.push("-Xmx2G".to_string()); // Default memory
+ args.push("-Xms1G".to_string());
+ args.push("-cp".to_string());
+ args.push(classpath);
+
+ // 7b. Main Class
+ args.push(version_details.main_class.clone());
+
+ // 7c. Game Arguments
+ // Replacements map
+ let mut replacements = std::collections::HashMap::new();
+ replacements.insert("${auth_player_name}", account.username.clone());
+ replacements.insert("${version_name}", version_id.clone());
+ replacements.insert("${game_directory}", game_dir.to_string_lossy().to_string());
+ replacements.insert("${assets_root}", assets_dir.to_string_lossy().to_string());
+ replacements.insert("${assets_index_name}", version_details.asset_index.id.clone());
+ replacements.insert("${auth_uuid}", account.uuid.clone());
+ replacements.insert("${auth_access_token}", "null".to_string()); // Offline
+ replacements.insert("${user_type}", "mojang".to_string());
+ replacements.insert("${version_type}", "release".to_string());
+
+ if let Some(minecraft_arguments) = &version_details.minecraft_arguments {
+ // Legacy string
+ for part in minecraft_arguments.split_whitespace() {
+ let mut arg = part.to_string();
+ for (key, val) in &replacements {
+ arg = arg.replace(key, val);
+ }
+ args.push(arg);
+ }
+ } else if let Some(args_obj) = &version_details.arguments {
+ if let Some(game_args) = &args_obj.game {
+ // Can be array of strings or objects
+ if let Some(list) = game_args.as_array() {
+ for item in list {
+ if let Some(s) = item.as_str() {
+ let mut arg = s.to_string();
+ for (key, val) in &replacements {
+ arg = arg.replace(key, val);
+ }
+ args.push(arg);
+ } else if let Some(obj) = item.as_object() {
+ // Check rules
+ // Simplified: if it has "value", and rules pass.
+ // For now, assuming rules pass if no "rules" field or simplistic check
+ // Ideally we should implement a helper to check rules for args just like libs
+
+ let allow = if let Some(rules_val) = obj.get("rules") {
+ if let Ok(rules) = serde_json::from_value::<Vec<core::game_version::Rule>>(rules_val.clone()) {
+ core::rules::is_library_allowed(&Some(rules))
+ } else {
+ true // Parse error, assume allow? or disallow.
+ }
+ } else {
+ true
+ };
+
+ if allow {
+ if let Some(val) = obj.get("value") {
+ if let Some(s) = val.as_str() {
+ let mut arg = s.to_string();
+ for (key, replacement) in &replacements {
+ arg = arg.replace(key, replacement);
+ }
+ args.push(arg);
+ } else if let Some(arr) = val.as_array() {
+ for sub in arr {
+ if let Some(s) = sub.as_str() {
+ let mut arg = s.to_string();
+ for (key, replacement) in &replacements {
+ arg = arg.replace(key, replacement);
+ }
+ args.push(arg);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ println!("Launching game with {} args...", args.len());
+
+ // Spawn the process
+ let mut command = std::process::Command::new("java");
+ command.args(&args);
+ command.current_dir(&game_dir); // Run in game directory
+
+ // We can just spawn it and let it detach, or keep track of it.
+ // For now, let's spawn and verify it started.
+ match command.spawn() {
+ Ok(_) => Ok(format!("Launched Minecraft {} successfully!", version_id)),
+ Err(e) => Err(format!("Failed to launch java: {}", e)),
+ }
}
#[tauri::command]