From e881dc5ef248ba1c73b7717efff92cd96162be77 Mon Sep 17 00:00:00 2001 From: HJfod <60038575+HJfod@users.noreply.github.com> Date: Thu, 14 Nov 2024 15:57:11 +0200 Subject: [PATCH] manually installing mods from files button --- loader/include/Geode/loader/Mod.hpp | 3 +- loader/include/Geode/loader/ModMetadata.hpp | 12 +- loader/include/Geode/ui/GeodeUI.hpp | 4 + loader/resources/file-add.png | Bin 0 -> 22588 bytes loader/src/loader/LoaderImpl.cpp | 187 +++++++++++++++++- loader/src/loader/LoaderImpl.hpp | 6 + loader/src/loader/ModImpl.cpp | 1 + loader/src/loader/ModMetadataImpl.cpp | 23 +++ loader/src/server/DownloadManager.cpp | 3 + loader/src/ui/GeodeUI.cpp | 81 +++++--- loader/src/ui/mods/ModsLayer.cpp | 53 ++++- loader/src/ui/mods/ModsLayer.hpp | 1 + loader/src/ui/mods/list/ModDeveloperList.cpp | 5 - loader/src/ui/mods/list/ModItem.cpp | 6 +- .../mods/sources/InstalledModListSource.cpp | 4 +- loader/src/ui/mods/sources/ModListSource.cpp | 15 +- loader/src/ui/mods/sources/ModListSource.hpp | 5 +- loader/src/ui/mods/sources/ModSource.cpp | 40 +--- loader/src/ui/mods/sources/ModSource.hpp | 2 +- 19 files changed, 344 insertions(+), 107 deletions(-) create mode 100644 loader/resources/file-add.png diff --git a/loader/include/Geode/loader/Mod.hpp b/loader/include/Geode/loader/Mod.hpp index 211615ed..0c006103 100644 --- a/loader/include/Geode/loader/Mod.hpp +++ b/loader/include/Geode/loader/Mod.hpp @@ -41,7 +41,8 @@ namespace geode { Enable, Disable, Uninstall, - UninstallWithSaveData + UninstallWithSaveData, + Update }; static constexpr bool modRequestedActionIsToggle(ModRequestedAction action) { diff --git a/loader/include/Geode/loader/ModMetadata.hpp b/loader/include/Geode/loader/ModMetadata.hpp index 435c4fca..af091d30 100644 --- a/loader/include/Geode/loader/ModMetadata.hpp +++ b/loader/include/Geode/loader/ModMetadata.hpp @@ -198,9 +198,19 @@ namespace geode { /** * Checks if mod can be installed on the current GD version. - * Returns Ok() if it can, Err otherwise. + * Returns Ok() if it can, Err explaining why not otherwise. */ Result<> checkGameVersion() const; + /** + * Checks if mod can be installed on the current Geode version. + * Returns Ok() if it can, Err explaining why not otherwise. + */ + Result<> checkGeodeVersion() const; + /** + * Checks if mod can be installed on the current GD & Geode version. + * Returns Ok() if it can, Err explaining why not otherwise. + */ + Result<> checkTargetVersions() const; #if defined(GEODE_EXPOSE_SECRET_INTERNALS_IN_HEADERS_DO_NOT_DEFINE_PLEASE) void setPath(std::filesystem::path const& value); diff --git a/loader/include/Geode/ui/GeodeUI.hpp b/loader/include/Geode/ui/GeodeUI.hpp index 32dbf3bd..0ac51b9b 100644 --- a/loader/include/Geode/ui/GeodeUI.hpp +++ b/loader/include/Geode/ui/GeodeUI.hpp @@ -155,6 +155,10 @@ namespace geode { * Create a logo sprite for a mod */ GEODE_DLL cocos2d::CCNode* createModLogo(Mod* mod); + /** + * Create a logo sprite for a mod from a .geode file + */ + GEODE_DLL cocos2d::CCNode* createModLogo(std::filesystem::path const& geodePackage); /** * Create a logo sprite for a mod downloaded from the Geode servers. The * logo is initially a loading circle, with the actual sprite downloaded diff --git a/loader/resources/file-add.png b/loader/resources/file-add.png new file mode 100644 index 0000000000000000000000000000000000000000..bae8b7d406b0aa412b0222a68651a24ff0007845 GIT binary patch literal 22588 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 #include +#include +#include + using namespace geode::prelude; Loader::Impl* LoaderImpl::get() { @@ -405,17 +408,16 @@ void Loader::Impl::loadModGraph(Mod* node, bool early) { return; } - if (!this->isModVersionSupported(node->getMetadata().getGeodeVersion())) { + auto geodeVerRes = node->getMetadata().checkGeodeVersion(); + if (!geodeVerRes) { this->addProblem({ - node->getMetadata().getGeodeVersion() > this->getVersion() ? LoadProblem::Type::NeedsNewerGeodeVersion : LoadProblem::Type::UnsupportedGeodeVersion, + node->getMetadata().getGeodeVersion() > this->getVersion() ? + LoadProblem::Type::NeedsNewerGeodeVersion : + LoadProblem::Type::UnsupportedGeodeVersion, node, - fmt::format( - "Geode version {}\nis required to run this mod\n(installed: {})", - node->getMetadata().getGeodeVersion().toVString(), - this->getVersion().toVString() - ) + geodeVerRes.unwrapErr() }); - log::error("Unsupported Geode version: {}", node->getMetadata().getGeodeVersion()); + log::error("{}", geodeVerRes.unwrapErr()); log::popNest(); return; } @@ -977,4 +979,171 @@ bool Loader::Impl::isSafeMode() const { void Loader::Impl::forceSafeMode() { m_forceSafeMode = true; -} \ No newline at end of file +} + +void Loader::Impl::installModManuallyFromFile(std::filesystem::path const& path, std::function after) { + auto res = ModMetadata::createFromGeodeFile(path); + if (!res) { + FLAlertLayer::create( + "Invalid File", + fmt::format( + "The path '{}' is not a valid Geode mod: {}", + path.string(), + res.unwrapErr() + ), + "OK" + )->show(); + return; + } + auto meta = res.unwrap(); + + auto check = meta.checkTargetVersions(); + if (!check) { + FLAlertLayer::create( + "Invalid Mod Version", + fmt::format( + "The mod {} can not be installed: {}", + meta.getID(), + check.unwrapErr() + ), + "OK" + )->show(); + } + + auto doInstallModFromFile = [this, path, meta, after]() { + std::error_code ec; + + static size_t MAX_ATTEMPTS = 10; + + // Figure out a free path to install to + auto installTo = dirs::getModsDir() / fmt::format("{}.geode", meta.getID()); + size_t counter = 0; + while (std::filesystem::exists(installTo, ec) && counter < MAX_ATTEMPTS) { + installTo = dirs::getModsDir() / fmt::format("{}-{}.geode", meta.getID(), counter); + counter += 1; + } + + // This is incredibly unlikely but theoretically possible + if (counter >= MAX_ATTEMPTS) { + FLAlertLayer::create( + "Unable to Install", + fmt::format( + "Unable to install mod {}: Can't find a free filename!", + meta.getID() + ), + "OK" + )->show(); + return; + } + + // Actually copy the file over to the install directory + std::filesystem::copy_file(path, installTo, ec); + if (ec) { + FLAlertLayer::create( + "Unable to Install", + fmt::format( + "Unable to install mod {}: {} (Error code {})", + meta.getID(), ec.message(), ec.value() + ), + "OK" + )->show(); + return; + } + + // Mark an updated mod as updated or add to the mods list + if (m_mods.contains(meta.getID())) { + m_mods.at(meta.getID())->m_impl->m_requestedAction = ModRequestedAction::Update; + } + // Otherwise add a new Mod + // This should be safe as all of the scary stuff in setup() is only relevant + // for mods that are actually running + else { + auto mod = new Mod(meta); + auto res = mod->m_impl->setup(); + if (!res) { + log::error("Unable to set up manually installed mod: {}", res.unwrapErr()); + } + (void)mod->enable(); + m_mods.insert({ meta.getID(), mod }); + } + + if (after) after(); + + // No need for the user to go and manually clean up the file + createQuickPopup( + "Mod Installed", + fmt::format( + "Mod {} has been succesfully installed from file! " + "Do you want to delete the original file?", + meta.getName() + ), + "OK", "Delete File", + [path](auto, bool btn2) { + if (btn2) { + std::error_code ec; + std::filesystem::remove(path, ec); + if (ec) { + FLAlertLayer::create( + "Unable to Delete", + fmt::format( + "Unable to delete {}: {} (Error code {})", + path, ec.message(), ec.value() + ), + "OK" + )->show(); + } + // No need to show a confirmation popup if succesful since that's + // to be assumed via pressing the button on the previous popup + } + } + ); + }; + + if (auto existing = Loader::get()->getInstalledMod(meta.getID())) { + createQuickPopup( + "Already Installed", + fmt::format( + "The mod {} v{} has already been installed " + "as version {}. Do you want to replace the " + "installed version with the file?", + meta.getID(), meta.getVersion(), + existing->getVersion() + ), + "Cancel", "Replace", + [doInstallModFromFile, path, existing, meta](auto, bool btn2) mutable { + std::error_code ec; + std::filesystem::remove(existing->getPackagePath(), ec); + if (ec) { + FLAlertLayer::create( + "Unable to Uninstall", + fmt::format( + "Unable to uninstall {}: {} (Error code {})", + existing->getID(), ec.message(), ec.value() + ), + "OK" + )->show(); + return; + } + doInstallModFromFile(); + } + ); + return; + } + + doInstallModFromFile(); +} + +bool Loader::Impl::isRestartRequired() const { + for (auto mod : Loader::get()->getAllMods()) { + if (mod->getRequestedAction() != ModRequestedAction::None) { + return true; + } + if (ModSettingsManager::from(mod)->restartRequired()) { + return true; + } + } + if (server::ModDownloadManager::get()->wantsRestart()) { + return true; + } + return false; +} diff --git a/loader/src/loader/LoaderImpl.hpp b/loader/src/loader/LoaderImpl.hpp index b44b5d48..928dd7ca 100644 --- a/loader/src/loader/LoaderImpl.hpp +++ b/loader/src/loader/LoaderImpl.hpp @@ -138,6 +138,12 @@ namespace geode { bool isSafeMode() const; // enables safe mode, even if the launch arg wasnt provided void forceSafeMode(); + + // This will potentially start a whole sequence of popups that guide the + // user through installing the specific .geode file + void installModManuallyFromFile(std::filesystem::path const& path, std::function after); + + bool isRestartRequired() const; }; class LoaderImpl : public Loader::Impl { diff --git a/loader/src/loader/ModImpl.cpp b/loader/src/loader/ModImpl.cpp index 04b45487..acd09801 100644 --- a/loader/src/loader/ModImpl.cpp +++ b/loader/src/loader/ModImpl.cpp @@ -30,6 +30,7 @@ static constexpr const char* humanReadableDescForAction(ModRequestedAction actio case ModRequestedAction::Disable: return "Mod has been disabled"; case ModRequestedAction::Uninstall: return "Mod has been uninstalled"; case ModRequestedAction::UninstallWithSaveData: return "Mod has been uninstalled"; + case ModRequestedAction::Update: return "Mod has been updated"; } } diff --git a/loader/src/loader/ModMetadataImpl.cpp b/loader/src/loader/ModMetadataImpl.cpp index 8c048b8d..1b4b1cac 100644 --- a/loader/src/loader/ModMetadataImpl.cpp +++ b/loader/src/loader/ModMetadataImpl.cpp @@ -572,6 +572,29 @@ Result<> ModMetadata::checkGameVersion() const { } return Ok(); } +Result<> ModMetadata::checkGeodeVersion() const { + if (!LoaderImpl::get()->isModVersionSupported(m_impl->m_geodeVersion)) { + auto current = LoaderImpl::get()->getVersion(); + if (m_impl->m_geodeVersion > current) { + return Err( + "This mod was made for a newer version of Geode ({}). You currently have version {}.", + m_impl->m_geodeVersion, current + ); + } + else { + return Err( + "This mod was made for an older version of Geode ({}). You currently have version {}.", + m_impl->m_geodeVersion, current + ); + } + } + return Ok(); +} +Result<> ModMetadata::checkTargetVersions() const { + GEODE_UNWRAP(this->checkGameVersion()); + GEODE_UNWRAP(this->checkGeodeVersion()); + return Ok(); +} #if defined(GEODE_EXPOSE_SECRET_INTERNALS_IN_HEADERS_DO_NOT_DEFINE_PLEASE) void ModMetadata::setPath(std::filesystem::path const& value) { diff --git a/loader/src/server/DownloadManager.cpp b/loader/src/server/DownloadManager.cpp index 573881e9..545fe3ed 100644 --- a/loader/src/server/DownloadManager.cpp +++ b/loader/src/server/DownloadManager.cpp @@ -4,6 +4,7 @@ #include #include #include +#include using namespace server; @@ -124,6 +125,8 @@ public: .details = fmt::format("Unable to delete existing .geode package (code {})", ec), }; } + // Mark mod as updated + ModImpl::getImpl(mod)->m_requestedAction = ModRequestedAction::Update; } // If this was an update, delete the old file first if (!removingInstalledWasError) { diff --git a/loader/src/ui/GeodeUI.cpp b/loader/src/ui/GeodeUI.cpp index a7621074..c730699b 100644 --- a/loader/src/ui/GeodeUI.cpp +++ b/loader/src/ui/GeodeUI.cpp @@ -166,40 +166,55 @@ Popup* geode::openSettingsPopup(Mod* mod, bool disableGeodeTheme) { return nullptr; } +using ModLogoSrc = std::variant; + class ModLogoSprite : public CCNode { protected: std::string m_modID; CCNode* m_sprite = nullptr; EventListener> m_listener; - bool init(std::string const& id, bool fetch) { + bool init(ModLogoSrc&& src) { if (!CCNode::init()) return false; this->setAnchorPoint({ .5f, .5f }); this->setContentSize({ 50, 50 }); - // This is a default ID, nothing should ever rely on the ID of any ModLogoSprite being this - this->setID(std::string(Mod::get()->expandSpriteName(fmt::format("sprite-{}", id)))); - - m_modID = id; m_listener.bind(this, &ModLogoSprite::onFetch); + + std::visit(makeVisitor { + [this](Mod* mod) { + m_modID = mod->getID(); - // Load from Resources - if (!fetch) { - this->setSprite(id == "geode.loader" ? - CCSprite::createWithSpriteFrameName("geode-logo.png"_spr) : - CCSprite::create(fmt::format("{}/logo.png", id).c_str()), - false - ); - } - // Asynchronously fetch from server - else { - this->setSprite(createLoadingCircle(25), false); - m_listener.setFilter(server::getModLogo(id)); - } + // Load from Resources + this->setSprite(mod->isInternal() ? + CCSprite::createWithSpriteFrameName("geode-logo.png"_spr) : + CCSprite::create(fmt::format("{}/logo.png", mod->getID()).c_str()), + false + ); + }, + [this](std::string const& id) { + m_modID = id; + + // Asynchronously fetch from server + this->setSprite(createLoadingCircle(25), false); + m_listener.setFilter(server::getModLogo(id)); + }, + [this](std::filesystem::path const& path) { + this->setSprite(nullptr, false); + if (auto unzip = file::Unzip::create(path)) { + if (auto logo = unzip.unwrap().extract("logo.png")) { + this->setSprite(std::move(logo.unwrap()), false); + } + } + }, + }, src); - ModLogoUIEvent(std::make_unique(this, id)).post(); + // This is a default ID, nothing should ever rely on the ID of any ModLogoSprite being this + this->setID(std::string(Mod::get()->expandSpriteName(fmt::format("sprite-{}", m_modID)))); + + ModLogoUIEvent(std::make_unique(this, m_modID)).post(); return true; } @@ -224,6 +239,13 @@ protected: ModLogoUIEvent(std::make_unique(this, m_modID)).post(); } } + void setSprite(ByteVector&& data, bool postEvent) { + auto image = Ref(new CCImage()); + image->initWithImageData(data.data(), data.size()); + + auto texture = CCTextureCache::get()->addUIImage(image, m_modID.c_str()); + this->setSprite(CCSprite::createWithTexture(texture), postEvent); + } void onFetch(server::ServerRequest::Event* event) { if (auto result = event->getValue()) { @@ -233,12 +255,7 @@ protected: } // Otherwise load downloaded sprite to memory else { - auto data = result->unwrap(); - auto image = Ref(new CCImage()); - image->initWithImageData(data.data(), data.size()); - - auto texture = CCTextureCache::get()->addUIImage(image, m_modID.c_str()); - this->setSprite(CCSprite::createWithTexture(texture), true); + this->setSprite(std::move(result->unwrap()), true); } } else if (event->isCancelled()) { @@ -247,9 +264,9 @@ protected: } public: - static ModLogoSprite* create(std::string const& id, bool fetch = false) { + static ModLogoSprite* create(ModLogoSrc&& src) { auto ret = new ModLogoSprite(); - if (ret->init(id, fetch)) { + if (ret->init(std::move(src))) { ret->autorelease(); return ret; } @@ -259,13 +276,17 @@ public: }; CCNode* geode::createDefaultLogo() { - return ModLogoSprite::create(""); + return ModLogoSprite::create(ModLogoSrc(nullptr)); } CCNode* geode::createModLogo(Mod* mod) { - return ModLogoSprite::create(mod->getID()); + return ModLogoSprite::create(ModLogoSrc(mod)); +} + +CCNode* geode::createModLogo(std::filesystem::path const& geodePackage) { + return ModLogoSprite::create(ModLogoSrc(geodePackage)); } CCNode* geode::createServerModLogo(std::string const& id) { - return ModLogoSprite::create(id, true); + return ModLogoSprite::create(ModLogoSrc(id)); } diff --git a/loader/src/ui/mods/ModsLayer.cpp b/loader/src/ui/mods/ModsLayer.cpp index b97bd46e..8f9b0d64 100644 --- a/loader/src/ui/mods/ModsLayer.cpp +++ b/loader/src/ui/mods/ModsLayer.cpp @@ -145,7 +145,7 @@ void ModsStatusNode::updateState() { switch (state) { // If there are no downloads happening, just show the restart button if needed case DownloadState::None: { - m_restartBtn->setVisible(ModListSource::isRestartRequired()); + m_restartBtn->setVisible(LoaderImpl::get()->isRestartRequired()); } break; // If some downloads were cancelled, show the restart button normally @@ -154,7 +154,7 @@ void ModsStatusNode::updateState() { m_status->setColor(ccWHITE); m_status->setVisible(true); - m_restartBtn->setVisible(ModListSource::isRestartRequired()); + m_restartBtn->setVisible(LoaderImpl::get()->isRestartRequired()); } break; // If all downloads were finished, show the restart button normally @@ -170,7 +170,7 @@ void ModsStatusNode::updateState() { m_status->setVisible(true); m_statusBG->setVisible(true); - m_restartBtn->setVisible(ModListSource::isRestartRequired()); + m_restartBtn->setVisible(LoaderImpl::get()->isRestartRequired()); } break; case DownloadState::SomeErrored: { @@ -274,6 +274,39 @@ void ModsLayer::onOpenModsFolder(CCObject*) { file::openFolder(dirs::getModsDir()); } +void ModsLayer::onAddModFromFile(CCObject*) { + if (!Mod::get()->setSavedValue("shown-manual-install-info", true)) { + return FLAlertLayer::create( + nullptr, + "Manually Installing Mods", + "You can manually install mods by selecting their .geode files. " + "Do note that manually installed mods are not verified to be safe and stable!\n" + "Proceed at your own risk!", + "OK", nullptr, + 350 + )->show(); + } + file::pick(file::PickMode::OpenFile, file::FilePickOptions { + .filters = { file::FilePickOptions::Filter { + .description = "Geode Mods", + .files = { "*.geode" }, + }} + }).listen([](Result* path) { + if (*path) { + LoaderImpl::get()->installModManuallyFromFile(path->unwrap(), []() { + InstalledModListSource::get(InstalledModListType::All)->clearCache(); + }); + } + else { + FLAlertLayer::create( + "Unable to Select File", + path->unwrapErr(), + "OK" + )->show(); + } + }); +} + void ModsStatusNode::onRestart(CCObject*) { // Update button state to let user know it's restarting but it might take a bit m_restartBtn->setEnabled(false); @@ -380,6 +413,20 @@ bool ModsLayer::init() { folderBtn->setID("mods-folder-button"); actionsMenu->addChild(folderBtn); + auto addSpr = createGeodeCircleButton( + CCSprite::createWithSpriteFrameName("file-add.png"_spr), 1.f, + CircleBaseSize::Medium + ); + addSpr->setScale(.8f); + addSpr->setTopRelativeScale(.8f); + auto addBtn = CCMenuItemSpriteExtra::create( + addSpr, + this, + menu_selector(ModsLayer::onAddModFromFile) + ); + addBtn->setID("mods-add-button"); + actionsMenu->addChild(addBtn); + actionsMenu->setLayout( ColumnLayout::create() ->setAxisAlignment(AxisAlignment::Start) diff --git a/loader/src/ui/mods/ModsLayer.hpp b/loader/src/ui/mods/ModsLayer.hpp index ed585239..2b2afaa0 100644 --- a/loader/src/ui/mods/ModsLayer.hpp +++ b/loader/src/ui/mods/ModsLayer.hpp @@ -76,6 +76,7 @@ protected: void onTab(CCObject* sender); void onOpenModsFolder(CCObject*); + void onAddModFromFile(CCObject*); void onBigView(CCObject*); void onSearch(CCObject*); void onGoToPage(CCObject*); diff --git a/loader/src/ui/mods/list/ModDeveloperList.cpp b/loader/src/ui/mods/list/ModDeveloperList.cpp index 6a3a62ee..0f3ae4c9 100644 --- a/loader/src/ui/mods/list/ModDeveloperList.cpp +++ b/loader/src/ui/mods/list/ModDeveloperList.cpp @@ -64,11 +64,6 @@ bool ModDeveloperList::init(DevListPopup* popup, ModSource const& source, CCSize m_list->m_contentLayer->addChild(ModDeveloperItem::create(popup, dev.username, itemSize, dev.displayName)); } }, - [this, popup, itemSize](ModSuggestion const& suggestion) { - for (std::string& dev : suggestion.suggestion.getDevelopers()) { - m_list->m_contentLayer->addChild(ModDeveloperItem::create(popup, dev, itemSize, std::nullopt, false)); - } - }, }); m_list->m_contentLayer->updateLayout(); m_list->scrollToTop(); diff --git a/loader/src/ui/mods/list/ModItem.cpp b/loader/src/ui/mods/list/ModItem.cpp index c257c3cf..9004a21c 100644 --- a/loader/src/ui/mods/list/ModItem.cpp +++ b/loader/src/ui/mods/list/ModItem.cpp @@ -284,7 +284,7 @@ bool ModItem::init(ModSource&& source) { m_recommendedBy->addChild(nameLabel); m_recommendedBy->setLayout( - RowLayout::create() + RowLayout::create() ->setDefaultScaleLimits(.1f, 1.f) ->setAxisAlignment(AxisAlignment::Start) ); @@ -390,10 +390,6 @@ void ModItem::updateState() { m_bg->setColor("mod-list-featured-color"_cc3b); m_bg->setOpacity(65); } - }, - [this](ModSuggestion const& suggestion) { - m_bg->setColor("mod-list-recommended-bg"_cc3b); - m_bg->setOpacity(isGeodeTheme() ? 25 : 90); } }); diff --git a/loader/src/ui/mods/sources/InstalledModListSource.cpp b/loader/src/ui/mods/sources/InstalledModListSource.cpp index 79b03a90..91e6930e 100644 --- a/loader/src/ui/mods/sources/InstalledModListSource.cpp +++ b/loader/src/ui/mods/sources/InstalledModListSource.cpp @@ -13,10 +13,10 @@ bool InstalledModsQuery::preCheck(ModSource const& src) const { } // If only errors requested, only show mods with errors (duh) if (type == InstalledModListType::OnlyOutdated) { - return src.asMod()->targetsOutdatedVersion().has_value(); + return src.asMod() && src.asMod()->targetsOutdatedVersion().has_value(); } if (type == InstalledModListType::OnlyErrors) { - return src.asMod()->hasLoadProblems(); + return src.asMod() && src.asMod()->hasLoadProblems(); } return true; } diff --git a/loader/src/ui/mods/sources/ModListSource.cpp b/loader/src/ui/mods/sources/ModListSource.cpp index 1c6f57b2..44900d20 100644 --- a/loader/src/ui/mods/sources/ModListSource.cpp +++ b/loader/src/ui/mods/sources/ModListSource.cpp @@ -1,6 +1,7 @@ #include "ModListSource.hpp" #include #include +#include #define FTS_FUZZY_MATCH_IMPLEMENTATION #include @@ -88,20 +89,6 @@ void ModListSource::clearAllCaches() { src->clearCache(); } } -bool ModListSource::isRestartRequired() { - for (auto mod : Loader::get()->getAllMods()) { - if (mod->getRequestedAction() != ModRequestedAction::None) { - return true; - } - if (ModSettingsManager::from(mod)->restartRequired()) { - return true; - } - } - if (server::ModDownloadManager::get()->wantsRestart()) { - return true; - } - return false; -} bool weightedFuzzyMatch(std::string const& str, std::string const& kw, double weight, double& out) { int score; diff --git a/loader/src/ui/mods/sources/ModListSource.hpp b/loader/src/ui/mods/sources/ModListSource.hpp index 46d02236..7e241a9d 100644 --- a/loader/src/ui/mods/sources/ModListSource.hpp +++ b/loader/src/ui/mods/sources/ModListSource.hpp @@ -79,7 +79,6 @@ public: void setPageSize(size_t size); static void clearAllCaches(); - static bool isRestartRequired(); }; template @@ -231,8 +230,8 @@ void filterModsWithLocalQuery(ModListSource::ProvidedMods& mods, Query const& qu return a.second > b.second; } // Make sure outdated mods are always last by default - auto aIsOutdated = a.first.getMetadata().checkGameVersion().isErr(); - auto bIsOutdated = b.first.getMetadata().checkGameVersion().isErr(); + auto aIsOutdated = a.first.getMetadata().checkTargetVersions().isErr(); + auto bIsOutdated = b.first.getMetadata().checkTargetVersions().isErr(); if (aIsOutdated != bIsOutdated) { return !aIsOutdated; } diff --git a/loader/src/ui/mods/sources/ModSource.cpp b/loader/src/ui/mods/sources/ModSource.cpp index 0860ee73..b41edee2 100644 --- a/loader/src/ui/mods/sources/ModSource.cpp +++ b/loader/src/ui/mods/sources/ModSource.cpp @@ -51,9 +51,6 @@ std::string ModSource::getID() const { [](server::ServerModMetadata const& metadata) { return metadata.id; }, - [](ModSuggestion const& suggestion) { - return suggestion.suggestion.getID(); - }, }, m_value); } ModMetadata ModSource::getMetadata() const { @@ -65,9 +62,6 @@ ModMetadata ModSource::getMetadata() const { // Versions should be guaranteed to have at least one item return metadata.versions.front().metadata; }, - [](ModSuggestion const& suggestion) { - return suggestion.suggestion; - }, }, m_value); } @@ -80,9 +74,6 @@ std::string ModSource::formatDevelopers() const { // Versions should be guaranteed to have at least one item return metadata.formatDevelopersToString(); }, - [](ModSuggestion const& suggestion) { - return ModMetadata::formatDeveloperDisplayString(suggestion.suggestion.getDevelopers()); - }, }, m_value); } @@ -94,9 +85,6 @@ CCNode* ModSource::createModLogo() const { [](server::ServerModMetadata const& metadata) { return createServerModLogo(metadata.id); }, - [](ModSuggestion const& suggestion) { - return createServerModLogo(suggestion.suggestion.getID()); - }, }, m_value); } bool ModSource::wantsRestart() const { @@ -113,9 +101,6 @@ bool ModSource::wantsRestart() const { [](server::ServerModMetadata const& metdata) { return false; }, - [](ModSuggestion const& suggestion) { - return false; - }, }, m_value); } std::optional ModSource::hasUpdates() const { @@ -133,9 +118,6 @@ ModSource ModSource::convertForPopup() const { } return ModSource(server::ServerModMetadata(metadata)); }, - [](ModSuggestion const& suggestion) { - return ModSource(ModSuggestion(suggestion)); - }, }, m_value); } @@ -148,6 +130,7 @@ server::ServerModMetadata const* ModSource::asServer() const { } server::ServerRequest> ModSource::fetchAbout() const { + // todo: write as visit if (auto mod = this->asMod()) { return server::ServerRequest>::immediate(Ok(mod->getMetadata().getDetails())); } @@ -181,12 +164,15 @@ server::ServerRequest ModSource::fetchServerInfo() co } server::ServerRequest> ModSource::fetchValidTags() const { return std::visit(makeVisitor { - [](Mod* mod) { + [](server::ServerModMetadata const& metadata) { + // Server info tags are always certain to be valid since the server has already validated them + return server::ServerRequest>::immediate(Ok(metadata.tags)); + }, + [this](auto const&) { return server::getTags().map( - [mod](auto* result) -> Result, server::ServerError> { + [modTags = this->getMetadata().getTags()](auto* result) -> Result, server::ServerError> { if (result->isOk()) { // Filter out invalid tags - auto modTags = mod->getMetadata().getTags(); auto finalTags = std::unordered_set(); for (auto& tag : modTags) { if (result->unwrap().contains(tag)) { @@ -202,14 +188,6 @@ server::ServerRequest> ModSource::fetchValidTags } ); }, - [](server::ServerModMetadata const& metadata) { - // Server info tags are always certain to be valid since the server has already validated them - return server::ServerRequest>::immediate(Ok(metadata.tags)); - }, - [](ModSuggestion const& suggestion) { - // Suggestions are also guaranteed to be valid since they come from the server - return server::ServerRequest>::immediate(Ok(suggestion.suggestion.getTags())); - }, }, m_value); } server::ServerRequest> ModSource::checkUpdates() { @@ -230,9 +208,5 @@ server::ServerRequest> ModSource::checkUp // Server mods aren't installed so you can't install updates for them return server::ServerRequest>::immediate(Ok(std::nullopt)); }, - [](ModSuggestion const& suggestion) { - // Suggestions also aren't installed so you can't install updates for them - return server::ServerRequest>::immediate(Ok(std::nullopt)); - }, }, m_value); } diff --git a/loader/src/ui/mods/sources/ModSource.hpp b/loader/src/ui/mods/sources/ModSource.hpp index aa13f3bb..909a8d56 100644 --- a/loader/src/ui/mods/sources/ModSource.hpp +++ b/loader/src/ui/mods/sources/ModSource.hpp @@ -2,6 +2,7 @@ #include #include +#include using namespace geode::prelude; @@ -23,7 +24,6 @@ public: ModSource() = default; ModSource(Mod* mod); ModSource(server::ServerModMetadata&& metadata); - ModSource(ModSuggestion&& suggestion); std::string getID() const; ModMetadata getMetadata() const;