Mods: an update deletes the copy it replaces instead of recycling it

Every update, and every install over an installed mod, sent the old
folder to the Recycle Bin, which filled up with old versions.

- commit() moves the installed copy aside into the staging folder, moves
  the new one in, then deletes the old for good. A new copy that cannot
  go in puts the old one back; a copy with a file in use is not replaced
  and stays whole.
- Uninstalling still goes to the Recycle Bin. The Replace dialog says the
  installed copy is deleted.
This commit is contained in:
AyeZeeBB committed 2026-10-16 20:01:41 -05:00
1 parent a14a6f44b5
commit 181e02454b
5 files changed
+29 -7

No files matched your search

+11
View File
@@ -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<bool> cancel{};
unsigned callbacks{};
+1 -1
View File
@@ -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();
+1 -1
View File
@@ -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";
+15 -4
View File
@@ -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.
+1 -1
View File
@@ -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<bool>& cancel, const InstallOptions& options = {});