diff --git a/loader/include/Geode/loader/Index.hpp b/loader/include/Geode/loader/Index.hpp index 4a027823..fb0830c1 100644 --- a/loader/include/Geode/loader/Index.hpp +++ b/loader/include/Geode/loader/Index.hpp @@ -115,6 +115,7 @@ namespace geode { std::unordered_set<PlatformID> getAvailablePlatforms() const; bool isFeatured() const; std::unordered_set<std::string> getTags() const; + bool isInstalled() const; IndexItem(); ~IndexItem(); diff --git a/loader/include/Geode/loader/Loader.hpp b/loader/include/Geode/loader/Loader.hpp index 96e542d8..1b38dd69 100644 --- a/loader/include/Geode/loader/Loader.hpp +++ b/loader/include/Geode/loader/Loader.hpp @@ -4,6 +4,7 @@ #include "../utils/Result.hpp" #include "../utils/MiniFunction.hpp" #include "Log.hpp" +#include "ModEvent.hpp" #include "ModInfo.hpp" #include "ModMetadata.hpp" #include "Types.hpp" @@ -19,6 +20,24 @@ namespace geode { std::string reason; }; + struct LoadProblem { + enum class Type : uint8_t { + Unknown, + Suggestion, + Recommendation, + InvalidFile, + Duplicate, + SetupFailed, + LoadFailed, + EnableFailed, + MissingDependency, + PresentIncompatibility + }; + Type type; + std::variant<ghc::filesystem::path, ModMetadata, Mod*> cause; + std::string message; + }; + class LoaderImpl; class GEODE_DLL Loader { @@ -37,8 +56,8 @@ namespace geode { void dispatchScheduledFunctions(Mod* mod); friend void GEODE_CALL ::geode_implicit_load(Mod*); - Result<Mod*> loadModFromInfo(ModInfo const& info); - + [[deprecated]] Result<Mod*> loadModFromInfo(ModInfo const& info); + Mod* takeNextMod(); public: @@ -53,16 +72,18 @@ namespace geode { VersionInfo maxModVersion(); bool isModVersionSupported(VersionInfo const& version); - Result<Mod*> loadModFromFile(ghc::filesystem::path const& file); - void loadModsFromDirectory(ghc::filesystem::path const& dir, bool recursive = true); - void refreshModsList(); + [[deprecated]] Result<Mod*> loadModFromFile(ghc::filesystem::path const& file); + [[deprecated]] void loadModsFromDirectory(ghc::filesystem::path const& dir, bool recursive = true); + [[deprecated]] void refreshModsList(); bool isModInstalled(std::string const& id) const; Mod* getInstalledMod(std::string const& id) const; bool isModLoaded(std::string const& id) const; Mod* getLoadedMod(std::string const& id) const; std::vector<Mod*> getAllMods(); - Mod* getModImpl(); [[deprecated("use Mod::get instead")]] Mod* getModImpl(); + [[deprecated]] void updateAllDependencies(); + [[deprecated("use getProblems instead")]] std::vector<InvalidGeodeFile> getFailedMods() const; + std::vector<LoadProblem> getProblems() const; void updateResources(); void updateResources(bool forceReload); diff --git a/loader/include/Geode/loader/Mod.hpp b/loader/include/Geode/loader/Mod.hpp index 8a0d869f..569a2a6d 100644 --- a/loader/include/Geode/loader/Mod.hpp +++ b/loader/include/Geode/loader/Mod.hpp @@ -80,11 +80,10 @@ namespace geode { bool isEnabled() const; bool isLoaded() const; bool supportsDisabling() const; - bool supportsUnloading() const; - bool wasSuccesfullyLoaded() const; - ModInfo getModInfo() const; + bool canDisable() const; + bool canEnable() const; [[deprecated]] bool supportsUnloading() const; - [[deprecated("wasSuccessfullyLoaded")]] bool wasSuccesfullyLoaded() const; + [[deprecated("use wasSuccessfullyLoaded instead")]] bool wasSuccesfullyLoaded() const; bool wasSuccessfullyLoaded() const; [[deprecated("use getMetadata instead")]] ModInfo getModInfo() const; ModMetadata getMetadata() const; @@ -299,7 +298,7 @@ namespace geode { * @returns Successful result on success, * errorful result with info on error */ - Result<> loadBinary(); + [[deprecated]] Result<> loadBinary(); /** * Disable & unload this mod @@ -308,7 +307,7 @@ namespace geode { * @returns Successful result on success, * errorful result with info on error */ - Result<> unloadBinary(); + [[deprecated]] Result<> unloadBinary(); /** * Enable this mod @@ -325,10 +324,7 @@ namespace geode { Result<> disable(); /** - * Disable & unload this mod (if supported), then delete the mod's - * .geode package. If unloading isn't supported, the mod's binary - * will stay loaded, and in all cases the Mod* instance will still - * exist and be interactable. + * Disable this mod (if supported), then delete the mod's .geode package. * @returns Successful result on success, * errorful result with info on error */ @@ -341,6 +337,16 @@ namespace geode { */ bool depends(std::string const& id) const; + /** + * Update the state of each of the + * dependencies. Depending on if the + * mod has unresolved dependencies, + * it will either be loaded or unloaded + * @returns Error. + * @deprecated No longer needed. + */ + [[deprecated("no longer needed")]] Result<> updateDependencies(); + /** * Check whether all the required * dependencies for this mod have @@ -350,21 +356,20 @@ namespace geode { */ bool hasUnresolvedDependencies() const; /** - * Update the state of each of the - * dependencies. Depending on if the - * mod has unresolved dependencies, - * it will either be loaded or unloaded + * Check whether none of the + * incompatibilities with this mod are loaded * @returns True if the mod has unresolved - * dependencies, false if not. + * incompatibilities, false if not. */ - Result<> updateDependencies(); + bool hasUnresolvedIncompatibilities() const; /** * Get a list of all the unresolved * dependencies this mod has * @returns List of all the unresolved * dependencies + * @deprecated Use Loader::getProblems instead. */ - std::vector<Dependency> getUnresolvedDependencies(); + [[deprecated("use Loader::getProblems instead")]] std::vector<Dependency> getUnresolvedDependencies(); char const* expandSpriteName(char const* name); diff --git a/loader/include/Geode/loader/ModInfo.hpp b/loader/include/Geode/loader/ModInfo.hpp index feefcbc6..431fd653 100644 --- a/loader/include/Geode/loader/ModInfo.hpp +++ b/loader/include/Geode/loader/ModInfo.hpp @@ -36,6 +36,7 @@ namespace geode { */ class GEODE_DLL [[deprecated("use ModMetadata instead")]] ModInfo { class Impl; +#pragma warning(suppress : 4996) std::unique_ptr<Impl> m_impl; public: @@ -200,18 +201,29 @@ namespace geode { operator ModMetadata() const; private: + ModJson& rawJSON(); + ModJson const& rawJSON() const; + /** + * Version is passed for backwards + * compatibility if we update the mod.json + * format + */ + static Result<ModInfo> createFromSchemaV010(ModJson const& json); + + Result<> addSpecialFiles(ghc::filesystem::path const& dir); + Result<> addSpecialFiles(utils::file::Unzip& zip); + + std::vector<std::pair<std::string, std::optional<std::string>*>> getSpecialFiles(); + friend class ModInfoImpl; friend class ModMetadata; }; } -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" template <> -struct json::Serialize<geode::ModInfo> { +struct [[deprecated]] json::Serialize<geode::ModInfo> { static json::Value to_json(geode::ModInfo const& info) { return info.toJSON(); } }; -#pragma clang diagnostic pop diff --git a/loader/include/Geode/loader/ModMetadata.hpp b/loader/include/Geode/loader/ModMetadata.hpp index 4364d77b..23d2f173 100644 --- a/loader/include/Geode/loader/ModMetadata.hpp +++ b/loader/include/Geode/loader/ModMetadata.hpp @@ -44,14 +44,10 @@ namespace geode { Mod* mod = nullptr; [[nodiscard]] bool isResolved() const; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" -#pragma ide diagnostic ignored "google-explicit-constructor" - operator geode::Dependency(); - operator geode::Dependency() const; + [[deprecated]] operator geode::Dependency(); + [[deprecated]] operator geode::Dependency() const; - static Dependency fromDeprecated(geode::Dependency const& value); -#pragma clang diagnostic pop + [[deprecated]] static Dependency fromDeprecated(geode::Dependency const& value); }; struct GEODE_DLL Incompatibility { @@ -65,29 +61,25 @@ namespace geode { std::string info; std::optional<std::string> url; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" -#pragma ide diagnostic ignored "google-explicit-constructor" - operator geode::IssuesInfo(); - operator geode::IssuesInfo() const; + [[deprecated]] operator geode::IssuesInfo(); + [[deprecated]] operator geode::IssuesInfo() const; - static IssuesInfo fromDeprecated(geode::IssuesInfo const& value); -#pragma clang diagnostic pop + [[deprecated]] static IssuesInfo fromDeprecated(geode::IssuesInfo const& value); }; /** * Path to the mod file */ - [[maybe_unused, nodiscard]] ghc::filesystem::path getPath() const; + [[nodiscard]] ghc::filesystem::path getPath() const; /** * Name of the platform binary within * the mod zip */ - [[maybe_unused, nodiscard]] std::string getBinaryName() const; + [[nodiscard]] std::string getBinaryName() const; /** * Mod Version. Should follow semantic versioning. */ - [[maybe_unused, nodiscard]] VersionInfo getVersion() const; + [[nodiscard]] VersionInfo getVersion() const; /** * Human-readable ID of the Mod. * Recommended to be in the format @@ -97,14 +89,14 @@ namespace geode { * be restricted to the ASCII * character set. */ - [[maybe_unused, nodiscard]] std::string getID() const; + [[nodiscard]] std::string getID() const; /** * Name of the mod. May contain * spaces & punctuation, but should * be restricted to the ASCII * character set. */ - [[maybe_unused, nodiscard]] std::string getName() const; + [[nodiscard]] std::string getName() const; /** * The name of the head developer. * Should be a single name, like @@ -116,98 +108,94 @@ namespace geode { * should be named in `m_credits` * instead. */ - [[maybe_unused, nodiscard]] std::string getDeveloper() const; + [[nodiscard]] std::string getDeveloper() const; /** * Short & concise description of the * mod. */ - [[maybe_unused, nodiscard]] std::optional<std::string> getDescription() const; + [[nodiscard]] std::optional<std::string> getDescription() const; /** * Detailed description of the mod, written in Markdown (see * <Geode/ui/MDTextArea.hpp>) for more info */ - [[maybe_unused, nodiscard]] std::optional<std::string> getDetails() const; + [[nodiscard]] std::optional<std::string> getDetails() const; /** * Changelog for the mod, written in Markdown (see * <Geode/ui/MDTextArea.hpp>) for more info */ - [[maybe_unused, nodiscard]] std::optional<std::string> getChangelog() const; + [[nodiscard]] std::optional<std::string> getChangelog() const; /** * Support info for the mod; this means anything to show ways to * support the mod's development, like donations. Written in Markdown * (see MDTextArea for more info) */ - [[maybe_unused, nodiscard]] std::optional<std::string> getSupportInfo() const; + [[nodiscard]] std::optional<std::string> getSupportInfo() const; /** * Git Repository of the mod */ - [[maybe_unused, nodiscard]] std::optional<std::string> getRepository() const; + [[nodiscard]] std::optional<std::string> getRepository() const; /** * Info about where users should report issues and request help */ - [[maybe_unused, nodiscard]] std::optional<IssuesInfo> getIssues() const; + [[nodiscard]] std::optional<IssuesInfo> getIssues() const; /** * Dependencies */ - [[maybe_unused, nodiscard]] std::vector<Dependency> getDependencies() const; + [[nodiscard]] std::vector<Dependency> getDependencies() const; /** * Incompatibilities */ - [[maybe_unused, nodiscard]] std::vector<Incompatibility> getIncompatibilities() const; + [[nodiscard]] std::vector<Incompatibility> getIncompatibilities() const; /** * Mod spritesheet names */ - [[maybe_unused, nodiscard]] std::vector<std::string> getSpritesheets() const; + [[nodiscard]] std::vector<std::string> getSpritesheets() const; /** * Mod settings * @note Not a map because insertion order must be preserved */ - [[maybe_unused, nodiscard]] std::vector<std::pair<std::string, Setting>> getSettings() const; + [[nodiscard]] std::vector<std::pair<std::string, Setting>> getSettings() const; /** * Whether this mod has to be loaded before the loading screen or not */ - [[maybe_unused, nodiscard]] bool needsEarlyLoad() const; + [[ nodiscard]] bool needsEarlyLoad() const; /** * Whether this mod is an API or not */ - [[maybe_unused, nodiscard]] bool isAPI() const; + [[nodiscard]] bool isAPI() const; /** * Create ModInfo from an unzipped .geode package */ - [[maybe_unused]] static Result<ModMetadata> createFromGeodeZip(utils::file::Unzip& zip); + static Result<ModMetadata> createFromGeodeZip(utils::file::Unzip& zip); /** * Create ModInfo from a .geode package */ - [[maybe_unused]] static Result<ModMetadata> createFromGeodeFile(ghc::filesystem::path const& path); + static Result<ModMetadata> createFromGeodeFile(ghc::filesystem::path const& path); /** * Create ModInfo from a mod.json file */ - [[maybe_unused]] static Result<ModMetadata> createFromFile(ghc::filesystem::path const& path); + static Result<ModMetadata> createFromFile(ghc::filesystem::path const& path); /** * Create ModInfo from a parsed json document */ - [[maybe_unused]] static Result<ModMetadata> create(ModJson const& json); + static Result<ModMetadata> create(ModJson const& json); /** * Convert to JSON. Essentially same as getRawJSON except dynamically * adds runtime fields like path */ - [[maybe_unused, nodiscard]] ModJson toJSON() const; + [[nodiscard]] ModJson toJSON() const; /** * Get the raw JSON file */ - [[maybe_unused, nodiscard]] ModJson getRawJSON() const; + [[nodiscard]] ModJson getRawJSON() const; - [[maybe_unused]] bool operator==(ModMetadata const& other) const; + bool operator==(ModMetadata const& other) const; - [[maybe_unused]] static bool validateID(std::string const& id); + static bool validateID(std::string const& id); -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" -#pragma ide diagnostic ignored "google-explicit-constructor" - operator ModInfo(); - operator ModInfo() const; -#pragma clang diagnostic pop + [[deprecated]] operator ModInfo(); + [[deprecated]] operator ModInfo() const; private: /** @@ -226,11 +214,7 @@ namespace geode { friend class ModMetadataImpl; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - // ModInfo => ModMetadata conversion stuff - friend class ModInfo::Impl; -#pragma clang diagnostic pop + friend class ModInfo; }; } diff --git a/loader/src/hooks/MenuLayer.cpp b/loader/src/hooks/MenuLayer.cpp index eb89a19d..4942a0ec 100644 --- a/loader/src/hooks/MenuLayer.cpp +++ b/loader/src/hooks/MenuLayer.cpp @@ -23,34 +23,34 @@ class CustomMenuLayer; static Ref<Notification> INDEX_UPDATE_NOTIF = nullptr; $execute { - new EventListener<IndexUpdateFilter>(+[](IndexUpdateEvent* event) { - if (!INDEX_UPDATE_NOTIF) return; - std::visit(makeVisitor { - [](UpdateProgress const& prog) {}, - [](UpdateFinished const&) { - INDEX_UPDATE_NOTIF->setIcon(NotificationIcon::Success); - INDEX_UPDATE_NOTIF->setString("Index Up-to-Date"); - INDEX_UPDATE_NOTIF->waitAndHide(); - INDEX_UPDATE_NOTIF = nullptr; - }, - [](UpdateFailed const& info) { - INDEX_UPDATE_NOTIF->setIcon(NotificationIcon::Error); - INDEX_UPDATE_NOTIF->setString(info); - INDEX_UPDATE_NOTIF->setTime(NOTIFICATION_LONG_TIME); - INDEX_UPDATE_NOTIF = nullptr; - }, - }, event->status); - }); + new EventListener<IndexUpdateFilter>(+[](IndexUpdateEvent* event) { + if (!INDEX_UPDATE_NOTIF) return; + std::visit(makeVisitor { + [](UpdateProgress const& prog) {}, + [](UpdateFinished const&) { + INDEX_UPDATE_NOTIF->setIcon(NotificationIcon::Success); + INDEX_UPDATE_NOTIF->setString("Index Up-to-Date"); + INDEX_UPDATE_NOTIF->waitAndHide(); + INDEX_UPDATE_NOTIF = nullptr; + }, + [](UpdateFailed const& info) { + INDEX_UPDATE_NOTIF->setIcon(NotificationIcon::Error); + INDEX_UPDATE_NOTIF->setString(info); + INDEX_UPDATE_NOTIF->setTime(NOTIFICATION_LONG_TIME); + INDEX_UPDATE_NOTIF = nullptr; + }, + }, event->status); + }); }; struct CustomMenuLayer : Modify<CustomMenuLayer, MenuLayer> { - static void onModify(auto& self) { - if (!self.setHookPriority("MenuLayer::init", GEODE_ID_PRIORITY)) { + static void onModify(auto& self) { + if (!self.setHookPriority("MenuLayer::init", GEODE_ID_PRIORITY)) { log::warn("Failed to set MenuLayer::init hook priority, node IDs may not work properly"); } } - CCSprite* m_geodeButton; + CCSprite* m_geodeButton; bool init() { if (!MenuLayer::init()) return false; @@ -61,28 +61,28 @@ struct CustomMenuLayer : Modify<CustomMenuLayer, MenuLayer> { auto winSize = CCDirector::sharedDirector()->getWinSize(); - // add geode button - - m_fields->m_geodeButton = CircleButtonSprite::createWithSpriteFrameName( - "geode-logo-outline-gold.png"_spr, - 1.0f, - CircleBaseColor::Green, - CircleBaseSize::MediumAlt - ); - auto geodeBtnSelector = &CustomMenuLayer::onGeode; - if (!m_fields->m_geodeButton) { - geodeBtnSelector = &CustomMenuLayer::onMissingTextures; - m_fields->m_geodeButton = ButtonSprite::create("!!"); - } + // add geode button + + m_fields->m_geodeButton = CircleButtonSprite::createWithSpriteFrameName( + "geode-logo-outline-gold.png"_spr, + 1.0f, + CircleBaseColor::Green, + CircleBaseSize::MediumAlt + ); + auto geodeBtnSelector = &CustomMenuLayer::onGeode; + if (!m_fields->m_geodeButton) { + geodeBtnSelector = &CustomMenuLayer::onMissingTextures; + m_fields->m_geodeButton = ButtonSprite::create("!!"); + } auto bottomMenu = static_cast<CCMenu*>(this->getChildByID("bottom-menu")); - auto btn = CCMenuItemSpriteExtra::create( - m_fields->m_geodeButton, this, - static_cast<SEL_MenuHandler>(geodeBtnSelector) - ); - btn->setID("geode-button"_spr); - bottomMenu->addChild(btn); + auto btn = CCMenuItemSpriteExtra::create( + m_fields->m_geodeButton, this, + static_cast<SEL_MenuHandler>(geodeBtnSelector) + ); + btn->setID("geode-button"_spr); + bottomMenu->addChild(btn); bottomMenu->updateLayout(); @@ -96,54 +96,57 @@ struct CustomMenuLayer : Modify<CustomMenuLayer, MenuLayer> { static bool shownFailedNotif = false; if (!shownFailedNotif) { shownFailedNotif = true; - if (Loader::get()->getFailedMods().size()) { - Notification::create("Some mods failed to load", NotificationIcon::Error)->show(); + auto problems = Loader::get()->getProblems(); + if (std::any_of(problems.begin(), problems.end(), [&](auto& item) { + return item.type != LoadProblem::Type::Suggestion && item.type != LoadProblem::Type::Recommendation; + })) { + Notification::create("There were problems loading some mods", NotificationIcon::Error)->show(); } } - // show if the user tried to be naughty and load arbitary DLLs - static bool shownTriedToLoadDlls = false; - if (!shownTriedToLoadDlls) { - shownTriedToLoadDlls = true; - if (Loader::get()->userTriedToLoadDLLs()) { - auto popup = FLAlertLayer::create( - "Hold up!", - "It appears that you have tried to <cr>load DLLs</c> with Geode. " - "Please note that <cy>Geode is incompatible with ALL DLLs</c>, " - "as they can cause Geode mods to <cr>error</c>, or even " - "<cr>crash</c>.\n\n" - "Remove the DLLs / other mod loaders you have, or <cr>proceed at " - "your own risk.</c>", - "OK" - ); - popup->m_scene = this; - popup->m_noElasticity = true; - popup->show(); - } - } + // show if the user tried to be naughty and load arbitrary DLLs + static bool shownTriedToLoadDlls = false; + if (!shownTriedToLoadDlls) { + shownTriedToLoadDlls = true; + if (Loader::get()->userTriedToLoadDLLs()) { + auto popup = FLAlertLayer::create( + "Hold up!", + "It appears that you have tried to <cr>load DLLs</c> with Geode. " + "Please note that <cy>Geode is incompatible with ALL DLLs</c>, " + "as they can cause Geode mods to <cr>error</c>, or even " + "<cr>crash</c>.\n\n" + "Remove the DLLs / other mod loaders you have, or <cr>proceed at " + "your own risk.</c>", + "OK" + ); + popup->m_scene = this; + popup->m_noElasticity = true; + popup->show(); + } + } - // show auto update message - static bool shownUpdateInfo = false; - if (LoaderImpl::get()->isNewUpdateDownloaded() && !shownUpdateInfo) { - shownUpdateInfo = true; - auto popup = FLAlertLayer::create( - "Update downloaded", - "A new <cy>update</c> for Geode has been installed! " - "Please <cy>restart the game</c> to apply.", - "OK" - ); - popup->m_scene = this; - popup->m_noElasticity = true; - popup->show(); - } + // show auto update message + static bool shownUpdateInfo = false; + if (LoaderImpl::get()->isNewUpdateDownloaded() && !shownUpdateInfo) { + shownUpdateInfo = true; + auto popup = FLAlertLayer::create( + "Update downloaded", + "A new <cy>update</c> for Geode has been installed! " + "Please <cy>restart the game</c> to apply.", + "OK" + ); + popup->m_scene = this; + popup->m_noElasticity = true; + popup->show(); + } // show crash info static bool shownLastCrash = false; if ( - Loader::get()->didLastLaunchCrash() && - !shownLastCrash && - !Mod::get()->template getSettingValue<bool>("disable-last-crashed-popup") - ) { + Loader::get()->didLastLaunchCrash() && + !shownLastCrash && + !Mod::get()->template getSettingValue<bool>("disable-last-crashed-popup") + ) { shownLastCrash = true; auto popup = createQuickPopup( "Crashed", @@ -163,94 +166,94 @@ struct CustomMenuLayer : Modify<CustomMenuLayer, MenuLayer> { popup->show(); } - // update mods index - if (!INDEX_UPDATE_NOTIF && !Index::get()->hasTriedToUpdate()) { - this->addChild(EventListenerNode<IndexUpdateFilter>::create( - this, &CustomMenuLayer::onIndexUpdate - )); - INDEX_UPDATE_NOTIF = Notification::create( - "Updating Index", NotificationIcon::Loading, 0 - ); - INDEX_UPDATE_NOTIF->show(); - Index::get()->update(); - } + // update mods index + if (!INDEX_UPDATE_NOTIF && !Index::get()->hasTriedToUpdate()) { + this->addChild(EventListenerNode<IndexUpdateFilter>::create( + this, &CustomMenuLayer::onIndexUpdate + )); + INDEX_UPDATE_NOTIF = Notification::create( + "Updating Index", NotificationIcon::Loading, 0 + ); + INDEX_UPDATE_NOTIF->show(); + Index::get()->update(); + } - this->addUpdateIndicator(); - - return true; - } + this->addUpdateIndicator(); + + return true; + } - void onIndexUpdate(IndexUpdateEvent* event) { - if ( - std::holds_alternative<UpdateFinished>(event->status) || - std::holds_alternative<UpdateFailed>(event->status) - ) { - this->addUpdateIndicator(); - } - } + void onIndexUpdate(IndexUpdateEvent* event) { + if ( + std::holds_alternative<UpdateFinished>(event->status) || + std::holds_alternative<UpdateFailed>(event->status) + ) { + this->addUpdateIndicator(); + } + } - void addUpdateIndicator() { - if (Index::get()->areUpdatesAvailable()) { - auto icon = CCSprite::createWithSpriteFrameName("updates-available.png"_spr); - icon->setPosition( - m_fields->m_geodeButton->getContentSize() - CCSize { 10.f, 10.f } - ); - icon->setZOrder(99); - icon->setScale(.5f); - m_fields->m_geodeButton->addChild(icon); - } - } + void addUpdateIndicator() { + if (Index::get()->areUpdatesAvailable()) { + auto icon = CCSprite::createWithSpriteFrameName("updates-available.png"_spr); + icon->setPosition( + m_fields->m_geodeButton->getContentSize() - CCSize { 10.f, 10.f } + ); + icon->setZOrder(99); + icon->setScale(.5f); + m_fields->m_geodeButton->addChild(icon); + } + } - void onMissingTextures(CCObject*) { - - #ifdef GEODE_IS_DESKTOP + void onMissingTextures(CCObject*) { + + #ifdef GEODE_IS_DESKTOP - (void) utils::file::createDirectoryAll(dirs::getGeodeDir() / "update" / "resources"); + (void) utils::file::createDirectoryAll(dirs::getGeodeDir() / "update" / "resources"); - createQuickPopup( - "Missing Textures", - "You appear to be missing textures, and the automatic texture fixer " - "hasn't fixed the issue.\n" - "Download <cy>resources.zip</c> from the latest release on GitHub, " - "and <cy>unzip its contents</c> into <cb>geode/update/resources</c>.\n" - "Afterwards, <cg>restart the game</c>.\n" - "You may also continue without installing resources, but be aware that " - "you won't be able to open <cr>the Geode menu</c>.", - "Dismiss", "Open Github", - [](auto, bool btn2) { - if (btn2) { - web::openLinkInBrowser("https://github.com/geode-sdk/geode/releases/latest"); - file::openFolder(dirs::getGeodeDir() / "update" / "resources"); - FLAlertLayer::create( - "Info", - "Opened GitHub in your browser and the destination in " - "your file browser.\n" - "Download <cy>resources.zip</c>, " - "and <cy>unzip its contents</c> into the destination " - "folder.\n" - "<cb>Don't add any new folders to the destination!</c>", - "OK" - )->show(); - } - } - ); + createQuickPopup( + "Missing Textures", + "You appear to be missing textures, and the automatic texture fixer " + "hasn't fixed the issue.\n" + "Download <cy>resources.zip</c> from the latest release on GitHub, " + "and <cy>unzip its contents</c> into <cb>geode/update/resources</c>.\n" + "Afterwards, <cg>restart the game</c>.\n" + "You may also continue without installing resources, but be aware that " + "you won't be able to open <cr>the Geode menu</c>.", + "Dismiss", "Open Github", + [](auto, bool btn2) { + if (btn2) { + web::openLinkInBrowser("https://github.com/geode-sdk/geode/releases/latest"); + file::openFolder(dirs::getGeodeDir() / "update" / "resources"); + FLAlertLayer::create( + "Info", + "Opened GitHub in your browser and the destination in " + "your file browser.\n" + "Download <cy>resources.zip</c>, " + "and <cy>unzip its contents</c> into the destination " + "folder.\n" + "<cb>Don't add any new folders to the destination!</c>", + "OK" + )->show(); + } + } + ); - #else + #else - // dunno if we can auto-create target directory on mobile, nor if the - // user has access to moving stuff there + // dunno if we can auto-create target directory on mobile, nor if the + // user has access to moving stuff there - FLAlertLayer::create( - "Missing Textures", - "You appear to be missing textures, and the automatic texture fixer " - "hasn't fixed the issue.\n" - "**<cy>Report this bug to the Geode developers</c>**. It is very likely " - "that your game <cr>will crash</c> until the issue is resolved.", - "OK" - )->show(); + FLAlertLayer::create( + "Missing Textures", + "You appear to be missing textures, and the automatic texture fixer " + "hasn't fixed the issue.\n" + "**<cy>Report this bug to the Geode developers</c>**. It is very likely " + "that your game <cr>will crash</c> until the issue is resolved.", + "OK" + )->show(); - #endif - } + #endif + } void onGeode(CCObject*) { ModListLayer::scene(); diff --git a/loader/src/internal/crashlog.cpp b/loader/src/internal/crashlog.cpp index e5b3af57..5995dfb3 100644 --- a/loader/src/internal/crashlog.cpp +++ b/loader/src/internal/crashlog.cpp @@ -18,7 +18,7 @@ static std::string getDateString(bool filesafe) { static void printGeodeInfo(std::stringstream& stream) { stream << "Loader Version: " << Loader::get()->getVersion().toString() << "\n" << "Installed mods: " << Loader::get()->getAllMods().size() << "\n" - << "Failed mods: " << Loader::get()->getFailedMods().size() << "\n"; + << "Problems: " << Loader::get()->getProblems().size() << "\n"; } static void printMods(std::stringstream& stream) { @@ -84,4 +84,4 @@ std::string crashlog::writeCrashlog(geode::Mod* faultyMod, std::string const& in actualFile.close(); return file.str(); -} \ No newline at end of file +} diff --git a/loader/src/loader/Index.cpp b/loader/src/loader/Index.cpp index bf366247..b564b410 100644 --- a/loader/src/loader/Index.cpp +++ b/loader/src/loader/Index.cpp @@ -64,6 +64,8 @@ public: static Result<std::shared_ptr<IndexItem>> create( ghc::filesystem::path const& dir ); + + bool isInstalled() const; }; IndexItem::IndexItem() : m_impl(std::make_unique<Impl>()) {} @@ -101,6 +103,10 @@ std::unordered_set<std::string> IndexItem::getTags() const { return m_impl->m_tags; } +bool IndexItem::isInstalled() const { + return m_impl->isInstalled(); +} + Result<IndexItemHandle> IndexItem::Impl::create(ghc::filesystem::path const& dir) { GEODE_UNWRAP_INTO( auto entry, file::readJson(dir / "entry.json") @@ -134,6 +140,10 @@ Result<IndexItemHandle> IndexItem::Impl::create(ghc::filesystem::path const& dir return Ok(item); } +bool IndexItem::Impl::isInstalled() const { + return ghc::filesystem::exists(dirs::getModsDir() / (m_metadata.getID() + ".geode")); +} + // Helpers static Result<> flattenGithubRepo(ghc::filesystem::path const& dir) { @@ -595,9 +605,6 @@ void Index::Impl::installNext(size_t index, IndexInstallList const& list) { } } - // load mods - Loader::get()->refreshModsList(); - auto const& eventModID = list.target->getMetadata().getID(); Loader::get()->queueInGDThread([eventModID]() { ModInstallEvent(eventModID, UpdateFinished()).post(); diff --git a/loader/src/loader/Loader.cpp b/loader/src/loader/Loader.cpp index 6335ccee..8d55893c 100644 --- a/loader/src/loader/Loader.cpp +++ b/loader/src/loader/Loader.cpp @@ -95,6 +95,10 @@ std::vector<InvalidGeodeFile> Loader::getFailedMods() const { return m_impl->getFailedMods(); } +std::vector<LoadProblem> Loader::getProblems() const { + return m_impl->getProblems(); +} + void Loader::updateResources() { return m_impl->updateResources(); } diff --git a/loader/src/loader/LoaderImpl.cpp b/loader/src/loader/LoaderImpl.cpp index 99d12cb6..23ea529b 100644 --- a/loader/src/loader/LoaderImpl.cpp +++ b/loader/src/loader/LoaderImpl.cpp @@ -1,4 +1,3 @@ - #include "LoaderImpl.hpp" #include <cocos2d.h> @@ -10,24 +9,19 @@ #include <Geode/loader/Loader.hpp> #include <Geode/loader/Log.hpp> #include <Geode/loader/Mod.hpp> -#include <Geode/loader/ModJsonTest.hpp> +#include <Geode/utils/JsonValidation.hpp> #include <Geode/utils/file.hpp> #include <Geode/utils/map.hpp> #include <Geode/utils/ranges.hpp> #include <Geode/utils/string.hpp> #include <Geode/utils/web.hpp> -#include <Geode/utils/JsonValidation.hpp> -#include "ModImpl.hpp" -#include "ModInfoImpl.hpp" #include <about.hpp> #include <crashlog.hpp> #include <fmt/format.h> #include <hash.hpp> #include <iostream> #include <resources.hpp> -#include <sstream> #include <string> -#include <thread> #include <vector> using namespace geode::prelude; @@ -36,9 +30,9 @@ Loader::Impl* LoaderImpl::get() { return Loader::get()->m_impl.get(); } -Loader::Impl::Impl() {} +Loader::Impl::Impl() = default; -Loader::Impl::~Impl() {} +Loader::Impl::~Impl() = default; // Initialization @@ -92,15 +86,10 @@ Result<> Loader::Impl::setup() { this->setupIPC(); this->createDirectories(); - auto sett = this->loadData(); - if (!sett) { - log::warn("Unable to load loader settings: {}", sett.unwrapErr()); - } - this->refreshModsList(); - this->queueInGDThread([]() { - Loader::get()->addSearchPaths(); - }); + this->addSearchPaths(); + + this->refreshModGraph(); m_isSetup = true; @@ -132,12 +121,19 @@ std::vector<Mod*> Loader::Impl::getAllMods() { return map::values(m_mods); } -Mod* Loader::Impl::getModImpl() { - return Mod::get(); -} - std::vector<InvalidGeodeFile> Loader::Impl::getFailedMods() const { - return m_invalidMods; + std::vector<InvalidGeodeFile> inv; + for (auto const& item : this->getProblems()) { + if (item.type != LoadProblem::Type::InvalidFile) + continue; + if (!holds_alternative<ghc::filesystem::path>(item.cause)) + continue; + inv.push_back({ + std::get<ghc::filesystem::path>(item.cause), + item.message + }); + } + return inv; } // Version info @@ -170,7 +166,7 @@ bool Loader::Impl::isModVersionSupported(VersionInfo const& version) { Result<> Loader::Impl::saveData() { // save mods' data for (auto& [id, mod] : m_mods) { - Mod::get()->setSavedValue("should-load-" + id, mod->isEnabled()); + Mod::get()->setSavedValue("should-load-" + id, mod->isUninstalled() || mod->isEnabled()); auto r = mod->saveData(); if (!r) { log::warn("Unable to save data for mod \"{}\": {}", mod->getID(), r.unwrapErr()); @@ -178,15 +174,11 @@ Result<> Loader::Impl::saveData() { } // save loader data GEODE_UNWRAP(Mod::get()->saveData()); - + return Ok(); } Result<> Loader::Impl::loadData() { - auto e = Mod::get()->loadData(); - if (!e) { - log::warn("Unable to load loader settings: {}", e.unwrapErr()); - } for (auto& [_, mod] : m_mods) { auto r = mod->loadData(); if (!r) { @@ -198,60 +190,6 @@ Result<> Loader::Impl::loadData() { // Mod loading -Result<Mod*> Loader::Impl::loadModFromInfo(ModInfo const& info) { - if (m_mods.count(info.id())) { - return Err(fmt::format("Mod with ID '{}' already loaded", info.id())); - } - - // create Mod instance - auto mod = new Mod(info); - auto setupRes = mod->m_impl->setup(); - if (!setupRes) { - // old code artifcat, idk why we are not using unique_ptr TBH - delete mod; - return Err(fmt::format( - "Unable to setup mod '{}': {}", - info.id(), setupRes.unwrapErr() - )); - } - - m_mods.insert({ info.id(), mod }); - - mod->m_impl->m_enabled = Mod::get()->getSavedValue<bool>( - "should-load-" + info.id(), true - ); - - // this loads the mod if its dependencies are resolved - auto dependenciesRes = mod->updateDependencies(); - if (!dependenciesRes) { - delete mod; - m_mods.erase(info.id()); - return Err(dependenciesRes.unwrapErr()); - } - - // add mod resources - this->queueInGDThread([this, mod]() { - auto searchPath = dirs::getModRuntimeDir() / mod->getID() / "resources"; - - CCFileUtils::get()->addSearchPath(searchPath.string().c_str()); - this->updateModResources(mod); - }); - - return Ok(mod); -} - -Result<Mod*> Loader::Impl::loadModFromFile(ghc::filesystem::path const& file) { - auto res = ModInfo::createFromGeodeFile(file); - if (!res) { - m_invalidMods.push_back(InvalidGeodeFile { - .path = file, - .reason = res.unwrapErr(), - }); - return Err(res.unwrapErr()); - } - return this->loadModFromInfo(res.unwrap()); -} - bool Loader::Impl::isModInstalled(std::string const& id) const { return m_mods.count(id) && !m_mods.at(id)->isUninstalled(); } @@ -308,116 +246,320 @@ void Loader::Impl::updateModResources(Mod* mod) { // Dependencies and refreshing -void Loader::Impl::loadModsFromDirectory( - ghc::filesystem::path const& dir, - bool recursive -) { - log::debug("Searching {}", dir); - for (auto const& entry : ghc::filesystem::directory_iterator(dir)) { - // recursively search directories - if (ghc::filesystem::is_directory(entry) && recursive) { - this->loadModsFromDirectory(entry.path(), true); - continue; - } +Result<Mod*> Loader::Impl::loadModFromInfo(ModInfo const& info) { + return Err("Loader::loadModFromInfo is deprecated"); +} - // skip this entry if it's not a file - if (!ghc::filesystem::is_regular_file(entry)) { - continue; - } +Result<Mod*> Loader::Impl::loadModFromFile(ghc::filesystem::path const& file) { + return Err("Loader::loadModFromFile is deprecated"); +} - // skip this entry if its extension is not .geode - if (entry.path().extension() != GEODE_MOD_EXTENSION) { - continue; - } - // skip this entry if it's already loaded - if (map::contains<std::string, Mod*>(m_mods, [entry](Mod* p) -> bool { - return p->m_impl->m_info.path() == entry.path(); - })) { - continue; - } - - // if mods should be loaded immediately, do that - if (m_earlyLoadFinished) { - auto load = this->loadModFromFile(entry); - if (!load) { - log::error("Unable to load {}: {}", entry, load.unwrapErr()); - } - } - // otherwise collect mods to load first to make sure the correct - // versions of the mods are loaded and that early-loaded mods are - // loaded early - else { - auto res = ModInfo::createFromGeodeFile(entry.path()); - if (!res) { - m_invalidMods.push_back(InvalidGeodeFile { - .path = entry.path(), - .reason = res.unwrapErr(), - }); - continue; - } - auto info = res.unwrap(); - - // skip this entry if it's already set to be loaded - if (ranges::contains(m_modsToLoad, info)) { - continue; - } - - // add to list of mods to load - m_modsToLoad.push_back(info); - } - } +void Loader::Impl::loadModsFromDirectory(ghc::filesystem::path const& dir, bool recursive) { + log::error("Called deprecated stub: Loader::loadModsFromDirectory"); } void Loader::Impl::refreshModsList() { - log::debug("Loading mods..."); - - // find mods - for (auto& dir : m_modSearchDirectories) { - this->loadModsFromDirectory(dir); - } - - // load early-load mods first - for (auto& mod : m_modsToLoad) { - if (mod.needsEarlyLoad()) { - auto load = this->loadModFromInfo(mod); - if (!load) { - log::error("Unable to load {}: {}", mod.id(), load.unwrapErr()); - - m_invalidMods.push_back(InvalidGeodeFile { - .path = mod.path(), - .reason = load.unwrapErr(), - }); - } - } - } - - // UI can be loaded now - m_earlyLoadFinished = true; - m_earlyLoadFinishedCV.notify_all(); - - // load the rest of the mods - for (auto& mod : m_modsToLoad) { - if (!mod.needsEarlyLoad()) { - auto load = this->loadModFromInfo(mod); - if (!load) { - log::error("Unable to load {}: {}", mod.id(), load.unwrapErr()); - - m_invalidMods.push_back(InvalidGeodeFile { - .path = mod.path(), - .reason = load.unwrapErr(), - }); - } - } - } - m_modsToLoad.clear(); + log::error("Called deprecated stub: Loader::refreshModsList"); } void Loader::Impl::updateAllDependencies() { - for (auto const& [_, mod] : m_mods) { - (void)mod->updateDependencies(); + log::error("Called deprecated stub: Loader::updateAllDependencies"); +} + +void Loader::Impl::queueMods(std::vector<ModMetadata>& modQueue) { + for (auto const& dir : m_modSearchDirectories) { + log::debug("Searching {}", dir); + log::pushNest(); + for (auto const& entry : ghc::filesystem::directory_iterator(dir)) { + if (!ghc::filesystem::is_regular_file(entry) || + entry.path().extension() != GEODE_MOD_EXTENSION) + continue; + + log::debug("Found {}", entry.path().filename()); + log::pushNest(); + + auto res = ModMetadata::createFromGeodeFile(entry.path()); + if (!res) { + m_problems.push_back({ + LoadProblem::Type::InvalidFile, + entry.path(), + res.unwrapErr() + }); + log::error("Failed to queue: {}", res.unwrapErr()); + log::popNest(); + continue; + } + auto modMetadata = res.unwrap(); + + log::debug("id: {}", modMetadata.getID()); + log::debug("version: {}", modMetadata.getVersion()); + + if (std::find_if(modQueue.begin(), modQueue.end(), [&](auto& item) { + return modMetadata.getID() == item.getID(); + }) != modQueue.end()) { + m_problems.push_back({ + LoadProblem::Type::Duplicate, + modMetadata, + "a mod with the same ID is already present" + }); + log::error("Failed to queue: a mod with the same ID is already queued"); + log::popNest(); + continue; + } + + modQueue.push_back(modMetadata); + log::popNest(); + } + log::popNest(); } } +void Loader::Impl::populateModList(std::vector<ModMetadata>& modQueue) { + std::vector<std::string> toRemove; + for (auto& [id, mod] : m_mods) { + if (id == "geode.loader") + continue; + delete mod; + toRemove.push_back(id); + } + for (auto const& id : toRemove) { + m_mods.erase(id); + } + + for (auto const& metadata : modQueue) { + log::debug("{} {}", metadata.getID(), metadata.getVersion()); + log::pushNest(); + + auto mod = new Mod(metadata); + + auto res = mod->m_impl->setup(); + if (!res) { + m_problems.push_back({ + LoadProblem::Type::SetupFailed, + mod, + res.unwrapErr() + }); + log::error("Failed to set up: {}", res.unwrapErr()); + log::popNest(); + continue; + } + + m_mods.insert({metadata.getID(), mod}); + + queueInGDThread([this, mod]() { + auto searchPath = dirs::getModRuntimeDir() / mod->getID() / "resources"; + CCFileUtils::get()->addSearchPath(searchPath.string().c_str()); + updateModResources(mod); + }); + + log::popNest(); + } +} + +void Loader::Impl::buildModGraph() { + for (auto const& [id, mod] : m_mods) { + log::debug("{}", mod->getID()); + log::pushNest(); + for (auto& dependency : mod->m_impl->m_metadata.m_impl->m_dependencies) { + log::debug("{}", dependency.id); + if (!m_mods.contains(dependency.id)) { + dependency.mod = nullptr; + continue; + } + + dependency.mod = m_mods[dependency.id]; + + if (!dependency.version.compare(dependency.mod->getVersion())) { + dependency.mod = nullptr; + continue; + } + + if (dependency.importance != ModMetadata::Dependency::Importance::Required || dependency.mod == nullptr) + continue; + + dependency.mod->m_impl->m_dependants.push_back(mod); + } + for (auto& incompatibility : mod->m_impl->m_metadata.m_impl->m_incompatibilities) { + incompatibility.mod = + m_mods.contains(incompatibility.id) ? m_mods[incompatibility.id] : nullptr; + } + log::popNest(); + } +} + +void Loader::Impl::loadModGraph(Mod* node) { + if (node->hasUnresolvedDependencies()) + return; + if (node->hasUnresolvedIncompatibilities()) + return; + + log::debug("{} {}", node->getID(), node->getVersion()); + log::pushNest(); + + if (node->isLoaded()) { + for (auto const& dep : node->m_impl->m_dependants) { + this->loadModGraph(dep); + } + log::popNest(); + return; + } + + log::debug("Load"); + auto res = node->m_impl->loadBinary(); + if (!res) { + m_problems.push_back({ + LoadProblem::Type::LoadFailed, + node, + res.unwrapErr() + }); + log::error("Failed to load binary: {}", res.unwrapErr()); + log::popNest(); + return; + } + + if (Mod::get()->getSavedValue<bool>("should-load-" + node->getID(), true)) { + log::debug("Enable"); + res = node->m_impl->enable(); + if (!res) { + node->m_impl->m_enabled = true; + (void)node->m_impl->disable(); + m_problems.push_back({ + LoadProblem::Type::EnableFailed, + node, + res.unwrapErr() + }); + log::error("Failed to enable: {}", res.unwrapErr()); + log::popNest(); + return; + } + + for (auto const& dep : node->m_impl->m_dependants) { + this->loadModGraph(dep); + } + } + + log::popNest(); +} + +void Loader::Impl::findProblems() { + for (auto const& [id, mod] : m_mods) { + log::debug(id); + log::pushNest(); + + for (auto const& dep : mod->getMetadata().getDependencies()) { + if (dep.mod && dep.mod->isLoaded() && dep.version.compare(dep.mod->getVersion())) + continue; + switch(dep.importance) { + case ModMetadata::Dependency::Importance::Suggested: + m_problems.push_back({ + LoadProblem::Type::Suggestion, + mod, + fmt::format("{} {}", dep.id, dep.version.toString()) + }); + log::info("{} suggests {} {}", id, dep.id, dep.version); + break; + case ModMetadata::Dependency::Importance::Recommended: + m_problems.push_back({ + LoadProblem::Type::Recommendation, + mod, + fmt::format("{} {}", dep.id, dep.version.toString()) + }); + log::warn("{} recommends {} {}", id, dep.id, dep.version); + break; + case ModMetadata::Dependency::Importance::Required: + m_problems.push_back({ + LoadProblem::Type::MissingDependency, + mod, + fmt::format("{} {}", dep.id, dep.version.toString()) + }); + log::error("{} requires {} {}", id, dep.id, dep.version); + break; + } + } + + for (auto const& dep : mod->getMetadata().getIncompatibilities()) { + if (!dep.mod || !dep.version.compare(dep.mod->getVersion())) + continue; + m_problems.push_back({ + LoadProblem::Type::PresentIncompatibility, + mod, + fmt::format("{} {}", dep.id, dep.version.toString()) + }); + log::error("{} is incompatible with {} {}", id, dep.id, dep.version); + } + + Mod* myEpicMod = mod; // clang fix + // if the mod is not loaded but there are no problems related to it + if (!mod->isLoaded() && !std::any_of(m_problems.begin(), m_problems.end(), [myEpicMod](auto& item) { + return std::holds_alternative<ModMetadata>(item.cause) && + std::get<ModMetadata>(item.cause).getID() == myEpicMod->getID() || + std::holds_alternative<Mod*>(item.cause) && + std::get<Mod*>(item.cause) == myEpicMod; + })) { + m_problems.push_back({ + LoadProblem::Type::Unknown, + mod, + "" + }); + log::error("{} failed to load for an unknown reason", id); + } + + log::popNest(); + } +} + +void Loader::Impl::refreshModGraph() { + log::info("Refreshing mod graph..."); + log::pushNest(); + + if (m_mods.size() > 1) { + log::error("Cannot refresh mod graph after startup"); + log::popNest(); + return; + } + + m_problems.clear(); + + log::debug("Queueing mods"); + log::pushNest(); + std::vector<ModMetadata> modQueue; + this->queueMods(modQueue); + log::popNest(); + + log::debug("Populating mod list"); + log::pushNest(); + this->populateModList(modQueue); + modQueue.clear(); + log::popNest(); + + log::debug("Building mod graph"); + log::pushNest(); + this->buildModGraph(); + log::popNest(); + + // TODO: not early load + log::debug("Loading mods"); + log::pushNest(); + for (auto const& dep : Mod::get()->m_impl->m_dependants) { + this->loadModGraph(dep); + } + log::popNest(); + + log::debug("Finding problems"); + log::pushNest(); + this->findProblems(); + log::popNest(); + + m_earlyLoadFinished = true; + m_earlyLoadFinishedCV.notify_all(); + + log::popNest(); +} + +std::vector<LoadProblem> Loader::Impl::getProblems() const { + return m_problems; +} + void Loader::Impl::waitForModsToBeLoaded() { auto lock = std::unique_lock(m_earlyLoadFinishedMutex); log::debug("Waiting for mods to be loaded... {}", bool(m_earlyLoadFinished)); diff --git a/loader/src/loader/LoaderImpl.hpp b/loader/src/loader/LoaderImpl.hpp index 7b4fd48e..aac71705 100644 --- a/loader/src/loader/LoaderImpl.hpp +++ b/loader/src/loader/LoaderImpl.hpp @@ -54,8 +54,7 @@ namespace geode { mutable std::mutex m_mutex; std::vector<ghc::filesystem::path> m_modSearchDirectories; - std::vector<ModInfo> m_modsToLoad; - std::vector<InvalidGeodeFile> m_invalidMods; + std::vector<LoadProblem> m_problems; std::unordered_map<std::string, Mod*> m_mods; std::vector<ghc::filesystem::path> m_texturePaths; bool m_isSetup = false; @@ -113,7 +112,7 @@ namespace geode { friend void GEODE_CALL ::geode_implicit_load(Mod*); - Result<Mod*> loadModFromInfo(ModInfo const& info); + [[deprecated]] Result<Mod*> loadModFromInfo(ModInfo const& info); Result<> setup(); void forceReset(); @@ -126,17 +125,24 @@ namespace geode { VersionInfo maxModVersion(); bool isModVersionSupported(VersionInfo const& version); - Result<Mod*> loadModFromFile(ghc::filesystem::path const& file); - void loadModsFromDirectory(ghc::filesystem::path const& dir, bool recursive = true); - void refreshModsList(); + [[deprecated]] Result<Mod*> loadModFromFile(ghc::filesystem::path const& file); + [[deprecated]] void loadModsFromDirectory(ghc::filesystem::path const& dir, bool recursive = true); + [[deprecated]] void refreshModsList(); + void queueMods(std::vector<ModMetadata>& modQueue); + void populateModList(std::vector<ModMetadata>& modQueue); + void buildModGraph(); + void loadModGraph(Mod* node); + void findProblems(); + void refreshModGraph(); bool isModInstalled(std::string const& id) const; Mod* getInstalledMod(std::string const& id) const; bool isModLoaded(std::string const& id) const; Mod* getLoadedMod(std::string const& id) const; std::vector<Mod*> getAllMods(); - Mod* getModImpl(); - void updateAllDependencies(); - std::vector<InvalidGeodeFile> getFailedMods() const; + [[deprecated]] Mod* getModImpl(); + [[deprecated]] void updateAllDependencies(); + [[deprecated]] std::vector<InvalidGeodeFile> getFailedMods() const; + std::vector<LoadProblem> getProblems() const; void updateResources(); void updateResources(bool forceReload); diff --git a/loader/src/loader/Mod.cpp b/loader/src/loader/Mod.cpp index 46b8774e..b6696fcc 100644 --- a/loader/src/loader/Mod.cpp +++ b/loader/src/loader/Mod.cpp @@ -4,6 +4,7 @@ using namespace geode::prelude; +#pragma warning(suppress : 4996) Mod::Mod(ModInfo const& info) : m_impl(std::make_unique<Impl>(this, info)) {} Mod::Mod(ModMetadata const& metadata) : m_impl(std::make_unique<Impl>(this, metadata)) {} @@ -53,6 +54,14 @@ bool Mod::supportsDisabling() const { return m_impl->supportsDisabling(); } +bool Mod::canDisable() const { + return m_impl->canDisable(); +} + +bool Mod::canEnable() const { + return m_impl->canEnable(); +} + bool Mod::supportsUnloading() const { return false; } @@ -153,11 +162,11 @@ Result<> Mod::unpatch(Patch* patch) { } Result<> Mod::loadBinary() { - return m_impl->loadBinary(); + return Err("Load mod binaries after startup is not supported"); } Result<> Mod::unloadBinary() { - return m_impl->unloadBinary(); + return Err("Unloading mod binaries is not supported"); } Result<> Mod::enable() { @@ -180,14 +189,19 @@ bool Mod::depends(std::string const& id) const { return m_impl->depends(id); } -bool Mod::hasUnresolvedDependencies() const { - return m_impl->hasUnresolvedDependencies(); -} - Result<> Mod::updateDependencies() { return m_impl->updateDependencies(); } +bool Mod::hasUnresolvedDependencies() const { + return m_impl->hasUnresolvedDependencies(); +} + +bool Mod::hasUnresolvedIncompatibilities() const { + return m_impl->hasUnresolvedIncompatibilities(); +} + +#pragma warning(suppress : 4996) std::vector<Dependency> Mod::getUnresolvedDependencies() { return m_impl->getUnresolvedDependencies(); } diff --git a/loader/src/loader/ModImpl.cpp b/loader/src/loader/ModImpl.cpp index 78f58c70..829cbcfd 100644 --- a/loader/src/loader/ModImpl.cpp +++ b/loader/src/loader/ModImpl.cpp @@ -28,9 +28,7 @@ Mod::Impl* ModImpl::getImpl(Mod* mod) { Mod::Impl::Impl(Mod* self, ModMetadata const& metadata) : m_self(self), m_metadata(metadata) { } -Mod::Impl::~Impl() { - (void)this->unloadBinary(); -} +Mod::Impl::~Impl() = default; Result<> Mod::Impl::setup() { m_saveDirPath = dirs::getModsSaveDir() / m_metadata.getID(); @@ -110,11 +108,23 @@ bool Mod::Impl::isLoaded() const { } bool Mod::Impl::supportsDisabling() const { - return m_info.supportsDisabling(); + return m_metadata.getID() != "geode.loader" && !m_metadata.isAPI(); } -bool Mod::Impl::supportsUnloading() const { - return m_info.supportsUnloading(); +bool Mod::Impl::canDisable() const { + auto deps = m_dependants; + return this->supportsDisabling() && + (deps.empty() || std::all_of(deps.begin(), deps.end(), [&](auto& item) { + return item->canDisable(); + })); +} + +bool Mod::Impl::canEnable() const { + auto deps = m_metadata.getDependencies(); + return !this->isUninstalled() && + (deps.empty() || std::all_of(deps.begin(), deps.end(), [&](auto& item) { + return item.isResolved(); + })); } bool Mod::Impl::wasSuccessfullyLoaded() const { @@ -304,11 +314,6 @@ Result<> Mod::Impl::loadBinary() { log::debug("Loading binary for mod {}", m_metadata.getID()); if (m_binaryLoaded) return Ok(); - } - - if (this->hasUnresolvedDependencies()) { - return Err("Mod has unresolved dependencies"); - } LoaderImpl::get()->provideNextMod(m_self); @@ -327,54 +332,30 @@ Result<> Mod::Impl::loadBinary() { ModStateEvent(m_self, ModEventType::Loaded).post(); }); - Loader::get()->updateAllDependencies(); - - log::debug("Enabling mod {}", m_info.id()); - GEODE_UNWRAP(this->enable()); - - return Ok(); -} - -Result<> Mod::Impl::unloadBinary() { - if (!m_binaryLoaded) { - return Ok(); - } - - if (!m_info.supportsUnloading()) { - return Err("Mod does not support unloading"); - } - - GEODE_UNWRAP(this->saveData()); - - GEODE_UNWRAP(this->disable()); - Loader::get()->queueInGDThread([&]() { - ModStateEvent(m_self, ModEventType::Unloaded).post(); - }); - - // Disabling unhooks and unpatches already - for (auto const& hook : m_hooks) { - delete hook; - } - m_hooks.clear(); - - for (auto const& patch : m_patches) { - delete patch; - } - m_patches.clear(); - - GEODE_UNWRAP(this->unloadPlatformBinary()); - m_binaryLoaded = false; - - Loader::get()->updateAllDependencies(); - return Ok(); } Result<> Mod::Impl::enable() { - if (!m_binaryLoaded) { - return this->loadBinary(); + if (!m_binaryLoaded) + return Err("Tried to enable {} but its binary is not loaded", m_metadata.getID()); + + bool enabledDependencies = true; + for (auto const& item : m_metadata.getDependencies()) { + if (!item.isResolved()) + continue; + auto res = item.mod->enable(); + if (!res) { + enabledDependencies = false; + log::error("Failed to enable {}: {}", item.id, res.unwrapErr()); + } } + if (!enabledDependencies) + return Err("Mod cannot be enabled because one or more of its dependencies cannot be enabled."); + + if (!this->canEnable()) + return Err("Mod cannot be enabled because it has unresolved dependencies."); + for (auto const& hook : m_hooks) { if (!hook) { log::warn("Hook is null in mod \"{}\"", m_metadata.getName()); @@ -401,41 +382,66 @@ Result<> Mod::Impl::enable() { } Result<> Mod::Impl::disable() { - if (!m_enabled) { + if (!m_enabled) return Ok(); + + if (!this->supportsDisabling()) + return Err("Mod does not support disabling."); + + if (!this->canDisable()) + return Err("Mod cannot be disabled because one or more of its dependants cannot be disabled."); + + // disable dependants + bool disabledDependants = true; + for (auto& item : m_dependants) { + auto res = item->disable(); + if (res) + continue; + disabledDependants = false; + log::error("Failed to disable {}: {}", item->getID(), res.unwrapErr()); } - if (!m_info.supportsDisabling()) { - return Err("Mod does not support disabling"); - } + + if (!disabledDependants) + return Err("Mod cannot be disabled because one or more of its dependants cannot be disabled."); Loader::get()->queueInGDThread([&]() { ModStateEvent(m_self, ModEventType::Disabled).post(); }); + std::vector<std::string> errors; for (auto const& hook : m_hooks) { - GEODE_UNWRAP(this->disableHook(hook)); + auto res = this->disableHook(hook); + if (!res) + errors.push_back(res.unwrapErr()); } for (auto const& patch : m_patches) { - if (!patch->restore()) { - return Err("Unable to restore patch at " + std::to_string(patch->getAddress())); - } + auto res = this->unpatch(patch); + if (!res) + errors.push_back(res.unwrapErr()); } m_enabled = false; + if (!errors.empty()) + return Err(utils::string::join(errors, "\n")); + return Ok(); } Result<> Mod::Impl::uninstall() { - if (m_info.supportsDisabling()) { + if (supportsDisabling()) { GEODE_UNWRAP(this->disable()); - if (m_info.supportsUnloading()) { - GEODE_UNWRAP(this->unloadBinary()); + } + else { + for (auto& item : m_dependants) { + if (!item->canDisable()) + continue; + GEODE_UNWRAP(item->disable()); } } try { - ghc::filesystem::remove(m_info.path()); + ghc::filesystem::remove(m_metadata.getPath()); } catch (std::exception& e) { return Err( @@ -455,51 +461,12 @@ bool Mod::Impl::isUninstalled() const { // Dependencies Result<> Mod::Impl::updateDependencies() { - bool hasUnresolved = false; - for (auto& dep : m_info.dependencies()) { - // set the dependency's loaded mod if such exists - if (!dep.mod) { - dep.mod = Loader::get()->getLoadedMod(dep.id); - // verify loaded dependency version - if (dep.mod && !dep.version.compare(dep.mod->getVersion())) { - dep.mod = nullptr; - } - } - - // check if the dependency is loaded - if (dep.mod) { - // update the dependency recursively - GEODE_UNWRAP(dep.mod->updateDependencies()); - - // enable mod if it's resolved & enabled - if (!dep.mod->hasUnresolvedDependencies()) { - if (dep.mod->isEnabled()) { - GEODE_UNWRAP(dep.mod->loadBinary().expect("Unable to load dependency: {error}")); - } - } - } - // check if the dependency is resolved now - if (!dep.isResolved()) { - GEODE_UNWRAP(this->unloadBinary().expect("Unable to unload mod: {error}")); - hasUnresolved = true; - } - } - // load if there weren't any unresolved dependencies - if (!hasUnresolved && !m_binaryLoaded) { - log::debug("All dependencies for {} found", m_info.id()); - if (m_enabled) { - log::debug("Resolved & loading {}", m_info.id()); - GEODE_UNWRAP(this->loadBinary()); - } - else { - log::debug("Resolved {}, however not loading it as it is disabled", m_info.id()); - } - } - return Ok(); + return Err("Mod::updateDependencies is no longer needed, " + "as this is handled by Loader::refreshModGraph"); } bool Mod::Impl::hasUnresolvedDependencies() const { - for (auto const& dep : m_info.dependencies()) { + for (auto const& dep : m_metadata.getDependencies()) { if (!dep.isResolved()) { return true; } @@ -507,10 +474,23 @@ bool Mod::Impl::hasUnresolvedDependencies() const { return false; } +bool Mod::Impl::hasUnresolvedIncompatibilities() const { + for (auto const& dep : m_metadata.getIncompatibilities()) { + if (!dep.isResolved()) { + return true; + } + } + return false; +} + +// msvc stop fucking screaming please i BEG YOU +#pragma warning(suppress : 4996) std::vector<Dependency> Mod::Impl::getUnresolvedDependencies() { +#pragma warning(suppress : 4996) std::vector<Dependency> unresolved; for (auto const& dep : m_metadata.getDependencies()) { if (!dep.isResolved()) { +#pragma warning(suppress : 4996) unresolved.push_back(dep); } } @@ -541,7 +521,7 @@ Result<> Mod::Impl::disableHook(Hook* hook) { Result<Hook*> Mod::Impl::addHook(Hook* hook) { m_hooks.push_back(hook); if (LoaderImpl::get()->isReadyToHook()) { - if (hook->getAutoEnable()) { + if (this->isEnabled() && hook->getAutoEnable()) { auto res = this->enableHook(hook); if (!res) { delete hook; @@ -582,21 +562,20 @@ Result<Patch*> Mod::Impl::patch(void* address, ByteVector const& data) { p->m_original = readMemory(address, data.size()); p->m_owner = m_self; p->m_patch = data; - if (!p->apply()) { + if (this->isEnabled() && !p->apply()) { delete p; - return Err("Unable to enable patch at " + std::to_string(p->getAddress())); + return Err("Unable to enable patch at " + std::to_string(reinterpret_cast<uintptr_t>(address))); } m_patches.push_back(p); return Ok(p); } Result<> Mod::Impl::unpatch(Patch* patch) { - if (patch->restore()) { - ranges::remove(m_patches, patch); - delete patch; - return Ok(); - } - return Err("Unable to restore patch!"); + if (!patch->restore()) + return Err("Unable to restore patch at " + std::to_string(patch->getAddress())); + ranges::remove(m_patches, patch); + delete patch; + return Ok(); } // Misc. @@ -697,27 +676,24 @@ static Result<ModMetadata> getModImplInfo() { Mod* Loader::Impl::createInternalMod() { auto& mod = Mod::sharedMod<>; - if (!mod) { - auto infoRes = getModImplInfo(); - if (!infoRes) { - LoaderImpl::get()->platformMessageBox( - "Fatal Internal Error", - "Unable to create internal mod info: \"" + infoRes.unwrapErr() + - "\"\n" - "This is a fatal internal error in the loader, please " - "contact Geode developers immediately!" - ); - auto info = ModInfo(); - info.id() = "geode.loader"; - mod = new Mod(info); - } - else { - mod = new Mod(infoRes.unwrap()); - } - mod->m_impl->m_binaryLoaded = true; - mod->m_impl->m_enabled = true; - m_mods.insert({ mod->getID(), mod }); + if (mod) return mod; + auto infoRes = getModImplInfo(); + if (!infoRes) { + LoaderImpl::get()->platformMessageBox( + "Fatal Internal Error", + "Unable to create internal mod info: \"" + infoRes.unwrapErr() + + "\"\n" + "This is a fatal internal error in the loader, please " + "contact Geode developers immediately!" + ); + mod = new Mod(ModMetadata("geode.loader")); } + else { + mod = new Mod(infoRes.unwrap()); + } + mod->m_impl->m_binaryLoaded = true; + mod->m_impl->m_enabled = true; + m_mods.insert({ mod->getID(), mod }); return mod; } diff --git a/loader/src/loader/ModImpl.hpp b/loader/src/loader/ModImpl.hpp index 14f1b7f6..a4b2560f 100644 --- a/loader/src/loader/ModImpl.hpp +++ b/loader/src/loader/ModImpl.hpp @@ -39,12 +39,11 @@ namespace geode { */ ghc::filesystem::path m_saveDirPath; /** - * Pointers to mods that depend on - * this Mod. Makes it possible to - * enable / disable them automatically, + * Pointers to mods that depend on this Mod. + * Makes it possible to enable / disable them automatically, * when their dependency is disabled. */ - std::vector<Mod*> m_parentDependencies; + std::vector<Mod*> m_dependants; /** * Saved values */ @@ -84,8 +83,8 @@ namespace geode { bool isEnabled() const; bool isLoaded() const; bool supportsDisabling() const; - bool wasSuccesfullyLoaded() const; - ModInfo getModInfo() const; + bool canDisable() const; + bool canEnable() const; bool wasSuccessfullyLoaded() const; ModMetadata getMetadata() const; ghc::filesystem::path getTempDir() const; @@ -113,16 +112,17 @@ namespace geode { Result<> removeHook(Hook* hook); Result<Patch*> patch(void* address, ByteVector const& data); Result<> unpatch(Patch* patch); - Result<> loadBinary(); - Result<> unloadBinary(); Result<> enable(); Result<> disable(); Result<> uninstall(); bool isUninstalled() const; bool depends(std::string const& id) const; - bool hasUnresolvedDependencies() const; Result<> updateDependencies(); - std::vector<Dependency> getUnresolvedDependencies(); + bool hasUnresolvedDependencies() const; + bool hasUnresolvedIncompatibilities() const; + [[deprecated]] std::vector<Dependency> getUnresolvedDependencies(); + + Result<> loadBinary(); char const* expandSpriteName(char const* name); ModJson getRuntimeInfo() const; diff --git a/loader/src/loader/ModInfoImpl.cpp b/loader/src/loader/ModInfoImpl.cpp index 7c1e4ed3..d968c386 100644 --- a/loader/src/loader/ModInfoImpl.cpp +++ b/loader/src/loader/ModInfoImpl.cpp @@ -5,8 +5,7 @@ #include "ModInfoImpl.hpp" -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" +#pragma warning(disable : 4996) // deprecation using namespace geode::prelude; @@ -20,34 +19,6 @@ bool Dependency::isResolved() const { this->version.compare(this->mod->getVersion())); } -bool ModInfo::Impl::validateID(std::string const& id) { - return ModMetadata::Impl::validateID(id); -} - -Result<ModInfo> ModInfo::Impl::create(ModJson const& json) { - return ModMetadata::Impl::create(json); -} - -Result<ModInfo> ModInfo::Impl::createFromFile(ghc::filesystem::path const& path) { - return ModMetadata::Impl::createFromFile(path); -} - -Result<ModInfo> ModInfo::Impl::createFromGeodeFile(ghc::filesystem::path const& path) { - return ModMetadata::Impl::createFromGeodeFile(path); -} - -Result<ModInfo> ModInfo::Impl::createFromGeodeZip(file::Unzip& unzip) { - return ModMetadata::Impl::createFromGeodeZip(unzip); -} - -ModJson ModInfo::Impl::toJSON() const { - return m_metadata.m_rawJSON; -} - -ModJson ModInfo::Impl::getRawJSON() const { - return m_metadata.m_rawJSON; -} - bool ModInfo::Impl::operator==(ModInfo::Impl const& other) const { return this->m_metadata.m_id == other.m_metadata.m_id; } @@ -186,33 +157,34 @@ bool const& ModInfo::isAPI() const { } Result<ModInfo> ModInfo::createFromGeodeZip(utils::file::Unzip& zip) { - return Impl::createFromGeodeZip(zip); + return ModMetadata::Impl::createFromGeodeZip(zip); } Result<ModInfo> ModInfo::createFromGeodeFile(ghc::filesystem::path const& path) { - return Impl::createFromGeodeFile(path); + return ModMetadata::Impl::createFromGeodeFile(path); } Result<ModInfo> ModInfo::createFromFile(ghc::filesystem::path const& path) { - return Impl::createFromFile(path); + return ModMetadata::Impl::createFromFile(path); } Result<ModInfo> ModInfo::create(ModJson const& json) { - return Impl::create(json); + return ModMetadata::Impl::create(json); } ModJson ModInfo::toJSON() const { - return m_impl->toJSON(); + return m_impl->m_metadata.m_rawJSON; } ModJson ModInfo::getRawJSON() const { - return m_impl->getRawJSON(); + return m_impl->m_metadata.m_rawJSON; } bool ModInfo::operator==(ModInfo const& other) const { return m_impl->operator==(*other.m_impl); } +#pragma warning(suppress : 4996) ModInfo::ModInfo() : m_impl(std::make_unique<Impl>()) {} ModInfo::ModInfo(ModInfo const& other) : m_impl(std::make_unique<Impl>(*other.m_impl)) {} @@ -247,6 +219,26 @@ ModInfo::operator ModMetadata() const { return metadata; } -ModInfo::~ModInfo() = default; +ModJson& ModInfo::rawJSON() { + return m_impl->m_metadata.m_rawJSON; +} +ModJson const& ModInfo::rawJSON() const { + return m_impl->m_metadata.m_rawJSON; +} -#pragma clang diagnostic pop +Result<ModInfo> ModInfo::createFromSchemaV010(geode::ModJson const& json) { + return ModMetadata::Impl::createFromSchemaV010(json); +} + +Result<> ModInfo::addSpecialFiles(ghc::filesystem::path const& dir) { + return m_impl->m_metadata.addSpecialFiles(dir); +} +Result<> ModInfo::addSpecialFiles(utils::file::Unzip& zip) { + return m_impl->m_metadata.addSpecialFiles(zip); +} + +std::vector<std::pair<std::string, std::optional<std::string>*>> ModInfo::getSpecialFiles() { + return m_impl->m_metadata.getSpecialFiles(); +} + +ModInfo::~ModInfo() = default; diff --git a/loader/src/loader/ModInfoImpl.hpp b/loader/src/loader/ModInfoImpl.hpp index 3ebd981c..fcd80232 100644 --- a/loader/src/loader/ModInfoImpl.hpp +++ b/loader/src/loader/ModInfoImpl.hpp @@ -7,10 +7,12 @@ #include <Geode/utils/JsonValidation.hpp> #include <Geode/utils/VersionInfo.hpp> +#pragma warning(disable : 4996) // deprecation + using namespace geode::prelude; namespace geode { - class ModInfo::Impl { + class [[deprecated]] ModInfo::Impl { public: ModMetadata::Impl m_metadata; std::optional<IssuesInfo> m_issues; @@ -18,31 +20,11 @@ namespace geode { bool m_supportsDisabling = true; bool m_supportsUnloading = false; - static Result<ModInfo> createFromGeodeZip(utils::file::Unzip& zip); - static Result<ModInfo> createFromGeodeFile(ghc::filesystem::path const& path); - static Result<ModInfo> createFromFile(ghc::filesystem::path const& path); - static Result<ModInfo> create(ModJson const& json); - - ModJson toJSON() const; - ModJson getRawJSON() const; - bool operator==(ModInfo::Impl const& other) const; - - static bool validateID(std::string const& id); - - static Result<ModInfo> createFromSchemaV010(ModJson const& json); - - Result<> addSpecialFiles(ghc::filesystem::path const& dir); - Result<> addSpecialFiles(utils::file::Unzip& zip); - - std::vector<std::pair<std::string, std::optional<std::string>*>> getSpecialFiles(); }; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - class ModInfoImpl { + class [[deprecated]] ModInfoImpl { public: static ModInfo::Impl& getImpl(ModInfo& info); }; -#pragma clang diagnostic pop } diff --git a/loader/src/loader/ModMetadataImpl.cpp b/loader/src/loader/ModMetadataImpl.cpp index f7157dd5..a814d611 100644 --- a/loader/src/loader/ModMetadataImpl.cpp +++ b/loader/src/loader/ModMetadataImpl.cpp @@ -22,11 +22,9 @@ bool ModMetadata::Dependency::isResolved() const { } bool ModMetadata::Incompatibility::isResolved() const { - return !this->mod || !this->mod->isLoaded() || !this->version.compare(this->mod->getVersion()); + return !this->mod || !this->version.compare(this->mod->getVersion()); } -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" ModMetadata::Dependency::operator geode::Dependency() { return {id, version, importance == Importance::Required, mod}; } @@ -53,7 +51,6 @@ ModMetadata::Dependency ModMetadata::Dependency::fromDeprecated(geode::Dependenc ModMetadata::IssuesInfo ModMetadata::IssuesInfo::fromDeprecated(geode::IssuesInfo const& value) { return {value.info, value.url}; } -#pragma clang diagnostic pop static std::string sanitizeDetailsData(std::string const& str) { // delete CRLF @@ -473,8 +470,6 @@ ModMetadata& ModMetadata::operator=(ModMetadata&& other) noexcept { return *this; } -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" ModMetadata::operator ModInfo() { ModInfo info; auto infoImpl = ModInfoImpl::getImpl(info); @@ -493,7 +488,6 @@ ModMetadata::operator ModInfo() const { infoImpl.m_dependencies.push_back(dep); return info; } -#pragma clang diagnostic pop ModMetadata::~ModMetadata() = default; @@ -504,6 +498,7 @@ struct json::Serialize<geode::ModMetadata::Dependency::Importance> { case geode::ModMetadata::Dependency::Importance::Required: return {"required"}; case geode::ModMetadata::Dependency::Importance::Recommended: return {"recommended"}; case geode::ModMetadata::Dependency::Importance::Suggested: return {"suggested"}; + default: return {"unknown"}; } } static geode::ModMetadata::Dependency::Importance GEODE_DLL from_json(json::Value const& importance) { diff --git a/loader/src/ui/internal/info/ModInfoPopup.cpp b/loader/src/ui/internal/info/ModInfoPopup.cpp index ce47c675..92068f44 100644 --- a/loader/src/ui/internal/info/ModInfoPopup.cpp +++ b/loader/src/ui/internal/info/ModInfoPopup.cpp @@ -573,9 +573,9 @@ void LocalModInfoPopup::onEnableMod(CCObject* sender) { )->show(); } if (as<CCMenuItemToggler*>(sender)->isToggled()) { - auto res = m_mod->loadBinary(); + auto res = m_mod->enable(); if (!res) { - FLAlertLayer::create(nullptr, "Error Loading Mod", res.unwrapErr(), "OK", nullptr)->show(); + FLAlertLayer::create(nullptr, "Error Enabling Mod", res.unwrapErr(), "OK", nullptr)->show(); } } else { @@ -585,7 +585,7 @@ void LocalModInfoPopup::onEnableMod(CCObject* sender) { } } if (m_layer) { - m_layer->updateAllStates(nullptr); + m_layer->updateAllStates(); } as<CCMenuItemToggler*>(sender)->toggle(m_mod->isEnabled()); } diff --git a/loader/src/ui/internal/list/ModListCell.cpp b/loader/src/ui/internal/list/ModListCell.cpp index 5944760d..4bd8c9fd 100644 --- a/loader/src/ui/internal/list/ModListCell.cpp +++ b/loader/src/ui/internal/list/ModListCell.cpp @@ -31,7 +31,8 @@ float ModListCell::getLogoSize() const { void ModListCell::setupInfo( ModMetadata const& metadata, bool spaceForTags, - ModListDisplay display + ModListDisplay display, + bool inactive ) { m_menu = CCMenu::create(); m_menu->setPosition(m_width - 40.f, m_height / 2); @@ -41,6 +42,10 @@ void ModListCell::setupInfo( auto logoSpr = this->createLogo({ logoSize, logoSize }); logoSpr->setPosition({ logoSize / 2 + 12.f, m_height / 2 }); + auto logoSprColor = typeinfo_cast<CCRGBAProtocol*>(logoSpr); + if (inactive && logoSprColor) { + logoSprColor->setColor({ 163, 163, 163 }); + } this->addChild(logoSpr); bool hasDesc = @@ -63,6 +68,9 @@ void ModListCell::setupInfo( titleLabel->setPositionY(m_height / 2 + 7.f); } titleLabel->limitLabelWidth(m_width / 2 - 40.f, .5f, .1f); + if (inactive) { + titleLabel->setColor({ 163, 163, 163 }); + } this->addChild(titleLabel); auto versionLabel = CCLabelBMFont::create( @@ -76,6 +84,9 @@ void ModListCell::setupInfo( titleLabel->getPositionY() - 1.f ); versionLabel->setColor({ 0, 255, 0 }); + if (inactive) { + versionLabel->setColor({ 0, 163, 0 }); + } this->addChild(versionLabel); if (auto tag = metadata.getVersion().getTag()) { @@ -93,6 +104,9 @@ void ModListCell::setupInfo( auto creatorStr = "by " + metadata.getDeveloper(); auto creatorLabel = CCLabelBMFont::create(creatorStr.c_str(), "goldFont.fnt"); creatorLabel->setScale(.43f); + if (inactive) { + creatorLabel->setColor({ 163, 163, 163 }); + } m_developerBtn = CCMenuItemSpriteExtra::create( creatorLabel, this, menu_selector(ModListCell::onViewDev) @@ -133,6 +147,9 @@ void ModListCell::setupInfo( m_description->setAnchorPoint({ .0f, .5f }); m_description->setPosition(m_height / 2 + logoSize / 2 + 18.f, descBG->getPositionY()); m_description->limitLabelWidth(m_width / 2 - 10.f, .5f, .1f); + if (inactive) { + m_description->setColor({ 163, 163, 163 }); + } this->addChild(m_description); } } @@ -187,11 +204,14 @@ void ModCell::onEnable(CCObject* sender) { else { tryOrAlert(m_mod->disable(), "Error disabling mod"); } - if (m_layer) { - m_layer->updateAllStates(this); - } + Loader::get()->queueInGDThread([this]() { + if (m_layer) { + m_layer->updateAllStates(); + } + }); } +// TODO: for fod maybe :3 show problems related to this mod void ModCell::onUnresolvedInfo(CCObject*) { std::string info = "This mod has the following " @@ -211,6 +231,11 @@ void ModCell::onInfo(CCObject*) { LocalModInfoPopup::create(m_mod, m_layer)->show(); } +void ModCell::onRestart(CCObject*) { + utils::game::restart(); +} + +// TODO: for fod maybe :3 check if there are any problems related to this mod void ModCell::updateState() { bool unresolved = m_mod->hasUnresolvedDependencies(); if (m_enableToggle) { @@ -232,18 +257,50 @@ bool ModCell::init( ) { if (!ModListCell::init(list, size)) return false; - m_mod = mod; this->setupInfo(mod->getMetadata(), false, display, m_mod->isUninstalled()); - auto viewSpr = ButtonSprite::create("View", "bigFont.fnt", "GJ_button_01.png", .8f); - viewSpr->setScale(.65f); + if (mod->isUninstalled()) { + auto restartSpr = ButtonSprite::create("Restart", "bigFont.fnt", "GJ_button_03.png", .8f); + restartSpr->setScale(.65f); - auto viewBtn = CCMenuItemSpriteExtra::create(viewSpr, this, menu_selector(ModCell::onInfo)); - m_menu->addChild(viewBtn); + auto restartBtn = CCMenuItemSpriteExtra::create(restartSpr, this, menu_selector(ModCell::onRestart)); + restartBtn->setPositionX(-16.f); + m_menu->addChild(restartBtn); + } + else { + auto viewSpr = ButtonSprite::create("View", "bigFont.fnt", "GJ_button_01.png", .8f); + viewSpr->setScale(.65f); - if (m_mod->wasSuccesfullyLoaded() && m_mod->supportsDisabling()) { + auto viewBtn = CCMenuItemSpriteExtra::create(viewSpr, this, menu_selector(ModCell::onInfo)); + m_menu->addChild(viewBtn); + + if (m_mod->wasSuccessfullyLoaded()) { + auto latestIndexItem = Index::get()->getMajorItem( + mod->getMetadata().getID() + ); + + if (latestIndexItem && Index::get()->isUpdateAvailable(latestIndexItem)) { + viewSpr->updateBGImage("GE_button_01.png"_spr); + + auto minorIndexItem = Index::get()->getItem( + mod->getMetadata().getID(), + ComparableVersionInfo(mod->getMetadata().getVersion(), VersionCompare::MoreEq) + ); + + if (latestIndexItem->getMetadata().getVersion().getMajor() > minorIndexItem->getMetadata().getVersion().getMajor()) { + 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); + } + } + } + } + + if (m_mod->wasSuccessfullyLoaded() && m_mod->supportsDisabling() && !m_mod->isUninstalled()) { m_enableToggle = CCMenuItemToggler::createWithStandardSprites(this, menu_selector(ModCell::onEnable), .7f); m_enableToggle->setPosition(-45.f, 0.f); @@ -259,30 +316,6 @@ bool ModCell::init( m_unresolvedExMark->setVisible(false); m_menu->addChild(m_unresolvedExMark); - if (m_mod->wasSuccesfullyLoaded()) { - - auto latestIndexItem = Index::get()->getMajorItem( - mod->getModInfo().id() - ); - - if (latestIndexItem && Index::get()->isUpdateAvailable(latestIndexItem)) { - viewSpr->updateBGImage("GE_button_01.png"_spr); - - auto minorIndexItem = Index::get()->getItem( - mod->getModInfo().id(), - ComparableVersionInfo(mod->getModInfo().version(), VersionCompare::MoreEq) - ); - - if (latestIndexItem->getModInfo().version().getMajor() > minorIndexItem->getModInfo().version().getMajor()) { - 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(); return true; @@ -302,6 +335,10 @@ void IndexItemCell::onInfo(CCObject*) { IndexItemInfoPopup::create(m_item, m_layer)->show(); } +void IndexItemCell::onRestart(CCObject*) { + utils::game::restart(); +} + IndexItemCell* IndexItemCell::create( IndexItemHandle item, ModListLayer* list, @@ -327,11 +364,24 @@ bool IndexItemCell::init( m_item = item; - this->setupInfo(item->getModInfo(), item->getTags().size(), display); this->setupInfo(item->getMetadata(), item->getTags().size(), display, item->isInstalled()); - auto viewBtn = CCMenuItemSpriteExtra::create(viewSpr, this, menu_selector(IndexItemCell::onInfo)); - m_menu->addChild(viewBtn); + if (item->isInstalled()) { + auto restartSpr = ButtonSprite::create("Restart", "bigFont.fnt", "GJ_button_03.png", .8f); + restartSpr->setScale(.65f); + + auto restartBtn = CCMenuItemSpriteExtra::create(restartSpr, this, menu_selector(IndexItemCell::onRestart)); + restartBtn->setPositionX(-16.f); + m_menu->addChild(restartBtn); + } + else { + 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 (item->getTags().size()) { float x = m_height / 2 + this->getLogoSize() / 2 + 13.f; @@ -401,7 +451,6 @@ void InvalidGeodeFileCell::FLAlert_Clicked(FLAlertLayer*, bool btn2) { ) ->show(); } - Loader::get()->refreshModsList(); if (m_layer) { m_layer->reloadList(); } diff --git a/loader/src/ui/internal/list/ModListCell.hpp b/loader/src/ui/internal/list/ModListCell.hpp index 3fe4c7ef..64892045 100644 --- a/loader/src/ui/internal/list/ModListCell.hpp +++ b/loader/src/ui/internal/list/ModListCell.hpp @@ -55,6 +55,7 @@ protected: ); void onInfo(CCObject*); + void onRestart(CCObject*); void onEnable(CCObject*); void onUnresolvedInfo(CCObject*); @@ -86,6 +87,7 @@ protected: ); void onInfo(CCObject*); + void onRestart(CCObject*); public: static IndexItemCell* create( diff --git a/loader/src/ui/internal/list/ModListLayer.cpp b/loader/src/ui/internal/list/ModListLayer.cpp index 58103ce0..8a768c65 100644 --- a/loader/src/ui/internal/list/ModListLayer.cpp +++ b/loader/src/ui/internal/list/ModListLayer.cpp @@ -155,26 +155,32 @@ CCArray* ModListLayer::createModCells(ModListType type, ModListQuery const& quer )); } - // sort the mods by match score - std::multimap<int, Mod*> sorted; + // sort the mods by match score + std::multimap<int, ModListCell*> sorted; // then other mods + + // newly installed + for (auto const& item : Index::get()->getItems()) { + if (!item->isInstalled()) + continue; + if (auto match = queryMatch(query, item)) { + auto cell = IndexItemCell::create(item, this, m_display, this->getCellSize()); + sorted.insert({ match.value(), cell }); + } + } + + // loaded for (auto const& mod : Loader::get()->getAllMods()) { - // 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; - // only show mods that match query in list if (auto match = queryMatch(query, mod)) { - sorted.insert({ match.value(), mod }); + auto cell = ModCell::create(mod, this, m_display, this->getCellSize()); + sorted.insert({ match.value(), cell }); } } // add the mods sorted - for (auto& [score, mod] : ranges::reverse(sorted)) { - mods->addObject(ModCell::create( - mod, this, m_display, this->getCellSize() - )); + for (auto& [score, cell] : ranges::reverse(sorted)) { + mods->addObject(cell); } } break; @@ -546,14 +552,11 @@ void ModListLayer::reloadList(std::optional<ModListQuery> const& query) { } } -void ModListLayer::updateAllStates(ModListCell* toggled) { +void ModListLayer::updateAllStates() { for (auto cell : CCArrayExt<GenericListCell>( m_list->m_listView->m_tableView->m_cellArray )) { - auto node = static_cast<ModListCell*>(cell->getChildByID("mod-list-cell")); - if (toggled != node) { - node->updateState(); - } + static_cast<ModListCell*>(cell->getChildByID("mod-list-cell"))->updateState(); } } @@ -612,7 +615,6 @@ void ModListLayer::onExit(CCObject*) { } void ModListLayer::onReload(CCObject*) { - Loader::get()->refreshModsList(); this->reloadList(); } diff --git a/loader/src/ui/internal/list/ModListLayer.hpp b/loader/src/ui/internal/list/ModListLayer.hpp index 4cff18d8..16ca3ec7 100644 --- a/loader/src/ui/internal/list/ModListLayer.hpp +++ b/loader/src/ui/internal/list/ModListLayer.hpp @@ -84,7 +84,7 @@ protected: public: static ModListLayer* create(); static ModListLayer* scene(); - void updateAllStates(ModListCell* except = nullptr); + void updateAllStates(); ModListDisplay getDisplay() const; ModListQuery& getQuery();