recommended mods list

This commit is contained in:
HJfod 2024-04-25 17:50:00 +03:00
parent 8a7a7a40cc
commit beeb7ca1f8
36 changed files with 1203 additions and 425 deletions

View file

@ -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

View file

@ -91,7 +91,9 @@ namespace geode {
bool isModLoaded(std::string const& id) const;
Mod* getLoadedMod(std::string const& id) const;
std::vector<Mod*> getAllMods();
std::vector<LoadProblem> getAllProblems() const;
std::vector<LoadProblem> getProblems() const;
std::vector<LoadProblem> getRecommendations() const;
/**
* Returns the available launch argument names.

View file

@ -435,6 +435,9 @@ namespace geode {
void setLoggingEnabled(bool enabled);
bool hasProblems() const;
std::vector<LoadProblem> getAllProblems() const;
std::vector<LoadProblem> getProblems() const;
std::vector<LoadProblem> getRecommendations() const;
bool shouldLoad() const;
bool isCurrentlyLoading() const;

View file

@ -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<T> m_resultValue;
bool m_finalEventPosted = false;
std::unique_ptr<void, void(*)(void*)> m_mapListener = { nullptr, +[](void*) {} };
std::string m_name;
std::unique_ptr<ExtraData> 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<std::recursive_mutex> lock(handle->m_mutex);
handle->m_finalEventPosted = true;
@ -155,11 +189,16 @@ namespace geode {
});
}
}
static void cancel(std::shared_ptr<Handle> handle) {
static void cancel(std::shared_ptr<Handle> handle, bool shallow = false) {
if (!handle) return;
std::unique_lock<std::recursive_mutex> 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<std::recursive_mutex> 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 <std::move_constructible NP>
static Task<std::vector<T*>, std::monostate> all(std::vector<Task<T, NP>>&& tasks, std::string const& name = "<Multiple Tasks>") {
using AllTask = Task<std::vector<T*>, std::monostate>;
template <class ResultMapper, class ProgressMapper>
auto map(ResultMapper&& resultMapper, ProgressMapper&& progressMapper, std::string const& name = "<Mapping Task>") {
// 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<T*> taskResults;
std::vector<Task<std::monostate>> taskListeners;
size_t taskCount;
};
task.m_handle->m_extraData = std::make_unique<AllTask::Handle::ExtraData>(
// Create the data
static_cast<void*>(new Waiting()),
// When the task is destroyed
+[](void* ptr) {
delete static_cast<Waiting*>(ptr);
},
// If the task is cancelled
+[](void* ptr) {
// The move clears the `taskListeners` vector (important!)
for (auto task : std::move(static_cast<Waiting*>(ptr)->taskListeners)) {
task.cancel();
}
}
);
// Store the task count in case some tasks finish immediately during the loop
static_cast<Waiting*>(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<Waiting*>(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<Waiting*>(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 <class ResultMapper, class ProgressMapper, class OnCancelled>
auto map(ResultMapper&& resultMapper, ProgressMapper&& progressMapper, OnCancelled&& onCancelled, std::string const& name = "<Mapping Task>") const {
using T2 = decltype(resultMapper(std::declval<T*>()));
using P2 = decltype(progressMapper(std::declval<P*>()));
static_assert(std::is_move_constructible_v<T2>, "The type being mapped to must be move-constructible!");
static_assert(std::is_move_constructible_v<P2>, "The type being mapped to must be move-constructible!");
auto task = Task<T2, P2>(Task<T2, P2>::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<T2, P2>::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<void, void(*)(void*)>(
task.m_handle->m_extraData = std::make_unique<Task<T2, P2>::Handle::ExtraData>(
static_cast<void*>(new EventListener<Task>(
[
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<T2, P2>::finish(handle.lock(), std::move(resultMapper(v)));
}
@ -316,6 +451,7 @@ namespace geode {
Task<T2, P2>::progress(handle.lock(), std::move(progressMapper(p)));
}
else if (event->isCancelled()) {
onCancelled();
Task<T2, P2>::cancel(handle.lock());
}
},
@ -323,15 +459,24 @@ namespace geode {
)),
+[](void* ptr) {
delete static_cast<EventListener<Task>*>(ptr);
},
+[](void* ptr) {
// Cancel the mapped task too
static_cast<EventListener<Task>*>(ptr)->getFilter().cancel();
}
);
}
return task;
}
template <class ResultMapper, class ProgressMapper>
auto map(ResultMapper&& resultMapper, ProgressMapper&& progressMapper, std::string const& name = "<Mapping Task>") const {
return this->map(std::move(resultMapper), std::move(progressMapper), +[]() {}, name);
}
template <class ResultMapper>
requires std::copy_constructible<P>
auto map(ResultMapper&& resultMapper, std::string const& name = "<Mapping Task>") {
auto map(ResultMapper&& resultMapper, std::string const& name = "<Mapping Task>") const {
return this->map(std::move(resultMapper), +[](P* p) -> P { return *p; }, name);
}

View file

@ -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);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -14,6 +14,7 @@
#include <loader/updater.hpp>
#include <Geode/binding/ButtonSprite.hpp>
#include <Geode/modify/LevelSelectLayer.hpp>
#include <ui/other/FixIssuesPopup.hpp>
using namespace geode::prelude;
@ -82,17 +83,8 @@ struct CustomMenuLayer : Modify<CustomMenuLayer, MenuLayer> {
}
// 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) {

View file

@ -65,9 +65,40 @@ std::vector<Mod*> Loader::getAllMods() {
return m_impl->getAllMods();
}
std::vector<LoadProblem> Loader::getProblems() const {
std::vector<LoadProblem> Loader::getAllProblems() const {
return m_impl->getProblems();
}
std::vector<LoadProblem> Loader::getProblems() const {
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;
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;
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));

View file

@ -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<bool>(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<bool>(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()) {

View file

@ -248,11 +248,30 @@ bool Mod::hasSavedValue(std::string_view const key) {
bool Mod::hasProblems() const {
return m_impl->hasProblems();
}
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> 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();
}
}

View file

@ -678,7 +678,7 @@ std::vector<std::pair<std::string, std::optional<std::string>*>> ModMetadata::ge
ModMetadata::ModMetadata() : m_impl(std::make_unique<Impl>()) {}
ModMetadata::ModMetadata(std::string id) : m_impl(std::make_unique<Impl>()) { m_impl->m_id = std::move(id); }
ModMetadata::ModMetadata(ModMetadata const& other) : m_impl(std::make_unique<Impl>(*other.m_impl)) {}
ModMetadata::ModMetadata(ModMetadata const& other) : m_impl(other.m_impl ? std::make_unique<Impl>(*other.m_impl) : std::make_unique<Impl>()) {}
ModMetadata::ModMetadata(ModMetadata&& other) noexcept : m_impl(std::move(other.m_impl)) {}
ModMetadata& ModMetadata::operator=(ModMetadata const& other) {

View file

@ -598,7 +598,8 @@ ServerRequest<ServerModVersion> 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<ServerModVersion, ServerError> {
if (response->ok()) {

View file

@ -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) {

View file

@ -6,17 +6,30 @@
using namespace geode::prelude;
enum class GeodePopupStyle {
Default,
Alt,
Alt2,
};
template <class... Args>
class GeodePopup : public Popup<Args...> {
protected:
bool init(float width, float height, Args... args, bool altBG = false) {
if (!Popup<Args...>::initAnchored(width, height, std::forward<Args>(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<Args...>::initAnchored(width, height, std::forward<Args>(args)..., bg))
return false;
this->setCloseButtonSpr(
CircleButtonSprite::createWithSpriteFrameName(
"close.png"_spr, .85f,
(altBG ? CircleBaseColor::DarkAqua : CircleBaseColor::DarkPurple)
(style == GeodePopupStyle::Default ? CircleBaseColor::DarkPurple : CircleBaseColor::DarkAqua)
)
);

View file

@ -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<std::tuple<const char*, const char*, ModListSource*>> {
{ "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*) {

View file

@ -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::ServerRequest<std::optional<server
void ModItem::onView(CCObject*) {
// Always open up the popup for the installed mod page if that is possible
ModPopup::create(m_source.tryConvertToMod())->show();
ModPopup::create(m_source.convertForPopup())->show();
}
void ModItem::onViewError(CCObject*) {
if (auto mod = m_source.asMod()) {
std::vector<std::string> problems;
for (auto problem : mod->getProblems()) {
problems.push_back(fmt::format("{} (code {})", problem.message, static_cast<int>(problem.type)));
}
FLAlertLayer::create(
fmt::format("Errors with {}", mod->getName()).c_str(),
ranges::join(problems, "\n"),
"OK"
)->show();
}
}
void ModItem::onEnable(CCObject*) {

View file

@ -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:

View file

@ -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 <cy>errors</c> 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<InstalledModListSource*>(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<InstalledModListSource*>(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<InstalledModListSource*>(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<InstalledModListSource*>(m_source)) {
auto mut = src->getQueryMut();
mut->type = mut->type == InstalledModListType::OnlyErrors ?
InstalledModListType::All :
InstalledModListType::OnlyErrors;
}
}

View file

@ -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<InvalidateCacheFilter> m_invalidateCacheListener;
@ -59,6 +61,7 @@ protected:
void onSort(CCObject*);
void onClearFilters(CCObject*);
void onToggleUpdates(CCObject*);
void onToggleErrors(CCObject*);
void onUpdateAll(CCObject*);
public:

View file

@ -0,0 +1,97 @@
#include "ModProblemItem.hpp"
#include <Geode/utils/ColorProvider.hpp>
#include <Geode/ui/GeodeUI.hpp>
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();
}

View file

@ -0,0 +1,25 @@
#pragma once
#include <Geode/DefaultInclude.hpp>
#include <Geode/loader/Loader.hpp>
#include <server/Server.hpp>
#include "../GeodeStyle.hpp"
using namespace geode::prelude;
using VersionDownload = server::ServerRequest<server::ServerModVersion>;
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);
};

View file

@ -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;
}

View file

@ -0,0 +1,16 @@
#pragma once
#include <Geode/DefaultInclude.hpp>
#include <Geode/ui/ScrollLayer.hpp>
using namespace geode::prelude;
class ModProblemItemList : public CCNode {
protected:
ScrollLayer* m_scrollLayer;
bool init(float height);
public:
static ModProblemItemList* create(float height);
};

View file

@ -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;
}

View file

@ -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::optional<server::ServerModUpdate>>;
std::vector<UpdateTask> 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<std::string> InstalledModListSource::getModTags() const {
return m_query.tags;
}
void InstalledModListSource::setModTags(std::unordered_set<std::string> const& tags) {
m_query.tags = tags;
this->clearCache();
}
InstalledModsQuery const& InstalledModListSource::getQuery() const {
return m_query;
}
InvalidateQueryAfter<InstalledModsQuery> InstalledModListSource::getQueryMut() {
return InvalidateQueryAfter(m_query, this);
}

View file

@ -5,95 +5,13 @@
#include <Geode/external/fts/fts_fuzzy_match.h>
static constexpr size_t PER_PAGE = 10;
static std::vector<ModListSource*> 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<std::pair<Mod*, double>> 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<Callback> 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<Task<std::monostate>> waitingTasks;
};
auto waiting = std::make_shared<Waiting>();
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<std::string> InstalledModListSource::getModTags() const {
return m_query.tags;
}
void InstalledModListSource::setModTags(std::unordered_set<std::string> const& tags) {
m_query.tags = tags;
this->clearCache();
}
InstalledModsQuery const& InstalledModListSource::getQuery() const {
return m_query;
}
InvalidateQueryAfter<InstalledModsQuery> 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<server::ServerModsList, server::ServerError>* 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<std::string> ServerModListSource::getModTags() const {
return m_query.tags;
}
void ServerModListSource::setModTags(std::unordered_set<std::string> const& tags) {
m_query.tags = tags;
this->clearCache();
}
server::ModsQuery const& ServerModListSource::getQuery() const {
return m_query;
}
InvalidateQueryAfter<server::ModsQuery> 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<std::string> ModPackListSource::getModTags() const {
return {};
}
void ModPackListSource::setModTags(std::unordered_set<std::string> 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;
}

View file

@ -26,14 +26,6 @@ public:
InvalidateCacheFilter(ModListSource* src);
};
struct InstalledModsQuery final {
std::optional<std::string> query;
bool onlyUpdates = false;
std::unordered_set<std::string> 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<size_t> getPageCount() const;
std::optional<size_t> getItemCount() const;
static void clearAllCaches();
static bool isRestartRequired();
};
template <class T>
@ -99,19 +94,37 @@ public:
}
};
struct LocalModsQueryBase {
std::optional<std::string> query;
std::unordered_set<std::string> 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<std::string> getModTags() const override;
void setModTags(std::unordered_set<std::string> const& tags) override;
@ -120,6 +133,28 @@ public:
InvalidateQueryAfter<InstalledModsQuery> 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<std::string> getModTags() const override;
void setModTags(std::unordered_set<std::string> const& tags) override;
};
enum class ServerModListType {
Download,
Featured,
@ -163,5 +198,58 @@ public:
void setModTags(std::unordered_set<std::string> 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 <std::derived_from<LocalModsQueryBase> Query>
void filterModsWithLocalQuery(ModListSource::ProvidedMods& mods, Query const& query) {
std::vector<std::pair<ModSource, double>> 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();
}

View file

@ -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<std::string> ModPackListSource::getModTags() const {
return {};
}
void ModPackListSource::setModTags(std::unordered_set<std::string> const& set) {}

View file

@ -1,9 +1,39 @@
#include "ModSource.hpp"
#include <Geode/ui/GeodeUI.hpp>
#include <server/DownloadManager.hpp>
#include <Geode/binding/GameObject.hpp>
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<Mod*>(&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<server::ServerModUpdate> 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<server::ServerModMetadata>(&m_value);
}
ModSuggestion const* ModSource::asSuggestion() const {
return std::get_if<ModSuggestion>(&m_value);
}
server::ServerRequest<std::optional<std::string>> ModSource::fetchAbout() const {
return std::visit(makeVisitor {
[](Mod* mod) {
return server::ServerRequest<std::optional<std::string>>::immediate(Ok(mod->getMetadata().getDetails()));
},
[](server::ServerModMetadata const& metadata) {
return server::getMod(metadata.id).map(
[](auto* result) -> Result<std::optional<std::string>, server::ServerError> {
if (result->isOk()) {
return Ok(result->unwrap().about);
}
return Err(result->unwrapErr());
}
);
if (auto mod = this->asMod()) {
return server::ServerRequest<std::optional<std::string>>::immediate(Ok(mod->getMetadata().getDetails()));
}
return server::getMod(this->getID()).map(
[](auto* result) -> Result<std::optional<std::string>, server::ServerError> {
if (result->isOk()) {
return Ok(result->unwrap().about);
}
return Err(result->unwrapErr());
}
}, m_value);
);
}
server::ServerRequest<std::optional<std::string>> ModSource::fetchChangelog() const {
return std::visit(makeVisitor {
[](Mod* mod) {
return server::ServerRequest<std::optional<std::string>>::immediate(Ok(mod->getMetadata().getChangelog()));
},
[](server::ServerModMetadata const& metadata) {
return server::getMod(metadata.id).map(
[](auto* result) -> Result<std::optional<std::string>, server::ServerError> {
if (result->isOk()) {
return Ok(result->unwrap().changelog);
}
return Err(result->unwrapErr());
}
);
if (auto mod = this->asMod()) {
return server::ServerRequest<std::optional<std::string>>::immediate(Ok(mod->getMetadata().getChangelog()));
}
return server::getMod(this->getID()).map(
[](auto* result) -> Result<std::optional<std::string>, server::ServerError> {
if (result->isOk()) {
return Ok(result->unwrap().changelog);
}
return Err(result->unwrapErr());
}
}, m_value);
);
}
server::ServerRequest<server::ServerModMetadata> ModSource::fetchServerInfo() const {
// Request the info even if this is already a server mod because this might
@ -144,7 +183,11 @@ server::ServerRequest<std::unordered_set<std::string>> 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<std::unordered_set<std::string>>::immediate(Ok(metadata.tags));
}
},
[](ModSuggestion const& suggestion) {
// Suggestions are also guaranteed to be valid since they come from the server
return server::ServerRequest<std::unordered_set<std::string>>::immediate(Ok(suggestion.suggestion.getTags()));
},
}, m_value);
}
server::ServerRequest<std::optional<server::ServerModUpdate>> ModSource::checkUpdates() {
@ -164,6 +207,10 @@ server::ServerRequest<std::optional<server::ServerModUpdate>> ModSource::checkUp
[](server::ServerModMetadata const& metadata) {
// Server mods aren't installed so you can't install updates for them
return server::ServerRequest<std::optional<server::ServerModUpdate>>::immediate(Ok(std::nullopt));
}
},
[](ModSuggestion const& suggestion) {
// Suggestions also aren't installed so you can't install updates for them
return server::ServerRequest<std::optional<server::ServerModUpdate>>::immediate(Ok(std::nullopt));
},
}, m_value);
}

View file

@ -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<std::optional<ModSuggestion>, server::ServerProgress>;
LoadModSuggestionTask loadModSuggestion(LoadProblem const& problem);
class ModSource final {
private:
std::variant<Mod*, server::ServerModMetadata> m_value;
std::variant<Mod*, server::ServerModMetadata, ModSuggestion> m_value;
std::optional<server::ServerModUpdate> 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<server::ServerModMetadata> fetchServerInfo() const;
server::ServerRequest<std::optional<std::string>> fetchAbout() const;

View file

@ -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<server::ServerModsList, server::ServerError>* 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<std::string> ServerModListSource::getModTags() const {
return m_query.tags;
}
void ServerModListSource::setModTags(std::unordered_set<std::string> const& tags) {
m_query.tags = tags;
this->clearCache();
}
server::ModsQuery const& ServerModListSource::getQuery() const {
return m_query;
}
InvalidateQueryAfter<server::ModsQuery> ServerModListSource::getQueryMut() {
return InvalidateQueryAfter(m_query, this);
}

View file

@ -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<LoadModSuggestionTask> 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<std::string> SuggestedModListSource::getModTags() const {
return m_query.tags;
}
void SuggestedModListSource::setModTags(std::unordered_set<std::string> const& tags) {
m_query.tags = tags;
this->clearCache();
}

View file

@ -0,0 +1,28 @@
#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();
}
}

View file

@ -0,0 +1,19 @@
#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);

View file

@ -114,14 +114,20 @@ Result<VersionInfo> 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) {