diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e486600e..553a07045 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,27 @@ * Fix json library not actually being dynamically exported/imported (5f65d97) * Update TulipHook, gets rid of keystone dependency and adds stdcall support (efcbf58, 7b90903) * Make resources.zip platform dependent (e41784e) + * Add utils::string::join (82e128b) + * Add logger nesting: log::pushNest, log::pushNest (7d74f16) + * Add "any" version to comparable versions (2b1dc17) + * Deprecate ModInfo, replaced with ModMetadata (53b52ea) + * Add utils::game::restart (7f449b9, 0e1d639) + * Rework the way dependencies and mod loading works (5200128) + * Early load loads mods before the game starts, while non-early load loads on the loading screen now (e718069) + * Add support for specifying incompatibilities (5200128, 8908235) + * Add support for specifying suggested and recommended optional dependencies (5200128) + * Add UI to select which mods to install (3707418, 73169fb, cd772bd) + * Dependencies/dependants automatically get toggled on toggle (5200128, 6ab542d) + * Add problems list (5200128, aee84c0) + * Show amount of currently loaded mods on the loading screen while they're loading (e718069, 1d5fae8) + * Improve index-related UI (73169fb) + * Remove Android and iOS filters for now + * Add filter to show non-installable mods + * API in quick popups to distinguish between pressing button 1 and the Escape key + * Add "API" label to API mods (cb8759b) + * Fix index not displaying tags (ed5b5c9) + * Change "Cancel" to say "Keep" in the remove mod data on mod uninstall dialogue (cd772bd) + * Fix typos in the word "successfully" (5200128, f316c86) * Fix MacOS HSV button missing in `CustomizeObjectLayer` (d98cb2d) * Make missing functions and members private (d98cb2d) * Update Broma to latest version (0a58432) diff --git a/loader/CMakeLists.txt b/loader/CMakeLists.txt index 39ce2a236..27cd7ed81 100644 --- a/loader/CMakeLists.txt +++ b/loader/CMakeLists.txt @@ -165,7 +165,10 @@ endif() target_compile_definitions(${PROJECT_NAME} PUBLIC GEODE_EXPORTING MAT_JSON_EXPORTING) -target_compile_definitions(${PROJECT_NAME} PRIVATE _CRT_SECURE_NO_WARNINGS) +target_compile_definitions(${PROJECT_NAME} PRIVATE + GEODE_EXPOSE_SECRET_INTERNALS_IN_HEADERS_DO_NOT_DEFINE_PLEASE + _CRT_SECURE_NO_WARNINGS +) # These are only needed for building source :-) if (NOT GEODE_BUILDING_DOCS) diff --git a/loader/include/Geode/loader/Index.hpp b/loader/include/Geode/loader/Index.hpp index 2e55ca6dd..d3db061a9 100644 --- a/loader/include/Geode/loader/Index.hpp +++ b/loader/include/Geode/loader/Index.hpp @@ -2,6 +2,7 @@ #include "Types.hpp" #include "ModInfo.hpp" +#include "ModMetadata.hpp" #include "Event.hpp" #include "../utils/Result.hpp" #include "../utils/web.hpp" @@ -107,12 +108,23 @@ namespace geode { public: ghc::filesystem::path getPath() const; - ModInfo getModInfo() const; + [[deprecated("use getMetadata instead")]] ModInfo getModInfo() const; + ModMetadata getMetadata() const; std::string getDownloadURL() const; std::string getPackageHash() const; std::unordered_set getAvailablePlatforms() const; bool isFeatured() const; std::unordered_set getTags() const; + bool isInstalled() const; + +#if defined(GEODE_EXPOSE_SECRET_INTERNALS_IN_HEADERS_DO_NOT_DEFINE_PLEASE) + void setMetadata(ModMetadata const& value); + void setDownloadURL(std::string const& value); + void setPackageHash(std::string const& value); + void setAvailablePlatforms(std::unordered_set const& value); + void setIsFeatured(bool const& value); + void setTags(std::unordered_set const& value); +#endif IndexItem(); ~IndexItem(); @@ -204,8 +216,15 @@ namespace geode { * Get an item from the index by its mod.json * @param info The mod's info * @returns The item, or nullptr if the item was not found + * @deprecated Use the ModMetadata overload instead + */ + [[deprecated]] IndexItemHandle getItem(ModInfo const& info) const; + /** + * Get an item from the index by its mod.json + * @param info The mod's metadata + * @returns The item, or nullptr if the item was not found */ - IndexItemHandle getItem(ModInfo const& info) const; + IndexItemHandle getItem(ModMetadata const& metadata) const; /** * Get an item from the index that corresponds to an installed mod * @param mod An installed mod @@ -224,6 +243,12 @@ namespace geode { * Check if any of the mods on the index have updates available */ bool areUpdatesAvailable() const; + /** + * Checks if the mod and its required dependencies can be installed + * @param item Item to get the list for + * @returns Success if the mod and its required dependencies can be installed, an error otherwise + */ + Result<> canInstall(IndexItemHandle item) const; /** * Get the list of items needed to install this item (dependencies, etc.) * @param item Item to get the list for diff --git a/loader/include/Geode/loader/Loader.hpp b/loader/include/Geode/loader/Loader.hpp index 8e8d1c1ce..cb63fed15 100644 --- a/loader/include/Geode/loader/Loader.hpp +++ b/loader/include/Geode/loader/Loader.hpp @@ -4,7 +4,9 @@ #include "../utils/Result.hpp" #include "../utils/MiniFunction.hpp" #include "Log.hpp" +#include "ModEvent.hpp" #include "ModInfo.hpp" +#include "ModMetadata.hpp" #include "Types.hpp" #include @@ -18,6 +20,25 @@ namespace geode { std::string reason; }; + struct LoadProblem { + enum class Type : uint8_t { + Unknown, + Suggestion, + Recommendation, + Conflict, + InvalidFile, + Duplicate, + SetupFailed, + LoadFailed, + EnableFailed, + MissingDependency, + PresentIncompatibility + }; + Type type; + std::variant cause; + std::string message; + }; + class LoaderImpl; class GEODE_DLL Loader { @@ -36,14 +57,25 @@ namespace geode { void dispatchScheduledFunctions(Mod* mod); friend void GEODE_CALL ::geode_implicit_load(Mod*); - Result loadModFromInfo(ModInfo const& info); - + [[deprecated]] Result loadModFromInfo(ModInfo const& info); + Mod* takeNextMod(); public: // TODO: do we want to expose all of these functions? static Loader* get(); + enum class LoadingState : uint8_t { + None, + Queue, + List, + Graph, + EarlyMods, + Mods, + Problems, + Done + }; + Result<> saveData(); Result<> loadData(); @@ -52,17 +84,19 @@ namespace geode { VersionInfo maxModVersion(); bool isModVersionSupported(VersionInfo const& version); - Result loadModFromFile(ghc::filesystem::path const& file); - void loadModsFromDirectory(ghc::filesystem::path const& dir, bool recursive = true); - void refreshModsList(); + [[deprecated]] Result loadModFromFile(ghc::filesystem::path const& file); + [[deprecated]] void loadModsFromDirectory(ghc::filesystem::path const& dir, bool recursive = true); + [[deprecated]] void refreshModsList(); + LoadingState getLoadingState(); 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 getAllMods(); - Mod* getModImpl(); - void updateAllDependencies(); - std::vector getFailedMods() const; + [[deprecated("use Mod::get instead")]] Mod* getModImpl(); + [[deprecated]] void updateAllDependencies(); + [[deprecated("use getProblems instead")]] std::vector getFailedMods() const; + std::vector getProblems() const; void updateResources(); void updateResources(bool forceReload); diff --git a/loader/include/Geode/loader/Log.hpp b/loader/include/Geode/loader/Log.hpp index b3c255bdf..817dd47bc 100644 --- a/loader/include/Geode/loader/Log.hpp +++ b/loader/include/Geode/loader/Log.hpp @@ -154,6 +154,7 @@ namespace geode { bool operator==(Log const& l); std::string toString(bool logTime = true) const; + std::string toString(bool logTime, uint32_t nestLevel) const; std::vector& getComponents(); log_clock::time_point getTime() const; @@ -170,6 +171,7 @@ namespace geode { private: static std::vector& logs(); static std::ofstream& logStream(); + static uint32_t& nestLevel(); Logger() = delete; ~Logger() = delete; @@ -179,9 +181,11 @@ namespace geode { static void setup(); static void push(Log&& log); - static void pop(Log* log); + static void pushNest(); + static void popNest(); + static std::vector list(); static void clear(); }; @@ -223,5 +227,12 @@ namespace geode { void error(Args... args) { internalLog(Severity::Error, getMod(), args...); } + + static void pushNest() { + Logger::pushNest(); + } + static void popNest() { + Logger::popNest(); + } } } diff --git a/loader/include/Geode/loader/Mod.hpp b/loader/include/Geode/loader/Mod.hpp index 1010f2a6c..d80de2dee 100644 --- a/loader/include/Geode/loader/Mod.hpp +++ b/loader/include/Geode/loader/Mod.hpp @@ -7,6 +7,7 @@ #include "../utils/general.hpp" #include "Hook.hpp" #include "ModInfo.hpp" +#include "ModMetadata.hpp" #include "Setting.hpp" #include "Types.hpp" @@ -46,7 +47,6 @@ namespace geode { std::unique_ptr m_impl; friend class Loader; - friend struct ModInfo; template static inline GEODE_HIDDEN Mod* sharedMod = nullptr; @@ -66,7 +66,8 @@ namespace geode { // Protected constructor/destructor Mod() = delete; - Mod(ModInfo const& info); + [[deprecated]] Mod(ModInfo const& info); + Mod(ModMetadata const& metadata); ~Mod(); std::string getID() const; @@ -79,9 +80,14 @@ 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; + bool needsEarlyLoad() const; + [[deprecated]] bool supportsUnloading() const; + [[deprecated("use wasSuccessfullyLoaded instead")]] bool wasSuccesfullyLoaded() const; + bool wasSuccessfullyLoaded() const; + [[deprecated("use getMetadata instead")]] ModInfo getModInfo() const; + ModMetadata getMetadata() const; ghc::filesystem::path getTempDir() const; /** * Get the path to the mod's platform binary (.dll on Windows, .dylib @@ -94,6 +100,11 @@ namespace geode { */ ghc::filesystem::path getResourcesDir() const; +#if defined(GEODE_EXPOSE_SECRET_INTERNALS_IN_HEADERS_DO_NOT_DEFINE_PLEASE) + void setMetadata(ModMetadata const& metadata); + std::vector getDependants() const; +#endif + Result<> saveData(); Result<> loadData(); @@ -293,7 +304,7 @@ namespace geode { * @returns Successful result on success, * errorful result with info on error */ - Result<> loadBinary(); + [[deprecated]] Result<> loadBinary(); /** * Disable & unload this mod @@ -302,7 +313,7 @@ namespace geode { * @returns Successful result on success, * errorful result with info on error */ - Result<> unloadBinary(); + [[deprecated]] Result<> unloadBinary(); /** * Enable this mod @@ -319,10 +330,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 */ @@ -335,6 +343,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 @@ -344,21 +362,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 getUnresolvedDependencies(); + [[deprecated("use Loader::getProblems instead")]] std::vector getUnresolvedDependencies(); char const* expandSpriteName(char const* name); diff --git a/loader/include/Geode/loader/ModInfo.hpp b/loader/include/Geode/loader/ModInfo.hpp index 07f867a32..431fd653c 100644 --- a/loader/include/Geode/loader/ModInfo.hpp +++ b/loader/include/Geode/loader/ModInfo.hpp @@ -13,7 +13,9 @@ namespace geode { class Unzip; } - struct GEODE_DLL Dependency { + class ModMetadata; + + struct GEODE_DLL [[deprecated("use ModMetadata::Dependency instead")]] Dependency { std::string id; ComparableVersionInfo version; bool required = false; @@ -21,7 +23,7 @@ namespace geode { bool isResolved() const; }; - struct IssuesInfo { + struct [[deprecated("use ModMetadata::IssuesInfo instead")]] IssuesInfo { std::string info; std::optional url; }; @@ -29,11 +31,12 @@ namespace geode { class ModInfoImpl; /** - * Represents all the data gatherable + * Represents all the data gather-able * from mod.json */ - class GEODE_DLL ModInfo { + class GEODE_DLL [[deprecated("use ModMetadata instead")]] ModInfo { class Impl; +#pragma warning(suppress : 4996) std::unique_ptr m_impl; public: @@ -82,7 +85,7 @@ namespace geode { /** * The name of the head developer. * Should be a single name, like - * "HJfod" or "The Geode Team". + * "HJfod" or "Geode Team". * If the mod has multiple * developers, this field should * be one of their name or a team @@ -194,6 +197,9 @@ namespace geode { static bool validateID(std::string const& id); + operator ModMetadata(); + operator ModMetadata() const; + private: ModJson& rawJSON(); ModJson const& rawJSON() const; @@ -210,11 +216,13 @@ namespace geode { std::vector*>> getSpecialFiles(); friend class ModInfoImpl; + + friend class ModMetadata; }; } template <> -struct json::Serialize { +struct [[deprecated]] json::Serialize { static json::Value to_json(geode::ModInfo const& info) { return info.toJSON(); } diff --git a/loader/include/Geode/loader/ModMetadata.hpp b/loader/include/Geode/loader/ModMetadata.hpp new file mode 100644 index 000000000..0647d98aa --- /dev/null +++ b/loader/include/Geode/loader/ModMetadata.hpp @@ -0,0 +1,251 @@ +#pragma once + +#include "../utils/Result.hpp" +#include "../utils/VersionInfo.hpp" +#include "ModInfo.hpp" +#include "Setting.hpp" +#include "Types.hpp" + +#include +#include + +namespace geode { + namespace utils::file { + class Unzip; + } + + struct GEODE_DLL [[deprecated("use ModMetadata::Dependency instead")]] Dependency; + struct [[deprecated("use ModMetadata::IssuesInfo instead")]] IssuesInfo; + + class ModMetadataImpl; + + /** + * Represents all the data gather-able + * from mod.json + */ + class GEODE_DLL ModMetadata { + class Impl; + std::unique_ptr m_impl; + + public: + ModMetadata(); + explicit ModMetadata(std::string id); + ModMetadata(ModMetadata const& other); + ModMetadata(ModMetadata&& other) noexcept; + ModMetadata& operator=(ModMetadata const& other); + ModMetadata& operator=(ModMetadata&& other) noexcept; + ~ModMetadata(); + + struct GEODE_DLL Dependency { + enum class Importance : uint8_t { Required, Recommended, Suggested }; + std::string id; + ComparableVersionInfo version; + Importance importance = Importance::Required; + Mod* mod = nullptr; + [[nodiscard]] bool isResolved() const; + + [[deprecated]] operator geode::Dependency(); + [[deprecated]] operator geode::Dependency() const; + + [[deprecated]] static Dependency fromDeprecated(geode::Dependency const& value); + }; + + struct GEODE_DLL Incompatibility { + enum class Importance : uint8_t { + Breaking, + Conflicting + }; + std::string id; + ComparableVersionInfo version; + Importance importance = Importance::Breaking; + Mod* mod = nullptr; + [[nodiscard]] bool isResolved() const; + }; + + struct IssuesInfo { + std::string info; + std::optional url; + + [[deprecated]] operator geode::IssuesInfo(); + [[deprecated]] operator geode::IssuesInfo() const; + + [[deprecated]] static IssuesInfo fromDeprecated(geode::IssuesInfo const& value); + }; + + /** + * Path to the mod file + */ + [[nodiscard]] ghc::filesystem::path getPath() const; + /** + * Name of the platform binary within + * the mod zip + */ + [[nodiscard]] std::string getBinaryName() const; + /** + * Mod Version. Should follow semantic versioning. + */ + [[nodiscard]] VersionInfo getVersion() const; + /** + * Human-readable ID of the Mod. + * Recommended to be in the format + * "developer.mod". Not + * guaranteed to be either case- + * nor space-sensitive. Should + * be restricted to the ASCII + * character set. + */ + [[nodiscard]] std::string getID() const; + /** + * Name of the mod. May contain + * spaces & punctuation, but should + * be restricted to the ASCII + * character set. + */ + [[nodiscard]] std::string getName() const; + /** + * The name of the head developer. + * Should be a single name, like + * "HJfod" or "Geode Team". + * If the mod has multiple + * developers, this field should + * be one of their name or a team + * name, and the rest of the credits + * should be named in `m_credits` + * instead. + */ + [[nodiscard]] std::string getDeveloper() const; + /** + * Short & concise description of the + * mod. + */ + [[nodiscard]] std::optional getDescription() const; + /** + * Detailed description of the mod, written in Markdown (see + * ) for more info + */ + [[nodiscard]] std::optional getDetails() const; + /** + * Changelog for the mod, written in Markdown (see + * ) for more info + */ + [[nodiscard]] std::optional 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) + */ + [[nodiscard]] std::optional getSupportInfo() const; + /** + * Git Repository of the mod + */ + [[nodiscard]] std::optional getRepository() const; + /** + * Info about where users should report issues and request help + */ + [[nodiscard]] std::optional getIssues() const; + /** + * Dependencies + */ + [[nodiscard]] std::vector getDependencies() const; + /** + * Incompatibilities + */ + [[nodiscard]] std::vector getIncompatibilities() const; + /** + * Mod spritesheet names + */ + [[nodiscard]] std::vector getSpritesheets() const; + /** + * Mod settings + * @note Not a map because insertion order must be preserved + */ + [[nodiscard]] std::vector> getSettings() const; + /** + * Whether this mod has to be loaded before the loading screen or not + */ + [[nodiscard]] bool needsEarlyLoad() const; + /** + * Whether this mod is an API or not + */ + [[nodiscard]] bool isAPI() const; + +#if defined(GEODE_EXPOSE_SECRET_INTERNALS_IN_HEADERS_DO_NOT_DEFINE_PLEASE) + void setPath(ghc::filesystem::path const& value); + void setBinaryName(std::string const& value); + void setVersion(VersionInfo const& value); + void setID(std::string const& value); + void setName(std::string const& value); + void setDeveloper(std::string const& value); + void setDescription(std::optional const& value); + void setDetails(std::optional const& value); + void setChangelog(std::optional const& value); + void setSupportInfo(std::optional const& value); + void setRepository(std::optional const& value); + void setIssues(std::optional const& value); + void setDependencies(std::vector const& value); + void setIncompatibilities(std::vector const& value); + void setSpritesheets(std::vector const& value); + void setSettings(std::vector> const& value); + void setNeedsEarlyLoad(bool const& value); + void setIsAPI(bool const& value); +#endif + + /** + * Create ModInfo from an unzipped .geode package + */ + static Result createFromGeodeZip(utils::file::Unzip& zip); + /** + * Create ModInfo from a .geode package + */ + static Result createFromGeodeFile(ghc::filesystem::path const& path); + /** + * Create ModInfo from a mod.json file + */ + static Result createFromFile(ghc::filesystem::path const& path); + /** + * Create ModInfo from a parsed json document + */ + static Result create(ModJson const& json); + + /** + * Convert to JSON. Essentially same as getRawJSON except dynamically + * adds runtime fields like path + */ + [[nodiscard]] ModJson toJSON() const; + /** + * Get the raw JSON file + */ + [[nodiscard]] ModJson getRawJSON() const; + + bool operator==(ModMetadata const& other) const; + + static bool validateID(std::string const& id); + + [[deprecated]] operator ModInfo(); + [[deprecated]] operator ModInfo() const; + + private: + /** + * Version is passed for backwards + * compatibility if we update the mod.json + * format + */ + static Result createFromSchemaV010(ModJson const& json); + + Result<> addSpecialFiles(ghc::filesystem::path const& dir); + Result<> addSpecialFiles(utils::file::Unzip& zip); + + std::vector*>> getSpecialFiles(); + + friend class Loader; + + friend class ModMetadataImpl; + }; +} + +template <> +struct json::Serialize { + static json::Value to_json(geode::ModMetadata const& info) { + return info.toJSON(); + } +}; diff --git a/loader/include/Geode/loader/Setting.hpp b/loader/include/Geode/loader/Setting.hpp index 3f2ff368c..fca764c9a 100644 --- a/loader/include/Geode/loader/Setting.hpp +++ b/loader/include/Geode/loader/Setting.hpp @@ -92,7 +92,7 @@ namespace geode { std::optional description; ValueType defaultValue; /** - * A regex the string must succesfully match against + * A regex the string must successfully match against */ std::optional match; diff --git a/loader/include/Geode/ui/Popup.hpp b/loader/include/Geode/ui/Popup.hpp index b0e4544ab..6a0eb5866 100644 --- a/loader/include/Geode/ui/Popup.hpp +++ b/loader/include/Geode/ui/Popup.hpp @@ -96,4 +96,14 @@ namespace geode { char const* title, std::string const& content, char const* btn1, char const* btn2, float width, utils::MiniFunction selected, bool doShow = true ); + + GEODE_DLL FLAlertLayer* createQuickPopup( + char const* title, std::string const& content, char const* btn1, char const* btn2, + utils::MiniFunction selected, bool doShow, bool cancelledByEscape + ); + + GEODE_DLL FLAlertLayer* createQuickPopup( + char const* title, std::string const& content, char const* btn1, char const* btn2, + float width, utils::MiniFunction selected, bool doShow, bool cancelledByEscape + ); } diff --git a/loader/include/Geode/utils/VersionInfo.hpp b/loader/include/Geode/utils/VersionInfo.hpp index 4ceea8d83..da690a0f0 100644 --- a/loader/include/Geode/utils/VersionInfo.hpp +++ b/loader/include/Geode/utils/VersionInfo.hpp @@ -13,6 +13,7 @@ namespace geode { MoreEq, Less, More, + Any }; /** @@ -185,7 +186,7 @@ namespace geode { protected: VersionInfo m_version; VersionCompare m_compare = VersionCompare::Exact; - + public: constexpr ComparableVersionInfo() = default; constexpr ComparableVersionInfo( @@ -196,12 +197,16 @@ namespace geode { static Result parse(std::string const& string); constexpr bool compare(VersionInfo const& version) const { + if (m_compare == VersionCompare::Any) { + return true; + } + // opposing major versions never match if (m_version.getMajor() != version.getMajor()) { return false; } - // the comparison works invertedly as a version like "v1.2.0" + // the comparison works invertedly as a version like "v1.2.0" // should return true for "<=v1.3.0" switch (m_compare) { case VersionCompare::LessEq: diff --git a/loader/include/Geode/utils/general.hpp b/loader/include/Geode/utils/general.hpp index ec736efb0..c4f5015f4 100644 --- a/loader/include/Geode/utils/general.hpp +++ b/loader/include/Geode/utils/general.hpp @@ -127,3 +127,7 @@ namespace geode::utils::clipboard { GEODE_DLL bool write(std::string const& data); GEODE_DLL std::string read(); } + +namespace geode::utils::game { + GEODE_DLL void restart(); +} diff --git a/loader/include/Geode/utils/string.hpp b/loader/include/Geode/utils/string.hpp index 4e024e4dd..ed98ce0fe 100644 --- a/loader/include/Geode/utils/string.hpp +++ b/loader/include/Geode/utils/string.hpp @@ -48,6 +48,9 @@ namespace geode::utils::string { GEODE_DLL std::vector split(std::string const& str, std::string const& split); GEODE_DLL std::vector split(std::wstring const& str, std::wstring const& split); + GEODE_DLL std::string join(std::vector const& strs, std::string const& separator); + GEODE_DLL std::wstring join(std::vector const& strs, std::wstring const& separator); + GEODE_DLL std::vector split(std::string const& str); GEODE_DLL std::vector split(std::wstring const& str); diff --git a/loader/launcher/windows/Updater.cpp b/loader/launcher/windows/Updater.cpp index ef5b3cfcf..3a1506ef1 100644 --- a/loader/launcher/windows/Updater.cpp +++ b/loader/launcher/windows/Updater.cpp @@ -107,6 +107,11 @@ int main(int argc, char* argv[]) { if (argc < 2) return 0; + if (!waitForFile(workingDir / argv[1])) { + showError("There was an error restarting GD. Please, restart the game manually."); + return 0; + } + // restart gd using the provided path ShellExecuteA(NULL, "open", (workingDir / argv[1]).string().c_str(), "", workingDir.string().c_str(), TRUE); return 0; diff --git a/loader/src/hooks/LoadingLayer.cpp b/loader/src/hooks/LoadingLayer.cpp index 081167ace..333b7c183 100644 --- a/loader/src/hooks/LoadingLayer.cpp +++ b/loader/src/hooks/LoadingLayer.cpp @@ -8,46 +8,48 @@ using namespace geode::prelude; struct CustomLoadingLayer : Modify { + CCLabelBMFont* m_loadedModsLabel; bool m_updatingResources; - CustomLoadingLayer() : m_updatingResources(false) {} + CustomLoadingLayer() : m_loadedModsLabel(nullptr), m_updatingResources(false) {} - bool init(bool fromReload) { - if (!fromReload) { - Loader::get()->waitForModsToBeLoaded(); - } + void updateLoadedModsLabel() { + auto allMods = Loader::get()->getAllMods(); + auto count = std::count_if(allMods.begin(), allMods.end(), [&](auto& item) { + return item->isLoaded(); + }); + auto str = fmt::format("Geode: Loaded {}/{} mods", count, allMods.size()); + m_fields->m_loadedModsLabel->setCString(str.c_str()); + } + bool init(bool fromReload) { CCFileUtils::get()->updatePaths(); if (!LoadingLayer::init(fromReload)) return false; - - if (!fromReload) { - auto winSize = CCDirector::sharedDirector()->getWinSize(); - - auto count = Loader::get()->getAllMods().size(); - - auto label = CCLabelBMFont::create( - fmt::format("Geode: Loaded {} mods", count).c_str(), - "goldFont.fnt" - ); - label->setPosition(winSize.width / 2, 30.f); - label->setScale(.45f); - label->setID("geode-loaded-info"); - this->addChild(label); - - // fields have unpredictable destructors - this->addChild(EventListenerNode::create( - this, &CustomLoadingLayer::updateResourcesProgress - )); - - // verify loader resources - if (!LoaderImpl::get()->verifyLoaderResources()) { - m_fields->m_updatingResources = true; - this->setUpdateText("Downloading Resources"); - } - else { - LoaderImpl::get()->updateSpecialFiles(); - } + + if (fromReload) return true; + + auto winSize = CCDirector::sharedDirector()->getWinSize(); + + m_fields->m_loadedModsLabel = CCLabelBMFont::create("Geode: Loaded 0/0 mods", "goldFont.fnt"); + m_fields->m_loadedModsLabel->setPosition(winSize.width / 2, 30.f); + m_fields->m_loadedModsLabel->setScale(.45f); + m_fields->m_loadedModsLabel->setID("geode-loaded-info"); + this->addChild(m_fields->m_loadedModsLabel); + this->updateLoadedModsLabel(); + + // fields have unpredictable destructors + this->addChild(EventListenerNode::create( + this, &CustomLoadingLayer::updateResourcesProgress + )); + + // verify loader resources + if (!LoaderImpl::get()->verifyLoaderResources()) { + m_fields->m_updatingResources = true; + this->setUpdateText("Downloading Resources"); + } + else { + LoaderImpl::get()->updateSpecialFiles(); } return true; @@ -87,6 +89,13 @@ struct CustomLoadingLayer : Modify { } void loadAssets() { + if (Loader::get()->getLoadingState() != Loader::LoadingState::Done) { + this->updateLoadedModsLabel(); + Loader::get()->queueInGDThread([this]() { + this->loadAssets(); + }); + return; + } if (m_fields->m_updatingResources) { return; } diff --git a/loader/src/hooks/MenuLayer.cpp b/loader/src/hooks/MenuLayer.cpp index eb89a19d7..4942a0ec0 100644 --- a/loader/src/hooks/MenuLayer.cpp +++ b/loader/src/hooks/MenuLayer.cpp @@ -23,34 +23,34 @@ class CustomMenuLayer; static Ref INDEX_UPDATE_NOTIF = nullptr; $execute { - new EventListener(+[](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(+[](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 { - 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 { 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(this->getChildByID("bottom-menu")); - auto btn = CCMenuItemSpriteExtra::create( - m_fields->m_geodeButton, this, - static_cast(geodeBtnSelector) - ); - btn->setID("geode-button"_spr); - bottomMenu->addChild(btn); + auto btn = CCMenuItemSpriteExtra::create( + m_fields->m_geodeButton, this, + static_cast(geodeBtnSelector) + ); + btn->setID("geode-button"_spr); + bottomMenu->addChild(btn); bottomMenu->updateLayout(); @@ -96,54 +96,57 @@ struct CustomMenuLayer : Modify { 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 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 load DLLs with Geode. " + "Please note that Geode is incompatible with ALL DLLs, " + "as they can cause Geode mods to error, or even " + "crash.\n\n" + "Remove the DLLs / other mod loaders you have, or proceed at " + "your own risk.", + "OK" + ); + popup->m_scene = this; + popup->m_noElasticity = true; + popup->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 load DLLs with Geode. " - "Please note that Geode is incompatible with ALL DLLs, " - "as they can cause Geode mods to error, or even " - "crash.\n\n" - "Remove the DLLs / other mod loaders you have, or proceed at " - "your own risk.", - "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 update for Geode has been installed! " - "Please restart the game 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 update for Geode has been installed! " + "Please restart the game 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("disable-last-crashed-popup") - ) { + Loader::get()->didLastLaunchCrash() && + !shownLastCrash && + !Mod::get()->template getSettingValue("disable-last-crashed-popup") + ) { shownLastCrash = true; auto popup = createQuickPopup( "Crashed", @@ -163,94 +166,94 @@ struct CustomMenuLayer : Modify { popup->show(); } - // update mods index - if (!INDEX_UPDATE_NOTIF && !Index::get()->hasTriedToUpdate()) { - this->addChild(EventListenerNode::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; - } - - void onIndexUpdate(IndexUpdateEvent* event) { - if ( - std::holds_alternative(event->status) || - std::holds_alternative(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 onMissingTextures(CCObject*) { - - #ifdef GEODE_IS_DESKTOP - - (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 resources.zip from the latest release on GitHub, " - "and unzip its contents into geode/update/resources.\n" - "Afterwards, restart the game.\n" - "You may also continue without installing resources, but be aware that " - "you won't be able to open the Geode menu.", - "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 resources.zip, " - "and unzip its contents into the destination " - "folder.\n" - "Don't add any new folders to the destination!", - "OK" - )->show(); - } - } - ); - - #else - - // 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" - "**Report this bug to the Geode developers**. It is very likely " - "that your game will crash until the issue is resolved.", - "OK" - )->show(); - - #endif - } + // update mods index + if (!INDEX_UPDATE_NOTIF && !Index::get()->hasTriedToUpdate()) { + this->addChild(EventListenerNode::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; + } + + void onIndexUpdate(IndexUpdateEvent* event) { + if ( + std::holds_alternative(event->status) || + std::holds_alternative(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 onMissingTextures(CCObject*) { + + #ifdef GEODE_IS_DESKTOP + + (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 resources.zip from the latest release on GitHub, " + "and unzip its contents into geode/update/resources.\n" + "Afterwards, restart the game.\n" + "You may also continue without installing resources, but be aware that " + "you won't be able to open the Geode menu.", + "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 resources.zip, " + "and unzip its contents into the destination " + "folder.\n" + "Don't add any new folders to the destination!", + "OK" + )->show(); + } + } + ); + + #else + + // 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" + "**Report this bug to the Geode developers**. It is very likely " + "that your game will crash until the issue is resolved.", + "OK" + )->show(); + + #endif + } void onGeode(CCObject*) { ModListLayer::scene(); diff --git a/loader/src/internal/crashlog.cpp b/loader/src/internal/crashlog.cpp index e5b3af573..5995dfb3f 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/internal/crashlog.hpp b/loader/src/internal/crashlog.hpp index 4d0e5cc84..4ed85293a 100644 --- a/loader/src/internal/crashlog.hpp +++ b/loader/src/internal/crashlog.hpp @@ -10,7 +10,7 @@ namespace crashlog { /** * Setup platform-specific crashlog handler - * @returns True if the handler was succesfully installed, false otherwise + * @returns True if the handler was successfully installed, false otherwise */ bool GEODE_DLL setupPlatformHandler(); /** diff --git a/loader/src/load.cpp b/loader/src/load.cpp index 29dff3fc7..85e05d422 100644 --- a/loader/src/load.cpp +++ b/loader/src/load.cpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include #include @@ -30,7 +29,7 @@ using namespace geode::prelude; }); listenForIPC("loader-info", [](IPCEvent* event) -> json::Value { - return Loader::get()->getModImpl()->getModInfo(); + return Mod::get()->getMetadata(); }); listenForIPC("list-mods", [](IPCEvent* event) -> json::Value { @@ -45,13 +44,13 @@ using namespace geode::prelude; if (!dontIncludeLoader) { res.push_back( - includeRunTimeInfo ? Loader::get()->getModImpl()->getRuntimeInfo() : - Loader::get()->getModImpl()->getModInfo().toJSON() + includeRunTimeInfo ? Mod::get()->getRuntimeInfo() : + Mod::get()->getMetadata().toJSON() ); } for (auto& mod : Loader::get()->getAllMods()) { - res.push_back(includeRunTimeInfo ? mod->getRuntimeInfo() : mod->getModInfo().toJSON()); + res.push_back(includeRunTimeInfo ? mod->getRuntimeInfo() : mod->getMetadata().toJSON()); } return res; @@ -67,7 +66,7 @@ int geodeEntry(void* platformData) { "There was a fatal error setting up " "the internal mod and Geode can not be loaded: " + internalSetupRes.unwrapErr() ); - LoaderImpl::get()->reset(); + LoaderImpl::get()->forceReset(); return 1; } @@ -85,7 +84,7 @@ int geodeEntry(void* platformData) { "the loader and Geode can not be loaded. " "(" + setupRes.unwrapErr() + ")" ); - LoaderImpl::get()->reset(); + LoaderImpl::get()->forceReset(); return 1; } diff --git a/loader/src/loader/Index.cpp b/loader/src/loader/Index.cpp index c3e7c394b..0ccfc6c96 100644 --- a/loader/src/loader/Index.cpp +++ b/loader/src/loader/Index.cpp @@ -48,7 +48,7 @@ IndexUpdateFilter::IndexUpdateFilter() {} class IndexItem::Impl final { private: ghc::filesystem::path m_path; - ModInfo m_info; + ModMetadata m_metadata; std::string m_downloadURL; std::string m_downloadHash; std::unordered_set m_platforms; @@ -64,6 +64,8 @@ class IndexItem::Impl final { static Result> create( ghc::filesystem::path const& dir ); + + bool isInstalled() const; }; IndexItem::IndexItem() : m_impl(std::make_unique()) {} @@ -74,7 +76,11 @@ ghc::filesystem::path IndexItem::getPath() const { } ModInfo IndexItem::getModInfo() const { - return m_impl->m_info; + return this->getMetadata(); +} + +ModMetadata IndexItem::getMetadata() const { + return m_impl->m_metadata; } std::string IndexItem::getDownloadURL() const { @@ -97,13 +103,43 @@ std::unordered_set IndexItem::getTags() const { return m_impl->m_tags; } +bool IndexItem::isInstalled() const { + return m_impl->isInstalled(); +} + +#if defined(GEODE_EXPOSE_SECRET_INTERNALS_IN_HEADERS_DO_NOT_DEFINE_PLEASE) +void IndexItem::setMetadata(ModMetadata const& value) { + m_impl->m_metadata = value; +} + +void IndexItem::setDownloadURL(std::string const& value) { + m_impl->m_downloadURL = value; +} + +void IndexItem::setPackageHash(std::string const& value) { + m_impl->m_downloadHash = value; +} + +void IndexItem::setAvailablePlatforms(std::unordered_set const& value) { + m_impl->m_platforms = value; +} + +void IndexItem::setIsFeatured(bool const& value) { + m_impl->m_isFeatured = value; +} + +void IndexItem::setTags(std::unordered_set const& value) { + m_impl->m_tags = value; +} +#endif + Result IndexItem::Impl::create(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") + auto metadata, ModMetadata::createFromFile(dir / "mod.json") .expect("Unable to read mod.json: {error}") ); @@ -112,17 +148,22 @@ Result IndexItem::Impl::create(ghc::filesystem::path const& dir std::unordered_set platforms; for (auto& plat : root.has("platforms").iterate()) { - platforms.insert(PlatformID::from(plat.template get())); + platforms.insert(PlatformID::from(plat.get())); + } + + std::unordered_set tags; + for (auto& tag : root.has("tags").iterate()) { + tags.insert(tag.get()); } auto item = std::make_shared(); item->m_impl->m_path = dir; - item->m_impl->m_info = info; - item->m_impl->m_downloadURL = root.has("mod").obj().has("download").template get(); - item->m_impl->m_downloadHash = root.has("mod").obj().has("hash").template get(); + item->m_impl->m_metadata = metadata; item->m_impl->m_platforms = platforms; - item->m_impl->m_isFeatured = root.has("featured").template get(); - item->m_impl->m_tags = root.has("tags").template get>(); + item->m_impl->m_tags = tags; + root.has("mod").obj().has("download").into(item->m_impl->m_downloadURL); + root.has("mod").obj().has("hash").into(item->m_impl->m_downloadHash); + root.has("featured").into(item->m_impl->m_isFeatured); if (checker.isError()) { return Err(checker.getError()); @@ -130,6 +171,10 @@ Result 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) { @@ -333,22 +378,23 @@ void Index::Impl::updateFromLocalTree() { continue; } auto add = addRes.unwrap(); - auto info = add->getModInfo(); + auto metadata = add->getMetadata(); // check if this major version of this item has already been added - if (m_items[info.id()].count(info.version().getMajor())) { + if (m_items[metadata.getID()].count(metadata.getVersion().getMajor())) { log::warn( "Item {}@{} has already been added, skipping", - info.id(), info.version() + metadata.getID(), + metadata.getVersion() ); continue; } // add new major version of this item - m_items[info.id()].insert({ - info.version().getMajor(), + m_items[metadata.getID()].insert({metadata.getVersion().getMajor(), add }); } } catch(std::exception& e) { + log::error("Unable to read local index tree: {}", e.what()); IndexUpdateEvent("Unable to read local index tree").post(); return; } @@ -408,7 +454,7 @@ std::vector Index::getItemsByDeveloper( std::vector res; for (auto& items : map::values(m_impl->m_items)) { for (auto& item : items) { - if (item.second->getModInfo().developer() == name) { + if (item.second->getMetadata().getDeveloper() == name) { res.push_back(item.second); } } @@ -441,12 +487,12 @@ IndexItemHandle Index::getItem( if (version) { // prefer most major version for (auto& [_, item] : ranges::reverse(m_impl->m_items.at(id))) { - if (version.value() == item->getModInfo().version()) { + if (version.value() == item->getMetadata().getVersion()) { return item; } } } else { - if (versions.size()) { + if (!versions.empty()) { return m_impl->m_items.at(id).rbegin()->second; } } @@ -461,7 +507,7 @@ IndexItemHandle Index::getItem( if (m_impl->m_items.count(id)) { // prefer most major version for (auto& [_, item] : ranges::reverse(m_impl->m_items.at(id))) { - if (version.compare(item->getModInfo().version())) { + if (version.compare(item->getMetadata().getVersion())) { return item; } } @@ -473,22 +519,26 @@ IndexItemHandle Index::getItem(ModInfo const& info) const { return this->getItem(info.id(), info.version()); } +IndexItemHandle Index::getItem(ModMetadata const& metadata) const { + return this->getItem(metadata.getID(), metadata.getVersion()); +} + IndexItemHandle Index::getItem(Mod* mod) const { return this->getItem(mod->getID(), mod->getVersion()); } bool Index::isUpdateAvailable(IndexItemHandle item) const { - auto installed = Loader::get()->getInstalledMod(item->getModInfo().id()); + auto installed = Loader::get()->getInstalledMod(item->getMetadata().getID()); if (!installed) { return false; } - return item->getModInfo().version() > installed->getVersion(); + return item->getMetadata().getVersion() > installed->getVersion(); } bool Index::areUpdatesAvailable() const { for (auto& mod : Loader::get()->getAllMods()) { auto item = this->getMajorItem(mod->getID()); - if (item && item->getModInfo().version() > mod->getVersion()) { + if (item && item->getMetadata().getVersion() > mod->getVersion()) { return true; } } @@ -497,41 +547,84 @@ bool Index::areUpdatesAvailable() const { // Item installation +Result<> Index::canInstall(IndexItemHandle item) const { + if (!item->getAvailablePlatforms().count(GEODE_PLATFORM_TARGET)) { + return Err("Mod is not available on {}", GEODE_PLATFORM_NAME); + } + + for (auto& dep : item->getMetadata().getDependencies()) { + // if the dep is resolved, then all its dependencies must be installed + // already in order for that to have happened + if (dep.isResolved()) continue; + + if (dep.importance != ModMetadata::Dependency::Importance::Required) continue; + + // check if this dep is available in the index + if (auto depItem = this->getItem(dep.id, dep.version)) { + if (!depItem->getAvailablePlatforms().count(GEODE_PLATFORM_TARGET)) { + return Err( + "Dependency {} is not available on {}", + dep.id, GEODE_PLATFORM_NAME + ); + } + // recursively add dependencies + GEODE_UNWRAP_INTO(auto deps, this->canInstall(depItem)); + } + // otherwise user must get this dependency manually from somewhere + else { + return Err( + "Dependency {} version {} not found in the index! Likely " + "reason is that the version of the dependency this mod " + "depends on is not available. Please let the developer " + "of the mod ({}) know!", + dep.id, dep.version.toString(), item->getMetadata().getDeveloper() + ); + } + } + + return Ok(); +} + Result Index::getInstallList(IndexItemHandle item) const { if (!item->getAvailablePlatforms().count(GEODE_PLATFORM_TARGET)) { return Err("Mod is not available on {}", GEODE_PLATFORM_NAME); } - + IndexInstallList list; list.target = item; - for (auto& dep : item->getModInfo().dependencies()) { - if (!dep.isResolved()) { - // check if this dep is available in the index - if (auto depItem = this->getItem(dep.id, dep.version)) { - if (!depItem->getAvailablePlatforms().count(GEODE_PLATFORM_TARGET)) { - return Err( - "Dependency {} is not available on {}", - dep.id, GEODE_PLATFORM_NAME - ); - } - // recursively add dependencies - GEODE_UNWRAP_INTO(auto deps, this->getInstallList(depItem)); - ranges::push(list.list, deps.list); - } - // otherwise user must get this dependency manually from somewhere - // else - else { + for (auto& dep : item->getMetadata().getDependencies()) { + // if the dep is resolved, then all its dependencies must be installed + // already in order for that to have happened + if (dep.isResolved()) continue; + + if (dep.importance == ModMetadata::Dependency::Importance::Suggested) continue; + + // check if this dep is available in the index + if (auto depItem = this->getItem(dep.id, dep.version)) { + if (!depItem->getAvailablePlatforms().count(GEODE_PLATFORM_TARGET)) { + // it's fine to not install optional dependencies + if (dep.importance != ModMetadata::Dependency::Importance::Required) continue; return Err( - "Dependency {} version {} not found in the index! Likely " - "reason is that the version of the dependency this mod " - "depends on is not available. Please let the the developer " - "({}) of the mod know!", - dep.id, dep.version.toString(), item->getModInfo().developer() + "Dependency {} is not available on {}", + dep.id, GEODE_PLATFORM_NAME ); } + // recursively add dependencies + GEODE_UNWRAP_INTO(auto deps, this->getInstallList(depItem)); + ranges::push(list.list, deps.list); + } + // otherwise user must get this dependency manually from somewhere + else { + // it's fine to not install optional dependencies + if (dep.importance != ModMetadata::Dependency::Importance::Required) continue; + return Err( + "Dependency {} version {} not found in the index! Likely " + "reason is that the version of the dependency this mod " + "depends on is not available. Please let the developer " + "of the mod ({}) know!", + dep.id, dep.version.toString(), item->getMetadata().getDeveloper() + ); } - // if the dep is resolved, then all its dependencies must be installed - // already in order for that to have happened } // add this item to the end of the list list.list.push_back(item); @@ -541,7 +634,7 @@ Result Index::getInstallList(IndexItemHandle item) const { void Index::Impl::installNext(size_t index, IndexInstallList const& list) { auto postError = [this, list](std::string const& error) { m_runningInstallations.erase(list.target); - ModInstallEvent(list.target->getModInfo().id(), error).post(); + ModInstallEvent(list.target->getMetadata().getID(), error).post(); }; // If we're at the end of the list, move the downloaded items to mods @@ -550,12 +643,12 @@ void Index::Impl::installNext(size_t index, IndexInstallList const& list) { // Move all downloaded files for (auto& item : list.list) { // If the mod is already installed, delete the old .geode file - if (auto mod = Loader::get()->getInstalledMod(item->getModInfo().id())) { + if (auto mod = Loader::get()->getInstalledMod(item->getMetadata().getID())) { auto res = mod->uninstall(); if (!res) { return postError(fmt::format( "Unable to uninstall old version of {}: {}", - item->getModInfo().id(), res.unwrapErr() + item->getMetadata().getID(), res.unwrapErr() )); } } @@ -563,21 +656,22 @@ void Index::Impl::installNext(size_t index, IndexInstallList const& list) { // Move the temp file try { ghc::filesystem::rename( - dirs::getTempDir() / (item->getModInfo().id() + ".index"), - dirs::getModsDir() / (item->getModInfo().id() + ".geode") + dirs::getTempDir() / (item->getMetadata().getID() + ".index"), + dirs::getModsDir() / (item->getMetadata().getID() + ".geode") ); } catch(std::exception& e) { return postError(fmt::format( "Unable to install {}: {}", - item->getModInfo().id(), e.what() + item->getMetadata().getID(), e.what() )); } } - - // load mods - Loader::get()->refreshModsList(); - ModInstallEvent(list.target->getModInfo().id(), UpdateFinished()).post(); + auto const& eventModID = list.target->getMetadata().getID(); + Loader::get()->queueInGDThread([eventModID]() { + ModInstallEvent(eventModID, UpdateFinished()).post(); + }); + return; } @@ -588,9 +682,9 @@ void Index::Impl::installNext(size_t index, IndexInstallList const& list) { }; auto item = list.list.at(index); - auto tempFile = dirs::getTempDir() / (item->getModInfo().id() + ".index"); + auto tempFile = dirs::getTempDir() / (item->getMetadata().getID() + ".index"); m_runningInstallations[list.target] = web::AsyncWebRequest() - .join("install_item_" + item->getModInfo().id()) + .join("install_item_" + item->getMetadata().getID()) .fetch(item->getDownloadURL()) .into(tempFile) .then([=](auto) { @@ -600,25 +694,25 @@ void Index::Impl::installNext(size_t index, IndexInstallList const& list) { return postError(fmt::format( "Binary file download for {} returned \"404 Not found\". " "Report this to the Geode development team.", - item->getModInfo().id() + item->getMetadata().getID() )); } // Verify checksum ModInstallEvent( - list.target->getModInfo().id(), + list.target->getMetadata().getID(), UpdateProgress( scaledProgress(100), - fmt::format("Verifying {}", item->getModInfo().id()) + fmt::format("Verifying {}", item->getMetadata().getID()) ) ).post(); - + if (::calculateHash(tempFile) != item->getPackageHash()) { return postError(fmt::format( "Checksum mismatch with {}! (Downloaded file did not match what " "was expected. Try again, and if the download fails another time, " "report this to the Geode development team.)", - item->getModInfo().id() + item->getMetadata().getID() )); } @@ -628,15 +722,15 @@ void Index::Impl::installNext(size_t index, IndexInstallList const& list) { .expect([postError, list, item](std::string const& err) { postError(fmt::format( "Unable to download {}: {}", - item->getModInfo().id(), err + item->getMetadata().getID(), err )); }) .progress([this, item, list, scaledProgress](auto&, double now, double total) { ModInstallEvent( - list.target->getModInfo().id(), + list.target->getMetadata().getID(), UpdateProgress( scaledProgress(now / total * 100.0), - fmt::format("Downloading {}", item->getModInfo().id()) + fmt::format("Downloading {}", item->getMetadata().getID()) ) ).post(); }) @@ -671,7 +765,7 @@ void Index::install(IndexItemHandle item) { this->install(list.unwrap()); } else { ModInstallEvent( - item->getModInfo().id(), + item->getMetadata().getID(), UpdateFailed(list.unwrapErr()) ).post(); } diff --git a/loader/src/loader/Loader.cpp b/loader/src/loader/Loader.cpp index a539a70a6..004e96957 100644 --- a/loader/src/loader/Loader.cpp +++ b/loader/src/loader/Loader.cpp @@ -63,6 +63,10 @@ void Loader::refreshModsList() { return m_impl->refreshModsList(); } +Loader::LoadingState Loader::getLoadingState() { + return m_impl->m_loadingState; +} + bool Loader::isModInstalled(std::string const& id) const { return m_impl->isModInstalled(id); } @@ -84,7 +88,7 @@ std::vector Loader::getAllMods() { } Mod* Loader::getModImpl() { - return m_impl->getModImpl(); + return Mod::get(); } void Loader::updateAllDependencies() { @@ -95,6 +99,10 @@ std::vector Loader::getFailedMods() const { return m_impl->getFailedMods(); } +std::vector 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 44fa2f6cc..4927e1c40 100644 --- a/loader/src/loader/LoaderImpl.cpp +++ b/loader/src/loader/LoaderImpl.cpp @@ -1,29 +1,27 @@ - #include "LoaderImpl.hpp" #include + +#include "ModImpl.hpp" +#include "ModMetadataImpl.hpp" + #include #include #include #include #include -#include +#include #include #include #include #include #include -#include -#include "ModImpl.hpp" -#include "ModInfoImpl.hpp" #include #include #include #include #include #include -#include #include -#include #include using namespace geode::prelude; @@ -32,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 @@ -88,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; @@ -128,12 +121,19 @@ std::vector Loader::Impl::getAllMods() { return map::values(m_mods); } -Mod* Loader::Impl::getModImpl() { - return Mod::get(); -} - std::vector Loader::Impl::getFailedMods() const { - return m_invalidMods; + std::vector inv; + for (auto const& item : this->getProblems()) { + if (item.type != LoadProblem::Type::InvalidFile) + continue; + if (!holds_alternative(item.cause)) + continue; + inv.push_back({ + std::get(item.cause), + item.message + }); + } + return inv; } // Version info @@ -166,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()); @@ -174,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) { @@ -194,60 +190,6 @@ Result<> Loader::Impl::loadData() { // Mod loading -Result 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( - "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 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(); } @@ -260,13 +202,13 @@ Mod* Loader::Impl::getInstalledMod(std::string const& id) const { } bool Loader::Impl::isModLoaded(std::string const& id) const { - return m_mods.count(id) && m_mods.at(id)->isLoaded() && m_mods.at(id)->isEnabled(); + return m_mods.count(id) && m_mods.at(id)->isLoaded(); } Mod* Loader::Impl::getLoadedMod(std::string const& id) const { if (m_mods.count(id)) { auto mod = m_mods.at(id); - if (mod->isLoaded() && mod->isEnabled()) { + if (mod->isLoaded()) { return mod; } } @@ -274,16 +216,15 @@ Mod* Loader::Impl::getLoadedMod(std::string const& id) const { } void Loader::Impl::updateModResources(Mod* mod) { - if (!mod->m_impl->m_info.spritesheets().size()) { + if (mod->getMetadata().getSpritesheets().empty()) return; - } auto searchPath = mod->getResourcesDir(); log::debug("Adding resources for {}", mod->getID()); // add spritesheets - for (auto const& sheet : mod->m_impl->m_info.spritesheets()) { + for (auto const& sheet : mod->getMetadata().getSpritesheets()) { log::debug("Adding sheet {}", sheet); auto png = sheet + ".png"; auto plist = sheet + ".plist"; @@ -292,8 +233,8 @@ void Loader::Impl::updateModResources(Mod* mod) { if (png == std::string(ccfu->fullPathForFilename(png.c_str(), false)) || plist == std::string(ccfu->fullPathForFilename(plist.c_str(), false))) { log::warn( - "The resource dir of \"{}\" is missing \"{}\" png and/or plist files", - mod->m_impl->m_info.id(), sheet + R"(The resource dir of "{}" is missing "{}" png and/or plist files)", + mod->getID(), sheet ); } else { @@ -305,131 +246,403 @@ 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 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 Loader::Impl::loadModFromFile(ghc::filesystem::path const& file) { + return Err("Loader::loadModFromFile is deprecated"); +} + +void Loader::Impl::loadModsFromDirectory(ghc::filesystem::path const& dir, bool recursive) { + log::error("Called deprecated stub: Loader::loadModsFromDirectory"); +} + +void Loader::Impl::refreshModsList() { + log::error("Called deprecated stub: Loader::refreshModsList"); +} + +void Loader::Impl::updateAllDependencies() { + log::error("Called deprecated stub: Loader::updateAllDependencies"); +} + +void Loader::Impl::queueMods(std::vector& 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()); + log::debug("early: {}", modMetadata.needsEarlyLoad() ? "yes" : "no"); + + 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(); + } +} - // skip this entry if its extension is not .geode - if (entry.path().extension() != GEODE_MOD_EXTENSION) { +void Loader::Impl::populateModList(std::vector& modQueue) { + std::vector toRemove; + for (auto& [id, mod] : m_mods) { + if (id == "geode.loader") continue; - } - // skip this entry if it's already loaded - if (map::contains(m_mods, [entry](Mod* p) -> bool { - return p->m_impl->m_info.path() == entry.path(); - })) { + 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; } - // 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(), - }); + 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; } - auto info = res.unwrap(); - // skip this entry if it's already set to be loaded - if (ranges::contains(m_modsToLoad, info)) { + dependency.mod = m_mods[dependency.id]; + + if (!dependency.version.compare(dependency.mod->getVersion())) { + dependency.mod = nullptr; continue; } - // add to list of mods to load - m_modsToLoad.push_back(info); + 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::refreshModsList() { - log::debug("Loading mods..."); +void Loader::Impl::loadModGraph(Mod* node, bool early) { + if (early && !node->needsEarlyLoad()) { + m_modsToLoad.push(node); + return; + } + + if (node->hasUnresolvedDependencies()) + return; + if (node->hasUnresolvedIncompatibilities()) + return; + + log::debug("{} {}", node->getID(), node->getVersion()); + log::pushNest(); - // find mods - for (auto& dir : m_modSearchDirectories) { - this->loadModsFromDirectory(dir); + if (node->isLoaded()) { + for (auto const& dep : node->m_impl->m_dependants) { + this->loadModGraph(dep, early); + } + log::popNest(); + return; } - - // 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(), - }); - } + + 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("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, early); } } - // UI can be loaded now - m_earlyLoadFinished = true; - m_earlyLoadFinishedCV.notify_all(); + log::popNest(); +} - // 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()); +void Loader::Impl::findProblems() { + for (auto const& [id, mod] : m_mods) { + log::debug(id); + log::pushNest(); - m_invalidMods.push_back(InvalidGeodeFile { - .path = mod.path(), - .reason = load.unwrapErr(), - }); + 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; + switch(dep.importance) { + case ModMetadata::Incompatibility::Importance::Conflicting: + m_problems.push_back({ + LoadProblem::Type::Conflict, + mod, + fmt::format("{} {}", dep.id, dep.version.toString()) + }); + log::warn("{} conflicts with {} {}", id, dep.id, dep.version); + break; + case ModMetadata::Incompatibility::Importance::Breaking: + m_problems.push_back({ + LoadProblem::Type::PresentIncompatibility, + mod, + fmt::format("{} {}", dep.id, dep.version.toString()) + }); + log::error("{} breaks {} {}", id, dep.id, dep.version); + break; } } + + 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(item.cause) && + std::get(item.cause).getID() == myEpicMod->getID() || + std::holds_alternative(item.cause) && + std::get(item.cause) == myEpicMod; + })) { + m_problems.push_back({ + LoadProblem::Type::Unknown, + mod, + "" + }); + log::error("{} failed to load for an unknown reason", id); + } + + log::popNest(); } - m_modsToLoad.clear(); } -void Loader::Impl::updateAllDependencies() { - for (auto const& [_, mod] : m_mods) { - (void)mod->updateDependencies(); +void Loader::Impl::refreshModGraph() { + log::info("Refreshing mod graph..."); + log::pushNest(); + + auto begin = std::chrono::high_resolution_clock::now(); + + if (m_mods.size() > 1) { + log::error("Cannot refresh mod graph after startup"); + log::popNest(); + return; } + + m_problems.clear(); + + m_loadingState = LoadingState::Queue; + log::debug("Queueing mods"); + log::pushNest(); + std::vector modQueue; + this->queueMods(modQueue); + log::popNest(); + + m_loadingState = LoadingState::List; + log::debug("Populating mod list"); + log::pushNest(); + this->populateModList(modQueue); + modQueue.clear(); + log::popNest(); + + m_loadingState = LoadingState::Graph; + log::debug("Building mod graph"); + log::pushNest(); + this->buildModGraph(); + log::popNest(); + + m_loadingState = LoadingState::EarlyMods; + log::debug("Loading early mods"); + log::pushNest(); + for (auto const& dep : Mod::get()->m_impl->m_dependants) { + this->loadModGraph(dep, true); + } + log::popNest(); + + auto end = std::chrono::high_resolution_clock::now(); + auto time = std::chrono::duration_cast(end - begin).count(); + log::info("Took {}s. Continuing next frame...", static_cast(time) / 1000.f); + + log::popNest(); + + if (m_modsToLoad.empty()) + m_loadingState = LoadingState::Problems; + else + m_loadingState = LoadingState::Mods; + + queueInGDThread([]() { + Loader::get()->m_impl->continueRefreshModGraph(); + }); +} + +void Loader::Impl::continueRefreshModGraph() { + log::info("Continuing mod graph refresh..."); + log::pushNest(); + + auto begin = std::chrono::high_resolution_clock::now(); + + switch (m_loadingState) { + case LoadingState::Mods: + log::debug("Loading mods"); + log::pushNest(); + this->loadModGraph(m_modsToLoad.front(), false); + log::popNest(); + m_modsToLoad.pop(); + if (m_modsToLoad.empty()) + m_loadingState = LoadingState::Problems; + break; + case LoadingState::Problems: + log::debug("Finding problems"); + log::pushNest(); + this->findProblems(); + log::popNest(); + m_loadingState = LoadingState::Done; + break; + default: + m_loadingState = LoadingState::Done; + log::warn("Impossible loading state, resetting to 'Done'! " + "Was Loader::Impl::continueRefreshModGraph() called from the wrong place?"); + break; + } + + auto end = std::chrono::high_resolution_clock::now(); + auto time = std::chrono::duration_cast(end - begin).count(); + log::info("Took {}s", static_cast(time) / 1000.f); + + if (m_loadingState != LoadingState::Done) { + queueInGDThread([]() { + Loader::get()->m_impl->continueRefreshModGraph(); + }); + } + + log::popNest(); +} + +std::vector 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)); - m_earlyLoadFinishedCV.wait(lock, [this] { - return bool(m_earlyLoadFinished); - }); + log::debug("Waiting for mods to be loaded..."); + // genius + log::warn("waitForModsToBeLoaded() does not wait for mods to be loaded!"); } bool Loader::Impl::didLastLaunchCrash() const { return crashlog::didLastLaunchCrash(); } -void Loader::Impl::reset() { +void Loader::Impl::forceReset() { this->closePlatformConsole(); - for (auto& [_, mod] : m_mods) { delete mod; } @@ -444,7 +657,7 @@ bool Loader::Impl::isReadyToHook() const { } void Loader::Impl::addInternalHook(Hook* hook, Mod* mod) { - m_internalHooks.push_back({hook, mod}); + m_internalHooks.emplace_back(hook, mod); } bool Loader::Impl::loadHooks() { @@ -560,7 +773,7 @@ void Loader::Impl::tryDownloadLoaderResources( void Loader::Impl::updateSpecialFiles() { auto resourcesDir = dirs::getGeodeResourcesDir() / Mod::get()->getID(); - auto res = ModInfoImpl::getImpl(ModImpl::get()->m_info).addSpecialFiles(resourcesDir); + auto res = ModMetadataImpl::getImpl(ModImpl::get()->m_metadata).addSpecialFiles(resourcesDir); if (res.isErr()) { log::warn("Unable to add special files: {}", res.unwrapErr()); } diff --git a/loader/src/loader/LoaderImpl.hpp b/loader/src/loader/LoaderImpl.hpp index 2869ac2ce..0fe1b87d3 100644 --- a/loader/src/loader/LoaderImpl.hpp +++ b/loader/src/loader/LoaderImpl.hpp @@ -1,3 +1,5 @@ +#pragma once + #include "FileWatcher.hpp" #include @@ -19,6 +21,7 @@ #include #include #include +#include #include // TODO: Find a file convention for impl headers @@ -54,9 +57,9 @@ namespace geode { mutable std::mutex m_mutex; std::vector m_modSearchDirectories; - std::vector m_modsToLoad; - std::vector m_invalidMods; + std::vector m_problems; std::unordered_map m_mods; + std::queue m_modsToLoad; std::vector m_texturePaths; bool m_isSetup = false; @@ -65,9 +68,8 @@ namespace geode { std::optional m_latestGithubRelease; bool m_isNewUpdateDownloaded = false; - std::condition_variable m_earlyLoadFinishedCV; - std::mutex m_earlyLoadFinishedMutex; - std::atomic_bool m_earlyLoadFinished = false; + LoadingState m_loadingState; + std::vector> m_gdThreadQueue; mutable std::mutex m_gdThreadMutex; bool m_platformConsoleOpen = false; @@ -113,10 +115,10 @@ namespace geode { friend void GEODE_CALL ::geode_implicit_load(Mod*); - Result loadModFromInfo(ModInfo const& info); + [[deprecated]] Result loadModFromInfo(ModInfo const& info); Result<> setup(); - void reset(); + void forceReset(); Result<> saveData(); Result<> loadData(); @@ -126,17 +128,26 @@ namespace geode { VersionInfo maxModVersion(); bool isModVersionSupported(VersionInfo const& version); - Result loadModFromFile(ghc::filesystem::path const& file); - void loadModsFromDirectory(ghc::filesystem::path const& dir, bool recursive = true); - void refreshModsList(); + [[deprecated]] Result loadModFromFile(ghc::filesystem::path const& file); + [[deprecated]] void loadModsFromDirectory(ghc::filesystem::path const& dir, bool recursive = true); + [[deprecated]] void refreshModsList(); + void queueMods(std::vector& modQueue); + void populateModList(std::vector& modQueue); + void buildModGraph(); + void loadModGraph(Mod* node, bool early); + void findProblems(); + void refreshModGraph(); + void continueRefreshModGraph(); + 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 getAllMods(); - Mod* getModImpl(); - void updateAllDependencies(); - std::vector getFailedMods() const; + [[deprecated]] Mod* getModImpl(); + [[deprecated]] void updateAllDependencies(); + [[deprecated]] std::vector getFailedMods() const; + std::vector getProblems() const; void updateResources(); void updateResources(bool forceReload); @@ -171,7 +182,7 @@ namespace geode { bool userTriedToLoadDLLs() const; }; - class LoaderImpl { + class LoaderImpl : public Loader::Impl { public: static Loader::Impl* get(); }; diff --git a/loader/src/loader/Log.cpp b/loader/src/loader/Log.cpp index 42e735ae2..ec69eed75 100644 --- a/loader/src/loader/Log.cpp +++ b/loader/src/loader/Log.cpp @@ -104,6 +104,9 @@ bool Log::operator==(Log const& l) { } std::string Log::toString(bool logTime) const { + return toString(logTime, 0); +} +std::string Log::toString(bool logTime, uint32_t nestLevel) const { std::string res; if (logTime) { @@ -112,6 +115,10 @@ std::string Log::toString(bool logTime) const { res += fmt::format(" [{}]: ", m_sender ? m_sender->getName() : "Geode?"); + for (uint32_t i = 0; i < nestLevel; i++) { + res += " "; + } + for (auto& i : m_components) { res += i->_toString(); } @@ -205,13 +212,17 @@ std::ofstream& Logger::logStream() { static std::ofstream logStream; return logStream; } +uint32_t& Logger::nestLevel() { + static std::uint32_t nestLevel = 0; + return nestLevel; +} void Logger::setup() { logStream() = std::ofstream(dirs::getGeodeLogDir() / log::generateLogName()); } void Logger::push(Log&& log) { - std::string logStr = log.toString(true); + std::string logStr = log.toString(true, nestLevel()); LoaderImpl::get()->logConsoleMessageWithSeverity(logStr, log.getSeverity()); logStream() << logStr << std::endl; @@ -223,6 +234,17 @@ void Logger::pop(Log* log) { geode::utils::ranges::remove(Logger::logs(), *log); } +void Logger::pushNest() { + if (nestLevel() == std::numeric_limits::max()) + return; + nestLevel()++; +} +void Logger::popNest() { + if (nestLevel() == 0) + return; + nestLevel()--; +} + std::vector Logger::list() { std::vector logs_; logs_.reserve(logs().size()); diff --git a/loader/src/loader/Mod.cpp b/loader/src/loader/Mod.cpp index b443402c9..cee3f4cf2 100644 --- a/loader/src/loader/Mod.cpp +++ b/loader/src/loader/Mod.cpp @@ -4,7 +4,9 @@ using namespace geode::prelude; +#pragma warning(suppress : 4996) Mod::Mod(ModInfo const& info) : m_impl(std::make_unique(this, info)) {} +Mod::Mod(ModMetadata const& metadata) : m_impl(std::make_unique(this, metadata)) {} Mod::~Mod() {} @@ -52,16 +54,35 @@ 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::needsEarlyLoad() const { + return m_impl->needsEarlyLoad(); +} + bool Mod::supportsUnloading() const { - return m_impl->supportsUnloading(); + return false; } bool Mod::wasSuccesfullyLoaded() const { - return m_impl->wasSuccesfullyLoaded(); + return this->wasSuccessfullyLoaded(); +} +bool Mod::wasSuccessfullyLoaded() const { + return m_impl->wasSuccessfullyLoaded(); } ModInfo Mod::getModInfo() const { - return m_impl->getModInfo(); + return this->getMetadata(); +} + +ModMetadata Mod::getMetadata() const { + return m_impl->getMetadata(); } ghc::filesystem::path Mod::getTempDir() const { @@ -76,6 +97,15 @@ ghc::filesystem::path Mod::getResourcesDir() const { return dirs::getModRuntimeDir() / this->getID() / "resources" / this->getID(); } +#if defined(GEODE_EXPOSE_SECRET_INTERNALS_IN_HEADERS_DO_NOT_DEFINE_PLEASE) +void Mod::setMetadata(ModMetadata const& metadata) { + m_impl->setMetadata(metadata); +} +std::vector Mod::getDependants() const { + return m_impl->getDependants(); +} +#endif + Result<> Mod::saveData() { return m_impl->saveData(); } @@ -145,11 +175,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() { @@ -172,14 +202,19 @@ bool Mod::depends(std::string const& id) const { return m_impl->depends(id); } +Result<> Mod::updateDependencies() { + return m_impl->updateDependencies(); +} + bool Mod::hasUnresolvedDependencies() const { return m_impl->hasUnresolvedDependencies(); } -Result<> Mod::updateDependencies() { - return m_impl->updateDependencies(); +bool Mod::hasUnresolvedIncompatibilities() const { + return m_impl->hasUnresolvedIncompatibilities(); } +#pragma warning(suppress : 4996) std::vector Mod::getUnresolvedDependencies() { return m_impl->getUnresolvedDependencies(); } diff --git a/loader/src/loader/ModImpl.cpp b/loader/src/loader/ModImpl.cpp index 35b188588..be0a189b5 100644 --- a/loader/src/loader/ModImpl.cpp +++ b/loader/src/loader/ModImpl.cpp @@ -1,6 +1,6 @@ #include "ModImpl.hpp" #include "LoaderImpl.hpp" -#include "ModInfoImpl.hpp" +#include "ModMetadataImpl.hpp" #include "about.hpp" #include @@ -25,24 +25,22 @@ Mod::Impl* ModImpl::getImpl(Mod* mod) { return mod->m_impl.get(); } -Mod::Impl::Impl(Mod* self, ModInfo const& info) : m_self(self), m_info(info) { +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_info.id(); + m_saveDirPath = dirs::getModsSaveDir() / m_metadata.getID(); (void) utils::file::createDirectoryAll(m_saveDirPath); - + // always create temp dir for all mods, even if disabled, so resources can be loaded GEODE_UNWRAP(this->createTempDir().expect("Unable to create temp dir: {error}")); this->setupSettings(); auto loadRes = this->loadData(); if (!loadRes) { - log::warn("Unable to load data for \"{}\": {}", m_info.id(), loadRes.unwrapErr()); + log::warn("Unable to load data for \"{}\": {}", m_metadata.getID(), loadRes.unwrapErr()); } if (LoaderImpl::get()->m_isSetup) { Loader::get()->updateResources(false); @@ -58,43 +56,52 @@ ghc::filesystem::path Mod::Impl::getSaveDir() const { } std::string Mod::Impl::getID() const { - return m_info.id(); + return m_metadata.getID(); } std::string Mod::Impl::getName() const { - return m_info.name(); + return m_metadata.getName(); } std::string Mod::Impl::getDeveloper() const { - return m_info.developer(); + return m_metadata.getDeveloper(); } std::optional Mod::Impl::getDescription() const { - return m_info.description(); + return m_metadata.getDescription(); } std::optional Mod::Impl::getDetails() const { - return m_info.details(); + return m_metadata.getDetails(); } -ModInfo Mod::Impl::getModInfo() const { - return m_info; +ModMetadata Mod::Impl::getMetadata() const { + return m_metadata; +} + +#if defined(GEODE_EXPOSE_SECRET_INTERNALS_IN_HEADERS_DO_NOT_DEFINE_PLEASE) +void Mod::Impl::setMetadata(ModMetadata const& metadata) { + m_metadata = metadata; +} +std::vector Mod::Impl::getDependants() const { + return m_dependants; } +#endif ghc::filesystem::path Mod::Impl::getTempDir() const { return m_tempDirName; } ghc::filesystem::path Mod::Impl::getBinaryPath() const { - return m_tempDirName / m_info.binaryName(); + return m_tempDirName / m_metadata.getBinaryName(); } ghc::filesystem::path Mod::Impl::getPackagePath() const { - return m_info.path(); + return m_metadata.getPath(); } VersionInfo Mod::Impl::getVersion() const { - return m_info.version(); + return m_metadata.getVersion(); } json::Value& Mod::Impl::getSaveContainer() { @@ -110,14 +117,34 @@ 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::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::supportsUnloading() const { - return m_info.supportsUnloading(); +bool Mod::Impl::needsEarlyLoad() const { + auto deps = m_dependants; + return getMetadata().needsEarlyLoad() || + !deps.empty() && std::any_of(deps.begin(), deps.end(), [&](auto& item) { + return item->needsEarlyLoad(); + }); } -bool Mod::Impl::wasSuccesfullyLoaded() const { +bool Mod::Impl::wasSuccessfullyLoaded() const { return !this->isEnabled() || this->isLoaded(); } @@ -155,7 +182,7 @@ Result<> Mod::Impl::loadData() { Severity::Error, m_self, "{}: Unable to load value for setting \"{}\"", - m_info.id(), + m_metadata.getID(), key ); } @@ -244,7 +271,7 @@ Result<> Mod::Impl::saveData() { } void Mod::Impl::setupSettings() { - for (auto& [key, sett] : m_info.settings()) { + for (auto& [key, sett] : m_metadata.getSettings()) { if (auto value = sett.createDefaultValue()) { m_settings.emplace(key, std::move(value)); } @@ -262,19 +289,19 @@ void Mod::Impl::registerCustomSetting(std::string const& key, std::unique_ptr Mod::Impl::getSettingKeys() const { std::vector keys; - for (auto& [key, _] : m_info.settings()) { + for (auto& [key, _] : m_metadata.getSettings()) { keys.push_back(key); } return keys; } std::optional Mod::Impl::getSettingDefinition(std::string const& key) const { - for (auto& setting : m_info.settings()) { + for (auto& setting : m_metadata.getSettings()) { if (setting.first == key) { return setting.second; } @@ -290,7 +317,7 @@ SettingValue* Mod::Impl::getSetting(std::string const& key) const { } bool Mod::Impl::hasSetting(std::string const& key) const { - for (auto& setting : m_info.settings()) { + for (auto& setting : m_metadata.getSettings()) { if (setting.first == key) { return true; } @@ -301,14 +328,9 @@ bool Mod::Impl::hasSetting(std::string const& key) const { // Loading, Toggling, Installing Result<> Mod::Impl::loadBinary() { - log::debug("Loading binary for mod {}", m_info.id()); - if (m_binaryLoaded) { + 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); @@ -316,7 +338,7 @@ Result<> Mod::Impl::loadBinary() { if (!res) { // make sure to free up the next mod mutex LoaderImpl::get()->releaseNextMod(); - log::warn("Failed to load binary for mod {}: {}", m_info.id(), res.unwrapErr()); + log::error("Failed to load binary for mod {}: {}", m_metadata.getID(), res.unwrapErr()); return res; } m_binaryLoaded = true; @@ -327,57 +349,33 @@ 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(); +Result<> Mod::Impl::enable() { + if (!m_binaryLoaded) + return Err("Tried to enable {} but its binary is not loaded", m_metadata.getID()); - for (auto const& patch : m_patches) { - delete patch; + bool enabledDependencies = true; + for (auto const& item : m_metadata.getDependencies()) { + if (!item.isResolved() || !item.mod) + continue; + auto res = item.mod->enable(); + if (!res) { + enabledDependencies = false; + log::error("Failed to enable {}: {}", item.id, res.unwrapErr()); + } } - m_patches.clear(); - - GEODE_UNWRAP(this->unloadPlatformBinary()); - m_binaryLoaded = false; - - Loader::get()->updateAllDependencies(); - return Ok(); -} + if (!enabledDependencies) + return Err("Mod cannot be enabled because one or more of its dependencies cannot be enabled."); -Result<> Mod::Impl::enable() { - if (!m_binaryLoaded) { - return this->loadBinary(); - } + 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_info.name()); + log::warn("Hook is null in mod \"{}\"", m_metadata.getName()); continue; } if (hook->getAutoEnable()) { @@ -401,41 +399,66 @@ Result<> Mod::Impl::enable() { } Result<> Mod::Impl::disable() { - if (!m_enabled) { + if (!m_enabled) return Ok(); - } - if (!m_info.supportsDisabling()) { - return Err("Mod does not support disabling"); + + 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 (!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 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( @@ -449,57 +472,27 @@ Result<> Mod::Impl::uninstall() { } bool Mod::Impl::isUninstalled() const { - return m_self != Mod::get() && !ghc::filesystem::exists(m_info.path()); + return m_self != Mod::get() && !ghc::filesystem::exists(m_metadata.getPath()); } // 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()); + return Err("Mod::updateDependencies is no longer needed, " + "as this is handled by Loader::refreshModGraph"); +} - // 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 +bool Mod::Impl::hasUnresolvedDependencies() const { + for (auto const& dep : m_metadata.getDependencies()) { 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 true; } } - return Ok(); + return false; } -bool Mod::Impl::hasUnresolvedDependencies() const { - for (auto const& dep : m_info.dependencies()) { +bool Mod::Impl::hasUnresolvedIncompatibilities() const { + for (auto const& dep : m_metadata.getIncompatibilities()) { if (!dep.isResolved()) { return true; } @@ -507,10 +500,14 @@ bool Mod::Impl::hasUnresolvedDependencies() const { return false; } +// msvc stop fucking screaming please i BEG YOU +#pragma warning(suppress : 4996) std::vector Mod::Impl::getUnresolvedDependencies() { +#pragma warning(suppress : 4996) std::vector unresolved; - for (auto const& dep : m_info.dependencies()) { + for (auto const& dep : m_metadata.getDependencies()) { if (!dep.isResolved()) { +#pragma warning(suppress : 4996) unresolved.push_back(dep); } } @@ -518,7 +515,7 @@ std::vector Mod::Impl::getUnresolvedDependencies() { } bool Mod::Impl::depends(std::string const& id) const { - return utils::ranges::contains(m_info.dependencies(), [id](Dependency const& t) { + return utils::ranges::contains(m_metadata.getDependencies(), [id](ModMetadata::Dependency const& t) { return t.id == id; }); } @@ -528,7 +525,7 @@ bool Mod::Impl::depends(std::string const& id) const { Result<> Mod::Impl::enableHook(Hook* hook) { auto res = hook->enable(); if (!res) { - log::error("Can't enable hook {} for mod {}: {}", hook->getDisplayName(), m_info.id(), res.unwrapErr()); + log::error("Can't enable hook {} for mod {}: {}", hook->getDisplayName(), m_metadata.getID(), res.unwrapErr()); } return res; @@ -541,7 +538,7 @@ Result<> Mod::Impl::disableHook(Hook* hook) { Result 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 +579,20 @@ Result 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(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. @@ -608,7 +604,7 @@ Result<> Mod::Impl::createTempDir() { } // If the info doesn't specify a path, don't do anything - if (m_info.path().string().empty()) { + if (m_metadata.getPath().string().empty()) { return Ok(); } @@ -619,16 +615,16 @@ Result<> Mod::Impl::createTempDir() { } // Create geode/temp/mod.id - auto tempPath = tempDir / m_info.id(); + auto tempPath = tempDir / m_metadata.getID(); if (!file::createDirectoryAll(tempPath)) { return Err("Unable to create mod runtime directory"); } // Unzip .geode file into temp dir - GEODE_UNWRAP_INTO(auto unzip, file::Unzip::create(m_info.path())); - if (!unzip.hasEntry(m_info.binaryName())) { + GEODE_UNWRAP_INTO(auto unzip, file::Unzip::create(m_metadata.getPath())); + if (!unzip.hasEntry(m_metadata.getBinaryName())) { return Err( - fmt::format("Unable to find platform binary under the name \"{}\"", m_info.binaryName()) + fmt::format("Unable to find platform binary under the name \"{}\"", m_metadata.getBinaryName()) ); } GEODE_UNWRAP(unzip.extractAllTo(tempPath)); @@ -640,7 +636,7 @@ Result<> Mod::Impl::createTempDir() { } ghc::filesystem::path Mod::Impl::getConfigDir(bool create) const { - auto dir = dirs::getModConfigDir() / m_info.id(); + auto dir = dirs::getModConfigDir() / m_metadata.getID(); if (create) { (void)file::createDirectoryAll(dir); } @@ -651,8 +647,8 @@ char const* Mod::Impl::expandSpriteName(char const* name) { static std::unordered_map expanded = {}; if (expanded.count(name)) return expanded[name]; - auto exp = new char[strlen(name) + 2 + m_info.id().size()]; - auto exps = m_info.id() + "/" + name; + auto exp = new char[strlen(name) + 2 + m_metadata.getID().size()]; + auto exps = m_metadata.getID() + "/" + name; memcpy(exp, exps.c_str(), exps.size() + 1); expanded[name] = exp; @@ -661,7 +657,7 @@ char const* Mod::Impl::expandSpriteName(char const* name) { } ModJson Mod::Impl::getRuntimeInfo() const { - auto json = m_info.toJSON(); + auto json = m_metadata.toJSON(); auto obj = json::Object(); obj["hooks"] = json::Array(); @@ -682,7 +678,7 @@ ModJson Mod::Impl::getRuntimeInfo() const { return json; } -static Result getModImplInfo() { +static Result getModImplInfo() { std::string err; json::Value json; try { @@ -691,34 +687,30 @@ static Result getModImplInfo() { return Err("Unable to parse mod.json: " + std::string(err.what())); } - GEODE_UNWRAP_INTO(auto info, ModInfo::create(json)); - info.supportsDisabling() = false; + GEODE_UNWRAP_INTO(auto info, ModMetadata::create(json)); return Ok(info); } 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 cf049d9d1..a33ae12a6 100644 --- a/loader/src/loader/ModImpl.hpp +++ b/loader/src/loader/ModImpl.hpp @@ -7,9 +7,9 @@ namespace geode { public: Mod* m_self; /** - * Mod info + * Mod metadata */ - ModInfo m_info; + ModMetadata m_metadata; /** * Platform-specific info */ @@ -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 m_parentDependencies; + std::vector m_dependants; /** * Saved values */ @@ -63,7 +62,7 @@ namespace geode { */ bool m_resourcesLoaded = false; - Impl(Mod* self, ModInfo const& info); + Impl(Mod* self, ModMetadata const& metadata); ~Impl(); Result<> setup(); @@ -84,14 +83,21 @@ 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; + bool needsEarlyLoad() const; + bool wasSuccessfullyLoaded() const; + ModMetadata getMetadata() const; ghc::filesystem::path getTempDir() const; ghc::filesystem::path getBinaryPath() const; json::Value& getSaveContainer(); +#if defined(GEODE_EXPOSE_SECRET_INTERNALS_IN_HEADERS_DO_NOT_DEFINE_PLEASE) + void setMetadata(ModMetadata const& metadata); + std::vector getDependants() const; +#endif + Result<> saveData(); Result<> loadData(); @@ -112,25 +118,26 @@ namespace geode { Result<> removeHook(Hook* hook); Result 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 getUnresolvedDependencies(); + bool hasUnresolvedDependencies() const; + bool hasUnresolvedIncompatibilities() const; + [[deprecated]] std::vector getUnresolvedDependencies(); + + Result<> loadBinary(); char const* expandSpriteName(char const* name); ModJson getRuntimeInfo() const; }; - class ModImpl : public Mod { + class ModImpl : public Mod::Impl { public: static Mod::Impl* get(); static Mod::Impl* getImpl(Mod* mod); }; -} \ No newline at end of file +} diff --git a/loader/src/loader/ModInfoImpl.cpp b/loader/src/loader/ModInfoImpl.cpp index 827514427..c482b7b60 100644 --- a/loader/src/loader/ModInfoImpl.cpp +++ b/loader/src/loader/ModInfoImpl.cpp @@ -1,18 +1,16 @@ #include -#include -#include #include #include -#include -#include #include #include "ModInfoImpl.hpp" +#pragma warning(disable : 4996) // deprecation + using namespace geode::prelude; ModInfo::Impl& ModInfoImpl::getImpl(ModInfo& info) { - return *info.m_impl.get(); + return *info.m_impl; } bool Dependency::isResolved() const { @@ -21,330 +19,85 @@ bool Dependency::isResolved() const { this->version.compare(this->mod->getVersion())); } -static std::string sanitizeDetailsData(std::string const& str) { - // delete CRLF - return utils::string::replace(str, "\r", ""); -} - -bool ModInfo::Impl::validateID(std::string const& id) { - // ids may not be empty - if (!id.size()) return false; - for (auto const& c : id) { - if (!(('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9') || - (c == '-') || (c == '_') || (c == '.'))) - return false; - } - return true; -} - -Result ModInfo::Impl::createFromSchemaV010(ModJson const& rawJson) { - ModInfo info; - - auto impl = info.m_impl.get(); - - impl->m_rawJSON = rawJson; - - JsonChecker checker(impl->m_rawJSON); - auto root = checker.root("[mod.json]").obj(); - - root.addKnownKey("geode"); - - // don't think its used locally yet - root.addKnownKey("tags"); - - root.needs("id").validate(MiniFunction(&ModInfo::validateID)).into(impl->m_id); - root.needs("version").into(impl->m_version); - root.needs("name").into(impl->m_name); - root.needs("developer").into(impl->m_developer); - root.has("description").into(impl->m_description); - root.has("repository").into(impl->m_repository); - root.has("toggleable").into(impl->m_supportsDisabling); - root.has("unloadable").into(impl->m_supportsUnloading); - root.has("early-load").into(impl->m_needsEarlyLoad); - if (root.has("api")) { - impl->m_isAPI = true; - } - - for (auto& dep : root.has("dependencies").iterate()) { - auto obj = dep.obj(); - - auto depobj = Dependency{}; - obj.needs("id").validate(MiniFunction(&ModInfo::validateID)).into(depobj.id); - obj.needs("version").into(depobj.version); - obj.has("required").into(depobj.required); - obj.checkUnknownKeys(); - - impl->m_dependencies.push_back(depobj); - } - - for (auto& [key, value] : root.has("settings").items()) { - GEODE_UNWRAP_INTO(auto sett, Setting::parse(key, impl->m_id, value)); - impl->m_settings.push_back({key, sett}); - } - - if (auto resources = root.has("resources").obj()) { - for (auto& [key, _] : resources.has("spritesheets").items()) { - impl->m_spritesheets.push_back(impl->m_id + "/" + key); - } - } - - if (auto issues = root.has("issues").obj()) { - IssuesInfo issuesInfo; - issues.needs("info").into(issuesInfo.info); - issues.has("url").intoAs(issuesInfo.url); - impl->m_issues = issuesInfo; - } - - // with new cli, binary name is always mod id - impl->m_binaryName = impl->m_id + GEODE_PLATFORM_EXTENSION; - - // removed keys - if (root.has("datastore")) { - log::error( - "{}: [mod.json].datastore has been deprecated " - "and removed. Use Saved Values instead (see TODO: DOCS LINK)", impl->m_id - ); - } - if (root.has("binary")) { - log::error("{}: [mod.json].binary has been deprecated and removed.", impl->m_id); - } - - if (checker.isError()) { - return Err(checker.getError()); - } - root.checkUnknownKeys(); - - return Ok(info); -} - -Result ModInfo::Impl::create(ModJson const& json) { - // Check mod.json target version - auto schema = LOADER_VERSION; - if (json.contains("geode") && json["geode"].is_string()) { - GEODE_UNWRAP_INTO( - schema, - VersionInfo::parse(json["geode"].as_string()) - .expect("[mod.json] has invalid target loader version: {error}") - ); - } - else { - return Err( - "[mod.json] has no target loader version " - "specified, or it is invalidally formatted (required: \"[v]X.X.X\")!" - ); - } - if (schema < Loader::get()->minModVersion()) { - return Err( - "[mod.json] is built for an older version (" + schema.toString() + - ") of Geode (current: " + Loader::get()->getVersion().toString() + - "). Please update the mod to the latest version, " - "and if the problem persists, contact the developer " - "to update it." - ); - } - if (schema > Loader::get()->maxModVersion()) { - return Err( - "[mod.json] is built for a newer version (" + schema.toString() + - ") of Geode (current: " + Loader::get()->getVersion().toString() + - "). You need to update Geode in order to use " - "this mod." - ); - } - - // Handle mod.json data based on target - if (schema >= VersionInfo(0, 1, 0)) { - return Impl::createFromSchemaV010(json); - } - - return Err( - "[mod.json] targets a version (" + schema.toString() + - ") that isn't supported by this version (v" + - LOADER_VERSION_STR + - ") of geode. This is probably a bug; report it to " - "the Geode Development Team." - ); -} - -Result ModInfo::Impl::createFromFile(ghc::filesystem::path const& path) { - GEODE_UNWRAP_INTO(auto read, utils::file::readString(path)); - - try { - GEODE_UNWRAP_INTO(auto info, ModInfo::create(json::parse(read))); - - auto impl = info.m_impl.get(); - - impl->m_path = path; - if (path.has_parent_path()) { - GEODE_UNWRAP(info.addSpecialFiles(path.parent_path())); - } - return Ok(info); - } - catch (std::exception& err) { - return Err(std::string("Unable to parse mod.json: ") + err.what()); - } -} - -Result ModInfo::Impl::createFromGeodeFile(ghc::filesystem::path const& path) { - GEODE_UNWRAP_INTO(auto unzip, file::Unzip::create(path)); - return ModInfo::createFromGeodeZip(unzip); -} - -Result ModInfo::Impl::createFromGeodeZip(file::Unzip& unzip) { - // Check if mod.json exists in zip - if (!unzip.hasEntry("mod.json")) { - return Err("\"" + unzip.getPath().string() + "\" is missing mod.json"); - } - - // Read mod.json & parse if possible - GEODE_UNWRAP_INTO( - auto jsonData, unzip.extract("mod.json").expect("Unable to read mod.json: {error}") - ); - - std::string err; - ModJson json; - try { - json = json::parse(std::string(jsonData.begin(), jsonData.end())); - } - catch (std::exception& err) { - return Err(err.what()); - } - - auto res = ModInfo::create(json); - if (!res) { - return Err("\"" + unzip.getPath().string() + "\" - " + res.unwrapErr()); - } - auto info = res.unwrap(); - auto impl = info.m_impl.get(); - impl->m_path = unzip.getPath(); - - GEODE_UNWRAP(info.addSpecialFiles(unzip).expect("Unable to add extra files: {error}")); - - return Ok(info); -} - -Result<> ModInfo::Impl::addSpecialFiles(file::Unzip& unzip) { - // unzip known MD files - for (auto& [file, target] : this->getSpecialFiles()) { - if (unzip.hasEntry(file)) { - GEODE_UNWRAP_INTO(auto data, unzip.extract(file).expect("Unable to extract \"{}\"", file)); - *target = sanitizeDetailsData(std::string(data.begin(), data.end())); - } - } - return Ok(); -} - -Result<> ModInfo::Impl::addSpecialFiles(ghc::filesystem::path const& dir) { - // unzip known MD files - for (auto& [file, target] : this->getSpecialFiles()) { - if (ghc::filesystem::exists(dir / file)) { - auto data = file::readString(dir / file); - if (!data) { - return Err("Unable to read \"" + file + "\": " + data.unwrapErr()); - } - *target = sanitizeDetailsData(data.unwrap()); - } - } - return Ok(); -} - -std::vector*>> ModInfo::Impl::getSpecialFiles() { - return { - {"about.md", &this->m_details}, - {"changelog.md", &this->m_changelog}, - {"support.md", &this->m_supportInfo}, - }; -} - -ModJson ModInfo::Impl::toJSON() const { - auto json = m_rawJSON; - json["path"] = this->m_path.string(); - json["binary"] = this->m_binaryName; - return json; -} - -ModJson ModInfo::Impl::getRawJSON() const { - return m_rawJSON; -} - bool ModInfo::Impl::operator==(ModInfo::Impl const& other) const { - return this->m_id == other.m_id; + return this->m_metadata.m_id == other.m_metadata.m_id; } ghc::filesystem::path& ModInfo::path() { - return m_impl->m_path; + return m_impl->m_metadata.m_path; } ghc::filesystem::path const& ModInfo::path() const { - return m_impl->m_path; + return m_impl->m_metadata.m_path; } std::string& ModInfo::binaryName() { - return m_impl->m_binaryName; + return m_impl->m_metadata.m_binaryName; } std::string const& ModInfo::binaryName() const { - return m_impl->m_binaryName; + return m_impl->m_metadata.m_binaryName; } VersionInfo& ModInfo::version() { - return m_impl->m_version; + return m_impl->m_metadata.m_version; } VersionInfo const& ModInfo::version() const { - return m_impl->m_version; + return m_impl->m_metadata.m_version; } std::string& ModInfo::id() { - return m_impl->m_id; + return m_impl->m_metadata.m_id; } std::string const& ModInfo::id() const { - return m_impl->m_id; + return m_impl->m_metadata.m_id; } std::string& ModInfo::name() { - return m_impl->m_name; + return m_impl->m_metadata.m_name; } std::string const& ModInfo::name() const { - return m_impl->m_name; + return m_impl->m_metadata.m_name; } std::string& ModInfo::developer() { - return m_impl->m_developer; + return m_impl->m_metadata.m_developer; } std::string const& ModInfo::developer() const { - return m_impl->m_developer; + return m_impl->m_metadata.m_developer; } std::optional& ModInfo::description() { - return m_impl->m_description; + return m_impl->m_metadata.m_description; } std::optional const& ModInfo::description() const { - return m_impl->m_description; + return m_impl->m_metadata.m_description; } std::optional& ModInfo::details() { - return m_impl->m_details; + return m_impl->m_metadata.m_details; } std::optional const& ModInfo::details() const { - return m_impl->m_details; + return m_impl->m_metadata.m_details; } std::optional& ModInfo::changelog() { - return m_impl->m_changelog; + return m_impl->m_metadata.m_changelog; } std::optional const& ModInfo::changelog() const { - return m_impl->m_changelog; + return m_impl->m_metadata.m_changelog; } std::optional& ModInfo::supportInfo() { - return m_impl->m_supportInfo; + return m_impl->m_metadata.m_supportInfo; } std::optional const& ModInfo::supportInfo() const { - return m_impl->m_supportInfo; + return m_impl->m_metadata.m_supportInfo; } std::optional& ModInfo::repository() { - return m_impl->m_repository; + return m_impl->m_metadata.m_repository; } std::optional const& ModInfo::repository() const { - return m_impl->m_repository; + return m_impl->m_metadata.m_repository; } std::optional& ModInfo::issues() { @@ -362,17 +115,17 @@ std::vector const& ModInfo::dependencies() const { } std::vector& ModInfo::spritesheets() { - return m_impl->m_spritesheets; + return m_impl->m_metadata.m_spritesheets; } std::vector const& ModInfo::spritesheets() const { - return m_impl->m_spritesheets; + return m_impl->m_metadata.m_spritesheets; } std::vector>& ModInfo::settings() { - return m_impl->m_settings; + return m_impl->m_metadata.m_settings; } std::vector> const& ModInfo::settings() const { - return m_impl->m_settings; + return m_impl->m_metadata.m_settings; } bool& ModInfo::supportsDisabling() { @@ -390,87 +143,102 @@ bool const& ModInfo::supportsUnloading() const { } bool& ModInfo::needsEarlyLoad() { - return m_impl->m_needsEarlyLoad; + return m_impl->m_metadata.m_needsEarlyLoad; } bool const& ModInfo::needsEarlyLoad() const { - return m_impl->m_needsEarlyLoad; + return m_impl->m_metadata.m_needsEarlyLoad; } bool& ModInfo::isAPI() { - return m_impl->m_isAPI; + return m_impl->m_metadata.m_isAPI; } bool const& ModInfo::isAPI() const { - return m_impl->m_isAPI; + return m_impl->m_metadata.m_isAPI; } Result ModInfo::createFromGeodeZip(utils::file::Unzip& zip) { - return Impl::createFromGeodeZip(zip); + return ModMetadataImpl::createFromGeodeZip(zip); } Result ModInfo::createFromGeodeFile(ghc::filesystem::path const& path) { - return Impl::createFromGeodeFile(path); + return ModMetadataImpl::createFromGeodeFile(path); } Result ModInfo::createFromFile(ghc::filesystem::path const& path) { - return Impl::createFromFile(path); + return ModMetadataImpl::createFromFile(path); } Result ModInfo::create(ModJson const& json) { - return Impl::create(json); + return ModMetadataImpl::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); } -bool ModInfo::validateID(std::string const& id) { - return Impl::validateID(id); +#pragma warning(suppress : 4996) +ModInfo::ModInfo() : m_impl(std::make_unique()) {} + +ModInfo::ModInfo(ModInfo const& other) : m_impl(std::make_unique(*other.m_impl)) {} + +ModInfo::ModInfo(ModInfo&& other) noexcept : m_impl(std::move(other.m_impl)) {} + +ModInfo& ModInfo::operator=(ModInfo const& other) { + m_impl = std::make_unique(*other.m_impl); + return *this; +} + +ModInfo& ModInfo::operator=(ModInfo&& other) noexcept { + m_impl = std::move(other.m_impl); + return *this; +} + +ModInfo::operator ModMetadata() { + ModMetadata metadata; + ModMetadataImpl::getImpl(metadata) = std::move(m_impl->m_metadata); + auto& metadataImpl = ModMetadataImpl::getImpl(metadata); + metadataImpl.m_issues = m_impl->m_issues ? + ModMetadata::IssuesInfo::fromDeprecated(m_impl->m_issues.value()) : + std::optional(); + for (auto& dep : m_impl->m_dependencies) + metadataImpl.m_dependencies.push_back(ModMetadata::Dependency::fromDeprecated(dep)); + return metadata; +} + +ModInfo::operator ModMetadata() const { + ModMetadata metadata; + ModMetadataImpl::getImpl(metadata) = std::move(m_impl->m_metadata); + return metadata; } ModJson& ModInfo::rawJSON() { - return m_impl->m_rawJSON; + return m_impl->m_metadata.m_rawJSON; } ModJson const& ModInfo::rawJSON() const { - return m_impl->m_rawJSON; + return m_impl->m_metadata.m_rawJSON; } -Result ModInfo::createFromSchemaV010(ModJson const& json) { - return Impl::createFromSchemaV010(json); +Result ModInfo::createFromSchemaV010(geode::ModJson const& json) { + return ModMetadataImpl::createFromSchemaV010(json); } Result<> ModInfo::addSpecialFiles(ghc::filesystem::path const& dir) { - return m_impl->addSpecialFiles(dir); + return m_impl->m_metadata.addSpecialFiles(dir); } Result<> ModInfo::addSpecialFiles(utils::file::Unzip& zip) { - return m_impl->addSpecialFiles(zip); + return m_impl->m_metadata.addSpecialFiles(zip); } std::vector*>> ModInfo::getSpecialFiles() { - return m_impl->getSpecialFiles(); -} - -ModInfo::ModInfo() : m_impl(std::make_unique()) {} - -ModInfo::ModInfo(ModInfo const& other) : m_impl(std::make_unique(*other.m_impl)) {} - -ModInfo::ModInfo(ModInfo&& other) noexcept : m_impl(std::move(other.m_impl)) {} - -ModInfo& ModInfo::operator=(ModInfo const& other) { - m_impl = std::make_unique(*other.m_impl); - return *this; -} - -ModInfo& ModInfo::operator=(ModInfo&& other) noexcept { - m_impl = std::move(other.m_impl); - return *this; + return m_impl->m_metadata.getSpecialFiles(); } -ModInfo::~ModInfo() {} \ No newline at end of file +ModInfo::~ModInfo() = default; diff --git a/loader/src/loader/ModInfoImpl.hpp b/loader/src/loader/ModInfoImpl.hpp index 422ffa898..e77576305 100644 --- a/loader/src/loader/ModInfoImpl.hpp +++ b/loader/src/loader/ModInfoImpl.hpp @@ -1,59 +1,30 @@ #pragma once +#include "ModMetadataImpl.hpp" + #include #include #include #include +#pragma warning(disable : 4996) // deprecation + using namespace geode::prelude; namespace geode { - class ModInfo::Impl { + class [[deprecated]] ModInfo::Impl { public: - ghc::filesystem::path m_path; - std::string m_binaryName; - VersionInfo m_version{1, 0, 0}; - std::string m_id; - std::string m_name; - std::string m_developer; - std::optional m_description; - std::optional m_details; - std::optional m_changelog; - std::optional m_supportInfo; - std::optional m_repository; + ModMetadataImpl m_metadata; std::optional m_issues; std::vector m_dependencies; - std::vector m_spritesheets; - std::vector> m_settings; bool m_supportsDisabling = true; bool m_supportsUnloading = false; - bool m_needsEarlyLoad = false; - bool m_isAPI = false; - - ModJson m_rawJSON; - - static Result createFromGeodeZip(utils::file::Unzip& zip); - static Result createFromGeodeFile(ghc::filesystem::path const& path); - static Result createFromFile(ghc::filesystem::path const& path); - static Result 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 createFromSchemaV010(ModJson const& json); - - Result<> addSpecialFiles(ghc::filesystem::path const& dir); - Result<> addSpecialFiles(utils::file::Unzip& zip); - - std::vector*>> getSpecialFiles(); }; - class ModInfoImpl { + class [[deprecated]] ModInfoImpl : public ModInfo::Impl { public: static ModInfo::Impl& getImpl(ModInfo& info); }; -} \ No newline at end of file +} diff --git a/loader/src/loader/ModMetadataImpl.cpp b/loader/src/loader/ModMetadataImpl.cpp new file mode 100644 index 000000000..884ace85d --- /dev/null +++ b/loader/src/loader/ModMetadataImpl.cpp @@ -0,0 +1,609 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ModMetadataImpl.hpp" +#include "ModInfoImpl.hpp" + +using namespace geode::prelude; + +ModMetadata::Impl& ModMetadataImpl::getImpl(ModMetadata& info) { + return *info.m_impl; +} + +bool ModMetadata::Dependency::isResolved() const { + return this->importance != Importance::Required || + this->mod && this->mod->isLoaded() && this->version.compare(this->mod->getVersion()); +} + +bool ModMetadata::Incompatibility::isResolved() const { + return this->importance != Importance::Breaking || + (!this->mod || !this->version.compare(this->mod->getVersion())); +} + +ModMetadata::Dependency::operator geode::Dependency() { + return {id, version, importance == Importance::Required, mod}; +} +ModMetadata::Dependency::operator geode::Dependency() const { + return {id, version, importance == Importance::Required, mod}; +} +ModMetadata::IssuesInfo::operator geode::IssuesInfo() { + return {info, url}; +} +ModMetadata::IssuesInfo::operator geode::IssuesInfo() const { + return {info, url}; +} + +ModMetadata::Dependency ModMetadata::Dependency::fromDeprecated(geode::Dependency const& value) { + return { + value.id, + value.version, + value.required ? + ModMetadata::Dependency::Importance::Required : + ModMetadata::Dependency::Importance::Suggested, + value.mod + }; +} +ModMetadata::IssuesInfo ModMetadata::IssuesInfo::fromDeprecated(geode::IssuesInfo const& value) { + return {value.info, value.url}; +} + +static std::string sanitizeDetailsData(std::string const& str) { + // delete CRLF + return utils::string::replace(str, "\r", ""); +} + +bool ModMetadata::Impl::validateID(std::string const& id) { + // ids may not be empty + if (id.empty()) return false; + for (auto const& c : id) { + if (!(('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9') || + (c == '-') || (c == '_') || (c == '.'))) + return false; + } + return true; +} + +Result ModMetadata::Impl::createFromSchemaV010(ModJson const& rawJson) { + ModMetadata info; + + auto impl = info.m_impl.get(); + + impl->m_rawJSON = rawJson; + + JsonChecker checker(impl->m_rawJSON); + auto root = checker.root("[mod.json]").obj(); + + root.addKnownKey("geode"); + + // don't think its used locally yet + root.addKnownKey("tags"); + + root.needs("id").validate(MiniFunction(&ModMetadata::validateID)).into(impl->m_id); + root.needs("version").into(impl->m_version); + root.needs("name").into(impl->m_name); + root.needs("developer").into(impl->m_developer); + root.has("description").into(impl->m_description); + root.has("repository").into(impl->m_repository); + root.has("early-load").into(impl->m_needsEarlyLoad); + // TODO for 2.0.0: fix this lol + // i think whoever wrote that intended that has would return the value if the key is present and false otherwise + // but the actual behavior here is false if key not present and true if key is present + if (root.has("api")) { + impl->m_isAPI = true; + } + + if (root.has("toggleable")) + log::warn("{}: [mod.json].toggleable is deprecated and will be removed in a future update.", impl->m_id); + if (root.has("unloadable")) + log::warn("{}: [mod.json].unloadable is deprecated and will be removed in a future update.", impl->m_id); + + // TODO for 2.0.0: specify this in mod.json manually + if (info.getID() != "geode.loader") { + impl->m_dependencies.push_back({ + "geode.loader", + {LOADER_VERSION, VersionCompare::Exact}, + Dependency::Importance::Required, + Mod::get() + }); + } + + for (auto& dep : root.has("dependencies").iterate()) { + auto obj = dep.obj(); + + Dependency dependency; + obj.needs("id").validate(MiniFunction(&ModMetadata::validateID)).into(dependency.id); + obj.needs("version").into(dependency.version); + auto required = obj.has("required"); + if (required) { + log::warn("{}: [mod.json].required has been deprecated and will be removed " + "in a future update. Use importance instead (see TODO: DOCS LINK)", impl->m_id); + dependency.importance = required.get() ? + Dependency::Importance::Required : + Dependency::Importance::Suggested; + } + obj.has("importance").into(dependency.importance); + obj.checkUnknownKeys(); + + impl->m_dependencies.push_back(dependency); + } + + for (auto& incompat : root.has("incompatibilities").iterate()) { + auto obj = incompat.obj(); + + Incompatibility incompatibility; + obj.needs("id").validate(MiniFunction(&ModMetadata::validateID)).into(incompatibility.id); + obj.needs("version").into(incompatibility.version); + obj.has("importance").into(incompatibility.importance); + obj.checkUnknownKeys(); + + impl->m_incompatibilities.push_back(incompatibility); + } + + for (auto& [key, value] : root.has("settings").items()) { + GEODE_UNWRAP_INTO(auto sett, Setting::parse(key, impl->m_id, value)); + impl->m_settings.emplace_back(key, sett); + } + + if (auto resources = root.has("resources").obj()) { + for (auto& [key, _] : resources.has("spritesheets").items()) { + impl->m_spritesheets.push_back(impl->m_id + "/" + key); + } + } + + if (auto issues = root.has("issues").obj()) { + IssuesInfo issuesInfo; + issues.needs("info").into(issuesInfo.info); + issues.has("url").intoAs(issuesInfo.url); + impl->m_issues = issuesInfo; + } + + // with new cli, binary name is always mod id + impl->m_binaryName = impl->m_id + GEODE_PLATFORM_EXTENSION; + + // removed keys + if (root.has("datastore")) { + log::error( + "{}: [mod.json].datastore has been removed. " + "Use Saved Values instead (see TODO: DOCS LINK)", impl->m_id + ); + } + if (root.has("binary")) { + log::error("{}: [mod.json].binary has been removed.", impl->m_id); + } + + if (checker.isError()) { + return Err(checker.getError()); + } + root.checkUnknownKeys(); + + return Ok(info); +} + +Result ModMetadata::Impl::create(ModJson const& json) { + // Check mod.json target version + auto schema = LOADER_VERSION; + if (json.contains("geode") && json["geode"].is_string()) { + GEODE_UNWRAP_INTO( + schema, + VersionInfo::parse(json["geode"].as_string()) + .expect("[mod.json] has invalid target loader version: {error}") + ); + } + else { + return Err( + "[mod.json] has no target loader version " + "specified, or its formatting is invalid (required: \"[v]X.X.X\")!" + ); + } + if (schema < Loader::get()->minModVersion()) { + return Err( + "[mod.json] is built for an older version (" + schema.toString() + + ") of Geode (current: " + Loader::get()->getVersion().toString() + + "). Please update the mod to the latest version, " + "and if the problem persists, contact the developer " + "to update it." + ); + } + if (schema > Loader::get()->maxModVersion()) { + return Err( + "[mod.json] is built for a newer version (" + schema.toString() + + ") of Geode (current: " + Loader::get()->getVersion().toString() + + "). You need to update Geode in order to use " + "this mod." + ); + } + + // Handle mod.json data based on target + if (schema < VersionInfo(0, 1, 0)) { + return Err( + "[mod.json] targets a version (" + schema.toString() + + ") that isn't supported by this version (v" + + LOADER_VERSION_STR + + ") of geode. This is probably a bug; report it to " + "the Geode Development Team." + ); + } + + return Impl::createFromSchemaV010(json); +} + +Result ModMetadata::Impl::createFromFile(ghc::filesystem::path const& path) { + GEODE_UNWRAP_INTO(auto read, utils::file::readString(path)); + + try { + GEODE_UNWRAP_INTO(auto info, ModMetadata::create(json::parse(read))); + + auto impl = info.m_impl.get(); + + impl->m_path = path; + if (path.has_parent_path()) { + GEODE_UNWRAP(info.addSpecialFiles(path.parent_path())); + } + return Ok(info); + } + catch (std::exception& err) { + return Err(std::string("Unable to parse mod.json: ") + err.what()); + } +} + +Result ModMetadata::Impl::createFromGeodeFile(ghc::filesystem::path const& path) { + GEODE_UNWRAP_INTO(auto unzip, file::Unzip::create(path)); + return ModMetadata::createFromGeodeZip(unzip); +} + +Result ModMetadata::Impl::createFromGeodeZip(file::Unzip& unzip) { + // Check if mod.json exists in zip + if (!unzip.hasEntry("mod.json")) { + return Err("\"" + unzip.getPath().string() + "\" is missing mod.json"); + } + + // Read mod.json & parse if possible + GEODE_UNWRAP_INTO( + auto jsonData, unzip.extract("mod.json").expect("Unable to read mod.json: {error}") + ); + + std::string err; + ModJson json; + try { + json = json::parse(std::string(jsonData.begin(), jsonData.end())); + } + catch (std::exception& err) { + return Err(err.what()); + } + + auto res = ModMetadata::create(json); + if (!res) { + return Err("\"" + unzip.getPath().string() + "\" - " + res.unwrapErr()); + } + auto info = res.unwrap(); + auto impl = info.m_impl.get(); + impl->m_path = unzip.getPath(); + + GEODE_UNWRAP(info.addSpecialFiles(unzip).expect("Unable to add extra files: {error}")); + + return Ok(info); +} + +Result<> ModMetadata::Impl::addSpecialFiles(file::Unzip& unzip) { + // unzip known MD files + for (auto& [file, target] : this->getSpecialFiles()) { + if (unzip.hasEntry(file)) { + GEODE_UNWRAP_INTO(auto data, unzip.extract(file).expect("Unable to extract \"{}\"", file)); + *target = sanitizeDetailsData(std::string(data.begin(), data.end())); + } + } + return Ok(); +} + +Result<> ModMetadata::Impl::addSpecialFiles(ghc::filesystem::path const& dir) { + // unzip known MD files + for (auto& [file, target] : this->getSpecialFiles()) { + if (ghc::filesystem::exists(dir / file)) { + auto data = file::readString(dir / file); + if (!data) { + return Err("Unable to read \"" + file + "\": " + data.unwrapErr()); + } + *target = sanitizeDetailsData(data.unwrap()); + } + } + return Ok(); +} + +std::vector*>> ModMetadata::Impl::getSpecialFiles() { + return { + {"about.md", &this->m_details}, + {"changelog.md", &this->m_changelog}, + {"support.md", &this->m_supportInfo}, + }; +} + +ModJson ModMetadata::Impl::toJSON() const { + auto json = m_rawJSON; + json["path"] = this->m_path.string(); + json["binary"] = this->m_binaryName; + return json; +} + +ModJson ModMetadata::Impl::getRawJSON() const { + return m_rawJSON; +} + +bool ModMetadata::Impl::operator==(ModMetadata::Impl const& other) const { + return this->m_id == other.m_id; +} + +[[maybe_unused]] ghc::filesystem::path ModMetadata::getPath() const { + return m_impl->m_path; +} + +std::string ModMetadata::getBinaryName() const { + return m_impl->m_binaryName; +} + +VersionInfo ModMetadata::getVersion() const { + return m_impl->m_version; +} + +std::string ModMetadata::getID() const { + return m_impl->m_id; +} + +std::string ModMetadata::getName() const { + return m_impl->m_name; +} + +std::string ModMetadata::getDeveloper() const { + return m_impl->m_developer; +} + +std::optional ModMetadata::getDescription() const { + return m_impl->m_description; +} + +std::optional ModMetadata::getDetails() const { + return m_impl->m_details; +} + +std::optional ModMetadata::getChangelog() const { + return m_impl->m_changelog; +} + +std::optional ModMetadata::getSupportInfo() const { + return m_impl->m_supportInfo; +} + +std::optional ModMetadata::getRepository() const { + return m_impl->m_repository; +} + +std::optional ModMetadata::getIssues() const { + return m_impl->m_issues; +} + +std::vector ModMetadata::getDependencies() const { + return m_impl->m_dependencies; +} + +std::vector ModMetadata::getIncompatibilities() const { + return m_impl->m_incompatibilities; +} + +std::vector ModMetadata::getSpritesheets() const { + return m_impl->m_spritesheets; +} + +std::vector> ModMetadata::getSettings() const { + return m_impl->m_settings; +} + +bool ModMetadata::needsEarlyLoad() const { + return m_impl->m_needsEarlyLoad; +} + +bool ModMetadata::isAPI() const { + return m_impl->m_isAPI; +} + +#if defined(GEODE_EXPOSE_SECRET_INTERNALS_IN_HEADERS_DO_NOT_DEFINE_PLEASE) +void ModMetadata::setPath(ghc::filesystem::path const& value) { + m_impl->m_path = value; +} + +void ModMetadata::setBinaryName(std::string const& value) { + m_impl->m_binaryName = value; +} + +void ModMetadata::setVersion(VersionInfo const& value) { + m_impl->m_version = value; +} + +void ModMetadata::setID(std::string const& value) { + m_impl->m_id = value; +} + +void ModMetadata::setName(std::string const& value) { + m_impl->m_name = value; +} + +void ModMetadata::setDeveloper(std::string const& value) { + m_impl->m_developer = value; +} + +void ModMetadata::setDescription(std::optional const& value) { + m_impl->m_description = value; +} + +void ModMetadata::setDetails(std::optional const& value) { + m_impl->m_details = value; +} + +void ModMetadata::setChangelog(std::optional const& value) { + m_impl->m_changelog = value; +} + +void ModMetadata::setSupportInfo(std::optional const& value) { + m_impl->m_supportInfo = value; +} + +void ModMetadata::setRepository(std::optional const& value) { + m_impl->m_repository = value; +} + +void ModMetadata::setIssues(std::optional const& value) { + m_impl->m_issues = value; +} + +void ModMetadata::setDependencies(std::vector const& value) { + m_impl->m_dependencies = value; +} + +void ModMetadata::setIncompatibilities(std::vector const& value) { + m_impl->m_incompatibilities = value; +} + +void ModMetadata::setSpritesheets(std::vector const& value) { + m_impl->m_spritesheets = value; +} + +void ModMetadata::setSettings(std::vector> const& value) { + m_impl->m_settings = value; +} + +void ModMetadata::setNeedsEarlyLoad(bool const& value) { + m_impl->m_needsEarlyLoad = value; +} + +void ModMetadata::setIsAPI(bool const& value) { + m_impl->m_isAPI = value; +} +#endif + +Result ModMetadata::createFromGeodeZip(utils::file::Unzip& zip) { + return Impl::createFromGeodeZip(zip); +} + +Result ModMetadata::createFromGeodeFile(ghc::filesystem::path const& path) { + return Impl::createFromGeodeFile(path); +} + +Result ModMetadata::createFromFile(ghc::filesystem::path const& path) { + return Impl::createFromFile(path); +} + +Result ModMetadata::create(ModJson const& json) { + return Impl::create(json); +} + +ModJson ModMetadata::toJSON() const { + return m_impl->toJSON(); +} + +ModJson ModMetadata::getRawJSON() const { + return m_impl->getRawJSON(); +} + +bool ModMetadata::operator==(ModMetadata const& other) const { + return m_impl->operator==(*other.m_impl); +} + +bool ModMetadata::validateID(std::string const& id) { + return Impl::validateID(id); +} + +Result ModMetadata::createFromSchemaV010(ModJson const& json) { + return Impl::createFromSchemaV010(json); +} + +Result<> ModMetadata::addSpecialFiles(ghc::filesystem::path const& dir) { + return m_impl->addSpecialFiles(dir); +} +Result<> ModMetadata::addSpecialFiles(utils::file::Unzip& zip) { + return m_impl->addSpecialFiles(zip); +} + +std::vector*>> ModMetadata::getSpecialFiles() { + return m_impl->getSpecialFiles(); +} + +ModMetadata::ModMetadata() : m_impl(std::make_unique()) {} +ModMetadata::ModMetadata(std::string id) : m_impl(std::make_unique()) { m_impl->m_id = std::move(id); } +ModMetadata::ModMetadata(ModMetadata const& other) : m_impl(std::make_unique(*other.m_impl)) {} +ModMetadata::ModMetadata(ModMetadata&& other) noexcept : m_impl(std::move(other.m_impl)) {} + +ModMetadata& ModMetadata::operator=(ModMetadata const& other) { + m_impl = std::make_unique(*other.m_impl); + return *this; +} + +ModMetadata& ModMetadata::operator=(ModMetadata&& other) noexcept { + m_impl = std::move(other.m_impl); + return *this; +} + +ModMetadata::operator ModInfo() { + ModInfo info; + auto infoImpl = ModInfoImpl::getImpl(info); + infoImpl.m_metadata.Impl::operator=(*m_impl); // im gonna cry what is this hack why are you not using pointers + infoImpl.m_issues = m_impl->m_issues; + for (auto& dep : m_impl->m_dependencies) + infoImpl.m_dependencies.push_back(dep); + return info; +} +ModMetadata::operator ModInfo() const { + ModInfo info; + auto infoImpl = ModInfoImpl::getImpl(info); + infoImpl.m_metadata.Impl::operator=(*m_impl); + infoImpl.m_issues = m_impl->m_issues; + for (auto& dep : m_impl->m_dependencies) + infoImpl.m_dependencies.push_back(dep); + return info; +} + +ModMetadata::~ModMetadata() = default; + +template <> +struct json::Serialize { + static json::Value GEODE_DLL to_json(geode::ModMetadata::Dependency::Importance const& importance) { + switch (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) { + auto impStr = importance.as_string(); + if (impStr == "required") + return geode::ModMetadata::Dependency::Importance::Required; + if (impStr == "recommended") + return geode::ModMetadata::Dependency::Importance::Recommended; + if (impStr == "suggested") + return geode::ModMetadata::Dependency::Importance::Suggested; + throw json::JsonException(R"(Expected importance to be "required", "recommended" or "suggested")"); + } +}; + +template <> +struct json::Serialize { + static json::Value GEODE_DLL to_json(geode::ModMetadata::Incompatibility::Importance const& importance) { + switch (importance) { + case geode::ModMetadata::Incompatibility::Importance::Breaking: return {"breaking"}; + case geode::ModMetadata::Incompatibility::Importance::Conflicting: return {"conflicting"}; + default: return {"unknown"}; + } + } + static geode::ModMetadata::Incompatibility::Importance GEODE_DLL from_json(json::Value const& importance) { + auto impStr = importance.as_string(); + if (impStr == "breaking") + return geode::ModMetadata::Incompatibility::Importance::Breaking; + if (impStr == "conflicting") + return geode::ModMetadata::Incompatibility::Importance::Conflicting; + throw json::JsonException(R"(Expected importance to be "breaking" or "conflicting")"); + } +}; diff --git a/loader/src/loader/ModMetadataImpl.hpp b/loader/src/loader/ModMetadataImpl.hpp new file mode 100644 index 000000000..fefa6acf0 --- /dev/null +++ b/loader/src/loader/ModMetadataImpl.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include +#include + +using namespace geode::prelude; + +namespace geode { + class ModMetadata::Impl { + public: + ghc::filesystem::path m_path; + std::string m_binaryName; + VersionInfo m_version{1, 0, 0}; + std::string m_id; + std::string m_name; + std::string m_developer; + std::optional m_description; + std::optional m_details; + std::optional m_changelog; + std::optional m_supportInfo; + std::optional m_repository; + std::optional m_issues; + std::vector m_dependencies; + std::vector m_incompatibilities; + std::vector m_spritesheets; + std::vector> m_settings; + bool m_needsEarlyLoad = false; + bool m_isAPI = false; + + ModJson m_rawJSON; + + static Result createFromGeodeZip(utils::file::Unzip& zip); + static Result createFromGeodeFile(ghc::filesystem::path const& path); + static Result createFromFile(ghc::filesystem::path const& path); + static Result create(ModJson const& json); + + ModJson toJSON() const; + ModJson getRawJSON() const; + + bool operator==(ModMetadata::Impl const& other) const; + + static bool validateID(std::string const& id); + + static Result createFromSchemaV010(ModJson const& rawJson); + + Result<> addSpecialFiles(ghc::filesystem::path const& dir); + Result<> addSpecialFiles(utils::file::Unzip& zip); + + std::vector*>> getSpecialFiles(); + }; + + class ModMetadataImpl : public ModMetadata::Impl { + public: + static ModMetadata::Impl& getImpl(ModMetadata& info); + }; +} diff --git a/loader/src/platform/Objcpp.mm b/loader/src/platform/Objcpp.mm index 8e5d54dfc..e8c49b912 100644 --- a/loader/src/platform/Objcpp.mm +++ b/loader/src/platform/Objcpp.mm @@ -5,6 +5,7 @@ #if defined(GEODE_IS_MACOS) +#include "mac/LoaderImpl.mm" #include "mac/main.mm" #include "mac/crashlog.mm" #include "mac/FileWatcher.mm" diff --git a/loader/src/platform/mac/LoaderImpl.cpp b/loader/src/platform/mac/LoaderImpl.mm similarity index 92% rename from loader/src/platform/mac/LoaderImpl.cpp rename to loader/src/platform/mac/LoaderImpl.mm index 1ed684ab9..a1a8148e9 100644 --- a/loader/src/platform/mac/LoaderImpl.cpp +++ b/loader/src/platform/mac/LoaderImpl.mm @@ -3,10 +3,7 @@ #include #include #include - -#ifdef GEODE_IS_MACOS - - #include +#import using namespace geode::prelude; @@ -36,6 +33,9 @@ void Loader::Impl::logConsoleMessageWithSeverity(std::string const& msg, Severit } void Loader::Impl::openPlatformConsole() { + // it's not possible to redirect stdout to a terminal + // and the console.app is too clunky + m_platformConsoleOpen = true; for (auto const& log : log::Logger::list()) { @@ -83,5 +83,3 @@ void Loader::Impl::setupIPC() { bool Loader::Impl::userTriedToLoadDLLs() const { return false; } - -#endif diff --git a/loader/src/platform/mac/ModImpl.cpp b/loader/src/platform/mac/ModImpl.cpp index b5232e4dd..762ad3ece 100644 --- a/loader/src/platform/mac/ModImpl.cpp +++ b/loader/src/platform/mac/ModImpl.cpp @@ -2,9 +2,9 @@ #ifdef GEODE_IS_MACOS - #include - #include - #include +#include +#include +#include using namespace geode::prelude; @@ -19,7 +19,7 @@ T findSymbolOrMangled(void* dylib, char const* name, char const* mangled) { Result<> Mod::Impl::loadPlatformBinary() { auto dylib = - dlopen((m_tempDirName / m_info.binaryName()).string().c_str(), RTLD_LAZY); + dlopen((m_tempDirName / m_metadata.getBinaryName()).string().c_str(), RTLD_LAZY); if (dylib) { if (m_platformInfo) { delete m_platformInfo; diff --git a/loader/src/platform/mac/util.mm b/loader/src/platform/mac/util.mm index 80838c564..cd2c83c06 100644 --- a/loader/src/platform/mac/util.mm +++ b/loader/src/platform/mac/util.mm @@ -10,6 +10,7 @@ #include #include #include +#include bool utils::clipboard::write(std::string const& data) { [[NSPasteboard generalPasteboard] clearContents]; @@ -180,7 +181,7 @@ @implementation FileDialog _NSGetExecutablePath(gddir.data(), &out); ghc::filesystem::path gdpath = gddir.data(); - auto currentPath = gdpath.parent_path().parent_path(); + auto currentPath = ghc::filesystem::canonical(gdpath.parent_path().parent_path()); return currentPath; }(); @@ -200,4 +201,38 @@ @implementation FileDialog return path; } +void geode::utils::game::restart() { + if (CCApplication::sharedApplication() && + (GameManager::get()->m_playLayer || GameManager::get()->m_levelEditorLayer)) { + log::error("Cannot restart in PlayLayer or LevelEditorLayer!"); + return; + } + + auto restart = +[] { + log::info("Restarting game..."); + auto gdExec = dirs::getGameDir() / "MacOS" / "Geometry Dash"; + + NSTask *task = [NSTask new]; + [task setLaunchPath: [NSString stringWithUTF8String: gdExec.string().c_str()]]; + [task launch]; + }; + + class Exit : public CCObject { + public: + void shutdown() { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wobjc-method-access" + [[[NSClassFromString(@"AppControllerManager") sharedInstance] controller] shutdownGame]; +#pragma clang diagnostic pop + } + }; + + std::atexit(restart); + CCDirector::get()->getActionManager()->addAction(CCSequence::create( + CCDelayTime::create(0.5f), + CCCallFunc::create(nullptr, callfunc_selector(Exit::shutdown)), + nullptr + ), CCDirector::get()->getRunningScene(), false); +} + #endif diff --git a/loader/src/platform/windows/LoaderImpl.cpp b/loader/src/platform/windows/LoaderImpl.cpp index fe70051c4..a3f632eb7 100644 --- a/loader/src/platform/windows/LoaderImpl.cpp +++ b/loader/src/platform/windows/LoaderImpl.cpp @@ -101,7 +101,7 @@ void ipcPipeThread(HANDLE pipe) { } void Loader::Impl::setupIPC() { - std::thread([]() { + std::thread ipcThread([]() { while (true) { auto pipe = CreateNamedPipeA( IPC_PIPE_NAME, @@ -125,14 +125,18 @@ void Loader::Impl::setupIPC() { // log::debug("Waiting for pipe connections"); if (ConnectNamedPipe(pipe, nullptr)) { // log::debug("Got connection, creating thread"); - std::thread(&ipcPipeThread, pipe).detach(); + std::thread pipeThread(&ipcPipeThread, pipe); + SetThreadDescription(pipeThread.native_handle(), L"Geode IPC Pipe"); + pipeThread.detach(); } else { // log::debug("No connection, cleaning pipe"); CloseHandle(pipe); } } - }).detach(); + }); + SetThreadDescription(ipcThread.native_handle(), L"Geode Main IPC"); + ipcThread.detach(); log::debug("IPC set up"); } diff --git a/loader/src/platform/windows/ModImpl.cpp b/loader/src/platform/windows/ModImpl.cpp index 17784dd16..30edebba6 100644 --- a/loader/src/platform/windows/ModImpl.cpp +++ b/loader/src/platform/windows/ModImpl.cpp @@ -73,7 +73,7 @@ std::string getLastWinError() { } Result<> Mod::Impl::loadPlatformBinary() { - auto load = LoadLibraryW((m_tempDirName / m_info.binaryName()).wstring().c_str()); + auto load = LoadLibraryW((m_tempDirName / m_metadata.getBinaryName()).wstring().c_str()); if (load) { if (m_platformInfo) { delete m_platformInfo; diff --git a/loader/src/platform/windows/main.cpp b/loader/src/platform/windows/main.cpp index 99376181e..28f9979df 100644 --- a/loader/src/platform/windows/main.cpp +++ b/loader/src/platform/windows/main.cpp @@ -24,16 +24,7 @@ void updateGeode() { ghc::filesystem::exists(updatesDir / "GeodeUpdater.exe")) ghc::filesystem::rename(updatesDir / "GeodeUpdater.exe", workingDir / "GeodeUpdater.exe"); - wchar_t buffer[MAX_PATH]; - GetModuleFileNameW(nullptr, buffer, MAX_PATH); - const auto gdName = ghc::filesystem::path(buffer).filename().string(); - - // launch updater - const auto updaterPath = (workingDir / "GeodeUpdater.exe").string(); - ShellExecuteA(nullptr, "open", updaterPath.c_str(), gdName.c_str(), workingDir.string().c_str(), false); - - // quit gd before it can even start - exit(0); + utils::game::restart(); } int WINAPI gdMainHook(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow) { diff --git a/loader/src/platform/windows/util.cpp b/loader/src/platform/windows/util.cpp index 062d0b191..7ecb73221 100644 --- a/loader/src/platform/windows/util.cpp +++ b/loader/src/platform/windows/util.cpp @@ -158,4 +158,30 @@ ghc::filesystem::path dirs::getSaveDir() { return path; } +void geode::utils::game::restart() { + if (CCApplication::sharedApplication() && + (GameManager::get()->m_playLayer || GameManager::get()->m_levelEditorLayer)) { + log::error("Cannot restart in PlayLayer or LevelEditorLayer!"); + return; + } + + const auto workingDir = dirs::getGameDir(); + + wchar_t buffer[MAX_PATH]; + GetModuleFileNameW(nullptr, buffer, MAX_PATH); + const auto gdName = ghc::filesystem::path(buffer).filename().string(); + + // launch updater + const auto updaterPath = (workingDir / "GeodeUpdater.exe").string(); + ShellExecuteA(nullptr, "open", updaterPath.c_str(), gdName.c_str(), workingDir.string().c_str(), false); + + if (CCApplication::sharedApplication()) + // please forgive me.. + // manually set the closed flag + // TODO: actually call glfwSetWindowShouldClose + *reinterpret_cast(reinterpret_cast(CCEGLView::sharedOpenGLView()->getWindow()) + 0xa) = true; + else + exit(0); +} + #endif diff --git a/loader/src/ui/internal/GeodeUI.cpp b/loader/src/ui/internal/GeodeUI.cpp index 20ea64fe1..b7e1d3e90 100644 --- a/loader/src/ui/internal/GeodeUI.cpp +++ b/loader/src/ui/internal/GeodeUI.cpp @@ -14,19 +14,19 @@ void geode::openModsList() { } void geode::openIssueReportPopup(Mod* mod) { - if (mod->getModInfo().issues()) { + if (mod->getMetadata().getIssues()) { MDPopup::create( "Issue Report", - mod->getModInfo().issues().value().info + + mod->getMetadata().getIssues().value().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().issues().value().url ? "Open URL" : ""), + "OK", (mod->getMetadata().getIssues().value().url ? "Open URL" : ""), [mod](bool btn2) { if (btn2) { web::openLinkInBrowser( - mod->getModInfo().issues().value().url.value() + mod->getMetadata().getIssues().value().url.value() ); } } @@ -73,13 +73,9 @@ CCNode* geode::createDefaultLogo(CCSize const& size) { } CCNode* geode::createModLogo(Mod* mod, CCSize const& size) { - CCNode* spr = nullptr; - if (mod == Loader::get()->getModImpl()) { - spr = CCSprite::createWithSpriteFrameName("geode-logo.png"_spr); - } - else { - spr = CCSprite::create(fmt::format("{}/logo.png", mod->getID()).c_str()); - } + CCNode* spr = mod == Mod::get() ? + CCSprite::createWithSpriteFrameName("geode-logo.png"_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); @@ -87,9 +83,8 @@ CCNode* geode::createModLogo(Mod* mod, CCSize const& size) { } CCNode* geode::createIndexItemLogo(IndexItemHandle item, CCSize const& size) { - CCNode* spr = nullptr; auto logoPath = ghc::filesystem::absolute(item->getPath() / "logo.png"); - spr = CCSprite::create(logoPath.string().c_str()); + CCNode* spr = CCSprite::create(logoPath.string().c_str()); if (!spr) { spr = CCSprite::createWithSpriteFrameName("no-logo.png"_spr); } diff --git a/loader/src/ui/internal/info/DevProfilePopup.cpp b/loader/src/ui/internal/info/DevProfilePopup.cpp index c736f7be3..f5146e846 100644 --- a/loader/src/ui/internal/info/DevProfilePopup.cpp +++ b/loader/src/ui/internal/info/DevProfilePopup.cpp @@ -27,7 +27,7 @@ bool DevProfilePopup::setup(std::string const& developer) { // index mods for (auto& item : Index::get()->getItemsByDeveloper(developer)) { - if (Loader::get()->isModInstalled(item->getModInfo().id())) { + if (Loader::get()->isModInstalled(item->getMetadata().getID())) { continue; } auto cell = IndexItemCell::create( diff --git a/loader/src/ui/internal/info/ModInfoPopup.cpp b/loader/src/ui/internal/info/ModInfoPopup.cpp index 50853e25d..3b30236d4 100644 --- a/loader/src/ui/internal/info/ModInfoPopup.cpp +++ b/loader/src/ui/internal/info/ModInfoPopup.cpp @@ -11,24 +11,23 @@ #include #include #include -#include #include #include #include #include -#include #include #include #include #include #include +#include static constexpr int const TAG_CONFIRM_UNINSTALL = 5; static constexpr int const TAG_CONFIRM_UPDATE = 6; static constexpr int const TAG_DELETE_SAVEDATA = 7; static const CCSize LAYER_SIZE = {440.f, 290.f}; -bool ModInfoPopup::init(ModInfo const& info, ModListLayer* list) { +bool ModInfoPopup::init(ModMetadata const& metadata, ModListLayer* list) { m_noElasticity = true; m_layer = list; @@ -50,7 +49,7 @@ bool ModInfoPopup::init(ModInfo const& info, ModListLayer* list) { constexpr float logoSize = 40.f; constexpr float logoOffset = 10.f; - auto nameLabel = CCLabelBMFont::create(info.name().c_str(), "bigFont.fnt"); + auto nameLabel = CCLabelBMFont::create(metadata.getName().c_str(), "bigFont.fnt"); nameLabel->setAnchorPoint({ .0f, .5f }); nameLabel->limitLabelWidth(200.f, .7f, .1f); m_mainLayer->addChild(nameLabel, 2); @@ -58,7 +57,7 @@ bool ModInfoPopup::init(ModInfo const& info, ModListLayer* list) { auto logoSpr = this->createLogo({logoSize, logoSize}); m_mainLayer->addChild(logoSpr); - auto developerStr = "by " + info.developer(); + auto developerStr = "by " + metadata.getDeveloper(); auto developerLabel = CCLabelBMFont::create(developerStr.c_str(), "goldFont.fnt"); developerLabel->setScale(.5f); developerLabel->setAnchorPoint({.0f, .5f}); @@ -78,8 +77,7 @@ bool ModInfoPopup::init(ModInfo const& info, ModListLayer* list) { winSize.width / 2 - logoTitleWidth / 2 + logoSize + logoOffset, winSize.height / 2 + 105.f ); - auto versionLabel = CCLabelBMFont::create( - info.version().toString().c_str(), + auto versionLabel = CCLabelBMFont::create(metadata.getVersion().toString().c_str(), "bigFont.fnt" ); versionLabel->setAnchorPoint({ .0f, .5f }); @@ -94,7 +92,7 @@ bool ModInfoPopup::init(ModInfo const& info, ModListLayer* list) { this->setTouchEnabled(true); m_detailsArea = MDTextArea::create( - (info.details() ? info.details().value() : "### No description provided."), + (metadata.getDetails() ? metadata.getDetails().value() : "### No description provided."), { 350.f, 137.5f } ); m_detailsArea->setPosition( @@ -111,7 +109,7 @@ bool ModInfoPopup::init(ModInfo const& info, ModListLayer* list) { m_mainLayer->addChild(m_scrollbar); // changelog - if (info.changelog()) { + if (metadata.getChangelog()) { // m_changelogArea is only created if the changelog button is clicked // because changelogs can get really long and take a while to load @@ -142,7 +140,7 @@ bool ModInfoPopup::init(ModInfo const& info, ModListLayer* list) { m_buttonMenu->addChild(changelogBtn); } - // mod info + // mod metadata auto infoSpr = CCSprite::createWithSpriteFrameName("GJ_infoIcon_001.png"); infoSpr->setScale(.85f); @@ -151,7 +149,7 @@ bool ModInfoPopup::init(ModInfo const& info, ModListLayer* list) { m_buttonMenu->addChild(m_infoBtn); // repo button - if (info.repository()) { + if (metadata.getRepository()) { auto repoBtn = CCMenuItemSpriteExtra::create( CCSprite::createWithSpriteFrameName("github.png"_spr), this, @@ -162,7 +160,7 @@ bool ModInfoPopup::init(ModInfo const& info, ModListLayer* list) { } // support button - if (info.supportInfo()) { + if (metadata.getSupportInfo()) { auto supportBtn = CCMenuItemSpriteExtra::create( CCSprite::createWithSpriteFrameName("gift.png"_spr), this, @@ -188,30 +186,30 @@ bool ModInfoPopup::init(ModInfo const& info, ModListLayer* list) { void ModInfoPopup::onSupport(CCObject*) { MDPopup::create( - "Support " + this->getModInfo().name(), - this->getModInfo().supportInfo().value(), + "Support " + this->getMetadata().getName(), + this->getMetadata().getSupportInfo().value(), "OK" )->show(); } void ModInfoPopup::onRepository(CCObject*) { - web::openLinkInBrowser(this->getModInfo().repository().value()); + web::openLinkInBrowser(this->getMetadata().getRepository().value()); } void ModInfoPopup::onInfo(CCObject*) { - auto info = this->getModInfo(); + auto info = this->getMetadata(); FLAlertLayer::create( nullptr, - ("About " + info.name()).c_str(), + ("About " + info.getName()).c_str(), fmt::format( "ID: {}\n" "Version: {}\n" "Developer: {}\n" "Path: {}\n", - info.id(), - info.version().toString(), - info.developer(), - info.path().string() + info.getID(), + info.getVersion().toString(), + info.getDeveloper(), + info.getPath().string() ), "OK", nullptr, @@ -224,7 +222,7 @@ void ModInfoPopup::onChangelog(CCObject* sender) { auto winSize = CCDirector::get()->getWinSize(); if (!m_changelogArea) { - m_changelogArea = MDTextArea::create(this->getModInfo().changelog().value(), { 350.f, 137.5f }); + m_changelogArea = MDTextArea::create(this->getMetadata().getChangelog().value(), { 350.f, 137.5f }); m_changelogArea->setPosition( -5000.f, winSize.height / 2 - m_changelogArea->getScaledContentSize().height / 2 - 20.f ); @@ -288,12 +286,12 @@ LocalModInfoPopup::LocalModInfoPopup() bool LocalModInfoPopup::init(Mod* mod, ModListLayer* list) { - m_item = Index::get()->getMajorItem(mod->getModInfo().id()); + m_item = Index::get()->getMajorItem(mod->getMetadata().getID()); if (m_item) - m_installListener.setFilter(m_item->getModInfo().id()); + m_installListener.setFilter(m_item->getMetadata().getID()); m_mod = mod; - if (!ModInfoPopup::init(mod->getModInfo(), list)) return false; + if (!ModInfoPopup::init(mod->getMetadata(), list)) return false; auto winSize = CCDirector::sharedDirector()->getWinSize(); @@ -344,10 +342,9 @@ bool LocalModInfoPopup::init(Mod* mod, ModListLayer* list) { disableBtnSpr->setColor({150, 150, 150}); } - if (mod != Loader::get()->getModImpl()) { - auto uninstallBtnSpr = ButtonSprite::create( - "Uninstall", "bigFont.fnt", "GJ_button_05.png", .6f - ); + if (mod != Mod::get()) { + auto uninstallBtnSpr = + ButtonSprite::create("Uninstall", "bigFont.fnt", "GJ_button_05.png", .6f); uninstallBtnSpr->setScale(.6f); auto uninstallBtn = CCMenuItemSpriteExtra::create( @@ -376,16 +373,16 @@ bool LocalModInfoPopup::init(Mod* mod, ModListLayer* list) { m_mainLayer->addChild(m_installStatus); auto minorIndexItem = Index::get()->getItem( - mod->getModInfo().id(), - ComparableVersionInfo(mod->getModInfo().version(), VersionCompare::MoreEq) + mod->getMetadata().getID(), + ComparableVersionInfo(mod->getMetadata().getVersion(), VersionCompare::MoreEq) ); // TODO: use column layout here? - if (m_item->getModInfo().version().getMajor() > minorIndexItem->getModInfo().version().getMajor()) { + if (m_item->getMetadata().getVersion().getMajor() > minorIndexItem->getMetadata().getVersion().getMajor()) { // has major update m_latestVersionLabel = CCLabelBMFont::create( - ("Available: " + m_item->getModInfo().version().toString()).c_str(), + ("Available: " + m_item->getMetadata().getVersion().toString()).c_str(), "bigFont.fnt" ); m_latestVersionLabel->setScale(.35f); @@ -395,10 +392,10 @@ bool LocalModInfoPopup::init(Mod* mod, ModListLayer* list) { m_mainLayer->addChild(m_latestVersionLabel); } - if (minorIndexItem->getModInfo().version() > mod->getModInfo().version()) { + if (minorIndexItem->getMetadata().getVersion() > mod->getMetadata().getVersion()) { // has minor update m_minorVersionLabel = CCLabelBMFont::create( - ("Available: " + minorIndexItem->getModInfo().version().toString()).c_str(), + ("Available: " + minorIndexItem->getMetadata().getVersion().toString()).c_str(), "bigFont.fnt" ); m_minorVersionLabel->setScale(.35f); @@ -429,7 +426,7 @@ bool LocalModInfoPopup::init(Mod* mod, ModListLayer* list) { } // issue report button - if (mod->getModInfo().issues()) { + if (mod->getMetadata().getIssues()) { auto issuesBtnSpr = ButtonSprite::create( "Report an Issue", "goldFont.fnt", "GJ_button_04.png", .8f ); @@ -449,8 +446,8 @@ CCNode* LocalModInfoPopup::createLogo(CCSize const& size) { return geode::createModLogo(m_mod, size); } -ModInfo LocalModInfoPopup::getModInfo() const { - return m_mod->getModInfo(); +ModMetadata LocalModInfoPopup::getMetadata() const { + return m_mod->getMetadata(); } void LocalModInfoPopup::onIssues(CCObject*) { @@ -464,7 +461,7 @@ void LocalModInfoPopup::onUpdateProgress(ModInstallEvent* event) { FLAlertLayer::create( "Update complete", - "Mod succesfully updated! :) " + "Mod successfully updated! :) " "(You may need to restart the game " "for the mod to take full effect)", "OK" @@ -516,8 +513,8 @@ void LocalModInfoPopup::onUpdate(CCObject*) { [](IndexItemHandle handle) { return fmt::format( " - {} ({})", - handle->getModInfo().name(), - handle->getModInfo().id() + handle->getMetadata().getName(), + handle->getMetadata().getID() ); } ), @@ -577,9 +574,9 @@ void LocalModInfoPopup::onEnableMod(CCObject* sender) { )->show(); } if (as(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 { @@ -589,7 +586,7 @@ void LocalModInfoPopup::onEnableMod(CCObject* sender) { } } if (m_layer) { - m_layer->updateAllStates(nullptr); + m_layer->updateAllStates(); } as(sender)->toggle(m_mod->isEnabled()); } @@ -651,12 +648,12 @@ void LocalModInfoPopup::doUninstall() { auto layer = FLAlertLayer::create( this, "Uninstall complete", - "Mod was succesfully uninstalled! :) " + "Mod was successfully 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", + "Keep", "Delete", 350.f ); @@ -684,11 +681,13 @@ IndexItemInfoPopup::IndexItemInfoPopup() bool IndexItemInfoPopup::init(IndexItemHandle item, ModListLayer* list) { m_item = item; - m_installListener.setFilter(m_item->getModInfo().id()); + m_installListener.setFilter(m_item->getMetadata().getID()); auto winSize = CCDirector::sharedDirector()->getWinSize(); - if (!ModInfoPopup::init(item->getModInfo(), list)) return false; + if (!ModInfoPopup::init(item->getMetadata(), list)) return false; + + if (item->isInstalled()) return true; m_installBtnSpr = IconButtonSprite::create( "GE_button_01.png"_spr, @@ -719,7 +718,7 @@ void IndexItemInfoPopup::onInstallProgress(ModInstallEvent* event) { FLAlertLayer::create( "Install complete", - "Mod succesfully installed! :) " + "Mod successfully installed! :) " "(You may need to restart the game " "for the mod to take full effect)", "OK" @@ -751,43 +750,37 @@ void IndexItemInfoPopup::onInstallProgress(ModInstallEvent* event) { } void IndexItemInfoPopup::onInstall(CCObject*) { - auto list = Index::get()->getInstallList(m_item); - if (!list) { - return FLAlertLayer::create( - "Unable to Install", - list.unwrapErr(), - "OK" - )->show(); - } - FLAlertLayer::create( - this, + createQuickPopup( "Confirm Install", - fmt::format( - "The following mods will be installed:\n {}", - // le nest - ranges::join( - ranges::map>( - list.unwrap().list, - [](IndexItemHandle handle) { - return fmt::format( - " - {} ({})", - handle->getModInfo().name(), - handle->getModInfo().id() - ); - } - ), - "\n " - ) - ), - "Cancel", "OK" - )->show(); -} - -void IndexItemInfoPopup::onCancel(CCObject*) { - Index::get()->cancelInstall(m_item); + "Installing this mod requires a few other mods to be installed. " + "Would you like to continue with recommended settings or " + "customize which mods to install?", + "Customize", "Recommended", 320.f, + [&](FLAlertLayer*, bool btn2) { + if (btn2) { + auto canInstall = Index::get()->canInstall(m_item); + if (!canInstall) { + FLAlertLayer::create( + "Unable to Install", + canInstall.unwrapErr(), + "OK" + )->show(); + return; + } + this->preInstall(); + Index::get()->install(m_item); + } + else { + InstallListPopup::create(m_item, [&](IndexInstallList const& list) { + this->preInstall(); + Index::get()->install(list); + })->show(); + } + }, true, true + ); } -void IndexItemInfoPopup::doInstall() { +void IndexItemInfoPopup::preInstall() { if (m_latestVersionLabel) { m_latestVersionLabel->setVisible(false); } @@ -798,22 +791,18 @@ void IndexItemInfoPopup::doInstall() { ); m_installBtnSpr->setString("Cancel"); m_installBtnSpr->setBG("GJ_button_06.png", false); - - Index::get()->install(m_item); } -void IndexItemInfoPopup::FLAlert_Clicked(FLAlertLayer*, bool btn2) { - if (btn2) { - this->doInstall(); - } +void IndexItemInfoPopup::onCancel(CCObject*) { + Index::get()->cancelInstall(m_item); } CCNode* IndexItemInfoPopup::createLogo(CCSize const& size) { return geode::createIndexItemLogo(m_item, size); } -ModInfo IndexItemInfoPopup::getModInfo() const { - return m_item->getModInfo(); +ModMetadata IndexItemInfoPopup::getMetadata() const { + return m_item->getMetadata(); } IndexItemInfoPopup* IndexItemInfoPopup::create( diff --git a/loader/src/ui/internal/info/ModInfoPopup.hpp b/loader/src/ui/internal/info/ModInfoPopup.hpp index f1ec3dd71..062230a0b 100644 --- a/loader/src/ui/internal/info/ModInfoPopup.hpp +++ b/loader/src/ui/internal/info/ModInfoPopup.hpp @@ -44,7 +44,7 @@ class ModInfoPopup : public FLAlertLayer { void onSupport(CCObject*); void onInfo(CCObject*); - bool init(ModInfo const& info, ModListLayer* list); + bool init(ModMetadata const& metadata, ModListLayer* list); void keyDown(cocos2d::enumKeyCodes) override; void onClose(cocos2d::CCObject*); @@ -52,7 +52,7 @@ class ModInfoPopup : public FLAlertLayer { void setInstallStatus(std::optional const& progress); virtual CCNode* createLogo(CCSize const& size) = 0; - virtual ModInfo getModInfo() const = 0; + virtual ModMetadata getMetadata() const = 0; }; class LocalModInfoPopup : public ModInfoPopup, public FLAlertLayerProtocol { @@ -62,7 +62,7 @@ class LocalModInfoPopup : public ModInfoPopup, public FLAlertLayerProtocol { Mod* m_mod; bool init(Mod* mod, ModListLayer* list); - + void onIssues(CCObject*); void onSettings(CCObject*); void onNoSettings(CCObject*); @@ -81,7 +81,7 @@ class LocalModInfoPopup : public ModInfoPopup, public FLAlertLayerProtocol { void FLAlert_Clicked(FLAlertLayer*, bool) override; CCNode* createLogo(CCSize const& size) override; - ModInfo getModInfo() const override; + ModMetadata getMetadata() const override; LocalModInfoPopup(); @@ -89,22 +89,21 @@ class LocalModInfoPopup : public ModInfoPopup, public FLAlertLayerProtocol { static LocalModInfoPopup* create(Mod* mod, ModListLayer* list); }; -class IndexItemInfoPopup : public ModInfoPopup, public FLAlertLayerProtocol { +class IndexItemInfoPopup : public ModInfoPopup { protected: IndexItemHandle m_item; EventListener m_installListener; bool init(IndexItemHandle item, ModListLayer* list); - + void onInstallProgress(ModInstallEvent* event); void onInstall(CCObject*); void onCancel(CCObject*); - void doInstall(); - void FLAlert_Clicked(FLAlertLayer*, bool) override; + void preInstall(); CCNode* createLogo(CCSize const& size) override; - ModInfo getModInfo() const override; + ModMetadata getMetadata() const override; IndexItemInfoPopup(); diff --git a/loader/src/ui/internal/list/InstallListCell.cpp b/loader/src/ui/internal/list/InstallListCell.cpp new file mode 100644 index 000000000..0ba4be3ec --- /dev/null +++ b/loader/src/ui/internal/list/InstallListCell.cpp @@ -0,0 +1,319 @@ +#include "InstallListCell.hpp" +#include "InstallListPopup.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include "../info/TagNode.hpp" +#include "../info/DevProfilePopup.hpp" + +// InstallListCell + +void InstallListCell::draw() { + reinterpret_cast(this)->StatsCell::draw(); +} + +float InstallListCell::getLogoSize() const { + return m_height / 1.5f; +} + +void InstallListCell::setupInfo( + std::string name, + std::optional developer, + std::variant version, + bool inactive +) { + m_menu = CCMenu::create(); + m_menu->setPosition(m_width - 10.f, m_height / 2); + this->addChild(m_menu); + + auto logoSize = this->getLogoSize(); + + auto logoSpr = this->createLogo({ logoSize, logoSize }); + logoSpr->setPosition({ logoSize / 2 + 12.f, m_height / 2 }); + auto logoSprColor = typeinfo_cast(logoSpr); + if (inactive && logoSprColor) { + logoSprColor->setColor({ 163, 163, 163 }); + } + this->addChild(logoSpr); + + auto titleLabel = CCLabelBMFont::create(name.c_str(), "bigFont.fnt"); + titleLabel->setAnchorPoint({ .0f, .5f }); + titleLabel->setPositionX(m_height / 2 + logoSize / 2 + 13.f); + titleLabel->setPositionY(m_height / 2); + titleLabel->limitLabelWidth(m_width / 2 - 70.f, .4f, .1f); + if (inactive) { + titleLabel->setColor({ 163, 163, 163 }); + } + this->addChild(titleLabel); + + m_developerBtn = nullptr; + if (developer) { + auto creatorStr = "by " + *developer; + auto creatorLabel = CCLabelBMFont::create(creatorStr.c_str(), "goldFont.fnt"); + creatorLabel->setScale(.34f); + if (inactive) { + creatorLabel->setColor({ 163, 163, 163 }); + } + + m_developerBtn = CCMenuItemSpriteExtra::create( + creatorLabel, this, menu_selector(InstallListCell::onViewDev) + ); + m_developerBtn->setPosition( + titleLabel->getPositionX() + titleLabel->getScaledContentSize().width + 3.f + + creatorLabel->getScaledContentSize().width / 2 - + m_menu->getPositionX(), + -0.5f + ); + m_menu->addChild(m_developerBtn); + } + + auto versionLabel = CCLabelBMFont::create( + std::holds_alternative(version) ? + std::get(version).toString(false).c_str() : + std::get(version).toString().c_str(), + "bigFont.fnt" + ); + versionLabel->setAnchorPoint({ .0f, .5f }); + versionLabel->setScale(.2f); + versionLabel->setPosition( + titleLabel->getPositionX() + titleLabel->getScaledContentSize().width + 3.f + + (m_developerBtn ? m_developerBtn->getScaledContentSize().width + 3.f : 0.f), + titleLabel->getPositionY() - 1.f + ); + versionLabel->setColor({ 0, 255, 0 }); + if (inactive) { + versionLabel->setColor({ 0, 163, 0 }); + } + this->addChild(versionLabel); + + if (!std::holds_alternative(version)) return; + if (auto tag = std::get(version).getTag()) { + auto tagLabel = TagNode::create(tag->toString()); + tagLabel->setAnchorPoint({.0f, .5f}); + tagLabel->setScale(.2f); + tagLabel->setPosition( + versionLabel->getPositionX() + versionLabel->getScaledContentSize().width + 3.f, + versionLabel->getPositionY() + ); + this->addChild(tagLabel); + } +} + +void InstallListCell::setupInfo(ModMetadata const& metadata, bool inactive) { + this->setupInfo(metadata.getName(), metadata.getDeveloper(), metadata.getVersion(), inactive); +} + +void InstallListCell::onViewDev(CCObject*) { + DevProfilePopup::create(getDeveloper())->show(); +} + +bool InstallListCell::init(InstallListPopup* list, CCSize const& size) { + m_width = size.width; + m_height = size.height; + m_layer = list; + this->setContentSize(size); + this->setID("install-list-cell"); + return true; +} + +bool InstallListCell::isIncluded() { + return m_toggle && m_toggle->isOn(); +} + +// ModInstallListCell + +bool ModInstallListCell::init(Mod* mod, InstallListPopup* list, CCSize const& size) { + if (!InstallListCell::init(list, size)) + return false; + m_mod = mod; + this->setupInfo(mod->getMetadata(), true); + auto message = CCLabelBMFont::create("Installed", "bigFont.fnt"); + message->setAnchorPoint({ 1.f, .5f }); + message->setPositionX(m_menu->getPositionX()); + message->setPositionY(16.f); + message->setScale(0.4f); + message->setColor({ 163, 163, 163 }); + this->addChild(message); + return true; +} + +ModInstallListCell* ModInstallListCell::create(Mod* mod, InstallListPopup* list, CCSize const& size) { + auto ret = new ModInstallListCell(); + if (ret->init(mod, list, size)) { + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +CCNode* ModInstallListCell::createLogo(CCSize const& size) { + return geode::createModLogo(m_mod, size); +} +std::string ModInstallListCell::getID() const { + return m_mod->getID(); +} +std::string ModInstallListCell::getDeveloper() const { + return m_mod->getDeveloper(); +} + +// IndexItemInstallListCell + +bool IndexItemInstallListCell::init( + IndexItemHandle item, + ModMetadata::Dependency::Importance importance, + InstallListPopup* list, + CCSize const& size, + std::optional selected +) { + if (!InstallListCell::init(list, size)) + return false; + m_item = item; + this->setupInfo(item->getMetadata(), item->isInstalled()); + if (item->isInstalled()) { + auto message = CCLabelBMFont::create("Installed", "bigFont.fnt"); + message->setAnchorPoint({ 1.f, .5f }); + message->setPositionX(m_menu->getPositionX()); + message->setPositionY(16.f); + message->setScale(0.4f); + message->setColor({ 163, 163, 163 }); + this->addChild(message); + return true; + } + + m_toggle = CCMenuItemToggler::createWithStandardSprites( + m_layer, + menu_selector(InstallListPopup::onCellToggle), + .6f + ); + m_toggle->setPosition(-m_toggle->getScaledContentSize().width / 2, 0.f); + + switch (importance) { + case ModMetadata::Dependency::Importance::Required: + m_toggle->setClickable(false); + m_toggle->toggle(true); + break; + case ModMetadata::Dependency::Importance::Recommended: + m_toggle->setClickable(true); + m_toggle->toggle(true); + break; + case ModMetadata::Dependency::Importance::Suggested: + m_toggle->setClickable(true); + m_toggle->toggle(false); + break; + } + + if (m_item->getAvailablePlatforms().count(GEODE_PLATFORM_TARGET) == 0) { + m_toggle->setClickable(false); + m_toggle->toggle(false); + + auto message = CCLabelBMFont::create("N/A", "bigFont.fnt"); + message->setAnchorPoint({ 1.f, .5f }); + message->setPositionX(m_menu->getPositionX() - m_toggle->getScaledContentSize().width - 5.f); + message->setPositionY(16.f); + message->setScale(0.4f); + message->setColor({ 240, 31, 31 }); + this->addChild(message); + + if (importance != ModMetadata::Dependency::Importance::Required) { + message->setCString("N/A (Optional)"); + message->setColor({ 163, 24, 24 }); + } + } + + if (m_toggle->m_notClickable) { + m_toggle->m_offButton->setOpacity(100); + m_toggle->m_offButton->setColor(cc3x(155)); + m_toggle->m_onButton->setOpacity(100); + m_toggle->m_onButton->setColor(cc3x(155)); + } + + if (!m_toggle->m_notClickable && selected) { + m_toggle->toggle(*selected); + } + + m_menu->addChild(m_toggle); + return true; +} + +IndexItemInstallListCell* IndexItemInstallListCell::create( + IndexItemHandle item, + ModMetadata::Dependency::Importance importance, + InstallListPopup* list, + CCSize const& size, + std::optional selected +) { + auto ret = new IndexItemInstallListCell(); + if (ret->init(std::move(item), importance, list, size, selected)) { + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +CCNode* IndexItemInstallListCell::createLogo(CCSize const& size) { + return geode::createIndexItemLogo(m_item, size); +} +std::string IndexItemInstallListCell::getID() const { + return m_item->getMetadata().getID(); +} +std::string IndexItemInstallListCell::getDeveloper() const { + return m_item->getMetadata().getDeveloper(); +} + +IndexItemHandle IndexItemInstallListCell::getItem() { + return m_item; +} + +// UnknownInstallListCell + +bool UnknownInstallListCell::init( + ModMetadata::Dependency const& dependency, + InstallListPopup* list, + CCSize const& size +) { + if (!InstallListCell::init(list, size)) + return false; + m_dependency = dependency; + bool optional = dependency.importance != ModMetadata::Dependency::Importance::Required; + this->setupInfo(dependency.id, std::nullopt, dependency.version, optional); + auto message = CCLabelBMFont::create("Missing", "bigFont.fnt"); + message->setAnchorPoint({ 1.f, .5f }); + message->setPositionX(m_menu->getPositionX()); + message->setPositionY(16.f); + message->setScale(0.4f); + message->setColor({ 240, 31, 31 }); + if (optional) { + message->setCString("Missing (Optional)"); + message->setColor({ 163, 24, 24 }); + } + this->addChild(message); + return true; +} + +UnknownInstallListCell* UnknownInstallListCell::create( + ModMetadata::Dependency const& dependency, + InstallListPopup* list, + CCSize const& size +) { + auto ret = new UnknownInstallListCell(); + if (ret->init(dependency, list, size)) { + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +CCNode* UnknownInstallListCell::createLogo(CCSize const& size) { + return geode::createDefaultLogo(size); +} +std::string UnknownInstallListCell::getID() const { + return m_dependency.id; +} +std::string UnknownInstallListCell::getDeveloper() const { + return ""; +} diff --git a/loader/src/ui/internal/list/InstallListCell.hpp b/loader/src/ui/internal/list/InstallListCell.hpp new file mode 100644 index 000000000..582ca24d6 --- /dev/null +++ b/loader/src/ui/internal/list/InstallListCell.hpp @@ -0,0 +1,114 @@ +#pragma once + +#include +#include +#include +#include +#include + +using namespace geode::prelude; + +class InstallListPopup; + +/** + * Base class for install list items + */ +class InstallListCell : public CCLayer { +protected: + float m_width; + float m_height; + InstallListPopup* m_layer; + CCMenu* m_menu; + CCMenuItemSpriteExtra* m_developerBtn; + CCMenuItemToggler* m_toggle = nullptr; + + void setupInfo( + std::string name, + std::optional developer, + std::variant version, + bool inactive + ); + + bool init(InstallListPopup* list, CCSize const& size); + void setupInfo(ModMetadata const& metadata, bool inactive); + void draw() override; + + float getLogoSize() const; + void onViewDev(CCObject*); + +public: + bool isIncluded(); + + virtual CCNode* createLogo(CCSize const& size) = 0; + [[nodiscard]] virtual std::string getID() const = 0; + [[nodiscard]] virtual std::string getDeveloper() const = 0; +}; + +/** + * Install list item for a mod + */ +class ModInstallListCell : public InstallListCell { +protected: + Mod* m_mod; + + bool init(Mod* mod, InstallListPopup* list, CCSize const& size); + +public: + static ModInstallListCell* create(Mod* mod, InstallListPopup* list, CCSize const& size); + + CCNode* createLogo(CCSize const& size) override; + [[nodiscard]] std::string getID() const override; + [[nodiscard]] std::string getDeveloper() const override; +}; + +/** + * Install list item for an index item + */ +class IndexItemInstallListCell : public InstallListCell { +protected: + IndexItemHandle m_item; + + bool init( + IndexItemHandle item, + ModMetadata::Dependency::Importance importance, + InstallListPopup* list, + CCSize const& size, + std::optional selected + ); + +public: + static IndexItemInstallListCell* create( + IndexItemHandle item, + ModMetadata::Dependency::Importance importance, + InstallListPopup* list, + CCSize const& size, + std::optional selected + ); + + CCNode* createLogo(CCSize const& size) override; + [[nodiscard]] std::string getID() const override; + [[nodiscard]] std::string getDeveloper() const override; + + IndexItemHandle getItem(); +}; + +/** + * Install list item for an unknown item + */ +class UnknownInstallListCell : public InstallListCell { +protected: + ModMetadata::Dependency m_dependency; + + bool init(ModMetadata::Dependency const& dependency, InstallListPopup* list, CCSize const& size); + +public: + static UnknownInstallListCell* create( + ModMetadata::Dependency const& dependency, + InstallListPopup* list, + CCSize const& size + ); + + CCNode* createLogo(CCSize const& size) override; + [[nodiscard]] std::string getID() const override; + [[nodiscard]] std::string getDeveloper() const override; +}; diff --git a/loader/src/ui/internal/list/InstallListPopup.cpp b/loader/src/ui/internal/list/InstallListPopup.cpp new file mode 100644 index 000000000..fa639355b --- /dev/null +++ b/loader/src/ui/internal/list/InstallListPopup.cpp @@ -0,0 +1,229 @@ +#include "InstallListPopup.hpp" +#include "InstallListCell.hpp" + +#include +#include + +bool InstallListPopup::setup(IndexItemHandle item, MiniFunction callback) { + m_noElasticity = true; + + m_item = item; + m_callback = callback; + + this->setTitle("Select Mods to Install"); + + this->createList(); + + auto installBtnSpr = IconButtonSprite::create( + "GE_button_01.png"_spr, + CCSprite::createWithSpriteFrameName("install.png"_spr), + "Install", + "bigFont.fnt" + ); + installBtnSpr->setScale(.6f); + + auto installBtn = CCMenuItemSpriteExtra::create( + installBtnSpr, + this, + menu_selector(InstallListPopup::onInstall) + ); + installBtn->setPositionY(-m_bgSprite->getScaledContentSize().height / 2 + 22.f); + m_buttonMenu->addChild(installBtn); + + return true; +} + +void InstallListPopup::createList() { + auto winSize = CCDirector::sharedDirector()->getWinSize(); + + std::unordered_map oldCells; + bool oldScrollAtBottom; + std::optional oldScroll; + if (m_list) { + CCArray* oldEntries = m_list->m_entries; + for (size_t i = 0; i < oldEntries->count(); i++) { + auto* itemCell = typeinfo_cast(oldEntries->objectAtIndex(i)); + oldCells[itemCell->getID()] = itemCell; + } + auto content = m_list->m_tableView->m_contentLayer; + oldScroll = content->getPositionY(); + oldScrollAtBottom = oldScroll >= 0.f; + if (!oldScrollAtBottom) + *oldScroll += content->getScaledContentSize().height; + m_list->removeFromParent(); + } + if (m_listParent) { + m_listParent->removeFromParent(); + } + + m_listParent = CCNode::create(); + m_mainLayer->addChild(m_listParent); + + auto items = this->createCells(oldCells); + m_list = ListView::create( + items, + this->getCellSize().height, + this->getListSize().width, + this->getListSize().height + ); + m_list->setPosition(winSize / 2 - m_list->getScaledContentSize() / 2); + m_listParent->addChild(m_list); + + // restore scroll on list recreation + // it's stored from the top unless was scrolled all the way to the bottom + if (oldScroll) { + auto content = m_list->m_tableView->m_contentLayer; + if (oldScrollAtBottom) + content->setPositionY(*oldScroll); + else + content->setPositionY(*oldScroll - content->getScaledContentSize().height); + } + + addListBorders(m_listParent, winSize / 2, m_list->getScaledContentSize()); +} + +CCArray* InstallListPopup::createCells(std::unordered_map const& oldCells) { + std::vector top; + std::vector middle; + std::vector bottom; + + std::queue queue; + std::unordered_set queued; + + auto id = m_item->getMetadata().getID(); + middle.push_back(IndexItemInstallListCell::create( + m_item, + ModMetadata::Dependency::Importance::Required, + this, + this->getCellSize(), + oldCells.contains(id) ? std::make_optional(oldCells.at(id)->isIncluded()) : std::nullopt + )); + for (auto const& dep : m_item->getMetadata().getDependencies()) { + queue.push(dep); + } + + auto index = Index::get(); + while (!queue.empty()) { + auto const& item = queue.front(); + if (queued.contains(item.id)) { + queue.pop(); + continue; + } + queued.insert(item.id); + + // installed + if (item.mod && !item.mod->isUninstalled()) { + bottom.push_back(ModInstallListCell::create(item.mod, this, this->getCellSize())); + for (auto const& dep : item.mod->getMetadata().getDependencies()) { + queue.push(dep); + } + queue.pop(); + continue; + } + + // on index + if (auto depItem = index->getItem(item.id, item.version)) { + auto cell = IndexItemInstallListCell::create( + depItem, + item.importance, + this, + this->getCellSize(), + oldCells.contains(item.id) ? + std::make_optional(oldCells.at(item.id)->isIncluded()) : + std::nullopt + ); + + // put missing dependencies at the top + if (depItem->getAvailablePlatforms().count(GEODE_PLATFORM_TARGET) == 0) + top.push_back(cell); + // put installed dependencies at the bottom + else if (depItem->isInstalled()) + bottom.push_back(cell); + else + middle.push_back(cell); + + if (!cell->isIncluded()) { + queue.pop(); + continue; + } + + for (auto const& dep : depItem->getMetadata().getDependencies()) { + queue.push(dep); + } + queue.pop(); + continue; + } + + // unknown (aka not installed and missing from index) + auto unknownCell = UnknownInstallListCell::create(item, this, this->getCellSize()); + top.push_back(unknownCell); + queue.pop(); + } + + auto mods = CCArray::create(); + for (auto const& item : top) { + mods->addObject(item); + } + for (auto const& item : middle) { + mods->addObject(item); + } + for (auto const& item : bottom) { + mods->addObject(item); + } + + return mods; +} + +// Getters + +CCSize InstallListPopup::getListSize() const { + return { 340.f, 170.f }; +} + +CCSize InstallListPopup::getCellSize() const { + return { getListSize().width, 30.f }; +} + +// Callbacks + +void InstallListPopup::onCellToggle(cocos2d::CCObject* obj) { + auto* toggler = typeinfo_cast(obj); + if (toggler && !toggler->m_notClickable) + toggler->toggle(!toggler->isOn()); + this->createList(); +} + +void InstallListPopup::onInstall(cocos2d::CCObject* obj) { + this->onBtn2(obj); + if (!m_callback) + return; + + IndexInstallList list; + list.target = m_item; + + CCArray* entries = m_list->m_entries; + for (size_t i = entries->count(); i > 0; i--) { + auto* itemCell = typeinfo_cast(entries->objectAtIndex(i - 1)); + if (!itemCell || !itemCell->isIncluded()) + continue; + IndexItemHandle item = itemCell->getItem(); + list.list.push_back(item); + } + + m_callback(list); +} + +// Static + +InstallListPopup* InstallListPopup::create( + IndexItemHandle item, + MiniFunction onInstall +) { + auto ret = new InstallListPopup(); + if (!ret->init(380.f, 250.f, std::move(item), std::move(onInstall))) { + CC_SAFE_DELETE(ret); + return nullptr; + } + ret->autorelease(); + return ret; +} diff --git a/loader/src/ui/internal/list/InstallListPopup.hpp b/loader/src/ui/internal/list/InstallListPopup.hpp new file mode 100644 index 000000000..7802bccb5 --- /dev/null +++ b/loader/src/ui/internal/list/InstallListPopup.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +#include "InstallListCell.hpp" + +using namespace geode::prelude; + +class InstallListPopup : public Popup> { +protected: + IndexItemHandle m_item; + CCNode* m_listParent; + ListView* m_list; + MiniFunction m_callback; + + bool setup(IndexItemHandle item, MiniFunction callback) override; + + void createList(); + CCArray* createCells(std::unordered_map const& oldCells); + CCSize getCellSize() const; + CCSize getListSize() const; + + void onInstall(CCObject* obj); + +public: + void onCellToggle(CCObject* obj); + + static InstallListPopup* create(IndexItemHandle item, MiniFunction onInstall); +}; diff --git a/loader/src/ui/internal/list/ModListCell.cpp b/loader/src/ui/internal/list/ModListCell.cpp index eaa237842..ba48fb89e 100644 --- a/loader/src/ui/internal/list/ModListCell.cpp +++ b/loader/src/ui/internal/list/ModListCell.cpp @@ -1,4 +1,3 @@ - #include "ModListCell.hpp" #include "ModListLayer.hpp" #include "../info/ModInfoPopup.hpp" @@ -11,6 +10,7 @@ #include #include "../info/TagNode.hpp" #include "../info/DevProfilePopup.hpp" +#include "ProblemsListPopup.hpp" template static bool tryOrAlert(Result const& res, char const* title) { @@ -29,9 +29,10 @@ float ModListCell::getLogoSize() const { } void ModListCell::setupInfo( - ModInfo const& info, + 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,13 +42,17 @@ void ModListCell::setupInfo( auto logoSpr = this->createLogo({ logoSize, logoSize }); logoSpr->setPosition({ logoSize / 2 + 12.f, m_height / 2 }); + auto logoSprColor = typeinfo_cast(logoSpr); + if (inactive && logoSprColor) { + logoSprColor->setColor({ 163, 163, 163 }); + } this->addChild(logoSpr); bool hasDesc = display == ModListDisplay::Expanded && - info.description().has_value(); + metadata.getDescription().has_value(); - auto titleLabel = CCLabelBMFont::create(info.name().c_str(), "bigFont.fnt"); + auto titleLabel = CCLabelBMFont::create(metadata.getName().c_str(), "bigFont.fnt"); titleLabel->setAnchorPoint({ .0f, .5f }); titleLabel->setPositionX(m_height / 2 + logoSize / 2 + 13.f); if (hasDesc && spaceForTags) { @@ -63,10 +68,13 @@ 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( - info.version().toString(false).c_str(), + metadata.getVersion().toString(false).c_str(), "bigFont.fnt" ); versionLabel->setAnchorPoint({ .0f, .5f }); @@ -76,23 +84,52 @@ 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 = info.version().getTag()) { + TagNode* apiLabel = nullptr; + if (metadata.isAPI()) { + apiLabel = TagNode::create("API"); + apiLabel->setAnchorPoint({ .0f, .5f }); + apiLabel->setScale(.3f); + apiLabel->setPosition( + versionLabel->getPositionX() + + versionLabel->getScaledContentSize().width + 5.f, + versionLabel->getPositionY() + ); + } + + if (auto tag = metadata.getVersion().getTag()) { auto tagLabel = TagNode::create(tag.value().toString().c_str()); tagLabel->setAnchorPoint({ .0f, .5f }); tagLabel->setScale(.3f); tagLabel->setPosition( - versionLabel->getPositionX() + + versionLabel->getPositionX() + versionLabel->getScaledContentSize().width + 5.f, versionLabel->getPositionY() ); this->addChild(tagLabel); + + if (apiLabel) { + apiLabel->setPosition( + tagLabel->getPositionX() + + tagLabel->getScaledContentSize().width + 5.f, + tagLabel->getPositionY() + ); + } } - auto creatorStr = "by " + info.developer(); + if (apiLabel) + this->addChild(apiLabel); + + 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) @@ -129,10 +166,13 @@ void ModListCell::setupInfo( descBG->setScale(.25f); this->addChild(descBG); - m_description = CCLabelBMFont::create(info.description().value().c_str(), "chatFont.fnt"); + m_description = CCLabelBMFont::create(metadata.getDescription().value().c_str(), "chatFont.fnt"); 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,30 +227,25 @@ 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(); + } + }); } 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.id, dep.version.toString() - ); - } - info.pop_back(); - info.pop_back(); - FLAlertLayer::create(nullptr, "Unresolved Dependencies", info, "OK", nullptr, 400.f)->show(); + ProblemsListPopup::create(m_mod)->show(); } void ModCell::onInfo(CCObject*) { LocalModInfoPopup::create(m_mod, m_layer)->show(); } +void ModCell::onRestart(CCObject*) { + utils::game::restart(); +} + void ModCell::updateState() { bool unresolved = m_mod->hasUnresolvedDependencies(); if (m_enableToggle) { @@ -221,7 +256,16 @@ void ModCell::updateState() { m_enableToggle->m_onButton->setOpacity(unresolved ? 100 : 255); m_enableToggle->m_onButton->setColor(unresolved ? cc3x(155) : cc3x(255)); } - m_unresolvedExMark->setVisible(unresolved); + bool hasProblems = false; + for (auto const& item : Loader::get()->getProblems()) { + if (!std::holds_alternative(item.cause) || + std::get(item.cause) != m_mod || + item.type <= LoadProblem::Type::Recommendation) + continue; + hasProblems = true; + break; + } + m_unresolvedExMark->setVisible(hasProblems); } bool ModCell::init( @@ -232,18 +276,50 @@ bool ModCell::init( ) { if (!ModListCell::init(list, size)) return false; - m_mod = mod; - this->setupInfo(mod->getModInfo(), false, display); + 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); + + 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->wasSuccesfullyLoaded() && m_mod->supportsDisabling()) { + 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 +335,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 +354,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,15 +383,26 @@ bool IndexItemCell::init( m_item = item; - this->setupInfo(item->getModInfo(), item->getTags().size(), display); - - auto viewSpr = ButtonSprite::create( - "View", "bigFont.fnt", "GJ_button_01.png", .8f - ); - viewSpr->setScale(.65f); + bool justInstalled = item->isInstalled() && !Loader::get()->isModInstalled(item->getMetadata().getID()); - auto viewBtn = CCMenuItemSpriteExtra::create(viewSpr, this, menu_selector(IndexItemCell::onInfo)); - m_menu->addChild(viewBtn); + this->setupInfo(item->getMetadata(), item->getTags().size(), display, justInstalled); + + if (justInstalled) { + 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; @@ -355,7 +422,7 @@ bool IndexItemCell::init( x += node->getScaledContentSize().width + 5.f; } } - + this->updateState(); return true; @@ -364,7 +431,7 @@ bool IndexItemCell::init( void IndexItemCell::updateState() {} std::string IndexItemCell::getDeveloper() const { - return m_item->getModInfo().developer(); + return m_item->getMetadata().getDeveloper(); } CCNode* IndexItemCell::createLogo(CCSize const& size) { @@ -405,7 +472,6 @@ void InvalidGeodeFileCell::FLAlert_Clicked(FLAlertLayer*, bool btn2) { ) ->show(); } - Loader::get()->refreshModsList(); if (m_layer) { m_layer->reloadList(); } @@ -443,7 +509,7 @@ bool InvalidGeodeFileCell::init( pathLabel->setColor({ 255, 255, 0 }); this->addChild(pathLabel); - auto whySpr = ButtonSprite::create("Info", 0, 0, "bigFont.fnt", "GJ_button_01.png", 0, .8f); + auto whySpr = ButtonSprite::create("Info", 0, false, "bigFont.fnt", "GJ_button_01.png", 0, .8f); whySpr->setScale(.65f); auto viewBtn = @@ -477,3 +543,112 @@ std::string InvalidGeodeFileCell::getDeveloper() const { CCNode* InvalidGeodeFileCell::createLogo(CCSize const& size) { return nullptr; } + +// ProblemsCell + +void ProblemsCell::onInfo(CCObject*) { + ProblemsListPopup::create(nullptr)->show(); +} + +bool ProblemsCell::init( + ModListLayer* list, + ModListDisplay display, + CCSize const& size +) { + if (!ModListCell::init(list, size)) + return false; + + LoadProblem::Type problemType = LoadProblem::Type::Unknown; + // iterate problems to find the most important severity + for (auto const& problem : Loader::get()->getProblems()) { + if (problemType < problem.type) + problemType = problem.type; + // already found the most important one (error) + if (problemType > LoadProblem::Type::Conflict) + break; + } + + std::string icon; + std::string title; + switch (problemType) { + case LoadProblem::Type::Unknown: + title = "?????"; + break; + case LoadProblem::Type::Suggestion: + icon = "GJ_infoIcon_001.png"; + title = "You have suggested mods"; + m_color = { 66, 135, 245 }; + break; + case LoadProblem::Type::Recommendation: + icon = "GJ_infoIcon_001.png"; + title = "You have recommended mods"; + m_color = { 66, 135, 245 }; + break; + case LoadProblem::Type::Conflict: + icon = "info-warning.png"_spr; + title = "Some mods had warnings when loading"; + m_color = { 250, 176, 37 }; + break; + default: + icon = "info-alert.png"_spr; + title = "Some mods had problems loading"; + m_color = { 245, 66, 66 }; + break; + } + + m_menu = CCMenu::create(); + m_menu->setPosition(m_width - 40.f, m_height / 2); + this->addChild(m_menu); + + auto logoSize = this->getLogoSize(); + + if (!icon.empty()) { + auto logoSpr = CCSprite::createWithSpriteFrameName(icon.c_str()); + limitNodeSize(logoSpr, size, 1.f, .1f); + logoSpr->setPosition({logoSize / 2 + 12.f, m_height / 2}); + this->addChild(logoSpr); + } + + auto titleLabel = CCLabelBMFont::create(title.c_str(), "bigFont.fnt"); + titleLabel->setAnchorPoint({ .0f, .5f }); + titleLabel->setPosition(m_height / 2 + logoSize / 2 + 13.f, m_height / 2); + titleLabel->limitLabelWidth(m_width - 120.f, 1.f, .1f); + this->addChild(titleLabel); + + auto viewSpr = ButtonSprite::create("View", "bigFont.fnt", "GJ_button_01.png", .8f); + viewSpr->setScale(.65f); + + auto viewBtn = + CCMenuItemSpriteExtra::create(viewSpr, this, menu_selector(ProblemsCell::onInfo)); + m_menu->addChild(viewBtn); + + return true; +} + +std::optional ProblemsCell::getColor() { + return m_color; +} + +ProblemsCell* ProblemsCell::create( + ModListLayer* list, + ModListDisplay display, + CCSize const& size +) { + auto ret = new ProblemsCell(); + if (ret->init(list, display, size)) { + ret->autorelease(); + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +void ProblemsCell::updateState() {} + +std::string ProblemsCell::getDeveloper() const { + return ""; +} + +CCNode* ProblemsCell::createLogo(CCSize const& size) { + return nullptr; +} diff --git a/loader/src/ui/internal/list/ModListCell.hpp b/loader/src/ui/internal/list/ModListCell.hpp index e4d47f22d..0944d54a2 100644 --- a/loader/src/ui/internal/list/ModListCell.hpp +++ b/loader/src/ui/internal/list/ModListCell.hpp @@ -3,7 +3,7 @@ #include #include #include -#include +#include #include using namespace geode::prelude; @@ -26,7 +26,7 @@ class ModListCell : public CCLayer { CCMenuItemSpriteExtra* m_developerBtn; bool init(ModListLayer* list, CCSize const& size); - void setupInfo(ModInfo const& info, bool spaceForTags, ModListDisplay display); + void setupInfo(ModMetadata const& metadata, bool spaceForTags, ModListDisplay display, bool inactive); void draw() override; float getLogoSize() const; @@ -55,6 +55,7 @@ class ModCell : public ModListCell { ); void onInfo(CCObject*); + void onRestart(CCObject*); void onEnable(CCObject*); void onUnresolvedInfo(CCObject*); @@ -86,6 +87,7 @@ class IndexItemCell : public ModListCell { ); void onInfo(CCObject*); + void onRestart(CCObject*); public: static IndexItemCell* create( @@ -129,3 +131,32 @@ class InvalidGeodeFileCell : public ModListCell, public FLAlertLayerProtocol { CCNode* createLogo(CCSize const& size) override; std::string getDeveloper() const override; }; + +/** + * Mod list item for an invalid Geode package + */ +class ProblemsCell : public ModListCell { +protected: + std::optional m_color; + + bool init( + ModListLayer* list, + ModListDisplay display, + CCSize const& size + ); + + void onInfo(CCObject*); + +public: + static ProblemsCell* create( + ModListLayer* list, + ModListDisplay display, + CCSize const& size + ); + + std::optional getColor(); + + void updateState() override; + CCNode* createLogo(CCSize const& size) override; + std::string getDeveloper() const override; +}; diff --git a/loader/src/ui/internal/list/ModListLayer.cpp b/loader/src/ui/internal/list/ModListLayer.cpp index f84da8440..344783142 100644 --- a/loader/src/ui/internal/list/ModListLayer.cpp +++ b/loader/src/ui/internal/list/ModListLayer.cpp @@ -55,18 +55,18 @@ static std::optional fuzzyMatch(std::string const& kw, std::string const& s static std::optional queryMatchKeywords( ModListQuery const& query, - ModInfo const& info + ModMetadata const& metadata ) { double weighted = 0; // fuzzy match keywords if (query.keywords) { bool someMatched = false; - WEIGHTED_MATCH_MAX(info.name(), 2); - WEIGHTED_MATCH_MAX(info.id(), 1); - WEIGHTED_MATCH_MAX(info.developer(), 0.5); - WEIGHTED_MATCH_MAX(info.details().value_or(""), 0.05); - WEIGHTED_MATCH_MAX(info.description().value_or(""), 0.2); + WEIGHTED_MATCH_MAX(metadata.getName(), 2); + WEIGHTED_MATCH_MAX(metadata.getID(), 1); + WEIGHTED_MATCH_MAX(metadata.getDeveloper(), 0.5); + WEIGHTED_MATCH_MAX(metadata.getDetails().value_or(""), 0.05); + WEIGHTED_MATCH_MAX(metadata.getDescription().value_or(""), 0.2); if (!someMatched) { return std::nullopt; } @@ -77,7 +77,7 @@ static std::optional queryMatchKeywords( // sorted, at least enough so that if you're scrolling it based on // alphabetical order you will find the part you're looking for easily // so it's fine - return static_cast(-tolower(info.name()[0])); + return static_cast(-tolower(metadata.getName()[0])); } // if the weight is relatively small we can ignore it @@ -93,13 +93,12 @@ static std::optional queryMatch(ModListQuery const& query, Mod* mod) { // Only checking keywords makes sense for mods since their // platform always matches, they are always visible and they don't // currently list their tags - return queryMatchKeywords(query, mod->getModInfo()); + return queryMatchKeywords(query, mod->getMetadata()); } static std::optional queryMatch(ModListQuery const& query, IndexItemHandle item) { - // if no force visibility was provided and item is already installed, don't - // show it - if (!query.forceVisibility && Loader::get()->isModInstalled(item->getModInfo().id())) { + // if no force visibility was provided and item is already installed, don't show it + if (!query.forceVisibility && Loader::get()->isModInstalled(item->getMetadata().getID())) { return std::nullopt; } // make sure all tags match @@ -114,8 +113,18 @@ static std::optional queryMatch(ModListQuery const& query, IndexItemHandle })) { return std::nullopt; } + // if no force visibility was provided and item is already installed, don't show it + auto canInstall = Index::get()->canInstall(item); + if (!query.forceInvalid && !canInstall) { + log::warn( + "Removing {} from the list because it cannot be installed: {}", + item->getMetadata().getID(), + canInstall.unwrapErr() + ); + return std::nullopt; + } // otherwise match keywords - if (auto match = queryMatchKeywords(query, item->getModInfo())) { + if (auto match = queryMatchKeywords(query, item->getMetadata())) { auto weighted = match.value(); // add extra weight on tag matches if (query.keywords) { @@ -136,7 +145,7 @@ static std::optional queryMatch(ModListQuery const& query, IndexItemHandle static std::optional queryMatch(ModListQuery const& query, InvalidGeodeFile const& info) { // if any explicit filters were provided, no match - if (query.tags.size() || query.keywords.has_value()) { + if (!query.tags.empty() || query.keywords.has_value()) { return std::nullopt; } return 0; @@ -147,34 +156,40 @@ CCArray* ModListLayer::createModCells(ModListType type, ModListQuery const& quer switch (type) { default: case ModListType::Installed: { - // failed mods first - for (auto const& mod : Loader::get()->getFailedMods()) { - if (!queryMatch(query, mod)) continue; - mods->addObject(InvalidGeodeFileCell::create( - mod, this, m_display, this->getCellSize() - )); + // problems first + if (!Loader::get()->getProblems().empty()) { + mods->addObject(ProblemsCell::create(this, m_display, this->getCellSize())); } - // sort the mods by match score - std::multimap sorted; + // sort the mods by match score + std::multimap sorted; // then other mods + + // newly installed + for (auto const& item : Index::get()->getItems()) { + if (!item->isInstalled() || + Loader::get()->isModInstalled(item->getMetadata().getID()) || + Loader::get()->isModLoaded(item->getMetadata().getID())) + continue; + // match the same as other installed mods + if (auto match = queryMatchKeywords(query, item->getMetadata())) { + 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; @@ -182,7 +197,8 @@ CCArray* ModListLayer::createModCells(ModListType type, ModListQuery const& quer // sort the mods by match score std::multimap sorted; - for (auto const& item : Index::get()->getItems()) { + auto index = Index::get(); + for (auto const& item : index->getItems()) { if (auto match = queryMatch(query, item)) { sorted.insert({ match.value(), item }); } @@ -455,6 +471,15 @@ void ModListLayer::reloadList(std::optional const& query) { this->getListSize().width, this->getListSize().height ); + // please forgive me for this code + auto problemsCell = typeinfo_cast(list->m_entries->objectAtIndex(0)); + if (problemsCell) { + auto cellView = + typeinfo_cast(list->m_tableView->m_cellArray->objectAtIndex(0)); + if (cellView && problemsCell->getColor()) { + cellView->m_backgroundLayer->setColor(*problemsCell->getColor()); + } + } // set list status if (!items->count()) { @@ -546,14 +571,11 @@ void ModListLayer::reloadList(std::optional const& query) { } } -void ModListLayer::updateAllStates(ModListCell* toggled) { +void ModListLayer::updateAllStates() { for (auto cell : CCArrayExt( m_list->m_listView->m_tableView->m_cellArray )) { - auto node = static_cast(cell->getChildByID("mod-list-cell")); - if (toggled != node) { - node->updateState(); - } + static_cast(cell->getChildByID("mod-list-cell"))->updateState(); } } @@ -612,7 +634,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 4cff18d83..648e8309f 100644 --- a/loader/src/ui/internal/list/ModListLayer.hpp +++ b/loader/src/ui/internal/list/ModListLayer.hpp @@ -25,10 +25,15 @@ struct ModListQuery { */ std::optional keywords; /** - * Force mods to be shown on the list unless they explicitly mismatch some + * Force already installed mods to be shown on the list unless they explicitly mismatch some * tags (used to show installed mods on index) */ bool forceVisibility; + /** + * Force not installable mods to be shown on the list unless they explicitly mismatch some + * tags (used to show installed mods on index) + */ + bool forceInvalid; /** * Empty means current platform */ @@ -84,7 +89,7 @@ class ModListLayer : public CCLayer, public TextInputDelegate { public: static ModListLayer* create(); static ModListLayer* scene(); - void updateAllStates(ModListCell* except = nullptr); + void updateAllStates(); ModListDisplay getDisplay() const; ModListQuery& getQuery(); diff --git a/loader/src/ui/internal/list/ProblemsListCell.cpp b/loader/src/ui/internal/list/ProblemsListCell.cpp new file mode 100644 index 000000000..faa2086b6 --- /dev/null +++ b/loader/src/ui/internal/list/ProblemsListCell.cpp @@ -0,0 +1,140 @@ +#include "ProblemsListCell.hpp" +#include "ProblemsListPopup.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +void ProblemsListCell::draw() { + reinterpret_cast(this)->StatsCell::draw(); +} + +float ProblemsListCell::getLogoSize() const { + return m_height / 1.5f; +} + +bool ProblemsListCell::init(LoadProblem problem, ProblemsListPopup* list, CCSize const& size) { + m_width = size.width; + m_height = size.height; + m_layer = list; + this->setContentSize(size); + this->setID("problems-list-cell"); + + std::string cause = "unknown"; + if (std::holds_alternative(problem.cause)) { + cause = std::get(problem.cause).filename().string(); + } + else if (std::holds_alternative(problem.cause)) { + cause = std::get(problem.cause).getName(); + } + else if (std::holds_alternative(problem.cause)) { + cause = std::get(problem.cause)->getName(); + } + + std::string icon; + std::string message; + switch (problem.type) { + case LoadProblem::Type::Unknown: + message = fmt::format("Unknown error in {}", cause); + m_longMessage = problem.message; + break; + case LoadProblem::Type::Suggestion: + icon = "GJ_infoIcon_001.png"; + message = fmt::format("{} suggests {}", cause, problem.message); + break; + case LoadProblem::Type::Recommendation: + icon = "GJ_infoIcon_001.png"; + message = fmt::format("{} recommends {}", cause, problem.message); + break; + case LoadProblem::Type::Conflict: + icon = "info-warning.png"_spr; + message = fmt::format("{} conflicts with {}", cause, problem.message); + break; + case LoadProblem::Type::InvalidFile: + icon = "info-alert.png"_spr; + message = fmt::format("{} is an invalid .geode file", cause); + m_longMessage = problem.message; + break; + case LoadProblem::Type::Duplicate: + icon = "info-alert.png"_spr; + message = fmt::format("{} is installed more than once", cause); + m_longMessage = problem.message; + break; + case LoadProblem::Type::SetupFailed: + icon = "info-alert.png"_spr; + message = fmt::format("{} has failed setting up", cause); + m_longMessage = problem.message; + break; + case LoadProblem::Type::LoadFailed: + icon = "info-alert.png"_spr; + message = fmt::format("{} has failed loading", cause); + m_longMessage = problem.message; + break; + case LoadProblem::Type::EnableFailed: + icon = "info-alert.png"_spr; + message = fmt::format("{} has failed enabling", cause); + m_longMessage = problem.message; + break; + case LoadProblem::Type::MissingDependency: + icon = "info-alert.png"_spr; + message = fmt::format("{} depends on {}", cause, problem.message); + break; + case LoadProblem::Type::PresentIncompatibility: + icon = "info-alert.png"_spr; + message = fmt::format("{} is incompatible with {}", cause, problem.message); + break; + } + + m_problem = std::move(problem); + + m_menu = CCMenu::create(); + m_menu->setPosition(m_width - 40.f, m_height / 2); + this->addChild(m_menu); + + auto logoSize = this->getLogoSize(); + + if (!icon.empty()) { + auto logoSpr = CCSprite::createWithSpriteFrameName(icon.c_str()); + limitNodeSize(logoSpr, size, 1.f, .1f); + logoSpr->setPosition({logoSize / 2 + 12.f, m_height / 2}); + this->addChild(logoSpr); + } + + auto messageLabel = CCLabelBMFont::create(message.c_str(), "bigFont.fnt"); + messageLabel->setAnchorPoint({ .0f, .5f }); + messageLabel->setPosition(m_height / 2 + logoSize / 2 + 13.f, m_height / 2); + messageLabel->limitLabelWidth(m_width - 120.f, 1.f, .1f); + this->addChild(messageLabel); + + if (!m_longMessage.empty()) { + auto viewSpr = ButtonSprite::create("More", "bigFont.fnt", "GJ_button_01.png", .8f); + viewSpr->setScale(.65f); + + auto viewBtn = + CCMenuItemSpriteExtra::create(viewSpr, this, menu_selector(ProblemsListCell::onMore)); + m_menu->addChild(viewBtn); + } + + return true; +} + +void ProblemsListCell::onMore(cocos2d::CCObject*) { + FLAlertLayer::create("Problem Info", m_longMessage, "OK")->show(); +} + +LoadProblem ProblemsListCell::getProblem() const { + return m_problem; +} + +ProblemsListCell* ProblemsListCell::create(LoadProblem problem, ProblemsListPopup* list, CCSize const& size) { + auto ret = new ProblemsListCell(); + if (ret->init(problem, list, size)) { + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} diff --git a/loader/src/ui/internal/list/ProblemsListCell.hpp b/loader/src/ui/internal/list/ProblemsListCell.hpp new file mode 100644 index 000000000..8e7fc19c7 --- /dev/null +++ b/loader/src/ui/internal/list/ProblemsListCell.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include +#include +#include + +using namespace geode::prelude; + +class ProblemsListPopup; + +class ProblemsListCell : public CCLayer { +protected: + float m_width; + float m_height; + ProblemsListPopup* m_layer; + CCMenu* m_menu; + LoadProblem m_problem; + std::string m_longMessage; + + bool init(LoadProblem problem, ProblemsListPopup* list, CCSize const& size); + void draw() override; + + void onMore(CCObject*); + + float getLogoSize() const; + +public: + LoadProblem getProblem() const; + + static ProblemsListCell* create(LoadProblem problem, ProblemsListPopup* list, CCSize const& size); +}; diff --git a/loader/src/ui/internal/list/ProblemsListPopup.cpp b/loader/src/ui/internal/list/ProblemsListPopup.cpp new file mode 100644 index 000000000..9140c1158 --- /dev/null +++ b/loader/src/ui/internal/list/ProblemsListPopup.cpp @@ -0,0 +1,106 @@ +#include "ProblemsListPopup.hpp" +#include "ProblemsListCell.hpp" + +#include +#include + +bool ProblemsListPopup::setup(Mod* scrollTo) { + m_noElasticity = true; + this->setTitle("Problems"); + this->createList(scrollTo); + return true; +} + +void ProblemsListPopup::createList(Mod* scrollTo) { + auto winSize = CCDirector::sharedDirector()->getWinSize(); + + m_listParent = CCNode::create(); + m_listParent->setPositionY(-7.f); + m_mainLayer->addChild(m_listParent); + + float scroll = 0.f; + auto items = this->createCells(scrollTo, scroll); + m_list = ListView::create( + items, + this->getCellSize().height, + this->getListSize().width, + this->getListSize().height + ); + m_list->setPosition(winSize / 2 - m_list->getScaledContentSize() / 2); + m_listParent->addChild(m_list); + + m_list->m_tableView->m_contentLayer->setPositionY(m_list->m_tableView->m_contentLayer->getPositionY() + scroll); + + addListBorders(m_listParent, winSize / 2, m_list->getScaledContentSize()); +} + +CCArray* ProblemsListPopup::createCells(Mod* scrollTo, float& scrollValue) { + std::vector top; + std::vector middle; + std::vector bottom; + + for (auto const& problem : Loader::get()->getProblems()) { + switch (problem.type) { + case geode::LoadProblem::Type::Suggestion: + bottom.push_back(ProblemsListCell::create(problem, this, this->getCellSize())); + break; + case geode::LoadProblem::Type::Recommendation: + middle.push_back(ProblemsListCell::create(problem, this, this->getCellSize())); + break; + default: + top.push_back(ProblemsListCell::create(problem, this, this->getCellSize())); + break; + } + } + + auto final = CCArray::create(); + + // find the highest scrollTo element + bool scrollFound = false; + auto tryFindScroll = [&](auto const& item) { + if (!scrollTo || scrollFound || + !std::holds_alternative(item->getProblem().cause) || + std::get(item->getProblem().cause) != scrollTo) + return; + scrollValue = (float)final->count() * this->getCellSize().height; + scrollFound = true; + }; + + for (auto const& item : top) { + tryFindScroll(item); + final->addObject(item); + } + for (auto const& item : middle) { + tryFindScroll(item); + final->addObject(item); + } + for (auto const& item : bottom) { + tryFindScroll(item); + final->addObject(item); + } + + return final; +} + +// Getters + +CCSize ProblemsListPopup::getListSize() const { + return { 340.f, 190.f }; +} + +CCSize ProblemsListPopup::getCellSize() const { + return { getListSize().width, 40.f }; +} + +// Static + +ProblemsListPopup* ProblemsListPopup::create(Mod* scrollTo) { + auto ret = new ProblemsListPopup(); + if (!ret->init(380.f, 250.f, scrollTo)) { + CC_SAFE_DELETE(ret); + return nullptr; + } + ret->autorelease(); + return ret; +} + diff --git a/loader/src/ui/internal/list/ProblemsListPopup.hpp b/loader/src/ui/internal/list/ProblemsListPopup.hpp new file mode 100644 index 000000000..b74d7a2d7 --- /dev/null +++ b/loader/src/ui/internal/list/ProblemsListPopup.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +using namespace geode::prelude; + +class ProblemsListPopup : public Popup { +protected: + CCNode* m_listParent; + ListView* m_list; + + bool setup(Mod* scrollTo) override; + + void createList(Mod* scrollTo); + CCArray* createCells(Mod* scrollTo, float& scrollValue); + CCSize getCellSize() const; + CCSize getListSize() const; + +public: + static ProblemsListPopup* create(Mod* scrollTo); +}; diff --git a/loader/src/ui/internal/list/SearchFilterPopup.cpp b/loader/src/ui/internal/list/SearchFilterPopup.cpp index e0927a38e..61353a7e9 100644 --- a/loader/src/ui/internal/list/SearchFilterPopup.cpp +++ b/loader/src/ui/internal/list/SearchFilterPopup.cpp @@ -5,7 +5,9 @@ #include #include -#include + +// re-add when we actually add the platforms +const float iosAndAndroidSize = 45.f; bool SearchFilterPopup::setup(ModListLayer* layer, ModListType type) { m_noElasticity = true; @@ -14,66 +16,77 @@ bool SearchFilterPopup::setup(ModListLayer* layer, ModListType type) { this->setTitle("Search Filters"); auto winSize = CCDirector::sharedDirector()->getWinSize(); - auto pos = CCPoint { winSize.width / 2 - 140.f, winSize.height / 2 + 45.f }; + auto pos = CCPoint { winSize.width / 2 - 140.f, winSize.height / 2 + 45.f - iosAndAndroidSize * 0.25f }; // platforms auto platformTitle = CCLabelBMFont::create("Platforms", "goldFont.fnt"); - platformTitle->setPosition(winSize.width / 2 - 85.f, winSize.height / 2 + 75.f); + platformTitle->setAnchorPoint({ 0.5f, 1.f }); + platformTitle->setPosition(winSize.width / 2 - 85.f, winSize.height / 2 + 81.5f - iosAndAndroidSize * 0.25f); platformTitle->setScale(.5f); m_mainLayer->addChild(platformTitle); auto platformBG = CCScale9Sprite::create("square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f }); platformBG->setColor({ 0, 0, 0 }); platformBG->setOpacity(90); - platformBG->setContentSize({ 290.f, 205.f }); - platformBG->setPosition(winSize.width / 2 - 85.f, winSize.height / 2 + 11.f); + platformBG->setContentSize({ 290.f, 205.f - iosAndAndroidSize * 2.f }); + platformBG->setAnchorPoint({ 0.5f, 1.f }); + platformBG->setPosition(winSize.width / 2 - 85.f, winSize.height / 2 + 62.25f - iosAndAndroidSize * 0.25f); platformBG->setScale(.5f); m_mainLayer->addChild(platformBG); this->enable(this->addPlatformToggle("Windows", PlatformID::Windows, pos), type); this->enable(this->addPlatformToggle("macOS", PlatformID::MacOS, pos), type); - this->enable(this->addPlatformToggle("IOS", PlatformID::iOS, pos), type); - this->enable(this->addPlatformToggle("Android", PlatformID::Android, pos), type); + //this->enable(this->addPlatformToggle("IOS", PlatformID::iOS, pos), type); + //this->enable(this->addPlatformToggle("Android", PlatformID::Android, pos), type); // show installed auto installedTitle = CCLabelBMFont::create("Other", "goldFont.fnt"); - installedTitle->setPosition(winSize.width / 2 - 85.f, winSize.height / 2 - 57.f); + installedTitle->setAnchorPoint({ 0.5f, 1.f }); + installedTitle->setPosition(winSize.width / 2 - 85.f, winSize.height / 2 - 50.5f + iosAndAndroidSize - iosAndAndroidSize * 0.25f); installedTitle->setScale(.5f); m_mainLayer->addChild(installedTitle); auto installedBG = CCScale9Sprite::create("square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f }); installedBG->setColor({ 0, 0, 0 }); installedBG->setOpacity(90); - installedBG->setContentSize({ 290.f, 65.f }); - installedBG->setPosition(winSize.width / 2 - 85.f, winSize.height / 2 - 85.f); + installedBG->setContentSize({ 290.f, 110.f }); + installedBG->setAnchorPoint({ 0.5f, 1.f }); + installedBG->setPosition(winSize.width / 2 - 85.f, winSize.height / 2 - 68.75f + iosAndAndroidSize - iosAndAndroidSize * 0.25f); installedBG->setScale(.5f); m_mainLayer->addChild(installedBG); - pos = CCPoint { winSize.width / 2 - 140.f, winSize.height / 2 - 85.f }; + pos = CCPoint { winSize.width / 2 - 140.f, winSize.height / 2 - 85.f + iosAndAndroidSize - iosAndAndroidSize * 0.25f }; this->addToggle( "Show Installed", menu_selector(SearchFilterPopup::onShowInstalled), m_modLayer->getQuery().forceVisibility, 0, pos ); + this->addToggle( + "Show Invalid", menu_selector(SearchFilterPopup::onShowInvalid), + m_modLayer->getQuery().forceInvalid, 1, pos + ); + // tags auto tagsTitle = CCLabelBMFont::create("Tags", "goldFont.fnt"); - tagsTitle->setPosition(winSize.width / 2 + 85.f, winSize.height / 2 + 75.f); + tagsTitle->setAnchorPoint({ 0.5f, 1.f }); + tagsTitle->setPosition(winSize.width / 2 + 85.f, winSize.height / 2 + 81.5f - iosAndAndroidSize * 0.25f); tagsTitle->setScale(.5f); m_mainLayer->addChild(tagsTitle); auto tagsBG = CCScale9Sprite::create("square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f }); tagsBG->setColor({ 0, 0, 0 }); tagsBG->setOpacity(90); - tagsBG->setContentSize({ 290.f, 328.f }); - tagsBG->setPosition(winSize.width / 2 + 85.f, winSize.height / 2 - 19.5f); + tagsBG->setContentSize({ 290.f, 328.f - iosAndAndroidSize }); + tagsBG->setAnchorPoint({ 0.5f, 1.f }); + tagsBG->setPosition(winSize.width / 2 + 85.f, winSize.height / 2 + 62.5f - iosAndAndroidSize * 0.25f); tagsBG->setScale(.5f); m_mainLayer->addChild(tagsBG); - pos = CCPoint { winSize.width / 2 + 30.f, winSize.height / 2 + 45.f }; + pos = CCPoint { winSize.width / 2 + 30.f, winSize.height / 2 + 45.f - iosAndAndroidSize * 0.25f }; for (auto& tag : Index::get()->getTags()) { auto toggle = CCMenuItemToggler::createWithStandardSprites( @@ -116,6 +129,11 @@ void SearchFilterPopup::onShowInstalled(CCObject* sender) { m_modLayer->getQuery().forceVisibility = !toggle->isToggled(); } +void SearchFilterPopup::onShowInvalid(CCObject* sender) { + auto toggle = static_cast(sender); + m_modLayer->getQuery().forceInvalid = !toggle->isToggled(); +} + void SearchFilterPopup::enable(CCMenuItemToggler* toggle, ModListType type) { if (type == ModListType::Installed) { toggle->setEnabled(false); @@ -162,7 +180,7 @@ void SearchFilterPopup::onClose(CCObject* sender) { SearchFilterPopup* SearchFilterPopup::create(ModListLayer* layer, ModListType type) { auto ret = new SearchFilterPopup(); - if (ret && ret->init(350.f, 240.f, layer, type)) { + if (ret && ret->init(350.f, 240.f - iosAndAndroidSize * 0.5f, layer, type)) { ret->autorelease(); return ret; } diff --git a/loader/src/ui/internal/list/SearchFilterPopup.hpp b/loader/src/ui/internal/list/SearchFilterPopup.hpp index 5038bf89d..a02fc22dc 100644 --- a/loader/src/ui/internal/list/SearchFilterPopup.hpp +++ b/loader/src/ui/internal/list/SearchFilterPopup.hpp @@ -19,6 +19,7 @@ class SearchFilterPopup : public Popup { void onPlatformToggle(CCObject*); void onShowInstalled(CCObject*); + void onShowInvalid(CCObject*); void onTag(CCObject*); void enable(CCMenuItemToggler* toggle, ModListType type); diff --git a/loader/src/ui/nodes/Popup.cpp b/loader/src/ui/nodes/Popup.cpp index 2703241ff..3e7286634 100644 --- a/loader/src/ui/nodes/Popup.cpp +++ b/loader/src/ui/nodes/Popup.cpp @@ -5,8 +5,18 @@ using namespace geode::prelude; class QuickPopup : public FLAlertLayer, public FLAlertLayerProtocol { protected: MiniFunction m_selected; + bool m_cancelledByEscape; + bool m_usedEscape = false; + + void keyBackClicked() override { + m_usedEscape = true; + FLAlertLayer::keyBackClicked(); + } void FLAlert_Clicked(FLAlertLayer* layer, bool btn2) override { + if (m_cancelledByEscape && m_usedEscape) { + return; + } if (m_selected) { m_selected(layer, btn2); } @@ -15,10 +25,11 @@ class QuickPopup : public FLAlertLayer, public FLAlertLayerProtocol { public: static QuickPopup* create( char const* title, std::string const& content, char const* btn1, char const* btn2, - float width, MiniFunction selected + float width, MiniFunction selected, bool cancelledByEscape ) { auto inst = new QuickPopup; inst->m_selected = selected; + inst->m_cancelledByEscape = cancelledByEscape; if (inst && inst->init(inst, title, content, btn1, btn2, width, false, .0f)) { inst->autorelease(); return inst; @@ -32,7 +43,7 @@ FLAlertLayer* geode::createQuickPopup( char const* title, std::string const& content, char const* btn1, char const* btn2, float width, MiniFunction selected, bool doShow ) { - auto ret = QuickPopup::create(title, content, btn1, btn2, width, selected); + auto ret = QuickPopup::create(title, content, btn1, btn2, width, selected, false); if (doShow) { ret->show(); } @@ -45,3 +56,21 @@ FLAlertLayer* geode::createQuickPopup( ) { return createQuickPopup(title, content, btn1, btn2, 350.f, selected, doShow); } + +FLAlertLayer* geode::createQuickPopup( + char const* title, std::string const& content, char const* btn1, char const* btn2, float width, + MiniFunction selected, bool doShow, bool cancelledByEscape +) { + auto ret = QuickPopup::create(title, content, btn1, btn2, width, selected, cancelledByEscape); + if (doShow) { + ret->show(); + } + return ret; +} + +FLAlertLayer* geode::createQuickPopup( + char const* title, std::string const& content, char const* btn1, char const* btn2, + MiniFunction selected, bool doShow, bool cancelledByEscape +) { + return createQuickPopup(title, content, btn1, btn2, 350.f, selected, doShow, cancelledByEscape); +} diff --git a/loader/src/utils/VersionInfo.cpp b/loader/src/utils/VersionInfo.cpp index 5cb514c7d..be379c3a5 100644 --- a/loader/src/utils/VersionInfo.cpp +++ b/loader/src/utils/VersionInfo.cpp @@ -133,6 +133,11 @@ std::ostream& geode::operator<<(std::ostream& stream, VersionInfo const& version Result ComparableVersionInfo::parse(std::string const& rawStr) { VersionCompare compare; auto string = rawStr; + + if (string == "*") { + return Ok(ComparableVersionInfo({0, 0, 0}, VersionCompare::Any)); + } + if (string.starts_with("<=")) { compare = VersionCompare::LessEq; string.erase(0, 2); @@ -162,13 +167,14 @@ Result ComparableVersionInfo::parse(std::string const& ra } std::string ComparableVersionInfo::toString() const { - std::string prefix = ""; + std::string prefix; switch (m_compare) { - case VersionCompare::Exact: prefix = "="; break; + case VersionCompare::Exact: prefix = "="; break; case VersionCompare::LessEq: prefix = "<="; break; case VersionCompare::MoreEq: prefix = ">="; break; - case VersionCompare::Less: prefix = "<"; break; - case VersionCompare::More: prefix = ">"; break; + case VersionCompare::Less: prefix = "<"; break; + case VersionCompare::More: prefix = ">"; break; + case VersionCompare::Any: return "*"; } return prefix + m_version.toString(); } diff --git a/loader/src/utils/string.cpp b/loader/src/utils/string.cpp index c839191c7..4011ac87b 100644 --- a/loader/src/utils/string.cpp +++ b/loader/src/utils/string.cpp @@ -127,29 +127,61 @@ std::wstring utils::string::replace( std::vector utils::string::split(std::string const& str, std::string const& split) { std::vector res; - if (str.size()) { - auto s = str; - size_t pos = 0; - while ((pos = s.find(split)) != std::string::npos) { - res.push_back(s.substr(0, pos)); - s.erase(0, pos + split.length()); - } - res.push_back(s); + if (str.empty()) return res; + auto s = str; + size_t pos; + while ((pos = s.find(split)) != std::string::npos) { + res.push_back(s.substr(0, pos)); + s.erase(0, pos + split.length()); } + res.push_back(s); return res; } std::vector utils::string::split(std::wstring const& str, std::wstring const& split) { std::vector res; - if (str.size()) { - auto s = str; - size_t pos = 0; - while ((pos = s.find(split)) != std::wstring::npos) { - res.push_back(s.substr(0, pos)); - s.erase(0, pos + split.length()); - } - res.push_back(s); + if (str.empty()) return res; + auto s = str; + size_t pos; + while ((pos = s.find(split)) != std::wstring::npos) { + res.push_back(s.substr(0, pos)); + s.erase(0, pos + split.length()); } + res.push_back(s); + return res; +} + +std::string utils::string::join(std::vector const& strs, std::string const& separator) { + std::string res; + if (strs.empty()) + return res; + if (strs.size() == 1) + return strs[0]; + // idk if less allocations but an extra loop is faster but + size_t size = 0; + for (auto const& str : strs) + size += str.size() + separator.size(); + res.reserve(size); + for (auto const& str : strs) + res += str + separator; + res.erase(res.size() - separator.size()); + return res; +} + +std::wstring utils::string::join(std::vector const& strs, std::wstring const& separator) { + std::wstring res; + if (strs.empty()) + return res; + if (strs.size() == 1) + return strs[0]; + // idk if less allocations but an extra loop is faster but + size_t size = 0; + for (auto const& str : strs) + size += str.size() + separator.size(); + res.reserve(size); + for (auto const& str : strs) + res += str + separator; + res.erase(res.size() - separator.size()); return res; }