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.rs508
1 files changed, 460 insertions, 48 deletions
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
index ae74a03..b69912e 100644
--- a/src-tauri/src/main.rs
+++ b/src-tauri/src/main.rs
@@ -6,6 +6,7 @@ use std::sync::Mutex;
use tauri::{Emitter, Manager, State, Window}; // Added Emitter
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Command;
+use serde::Serialize; // Added Serialize
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
@@ -40,6 +41,27 @@ impl MsRefreshTokenState {
}
}
+/// Check if a string contains unresolved placeholders in the form ${...}
+///
+/// After the replacement phase, if a string still contains ${...}, it means
+/// that placeholder variable was not found in the replacements map and is
+/// therefore unresolved. We should skip adding such arguments to avoid
+/// passing malformed arguments to the game launcher.
+fn has_unresolved_placeholder(s: &str) -> bool {
+ // Look for the opening sequence
+ if let Some(start_pos) = s.find("${") {
+ // Check if there's a closing brace after the opening sequence
+ if s[start_pos + 2..].find('}').is_some() {
+ // Found a complete ${...} pattern - this is an unresolved placeholder
+ return true;
+ }
+ // Found ${ but no closing } - also treat as unresolved/malformed
+ return true;
+ }
+ // No ${ found - the string is fully resolved
+ false
+}
+
#[tauri::command]
async fn start_game(
window: Window,
@@ -93,35 +115,16 @@ async fn start_game(
emit_log!(window, format!("Game directory: {:?}", game_dir));
- // 1. Fetch manifest to find the version URL
- emit_log!(window, "Fetching version manifest...".to_string());
- let manifest = core::manifest::fetch_version_manifest()
- .await
- .map_err(|e| e.to_string())?;
+ // 1. Load version (supports both vanilla and modded versions with inheritance)
emit_log!(
window,
- format!("Found {} versions in manifest", manifest.versions.len())
+ format!("Loading version details for {}...", version_id)
);
- // Find the version info
- let version_info = manifest
- .versions
- .iter()
- .find(|v| v.id == version_id)
- .ok_or_else(|| format!("Version {} not found in manifest", version_id))?;
-
- // 2. Fetch specific version JSON (client.jar info)
- emit_log!(
- window,
- format!("Fetching version details for {}...", version_id)
- );
- let version_url = &version_info.url;
- let version_details: core::game_version::GameVersion = reqwest::get(version_url)
- .await
- .map_err(|e| e.to_string())?
- .json()
+ let version_details = core::manifest::load_version(&game_dir, &version_id)
.await
.map_err(|e| e.to_string())?;
+
emit_log!(
window,
format!(
@@ -130,20 +133,33 @@ async fn start_game(
)
);
- // 3. Prepare download tasks
+ // Determine the actual minecraft version for client.jar
+ // (for modded versions, this is the parent vanilla version)
+ let minecraft_version = version_details
+ .inherits_from
+ .clone()
+ .unwrap_or_else(|| version_id.clone());
+
+ // 2. Prepare download tasks
emit_log!(window, "Preparing download tasks...".to_string());
let mut download_tasks = Vec::new();
// --- Client Jar ---
- let client_jar = version_details.downloads.client;
+ // Get downloads from version_details (may be inherited)
+ let downloads = version_details
+ .downloads
+ .as_ref()
+ .ok_or("Version has no downloads information")?;
+ let client_jar = &downloads.client;
let mut client_path = game_dir.join("versions");
- client_path.push(&version_id);
- client_path.push(format!("{}.jar", version_id));
+ client_path.push(&minecraft_version);
+ client_path.push(format!("{}.jar", minecraft_version));
download_tasks.push(core::downloader::DownloadTask {
- url: client_jar.url,
+ url: client_jar.url.clone(),
path: client_path.clone(),
- sha1: Some(client_jar.sha1),
+ sha1: client_jar.sha1.clone(),
+ sha256: None,
});
// --- Libraries ---
@@ -153,7 +169,7 @@ async fn start_game(
for lib in &version_details.libraries {
if core::rules::is_library_allowed(&lib.rules) {
- // 1. Standard Library
+ // 1. Standard Library - check for explicit downloads first
if let Some(downloads) = &lib.downloads {
if let Some(artifact) = &downloads.artifact {
let path_str = artifact
@@ -167,7 +183,8 @@ async fn start_game(
download_tasks.push(core::downloader::DownloadTask {
url: artifact.url.clone(),
path: lib_path,
- sha1: Some(artifact.sha1.clone()),
+ sha1: artifact.sha1.clone(),
+ sha256: None,
});
}
@@ -200,13 +217,30 @@ async fn start_game(
download_tasks.push(core::downloader::DownloadTask {
url: native_artifact.url,
path: native_path.clone(),
- sha1: Some(native_artifact.sha1),
+ sha1: native_artifact.sha1,
+ sha256: None,
});
native_libs_paths.push(native_path);
}
}
}
+ } else {
+ // 3. Library without explicit downloads (mod loader libraries)
+ // Use Maven coordinate resolution
+ if let Some(url) =
+ core::maven::resolve_library_url(&lib.name, None, lib.url.as_deref())
+ {
+ if let Some(lib_path) = core::maven::get_library_path(&lib.name, &libraries_dir)
+ {
+ download_tasks.push(core::downloader::DownloadTask {
+ url,
+ path: lib_path,
+ sha1: None, // Maven libraries often don't have SHA1 in the JSON
+ sha256: None,
+ });
+ }
+ }
}
}
}
@@ -217,8 +251,14 @@ async fn start_game(
let objects_dir = assets_dir.join("objects");
let indexes_dir = assets_dir.join("indexes");
+ // Get asset index (may be inherited from parent)
+ let asset_index = version_details
+ .asset_index
+ .as_ref()
+ .ok_or("Version has no asset index information")?;
+
// Download Asset Index JSON
- let asset_index_path = indexes_dir.join(format!("{}.json", version_details.asset_index.id));
+ let asset_index_path = indexes_dir.join(format!("{}.json", asset_index.id));
// Check if index exists or download it
// Note: We need the content of this file to parse it.
@@ -230,11 +270,8 @@ async fn start_game(
.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)
+ println!("Downloading asset index from {}", asset_index.url);
+ let content = reqwest::get(&asset_index.url)
.await
.map_err(|e| e.to_string())?
.text()
@@ -254,6 +291,7 @@ async fn start_game(
#[derive(serde::Deserialize, Debug)]
struct AssetObject {
hash: String,
+ #[allow(dead_code)]
size: u64,
}
@@ -280,6 +318,7 @@ async fn start_game(
url,
path,
sha1: Some(hash),
+ sha256: None,
});
}
@@ -394,10 +433,7 @@ async fn start_game(
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("${assets_index_name}", asset_index.id.clone());
replacements.insert("${auth_uuid}", account.uuid());
replacements.insert("${auth_access_token}", account.access_token());
replacements.insert("${user_type}", "mojang".to_string());
@@ -449,7 +485,10 @@ async fn start_game(
for (key, replacement) in &replacements {
arg = arg.replace(key, replacement);
}
- args.push(arg);
+ // Skip arguments with unresolved placeholders
+ if !has_unresolved_placeholder(&arg) {
+ args.push(arg);
+ }
} else if let Some(arr) = val.as_array() {
for sub in arr {
if let Some(s) = sub.as_str() {
@@ -457,7 +496,10 @@ async fn start_game(
for (key, replacement) in &replacements {
arg = arg.replace(key, replacement);
}
- args.push(arg);
+ // Skip arguments with unresolved placeholders
+ if !has_unresolved_placeholder(&arg) {
+ args.push(arg);
+ }
}
}
}
@@ -785,7 +827,7 @@ async fn refresh_account(
.map_err(|e| e.to_string())?;
let storage = core::account_storage::AccountStorage::new(app_dir.clone());
- let (stored_account, ms_refresh) = storage
+ let (_stored_account, ms_refresh) = storage
.get_active_account()
.ok_or("No active account found")?;
@@ -807,8 +849,8 @@ async fn refresh_account(
/// Detect Java installations on the system
#[tauri::command]
-async fn detect_java() -> Result<Vec<core::java::JavaInstallation>, String> {
- Ok(core::java::detect_java_installations())
+async fn detect_java(app_handle: tauri::AppHandle) -> Result<Vec<core::java::JavaInstallation>, String> {
+ Ok(core::java::detect_all_java_installations(&app_handle))
}
/// Get recommended Java for a specific Minecraft version
@@ -819,8 +861,349 @@ async fn get_recommended_java(
Ok(core::java::get_recommended_java(required_major_version))
}
+/// Get Adoptium Java download info
+#[tauri::command]
+async fn fetch_adoptium_java(
+ major_version: u32,
+ image_type: String,
+) -> Result<core::java::JavaDownloadInfo, String> {
+ let img_type = match image_type.to_lowercase().as_str() {
+ "jdk" => core::java::ImageType::Jdk,
+ _ => core::java::ImageType::Jre,
+ };
+ core::java::fetch_java_release(major_version, img_type).await
+}
+
+/// Download and install Adoptium Java
+#[tauri::command]
+async fn download_adoptium_java(
+ app_handle: tauri::AppHandle,
+ major_version: u32,
+ image_type: String,
+ custom_path: Option<String>,
+) -> Result<core::java::JavaInstallation, String> {
+ let img_type = match image_type.to_lowercase().as_str() {
+ "jdk" => core::java::ImageType::Jdk,
+ _ => core::java::ImageType::Jre,
+ };
+ let path = custom_path.map(std::path::PathBuf::from);
+ core::java::download_and_install_java(&app_handle, major_version, img_type, path).await
+}
+
+/// Get available Adoptium Java versions
+#[tauri::command]
+async fn fetch_available_java_versions() -> Result<Vec<u32>, String> {
+ core::java::fetch_available_versions().await
+}
+
+/// Fetch Java catalog with platform availability (uses cache)
+#[tauri::command]
+async fn fetch_java_catalog(
+ app_handle: tauri::AppHandle,
+) -> Result<core::java::JavaCatalog, String> {
+ core::java::fetch_java_catalog(&app_handle, false).await
+}
+
+/// Refresh Java catalog (bypass cache)
+#[tauri::command]
+async fn refresh_java_catalog(
+ app_handle: tauri::AppHandle,
+) -> Result<core::java::JavaCatalog, String> {
+ core::java::fetch_java_catalog(&app_handle, true).await
+}
+
+/// Cancel current Java download
+#[tauri::command]
+async fn cancel_java_download() -> Result<(), String> {
+ core::java::cancel_current_download();
+ Ok(())
+}
+
+/// Get pending Java downloads
+#[tauri::command]
+async fn get_pending_java_downloads(
+ app_handle: tauri::AppHandle,
+) -> Result<Vec<core::downloader::PendingJavaDownload>, String> {
+ Ok(core::java::get_pending_downloads(&app_handle))
+}
+
+/// Resume pending Java downloads
+#[tauri::command]
+async fn resume_java_downloads(
+ app_handle: tauri::AppHandle,
+) -> Result<Vec<core::java::JavaInstallation>, String> {
+ core::java::resume_pending_downloads(&app_handle).await
+}
+
+/// Get Minecraft versions supported by Fabric
+#[tauri::command]
+async fn get_fabric_game_versions() -> Result<Vec<core::fabric::FabricGameVersion>, String> {
+ core::fabric::fetch_supported_game_versions()
+ .await
+ .map_err(|e| e.to_string())
+}
+
+/// Get available Fabric loader versions
+#[tauri::command]
+async fn get_fabric_loader_versions() -> Result<Vec<core::fabric::FabricLoaderVersion>, String> {
+ core::fabric::fetch_loader_versions()
+ .await
+ .map_err(|e| e.to_string())
+}
+
+/// Get Fabric loaders available for a specific Minecraft version
+#[tauri::command]
+async fn get_fabric_loaders_for_version(
+ game_version: String,
+) -> Result<Vec<core::fabric::FabricLoaderEntry>, String> {
+ core::fabric::fetch_loaders_for_game_version(&game_version)
+ .await
+ .map_err(|e| e.to_string())
+}
+
+/// Install Fabric loader for a specific Minecraft version
+#[tauri::command]
+async fn install_fabric(
+ window: Window,
+ game_version: String,
+ loader_version: String,
+) -> Result<core::fabric::InstalledFabricVersion, String> {
+ emit_log!(
+ window,
+ format!(
+ "Installing Fabric {} for Minecraft {}...",
+ loader_version, game_version
+ )
+ );
+
+ 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))?;
+
+ let result = core::fabric::install_fabric(&game_dir, &game_version, &loader_version)
+ .await
+ .map_err(|e| e.to_string())?;
+
+ emit_log!(
+ window,
+ format!("Fabric installed successfully: {}", result.id)
+ );
+
+ Ok(result)
+}
+
+/// List installed Fabric versions
+#[tauri::command]
+async fn list_installed_fabric_versions(window: Window) -> Result<Vec<String>, String> {
+ 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))?;
+
+ core::fabric::list_installed_fabric_versions(&game_dir)
+ .await
+ .map_err(|e| e.to_string())
+}
+
+/// Check if Fabric is installed for a specific version
+#[tauri::command]
+async fn is_fabric_installed(
+ window: Window,
+ game_version: String,
+ loader_version: String,
+) -> Result<bool, String> {
+ 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))?;
+
+ Ok(core::fabric::is_fabric_installed(
+ &game_dir,
+ &game_version,
+ &loader_version,
+ ))
+}
+
+/// Get Minecraft versions supported by Forge
+#[tauri::command]
+async fn get_forge_game_versions() -> Result<Vec<String>, String> {
+ core::forge::fetch_supported_game_versions()
+ .await
+ .map_err(|e| e.to_string())
+}
+
+/// Get available Forge versions for a specific Minecraft version
+#[tauri::command]
+async fn get_forge_versions_for_game(
+ game_version: String,
+) -> Result<Vec<core::forge::ForgeVersion>, String> {
+ core::forge::fetch_forge_versions(&game_version)
+ .await
+ .map_err(|e| e.to_string())
+}
+
+/// Install Forge for a specific Minecraft version
+#[tauri::command]
+async fn install_forge(
+ window: Window,
+ game_version: String,
+ forge_version: String,
+) -> Result<core::forge::InstalledForgeVersion, String> {
+ emit_log!(
+ window,
+ format!(
+ "Installing Forge {} for Minecraft {}...",
+ forge_version, game_version
+ )
+ );
+
+ 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))?;
+
+ let result = core::forge::install_forge(&game_dir, &game_version, &forge_version)
+ .await
+ .map_err(|e| e.to_string())?;
+
+ emit_log!(
+ window,
+ format!("Forge installed successfully: {}", result.id)
+ );
+
+ Ok(result)
+}
+
+#[derive(serde::Serialize)]
+struct GithubRelease {
+ tag_name: String,
+ name: String,
+ published_at: String,
+ body: String,
+ html_url: String,
+}
+
+#[tauri::command]
+async fn get_github_releases() -> Result<Vec<GithubRelease>, String> {
+ let client = reqwest::Client::new();
+ let res = client
+ .get("https://api.github.com/repos/HsiangNianian/DropOut/releases")
+ .header("User-Agent", "DropOut-Launcher")
+ .send()
+ .await
+ .map_err(|e| e.to_string())?;
+
+ if !res.status().is_success() {
+ return Err(format!("GitHub API returned status: {}", res.status()));
+ }
+
+ let releases: Vec<serde_json::Value> = res.json().await.map_err(|e| e.to_string())?;
+
+ let mut result = Vec::new();
+ for r in releases {
+ if let (Some(tag), Some(name), Some(date), Some(body), Some(url)) = (
+ r["tag_name"].as_str(),
+ r["name"].as_str(),
+ r["published_at"].as_str(),
+ r["body"].as_str(),
+ r["html_url"].as_str()
+ ) {
+ result.push(GithubRelease {
+ tag_name: tag.to_string(),
+ name: name.to_string(),
+ published_at: date.to_string(),
+ body: body.to_string(),
+ html_url: url.to_string(),
+ });
+ }
+ }
+ Ok(result)
+}
+
+#[derive(Serialize)]
+struct PastebinResponse {
+ url: String,
+}
+
+#[tauri::command]
+async fn upload_to_pastebin(
+ state: State<'_, core::config::ConfigState>,
+ content: String,
+) -> Result<PastebinResponse, String> {
+ // Check content length limit
+ if content.len() > 500 * 1024 {
+ return Err("Log file too large (max 500KB)".to_string());
+ }
+
+ // Extract config values before any async calls to avoid holding MutexGuard across await
+ let (service, api_key) = {
+ let config = state.config.lock().unwrap();
+ (
+ config.log_upload_service.clone(),
+ config.pastebin_api_key.clone(),
+ )
+ };
+
+ let client = reqwest::Client::new();
+
+ match service.as_str() {
+ "pastebin.com" => {
+ let api_key = api_key
+ .ok_or("Pastebin API Key not configured in settings")?;
+
+ let res = client
+ .post("https://pastebin.com/api/api_post.php")
+ .form(&[
+ ("api_dev_key", api_key.as_str()),
+ ("api_option", "paste"),
+ ("api_paste_code", content.as_str()),
+ ("api_paste_private", "1"), // Unlisted
+ ("api_paste_name", "DropOut Launcher Log"),
+ ("api_paste_expire_date", "1W"),
+ ])
+ .send()
+ .await
+ .map_err(|e| e.to_string())?;
+
+ if !res.status().is_success() {
+ return Err(format!("Pastebin upload failed: {}", res.status()));
+ }
+
+ let url = res.text().await.map_err(|e| e.to_string())?;
+ if url.starts_with("Bad API Request") {
+ return Err(format!("Pastebin API error: {}", url));
+ }
+ Ok(PastebinResponse { url })
+ }
+ // Default to paste.rs
+ _ => {
+ let res = client
+ .post("https://paste.rs/")
+ .body(content)
+ .send()
+ .await
+ .map_err(|e| e.to_string())?;
+
+ if !res.status().is_success() {
+ return Err(format!("paste.rs upload failed: {}", res.status()));
+ }
+
+ let url = res.text().await.map_err(|e| e.to_string())?;
+ let url = url.trim().to_string();
+ Ok(PastebinResponse { url })
+ }
+ }
+}
+
fn main() {
tauri::Builder::default()
+ .plugin(tauri_plugin_fs::init())
+ .plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_shell::init())
.manage(core::auth::AccountState::new())
.manage(MsRefreshTokenState::new())
@@ -846,6 +1229,13 @@ fn main() {
println!("[Startup] Loaded saved account");
}
+ // Check for pending Java downloads and notify frontend
+ let pending = core::java::get_pending_downloads(&app.app_handle());
+ if !pending.is_empty() {
+ println!("[Startup] Found {} pending Java download(s)", pending.len());
+ let _ = app.emit("pending-java-downloads", pending.len());
+ }
+
Ok(())
})
.invoke_handler(tauri::generate_handler![
@@ -859,8 +1249,30 @@ fn main() {
start_microsoft_login,
complete_microsoft_login,
refresh_account,
+ // Java commands
detect_java,
- get_recommended_java
+ get_recommended_java,
+ fetch_adoptium_java,
+ download_adoptium_java,
+ fetch_available_java_versions,
+ fetch_java_catalog,
+ refresh_java_catalog,
+ cancel_java_download,
+ get_pending_java_downloads,
+ resume_java_downloads,
+ // Fabric commands
+ get_fabric_game_versions,
+ get_fabric_loader_versions,
+ get_fabric_loaders_for_version,
+ install_fabric,
+ list_installed_fabric_versions,
+ is_fabric_installed,
+ // Forge commands
+ get_forge_game_versions,
+ get_forge_versions_for_game,
+ install_forge,
+ get_github_releases,
+ upload_to_pastebin
])
.run(tauri::generate_context!())
.expect("error while running tauri application");