From 40cf59542d95f6484b3876e3d72da0e417c5b772 Mon Sep 17 00:00:00 2001 From: orange-bot Date: Thu, 15 Oct 2026 18:20:48 +0800 Subject: [PATCH] fix(download): keep downloaded Mods enabled when the profile state is missing The UI passes its currently selected profile name to the download backend together with profile mode. On a fresh install that name is empty until the first profile reload finishes (and the reload itself can fail while Mods/ does not exist yet), and a name the profile folder no longer owns (renamed or deleted profile) failed the switch with "Profile not found". Either way the freshly downloaded Mod was never added to a profile and the follow-up apply_mod_blacklist_profiles - the UI re-applies profiles when the download finishes - rewrote blacklist.txt as a whitelist without it, so the Mod was disabled until the user enabled it by hand in the Mod manager. With an empty name and no profile on disk the apply even happened with an empty profile list, which blacklisted every installed Mod at once. Resolve the profile names defensively (keep the given name only while it still exists, otherwise use the active profiles or the first one) and leave blacklist.txt untouched when no profile exists yet - the UI seeds its default profile on the next reload and adopts the new Mod. --- src-tauri/src/backend.rs | 189 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 179 insertions(+), 10 deletions(-) diff --git a/src-tauri/src/backend.rs b/src-tauri/src/backend.rs index 24a1165..93e04eb 100644 --- a/src-tauri/src/backend.rs +++ b/src-tauri/src/backend.rs @@ -3069,6 +3069,30 @@ fn download_and_install_crash_mod_fix_impl( result } +/// Profile names a download should switch Mods in. The UI may pass a name that +/// no longer exists (its profile state has not been loaded yet, or the active +/// profile was renamed), and an empty list makes `apply_mod_blacklist_profiles` +/// rewrite `blacklist.txt` as a whitelist without a single enabled Mod, which +/// disables every installed Mod. +fn resolve_switch_profile_names(game_path: &str, current_profile_name: &str) -> Vec { + let profiles = blacklist::get_mod_blacklist_profiles(game_path); + if !current_profile_name.is_empty() + && profiles + .iter() + .any(|profile| profile.name.eq_ignore_ascii_case(current_profile_name)) + { + return vec![current_profile_name.to_string()]; + } + let active = blacklist::get_current_profiles(game_path); + if !active.is_empty() { + return active; + } + profiles + .first() + .map(|profile| vec![profile.name.clone()]) + .unwrap_or_default() +} + fn disable_installed_local_mods( game_path: &str, installed_mods: &[(String, String)], @@ -3098,11 +3122,13 @@ fn disable_installed_local_mods( for profile in blacklist::get_mod_blacklist_profiles(game_path) { blacklist::switch_mod_profile_mods(game_path, &profile.name, &names, false)?; } - let profiles = if current_profile_name.is_empty() { - blacklist::get_current_profiles(game_path) - } else { - vec![current_profile_name.to_string()] - }; + let profiles = resolve_switch_profile_names(game_path, current_profile_name); + if profiles.is_empty() { + // No profile owns blacklist.txt yet - the UI creates the default profile + // on its next reload. Applying an empty list here would blacklist every + // installed Mod, including the one that was just downloaded. + return Ok(()); + } blacklist::apply_mod_blacklist_profiles(game_path, &profiles, always_on_mods)?; Ok(()) } @@ -3138,11 +3164,13 @@ fn enable_installed_local_mods( .iter() .map(|(name, _)| (*name).clone()) .collect::>(); - let profiles = if current_profile_name.is_empty() { - blacklist::get_current_profiles(game_path) - } else { - vec![current_profile_name.to_string()] - }; + let profiles = resolve_switch_profile_names(game_path, current_profile_name); + if profiles.is_empty() { + // No profile owns blacklist.txt yet - the UI creates the default profile + // on its next reload. Applying an empty list here would blacklist every + // installed Mod, including the one that was just downloaded. + return Ok(()); + } for profile_name in &profiles { blacklist::switch_mod_profile_mods(game_path, profile_name, &names, true)?; } @@ -3973,6 +4001,147 @@ mod local_package_tests { fs::remove_dir_all(root).unwrap(); } + fn install_mod(game_path: &Path, root: &Path, file: &str, name: &str) { + let package = root.join(file); + write_zip( + &package, + &[( + "everest.yaml", + format!("- Name: {name}\n Version: 1.0.0\n").as_bytes(), + )], + ); + install_local_mod(game_path, &package).unwrap(); + } + + /// A fresh game folder has no profiles yet, so the UI passes no profile name + /// and profile mode is on by default. The download must not rewrite + /// blacklist.txt as an empty whitelist, which would disable every Mod. + #[test] + fn downloaded_mod_stays_enabled_without_any_profile() { + let root = test_dir("download-no-profile"); + let game_path = root.join("game"); + fs::create_dir_all(game_path.join("Mods")).unwrap(); + let game_path_string = game_path.to_string_lossy().into_owned(); + + install_mod(&game_path, &root, "Existing.zip", "Existing"); + let installed = { + let package = root.join("New Mod.zip"); + write_zip( + &package, + &[("everest.yaml", b"- Name: NewMod\n Version: 1.0.0\n")], + ); + install_local_mod(&game_path, &package).unwrap() + }; + + enable_installed_local_mods( + &game_path_string, + std::slice::from_ref(&installed), + true, + "", + &[], + ) + .unwrap(); + + let blacklist = + fs::read_to_string(game_path.join("Mods").join("blacklist.txt")).unwrap_or_default(); + assert!( + !blacklist + .lines() + .any(|line| line.trim().eq_ignore_ascii_case(&installed.1)), + "a downloaded Mod must not be blacklisted when no profile exists yet" + ); + assert!( + !blacklist + .lines() + .any(|line| line.trim().eq_ignore_ascii_case("Existing.zip")), + "existing Mods must not be blacklisted either" + ); + + fs::remove_dir_all(root).unwrap(); + } + + /// The profile exists on disk but the UI has not loaded its state yet (fresh + /// start, or the first load failed), so it passes an empty name. + #[test] + fn downloaded_mod_is_enabled_when_profile_state_is_not_loaded() { + let root = test_dir("download-profile-not-loaded"); + let game_path = root.join("game"); + fs::create_dir_all(game_path.join("Mods")).unwrap(); + let game_path_string = game_path.to_string_lossy().into_owned(); + + blacklist::new_mod_blacklist_profile(&game_path_string, "Default").unwrap(); + blacklist::apply_mod_blacklist_profiles(&game_path_string, &["Default".into()], &[]) + .unwrap(); + + install_mod(&game_path, &root, "New Mod.zip", "NewMod"); + let installed = get_installed_mods_sync(game_path.join("Mods").to_string_lossy().into()) + .into_iter() + .map(|mod_info| (mod_info.name, mod_info.file)) + .find(|(name, _)| name == "NewMod") + .unwrap(); + + enable_installed_local_mods( + &game_path_string, + std::slice::from_ref(&installed), + true, + "", + &[], + ) + .unwrap(); + + let profiles = blacklist::get_mod_blacklist_profiles(&game_path_string); + let default_profile = profiles.iter().find(|p| p.name == "Default").unwrap(); + assert!( + default_profile + .enabled_mods + .iter() + .any(|name| name == &installed.0), + "the downloaded Mod must be enabled in the only profile" + ); + + fs::remove_dir_all(root).unwrap(); + } + + /// The name the UI kept can be stale (renamed or deleted profile); the Mod + /// must still land in a profile that exists instead of failing silently. + #[test] + fn downloaded_mod_is_enabled_when_profile_name_is_stale() { + let root = test_dir("download-profile-stale-name"); + let game_path = root.join("game"); + fs::create_dir_all(game_path.join("Mods")).unwrap(); + let game_path_string = game_path.to_string_lossy().into_owned(); + + blacklist::new_mod_blacklist_profile(&game_path_string, "Default").unwrap(); + blacklist::apply_mod_blacklist_profiles(&game_path_string, &["Default".into()], &[]) + .unwrap(); + + install_mod(&game_path, &root, "New Mod.zip", "NewMod"); + let installed = get_installed_mods_sync(game_path.join("Mods").to_string_lossy().into()) + .into_iter() + .map(|mod_info| (mod_info.name, mod_info.file)) + .find(|(name, _)| name == "NewMod") + .unwrap(); + + enable_installed_local_mods( + &game_path_string, + std::slice::from_ref(&installed), + true, + "Removed Profile", + &[], + ) + .unwrap(); + + let profiles = blacklist::get_mod_blacklist_profiles(&game_path_string); + assert!( + profiles + .iter() + .any(|profile| profile.enabled_mods.iter().any(|name| name == &installed.0)), + "a stale profile name must fall back to a profile that exists" + ); + + fs::remove_dir_all(root).unwrap(); + } + #[test] fn enable_downloaded_mod_removes_direct_blacklist_entry() { let root = test_dir("download-default-enable-direct");