aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/src-tauri/src/main.rs
diff options
context:
space:
mode:
author简律纯 <i@jyunko.cn>2026-01-18 12:24:29 +0800
committerGitHub <noreply@github.com>2026-01-18 12:24:29 +0800
commitfd00ac6878b2cee9337b9e92d0c990ecdce9a346 (patch)
treebb5540f763dc0061877c9d9ac53747d79193eecc /src-tauri/src/main.rs
parentad36e0ce82770f9b3509ddb1cf96bc3422969806 (diff)
parent6d82ab2275130f3bafdb7ec664297eb700321526 (diff)
downloadDropOut-fd00ac6878b2cee9337b9e92d0c990ecdce9a346.tar.gz
DropOut-fd00ac6878b2cee9337b9e92d0c990ecdce9a346.zip
Merge pull request #58 from HsiangNianian/main
Diffstat (limited to 'src-tauri/src/main.rs')
-rw-r--r--src-tauri/src/main.rs765
1 files changed, 671 insertions, 94 deletions
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
index 3671166..2871b03 100644
--- a/src-tauri/src/main.rs
+++ b/src-tauri/src/main.rs
@@ -1,12 +1,12 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
+use serde::Serialize;
use std::process::Stdio;
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
+use tokio::process::Command; // Added Serialize
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
@@ -67,11 +67,17 @@ async fn start_game(
window: Window,
auth_state: State<'_, core::auth::AccountState>,
config_state: State<'_, core::config::ConfigState>,
+ assistant_state: State<'_, core::assistant::AssistantState>,
+ instance_state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
version_id: String,
) -> Result<String, String> {
emit_log!(
window,
- format!("Starting game launch for version: {}", version_id)
+ format!(
+ "Starting game launch for version: {} in instance: {}",
+ version_id, instance_id
+ )
);
// Check for active account
@@ -83,14 +89,7 @@ async fn start_game(
.clone()
.ok_or("No active account found. Please login first.")?;
- let account_type = match &account {
- core::auth::Account::Offline(_) => "Offline",
- core::auth::Account::Microsoft(_) => "Microsoft",
- };
- emit_log!(
- window,
- format!("Account found: {} ({})", account.username(), account_type)
- );
+ emit_log!(window, format!("Account found: {}", account.username()));
let config = config_state.config.lock().unwrap().clone();
emit_log!(window, format!("Java path: {}", config.java_path));
@@ -99,14 +98,10 @@ async fn start_game(
format!("Memory: {}MB - {}MB", config.min_memory, config.max_memory)
);
- // 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))?;
+ // Get game directory from instance
+ let game_dir = instance_state
+ .get_instance_game_dir(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
// Ensure game directory exists
tokio::fs::create_dir_all(&game_dir)
@@ -123,10 +118,11 @@ async fn start_game(
// First, load the local version to get the original inheritsFrom value
// (before merge clears it)
- let original_inherits_from = match core::manifest::load_local_version(&game_dir, &version_id).await {
- Ok(local_version) => local_version.inherits_from.clone(),
- Err(_) => None,
- };
+ let original_inherits_from =
+ match core::manifest::load_local_version(&game_dir, &version_id).await {
+ Ok(local_version) => local_version.inherits_from.clone(),
+ Err(_) => None,
+ };
let version_details = core::manifest::load_version(&game_dir, &version_id)
.await
@@ -142,8 +138,123 @@ async fn start_game(
// Determine the actual minecraft version for client.jar
// (for modded versions, this is the parent vanilla version)
- let minecraft_version = original_inherits_from
- .unwrap_or_else(|| version_id.clone());
+ let minecraft_version = original_inherits_from.unwrap_or_else(|| version_id.clone());
+
+ // Get required Java version from version file's javaVersion field
+ // The version file (after merging with parent) should contain the correct javaVersion
+ let required_java_major = version_details
+ .java_version
+ .as_ref()
+ .map(|jv| jv.major_version);
+
+ // For older Minecraft versions (1.13.x and below), if javaVersion specifies Java 8,
+ // we should only allow Java 8 (not higher) due to compatibility issues with old Forge
+ // For newer versions, javaVersion.majorVersion is the minimum required version
+ let max_java_major = if let Some(required) = required_java_major {
+ // If version file specifies Java 8, enforce it as maximum (old versions need exactly Java 8)
+ // For Java 9+, allow that version or higher
+ if required <= 8 {
+ Some(8)
+ } else {
+ None // No upper bound for Java 9+
+ }
+ } else {
+ // If version file doesn't specify javaVersion, this shouldn't happen for modern versions
+ // But if it does, we can't determine compatibility - log a warning
+ emit_log!(
+ window,
+ "Warning: Version file does not specify javaVersion. Using system default Java."
+ .to_string()
+ );
+ None
+ };
+
+ // Check if configured Java is compatible
+ let app_handle = window.app_handle();
+ let mut java_path_to_use = config.java_path.clone();
+ if !java_path_to_use.is_empty() && java_path_to_use != "java" {
+ let is_compatible =
+ core::java::is_java_compatible(&java_path_to_use, required_java_major, max_java_major);
+
+ if !is_compatible {
+ emit_log!(
+ window,
+ format!(
+ "Configured Java version may not be compatible. Looking for compatible Java..."
+ )
+ );
+
+ // Try to find a compatible Java version
+ if let Some(compatible_java) =
+ core::java::get_compatible_java(app_handle, required_java_major, max_java_major)
+ {
+ emit_log!(
+ window,
+ format!(
+ "Found compatible Java {} at: {}",
+ compatible_java.version, compatible_java.path
+ )
+ );
+ java_path_to_use = compatible_java.path;
+ } else {
+ let version_constraint = if let Some(max) = max_java_major {
+ if let Some(min) = required_java_major {
+ if min == max as u64 {
+ format!("Java {}", min)
+ } else {
+ format!("Java {} to {}", min, max)
+ }
+ } else {
+ format!("Java {} (or lower)", max)
+ }
+ } else if let Some(min) = required_java_major {
+ format!("Java {} or higher", min)
+ } else {
+ "any Java version".to_string()
+ };
+
+ return Err(format!(
+ "No compatible Java installation found. This version requires {}. Please install a compatible Java version in settings.",
+ version_constraint
+ ));
+ }
+ }
+ } else {
+ // No Java configured, try to find a compatible one
+ if let Some(compatible_java) =
+ core::java::get_compatible_java(app_handle, required_java_major, max_java_major)
+ {
+ emit_log!(
+ window,
+ format!(
+ "Using Java {} at: {}",
+ compatible_java.version, compatible_java.path
+ )
+ );
+ java_path_to_use = compatible_java.path;
+ } else {
+ let version_constraint = if let Some(max) = max_java_major {
+ if let Some(min) = required_java_major {
+ if min == max as u64 {
+ format!("Java {}", min)
+ } else {
+ format!("Java {} to {}", min, max)
+ }
+ } else {
+ format!("Java {} (or lower)", max)
+ }
+ } else if let Some(min) = required_java_major {
+ format!("Java {} or higher", min)
+ } else {
+ "any Java version".to_string()
+ };
+
+ return Err(format!(
+ "No compatible Java installation found. This version requires {}. Please install a compatible Java version in settings.",
+ version_constraint
+ ));
+ }
+ }
// 2. Prepare download tasks
emit_log!(window, "Preparing download tasks...".to_string());
@@ -527,17 +638,67 @@ async fn start_game(
window,
format!("Preparing to launch game with {} arguments...", args.len())
);
- // Debug: Log arguments (only first few to avoid spam)
- if args.len() > 10 {
- emit_log!(window, format!("First 10 args: {:?}", &args[..10]));
- }
+
+ // Format Java command with sensitive information masked
+ let masked_args: Vec<String> = args
+ .iter()
+ .enumerate()
+ .map(|(i, arg)| {
+ // Check if previous argument was a sensitive flag
+ if i > 0 {
+ let prev_arg = &args[i - 1];
+ if prev_arg == "--accessToken" || prev_arg == "--uuid" {
+ return "***".to_string();
+ }
+ }
+
+ // Mask sensitive argument values
+ if arg == "--accessToken" || arg == "--uuid" {
+ arg.clone()
+ } else if arg.starts_with("token:") {
+ // Mask token: prefix tokens (Session ID format)
+ "token:***".to_string()
+ } else if arg.len() > 100
+ && arg.contains('.')
+ && !arg.contains('/')
+ && !arg.contains('\\')
+ && !arg.contains(':')
+ {
+ // Likely a JWT token (very long string with dots, no paths)
+ "***".to_string()
+ } else if arg.len() == 36
+ && arg.contains('-')
+ && arg.chars().all(|c| c.is_ascii_hexdigit() || c == '-')
+ {
+ // Likely a UUID (36 chars with dashes)
+ "***".to_string()
+ } else {
+ arg.clone()
+ }
+ })
+ .collect();
+
+ // Format as actual Java command (properly quote arguments with spaces)
+ let masked_args_str: Vec<String> = masked_args
+ .iter()
+ .map(|arg| {
+ if arg.contains(' ') {
+ format!("\"{}\"", arg)
+ } else {
+ arg.clone()
+ }
+ })
+ .collect();
+
+ let java_command = format!("{} {}", java_path_to_use, masked_args_str.join(" "));
+ emit_log!(window, format!("Java Command: {}", java_command));
// Spawn the process
emit_log!(
window,
- format!("Starting Java process: {}", config.java_path)
+ format!("Starting Java process: {}", java_path_to_use)
);
- let mut command = Command::new(&config.java_path);
+ let mut command = Command::new(&java_path_to_use);
command.args(&args);
command.current_dir(&game_dir); // Run in game directory
command.stdout(Stdio::piped());
@@ -557,7 +718,7 @@ async fn start_game(
// Spawn and handle output
let mut child = command
.spawn()
- .map_err(|e| format!("Failed to launch java: {}", e))?;
+ .map_err(|e| format!("Failed to launch Java at '{}': {}\nPlease check your Java installation and path configuration in Settings.", java_path_to_use, e))?;
emit_log!(window, "Java process started successfully".to_string());
@@ -577,9 +738,11 @@ async fn start_game(
);
let window_rx = window.clone();
+ let assistant_arc = assistant_state.assistant.clone();
tokio::spawn(async move {
let mut reader = BufReader::new(stdout).lines();
while let Ok(Some(line)) = reader.next_line().await {
+ assistant_arc.lock().unwrap().add_log(line.clone());
let _ = window_rx.emit("game-stdout", line);
}
// Emit log when stdout stream ends (game closing)
@@ -587,10 +750,12 @@ async fn start_game(
});
let window_rx_err = window.clone();
+ let assistant_arc_err = assistant_state.assistant.clone();
let window_exit = window.clone();
tokio::spawn(async move {
let mut reader = BufReader::new(stderr).lines();
while let Ok(Some(line)) = reader.next_line().await {
+ assistant_arc_err.lock().unwrap().add_log(line.clone());
let _ = window_rx_err.emit("game-stderr", line);
}
// Emit log when stderr stream ends
@@ -685,29 +850,73 @@ fn parse_jvm_arguments(
}
#[tauri::command]
-async fn get_versions() -> Result<Vec<core::manifest::Version>, String> {
+async fn get_versions(window: Window) -> Result<Vec<core::manifest::Version>, 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))?;
+
match core::manifest::fetch_version_manifest().await {
- Ok(manifest) => Ok(manifest.versions),
+ Ok(manifest) => {
+ let mut versions = manifest.versions;
+
+ // For each version, try to load Java version info and check installation status
+ for version in &mut versions {
+ // Check if version is installed
+ let version_dir = game_dir.join("versions").join(&version.id);
+ let json_path = version_dir.join(format!("{}.json", version.id));
+ let client_jar_path = version_dir.join(format!("{}.jar", version.id));
+
+ // Version is installed if both JSON and client jar exist
+ let is_installed = json_path.exists() && client_jar_path.exists();
+ version.is_installed = Some(is_installed);
+
+ // If installed, try to load the version JSON to get javaVersion
+ if is_installed {
+ if let Ok(game_version) =
+ core::manifest::load_local_version(&game_dir, &version.id).await
+ {
+ if let Some(java_ver) = game_version.java_version {
+ version.java_version = Some(java_ver.major_version);
+ }
+ }
+ }
+ }
+
+ Ok(versions)
+ }
Err(e) => Err(e.to_string()),
}
}
/// Check if a version is installed (has client.jar)
#[tauri::command]
-async fn check_version_installed(window: Window, version_id: 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))?;
+async fn check_version_installed(
+ _window: Window,
+ instance_state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
+ version_id: String,
+) -> Result<bool, String> {
+ let game_dir = instance_state
+ .get_instance_game_dir(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
// For modded versions, check the parent vanilla version
let minecraft_version = if version_id.starts_with("fabric-loader-") {
// Format: fabric-loader-X.X.X-1.20.4
- version_id.split('-').last().unwrap_or(&version_id).to_string()
+ version_id
+ .split('-')
+ .next_back()
+ .unwrap_or(&version_id)
+ .to_string()
} else if version_id.contains("-forge-") {
// Format: 1.20.4-forge-49.0.38
- version_id.split("-forge-").next().unwrap_or(&version_id).to_string()
+ version_id
+ .split("-forge-")
+ .next()
+ .unwrap_or(&version_id)
+ .to_string()
} else {
version_id.clone()
};
@@ -725,19 +934,24 @@ async fn check_version_installed(window: Window, version_id: String) -> Result<b
async fn install_version(
window: Window,
config_state: State<'_, core::config::ConfigState>,
+ instance_state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
version_id: String,
) -> Result<(), String> {
emit_log!(
window,
- format!("Starting installation for version: {}", version_id)
+ format!(
+ "Starting installation for version: {} in instance: {}",
+ version_id, instance_id
+ )
);
let config = config_state.config.lock().unwrap().clone();
- 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))?;
+
+ // Get game directory from instance
+ let game_dir = instance_state
+ .get_instance_game_dir(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
// Ensure game directory exists
tokio::fs::create_dir_all(&game_dir)
@@ -753,21 +967,24 @@ async fn install_version(
);
// First, try to fetch the vanilla version from Mojang and save it locally
- let version_details = match core::manifest::load_local_version(&game_dir, &version_id).await {
+ let _version_details = match core::manifest::load_local_version(&game_dir, &version_id).await {
Ok(v) => v,
Err(_) => {
// Not found locally, fetch from Mojang
- emit_log!(window, format!("Fetching version {} from Mojang...", version_id));
+ emit_log!(
+ window,
+ format!("Fetching version {} from Mojang...", version_id)
+ );
let fetched = core::manifest::fetch_vanilla_version(&version_id)
.await
.map_err(|e| e.to_string())?;
-
+
// Save the version JSON locally
emit_log!(window, format!("Saving version JSON..."));
core::manifest::save_local_version(&game_dir, &fetched)
.await
.map_err(|e| e.to_string())?;
-
+
fetched
}
};
@@ -983,6 +1200,9 @@ async fn install_version(
format!("Installation of {} completed successfully!", version_id)
);
+ // Emit event to notify frontend that version installation is complete
+ let _ = window.emit("version-installed", &version_id);
+
Ok(())
}
@@ -1060,6 +1280,38 @@ async fn save_settings(
}
#[tauri::command]
+async fn get_config_path(state: State<'_, core::config::ConfigState>) -> Result<String, String> {
+ Ok(state.file_path.to_string_lossy().to_string())
+}
+
+#[tauri::command]
+async fn read_raw_config(state: State<'_, core::config::ConfigState>) -> Result<String, String> {
+ tokio::fs::read_to_string(&state.file_path)
+ .await
+ .map_err(|e| e.to_string())
+}
+
+#[tauri::command]
+async fn save_raw_config(
+ state: State<'_, core::config::ConfigState>,
+ content: String,
+) -> Result<(), String> {
+ // Validate JSON
+ let new_config: core::config::LauncherConfig =
+ serde_json::from_str(&content).map_err(|e| format!("Invalid JSON: {}", e))?;
+
+ // Save to file
+ tokio::fs::write(&state.file_path, &content)
+ .await
+ .map_err(|e| e.to_string())?;
+
+ // Update in-memory state
+ *state.config.lock().unwrap() = new_config;
+
+ Ok(())
+}
+
+#[tauri::command]
async fn start_microsoft_login() -> Result<core::auth::DeviceCodeResponse, String> {
core::auth::start_device_flow().await
}
@@ -1170,7 +1422,9 @@ async fn refresh_account(
/// Detect Java installations on the system
#[tauri::command]
-async fn detect_java(app_handle: tauri::AppHandle) -> Result<Vec<core::java::JavaInstallation>, String> {
+async fn detect_java(
+ app_handle: tauri::AppHandle,
+) -> Result<Vec<core::java::JavaInstallation>, String> {
Ok(core::java::detect_all_java_installations(&app_handle))
}
@@ -1286,22 +1540,22 @@ async fn get_fabric_loaders_for_version(
#[tauri::command]
async fn install_fabric(
window: Window,
+ instance_state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
game_version: String,
loader_version: String,
) -> Result<core::fabric::InstalledFabricVersion, String> {
emit_log!(
window,
format!(
- "Installing Fabric {} for Minecraft {}...",
- loader_version, game_version
+ "Installing Fabric {} for Minecraft {} in instance {}...",
+ loader_version, game_version, instance_id
)
);
- 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 game_dir = instance_state
+ .get_instance_game_dir(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
let result = core::fabric::install_fabric(&game_dir, &game_version, &loader_version)
.await
@@ -1312,23 +1566,172 @@ async fn install_fabric(
format!("Fabric installed successfully: {}", result.id)
);
+ // Emit event to notify frontend
+ let _ = window.emit("fabric-installed", &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))?;
+async fn list_installed_fabric_versions(
+ _window: Window,
+ instance_state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
+) -> Result<Vec<String>, String> {
+ let game_dir = instance_state
+ .get_instance_game_dir(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
core::fabric::list_installed_fabric_versions(&game_dir)
.await
.map_err(|e| e.to_string())
}
+/// Get Java version requirement for a specific version
+#[tauri::command]
+async fn get_version_java_version(
+ _window: Window,
+ instance_state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
+ version_id: String,
+) -> Result<Option<u64>, String> {
+ let game_dir = instance_state
+ .get_instance_game_dir(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
+
+ // Try to load the version JSON to get javaVersion
+ match core::manifest::load_version(&game_dir, &version_id).await {
+ Ok(game_version) => Ok(game_version.java_version.map(|jv| jv.major_version)),
+ Err(_) => Ok(None), // Version not found or can't be loaded
+ }
+}
+
+/// Version metadata for display in the UI
+#[derive(serde::Serialize)]
+struct VersionMetadata {
+ id: String,
+ #[serde(rename = "javaVersion")]
+ java_version: Option<u64>,
+ #[serde(rename = "isInstalled")]
+ is_installed: bool,
+}
+
+/// Delete a version (remove version directory)
+#[tauri::command]
+async fn delete_version(
+ window: Window,
+ instance_state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
+ version_id: String,
+) -> Result<(), String> {
+ let game_dir = instance_state
+ .get_instance_game_dir(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
+
+ let version_dir = game_dir.join("versions").join(&version_id);
+
+ if !version_dir.exists() {
+ return Err(format!("Version {} not found", version_id));
+ }
+
+ // Remove the entire version directory
+ tokio::fs::remove_dir_all(&version_dir)
+ .await
+ .map_err(|e| format!("Failed to delete version: {}", e))?;
+
+ // Emit event to notify frontend
+ let _ = window.emit("version-deleted", &version_id);
+
+ Ok(())
+}
+
+/// Get detailed metadata for a specific version
+#[tauri::command]
+async fn get_version_metadata(
+ _window: Window,
+ instance_state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
+ version_id: String,
+) -> Result<VersionMetadata, String> {
+ let game_dir = instance_state
+ .get_instance_game_dir(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
+
+ // Initialize metadata
+ let mut metadata = VersionMetadata {
+ id: version_id.clone(),
+ java_version: None,
+ is_installed: false,
+ };
+
+ // Check if version is in manifest and get Java version if available
+ if let Ok(manifest) = core::manifest::fetch_version_manifest().await {
+ if let Some(version_entry) = manifest.versions.iter().find(|v| v.id == version_id) {
+ // Note: version_entry.java_version is only set if version is installed locally
+ // For uninstalled versions, we'll fetch from remote below
+ if let Some(java_ver) = version_entry.java_version {
+ metadata.java_version = Some(java_ver);
+ }
+ }
+ }
+
+ // Check if version is installed (both JSON and client jar must exist)
+ let version_dir = game_dir.join("versions").join(&version_id);
+ let json_path = version_dir.join(format!("{}.json", version_id));
+
+ // For modded versions, check the parent vanilla version's client jar
+ let client_jar_path = if version_id.starts_with("fabric-loader-") {
+ // Format: fabric-loader-X.X.X-1.20.4
+ let minecraft_version = version_id
+ .split('-')
+ .next_back()
+ .unwrap_or(&version_id)
+ .to_string();
+ game_dir
+ .join("versions")
+ .join(&minecraft_version)
+ .join(format!("{}.jar", minecraft_version))
+ } else if version_id.contains("-forge-") {
+ // Format: 1.20.4-forge-49.0.38
+ let minecraft_version = version_id
+ .split("-forge-")
+ .next()
+ .unwrap_or(&version_id)
+ .to_string();
+ game_dir
+ .join("versions")
+ .join(&minecraft_version)
+ .join(format!("{}.jar", minecraft_version))
+ } else {
+ version_dir.join(format!("{}.jar", version_id))
+ };
+
+ metadata.is_installed = json_path.exists() && client_jar_path.exists();
+
+ // Try to get Java version - from local if installed, or from remote if not
+ if metadata.is_installed {
+ // If installed, load from local version JSON
+ if let Ok(game_version) = core::manifest::load_version(&game_dir, &version_id).await {
+ if let Some(java_ver) = game_version.java_version {
+ metadata.java_version = Some(java_ver.major_version);
+ }
+ }
+ } else if metadata.java_version.is_none() {
+ // If not installed and we don't have Java version yet, try to fetch from remote
+ // This is for vanilla versions that are not installed
+ if !version_id.starts_with("fabric-loader-") && !version_id.contains("-forge-") {
+ if let Ok(game_version) = core::manifest::fetch_vanilla_version(&version_id).await {
+ if let Some(java_ver) = game_version.java_version {
+ metadata.java_version = Some(java_ver.major_version);
+ }
+ }
+ }
+ }
+
+ Ok(metadata)
+}
+
/// Installed version info
#[derive(serde::Serialize)]
struct InstalledVersion {
@@ -1340,12 +1743,14 @@ struct InstalledVersion {
/// List all installed versions from the data directory
/// Simply lists all folders in the versions directory without validation
#[tauri::command]
-async fn list_installed_versions(window: Window) -> Result<Vec<InstalledVersion>, 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))?;
+async fn list_installed_versions(
+ _window: Window,
+ instance_state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
+) -> Result<Vec<InstalledVersion>, String> {
+ let game_dir = instance_state
+ .get_instance_game_dir(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
let versions_dir = game_dir.join("versions");
let mut installed = Vec::new();
@@ -1425,15 +1830,15 @@ async fn list_installed_versions(window: Window) -> Result<Vec<InstalledVersion>
/// Check if Fabric is installed for a specific version
#[tauri::command]
async fn is_fabric_installed(
- window: Window,
+ _window: Window,
+ instance_state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
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))?;
+ let game_dir = instance_state
+ .get_instance_game_dir(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
Ok(core::fabric::is_fabric_installed(
&game_dir,
@@ -1465,37 +1870,40 @@ async fn get_forge_versions_for_game(
async fn install_forge(
window: Window,
config_state: State<'_, core::config::ConfigState>,
+ instance_state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
game_version: String,
forge_version: String,
) -> Result<core::forge::InstalledForgeVersion, String> {
emit_log!(
window,
format!(
- "Installing Forge {} for Minecraft {}...",
- forge_version, game_version
+ "Installing Forge {} for Minecraft {} in instance {}...",
+ forge_version, game_version, instance_id
)
);
- 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 game_dir = instance_state
+ .get_instance_game_dir(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))?;
// Get Java path from config or detect
let config = config_state.config.lock().unwrap().clone();
+ let app_handle = window.app_handle();
let java_path_str = if !config.java_path.is_empty() && config.java_path != "java" {
config.java_path.clone()
} else {
// Try to find a suitable Java installation
- let javas = core::java::detect_all_java_installations(&app_handle);
+ let javas = core::java::detect_all_java_installations(app_handle);
if let Some(java) = javas.first() {
java.path.clone()
} else {
- return Err("No Java installation found. Please configure Java in settings.".to_string());
+ return Err(
+ "No Java installation found. Please configure Java in settings.".to_string(),
+ );
}
};
- let java_path = std::path::PathBuf::from(&java_path_str);
+ let java_path = utils::path::normalize_java_path(&java_path_str)?;
emit_log!(window, "Running Forge installer...".to_string());
@@ -1504,7 +1912,10 @@ async fn install_forge(
.await
.map_err(|e| format!("Forge installer failed: {}", e))?;
- emit_log!(window, "Forge installer completed, creating version profile...".to_string());
+ emit_log!(
+ window,
+ "Forge installer completed, creating version profile...".to_string()
+ );
// Now create the version JSON
let result = core::forge::install_forge(&game_dir, &game_version, &forge_version)
@@ -1516,6 +1927,9 @@ async fn install_forge(
format!("Forge installed successfully: {}", result.id)
);
+ // Emit event to notify frontend
+ let _ = window.emit("forge-installed", &result.id);
+
Ok(result)
}
@@ -1551,7 +1965,7 @@ async fn get_github_releases() -> Result<Vec<GithubRelease>, String> {
r["name"].as_str(),
r["published_at"].as_str(),
r["body"].as_str(),
- r["html_url"].as_str()
+ r["html_url"].as_str(),
) {
result.push(GithubRelease {
tag_name: tag.to_string(),
@@ -1593,8 +2007,7 @@ async fn upload_to_pastebin(
match service.as_str() {
"pastebin.com" => {
- let api_key = api_key
- .ok_or("Pastebin API Key not configured in settings")?;
+ 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")
@@ -1640,6 +2053,139 @@ async fn upload_to_pastebin(
}
}
+#[tauri::command]
+async fn assistant_check_health(
+ assistant_state: State<'_, core::assistant::AssistantState>,
+ config_state: State<'_, core::config::ConfigState>,
+) -> Result<bool, String> {
+ let assistant = assistant_state.assistant.lock().unwrap().clone();
+ let config = config_state.config.lock().unwrap().clone();
+ Ok(assistant.check_health(&config.assistant).await)
+}
+
+#[tauri::command]
+async fn assistant_chat(
+ assistant_state: State<'_, core::assistant::AssistantState>,
+ config_state: State<'_, core::config::ConfigState>,
+ messages: Vec<core::assistant::Message>,
+) -> Result<core::assistant::Message, String> {
+ let assistant = assistant_state.assistant.lock().unwrap().clone();
+ let config = config_state.config.lock().unwrap().clone();
+ assistant.chat(messages, &config.assistant).await
+}
+
+#[tauri::command]
+async fn list_ollama_models(
+ assistant_state: State<'_, core::assistant::AssistantState>,
+ endpoint: String,
+) -> Result<Vec<core::assistant::ModelInfo>, String> {
+ let assistant = assistant_state.assistant.lock().unwrap().clone();
+ assistant.list_ollama_models(&endpoint).await
+}
+
+#[tauri::command]
+async fn list_openai_models(
+ assistant_state: State<'_, core::assistant::AssistantState>,
+ config_state: State<'_, core::config::ConfigState>,
+) -> Result<Vec<core::assistant::ModelInfo>, String> {
+ let assistant = assistant_state.assistant.lock().unwrap().clone();
+ let config = config_state.config.lock().unwrap().clone();
+ assistant.list_openai_models(&config.assistant).await
+}
+
+// ==================== Instance Management Commands ====================
+
+/// Create a new instance
+#[tauri::command]
+async fn create_instance(
+ window: Window,
+ state: State<'_, core::instance::InstanceState>,
+ name: String,
+) -> Result<core::instance::Instance, String> {
+ let app_handle = window.app_handle();
+ state.create_instance(name, app_handle)
+}
+
+/// Delete an instance
+#[tauri::command]
+async fn delete_instance(
+ state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
+) -> Result<(), String> {
+ state.delete_instance(&instance_id)
+}
+
+/// Update an instance
+#[tauri::command]
+async fn update_instance(
+ state: State<'_, core::instance::InstanceState>,
+ instance: core::instance::Instance,
+) -> Result<(), String> {
+ state.update_instance(instance)
+}
+
+/// Get all instances
+#[tauri::command]
+async fn list_instances(
+ state: State<'_, core::instance::InstanceState>,
+) -> Result<Vec<core::instance::Instance>, String> {
+ Ok(state.list_instances())
+}
+
+/// Get a single instance by ID
+#[tauri::command]
+async fn get_instance(
+ state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
+) -> Result<core::instance::Instance, String> {
+ state
+ .get_instance(&instance_id)
+ .ok_or_else(|| format!("Instance {} not found", instance_id))
+}
+
+/// Set the active instance
+#[tauri::command]
+async fn set_active_instance(
+ state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
+) -> Result<(), String> {
+ state.set_active_instance(&instance_id)
+}
+
+/// Get the active instance
+#[tauri::command]
+async fn get_active_instance(
+ state: State<'_, core::instance::InstanceState>,
+) -> Result<Option<core::instance::Instance>, String> {
+ Ok(state.get_active_instance())
+}
+
+/// Duplicate an instance
+#[tauri::command]
+async fn duplicate_instance(
+ window: Window,
+ state: State<'_, core::instance::InstanceState>,
+ instance_id: String,
+ new_name: String,
+) -> Result<core::instance::Instance, String> {
+ let app_handle = window.app_handle();
+ state.duplicate_instance(&instance_id, new_name, app_handle)
+}
+
+#[tauri::command]
+async fn assistant_chat_stream(
+ window: tauri::Window,
+ assistant_state: State<'_, core::assistant::AssistantState>,
+ config_state: State<'_, core::config::ConfigState>,
+ messages: Vec<core::assistant::Message>,
+) -> Result<String, String> {
+ let assistant = assistant_state.assistant.lock().unwrap().clone();
+ let config = config_state.config.lock().unwrap().clone();
+ assistant
+ .chat_stream(messages, &config.assistant, &window)
+ .await
+}
+
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_fs::init())
@@ -1647,10 +2193,21 @@ fn main() {
.plugin(tauri_plugin_shell::init())
.manage(core::auth::AccountState::new())
.manage(MsRefreshTokenState::new())
+ .manage(core::assistant::AssistantState::new())
.setup(|app| {
let config_state = core::config::ConfigState::new(app.handle());
app.manage(config_state);
+ // Initialize instance state
+ let instance_state = core::instance::InstanceState::new(app.handle());
+
+ // Migrate legacy data if needed
+ if let Err(e) = core::instance::migrate_legacy_data(app.handle(), &instance_state) {
+ eprintln!("[Startup] Warning: Failed to migrate legacy data: {}", e);
+ }
+
+ app.manage(instance_state);
+
// Load saved account on startup
let app_dir = app.path().app_data_dir().unwrap();
let storage = core::account_storage::AccountStorage::new(app_dir);
@@ -1670,7 +2227,7 @@ fn main() {
}
// Check for pending Java downloads and notify frontend
- let pending = core::java::get_pending_downloads(&app.app_handle());
+ 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());
@@ -1684,11 +2241,17 @@ fn main() {
check_version_installed,
install_version,
list_installed_versions,
+ get_version_java_version,
+ get_version_metadata,
+ delete_version,
login_offline,
get_active_account,
logout,
get_settings,
save_settings,
+ get_config_path,
+ read_raw_config,
+ save_raw_config,
start_microsoft_login,
complete_microsoft_login,
refresh_account,
@@ -1715,7 +2278,21 @@ fn main() {
get_forge_versions_for_game,
install_forge,
get_github_releases,
- upload_to_pastebin
+ upload_to_pastebin,
+ assistant_check_health,
+ assistant_chat,
+ assistant_chat_stream,
+ list_ollama_models,
+ list_openai_models,
+ // Instance management commands
+ create_instance,
+ delete_instance,
+ update_instance,
+ list_instances,
+ get_instance,
+ set_active_instance,
+ get_active_instance,
+ duplicate_instance
])
.run(tauri::generate_context!())
.expect("error while running tauri application");