From e6de1dfd648e17ff11461f99c29f98624bc67324 Mon Sep 17 00:00:00 2001 From: AyeZeeBB Date: Tue, 20 Oct 2026 14:09:09 -0500 Subject: [PATCH] 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. --- Engine/Vfs/Test/mod_build_stamp_tests.cpp | 20 +++++++++++++++- Engine/Vfs/mod_list.cpp | 27 +++++++++++++++++++++ Engine/Vfs/mod_list.h | 5 ++++ Engine/Vfs/mod_merge.cpp | 4 ++++ Extension/Plugins/plugin_game_runtime.cpp | 29 ++++++++++------------- 5 files changed, 67 insertions(+), 18 deletions(-) diff --git a/Engine/Vfs/Test/mod_build_stamp_tests.cpp b/Engine/Vfs/Test/mod_build_stamp_tests.cpp index 3d3e88c3..8257eb72 100644 --- a/Engine/Vfs/Test/mod_build_stamp_tests.cpp +++ b/Engine/Vfs/Test/mod_build_stamp_tests.cpp @@ -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"; } diff --git a/Engine/Vfs/mod_list.cpp b/Engine/Vfs/mod_list.cpp index 19ba80c9..57135953 100644 --- a/Engine/Vfs/mod_list.cpp +++ b/Engine/Vfs/mod_list.cpp @@ -318,6 +318,33 @@ std::vector> read_order(const fs::path& path) { } // namespace +std::vector keep_plugin_data(const std::filesystem::path& mods_root) noexcept { + std::vector 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 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 files; std::error_code error; diff --git a/Engine/Vfs/mod_list.h b/Engine/Vfs/mod_list.h index 370ad530..d8c40459 100644 --- a/Engine/Vfs/mod_list.h +++ b/Engine/Vfs/mod_list.h @@ -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//, inside the generated +// folder a full merge deletes) move to Mods/.reskate_plugin_data//: 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 keep_plugin_data(const std::filesystem::path& mods_root) noexcept; std::map> 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, diff --git a/Engine/Vfs/mod_merge.cpp b/Engine/Vfs/mod_merge.cpp index 13950f65..a1aa5808 100644 --- a/Engine/Vfs/mod_merge.cpp +++ b/Engine/Vfs/mod_merge.cpp @@ -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 mods; for (const auto& mod : catalog.mods) if (mod.provides_layout) mods.push_back(&mod); diff --git a/Extension/Plugins/plugin_game_runtime.cpp b/Extension/Plugins/plugin_game_runtime.cpp index 96a440e1..42d637f7 100644 --- a/Extension/Plugins/plugin_game_runtime.cpp +++ b/Extension/Plugins/plugin_game_runtime.cpp @@ -19,6 +19,7 @@ #include "Extension/Skater/board_pose.h" #include "Engine/Vfs/mod_catalog.h" #include "Engine/Core/Log/logging.h" +#include #include "Extension/Assets/live_mods.h" #include "Extension/Skater/game_speed.h" #include "Extension/Skater/grind_exit.h" @@ -482,29 +483,23 @@ public: } // ---- files: /Mods/.reskate_plugin_data// (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//, 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//, 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);