From 35824f4d1c1380a7e36f8dbedcaeba9866f32cc6 Mon Sep 17 00:00:00 2001 From: AyeZeeBB Date: Thu, 22 Oct 2026 19:05:51 -0500 Subject: [PATCH] Launcher: categories shown only or hidden in GET MODS and MY MODS, and the game's shader cache emptied when the mods change - The category menu on GET MODS, and a new one on MY MODS, take each category as shown only, hidden or either (a click goes round them), with how many mods are in each; each page's choice is kept in the launcher's settings. On MY MODS a mod Thunderstore does not list, or lists in no category, is under "No category". - Before a launch whose mods give the game other data than the last launch's (the mods with a layout, their files and load order, and the variants in play), the game's own shader cache (%LOCALAPPDATA%\ReSkate\Game\Skate\cache) is emptied: shaders kept from earlier mods were found to break the picture after many changes. The shader_cache folder the game ships with is left alone. --- Launcher/Test/mod_manager_tests.cpp | 25 +++++++ Launcher/Test/thunderstore_tests.cpp | 15 ++++ Launcher/gui_internal.h | 17 ++++- Launcher/gui_launcher.cpp | 51 ++++++++++++- Launcher/gui_mods.cpp | 25 ++++++- Launcher/gui_mods_browse.cpp | 103 ++++++++++++++++++++++----- Launcher/mod_manager.cpp | 27 +++++++ Launcher/mod_manager.h | 17 +++++ Launcher/thunderstore.cpp | 7 ++ Launcher/thunderstore.h | 9 +++ 10 files changed, 274 insertions(+), 22 deletions(-) diff --git a/Launcher/Test/mod_manager_tests.cpp b/Launcher/Test/mod_manager_tests.cpp index a418dec0..9d7b1013 100644 --- a/Launcher/Test/mod_manager_tests.cpp +++ b/Launcher/Test/mod_manager_tests.cpp @@ -466,6 +466,31 @@ int main() { "Only a retired mod is left out"); } + { + // What the mods give the game's data, for emptying the game's shader cache when it changes: the mods + // with a layout, in load order, with their files, and the variants in play. + const auto park = root / L"signature" / L"Park", skin = root / L"signature" / L"Skin"; + write(park / L"layout.toc", "toc"); + write(skin / L"layout.toc", "toc"); + const std::vector both{{"Park", park}, {"Skin", skin}}, swapped{{"Skin", skin}, {"Park", park}}; + const auto before = launcher_mods::game_data_signature(both, {}); + check(before == launcher_mods::game_data_signature(both, {}), "The same mods give the same signature"); + check(before != launcher_mods::game_data_signature(swapped, {}), "Another load order is another signature"); + check(before != launcher_mods::game_data_signature({{"Park", park}}, {}), "A mod taken out is another"); + check(before != launcher_mods::game_data_signature(both, {{"Skin", "red"}}), "Another variant is another"); + write(skin / L"Win32" / L"skin.sb", "bundle"); + check(before != launcher_mods::game_data_signature(both, {}), "A mod's files changing is another"); + // Emptying the cache: everything in it goes, the folder stays. + const auto cache = root / L"cache"; + write(cache / L"6803938.PcDx12", "shaders"); + write(cache / L"nested" / L"cache.manifest", "manifest"); + check(launcher_mods::empty_folder(cache).empty() && fs::is_directory(cache) && fs::is_empty(cache), "The shader cache is emptied, its folder kept"); + check(launcher_mods::empty_folder(root / L"no such folder").empty(), "No cache folder is nothing to do"); + check(launcher_mods::game_shader_cache().filename() == L"cache" && + launcher_mods::game_shader_cache().parent_path().filename() == L"Skate", + "The game's shader cache is ReSkate's own, under Game/Skate"); + } + std::error_code ignored; fs::remove_all(root, ignored); if (failures) { diff --git a/Launcher/Test/thunderstore_tests.cpp b/Launcher/Test/thunderstore_tests.cpp index b124df25..13df5082 100644 --- a/Launcher/Test/thunderstore_tests.cpp +++ b/Launcher/Test/thunderstore_tests.cpp @@ -87,6 +87,21 @@ int main() { check(desert.latest().file_size == 5368709120ull, "Sizes over 4 GB survive"); check(desert.downloads == 15 && desert.in_category("Maps") && !desert.in_category("Audio"), "Downloads add up across versions and categories are listed"); + { + // The GET MODS list's categories: hidden ones win, and with any shown only, a package needs one. + thunderstore::Package both; + both.categories = {"Maps", "Tools"}; + thunderstore::Package plain; + check(thunderstore::shown_by(both, {}) && thunderstore::shown_by(plain, {}), "No filter shows everything"); + check(thunderstore::shown_by(both, {{"Audio", "Maps"}, {}}) && !thunderstore::shown_by(plain, {{"Maps"}, {}}), + "Shown only: a package in any of them shows, one in none does not"); + check(!thunderstore::shown_by(both, {{}, {"Tools"}}) && thunderstore::shown_by(plain, {{}, {"Tools"}}), + "Hidden: a package in it is left out, one without it shows"); + check(!thunderstore::shown_by(both, {{"Maps"}, {"Tools"}}), "A hidden category wins over one shown only"); + const std::vector none{"No category"}; + check(thunderstore::shown_by(none, {{"No category"}, {}}) && !thunderstore::shown_by(none, {{}, {"No category"}}), + "An installed mod's own categories go by the same rule (MY MODS)"); + } const auto& cam = packages.at(1); check(cam.deprecated && cam.latest().icon.empty(), "Flags are read and non-HTTPS icons are dropped"); check(!error_of([] { thunderstore::parse_listing(R"({"error": "No cache available"})"); }).empty(), diff --git a/Launcher/gui_internal.h b/Launcher/gui_internal.h index 610cf583..5d8f0756 100644 --- a/Launcher/gui_internal.h +++ b/Launcher/gui_internal.h @@ -218,6 +218,14 @@ struct Settings { std::uint64_t accepted_privacy{}, accepted_terms{}; // The version of the home screen's announcement the player hid (0: none): one newer shows. std::uint64_t announcement_hidden{}; + // GET MODS: the Thunderstore categories shown only, and those hidden (thunderstore.h CategoryFilter). + std::vector mod_categories_only, mod_categories_hidden; + // MY MODS: the same for the installed mods, by their Thunderstore categories ("No category" for a + // mod Thunderstore does not list, or lists in none). + std::vector installed_categories_only, installed_categories_hidden; + // What the mods gave the game's data at the last launch (launcher_mods::game_data_signature): a + // launch with other mods empties the game's shader cache first. + std::string shader_cache_mods; }; inline constexpr std::array log_levels{"trace", "debug", "info", "warning", "error", "critical", "off"}; @@ -495,7 +503,6 @@ struct Store { // The GET MODS page. std::array search{}; - std::string category; // empty = all int sort{}; std::string selected; // full_name, while its overview is open bool overview{}; // the overview popup is showing @@ -570,6 +577,8 @@ struct ModsPanel { std::array search{}; int filter{}; int order{}; + // Each Thunderstore package's categories by the folder it installs into, made again when a listing arrives. + std::map, std::less<>> categories_by_folder; std::set> marked; std::string anchor; // the last mod ticked: where a Shift-click range starts int place{}; // typed in a row's "move to position" popup @@ -700,6 +709,12 @@ ImTextureID package_icon(ModsPanel& panel, const thunderstore::Package& package) // when the mod never came from Thunderstore. void mod_icon(ModsPanel& panel, const thunderstore::Package* package, ImVec2 position, float size); void pump_icons(ModsPanel& panel); +// A category menu for a list of mods (GET MODS, MY MODS): each category shown only, hidden, or either, +// a click going round them, with how many mods are in each (category_count adds to that). One chosen +// before that no mod has now stays in the menu. True when the player changed what is chosen. +int& category_count(std::vector>& categories, const std::string& category); +bool category_menu(const char* id, std::vector& only, std::vector& hidden, + std::vector> categories, float width); // The GET MODS page body, under the page header. void browse_page(Launcher& launcher, const Fonts& fonts, ModsPanel& panel, float height, bool installing); // Everything Thunderstore knows about one package, in a popup over the page. diff --git a/Launcher/gui_launcher.cpp b/Launcher/gui_launcher.cpp index 8fa842ef..a0639b0b 100644 --- a/Launcher/gui_launcher.cpp +++ b/Launcher/gui_launcher.cpp @@ -1,6 +1,8 @@ #include "gui_internal.h" #include "depot_output.h" +#include "mod_manager.h" +#include "text_encoding.h" #include "start_maps.h" #include "Engine/Core/Json/json.h" @@ -76,6 +78,20 @@ Settings load_settings(const fs::path& path) { settings.accepted_privacy = json.value("accepted_privacy_version", settings.accepted_privacy); settings.accepted_terms = json.value("accepted_terms_version", settings.accepted_terms); settings.announcement_hidden = json.value("announcement_hidden", settings.announcement_hidden); + // At most 64 categories each, every one a short name. + const auto categories = [&](const char* key) { + std::vector list; + if (const auto found = json.find(key); found != json.end() && found->is_array()) + for (const auto& item : *found) + if (item.is_string() && !item.get().empty() && item.get().size() <= 64 && list.size() < 64) + list.push_back(item.get()); + return list; + }; + settings.mod_categories_only = categories("mod_categories_only"); + settings.mod_categories_hidden = categories("mod_categories_hidden"); + settings.installed_categories_only = categories("installed_categories_only"); + settings.installed_categories_hidden = categories("installed_categories_hidden"); + settings.shader_cache_mods = json.value("shader_cache_mods", std::string()); } catch (const std::exception& exception) { logging::log(logging::Level::warning, logging::Channel::launcher, "Ignoring launcher settings: {}", exception.what()); } @@ -107,6 +123,11 @@ void save_settings(const fs::path& path, const Settings& settings) { json["accepted_privacy_version"] = settings.accepted_privacy; json["announcement_hidden"] = settings.announcement_hidden; json["accepted_terms_version"] = settings.accepted_terms; + json["mod_categories_only"] = settings.mod_categories_only; + json["mod_categories_hidden"] = settings.mod_categories_hidden; + json["installed_categories_only"] = settings.installed_categories_only; + json["installed_categories_hidden"] = settings.installed_categories_hidden; + json["shader_cache_mods"] = settings.shader_cache_mods; std::ofstream output(path, std::ios::binary | std::ios::trunc); output << json.dump(2) << '\n'; } @@ -509,13 +530,35 @@ void Launcher::run_download(bool validate) { fail(std::format("The Steam download did not finish (exit code {}). {}", code, output.message)); } +// Shaders the game built for one set of mods, kept in its shader cache and reused by the next launch, +// were found to break the picture once the mods had changed many times: a launch with other mods than +// the last one's (what they give the game's data: launcher_mods::game_data_signature) starts the game +// with its shader cache empty, and it builds the shaders again. +void clear_shader_cache_if_mods_changed(Settings& settings, const std::string& signature) { + if (signature == settings.shader_cache_mods) return; + const auto cache = launcher_mods::game_shader_cache(); + const auto failed = launcher_mods::empty_folder(cache); + if (failed.empty()) { + logging::log(logging::Level::info, logging::Channel::launcher, "The mods changed since the last launch: the game's shader cache is emptied ({}).", + launcher_text::utf8(cache.wstring())); + settings.shader_cache_mods = signature; + return; + } + // (Tried again at the next launch.) + for (const auto& line : failed) + logging::log(logging::Level::warning, logging::Channel::launcher, "The game's shader cache could not be emptied: {}", line); +} + // The merge the game does behind its splash, done here instead, so a mod that // cannot be merged is a question the launcher can ask rather than a surprise // three minutes into a loading screen. The game reuses what this builds. bool Launcher::run_mod_merge() { const auto root = mods_data_root(); std::error_code error; - if (!fs::is_directory(root / mods::mods_folder, error)) return true; + if (!fs::is_directory(root / mods::mods_folder, error)) { + clear_shader_cache_if_mods_changed(settings_, {}); + return true; + } set(Phase::merging, "Preparing mods", "Merging what is installed", 0); trace::Scope timing("launcher.mod_merge", "task"); auto catalog = mods::load_catalog(root, [this](const mods::MergeProgress& progress) { @@ -529,6 +572,12 @@ bool Launcher::run_mod_merge() { logging::write(logging::Level::info, logging::Channel::launcher, note); for (const auto& warning : catalog.warnings) logging::write(logging::Level::warning, logging::Channel::launcher, warning); + { + std::vector data; + for (const auto& mod : catalog.mods) + if (mod.provides_layout) data.push_back({mod.name, mod.directory}); + clear_shader_cache_if_mods_changed(settings_, launcher_mods::game_data_signature(data, catalog.variants_in_play)); + } std::vector problems; if (!catalog.issue.empty()) diff --git a/Launcher/gui_mods.cpp b/Launcher/gui_mods.cpp index cd5c7684..9dcf3908 100644 --- a/Launcher/gui_mods.cpp +++ b/Launcher/gui_mods.cpp @@ -401,7 +401,16 @@ std::string kind_of(const mods::Mod& mod) { // The rows the list shows, as places in the load order: what the search and the // filter leave, in the order asked for. -std::vector visible_mods(const ModsPanel& panel, const thunderstore::Installed& installed) { +// An installed mod's categories, as Thunderstore lists its package ("No category" when it lists none, +// or does not list the mod at all). +constexpr const char* no_category = "No category"; +std::vector categories_of(const ModsPanel& panel, const mods::Mod& mod) { + const auto found = panel.categories_by_folder.find(mod.name); + if (found == panel.categories_by_folder.end() || found->second.empty()) return {no_category}; + return found->second; +} + +std::vector visible_mods(const ModsPanel& panel, const thunderstore::Installed& installed, const thunderstore::CategoryFilter& categories) { const auto& entries = panel.list.entries; const auto query = folded(panel.search.data()); std::vector view; @@ -419,6 +428,7 @@ std::vector visible_mods(const ModsPanel& panel, const thunderstore::Instal case 7: keep = mod.outdated.empty() && (!conflict_winner(panel, mod.name).empty() || conflict_beats(panel, mod.name)); break; default: break; } + if (keep && !categories.empty()) keep = thunderstore::shown_by(categories_of(panel, mod), categories); if (keep && !query.empty()) keep = folded(mod.title).find(query) != std::string::npos || folded(mod.name).find(query) != std::string::npos || @@ -1124,7 +1134,7 @@ void installed_page(Launcher& launcher, const Fonts& fonts, ModsPanel& panel, co // ------------------------------------------------ search, filter, order, check, refresh const float combo = S(150), check = S(160), refresh = S(96), gap = S(10); - ImGui::SetNextItemWidth(std::max(S(140), ImGui::GetContentRegionAvail().x - (combo + gap) * 2 - check - gap - refresh - gap)); + ImGui::SetNextItemWidth(std::max(S(140), ImGui::GetContentRegionAvail().x - (combo + gap) * 3 - check - gap - refresh - gap)); // Ctrl+F jumps to the search, as it does in a browser. if (!installing && ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_F, false) && !ImGui::IsPopupOpen(nullptr, ImGuiPopupFlags_AnyPopupId)) @@ -1142,6 +1152,15 @@ void installed_page(Launcher& launcher, const Fonts& fonts, ModsPanel& panel, co }; choice("##installed_filter", filter_names, panel.filter); ImGui::SameLine(0, gap); + { + std::vector> categories; + for (const auto& entry : entries) + for (const auto& category : categories_of(panel, entry.mod)) ++category_count(categories, category); + auto& settings = launcher.settings(); + if (category_menu("##installed_category", settings.installed_categories_only, settings.installed_categories_hidden, std::move(categories), combo)) + launcher.save(); + } + ImGui::SameLine(0, gap); choice("##installed_order", order_names, panel.order); ImGui::SameLine(0, gap); { @@ -1156,7 +1175,7 @@ void installed_page(Launcher& launcher, const Fonts& fonts, ModsPanel& panel, co ImGui::SameLine(0, gap); if (ImGui::Button("Refresh", ImVec2(refresh, 0))) refresh_mods(launcher, panel); - const auto view = visible_mods(panel, installed); + const auto view = visible_mods(panel, installed, {launcher.settings().installed_categories_only, launcher.settings().installed_categories_hidden}); // Dragging, and typing a place, move a mod among the others in the load order: the order the // list must be shown in for where it lands to be what it looks like. const bool reorder = panel.order == 0 && !installing; diff --git a/Launcher/gui_mods_browse.cpp b/Launcher/gui_mods_browse.cpp index 3ba0f5c8..1477fe1a 100644 --- a/Launcher/gui_mods_browse.cpp +++ b/Launcher/gui_mods_browse.cpp @@ -208,7 +208,7 @@ bool nsfw(const ts::Package& package) { [](const std::string& category) { return lower(category) == "nsfw"; }); } -std::vector visible_packages(const Store& store, const ts::Installed& installed) { +std::vector visible_packages(const Store& store, const ts::Installed& installed, const ts::CategoryFilter& categories) { const auto query = lower(store.search.data()); std::vector result; for (const auto& package : store.packages) { @@ -217,7 +217,7 @@ std::vector visible_packages(const Store& store, const ts::I // or a category of that name. A deprecated one only shows once installed. if (nsfw(package)) continue; if (package.deprecated && !have) continue; - if (!store.category.empty() && !package.in_category(store.category)) continue; + if (!ts::shown_by(package, categories)) continue; if (!query.empty() && lower(package.title()).find(query) == std::string::npos && lower(package.owner).find(query) == std::string::npos && lower(package.latest().description).find(query) == std::string::npos) continue; @@ -240,6 +240,17 @@ std::vector visible_packages(const Store& store, const ts::I constexpr std::array sort_names{"Last updated", "Most downloaded", "Newest", "Top rated", "Name"}; +// GET MODS's categories: Thunderstore's, with how many mods are in each, kept in the launcher's settings. +void category_filter(Launcher& launcher, const Store& store, float width) { + std::vector> categories; + for (const auto& package : store.packages) { + if (nsfw(package)) continue; // (and no category that only they have) + for (const auto& category : package.categories) ++category_count(categories, category); + } + if (category_menu("##category", launcher.settings().mod_categories_only, launcher.settings().mod_categories_hidden, std::move(categories), width)) + launcher.save(); +} + // What installing `package` does now: install, update or reinstall. std::string action_label(const ts::Package& package, const ts::Installed& installed) { const auto found = installed.find(ts::folder_for(package.full_name)); @@ -250,6 +261,75 @@ std::string action_label(const ts::Package& package, const ts::Installed& instal } // namespace +int& category_count(std::vector>& categories, const std::string& category) { + for (auto& entry : categories) + if (entry.first == category) return entry.second; + return categories.emplace_back(category, 0).second; +} + +bool category_menu(const char* id, std::vector& only, std::vector& hidden, + std::vector> categories, float width) { + // One chosen before that no mod has now stays in the list, so it can be put back. + for (const auto* list : {&only, &hidden}) + for (const auto& category : *list) (void)category_count(categories, category); + std::sort(categories.begin(), categories.end(), [](const auto& a, const auto& b) { return lower(a.first) < lower(b.first); }); + const auto in = [](const std::vector& list, const std::string& name) { return std::find(list.begin(), list.end(), name) != list.end(); }; + std::string shown; + if (only.empty() && hidden.empty()) shown = "All categories"; + else if (hidden.empty()) shown = only.size() == 1 ? "Only " + only.front() : std::format("Only {} categories", only.size()); + else if (only.empty()) shown = hidden.size() == 1 ? "Hiding " + hidden.front() : std::format("Hiding {} categories", hidden.size()); + else shown = std::format("Only {}, hiding {}", only.size(), hidden.size()); + ImGui::SetNextItemWidth(width); + if (!ImGui::BeginCombo(id, shown.c_str(), ImGuiComboFlags_HeightLarge)) { + if (ImGui::IsItemHovered() && !(only.empty() && hidden.empty())) { + std::string tip; + if (!only.empty()) { + tip = "Showing only:"; + for (const auto& category : only) tip += "\n " + category; + } + if (!hidden.empty()) { + tip += std::string(tip.empty() ? "" : "\n") + "Hiding:"; + for (const auto& category : hidden) tip += "\n " + category; + } + ImGui::SetTooltip("%s", tip.c_str()); + } + return false; + } + bool changed = false; + ImGui::PushStyleColor(ImGuiCol_Text, color::muted); + ImGui::TextUnformatted("Click a category to show only it,"); + ImGui::TextUnformatted("again to hide it, again for either."); + ImGui::PopStyleColor(); + if (ImGui::Selectable("Every category", only.empty() && hidden.empty(), ImGuiSelectableFlags_DontClosePopups)) { + changed = !only.empty() || !hidden.empty(); + only.clear(); + hidden.clear(); + } + ImGui::Separator(); + for (const auto& [category, mods] : categories) { + const bool is_only = in(only, category), is_hidden = in(hidden, category); + const auto label = std::format("{} {} ({})##{}", is_only ? "+" : is_hidden ? "-" : " ", category, mods, category); + ImGui::PushStyleColor(ImGuiCol_Text, is_only ? color::good : is_hidden ? color::danger : color::text); + if (ImGui::Selectable(label.c_str(), is_only || is_hidden, ImGuiSelectableFlags_DontClosePopups)) { + // Either, then shown only, then hidden, then either again. + if (is_only) { + std::erase(only, category); + hidden.push_back(category); + } else if (is_hidden) { + std::erase(hidden, category); + } else { + only.push_back(category); + } + changed = true; + } + ImGui::PopStyleColor(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", is_only ? "Shown only. Click to hide it." : is_hidden ? "Hidden. Click to show it as any other." : "Click to show only this category (and any others shown only)."); + } + ImGui::EndCombo(); + return changed; +} + // Placed by hand: a row lays its own icon, text and badges out. void mod_icon(ModsPanel& panel, const thunderstore::Package* package, ImVec2 position, float size) { const ImVec2 end(position.x + size, position.y + size); @@ -300,6 +380,8 @@ void collect_listing(ModsPanel& panel) { if (store.incoming_error.empty()) { store.packages = std::move(store.incoming); store.loaded = true; + panel.categories_by_folder.clear(); + for (const auto& package : store.packages) panel.categories_by_folder[ts::folder_for(package.full_name)] = package.categories; store.error.clear(); } else { store.error = std::move(store.incoming_error); @@ -484,20 +566,7 @@ void browse_page(Launcher& launcher, const Fonts& fonts, ModsPanel& panel, float ImGui::SetKeyboardFocusHere(); ImGui::InputTextWithHint("##search", "Search mods (Ctrl+F)", store.search.data(), store.search.size()); ImGui::SameLine(); - std::vector categories; - for (const auto& package : store.packages) { - if (nsfw(package)) continue; // (and no category that only they have) - for (const auto& category : package.categories) - if (std::find(categories.begin(), categories.end(), category) == categories.end()) categories.push_back(category); - } - std::sort(categories.begin(), categories.end()); - ImGui::SetNextItemWidth(combo); - if (ImGui::BeginCombo("##category", store.category.empty() ? "All categories" : store.category.c_str())) { - if (ImGui::Selectable("All categories", store.category.empty())) store.category.clear(); - for (const auto& category : categories) - if (ImGui::Selectable(category.c_str(), store.category == category)) store.category = category; - ImGui::EndCombo(); - } + category_filter(launcher, store, combo); ImGui::SameLine(); ImGui::SetNextItemWidth(combo); if (ImGui::BeginCombo("##sort", sort_names[static_cast(std::clamp(store.sort, 0, 4))])) { @@ -550,7 +619,7 @@ void browse_page(Launcher& launcher, const Fonts& fonts, ModsPanel& panel, float } ImGui::Spacing(); const float body = std::max(S(120), height - (ImGui::GetCursorPosY() - top)); - const auto packages = visible_packages(store, installed); + const auto packages = visible_packages(store, installed, {launcher.settings().mod_categories_only, launcher.settings().mod_categories_hidden}); // ------------------------------------------------ the list ImGui::BeginChild("##store_list", ImVec2(0, body), ImGuiChildFlags_NavFlattened); diff --git a/Launcher/mod_manager.cpp b/Launcher/mod_manager.cpp index cd44bb60..44ee9845 100644 --- a/Launcher/mod_manager.cpp +++ b/Launcher/mod_manager.cpp @@ -436,6 +436,33 @@ std::string install(const fs::path& mods, const fs::path& source, bool replace, } } +std::string game_data_signature(const std::vector& mods, const std::map>& variants) { + std::string text; + for (const auto& mod : mods) text += mod.name + '=' + mods::mod_fingerprint(mod.directory) + ';'; + for (const auto& [mod, variant] : variants) text += "variant " + mod + '=' + variant + ';'; + return text; +} + +std::filesystem::path game_shader_cache() { + std::array local{}; + const auto size = GetEnvironmentVariableW(L"LOCALAPPDATA", local.data(), static_cast(local.size())); + if (!size || size >= local.size()) return {}; + return fs::path(local.data()) / L"ReSkate" / L"Game" / L"Skate" / L"cache"; +} + +std::vector empty_folder(const std::filesystem::path& folder) { + std::vector failed; + std::error_code error; + if (folder.empty() || !fs::is_directory(folder, error)) return failed; + for (fs::directory_iterator it(folder, error), end; !error && it != end; it.increment(error)) { + std::error_code removing; + fs::remove_all(it->path(), removing); + if (removing) failed.push_back(utf8(it->path().filename().wstring()) + ": " + removing.message()); + } + if (error) failed.push_back("the folder could not be read: " + error.message()); + return failed; +} + void remove(const fs::path& mods, const std::string& name) { if (!mods::valid_mod_name(name)) fail("\"" + name + "\" is not a mod folder."); const auto target = mods / from_utf8(name); diff --git a/Launcher/mod_manager.h b/Launcher/mod_manager.h index 5bfabddb..5f9f69ae 100644 --- a/Launcher/mod_manager.h +++ b/Launcher/mod_manager.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -53,5 +54,21 @@ std::string install(const std::filesystem::path& mods_root, const std::filesyste // Moves Mods/ to the Recycle Bin. Throws a user-facing message. void remove(const std::filesystem::path& mods_root, const std::string& name); +// What the mods give the game's data, as one line: each mod that ships a layout, in load order, with +// its folder name and its files' fingerprint (Engine/Vfs/mod_list.h mod_fingerprint), then the +// variants in play. A mod that only holds a plugin, or loose files the game does not draw with, is not +// in it. When it changes between launches, the launcher empties the game's shader cache: shaders built +// for the last set of mods, kept and reused, were found to break the picture after many changes. +struct DataMod { + std::string name; + std::filesystem::path directory; +}; +std::string game_data_signature(const std::vector& mods, const std::map>& variants); +// The game's own shader cache, kept apart from the normal game's (Extension/Boot/user_data_redirect.cpp): +// %LOCALAPPDATA%\ReSkate\Game\Skate\cache. Not the shader_cache folder the game ships with. +std::filesystem::path game_shader_cache(); +// Removes everything in `folder`, never the folder itself. What could not be removed, a line each. +std::vector empty_folder(const std::filesystem::path& folder); + } // namespace dingosdk::launcher_mods diff --git a/Launcher/thunderstore.cpp b/Launcher/thunderstore.cpp index 25aefea4..86263fd9 100644 --- a/Launcher/thunderstore.cpp +++ b/Launcher/thunderstore.cpp @@ -203,6 +203,13 @@ bool Package::in_category(std::string_view category) const { return std::find(categories.begin(), categories.end(), category) != categories.end(); } +bool shown_by(const Package& package, const CategoryFilter& filter) { return shown_by(package.categories, filter); } +bool shown_by(const std::vector& categories, const CategoryFilter& filter) { + const auto in = [&](const std::string& category) { return std::find(categories.begin(), categories.end(), category) != categories.end(); }; + if (std::any_of(filter.hidden.begin(), filter.hidden.end(), in)) return false; + return filter.only.empty() || std::any_of(filter.only.begin(), filter.only.end(), in); +} + std::vector parse_listing(std::string_view json) { const auto root = Json::parse(json, listing_limits); if (!root.is_array()) fail("The Thunderstore listing is not a JSON array."); diff --git a/Launcher/thunderstore.h b/Launcher/thunderstore.h index 481db0c5..7bd9516b 100644 --- a/Launcher/thunderstore.h +++ b/Launcher/thunderstore.h @@ -63,6 +63,15 @@ struct Package { Package at_version(std::string_view number) const; }; +// The GET MODS list's categories: those shown only (a package needs one of them, when there are +// any) and those hidden (a package in any of them is left out, whatever else it is in). +struct CategoryFilter { + std::vector only, hidden; + bool empty() const { return only.empty() && hidden.empty(); } +}; +bool shown_by(const Package& package, const CategoryFilter& filter); +bool shown_by(const std::vector& categories, const CategoryFilter& filter); + // "Team-Mod-1.2.3" as {"Team-Mod", "1.2.3"}: how a dependency is named. The version is // empty when the text does not end in one. std::pair split_dependency(std::string_view text);