diff --git a/loader/include/Geode/loader/Loader.hpp b/loader/include/Geode/loader/Loader.hpp index 1ee17351..ec77356f 100644 --- a/loader/include/Geode/loader/Loader.hpp +++ b/loader/include/Geode/loader/Loader.hpp @@ -46,6 +46,21 @@ namespace geode { Type type; std::variant cause; std::string message; + + bool isSuggestion() const { + return + type == LoadProblem::Type::Recommendation || + type == LoadProblem::Type::Suggestion; + } + bool isOutdated() const { + return + type == LoadProblem::Type::UnsupportedVersion || + type == LoadProblem::Type::NeedsNewerGeodeVersion || + type == LoadProblem::Type::UnsupportedGeodeVersion; + } + bool isProblem() const { + return !isSuggestion() && !isOutdated(); + } }; class LoaderImpl; diff --git a/loader/include/Geode/loader/Mod.hpp b/loader/include/Geode/loader/Mod.hpp index c174f7e3..211615ed 100644 --- a/loader/include/Geode/loader/Mod.hpp +++ b/loader/include/Geode/loader/Mod.hpp @@ -446,7 +446,16 @@ namespace geode { bool isLoggingEnabled() const; void setLoggingEnabled(bool enabled); - bool targetsOutdatedGDVersion() const; + /** + * If this mod is built for an outdated GD or Geode version, returns the + * `LoadProblem` describing the situation. Otherwise `nullopt` if the + * mod is made for the correct version of the game and Geode + */ + std::optional targetsOutdatedVersion() const; + /** + * @note Make sure to also call `targetsOutdatedVersion` if you want to + * make sure the mod is actually loadable + */ bool hasLoadProblems() const; std::vector getAllProblems() const; std::vector getProblems() const; diff --git a/loader/src/internal/crashlog.cpp b/loader/src/internal/crashlog.cpp index 4350ca2f..8341afef 100644 --- a/loader/src/internal/crashlog.cpp +++ b/loader/src/internal/crashlog.cpp @@ -46,7 +46,7 @@ void crashlog::printMods(std::stringstream& stream) { mod->isCurrentlyLoading() ? "o"sv : mod->isEnabled() ? "x"sv : mod->hasLoadProblems() ? "!"sv : // thank you for this bug report - mod->targetsOutdatedGDVersion() ? "*"sv : // thank you very much for this bug report + mod->targetsOutdatedVersion() ? "*"sv : // thank you very much for this bug report mod->shouldLoad() ? "~"sv : " "sv, mod->getVersion().toVString(), mod->getID() diff --git a/loader/src/loader/Loader.cpp b/loader/src/loader/Loader.cpp index 38761b52..b2575850 100644 --- a/loader/src/loader/Loader.cpp +++ b/loader/src/loader/Loader.cpp @@ -71,11 +71,7 @@ std::vector Loader::getAllProblems() const { std::vector Loader::getLoadProblems() const { std::vector result; for (auto problem : this->getAllProblems()) { - if ( - problem.type != LoadProblem::Type::Recommendation && - problem.type != LoadProblem::Type::Suggestion && - problem.type != LoadProblem::Type::UnsupportedVersion - ) { + if (problem.isProblem()) { result.push_back(problem); } } @@ -84,7 +80,7 @@ std::vector Loader::getLoadProblems() const { std::vector Loader::getOutdated() const { std::vector result; for (auto problem : this->getAllProblems()) { - if (problem.type == LoadProblem::Type::UnsupportedVersion) { + if (problem.isOutdated()) { result.push_back(problem); } } @@ -93,10 +89,7 @@ std::vector Loader::getOutdated() const { std::vector Loader::getRecommendations() const { std::vector result; for (auto problem : this->getAllProblems()) { - if ( - problem.type == LoadProblem::Type::Recommendation || - problem.type == LoadProblem::Type::Suggestion - ) { + if (problem.isSuggestion()) { result.push_back(problem); } } diff --git a/loader/src/loader/LoaderImpl.cpp b/loader/src/loader/LoaderImpl.cpp index 1806eb6a..65fe6883 100644 --- a/loader/src/loader/LoaderImpl.cpp +++ b/loader/src/loader/LoaderImpl.cpp @@ -384,6 +384,37 @@ void Loader::Impl::buildModGraph() { } void Loader::Impl::loadModGraph(Mod* node, bool early) { + // Check version first, as it's not worth trying to load a mod with an + // invalid target version + // Also this makes it so that when GD updates, outdated mods get shown as + // "Outdated" in the UI instead of "Missing Dependencies" + auto res = node->getMetadata().checkGameVersion(); + if (!res) { + this->addProblem({ + LoadProblem::Type::UnsupportedVersion, + node, + res.unwrapErr() + }); + log::error("{}", res.unwrapErr()); + log::popNest(); + return; + } + + if (!this->isModVersionSupported(node->getMetadata().getGeodeVersion())) { + this->addProblem({ + 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() + ) + }); + log::error("Unsupported Geode version: {}", node->getMetadata().getGeodeVersion()); + log::popNest(); + return; + } + if (node->hasUnresolvedDependencies()) { log::debug("{} {} has unresolved dependencies", node->getID(), node->getVersion()); return; @@ -444,35 +475,6 @@ void Loader::Impl::loadModGraph(Mod* node, bool early) { log::popNest(); return; } - - auto res = node->getMetadata().checkGameVersion(); - if (!res) { - this->addProblem({ - LoadProblem::Type::UnsupportedVersion, - node, - res.unwrapErr() - }); - log::error("{}", res.unwrapErr()); - m_refreshingModCount -= 1; - log::popNest(); - return; - } - - if (!this->isModVersionSupported(node->getMetadata().getGeodeVersion())) { - this->addProblem({ - 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() - ) - }); - log::error("Unsupported Geode version: {}", node->getMetadata().getGeodeVersion()); - m_refreshingModCount -= 1; - log::popNest(); - return; - } } if (early) { @@ -524,6 +526,10 @@ void Loader::Impl::findProblems() { log::debug("{} is not enabled", id); continue; } + if (mod->targetsOutdatedVersion()) { + log::debug("{} is outdated", id); + continue; + } log::debug("{}", id); log::pushNest(); diff --git a/loader/src/loader/Mod.cpp b/loader/src/loader/Mod.cpp index e3e7bd7f..11748486 100644 --- a/loader/src/loader/Mod.cpp +++ b/loader/src/loader/Mod.cpp @@ -255,13 +255,13 @@ bool Mod::hasSavedValue(std::string_view key) { bool Mod::hasLoadProblems() const { return m_impl->hasLoadProblems(); } -bool Mod::targetsOutdatedGDVersion() const { +std::optional Mod::targetsOutdatedVersion() const { for (auto problem : this->getAllProblems()) { - if (problem.type == LoadProblem::Type::UnsupportedVersion) { - return true; + if (problem.isOutdated()) { + return problem; } } - return false; + return std::nullopt; } std::vector Mod::getAllProblems() const { return m_impl->getProblems(); @@ -269,11 +269,7 @@ std::vector Mod::getAllProblems() const { std::vector Mod::getProblems() const { std::vector result; for (auto problem : this->getAllProblems()) { - if ( - problem.type != LoadProblem::Type::Recommendation && - problem.type != LoadProblem::Type::Suggestion && - problem.type != LoadProblem::Type::UnsupportedVersion - ) { + if (problem.isProblem()) { result.push_back(problem); } } @@ -282,10 +278,7 @@ std::vector Mod::getProblems() const { std::vector Mod::getRecommendations() const { std::vector result; for (auto problem : this->getAllProblems()) { - if ( - problem.type == LoadProblem::Type::Recommendation || - problem.type == LoadProblem::Type::Suggestion - ) { + if (problem.isSuggestion()) { result.push_back(problem); } } diff --git a/loader/src/loader/ModImpl.cpp b/loader/src/loader/ModImpl.cpp index 18a1dea1..04b45487 100644 --- a/loader/src/loader/ModImpl.cpp +++ b/loader/src/loader/ModImpl.cpp @@ -706,11 +706,7 @@ bool Mod::Impl::isCurrentlyLoading() const { bool Mod::Impl::hasLoadProblems() const { for (auto const& problem : m_problems) { - if ( - problem.type != LoadProblem::Type::Recommendation && - problem.type != LoadProblem::Type::Suggestion && - problem.type != LoadProblem::Type::UnsupportedVersion - ) { + if (problem.isProblem()) { return true; } } diff --git a/loader/src/ui/mods/GeodeStyle.cpp b/loader/src/ui/mods/GeodeStyle.cpp index e5fa9039..584287be 100644 --- a/loader/src/ui/mods/GeodeStyle.cpp +++ b/loader/src/ui/mods/GeodeStyle.cpp @@ -12,6 +12,8 @@ $on_mod(Loaded) { ColorProvider::get()->define("mod-list-version-label-updates-available"_spr, ccc3(88, 202, 255)); ColorProvider::get()->define("mod-list-restart-required-label"_spr, ccc3(153, 245, 245)); ColorProvider::get()->define("mod-list-restart-required-label-bg"_spr, ccc3(123, 156, 163)); + ColorProvider::get()->define("mod-list-outdated-label"_spr, ccc3(245, 153, 245)); + ColorProvider::get()->define("mod-list-outdated-label-bg"_spr, ccc3(156, 123, 163)); ColorProvider::get()->define("mod-list-search-bg"_spr, { 83, 65, 109, 255 }); ColorProvider::get()->define("mod-list-updates-available-bg"_spr, { 139, 89, 173, 255 }); ColorProvider::get()->define("mod-list-updates-available-bg-2"_spr, { 45, 110, 222, 255 }); diff --git a/loader/src/ui/mods/list/ModItem.cpp b/loader/src/ui/mods/list/ModItem.cpp index d8e7f350..286d2012 100644 --- a/loader/src/ui/mods/list/ModItem.cpp +++ b/loader/src/ui/mods/list/ModItem.cpp @@ -95,6 +95,17 @@ bool ModItem::init(ModSource&& source) { m_restartRequiredLabel->setLayoutOptions(AxisLayoutOptions::create()->setScaleLimits(std::nullopt, .75f)); m_infoContainer->addChild(m_restartRequiredLabel); + m_outdatedLabel = createTagLabel( + fmt::format("Outdated (GD {})", m_source.getMetadata().getGameVersion().value_or("*")), + { + to3B(ColorProvider::get()->color("mod-list-outdated-label"_spr)), + to3B(ColorProvider::get()->color("mod-list-outdated-label-bg"_spr)) + } + ); + m_outdatedLabel->setID("outdated-label"); + m_outdatedLabel->setLayoutOptions(AxisLayoutOptions::create()->setScaleLimits(std::nullopt, .75f)); + m_infoContainer->addChild(m_outdatedLabel); + m_downloadBarContainer = CCNode::create(); m_downloadBarContainer->setID("download-bar-container"); m_downloadBarContainer->setContentSize({ 320, 30 }); @@ -185,7 +196,7 @@ bool ModItem::init(ModSource&& source) { m_viewMenu->addChild(m_enableToggle); m_viewMenu->updateLayout(); } - if (mod->hasLoadProblems() || mod->targetsOutdatedGDVersion()) { + if (mod->hasLoadProblems() || mod->targetsOutdatedVersion()) { auto viewErrorSpr = createGeodeCircleButton( CCSprite::createWithSpriteFrameName("exclamation.png"_spr), 1.f, CircleBaseSize::Small @@ -342,7 +353,6 @@ void ModItem::updateState() { m_downloadBarContainer->setVisible(false); m_downloadWaiting->setVisible(false); } - m_infoContainer->updateLayout(); // Set default colors based on source to start off with // (possibly overriding later based on state) @@ -410,16 +420,22 @@ void ModItem::updateState() { m_titleContainer->updateLayout(); // If there were problems, tint the BG red + m_outdatedLabel->setVisible(false); if (m_source.asMod()) { if (m_source.asMod()->hasLoadProblems()) { m_bg->setColor("mod-list-errors-found"_cc3b); m_bg->setOpacity(isGeodeTheme() ? 25 : 90); } - if (m_source.asMod()->targetsOutdatedGDVersion()) { - m_bg->setOpacity(isGeodeTheme() ? 0 : 0); + if (m_source.asMod()->targetsOutdatedVersion()) { + m_bg->setColor("mod-list-outdated-label"_cc3b); + m_bg->setOpacity(isGeodeTheme() ? 25 : 90); + m_outdatedLabel->setVisible(true); + m_developers->setVisible(false); } } + m_infoContainer->updateLayout(); + // Highlight item via BG if it wants to restart for extra UI attention if (wantsRestart) { m_bg->setColor("mod-list-restart-required-label"_cc3b); @@ -541,7 +557,38 @@ void ModItem::onView(CCObject*) { } void ModItem::onViewError(CCObject*) { if (auto mod = m_source.asMod()) { - ModErrorPopup::create(mod)->show(); + if (auto problem = mod->targetsOutdatedVersion()) { + std::string issue; + std::string howToFix; + switch (problem->type) { + default: + case LoadProblem::Type::UnsupportedVersion: { + issue = fmt::format("{}", problem->message); + howToFix = "wait for the developer to release an update to " + "the mod that supports the newer version."; + } break; + + case LoadProblem::Type::UnsupportedGeodeVersion: { + issue = "This mod is made for a newer version of Geode."; + howToFix = "update Geode by enabling Automatic Updates " + "or redownloading it from the Geode website."; + } break; + + case LoadProblem::Type::NeedsNewerGeodeVersion: { + issue = "This mod is made for an older version of Geode."; + howToFix = "wait for the developer to release an update to " + "the mod that supports the newer version."; + } break; + } + FLAlertLayer::create( + "Outdated", + fmt::format("{} Please {}", issue, howToFix), + "OK" + )->show(); + } + else { + ModErrorPopup::create(mod)->show(); + } } } void ModItem::onEnable(CCObject*) { diff --git a/loader/src/ui/mods/list/ModItem.hpp b/loader/src/ui/mods/list/ModItem.hpp index c6a3fa75..89ac4a2d 100644 --- a/loader/src/ui/mods/list/ModItem.hpp +++ b/loader/src/ui/mods/list/ModItem.hpp @@ -25,6 +25,7 @@ protected: CCNode* m_recommendedBy; CCLabelBMFont* m_developerLabel; ButtonSprite* m_restartRequiredLabel; + ButtonSprite* m_outdatedLabel; CCNode* m_downloadWaiting; CCNode* m_downloadBarContainer; Slider* m_downloadBar; diff --git a/loader/src/ui/mods/sources/InstalledModListSource.cpp b/loader/src/ui/mods/sources/InstalledModListSource.cpp index 0d5e1acd..79b03a90 100644 --- a/loader/src/ui/mods/sources/InstalledModListSource.cpp +++ b/loader/src/ui/mods/sources/InstalledModListSource.cpp @@ -13,7 +13,7 @@ bool InstalledModsQuery::preCheck(ModSource const& src) const { } // If only errors requested, only show mods with errors (duh) if (type == InstalledModListType::OnlyOutdated) { - return src.asMod()->targetsOutdatedGDVersion(); + return src.asMod()->targetsOutdatedVersion().has_value(); } if (type == InstalledModListType::OnlyErrors) { return src.asMod()->hasLoadProblems(); diff --git a/loader/src/ui/mods/sources/ModListSource.hpp b/loader/src/ui/mods/sources/ModListSource.hpp index 10e421ca..46d02236 100644 --- a/loader/src/ui/mods/sources/ModListSource.hpp +++ b/loader/src/ui/mods/sources/ModListSource.hpp @@ -230,7 +230,13 @@ void filterModsWithLocalQuery(ModListSource::ProvidedMods& mods, Query const& qu if (a.second != b.second) { return a.second > b.second; } - // Sort secondarily alphabetically + // Make sure outdated mods are always last by default + auto aIsOutdated = a.first.getMetadata().checkGameVersion().isErr(); + auto bIsOutdated = b.first.getMetadata().checkGameVersion().isErr(); + if (aIsOutdated != bIsOutdated) { + return !aIsOutdated; + } + // Fallback sort alphabetically return utils::string::caseInsensitiveCompare( a.first.getMetadata().getName(), b.first.getMetadata().getName()