diff --git a/loader/include/Geode/loader/Index.hpp b/loader/include/Geode/loader/Index.hpp index 79249af2..d3db061a 100644 --- a/loader/include/Geode/loader/Index.hpp +++ b/loader/include/Geode/loader/Index.hpp @@ -243,6 +243,12 @@ namespace geode { * Check if any of the mods on the index have updates available */ bool areUpdatesAvailable() const; + /** + * Checks if the mod and its required dependencies can be installed + * @param item Item to get the list for + * @returns Success if the mod and its required dependencies can be installed, an error otherwise + */ + Result<> canInstall(IndexItemHandle item) const; /** * Get the list of items needed to install this item (dependencies, etc.) * @param item Item to get the list for diff --git a/loader/include/Geode/ui/Popup.hpp b/loader/include/Geode/ui/Popup.hpp index b0e4544a..6a0eb586 100644 --- a/loader/include/Geode/ui/Popup.hpp +++ b/loader/include/Geode/ui/Popup.hpp @@ -96,4 +96,14 @@ namespace geode { char const* title, std::string const& content, char const* btn1, char const* btn2, float width, utils::MiniFunction selected, bool doShow = true ); + + GEODE_DLL FLAlertLayer* createQuickPopup( + char const* title, std::string const& content, char const* btn1, char const* btn2, + utils::MiniFunction selected, bool doShow, bool cancelledByEscape + ); + + GEODE_DLL FLAlertLayer* createQuickPopup( + char const* title, std::string const& content, char const* btn1, char const* btn2, + float width, utils::MiniFunction selected, bool doShow, bool cancelledByEscape + ); } diff --git a/loader/src/loader/Index.cpp b/loader/src/loader/Index.cpp index bf373fc6..1f61124d 100644 --- a/loader/src/loader/Index.cpp +++ b/loader/src/loader/Index.cpp @@ -547,19 +547,18 @@ bool Index::areUpdatesAvailable() const { // Item installation -Result Index::getInstallList(IndexItemHandle item) const { +Result<> Index::canInstall(IndexItemHandle item) const { if (!item->getAvailablePlatforms().count(GEODE_PLATFORM_TARGET)) { return Err("Mod is not available on {}", GEODE_PLATFORM_NAME); } - IndexInstallList list; - list.target = item; - // TODO: ui for picking recommended and suggested mods for (auto& dep : item->getMetadata().getDependencies()) { // if the dep is resolved, then all its dependencies must be installed // already in order for that to have happened if (dep.isResolved()) continue; + if (dep.importance != ModMetadata::Dependency::Importance::Required) continue; + // check if this dep is available in the index if (auto depItem = this->getItem(dep.id, dep.version)) { if (!depItem->getAvailablePlatforms().count(GEODE_PLATFORM_TARGET)) { @@ -569,12 +568,55 @@ Result Index::getInstallList(IndexItemHandle item) const { ); } // recursively add dependencies + GEODE_UNWRAP_INTO(auto deps, this->canInstall(depItem)); + } + // otherwise user must get this dependency manually from somewhere + else { + return Err( + "Dependency {} version {} not found in the index! Likely " + "reason is that the version of the dependency this mod " + "depends on is not available. Please let the developer " + "of the mod ({}) know!", + dep.id, dep.version.toString(), item->getMetadata().getDeveloper() + ); + } + } + + return Ok(); +} + +Result Index::getInstallList(IndexItemHandle item) const { + if (!item->getAvailablePlatforms().count(GEODE_PLATFORM_TARGET)) { + return Err("Mod is not available on {}", GEODE_PLATFORM_NAME); + } + + IndexInstallList list; + list.target = item; + for (auto& dep : item->getMetadata().getDependencies()) { + // if the dep is resolved, then all its dependencies must be installed + // already in order for that to have happened + if (dep.isResolved()) continue; + + if (dep.importance == ModMetadata::Dependency::Importance::Suggested) continue; + + // check if this dep is available in the index + if (auto depItem = this->getItem(dep.id, dep.version)) { + if (!depItem->getAvailablePlatforms().count(GEODE_PLATFORM_TARGET)) { + // it's fine to not install optional dependencies + if (dep.importance != ModMetadata::Dependency::Importance::Required) continue; + return Err( + "Dependency {} is not available on {}", + dep.id, GEODE_PLATFORM_NAME + ); + } + // recursively add dependencies GEODE_UNWRAP_INTO(auto deps, this->getInstallList(depItem)); ranges::push(list.list, deps.list); } // otherwise user must get this dependency manually from somewhere - // else else { + // it's fine to not install optional dependencies + if (dep.importance != ModMetadata::Dependency::Importance::Required) continue; return Err( "Dependency {} version {} not found in the index! Likely " "reason is that the version of the dependency this mod " diff --git a/loader/src/ui/internal/info/ModInfoPopup.cpp b/loader/src/ui/internal/info/ModInfoPopup.cpp index f9a69f04..0e08853b 100644 --- a/loader/src/ui/internal/info/ModInfoPopup.cpp +++ b/loader/src/ui/internal/info/ModInfoPopup.cpp @@ -750,20 +750,47 @@ void IndexItemInfoPopup::onInstallProgress(ModInstallEvent* event) { } void IndexItemInfoPopup::onInstall(CCObject*) { - InstallListPopup::create(m_item, [&](IndexInstallList const& list) { - if (m_latestVersionLabel) { - m_latestVersionLabel->setVisible(false); - } - this->setInstallStatus(UpdateProgress(0, "Starting install")); + createQuickPopup( + "Confirm Install", + "Installing this mod requires a few other mods to be installed. " + "Would you like to continue with recommended settings or " + "customize which mods to install?", + "Recommended", "Customize", 320.f, + [&](FLAlertLayer*, bool btn2) { + if (!btn2) { + auto canInstall = Index::get()->canInstall(m_item); + if (!canInstall) { + FLAlertLayer::create( + "Unable to Install", + canInstall.unwrapErr(), + "OK" + )->show(); + return; + } + this->preInstall(); + Index::get()->install(m_item); + } + else { + InstallListPopup::create(m_item, [&](IndexInstallList const& list) { + this->preInstall(); + Index::get()->install(list); + })->show(); + } + }, true, true + ); +} - m_installBtn->setTarget( - this, menu_selector(IndexItemInfoPopup::onCancel) - ); - m_installBtnSpr->setString("Cancel"); - m_installBtnSpr->setBG("GJ_button_06.png", false); +void IndexItemInfoPopup::preInstall() { + if (m_latestVersionLabel) { + m_latestVersionLabel->setVisible(false); + } + this->setInstallStatus(UpdateProgress(0, "Starting install")); - Index::get()->install(list); - })->show(); + m_installBtn->setTarget( + this, menu_selector(IndexItemInfoPopup::onCancel) + ); + m_installBtnSpr->setString("Cancel"); + m_installBtnSpr->setBG("GJ_button_06.png", false); } void IndexItemInfoPopup::onCancel(CCObject*) { diff --git a/loader/src/ui/internal/info/ModInfoPopup.hpp b/loader/src/ui/internal/info/ModInfoPopup.hpp index e5bd0aec..062230a0 100644 --- a/loader/src/ui/internal/info/ModInfoPopup.hpp +++ b/loader/src/ui/internal/info/ModInfoPopup.hpp @@ -100,6 +100,8 @@ protected: void onInstall(CCObject*); void onCancel(CCObject*); + void preInstall(); + CCNode* createLogo(CCSize const& size) override; ModMetadata getMetadata() const override; diff --git a/loader/src/ui/internal/list/InstallListCell.cpp b/loader/src/ui/internal/list/InstallListCell.cpp index 92803a61..26f3bddc 100644 --- a/loader/src/ui/internal/list/InstallListCell.cpp +++ b/loader/src/ui/internal/list/InstallListCell.cpp @@ -229,7 +229,7 @@ bool IndexItemInstallListCell::init( if (importance != ModMetadata::Dependency::Importance::Required) { message->setCString("N/A (Optional)"); - message->setColor({ 120, 15, 15 }); + message->setColor({ 163, 24, 24 }); } } @@ -297,7 +297,7 @@ bool UnknownInstallListCell::init( message->setColor({ 240, 31, 31 }); if (optional) { message->setCString("Missing (Optional)"); - message->setColor({ 120, 15, 15 }); + message->setColor({ 163, 24, 24 }); } this->addChild(message); return true; diff --git a/loader/src/ui/internal/list/ModListLayer.cpp b/loader/src/ui/internal/list/ModListLayer.cpp index 8bb873a3..b12a5bed 100644 --- a/loader/src/ui/internal/list/ModListLayer.cpp +++ b/loader/src/ui/internal/list/ModListLayer.cpp @@ -97,8 +97,7 @@ static std::optional queryMatch(ModListQuery const& query, Mod* mod) { } static std::optional queryMatch(ModListQuery const& query, IndexItemHandle item) { - // if no force visibility was provided and item is already installed, don't - // show it + // if no force visibility was provided and item is already installed, don't show it if (!query.forceVisibility && Loader::get()->isModInstalled(item->getMetadata().getID())) { return std::nullopt; } @@ -114,6 +113,16 @@ static std::optional queryMatch(ModListQuery const& query, IndexItemHandle })) { return std::nullopt; } + // if no force visibility was provided and item is already installed, don't show it + auto canInstall = Index::get()->canInstall(item); + if (!query.forceInvalid && !canInstall) { + log::warn( + "Removing {} from the list because it cannot be installed: {}", + item->getMetadata().getID(), + canInstall.unwrapErr() + ); + return std::nullopt; + } // otherwise match keywords if (auto match = queryMatchKeywords(query, item->getMetadata())) { auto weighted = match.value(); @@ -190,7 +199,8 @@ CCArray* ModListLayer::createModCells(ModListType type, ModListQuery const& quer // sort the mods by match score std::multimap sorted; - for (auto const& item : Index::get()->getItems()) { + auto index = Index::get(); + for (auto const& item : index->getItems()) { if (auto match = queryMatch(query, item)) { sorted.insert({ match.value(), item }); } diff --git a/loader/src/ui/internal/list/ModListLayer.hpp b/loader/src/ui/internal/list/ModListLayer.hpp index 16ca3ec7..648e8309 100644 --- a/loader/src/ui/internal/list/ModListLayer.hpp +++ b/loader/src/ui/internal/list/ModListLayer.hpp @@ -25,10 +25,15 @@ struct ModListQuery { */ std::optional keywords; /** - * Force mods to be shown on the list unless they explicitly mismatch some + * Force already installed mods to be shown on the list unless they explicitly mismatch some * tags (used to show installed mods on index) */ bool forceVisibility; + /** + * Force not installable mods to be shown on the list unless they explicitly mismatch some + * tags (used to show installed mods on index) + */ + bool forceInvalid; /** * Empty means current platform */ diff --git a/loader/src/ui/internal/list/SearchFilterPopup.cpp b/loader/src/ui/internal/list/SearchFilterPopup.cpp index e0927a38..61353a7e 100644 --- a/loader/src/ui/internal/list/SearchFilterPopup.cpp +++ b/loader/src/ui/internal/list/SearchFilterPopup.cpp @@ -5,7 +5,9 @@ #include #include -#include + +// re-add when we actually add the platforms +const float iosAndAndroidSize = 45.f; bool SearchFilterPopup::setup(ModListLayer* layer, ModListType type) { m_noElasticity = true; @@ -14,66 +16,77 @@ bool SearchFilterPopup::setup(ModListLayer* layer, ModListType type) { this->setTitle("Search Filters"); auto winSize = CCDirector::sharedDirector()->getWinSize(); - auto pos = CCPoint { winSize.width / 2 - 140.f, winSize.height / 2 + 45.f }; + auto pos = CCPoint { winSize.width / 2 - 140.f, winSize.height / 2 + 45.f - iosAndAndroidSize * 0.25f }; // platforms auto platformTitle = CCLabelBMFont::create("Platforms", "goldFont.fnt"); - platformTitle->setPosition(winSize.width / 2 - 85.f, winSize.height / 2 + 75.f); + platformTitle->setAnchorPoint({ 0.5f, 1.f }); + platformTitle->setPosition(winSize.width / 2 - 85.f, winSize.height / 2 + 81.5f - iosAndAndroidSize * 0.25f); platformTitle->setScale(.5f); m_mainLayer->addChild(platformTitle); auto platformBG = CCScale9Sprite::create("square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f }); platformBG->setColor({ 0, 0, 0 }); platformBG->setOpacity(90); - platformBG->setContentSize({ 290.f, 205.f }); - platformBG->setPosition(winSize.width / 2 - 85.f, winSize.height / 2 + 11.f); + platformBG->setContentSize({ 290.f, 205.f - iosAndAndroidSize * 2.f }); + platformBG->setAnchorPoint({ 0.5f, 1.f }); + platformBG->setPosition(winSize.width / 2 - 85.f, winSize.height / 2 + 62.25f - iosAndAndroidSize * 0.25f); platformBG->setScale(.5f); m_mainLayer->addChild(platformBG); this->enable(this->addPlatformToggle("Windows", PlatformID::Windows, pos), type); this->enable(this->addPlatformToggle("macOS", PlatformID::MacOS, pos), type); - this->enable(this->addPlatformToggle("IOS", PlatformID::iOS, pos), type); - this->enable(this->addPlatformToggle("Android", PlatformID::Android, pos), type); + //this->enable(this->addPlatformToggle("IOS", PlatformID::iOS, pos), type); + //this->enable(this->addPlatformToggle("Android", PlatformID::Android, pos), type); // show installed auto installedTitle = CCLabelBMFont::create("Other", "goldFont.fnt"); - installedTitle->setPosition(winSize.width / 2 - 85.f, winSize.height / 2 - 57.f); + installedTitle->setAnchorPoint({ 0.5f, 1.f }); + installedTitle->setPosition(winSize.width / 2 - 85.f, winSize.height / 2 - 50.5f + iosAndAndroidSize - iosAndAndroidSize * 0.25f); installedTitle->setScale(.5f); m_mainLayer->addChild(installedTitle); auto installedBG = CCScale9Sprite::create("square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f }); installedBG->setColor({ 0, 0, 0 }); installedBG->setOpacity(90); - installedBG->setContentSize({ 290.f, 65.f }); - installedBG->setPosition(winSize.width / 2 - 85.f, winSize.height / 2 - 85.f); + installedBG->setContentSize({ 290.f, 110.f }); + installedBG->setAnchorPoint({ 0.5f, 1.f }); + installedBG->setPosition(winSize.width / 2 - 85.f, winSize.height / 2 - 68.75f + iosAndAndroidSize - iosAndAndroidSize * 0.25f); installedBG->setScale(.5f); m_mainLayer->addChild(installedBG); - pos = CCPoint { winSize.width / 2 - 140.f, winSize.height / 2 - 85.f }; + pos = CCPoint { winSize.width / 2 - 140.f, winSize.height / 2 - 85.f + iosAndAndroidSize - iosAndAndroidSize * 0.25f }; this->addToggle( "Show Installed", menu_selector(SearchFilterPopup::onShowInstalled), m_modLayer->getQuery().forceVisibility, 0, pos ); + this->addToggle( + "Show Invalid", menu_selector(SearchFilterPopup::onShowInvalid), + m_modLayer->getQuery().forceInvalid, 1, pos + ); + // tags auto tagsTitle = CCLabelBMFont::create("Tags", "goldFont.fnt"); - tagsTitle->setPosition(winSize.width / 2 + 85.f, winSize.height / 2 + 75.f); + tagsTitle->setAnchorPoint({ 0.5f, 1.f }); + tagsTitle->setPosition(winSize.width / 2 + 85.f, winSize.height / 2 + 81.5f - iosAndAndroidSize * 0.25f); tagsTitle->setScale(.5f); m_mainLayer->addChild(tagsTitle); auto tagsBG = CCScale9Sprite::create("square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f }); tagsBG->setColor({ 0, 0, 0 }); tagsBG->setOpacity(90); - tagsBG->setContentSize({ 290.f, 328.f }); - tagsBG->setPosition(winSize.width / 2 + 85.f, winSize.height / 2 - 19.5f); + tagsBG->setContentSize({ 290.f, 328.f - iosAndAndroidSize }); + tagsBG->setAnchorPoint({ 0.5f, 1.f }); + tagsBG->setPosition(winSize.width / 2 + 85.f, winSize.height / 2 + 62.5f - iosAndAndroidSize * 0.25f); tagsBG->setScale(.5f); m_mainLayer->addChild(tagsBG); - pos = CCPoint { winSize.width / 2 + 30.f, winSize.height / 2 + 45.f }; + pos = CCPoint { winSize.width / 2 + 30.f, winSize.height / 2 + 45.f - iosAndAndroidSize * 0.25f }; for (auto& tag : Index::get()->getTags()) { auto toggle = CCMenuItemToggler::createWithStandardSprites( @@ -116,6 +129,11 @@ void SearchFilterPopup::onShowInstalled(CCObject* sender) { m_modLayer->getQuery().forceVisibility = !toggle->isToggled(); } +void SearchFilterPopup::onShowInvalid(CCObject* sender) { + auto toggle = static_cast(sender); + m_modLayer->getQuery().forceInvalid = !toggle->isToggled(); +} + void SearchFilterPopup::enable(CCMenuItemToggler* toggle, ModListType type) { if (type == ModListType::Installed) { toggle->setEnabled(false); @@ -162,7 +180,7 @@ void SearchFilterPopup::onClose(CCObject* sender) { SearchFilterPopup* SearchFilterPopup::create(ModListLayer* layer, ModListType type) { auto ret = new SearchFilterPopup(); - if (ret && ret->init(350.f, 240.f, layer, type)) { + if (ret && ret->init(350.f, 240.f - iosAndAndroidSize * 0.5f, layer, type)) { ret->autorelease(); return ret; } diff --git a/loader/src/ui/internal/list/SearchFilterPopup.hpp b/loader/src/ui/internal/list/SearchFilterPopup.hpp index 5038bf89..a02fc22d 100644 --- a/loader/src/ui/internal/list/SearchFilterPopup.hpp +++ b/loader/src/ui/internal/list/SearchFilterPopup.hpp @@ -19,6 +19,7 @@ protected: void onPlatformToggle(CCObject*); void onShowInstalled(CCObject*); + void onShowInvalid(CCObject*); void onTag(CCObject*); void enable(CCMenuItemToggler* toggle, ModListType type); diff --git a/loader/src/ui/nodes/Popup.cpp b/loader/src/ui/nodes/Popup.cpp index 2703241f..3e728663 100644 --- a/loader/src/ui/nodes/Popup.cpp +++ b/loader/src/ui/nodes/Popup.cpp @@ -5,8 +5,18 @@ using namespace geode::prelude; class QuickPopup : public FLAlertLayer, public FLAlertLayerProtocol { protected: MiniFunction m_selected; + bool m_cancelledByEscape; + bool m_usedEscape = false; + + void keyBackClicked() override { + m_usedEscape = true; + FLAlertLayer::keyBackClicked(); + } void FLAlert_Clicked(FLAlertLayer* layer, bool btn2) override { + if (m_cancelledByEscape && m_usedEscape) { + return; + } if (m_selected) { m_selected(layer, btn2); } @@ -15,10 +25,11 @@ protected: public: static QuickPopup* create( char const* title, std::string const& content, char const* btn1, char const* btn2, - float width, MiniFunction selected + float width, MiniFunction selected, bool cancelledByEscape ) { auto inst = new QuickPopup; inst->m_selected = selected; + inst->m_cancelledByEscape = cancelledByEscape; if (inst && inst->init(inst, title, content, btn1, btn2, width, false, .0f)) { inst->autorelease(); return inst; @@ -32,7 +43,7 @@ FLAlertLayer* geode::createQuickPopup( char const* title, std::string const& content, char const* btn1, char const* btn2, float width, MiniFunction selected, bool doShow ) { - auto ret = QuickPopup::create(title, content, btn1, btn2, width, selected); + auto ret = QuickPopup::create(title, content, btn1, btn2, width, selected, false); if (doShow) { ret->show(); } @@ -45,3 +56,21 @@ FLAlertLayer* geode::createQuickPopup( ) { return createQuickPopup(title, content, btn1, btn2, 350.f, selected, doShow); } + +FLAlertLayer* geode::createQuickPopup( + char const* title, std::string const& content, char const* btn1, char const* btn2, float width, + MiniFunction selected, bool doShow, bool cancelledByEscape +) { + auto ret = QuickPopup::create(title, content, btn1, btn2, width, selected, cancelledByEscape); + if (doShow) { + ret->show(); + } + return ret; +} + +FLAlertLayer* geode::createQuickPopup( + char const* title, std::string const& content, char const* btn1, char const* btn2, + MiniFunction selected, bool doShow, bool cancelledByEscape +) { + return createQuickPopup(title, content, btn1, btn2, 350.f, selected, doShow, cancelledByEscape); +}