diff --git a/loader/resources/exclamation-red.png b/loader/resources/exclamation-red.png new file mode 100644 index 00000000..fa265a25 Binary files /dev/null and b/loader/resources/exclamation-red.png differ diff --git a/loader/src/hooks/MenuLayer.cpp b/loader/src/hooks/MenuLayer.cpp index ce1819e7..26606bdb 100644 --- a/loader/src/hooks/MenuLayer.cpp +++ b/loader/src/hooks/MenuLayer.cpp @@ -14,7 +14,6 @@ #include <loader/updater.hpp> #include <Geode/binding/ButtonSprite.hpp> #include <Geode/modify/LevelSelectLayer.hpp> -#include <ui/other/FixIssuesPopup.hpp> using namespace geode::prelude; @@ -83,7 +82,20 @@ struct CustomMenuLayer : Modify<CustomMenuLayer, MenuLayer> { } // show if some mods failed to load - checkLoadingIssues(this); + if (Loader::get()->getProblems().size()) { + static bool shownProblemPopup = false; + if (!shownProblemPopup) { + shownProblemPopup = true; + Notification::create("There were errors - see Geode page!", NotificationIcon::Error)->show(); + } + + auto icon = CCSprite::createWithSpriteFrameName("exclamation-red.png"_spr); + icon->setPosition(m_fields->m_geodeButton->getContentSize() - ccp(10, 10)); + icon->setID("errors-found"); + icon->setZOrder(99); + icon->setScale(.8f); + m_fields->m_geodeButton->addChild(icon); + } // show if the user tried to be naughty and load arbitrary DLLs static bool shownTriedToLoadDlls = false; @@ -154,6 +166,7 @@ struct CustomMenuLayer : Modify<CustomMenuLayer, MenuLayer> { auto updatesFound = result->unwrap(); if (updatesFound.size() && !m_fields->m_geodeButton->getChildByID("updates-available")) { log::info("Found updates for mods: {}!", updatesFound); + auto icon = CCSprite::createWithSpriteFrameName("updates-available.png"_spr); icon->setPosition( m_fields->m_geodeButton->getContentSize() - CCSize { 10.f, 10.f } diff --git a/loader/src/internal/FixModIssues.cpp b/loader/src/internal/FixModIssues.cpp new file mode 100644 index 00000000..71cf326a --- /dev/null +++ b/loader/src/internal/FixModIssues.cpp @@ -0,0 +1,187 @@ +#include "FixModIssues.hpp" +#include <Geode/loader/Loader.hpp> +#include <server/DownloadManager.hpp> +#include <ui/mods/sources/ModSource.hpp> + +// TODO: UNFINISHED!!! +// If you want to bring this back, you are free to do so - +// I just didn't feel like the engineering effort is worth it. +// The point of this is to be a mod load issue auto-resolver + +using namespace geode::prelude; + +class AutoFixStatus final { +protected: + struct Question final { + std::string title; + std::string content; + std::string optionA; + std::string optionB; + MiniFunction<void(bool)> after; + }; + + EventListener<server::ModDownloadFilter> m_download; + std::vector<std::string> m_unsolved; + std::deque<Question> m_questionQueue; + + static ghc::filesystem::path getPath(LoadProblem const& problem) { + return std::visit(makeVisitor { + [](ghc::filesystem::path const& path) { + return path; + }, + [](ModMetadata const& meta) { + return meta.getPath(); + }, + [](Mod* mod) { + return mod->getPackagePath(); + }, + }, problem.cause); + } + static std::string getName(LoadProblem const& problem) { + return std::visit(makeVisitor { + [](ghc::filesystem::path const& path) { + return path.string(); + }, + [](ModMetadata const& meta) { + return meta.getID(); + }, + [](Mod* mod) { + return mod->getID(); + }, + }, problem.cause); + } + + void nextQuestion() { + auto& question = m_questionQueue.front(); + createQuickPopup( + question.title.c_str(), + question.content, + question.optionA.c_str(), question.optionB.c_str(), + [this, &question](auto*, bool btn2) { + question.after(btn2); + m_questionQueue.pop_front(); + if (!m_questionQueue.empty()) { + this->nextQuestion(); + } + } + ); + } + void ask(Question&& question) { + m_questionQueue.push_back(question); + // If this was the first question in the queue, start asking + if (m_questionQueue.size() == 1) { + this->nextQuestion(); + } + } + +public: + void start() { + for (auto problem : Loader::get()->getProblems()) { + switch (problem.type) { + // Errors where the correct solution is to just delete the invalid .geode package + case LoadProblem::Type::InvalidFile: + // todo: maybe duplicate should prompt which one to delete? + // or maybe the user can just figure that one out since that only happens + // on manual install (as server installs delete the old version using the + // real path and not the old index filename trickery) + case LoadProblem::Type::Duplicate: + case LoadProblem::Type::SetupFailed: + case LoadProblem::Type::LoadFailed: + case LoadProblem::Type::EnableFailed: + case LoadProblem::Type::UnsupportedGeodeVersion: + case LoadProblem::Type::NeedsNewerGeodeVersion: + case LoadProblem::Type::UnsupportedVersion: + { + auto path = getPath(problem); + std::error_code ec; + ghc::filesystem::remove(path, ec); + if (ec) { + m_unsolved.push_back(fmt::format("Failed to delete '{}'", path)); + } + } + break; + + // Missing / bad dependencies + case LoadProblem::Type::MissingDependency: + case LoadProblem::Type::OutdatedDependency: + { + // Parse the damn "{id} {version}" string to get the id + // God I wish this was Rust so the enum variant could just have the ID right there + auto id = problem.message.substr(0, problem.message.find(' ')); + + // If this mod is already installed, see if it can be updated + if (auto mod = Loader::get()->getInstalledMod(id)) { + // todo: after update check, if there are no updates, mark this as unsolved, otherwise start update + ModSource(mod).checkUpdates(); + } + // Otherwise try to install the mod + // todo: Check if the mod can be downloaded, and if not mark this as unsolved + else { + server::ModDownloadManager::get()->startDownload(id, std::nullopt); + } + } + break; + + // Enable the dependency duh + case LoadProblem::Type::DisabledDependency: + { + auto mod = std::get<Mod*>(problem.cause); + if (!mod->enable()) { + m_unsolved.push_back(fmt::format("Failed to enable '{}'", mod->getID())); + } + } + break; + + // Incompatabilities; the user should choose which to disable + case LoadProblem::Type::PresentIncompatibility: + case LoadProblem::Type::OutdatedIncompatibility: + case LoadProblem::Type::OutdatedConflict: + case LoadProblem::Type::Conflict: + { + auto modA = std::get<Mod*>(problem.cause)->getID(); + auto modB = problem.message; + this->ask(Question { + .title = "Select Mod", + .content = fmt::format( + "The mods <cy>'{}'</c> and <cp>{}</c> are <cr>incompatible</c>.\n" + "<cj>Please select which one to disable</c>.", + modA, modB + ), + .optionA = modA, + .optionB = modB, + .after = [modA, modB](bool b) { + + } + }); + } + break; + + // Errors we can't fix, or ones where you should probably just restart the game / PC + default: + case LoadProblem::Type::UnzipFailed: + case LoadProblem::Type::Unknown: + { + auto name = getName(problem); + m_unsolved.push_back(fmt::format( + "<cr>Unknown/unsolvable error</c> with <cg>'{}'</c>: {}\n" + "<cy>Maybe try restarting your computer?</c>", + name, problem.message + )); + } + break; + } + } + } +}; + +static std::optional<AutoFixStatus> STATUS = {}; + +void internal::tryAutoFixModIssues() { + if (!STATUS) { + STATUS.emplace(AutoFixStatus()); + STATUS->start(); + } +} +bool internal::hasTriedToFixIssues() { + return STATUS.has_value(); +} diff --git a/loader/src/internal/FixModIssues.hpp b/loader/src/internal/FixModIssues.hpp new file mode 100644 index 00000000..539fe991 --- /dev/null +++ b/loader/src/internal/FixModIssues.hpp @@ -0,0 +1,6 @@ +#pragma once + +namespace internal { + void tryAutoFixModIssues(); + bool hasTriedToFixIssues(); +} diff --git a/loader/src/loader/Loader.cpp b/loader/src/loader/Loader.cpp index ce46c9ad..3f1d59e8 100644 --- a/loader/src/loader/Loader.cpp +++ b/loader/src/loader/Loader.cpp @@ -79,13 +79,6 @@ std::vector<LoadProblem> Loader::getProblems() const { } } return result; - return ranges::filter( - m_impl->getProblems(), - [](auto const& problem) { - return problem.type != LoadProblem::Type::Recommendation && - problem.type != LoadProblem::Type::Suggestion; - } - ); } std::vector<LoadProblem> Loader::getRecommendations() const { std::vector<LoadProblem> result; diff --git a/loader/src/loader/Mod.cpp b/loader/src/loader/Mod.cpp index 695663c7..e701a9bc 100644 --- a/loader/src/loader/Mod.cpp +++ b/loader/src/loader/Mod.cpp @@ -252,22 +252,28 @@ std::vector<LoadProblem> Mod::getAllProblems() const { return m_impl->getProblems(); } std::vector<LoadProblem> Mod::getProblems() const { - return ranges::filter( - this->getAllProblems(), - [](auto const& problem) { - return problem.type != LoadProblem::Type::Recommendation && - problem.type != LoadProblem::Type::Suggestion; + std::vector<LoadProblem> result; + for (auto problem : this->getAllProblems()) { + if ( + problem.type != LoadProblem::Type::Recommendation && + problem.type != LoadProblem::Type::Suggestion + ) { + result.push_back(problem); } - ); + } + return result; } std::vector<LoadProblem> Mod::getRecommendations() const { - return ranges::filter( - this->getAllProblems(), - [](auto const& problem) { - return problem.type == LoadProblem::Type::Recommendation || - problem.type == LoadProblem::Type::Suggestion; + std::vector<LoadProblem> result; + for (auto problem : this->getAllProblems()) { + if ( + problem.type == LoadProblem::Type::Recommendation || + problem.type == LoadProblem::Type::Suggestion + ) { + result.push_back(problem); } - ); + } + return result; } bool Mod::shouldLoad() const { return m_impl->shouldLoad(); diff --git a/loader/src/ui/other/FixIssuesPopup.cpp b/loader/src/ui/other/FixIssuesPopup.cpp deleted file mode 100644 index 35e7cb67..00000000 --- a/loader/src/ui/other/FixIssuesPopup.cpp +++ /dev/null @@ -1,28 +0,0 @@ -#include "FixIssuesPopup.hpp" -#include <Geode/loader/Loader.hpp> - -bool FixIssuesPopup::setup() { - m_noElasticity = true; - - this->setTitle("Problems Loading Mods"); - - return true; -} - -FixIssuesPopup* FixIssuesPopup::create() { - auto ret = new FixIssuesPopup(); - if (ret && ret->init(350, 280)) { - ret->autorelease(); - return ret; - } - CC_SAFE_DELETE(ret); - return nullptr; -} - -void checkLoadingIssues(CCNode* targetScene) { - if (Loader::get()->getProblems().size()) { - auto popup = FixIssuesPopup::create(); - popup->m_scene = targetScene; - popup->show(); - } -} diff --git a/loader/src/ui/other/FixIssuesPopup.hpp b/loader/src/ui/other/FixIssuesPopup.hpp deleted file mode 100644 index 8e0bae3a..00000000 --- a/loader/src/ui/other/FixIssuesPopup.hpp +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include <Geode/ui/Popup.hpp> -#include "../mods/GeodeStyle.hpp" -#include "../mods/list/ModProblemItemList.hpp" - -using namespace geode::prelude; - -class FixIssuesPopup : public GeodePopup<> { -protected: - ModProblemItemList* m_list; - - bool setup() override; - -public: - static FixIssuesPopup* create(); -}; - -void checkLoadingIssues(CCNode* targetScene = nullptr);