Mod profiles: an option to keep other mods on when loading a profile

Loading a profile switches every installed mod it does not list off. With
"Keep my other mods on when loading a profile" (saved in profiles.json),
a profile switches its own mods on and leaves the rest as they are, so a
shared profile (a pack of mods) can be added to a player's own set. A
profile then counts as in use when its own mods match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
nxrootandClaude Opus 5.5 committed 2026-10-22 12:27:58 +01:00
1 parent 45346b122c
commit 076c693e64
4 files changed
+50 -15

No files matched your search

+18
View File
@@ -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<std::string>{"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<std::string>{"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();
+18 -8
View File
@@ -68,7 +68,7 @@ std::vector<std::string> 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<profiles::ProfileMod>& 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<int>(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<std::string> names;
for (const auto& mod : profile.mods)
+6 -4
View File
@@ -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<std::string, const mods::ModEntry*> 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<mods::ModEntry>& entries) {
std::vector<std::string> names;
+8 -3
View File
@@ -34,6 +34,9 @@ struct Profile {
struct Profiles {
std::string active; // the profile last loaded or saved; empty for none
std::vector<Profile> 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<mods::ModEntry> entries;
// The mods it switches on that are not installed, to be downloaded.
std::vector<ProfileMod> 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);