diff --git a/Launcher/Test/mod_manager_tests.cpp b/Launcher/Test/mod_manager_tests.cpp index 2bbfea9..40fcf41 100644 --- a/Launcher/Test/mod_manager_tests.cpp +++ b/Launcher/Test/mod_manager_tests.cpp @@ -121,8 +121,19 @@ int main() { } check(conflict, "Installing over an installed mod asks first"); make_zip(sources / L"v2.zip", {{"Cool Park/layout.toc", "toc v2"}}); + // A file of the installed copy held open: the replacement is refused and the copy stays whole. + { + const auto held = CreateFileW((mods / L"Cool Park" / L"layout.toc").c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + bool refused = false; + try { install(mods, sources / L"v2.zip", true); } catch (const std::exception&) { refused = true; } + CloseHandle(held); + check(refused && read(mods / L"Cool Park" / L"layout.toc") == "toc" && !fs::exists(mods / L".reskate-install"), + "A copy in use is not replaced, and stays as it was"); + } check(install(mods, sources / L"v2.zip", true) == "Cool Park" && read(mods / L"Cool Park" / L"layout.toc") == "toc v2", "Replacing installs the new copy"); + check(!fs::exists(mods / L".reskate-install"), "The replaced copy is deleted, not left beside the mods"); { std::atomic cancel{}; unsigned callbacks{}; diff --git a/Launcher/gui_mods.cpp b/Launcher/gui_mods.cpp index 9db2b42..4d7b1a3 100644 --- a/Launcher/gui_mods.cpp +++ b/Launcher/gui_mods.cpp @@ -1219,7 +1219,7 @@ void mods_window(Launcher& launcher, const Fonts& fonts, ImVec2 size, Ui& ui, Mo ImGui::SetNextWindowSize(ImVec2(S(440), 0)); if (ImGui::BeginPopupModal("Replace mod", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) { ImGui::PushTextWrapPos(0); - ImGui::Text("\"%s\" is already installed. Replace it? The installed copy goes to the Recycle Bin.", + ImGui::Text("\"%s\" is already installed. Replace it? The installed copy is deleted.", panel.conflict_name.c_str()); ImGui::PopTextWrapPos(); ImGui::Spacing(); diff --git a/Launcher/gui_mods_browse.cpp b/Launcher/gui_mods_browse.cpp index c671f6c..f53a8f1 100644 --- a/Launcher/gui_mods_browse.cpp +++ b/Launcher/gui_mods_browse.cpp @@ -147,7 +147,7 @@ void set_activity(ModsPanel& panel, std::string text) { } // Downloads one package into Mods/.reskate-download, then installs it over any -// installed copy (which goes to the Recycle Bin, keeping its place in mods.json). +// installed copy (which is deleted, its place in mods.json kept). std::string install_package(ModsPanel& panel, const fs::path& root, const ts::Package& package) { const auto& version = package.latest(); const auto folder = root / L".reskate-download"; diff --git a/Launcher/mod_manager.cpp b/Launcher/mod_manager.cpp index aee3c34..bac1888 100644 --- a/Launcher/mod_manager.cpp +++ b/Launcher/mod_manager.cpp @@ -225,16 +225,27 @@ private: fs::path path_; }; -// Moves the finished staging folder into place, replacing only when asked. +// Moves the finished staging folder into place, replacing only when asked. The copy it +// replaces (an update, mostly) is deleted for good rather than recycled: it is moved aside +// into the staging folder first, so a new copy that cannot go in leaves the old one where it was. void commit(const fs::path& mods, const Staging& staging, const std::string& name, bool replace) { const auto target = mods / from_utf8(name); std::error_code error; + fs::path aside; if (fs::exists(target, error)) { if (!replace) throw AlreadyInstalled(name); - recycle(target); + aside = staging.path().parent_path() / (from_utf8(name).wstring() + L".replaced"); + fs::remove_all(aside, error); + if (!MoveFileExW(target.c_str(), aside.c_str(), MOVEFILE_WRITE_THROUGH)) + fail("Could not replace " + name + ": close anything using its files (the game, an editor) and try again."); } - if (!MoveFileExW(staging.path().c_str(), target.c_str(), MOVEFILE_WRITE_THROUGH)) - fail("Could not move the mod into the Mods folder (Windows error " + std::to_string(GetLastError()) + ")."); + if (!MoveFileExW(staging.path().c_str(), target.c_str(), MOVEFILE_WRITE_THROUGH)) { + const auto code = GetLastError(); + if (!aside.empty()) MoveFileExW(aside.c_str(), target.c_str(), MOVEFILE_WRITE_THROUGH); + fail("Could not move the mod into the Mods folder (Windows error " + std::to_string(code) + ")."); + } + // (Whatever of it cannot be deleted now goes with the staging folder.) + if (!aside.empty()) fs::remove_all(aside, error); } // miniz is built without stdio, so the archive is read through a handle. diff --git a/Launcher/mod_manager.h b/Launcher/mod_manager.h index 98d40e6..5bfabdd 100644 --- a/Launcher/mod_manager.h +++ b/Launcher/mod_manager.h @@ -45,7 +45,7 @@ struct InstallOptions { // CHANGELOG.md at the top of a package are copied in when the mod sits one // folder deeper. A top-level mod takes the archive's name without a trailing // version ("Team-Map-1.0.0.zip" installs as Team-Map). With `replace`, an -// installed mod of the same name goes to the Recycle Bin first. Throws a +// installed mod of the same name is deleted for good once the new copy is in. Throws a // user-facing message; a failed or cancelled install leaves the Mods folder as it was. std::string install(const std::filesystem::path& mods_root, const std::filesystem::path& source, bool replace, const Progress& progress, const std::atomic& cancel, const InstallOptions& options = {});