diff --git a/Extension/Assets/live_mods.cpp b/Extension/Assets/live_mods.cpp index 59d9f5c..94dd560 100644 --- a/Extension/Assets/live_mods.cpp +++ b/Extension/Assets/live_mods.cpp @@ -8,6 +8,7 @@ #include "Engine/Core/Platform/path_text.h" #include "Engine/Game/Build/20260929/live_mods.h" #include "Engine/Game/Build/addresses.h" +#include "Engine/Vfs/builtin_packs.h" #include "Engine/Vfs/mod_catalog.h" #include "Engine/Vfs/mod_scoring.h" #include "Extension/World/loading_screen.h" @@ -508,6 +509,8 @@ ApplyEffect preview(const mods::ModList &list) { // One of a Board Break deck pack's decks stands for the pack (mod_catalog.h). if (const auto pack = mods::board_break_pack_of(name); named(now, pack.empty() ? name : pack)) continue; const auto *entry = entry_of(name); + // Content ReSkate keeps itself (builtin_packs.h) is in no mod list, and stays. + if (!entry && (name.starts_with("reskate-builtin-") || mods::kept_by_reskate(list.root, name))) continue; // A folder deleted since: what it held is unknown, so assume the worst. if (!entry) { reload = true; diff --git a/Extension/UI/Overlay/modding_menu.cpp b/Extension/UI/Overlay/modding_menu.cpp index 19f8a80..016cb15 100644 --- a/Extension/UI/Overlay/modding_menu.cpp +++ b/Extension/UI/Overlay/modding_menu.cpp @@ -1,6 +1,7 @@ #include "modding_menu.h" #include "skate_menu.h" #include "skate_menu_internal.h" +#include "Engine/Vfs/builtin_packs.h" #include "Engine/Vfs/initfs.h" #include "Engine/Vfs/mod_catalog.h" #include "Extension/Assets/live_mods.h" @@ -151,6 +152,7 @@ std::string mod_contents(const mods::Mod& mod) { // mod folder added since launch. void draw_installed_mods(SkateMenu& menu) { static auto* scans = new FolderRead; // process lifetime, as the threads that fill it + static auto* officials = new FolderRead>; static std::string action_error; const auto scanned = scans->get([] { return mods::scan_mods(mods::engine_data_root()); }, 1500); if (!scanned) { @@ -160,6 +162,17 @@ void draw_installed_mods(SkateMenu& menu) { const auto& list = *scanned; const auto& launch = mods::catalog(); const auto applied = live_mods::applied_mods(); + // Official mods (builtin_packs.h) live outside the Mods folder's list: ReSkate keeps them, + // always on. Without them here, the ones in effect read as mods whose folder was deleted. + const auto official_read = officials->get([root = list.root] { return mods::official_packs(root); }, 1500); + const auto official = official_read ? *official_read : std::vector{}; + const auto kept = [&](const std::string& name) { + const auto pack = mods::board_break_pack_of(name); + const auto& base = pack.empty() ? name : pack; + return base.starts_with("reskate-builtin-") || std::ranges::any_of(official, [&](const mods::BuiltinPack& other) { + return _stricmp(other.name.c_str(), base.c_str()) == 0; + }); + }; // A Board Break deck pack is in effect when one of the decks inside it is (mod_catalog.h). const auto in_effect = [&](const std::string& name) { return std::ranges::any_of(applied, [&](const std::string& other) { @@ -182,10 +195,17 @@ void draw_installed_mods(SkateMenu& menu) { if (effect) ++loaded; if (entry.enabled != effect && !excluded(entry.mod.name) && !set_aside(entry)) ++pending; } + const auto official_aside = [&](const std::string& name) { + return !in_effect(name) && std::ranges::any_of(aside, [&](const std::string& other) { return _stricmp(other.c_str(), name.c_str()) == 0; }); + }; + for (const auto& pack : official) { + if (in_effect(pack.name)) ++loaded; + else if (!official_aside(pack.name)) ++pending; // put in place since the game started: applying loads it + } // Mods still in effect whose folder has been deleted: applying unloads them. std::vector removed; for (const auto& name : applied) - if (mods::board_break_pack_of(name).empty() && + if (mods::board_break_pack_of(name).empty() && !kept(name) && std::ranges::none_of(list.entries, [&](const mods::ModEntry& entry) { return _stricmp(entry.mod.name.c_str(), name.c_str()) == 0; })) removed.push_back(name); pending += removed.size(); @@ -300,6 +320,23 @@ void draw_installed_mods(SkateMenu& menu) { entry.enabled ? "Disable" : "Enable")) toggle = i; } + for (const auto& pack : official) { + const auto* known = [&]() -> const mods::Mod* { + for (const auto& mod : launch.mods) + if (_stricmp(mod.name.c_str(), pack.name.c_str()) == 0) return &mod; + return nullptr; + }(); + auto title = known && !known->title.empty() ? known->title : pack.name.substr(pack.name.find('-') + 1); + if (!known) std::replace(title.begin(), title.end(), '_', ' '); + auto line = known ? byline(*known) : "by " + pack.name.substr(0, pack.name.find('-')); + line += " - always on"; + const bool effect = in_effect(pack.name), set_by_deck = official_aside(pack.name); + tile(index++, title, line, effect ? "OFFICIAL" : set_by_deck ? "SET ASIDE" : "APPLY TO LOAD", + effect ? skate_theme::good : set_by_deck ? skate_theme::tile_light : skate_theme::danger, + known ? mod_contents(*known) : "", + "An official mod: ReSkate keeps it for every player at its latest version, always on. " + "The launcher installs and updates it, so it cannot be disabled here.", nullptr); + } for (const auto& name : removed) tile(index++, name, "Folder deleted", "APPLY TO UNLOAD", skate_theme::danger, "", "This mod's folder is gone, but it stays loaded until you apply.", nullptr);