mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-26 12:46:43 +01:00
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:
1 parent
118c489fdb
commit
35824f4d1c
10 files changed
+274
-22
No files matched your search
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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.");
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user