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.
This commit is contained in:
AyeZeeBB committed 2026-10-22 19:05:51 -05:00
1 parent 118c489fdb
commit 35824f4d1c
10 files changed
+274 -22

No files matched your search

+25
View File
@@ -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<launcher_mods::DataMod> 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) {
+15
View File
@@ -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<std::string> 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(),
+16 -1
View File
@@ -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<std::string> 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<std::string> 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<const char*, 7> log_levels{"trace", "debug", "info", "warning", "error", "critical", "off"};
@@ -495,7 +503,6 @@ struct Store {
// The GET MODS page.
std::array<char, 96> 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<char, 96> 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::string, std::vector<std::string>, std::less<>> categories_by_folder;
std::set<std::string, std::less<>> 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<std::pair<std::string, int>>& categories, const std::string& category);
bool category_menu(const char* id, std::vector<std::string>& only, std::vector<std::string>& hidden,
std::vector<std::pair<std::string, int>> 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.
+50 -1
View File
@@ -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<std::string> 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<std::string>().empty() && item.get<std::string>().size() <= 64 && list.size() < 64)
list.push_back(item.get<std::string>());
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<launcher_mods::DataMod> 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<ModProblem> problems;
if (!catalog.issue.empty())
+22 -3
View File
@@ -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<int> 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<std::string> 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<int> 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<int> view;
@@ -419,6 +428,7 @@ std::vector<int> 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<std::pair<std::string, int>> 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;
+86 -17
View File
@@ -208,7 +208,7 @@ bool nsfw(const ts::Package& package) {
[](const std::string& category) { return lower(category) == "nsfw"; });
}
std::vector<const ts::Package*> visible_packages(const Store& store, const ts::Installed& installed) {
std::vector<const ts::Package*> visible_packages(const Store& store, const ts::Installed& installed, const ts::CategoryFilter& categories) {
const auto query = lower(store.search.data());
std::vector<const ts::Package*> result;
for (const auto& package : store.packages) {
@@ -217,7 +217,7 @@ std::vector<const ts::Package*> 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<const ts::Package*> visible_packages(const Store& store, const ts::I
constexpr std::array<const char*, 5> 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<std::pair<std::string, int>> 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<std::pair<std::string, int>>& 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<std::string>& only, std::vector<std::string>& hidden,
std::vector<std::pair<std::string, int>> 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<std::string>& 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<std::string> 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::size_t>(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);
+27
View File
@@ -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<DataMod>& mods, const std::map<std::string, std::string, std::less<>>& 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<wchar_t, 32768> local{};
const auto size = GetEnvironmentVariableW(L"LOCALAPPDATA", local.data(), static_cast<DWORD>(local.size()));
if (!size || size >= local.size()) return {};
return fs::path(local.data()) / L"ReSkate" / L"Game" / L"Skate" / L"cache";
}
std::vector<std::string> empty_folder(const std::filesystem::path& folder) {
std::vector<std::string> 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);
+17
View File
@@ -3,6 +3,7 @@
#include <atomic>
#include <filesystem>
#include <functional>
#include <map>
#include <stdexcept>
#include <string>
#include <vector>
@@ -53,5 +54,21 @@ std::string install(const std::filesystem::path& mods_root, const std::filesyste
// Moves Mods/<name> 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<DataMod>& mods, const std::map<std::string, std::string, std::less<>>& 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<std::string> empty_folder(const std::filesystem::path& folder);
} // namespace dingosdk::launcher_mods
+7
View File
@@ -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<std::string>& 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<Package> 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.");
+9
View File
@@ -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<std::string> 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<std::string>& 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<std::string, std::string> split_dependency(std::string_view text);