diff --git a/Launcher/Test/mod_profiles_tests.cpp b/Launcher/Test/mod_profiles_tests.cpp index 9d8f6dd6..755a81c4 100644 --- a/Launcher/Test/mod_profiles_tests.cpp +++ b/Launcher/Test/mod_profiles_tests.cpp @@ -79,6 +79,21 @@ void applying() { check(profiles::in_use(have, after) && !profiles::in_use(have, list), "In use does not follow which mods are on"); } +// "Keep my other mods on": a profile adds its mods to the player's own, and is in use when its own mods match. +void keeping_others() { + const auto list = installed({{"Team-A", "1.0.0", true}, {"Team-B", "2.1.0", false}, {"Local Mod", "wip", true}}); + const profiles::Profile pack{"Pack", {{"Team-B", "2.1.0", true}}}; + const auto applied = profiles::apply(pack, list, true); + check(order(applied.entries) == std::vector{"Team-B+", "Team-A+", "Local Mod+"}, + "Keeping other mods switched a mod the profile does not name off"); + check(order(profiles::apply(pack, list).entries) == std::vector{"Team-B+", "Team-A-", "Local Mod-"}, + "Without keeping other mods, the mods outside the profile were not switched off"); + auto after = list; + after.entries = applied.entries; + check(profiles::in_use(pack, after, true) && !profiles::in_use(pack, after), "In use with kept mods does not follow the profile's own mods"); + check(!profiles::in_use(pack, list, true), "A profile whose mod is off counts as in use"); +} + void storage(const std::filesystem::path& root) { std::filesystem::remove_all(root); std::filesystem::create_directories(root); @@ -86,9 +101,11 @@ void storage(const std::filesystem::path& root) { profiles::Profiles set; set.list = {{"Street", {{"Team-A", "1.0.0", true}, {"Team-B", "2.0.0", false}}}, {"Empty", {}}}; set.active = "Street"; + set.keep_others = true; profiles::save(root, set); const auto read = profiles::load(root); check(read.active == "Street" && read.list == set.list, "Profiles did not survive saving and loading"); + check(read.keep_others, "Keeping other mods did not survive saving and loading"); std::ofstream(root / profiles::profiles_file) << R"({"active":"Gone","profiles":[{"name":"ok","mods":[{"name":"../x"},{"name":"Fine"}]},)" R"({"name":"bad/name"},{"name":"ok"},7]})"; const auto loose = profiles::load(root); @@ -149,6 +166,7 @@ int wmain(int argc, wchar_t** argv) { } names(); applying(); + keeping_others(); storage(std::filesystem::path(argv[1]) / L"profiles"); exporting(std::filesystem::path(argv[1]) / L"profiles"); codes(); diff --git a/Launcher/gui_mods_profiles.cpp b/Launcher/gui_mods_profiles.cpp index 607556e9..aa9b9840 100644 --- a/Launcher/gui_mods_profiles.cpp +++ b/Launcher/gui_mods_profiles.cpp @@ -68,7 +68,7 @@ std::vector unobtainable(const ModsPanel& panel, const std::vector< // Writes mods.json as the profile has it. False, with the reason shown, when that failed. bool write_order(ModsPanel& panel, const profiles::Profile& profile, std::vector& missing) { - auto applied = profiles::apply(profile, panel.list); + auto applied = profiles::apply(profile, panel.list, panel.profiles.keep_others); try { mods::save_mod_order(panel.root, applied.entries); } catch (const std::exception& failure) { @@ -122,7 +122,7 @@ void add_profile(ModsPanel& panel, profiles::Profile profile, std::string_view v profile.name = panel.profiles.free_name(profiles::clean_profile_name(profile.name)); const auto name = profile.name; const auto count = profile.mods.size(); - const auto missing = profiles::apply(profile, panel.list).missing.size(); + const auto missing = profiles::apply(profile, panel.list, panel.profiles.keep_others).missing.size(); panel.profiles.list.push_back(std::move(profile)); if (!store(panel)) { panel.profiles.list.pop_back(); @@ -319,10 +319,14 @@ void profiles_page(const Fonts& fonts, ModsPanel& panel, HWND window, float heig const profiles::Profile* using_now = nullptr; for (const auto& profile : panel.profiles.list) - if (!using_now && profiles::in_use(profile, panel.list)) using_now = &profile; + if (!using_now && profiles::in_use(profile, panel.list, panel.profiles.keep_others)) using_now = &profile; page_banner(fonts, "PROFILES", nullptr, - "A profile remembers which of your mods are on and in what order. Every mod stays installed once; loading a " - "profile switches the rest off. Share one as a code or a file, with ReSkate or r2modman.", + panel.profiles.keep_others + ? "A profile remembers which of your mods are on and in what order. Every mod stays installed once; loading a " + "profile switches its mods on and leaves your other mods as they are. Share one as a code or a file, with " + "ReSkate or r2modman." + : "A profile remembers which of your mods are on and in what order. Every mod stays installed once; loading a " + "profile switches the rest off. Share one as a code or a file, with ReSkate or r2modman.", using_now ? "IN USE: " + using_now->name : "NO PROFILE IN USE", using_now ? color::good : color::tile_grey, std::format("{} of {} saved", panel.profiles.list.size(), profiles::max_profiles)); @@ -390,7 +394,13 @@ void profiles_page(const Fonts& fonts, ModsPanel& panel, HWND window, float heig ImGui::EndDisabled(); ImGui::SetCursorScreenPos(ImVec2(start.x, start.y + tall + S(12))); - ImGui::Dummy(ImVec2(0, 0)); + ImGui::BeginDisabled(busy); + if (ImGui::Checkbox("Keep my other mods on when loading a profile", &panel.profiles.keep_others)) store(panel); + ImGui::EndDisabled(); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + ImGui::SetTooltip("On: a profile switches its own mods on and leaves the rest as they are.\n" + "Off: a profile switches every mod it does not list off."); + ImGui::Dummy(ImVec2(0, S(4))); } // ------------------------------------------------ the profiles, a card each @@ -408,8 +418,8 @@ void profiles_page(const Fonts& fonts, ModsPanel& panel, HWND window, float heig const auto& profile = panel.profiles.list[index]; ImGui::PushID(static_cast(index)); const auto on = std::count_if(profile.mods.begin(), profile.mods.end(), [](const auto& mod) { return mod.enabled; }); - const auto missing = profiles::apply(profile, panel.list).missing.size(); - const bool current = profiles::in_use(profile, panel.list); + const auto missing = profiles::apply(profile, panel.list, panel.profiles.keep_others).missing.size(); + const bool current = profiles::in_use(profile, panel.list, panel.profiles.keep_others); const bool last = !current && profile.name == panel.profiles.active; std::vector names; for (const auto& mod : profile.mods) diff --git a/Launcher/mod_profiles.cpp b/Launcher/mod_profiles.cpp index 81dec324..d1b3749b 100644 --- a/Launcher/mod_profiles.cpp +++ b/Launcher/mod_profiles.cpp @@ -228,6 +228,7 @@ Profiles load(const fs::path& mods_root) noexcept { } profiles.active = root.value("active", ""); if (!profiles.find(profiles.active)) profiles.active.clear(); + profiles.keep_others = root.value("keep_others", false); } catch (...) { profiles = {}; } @@ -252,6 +253,7 @@ void save(const fs::path& mods_root, const Profiles& profiles) { } Json root = Json::object(); root["active"] = profiles.active; + root["keep_others"] = profiles.keep_others; root["profiles"] = std::move(list); const auto target = mods_root / profiles_file; auto temporary = target; @@ -280,7 +282,7 @@ Profile capture(std::string name, const mods::ModList& list) { return profile; } -Applied apply(const Profile& profile, const mods::ModList& list) { +Applied apply(const Profile& profile, const mods::ModList& list, bool keep_others) { Applied result; std::map installed; for (const auto& entry : list.entries) installed.emplace(folded(entry.mod.name), &entry); @@ -299,13 +301,13 @@ Applied apply(const Profile& profile, const mods::ModList& list) { for (const auto& entry : list.entries) if (!used.contains(folded(entry.mod.name))) { result.entries.push_back(entry); - result.entries.back().enabled = false; + if (!keep_others) result.entries.back().enabled = false; } return result; } -bool in_use(const Profile& profile, const mods::ModList& list) { - const auto applied = apply(profile, list); +bool in_use(const Profile& profile, const mods::ModList& list, bool keep_others) { + const auto applied = apply(profile, list, keep_others); if (!applied.missing.empty()) return false; const auto enabled = [](const std::vector& entries) { std::vector names; diff --git a/Launcher/mod_profiles.h b/Launcher/mod_profiles.h index dea21e9e..85dfe9de 100644 --- a/Launcher/mod_profiles.h +++ b/Launcher/mod_profiles.h @@ -34,6 +34,9 @@ struct Profile { struct Profiles { std::string active; // the profile last loaded or saved; empty for none std::vector list; + // Loading a profile leaves the mods it does not name as they are, instead of switching them off: + // a profile then adds its mods to the player's own. + bool keep_others{}; Profile* find(std::string_view name); // `wanted`, or `wanted (2)`, `wanted (3)`... when a profile already has that name. @@ -59,14 +62,16 @@ Profile capture(std::string name, const mods::ModList& list); struct Applied { // What mods.json should hold: the profile's installed mods in its order with its - // switches, then every installed mod it does not name, switched off. + // switches, then every installed mod it does not name, switched off (or left as it is + // with keep_others). std::vector entries; // The mods it switches on that are not installed, to be downloaded. std::vector missing; }; -Applied apply(const Profile& profile, const mods::ModList& list); +Applied apply(const Profile& profile, const mods::ModList& list, bool keep_others = false); // True when loading `profile` would change nothing: the same mods on, in the same order. -bool in_use(const Profile& profile, const mods::ModList& list); +// With keep_others only the profile's own mods count. +bool in_use(const Profile& profile, const mods::ModList& list, bool keep_others = false); // export.r2x. parse_export throws a readable message for text that is not one. std::string export_text(const Profile& profile);