diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 095289c..0f7ed26 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1,17 +1,244 @@ +use std::path::PathBuf; + +use serde::Deserialize; use tauri::State; +use tokio::fs; -use crate::{save_state, LauncherSettings, Mod, SharedState}; +use crate::{launcher::LAUNCHER_DIR, save_state, LauncherSettings, Mod, SharedState}; -#[tauri::command] -pub fn get_installed_mods(state: State, version_id: String) -> Vec { - Vec::new() +#[derive(Deserialize)] +struct ModrinthVersion { + version_number: String, + game_versions: Vec, + loaders: Vec, + files: Vec, +} + +#[derive(Deserialize)] +struct ModrinthFile { + url: String, + filename: String, + primary: bool, } #[tauri::command] -pub fn toggle_mod(state: State, mod_id: String) {} +pub async fn get_installed_mods(version_id: String) -> Result, String> { + let mut mods = Vec::new(); + + let base_dir = LAUNCHER_DIR.data_dir().join(&version_id).join("mods"); + + let enabled_dir = base_dir.clone(); + let disabled_dir = base_dir.join("disabled_mods"); + + let cache_dir = LAUNCHER_DIR.data_dir().join("cache"); + + // process a directory async + async fn process_dir(dir: PathBuf, enabled: bool, cache_dir: PathBuf) -> Vec { + let mut mods = Vec::new(); + + if fs::metadata(&dir).await.is_err() { + return mods; + } + + let mut entries = match fs::read_dir(&dir).await { + Ok(e) => e, + Err(_) => return mods, + }; + + while let Ok(Some(entry)) = entries.next_entry().await { + let path = entry.path(); + + // only .jar files + if path.extension().and_then(|s| s.to_str()) != Some("jar") { + continue; + } + + let file_name = match path.file_name().and_then(|s| s.to_str()) { + Some(name) => name.to_string(), + None => continue, + }; + + let cache_path = cache_dir.join(format!("{}.json", file_name)); + + let mod_item = if fs::metadata(&cache_path).await.is_ok() { + match fs::read_to_string(&cache_path).await { + Ok(content) => match serde_json::from_str::(&content) { + Ok(mut m) => { + m.enabled = enabled; + m + } + Err(_) => fallback_mod(&file_name, enabled), + }, + Err(_) => fallback_mod(&file_name, enabled), + } + } else { + fallback_mod(&file_name, enabled) + }; + + mods.push(mod_item); + } + + mods + } + + // run both dirs (sequential, but async-safe) + let mut enabled_mods = process_dir(enabled_dir, true, cache_dir.clone()).await; + let mut disabled_mods = process_dir(disabled_dir, false, cache_dir.clone()).await; + + mods.append(&mut enabled_mods); + mods.append(&mut disabled_mods); + + Ok(mods) +} + +// fallback +fn fallback_mod(file_name: &str, enabled: bool) -> Mod { + Mod { + id: file_name.to_string(), + name: file_name.to_string(), + version: String::new(), + author: String::new(), + description: String::new(), + enabled, + icon: None, + supported_versions: vec![], + } +} #[tauri::command] -pub fn install_mod(state: State, mut mod_item: Mod) {} +pub async fn disable_mod(version: String, file_name: String) -> Result<(), String> { + // mods folder + let mods_dir: PathBuf = LAUNCHER_DIR.data_dir().join(&version).join("mods"); + + // disabled_mods folder + let disabled_dir = mods_dir.join("disabled_mods"); + + // create disabled_mods if it doesn't exist + fs::create_dir_all(&disabled_dir) + .await + .map_err(|e| e.to_string())?; + + let source = mods_dir.join(&file_name); + let destination = disabled_dir.join(&file_name); + + if !source.exists() { + return Err(format!("Mod file not found: {}", file_name)); + } + + // move file + fs::rename(&source, &destination) + .await + .map_err(|e| e.to_string())?; + + Ok(()) +} + +#[tauri::command] +pub async fn enable_mod(version: String, file_name: String) -> Result<(), String> { + let mods_dir = LAUNCHER_DIR.data_dir().join(&version).join("mods"); + + let disabled_dir = mods_dir.join("disabled_mods"); + + let source = disabled_dir.join(&file_name); + let destination = mods_dir.join(&file_name); + + if !source.exists() { + return Err(format!("Disabled mod not found: {}", file_name)); + } + + fs::rename(&source, &destination) + .await + .map_err(|e| e.to_string())?; + + Ok(()) +} + +#[tauri::command] +pub async fn install_mod(mut mod_meta: Mod, versions: Vec) -> Result<(), String> { + let client = reqwest::Client::new(); + + println!("Installing {} for {:?}", mod_meta.name, versions); + + let cache_dir = LAUNCHER_DIR.data_dir().join("cache"); + fs::create_dir_all(&cache_dir) + .await + .map_err(|e| e.to_string())?; + + for mc_version in versions { + let url = format!( + "https://api.modrinth.com/v2/project/{}/version", + mod_meta.id + ); + + let res = client.get(&url).send().await.map_err(|e| e.to_string())?; + + let versions_list: Vec = res.json().await.map_err(|e| e.to_string())?; + + let mut compatible: Vec<&ModrinthVersion> = versions_list + .iter() + .filter(|v| { + v.game_versions.contains(&mc_version) && v.loaders.iter().any(|l| l == "fabric") + }) + .collect(); + + if compatible.is_empty() { + println!("No compatible version found for {}", mc_version); + continue; + } + + compatible.sort_by(|a, b| b.version_number.cmp(&a.version_number)); + + let selected = compatible[0]; + + let file = selected + .files + .iter() + .find(|f| f.primary) + .or_else(|| selected.files.first()) + .ok_or("No files found for version")?; + + let bytes = client + .get(&file.url) + .send() + .await + .map_err(|e| e.to_string())? + .bytes() + .await + .map_err(|e| e.to_string())?; + + let dir: PathBuf = LAUNCHER_DIR.data_dir().join(&mc_version).join("mods"); + + fs::create_dir_all(&dir).await.map_err(|e| e.to_string())?; + + let file_path = dir.join(&file.filename); + + // 1. Save JAR + fs::write(&file_path, &bytes) + .await + .map_err(|e| e.to_string())?; + + // 2. Prepare cache metadata + mod_meta.id = file.filename.clone(); // ✅ ID is filename + mod_meta.version = selected.version_number.clone(); + mod_meta.enabled = true; + + let cache_path = cache_dir.join(format!("{}.json", file.filename)); + + // 3. Save metadata JSON + let json = serde_json::to_string_pretty(&mod_meta).map_err(|e| e.to_string())?; + + fs::write(&cache_path, json) + .await + .map_err(|e| e.to_string())?; + + println!( + "Installed {} for {} (cached as {})", + mod_meta.name, mc_version, file.filename + ); + } + + Ok(()) +} #[tauri::command] pub fn get_settings(state: State) -> LauncherSettings { diff --git a/src-tauri/src/launcher/mod.rs b/src-tauri/src/launcher/mod.rs index 4b424b4..4b27879 100644 --- a/src-tauri/src/launcher/mod.rs +++ b/src-tauri/src/launcher/mod.rs @@ -13,9 +13,9 @@ use lyceris::{ }, }; use once_cell::sync::Lazy; -use tauri::Emitter as _; +use tauri::{Emitter as _, State}; -use crate::GameVersion; +use crate::{GameVersion, SharedState}; pub static LAUNCHER_DIR: Lazy = Lazy::new(|| { ProjectDirs::from("org", "SaturnLauncher", "SaturnLauncher") @@ -25,6 +25,7 @@ pub static LAUNCHER_DIR: Lazy = Lazy::new(|| { #[tauri::command] pub async fn launch_game( app: tauri::AppHandle, + state: State<'_, SharedState>, version: GameVersion, account: MinecraftAccount, ) -> Result<(), String> { @@ -83,6 +84,9 @@ pub async fn launch_game( }, ) .loader(Fabric(version.loader_version).into()) + .memory(lyceris::minecraft::config::Memory::Megabyte( + state.lock().unwrap().settings.max_memory as u64, + )) .build(); println!("Starting installation"); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ff5c48e..fc60075 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -30,7 +30,6 @@ pub struct GameVersion { #[derive(Clone, Serialize, Deserialize)] pub struct LauncherSettings { - pub jvm_arguments: String, pub max_memory: u32, } @@ -44,11 +43,7 @@ pub struct AppState { impl Default for AppState { fn default() -> Self { Self { - settings: LauncherSettings { - jvm_arguments: "-XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=200" - .into(), - max_memory: 4096, - }, + settings: LauncherSettings { max_memory: 4096 }, version: String::from("1.21.6"), } } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index fc8dabb..5bbb611 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -11,7 +11,8 @@ fn main() { .invoke_handler(tauri::generate_handler![ // Saturn Launcher Commands saturn_launcher_lib::commands::get_installed_mods, - saturn_launcher_lib::commands::toggle_mod, + saturn_launcher_lib::commands::enable_mod, + saturn_launcher_lib::commands::disable_mod, saturn_launcher_lib::commands::install_mod, saturn_launcher_lib::commands::get_settings, saturn_launcher_lib::commands::update_settings, diff --git a/src/components/ModsSection.tsx b/src/components/ModsSection.tsx index 1431e79..f62aeb6 100644 --- a/src/components/ModsSection.tsx +++ b/src/components/ModsSection.tsx @@ -11,8 +11,9 @@ import { import { getInstalledMods, discoverMods, - toggleMod, + enableMod, installMod, + disableMod, } from "@/lib/saturn"; import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; @@ -41,21 +42,32 @@ const ModsSection: React.FC = ({ version }) => { string[] >([]); - useEffect(() => { - const loadMods = async () => { - if (activeTab === "installed") { - const m = await getInstalledMods(version.id); - setMods(m); - } else { - const m = await discoverMods(searchQuery); + const loadMods = async () => { + if (activeTab === "installed") { + const m = await getInstalledMods(version.id); + setMods(m); + } else { + const m = await discoverMods(searchQuery); + if (activeTab === "discover") { + // Fix: If discover mods takes too long and the user switches tabs it glitches setMods(m); } - }; - loadMods(); - }, [activeTab, version.id, searchQuery]); + } + }; - const handleToggleMod = async (modId: string) => { - await toggleMod(modId); + useEffect(() => { + const id = setTimeout(loadMods, 800); + return () => clearTimeout(id); + }, [version.id, searchQuery]); + + useEffect(() => { + setMods([]); + loadMods(); + }, [activeTab]); + + const handleToggleMod = async (modId: string, enable: boolean) => { + if (enable) await enableMod(version.id, modId); + else await disableMod(version.id, modId); const m = await getInstalledMods(version.id); setMods(m); }; @@ -143,7 +155,15 @@ const ModsSection: React.FC = ({ version }) => { className="panel p-4 flex items-center gap-4 hover:border-white/10 transition-colors" >
- + {mod.icon ? ( + Mod Icon + ) : ( + + )}
@@ -165,7 +185,7 @@ const ModsSection: React.FC = ({ version }) => { {activeTab === "installed" ? ( <>
- - {/* JVM Arguments */} - -
-
- -
-

Custom JVM Arguments

-
- -
-

- Advanced flags for the Java Virtual Machine. Saturn Client - includes optimized defaults, but you can override them here. -

- -