From beeb7ca1f8fd848e7aacd01a424455c4bad24ea6 Mon Sep 17 00:00:00 2001 From: HJfod <60038575+HJfod@users.noreply.github.com> Date: Thu, 25 Apr 2024 17:50:00 +0300 Subject: [PATCH] recommended mods list --- loader/CMakeLists.txt | 1 + loader/include/Geode/loader/Loader.hpp | 2 + loader/include/Geode/loader/Mod.hpp | 3 + loader/include/Geode/utils/Task.hpp | 163 ++++++++- loader/include/Geode/utils/VersionInfo.hpp | 5 +- loader/resources/exclamation.png | Bin 0 -> 2159 bytes loader/resources/images/GE_square03.png | Bin 0 -> 10272 bytes loader/src/hooks/MenuLayer.cpp | 14 +- loader/src/loader/Loader.cpp | 33 +- loader/src/loader/LoaderImpl.cpp | 37 +- loader/src/loader/Mod.cpp | 25 +- loader/src/loader/ModMetadataImpl.cpp | 2 +- loader/src/server/Server.cpp | 3 +- loader/src/ui/mods/GeodeStyle.cpp | 5 + loader/src/ui/mods/GeodeStyle.hpp | 19 +- loader/src/ui/mods/ModsLayer.cpp | 14 +- loader/src/ui/mods/list/ModItem.cpp | 70 +++- loader/src/ui/mods/list/ModItem.hpp | 2 + loader/src/ui/mods/list/ModList.cpp | 85 ++++- loader/src/ui/mods/list/ModList.hpp | 3 + loader/src/ui/mods/list/ModProblemItem.cpp | 97 +++++ loader/src/ui/mods/list/ModProblemItem.hpp | 25 ++ .../src/ui/mods/list/ModProblemItemList.cpp | 33 ++ .../src/ui/mods/list/ModProblemItemList.hpp | 16 + loader/src/ui/mods/popups/ModPopup.cpp | 37 +- .../mods/sources/InstalledModListSource.cpp | 112 ++++++ loader/src/ui/mods/sources/ModListSource.cpp | 339 ++---------------- loader/src/ui/mods/sources/ModListSource.hpp | 114 +++++- .../src/ui/mods/sources/ModPackListSource.cpp | 20 ++ loader/src/ui/mods/sources/ModSource.cpp | 121 +++++-- loader/src/ui/mods/sources/ModSource.hpp | 15 +- .../ui/mods/sources/ServerModListSource.cpp | 99 +++++ .../mods/sources/SuggestedModListSource.cpp | 57 +++ loader/src/ui/other/FixIssuesPopup.cpp | 28 ++ loader/src/ui/other/FixIssuesPopup.hpp | 19 + loader/src/utils/VersionInfo.cpp | 10 +- 36 files changed, 1203 insertions(+), 425 deletions(-) create mode 100644 loader/resources/exclamation.png create mode 100644 loader/resources/images/GE_square03.png create mode 100644 loader/src/ui/mods/list/ModProblemItem.cpp create mode 100644 loader/src/ui/mods/list/ModProblemItem.hpp create mode 100644 loader/src/ui/mods/list/ModProblemItemList.cpp create mode 100644 loader/src/ui/mods/list/ModProblemItemList.hpp create mode 100644 loader/src/ui/mods/sources/InstalledModListSource.cpp create mode 100644 loader/src/ui/mods/sources/ModPackListSource.cpp create mode 100644 loader/src/ui/mods/sources/ServerModListSource.cpp create mode 100644 loader/src/ui/mods/sources/SuggestedModListSource.cpp create mode 100644 loader/src/ui/other/FixIssuesPopup.cpp create mode 100644 loader/src/ui/other/FixIssuesPopup.hpp diff --git a/loader/CMakeLists.txt b/loader/CMakeLists.txt index 94422451..49421a7f 100644 --- a/loader/CMakeLists.txt +++ b/loader/CMakeLists.txt @@ -73,6 +73,7 @@ file(GLOB SOURCES CONFIGURE_DEPENDS src/utils/*.cpp src/ui/*.cpp src/ui/nodes/*.cpp + src/ui/other/*.cpp src/ui/mods/*.cpp src/ui/mods/list/*.cpp src/ui/mods/popups/*.cpp diff --git a/loader/include/Geode/loader/Loader.hpp b/loader/include/Geode/loader/Loader.hpp index c310d8b3..ca4b23c6 100644 --- a/loader/include/Geode/loader/Loader.hpp +++ b/loader/include/Geode/loader/Loader.hpp @@ -91,7 +91,9 @@ namespace geode { bool isModLoaded(std::string const& id) const; Mod* getLoadedMod(std::string const& id) const; std::vector getAllMods(); + std::vector getAllProblems() const; std::vector getProblems() const; + std::vector getRecommendations() const; /** * Returns the available launch argument names. diff --git a/loader/include/Geode/loader/Mod.hpp b/loader/include/Geode/loader/Mod.hpp index 2d5c7daf..690a3aa8 100644 --- a/loader/include/Geode/loader/Mod.hpp +++ b/loader/include/Geode/loader/Mod.hpp @@ -435,6 +435,9 @@ namespace geode { void setLoggingEnabled(bool enabled); bool hasProblems() const; + std::vector getAllProblems() const; + std::vector getProblems() const; + std::vector getRecommendations() const; bool shouldLoad() const; bool isCurrentlyLoading() const; diff --git a/loader/include/Geode/utils/Task.hpp b/loader/include/Geode/utils/Task.hpp index f63a0f31..11cbbc20 100644 --- a/loader/include/Geode/utils/Task.hpp +++ b/loader/include/Geode/utils/Task.hpp @@ -49,12 +49,44 @@ namespace geode { class Handle final { private: + // Handles may contain extra data, for example for holding ownership + // of other Tasks for `Task::map` and `Task::all`. This struct + // provides type erasure for that extra data + struct ExtraData final { + // Pointer to the owned extra data + void* ptr; + // Pointer to a function that deletes that extra data + // The function MUST have a static lifetime + void(*onDestroy)(void*); + // Pointer to a function that handles cancelling any tasks within + // that extra data when this task is cancelled. Note that the + // task may not free up the memory associated with itself here + // and this function may not be called if the user uses + // `Task::shallowCancel`. However, this pointer *must* always be + // valid + // The function MUST have a static lifetime + void(*onCancelled)(void*); + + ExtraData(void* ptr, void(*onDestroy)(void*), void(*onCancelled)(void*)) + : ptr(ptr), onDestroy(onDestroy), onCancelled(onCancelled) + {} + ExtraData(ExtraData const&) = delete; + ExtraData(ExtraData&&) = delete; + + ~ExtraData() { + onDestroy(ptr); + } + void cancel() { + onCancelled(ptr); + } + }; + std::recursive_mutex m_mutex; Status m_status = Status::Pending; std::optional m_resultValue; bool m_finalEventPosted = false; - std::unique_ptr m_mapListener = { nullptr, +[](void*) {} }; std::string m_name; + std::unique_ptr m_extraData = nullptr; class PrivateMarker final {}; @@ -140,6 +172,8 @@ namespace geode { handle->m_status = Status::Finished; handle->m_resultValue.emplace(std::move(value)); Loader::get()->queueInMainThread([handle, value = &*handle->m_resultValue]() mutable { + // SAFETY: Task::all() depends on the lifetime of the value pointer + // being as long as the lifetime of the task itself Event::createFinished(handle, value).post(); std::unique_lock lock(handle->m_mutex); handle->m_finalEventPosted = true; @@ -155,11 +189,16 @@ namespace geode { }); } } - static void cancel(std::shared_ptr handle) { + static void cancel(std::shared_ptr handle, bool shallow = false) { if (!handle) return; std::unique_lock lock(handle->m_mutex); if (handle->m_status == Status::Pending) { handle->m_status = Status::Cancelled; + // If this task carries extra data, call the extra data's handling method + // (unless shallow cancelling was specifically requested) + if (!shallow && handle->m_extraData) { + handle->m_extraData->cancel(); + } Loader::get()->queueInMainThread([handle]() mutable { Event::createCancelled(handle).post(); std::unique_lock lock(handle->m_mutex); @@ -213,6 +252,17 @@ namespace geode { void cancel() { Task::cancel(m_handle); } + /** + * If this is a Task that owns other Task(s) (for example created + * through `Task::map` or `Task::all`), then this method cancels *only* + * this Task and *not* any of the Task(s) it is built on top of. + * Ownership of the other Task(s) will be released, so if this is the + * only Task listening to them, they will still be destroyed due to a + * lack of listeners + */ + void shallowCancel() { + Task::cancel(m_handle, true); + } bool isPending() const { return m_handle && m_handle->is(Status::Pending); } @@ -275,18 +325,101 @@ namespace geode { // The task has been cancelled if the user has explicitly cancelled it, // or if there is no one listening anymore auto lock = handle.lock(); - return !(lock && lock->is(Status::Pending)); + return !lock || lock->is(Status::Cancelled); } ); }).detach(); return task; } + /** + * @warning The result vector may contain nulls if any of the tasks + * were cancelled! + */ + template + static Task, std::monostate> all(std::vector>&& tasks, std::string const& name = "") { + using AllTask = Task, std::monostate>; - template - auto map(ResultMapper&& resultMapper, ProgressMapper&& progressMapper, std::string const& name = "") { + // Create a new supervising task for all of the provided tasks + auto task = AllTask(AllTask::Handle::create(name)); + + // Storage for storing the results received so far & keeping + // ownership of the running tasks + struct Waiting final { + std::vector taskResults; + std::vector> taskListeners; + size_t taskCount; + }; + task.m_handle->m_extraData = std::make_unique( + // Create the data + static_cast(new Waiting()), + // When the task is destroyed + +[](void* ptr) { + delete static_cast(ptr); + }, + // If the task is cancelled + +[](void* ptr) { + // The move clears the `taskListeners` vector (important!) + for (auto task : std::move(static_cast(ptr)->taskListeners)) { + task.cancel(); + } + } + ); + + // Store the task count in case some tasks finish immediately during the loop + static_cast(task.m_handle->m_extraData->ptr)->taskCount = tasks.size(); + + // Make sure to only give a weak pointer to avoid circular references! + // (Tasks should NEVER own themselves!!) + auto markAsDone = [handle = std::weak_ptr(task.m_handle)](T* result) { + auto lock = handle.lock(); + + // If this task handle has expired, consider the task cancelled + // (We don't have to do anything because the lack of a handle + // means all the memory has been freed or is managed by + // something else) + if (!lock) return; + + // Get the waiting handle from the task handle + auto waiting = static_cast(lock->m_extraData->ptr); + + // SAFETY: The lifetime of result pointer is the same as the task that + // produced that pointer, so as long as we have an owning reference to + // the tasks through `taskListeners` we can be sure `result` is valid + waiting->taskResults.push_back(result); + + // If all tasks are done, finish + log::debug("waiting for {}/{} tasks", waiting->taskResults.size(), waiting->taskCount); + if (waiting->taskResults.size() >= waiting->taskCount) { + // SAFETY: The task results' lifetimes are tied to the tasks + // which could have their only owner be `waiting->taskListeners`, + // but since Waiting is owned by the returned AllTask it should + // be safe to access as long as it's accessible + AllTask::finish(lock, std::move(waiting->taskResults)); + } + }; + + // Iterate the tasks & start listening to them using + for (auto& taskToWait : tasks) { + static_cast(task.m_handle->m_extraData->ptr)->taskListeners.emplace_back(taskToWait.map( + [markAsDone](auto* result) { + markAsDone(result); + return std::monostate(); + }, + [](auto*) { return std::monostate(); }, + [markAsDone]() { markAsDone(nullptr); } + )); + } + return task; + } + + template + auto map(ResultMapper&& resultMapper, ProgressMapper&& progressMapper, OnCancelled&& onCancelled, std::string const& name = "") const { using T2 = decltype(resultMapper(std::declval())); using P2 = decltype(progressMapper(std::declval())); + static_assert(std::is_move_constructible_v, "The type being mapped to must be move-constructible!"); + static_assert(std::is_move_constructible_v, "The type being mapped to must be move-constructible!"); + auto task = Task(Task::Handle::create(fmt::format("{} <= {}", name, m_handle->m_name))); // Lock the current task until we have managed to create our new one @@ -294,6 +427,7 @@ namespace geode { // If the current task is cancelled, cancel the new one immediately if (m_handle->m_status == Status::Cancelled) { + onCancelled(); Task::cancel(task.m_handle); } // If the current task is finished, immediately map the value and post that @@ -302,13 +436,14 @@ namespace geode { } // Otherwise start listening and waiting for the current task to finish else { - task.m_handle->m_mapListener = std::unique_ptr( + task.m_handle->m_extraData = std::make_unique::Handle::ExtraData>( static_cast(new EventListener( [ handle = std::weak_ptr(task.m_handle), resultMapper = std::move(resultMapper), - progressMapper = std::move(progressMapper) - ](Event* event) { + progressMapper = std::move(progressMapper), + onCancelled = std::move(onCancelled) + ](Event* event) mutable { if (auto v = event->getValue()) { Task::finish(handle.lock(), std::move(resultMapper(v))); } @@ -316,6 +451,7 @@ namespace geode { Task::progress(handle.lock(), std::move(progressMapper(p))); } else if (event->isCancelled()) { + onCancelled(); Task::cancel(handle.lock()); } }, @@ -323,15 +459,24 @@ namespace geode { )), +[](void* ptr) { delete static_cast*>(ptr); + }, + +[](void* ptr) { + // Cancel the mapped task too + static_cast*>(ptr)->getFilter().cancel(); } ); } return task; } + template + auto map(ResultMapper&& resultMapper, ProgressMapper&& progressMapper, std::string const& name = "") const { + return this->map(std::move(resultMapper), std::move(progressMapper), +[]() {}, name); + } + template requires std::copy_constructible

- auto map(ResultMapper&& resultMapper, std::string const& name = "") { + auto map(ResultMapper&& resultMapper, std::string const& name = "") const { return this->map(std::move(resultMapper), +[](P* p) -> P { return *p; }, name); } diff --git a/loader/include/Geode/utils/VersionInfo.hpp b/loader/include/Geode/utils/VersionInfo.hpp index 04ba72ec..b2acdeb9 100644 --- a/loader/include/Geode/utils/VersionInfo.hpp +++ b/loader/include/Geode/utils/VersionInfo.hpp @@ -186,8 +186,11 @@ namespace geode { std::tie(other.m_major, other.m_minor, other.m_patch, other.m_tag); } + [[deprecated("Use toNonVString or toVString instead")]] std::string toString(bool includeTag = true) const; - + std::string toVString(bool includeTag = true) const; + std::string toNonVString(bool includeTag = true) const; + friend GEODE_DLL std::string format_as(VersionInfo const& version); }; diff --git a/loader/resources/exclamation.png b/loader/resources/exclamation.png new file mode 100644 index 0000000000000000000000000000000000000000..d62613490da39408dcdefd465643284b6fb673d3 GIT binary patch literal 2159 zcmV-#2$1)QP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D2n0z)K~#8N?VMjo zR9PIy&nWniMX^L>K}wBMy=e5X5jMr{1mgr*v`V-S{)6lxHE5xK^k<3np?&DjyjX!& z?u#yEEoDTy)jkv~fm5O2pUR}timXU0dU1c>(>ddvnRD;V+_~e-!u`PUoco)5&-vc- z|J-}74xi7*91aHq@THT-(wE`SpFgv{zCQN+`E&R+WXj+_5OmI05oB}c&b_{D*|Oo~ z%a^lx^X74zHXi8%m7UuniTm{F6B`&9V1B>f+27xP>h0UNMf}GYfY2>lws_aATNf0! zBs5Lw8;ZMj?HcRq>guQ5%H@VQfyh;>R(XqxiUPR_v}tO6r8pE>V`F2-*w|RixfCrP zkH(J zbn?!fJHir7Am<)Dc)-q{JsYzyRPs=UhKAU)XV1h@bR0T#$Pt=@T-=v0U+`m86(3^O zuU{W zeL5K{@7}%R*V0WX)E_{ zvj_!)zHee;f`^fFr+6p?++89uieL7TK_i5gM2j=3%jhG4uDIzSKR;iDf(aBW|Es{L z_qPhY+J>zr9^ab~f_nZ*`omZXY{GzukE zT%=LRE><)2_V(I2XBu0aS&}Nw@Ar#PIO*x>$rbnh{d*A#Co?m1Q_70V&CUHgWs7yz ztXbnp6^D2IdgSHhC9lq5LyX=wp#=*T^vKOG%KaVf?d@VWi1lEfiqZa(^XJdAd-v{9 zQ1K&Nety1)%6jVS>y>H3dA;6!5XQw(lf?^l<%z|`#UeUuxOMB6;kF?v%9j@}UgW~K zIJeuqNp+%h$UziwtoxX7KE&W!LkPVdSl%GedRtmrex||sw`j*=i~xm&g}+u-R-UGoE36iZqizJBAe?;o@S$=) zakNlQHr3VDRny22zc`E-B++0#n7 zR?3YQBgR#?c8UZLmdDE0nrLBQaB%QPnn!ki{PvYa?6`5`2Ggz~0^(@0iAUSY3*iuXcXzi)GInrn zB!Q)+rIlK7yjU?Ux^m@8H^M5g6B-MccwP|WTEv04^k{2qYoePFM+9RcT3g>Cz=GE}aev`uh5IkRQh3!-v_n zZQI0=b%Hj+KajVjVqn6vri&bmXI;)?Ps6ZO3wBc^v zys1Cg)cXv@N{rILsG%z^o^B&rjM5kx8Ci_sAK4fN<+=zZ7VV5gOLcX1v2M<#0=77_ z=!!FXM_?5Z-b0M0tT^h3|J4IZQs7#Z}g}iZ}RXd)VXu# zge7`#4X#BTdf;gk4ff6AkQ4N*7Z9Xxo@ zk(rqp+kdBs0o1XEh6ab%>s1N^N^xM#nl96I&|x>C!m002ovPDHLkV1k^EKl}gy literal 0 HcmV?d00001 diff --git a/loader/resources/images/GE_square03.png b/loader/resources/images/GE_square03.png new file mode 100644 index 0000000000000000000000000000000000000000..bcf8ebcbc7e7a5b3f9c8e5d435652e7f65621646 GIT binary patch literal 10272 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 #include #include +#include using namespace geode::prelude; @@ -82,17 +83,8 @@ struct CustomMenuLayer : Modify { } // show if some mods failed to load - static bool shownFailedNotif = false; - if (!shownFailedNotif) { - shownFailedNotif = true; - auto problems = Loader::get()->getProblems(); - if (std::any_of(problems.begin(), problems.end(), [&](auto& item) { - return item.type != LoadProblem::Type::Suggestion && item.type != LoadProblem::Type::Recommendation; - })) { - Notification::create("There were problems loading some mods", NotificationIcon::Error)->show(); - } - } - + checkLoadingIssues(this); + // show if the user tried to be naughty and load arbitrary DLLs static bool shownTriedToLoadDlls = false; if (!shownTriedToLoadDlls) { diff --git a/loader/src/loader/Loader.cpp b/loader/src/loader/Loader.cpp index d9a011a1..ce46c9ad 100644 --- a/loader/src/loader/Loader.cpp +++ b/loader/src/loader/Loader.cpp @@ -65,9 +65,40 @@ std::vector Loader::getAllMods() { return m_impl->getAllMods(); } -std::vector Loader::getProblems() const { +std::vector Loader::getAllProblems() const { return m_impl->getProblems(); } +std::vector Loader::getProblems() const { + std::vector result; + for (auto problem : this->getAllProblems()) { + if ( + problem.type != LoadProblem::Type::Recommendation && + problem.type != LoadProblem::Type::Suggestion + ) { + result.push_back(problem); + } + } + return result; + return ranges::filter( + m_impl->getProblems(), + [](auto const& problem) { + return problem.type != LoadProblem::Type::Recommendation && + problem.type != LoadProblem::Type::Suggestion; + } + ); +} +std::vector Loader::getRecommendations() const { + std::vector result; + for (auto problem : this->getAllProblems()) { + if ( + problem.type == LoadProblem::Type::Recommendation || + problem.type == LoadProblem::Type::Suggestion + ) { + result.push_back(problem); + } + } + return result; +} void Loader::queueInMainThread(ScheduledFunction func) { return m_impl->queueInMainThread(std::move(func)); diff --git a/loader/src/loader/LoaderImpl.cpp b/loader/src/loader/LoaderImpl.cpp index 6b20c219..cffde653 100644 --- a/loader/src/loader/LoaderImpl.cpp +++ b/loader/src/loader/LoaderImpl.cpp @@ -521,22 +521,35 @@ void Loader::Impl::findProblems() { for (auto const& dep : mod->getMetadata().getDependencies()) { if (dep.mod && dep.mod->isEnabled() && dep.version.compare(dep.mod->getVersion())) continue; + + auto dismissKey = fmt::format("dismiss-optional-dependency-{}-for-{}", dep.id, id); + switch(dep.importance) { case ModMetadata::Dependency::Importance::Suggested: - this->addProblem({ - LoadProblem::Type::Suggestion, - mod, - fmt::format("{} {}", dep.id, dep.version.toString()) - }); - log::info("{} suggests {} {}", id, dep.id, dep.version); + if (!Mod::get()->template getSavedValue(dismissKey)) { + this->addProblem({ + LoadProblem::Type::Suggestion, + mod, + fmt::format("{} {}", dep.id, dep.version.toString()) + }); + log::info("{} suggests {} {}", id, dep.id, dep.version); + } + else { + log::info("{} suggests {} {}, but that suggestion was dismissed", id, dep.id, dep.version); + } break; case ModMetadata::Dependency::Importance::Recommended: - this->addProblem({ - LoadProblem::Type::Recommendation, - mod, - fmt::format("{} {}", dep.id, dep.version.toString()) - }); - log::warn("{} recommends {} {}", id, dep.id, dep.version); + if (!Mod::get()->template getSavedValue(dismissKey)) { + this->addProblem({ + LoadProblem::Type::Recommendation, + mod, + fmt::format("{} {}", dep.id, dep.version.toString()) + }); + log::warn("{} recommends {} {}", id, dep.id, dep.version); + } + else { + log::warn("{} recommends {} {}, but that suggestion was dismissed", id, dep.id, dep.version); + } break; case ModMetadata::Dependency::Importance::Required: if(m_mods.find(dep.id) == m_mods.end()) { diff --git a/loader/src/loader/Mod.cpp b/loader/src/loader/Mod.cpp index 37187ef4..695663c7 100644 --- a/loader/src/loader/Mod.cpp +++ b/loader/src/loader/Mod.cpp @@ -248,11 +248,30 @@ bool Mod::hasSavedValue(std::string_view const key) { bool Mod::hasProblems() const { return m_impl->hasProblems(); } - +std::vector Mod::getAllProblems() const { + return m_impl->getProblems(); +} +std::vector Mod::getProblems() const { + return ranges::filter( + this->getAllProblems(), + [](auto const& problem) { + return problem.type != LoadProblem::Type::Recommendation && + problem.type != LoadProblem::Type::Suggestion; + } + ); +} +std::vector Mod::getRecommendations() const { + return ranges::filter( + this->getAllProblems(), + [](auto const& problem) { + return problem.type == LoadProblem::Type::Recommendation || + problem.type == LoadProblem::Type::Suggestion; + } + ); +} bool Mod::shouldLoad() const { return m_impl->shouldLoad(); } - bool Mod::isCurrentlyLoading() const { return m_impl->isCurrentlyLoading(); -} \ No newline at end of file +} diff --git a/loader/src/loader/ModMetadataImpl.cpp b/loader/src/loader/ModMetadataImpl.cpp index c9eb4432..e78a746c 100644 --- a/loader/src/loader/ModMetadataImpl.cpp +++ b/loader/src/loader/ModMetadataImpl.cpp @@ -678,7 +678,7 @@ std::vector*>> ModMetadata::ge ModMetadata::ModMetadata() : m_impl(std::make_unique()) {} ModMetadata::ModMetadata(std::string id) : m_impl(std::make_unique()) { m_impl->m_id = std::move(id); } -ModMetadata::ModMetadata(ModMetadata const& other) : m_impl(std::make_unique(*other.m_impl)) {} +ModMetadata::ModMetadata(ModMetadata const& other) : m_impl(other.m_impl ? std::make_unique(*other.m_impl) : std::make_unique()) {} ModMetadata::ModMetadata(ModMetadata&& other) noexcept : m_impl(std::move(other.m_impl)) {} ModMetadata& ModMetadata::operator=(ModMetadata const& other) { diff --git a/loader/src/server/Server.cpp b/loader/src/server/Server.cpp index 31fa637c..45956d98 100644 --- a/loader/src/server/Server.cpp +++ b/loader/src/server/Server.cpp @@ -598,7 +598,8 @@ ServerRequest server::getModVersion(std::string const& id, std auto req = web::WebRequest(); req.userAgent(getServerUserAgent()); - auto versionURL = version ? version->toString(false) : "latest"; + auto versionURL = version ? version->toNonVString(false) : "latest"; + log::info("{}", getServerAPIBaseURL() + "/mods/" + id + "/versions/" + versionURL); return req.get(getServerAPIBaseURL() + "/mods/" + id + "/versions/" + versionURL).map( [](web::WebResponse* response) -> Result { if (response->ok()) { diff --git a/loader/src/ui/mods/GeodeStyle.cpp b/loader/src/ui/mods/GeodeStyle.cpp index c93cb276..75a9f32d 100644 --- a/loader/src/ui/mods/GeodeStyle.cpp +++ b/loader/src/ui/mods/GeodeStyle.cpp @@ -12,11 +12,16 @@ $execute { 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 }); + ColorProvider::get()->define("mod-list-errors-found"_spr, { 235, 35, 112, 255 }); + ColorProvider::get()->define("mod-list-errors-found-2"_spr, { 245, 27, 27, 255 }); ColorProvider::get()->define("mod-list-tab-selected-bg"_spr, { 168, 147, 185, 255 }); ColorProvider::get()->define("mod-list-tab-selected-bg-alt"_spr, { 147, 163, 185, 255 }); ColorProvider::get()->define("mod-list-featured-color"_spr, { 255, 255, 120, 255 }); ColorProvider::get()->define("mod-list-enabled"_spr, { 120, 255, 100, 255 }); ColorProvider::get()->define("mod-list-disabled"_spr, { 255, 120, 100, 255 }); + ColorProvider::get()->define("mod-list-recommended-bg"_spr, ccc3(25, 255, 167)); + ColorProvider::get()->define("mod-list-recommended-by"_spr, ccc3(25, 255, 167)); + ColorProvider::get()->define("mod-list-recommended-by-2"_spr, ccc3(47, 255, 255)); } bool GeodeSquareSprite::init(CCSprite* top, bool* state) { diff --git a/loader/src/ui/mods/GeodeStyle.hpp b/loader/src/ui/mods/GeodeStyle.hpp index 46935e25..e36102ec 100644 --- a/loader/src/ui/mods/GeodeStyle.hpp +++ b/loader/src/ui/mods/GeodeStyle.hpp @@ -6,17 +6,30 @@ using namespace geode::prelude; +enum class GeodePopupStyle { + Default, + Alt, + Alt2, +}; + template class GeodePopup : public Popup { protected: - bool init(float width, float height, Args... args, bool altBG = false) { - if (!Popup::initAnchored(width, height, std::forward(args)..., (altBG ? "GE_square02.png"_spr : "GE_square01.png"_spr))) + bool init(float width, float height, Args... args, GeodePopupStyle style = GeodePopupStyle::Default) { + const char* bg; + switch (style) { + default: + case GeodePopupStyle::Default: bg = "GE_square01.png"_spr; break; + case GeodePopupStyle::Alt: bg = "GE_square02.png"_spr; break; + case GeodePopupStyle::Alt2: bg = "GE_square03.png"_spr; break; + } + if (!Popup::initAnchored(width, height, std::forward(args)..., bg)) return false; this->setCloseButtonSpr( CircleButtonSprite::createWithSpriteFrameName( "close.png"_spr, .85f, - (altBG ? CircleBaseColor::DarkAqua : CircleBaseColor::DarkPurple) + (style == GeodePopupStyle::Default ? CircleBaseColor::DarkPurple : CircleBaseColor::DarkAqua) ) ); diff --git a/loader/src/ui/mods/ModsLayer.cpp b/loader/src/ui/mods/ModsLayer.cpp index 16023082..c701b8b9 100644 --- a/loader/src/ui/mods/ModsLayer.cpp +++ b/loader/src/ui/mods/ModsLayer.cpp @@ -126,7 +126,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(isRestartRequired()); + m_restartBtn->setVisible(ModListSource::isRestartRequired()); } break; // If some downloads were cancelled, show the restart button normally @@ -135,7 +135,7 @@ void ModsStatusNode::updateState() { m_status->setColor(ccWHITE); m_status->setVisible(true); - m_restartBtn->setVisible(isRestartRequired()); + m_restartBtn->setVisible(ModListSource::isRestartRequired()); } break; // If all downloads were finished, show the restart button normally @@ -151,7 +151,7 @@ void ModsStatusNode::updateState() { m_status->setVisible(true); m_statusBG->setVisible(true); - m_restartBtn->setVisible(isRestartRequired()); + m_restartBtn->setVisible(ModListSource::isRestartRequired()); } break; case DownloadState::SomeErrored: { @@ -349,8 +349,8 @@ bool ModsLayer::init() { mainTabs->setPosition(m_frame->convertToWorldSpace(tabsTop->getPosition() + ccp(0, 10))); for (auto item : std::initializer_list> { - { "download.png"_spr, "Installed", InstalledModListSource::get(false) }, - { "GJ_timeIcon_001.png", "Updates", InstalledModListSource::get(true) }, + { "download.png"_spr, "Installed", InstalledModListSource::get(InstalledModListType::All) }, + { "GJ_starsIcon_001.png", "Recommended", SuggestedModListSource::get() }, { "globe.png"_spr, "Download", ServerModListSource::get(ServerModListType::Download) }, { "GJ_sTrendingIcon_001.png", "Trending", ServerModListSource::get(ServerModListType::Trending) }, { "gj_folderBtn_001.png", "Mod Packs", ModPackListSource::get() }, @@ -416,7 +416,7 @@ bool ModsLayer::init() { this->addChildAtPosition(m_pageMenu, Anchor::TopRight, ccp(-5, -5), false); // Go to installed mods list - this->gotoTab(InstalledModListSource::get(false)); + this->gotoTab(InstalledModListSource::get(InstalledModListType::All)); this->setKeypadEnabled(true); cocos::handleTouchPriority(this, true); @@ -517,7 +517,7 @@ void ModsLayer::onBack(CCObject*) { // To avoid memory overloading, clear caches after leaving the layer server::clearServerCaches(true); - clearAllModListSourceCaches(); + ModListSource::clearAllCaches(); } void ModsLayer::onGoToPage(CCObject*) { diff --git a/loader/src/ui/mods/list/ModItem.cpp b/loader/src/ui/mods/list/ModItem.cpp index 5fb757dc..b7162db1 100644 --- a/loader/src/ui/mods/list/ModItem.cpp +++ b/loader/src/ui/mods/list/ModItem.cpp @@ -122,6 +122,7 @@ bool ModItem::init(ModSource&& source) { ->setAxisAlignment(AxisAlignment::End) ->setGap(10) ); + m_viewMenu->getLayout()->ignoreInvisibleChildren(true); this->addChildAtPosition(m_viewMenu, Anchor::Right, ccp(-10, 0)); // Handle source-specific stuff @@ -137,6 +138,16 @@ bool ModItem::init(ModSource&& source) { m_viewMenu->addChild(m_enableToggle); m_viewMenu->updateLayout(); } + if (mod->hasProblems()) { + auto viewErrorSpr = CircleButtonSprite::createWithSpriteFrameName( + "exclamation.png"_spr, 1.f, + CircleBaseColor::DarkPurple, CircleBaseSize::Small + ); + auto viewErrorBtn = CCMenuItemSpriteExtra::create( + viewErrorSpr, this, menu_selector(ModItem::onViewError) + ); + m_viewMenu->addChild(viewErrorBtn); + } }, [this](server::ServerModMetadata const& metadata) { if (metadata.featured) { @@ -151,6 +162,25 @@ bool ModItem::init(ModSource&& source) { m_titleContainer->addChild(starBG); } }, + [this](ModSuggestion const& suggestion) { + m_recommendedBy = CCNode::create(); + m_recommendedBy->setContentWidth(225); + + auto byLabel = CCLabelBMFont::create("Recommended by ", "bigFont.fnt"); + byLabel->setColor("mod-list-recommended-by"_cc3b); + m_recommendedBy->addChild(byLabel); + + auto nameLabel = CCLabelBMFont::create(suggestion.forMod->getName().c_str(), "bigFont.fnt"); + nameLabel->setColor("mod-list-recommended-by-2"_cc3b); + m_recommendedBy->addChild(nameLabel); + + m_recommendedBy->setLayout( + RowLayout::create() + ->setDefaultScaleLimits(.1f, 1.f) + ->setAxisAlignment(AxisAlignment::Start) + ); + m_infoContainer->addChild(m_recommendedBy); + }, }); auto updateSpr = CircleButtonSprite::createWithSpriteFrameName( @@ -160,7 +190,6 @@ bool ModItem::init(ModSource&& source) { updateSpr, this, menu_selector(ModItem::onInstall) ); m_viewMenu->addChild(m_updateBtn); - m_updateBtn->setVisible(false); if (m_source.asMod()) { m_checkUpdateListener.bind(this, &ModItem::onCheckUpdates); @@ -217,20 +246,24 @@ void ModItem::updateState() { // (possibly overriding later based on state) m_source.visit(makeVisitor { [this](Mod* mod) { - m_bg->setColor({ 255, 255, 255 }); + m_bg->setColor(ccWHITE); m_bg->setOpacity(mod->isOrWillBeEnabled() ? 25 : 10); m_titleLabel->setOpacity(mod->isOrWillBeEnabled() ? 255 : 155); m_versionLabel->setOpacity(mod->isOrWillBeEnabled() ? 255 : 155); m_developerLabel->setOpacity(mod->isOrWillBeEnabled() ? 255 : 155); }, [this](server::ServerModMetadata const& metadata) { - m_bg->setColor({ 255, 255, 255 }); + m_bg->setColor(ccWHITE); m_bg->setOpacity(25); if (metadata.featured) { - m_bg->setColor(to3B(ColorProvider::get()->color("mod-list-featured-color"_spr))); + m_bg->setColor("mod-list-featured-color"_cc3b); m_bg->setOpacity(40); } }, + [this](ModSuggestion const& suggestion) { + m_bg->setColor("mod-list-recommended-bg"_cc3b); + m_bg->setOpacity(25); + } }); if ( @@ -253,6 +286,12 @@ void ModItem::updateState() { m_viewMenu->updateLayout(); m_titleContainer->updateLayout(); + // If there were problems, tint the BG red + if (m_source.asMod() && m_source.asMod()->hasProblems()) { + m_bg->setColor("mod-list-errors-found"_cc3b); + m_bg->setOpacity(40); + } + // Highlight item via BG if it wants to restart for extra UI attention if (wantsRestart) { m_bg->setColor(to3B(ColorProvider::get()->color("mod-list-restart-required-label"_spr))); @@ -299,11 +338,16 @@ void ModItem::updateSize(float width, bool big) { m_developers->setContentWidth(titleSpace.width / m_infoContainer->getScale()); m_developers->updateLayout(); + if (m_recommendedBy) { + m_recommendedBy->setContentWidth(titleSpace.width / m_infoContainer->getScale()); + m_recommendedBy->updateLayout(); + } + m_infoContainer->setPosition(m_obContentSize.height + 10, m_obContentSize.height / 2); m_infoContainer->setContentSize(ccp(titleSpace.width, titleSpace.height) / m_infoContainer->getScale()); m_infoContainer->updateLayout(); - m_viewMenu->setContentWidth(m_obContentSize.width / 2 - 20); + m_viewMenu->setContentWidth(m_obContentSize.width / m_viewMenu->getScaleX() / 2 - 20); m_viewMenu->updateLayout(); this->updateLayout(); @@ -317,7 +361,21 @@ void ModItem::onCheckUpdates(typename server::ServerRequestshow(); + ModPopup::create(m_source.convertForPopup())->show(); +} + +void ModItem::onViewError(CCObject*) { + if (auto mod = m_source.asMod()) { + std::vector problems; + for (auto problem : mod->getProblems()) { + problems.push_back(fmt::format("{} (code {})", problem.message, static_cast(problem.type))); + } + FLAlertLayer::create( + fmt::format("Errors with {}", mod->getName()).c_str(), + ranges::join(problems, "\n"), + "OK" + )->show(); + } } void ModItem::onEnable(CCObject*) { diff --git a/loader/src/ui/mods/list/ModItem.hpp b/loader/src/ui/mods/list/ModItem.hpp index 2ab1c5a0..d79d38df 100644 --- a/loader/src/ui/mods/list/ModItem.hpp +++ b/loader/src/ui/mods/list/ModItem.hpp @@ -18,6 +18,7 @@ protected: CCLabelBMFont* m_titleLabel; CCLabelBMFont* m_versionLabel; CCNode* m_developers; + CCNode* m_recommendedBy; CCLabelBMFont* m_developerLabel; ButtonSprite* m_restartRequiredLabel; CCNode* m_downloadWaiting; @@ -43,6 +44,7 @@ protected: void onEnable(CCObject*); void onView(CCObject*); + void onViewError(CCObject*); void onInstall(CCObject*); public: diff --git a/loader/src/ui/mods/list/ModList.cpp b/loader/src/ui/mods/list/ModList.cpp index c48abc7f..ec80bb86 100644 --- a/loader/src/ui/mods/list/ModList.cpp +++ b/loader/src/ui/mods/list/ModList.cpp @@ -95,12 +95,62 @@ bool ModList::init(ModListSource* src, CCSize const& size) { m_updateAllMenu->setLayout( RowLayout::create() ->setAxisAlignment(AxisAlignment::End) - ->setDefaultScaleLimits(.1f, .75f) + ->setDefaultScaleLimits(.1f, .6f) ); m_updateAllMenu->getLayout()->ignoreInvisibleChildren(true); m_updateAllContainer->addChildAtPosition(m_updateAllMenu, Anchor::Right, ccp(-10, 0)); m_topContainer->addChild(m_updateAllContainer); + + if (Loader::get()->getProblems().size()) { + m_errorsContainer = CCNode::create(); + m_errorsContainer->ignoreAnchorPointForPosition(false); + m_errorsContainer->setContentSize({ size.width, 30 }); + m_errorsContainer->setVisible(false); + + auto errorsBG = CCLayerGradient::create( + "mod-list-errors-found"_cc4b, + "mod-list-errors-found-2"_cc4b, + ccp(1, -.5f) + ); + errorsBG->setContentSize(m_errorsContainer->getContentSize()); + errorsBG->ignoreAnchorPointForPosition(false); + + m_errorsContainer->addChildAtPosition(errorsBG, Anchor::Center); + + auto errorsLabel = TextArea::create( + "There were errors
loading some mods", + "bigFont.fnt", .35f, size.width / 2 - 30, ccp(0, 1), 12.f, false + ); + m_errorsContainer->addChildAtPosition(errorsLabel, Anchor::Left, ccp(10, 0), ccp(0, 0)); + + auto errorsMenu = CCMenu::create(); + errorsMenu->setContentWidth(size.width / 2); + errorsMenu->setAnchorPoint({ 1, .5f }); + + auto showErrorsSpr = createGeodeButton( + CCSprite::createWithSpriteFrameName("GJ_filterIcon_001.png"), + "Show Errors Only", "GJ_button_06.png" + ); + auto hideErrorsSpr = createGeodeButton( + CCSprite::createWithSpriteFrameName("GJ_filterIcon_001.png"), + "Hide Errors Only", "GE_button_05.png"_spr + ); + m_toggleErrorsOnlyBtn = CCMenuItemToggler::create( + showErrorsSpr, hideErrorsSpr, this, menu_selector(ModList::onToggleErrors) + ); + m_toggleErrorsOnlyBtn->m_notClickable = true; + errorsMenu->addChild(m_toggleErrorsOnlyBtn); + + errorsMenu->setLayout( + RowLayout::create() + ->setAxisAlignment(AxisAlignment::End) + ->setDefaultScaleLimits(.1f, .6f) + ); + m_errorsContainer->addChildAtPosition(errorsMenu, Anchor::Right, ccp(-10, 0)); + + m_topContainer->addChild(m_errorsContainer); + } } m_searchMenu = CCNode::create(); @@ -418,6 +468,12 @@ void ModList::updateTopContainer() { m_updateAllMenu->updateLayout(); } + // If there are errors, show the error banner + if (m_errorsContainer) { + auto noErrors = Loader::get()->getProblems().empty(); + m_errorsContainer->setVisible(!noErrors); + } + // ModList uses an anchor layout, so this puts the list in the right place this->updateLayout(); } @@ -449,11 +505,14 @@ void ModList::updateSize(bool big) { } void ModList::updateState() { - // Update the "Show Updates" button on the updates available banner - if (m_toggleUpdatesOnlyBtn) { - auto src = typeinfo_cast(m_source); - if (src) { - m_toggleUpdatesOnlyBtn->toggle(src->getQuery().onlyUpdates); + // Update the "Show Updates" and "Show Errors" buttons on + // the updates available / errors banners + if (auto src = typeinfo_cast(m_source)) { + if (m_toggleUpdatesOnlyBtn) { + m_toggleUpdatesOnlyBtn->toggle(src->getQuery().type == InstalledModListType::OnlyUpdates); + } + if (m_toggleErrorsOnlyBtn) { + m_toggleErrorsOnlyBtn->toggle(src->getQuery().type == InstalledModListType::OnlyErrors); } } @@ -525,7 +584,19 @@ void ModList::onClearFilters(CCObject*) { void ModList::onToggleUpdates(CCObject*) { if (auto src = typeinfo_cast(m_source)) { - src->getQueryMut()->onlyUpdates = !src->getQuery().onlyUpdates; + auto mut = src->getQueryMut(); + mut->type = mut->type == InstalledModListType::OnlyUpdates ? + InstalledModListType::All : + InstalledModListType::OnlyUpdates; + } +} + +void ModList::onToggleErrors(CCObject*) { + if (auto src = typeinfo_cast(m_source)) { + auto mut = src->getQueryMut(); + mut->type = mut->type == InstalledModListType::OnlyErrors ? + InstalledModListType::All : + InstalledModListType::OnlyErrors; } } diff --git a/loader/src/ui/mods/list/ModList.hpp b/loader/src/ui/mods/list/ModList.hpp index 04e1c7b9..830fa222 100644 --- a/loader/src/ui/mods/list/ModList.hpp +++ b/loader/src/ui/mods/list/ModList.hpp @@ -38,6 +38,8 @@ protected: CCMenuItemSpriteExtra* m_updateAllBtn = nullptr; CCNode* m_updateAllLoadingCircle = nullptr; CCMenuItemToggler* m_toggleUpdatesOnlyBtn = nullptr; + CCNode* m_errorsContainer = nullptr; + CCMenuItemToggler* m_toggleErrorsOnlyBtn = nullptr; TextArea* m_updateCountLabel = nullptr; TextInput* m_searchInput; EventListener m_invalidateCacheListener; @@ -59,6 +61,7 @@ protected: void onSort(CCObject*); void onClearFilters(CCObject*); void onToggleUpdates(CCObject*); + void onToggleErrors(CCObject*); void onUpdateAll(CCObject*); public: diff --git a/loader/src/ui/mods/list/ModProblemItem.cpp b/loader/src/ui/mods/list/ModProblemItem.cpp new file mode 100644 index 00000000..dff26832 --- /dev/null +++ b/loader/src/ui/mods/list/ModProblemItem.cpp @@ -0,0 +1,97 @@ +#include "ModProblemItem.hpp" +#include +#include + +bool ModProblemItem::init() { + if (!CCNode::init()) + return false; + + this->setContentSize({ 250, 28 }); + + auto bg = CCScale9Sprite::create("square02b_small.png"); + bg->setOpacity(20); + bg->ignoreAnchorPointForPosition(false); + bg->setAnchorPoint({ .5f, .5f }); + bg->setScale(.7f); + bg->setContentSize(m_obContentSize / bg->getScale()); + this->addChildAtPosition(bg, Anchor::Center); + + m_menu = CCMenu::create(); + m_menu->setContentWidth(m_obContentSize.width / 2 - 10); + m_menu->setLayout( + RowLayout::create() + ->setDefaultScaleLimits(.1f, .35f) + ->setAxisAlignment(AxisAlignment::End) + ); + this->addChildAtPosition(m_menu, Anchor::Right, ccp(-5, 0), ccp(1, .5f)); + + this->setLoading(); + + return true; +} + +void ModProblemItem::clearState() { + this->removeChildByID("loading-spinner"); + this->removeChildByID("error-label"); +} +void ModProblemItem::setLoading() { + this->clearState(); + + auto loadingCircle = createLoadingCircle(20); + this->addChildAtPosition(loadingCircle, Anchor::Center); +} +void ModProblemItem::setError(std::string const& error) { + this->clearState(); + + auto label = CCLabelBMFont::create(error.c_str(), "bigFont.fnt"); + label->setColor("mod-list-errors-found"_cc3b); + label->limitLabelWidth(m_obContentSize.width, .5f, .1f); + label->setID("error-label"); + this->addChildAtPosition(label, Anchor::Center); +} +void ModProblemItem::setMod(ModMetadata const& metadata) { + this->clearState(); + + auto h = m_obContentSize.height; + + auto logo = createServerModLogo(metadata.getID()); + limitNodeSize(logo, { h / 1.4f, h / 1.4f }, 5.f, .1f); + this->addChildAtPosition(logo, Anchor::Left, ccp(h / 2, 0)); + + auto title = CCLabelBMFont::create(metadata.getName().c_str(), "bigFont.fnt"); + title->limitLabelWidth(m_obContentSize.width / 2, .35f, .1f); + this->addChildAtPosition(title, Anchor::Left, ccp(h, h / 5), ccp(0, .5f)); + + auto versionLabel = CCLabelBMFont::create( + metadata.getVersion().toVString().c_str(), + "bigFont.fnt" + ); + versionLabel->setColor("mod-list-version-label"_cc3b); + versionLabel->limitLabelWidth(m_obContentSize.width / 2, .25f, .1f); + this->addChildAtPosition( + versionLabel, Anchor::Left, + ccp(h + title->getScaledContentWidth() + 3, h / 5), ccp(0, .5f) + ); + + auto developer = CCLabelBMFont::create( + fmt::format("By {}", metadata.getDeveloper()).c_str(), + "goldFont.fnt" + ); + developer->limitLabelWidth(m_obContentSize.width / 2, .35f, .1f); + this->addChildAtPosition(developer, Anchor::Left, ccp(h, -h / 5), ccp(0, .5f)); +} + +ModProblemItem* ModProblemItem::create() { + auto ret = new ModProblemItem(); + if (ret && ret->init()) { + ret->setError("Unknown Type"); + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +ModProblemItem* ModProblemItem::parse(LoadProblem const& problem) { + return ModProblemItem::create(); +} diff --git a/loader/src/ui/mods/list/ModProblemItem.hpp b/loader/src/ui/mods/list/ModProblemItem.hpp new file mode 100644 index 00000000..f2a0e76f --- /dev/null +++ b/loader/src/ui/mods/list/ModProblemItem.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include +#include "../GeodeStyle.hpp" + +using namespace geode::prelude; +using VersionDownload = server::ServerRequest; + +class ModProblemItem : public CCNode { +protected: + CCMenu* m_menu; + + bool init(); + + void clearState(); + void setLoading(); + void setError(std::string const& error); + void setMod(ModMetadata const& metadata); + +public: + static ModProblemItem* create(); + static ModProblemItem* parse(LoadProblem const& problem); +}; diff --git a/loader/src/ui/mods/list/ModProblemItemList.cpp b/loader/src/ui/mods/list/ModProblemItemList.cpp new file mode 100644 index 00000000..bae36133 --- /dev/null +++ b/loader/src/ui/mods/list/ModProblemItemList.cpp @@ -0,0 +1,33 @@ +#include "ModProblemItemList.hpp" +#include "ModProblemItem.hpp" + +bool ModProblemItemList::init(float height) { + if (!CCNode::init()) + return false; + + this->setContentSize({ 250, height }); + + m_scrollLayer = ScrollLayer::create({ m_obContentSize.width, height }); + for (auto problem : Loader::get()->getProblems()) { + m_scrollLayer->m_contentLayer->addChild(ModProblemItem::parse(problem)); + } + m_scrollLayer->m_contentLayer->setLayout( + ColumnLayout::create() + ->setAutoGrowAxis(height) + ->setAxisReverse(true) + ->setAxisAlignment(AxisAlignment::End) + ); + this->addChildAtPosition(m_scrollLayer, Anchor::BottomLeft); + + return true; +} + +ModProblemItemList* ModProblemItemList::create(float height) { + auto ret = new ModProblemItemList(); + if (ret && ret->init(height)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} diff --git a/loader/src/ui/mods/list/ModProblemItemList.hpp b/loader/src/ui/mods/list/ModProblemItemList.hpp new file mode 100644 index 00000000..a7be7fd0 --- /dev/null +++ b/loader/src/ui/mods/list/ModProblemItemList.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + +using namespace geode::prelude; + +class ModProblemItemList : public CCNode { +protected: + ScrollLayer* m_scrollLayer; + + bool init(float height); + +public: + static ModProblemItemList* create(float height); +}; diff --git a/loader/src/ui/mods/popups/ModPopup.cpp b/loader/src/ui/mods/popups/ModPopup.cpp index 17830c12..0284946d 100644 --- a/loader/src/ui/mods/popups/ModPopup.cpp +++ b/loader/src/ui/mods/popups/ModPopup.cpp @@ -104,6 +104,30 @@ bool ModPopup::setup(ModSource&& src) { dev->setAnchorPoint({ .0f, .5f }); titleContainer->addChildAtPosition(dev, Anchor::BottomLeft, ccp(devAndTitlePos, titleContainer->getContentHeight() * .25f)); + if (auto suggestion = m_source.asSuggestion()) { + title->updateAnchoredPosition(Anchor::TopLeft, ccp(devAndTitlePos, -2)); + dev->updateAnchoredPosition(Anchor::Left, ccp(devAndTitlePos, 0)); + + auto recommendedBy = CCNode::create(); + recommendedBy->setContentWidth(titleContainer->getContentWidth() - devAndTitlePos); + recommendedBy->setAnchorPoint({ .0f, .5f }); + + auto byLabel = CCLabelBMFont::create("Recommended by ", "bigFont.fnt"); + byLabel->setColor("mod-list-recommended-by"_cc3b); + recommendedBy->addChild(byLabel); + + auto nameLabel = CCLabelBMFont::create(suggestion->forMod->getName().c_str(), "bigFont.fnt"); + nameLabel->setColor("mod-list-recommended-by-2"_cc3b); + recommendedBy->addChild(nameLabel); + + recommendedBy->setLayout( + RowLayout::create() + ->setDefaultScaleLimits(.1f, 1.f) + ->setAxisAlignment(AxisAlignment::Start) + ); + titleContainer->addChildAtPosition(recommendedBy, Anchor::BottomLeft, ccp(devAndTitlePos, 4)); + } + leftColumn->addChild(titleContainer); auto idStr = "(ID: " + m_source.getMetadata().getID() + ")"; @@ -512,7 +536,6 @@ bool ModPopup::setup(ModSource&& src) { void ModPopup::updateState() { auto asMod = m_source.asMod(); - auto asServer = m_source.asServer(); auto wantsRestart = m_source.wantsRestart(); m_installBG->setColor(wantsRestart ? to3B(ColorProvider::get()->color("mod-list-restart-required-label"_spr)) : ccc3(0, 0, 0)); @@ -543,7 +566,7 @@ void ModPopup::updateState() { m_reenableBtn->setVisible(asMod && modRequestedActionIsToggle(asMod->getRequestedAction())); m_updateBtn->setVisible(m_source.hasUpdates().has_value() && asMod->getRequestedAction() == ModRequestedAction::None); - m_installBtn->setVisible(asServer); + m_installBtn->setVisible(m_source.asServer() || m_source.asSuggestion()); m_uninstallBtn->setVisible(asMod && asMod->getRequestedAction() == ModRequestedAction::None); if (asMod && modRequestedActionIsUninstall(asMod->getRequestedAction())) { @@ -878,8 +901,14 @@ void ModPopup::onSupport(CCObject*) { ModPopup* ModPopup::create(ModSource&& src) { auto ret = new ModPopup(); - bool isServer = src.asServer(); - if (ret && ret->init(440, 280, std::move(src), isServer)) { + GeodePopupStyle style = GeodePopupStyle::Default; + if (src.asServer()) { + style = GeodePopupStyle::Alt; + } + else if (src.asSuggestion()) { + style = GeodePopupStyle::Alt2; + } + if (ret && ret->init(440, 280, std::move(src), style)) { ret->autorelease(); return ret; } diff --git a/loader/src/ui/mods/sources/InstalledModListSource.cpp b/loader/src/ui/mods/sources/InstalledModListSource.cpp new file mode 100644 index 00000000..bf3d9f24 --- /dev/null +++ b/loader/src/ui/mods/sources/InstalledModListSource.cpp @@ -0,0 +1,112 @@ +#include "ModListSource.hpp" + +bool InstalledModsQuery::preCheck(ModSource const& src) const { + // If we only want mods with updates, then only give mods with updates + // NOTE: The caller of filterModsWithQuery() should have ensured that + // `src.checkUpdates()` has been called and has finished + if ( + auto updates = src.hasUpdates(); + type == InstalledModListType::OnlyUpdates && + !(updates && updates->hasUpdateForInstalledMod()) + ) { + return false; + } + // If only errors requested, only show mods with errors (duh) + if (type == InstalledModListType::OnlyErrors) { + return src.asMod()->hasProblems(); + } + return true; +} +bool InstalledModsQuery::queryCheck(ModSource const& src, double& weighted) const { + auto addToList = modFuzzyMatch(src.asMod()->getMetadata(), *query, weighted); + // Loader gets boost to ensure it's normally always top of the list + if (addToList && src.asMod()->getID() == "geode.loader") { + weighted += 5; + } + // todo: favorites + return addToList; +} + +InstalledModListSource::InstalledModListSource(InstalledModListType type) + : m_type(type) +{ + this->resetQuery(); +} + +InstalledModListSource* InstalledModListSource::get(InstalledModListType type) { + switch (type) { + default: + case InstalledModListType::All: { + static auto inst = new InstalledModListSource(InstalledModListType::All); + return inst; + } break; + + case InstalledModListType::OnlyUpdates: { + static auto inst = new InstalledModListSource(InstalledModListType::OnlyUpdates); + return inst; + } break; + + case InstalledModListType::OnlyErrors: { + static auto inst = new InstalledModListSource(InstalledModListType::OnlyErrors); + return inst; + } break; + } +} + +void InstalledModListSource::resetQuery() { + m_query = InstalledModsQuery { + .type = m_type, + }; +} + +InstalledModListSource::ProviderTask InstalledModListSource::fetchPage(size_t page, size_t pageSize, bool forceUpdate) { + m_query.page = page; + m_query.pageSize = pageSize; + + auto content = ModListSource::ProvidedMods(); + for (auto& mod : Loader::get()->getAllMods()) { + content.mods.push_back(ModSource(mod)); + } + // If we're only checking mods that have updates, we first have to run + // update checks every mod... + if (m_query.type == InstalledModListType::OnlyUpdates && content.mods.size()) { + using UpdateTask = server::ServerRequest>; + std::vector tasks; + for (auto& src : content.mods) { + tasks.push_back(src.checkUpdates()); + } + return UpdateTask::all(std::move(tasks)).map( + [content = std::move(content), query = m_query](auto*) mutable -> ProviderTask::Value { + // Filter the results based on the current search + // query and return them + filterModsWithLocalQuery(content, query); + return Ok(content); + }, + [](auto*) -> ProviderTask::Progress { return std::nullopt; } + ); + } + // Otherwise simply construct the result right away + else { + filterModsWithLocalQuery(content, m_query); + return ProviderTask::immediate(Ok(content)); + } +} + +void InstalledModListSource::setSearchQuery(std::string const& query) { + m_query.query = query.size() ? std::optional(query) : std::nullopt; +} + +std::unordered_set InstalledModListSource::getModTags() const { + return m_query.tags; +} +void InstalledModListSource::setModTags(std::unordered_set const& tags) { + m_query.tags = tags; + this->clearCache(); +} + +InstalledModsQuery const& InstalledModListSource::getQuery() const { + return m_query; +} +InvalidateQueryAfter InstalledModListSource::getQueryMut() { + return InvalidateQueryAfter(m_query, this); +} diff --git a/loader/src/ui/mods/sources/ModListSource.cpp b/loader/src/ui/mods/sources/ModListSource.cpp index 6fc56633..d372a79e 100644 --- a/loader/src/ui/mods/sources/ModListSource.cpp +++ b/loader/src/ui/mods/sources/ModListSource.cpp @@ -5,95 +5,13 @@ #include static constexpr size_t PER_PAGE = 10; +static std::vector ALL_EXTANT_SOURCES {}; static size_t ceildiv(size_t a, size_t b) { // https://stackoverflow.com/questions/2745074/fast-ceiling-of-an-integer-division-in-c-c return a / b + (a % b != 0); } -static bool weightedFuzzyMatch(std::string const& str, std::string const& kw, double weight, double& out) { - int score; - if (fts::fuzzy_match(kw.c_str(), str.c_str(), score)) { - out = std::max(out, score * weight); - return true; - } - return false; -} - -static void filterModsWithQuery(InstalledModListSource::ProvidedMods& mods, InstalledModsQuery const& query) { - std::vector> filtered; - - // Filter installed mods based on query - for (auto& src : mods.mods) { - auto mod = src.asMod(); - double weighted = 0; - bool addToList = true; - // If we only want mods with updates, then only give mods with updates - // NOTE: The caller of filterModsWithQuery() should have ensured that - // `src.checkUpdates()` has been called and has finished - if (auto updates = src.hasUpdates(); query.onlyUpdates && !(updates && updates->hasUpdateForInstalledMod())) { - addToList = false; - } - // If some tags are provided, only return mods that match - if (addToList && query.tags.size()) { - auto compare = mod->getMetadata().getTags(); - for (auto& tag : query.tags) { - if (!compare.contains(tag)) { - addToList = false; - } - } - } - // Don't bother with unnecessary fuzzy match calculations if this mod isn't going to be added anyway - if (addToList && query.query) { - // By default don't add anything - addToList = false; - addToList |= weightedFuzzyMatch(mod->getName(), *query.query, 1, weighted); - addToList |= weightedFuzzyMatch(mod->getID(), *query.query, 0.5, weighted); - for (auto& dev : mod->getDevelopers()) { - addToList |= weightedFuzzyMatch(dev, *query.query, 0.25, weighted); - } - if (auto details = mod->getDetails()) { - addToList |= weightedFuzzyMatch(*details, *query.query, 0.005, weighted); - } - if (auto desc = mod->getDescription()) { - addToList |= weightedFuzzyMatch(*desc, *query.query, 0.02, weighted); - } - if (weighted < 2) { - addToList = false; - } - } - // Loader gets boost to ensure it's normally always top of the list - if (addToList && mod->getID() == "geode.loader") { - weighted += 5; - } - if (addToList) { - filtered.push_back({ mod, weighted }); - } - } - - // Sort list based on score - std::sort(filtered.begin(), filtered.end(), [](auto a, auto b) { - // Sort primarily by score - if (a.second != b.second) { - return a.second > b.second; - } - // Sort secondarily alphabetically - return a.first->getName() < b.first->getName(); - }); - - mods.mods.clear(); - // Pick out only the mods in the page and page size specified in the query - for ( - size_t i = query.page * query.pageSize; - i < filtered.size() && i < (query.page + 1) * query.pageSize; - i += 1 - ) { - mods.mods.push_back(filtered.at(i).first); - } - - mods.totalModCount = filtered.size(); -} - InvalidateCacheEvent::InvalidateCacheEvent(ModListSource* src) : source(src) {} ListenerResult InvalidateCacheFilter::handle(MiniFunction fn, InvalidateCacheEvent* event) { @@ -154,232 +72,16 @@ void ModListSource::search(std::string const& query) { this->clearCache(); } -ModListSource::ModListSource() {} - -InstalledModListSource::InstalledModListSource(bool onlyUpdates) - : m_onlyUpdates(onlyUpdates) -{ - this->resetQuery(); +ModListSource::ModListSource() { + ALL_EXTANT_SOURCES.push_back(this); } -InstalledModListSource* InstalledModListSource::get(bool onlyUpdates) { - if (onlyUpdates) { - static auto inst = new InstalledModListSource(true); - return inst; - } - else { - static auto inst = new InstalledModListSource(false); - return inst; +void ModListSource::clearAllCaches() { + for (auto src : ALL_EXTANT_SOURCES) { + src->clearCache(); } } - -void InstalledModListSource::resetQuery() { - m_query = InstalledModsQuery { - .onlyUpdates = m_onlyUpdates, - }; -} - -InstalledModListSource::ProviderTask InstalledModListSource::fetchPage(size_t page, size_t pageSize, bool forceUpdate) { - m_query.page = page; - m_query.pageSize = pageSize; - - auto content = ModListSource::ProvidedMods(); - for (auto& mod : Loader::get()->getAllMods()) { - content.mods.push_back(ModSource(mod)); - } - // If we're only checking mods that have updates, we first have to run - // update checks every mod... - if (m_query.onlyUpdates && content.mods.size()) { - // return ProviderTask::immediate(Ok(content)); - return ProviderTask::runWithCallback([content, query = m_query](auto finish, auto progress, auto hasBeenCancelled) { - struct Waiting final { - ModListSource::ProvidedMods content; - std::atomic_size_t waitingFor; - std::vector> waitingTasks; - }; - auto waiting = std::make_shared(); - waiting->waitingFor = content.mods.size(); - waiting->content = std::move(content); - waiting->waitingTasks.reserve(content.mods.size()); - for (auto& src : waiting->content.mods) { - // Need to store the created task so it doesn't get destroyed - waiting->waitingTasks.emplace_back(src.checkUpdates().map( - [waiting, finish, query](auto* result) { - waiting->waitingFor -= 1; - if (waiting->waitingFor == 0) { - // Make sure to clear our waiting tasks vector so - // we don't have a funky lil circular ref and a - // memory leak! - waiting->waitingTasks.clear(); - - // Filter the results based on the current search - // query and return them - filterModsWithQuery(waiting->content, query); - finish(Ok(std::move(waiting->content))); - } - return std::monostate(); - }, - [](auto*) { return std::monostate(); } - )); - } - }); - } - // Otherwise simply construct the result right away - else { - filterModsWithQuery(content, m_query); - return ProviderTask::immediate(Ok(content)); - } -} - -void InstalledModListSource::setSearchQuery(std::string const& query) { - m_query.query = query.size() ? std::optional(query) : std::nullopt; -} - -std::unordered_set InstalledModListSource::getModTags() const { - return m_query.tags; -} -void InstalledModListSource::setModTags(std::unordered_set const& tags) { - m_query.tags = tags; - this->clearCache(); -} - -InstalledModsQuery const& InstalledModListSource::getQuery() const { - return m_query; -} - -InvalidateQueryAfter InstalledModListSource::getQueryMut() { - return InvalidateQueryAfter(m_query, this); -} - -void ServerModListSource::resetQuery() { - switch (m_type) { - case ServerModListType::Download: { - m_query = server::ModsQuery {}; - } break; - - case ServerModListType::Featured: { - m_query = server::ModsQuery { - .featured = true, - }; - } break; - - case ServerModListType::Trending: { - m_query = server::ModsQuery { - .sorting = server::ModsSort::RecentlyUpdated, - }; - } break; - - case ServerModListType::Recent: { - m_query = server::ModsQuery { - .sorting = server::ModsSort::RecentlyPublished, - }; - } break; - } -} - -ServerModListSource::ProviderTask ServerModListSource::fetchPage(size_t page, size_t pageSize, bool forceUpdate) { - m_query.page = page; - m_query.pageSize = pageSize; - return server::getMods(m_query, !forceUpdate).map( - [](Result* result) -> ProviderTask::Value { - if (result->isOk()) { - auto list = result->unwrap(); - auto content = ModListSource::ProvidedMods(); - for (auto&& mod : std::move(list.mods)) { - content.mods.push_back(ModSource(std::move(mod))); - } - content.totalModCount = list.totalModCount; - return Ok(content); - } - return Err(LoadPageError("Error loading mods", result->unwrapErr().details)); - }, - [](auto* prog) { - return prog->percentage; - } - ); -} - -ServerModListSource::ServerModListSource(ServerModListType type) - : m_type(type) -{ - this->resetQuery(); -} - -ServerModListSource* ServerModListSource::get(ServerModListType type) { - switch (type) { - default: - case ServerModListType::Download: { - static auto inst = new ServerModListSource(ServerModListType::Download); - return inst; - } break; - - case ServerModListType::Featured: { - static auto inst = new ServerModListSource(ServerModListType::Featured); - return inst; - } break; - - case ServerModListType::Trending: { - static auto inst = new ServerModListSource(ServerModListType::Trending); - return inst; - } break; - - case ServerModListType::Recent: { - static auto inst = new ServerModListSource(ServerModListType::Recent); - return inst; - } break; - } -} - -void ServerModListSource::setSearchQuery(std::string const& query) { - m_query.query = query.size() ? std::optional(query) : std::nullopt; -} - -std::unordered_set ServerModListSource::getModTags() const { - return m_query.tags; -} -void ServerModListSource::setModTags(std::unordered_set const& tags) { - m_query.tags = tags; - this->clearCache(); -} - -server::ModsQuery const& ServerModListSource::getQuery() const { - return m_query; -} -InvalidateQueryAfter ServerModListSource::getQueryMut() { - return InvalidateQueryAfter(m_query, this); -} - -void ModPackListSource::resetQuery() {} -ModPackListSource::ProviderTask ModPackListSource::fetchPage(size_t page, size_t pageSize, bool forceUpdate) { - return ProviderTask::immediate(Err(LoadPageError("Coming soon ;)"))); -} - -ModPackListSource::ModPackListSource() {} - -ModPackListSource* ModPackListSource::get() { - static auto inst = new ModPackListSource(); - return inst; -} - -void ModPackListSource::setSearchQuery(std::string const& query) {} - -std::unordered_set ModPackListSource::getModTags() const { - return {}; -} -void ModPackListSource::setModTags(std::unordered_set const& set) {} - -void clearAllModListSourceCaches() { - InstalledModListSource::get(false)->clearCache(); - InstalledModListSource::get(true)->clearCache(); - - ServerModListSource::get(ServerModListType::Download)->clearCache(); - ServerModListSource::get(ServerModListType::Featured)->clearCache(); - ServerModListSource::get(ServerModListType::Trending)->clearCache(); - ServerModListSource::get(ServerModListType::Recent)->clearCache(); - - ModPackListSource::get()->clearCache(); -} -bool isRestartRequired() { +bool ModListSource::isRestartRequired() { for (auto mod : Loader::get()->getAllMods()) { if (mod->getRequestedAction() != ModRequestedAction::None) { return true; @@ -390,3 +92,30 @@ bool isRestartRequired() { } return false; } + +bool weightedFuzzyMatch(std::string const& str, std::string const& kw, double weight, double& out) { + int score; + if (fts::fuzzy_match(kw.c_str(), str.c_str(), score)) { + out = std::max(out, score * weight); + return true; + } + return false; +} +bool modFuzzyMatch(ModMetadata const& metadata, std::string const& kw, double& weighted) { + bool addToList = false; + addToList |= weightedFuzzyMatch(metadata.getName(), kw, 1, weighted); + addToList |= weightedFuzzyMatch(metadata.getID(), kw, 0.5, weighted); + for (auto& dev : metadata.getDevelopers()) { + addToList |= weightedFuzzyMatch(dev, kw, 0.25, weighted); + } + if (auto details = metadata.getDetails()) { + addToList |= weightedFuzzyMatch(*details, kw, 0.005, weighted); + } + if (auto desc = metadata.getDescription()) { + addToList |= weightedFuzzyMatch(*desc, kw, 0.02, weighted); + } + if (weighted < 2) { + addToList = false; + } + return addToList; +} diff --git a/loader/src/ui/mods/sources/ModListSource.hpp b/loader/src/ui/mods/sources/ModListSource.hpp index 33d15f83..9d25a220 100644 --- a/loader/src/ui/mods/sources/ModListSource.hpp +++ b/loader/src/ui/mods/sources/ModListSource.hpp @@ -26,14 +26,6 @@ public: InvalidateCacheFilter(ModListSource* src); }; -struct InstalledModsQuery final { - std::optional query; - bool onlyUpdates = false; - std::unordered_set tags = {}; - size_t page = 0; - size_t pageSize = 10; -}; - // Handles loading the entries for the mods list class ModListSource { public: @@ -81,6 +73,9 @@ public: PageLoadTask loadPage(size_t page, bool forceUpdate = false); std::optional getPageCount() const; std::optional getItemCount() const; + + static void clearAllCaches(); + static bool isRestartRequired(); }; template @@ -99,19 +94,37 @@ public: } }; +struct LocalModsQueryBase { + std::optional query; + std::unordered_set tags = {}; + size_t page = 0; + size_t pageSize = 10; +}; + +enum class InstalledModListType { + All, + OnlyUpdates, + OnlyErrors, +}; +struct InstalledModsQuery final : public LocalModsQueryBase { + InstalledModListType type = InstalledModListType::All; + bool preCheck(ModSource const& src) const; + bool queryCheck(ModSource const& src, double& weighted) const; +}; + class InstalledModListSource : public ModListSource { protected: - bool m_onlyUpdates; + InstalledModListType m_type; InstalledModsQuery m_query; void resetQuery() override; ProviderTask fetchPage(size_t page, size_t pageSize, bool forceUpdate) override; void setSearchQuery(std::string const& query) override; - InstalledModListSource(bool onlyUpdates); + InstalledModListSource(InstalledModListType type); public: - static InstalledModListSource* get(bool onlyUpdates); + static InstalledModListSource* get(InstalledModListType type); std::unordered_set getModTags() const override; void setModTags(std::unordered_set const& tags) override; @@ -120,6 +133,28 @@ public: InvalidateQueryAfter getQueryMut(); }; +struct SuggestedModsQuery final : public LocalModsQueryBase { + bool preCheck(ModSource const& src) const; + bool queryCheck(ModSource const& src, double& weighted) const; +}; + +class SuggestedModListSource : public ModListSource { +protected: + SuggestedModsQuery m_query; + + void resetQuery() override; + ProviderTask fetchPage(size_t page, size_t pageSize, bool forceUpdate) override; + void setSearchQuery(std::string const& query) override; + + SuggestedModListSource(); + +public: + static SuggestedModListSource* get(); + + std::unordered_set getModTags() const override; + void setModTags(std::unordered_set const& tags) override; +}; + enum class ServerModListType { Download, Featured, @@ -163,5 +198,58 @@ public: void setModTags(std::unordered_set const& tags) override; }; -void clearAllModListSourceCaches(); -bool isRestartRequired(); +bool weightedFuzzyMatch(std::string const& str, std::string const& kw, double weight, double& out); +bool modFuzzyMatch(ModMetadata const& metadata, std::string const& kw, double& out); + +template Query> +void filterModsWithLocalQuery(ModListSource::ProvidedMods& mods, Query const& query) { + std::vector> filtered; + + // Filter installed mods based on query + for (auto& src : mods.mods) { + double weighted = 0; + bool addToList = true; + // Do any checks additional this query has to start off with + if (!query.preCheck(src)) { + addToList = false; + } + // If some tags are provided, only return mods that match + if (addToList && query.tags.size()) { + auto compare = src.getMetadata().getTags(); + for (auto& tag : query.tags) { + if (!compare.contains(tag)) { + addToList = false; + } + } + } + // Don't bother with unnecessary fuzzy match calculations if this mod isn't going to be added anyway + if (addToList && query.query) { + addToList = query.queryCheck(src, weighted); + } + if (addToList) { + filtered.push_back({ src, weighted }); + } + } + + // Sort list based on score + std::sort(filtered.begin(), filtered.end(), [](auto a, auto b) { + // Sort primarily by score + if (a.second != b.second) { + return a.second > b.second; + } + // Sort secondarily alphabetically + return a.first.getMetadata().getName() < b.first.getMetadata().getName(); + }); + + mods.mods.clear(); + // Pick out only the mods in the page and page size specified in the query + for ( + size_t i = query.page * query.pageSize; + i < filtered.size() && i < (query.page + 1) * query.pageSize; + i += 1 + ) { + mods.mods.push_back(filtered.at(i).first); + } + + mods.totalModCount = filtered.size(); +} diff --git a/loader/src/ui/mods/sources/ModPackListSource.cpp b/loader/src/ui/mods/sources/ModPackListSource.cpp new file mode 100644 index 00000000..bec26303 --- /dev/null +++ b/loader/src/ui/mods/sources/ModPackListSource.cpp @@ -0,0 +1,20 @@ +#include "ModListSource.hpp" + +void ModPackListSource::resetQuery() {} +ModPackListSource::ProviderTask ModPackListSource::fetchPage(size_t page, size_t pageSize, bool forceUpdate) { + return ProviderTask::immediate(Err(LoadPageError("Coming soon ;)"))); +} + +ModPackListSource::ModPackListSource() {} + +ModPackListSource* ModPackListSource::get() { + static auto inst = new ModPackListSource(); + return inst; +} + +void ModPackListSource::setSearchQuery(std::string const& query) {} + +std::unordered_set ModPackListSource::getModTags() const { + return {}; +} +void ModPackListSource::setModTags(std::unordered_set const& set) {} diff --git a/loader/src/ui/mods/sources/ModSource.cpp b/loader/src/ui/mods/sources/ModSource.cpp index 3ec78f19..019de880 100644 --- a/loader/src/ui/mods/sources/ModSource.cpp +++ b/loader/src/ui/mods/sources/ModSource.cpp @@ -1,9 +1,39 @@ #include "ModSource.hpp" #include #include +#include + +LoadModSuggestionTask loadModSuggestion(LoadProblem const& problem) { + // Recommended / suggested are essentially the same thing for the purposes of this + if (problem.type == LoadProblem::Type::Recommendation || problem.type == LoadProblem::Type::Suggestion) { + auto suggestionID = problem.message.substr(0, problem.message.find(' ')); + auto suggestionVersionStr = problem.message.substr(problem.message.find(' ') + 1); + + if (auto suggestionVersionRes = ComparableVersionInfo::parse(suggestionVersionStr)) { + // todo: should this just always default to installing the latest available version? + auto suggestionVersion = suggestionVersionRes->getUnderlyingVersion(); + + if (auto mod = std::get_if(&problem.cause)) { + return server::getModVersion(suggestionID, suggestionVersion).map( + [mod = *mod](auto* result) -> LoadModSuggestionTask::Value { + if (result->isOk()) { + return ModSuggestion { + .suggestion = result->unwrap().metadata, + .forMod = mod, + }; + } + return std::nullopt; + } + ); + } + } + } + return LoadModSuggestionTask::immediate(std::nullopt); +} ModSource::ModSource(Mod* mod) : m_value(mod) {} ModSource::ModSource(server::ServerModMetadata&& metadata) : m_value(metadata) {} +ModSource::ModSource(ModSuggestion&& suggestion) : m_value(suggestion) {} std::string ModSource::getID() const { return std::visit(makeVisitor { @@ -11,9 +41,11 @@ std::string ModSource::getID() const { return mod->getID(); }, [](server::ServerModMetadata const& metadata) { - // Versions should be guaranteed to have at least one item return metadata.id; - } + }, + [](ModSuggestion const& suggestion) { + return suggestion.suggestion.getID(); + }, }, m_value); } ModMetadata ModSource::getMetadata() const { @@ -24,7 +56,10 @@ ModMetadata ModSource::getMetadata() const { [](server::ServerModMetadata const& metadata) { // Versions should be guaranteed to have at least one item return metadata.versions.front().metadata; - } + }, + [](ModSuggestion const& suggestion) { + return suggestion.suggestion; + }, }, m_value); } CCNode* ModSource::createModLogo() const { @@ -34,7 +69,10 @@ 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 { @@ -49,14 +87,17 @@ bool ModSource::wantsRestart() const { }, [](server::ServerModMetadata const& metdata) { return false; - } + }, + [](ModSuggestion const& suggestion) { + return false; + }, }, m_value); } std::optional ModSource::hasUpdates() const { return m_availableUpdate; } -ModSource ModSource::tryConvertToMod() const { +ModSource ModSource::convertForPopup() const { return std::visit(makeVisitor { [](Mod* mod) { return ModSource(mod); @@ -66,7 +107,10 @@ ModSource ModSource::tryConvertToMod() const { return ModSource(mod); } return ModSource(server::ServerModMetadata(metadata)); - } + }, + [](ModSuggestion const& suggestion) { + return ModSource(ModSuggestion(suggestion)); + }, }, m_value); } @@ -77,40 +121,35 @@ Mod* ModSource::asMod() const { server::ServerModMetadata const* ModSource::asServer() const { return std::get_if(&m_value); } +ModSuggestion const* ModSource::asSuggestion() const { + return std::get_if(&m_value); +} server::ServerRequest> ModSource::fetchAbout() const { - return std::visit(makeVisitor { - [](Mod* mod) { - return server::ServerRequest>::immediate(Ok(mod->getMetadata().getDetails())); - }, - [](server::ServerModMetadata const& metadata) { - return server::getMod(metadata.id).map( - [](auto* result) -> Result, server::ServerError> { - if (result->isOk()) { - return Ok(result->unwrap().about); - } - return Err(result->unwrapErr()); - } - ); + if (auto mod = this->asMod()) { + return server::ServerRequest>::immediate(Ok(mod->getMetadata().getDetails())); + } + return server::getMod(this->getID()).map( + [](auto* result) -> Result, server::ServerError> { + if (result->isOk()) { + return Ok(result->unwrap().about); + } + return Err(result->unwrapErr()); } - }, m_value); + ); } server::ServerRequest> ModSource::fetchChangelog() const { - return std::visit(makeVisitor { - [](Mod* mod) { - return server::ServerRequest>::immediate(Ok(mod->getMetadata().getChangelog())); - }, - [](server::ServerModMetadata const& metadata) { - return server::getMod(metadata.id).map( - [](auto* result) -> Result, server::ServerError> { - if (result->isOk()) { - return Ok(result->unwrap().changelog); - } - return Err(result->unwrapErr()); - } - ); + if (auto mod = this->asMod()) { + return server::ServerRequest>::immediate(Ok(mod->getMetadata().getChangelog())); + } + return server::getMod(this->getID()).map( + [](auto* result) -> Result, server::ServerError> { + if (result->isOk()) { + return Ok(result->unwrap().changelog); + } + return Err(result->unwrapErr()); } - }, m_value); + ); } server::ServerRequest ModSource::fetchServerInfo() const { // Request the info even if this is already a server mod because this might @@ -144,7 +183,11 @@ 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() { @@ -164,6 +207,10 @@ server::ServerRequest> ModSource::checkUp [](server::ServerModMetadata const& metadata) { // 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 51ebd99d..f0c02fa3 100644 --- a/loader/src/ui/mods/sources/ModSource.hpp +++ b/loader/src/ui/mods/sources/ModSource.hpp @@ -5,15 +5,25 @@ using namespace geode::prelude; +struct ModSuggestion final { + ModMetadata suggestion; + Mod* forMod; +}; + +// you can't put these in ModSuggestion itself because of the concepts in Task :sob: +using LoadModSuggestionTask = Task, server::ServerProgress>; +LoadModSuggestionTask loadModSuggestion(LoadProblem const& problem); + class ModSource final { private: - std::variant m_value; + std::variant m_value; std::optional m_availableUpdate; public: ModSource() = default; ModSource(Mod* mod); ModSource(server::ServerModMetadata&& metadata); + ModSource(ModSuggestion&& suggestion); std::string getID() const; ModMetadata getMetadata() const; @@ -28,10 +38,11 @@ public: // Returns a new ModSource that is either a copy of the current source or // an installed version of a server mod - ModSource tryConvertToMod() const; + ModSource convertForPopup() const; Mod* asMod() const; server::ServerModMetadata const* asServer() const; + ModSuggestion const* asSuggestion() const; server::ServerRequest fetchServerInfo() const; server::ServerRequest> fetchAbout() const; diff --git a/loader/src/ui/mods/sources/ServerModListSource.cpp b/loader/src/ui/mods/sources/ServerModListSource.cpp new file mode 100644 index 00000000..e737342e --- /dev/null +++ b/loader/src/ui/mods/sources/ServerModListSource.cpp @@ -0,0 +1,99 @@ +#include "ModListSource.hpp" + +void ServerModListSource::resetQuery() { + switch (m_type) { + case ServerModListType::Download: { + m_query = server::ModsQuery {}; + } break; + + case ServerModListType::Featured: { + m_query = server::ModsQuery { + .featured = true, + }; + } break; + + case ServerModListType::Trending: { + m_query = server::ModsQuery { + .sorting = server::ModsSort::RecentlyUpdated, + }; + } break; + + case ServerModListType::Recent: { + m_query = server::ModsQuery { + .sorting = server::ModsSort::RecentlyPublished, + }; + } break; + } +} + +ServerModListSource::ProviderTask ServerModListSource::fetchPage(size_t page, size_t pageSize, bool forceUpdate) { + m_query.page = page; + m_query.pageSize = pageSize; + return server::getMods(m_query, !forceUpdate).map( + [](Result* result) -> ProviderTask::Value { + if (result->isOk()) { + auto list = result->unwrap(); + auto content = ModListSource::ProvidedMods(); + for (auto&& mod : std::move(list.mods)) { + content.mods.push_back(ModSource(std::move(mod))); + } + content.totalModCount = list.totalModCount; + return Ok(content); + } + return Err(LoadPageError("Error loading mods", result->unwrapErr().details)); + }, + [](auto* prog) { + return prog->percentage; + } + ); +} + +ServerModListSource::ServerModListSource(ServerModListType type) + : m_type(type) +{ + this->resetQuery(); +} + +ServerModListSource* ServerModListSource::get(ServerModListType type) { + switch (type) { + default: + case ServerModListType::Download: { + static auto inst = new ServerModListSource(ServerModListType::Download); + return inst; + } break; + + case ServerModListType::Featured: { + static auto inst = new ServerModListSource(ServerModListType::Featured); + return inst; + } break; + + case ServerModListType::Trending: { + static auto inst = new ServerModListSource(ServerModListType::Trending); + return inst; + } break; + + case ServerModListType::Recent: { + static auto inst = new ServerModListSource(ServerModListType::Recent); + return inst; + } break; + } +} + +void ServerModListSource::setSearchQuery(std::string const& query) { + m_query.query = query.size() ? std::optional(query) : std::nullopt; +} + +std::unordered_set ServerModListSource::getModTags() const { + return m_query.tags; +} +void ServerModListSource::setModTags(std::unordered_set const& tags) { + m_query.tags = tags; + this->clearCache(); +} + +server::ModsQuery const& ServerModListSource::getQuery() const { + return m_query; +} +InvalidateQueryAfter ServerModListSource::getQueryMut() { + return InvalidateQueryAfter(m_query, this); +} diff --git a/loader/src/ui/mods/sources/SuggestedModListSource.cpp b/loader/src/ui/mods/sources/SuggestedModListSource.cpp new file mode 100644 index 00000000..49f3bca0 --- /dev/null +++ b/loader/src/ui/mods/sources/SuggestedModListSource.cpp @@ -0,0 +1,57 @@ +#include "ModListSource.hpp" + +bool SuggestedModsQuery::preCheck(ModSource const& src) const { + // There are no extra fields in SuggestedModsQuery + return true; +} +bool SuggestedModsQuery::queryCheck(ModSource const& src, double& weighted) const { + if (!modFuzzyMatch(src.asSuggestion()->suggestion, *query, weighted)) { + return false; + } + return modFuzzyMatch(src.asSuggestion()->forMod->getMetadata(), *query, weighted); +} + +void SuggestedModListSource::resetQuery() { + m_query = SuggestedModsQuery(); +} +SuggestedModListSource::ProviderTask SuggestedModListSource::fetchPage(size_t page, size_t pageSize, bool forceUpdate) { + m_query.page = page; + m_query.pageSize = pageSize; + std::vector tasks; + for (auto problem : Loader::get()->getRecommendations()) { + tasks.push_back(loadModSuggestion(problem)); + } + return LoadModSuggestionTask::all(std::move(tasks)).map( + [query = m_query](auto* results) -> ProviderTask::Value { + auto content = ProvidedMods(); + for (auto& result : *results) { + if (result && *result) { + content.mods.push_back(ModSource(ModSuggestion(**result))); + } + } + // Filter the results based on the current search + // query and return them + filterModsWithLocalQuery(content, query); + return Ok(std::move(content)); + }, + [](auto*) -> ProviderTask::Progress { return std::nullopt; } + ); +} + +SuggestedModListSource::SuggestedModListSource() {} + +SuggestedModListSource* SuggestedModListSource::get() { + static auto inst = new SuggestedModListSource(); + return inst; +} + +void SuggestedModListSource::setSearchQuery(std::string const& query) { + m_query.query = query.size() ? std::optional(query) : std::nullopt; +} +std::unordered_set SuggestedModListSource::getModTags() const { + return m_query.tags; +} +void SuggestedModListSource::setModTags(std::unordered_set const& tags) { + m_query.tags = tags; + this->clearCache(); +} diff --git a/loader/src/ui/other/FixIssuesPopup.cpp b/loader/src/ui/other/FixIssuesPopup.cpp new file mode 100644 index 00000000..35e7cb67 --- /dev/null +++ b/loader/src/ui/other/FixIssuesPopup.cpp @@ -0,0 +1,28 @@ +#include "FixIssuesPopup.hpp" +#include + +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 new file mode 100644 index 00000000..8e0bae3a --- /dev/null +++ b/loader/src/ui/other/FixIssuesPopup.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#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); diff --git a/loader/src/utils/VersionInfo.cpp b/loader/src/utils/VersionInfo.cpp index 40a28e89..308e450c 100644 --- a/loader/src/utils/VersionInfo.cpp +++ b/loader/src/utils/VersionInfo.cpp @@ -114,14 +114,20 @@ Result VersionInfo::parse(std::string const& string) { } std::string VersionInfo::toString(bool includeTag) const { + return this->toVString(); +} +std::string VersionInfo::toVString(bool includeTag) const { + return fmt::format("v{}", this->toNonVString(includeTag)); +} +std::string VersionInfo::toNonVString(bool includeTag) const { if (includeTag && m_tag) { return fmt::format( - "v{}.{}.{}{}", + "{}.{}.{}{}", m_major, m_minor, m_patch, m_tag.value().toSuffixString() ); } - return fmt::format("v{}.{}.{}", m_major, m_minor, m_patch); + return fmt::format("{}.{}.{}", m_major, m_minor, m_patch); } std::string geode::format_as(VersionInfo const& version) {