diff --git a/src-tauri/src/instance/commands.rs b/src-tauri/src/instance/commands.rs index 05d12c88e..5db0dd590 100644 --- a/src-tauri/src/instance/commands.rs +++ b/src-tauri/src/instance/commands.rs @@ -1,5 +1,6 @@ use super::helpers::loader::fabric::remove_fabric_api_mods; use crate::error::SJMCLResult; +use crate::instance::constants::TRANSLATION_CACHE_EXPIRY_HOURS; use crate::instance::helpers::client_json::{replace_native_libraries, McClientInfo, PatchesInfo}; use crate::instance::helpers::game_version::{compare_game_versions, get_major_game_version}; use crate::instance::helpers::loader::common::{execute_processors, install_mod_loader}; @@ -14,6 +15,7 @@ use crate::instance::helpers::modpack::modrinth::ModrinthManifest; use crate::instance::helpers::modpack::multimc::MultiMcManifest; use crate::instance::helpers::mods::common::{ add_local_mod_translations, get_mod_info_from_dir, get_mod_info_from_jar, + LocalModTranslationEntry, LocalModTranslationsCache, }; use crate::instance::helpers::options_txt::get_zh_hans_lang_tag; use crate::instance::helpers::resourcepack::{ @@ -48,11 +50,12 @@ use regex::{Regex, RegexBuilder}; use std::collections::HashMap; use std::fs; use std::path::{Path, PathBuf}; -use std::sync::Mutex; +use std::sync::{Arc, Mutex}; use std::time::SystemTime; -use tauri::{AppHandle, Manager}; +use tauri::{AppHandle, Manager, State}; use tauri_plugin_http::reqwest; use tokio; +use tokio::sync::Semaphore; use url::Url; use zip::read::ZipArchive; @@ -469,6 +472,7 @@ pub async fn retrieve_game_server_list( pub async fn retrieve_local_mod_list( app: AppHandle, instance_id: String, + local_mod_translations_cache_state: State<'_, Mutex>, ) -> SJMCLResult> { let mods_dir = match get_instance_subdir_path_by_id(&app, &instance_id, &InstanceSubdirType::Mods) { @@ -483,8 +487,21 @@ pub async fn retrieve_local_mod_list( let mod_paths = get_files_with_regex(&mods_dir, &valid_extensions).unwrap_or_default(); let mut tasks = Vec::new(); + let semaphore = Arc::new(Semaphore::new( + std::thread::available_parallelism().unwrap().into(), + )); for path in mod_paths { - let task = tokio::spawn(async move { get_mod_info_from_jar(&path).await.ok() }); + let permit = semaphore + .clone() + .acquire_owned() + .await + .map_err(|_| InstanceError::SemaphoreAcquireFailed)?; + let task = tokio::spawn(async move { + log::debug!("Load mod info from dir: {}", path.display()); + let info = get_mod_info_from_jar(&path).await.ok(); + drop(permit); + info + }); tasks.push(task); } #[cfg(debug_assertions)] @@ -492,7 +509,17 @@ pub async fn retrieve_local_mod_list( // mod information detection from folders is only used for debugging. let mod_paths = get_subdirectories(&mods_dir).unwrap_or_default(); for path in mod_paths { - let task = tokio::spawn(async move { get_mod_info_from_dir(&path).await.ok() }); + let permit = semaphore + .clone() + .acquire_owned() + .await + .map_err(|_| InstanceError::SemaphoreAcquireFailed)?; + let task = tokio::spawn(async move { + log::debug!("Load mod info from dir: {}", path.display()); + let info = get_mod_info_from_dir(&path).await.ok(); + drop(permit); + info + }); tasks.push(task); } } @@ -530,8 +557,15 @@ pub async fn retrieve_local_mod_list( let mut translation_tasks = Vec::new(); for mut mod_info in mod_infos { let app = app.clone(); + let permit = semaphore + .clone() + .acquire_owned() + .await + .map_err(|_| InstanceError::SemaphoreAcquireFailed)?; let task = tokio::spawn(async move { + log::debug!("Translating mod: {}", mod_info.file_name); let _ = add_local_mod_translations(&app, &mut mod_info).await; + drop(permit); mod_info }); translation_tasks.push(task); @@ -542,9 +576,24 @@ pub async fn retrieve_local_mod_list( mod_infos.push(mod_info); } } - // sort by name (and version) mod_infos.sort(); + let mut cache = local_mod_translations_cache_state.lock()?; + for info in mod_infos.iter() { + if let Some(entry) = cache.translations.get(&info.file_name) { + if !entry.is_expired(TRANSLATION_CACHE_EXPIRY_HOURS) { + continue; + } + } + cache.translations.insert( + info.file_name.clone(), + LocalModTranslationEntry::new( + info.translated_name.clone(), + info.translated_description.clone(), + ), + ); + } + cache.save()?; Ok(mod_infos) } diff --git a/src-tauri/src/instance/constants.rs b/src-tauri/src/instance/constants.rs index 5ee16b40e..a5e111c6c 100644 --- a/src-tauri/src/instance/constants.rs +++ b/src-tauri/src/instance/constants.rs @@ -1 +1,3 @@ pub const INSTANCE_CFG_FILE_NAME: &str = "sjmclcfg.json"; +pub const TRANSLATION_CACHE_EXPIRY_HOURS: u64 = 24; +pub const TRANSLATION_CACHE_FILE_NAME: &str = "local_mod_translations.json"; diff --git a/src-tauri/src/instance/helpers/mods/common.rs b/src-tauri/src/instance/helpers/mods/common.rs index e20085ef9..31487a462 100644 --- a/src-tauri/src/instance/helpers/mods/common.rs +++ b/src-tauri/src/instance/helpers/mods/common.rs @@ -1,4 +1,5 @@ use crate::error::{SJMCLError, SJMCLResult}; +use crate::instance::constants::{TRANSLATION_CACHE_EXPIRY_HOURS, TRANSLATION_CACHE_FILE_NAME}; use crate::instance::helpers::mods::{fabric, forge, legacy_forge, liteloader, quilt}; use crate::instance::models::misc::{LocalModInfo, ModLoaderType}; use crate::resource::helpers::curseforge::{ @@ -7,31 +8,43 @@ use crate::resource::helpers::curseforge::{ use crate::resource::helpers::modrinth::{ fetch_remote_resource_by_id_modrinth, fetch_remote_resource_by_local_modrinth, }; +use crate::storage::Storage; use crate::utils::image::{load_image_from_dir_async, load_image_from_jar}; +use crate::APP_DATA_DIR; +use log::info; use serde::{Deserialize, Serialize}; use std::io::Cursor; use std::path::{Path, PathBuf}; +use std::sync::Mutex; use std::time::{SystemTime, UNIX_EPOCH}; use tauri::{AppHandle, Manager}; -use tokio::fs; use zip::ZipArchive; // Cache structure for local mod translations #[derive(Debug, Clone, Serialize, Deserialize, Default)] -struct LocalModTranslationsCache { +pub struct LocalModTranslationsCache { #[serde(flatten)] - translations: std::collections::HashMap, + pub translations: std::collections::HashMap, +} + +impl Storage for LocalModTranslationsCache { + fn file_path() -> PathBuf { + APP_DATA_DIR + .get() + .unwrap() + .join(TRANSLATION_CACHE_FILE_NAME) + } } #[derive(Debug, Clone, Serialize, Deserialize)] -struct LocalModTranslationEntry { - translated_name: Option, - translated_description: Option, - timestamp: u64, +pub struct LocalModTranslationEntry { + pub translated_name: Option, + pub translated_description: Option, + pub timestamp: u64, } impl LocalModTranslationEntry { - fn new(translated_name: Option, translated_description: Option) -> Self { + pub fn new(translated_name: Option, translated_description: Option) -> Self { Self { translated_name, translated_description, @@ -42,7 +55,7 @@ impl LocalModTranslationEntry { } } - fn is_expired(&self, max_age_hours: u64) -> bool { + pub fn is_expired(&self, max_age_hours: u64) -> bool { let current_time = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() @@ -51,37 +64,6 @@ impl LocalModTranslationEntry { } } -async fn load_local_mod_translations_cache(app: &AppHandle) -> LocalModTranslationsCache { - let cache_path = match app.path().app_cache_dir() { - Ok(cache_dir) => cache_dir.join("local_mod_translations.json"), - Err(_) => return LocalModTranslationsCache::default(), - }; - - let content = match fs::read_to_string(&cache_path).await { - Ok(content) => content, - Err(_) => return LocalModTranslationsCache::default(), - }; - - serde_json::from_str(&content).unwrap_or_else(|_| LocalModTranslationsCache::default()) -} - -async fn save_local_mod_translations_cache( - app: &AppHandle, - cache: &LocalModTranslationsCache, -) -> bool { - let cache_path = match app.path().app_cache_dir() { - Ok(cache_dir) => cache_dir.join("local_mod_translations.json"), - Err(_) => return false, - }; - - let content = match serde_json::to_string_pretty(cache) { - Ok(content) => content, - Err(_) => return false, - }; - - fs::write(cache_path, content).await.is_ok() -} - pub async fn get_mod_info_from_jar(path: &PathBuf) -> SJMCLResult { let file = Cursor::new(tokio::fs::read(path).await?); let file_name = path.file_name().unwrap().to_string_lossy().to_string(); @@ -312,13 +294,17 @@ pub async fn add_local_mod_translations( app: &AppHandle, mod_info: &mut LocalModInfo, ) -> SJMCLResult<()> { - const CACHE_EXPIRY_HOURS: u64 = 24; - + let cache = { + let translation_cache_state = app.state::>(); + let cache = translation_cache_state.lock()?.clone(); + cache + }; let file_path = mod_info.file_path.to_string_lossy().to_string(); - let cache = load_local_mod_translations_cache(app).await; + let file_name = mod_info.file_name.clone(); - if let Some(entry) = cache.translations.get(&file_path) { - if !entry.is_expired(CACHE_EXPIRY_HOURS) { + if let Some(entry) = cache.translations.get(&file_name) { + if !entry.is_expired(TRANSLATION_CACHE_EXPIRY_HOURS) { + info!("Using cached translation for mod: {}", file_name); mod_info.translated_name = entry.translated_name.clone(); mod_info.translated_description = entry.translated_description.clone(); return Ok(()); @@ -358,25 +344,10 @@ pub async fn add_local_mod_translations( _ => None, }; - let resource_info = match final_result { - Some(data) => data, - None => return Ok(()), - }; - - mod_info.translated_name = resource_info.translated_name.clone(); - mod_info.translated_description = resource_info.translated_description.clone(); - - // Save to cache - let mut cache = load_local_mod_translations_cache(app).await; - cache.translations.insert( - file_path.clone(), - LocalModTranslationEntry::new( - resource_info.translated_name, - resource_info.translated_description, - ), - ); - - save_local_mod_translations_cache(app, &cache).await; - + if let Some(resource_info) = final_result { + info!("Fetched translation for mod: {}", file_name); + mod_info.translated_name = resource_info.translated_name.clone(); + mod_info.translated_description = resource_info.translated_description.clone(); + } Ok(()) } diff --git a/src-tauri/src/instance/models/misc.rs b/src-tauri/src/instance/models/misc.rs index ad23e5d6d..b657761eb 100644 --- a/src-tauri/src/instance/models/misc.rs +++ b/src-tauri/src/instance/models/misc.rs @@ -250,6 +250,7 @@ pub enum InstanceError { MainClassNotFound, InstallationDuplicated, ProcessorExecutionFailed, + SemaphoreAcquireFailed, } impl std::error::Error for InstanceError {} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index cf51d8601..5f5881541 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -13,6 +13,7 @@ mod utils; use account::helpers::authlib_injector::info::refresh_and_update_auth_servers; use account::models::AccountInfo; use instance::helpers::misc::refresh_and_update_instances; +use instance::helpers::mods::common::LocalModTranslationsCache; use instance::models::misc::Instance; use launch::models::LaunchingState; use launcher_config::helpers::java::refresh_and_update_javas; @@ -191,6 +192,9 @@ pub async fn run() { app.manage(Box::pin(TaskMonitor::new(app.handle().clone()))); + let local_mod_translations = LocalModTranslationsCache::load().unwrap_or_default(); + app.manage(Mutex::new(local_mod_translations)); + let client = build_sjmcl_client(app.handle(), true, false); app.manage(client);