diff --git a/loader/src/server/DownloadManager.cpp b/loader/src/server/DownloadManager.cpp index 75e9b535..a2c9cf6d 100644 --- a/loader/src/server/DownloadManager.cpp +++ b/loader/src/server/DownloadManager.cpp @@ -1,4 +1,6 @@ #include "DownloadManager.hpp" +#include "Geode/loader/Mod.hpp" +#include <optional> using namespace server; @@ -18,6 +20,7 @@ public: std::string m_id; std::optional<VersionInfo> m_version; std::optional<DependencyFor> m_dependencyFor; + std::optional<std::string> m_replacesMod; DownloadStatus m_status; EventListener<ServerRequest<ServerModVersion>> m_infoListener; EventListener<web::WebTask> m_downloadListener; @@ -25,11 +28,13 @@ public: Impl( std::string const& id, std::optional<VersionInfo> const& version, - std::optional<DependencyFor> const& dependencyFor + std::optional<DependencyFor> const& dependencyFor, + std::optional<std::string> const& replacesMod ) : m_id(id), m_version(version), m_dependencyFor(dependencyFor), + m_replacesMod(replacesMod), m_status(DownloadStatusFetching { .percentage = 0, }) @@ -97,8 +102,9 @@ public: if (auto value = event->getValue()) { if (value->ok()) { bool removingInstalledWasError = false; - // If this was an update, delete the old file first - if (auto mod = Loader::get()->getInstalledMod(m_id)) { + bool disablingReplacedWasError = false; + std::string id = m_replacesMod.has_value() ? m_replacesMod.value() : m_id; + if (auto mod = Loader::get()->getInstalledMod(id)) { std::error_code ec; std::filesystem::remove(mod->getPackagePath(), ec); if (ec) { @@ -108,6 +114,7 @@ public: }; } } + // If this was an update, delete the old file first if (!removingInstalledWasError) { auto ok = file::writeBinary(dirs::getModsDir() / (m_id + ".geode"), value->data()); if (!ok) { @@ -146,8 +153,9 @@ public: ModDownload::ModDownload( std::string const& id, std::optional<VersionInfo> const& version, - std::optional<DependencyFor> const& dependencyFor -) : m_impl(std::make_shared<Impl>(id, version, dependencyFor)) {} + std::optional<DependencyFor> const& dependencyFor, + std::optional<std::string> const& replacesMod +) : m_impl(std::make_shared<Impl>(id, version, dependencyFor, replacesMod)) {} void ModDownload::confirm() { m_impl->confirm(); @@ -156,6 +164,9 @@ void ModDownload::confirm() { std::optional<DependencyFor> ModDownload::getDependencyFor() const { return m_impl->m_dependencyFor; } +std::optional<std::string> ModDownload::getReplacesMod() const { + return m_impl->m_replacesMod; +} bool ModDownload::isDone() const { return std::holds_alternative<DownloadStatusDone>(m_impl->m_status); } @@ -225,7 +236,8 @@ void ModDownload::cancel() { std::optional<ModDownload> ModDownloadManager::startDownload( std::string const& id, std::optional<VersionInfo> const& version, - std::optional<DependencyFor> const& dependencyFor + std::optional<DependencyFor> const& dependencyFor, + std::optional<std::string> const& replacesMod ) { // If this mod has already been succesfully downloaded or is currently // being downloaded, return as you can't download multiple versions of the @@ -242,7 +254,7 @@ std::optional<ModDownload> ModDownloadManager::startDownload( // Start a new download by constructing a ModDownload (which starts the // download) - m_impl->m_downloads.emplace(id, ModDownload(id, version, dependencyFor)); + m_impl->m_downloads.emplace(id, ModDownload(id, version, dependencyFor, replacesMod)); return m_impl->m_downloads.at(id); } void ModDownloadManager::cancelAll() { @@ -261,7 +273,16 @@ void ModDownloadManager::startUpdateAll() { if (result->isOk()) { for (auto& mod : result->unwrap()) { if (mod.hasUpdateForInstalledMod()) { - this->startDownload(mod.id, std::nullopt); + if (mod.replacement.has_value()) { + this->startDownload( + mod.replacement.value().id, + mod.replacement.value().version, + std::nullopt, + mod.id + ); + } else { + this->startDownload(mod.id, std::nullopt); + } } } } diff --git a/loader/src/server/DownloadManager.hpp b/loader/src/server/DownloadManager.hpp index 4b443267..569564b9 100644 --- a/loader/src/server/DownloadManager.hpp +++ b/loader/src/server/DownloadManager.hpp @@ -65,7 +65,8 @@ namespace server { ModDownload( std::string const& id, std::optional<VersionInfo> const& version, - std::optional<DependencyFor> const& dependencyFor + std::optional<DependencyFor> const& dependencyFor, + std::optional<std::string> const& replacesMod ); friend class ModDownloadManager; @@ -77,6 +78,7 @@ namespace server { bool isDone() const; bool isActive() const; bool canRetry() const; + std::optional<std::string> getReplacesMod() const; std::optional<DependencyFor> getDependencyFor() const; std::string getID() const; DownloadStatus getStatus() const; @@ -100,7 +102,8 @@ namespace server { std::optional<ModDownload> startDownload( std::string const& id, std::optional<VersionInfo> const& version, - std::optional<DependencyFor> const& dependencyFor = std::nullopt + std::optional<DependencyFor> const& dependencyFor = std::nullopt, + std::optional<std::string> const& replacesMod = std::nullopt ); void startUpdateAll(); void confirmAll(); diff --git a/loader/src/server/Server.cpp b/loader/src/server/Server.cpp index 949b298c..9d2c72e2 100644 --- a/loader/src/server/Server.cpp +++ b/loader/src/server/Server.cpp @@ -348,6 +348,23 @@ Result<ServerModVersion> ServerModVersion::parse(matjson::Value const& raw) { return Ok(res); } +Result<ServerModReplacement> ServerModReplacement::parse(matjson::Value const& raw) { + log::info("entered replacement parse"); + auto json = raw; + log::info("{}", json); + JsonChecker checker(json); + auto root = checker.root("ServerModReplacement").obj(); + auto res = ServerModReplacement(); + + root.needs("id").into(res.id); + root.needs("version").into(res.version); + + if (root.isError()) { + return Err(root.getError()); + } + return Ok(res); +} + Result<ServerModUpdate> ServerModUpdate::parse(matjson::Value const& raw) { auto json = raw; JsonChecker checker(json); @@ -357,6 +374,9 @@ Result<ServerModUpdate> ServerModUpdate::parse(matjson::Value const& raw) { root.needs("id").into(res.id); root.needs("version").into(res.version); + if (root.has("replacement")) { + GEODE_UNWRAP_INTO(res.replacement, ServerModReplacement::parse(root.has("replacement").json())); + } // Check for errors and return result if (root.isError()) { @@ -390,7 +410,7 @@ Result<std::vector<ServerModUpdate>> ServerModUpdate::parseList(matjson::Value c bool ServerModUpdate::hasUpdateForInstalledMod() const { if (auto mod = Loader::get()->getInstalledMod(this->id)) { - return mod->getVersion() < this->version; + return mod->getVersion() < this->version || this->replacement.has_value(); } return false; } @@ -739,7 +759,10 @@ ServerRequest<std::optional<ServerModUpdate>> server::checkUpdates(Mod* mod) { [mod](Result<std::vector<ServerModUpdate>, ServerError>* result) -> Result<std::optional<ServerModUpdate>, ServerError> { if (result->isOk()) { for (auto& update : result->unwrap()) { - if (update.id == mod->getID() && update.version > mod->getVersion()) { + if ( + update.id == mod->getID() && + (update.version > mod->getVersion() || update.replacement.has_value()) + ) { return Ok(update); } } diff --git a/loader/src/server/Server.hpp b/loader/src/server/Server.hpp index 45b5c3dd..4c0b111f 100644 --- a/loader/src/server/Server.hpp +++ b/loader/src/server/Server.hpp @@ -1,8 +1,11 @@ #pragma once +#include "Geode/utils/VersionInfo.hpp" #include <Geode/DefaultInclude.hpp> #include <Geode/utils/web2.hpp> #include <Geode/loader/SettingEvent.hpp> +#include <matjson.hpp> +#include <vector> using namespace geode::prelude; @@ -34,10 +37,19 @@ namespace server { static Result<ServerModVersion> parse(matjson::Value const& json); }; + + struct ServerModReplacement final { + std::string id; + VersionInfo version; + std::string download_link; + + static Result<ServerModReplacement> parse(matjson::Value const& json); + }; struct ServerModUpdate final { std::string id; VersionInfo version; + std::optional<ServerModReplacement> replacement; static Result<ServerModUpdate> parse(matjson::Value const& json); static Result<std::vector<ServerModUpdate>> parseList(matjson::Value const& json); diff --git a/loader/src/ui/mods/list/ModItem.cpp b/loader/src/ui/mods/list/ModItem.cpp index bc177b29..790abf40 100644 --- a/loader/src/ui/mods/list/ModItem.cpp +++ b/loader/src/ui/mods/list/ModItem.cpp @@ -325,7 +325,12 @@ void ModItem::updateState() { update && !(download && (download->isActive() || download->isDone())) ) { m_updateBtn->setVisible(true); - auto updateString = m_source.getMetadata().getVersion().toString() + " -> " + update->version.toString(); + std::string updateString = ""; + if (update->replacement.has_value()) { + updateString += " -> " + update->replacement.value().id; + } else { + updateString += m_source.getMetadata().getVersion().toVString() + " -> " + update->version.toVString(); + } m_versionLabel->setString(updateString.c_str()); m_versionLabel->setColor(to3B(ColorProvider::get()->color("mod-list-version-label-updates-available"_spr))); @@ -439,6 +444,17 @@ void ModItem::onEnable(CCObject*) { UpdateModListStateEvent(UpdateModState(m_source.getID())).post(); } void ModItem::onInstall(CCObject*) { + if (auto updates = m_source.hasUpdates()) { + if (updates->replacement.has_value()) { + server::ModDownloadManager::get()->startDownload( + updates->replacement->id, + updates->replacement->version, + std::nullopt, + m_source.getID() + ); + return; + } + } server::ModDownloadManager::get()->startDownload(m_source.getID(), std::nullopt); } void ModItem::onDevelopers(CCObject*) { diff --git a/loader/src/ui/mods/popups/ConfirmInstall.cpp b/loader/src/ui/mods/popups/ConfirmInstall.cpp index 0bc105aa..fe52c2e7 100644 --- a/loader/src/ui/mods/popups/ConfirmInstall.cpp +++ b/loader/src/ui/mods/popups/ConfirmInstall.cpp @@ -7,6 +7,7 @@ using namespace server; void askConfirmModInstalls() { struct ToConfirm final { size_t modCount = 0; + size_t replacementCount = 0; size_t dependencyCount = 0; std::unordered_set<Mod*> toDisable; std::unordered_set<Mod*> toEnable; @@ -23,6 +24,9 @@ void askConfirmModInstalls() { } else { toConfirm.modCount += 1; + if (download.getReplacesMod()) { + toConfirm.replacementCount += 1; + } // Since the user has already explicitly chosen to download these mods, we // are going to assume they want these mods enabled over already installed @@ -68,10 +72,10 @@ void askConfirmModInstalls() { createQuickPopup( "Confirm Install", fmt::format( - "<cj>{}</c> mods will be installed, of which <cy>{}</c> are <cy>dependencies</c>.\n" + "<cj>{}</c> mods will be installed, of which <cy>{}</c> are <cy>dependencies</c> and <cy>{}</c> are <cy>replacements</c>.\n" "<cr>{} mods will be force-disabled, as they are incompatible</c>: {}\n" "<cg>{} mods will be force-enabled</c>: {}", - toConfirm.modCount, toConfirm.dependencyCount, + toConfirm.modCount, toConfirm.dependencyCount, toConfirm.replacementCount, toConfirm.toDisable.size(), joinModsToIDs(toConfirm.toDisable), toConfirm.toEnable.size(), joinModsToIDs(toConfirm.toEnable) ),