diff --git a/loader/include/Geode/loader/Event.hpp b/loader/include/Geode/loader/Event.hpp index 75231573..1c48d268 100644 --- a/loader/include/Geode/loader/Event.hpp +++ b/loader/include/Geode/loader/Event.hpp @@ -93,19 +93,19 @@ namespace geode { }; class GEODE_DLL Event { + private: static std::unordered_set s_listeners; - Mod* m_sender; friend EventListenerProtocol; - public: - void postFrom(Mod* sender); + public: + Mod* sender; + + void postFrom(Mod* sender); template void post() { postFrom(Mod::get()); } - Mod* getSender(); - virtual ~Event(); }; } diff --git a/loader/include/Geode/loader/Index.hpp b/loader/include/Geode/loader/Index.hpp index e721767b..1a023bac 100644 --- a/loader/include/Geode/loader/Index.hpp +++ b/loader/include/Geode/loader/Index.hpp @@ -3,6 +3,7 @@ #include "Types.hpp" #include "ModInfo.hpp" #include "Event.hpp" +#include "../utils/Result.hpp" #include namespace geode { @@ -11,43 +12,12 @@ namespace geode { using UpdateError = std::string; using UpdateStatus = std::variant; - class GEODE_DLL IndexUpdateEvent : public Event { - protected: - std::string m_sourceRepository; - UpdateStatus m_status; - - public: - IndexUpdateEvent( - std::string const& src, - UpdateStatus status - ); - std::string getSource() const; - UpdateStatus getStatus() const; + struct GEODE_DLL ModInstallEvent : public Event { + const std::string modID; + const UpdateStatus status; }; - class GEODE_DLL IndexUpdateFilter : public EventFilter { - public: - using Callback = void(IndexUpdateEvent*); - - ListenerResult handle(std::function fn, IndexUpdateEvent* event); - IndexUpdateFilter(); - }; - - class GEODE_DLL ModInstallEvent : public Event { - protected: - std::string m_id; - UpdateStatus m_status; - - public: - ModInstallEvent( - std::string const& id, - UpdateStatus status - ); - std::string getModID() const; - UpdateStatus getStatus() const; - }; - - class GEODE_DLL ModInstallFilter : public EventFilter { + class GEODE_DLL ModInstallFilter : public EventFilter { protected: std::string m_id; @@ -58,6 +28,47 @@ namespace geode { ModInstallFilter(std::string const& id); }; + struct GEODE_DLL IndexUpdateEvent : public Event { + const UpdateStatus status; + IndexUpdateEvent(const UpdateStatus status); + }; + + class GEODE_DLL IndexUpdateFilter : public EventFilter { + public: + using Callback = void(IndexUpdateEvent*); + + ListenerResult handle(std::function fn, IndexUpdateEvent* event); + IndexUpdateFilter(); + }; + + namespace impl { + // The reason sources have private implementation events that are + // turned into the global IndexUpdateEvent is because it makes it much + // simpler to keep track of progress, what errors were received, etc. + // without having to store a ton of members + + struct GEODE_DLL IndexSource final { + std::string repository; + bool isUpToDate = false; + + std::string dirname() const; + }; + + struct GEODE_DLL SourceUpdateEvent : public Event { + const IndexSource& source; + const UpdateStatus status; + SourceUpdateEvent(IndexSource const& src, const UpdateStatus status); + }; + + class GEODE_DLL SourceUpdateFilter : public EventFilter { + public: + using Callback = void(SourceUpdateEvent*); + + ListenerResult handle(std::function fn, SourceUpdateEvent* event); + SourceUpdateFilter(); + }; + } + struct GEODE_DLL IndexItem { std::string sourceRepository; ghc::filesystem::path path; @@ -68,29 +79,34 @@ namespace geode { std::unordered_set platforms; } download; bool isFeatured; - }; - struct GEODE_DLL IndexSource final { - std::string repository; - bool isUpToDate = false; - - std::string dirname() const; + /** + * Create IndexItem from a directory + */ + static Result> createFromDir( + std::string const& sourceRepository, + ghc::filesystem::path const& dir + ); }; + using IndexItemHandle = std::shared_ptr; class GEODE_DLL Index final { protected: // for once, the fact that std::map is ordered is useful (this makes // getting the latest version of a mod as easy as items.rbegin()) - using ItemVersions = std::map; + using ItemVersions = std::map; - std::vector m_sources; + std::vector m_sources; + std::unordered_map m_sourceStatuses; + std::atomic m_triedToUpdate = false; std::unordered_map m_items; Index(); - void checkSourceUpdates(IndexSource& src); - void downloadSource(IndexSource& src); - void updateSourceFromLocal(IndexSource& src); + void onSourceUpdate(impl::SourceUpdateEvent* event); + void checkSourceUpdates(impl::IndexSource& src); + void downloadSource(impl::IndexSource& src); + void updateSourceFromLocal(impl::IndexSource& src); void cleanupItems(); public: @@ -98,12 +114,16 @@ namespace geode { void addSource(std::string const& repository); void removeSource(std::string const& repository); - std::vector getSources() const; + std::vector getSources() const; - std::vector getItems() const; + std::vector getItems() const; bool isKnownItem(std::string const& id, std::optional version) const; - std::optional getItem(std::string const& id, std::optional version) const; + IndexItemHandle getItem(std::string const& id, std::optional version) const; + IndexItemHandle getItem(ModInfo const& info) const; + IndexItemHandle getItem(Mod* mod) const; + bool updateAvailable(IndexItemHandle item) const; + bool hasTriedToUpdate() const; bool isUpToDate() const; void update(bool force = false); }; diff --git a/loader/include/Geode/ui/GeodeUI.hpp b/loader/include/Geode/ui/GeodeUI.hpp index e767d968..d347030a 100644 --- a/loader/include/Geode/ui/GeodeUI.hpp +++ b/loader/include/Geode/ui/GeodeUI.hpp @@ -1,6 +1,7 @@ #pragma once #include "../loader/Mod.hpp" +#include "../loader/Index.hpp" namespace geode { /** @@ -11,6 +12,10 @@ namespace geode { * Open the info popup for a mod */ GEODE_DLL void openInfoPopup(Mod* mod); + /** + * Open the issue report popup for a mod + */ + GEODE_DLL void openIssueReportPopup(Mod* mod); /** * Open the store page for a mod (if it exists) */ @@ -19,4 +24,25 @@ namespace geode { * Open the settings popup for a mod (if it has any settings) */ GEODE_DLL void openSettingsPopup(Mod* mod); + /** + * Create a default logo sprite + * @param size Size of the sprite + */ + GEODE_DLL cocos2d::CCNode* createDefaultLogo( + cocos2d::CCSize const& size + ); + /** + * Create a logo sprite for a mod + * @param size Size of the sprite + */ + GEODE_DLL cocos2d::CCNode* createModLogo( + Mod* mod, cocos2d::CCSize const& size + ); + /** + * Create a logo sprite for an index item + * @param size Size of the sprite + */ + GEODE_DLL cocos2d::CCNode* createIndexItemLogo( + IndexItemHandle item, cocos2d::CCSize const& size + ); } diff --git a/loader/include/Geode/utils/file.hpp b/loader/include/Geode/utils/file.hpp index 4ed20b8d..0a6592fe 100644 --- a/loader/include/Geode/utils/file.hpp +++ b/loader/include/Geode/utils/file.hpp @@ -3,6 +3,7 @@ #include "Result.hpp" #include "general.hpp" +#include "../external/json/json.hpp" #include #include #include @@ -10,6 +11,7 @@ namespace geode::utils::file { GEODE_DLL Result readString(ghc::filesystem::path const& path); + GEODE_DLL Result readJson(ghc::filesystem::path const& path); GEODE_DLL Result readBinary(ghc::filesystem::path const& path); GEODE_DLL Result<> writeString(ghc::filesystem::path const& path, std::string const& data); diff --git a/loader/include/Geode/utils/general.hpp b/loader/include/Geode/utils/general.hpp index 707a0f78..480a1a77 100644 --- a/loader/include/Geode/utils/general.hpp +++ b/loader/include/Geode/utils/general.hpp @@ -41,6 +41,16 @@ namespace geode { using TypeIdentityType = typename TypeIdentity::type; namespace utils { + // helper for std::visit + template struct makeVisitor : Ts... { using Ts::operator()...; }; + template makeVisitor(Ts...) -> makeVisitor; + + template + constexpr T getOr(std::variant const& variant, T const& defValue) { + return std::holds_alternative(variant) ? + std::get(variant) : defValue; + } + constexpr unsigned int hash(char const* str, int h = 0) { return !str[h] ? 5381 : (hash(str, h + 1) * 33) ^ str[h]; } diff --git a/loader/src/hooks/LevelSearchLayer.cpp b/loader/src/hooks/LevelSearchLayer.cpp deleted file mode 100644 index d54cf2ee..00000000 --- a/loader/src/hooks/LevelSearchLayer.cpp +++ /dev/null @@ -1,104 +0,0 @@ -#include "../ui/internal/info/ModInfoLayer.hpp" -#include "../ui/internal/list/ModListLayer.hpp" - -#include -#include -#include -#include - -USE_GEODE_NAMESPACE(); - -#pragma warning(disable : 4217) - -template -requires std::is_base_of_v -T* setIDSafe(CCNode* node, int index, char const* id) { - if constexpr (std::is_same_v) { - if (auto child = getChild(node, index)) { - child->setID(id); - return child; - } - } - else { - if (auto child = getChildOfType(node, index)) { - child->setID(id); - return child; - } - } - return nullptr; -} - -// clang-format off -#include -struct LevelSearchLayerIDs : Modify { - bool init() { - if (!LevelSearchLayer::init()) - return false; - - // set the funny ids - this->setID("creator-layer"); - setIDSafe(this, 0, "creator-layer-bg"); - getChildOfType(this, 0)->setID("search-bar"); - getChildOfType(this, 0)->setID("level-search-bg"); - getChildOfType(this, 1)->setID("level-search-bar-bg"); - getChildOfType(this, 2)->setID("quick-search-bg"); - getChildOfType(this, 3)->setID("difficulty-filters-bg"); - getChildOfType(this, 4)->setID("length-filters-bg"); - getChildOfType(this, 0)->setID("quick-search-title"); - getChildOfType(this, 1)->setID("filters-title"); - getChildOfType(this, 1)->setID("left-corner"); - getChildOfType(this, 2)->setID("right-corner"); - - if (auto filtermenu = getChildOfType(this, 0)) { - filtermenu->setID("other-filter-menu"); - setIDSafe(filtermenu, 0, "clear-filters-button"); - setIDSafe(filtermenu, 1, "advanced-filters-button"); - } - if (auto searchmenu = getChildOfType(this, 1)) { - searchmenu->setID("search-button-menu"); - setIDSafe(searchmenu, 0, "search-level-button"); - setIDSafe(searchmenu, 1, "search-user-button"); - - } - if (auto quickmenu = getChildOfType(this, 2)) { - quickmenu->setID("quick-search-menu"); - setIDSafe(quickmenu, 0, "most-downloaded-button"); - setIDSafe(quickmenu, 1, "most-liked-button"); - setIDSafe(quickmenu, 2, "trending-button"); - setIDSafe(quickmenu, 3, "recent-button"); - setIDSafe(quickmenu, 4, "magic-button"); - setIDSafe(quickmenu, 5, "awarded-button"); - setIDSafe(quickmenu, 6, "followed-button"); - setIDSafe(quickmenu, 7, "friends-button"); - } - if (auto filtersmenu = getChildOfType(this, 3)) { - filtersmenu->setID("difficulty-filter-menu"); - setIDSafe(filtersmenu, 0, "na-filter-button"); - setIDSafe(filtersmenu, 1, "easy-filter-button"); - setIDSafe(filtersmenu, 2, "normal-filter-button"); - setIDSafe(filtersmenu, 3, "hard-filter-button"); - setIDSafe(filtersmenu, 4, "harder-filter-button"); - setIDSafe(filtersmenu, 5, "insane-filter-button"); - setIDSafe(filtersmenu, 6, "demon-filter-button"); - setIDSafe(filtersmenu, 7, "auto-filter-button"); - setIDSafe(filtersmenu, 8, "demon-type-filter-button"); - } - if (auto filtersmenu = getChildOfType(this, 4)) { - filtersmenu->setID("length-filter-menu"); - setIDSafe(filtersmenu, 0, "clock-icon"); - setIDSafe(filtersmenu, 1, "tiny-filter-button"); - setIDSafe(filtersmenu, 2, "short-filter-button"); - setIDSafe(filtersmenu, 3, "medium-filter-button"); - setIDSafe(filtersmenu, 4, "long-filter-button"); - setIDSafe(filtersmenu, 5, "xl-filter-button"); - setIDSafe(filtersmenu, 6, "star-filter-button"); - } - if (auto backmenu = getChildOfType(this, 5)) { - backmenu->setID("exit-menu"); - setIDSafe(backmenu, 0, "exit-button"); - } - return true; - } -}; - -// clang-format on diff --git a/loader/src/hooks/LoadingLayer.cpp b/loader/src/hooks/LoadingLayer.cpp index 20257afa..65045549 100644 --- a/loader/src/hooks/LoadingLayer.cpp +++ b/loader/src/hooks/LoadingLayer.cpp @@ -44,39 +44,33 @@ struct CustomLoadingLayer : Modify { void setUpdateText(std::string const& text) { m_textArea->setString(text.c_str()); - // m_fields->m_updatingResources->setString(text.c_str()); - // m_fields->m_updatingResourcesBG->setContentSize({ - // m_fields->m_updatingResources->getScaledContentSize().width + 30.f, - // 50.f - // }); - // m_fields->m_updatingResources->setPosition( - // m_fields->m_updatingResourcesBG->getContentSize() / 2 - // ); } void updateResourcesProgress(ResourceDownloadEvent* event) { - auto status = event->getStatus(); - if (std::holds_alternative(status)) { - auto prog = std::get(status); - this->setUpdateText("Downloading Resources: " + std::to_string(prog.first) + "%"); - } - else if (std::holds_alternative(status)) { - this->setUpdateText("Resources Downloaded"); - m_fields->m_updatingResources = false; - this->loadAssets(); - } - else { - InternalLoader::platformMessageBox( - "Error updating resources", - "Unable to update Geode resources: " + - std::get(status) + ".\n" - "The game will be loaded as normal, but please be aware " - "that it may very likely crash." - ); - this->setUpdateText("Resource Download Failed"); - m_fields->m_updatingResources = false; - this->loadAssets(); - } + std::visit(makeVisitor { + [&](UpdateProgress const& progress) { + this->setUpdateText(fmt::format( + "Downloading Resources: {}%", progress.first + )); + }, + [&](UpdateFinished) { + this->setUpdateText("Resources Downloaded"); + m_fields->m_updatingResources = false; + this->loadAssets(); + }, + [&](UpdateError const& error) { + InternalLoader::platformMessageBox( + "Error updating resources", + "Unable to update Geode resources: " + + error + ".\n" + "The game will be loaded as normal, but please be aware " + "that it may very likely crash." + ); + this->setUpdateText("Resource Download Failed"); + m_fields->m_updatingResources = false; + this->loadAssets(); + } + }, event->status); } void loadAssets() { diff --git a/loader/src/hooks/MenuLayer.cpp b/loader/src/hooks/MenuLayer.cpp index e48ea129..d7c02013 100644 --- a/loader/src/hooks/MenuLayer.cpp +++ b/loader/src/hooks/MenuLayer.cpp @@ -1,16 +1,17 @@ #include -#include "../ui/internal/info/ModInfoLayer.hpp" #include "../ui/internal/list/ModListLayer.hpp" #include -#include #include +#include +#include #include -#include +#include #include #include "../ids/AddIDs.hpp" #include #include +#include USE_GEODE_NAMESPACE(); @@ -21,39 +22,6 @@ class CustomMenuLayer; static Ref g_indexUpdateNotif = nullptr; static Ref g_geodeButton = nullptr; -static void addUpdateIcon(char const* icon = "updates-available.png"_spr) { - if (g_geodeButton && Index::get()->areUpdatesAvailable()) { - auto updateIcon = CCSprite::createWithSpriteFrameName(icon); - updateIcon->setPosition(g_geodeButton->getContentSize() - CCSize { 10.f, 10.f }); - updateIcon->setZOrder(99); - updateIcon->setScale(.5f); - g_geodeButton->addChild(updateIcon); - } -} - -static void updateIndexProgress(UpdateStatus status, std::string const& info, uint8_t progress) { - if (status == UpdateStatus::Failed) { - g_indexUpdateNotif->setIcon(NotificationIcon::Error); - g_indexUpdateNotif->setString("Index update failed"); - g_indexUpdateNotif->setTime(2.f); - g_indexUpdateNotif = nullptr; - addUpdateIcon("updates-failed.png"_spr); - } - - if (status == UpdateStatus::Finished) { - g_indexUpdateNotif->setIcon(NotificationIcon::Success); - if (Index::get()->areUpdatesAvailable()) { - g_indexUpdateNotif->setString("Updates Available"); - addUpdateIcon(); - } else { - g_indexUpdateNotif->setString("Everything Up-to-Date"); - } - g_indexUpdateNotif->setTime(2.f); - g_indexUpdateNotif = nullptr; - } -} - -#include struct CustomMenuLayer : Modify { void destructor() { g_geodeButton = nullptr; @@ -81,8 +49,6 @@ struct CustomMenuLayer : Modify { )) .orMake("!!"); - addUpdateIcon(); - auto bottomMenu = static_cast(this->getChildByID("bottom-menu")); auto btn = CCMenuItemSpriteExtra::create( @@ -122,9 +88,7 @@ struct CustomMenuLayer : Modify { "No", "Send", [](auto, bool btn2) { if (btn2) { - ModInfoLayer::showIssueReportPopup( - InternalMod::get()->getModInfo() - ); + geode::openIssueReportPopup(InternalMod::get()); } }, false @@ -135,13 +99,13 @@ struct CustomMenuLayer : Modify { } // update mods index - if (!g_indexUpdateNotif && !Index::get()->isIndexUpdated()) { + if (!g_indexUpdateNotif && !Index::get()->hasTriedToUpdate()) { g_indexUpdateNotif = Notification::create( "Updating Index", NotificationIcon::Loading, 0 ); g_indexUpdateNotif->show(); - Index::get()->updateIndex(updateIndexProgress); + Index::get()->update(); } return true; diff --git a/loader/src/internal/InternalLoader.cpp b/loader/src/internal/InternalLoader.cpp index 0694b47e..1353eb27 100644 --- a/loader/src/internal/InternalLoader.cpp +++ b/loader/src/internal/InternalLoader.cpp @@ -17,6 +17,10 @@ #include #include +ResourceDownloadEvent::ResourceDownloadEvent( + UpdateStatus const& status +) : status(status) {} + ListenerResult ResourceDownloadFilter::handle( std::function fn, ResourceDownloadEvent* event diff --git a/loader/src/internal/InternalLoader.hpp b/loader/src/internal/InternalLoader.hpp index 43ad26ba..57707acc 100644 --- a/loader/src/internal/InternalLoader.hpp +++ b/loader/src/internal/InternalLoader.hpp @@ -17,13 +17,9 @@ USE_GEODE_NAMESPACE(); -class ResourceDownloadEvent : public Event { -protected: - UpdateStatus m_status; - -public: - ResourceDownloadEvent(UpdateStatus status); - UpdateStatus getStatus() const; +struct ResourceDownloadEvent : public Event { + const UpdateStatus status; + ResourceDownloadEvent(UpdateStatus const& status); }; class GEODE_DLL ResourceDownloadFilter : public EventFilter { diff --git a/loader/src/loader/Event.cpp b/loader/src/loader/Event.cpp index 2b4244e5..4f56aa69 100644 --- a/loader/src/loader/Event.cpp +++ b/loader/src/loader/Event.cpp @@ -19,7 +19,7 @@ EventListenerProtocol::~EventListenerProtocol() { Event::~Event() {} void Event::postFrom(Mod* m) { - if (m) m_sender = m; + if (m) this->sender = m; for (auto h : Event::s_listeners) { if (h->passThrough(this) == ListenerResult::Stop) { @@ -27,7 +27,3 @@ void Event::postFrom(Mod* m) { } } } - -Mod* Event::getSender() { - return m_sender; -} diff --git a/loader/src/loader/Index.cpp b/loader/src/loader/Index.cpp index f9e0fd32..092682f7 100644 --- a/loader/src/loader/Index.cpp +++ b/loader/src/loader/Index.cpp @@ -7,6 +7,7 @@ #include USE_GEODE_NAMESPACE(); +using namespace geode::impl; struct IndexSourceSaveData { std::string downloadedCommitSHA; @@ -22,20 +23,41 @@ std::string IndexSource::dirname() const { return string::replace(this->repository, "/", "_"); } +// SourceUpdateEvent + +SourceUpdateEvent::SourceUpdateEvent( + IndexSource const& src, + const UpdateStatus status +) : source(src), status(status) {} + +ListenerResult SourceUpdateFilter::handle( + std::function fn, + SourceUpdateEvent* event +) { + fn(event); + return ListenerResult::Propagate; +} + +SourceUpdateFilter::SourceUpdateFilter() {} + +// ModInstallEvent + +ListenerResult ModInstallFilter::handle(std::function fn, ModInstallEvent* event) { + if (m_id == event->modID) { + fn(event); + } + return ListenerResult::Propagate; +} + +ModInstallFilter::ModInstallFilter( + std::string const& id +) : m_id(id) {} + // IndexUpdateEvent IndexUpdateEvent::IndexUpdateEvent( - std::string const& src, - UpdateStatus status -) : m_sourceRepository(src), m_status(status) {} - -std::string IndexUpdateEvent::getSource() const { - return m_sourceRepository; -} - -UpdateStatus IndexUpdateEvent::getStatus() const { - return m_status; -} + const UpdateStatus status +) : status(status) {} ListenerResult IndexUpdateFilter::handle( std::function fn, @@ -47,35 +69,53 @@ ListenerResult IndexUpdateFilter::handle( IndexUpdateFilter::IndexUpdateFilter() {} -// ModInstallEvent +// IndexItem -ModInstallEvent::ModInstallEvent( - std::string const& id, - UpdateStatus status -) : m_id(id), m_status(status) {} +Result IndexItem::createFromDir( + std::string const& sourceRepository, + ghc::filesystem::path const& dir +) { + GEODE_UNWRAP_INTO( + auto entry, file::readJson(dir / "entry.json") + .expect("Unable to read entry.json") + ); + GEODE_UNWRAP_INTO( + auto info, ModInfo::createFromFile(dir / "mod.json") + .expect("Unable to read mod.json: {error}") + ); -std::string ModInstallEvent::getModID() const { - return m_id; -} + JsonChecker checker(entry); + auto root = checker.root("[entry.json]").obj(); -UpdateStatus ModInstallEvent::getStatus() const { - return m_status; -} - -ListenerResult ModInstallFilter::handle(std::function fn, ModInstallEvent* event) { - if (m_id == event->getModID()) { - fn(event); + std::unordered_set platforms; + for (auto& plat : root.has("platforms").iterate()) { + platforms.insert(PlatformID::from(plat.template get())); } - return ListenerResult::Propagate; -} -ModInstallFilter::ModInstallFilter( - std::string const& id -) : m_id(id) {} + auto item = std::make_shared(IndexItem { + .sourceRepository = sourceRepository, + .path = dir, + .info = info, + .download = { + .url = root.has("mod").obj().has("download").template get(), + .hash = root.has("mod").obj().has("hash").template get(), + .platforms = platforms, + }, + .isFeatured = root.has("is-featured").template get(), + }); + if (checker.isError()) { + return Err(checker.getError()); + } + return Ok(item); +} // Index Index::Index() { + new EventListener( + std::bind(&Index::onSourceUpdate, this, std::placeholders::_1), + SourceUpdateFilter() + ); this->addSource("https://github.com/geode-sdk/index-test"); } @@ -100,20 +140,74 @@ std::vector Index::getSources() const { return m_sources; } -bool Index::isUpToDate() const { - for (auto& source : m_sources) { - if (!source.isUpToDate) { - return false; +void Index::onSourceUpdate(SourceUpdateEvent* event) { + // save status for aggregating SourceUpdateEvents to a single global + // IndexUpdateEvent + m_sourceStatuses[event->source.repository] = event->status; + + // figure out aggregate event + enum { Finished, Progress, Failed, } whatToPost = Finished; + for (auto& [src, status] : m_sourceStatuses) { + // if some source is still updating, post progress + if (std::holds_alternative(status)) { + whatToPost = Progress; + break; } + // otherwise, if some source failed, then post failed + else if (std::holds_alternative(status)) { + if (whatToPost != Progress) { + whatToPost = Failed; + } + } + // otherwise if all are finished, whatToPost is already set to that + } + + switch (whatToPost) { + case Finished: { + // clear source statuses to allow updating index again + m_sourceStatuses.clear(); + // post finish event + IndexUpdateEvent(UpdateFinished()).post(); + } break; + + case Progress: { + // get total progress + size_t total = 0; + for (auto& [src, status] : m_sourceStatuses) { + if (std::holds_alternative(status)) { + total += std::get(status).first; + } else { + total += 100; + } + } + IndexUpdateEvent( + UpdateProgress( + static_cast(total / m_sourceStatuses.size()), + "Downloading" + ) + ).post(); + } break; + + case Failed: { + std::string info = ""; + for (auto& [src, status] : m_sourceStatuses) { + if (std::holds_alternative(status)) { + info += src + ": " + std::get(status) + "\n"; + } + } + // clear source statuses to allow updating index again + m_sourceStatuses.clear(); + // post finish event + IndexUpdateEvent(UpdateError(info)).post(); + } break; } - return true; } void Index::checkSourceUpdates(IndexSource& src) { if (src.isUpToDate) { return this->updateSourceFromLocal(src); } - IndexUpdateEvent(src.repository, UpdateProgress(0, "Checking status")).post(); + SourceUpdateEvent(src, UpdateProgress(0, "Checking status")).post(); auto data = Mod::get()->getSavedMutable("index"); auto oldSHA = data.sources[src.repository].downloadedCommitSHA; web::AsyncWebRequest() @@ -136,15 +230,15 @@ void Index::checkSourceUpdates(IndexSource& src) { } }) .expect([&src](std::string const& err) { - IndexUpdateEvent( - src.repository, + SourceUpdateEvent( + src, UpdateError(fmt::format("Error checking for updates: {}", err)) ).post(); }); } void Index::downloadSource(IndexSource& src) { - IndexUpdateEvent(src.repository, UpdateProgress(0, "Beginning download")).post(); + SourceUpdateEvent(src, UpdateProgress(0, "Beginning download")).post(); auto targetFile = dirs::getIndexDir() / fmt::format("{}.zip", src.dirname()); @@ -161,18 +255,16 @@ void Index::downloadSource(IndexSource& src) { } } catch(...) { - return IndexUpdateEvent( - src.repository, - UpdateError("Unable to clear cached index") + return SourceUpdateEvent( + src, UpdateError("Unable to clear cached index") ).post(); } // unzip new index auto unzip = file::Unzip::intoDir(targetFile, targetDir, true); if (!unzip) { - return IndexUpdateEvent( - src.repository, - UpdateError("Unable to unzip new index") + return SourceUpdateEvent( + src, UpdateError("Unable to unzip new index") ).post(); } @@ -180,25 +272,27 @@ void Index::downloadSource(IndexSource& src) { this->updateSourceFromLocal(src); }) .expect([&src](std::string const& err) { - IndexUpdateEvent( - src.repository, - UpdateError(fmt::format("Error downloading: {}", err)) + SourceUpdateEvent( + src, UpdateError(fmt::format("Error downloading: {}", err)) ).post(); }) .progress([&src](auto&, double now, double total) { - IndexUpdateEvent( - src.repository, - UpdateProgress(static_cast(now / total * 100.0), "Downloading") + SourceUpdateEvent( + src, + UpdateProgress( + static_cast(now / total * 100.0), + "Downloading" + ) ).post(); }); } void Index::updateSourceFromLocal(IndexSource& src) { - IndexUpdateEvent(src.repository, UpdateProgress(100, "Updating local cache")).post(); + SourceUpdateEvent(src, UpdateProgress(100, "Updating local cache")).post(); // delete old items from this url if such exist for (auto& [_, versions] : m_items) { for (auto it = versions.begin(); it != versions.end(); ) { - if (it->second.sourceRepository == src.repository) { + if (it->second->sourceRepository == src.repository) { it = versions.erase(it); } else { ++it; @@ -207,10 +301,32 @@ void Index::updateSourceFromLocal(IndexSource& src) { } this->cleanupItems(); - // todo: add shit + // read directory and add new items + for (auto& dir : ghc::filesystem::directory_iterator(src.dirname())) { + auto addRes = IndexItem::createFromDir(src.repository, dir); + if (!addRes) { + log::warn("Unable to add index item from {}: {}", dir, addRes.unwrapErr()); + continue; + } + auto add = addRes.unwrap(); + // check if this major version of this item has already been added + if (m_items[add->info.m_id].count(add->info.m_version.getMajor())) { + log::warn( + "Item {}@{} has already been added, skipping", + add->info.m_id, add->info.m_version + ); + continue; + } + // add new major version of this item + m_items[add->info.m_id].insert({ + add->info.m_version.getMajor(), + add + }); + } + // mark source as finished src.isUpToDate = true; - IndexUpdateEvent(src.repository, UpdateFinished()).post(); + SourceUpdateEvent(src, UpdateFinished()).post(); } void Index::cleanupItems() { @@ -224,12 +340,33 @@ void Index::cleanupItems() { } } +bool Index::isUpToDate() const { + for (auto& source : m_sources) { + if (!source.isUpToDate) { + return false; + } + } + return true; +} + +bool Index::hasTriedToUpdate() const { + return m_triedToUpdate; +} + void Index::update(bool force) { // create index dir if it doesn't exist (void)file::createDirectoryAll(dirs::getIndexDir()); + m_triedToUpdate = true; + // update all sources in GD thread Loader::get()->queueInGDThread([force, this]() { + // check if some sources are already being updated + if (m_sourceStatuses.size()) { + return; + } + + // update sources for (auto& src : m_sources) { if (force) { this->downloadSource(src); @@ -240,8 +377,8 @@ void Index::update(bool force) { }); } -std::vector Index::getItems() const { - std::vector res; +std::vector Index::getItems() const { + std::vector res; for (auto& items : map::values(m_items)) { if (items.size()) { res.push_back(items.rbegin()->second); @@ -265,7 +402,7 @@ bool Index::isKnownItem( } } -std::optional Index::getItem( +IndexItemHandle Index::getItem( std::string const& id, std::optional version ) const { @@ -281,5 +418,21 @@ std::optional Index::getItem( } } } - return std::nullopt; + return nullptr; +} + +IndexItemHandle Index::getItem(ModInfo const& info) const { + return this->getItem(info.m_id, info.m_version.getMajor()); +} + +IndexItemHandle Index::getItem(Mod* mod) const { + return this->getItem(mod->getID(), mod->getVersion().getMajor()); +} + +bool Index::updateAvailable(IndexItemHandle item) const { + auto installed = Loader::get()->getInstalledMod(item->info.m_id); + if (!installed) { + return false; + } + return item->info.m_version > installed->getVersion(); } diff --git a/loader/src/ui/internal/GeodeUI.cpp b/loader/src/ui/internal/GeodeUI.cpp index 8a61232c..807a0e45 100644 --- a/loader/src/ui/internal/GeodeUI.cpp +++ b/loader/src/ui/internal/GeodeUI.cpp @@ -1,24 +1,58 @@ #include -#include "info/ModInfoLayer.hpp" +#include +#include "info/ModInfoPopup.hpp" #include "list/ModListLayer.hpp" #include "settings/ModSettingsPopup.hpp" - +#include #include +#include void geode::openModsList() { ModListLayer::scene(); } +void geode::openIssueReportPopup(Mod* mod) { + if (mod->getModInfo().m_issues) { + MDPopup::create( + "Issue Report", + mod->getModInfo().m_issues.value().m_info + + "\n\n" + "If your issue relates to a game crash, please include the " + "latest crash log(s) from `" + + dirs::getCrashlogsDir().string() + "`", + "OK", (mod->getModInfo().m_issues.value().m_url ? "Open URL" : ""), + [mod](bool btn2) { + if (btn2) { + web::openLinkInBrowser( + mod->getModInfo().m_issues.value().m_url.value() + ); + } + } + )->show(); + } + else { + MDPopup::create( + "Issue Report", + "Please report your issue on the " + "[#support](https://discord.com/channels/911701438269386882/979352389985390603) " + "channnel in the [Geode Discord Server](https://discord.gg/9e43WMKzhp)\n\n" + "If your issue relates to a game crash, please include the " + "latest crash log(s) from `" + dirs::getCrashlogsDir().string() + "`", + "OK" + )->show(); + } +} + void geode::openInfoPopup(Mod* mod) { - ModInfoLayer::create(mod, nullptr)->show(); + LocalModInfoPopup::create(mod, nullptr)->show(); } void geode::openIndexPopup(Mod* mod) { - if (Index::get()->isKnownItem(mod->getID())) { - ModInfoLayer::create( - new ModObject(Index::get()->getKnownItem(mod->getID())) - )->show(); + if (auto item = Index::get()->getItem( + mod->getID(), mod->getVersion().getMajor() + )) { + IndexItemInfoPopup::create(item, nullptr)->show(); } } @@ -27,3 +61,61 @@ void geode::openSettingsPopup(Mod* mod) { ModSettingsPopup::create(mod)->show(); } } + +CCNode* geode::createDefaultLogo(CCSize const& size) { + CCNode* spr = CCSprite::createWithSpriteFrameName("no-logo.png"_spr); + if (!spr) { + spr = CCLabelBMFont::create("OwO", "goldFont.fnt"); + } + limitNodeSize(spr, size, 1.f, .1f); + return spr; +} + +CCNode* geode::createModLogo(Mod* mod, CCSize const& size) { + CCNode* spr = nullptr; + if (mod == Loader::getInternalMod()) { + spr = CCSprite::createWithSpriteFrameName("geode-logo.png"_spr); + } + else { + spr = CCSprite::create( + fmt::format("{}/logo.png", mod->getID()).c_str() + ); + } + if (!spr) spr = CCSprite::createWithSpriteFrameName("no-logo.png"_spr); + if (!spr) spr = CCLabelBMFont::create("N/A", "goldFont.fnt"); + limitNodeSize(spr, size, 1.f, .1f); + return spr; +} + +CCNode* geode::createIndexItemLogo(IndexItemHandle item, CCSize const& size) { + CCNode* spr = nullptr; + auto logoPath = ghc::filesystem::absolute(item->path / "logo.png"); + spr = CCSprite::create(logoPath.string().c_str()); + if (!spr) { + spr = CCSprite::createWithSpriteFrameName("no-logo.png"_spr); + } + if (!spr) { + spr = CCLabelBMFont::create("N/A", "goldFont.fnt"); + } + if (item->isFeatured) { + auto glowSize = size + CCSize(4.f, 4.f); + + auto logoGlow = CCSprite::createWithSpriteFrameName("logo-glow.png"_spr); + logoGlow->setScaleX(glowSize.width / logoGlow->getContentSize().width); + logoGlow->setScaleY(glowSize.height / logoGlow->getContentSize().height); + + // i dont know why + 1 is needed and its too late for me to figure out why + spr->setPosition( + logoGlow->getContentSize().width / 2, logoGlow->getContentSize().height / 2 + ); + // scary mathematics + spr->setScaleX(size.width / spr->getContentSize().width / logoGlow->getScaleX()); + spr->setScaleY(size.height / spr->getContentSize().height / logoGlow->getScaleY()); + logoGlow->addChild(spr); + spr = logoGlow; + } + else { + limitNodeSize(spr, size, 1.f, .1f); + } + return spr; +} diff --git a/loader/src/ui/internal/info/DownloadStatusNode.cpp b/loader/src/ui/internal/info/DownloadStatusNode.cpp new file mode 100644 index 00000000..cf0fdfb0 --- /dev/null +++ b/loader/src/ui/internal/info/DownloadStatusNode.cpp @@ -0,0 +1,53 @@ +#include "ModInfoPopup.hpp" +#include +#include +#include + +bool DownloadStatusNode::init() { + if (!CCNode::init()) return false; + + this->setContentSize({ 150.f, 25.f }); + + auto bg = CCScale9Sprite::create("square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f }); + bg->setScale(.33f); + bg->setColor({ 0, 0, 0 }); + bg->setOpacity(75); + bg->setContentSize(m_obContentSize * 3); + this->addChild(bg); + + m_bar = Slider::create(this, nullptr, .6f); + m_bar->setValue(.0f); + m_bar->updateBar(); + m_bar->setPosition(0.f, -5.f); + m_bar->m_touchLogic->m_thumb->setVisible(false); + this->addChild(m_bar); + + m_label = CCLabelBMFont::create("", "bigFont.fnt"); + m_label->setAnchorPoint({ .0f, .5f }); + m_label->setScale(.45f); + m_label->setPosition(-m_obContentSize.width / 2 + 15.f, 5.f); + this->addChild(m_label); + + return true; +} + +DownloadStatusNode* DownloadStatusNode::create() { + auto ret = new DownloadStatusNode(); + if (ret && ret->init()) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +void DownloadStatusNode::setProgress(uint8_t progress) { + m_bar->setValue(progress / 100.f); + m_bar->updateBar(); +} + +void DownloadStatusNode::setStatus(std::string const& text) { + m_label->setString(text.c_str()); + m_label->limitLabelWidth(m_obContentSize.width - 30.f, .5f, .1f); +} + diff --git a/loader/src/ui/internal/info/ModInfoLayer.cpp b/loader/src/ui/internal/info/ModInfoLayer.cpp deleted file mode 100644 index fd9510de..00000000 --- a/loader/src/ui/internal/info/ModInfoLayer.cpp +++ /dev/null @@ -1,832 +0,0 @@ -#include "ModInfoLayer.hpp" - -#include "../dev/HookListLayer.hpp" -#include "../list/ModListView.hpp" -#include "../settings/ModSettingsPopup.hpp" -#include "../settings/AdvancedSettingsPopup.hpp" -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// TODO: die -#undef min -#undef max - -static constexpr int const TAG_CONFIRM_UNINSTALL = 5; -static constexpr int const TAG_DELETE_SAVEDATA = 6; - -bool DownloadStatusNode::init() { - if (!CCNode::init()) return false; - - this->setContentSize({ 150.f, 25.f }); - - auto bg = CCScale9Sprite::create("square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f }); - bg->setScale(.33f); - bg->setColor({ 0, 0, 0 }); - bg->setOpacity(75); - bg->setContentSize(m_obContentSize * 3); - this->addChild(bg); - - m_bar = Slider::create(this, nullptr, .6f); - m_bar->setValue(.0f); - m_bar->updateBar(); - m_bar->setPosition(0.f, -5.f); - m_bar->m_touchLogic->m_thumb->setVisible(false); - this->addChild(m_bar); - - m_label = CCLabelBMFont::create("", "bigFont.fnt"); - m_label->setAnchorPoint({ .0f, .5f }); - m_label->setScale(.45f); - m_label->setPosition(-m_obContentSize.width / 2 + 15.f, 5.f); - this->addChild(m_label); - - return true; -} - -DownloadStatusNode* DownloadStatusNode::create() { - auto ret = new DownloadStatusNode(); - if (ret && ret->init()) { - ret->autorelease(); - return ret; - } - CC_SAFE_DELETE(ret); - return nullptr; -} - -void DownloadStatusNode::setProgress(uint8_t progress) { - m_bar->setValue(progress / 100.f); - m_bar->updateBar(); -} - -void DownloadStatusNode::setStatus(std::string const& text) { - m_label->setString(text.c_str()); - m_label->limitLabelWidth(m_obContentSize.width - 30.f, .5f, .1f); -} - -void ModInfoLayer::onChangelog(CCObject* sender) { - auto toggle = static_cast(sender); - auto winSize = CCDirector::get()->getWinSize(); - - m_detailsArea->setVisible(toggle->isToggled()); - // as it turns out, cocos2d is stupid and still passes touch - // events to invisible nodes - m_detailsArea->setPositionX( - toggle->isToggled() ? winSize.width / 2 - m_detailsArea->getScaledContentSize().width / 2 - : -5000.f - ); - - m_changelogArea->setVisible(!toggle->isToggled()); - // as it turns out, cocos2d is stupid and still passes touch - // events to invisible nodes - m_changelogArea->setPositionX( - !toggle->isToggled() ? winSize.width / 2 - m_changelogArea->getScaledContentSize().width / 2 - : -5000.f - ); -} - -bool ModInfoLayer::init(ModObject* obj, ModListView* list) { - m_noElasticity = true; - m_list = list; - m_mod = obj->m_mod; - - bool isInstalledMod; - switch (obj->m_type) { - case ModObjectType::Mod: - { - m_info = obj->m_mod->getModInfo(); - isInstalledMod = true; - } - break; - - case ModObjectType::Index: - { - m_info = obj->m_index.m_info; - isInstalledMod = false; - } - break; - - default: return false; - } - - auto winSize = CCDirector::sharedDirector()->getWinSize(); - CCSize size { 440.f, 290.f }; - - if (!this->initWithColor({ 0, 0, 0, 105 })) return false; - m_mainLayer = CCLayer::create(); - this->addChild(m_mainLayer); - - auto bg = CCScale9Sprite::create("GJ_square01.png", { 0.0f, 0.0f, 80.0f, 80.0f }); - bg->setContentSize(size); - bg->setPosition(winSize.width / 2, winSize.height / 2); - bg->setZOrder(-10); - m_mainLayer->addChild(bg); - - m_buttonMenu = CCMenu::create(); - m_mainLayer->addChild(m_buttonMenu); - - constexpr float logoSize = 40.f; - constexpr float logoOffset = 10.f; - - auto nameLabel = CCLabelBMFont::create(m_info.m_name.c_str(), "bigFont.fnt"); - nameLabel->setAnchorPoint({ .0f, .5f }); - nameLabel->limitLabelWidth(200.f, .7f, .1f); - m_mainLayer->addChild(nameLabel, 2); - - auto logoSpr = this->createLogoSpr(obj, { logoSize, logoSize }); - m_mainLayer->addChild(logoSpr); - - auto developerStr = "by " + m_info.m_developer; - auto developerLabel = CCLabelBMFont::create(developerStr.c_str(), "goldFont.fnt"); - developerLabel->setScale(.5f); - developerLabel->setAnchorPoint({ .0f, .5f }); - m_mainLayer->addChild(developerLabel); - - auto logoTitleWidth = - std::max( - nameLabel->getScaledContentSize().width, developerLabel->getScaledContentSize().width - ) + - logoSize + logoOffset; - - nameLabel->setPosition( - winSize.width / 2 - logoTitleWidth / 2 + logoSize + logoOffset, winSize.height / 2 + 125.f - ); - logoSpr->setPosition({ winSize.width / 2 - logoTitleWidth / 2 + logoSize / 2, - winSize.height / 2 + 115.f }); - developerLabel->setPosition( - winSize.width / 2 - logoTitleWidth / 2 + logoSize + logoOffset, winSize.height / 2 + 105.f - ); - - auto versionLabel = CCLabelBMFont::create(m_info.m_version.toString().c_str(), "bigFont.fnt"); - versionLabel->setAnchorPoint({ .0f, .5f }); - versionLabel->setScale(.4f); - versionLabel->setPosition( - nameLabel->getPositionX() + nameLabel->getScaledContentSize().width + 5.f, - winSize.height / 2 + 125.f - ); - versionLabel->setColor({ 0, 255, 0 }); - m_mainLayer->addChild(versionLabel); - - CCDirector::sharedDirector()->getTouchDispatcher()->incrementForcePrio(2); - this->registerWithTouchDispatcher(); - - m_detailsArea = MDTextArea::create( - m_info.m_details ? m_info.m_details.value() : "### No description provided.", - { 350.f, 137.5f } - ); - m_detailsArea->setPosition( - winSize.width / 2 - m_detailsArea->getScaledContentSize().width / 2, - winSize.height / 2 - m_detailsArea->getScaledContentSize().height / 2 - 20.f - ); - m_mainLayer->addChild(m_detailsArea); - - m_scrollbar = Scrollbar::create(m_detailsArea->getScrollLayer()); - m_scrollbar->setPosition( - winSize.width / 2 + m_detailsArea->getScaledContentSize().width / 2 + 20.f, - winSize.height / 2 - 20.f - ); - m_mainLayer->addChild(m_scrollbar); - - // changelog - if (m_info.m_changelog) { - m_changelogArea = MDTextArea::create(m_info.m_changelog.value(), { 350.f, 137.5f }); - m_changelogArea->setPosition( - -5000.f, winSize.height / 2 - m_changelogArea->getScaledContentSize().height / 2 - 20.f - ); - m_changelogArea->setVisible(false); - m_mainLayer->addChild(m_changelogArea); - - auto changelogBtnOffSpr = ButtonSprite::create( - CCSprite::createWithSpriteFrameName("changelog.png"_spr), 0x20, true, 32.f, - "GJ_button_01.png", 1.f - ); - changelogBtnOffSpr->setScale(.65f); - - auto changelogBtnOnSpr = ButtonSprite::create( - CCSprite::createWithSpriteFrameName("changelog.png"_spr), 0x20, true, 32.f, - "GJ_button_02.png", 1.f - ); - changelogBtnOnSpr->setScale(.65f); - - auto changelogBtn = CCMenuItemToggler::create( - changelogBtnOffSpr, changelogBtnOnSpr, this, menu_selector(ModInfoLayer::onChangelog) - ); - changelogBtn->setPosition(-size.width / 2 + 21.5f, .0f); - m_buttonMenu->addChild(changelogBtn); - } - - // mod info - auto infoSpr = CCSprite::createWithSpriteFrameName("GJ_infoIcon_001.png"); - infoSpr->setScale(.85f); - - auto infoBtn = - CCMenuItemSpriteExtra::create(infoSpr, this, menu_selector(ModInfoLayer::onInfo)); - infoBtn->setPosition(size.width / 2 - 25.f, size.height / 2 - 25.f); - m_buttonMenu->addChild(infoBtn); - - // issue report button - if (m_info.m_issues) { - auto issuesBtnSpr = - ButtonSprite::create("Report an Issue", "goldFont.fnt", "GJ_button_04.png", .8f); - issuesBtnSpr->setScale(.75f); - - auto issuesBtn = CCMenuItemSpriteExtra::create( - issuesBtnSpr, this, menu_selector(ModInfoLayer::onIssues) - ); - issuesBtn->setPosition(0.f, -size.height / 2 + 25.f); - m_buttonMenu->addChild(issuesBtn); - } - - if (isInstalledMod) { - // mod settings - auto settingsSpr = CCSprite::createWithSpriteFrameName( - "GJ_optionsBtn_001.png" - ); - settingsSpr->setScale(.65f); - - auto settingsBtn = CCMenuItemSpriteExtra::create( - settingsSpr, this, menu_selector(ModInfoLayer::onSettings) - ); - settingsBtn->setPosition(-size.width / 2 + 25.f, -size.height / 2 + 25.f); - m_buttonMenu->addChild(settingsBtn); - - // Check if a config directory for the mod exists - // Mod::getConfigDir auto-creates the directory for user convenience, so - // have to do it manually - if (ghc::filesystem::exists(m_mod->getConfigDir(false))) { - auto configSpr = CircleButtonSprite::createWithSpriteFrameName( - "pencil.png"_spr, 1.f, CircleBaseColor::Green, CircleBaseSize::Medium2 - ); - configSpr->setScale(.65f); - - auto configBtn = CCMenuItemSpriteExtra::create( - configSpr, this, menu_selector(ModInfoLayer::onOpenConfigDir) - ); - configBtn->setPosition(-size.width / 2 + 65.f, -size.height / 2 + 25.f); - m_buttonMenu->addChild(configBtn); - } - - if (!m_mod->hasSettings()) { - settingsSpr->setColor({ 150, 150, 150 }); - settingsBtn->setTarget(this, menu_selector(ModInfoLayer::onNoSettings)); - } - - if (m_mod->getModInfo().m_repository) { - auto repoBtn = CCMenuItemSpriteExtra::create( - CCSprite::createWithSpriteFrameName("github.png"_spr), this, - menu_selector(ModInfoLayer::onRepository) - ); - repoBtn->setPosition(size.width / 2 - 25.f, -size.height / 2 + 25.f); - m_buttonMenu->addChild(repoBtn); - } - - if (m_mod->getModInfo().m_supportInfo) { - auto supportBtn = CCMenuItemSpriteExtra::create( - CCSprite::createWithSpriteFrameName("gift.png"_spr), this, - menu_selector(ModInfoLayer::onSupport) - ); - supportBtn->setPosition(size.width / 2 - 60.f, -size.height / 2 + 25.f); - m_buttonMenu->addChild(supportBtn); - } - - auto enableBtnSpr = ButtonSprite::create("Enable", "bigFont.fnt", "GJ_button_01.png", .6f); - enableBtnSpr->setScale(.6f); - - auto disableBtnSpr = - ButtonSprite::create("Disable", "bigFont.fnt", "GJ_button_06.png", .6f); - disableBtnSpr->setScale(.6f); - - auto enableBtn = CCMenuItemToggler::create( - disableBtnSpr, enableBtnSpr, this, menu_selector(ModInfoLayer::onEnableMod) - ); - enableBtn->setPosition(-155.f, 75.f); - enableBtn->toggle(!obj->m_mod->isEnabled()); - m_buttonMenu->addChild(enableBtn); - - if (!m_info.m_supportsDisabling) { - enableBtn->setTarget(this, menu_selector(ModInfoLayer::onDisablingNotSupported)); - enableBtnSpr->setColor({ 150, 150, 150 }); - disableBtnSpr->setColor({ 150, 150, 150 }); - } - - if ( - m_mod != Loader::get()->getInternalMod() && - m_mod != Mod::get() - ) { - // advanced settings - auto advSettSpr = CCSprite::createWithSpriteFrameName("GJ_optionsBtn02_001.png"); - advSettSpr->setScale(.65f); - - auto advSettBtn = CCMenuItemSpriteExtra::create( - advSettSpr, this, menu_selector(ModInfoLayer::onAdvancedSettings) - ); - advSettBtn->setPosition( - infoBtn->getPositionX() - 30.f, - infoBtn->getPositionY() - ); - m_buttonMenu->addChild(advSettBtn); - - auto uninstallBtnSpr = ButtonSprite::create( - "Uninstall", "bigFont.fnt", "GJ_button_05.png", .6f - ); - uninstallBtnSpr->setScale(.6f); - - auto uninstallBtn = CCMenuItemSpriteExtra::create( - uninstallBtnSpr, this, menu_selector(ModInfoLayer::onUninstall) - ); - uninstallBtn->setPosition(-85.f, 75.f); - m_buttonMenu->addChild(uninstallBtn); - - // todo: show update button on loader that invokes the installer - if (Index::get()->isUpdateAvailableForItem(m_info.m_id)) { - m_installBtnSpr = IconButtonSprite::create( - "GE_button_01.png"_spr, CCSprite::createWithSpriteFrameName("install.png"_spr), - "Update", "bigFont.fnt" - ); - m_installBtnSpr->setScale(.6f); - - m_installBtn = CCMenuItemSpriteExtra::create( - m_installBtnSpr, this, menu_selector(ModInfoLayer::onInstallMod) - ); - m_installBtn->setPosition(-8.0f, 75.f); - m_buttonMenu->addChild(m_installBtn); - - m_installStatus = DownloadStatusNode::create(); - m_installStatus->setPosition(winSize.width / 2 + 105.f, winSize.height / 2 + 75.f); - m_installStatus->setVisible(false); - m_mainLayer->addChild(m_installStatus); - - auto incomingVersion = - Index::get()->getKnownItem(m_info.m_id).m_info.m_version.toString(); - - m_updateVersionLabel = - CCLabelBMFont::create(("Available: " + incomingVersion).c_str(), "bigFont.fnt"); - m_updateVersionLabel->setScale(.35f); - m_updateVersionLabel->setAnchorPoint({ .0f, .5f }); - m_updateVersionLabel->setColor({ 94, 219, 255 }); - m_updateVersionLabel->setPosition( - winSize.width / 2 + 35.f, winSize.height / 2 + 75.f - ); - m_mainLayer->addChild(m_updateVersionLabel); - } - } - } - else { - m_installBtnSpr = IconButtonSprite::create( - "GE_button_01.png"_spr, CCSprite::createWithSpriteFrameName("install.png"_spr), - "Install", "bigFont.fnt" - ); - m_installBtnSpr->setScale(.6f); - - m_installBtn = CCMenuItemSpriteExtra::create( - m_installBtnSpr, this, menu_selector(ModInfoLayer::onInstallMod) - ); - m_installBtn->setPosition(-143.0f, 75.f); - m_buttonMenu->addChild(m_installBtn); - - m_installStatus = DownloadStatusNode::create(); - m_installStatus->setPosition(winSize.width / 2 - 25.f, winSize.height / 2 + 75.f); - m_installStatus->setVisible(false); - m_mainLayer->addChild(m_installStatus); - } - - // check if this mod is being installed/updated, and if so, update UI - if (auto handle = Index::get()->isInstallingItem(m_info.m_id)) { - m_installation = handle; - this->install(); - } - - auto closeSpr = CCSprite::createWithSpriteFrameName("GJ_closeBtn_001.png"); - closeSpr->setScale(.8f); - - auto closeBtn = - CCMenuItemSpriteExtra::create(closeSpr, this, menu_selector(ModInfoLayer::onClose)); - closeBtn->setPosition(-size.width / 2 + 3.f, size.height / 2 - 3.f); - m_buttonMenu->addChild(closeBtn); - - this->setKeypadEnabled(true); - this->setTouchEnabled(true); - - return true; -} - -void ModInfoLayer::onIssues(CCObject*) { - ModInfoLayer::showIssueReportPopup(m_info); -} - -void ModInfoLayer::onSupport(CCObject*) { - MDPopup::create("Support " + m_mod->getName(), m_mod->getModInfo().m_supportInfo.value(), "OK") - ->show(); -} - -void ModInfoLayer::onEnableMod(CCObject* pSender) { - if (!InternalLoader::get()->shownInfoAlert("mod-disable-vs-unload")) { - FLAlertLayer::create( - "Notice", - "You may still see some effects of the mod left, and you may " - "need to restart the game to have it fully unloaded.", - "OK" - ) - ->show(); - if (m_list) m_list->updateAllStates(nullptr); - return; - } - if (as(pSender)->isToggled()) { - auto res = m_mod->loadBinary(); - if (!res) { - FLAlertLayer::create( - nullptr, "Error Loading Mod", - res.unwrapErr(), "OK", nullptr - )->show(); - } - } - else { - auto res = m_mod->disable(); - if (!res) { - FLAlertLayer::create( - nullptr, "Error Disabling Mod", - res.unwrapErr(), "OK", nullptr - )->show(); - } - } - if (m_list) m_list->updateAllStates(nullptr); - as(pSender)->toggle(m_mod->isEnabled()); -} - -void ModInfoLayer::onRepository(CCObject*) { - web::openLinkInBrowser(m_mod->getModInfo().m_repository.value()); -} - -void ModInfoLayer::onInstallMod(CCObject*) { - auto ticketRes = Index::get()->installItem(Index::get()->getKnownItem(m_info.m_id)); - if (!ticketRes) { - return FLAlertLayer::create( - "Unable to install", ticketRes.unwrapErr(), "OK" - )->show(); - } - m_installation = ticketRes.unwrap(); - - createQuickPopup( - "Install", - "The following mods will be installed: " + - ranges::join(m_installation->toInstall(), ",") + ".", - "Cancel", "OK", - [this](FLAlertLayer*, bool btn2) { - if (btn2) { - this->install(); - } - else { - this->updateInstallStatus("", 0); - } - } - ); -} - -void ModInfoLayer::onCancelInstall(CCObject*) { - m_installBtn->setEnabled(false); - m_installBtnSpr->setString("Cancelling"); - m_installation->cancel(); - m_installation = nullptr; - if (m_updateVersionLabel) { - m_updateVersionLabel->setVisible(true); - } -} - -void ModInfoLayer::onOpenConfigDir(CCObject*) { - file::openFolder(m_mod->getConfigDir()); -} - -void ModInfoLayer::onUninstall(CCObject*) { - auto layer = FLAlertLayer::create( - this, "Confirm Uninstall", - "Are you sure you want to uninstall " + m_info.m_name + "?", "Cancel", "OK" - ); - layer->setTag(TAG_CONFIRM_UNINSTALL); - layer->show(); -} - -void ModInfoLayer::FLAlert_Clicked(FLAlertLayer* layer, bool btn2) { - switch (layer->getTag()) { - case TAG_CONFIRM_UNINSTALL: - { - if (btn2) { - this->uninstall(); - } - } - break; - - case TAG_DELETE_SAVEDATA: - { - if (btn2) { - if (ghc::filesystem::remove_all(m_mod->getSaveDir())) { - FLAlertLayer::create("Deleted", "The mod's save data was deleted.", "OK") - ->show(); - } - else { - FLAlertLayer::create( - "Error", "Unable to delete mod's save directory!", "OK" - ) - ->show(); - } - } - if (m_list) m_list->refreshList(); - this->onClose(nullptr); - } - break; - } -} - -void ModInfoLayer::updateInstallStatus(std::string const& status, uint8_t progress) { - if (status.size()) { - m_installStatus->setVisible(true); - m_installStatus->setStatus(status); - m_installStatus->setProgress(progress); - } - else { - m_installStatus->setVisible(false); - } -} - -void ModInfoLayer::modInstallProgress( - InstallHandle, UpdateStatus status, std::string const& info, uint8_t percentage -) { - switch (status) { - case UpdateStatus::Failed: - { - FLAlertLayer::create("Installation failed :(", info, "OK")->show(); - this->updateInstallStatus("", 0); - - m_installBtn->setEnabled(true); - m_installBtn->setTarget(this, menu_selector(ModInfoLayer::onInstallMod)); - m_installBtnSpr->setString("Install"); - m_installBtnSpr->setBG("GE_button_01.png"_spr, false); - - m_installation = nullptr; - } - break; - - case UpdateStatus::Finished: - { - this->updateInstallStatus("", 100); - - FLAlertLayer::create( - "Install complete", - "Mod succesfully installed! :) " - "(You may need to restart the game " - "for the mod to take full effect)", - "OK" - ) - ->show(); - - m_installation = nullptr; - - if (m_list) m_list->refreshList(); - this->onClose(nullptr); - } - break; - - default: - { - this->updateInstallStatus(info, percentage); - } - break; - } -} - -void ModInfoLayer::install() { - if (m_updateVersionLabel) { - m_updateVersionLabel->setVisible(false); - } - this->updateInstallStatus("Starting install", 0); - - m_installBtn->setTarget(this, menu_selector(ModInfoLayer::onCancelInstall)); - m_installBtnSpr->setString("Cancel"); - m_installBtnSpr->setBG("GJ_button_06.png", false); - - m_callbackID = m_installation->start(std::bind( - &ModInfoLayer::modInstallProgress, this, std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3, std::placeholders::_4 - )); -} - -void ModInfoLayer::uninstall() { - auto res = m_mod->uninstall(); - if (!res) { - return FLAlertLayer::create( - "Uninstall failed :(", res.unwrapErr(), "OK" - )->show(); - } - auto layer = FLAlertLayer::create( - this, "Uninstall complete", - "Mod was succesfully uninstalled! :) " - "(You may need to restart the game " - "for the mod to take full effect). " - "Would you also like to delete the mod's " - "save data?", - "Cancel", "Delete", 350.f - ); - layer->setTag(TAG_DELETE_SAVEDATA); - layer->show(); -} - -void ModInfoLayer::onDisablingNotSupported(CCObject* pSender) { - FLAlertLayer::create("Unsupported", "Disabling is not supported for this mod.", "OK") - ->show(); - as(pSender)->toggle(m_mod->isEnabled()); -} - -void ModInfoLayer::onHooks(CCObject*) { - auto layer = HookListLayer::create(this->m_mod); - this->addChild(layer); - layer->showLayer(false); -} - -void ModInfoLayer::onSettings(CCObject*) { - ModSettingsPopup::create(m_mod)->show(); -} - -void ModInfoLayer::onNoSettings(CCObject*) { - FLAlertLayer::create("No Settings Found", "This mod has no customizable settings.", "OK") - ->show(); -} - -void ModInfoLayer::onAdvancedSettings(CCObject*) { - AdvancedSettingsPopup::create(m_mod)->show(); -} - -void ModInfoLayer::onInfo(CCObject*) { - FLAlertLayer::create( - nullptr, ("About " + m_info.m_name).c_str(), - "ID: " + m_info.m_id + - "\n" - "Version: " + - m_info.m_version.toString() + - "\n" - "Developer: " + - m_info.m_developer + - "\n" - "Path: " + - m_info.m_path.string() + "\n", - "OK", nullptr, 400.f - ) - ->show(); -} - -void ModInfoLayer::keyDown(enumKeyCodes key) { - if (key == KEY_Escape) return this->onClose(nullptr); - if (key == KEY_Space) return; - - return FLAlertLayer::keyDown(key); -} - -void ModInfoLayer::onClose(CCObject* pSender) { - this->setKeyboardEnabled(false); - this->removeFromParentAndCleanup(true); - if (m_installation) { - m_installation->leave(m_callbackID); - } -}; - -ModInfoLayer* ModInfoLayer::create(Mod* mod, ModListView* list) { - auto ret = new ModInfoLayer; - if (ret && ret->init(new ModObject(mod), list)) { - ret->autorelease(); - return ret; - } - CC_SAFE_DELETE(ret); - return nullptr; -} - -ModInfoLayer* ModInfoLayer::create(ModObject* obj, ModListView* list) { - auto ret = new ModInfoLayer; - if (ret && ret->init(obj, list)) { - ret->autorelease(); - return ret; - } - CC_SAFE_DELETE(ret); - return nullptr; -} - -CCNode* ModInfoLayer::createLogoSpr(ModObject* modObj, CCSize const& size) { - switch (modObj->m_type) { - case ModObjectType::Mod: - { - return ModInfoLayer::createLogoSpr(modObj->m_mod, size); - } - break; - - case ModObjectType::Index: - { - return ModInfoLayer::createLogoSpr(modObj->m_index, size); - } - break; - - default: - { - auto spr = CCSprite::createWithSpriteFrameName("no-logo.png"_spr); - spr->setScaleX(size.width / spr->getContentSize().width); - spr->setScaleY(size.height / spr->getContentSize().height); - if (!spr) { - return CCLabelBMFont::create("OwO", "goldFont.fnt"); - } - return spr; - } - break; - } -} - -CCNode* ModInfoLayer::createLogoSpr(Mod* mod, CCSize const& size) { - CCNode* spr = nullptr; - if (mod == Loader::getInternalMod()) { - spr = CCSprite::createWithSpriteFrameName("geode-logo.png"_spr); - } - else { - spr = CCSprite::create( - fmt::format("{}/logo.png", mod->getID()).c_str() - ); - } - if (!spr) spr = CCSprite::createWithSpriteFrameName("no-logo.png"_spr); - if (!spr) spr = CCLabelBMFont::create("OwO", "goldFont.fnt"); - spr->setScaleX(size.width / spr->getContentSize().width); - spr->setScaleY(size.height / spr->getContentSize().height); - return spr; -} - -CCNode* ModInfoLayer::createLogoSpr(IndexItem const& item, CCSize const& size) { - CCNode* spr = nullptr; - auto logoPath = ghc::filesystem::absolute(item.m_path / "logo.png"); - spr = CCSprite::create(logoPath.string().c_str()); - if (!spr) { - spr = CCSprite::createWithSpriteFrameName("no-logo.png"_spr); - } - if (!spr) { - spr = CCLabelBMFont::create("OwO", "goldFont.fnt"); - } - - if (Index::get()->isFeaturedItem(item.m_info.m_id)) { - auto glowSize = size + CCSize(4.f, 4.f); - - auto logoGlow = CCSprite::createWithSpriteFrameName("logo-glow.png"_spr); - logoGlow->setScaleX(glowSize.width / logoGlow->getContentSize().width); - logoGlow->setScaleY(glowSize.height / logoGlow->getContentSize().height); - - // i dont know why + 1 is needed and its too late for me to figure out why - spr->setPosition( - logoGlow->getContentSize().width / 2, logoGlow->getContentSize().height / 2 - ); - // scary mathematics - spr->setScaleX(size.width / spr->getContentSize().width / logoGlow->getScaleX()); - spr->setScaleY(size.height / spr->getContentSize().height / logoGlow->getScaleY()); - logoGlow->addChild(spr); - spr = logoGlow; - } - else { - spr->setScaleX(size.width / spr->getContentSize().width); - spr->setScaleY(size.height / spr->getContentSize().height); - } - - return spr; -} - -void ModInfoLayer::showIssueReportPopup(ModInfo const& info) { - if (info.m_issues) { - MDPopup::create( - "Issue Report", - info.m_issues.value().m_info + - "\n\n" - "If your issue relates to a game crash, please include the " - "latest crash log(s) from `" + - dirs::getCrashlogsDir().string() + "`", - "OK", (info.m_issues.value().m_url ? "Open URL" : ""), - [info](bool btn2) { - if (btn2) { - web::openLinkInBrowser(info.m_issues.value().m_url.value()); - } - } - )->show(); - } - else { - MDPopup::create( - "Issue Report", - "Please report your issue on the " - "[#support](https://discord.com/channels/911701438269386882/979352389985390603) " - "channnel in the [Geode Discord Server](https://discord.gg/9e43WMKzhp)\n\n" - "If your issue relates to a game crash, please include the " - "latest crash log(s) from `" + dirs::getCrashlogsDir().string() + "`", - "OK" - ) - ->show(); - } -} diff --git a/loader/src/ui/internal/info/ModInfoPopup.cpp b/loader/src/ui/internal/info/ModInfoPopup.cpp new file mode 100644 index 00000000..50ce37d7 --- /dev/null +++ b/loader/src/ui/internal/info/ModInfoPopup.cpp @@ -0,0 +1,620 @@ +#include "ModInfoPopup.hpp" + +#include "../dev/HookListLayer.hpp" +#include "../list/ModListView.hpp" +#include "../settings/ModSettingsPopup.hpp" +#include "../settings/AdvancedSettingsPopup.hpp" +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// TODO: die +#undef min +#undef max + +static constexpr int const TAG_CONFIRM_UNINSTALL = 5; +static constexpr int const TAG_DELETE_SAVEDATA = 6; +static const CCSize LAYER_SIZE = { 440.f, 290.f }; + +bool ModInfoPopup::init(ModInfo const& info, ModListView* list) { + m_noElasticity = true; + m_list = list; + + auto winSize = CCDirector::sharedDirector()->getWinSize(); + + if (!this->initWithColor({ 0, 0, 0, 105 })) return false; + m_mainLayer = CCLayer::create(); + this->addChild(m_mainLayer); + + auto bg = CCScale9Sprite::create("GJ_square01.png", { 0.0f, 0.0f, 80.0f, 80.0f }); + bg->setContentSize(LAYER_SIZE); + bg->setPosition(winSize.width / 2, winSize.height / 2); + bg->setZOrder(-10); + m_mainLayer->addChild(bg); + + m_buttonMenu = CCMenu::create(); + m_mainLayer->addChild(m_buttonMenu); + + constexpr float logoSize = 40.f; + constexpr float logoOffset = 10.f; + + auto nameLabel = CCLabelBMFont::create(info.m_name.c_str(), "bigFont.fnt"); + nameLabel->setAnchorPoint({ .0f, .5f }); + nameLabel->limitLabelWidth(200.f, .7f, .1f); + m_mainLayer->addChild(nameLabel, 2); + + auto logoSpr = this->createLogo({ logoSize, logoSize }); + m_mainLayer->addChild(logoSpr); + + auto developerStr = "by " + info.m_developer; + auto developerLabel = CCLabelBMFont::create(developerStr.c_str(), "goldFont.fnt"); + developerLabel->setScale(.5f); + developerLabel->setAnchorPoint({ .0f, .5f }); + m_mainLayer->addChild(developerLabel); + + auto logoTitleWidth = + std::max( + nameLabel->getScaledContentSize().width, + developerLabel->getScaledContentSize().width + ) + + logoSize + logoOffset; + + nameLabel->setPosition( + winSize.width / 2 - logoTitleWidth / 2 + logoSize + logoOffset, + winSize.height / 2 + 125.f + ); + logoSpr->setPosition({ + winSize.width / 2 - logoTitleWidth / 2 + logoSize / 2, + winSize.height / 2 + 115.f + }); + developerLabel->setPosition( + winSize.width / 2 - logoTitleWidth / 2 + logoSize + logoOffset, + winSize.height / 2 + 105.f + ); + + auto versionLabel = CCLabelBMFont::create( + info.m_version.toString().c_str(), + "bigFont.fnt" + ); + versionLabel->setAnchorPoint({ .0f, .5f }); + versionLabel->setScale(.4f); + versionLabel->setPosition( + nameLabel->getPositionX() + nameLabel->getScaledContentSize().width + 5.f, + winSize.height / 2 + 125.f + ); + versionLabel->setColor({ 0, 255, 0 }); + m_mainLayer->addChild(versionLabel); + + CCDirector::sharedDirector()->getTouchDispatcher()->incrementForcePrio(2); + this->registerWithTouchDispatcher(); + + m_detailsArea = MDTextArea::create( + (info.m_details ? info.m_details.value() : "### No description provided."), + { 350.f, 137.5f } + ); + m_detailsArea->setPosition( + winSize.width / 2 - m_detailsArea->getScaledContentSize().width / 2, + winSize.height / 2 - m_detailsArea->getScaledContentSize().height / 2 - 20.f + ); + m_mainLayer->addChild(m_detailsArea); + + m_scrollbar = Scrollbar::create(m_detailsArea->getScrollLayer()); + m_scrollbar->setPosition( + winSize.width / 2 + m_detailsArea->getScaledContentSize().width / 2 + 20.f, + winSize.height / 2 - 20.f + ); + m_mainLayer->addChild(m_scrollbar); + + // changelog + if (info.m_changelog) { + m_changelogArea = MDTextArea::create(info.m_changelog.value(), { 350.f, 137.5f }); + m_changelogArea->setPosition( + -5000.f, winSize.height / 2 - + m_changelogArea->getScaledContentSize().height / 2 - 20.f + ); + m_changelogArea->setVisible(false); + m_mainLayer->addChild(m_changelogArea); + + auto changelogBtnOffSpr = ButtonSprite::create( + CCSprite::createWithSpriteFrameName("changelog.png"_spr), + 0x20, true, 32.f, "GJ_button_01.png", 1.f + ); + changelogBtnOffSpr->setScale(.65f); + + auto changelogBtnOnSpr = ButtonSprite::create( + CCSprite::createWithSpriteFrameName("changelog.png"_spr), + 0x20, true, 32.f, "GJ_button_02.png", 1.f + ); + changelogBtnOnSpr->setScale(.65f); + + auto changelogBtn = CCMenuItemToggler::create( + changelogBtnOffSpr, changelogBtnOnSpr, + this, menu_selector(ModInfoPopup::onChangelog) + ); + changelogBtn->setPosition(-LAYER_SIZE.width / 2 + 21.5f, .0f); + m_buttonMenu->addChild(changelogBtn); + } + + // mod info + auto infoSpr = CCSprite::createWithSpriteFrameName("GJ_infoIcon_001.png"); + infoSpr->setScale(.85f); + + m_infoBtn = CCMenuItemSpriteExtra::create( + infoSpr, this, menu_selector(ModInfoPopup::onInfo) + ); + m_infoBtn->setPosition( + LAYER_SIZE.width / 2 - 25.f, + LAYER_SIZE.height / 2 - 25.f + ); + m_buttonMenu->addChild(m_infoBtn); + + // repo button + if (info.m_repository) { + auto repoBtn = CCMenuItemSpriteExtra::create( + CCSprite::createWithSpriteFrameName("github.png"_spr), this, + menu_selector(ModInfoPopup::onRepository) + ); + repoBtn->setPosition( + LAYER_SIZE.width / 2 - 25.f, + -LAYER_SIZE.height / 2 + 25.f + ); + m_buttonMenu->addChild(repoBtn); + } + + // support button + if (info.m_supportInfo) { + auto supportBtn = CCMenuItemSpriteExtra::create( + CCSprite::createWithSpriteFrameName("gift.png"_spr), this, + menu_selector(ModInfoPopup::onSupport) + ); + supportBtn->setPosition( + LAYER_SIZE.width / 2 - 60.f, + -LAYER_SIZE.height / 2 + 25.f + ); + m_buttonMenu->addChild(supportBtn); + } + + auto closeSpr = CCSprite::createWithSpriteFrameName("GJ_closeBtn_001.png"); + closeSpr->setScale(.8f); + + auto closeBtn = CCMenuItemSpriteExtra::create( + closeSpr, this, menu_selector(ModInfoPopup::onClose) + ); + closeBtn->setPosition( + -LAYER_SIZE.width / 2 + 3.f, + LAYER_SIZE.height / 2 - 3.f + ); + m_buttonMenu->addChild(closeBtn); + + this->setKeypadEnabled(true); + this->setTouchEnabled(true); + + return true; +} + +void ModInfoPopup::onSupport(CCObject*) { + MDPopup::create( + "Support " + this->getModInfo().m_name, + this->getModInfo().m_supportInfo.value(), + "OK" + )->show(); +} + +void ModInfoPopup::onRepository(CCObject*) { + web::openLinkInBrowser(this->getModInfo().m_repository.value()); +} + +void ModInfoPopup::onInfo(CCObject*) { + auto info = this->getModInfo(); + FLAlertLayer::create( + nullptr, + ("About " + info.m_name).c_str(), + fmt::format( + "ID: {}\n" + "Version: {}\n" + "Developer: {}\n" + "Path: {}\n", + info.m_id, + info.m_version.toString(), + info.m_developer, + info.m_path.string() + ), + "OK", nullptr, 400.f + )->show(); +} + +void ModInfoPopup::onChangelog(CCObject* sender) { + auto toggle = static_cast(sender); + auto winSize = CCDirector::get()->getWinSize(); + + m_detailsArea->setVisible(toggle->isToggled()); + // as it turns out, cocos2d is stupid and still passes touch + // events to invisible nodes + m_detailsArea->setPositionX( + toggle->isToggled() ? + winSize.width / 2 - m_detailsArea->getScaledContentSize().width / 2 : + -5000.f + ); + + m_changelogArea->setVisible(!toggle->isToggled()); + // as it turns out, cocos2d is stupid and still passes touch + // events to invisible nodes + m_changelogArea->setPositionX( + !toggle->isToggled() ? + winSize.width / 2 - m_changelogArea->getScaledContentSize().width / 2 : + -5000.f + ); +} + +void ModInfoPopup::keyDown(enumKeyCodes key) { + if (key == KEY_Escape) return this->onClose(nullptr); + if (key == KEY_Space) return; + + return FLAlertLayer::keyDown(key); +} + +void ModInfoPopup::onClose(CCObject* pSender) { + this->setKeyboardEnabled(false); + this->removeFromParentAndCleanup(true); +}; + +// LocalModInfoPopup + +bool LocalModInfoPopup::init(Mod* mod, ModListView* list) { + m_mod = mod; + + if (!ModInfoPopup::init(mod->getModInfo(), list)) + return false; + + auto winSize = CCDirector::sharedDirector()->getWinSize(); + + // mod settings + auto settingsSpr = CCSprite::createWithSpriteFrameName( + "GJ_optionsBtn_001.png" + ); + settingsSpr->setScale(.65f); + + auto settingsBtn = CCMenuItemSpriteExtra::create( + settingsSpr, this, menu_selector(LocalModInfoPopup::onSettings) + ); + settingsBtn->setPosition( + -LAYER_SIZE.width / 2 + 25.f, + -LAYER_SIZE.height / 2 + 25.f + ); + m_buttonMenu->addChild(settingsBtn); + + // Check if a config directory for the mod exists + if (ghc::filesystem::exists(mod->getConfigDir(false))) { + auto configSpr = CircleButtonSprite::createWithSpriteFrameName( + "pencil.png"_spr, 1.f, CircleBaseColor::Green, CircleBaseSize::Medium2 + ); + configSpr->setScale(.65f); + + auto configBtn = CCMenuItemSpriteExtra::create( + configSpr, this, menu_selector(LocalModInfoPopup::onOpenConfigDir) + ); + configBtn->setPosition( + -LAYER_SIZE.width / 2 + 65.f, + -LAYER_SIZE.height / 2 + 25.f + ); + m_buttonMenu->addChild(configBtn); + } + + if (!mod->hasSettings()) { + settingsSpr->setColor({ 150, 150, 150 }); + settingsBtn->setTarget(this, menu_selector(LocalModInfoPopup::onNoSettings)); + } + + auto enableBtnSpr = ButtonSprite::create( + "Enable", "bigFont.fnt", "GJ_button_01.png", .6f + ); + enableBtnSpr->setScale(.6f); + + auto disableBtnSpr = ButtonSprite::create( + "Disable", "bigFont.fnt", "GJ_button_06.png", .6f + ); + disableBtnSpr->setScale(.6f); + + auto enableBtn = CCMenuItemToggler::create( + disableBtnSpr, enableBtnSpr, + this, menu_selector(LocalModInfoPopup::onEnableMod) + ); + enableBtn->setPosition(-155.f, 75.f); + enableBtn->toggle(!mod->isEnabled()); + m_buttonMenu->addChild(enableBtn); + + if (!mod->supportsDisabling()) { + enableBtn->setTarget(this, menu_selector(LocalModInfoPopup::onDisablingNotSupported)); + enableBtnSpr->setColor({ 150, 150, 150 }); + disableBtnSpr->setColor({ 150, 150, 150 }); + } + + if (mod != Loader::get()->getInternalMod()) { + // advanced settings + auto advSettSpr = CCSprite::createWithSpriteFrameName("GJ_optionsBtn02_001.png"); + advSettSpr->setScale(.65f); + + auto advSettBtn = CCMenuItemSpriteExtra::create( + advSettSpr, this, menu_selector(LocalModInfoPopup::onAdvancedSettings) + ); + advSettBtn->setPosition( + m_infoBtn->getPositionX() - 30.f, + m_infoBtn->getPositionY() + ); + m_buttonMenu->addChild(advSettBtn); + + auto uninstallBtnSpr = ButtonSprite::create( + "Uninstall", "bigFont.fnt", "GJ_button_05.png", .6f + ); + uninstallBtnSpr->setScale(.6f); + + auto uninstallBtn = CCMenuItemSpriteExtra::create( + uninstallBtnSpr, this, menu_selector(LocalModInfoPopup::onUninstall) + ); + uninstallBtn->setPosition(-85.f, 75.f); + m_buttonMenu->addChild(uninstallBtn); + + auto indexItem = Index::get()->getItem(mod->getModInfo()); + + // todo: show update button on loader that invokes the installer + if (indexItem && Index::get()->updateAvailable(indexItem)) { + m_installBtnSpr = IconButtonSprite::create( + "GE_button_01.png"_spr, + CCSprite::createWithSpriteFrameName("install.png"_spr), + "Update", "bigFont.fnt" + ); + m_installBtnSpr->setScale(.6f); + + m_installBtn = CCMenuItemSpriteExtra::create( + m_installBtnSpr, this, nullptr + ); + m_installBtn->setPosition(-8.0f, 75.f); + m_buttonMenu->addChild(m_installBtn); + + m_installStatus = DownloadStatusNode::create(); + m_installStatus->setPosition( + winSize.width / 2 + 105.f, + winSize.height / 2 + 75.f + ); + m_installStatus->setVisible(false); + m_mainLayer->addChild(m_installStatus); + + m_updateVersionLabel = CCLabelBMFont::create( + ("Available: " + indexItem->info.m_version.toString()).c_str(), + "bigFont.fnt" + ); + m_updateVersionLabel->setScale(.35f); + m_updateVersionLabel->setAnchorPoint({ .0f, .5f }); + m_updateVersionLabel->setColor({ 94, 219, 255 }); + m_updateVersionLabel->setPosition( + winSize.width / 2 + 35.f, winSize.height / 2 + 75.f + ); + m_mainLayer->addChild(m_updateVersionLabel); + } + } + + // issue report button + if (mod->getModInfo().m_issues) { + auto issuesBtnSpr = ButtonSprite::create( + "Report an Issue", "goldFont.fnt", "GJ_button_04.png", .8f + ); + issuesBtnSpr->setScale(.75f); + + auto issuesBtn = CCMenuItemSpriteExtra::create( + issuesBtnSpr, this, menu_selector(LocalModInfoPopup::onIssues) + ); + issuesBtn->setPosition(0.f, -LAYER_SIZE.height / 2 + 25.f); + m_buttonMenu->addChild(issuesBtn); + } + + return true; +} + +CCNode* LocalModInfoPopup::createLogo(CCSize const& size) { + return geode::createModLogo(m_mod, size); +} + +ModInfo LocalModInfoPopup::getModInfo() const { + return m_mod->getModInfo(); +} + +void LocalModInfoPopup::onIssues(CCObject*) { + geode::openIssueReportPopup(m_mod); +} + +void LocalModInfoPopup::onUninstall(CCObject*) { + auto layer = FLAlertLayer::create( + this, "Confirm Uninstall", + fmt::format( + "Are you sure you want to uninstall {}?", + m_mod->getName() + ), + "Cancel", "OK" + ); + layer->setTag(TAG_CONFIRM_UNINSTALL); + layer->show(); +} + +void LocalModInfoPopup::onEnableMod(CCObject* sender) { + if (!InternalLoader::get()->shownInfoAlert("mod-disable-vs-unload")) { + FLAlertLayer::create( + "Notice", + "You may still see some effects of the mod left, and you may " + "need to restart the game to have it fully unloaded.", + "OK" + ) + ->show(); + if (m_list) m_list->updateAllStates(nullptr); + return; + } + if (as(sender)->isToggled()) { + auto res = m_mod->loadBinary(); + if (!res) { + FLAlertLayer::create( + nullptr, "Error Loading Mod", + res.unwrapErr(), "OK", nullptr + )->show(); + } + } + else { + auto res = m_mod->disable(); + if (!res) { + FLAlertLayer::create( + nullptr, "Error Disabling Mod", + res.unwrapErr(), "OK", nullptr + )->show(); + } + } + if (m_list) m_list->updateAllStates(nullptr); + as(sender)->toggle(m_mod->isEnabled()); +} + +void LocalModInfoPopup::onOpenConfigDir(CCObject*) { + file::openFolder(m_mod->getConfigDir()); +} + +void LocalModInfoPopup::onDisablingNotSupported(CCObject* pSender) { + FLAlertLayer::create( + "Unsupported", + "Disabling is not supported for this mod.", + "OK" + )->show(); + as(pSender)->toggle(m_mod->isEnabled()); +} + +void LocalModInfoPopup::onSettings(CCObject*) { + ModSettingsPopup::create(m_mod)->show(); +} + +void LocalModInfoPopup::onNoSettings(CCObject*) { + FLAlertLayer::create("No Settings Found", "This mod has no customizable settings.", "OK") + ->show(); +} + +void LocalModInfoPopup::onAdvancedSettings(CCObject*) { + AdvancedSettingsPopup::create(m_mod)->show(); +} + +void LocalModInfoPopup::FLAlert_Clicked(FLAlertLayer* layer, bool btn2) { + switch (layer->getTag()) { + case TAG_CONFIRM_UNINSTALL: { + if (btn2) { + this->uninstall(); + } + } break; + + case TAG_DELETE_SAVEDATA: { + if (btn2) { + if (ghc::filesystem::remove_all(m_mod->getSaveDir())) { + FLAlertLayer::create( + "Deleted", "The mod's save data was deleted.", "OK" + )->show(); + } + else { + FLAlertLayer::create( + "Error", "Unable to delete mod's save directory!", "OK" + )->show(); + } + } + if (m_list) m_list->refreshList(); + this->onClose(nullptr); + } break; + } +} + +void LocalModInfoPopup::uninstall() { + auto res = m_mod->uninstall(); + if (!res) { + return FLAlertLayer::create( + "Uninstall failed :(", res.unwrapErr(), "OK" + )->show(); + } + auto layer = FLAlertLayer::create( + this, "Uninstall complete", + "Mod was succesfully uninstalled! :) " + "(You may need to restart the game " + "for the mod to take full effect). " + "Would you also like to delete the mod's " + "save data?", + "Cancel", "Delete", 350.f + ); + layer->setTag(TAG_DELETE_SAVEDATA); + layer->show(); +} + +LocalModInfoPopup* LocalModInfoPopup::create(Mod* mod, ModListView* list) { + auto ret = new LocalModInfoPopup; + if (ret && ret->init(mod, list)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +// IndexItemInfoPopup + +bool IndexItemInfoPopup::init(IndexItemHandle item, ModListView* list) { + m_item = item; + + auto winSize = CCDirector::sharedDirector()->getWinSize(); + + if (!ModInfoPopup::init(item->info, list)) + return false; + + m_installBtnSpr = IconButtonSprite::create( + "GE_button_01.png"_spr, CCSprite::createWithSpriteFrameName("install.png"_spr), + "Install", "bigFont.fnt" + ); + m_installBtnSpr->setScale(.6f); + + m_installBtn = CCMenuItemSpriteExtra::create( + m_installBtnSpr, this, nullptr + ); + m_installBtn->setPosition(-143.0f, 75.f); + m_buttonMenu->addChild(m_installBtn); + + m_installStatus = DownloadStatusNode::create(); + m_installStatus->setPosition( + winSize.width / 2 - 25.f, + winSize.height / 2 + 75.f + ); + m_installStatus->setVisible(false); + m_mainLayer->addChild(m_installStatus); + + return true; +} + +CCNode* IndexItemInfoPopup::createLogo(CCSize const& size) { + return geode::createIndexItemLogo(m_item, size); +} + +ModInfo IndexItemInfoPopup::getModInfo() const { + return m_item->info; +} + +IndexItemInfoPopup* IndexItemInfoPopup::create( + IndexItemHandle item, ModListView* list +) { + auto ret = new IndexItemInfoPopup; + if (ret && ret->init(item, list)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} diff --git a/loader/src/ui/internal/info/ModInfoLayer.hpp b/loader/src/ui/internal/info/ModInfoPopup.hpp similarity index 59% rename from loader/src/ui/internal/info/ModInfoLayer.hpp rename to loader/src/ui/internal/info/ModInfoPopup.hpp index e9daf86f..c47a332a 100644 --- a/loader/src/ui/internal/info/ModInfoLayer.hpp +++ b/loader/src/ui/internal/info/ModInfoPopup.hpp @@ -26,58 +26,66 @@ public: void setStatus(std::string const& text); }; -class ModInfoLayer : public FLAlertLayer, public FLAlertLayerProtocol { +class ModInfoPopup : public FLAlertLayer { protected: - Mod* m_mod = nullptr; - ModInfo m_info; - bool m_isIndexMod = false; ModListView* m_list = nullptr; DownloadStatusNode* m_installStatus = nullptr; IconButtonSprite* m_installBtnSpr; CCMenuItemSpriteExtra* m_installBtn; + CCMenuItemSpriteExtra* m_infoBtn; CCLabelBMFont* m_updateVersionLabel = nullptr; - InstallHandle m_installation; - InstallItems::CallbackID m_callbackID; MDTextArea* m_detailsArea; MDTextArea* m_changelogArea; Scrollbar* m_scrollbar; - void onHooks(CCObject*); - void onSettings(CCObject*); - void onNoSettings(CCObject*); - void onInfo(CCObject*); - void onEnableMod(CCObject*); - void onInstallMod(CCObject*); - void onCancelInstall(CCObject*); - void onUninstall(CCObject*); - void onDisablingNotSupported(CCObject*); void onChangelog(CCObject*); - void onIssues(CCObject*); void onRepository(CCObject*); void onSupport(CCObject*); - void onOpenConfigDir(CCObject*); - void onAdvancedSettings(CCObject*); - void install(); - void uninstall(); - void updateInstallStatus(std::string const& status, uint8_t progress); + void onInfo(CCObject*); - void modInstallProgress( - InstallHandle handle, UpdateStatus status, std::string const& info, uint8_t percentage - ); - void FLAlert_Clicked(FLAlertLayer*, bool) override; - - bool init(ModObject* obj, ModListView* list); + bool init(ModInfo const& info, ModListView* list); void keyDown(cocos2d::enumKeyCodes) override; void onClose(cocos2d::CCObject*); -public: - static ModInfoLayer* create(Mod* mod, ModListView* list); - static ModInfoLayer* create(ModObject* obj, ModListView* list); - - static void showIssueReportPopup(ModInfo const& info); - - static CCNode* createLogoSpr(ModObject* modObj, CCSize const& size); - static CCNode* createLogoSpr(Mod* mod, CCSize const& size); - static CCNode* createLogoSpr(IndexItem const& item, CCSize const& size); + virtual CCNode* createLogo(CCSize const& size) = 0; + virtual ModInfo getModInfo() const = 0; +}; + +class LocalModInfoPopup : public ModInfoPopup, public FLAlertLayerProtocol { +protected: + Mod* m_mod; + + bool init(Mod* mod, ModListView* list); + + void onIssues(CCObject*); + void onSettings(CCObject*); + void onNoSettings(CCObject*); + void onDisablingNotSupported(CCObject*); + void onEnableMod(CCObject*); + void onUninstall(CCObject*); + void onOpenConfigDir(CCObject*); + void onAdvancedSettings(CCObject*); + void uninstall(); + + void FLAlert_Clicked(FLAlertLayer*, bool) override; + + CCNode* createLogo(CCSize const& size) override; + ModInfo getModInfo() const override; + +public: + static LocalModInfoPopup* create(Mod* mod, ModListView* list); +}; + +class IndexItemInfoPopup : public ModInfoPopup { +protected: + IndexItemHandle m_item; + + bool init(IndexItemHandle item, ModListView* list); + + CCNode* createLogo(CCSize const& size) override; + ModInfo getModInfo() const override; + +public: + static IndexItemInfoPopup* create(IndexItemHandle item, ModListView* list); }; diff --git a/loader/src/ui/internal/list/ModListCell.cpp b/loader/src/ui/internal/list/ModListCell.cpp new file mode 100644 index 00000000..8b9ce74b --- /dev/null +++ b/loader/src/ui/internal/list/ModListCell.cpp @@ -0,0 +1,400 @@ +#include "ModListCell.hpp" +#include "ModListView.hpp" +#include "../info/ModInfoPopup.hpp" +#include +#include +#include +#include +#include +#include +#include + +template +static bool tryOrAlert(Result const& res, char const* title) { + if (!res) { + FLAlertLayer::create(title, res.unwrapErr(), "OK")->show(); + } + return res.isOk(); +} + +ModListCell::ModListCell(char const* name, CCSize const& size) + : TableViewCell(name, size.width, size.height) {} + +void ModListCell::draw() { + reinterpret_cast(this)->StatsCell::draw(); +} + +void ModListCell::setupInfo(ModInfo const& info, bool spaceForCategories) { + m_mainLayer->setVisible(true); + m_backgroundLayer->setOpacity(255); + + m_menu = CCMenu::create(); + m_menu->setPosition(m_width - 40.f, m_height / 2); + m_mainLayer->addChild(m_menu); + + auto logoSize = m_height / 1.5f; + + auto logoSpr = this->createLogo({ logoSize, logoSize }); + logoSpr->setPosition({ logoSize / 2 + 12.f, m_height / 2 }); + m_mainLayer->addChild(logoSpr); + + bool hasDesc = + m_display == ModListDisplay::Expanded && + info.m_description.has_value(); + + auto titleLabel = CCLabelBMFont::create(info.m_name.c_str(), "bigFont.fnt"); + titleLabel->setAnchorPoint({ .0f, .5f }); + titleLabel->setPositionX(m_height / 2 + logoSize / 2 + 13.f); + if (hasDesc && spaceForCategories) { + titleLabel->setPositionY(m_height / 2 + 20.f); + } + else if (hasDesc || spaceForCategories) { + titleLabel->setPositionY(m_height / 2 + 15.f); + } + else { + titleLabel->setPositionY(m_height / 2 + 7.f); + } + titleLabel->limitLabelWidth(m_width / 2 - 40.f, .5f, .1f); + m_mainLayer->addChild(titleLabel); + + auto versionLabel = CCLabelBMFont::create(info.m_version.toString().c_str(), "bigFont.fnt"); + versionLabel->setAnchorPoint({ .0f, .5f }); + versionLabel->setScale(.3f); + versionLabel->setPosition( + titleLabel->getPositionX() + titleLabel->getScaledContentSize().width + 5.f, + titleLabel->getPositionY() - 1.f + ); + versionLabel->setColor({ 0, 255, 0 }); + m_mainLayer->addChild(versionLabel); + + auto creatorStr = "by " + info.m_developer; + auto creatorLabel = CCLabelBMFont::create(creatorStr.c_str(), "goldFont.fnt"); + creatorLabel->setAnchorPoint({ .0f, .5f }); + creatorLabel->setScale(.43f); + creatorLabel->setPositionX(m_height / 2 + logoSize / 2 + 13.f); + if (hasDesc && spaceForCategories) { + creatorLabel->setPositionY(m_height / 2 + 7.5f); + } + else if (hasDesc || spaceForCategories) { + creatorLabel->setPositionY(m_height / 2); + } + else { + creatorLabel->setPositionY(m_height / 2 - 7.f); + } + m_mainLayer->addChild(creatorLabel); + + if (hasDesc) { + auto descBG = CCScale9Sprite::create("square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f }); + descBG->setColor({ 0, 0, 0 }); + descBG->setOpacity(90); + descBG->setContentSize({ m_width * 2, 60.f }); + descBG->setAnchorPoint({ .0f, .5f }); + descBG->setPositionX(m_height / 2 + logoSize / 2 + 13.f); + if (spaceForCategories) { + descBG->setPositionY(m_height / 2 - 7.5f); + } + else { + descBG->setPositionY(m_height / 2 - 17.f); + } + descBG->setScale(.25f); + m_mainLayer->addChild(descBG); + + auto descText = CCLabelBMFont::create(info.m_description.value().c_str(), "chatFont.fnt"); + descText->setAnchorPoint({ .0f, .5f }); + descText->setPosition(m_height / 2 + logoSize / 2 + 18.f, descBG->getPositionY()); + descText->limitLabelWidth(m_width / 2 - 10.f, .5f, .1f); + m_mainLayer->addChild(descText); + } +} + +void ModListCell::updateBGColor(int index) { + if (index % 2) { + m_backgroundLayer->setColor(ccc3(0xc2, 0x72, 0x3e)); + } + else m_backgroundLayer->setColor(ccc3(0xa1, 0x58, 0x2c)); + m_backgroundLayer->setOpacity(0xff); +} + +bool ModListCell::init(ModListView* list, ModListDisplay display) { + m_list = list; + m_display = display; + return true; +} + +// ModCell + +ModCell::ModCell(const char* name, CCSize const& size) + : ModListCell(name, size) {} + +ModCell* ModCell::create( + ModListView* list, ModListDisplay display, + const char* key, CCSize const& size +) { + auto ret = new ModCell(key, size); + if (ret && ret->init(list, display)) { + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +void ModCell::onEnable(CCObject* sender) { + if (!InternalLoader::get()->shownInfoAlert("mod-disable-vs-unload")) { + FLAlertLayer::create( + "Notice", + "Disabling a mod removes its hooks & patches and " + "calls its user-defined disable function if one exists. You may " + "still see some effects of the mod left however, and you may " + "need to restart the game to have it fully unloaded.", + "OK" + )->show(); + m_list->updateAllStates(this); + return; + } + if (!as(sender)->isToggled()) { + tryOrAlert(m_mod->enable(), "Error enabling mod"); + } + else { + tryOrAlert(m_mod->disable(), "Error disabling mod"); + } + m_list->updateAllStates(this); +} + +void ModCell::onUnresolvedInfo(CCObject*) { + std::string info = + "This mod has the following " + "unresolved dependencies: "; + for (auto const& dep : m_mod->getUnresolvedDependencies()) { + info += fmt::format( + "{} ({}), ", + dep.m_id, dep.m_version.toString() + ); + } + info.pop_back(); + info.pop_back(); + FLAlertLayer::create(nullptr, "Unresolved Dependencies", info, "OK", nullptr, 400.f)->show(); +} + +void ModCell::onInfo(CCObject*) { + LocalModInfoPopup::create(m_mod, m_list)->show(); +} + +void ModCell::updateState() { + bool unresolved = m_mod->hasUnresolvedDependencies(); + if (m_enableToggle) { + m_enableToggle->toggle(m_mod->isEnabled()); + m_enableToggle->setEnabled(!unresolved); + m_enableToggle->m_offButton->setOpacity(unresolved ? 100 : 255); + m_enableToggle->m_offButton->setColor(unresolved ? cc3x(155) : cc3x(255)); + m_enableToggle->m_onButton->setOpacity(unresolved ? 100 : 255); + m_enableToggle->m_onButton->setColor(unresolved ? cc3x(155) : cc3x(255)); + } + m_unresolvedExMark->setVisible(unresolved); +} + +void ModCell::loadFromMod(Mod* mod) { + this->setupInfo(mod->getModInfo(), false); + + auto viewSpr = ButtonSprite::create( + "View", "bigFont.fnt", "GJ_button_01.png", .8f + ); + viewSpr->setScale(.65f); + + auto viewBtn = CCMenuItemSpriteExtra::create( + viewSpr, this, menu_selector(ModCell::onInfo) + ); + m_menu->addChild(viewBtn); + + if (m_mod->wasSuccesfullyLoaded() && m_mod->supportsDisabling()) { + m_enableToggle = CCMenuItemToggler::createWithStandardSprites( + this, menu_selector(ModCell::onEnable), .7f + ); + m_enableToggle->setPosition(-45.f, 0.f); + m_menu->addChild(m_enableToggle); + } + + auto exMark = CCSprite::createWithSpriteFrameName("exMark_001.png"); + exMark->setScale(.5f); + + m_unresolvedExMark = CCMenuItemSpriteExtra::create( + exMark, this, menu_selector(ModCell::onUnresolvedInfo) + ); + m_unresolvedExMark->setPosition(-80.f, 0.f); + m_unresolvedExMark->setVisible(false); + m_menu->addChild(m_unresolvedExMark); + + // if (m_mod->wasSuccesfullyLoaded()) { + // if (Index::get()->isUpdateAvailableForItem(m_obj->m_mod->getID())) { + // viewSpr->updateBGImage("GE_button_01.png"_spr); + + // auto updateIcon = CCSprite::createWithSpriteFrameName("updates-available.png"_spr); + // updateIcon->setPosition(viewSpr->getContentSize() - CCSize { 2.f, 2.f }); + // updateIcon->setZOrder(99); + // updateIcon->setScale(.5f); + // viewSpr->addChild(updateIcon); + // } + // } + + this->updateState(); +} + +CCNode* ModCell::createLogo(CCSize const& size) { + return geode::createModLogo(m_mod, size); +} + +// IndexItemCell + +IndexItemCell::IndexItemCell(char const* name, CCSize const& size) + : ModListCell(name, size) {} + +void IndexItemCell::onInfo(CCObject*) { + IndexItemInfoPopup::create(m_item, m_list)->show(); +} + +IndexItemCell* IndexItemCell::create( + ModListView* list, ModListDisplay display, + const char* key, CCSize const& size +) { + auto ret = new IndexItemCell(key, size); + if (ret && ret->init(list, display)) { + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +void IndexItemCell::loadFromItem(IndexItemHandle item) { + this->setupInfo(item->info, true); + + auto viewSpr = ButtonSprite::create( + "View", "bigFont.fnt", "GJ_button_01.png", .8f + ); + viewSpr->setScale(.65f); + + auto viewBtn = CCMenuItemSpriteExtra::create( + viewSpr, this, menu_selector(IndexItemCell::onInfo) + ); + m_menu->addChild(viewBtn); + + // if (hasCategories) { + // float x = m_height / 2 + logoSize / 2 + 13.f; + // for (auto& category : modobj->m_index.m_categories) { + // auto node = CategoryNode::create(category); + // node->setAnchorPoint({ .0f, .5f }); + // node->setPositionX(x); + // node->setScale(.3f); + // if (hasDesc) { + // node->setPositionY(m_height / 2 - 23.f); + // } + // else { + // node->setPositionY(m_height / 2 - 17.f); + // } + // m_mainLayer->addChild(node); + + // x += node->getScaledContentSize().width + 5.f; + // } + // } + + this->updateState(); +} + +void IndexItemCell::updateState() {} + +CCNode* IndexItemCell::createLogo(CCSize const& size) { + return geode::createIndexItemLogo(m_item, size); +} + +// InvalidGeodeFileCell + +InvalidGeodeFileCell::InvalidGeodeFileCell(const char* name, CCSize const& size) + : ModListCell(name, size) {} + +void InvalidGeodeFileCell::onInfo(CCObject*) { + FLAlertLayer::create( + this, "Error Info", + m_info.m_reason, + "OK", "Remove file", 360.f + )->show(); +} + +void InvalidGeodeFileCell::FLAlert_Clicked(FLAlertLayer*, bool btn2) { + if (btn2) { + try { + if (ghc::filesystem::remove(m_info.m_path)) { + FLAlertLayer::create( + "File removed", "Removed " + m_info.m_path.string() + "", "OK" + )->show(); + } + else { + FLAlertLayer::create( + "Unable to remove file", + "Unable to remove " + m_info.m_path.string() + "", "OK" + )->show(); + } + } + catch (std::exception& e) { + FLAlertLayer::create( + "Unable to remove file", + "Unable to remove " + m_info.m_path.string() + ": " + + std::string(e.what()) + "", + "OK" + )->show(); + } + (void)Loader::get()->refreshModsList(); + m_list->refreshList(); + } +} + +InvalidGeodeFileCell* InvalidGeodeFileCell::create( + ModListView* list, ModListDisplay display, + char const* key, CCSize const& size +) { + auto ret = new InvalidGeodeFileCell(key, size); + if (ret && ret->init(list, display)) { + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +void InvalidGeodeFileCell::loadFromInfo(InvalidGeodeFile const& info) { + m_info = info; + + m_mainLayer->setVisible(true); + + auto menu = CCMenu::create(); + menu->setPosition(m_width - m_height, m_height / 2); + m_mainLayer->addChild(menu); + + auto titleLabel = CCLabelBMFont::create("Failed to Load", "bigFont.fnt"); + titleLabel->setAnchorPoint({ .0f, .5f }); + titleLabel->setScale(.5f); + titleLabel->setPosition(m_height / 2, m_height / 2 + 7.f); + m_mainLayer->addChild(titleLabel); + + auto pathLabel = CCLabelBMFont::create( + m_info.m_path.string().c_str(), + "chatFont.fnt" + ); + pathLabel->setAnchorPoint({ .0f, .5f }); + pathLabel->setScale(.43f); + pathLabel->setPosition(m_height / 2, m_height / 2 - 7.f); + pathLabel->setColor({ 255, 255, 0 }); + m_mainLayer->addChild(pathLabel); + + auto whySpr = ButtonSprite::create( + "Info", 0, 0, "bigFont.fnt", "GJ_button_01.png", 0, .8f + ); + whySpr->setScale(.65f); + + auto viewBtn = CCMenuItemSpriteExtra::create( + whySpr, this, menu_selector(InvalidGeodeFileCell::onInfo) + ); + menu->addChild(viewBtn); +} + +void InvalidGeodeFileCell::updateState() {} + +CCNode* InvalidGeodeFileCell::createLogo(CCSize const& size) { + return nullptr; +} diff --git a/loader/src/ui/internal/list/ModListCell.hpp b/loader/src/ui/internal/list/ModListCell.hpp new file mode 100644 index 00000000..1212011b --- /dev/null +++ b/loader/src/ui/internal/list/ModListCell.hpp @@ -0,0 +1,91 @@ +#pragma once + +#include +#include +#include +#include +#include + +USE_GEODE_NAMESPACE(); + +class ModListView; +enum class ModListDisplay; + +class ModListCell : public TableViewCell { +protected: + ModListView* m_list; + CCMenu* m_menu; + CCMenuItemToggler* m_enableToggle = nullptr; + CCMenuItemSpriteExtra* m_unresolvedExMark; + ModListDisplay m_display; + + ModListCell(char const* name, CCSize const& size); + bool init(ModListView* list, ModListDisplay display); + void setupInfo(ModInfo const& info, bool spaceForCategories); + void draw() override; + +public: + void updateBGColor(int index); + virtual void updateState() = 0; + virtual CCNode* createLogo(CCSize const& size) = 0; +}; + +class ModCell : public ModListCell { +protected: + Mod* m_mod; + + ModCell(char const* name, CCSize const& size); + + void onInfo(CCObject*); + void onEnable(CCObject*); + void onUnresolvedInfo(CCObject*); + +public: + static ModCell* create( + ModListView* list, ModListDisplay display, + const char* key, CCSize const& size + ); + + void loadFromMod(Mod* mod); + void updateState() override; + CCNode* createLogo(CCSize const& size) override; +}; + +class IndexItemCell : public ModListCell { +protected: + IndexItemHandle m_item; + + IndexItemCell(char const* name, CCSize const& size); + + void onInfo(CCObject*); + +public: + static IndexItemCell* create( + ModListView* list, ModListDisplay display, + const char* key, CCSize const& size + ); + + void loadFromItem(IndexItemHandle item); + void updateState() override; + CCNode* createLogo(CCSize const& size) override; +}; + +class InvalidGeodeFileCell : public ModListCell, public FLAlertLayerProtocol { +protected: + InvalidGeodeFile m_info; + + InvalidGeodeFileCell(char const* name, CCSize const& size); + + void onInfo(CCObject*); + void FLAlert_Clicked(FLAlertLayer*, bool btn2) override; + +public: + static InvalidGeodeFileCell* create( + ModListView* list, ModListDisplay display, + const char* key, CCSize const& size + ); + + void loadFromInfo(InvalidGeodeFile const& file); + void updateState() override; + CCNode* createLogo(CCSize const& size) override; +}; diff --git a/loader/src/ui/internal/list/ModListLayer.cpp b/loader/src/ui/internal/list/ModListLayer.cpp index 0b8cb3bd..30146aa1 100644 --- a/loader/src/ui/internal/list/ModListLayer.cpp +++ b/loader/src/ui/internal/list/ModListLayer.cpp @@ -205,40 +205,6 @@ void ModListLayer::createSearchControl() { this->addChild(m_searchInput); } -void ModListLayer::indexUpdateProgress( - UpdateStatus status, std::string const& info, uint8_t percentage -) { - // if we have a check for updates button - // visible, disable it from being clicked - // again - if (m_checkForUpdatesBtn) { - m_checkForUpdatesBtn->setEnabled(false); - as(m_checkForUpdatesBtn->getNormalImage())->setString("Updating Index"); - } - - // if finished, refresh list - if (status == UpdateStatus::Finished) { - m_indexUpdateLabel->setVisible(false); - this->reloadList(); - - // make sure to release global instance - // and set it back to null - CC_SAFE_RELEASE_NULL(g_instance); - } - else { - m_indexUpdateLabel->setVisible(true); - m_indexUpdateLabel->setString(info.c_str()); - } - - if (status == UpdateStatus::Failed) { - FLAlertLayer::create("Error Updating Index", info, "OK")->show(); - - // make sure to release global instance - // and set it back to null - CC_SAFE_RELEASE(g_instance); - } -} - void ModListLayer::reloadList() { auto winSize = CCDirector::sharedDirector()->getWinSize(); @@ -249,25 +215,21 @@ void ModListLayer::reloadList() { } // create new list - m_query.m_searchFilter = - m_searchInput && m_searchInput->getString() && strlen(m_searchInput->getString()) - ? std::optional(m_searchInput->getString()) - : std::nullopt; - auto list = ModListView::create(g_tab, m_expandedList, 358.f, 190.f, m_query); + auto list = ModListView::create(g_tab, m_display); list->setLayer(this); // set list status - auto status = list->getStatusAsString(); - if (status.size()) { - m_listLabel->setVisible(true); - m_listLabel->setString(status.c_str()); - } - else { + // auto status = list->getStatusAsString(); + // if (status.size()) { + // m_listLabel->setVisible(true); + // m_listLabel->setString(status.c_str()); + // } + // else { m_listLabel->setVisible(false); - } + // } // update index if needed - if (g_tab == ModListType::Download && !Index::get()->isIndexUpdated()) { + if (g_tab == ModListType::Download && !Index::get()->isUpToDate()) { m_listLabel->setString("Updating index..."); if (!m_loadingCircle) { m_loadingCircle = LoadingCircle::create(); @@ -311,9 +273,9 @@ void ModListLayer::reloadList() { // check if the user has searched something, // and show visual indicator if so - auto hasQuery = m_query.m_searchFilter.has_value(); - m_searchBtn->setVisible(!hasQuery); - m_searchClearBtn->setVisible(hasQuery); + // auto hasQuery = m_query.m_searchFilter.has_value(); + // m_searchBtn->setVisible(!hasQuery); + // m_searchClearBtn->setVisible(hasQuery); // add/remove "Check for Updates" button if ( @@ -321,7 +283,7 @@ void ModListLayer::reloadList() { g_tab == ModListType::Installed && // check if index is updated, and if not // add button if it doesn't exist yet - !Index::get()->isIndexUpdated() + !Index::get()->isUpToDate() ) { if (!m_checkForUpdatesBtn) { auto checkSpr = ButtonSprite::create("Check for Updates"); @@ -351,11 +313,7 @@ void ModListLayer::onCheckForUpdates(CCObject*) { g_instance->retain(); // update index - Index::get()->updateIndex( - [](UpdateStatus status, std::string const& info, uint8_t progress) -> void { - g_instance->indexUpdateProgress(status, info, progress); - } - ); + Index::get()->update(); } void ModListLayer::textChanged(CCTextInputNode* input) { @@ -374,7 +332,9 @@ void ModListLayer::onReload(CCObject*) { } void ModListLayer::onExpand(CCObject* sender) { - m_expandedList = !static_cast(sender)->isToggled(); + m_display = static_cast(sender)->isToggled() ? + ModListDisplay::Concise : + ModListDisplay::Expanded; this->reloadList(); } @@ -443,5 +403,5 @@ ModListLayer* ModListLayer::scene() { } ModListLayer::~ModListLayer() { - removeAllChildrenWithCleanup(true); + this->removeAllChildrenWithCleanup(true); } diff --git a/loader/src/ui/internal/list/ModListLayer.hpp b/loader/src/ui/internal/list/ModListLayer.hpp index 233806a1..68270341 100644 --- a/loader/src/ui/internal/list/ModListLayer.hpp +++ b/loader/src/ui/internal/list/ModListLayer.hpp @@ -25,9 +25,8 @@ protected: CCNode* m_searchBG = nullptr; CCTextInputNode* m_searchInput = nullptr; LoadingCircle* m_loadingCircle = nullptr; - ModListQuery m_query; CCMenuItemSpriteExtra* m_filterBtn; - bool m_expandedList = false; + ModListDisplay m_display = ModListDisplay::Concise; virtual ~ModListLayer(); @@ -43,7 +42,6 @@ protected: void onFilters(CCObject*); void keyDown(enumKeyCodes) override; void textChanged(CCTextInputNode*) override; - void indexUpdateProgress(UpdateStatus status, std::string const& info, uint8_t percentage); void createSearchControl(); friend class SearchFilterPopup; diff --git a/loader/src/ui/internal/list/ModListView.cpp b/loader/src/ui/internal/list/ModListView.cpp index 229a8d5f..673f5096 100644 --- a/loader/src/ui/internal/list/ModListView.cpp +++ b/loader/src/ui/internal/list/ModListView.cpp @@ -1,369 +1,30 @@ #include "ModListView.hpp" #include "../info/CategoryNode.hpp" -#include "../info/ModInfoLayer.hpp" #include "ModListLayer.hpp" +#include "ModListCell.hpp" #include #include -#include #include #include #include #include #include #include -#include +#include #include -template -static bool tryOrAlert(Result const& res, char const* title) { - if (!res) { - FLAlertLayer::create(title, res.unwrapErr(), "OK")->show(); - } - return res.isOk(); -} - -ModCell::ModCell(char const* name, CCSize size) : TableViewCell(name, size.width, size.height) {} - -void ModCell::draw() { - reinterpret_cast(this)->StatsCell::draw(); -} - -void ModCell::onFailedInfo(CCObject*) { - FLAlertLayer::create( - this, "Error Info", - m_obj->m_info.m_reason.size() ? - m_obj->m_info.m_reason : - "Unable to load mod", - "OK", "Remove file", 360.f - )->show(); -} - -void ModCell::FLAlert_Clicked(FLAlertLayer*, bool btn2) { - if (btn2) { - try { - if (ghc::filesystem::remove(m_obj->m_info.m_path)) { - FLAlertLayer::create( - "File removed", "Removed " + m_obj->m_info.m_path.string() + "", "OK" - )->show(); - } - else { - FLAlertLayer::create( - "Unable to remove file", - "Unable to remove " + m_obj->m_info.m_path.string() + "", "OK" - )->show(); - } +void ModListView::updateAllStates(ModListCell* toggled) { + for (auto cell : CCArrayExt(m_tableView->m_cellArray)) { + if (toggled != cell) { + cell->updateState(); } - catch (std::exception& e) { - FLAlertLayer::create( - "Unable to remove file", - "Unable to remove " + m_obj->m_info.m_path.string() + ": " + - std::string(e.what()) + "", - "OK" - )->show(); - } - (void)Loader::get()->refreshModsList(); - m_list->refreshList(); - } -} - -void ModCell::setupUnloaded() { - m_mainLayer->setVisible(true); - - auto menu = CCMenu::create(); - menu->setPosition(m_width - m_height, m_height / 2); - m_mainLayer->addChild(menu); - - auto titleLabel = CCLabelBMFont::create("Failed to Load", "bigFont.fnt"); - titleLabel->setAnchorPoint({ .0f, .5f }); - titleLabel->setScale(.5f); - titleLabel->setPosition(m_height / 2, m_height / 2 + 7.f); - m_mainLayer->addChild(titleLabel); - - auto pathLabel = CCLabelBMFont::create( - m_obj->m_info.m_path.string().c_str(), - "chatFont.fnt" - ); - pathLabel->setAnchorPoint({ .0f, .5f }); - pathLabel->setScale(.43f); - pathLabel->setPosition(m_height / 2, m_height / 2 - 7.f); - pathLabel->setColor({ 255, 255, 0 }); - m_mainLayer->addChild(pathLabel); - - auto whySpr = ButtonSprite::create("Info", 0, 0, "bigFont.fnt", "GJ_button_01.png", 0, .8f); - whySpr->setScale(.65f); - - auto viewBtn = - CCMenuItemSpriteExtra::create(whySpr, this, menu_selector(ModCell::onFailedInfo)); - menu->addChild(viewBtn); -} - -void ModCell::setupLoadedButtons() { - auto viewSpr = m_obj->m_mod->wasSuccesfullyLoaded() - ? ButtonSprite::create("View", "bigFont.fnt", "GJ_button_01.png", .8f) - : ButtonSprite::create("Why", "bigFont.fnt", "GJ_button_06.png", .8f); - viewSpr->setScale(.65f); - - auto viewBtn = CCMenuItemSpriteExtra::create( - viewSpr, this, - m_obj->m_mod->wasSuccesfullyLoaded() ? menu_selector(ModCell::onInfo) - : menu_selector(ModCell::onFailedInfo) - ); - m_menu->addChild(viewBtn); - - if (m_obj->m_mod->wasSuccesfullyLoaded() && m_obj->m_mod->supportsDisabling()) { - m_enableToggle = CCMenuItemToggler::createWithStandardSprites( - this, menu_selector(ModCell::onEnable), .7f - ); - m_enableToggle->setPosition(-45.f, 0.f); - m_menu->addChild(m_enableToggle); - } - - auto exMark = CCSprite::createWithSpriteFrameName("exMark_001.png"); - exMark->setScale(.5f); - - m_unresolvedExMark = - CCMenuItemSpriteExtra::create(exMark, this, menu_selector(ModCell::onUnresolvedInfo)); - m_unresolvedExMark->setPosition(-80.f, 0.f); - m_unresolvedExMark->setVisible(false); - m_menu->addChild(m_unresolvedExMark); - - if (m_obj->m_mod->wasSuccesfullyLoaded()) { - if (Index::get()->isUpdateAvailableForItem(m_obj->m_mod->getID())) { - viewSpr->updateBGImage("GE_button_01.png"_spr); - - auto updateIcon = CCSprite::createWithSpriteFrameName("updates-available.png"_spr); - updateIcon->setPosition(viewSpr->getContentSize() - CCSize { 2.f, 2.f }); - updateIcon->setZOrder(99); - updateIcon->setScale(.5f); - viewSpr->addChild(updateIcon); - } - } -} - -void ModCell::setupIndexButtons() { - auto viewSpr = ButtonSprite::create("View", "bigFont.fnt", "GJ_button_01.png", .8f); - viewSpr->setScale(.65f); - - auto viewBtn = CCMenuItemSpriteExtra::create(viewSpr, this, menu_selector(ModCell::onInfo)); - m_menu->addChild(viewBtn); -} - -void ModCell::loadFromObject(ModObject* modobj) { - m_obj = modobj; - - if (modobj->m_type == ModObjectType::Unloaded) { - return this->setupUnloaded(); - } - - m_mainLayer->setVisible(true); - m_backgroundLayer->setOpacity(255); - - m_menu = CCMenu::create(); - m_menu->setPosition(m_width - 40.f, m_height / 2); - m_mainLayer->addChild(m_menu); - - auto logoSize = m_height / 1.5f; - - auto logoSpr = ModInfoLayer::createLogoSpr(modobj, { logoSize, logoSize }); - logoSpr->setPosition({ logoSize / 2 + 12.f, m_height / 2 }); - m_mainLayer->addChild(logoSpr); - - bool hasCategories = false; - - ModInfo info; - switch (modobj->m_type) { - case ModObjectType::Mod: info = modobj->m_mod->getModInfo(); break; - - case ModObjectType::Index: - info = modobj->m_index.m_info; - hasCategories = m_expanded && modobj->m_index.m_categories.size(); - break; - - default: return; - } - - bool hasDesc = m_expanded && info.m_description.has_value(); - - auto titleLabel = CCLabelBMFont::create(info.m_name.c_str(), "bigFont.fnt"); - titleLabel->setAnchorPoint({ .0f, .5f }); - titleLabel->setPositionX(m_height / 2 + logoSize / 2 + 13.f); - if (hasDesc && hasCategories) { - titleLabel->setPositionY(m_height / 2 + 20.f); - } - else if (hasDesc || hasCategories) { - titleLabel->setPositionY(m_height / 2 + 15.f); - } - else { - titleLabel->setPositionY(m_height / 2 + 7.f); - } - titleLabel->limitLabelWidth(m_width / 2 - 40.f, .5f, .1f); - m_mainLayer->addChild(titleLabel); - - auto versionLabel = CCLabelBMFont::create(info.m_version.toString().c_str(), "bigFont.fnt"); - versionLabel->setAnchorPoint({ .0f, .5f }); - versionLabel->setScale(.3f); - versionLabel->setPosition( - titleLabel->getPositionX() + titleLabel->getScaledContentSize().width + 5.f, - titleLabel->getPositionY() - 1.f - ); - versionLabel->setColor({ 0, 255, 0 }); - m_mainLayer->addChild(versionLabel); - - auto creatorStr = "by " + info.m_developer; - auto creatorLabel = CCLabelBMFont::create(creatorStr.c_str(), "goldFont.fnt"); - creatorLabel->setAnchorPoint({ .0f, .5f }); - creatorLabel->setScale(.43f); - creatorLabel->setPositionX(m_height / 2 + logoSize / 2 + 13.f); - if (hasDesc && hasCategories) { - creatorLabel->setPositionY(m_height / 2 + 7.5f); - } - else if (hasDesc || hasCategories) { - creatorLabel->setPositionY(m_height / 2); - } - else { - creatorLabel->setPositionY(m_height / 2 - 7.f); - } - m_mainLayer->addChild(creatorLabel); - - if (hasDesc) { - auto descBG = CCScale9Sprite::create("square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f }); - descBG->setColor({ 0, 0, 0 }); - descBG->setOpacity(90); - descBG->setContentSize({ m_width * 2, 60.f }); - descBG->setAnchorPoint({ .0f, .5f }); - descBG->setPositionX(m_height / 2 + logoSize / 2 + 13.f); - if (hasCategories) { - descBG->setPositionY(m_height / 2 - 7.5f); - } - else { - descBG->setPositionY(m_height / 2 - 17.f); - } - descBG->setScale(.25f); - m_mainLayer->addChild(descBG); - - auto descText = CCLabelBMFont::create(info.m_description.value().c_str(), "chatFont.fnt"); - descText->setAnchorPoint({ .0f, .5f }); - descText->setPosition(m_height / 2 + logoSize / 2 + 18.f, descBG->getPositionY()); - descText->limitLabelWidth(m_width / 2 - 10.f, .5f, .1f); - m_mainLayer->addChild(descText); - } - - if (hasCategories) { - float x = m_height / 2 + logoSize / 2 + 13.f; - for (auto& category : modobj->m_index.m_categories) { - auto node = CategoryNode::create(category); - node->setAnchorPoint({ .0f, .5f }); - node->setPositionX(x); - node->setScale(.3f); - if (hasDesc) { - node->setPositionY(m_height / 2 - 23.f); - } - else { - node->setPositionY(m_height / 2 - 17.f); - } - m_mainLayer->addChild(node); - - x += node->getScaledContentSize().width + 5.f; - } - } - - switch (modobj->m_type) { - case ModObjectType::Mod: this->setupLoadedButtons(); break; - - case ModObjectType::Index: this->setupIndexButtons(); break; - - default: break; - } - this->updateState(); -} - -void ModCell::onInfo(CCObject*) { - ModInfoLayer::create(m_obj, m_list)->show(); -} - -void ModCell::updateBGColor(int index) { - if (index & 1) m_backgroundLayer->setColor(ccc3(0xc2, 0x72, 0x3e)); - else m_backgroundLayer->setColor(ccc3(0xa1, 0x58, 0x2c)); - m_backgroundLayer->setOpacity(0xff); -} - -void ModCell::onEnable(CCObject* pSender) { - if (!InternalLoader::get()->shownInfoAlert("mod-disable-vs-unload")) { - FLAlertLayer::create( - "Notice", - "Disabling a mod removes its hooks & patches and " - "calls its user-defined disable function if one exists. You may " - "still see some effects of the mod left however, and you may " - "need to restart the game to have it fully unloaded.", - "OK" - )->show(); - m_list->updateAllStates(this); - return; - } - if (!as(pSender)->isToggled()) { - tryOrAlert(m_obj->m_mod->enable(), "Error enabling mod"); - } - else { - tryOrAlert(m_obj->m_mod->disable(), "Error disabling mod"); - } - m_list->updateAllStates(this); -} - -void ModCell::onUnresolvedInfo(CCObject* pSender) { - std::string info = - "This mod has the following " - "unresolved dependencies: "; - for (auto const& dep : m_obj->m_mod->getUnresolvedDependencies()) { - info += "" + dep.m_id + - " " - "(" + - dep.m_version.toString() + "), "; - } - info.pop_back(); - info.pop_back(); - FLAlertLayer::create(nullptr, "Unresolved Dependencies", info, "OK", nullptr, 400.f)->show(); -} - -bool ModCell::init(ModListView* list, bool expanded) { - m_list = list; - m_expanded = expanded; - return true; -} - -void ModCell::updateState(bool invert) { - if (m_obj->m_type == ModObjectType::Mod) { - bool unresolved = m_obj->m_mod->hasUnresolvedDependencies(); - if (m_enableToggle) { - m_enableToggle->toggle(m_obj->m_mod->isEnabled() ^ invert); - m_enableToggle->setEnabled(!unresolved); - m_enableToggle->m_offButton->setOpacity(unresolved ? 100 : 255); - m_enableToggle->m_offButton->setColor(unresolved ? cc3x(155) : cc3x(255)); - m_enableToggle->m_onButton->setOpacity(unresolved ? 100 : 255); - m_enableToggle->m_onButton->setColor(unresolved ? cc3x(155) : cc3x(255)); - } - m_unresolvedExMark->setVisible(unresolved); - } -} - -ModCell* ModCell::create(ModListView* list, bool expanded, char const* key, CCSize size) { - auto pRet = new ModCell(key, size); - if (pRet && pRet->init(list, expanded)) { - return pRet; - } - CC_SAFE_DELETE(pRet); - return nullptr; -} - -void ModListView::updateAllStates(ModCell* toggled) { - for (auto cell : CCArrayExt(m_tableView->m_cellArray)) { - cell->updateState(toggled == cell); } } void ModListView::setupList() { - m_itemSeparation = m_expandedList ? 60.f : 40.0f; + m_itemSeparation = m_display == ModListDisplay::Expanded ? 60.f : 40.0f; if (!m_entries->count()) return; @@ -372,8 +33,10 @@ void ModListView::setupList() { // fix content layer content size so the // list is properly aligned to the top auto coverage = calculateChildCoverage(m_tableView->m_contentLayer); - m_tableView->m_contentLayer->setContentSize({ -coverage.origin.x + coverage.size.width, - -coverage.origin.y + coverage.size.height }); + m_tableView->m_contentLayer->setContentSize({ + -coverage.origin.x + coverage.size.width, + -coverage.origin.y + coverage.size.height + }); if (m_entries->count() == 1) { m_tableView->moveToTopWithOffset(m_itemSeparation * 2); @@ -387,72 +50,21 @@ void ModListView::setupList() { } TableViewCell* ModListView::getListCell(char const* key) { - return ModCell::create(this, m_expandedList, key, { m_width, m_itemSeparation }); + return ModCell::create(this, m_display, key, { m_width, m_itemSeparation }); } void ModListView::loadCell(TableViewCell* cell, unsigned int index) { - auto obj = as(m_entries->objectAtIndex(index)); - as(cell)->loadFromObject(obj); - if (obj->m_type == ModObjectType::Mod) { - if (obj->m_mod->wasSuccesfullyLoaded()) { - as(cell)->updateBGColor(index); - } - else { - cell->m_backgroundLayer->setOpacity(255); - cell->m_backgroundLayer->setColor({ 153, 0, 0 }); - } - if (obj->m_mod->isUninstalled()) { - cell->m_backgroundLayer->setColor({ 50, 50, 50 }); - } + auto obj = m_entries->objectAtIndex(index); + if (auto mod = typeinfo_cast(obj)) { + as(cell)->loadFromMod(mod->mod); } - else { - as(cell)->updateBGColor(index); + if (auto mod = typeinfo_cast(obj)) { + // as(cell)->loadFromItem(mod->item); } -} - -bool ModListView::filter(ModInfo const& info, ModListQuery const& query) { - // the UI for this functionality has been removed, however - // the code has been kept in case we want to add it back at - // some point. - - if (!query.m_searchFilter) return true; - auto check = [query](SearchFlags flag, std::string const& name) -> bool { - if (!(query.m_searchFlags & flag)) return false; - return utils::string::contains( - utils::string::toLower(name), utils::string::toLower(query.m_searchFilter.value()) - ); - }; - if (check(SearchFlag::Name, info.m_name)) return true; - if (check(SearchFlag::ID, info.m_id)) return true; - if (check(SearchFlag::Developer, info.m_developer)) return true; - if (check(SearchFlag::Description, info.m_description.value_or(""))) return true; - if (check(SearchFlag::Details, info.m_details.value_or(""))) return true; - return false; -} - -bool ModListView::filter(IndexItem const& item, ModListQuery const& query) { - if (!query.m_showInstalled) { - if (Loader::get()->isModInstalled(item.m_info.m_id)) { - return false; - } + if (auto failed = typeinfo_cast(obj)) { + as(cell)->loadFromInfo(failed->info); } - if (query.m_categories.size()) { - bool found = false; - for (auto& cat : query.m_categories) { - if (item.m_categories.count(cat)) { - found = true; - } - } - if (!found) { - return false; - } - } - for (auto& plat : query.m_platforms) { - if (item.m_download.m_platforms.count(plat)) { - return filter(item.m_info, query); - } - } - return false; + as(cell)->updateBGColor(index); } static void sortInstalledMods(std::vector& mods) { @@ -462,16 +74,18 @@ static void sortInstalledMods(std::vector& mods) { auto front = mods.front(); for (auto mod = mods.begin(); mod != mods.end(); mod++) { // move mods with updates to front - if (Index::get()->isUpdateAvailableForItem((*mod)->getID())) { - // swap first object and updatable mod - // if the updatable mod is the first object, - // nothing changes - std::rotate(mods.begin(), mod, mod + 1); + if (auto item = Index::get()->getItem(*mod)) { + if (Index::get()->updateAvailable(item)) { + // swap first object and updatable mod + // if the updatable mod is the first object, + // nothing changes + std::rotate(mods.begin(), mod, mod + 1); - // get next object at front for next mod - // to sort - frontIndex++; - front = mods[frontIndex]; + // get next object at front for next mod + // to sort + frontIndex++; + front = mods[frontIndex]; + } } } } @@ -482,81 +96,50 @@ static std::vector sortedInstalledMods() { return std::move(mods); } -bool ModListView::init( - CCArray* mods, ModListType type, bool expanded, float width, float height, ModListQuery query -) { - m_expandedList = expanded; - if (!mods) { - switch (type) { - case ModListType::Installed: - { - mods = CCArray::create(); - // failed mods first - for (auto const& mod : Loader::get()->getFailedMods()) { - mods->addObject(new ModObject(mod)); - } - // internal geode representation always at the top - auto imod = Loader::getInternalMod(); - if (this->filter(imod->getModInfo(), query)) { - mods->addObject(new ModObject(imod)); - } - // then other mods - for (auto const& mod : sortedInstalledMods()) { - // if the mod is no longer installed nor - // loaded, it's as good as not existing - // (because it doesn't) - if (mod->isUninstalled() && !mod->isLoaded()) continue; - if (this->filter(mod->getModInfo(), query)) { - mods->addObject(new ModObject(mod)); - } - } - if (!mods->count()) { - m_status = Status::SearchEmpty; - } - } - break; - - case ModListType::Download: - { - mods = CCArray::create(); - for (auto const& item : Index::get()->getItems()) { - if (this->filter(item, query)) { - mods->addObject(new ModObject(item)); - } - } - if (!mods->count()) { - m_status = Status::NoModsFound; - } - } - break; - - case ModListType::Featured: - { - mods = CCArray::create(); - for (auto const& item : Index::get()->getFeaturedItems()) { - if (this->filter(item, query)) { - mods->addObject(new ModObject(item)); - } - } - if (!mods->count()) { - m_status = Status::NoModsFound; - } - } - break; - - default: return false; - } - } - return CustomListView::init(mods, BoomListType::Default, width, height); +bool ModListView::init(CCArray* mods, ModListDisplay display) { + m_display = display; + return CustomListView::init(mods, BoomListType::Default, 358.f, 190.f); } -ModListView* ModListView::create( - CCArray* mods, ModListType type, bool expanded, float width, float height, - ModListQuery const& query -) { +CCArray* ModListView::getModsForType(ModListType type) { + auto mods = CCArray::create(); + switch (type) { + default: + case ModListType::Installed: { + // failed mods first + for (auto const& mod : Loader::get()->getFailedMods()) { + mods->addObject(new InvalidGeodeFileObject(mod)); + } + // internal geode representation always at the top + auto imod = Loader::getInternalMod(); + mods->addObject(new ModObject(imod)); + + // then other mods + for (auto const& mod : sortedInstalledMods()) { + // if the mod is no longer installed nor + // loaded, it's as good as not existing + // (because it doesn't) + if (mod->isUninstalled() && !mod->isLoaded()) continue; + mods->addObject(new ModObject(mod)); + } + } break; + + case ModListType::Download: { + for (auto const& item : Index::get()->getItems()) { + mods->addObject(new IndexItemObject(item)); + } + } break; + + case ModListType::Featured: { + } break; + } + return mods; +} + +ModListView* ModListView::create(CCArray* mods, ModListDisplay display) { auto pRet = new ModListView; if (pRet) { - if (pRet->init(mods, type, expanded, width, height, query)) { + if (pRet->init(mods, display)) { pRet->autorelease(); return pRet; } @@ -565,24 +148,8 @@ ModListView* ModListView::create( return nullptr; } -ModListView* ModListView::create( - ModListType type, bool expanded, float width, float height, ModListQuery const& query -) { - return ModListView::create(nullptr, type, expanded, width, height, query); -} - -ModListView::Status ModListView::getStatus() const { - return m_status; -} - -std::string ModListView::getStatusAsString() const { - switch (m_status) { - case Status::OK: return ""; - case Status::Unknown: return "Unknown Issue"; - case Status::NoModsFound: return "No Mods Found"; - case Status::SearchEmpty: return "No Mods Match Search Query"; - } - return "Unrecorded Status"; +ModListView* ModListView::create(ModListType type, ModListDisplay display) { + return ModListView::create(getModsForType(type), display); } void ModListView::setLayer(ModListLayer* layer) { diff --git a/loader/src/ui/internal/list/ModListView.hpp b/loader/src/ui/internal/list/ModListView.hpp index 0813078f..7d363335 100644 --- a/loader/src/ui/internal/list/ModListView.hpp +++ b/loader/src/ui/internal/list/ModListView.hpp @@ -9,143 +9,59 @@ USE_GEODE_NAMESPACE(); -struct ModListQuery; - enum class ModListType { Installed, Download, Featured, }; -enum class ModObjectType { - Mod, - Invalid, - Index, +enum class ModListDisplay { + Concise, + Expanded, }; class ModListLayer; +class ModListCell; + +// for passing invalid files as CCObject +struct InvalidGeodeFileObject : public CCObject { + InvalidGeodeFile info; + inline InvalidGeodeFileObject(InvalidGeodeFile const& info) : info(info) { + this->autorelease(); + } +}; -// Wrapper so you can pass Mods in a CCArray struct ModObject : public CCObject { - ModObjectType m_type; - std::variant m_data; - - inline ModObject(Mod* mod) - : m_data(mod), m_type(ModObjectType::Mod) - { - this->autorelease(); - } - - inline ModObject(InvalidGeodeFile const& info) - : m_data(info), m_type(ModObjectType::Invalid) - { - this->autorelease(); - } - - inline ModObject(IndexItem const& index) - : m_data(index), m_type(ModObjectType::Index) - { + Mod* mod; + inline ModObject(Mod* mod) : mod(mod) { this->autorelease(); } }; -class ModListView; - -class ModCell : public TableViewCell, public FLAlertLayerProtocol { -protected: - ModListView* m_list; - ModObject* m_obj; - CCMenu* m_menu; - CCMenuItemToggler* m_enableToggle = nullptr; - CCMenuItemSpriteExtra* m_unresolvedExMark; - bool m_expanded; - - ModCell(char const* name, CCSize size); - - void draw() override; - void onInfo(CCObject*); - void onFailedInfo(CCObject*); - void onEnable(CCObject*); - void onUnresolvedInfo(CCObject*); - - void setupUnloaded(); - void setupLoadedButtons(); - void setupIndexButtons(); - - void FLAlert_Clicked(FLAlertLayer*, bool btn2) override; - - bool init(ModListView* list, bool expanded); - -public: - void updateBGColor(int index); - void loadFromObject(ModObject*); - void updateState(bool invert = false); - - static ModCell* create(ModListView* list, bool expanded, char const* key, CCSize size); -}; - -struct SearchFlag { - enum : int { - Name = 0b1, - ID = 0b10, - Developer = 0b100, - Credits = 0b1000, - Description = 0b10000, - Details = 0b100000, - }; -}; - -using SearchFlags = int; - -static constexpr SearchFlags ALL_FLAGS = SearchFlag::Name | SearchFlag::ID | SearchFlag::Developer | - SearchFlag::Credits | SearchFlag::Description | SearchFlag::Details; - -struct ModListQuery { - std::optional m_searchFilter = std::nullopt; - int m_searchFlags = ALL_FLAGS; - bool m_showInstalled = false; - std::unordered_set m_platforms { GEODE_PLATFORM_TARGET }; - std::unordered_set m_categories {}; +struct IndexItemObject : public CCObject { + IndexItemHandle item; + inline IndexItemObject(IndexItemHandle item) : item(item) { + this->autorelease(); + } }; class ModListView : public CustomListView { protected: - enum class Status { - OK, - Unknown, - NoModsFound, - SearchEmpty, - }; - - Status m_status = Status::OK; ModListLayer* m_layer = nullptr; - bool m_expandedList; + ModListDisplay m_display; void setupList() override; TableViewCell* getListCell(char const* key) override; void loadCell(TableViewCell* cell, unsigned int index) override; - bool init( - CCArray* mods, ModListType type, bool expanded, float width, float height, - ModListQuery query - ); - bool filter(ModInfo const& info, ModListQuery const& query); - bool filter(IndexItem const& item, ModListQuery const& query); + bool init(CCArray* mods, ModListDisplay display); public: - static ModListView* create( - CCArray* mods, ModListType type = ModListType::Installed, bool expanded = false, - float width = 358.f, float height = 220.f, ModListQuery const& query = ModListQuery() - ); - static ModListView* create( - ModListType type, bool expanded = false, float width = 358.f, float height = 220.f, - ModListQuery const& query = ModListQuery() - ); + static ModListView* create(CCArray* mods, ModListDisplay display); + static ModListView* create(ModListType type, ModListDisplay display); + static CCArray* getModsForType(ModListType type); - void updateAllStates(ModCell* toggled = nullptr); + void updateAllStates(ModListCell* except = nullptr); void setLayer(ModListLayer* layer); void refreshList(); - - Status getStatus() const; - std::string getStatusAsString() const; }; diff --git a/loader/src/ui/internal/list/SearchFilterPopup.cpp b/loader/src/ui/internal/list/SearchFilterPopup.cpp index 0d384a4c..504be2d5 100644 --- a/loader/src/ui/internal/list/SearchFilterPopup.cpp +++ b/loader/src/ui/internal/list/SearchFilterPopup.cpp @@ -5,6 +5,7 @@ #include "ModListView.hpp" #include +#include #include bool SearchFilterPopup::setup(ModListLayer* layer, ModListType type) { @@ -53,10 +54,10 @@ bool SearchFilterPopup::setup(ModListLayer* layer, ModListType type) { pos = CCPoint { winSize.width / 2 - 140.f, winSize.height / 2 - 85.f }; - this->addToggle( - "Show Installed", menu_selector(SearchFilterPopup::onShowInstalled), - layer->m_query.m_showInstalled, 0, pos - ); + // this->addToggle( + // "Show Installed", menu_selector(SearchFilterPopup::onShowInstalled), + // layer->m_query.m_showInstalled, 0, pos + // ); // categories @@ -75,23 +76,23 @@ bool SearchFilterPopup::setup(ModListLayer* layer, ModListType type) { pos = CCPoint { winSize.width / 2 + 30.f, winSize.height / 2 + 45.f }; - for (auto& category : Index::get()->getCategories()) { - auto toggle = CCMenuItemToggler::createWithStandardSprites( - this, menu_selector(SearchFilterPopup::onCategory), .5f - ); - toggle->toggle(m_modLayer->m_query.m_categories.count(category)); - toggle->setPosition(pos - winSize / 2); - toggle->setUserObject(CCString::create(category)); - m_buttonMenu->addChild(toggle); + // for (auto& category : Index::get()->getCategories()) { + // auto toggle = CCMenuItemToggler::createWithStandardSprites( + // this, menu_selector(SearchFilterPopup::onCategory), .5f + // ); + // toggle->toggle(m_modLayer->m_query.m_categories.count(category)); + // toggle->setPosition(pos - winSize / 2); + // toggle->setUserObject(CCString::create(category)); + // m_buttonMenu->addChild(toggle); - auto label = CategoryNode::create(category, CategoryNodeStyle::Dot); - label->setScale(.4f); - label->setAnchorPoint({ .0f, .5f }); - label->setPosition(pos.x + 10.f, pos.y); - m_mainLayer->addChild(label); + // auto label = CategoryNode::create(category, CategoryNodeStyle::Dot); + // label->setScale(.4f); + // label->setAnchorPoint({ .0f, .5f }); + // label->setPosition(pos.x + 10.f, pos.y); + // m_mainLayer->addChild(label); - pos.y -= 22.5f; - } + // pos.y -= 22.5f; + // } return true; } @@ -99,13 +100,13 @@ bool SearchFilterPopup::setup(ModListLayer* layer, ModListType type) { void SearchFilterPopup::onCategory(CCObject* sender) { try { auto toggle = static_cast(sender); - auto category = static_cast(toggle->getUserObject())->getCString(); - if (!toggle->isToggled()) { - m_modLayer->m_query.m_categories.insert(category); - } - else { - m_modLayer->m_query.m_categories.erase(category); - } + // auto category = static_cast(toggle->getUserObject())->getCString(); + // if (!toggle->isToggled()) { + // m_modLayer->m_query.m_categories.insert(category); + // } + // else { + // m_modLayer->m_query.m_categories.erase(category); + // } } catch (...) { } @@ -113,7 +114,7 @@ void SearchFilterPopup::onCategory(CCObject* sender) { void SearchFilterPopup::onShowInstalled(CCObject* sender) { auto toggle = static_cast(sender); - m_modLayer->m_query.m_showInstalled = !toggle->isToggled(); + // m_modLayer->m_query.m_showInstalled = !toggle->isToggled(); } void SearchFilterPopup::enable(CCMenuItemToggler* toggle, ModListType type) { @@ -137,37 +138,39 @@ CCMenuItemToggler* SearchFilterPopup::addToggle( } CCMenuItemToggler* SearchFilterPopup::addSearchMatch(char const* title, int flag, CCPoint& pos) { - return this->addToggle( - title, menu_selector(SearchFilterPopup::onSearchToggle), - m_modLayer->m_query.m_searchFlags & flag, flag, pos - ); + // return this->addToggle( + // title, menu_selector(SearchFilterPopup::onSearchToggle), + // m_modLayer->m_query.m_searchFlags & flag, flag, pos + // ); + return nullptr; } CCMenuItemToggler* SearchFilterPopup::addPlatformToggle( char const* title, PlatformID id, CCPoint& pos ) { - return this->addToggle( - title, menu_selector(SearchFilterPopup::onPlatformToggle), - m_modLayer->m_query.m_platforms.count(id), id.to(), pos - ); + // return this->addToggle( + // title, menu_selector(SearchFilterPopup::onPlatformToggle), + // m_modLayer->m_query.m_platforms.count(id), id.to(), pos + // ); + return nullptr; } void SearchFilterPopup::onSearchToggle(CCObject* sender) { - if (static_cast(sender)->isToggled()) { - m_modLayer->m_query.m_searchFlags &= ~sender->getTag(); - } - else { - m_modLayer->m_query.m_searchFlags |= sender->getTag(); - } + // if (static_cast(sender)->isToggled()) { + // m_modLayer->m_query.m_searchFlags &= ~sender->getTag(); + // } + // else { + // m_modLayer->m_query.m_searchFlags |= sender->getTag(); + // } } void SearchFilterPopup::onPlatformToggle(CCObject* sender) { - if (static_cast(sender)->isToggled()) { - m_modLayer->m_query.m_platforms.erase(PlatformID::from(sender->getTag())); - } - else { - m_modLayer->m_query.m_platforms.insert(PlatformID::from(sender->getTag())); - } + // if (static_cast(sender)->isToggled()) { + // m_modLayer->m_query.m_platforms.erase(PlatformID::from(sender->getTag())); + // } + // else { + // m_modLayer->m_query.m_platforms.insert(PlatformID::from(sender->getTag())); + // } } void SearchFilterPopup::onClose(CCObject* sender) { diff --git a/loader/src/utils/file.cpp b/loader/src/utils/file.cpp index a6998652..0414e6e4 100644 --- a/loader/src/utils/file.cpp +++ b/loader/src/utils/file.cpp @@ -28,6 +28,22 @@ Result utils::file::readString(ghc::filesystem::path const& path) { return Err("Unable to open file"); } +Result utils::file::readJson(ghc::filesystem::path const& path) { +#if _WIN32 + std::ifstream in(path.wstring(), std::ios::in | std::ios::binary); +#else + std::ifstream in(path.string(), std::ios::in | std::ios::binary); +#endif + if (in) { + try { + return Ok(nlohmann::json::parse(in)); + } catch(std::exception const& e) { + return Err("Unable to parse JSON: " + std::string(e.what())); + } + } + return Err("Unable to open file"); +} + Result utils::file::readBinary(ghc::filesystem::path const& path) { #if _WIN32 std::ifstream in(path.wstring(), std::ios::in | std::ios::binary);