Plugins: the merge moves plugins' old files out before it deletes its folder

Plugins' files from before Mods/.reskate_plugin_data were still in Mods/.reskate/data, and the
launcher's merge deletes Mods/.reskate before the game ever looks for a plugin's folder: a saved
Deathrace route went at the next launch that rebuilt the merge. mods::keep_plugin_data moves each
plugin's old folder to Mods/.reskate_plugin_data first (whole, or the files a folder there lacks,
its own kept), at the start of every merge; the game still runs it once for a merge that kept its
patch. Checked in the build stamp test.
This commit is contained in:
AyeZeeBB committed 2026-10-20 14:09:09 -05:00
1 parent 39f322097a
commit e6de1dfd64
5 files changed
+67 -18

No files matched your search

+19 -1
View File
@@ -76,6 +76,24 @@ int main() {
check(listed(catalog.outdated, "OffOld") && !listed(catalog.inactive, "OffOld"), "A disabled outdated mod stayed in the merge");
check(std::ranges::any_of(catalog.warnings, [](const std::string &w) { return w.find("OtherBuild") != std::string::npos && w.find("outdated") != std::string::npos; }),
"No warning names the outdated mod");
// Plugins' files from before their own folder survive the merge's generated folder: moved whole,
// or merged into a folder the plugin has already, its own files kept.
write(mods_root / ".reskate" / "data" / "racer" / "routes" / "bam" / "yes.json", "{\"name\":\"yes\"}");
write(mods_root / ".reskate" / "data" / "both" / "old.txt", "old");
write(mods_root / ".reskate" / "data" / "both" / "same.txt", "old");
write(mods_root / ".reskate_plugin_data" / "both" / "same.txt", "new");
const auto moved = mods::keep_plugin_data(mods_root);
check(moved.size() == 2, "Both plugins' folders were not moved");
check(fs::is_regular_file(mods_root / ".reskate_plugin_data" / "racer" / "routes" / "bam" / "yes.json") &&
!fs::exists(mods_root / ".reskate" / "data" / "racer"),
"A plugin's folder was not moved whole");
std::ifstream same(mods_root / ".reskate_plugin_data" / "both" / "same.txt");
std::string kept;
std::getline(same, kept);
same.close();
check(fs::is_regular_file(mods_root / ".reskate_plugin_data" / "both" / "old.txt") && kept == "new",
"A plugin with a folder already did not get the files it lacked, or lost its own");
check(mods::keep_plugin_data(mods_root).size() <= 1, "Moving again moved more than the leftover copy");
fs::remove_all(root);
std::cout << "Mod build stamps: current, older Studio, other build, unstamped, park-only and disabled mods passed.\n";
std::cout << "Mod build stamps: current, older Studio, other build, unstamped, park-only and disabled mods passed; plugin data kept.\n";
}
+27
View File
@@ -318,6 +318,33 @@ std::vector<std::pair<std::string, bool>> read_order(const fs::path& path) {
} // namespace
std::vector<std::string> keep_plugin_data(const std::filesystem::path& mods_root) noexcept {
std::vector<std::string> moved;
try {
std::error_code error;
const auto old = mods_root / generated_folder / "data";
if (!fs::is_directory(old, error)) return moved;
const auto kept = mods_root / plugin_data_folder;
std::vector<fs::path> plugins; // listed first: the folder changes as they move
for (fs::directory_iterator it(old, error), end; !error && it != end; it.increment(error))
if (it->is_directory(error)) plugins.push_back(it->path());
for (const auto& from : plugins) {
const auto target = kept / from.filename();
std::error_code step;
fs::create_directories(kept, step);
if (!fs::exists(target, step)) fs::rename(from, target, step);
if (step || fs::exists(from, step)) {
// Another volume, a file in use, or the plugin has a folder there already: copy what
// it lacks, leaving its own files as they are.
step.clear();
fs::copy(from, target, fs::copy_options::recursive | fs::copy_options::skip_existing, step);
}
moved.push_back(path_utf8(from.filename()) + (step ? " (partly: " + step.message() + ")" : std::string{}));
}
} catch (...) {}
return moved;
}
std::string mod_fingerprint(const std::filesystem::path& directory) {
std::vector<std::string> files;
std::error_code error;
+5
View File
@@ -117,6 +117,11 @@ struct Exclusion {
};
// Names, sizes and write times of every file in the mod folder.
std::string mod_fingerprint(const std::filesystem::path& directory);
// Plugins' files from before plugin_data_folder (Mods/.reskate/data/<plugin>/, inside the generated
// folder a full merge deletes) move to Mods/.reskate_plugin_data/<plugin>/: a plugin's folder whole
// when it has none there yet, else the files it lacks. Run before anything deletes the generated
// folder. Never throws; returns what it moved, a line each, for the log.
std::vector<std::string> keep_plugin_data(const std::filesystem::path& mods_root) noexcept;
std::map<std::string, Exclusion, std::less<>> read_exclusions(const std::filesystem::path& mods_root) noexcept;
// Replaces the file; an empty map removes it.
void write_exclusions(const std::filesystem::path& mods_root,
+4
View File
@@ -89,6 +89,10 @@ MergeReport merge_mods(const Catalog& catalog, const MergeObserver& observe, con
};
const auto output = catalog.root / generated_folder;
std::error_code error;
// A merge that rebuilds deletes the generated folder: plugins' files from before they had
// a folder of their own go to it first.
for (auto& plugin : keep_plugin_data(catalog.root))
report.notes.push_back("Plugin data of " + plugin + " moved to Mods/" + plugin_data_folder);
std::vector<const Mod*> mods;
for (const auto& mod : catalog.mods) if (mod.provides_layout) mods.push_back(&mod);
+12 -17
View File
@@ -19,6 +19,7 @@
#include "Extension/Skater/board_pose.h"
#include "Engine/Vfs/mod_catalog.h"
#include "Engine/Core/Log/logging.h"
#include <mutex>
#include "Extension/Assets/live_mods.h"
#include "Extension/Skater/game_speed.h"
#include "Extension/Skater/grind_exit.h"
@@ -482,29 +483,23 @@ public:
}
// ---- files: <game>/Mods/.reskate_plugin_data/<plugin>/ (mod_list.h plugin_data_folder) -------------
static constexpr std::size_t most_file_bytes = 1u << 20, most_files = 1000;
// Before 2026-10-20 the folder was Mods/.reskate/data/<plugin>/, which a full merge deletes: what is
// still there moves across once, the first time the plugin's folder is looked for.
static void move_old_data(const std::filesystem::path& root, std::string_view plugin) {
std::error_code error;
if (std::filesystem::exists(root, error)) return;
const auto old = game_folder() / mods::mods_folder / mods::generated_folder / "data" / std::filesystem::path(std::string(plugin));
if (!std::filesystem::is_directory(old, error)) return;
std::filesystem::create_directories(root.parent_path(), error);
std::filesystem::rename(old, root, error);
if (error) {
// Another volume or a file in use: copy what can be copied, and leave the old folder.
error.clear();
std::filesystem::copy(old, root, std::filesystem::copy_options::recursive | std::filesystem::copy_options::skip_existing, error);
}
logging::write(logging::Level::info, logging::Channel::plugins,
"Plugins: " + std::string(plugin) + "'s data folder moved to Mods/" + mods::plugin_data_folder + (error ? " (partly: " + error.message() + ")" : ""));
// Before 2026-10-20 the folder was Mods/.reskate/data/<plugin>/, which a full merge deletes. The
// merge moves what is still there first (mods::keep_plugin_data); a merge that kept its patch did
// not run it, so the first look for a plugin's folder does.
static void move_old_data() {
static std::once_flag once;
std::call_once(once, [] {
for (const auto& plugin : mods::keep_plugin_data(game_folder() / mods::mods_folder))
logging::write(logging::Level::info, logging::Channel::plugins,
"Plugins: " + plugin + "'s data folder moved to Mods/" + mods::plugin_data_folder);
});
}
// The plugin's folder and the path in it, or why not. A path that resolves outside the folder
// (through a junction someone made in it) is refused.
static std::string data_file(std::string_view plugin, std::string_view path, std::filesystem::path& root, std::filesystem::path& file) {
try {
root = game_folder() / mods::mods_folder / mods::plugin_data_folder / std::filesystem::path(std::string(plugin));
move_old_data(root, plugin);
move_old_data();
file = path.empty() ? root : root / std::filesystem::path(std::string(path));
std::error_code error;
const auto real_root = std::filesystem::weakly_canonical(root, error);