From 82e128bb577b67fb7e4c32bb9f290349f15693c8 Mon Sep 17 00:00:00 2001 From: ConfiG Date: Mon, 7 Aug 2023 01:47:14 +0300 Subject: [PATCH 01/36] add utils::string::join --- loader/include/Geode/utils/string.hpp | 3 ++ loader/src/utils/string.cpp | 64 ++++++++++++++++++++------- 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/loader/include/Geode/utils/string.hpp b/loader/include/Geode/utils/string.hpp index 4e024e4d..ed98ce0f 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/src/utils/string.cpp b/loader/src/utils/string.cpp index c839191c..4011ac87 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; } From 7d74f16c01a7c9caca9426cbf1b43f5865867c3b Mon Sep 17 00:00:00 2001 From: ConfiG Date: Mon, 7 Aug 2023 01:48:01 +0300 Subject: [PATCH 02/36] add logger nesting --- loader/include/Geode/loader/Log.hpp | 13 ++++++++++++- loader/src/loader/Log.cpp | 24 +++++++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/loader/include/Geode/loader/Log.hpp b/loader/include/Geode/loader/Log.hpp index b3c255bd..817dd47b 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/src/loader/Log.cpp b/loader/src/loader/Log.cpp index 42e735ae..ec69eed7 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()); From 2b1dc178a473540f54fe4b8aa8eab63665c6d7c6 Mon Sep 17 00:00:00 2001 From: ConfiG Date: Mon, 7 Aug 2023 21:13:40 +0300 Subject: [PATCH 03/36] add any (`*`) to ComparableVersionInfo --- loader/include/Geode/utils/VersionInfo.hpp | 9 +++++++-- loader/src/utils/VersionInfo.cpp | 14 ++++++++++---- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/loader/include/Geode/utils/VersionInfo.hpp b/loader/include/Geode/utils/VersionInfo.hpp index 4ceea8d8..da690a0f 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/src/utils/VersionInfo.cpp b/loader/src/utils/VersionInfo.cpp index 5cb514c7..be379c3a 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(); } From 53b52eaeeb8cd9b92b1e9a0f2b928ba0acfd28b6 Mon Sep 17 00:00:00 2001 From: ConfiG Date: Tue, 8 Aug 2023 21:55:49 +0300 Subject: [PATCH 04/36] ModInfo => ModMetadata --- loader/include/Geode/loader/Index.hpp | 13 +- loader/include/Geode/loader/Loader.hpp | 4 +- loader/include/Geode/loader/Mod.hpp | 10 +- loader/include/Geode/loader/ModInfo.hpp | 34 +- loader/include/Geode/loader/ModMetadata.hpp | 242 ++++++++ loader/src/load.cpp | 9 +- loader/src/loader/Index.cpp | 145 +++-- loader/src/loader/Loader.cpp | 2 +- loader/src/loader/LoaderImpl.cpp | 15 +- loader/src/loader/Mod.cpp | 14 +- loader/src/loader/ModImpl.cpp | 83 ++- loader/src/loader/ModImpl.hpp | 11 +- loader/src/loader/ModInfoImpl.cpp | 350 +++--------- loader/src/loader/ModInfoImpl.hpp | 25 +- loader/src/loader/ModMetadataImpl.cpp | 519 ++++++++++++++++++ loader/src/loader/ModMetadataImpl.hpp | 58 ++ loader/src/platform/mac/ModImpl.cpp | 8 +- loader/src/platform/windows/ModImpl.cpp | 2 +- loader/src/ui/internal/GeodeUI.cpp | 21 +- .../src/ui/internal/info/DevProfilePopup.cpp | 2 +- loader/src/ui/internal/info/ModInfoPopup.cpp | 90 ++- loader/src/ui/internal/info/ModInfoPopup.hpp | 12 +- loader/src/ui/internal/list/ModListCell.cpp | 26 +- loader/src/ui/internal/list/ModListCell.hpp | 4 +- loader/src/ui/internal/list/ModListLayer.cpp | 20 +- 25 files changed, 1168 insertions(+), 551 deletions(-) create mode 100644 loader/include/Geode/loader/ModMetadata.hpp create mode 100644 loader/src/loader/ModMetadataImpl.cpp create mode 100644 loader/src/loader/ModMetadataImpl.hpp diff --git a/loader/include/Geode/loader/Index.hpp b/loader/include/Geode/loader/Index.hpp index 2e55ca6d..4a027823 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,7 +108,8 @@ 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; @@ -204,8 +206,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 */ - IndexItemHandle getItem(ModInfo const& info) const; + [[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(ModMetadata const& metadata) const; /** * Get an item from the index that corresponds to an installed mod * @param mod An installed mod diff --git a/loader/include/Geode/loader/Loader.hpp b/loader/include/Geode/loader/Loader.hpp index 8e8d1c1c..96e542d8 100644 --- a/loader/include/Geode/loader/Loader.hpp +++ b/loader/include/Geode/loader/Loader.hpp @@ -5,6 +5,7 @@ #include "../utils/MiniFunction.hpp" #include "Log.hpp" #include "ModInfo.hpp" +#include "ModMetadata.hpp" #include "Types.hpp" #include @@ -61,8 +62,7 @@ namespace geode { 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(); void updateResources(); void updateResources(bool forceReload); diff --git a/loader/include/Geode/loader/Mod.hpp b/loader/include/Geode/loader/Mod.hpp index 1010f2a6..8a0d869f 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; @@ -82,6 +83,11 @@ namespace geode { bool supportsUnloading() const; bool wasSuccesfullyLoaded() const; ModInfo getModInfo() const; + [[deprecated]] bool supportsUnloading() const; + [[deprecated("wasSuccessfullyLoaded")]] 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 diff --git a/loader/include/Geode/loader/ModInfo.hpp b/loader/include/Geode/loader/ModInfo.hpp index 07f867a3..feefcbc6 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,10 +31,10 @@ 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; std::unique_ptr m_impl; @@ -82,7 +84,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,28 +196,22 @@ namespace geode { static bool validateID(std::string const& id); + operator ModMetadata(); + operator ModMetadata() const; + private: - ModJson& rawJSON(); - ModJson const& rawJSON() const; - /** - * Version is passed for backwards - * compatibility if we update the mod.json - * format - */ - static Result createFromSchemaV010(ModJson const& json); - - Result<> addSpecialFiles(ghc::filesystem::path const& dir); - Result<> addSpecialFiles(utils::file::Unzip& zip); - - std::vector*>> getSpecialFiles(); - friend class ModInfoImpl; + + friend class ModMetadata; }; } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" template <> struct json::Serialize { static json::Value to_json(geode::ModInfo const& info) { return info.toJSON(); } }; +#pragma clang diagnostic pop diff --git a/loader/include/Geode/loader/ModMetadata.hpp b/loader/include/Geode/loader/ModMetadata.hpp new file mode 100644 index 00000000..4364d77b --- /dev/null +++ b/loader/include/Geode/loader/ModMetadata.hpp @@ -0,0 +1,242 @@ +#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; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +#pragma ide diagnostic ignored "google-explicit-constructor" + operator geode::Dependency(); + operator geode::Dependency() const; + + static Dependency fromDeprecated(geode::Dependency const& value); +#pragma clang diagnostic pop + }; + + struct GEODE_DLL Incompatibility { + std::string id; + ComparableVersionInfo version; + Mod* mod = nullptr; + [[nodiscard]] bool isResolved() const; + }; + + struct IssuesInfo { + std::string info; + std::optional url; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +#pragma ide diagnostic ignored "google-explicit-constructor" + operator geode::IssuesInfo(); + operator geode::IssuesInfo() const; + + static IssuesInfo fromDeprecated(geode::IssuesInfo const& value); +#pragma clang diagnostic pop + }; + + /** + * Path to the mod file + */ + [[maybe_unused, nodiscard]] ghc::filesystem::path getPath() const; + /** + * Name of the platform binary within + * the mod zip + */ + [[maybe_unused, nodiscard]] std::string getBinaryName() const; + /** + * Mod Version. Should follow semantic versioning. + */ + [[maybe_unused, 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. + */ + [[maybe_unused, nodiscard]] std::string getID() const; + /** + * Name of the mod. May contain + * spaces & punctuation, but should + * be restricted to the ASCII + * character set. + */ + [[maybe_unused, nodiscard]] std::string getName() const; + /** + * 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. + */ + [[maybe_unused, nodiscard]] std::string getDeveloper() const; + /** + * Short & concise description of the + * mod. + */ + [[maybe_unused, nodiscard]] std::optional getDescription() const; + /** + * Detailed description of the mod, written in Markdown (see + * ) for more info + */ + [[maybe_unused, nodiscard]] std::optional getDetails() const; + /** + * Changelog for the mod, written in Markdown (see + * ) for more info + */ + [[maybe_unused, 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) + */ + [[maybe_unused, nodiscard]] std::optional getSupportInfo() const; + /** + * Git Repository of the mod + */ + [[maybe_unused, nodiscard]] std::optional getRepository() const; + /** + * Info about where users should report issues and request help + */ + [[maybe_unused, nodiscard]] std::optional getIssues() const; + /** + * Dependencies + */ + [[maybe_unused, nodiscard]] std::vector getDependencies() const; + /** + * Incompatibilities + */ + [[maybe_unused, nodiscard]] std::vector getIncompatibilities() const; + /** + * Mod spritesheet names + */ + [[maybe_unused, nodiscard]] std::vector getSpritesheets() const; + /** + * Mod settings + * @note Not a map because insertion order must be preserved + */ + [[maybe_unused, nodiscard]] std::vector> getSettings() const; + /** + * Whether this mod has to be loaded before the loading screen or not + */ + [[maybe_unused, nodiscard]] bool needsEarlyLoad() const; + /** + * Whether this mod is an API or not + */ + [[maybe_unused, nodiscard]] bool isAPI() const; + /** + * Create ModInfo from an unzipped .geode package + */ + [[maybe_unused]] static Result createFromGeodeZip(utils::file::Unzip& zip); + /** + * Create ModInfo from a .geode package + */ + [[maybe_unused]] static Result createFromGeodeFile(ghc::filesystem::path const& path); + /** + * Create ModInfo from a mod.json file + */ + [[maybe_unused]] static Result createFromFile(ghc::filesystem::path const& path); + /** + * Create ModInfo from a parsed json document + */ + [[maybe_unused]] static Result create(ModJson const& json); + + /** + * Convert to JSON. Essentially same as getRawJSON except dynamically + * adds runtime fields like path + */ + [[maybe_unused, nodiscard]] ModJson toJSON() const; + /** + * Get the raw JSON file + */ + [[maybe_unused, nodiscard]] ModJson getRawJSON() const; + + [[maybe_unused]] bool operator==(ModMetadata const& other) const; + + [[maybe_unused]] static bool validateID(std::string const& id); + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +#pragma ide diagnostic ignored "google-explicit-constructor" + operator ModInfo(); + operator ModInfo() const; +#pragma clang diagnostic pop + + 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; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + // ModInfo => ModMetadata conversion stuff + friend class ModInfo::Impl; +#pragma clang diagnostic pop + }; +} + +template <> +struct json::Serialize { + static json::Value to_json(geode::ModMetadata const& info) { + return info.toJSON(); + } +}; diff --git a/loader/src/load.cpp b/loader/src/load.cpp index 29dff3fc..a60259fa 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 @@ $execute { }); 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 @@ $execute { 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; diff --git a/loader/src/loader/Index.cpp b/loader/src/loader/Index.cpp index c3e7c394..bf366247 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; @@ -74,7 +74,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 { @@ -103,7 +107,7 @@ Result IndexItem::Impl::create(ghc::filesystem::path const& dir .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}") ); @@ -117,7 +121,7 @@ Result IndexItem::Impl::create(ghc::filesystem::path const& dir auto item = std::make_shared(); item->m_impl->m_path = dir; - item->m_impl->m_info = info; + item->m_impl->m_metadata = metadata; 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_platforms = platforms; @@ -333,18 +337,18 @@ 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 }); } @@ -408,7 +412,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 +445,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 +465,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 +477,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; } } @@ -501,37 +509,38 @@ 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 { + // TODO: ui for picking recommended and suggested mods + for (auto& dep : item->getMetadata().getDependencies()) { + // if the dep is resolved, then all its dependencies must be installed + // already in order for that to have happened + if (dep.isResolved()) continue; + + // 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 {} 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 + 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() + ); } - // 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 +550,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,34 +559,50 @@ 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() )); } + + // If the mod is already loaded, enable it + if (mod->isLoaded()) { + res = mod->enable(); + if (!res) { + return postError(fmt::format( + "Unable to enable {}: {}", + item->getMetadata().getID(), res.unwrapErr() + )); + } + } } + // 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 +613,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 +625,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 +653,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 +696,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 a539a70a..6335ccee 100644 --- a/loader/src/loader/Loader.cpp +++ b/loader/src/loader/Loader.cpp @@ -84,7 +84,7 @@ std::vector Loader::getAllMods() { } Mod* Loader::getModImpl() { - return m_impl->getModImpl(); + return Mod::get(); } void Loader::updateAllDependencies() { diff --git a/loader/src/loader/LoaderImpl.cpp b/loader/src/loader/LoaderImpl.cpp index 44fa2f6c..caca7b4d 100644 --- a/loader/src/loader/LoaderImpl.cpp +++ b/loader/src/loader/LoaderImpl.cpp @@ -1,6 +1,10 @@ #include "LoaderImpl.hpp" #include + +#include "ModImpl.hpp" +#include "ModMetadataImpl.hpp" + #include #include #include @@ -274,16 +278,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 +295,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 { @@ -560,7 +563,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/Mod.cpp b/loader/src/loader/Mod.cpp index b443402c..46b8774e 100644 --- a/loader/src/loader/Mod.cpp +++ b/loader/src/loader/Mod.cpp @@ -5,6 +5,7 @@ using namespace geode::prelude; 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() {} @@ -53,15 +54,22 @@ bool Mod::supportsDisabling() const { } 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 { diff --git a/loader/src/loader/ModImpl.cpp b/loader/src/loader/ModImpl.cpp index 35b18858..78f58c70 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,7 +25,7 @@ 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() { @@ -33,16 +33,16 @@ Mod::Impl::~Impl() { } 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,27 +58,27 @@ 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; } ghc::filesystem::path Mod::Impl::getTempDir() const { @@ -86,15 +86,15 @@ ghc::filesystem::path Mod::Impl::getTempDir() const { } 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() { @@ -117,7 +117,7 @@ bool Mod::Impl::supportsUnloading() const { return m_info.supportsUnloading(); } -bool Mod::Impl::wasSuccesfullyLoaded() const { +bool Mod::Impl::wasSuccessfullyLoaded() const { return !this->isEnabled() || this->isLoaded(); } @@ -155,7 +155,7 @@ Result<> Mod::Impl::loadData() { Severity::Error, m_self, "{}: Unable to load value for setting \"{}\"", - m_info.id(), + m_metadata.getID(), key ); } @@ -244,7 +244,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 +262,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 +290,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,8 +301,8 @@ 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(); } @@ -316,7 +316,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; @@ -377,7 +377,7 @@ Result<> Mod::Impl::enable() { 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()) { @@ -449,7 +449,7 @@ 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 @@ -509,7 +509,7 @@ bool Mod::Impl::hasUnresolvedDependencies() const { std::vector Mod::Impl::getUnresolvedDependencies() { std::vector unresolved; - for (auto const& dep : m_info.dependencies()) { + for (auto const& dep : m_metadata.getDependencies()) { if (!dep.isResolved()) { unresolved.push_back(dep); } @@ -518,7 +518,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 +528,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; @@ -608,7 +608,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 +619,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 +640,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 +651,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 +661,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 +682,7 @@ ModJson Mod::Impl::getRuntimeInfo() const { return json; } -static Result getModImplInfo() { +static Result getModImplInfo() { std::string err; json::Value json; try { @@ -691,8 +691,7 @@ 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); } diff --git a/loader/src/loader/ModImpl.hpp b/loader/src/loader/ModImpl.hpp index cf049d9d..14f1b7f6 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 */ @@ -63,7 +63,7 @@ namespace geode { */ bool m_resourcesLoaded = false; - Impl(Mod* self, ModInfo const& info); + Impl(Mod* self, ModMetadata const& metadata); ~Impl(); Result<> setup(); @@ -84,9 +84,10 @@ namespace geode { bool isEnabled() const; bool isLoaded() const; bool supportsDisabling() const; - bool supportsUnloading() const; bool wasSuccesfullyLoaded() const; ModInfo getModInfo() const; + bool wasSuccessfullyLoaded() const; + ModMetadata getMetadata() const; ghc::filesystem::path getTempDir() const; ghc::filesystem::path getBinaryPath() const; @@ -133,4 +134,4 @@ namespace geode { 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 82751442..7c1e4ed3 100644 --- a/loader/src/loader/ModInfoImpl.cpp +++ b/loader/src/loader/ModInfoImpl.cpp @@ -1,18 +1,17 @@ #include -#include -#include #include #include -#include -#include #include #include "ModInfoImpl.hpp" +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + 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 +20,113 @@ 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); + return ModMetadata::Impl::validateID(id); } 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." - ); + return ModMetadata::Impl::create(json); } 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()); - } + return ModMetadata::Impl::createFromFile(path); } Result ModInfo::Impl::createFromGeodeFile(ghc::filesystem::path const& path) { - GEODE_UNWRAP_INTO(auto unzip, file::Unzip::create(path)); - return ModInfo::createFromGeodeZip(unzip); + return ModMetadata::Impl::createFromGeodeFile(path); } 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}, - }; + return ModMetadata::Impl::createFromGeodeZip(unzip); } ModJson ModInfo::Impl::toJSON() const { - auto json = m_rawJSON; - json["path"] = this->m_path.string(); - json["binary"] = this->m_binaryName; - return json; + return m_metadata.m_rawJSON; } ModJson ModInfo::Impl::getRawJSON() const { - return m_rawJSON; + return m_metadata.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 +144,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,17 +172,17 @@ 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) { @@ -431,32 +213,6 @@ 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); -} - -ModJson& ModInfo::rawJSON() { - return m_impl->m_rawJSON; -} -ModJson const& ModInfo::rawJSON() const { - return m_impl->m_rawJSON; -} - -Result ModInfo::createFromSchemaV010(ModJson const& json) { - return Impl::createFromSchemaV010(json); -} - -Result<> ModInfo::addSpecialFiles(ghc::filesystem::path const& dir) { - return m_impl->addSpecialFiles(dir); -} -Result<> ModInfo::addSpecialFiles(utils::file::Unzip& zip) { - return m_impl->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)) {} @@ -473,4 +229,24 @@ ModInfo& ModInfo::operator=(ModInfo&& other) noexcept { return *this; } -ModInfo::~ModInfo() {} \ No newline at end of file +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; +} + +ModInfo::~ModInfo() = default; + +#pragma clang diagnostic pop diff --git a/loader/src/loader/ModInfoImpl.hpp b/loader/src/loader/ModInfoImpl.hpp index 422ffa89..3ebd981c 100644 --- a/loader/src/loader/ModInfoImpl.hpp +++ b/loader/src/loader/ModInfoImpl.hpp @@ -1,5 +1,7 @@ #pragma once +#include "ModMetadataImpl.hpp" + #include #include #include @@ -10,27 +12,11 @@ using namespace geode::prelude; namespace geode { class 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; + ModMetadata::Impl 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); @@ -52,8 +38,11 @@ namespace geode { std::vector*>> getSpecialFiles(); }; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" class ModInfoImpl { public: static ModInfo::Impl& getImpl(ModInfo& info); }; -} \ No newline at end of file +#pragma clang diagnostic pop +} diff --git a/loader/src/loader/ModMetadataImpl.cpp b/loader/src/loader/ModMetadataImpl.cpp new file mode 100644 index 00000000..f7157dd5 --- /dev/null +++ b/loader/src/loader/ModMetadataImpl.cpp @@ -0,0 +1,519 @@ +#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->mod || !this->mod->isLoaded() || !this->version.compare(this->mod->getVersion()); +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +ModMetadata::Dependency::operator geode::Dependency() { + return {id, version, importance == Importance::Required, mod}; +} +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}; +} +#pragma clang diagnostic pop + +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.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; +} + +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; +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +ModMetadata::operator ModInfo() { + ModInfo info; + auto infoImpl = ModInfoImpl::getImpl(info); + infoImpl.m_metadata = *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::operator ModInfo() const { + ModInfo info; + auto infoImpl = ModInfoImpl::getImpl(info); + infoImpl.m_metadata = *m_impl; + infoImpl.m_issues = m_impl->m_issues; + for (auto& dep : m_impl->m_dependencies) + infoImpl.m_dependencies.push_back(dep); + return info; +} +#pragma clang diagnostic pop + +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"}; + } + } + 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")"); + } +}; diff --git a/loader/src/loader/ModMetadataImpl.hpp b/loader/src/loader/ModMetadataImpl.hpp new file mode 100644 index 00000000..c1cedbfa --- /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: + static ModMetadata::Impl& getImpl(ModMetadata& info); + }; +} diff --git a/loader/src/platform/mac/ModImpl.cpp b/loader/src/platform/mac/ModImpl.cpp index b5232e4d..762ad3ec 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/windows/ModImpl.cpp b/loader/src/platform/windows/ModImpl.cpp index 17784dd1..30edebba 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/ui/internal/GeodeUI.cpp b/loader/src/ui/internal/GeodeUI.cpp index 20ea64fe..b7e1d3e9 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 c736f7be..f5146e84 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 50853e25..ce47c675 100644 --- a/loader/src/ui/internal/info/ModInfoPopup.cpp +++ b/loader/src/ui/internal/info/ModInfoPopup.cpp @@ -11,12 +11,10 @@ #include #include #include -#include #include #include #include #include -#include #include #include #include @@ -28,7 +26,7 @@ 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 +48,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 +56,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 +76,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 +91,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 +108,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 +139,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 +148,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 +159,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 +185,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 +221,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 +285,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 +341,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 +372,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 +391,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 +425,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 +445,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 +460,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 +512,8 @@ void LocalModInfoPopup::onUpdate(CCObject*) { [](IndexItemHandle handle) { return fmt::format( " - {} ({})", - handle->getModInfo().name(), - handle->getModInfo().id() + handle->getMetadata().getName(), + handle->getMetadata().getID() ); } ), @@ -684,11 +680,11 @@ 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; m_installBtnSpr = IconButtonSprite::create( "GE_button_01.png"_spr, @@ -771,8 +767,8 @@ void IndexItemInfoPopup::onInstall(CCObject*) { [](IndexItemHandle handle) { return fmt::format( " - {} ({})", - handle->getModInfo().name(), - handle->getModInfo().id() + handle->getMetadata().getName(), + handle->getMetadata().getID() ); } ), @@ -812,8 +808,8 @@ 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 f1ec3dd7..773c41b2 100644 --- a/loader/src/ui/internal/info/ModInfoPopup.hpp +++ b/loader/src/ui/internal/info/ModInfoPopup.hpp @@ -44,7 +44,7 @@ protected: 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 @@ protected: 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 @@ protected: Mod* m_mod; bool init(Mod* mod, ModListLayer* list); - + void onIssues(CCObject*); void onSettings(CCObject*); void onNoSettings(CCObject*); @@ -81,7 +81,7 @@ protected: void FLAlert_Clicked(FLAlertLayer*, bool) override; CCNode* createLogo(CCSize const& size) override; - ModInfo getModInfo() const override; + ModMetadata getMetadata() const override; LocalModInfoPopup(); @@ -95,7 +95,7 @@ protected: EventListener m_installListener; bool init(IndexItemHandle item, ModListLayer* list); - + void onInstallProgress(ModInstallEvent* event); void onInstall(CCObject*); void onCancel(CCObject*); @@ -104,7 +104,7 @@ protected: void FLAlert_Clicked(FLAlertLayer*, bool) override; CCNode* createLogo(CCSize const& size) override; - ModInfo getModInfo() const override; + ModMetadata getMetadata() const override; IndexItemInfoPopup(); diff --git a/loader/src/ui/internal/list/ModListCell.cpp b/loader/src/ui/internal/list/ModListCell.cpp index eaa23784..5944760d 100644 --- a/loader/src/ui/internal/list/ModListCell.cpp +++ b/loader/src/ui/internal/list/ModListCell.cpp @@ -29,7 +29,7 @@ float ModListCell::getLogoSize() const { } void ModListCell::setupInfo( - ModInfo const& info, + ModMetadata const& metadata, bool spaceForTags, ModListDisplay display ) { @@ -45,9 +45,9 @@ void ModListCell::setupInfo( 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) { @@ -66,7 +66,7 @@ void ModListCell::setupInfo( 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 }); @@ -78,7 +78,7 @@ void ModListCell::setupInfo( versionLabel->setColor({ 0, 255, 0 }); this->addChild(versionLabel); - if (auto tag = info.version().getTag()) { + if (auto tag = metadata.getVersion().getTag()) { auto tagLabel = TagNode::create(tag.value().toString().c_str()); tagLabel->setAnchorPoint({ .0f, .5f }); tagLabel->setScale(.3f); @@ -90,7 +90,7 @@ void ModListCell::setupInfo( this->addChild(tagLabel); } - auto creatorStr = "by " + info.developer(); + auto creatorStr = "by " + metadata.getDeveloper(); auto creatorLabel = CCLabelBMFont::create(creatorStr.c_str(), "goldFont.fnt"); creatorLabel->setScale(.43f); @@ -129,7 +129,7 @@ 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); @@ -235,7 +235,7 @@ bool ModCell::init( 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); @@ -328,11 +328,7 @@ 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); + this->setupInfo(item->getMetadata(), item->getTags().size(), display, item->isInstalled()); auto viewBtn = CCMenuItemSpriteExtra::create(viewSpr, this, menu_selector(IndexItemCell::onInfo)); m_menu->addChild(viewBtn); @@ -355,7 +351,7 @@ bool IndexItemCell::init( x += node->getScaledContentSize().width + 5.f; } } - + this->updateState(); return true; @@ -364,7 +360,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) { diff --git a/loader/src/ui/internal/list/ModListCell.hpp b/loader/src/ui/internal/list/ModListCell.hpp index e4d47f22..3fe4c7ef 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 @@ protected: 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; diff --git a/loader/src/ui/internal/list/ModListLayer.cpp b/loader/src/ui/internal/list/ModListLayer.cpp index f84da844..58103ce0 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,13 @@ 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 (!query.forceVisibility && Loader::get()->isModInstalled(item->getMetadata().getID())) { return std::nullopt; } // make sure all tags match @@ -115,7 +115,7 @@ static std::optional queryMatch(ModListQuery const& query, IndexItemHandle 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) { From 7f449b996e3cb735bbf8c1bb02d53e7be613f80f Mon Sep 17 00:00:00 2001 From: ConfiG Date: Tue, 8 Aug 2023 21:59:13 +0300 Subject: [PATCH 05/36] add utils::game::restart, reset => forceReset --- loader/include/Geode/utils/general.hpp | 4 ++++ loader/launcher/windows/Updater.cpp | 5 +++++ loader/src/load.cpp | 4 ++-- loader/src/loader/LoaderImpl.cpp | 5 ++--- loader/src/loader/LoaderImpl.hpp | 2 +- loader/src/platform/mac/util.mm | 14 +++++++++++++- loader/src/platform/windows/main.cpp | 11 +---------- loader/src/platform/windows/util.cpp | 26 ++++++++++++++++++++++++++ 8 files changed, 54 insertions(+), 17 deletions(-) diff --git a/loader/include/Geode/utils/general.hpp b/loader/include/Geode/utils/general.hpp index ec736efb..c4f5015f 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/launcher/windows/Updater.cpp b/loader/launcher/windows/Updater.cpp index ef5b3cfc..3a1506ef 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/load.cpp b/loader/src/load.cpp index a60259fa..85e05d42 100644 --- a/loader/src/load.cpp +++ b/loader/src/load.cpp @@ -66,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; } @@ -84,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/LoaderImpl.cpp b/loader/src/loader/LoaderImpl.cpp index caca7b4d..99d12cb6 100644 --- a/loader/src/loader/LoaderImpl.cpp +++ b/loader/src/loader/LoaderImpl.cpp @@ -430,9 +430,8 @@ 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; } @@ -447,7 +446,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() { diff --git a/loader/src/loader/LoaderImpl.hpp b/loader/src/loader/LoaderImpl.hpp index 2869ac2c..7b4fd48e 100644 --- a/loader/src/loader/LoaderImpl.hpp +++ b/loader/src/loader/LoaderImpl.hpp @@ -116,7 +116,7 @@ namespace geode { Result loadModFromInfo(ModInfo const& info); Result<> setup(); - void reset(); + void forceReset(); Result<> saveData(); Result<> loadData(); diff --git a/loader/src/platform/mac/util.mm b/loader/src/platform/mac/util.mm index 80838c56..250e0047 100644 --- a/loader/src/platform/mac/util.mm +++ b/loader/src/platform/mac/util.mm @@ -10,6 +10,7 @@ using namespace geode::prelude; #include #include #include +#include bool utils::clipboard::write(std::string const& data) { [[NSPasteboard generalPasteboard] clearContents]; @@ -180,7 +181,7 @@ ghc::filesystem::path dirs::getGameDir() { _NSGetExecutablePath(gddir.data(), &out); ghc::filesystem::path gdpath = gddir.data(); - auto currentPath = gdpath.parent_path().parent_path(); + auto currentPath = gdpath.parent_path().parent_path(); return currentPath; }(); @@ -200,4 +201,15 @@ 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; + } + + // TODO: implement restarting on mac + log::warn("Restarting is not yet implemented on macOS!"); +} + #endif diff --git a/loader/src/platform/windows/main.cpp b/loader/src/platform/windows/main.cpp index 99376181..28f9979d 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 062d0b19..7ecb7322 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 From 52001285441001e372c961069db46a0fbbdbd729 Mon Sep 17 00:00:00 2001 From: ConfiG Date: Tue, 8 Aug 2023 23:40:14 +0300 Subject: [PATCH 06/36] huge dependency stuff refactor --- loader/include/Geode/loader/Index.hpp | 1 + loader/include/Geode/loader/Loader.hpp | 33 +- loader/include/Geode/loader/Mod.hpp | 39 +- loader/include/Geode/loader/ModInfo.hpp | 20 +- loader/include/Geode/loader/ModMetadata.hpp | 86 ++-- loader/src/hooks/MenuLayer.cpp | 319 ++++++------ loader/src/internal/crashlog.cpp | 4 +- loader/src/loader/Index.cpp | 13 +- loader/src/loader/Loader.cpp | 4 + loader/src/loader/LoaderImpl.cpp | 504 ++++++++++++------- loader/src/loader/LoaderImpl.hpp | 24 +- loader/src/loader/Mod.cpp | 26 +- loader/src/loader/ModImpl.cpp | 248 +++++---- loader/src/loader/ModImpl.hpp | 20 +- loader/src/loader/ModInfoImpl.cpp | 68 ++- loader/src/loader/ModInfoImpl.hpp | 26 +- loader/src/loader/ModMetadataImpl.cpp | 9 +- loader/src/ui/internal/info/ModInfoPopup.cpp | 6 +- loader/src/ui/internal/list/ModListCell.cpp | 125 +++-- loader/src/ui/internal/list/ModListCell.hpp | 2 + loader/src/ui/internal/list/ModListLayer.cpp | 38 +- loader/src/ui/internal/list/ModListLayer.hpp | 2 +- 22 files changed, 907 insertions(+), 710 deletions(-) diff --git a/loader/include/Geode/loader/Index.hpp b/loader/include/Geode/loader/Index.hpp index 4a027823..fb0830c1 100644 --- a/loader/include/Geode/loader/Index.hpp +++ b/loader/include/Geode/loader/Index.hpp @@ -115,6 +115,7 @@ namespace geode { std::unordered_set getAvailablePlatforms() const; bool isFeatured() const; std::unordered_set getTags() const; + bool isInstalled() const; IndexItem(); ~IndexItem(); diff --git a/loader/include/Geode/loader/Loader.hpp b/loader/include/Geode/loader/Loader.hpp index 96e542d8..1b38dd69 100644 --- a/loader/include/Geode/loader/Loader.hpp +++ b/loader/include/Geode/loader/Loader.hpp @@ -4,6 +4,7 @@ #include "../utils/Result.hpp" #include "../utils/MiniFunction.hpp" #include "Log.hpp" +#include "ModEvent.hpp" #include "ModInfo.hpp" #include "ModMetadata.hpp" #include "Types.hpp" @@ -19,6 +20,24 @@ namespace geode { std::string reason; }; + struct LoadProblem { + enum class Type : uint8_t { + Unknown, + Suggestion, + Recommendation, + InvalidFile, + Duplicate, + SetupFailed, + LoadFailed, + EnableFailed, + MissingDependency, + PresentIncompatibility + }; + Type type; + std::variant cause; + std::string message; + }; + class LoaderImpl; class GEODE_DLL Loader { @@ -37,8 +56,8 @@ namespace geode { void dispatchScheduledFunctions(Mod* mod); friend void GEODE_CALL ::geode_implicit_load(Mod*); - Result loadModFromInfo(ModInfo const& info); - + [[deprecated]] Result loadModFromInfo(ModInfo const& info); + Mod* takeNextMod(); public: @@ -53,16 +72,18 @@ 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(); 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(); [[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/Mod.hpp b/loader/include/Geode/loader/Mod.hpp index 8a0d869f..569a2a6d 100644 --- a/loader/include/Geode/loader/Mod.hpp +++ b/loader/include/Geode/loader/Mod.hpp @@ -80,11 +80,10 @@ namespace geode { bool isEnabled() const; bool isLoaded() const; bool supportsDisabling() const; - bool supportsUnloading() const; - bool wasSuccesfullyLoaded() const; - ModInfo getModInfo() const; + bool canDisable() const; + bool canEnable() const; [[deprecated]] bool supportsUnloading() const; - [[deprecated("wasSuccessfullyLoaded")]] bool wasSuccesfullyLoaded() const; + [[deprecated("use wasSuccessfullyLoaded instead")]] bool wasSuccesfullyLoaded() const; bool wasSuccessfullyLoaded() const; [[deprecated("use getMetadata instead")]] ModInfo getModInfo() const; ModMetadata getMetadata() const; @@ -299,7 +298,7 @@ namespace geode { * @returns Successful result on success, * errorful result with info on error */ - Result<> loadBinary(); + [[deprecated]] Result<> loadBinary(); /** * Disable & unload this mod @@ -308,7 +307,7 @@ namespace geode { * @returns Successful result on success, * errorful result with info on error */ - Result<> unloadBinary(); + [[deprecated]] Result<> unloadBinary(); /** * Enable this mod @@ -325,10 +324,7 @@ namespace geode { Result<> disable(); /** - * Disable & unload this mod (if supported), then delete the mod's - * .geode package. If unloading isn't supported, the mod's binary - * will stay loaded, and in all cases the Mod* instance will still - * exist and be interactable. + * Disable this mod (if supported), then delete the mod's .geode package. * @returns Successful result on success, * errorful result with info on error */ @@ -341,6 +337,16 @@ namespace geode { */ bool depends(std::string const& id) const; + /** + * Update the state of each of the + * dependencies. Depending on if the + * mod has unresolved dependencies, + * it will either be loaded or unloaded + * @returns Error. + * @deprecated No longer needed. + */ + [[deprecated("no longer needed")]] Result<> updateDependencies(); + /** * Check whether all the required * dependencies for this mod have @@ -350,21 +356,20 @@ namespace geode { */ bool hasUnresolvedDependencies() const; /** - * Update the state of each of the - * dependencies. Depending on if the - * mod has unresolved dependencies, - * it will either be loaded or unloaded + * Check whether none of the + * incompatibilities with this mod are loaded * @returns True if the mod has unresolved - * dependencies, false if not. + * incompatibilities, false if not. */ - Result<> updateDependencies(); + bool hasUnresolvedIncompatibilities() const; /** * Get a list of all the unresolved * dependencies this mod has * @returns List of all the unresolved * dependencies + * @deprecated Use Loader::getProblems instead. */ - std::vector 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 feefcbc6..431fd653 100644 --- a/loader/include/Geode/loader/ModInfo.hpp +++ b/loader/include/Geode/loader/ModInfo.hpp @@ -36,6 +36,7 @@ namespace geode { */ class GEODE_DLL [[deprecated("use ModMetadata instead")]] ModInfo { class Impl; +#pragma warning(suppress : 4996) std::unique_ptr m_impl; public: @@ -200,18 +201,29 @@ namespace geode { operator ModMetadata() const; private: + ModJson& rawJSON(); + ModJson const& rawJSON() const; + /** + * Version is passed for backwards + * compatibility if we update the mod.json + * format + */ + static Result createFromSchemaV010(ModJson const& json); + + Result<> addSpecialFiles(ghc::filesystem::path const& dir); + Result<> addSpecialFiles(utils::file::Unzip& zip); + + std::vector*>> getSpecialFiles(); + friend class ModInfoImpl; friend class ModMetadata; }; } -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" template <> -struct json::Serialize { +struct [[deprecated]] json::Serialize { static json::Value to_json(geode::ModInfo const& info) { return info.toJSON(); } }; -#pragma clang diagnostic pop diff --git a/loader/include/Geode/loader/ModMetadata.hpp b/loader/include/Geode/loader/ModMetadata.hpp index 4364d77b..23d2f173 100644 --- a/loader/include/Geode/loader/ModMetadata.hpp +++ b/loader/include/Geode/loader/ModMetadata.hpp @@ -44,14 +44,10 @@ namespace geode { Mod* mod = nullptr; [[nodiscard]] bool isResolved() const; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" -#pragma ide diagnostic ignored "google-explicit-constructor" - operator geode::Dependency(); - operator geode::Dependency() const; + [[deprecated]] operator geode::Dependency(); + [[deprecated]] operator geode::Dependency() const; - static Dependency fromDeprecated(geode::Dependency const& value); -#pragma clang diagnostic pop + [[deprecated]] static Dependency fromDeprecated(geode::Dependency const& value); }; struct GEODE_DLL Incompatibility { @@ -65,29 +61,25 @@ namespace geode { std::string info; std::optional url; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" -#pragma ide diagnostic ignored "google-explicit-constructor" - operator geode::IssuesInfo(); - operator geode::IssuesInfo() const; + [[deprecated]] operator geode::IssuesInfo(); + [[deprecated]] operator geode::IssuesInfo() const; - static IssuesInfo fromDeprecated(geode::IssuesInfo const& value); -#pragma clang diagnostic pop + [[deprecated]] static IssuesInfo fromDeprecated(geode::IssuesInfo const& value); }; /** * Path to the mod file */ - [[maybe_unused, nodiscard]] ghc::filesystem::path getPath() const; + [[nodiscard]] ghc::filesystem::path getPath() const; /** * Name of the platform binary within * the mod zip */ - [[maybe_unused, nodiscard]] std::string getBinaryName() const; + [[nodiscard]] std::string getBinaryName() const; /** * Mod Version. Should follow semantic versioning. */ - [[maybe_unused, nodiscard]] VersionInfo getVersion() const; + [[nodiscard]] VersionInfo getVersion() const; /** * Human-readable ID of the Mod. * Recommended to be in the format @@ -97,14 +89,14 @@ namespace geode { * be restricted to the ASCII * character set. */ - [[maybe_unused, nodiscard]] std::string getID() const; + [[nodiscard]] std::string getID() const; /** * Name of the mod. May contain * spaces & punctuation, but should * be restricted to the ASCII * character set. */ - [[maybe_unused, nodiscard]] std::string getName() const; + [[nodiscard]] std::string getName() const; /** * The name of the head developer. * Should be a single name, like @@ -116,98 +108,94 @@ namespace geode { * should be named in `m_credits` * instead. */ - [[maybe_unused, nodiscard]] std::string getDeveloper() const; + [[nodiscard]] std::string getDeveloper() const; /** * Short & concise description of the * mod. */ - [[maybe_unused, nodiscard]] std::optional getDescription() const; + [[nodiscard]] std::optional getDescription() const; /** * Detailed description of the mod, written in Markdown (see * ) for more info */ - [[maybe_unused, nodiscard]] std::optional getDetails() const; + [[nodiscard]] std::optional getDetails() const; /** * Changelog for the mod, written in Markdown (see * ) for more info */ - [[maybe_unused, nodiscard]] std::optional getChangelog() const; + [[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) */ - [[maybe_unused, nodiscard]] std::optional getSupportInfo() const; + [[nodiscard]] std::optional getSupportInfo() const; /** * Git Repository of the mod */ - [[maybe_unused, nodiscard]] std::optional getRepository() const; + [[nodiscard]] std::optional getRepository() const; /** * Info about where users should report issues and request help */ - [[maybe_unused, nodiscard]] std::optional getIssues() const; + [[nodiscard]] std::optional getIssues() const; /** * Dependencies */ - [[maybe_unused, nodiscard]] std::vector getDependencies() const; + [[nodiscard]] std::vector getDependencies() const; /** * Incompatibilities */ - [[maybe_unused, nodiscard]] std::vector getIncompatibilities() const; + [[nodiscard]] std::vector getIncompatibilities() const; /** * Mod spritesheet names */ - [[maybe_unused, nodiscard]] std::vector getSpritesheets() const; + [[nodiscard]] std::vector getSpritesheets() const; /** * Mod settings * @note Not a map because insertion order must be preserved */ - [[maybe_unused, nodiscard]] std::vector> getSettings() const; + [[nodiscard]] std::vector> getSettings() const; /** * Whether this mod has to be loaded before the loading screen or not */ - [[maybe_unused, nodiscard]] bool needsEarlyLoad() const; + [[ nodiscard]] bool needsEarlyLoad() const; /** * Whether this mod is an API or not */ - [[maybe_unused, nodiscard]] bool isAPI() const; + [[nodiscard]] bool isAPI() const; /** * Create ModInfo from an unzipped .geode package */ - [[maybe_unused]] static Result createFromGeodeZip(utils::file::Unzip& zip); + static Result createFromGeodeZip(utils::file::Unzip& zip); /** * Create ModInfo from a .geode package */ - [[maybe_unused]] static Result createFromGeodeFile(ghc::filesystem::path const& path); + static Result createFromGeodeFile(ghc::filesystem::path const& path); /** * Create ModInfo from a mod.json file */ - [[maybe_unused]] static Result createFromFile(ghc::filesystem::path const& path); + static Result createFromFile(ghc::filesystem::path const& path); /** * Create ModInfo from a parsed json document */ - [[maybe_unused]] static Result create(ModJson const& json); + static Result create(ModJson const& json); /** * Convert to JSON. Essentially same as getRawJSON except dynamically * adds runtime fields like path */ - [[maybe_unused, nodiscard]] ModJson toJSON() const; + [[nodiscard]] ModJson toJSON() const; /** * Get the raw JSON file */ - [[maybe_unused, nodiscard]] ModJson getRawJSON() const; + [[nodiscard]] ModJson getRawJSON() const; - [[maybe_unused]] bool operator==(ModMetadata const& other) const; + bool operator==(ModMetadata const& other) const; - [[maybe_unused]] static bool validateID(std::string const& id); + static bool validateID(std::string const& id); -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" -#pragma ide diagnostic ignored "google-explicit-constructor" - operator ModInfo(); - operator ModInfo() const; -#pragma clang diagnostic pop + [[deprecated]] operator ModInfo(); + [[deprecated]] operator ModInfo() const; private: /** @@ -226,11 +214,7 @@ namespace geode { friend class ModMetadataImpl; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - // ModInfo => ModMetadata conversion stuff - friend class ModInfo::Impl; -#pragma clang diagnostic pop + friend class ModInfo; }; } diff --git a/loader/src/hooks/MenuLayer.cpp b/loader/src/hooks/MenuLayer.cpp index eb89a19d..4942a0ec 100644 --- a/loader/src/hooks/MenuLayer.cpp +++ b/loader/src/hooks/MenuLayer.cpp @@ -23,34 +23,34 @@ class CustomMenuLayer; static Ref 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 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 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 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(); - } + // 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; - } + this->addUpdateIndicator(); + + return true; + } - void onIndexUpdate(IndexUpdateEvent* event) { - if ( - std::holds_alternative(event->status) || - std::holds_alternative(event->status) - ) { - this->addUpdateIndicator(); - } - } + 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 addUpdateIndicator() { + if (Index::get()->areUpdatesAvailable()) { + auto icon = CCSprite::createWithSpriteFrameName("updates-available.png"_spr); + icon->setPosition( + m_fields->m_geodeButton->getContentSize() - CCSize { 10.f, 10.f } + ); + icon->setZOrder(99); + icon->setScale(.5f); + m_fields->m_geodeButton->addChild(icon); + } + } - void onMissingTextures(CCObject*) { - - #ifdef GEODE_IS_DESKTOP + void onMissingTextures(CCObject*) { + + #ifdef GEODE_IS_DESKTOP - (void) utils::file::createDirectoryAll(dirs::getGeodeDir() / "update" / "resources"); + (void) utils::file::createDirectoryAll(dirs::getGeodeDir() / "update" / "resources"); - createQuickPopup( - "Missing Textures", - "You appear to be missing textures, and the automatic texture fixer " - "hasn't fixed the issue.\n" - "Download 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(); - } - } - ); + 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 + #else - // dunno if we can auto-create target directory on mobile, nor if the - // user has access to moving stuff there + // dunno if we can auto-create target directory on mobile, nor if the + // user has access to moving stuff there - FLAlertLayer::create( - "Missing Textures", - "You appear to be missing textures, and the automatic texture fixer " - "hasn't fixed the issue.\n" - "**Report this bug to the Geode developers**. It is very likely " - "that your game will crash until the issue is resolved.", - "OK" - )->show(); + FLAlertLayer::create( + "Missing Textures", + "You appear to be missing textures, and the automatic texture fixer " + "hasn't fixed the issue.\n" + "**Report this bug to the Geode developers**. It is very likely " + "that your game will crash until the issue is resolved.", + "OK" + )->show(); - #endif - } + #endif + } void onGeode(CCObject*) { ModListLayer::scene(); diff --git a/loader/src/internal/crashlog.cpp b/loader/src/internal/crashlog.cpp index e5b3af57..5995dfb3 100644 --- a/loader/src/internal/crashlog.cpp +++ b/loader/src/internal/crashlog.cpp @@ -18,7 +18,7 @@ static std::string getDateString(bool filesafe) { static void printGeodeInfo(std::stringstream& stream) { stream << "Loader Version: " << Loader::get()->getVersion().toString() << "\n" << "Installed mods: " << Loader::get()->getAllMods().size() << "\n" - << "Failed mods: " << Loader::get()->getFailedMods().size() << "\n"; + << "Problems: " << Loader::get()->getProblems().size() << "\n"; } static void printMods(std::stringstream& stream) { @@ -84,4 +84,4 @@ std::string crashlog::writeCrashlog(geode::Mod* faultyMod, std::string const& in actualFile.close(); return file.str(); -} \ No newline at end of file +} diff --git a/loader/src/loader/Index.cpp b/loader/src/loader/Index.cpp index bf366247..b564b410 100644 --- a/loader/src/loader/Index.cpp +++ b/loader/src/loader/Index.cpp @@ -64,6 +64,8 @@ public: static Result> create( ghc::filesystem::path const& dir ); + + bool isInstalled() const; }; IndexItem::IndexItem() : m_impl(std::make_unique()) {} @@ -101,6 +103,10 @@ std::unordered_set IndexItem::getTags() const { return m_impl->m_tags; } +bool IndexItem::isInstalled() const { + return m_impl->isInstalled(); +} + Result IndexItem::Impl::create(ghc::filesystem::path const& dir) { GEODE_UNWRAP_INTO( auto entry, file::readJson(dir / "entry.json") @@ -134,6 +140,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) { @@ -595,9 +605,6 @@ void Index::Impl::installNext(size_t index, IndexInstallList const& list) { } } - // load mods - Loader::get()->refreshModsList(); - auto const& eventModID = list.target->getMetadata().getID(); Loader::get()->queueInGDThread([eventModID]() { ModInstallEvent(eventModID, UpdateFinished()).post(); diff --git a/loader/src/loader/Loader.cpp b/loader/src/loader/Loader.cpp index 6335ccee..8d55893c 100644 --- a/loader/src/loader/Loader.cpp +++ b/loader/src/loader/Loader.cpp @@ -95,6 +95,10 @@ std::vector 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 99d12cb6..23ea529b 100644 --- a/loader/src/loader/LoaderImpl.cpp +++ b/loader/src/loader/LoaderImpl.cpp @@ -1,4 +1,3 @@ - #include "LoaderImpl.hpp" #include @@ -10,24 +9,19 @@ #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; @@ -36,9 +30,9 @@ Loader::Impl* LoaderImpl::get() { return Loader::get()->m_impl.get(); } -Loader::Impl::Impl() {} +Loader::Impl::Impl() = default; -Loader::Impl::~Impl() {} +Loader::Impl::~Impl() = default; // Initialization @@ -92,15 +86,10 @@ Result<> Loader::Impl::setup() { this->setupIPC(); this->createDirectories(); - auto sett = this->loadData(); - if (!sett) { - log::warn("Unable to load loader settings: {}", sett.unwrapErr()); - } - this->refreshModsList(); - this->queueInGDThread([]() { - Loader::get()->addSearchPaths(); - }); + this->addSearchPaths(); + + this->refreshModGraph(); m_isSetup = true; @@ -132,12 +121,19 @@ std::vector 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 @@ -170,7 +166,7 @@ bool Loader::Impl::isModVersionSupported(VersionInfo const& version) { Result<> Loader::Impl::saveData() { // save mods' data for (auto& [id, mod] : m_mods) { - Mod::get()->setSavedValue("should-load-" + id, mod->isEnabled()); + Mod::get()->setSavedValue("should-load-" + id, mod->isUninstalled() || mod->isEnabled()); auto r = mod->saveData(); if (!r) { log::warn("Unable to save data for mod \"{}\": {}", mod->getID(), r.unwrapErr()); @@ -178,15 +174,11 @@ Result<> Loader::Impl::saveData() { } // save loader data GEODE_UNWRAP(Mod::get()->saveData()); - + return Ok(); } Result<> Loader::Impl::loadData() { - auto e = Mod::get()->loadData(); - if (!e) { - log::warn("Unable to load loader settings: {}", e.unwrapErr()); - } for (auto& [_, mod] : m_mods) { auto r = mod->loadData(); if (!r) { @@ -198,60 +190,6 @@ Result<> Loader::Impl::loadData() { // Mod loading -Result 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(); } @@ -308,116 +246,320 @@ void Loader::Impl::updateModResources(Mod* mod) { // Dependencies and refreshing -void Loader::Impl::loadModsFromDirectory( - ghc::filesystem::path const& dir, - bool recursive -) { - log::debug("Searching {}", dir); - for (auto const& entry : ghc::filesystem::directory_iterator(dir)) { - // recursively search directories - if (ghc::filesystem::is_directory(entry) && recursive) { - this->loadModsFromDirectory(entry.path(), true); - continue; - } +Result 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"); +} - // skip this entry if its extension is not .geode - if (entry.path().extension() != GEODE_MOD_EXTENSION) { - continue; - } - // skip this entry if it's already loaded - if (map::contains(m_mods, [entry](Mod* p) -> bool { - return p->m_impl->m_info.path() == entry.path(); - })) { - continue; - } - - // if mods should be loaded immediately, do that - if (m_earlyLoadFinished) { - auto load = this->loadModFromFile(entry); - if (!load) { - log::error("Unable to load {}: {}", entry, load.unwrapErr()); - } - } - // otherwise collect mods to load first to make sure the correct - // versions of the mods are loaded and that early-loaded mods are - // loaded early - else { - auto res = ModInfo::createFromGeodeFile(entry.path()); - if (!res) { - m_invalidMods.push_back(InvalidGeodeFile { - .path = entry.path(), - .reason = res.unwrapErr(), - }); - continue; - } - auto info = res.unwrap(); - - // skip this entry if it's already set to be loaded - if (ranges::contains(m_modsToLoad, info)) { - continue; - } - - // add to list of mods to load - m_modsToLoad.push_back(info); - } - } +void Loader::Impl::loadModsFromDirectory(ghc::filesystem::path const& dir, bool recursive) { + log::error("Called deprecated stub: Loader::loadModsFromDirectory"); } void Loader::Impl::refreshModsList() { - log::debug("Loading mods..."); - - // find mods - for (auto& dir : m_modSearchDirectories) { - this->loadModsFromDirectory(dir); - } - - // load early-load mods first - for (auto& mod : m_modsToLoad) { - if (mod.needsEarlyLoad()) { - auto load = this->loadModFromInfo(mod); - if (!load) { - log::error("Unable to load {}: {}", mod.id(), load.unwrapErr()); - - m_invalidMods.push_back(InvalidGeodeFile { - .path = mod.path(), - .reason = load.unwrapErr(), - }); - } - } - } - - // UI can be loaded now - m_earlyLoadFinished = true; - m_earlyLoadFinishedCV.notify_all(); - - // load the rest of the mods - for (auto& mod : m_modsToLoad) { - if (!mod.needsEarlyLoad()) { - auto load = this->loadModFromInfo(mod); - if (!load) { - log::error("Unable to load {}: {}", mod.id(), load.unwrapErr()); - - m_invalidMods.push_back(InvalidGeodeFile { - .path = mod.path(), - .reason = load.unwrapErr(), - }); - } - } - } - m_modsToLoad.clear(); + log::error("Called deprecated stub: Loader::refreshModsList"); } void Loader::Impl::updateAllDependencies() { - for (auto const& [_, mod] : m_mods) { - (void)mod->updateDependencies(); + log::error("Called deprecated stub: Loader::updateAllDependencies"); +} + +void Loader::Impl::queueMods(std::vector& modQueue) { + for (auto const& dir : m_modSearchDirectories) { + log::debug("Searching {}", dir); + log::pushNest(); + for (auto const& entry : ghc::filesystem::directory_iterator(dir)) { + if (!ghc::filesystem::is_regular_file(entry) || + entry.path().extension() != GEODE_MOD_EXTENSION) + continue; + + log::debug("Found {}", entry.path().filename()); + log::pushNest(); + + auto res = ModMetadata::createFromGeodeFile(entry.path()); + if (!res) { + m_problems.push_back({ + LoadProblem::Type::InvalidFile, + entry.path(), + res.unwrapErr() + }); + log::error("Failed to queue: {}", res.unwrapErr()); + log::popNest(); + continue; + } + auto modMetadata = res.unwrap(); + + log::debug("id: {}", modMetadata.getID()); + log::debug("version: {}", modMetadata.getVersion()); + + if (std::find_if(modQueue.begin(), modQueue.end(), [&](auto& item) { + return modMetadata.getID() == item.getID(); + }) != modQueue.end()) { + m_problems.push_back({ + LoadProblem::Type::Duplicate, + modMetadata, + "a mod with the same ID is already present" + }); + log::error("Failed to queue: a mod with the same ID is already queued"); + log::popNest(); + continue; + } + + modQueue.push_back(modMetadata); + log::popNest(); + } + log::popNest(); } } +void Loader::Impl::populateModList(std::vector& modQueue) { + std::vector toRemove; + for (auto& [id, mod] : m_mods) { + if (id == "geode.loader") + continue; + delete mod; + toRemove.push_back(id); + } + for (auto const& id : toRemove) { + m_mods.erase(id); + } + + for (auto const& metadata : modQueue) { + log::debug("{} {}", metadata.getID(), metadata.getVersion()); + log::pushNest(); + + auto mod = new Mod(metadata); + + auto res = mod->m_impl->setup(); + if (!res) { + m_problems.push_back({ + LoadProblem::Type::SetupFailed, + mod, + res.unwrapErr() + }); + log::error("Failed to set up: {}", res.unwrapErr()); + log::popNest(); + continue; + } + + m_mods.insert({metadata.getID(), mod}); + + queueInGDThread([this, mod]() { + auto searchPath = dirs::getModRuntimeDir() / mod->getID() / "resources"; + CCFileUtils::get()->addSearchPath(searchPath.string().c_str()); + updateModResources(mod); + }); + + log::popNest(); + } +} + +void Loader::Impl::buildModGraph() { + for (auto const& [id, mod] : m_mods) { + log::debug("{}", mod->getID()); + log::pushNest(); + for (auto& dependency : mod->m_impl->m_metadata.m_impl->m_dependencies) { + log::debug("{}", dependency.id); + if (!m_mods.contains(dependency.id)) { + dependency.mod = nullptr; + continue; + } + + dependency.mod = m_mods[dependency.id]; + + if (!dependency.version.compare(dependency.mod->getVersion())) { + dependency.mod = nullptr; + continue; + } + + if (dependency.importance != ModMetadata::Dependency::Importance::Required || dependency.mod == nullptr) + continue; + + dependency.mod->m_impl->m_dependants.push_back(mod); + } + for (auto& incompatibility : mod->m_impl->m_metadata.m_impl->m_incompatibilities) { + incompatibility.mod = + m_mods.contains(incompatibility.id) ? m_mods[incompatibility.id] : nullptr; + } + log::popNest(); + } +} + +void Loader::Impl::loadModGraph(Mod* node) { + if (node->hasUnresolvedDependencies()) + return; + if (node->hasUnresolvedIncompatibilities()) + return; + + log::debug("{} {}", node->getID(), node->getVersion()); + log::pushNest(); + + if (node->isLoaded()) { + for (auto const& dep : node->m_impl->m_dependants) { + this->loadModGraph(dep); + } + log::popNest(); + return; + } + + log::debug("Load"); + auto res = node->m_impl->loadBinary(); + if (!res) { + m_problems.push_back({ + LoadProblem::Type::LoadFailed, + node, + res.unwrapErr() + }); + log::error("Failed to load binary: {}", res.unwrapErr()); + log::popNest(); + return; + } + + if (Mod::get()->getSavedValue("should-load-" + node->getID(), true)) { + log::debug("Enable"); + res = node->m_impl->enable(); + if (!res) { + node->m_impl->m_enabled = true; + (void)node->m_impl->disable(); + m_problems.push_back({ + LoadProblem::Type::EnableFailed, + node, + res.unwrapErr() + }); + log::error("Failed to enable: {}", res.unwrapErr()); + log::popNest(); + return; + } + + for (auto const& dep : node->m_impl->m_dependants) { + this->loadModGraph(dep); + } + } + + log::popNest(); +} + +void Loader::Impl::findProblems() { + for (auto const& [id, mod] : m_mods) { + log::debug(id); + log::pushNest(); + + for (auto const& dep : mod->getMetadata().getDependencies()) { + if (dep.mod && dep.mod->isLoaded() && dep.version.compare(dep.mod->getVersion())) + continue; + switch(dep.importance) { + case ModMetadata::Dependency::Importance::Suggested: + m_problems.push_back({ + LoadProblem::Type::Suggestion, + mod, + fmt::format("{} {}", dep.id, dep.version.toString()) + }); + log::info("{} suggests {} {}", id, dep.id, dep.version); + break; + case ModMetadata::Dependency::Importance::Recommended: + m_problems.push_back({ + LoadProblem::Type::Recommendation, + mod, + fmt::format("{} {}", dep.id, dep.version.toString()) + }); + log::warn("{} recommends {} {}", id, dep.id, dep.version); + break; + case ModMetadata::Dependency::Importance::Required: + m_problems.push_back({ + LoadProblem::Type::MissingDependency, + mod, + fmt::format("{} {}", dep.id, dep.version.toString()) + }); + log::error("{} requires {} {}", id, dep.id, dep.version); + break; + } + } + + for (auto const& dep : mod->getMetadata().getIncompatibilities()) { + if (!dep.mod || !dep.version.compare(dep.mod->getVersion())) + continue; + m_problems.push_back({ + LoadProblem::Type::PresentIncompatibility, + mod, + fmt::format("{} {}", dep.id, dep.version.toString()) + }); + log::error("{} is incompatible with {} {}", id, dep.id, dep.version); + } + + Mod* myEpicMod = mod; // clang fix + // if the mod is not loaded but there are no problems related to it + if (!mod->isLoaded() && !std::any_of(m_problems.begin(), m_problems.end(), [myEpicMod](auto& item) { + return std::holds_alternative(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(); + } +} + +void Loader::Impl::refreshModGraph() { + log::info("Refreshing mod graph..."); + log::pushNest(); + + if (m_mods.size() > 1) { + log::error("Cannot refresh mod graph after startup"); + log::popNest(); + return; + } + + m_problems.clear(); + + log::debug("Queueing mods"); + log::pushNest(); + std::vector modQueue; + this->queueMods(modQueue); + log::popNest(); + + log::debug("Populating mod list"); + log::pushNest(); + this->populateModList(modQueue); + modQueue.clear(); + log::popNest(); + + log::debug("Building mod graph"); + log::pushNest(); + this->buildModGraph(); + log::popNest(); + + // TODO: not early load + log::debug("Loading mods"); + log::pushNest(); + for (auto const& dep : Mod::get()->m_impl->m_dependants) { + this->loadModGraph(dep); + } + log::popNest(); + + log::debug("Finding problems"); + log::pushNest(); + this->findProblems(); + log::popNest(); + + m_earlyLoadFinished = true; + m_earlyLoadFinishedCV.notify_all(); + + log::popNest(); +} + +std::vector Loader::Impl::getProblems() const { + return m_problems; +} + void Loader::Impl::waitForModsToBeLoaded() { auto lock = std::unique_lock(m_earlyLoadFinishedMutex); log::debug("Waiting for mods to be loaded... {}", bool(m_earlyLoadFinished)); diff --git a/loader/src/loader/LoaderImpl.hpp b/loader/src/loader/LoaderImpl.hpp index 7b4fd48e..aac71705 100644 --- a/loader/src/loader/LoaderImpl.hpp +++ b/loader/src/loader/LoaderImpl.hpp @@ -54,8 +54,7 @@ namespace geode { mutable std::mutex m_mutex; std::vector m_modSearchDirectories; - std::vector m_modsToLoad; - std::vector m_invalidMods; + std::vector m_problems; std::unordered_map m_mods; std::vector m_texturePaths; bool m_isSetup = false; @@ -113,7 +112,7 @@ namespace geode { friend void GEODE_CALL ::geode_implicit_load(Mod*); - Result loadModFromInfo(ModInfo const& info); + [[deprecated]] Result loadModFromInfo(ModInfo const& info); Result<> setup(); void forceReset(); @@ -126,17 +125,24 @@ 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); + void findProblems(); + void refreshModGraph(); bool isModInstalled(std::string const& id) const; Mod* getInstalledMod(std::string const& id) const; bool isModLoaded(std::string const& id) const; Mod* getLoadedMod(std::string const& id) const; std::vector 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); diff --git a/loader/src/loader/Mod.cpp b/loader/src/loader/Mod.cpp index 46b8774e..b6696fcc 100644 --- a/loader/src/loader/Mod.cpp +++ b/loader/src/loader/Mod.cpp @@ -4,6 +4,7 @@ using namespace geode::prelude; +#pragma warning(suppress : 4996) Mod::Mod(ModInfo const& info) : m_impl(std::make_unique(this, info)) {} Mod::Mod(ModMetadata const& metadata) : m_impl(std::make_unique(this, metadata)) {} @@ -53,6 +54,14 @@ bool Mod::supportsDisabling() const { return m_impl->supportsDisabling(); } +bool Mod::canDisable() const { + return m_impl->canDisable(); +} + +bool Mod::canEnable() const { + return m_impl->canEnable(); +} + bool Mod::supportsUnloading() const { return false; } @@ -153,11 +162,11 @@ Result<> Mod::unpatch(Patch* patch) { } Result<> Mod::loadBinary() { - return m_impl->loadBinary(); + return Err("Load mod binaries after startup is not supported"); } Result<> Mod::unloadBinary() { - return m_impl->unloadBinary(); + return Err("Unloading mod binaries is not supported"); } Result<> Mod::enable() { @@ -180,14 +189,19 @@ bool Mod::depends(std::string const& id) const { return m_impl->depends(id); } -bool Mod::hasUnresolvedDependencies() const { - return m_impl->hasUnresolvedDependencies(); -} - Result<> Mod::updateDependencies() { return m_impl->updateDependencies(); } +bool Mod::hasUnresolvedDependencies() const { + return m_impl->hasUnresolvedDependencies(); +} + +bool Mod::hasUnresolvedIncompatibilities() const { + return m_impl->hasUnresolvedIncompatibilities(); +} + +#pragma warning(suppress : 4996) std::vector Mod::getUnresolvedDependencies() { return m_impl->getUnresolvedDependencies(); } diff --git a/loader/src/loader/ModImpl.cpp b/loader/src/loader/ModImpl.cpp index 78f58c70..829cbcfd 100644 --- a/loader/src/loader/ModImpl.cpp +++ b/loader/src/loader/ModImpl.cpp @@ -28,9 +28,7 @@ Mod::Impl* ModImpl::getImpl(Mod* mod) { Mod::Impl::Impl(Mod* self, ModMetadata const& metadata) : m_self(self), m_metadata(metadata) { } -Mod::Impl::~Impl() { - (void)this->unloadBinary(); -} +Mod::Impl::~Impl() = default; Result<> Mod::Impl::setup() { m_saveDirPath = dirs::getModsSaveDir() / m_metadata.getID(); @@ -110,11 +108,23 @@ bool Mod::Impl::isLoaded() const { } bool Mod::Impl::supportsDisabling() const { - return m_info.supportsDisabling(); + return m_metadata.getID() != "geode.loader" && !m_metadata.isAPI(); } -bool Mod::Impl::supportsUnloading() const { - return m_info.supportsUnloading(); +bool Mod::Impl::canDisable() const { + auto deps = m_dependants; + return this->supportsDisabling() && + (deps.empty() || std::all_of(deps.begin(), deps.end(), [&](auto& item) { + return item->canDisable(); + })); +} + +bool Mod::Impl::canEnable() const { + auto deps = m_metadata.getDependencies(); + return !this->isUninstalled() && + (deps.empty() || std::all_of(deps.begin(), deps.end(), [&](auto& item) { + return item.isResolved(); + })); } bool Mod::Impl::wasSuccessfullyLoaded() const { @@ -304,11 +314,6 @@ Result<> Mod::Impl::loadBinary() { log::debug("Loading binary for mod {}", m_metadata.getID()); if (m_binaryLoaded) return Ok(); - } - - if (this->hasUnresolvedDependencies()) { - return Err("Mod has unresolved dependencies"); - } LoaderImpl::get()->provideNextMod(m_self); @@ -327,54 +332,30 @@ Result<> Mod::Impl::loadBinary() { ModStateEvent(m_self, ModEventType::Loaded).post(); }); - Loader::get()->updateAllDependencies(); - - log::debug("Enabling mod {}", m_info.id()); - GEODE_UNWRAP(this->enable()); - - return Ok(); -} - -Result<> Mod::Impl::unloadBinary() { - if (!m_binaryLoaded) { - return Ok(); - } - - if (!m_info.supportsUnloading()) { - return Err("Mod does not support unloading"); - } - - GEODE_UNWRAP(this->saveData()); - - GEODE_UNWRAP(this->disable()); - Loader::get()->queueInGDThread([&]() { - ModStateEvent(m_self, ModEventType::Unloaded).post(); - }); - - // Disabling unhooks and unpatches already - for (auto const& hook : m_hooks) { - delete hook; - } - m_hooks.clear(); - - for (auto const& patch : m_patches) { - delete patch; - } - m_patches.clear(); - - GEODE_UNWRAP(this->unloadPlatformBinary()); - m_binaryLoaded = false; - - Loader::get()->updateAllDependencies(); - return Ok(); } Result<> Mod::Impl::enable() { - if (!m_binaryLoaded) { - return this->loadBinary(); + if (!m_binaryLoaded) + return Err("Tried to enable {} but its binary is not loaded", m_metadata.getID()); + + bool enabledDependencies = true; + for (auto const& item : m_metadata.getDependencies()) { + if (!item.isResolved()) + continue; + auto res = item.mod->enable(); + if (!res) { + enabledDependencies = false; + log::error("Failed to enable {}: {}", item.id, res.unwrapErr()); + } } + if (!enabledDependencies) + return Err("Mod cannot be enabled because one or more of its dependencies cannot be enabled."); + + if (!this->canEnable()) + return Err("Mod cannot be enabled because it has unresolved dependencies."); + for (auto const& hook : m_hooks) { if (!hook) { log::warn("Hook is null in mod \"{}\"", m_metadata.getName()); @@ -401,41 +382,66 @@ Result<> Mod::Impl::enable() { } Result<> Mod::Impl::disable() { - if (!m_enabled) { + if (!m_enabled) return Ok(); + + if (!this->supportsDisabling()) + return Err("Mod does not support disabling."); + + if (!this->canDisable()) + return Err("Mod cannot be disabled because one or more of its dependants cannot be disabled."); + + // disable dependants + bool disabledDependants = true; + for (auto& item : m_dependants) { + auto res = item->disable(); + if (res) + continue; + disabledDependants = false; + log::error("Failed to disable {}: {}", item->getID(), res.unwrapErr()); } - if (!m_info.supportsDisabling()) { - return Err("Mod does not support disabling"); - } + + if (!disabledDependants) + return Err("Mod cannot be disabled because one or more of its dependants cannot be disabled."); Loader::get()->queueInGDThread([&]() { ModStateEvent(m_self, ModEventType::Disabled).post(); }); + std::vector errors; for (auto const& hook : m_hooks) { - GEODE_UNWRAP(this->disableHook(hook)); + auto res = this->disableHook(hook); + if (!res) + errors.push_back(res.unwrapErr()); } for (auto const& patch : m_patches) { - if (!patch->restore()) { - return Err("Unable to restore patch at " + std::to_string(patch->getAddress())); - } + auto res = this->unpatch(patch); + if (!res) + errors.push_back(res.unwrapErr()); } m_enabled = false; + if (!errors.empty()) + return Err(utils::string::join(errors, "\n")); + return Ok(); } Result<> Mod::Impl::uninstall() { - if (m_info.supportsDisabling()) { + if (supportsDisabling()) { GEODE_UNWRAP(this->disable()); - if (m_info.supportsUnloading()) { - GEODE_UNWRAP(this->unloadBinary()); + } + else { + for (auto& item : m_dependants) { + if (!item->canDisable()) + continue; + GEODE_UNWRAP(item->disable()); } } try { - ghc::filesystem::remove(m_info.path()); + ghc::filesystem::remove(m_metadata.getPath()); } catch (std::exception& e) { return Err( @@ -455,51 +461,12 @@ bool Mod::Impl::isUninstalled() const { // Dependencies Result<> Mod::Impl::updateDependencies() { - bool hasUnresolved = false; - for (auto& dep : m_info.dependencies()) { - // set the dependency's loaded mod if such exists - if (!dep.mod) { - dep.mod = Loader::get()->getLoadedMod(dep.id); - // verify loaded dependency version - if (dep.mod && !dep.version.compare(dep.mod->getVersion())) { - dep.mod = nullptr; - } - } - - // check if the dependency is loaded - if (dep.mod) { - // update the dependency recursively - GEODE_UNWRAP(dep.mod->updateDependencies()); - - // enable mod if it's resolved & enabled - if (!dep.mod->hasUnresolvedDependencies()) { - if (dep.mod->isEnabled()) { - GEODE_UNWRAP(dep.mod->loadBinary().expect("Unable to load dependency: {error}")); - } - } - } - // check if the dependency is resolved now - if (!dep.isResolved()) { - GEODE_UNWRAP(this->unloadBinary().expect("Unable to unload mod: {error}")); - hasUnresolved = true; - } - } - // load if there weren't any unresolved dependencies - if (!hasUnresolved && !m_binaryLoaded) { - log::debug("All dependencies for {} found", m_info.id()); - if (m_enabled) { - log::debug("Resolved & loading {}", m_info.id()); - GEODE_UNWRAP(this->loadBinary()); - } - else { - log::debug("Resolved {}, however not loading it as it is disabled", m_info.id()); - } - } - return Ok(); + return Err("Mod::updateDependencies is no longer needed, " + "as this is handled by Loader::refreshModGraph"); } bool Mod::Impl::hasUnresolvedDependencies() const { - for (auto const& dep : m_info.dependencies()) { + for (auto const& dep : m_metadata.getDependencies()) { if (!dep.isResolved()) { return true; } @@ -507,10 +474,23 @@ bool Mod::Impl::hasUnresolvedDependencies() const { return false; } +bool Mod::Impl::hasUnresolvedIncompatibilities() const { + for (auto const& dep : m_metadata.getIncompatibilities()) { + if (!dep.isResolved()) { + return true; + } + } + return false; +} + +// msvc stop fucking screaming please i BEG YOU +#pragma warning(suppress : 4996) std::vector Mod::Impl::getUnresolvedDependencies() { +#pragma warning(suppress : 4996) std::vector unresolved; for (auto const& dep : m_metadata.getDependencies()) { if (!dep.isResolved()) { +#pragma warning(suppress : 4996) unresolved.push_back(dep); } } @@ -541,7 +521,7 @@ Result<> Mod::Impl::disableHook(Hook* hook) { Result Mod::Impl::addHook(Hook* hook) { m_hooks.push_back(hook); if (LoaderImpl::get()->isReadyToHook()) { - if (hook->getAutoEnable()) { + if (this->isEnabled() && hook->getAutoEnable()) { auto res = this->enableHook(hook); if (!res) { delete hook; @@ -582,21 +562,20 @@ Result 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. @@ -697,27 +676,24 @@ static Result getModImplInfo() { Mod* Loader::Impl::createInternalMod() { auto& mod = Mod::sharedMod<>; - if (!mod) { - auto infoRes = getModImplInfo(); - if (!infoRes) { - LoaderImpl::get()->platformMessageBox( - "Fatal Internal Error", - "Unable to create internal mod info: \"" + infoRes.unwrapErr() + - "\"\n" - "This is a fatal internal error in the loader, please " - "contact Geode developers immediately!" - ); - auto info = ModInfo(); - info.id() = "geode.loader"; - mod = new Mod(info); - } - else { - mod = new Mod(infoRes.unwrap()); - } - mod->m_impl->m_binaryLoaded = true; - mod->m_impl->m_enabled = true; - m_mods.insert({ mod->getID(), mod }); + if (mod) return mod; + auto infoRes = getModImplInfo(); + if (!infoRes) { + LoaderImpl::get()->platformMessageBox( + "Fatal Internal Error", + "Unable to create internal mod info: \"" + infoRes.unwrapErr() + + "\"\n" + "This is a fatal internal error in the loader, please " + "contact Geode developers immediately!" + ); + mod = new Mod(ModMetadata("geode.loader")); } + else { + mod = new Mod(infoRes.unwrap()); + } + mod->m_impl->m_binaryLoaded = true; + mod->m_impl->m_enabled = true; + m_mods.insert({ mod->getID(), mod }); return mod; } diff --git a/loader/src/loader/ModImpl.hpp b/loader/src/loader/ModImpl.hpp index 14f1b7f6..a4b2560f 100644 --- a/loader/src/loader/ModImpl.hpp +++ b/loader/src/loader/ModImpl.hpp @@ -39,12 +39,11 @@ namespace geode { */ ghc::filesystem::path m_saveDirPath; /** - * Pointers to mods that depend on - * this Mod. Makes it possible to - * enable / disable them automatically, + * Pointers to mods that depend on this Mod. + * Makes it possible to enable / disable them automatically, * when their dependency is disabled. */ - std::vector m_parentDependencies; + std::vector m_dependants; /** * Saved values */ @@ -84,8 +83,8 @@ namespace geode { bool isEnabled() const; bool isLoaded() const; bool supportsDisabling() const; - bool wasSuccesfullyLoaded() const; - ModInfo getModInfo() const; + bool canDisable() const; + bool canEnable() const; bool wasSuccessfullyLoaded() const; ModMetadata getMetadata() const; ghc::filesystem::path getTempDir() const; @@ -113,16 +112,17 @@ namespace geode { Result<> removeHook(Hook* hook); Result patch(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; diff --git a/loader/src/loader/ModInfoImpl.cpp b/loader/src/loader/ModInfoImpl.cpp index 7c1e4ed3..d968c386 100644 --- a/loader/src/loader/ModInfoImpl.cpp +++ b/loader/src/loader/ModInfoImpl.cpp @@ -5,8 +5,7 @@ #include "ModInfoImpl.hpp" -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" +#pragma warning(disable : 4996) // deprecation using namespace geode::prelude; @@ -20,34 +19,6 @@ bool Dependency::isResolved() const { this->version.compare(this->mod->getVersion())); } -bool ModInfo::Impl::validateID(std::string const& id) { - return ModMetadata::Impl::validateID(id); -} - -Result ModInfo::Impl::create(ModJson const& json) { - return ModMetadata::Impl::create(json); -} - -Result ModInfo::Impl::createFromFile(ghc::filesystem::path const& path) { - return ModMetadata::Impl::createFromFile(path); -} - -Result ModInfo::Impl::createFromGeodeFile(ghc::filesystem::path const& path) { - return ModMetadata::Impl::createFromGeodeFile(path); -} - -Result ModInfo::Impl::createFromGeodeZip(file::Unzip& unzip) { - return ModMetadata::Impl::createFromGeodeZip(unzip); -} - -ModJson ModInfo::Impl::toJSON() const { - return m_metadata.m_rawJSON; -} - -ModJson ModInfo::Impl::getRawJSON() const { - return m_metadata.m_rawJSON; -} - bool ModInfo::Impl::operator==(ModInfo::Impl const& other) const { return this->m_metadata.m_id == other.m_metadata.m_id; } @@ -186,33 +157,34 @@ bool const& ModInfo::isAPI() const { } Result ModInfo::createFromGeodeZip(utils::file::Unzip& zip) { - return Impl::createFromGeodeZip(zip); + return ModMetadata::Impl::createFromGeodeZip(zip); } Result ModInfo::createFromGeodeFile(ghc::filesystem::path const& path) { - return Impl::createFromGeodeFile(path); + return ModMetadata::Impl::createFromGeodeFile(path); } Result ModInfo::createFromFile(ghc::filesystem::path const& path) { - return Impl::createFromFile(path); + return ModMetadata::Impl::createFromFile(path); } Result ModInfo::create(ModJson const& json) { - return Impl::create(json); + return ModMetadata::Impl::create(json); } ModJson ModInfo::toJSON() const { - return m_impl->toJSON(); + return m_impl->m_metadata.m_rawJSON; } ModJson ModInfo::getRawJSON() const { - return m_impl->getRawJSON(); + return m_impl->m_metadata.m_rawJSON; } bool ModInfo::operator==(ModInfo const& other) const { return m_impl->operator==(*other.m_impl); } +#pragma warning(suppress : 4996) ModInfo::ModInfo() : m_impl(std::make_unique()) {} ModInfo::ModInfo(ModInfo const& other) : m_impl(std::make_unique(*other.m_impl)) {} @@ -247,6 +219,26 @@ ModInfo::operator ModMetadata() const { return metadata; } -ModInfo::~ModInfo() = default; +ModJson& ModInfo::rawJSON() { + return m_impl->m_metadata.m_rawJSON; +} +ModJson const& ModInfo::rawJSON() const { + return m_impl->m_metadata.m_rawJSON; +} -#pragma clang diagnostic pop +Result ModInfo::createFromSchemaV010(geode::ModJson const& json) { + return ModMetadata::Impl::createFromSchemaV010(json); +} + +Result<> ModInfo::addSpecialFiles(ghc::filesystem::path const& dir) { + return m_impl->m_metadata.addSpecialFiles(dir); +} +Result<> ModInfo::addSpecialFiles(utils::file::Unzip& zip) { + return m_impl->m_metadata.addSpecialFiles(zip); +} + +std::vector*>> ModInfo::getSpecialFiles() { + return m_impl->m_metadata.getSpecialFiles(); +} + +ModInfo::~ModInfo() = default; diff --git a/loader/src/loader/ModInfoImpl.hpp b/loader/src/loader/ModInfoImpl.hpp index 3ebd981c..fcd80232 100644 --- a/loader/src/loader/ModInfoImpl.hpp +++ b/loader/src/loader/ModInfoImpl.hpp @@ -7,10 +7,12 @@ #include #include +#pragma warning(disable : 4996) // deprecation + using namespace geode::prelude; namespace geode { - class ModInfo::Impl { + class [[deprecated]] ModInfo::Impl { public: ModMetadata::Impl m_metadata; std::optional m_issues; @@ -18,31 +20,11 @@ namespace geode { bool m_supportsDisabling = true; bool m_supportsUnloading = false; - 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(); }; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - class ModInfoImpl { + class [[deprecated]] ModInfoImpl { public: static ModInfo::Impl& getImpl(ModInfo& info); }; -#pragma clang diagnostic pop } diff --git a/loader/src/loader/ModMetadataImpl.cpp b/loader/src/loader/ModMetadataImpl.cpp index f7157dd5..a814d611 100644 --- a/loader/src/loader/ModMetadataImpl.cpp +++ b/loader/src/loader/ModMetadataImpl.cpp @@ -22,11 +22,9 @@ bool ModMetadata::Dependency::isResolved() const { } bool ModMetadata::Incompatibility::isResolved() const { - return !this->mod || !this->mod->isLoaded() || !this->version.compare(this->mod->getVersion()); + return !this->mod || !this->version.compare(this->mod->getVersion()); } -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" ModMetadata::Dependency::operator geode::Dependency() { return {id, version, importance == Importance::Required, mod}; } @@ -53,7 +51,6 @@ ModMetadata::Dependency ModMetadata::Dependency::fromDeprecated(geode::Dependenc ModMetadata::IssuesInfo ModMetadata::IssuesInfo::fromDeprecated(geode::IssuesInfo const& value) { return {value.info, value.url}; } -#pragma clang diagnostic pop static std::string sanitizeDetailsData(std::string const& str) { // delete CRLF @@ -473,8 +470,6 @@ ModMetadata& ModMetadata::operator=(ModMetadata&& other) noexcept { return *this; } -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" ModMetadata::operator ModInfo() { ModInfo info; auto infoImpl = ModInfoImpl::getImpl(info); @@ -493,7 +488,6 @@ ModMetadata::operator ModInfo() const { infoImpl.m_dependencies.push_back(dep); return info; } -#pragma clang diagnostic pop ModMetadata::~ModMetadata() = default; @@ -504,6 +498,7 @@ struct json::Serialize { case geode::ModMetadata::Dependency::Importance::Required: return {"required"}; case geode::ModMetadata::Dependency::Importance::Recommended: return {"recommended"}; case geode::ModMetadata::Dependency::Importance::Suggested: return {"suggested"}; + default: return {"unknown"}; } } static geode::ModMetadata::Dependency::Importance GEODE_DLL from_json(json::Value const& importance) { diff --git a/loader/src/ui/internal/info/ModInfoPopup.cpp b/loader/src/ui/internal/info/ModInfoPopup.cpp index ce47c675..92068f44 100644 --- a/loader/src/ui/internal/info/ModInfoPopup.cpp +++ b/loader/src/ui/internal/info/ModInfoPopup.cpp @@ -573,9 +573,9 @@ void LocalModInfoPopup::onEnableMod(CCObject* sender) { )->show(); } if (as(sender)->isToggled()) { - auto res = m_mod->loadBinary(); + auto res = m_mod->enable(); if (!res) { - FLAlertLayer::create(nullptr, "Error Loading Mod", res.unwrapErr(), "OK", nullptr)->show(); + FLAlertLayer::create(nullptr, "Error Enabling Mod", res.unwrapErr(), "OK", nullptr)->show(); } } else { @@ -585,7 +585,7 @@ void LocalModInfoPopup::onEnableMod(CCObject* sender) { } } if (m_layer) { - m_layer->updateAllStates(nullptr); + m_layer->updateAllStates(); } as(sender)->toggle(m_mod->isEnabled()); } diff --git a/loader/src/ui/internal/list/ModListCell.cpp b/loader/src/ui/internal/list/ModListCell.cpp index 5944760d..4bd8c9fd 100644 --- a/loader/src/ui/internal/list/ModListCell.cpp +++ b/loader/src/ui/internal/list/ModListCell.cpp @@ -31,7 +31,8 @@ float ModListCell::getLogoSize() const { void ModListCell::setupInfo( ModMetadata const& metadata, bool spaceForTags, - ModListDisplay display + ModListDisplay display, + bool inactive ) { m_menu = CCMenu::create(); m_menu->setPosition(m_width - 40.f, m_height / 2); @@ -41,6 +42,10 @@ void ModListCell::setupInfo( auto logoSpr = this->createLogo({ logoSize, logoSize }); logoSpr->setPosition({ logoSize / 2 + 12.f, m_height / 2 }); + auto logoSprColor = typeinfo_cast(logoSpr); + if (inactive && logoSprColor) { + logoSprColor->setColor({ 163, 163, 163 }); + } this->addChild(logoSpr); bool hasDesc = @@ -63,6 +68,9 @@ void ModListCell::setupInfo( titleLabel->setPositionY(m_height / 2 + 7.f); } titleLabel->limitLabelWidth(m_width / 2 - 40.f, .5f, .1f); + if (inactive) { + titleLabel->setColor({ 163, 163, 163 }); + } this->addChild(titleLabel); auto versionLabel = CCLabelBMFont::create( @@ -76,6 +84,9 @@ void ModListCell::setupInfo( titleLabel->getPositionY() - 1.f ); versionLabel->setColor({ 0, 255, 0 }); + if (inactive) { + versionLabel->setColor({ 0, 163, 0 }); + } this->addChild(versionLabel); if (auto tag = metadata.getVersion().getTag()) { @@ -93,6 +104,9 @@ void ModListCell::setupInfo( auto creatorStr = "by " + metadata.getDeveloper(); auto creatorLabel = CCLabelBMFont::create(creatorStr.c_str(), "goldFont.fnt"); creatorLabel->setScale(.43f); + if (inactive) { + creatorLabel->setColor({ 163, 163, 163 }); + } m_developerBtn = CCMenuItemSpriteExtra::create( creatorLabel, this, menu_selector(ModListCell::onViewDev) @@ -133,6 +147,9 @@ void ModListCell::setupInfo( m_description->setAnchorPoint({ .0f, .5f }); m_description->setPosition(m_height / 2 + logoSize / 2 + 18.f, descBG->getPositionY()); m_description->limitLabelWidth(m_width / 2 - 10.f, .5f, .1f); + if (inactive) { + m_description->setColor({ 163, 163, 163 }); + } this->addChild(m_description); } } @@ -187,11 +204,14 @@ void ModCell::onEnable(CCObject* sender) { else { tryOrAlert(m_mod->disable(), "Error disabling mod"); } - if (m_layer) { - m_layer->updateAllStates(this); - } + Loader::get()->queueInGDThread([this]() { + if (m_layer) { + m_layer->updateAllStates(); + } + }); } +// TODO: for fod maybe :3 show problems related to this mod void ModCell::onUnresolvedInfo(CCObject*) { std::string info = "This mod has the following " @@ -211,6 +231,11 @@ void ModCell::onInfo(CCObject*) { LocalModInfoPopup::create(m_mod, m_layer)->show(); } +void ModCell::onRestart(CCObject*) { + utils::game::restart(); +} + +// TODO: for fod maybe :3 check if there are any problems related to this mod void ModCell::updateState() { bool unresolved = m_mod->hasUnresolvedDependencies(); if (m_enableToggle) { @@ -232,18 +257,50 @@ bool ModCell::init( ) { if (!ModListCell::init(list, size)) return false; - m_mod = mod; this->setupInfo(mod->getMetadata(), false, display, m_mod->isUninstalled()); - auto viewSpr = ButtonSprite::create("View", "bigFont.fnt", "GJ_button_01.png", .8f); - viewSpr->setScale(.65f); + if (mod->isUninstalled()) { + auto restartSpr = ButtonSprite::create("Restart", "bigFont.fnt", "GJ_button_03.png", .8f); + restartSpr->setScale(.65f); - auto viewBtn = CCMenuItemSpriteExtra::create(viewSpr, this, menu_selector(ModCell::onInfo)); - m_menu->addChild(viewBtn); + auto restartBtn = CCMenuItemSpriteExtra::create(restartSpr, this, menu_selector(ModCell::onRestart)); + restartBtn->setPositionX(-16.f); + m_menu->addChild(restartBtn); + } + else { + auto viewSpr = ButtonSprite::create("View", "bigFont.fnt", "GJ_button_01.png", .8f); + viewSpr->setScale(.65f); - if (m_mod->wasSuccesfullyLoaded() && m_mod->supportsDisabling()) { + auto viewBtn = CCMenuItemSpriteExtra::create(viewSpr, this, menu_selector(ModCell::onInfo)); + m_menu->addChild(viewBtn); + + if (m_mod->wasSuccessfullyLoaded()) { + auto latestIndexItem = Index::get()->getMajorItem( + mod->getMetadata().getID() + ); + + if (latestIndexItem && Index::get()->isUpdateAvailable(latestIndexItem)) { + viewSpr->updateBGImage("GE_button_01.png"_spr); + + auto minorIndexItem = Index::get()->getItem( + mod->getMetadata().getID(), + ComparableVersionInfo(mod->getMetadata().getVersion(), VersionCompare::MoreEq) + ); + + if (latestIndexItem->getMetadata().getVersion().getMajor() > minorIndexItem->getMetadata().getVersion().getMajor()) { + auto updateIcon = CCSprite::createWithSpriteFrameName("updates-available.png"_spr); + updateIcon->setPosition(viewSpr->getContentSize() - CCSize { 2.f, 2.f }); + updateIcon->setZOrder(99); + updateIcon->setScale(.5f); + viewSpr->addChild(updateIcon); + } + } + } + } + + if (m_mod->wasSuccessfullyLoaded() && m_mod->supportsDisabling() && !m_mod->isUninstalled()) { m_enableToggle = CCMenuItemToggler::createWithStandardSprites(this, menu_selector(ModCell::onEnable), .7f); m_enableToggle->setPosition(-45.f, 0.f); @@ -259,30 +316,6 @@ bool ModCell::init( m_unresolvedExMark->setVisible(false); m_menu->addChild(m_unresolvedExMark); - if (m_mod->wasSuccesfullyLoaded()) { - - auto latestIndexItem = Index::get()->getMajorItem( - mod->getModInfo().id() - ); - - if (latestIndexItem && Index::get()->isUpdateAvailable(latestIndexItem)) { - viewSpr->updateBGImage("GE_button_01.png"_spr); - - auto minorIndexItem = Index::get()->getItem( - mod->getModInfo().id(), - ComparableVersionInfo(mod->getModInfo().version(), VersionCompare::MoreEq) - ); - - if (latestIndexItem->getModInfo().version().getMajor() > minorIndexItem->getModInfo().version().getMajor()) { - auto updateIcon = CCSprite::createWithSpriteFrameName("updates-available.png"_spr); - updateIcon->setPosition(viewSpr->getContentSize() - CCSize { 2.f, 2.f }); - updateIcon->setZOrder(99); - updateIcon->setScale(.5f); - viewSpr->addChild(updateIcon); - } - } - } - this->updateState(); return true; @@ -302,6 +335,10 @@ void IndexItemCell::onInfo(CCObject*) { IndexItemInfoPopup::create(m_item, m_layer)->show(); } +void IndexItemCell::onRestart(CCObject*) { + utils::game::restart(); +} + IndexItemCell* IndexItemCell::create( IndexItemHandle item, ModListLayer* list, @@ -327,11 +364,24 @@ bool IndexItemCell::init( m_item = item; - this->setupInfo(item->getModInfo(), item->getTags().size(), display); this->setupInfo(item->getMetadata(), item->getTags().size(), display, item->isInstalled()); - auto viewBtn = CCMenuItemSpriteExtra::create(viewSpr, this, menu_selector(IndexItemCell::onInfo)); - m_menu->addChild(viewBtn); + if (item->isInstalled()) { + auto restartSpr = ButtonSprite::create("Restart", "bigFont.fnt", "GJ_button_03.png", .8f); + restartSpr->setScale(.65f); + + auto restartBtn = CCMenuItemSpriteExtra::create(restartSpr, this, menu_selector(IndexItemCell::onRestart)); + restartBtn->setPositionX(-16.f); + m_menu->addChild(restartBtn); + } + else { + auto viewSpr = ButtonSprite::create("View", "bigFont.fnt", "GJ_button_01.png", .8f); + viewSpr->setScale(.65f); + + auto viewBtn = + CCMenuItemSpriteExtra::create(viewSpr, this, menu_selector(IndexItemCell::onInfo)); + m_menu->addChild(viewBtn); + } if (item->getTags().size()) { float x = m_height / 2 + this->getLogoSize() / 2 + 13.f; @@ -401,7 +451,6 @@ void InvalidGeodeFileCell::FLAlert_Clicked(FLAlertLayer*, bool btn2) { ) ->show(); } - Loader::get()->refreshModsList(); if (m_layer) { m_layer->reloadList(); } diff --git a/loader/src/ui/internal/list/ModListCell.hpp b/loader/src/ui/internal/list/ModListCell.hpp index 3fe4c7ef..64892045 100644 --- a/loader/src/ui/internal/list/ModListCell.hpp +++ b/loader/src/ui/internal/list/ModListCell.hpp @@ -55,6 +55,7 @@ protected: ); void onInfo(CCObject*); + void onRestart(CCObject*); void onEnable(CCObject*); void onUnresolvedInfo(CCObject*); @@ -86,6 +87,7 @@ protected: ); void onInfo(CCObject*); + void onRestart(CCObject*); public: static IndexItemCell* create( diff --git a/loader/src/ui/internal/list/ModListLayer.cpp b/loader/src/ui/internal/list/ModListLayer.cpp index 58103ce0..8a768c65 100644 --- a/loader/src/ui/internal/list/ModListLayer.cpp +++ b/loader/src/ui/internal/list/ModListLayer.cpp @@ -155,26 +155,32 @@ CCArray* ModListLayer::createModCells(ModListType type, ModListQuery const& quer )); } - // sort the mods by match score - std::multimap 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()) + continue; + if (auto match = queryMatch(query, item)) { + auto cell = IndexItemCell::create(item, this, m_display, this->getCellSize()); + sorted.insert({ match.value(), cell }); + } + } + + // loaded for (auto const& mod : Loader::get()->getAllMods()) { - // if the mod is no longer installed nor - // loaded, it's as good as not existing - // (because it doesn't) - if (mod->isUninstalled() && !mod->isLoaded()) continue; - // only show mods that match query in list if (auto match = queryMatch(query, mod)) { - sorted.insert({ match.value(), mod }); + auto cell = ModCell::create(mod, this, m_display, this->getCellSize()); + sorted.insert({ match.value(), cell }); } } // add the mods sorted - for (auto& [score, mod] : ranges::reverse(sorted)) { - mods->addObject(ModCell::create( - mod, this, m_display, this->getCellSize() - )); + for (auto& [score, cell] : ranges::reverse(sorted)) { + mods->addObject(cell); } } break; @@ -546,14 +552,11 @@ void ModListLayer::reloadList(std::optional 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 +615,6 @@ void ModListLayer::onExit(CCObject*) { } void ModListLayer::onReload(CCObject*) { - Loader::get()->refreshModsList(); this->reloadList(); } diff --git a/loader/src/ui/internal/list/ModListLayer.hpp b/loader/src/ui/internal/list/ModListLayer.hpp index 4cff18d8..16ca3ec7 100644 --- a/loader/src/ui/internal/list/ModListLayer.hpp +++ b/loader/src/ui/internal/list/ModListLayer.hpp @@ -84,7 +84,7 @@ protected: public: static ModListLayer* create(); static ModListLayer* scene(); - void updateAllStates(ModListCell* except = nullptr); + void updateAllStates(); ModListDisplay getDisplay() const; ModListQuery& getQuery(); From cb8759bef06c134b7ecb787e460ba967e9e2e955 Mon Sep 17 00:00:00 2001 From: ConfiG Date: Wed, 9 Aug 2023 03:16:33 +0300 Subject: [PATCH 07/36] add api label --- loader/src/ui/internal/list/ModListCell.cpp | 25 ++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/loader/src/ui/internal/list/ModListCell.cpp b/loader/src/ui/internal/list/ModListCell.cpp index 4bd8c9fd..c9fcdf8f 100644 --- a/loader/src/ui/internal/list/ModListCell.cpp +++ b/loader/src/ui/internal/list/ModListCell.cpp @@ -89,18 +89,41 @@ void ModListCell::setupInfo( } this->addChild(versionLabel); + 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() + ); + } } + if (apiLabel) + this->addChild(apiLabel); + auto creatorStr = "by " + metadata.getDeveloper(); auto creatorLabel = CCLabelBMFont::create(creatorStr.c_str(), "goldFont.fnt"); creatorLabel->setScale(.43f); From ed5b5c9685050fe19ba3e8938743992a8403e23e Mon Sep 17 00:00:00 2001 From: ConfiG Date: Wed, 9 Aug 2023 03:17:10 +0300 Subject: [PATCH 08/36] fix index not parsing tags correctly --- loader/src/loader/Index.cpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/loader/src/loader/Index.cpp b/loader/src/loader/Index.cpp index b564b410..9d3da7c8 100644 --- a/loader/src/loader/Index.cpp +++ b/loader/src/loader/Index.cpp @@ -122,17 +122,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_metadata = metadata; - 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_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()); @@ -363,6 +368,7 @@ void Index::Impl::updateFromLocalTree() { }); } } catch(std::exception& e) { + log::error("Unable to read local index tree: {}", e.what()); IndexUpdateEvent("Unable to read local index tree").post(); return; } From 80f387d9d1035eba444c28e994ae01ec7782fef6 Mon Sep 17 00:00:00 2001 From: ConfiG Date: Wed, 9 Aug 2023 18:37:26 +0300 Subject: [PATCH 09/36] name ipc threads --- loader/src/platform/windows/LoaderImpl.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/loader/src/platform/windows/LoaderImpl.cpp b/loader/src/platform/windows/LoaderImpl.cpp index fe70051c..a3f632eb 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"); } From e7180695b8654c427da948f932eae4dc7039451f Mon Sep 17 00:00:00 2001 From: ConfiG Date: Wed, 9 Aug 2023 18:55:14 +0300 Subject: [PATCH 10/36] early load --- loader/include/Geode/loader/Loader.hpp | 12 ++++ loader/include/Geode/loader/Mod.hpp | 1 + loader/src/hooks/LoadingLayer.cpp | 67 ++++++++++--------- loader/src/loader/Loader.cpp | 4 ++ loader/src/loader/LoaderImpl.cpp | 89 +++++++++++++++++++++----- loader/src/loader/LoaderImpl.hpp | 11 ++-- loader/src/loader/Mod.cpp | 4 ++ loader/src/loader/ModImpl.cpp | 8 +++ loader/src/loader/ModImpl.hpp | 1 + 9 files changed, 148 insertions(+), 49 deletions(-) diff --git a/loader/include/Geode/loader/Loader.hpp b/loader/include/Geode/loader/Loader.hpp index 1b38dd69..6384ef68 100644 --- a/loader/include/Geode/loader/Loader.hpp +++ b/loader/include/Geode/loader/Loader.hpp @@ -64,6 +64,17 @@ namespace geode { // 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(); @@ -75,6 +86,7 @@ namespace geode { [[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; diff --git a/loader/include/Geode/loader/Mod.hpp b/loader/include/Geode/loader/Mod.hpp index 569a2a6d..aacddb7e 100644 --- a/loader/include/Geode/loader/Mod.hpp +++ b/loader/include/Geode/loader/Mod.hpp @@ -82,6 +82,7 @@ namespace geode { bool supportsDisabling() const; bool canDisable() const; bool canEnable() const; + bool needsEarlyLoad() const; [[deprecated]] bool supportsUnloading() const; [[deprecated("use wasSuccessfullyLoaded instead")]] bool wasSuccesfullyLoaded() const; bool wasSuccessfullyLoaded() const; diff --git a/loader/src/hooks/LoadingLayer.cpp b/loader/src/hooks/LoadingLayer.cpp index 081167ac..d92f0879 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) {} + + void updateLoadedModsLabel() const { + 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_loadedModsLabel->setCString(str.c_str()); + } bool init(bool fromReload) { - if (!fromReload) { - Loader::get()->waitForModsToBeLoaded(); - } - CCFileUtils::get()->updatePaths(); if (!LoadingLayer::init(fromReload)) return false; - - if (!fromReload) { - auto winSize = CCDirector::sharedDirector()->getWinSize(); - auto count = Loader::get()->getAllMods().size(); + if (fromReload) return true; - 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); + auto winSize = CCDirector::sharedDirector()->getWinSize(); - // fields have unpredictable destructors - this->addChild(EventListenerNode::create( - this, &CustomLoadingLayer::updateResourcesProgress - )); + m_loadedModsLabel = CCLabelBMFont::create("Geode: Loaded 0/0 mods", "goldFont.fnt"); + m_loadedModsLabel->setPosition(winSize.width / 2, 30.f); + m_loadedModsLabel->setScale(.45f); + m_loadedModsLabel->setID("geode-loaded-info"); + this->addChild(m_loadedModsLabel); + this->updateLoadedModsLabel(); - // verify loader resources - if (!LoaderImpl::get()->verifyLoaderResources()) { - m_fields->m_updatingResources = true; - this->setUpdateText("Downloading Resources"); - } - else { - LoaderImpl::get()->updateSpecialFiles(); - } + // 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/loader/Loader.cpp b/loader/src/loader/Loader.cpp index 8d55893c..004e9695 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); } diff --git a/loader/src/loader/LoaderImpl.cpp b/loader/src/loader/LoaderImpl.cpp index 23ea529b..5536a7f8 100644 --- a/loader/src/loader/LoaderImpl.cpp +++ b/loader/src/loader/LoaderImpl.cpp @@ -293,6 +293,7 @@ void Loader::Impl::queueMods(std::vector& modQueue) { 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(); @@ -387,7 +388,12 @@ void Loader::Impl::buildModGraph() { } } -void Loader::Impl::loadModGraph(Mod* node) { +void Loader::Impl::loadModGraph(Mod* node, bool early) { + if (early && !node->needsEarlyLoad()) { + m_modsToLoad.push(node); + return; + } + if (node->hasUnresolvedDependencies()) return; if (node->hasUnresolvedIncompatibilities()) @@ -398,7 +404,7 @@ void Loader::Impl::loadModGraph(Mod* node) { if (node->isLoaded()) { for (auto const& dep : node->m_impl->m_dependants) { - this->loadModGraph(dep); + this->loadModGraph(dep, early); } log::popNest(); return; @@ -434,7 +440,7 @@ void Loader::Impl::loadModGraph(Mod* node) { } for (auto const& dep : node->m_impl->m_dependants) { - this->loadModGraph(dep); + this->loadModGraph(dep, early); } } @@ -512,6 +518,8 @@ 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(); @@ -520,38 +528,89 @@ void Loader::Impl::refreshModGraph() { 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(); - // TODO: not early load - log::debug("Loading mods"); + m_loadingState = LoadingState::EarlyMods; + log::debug("Loading early mods"); log::pushNest(); for (auto const& dep : Mod::get()->m_impl->m_dependants) { - this->loadModGraph(dep); + this->loadModGraph(dep, true); } log::popNest(); - log::debug("Finding problems"); - log::pushNest(); - this->findProblems(); + 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(); - m_earlyLoadFinished = true; - m_earlyLoadFinishedCV.notify_all(); + 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(); } @@ -561,11 +620,9 @@ std::vector Loader::Impl::getProblems() const { } 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 { diff --git a/loader/src/loader/LoaderImpl.hpp b/loader/src/loader/LoaderImpl.hpp index aac71705..17d36117 100644 --- a/loader/src/loader/LoaderImpl.hpp +++ b/loader/src/loader/LoaderImpl.hpp @@ -19,6 +19,7 @@ #include #include #include +#include #include // TODO: Find a file convention for impl headers @@ -56,6 +57,7 @@ namespace geode { std::vector m_modSearchDirectories; std::vector m_problems; std::unordered_map m_mods; + std::queue m_modsToLoad; std::vector m_texturePaths; bool m_isSetup = false; @@ -64,9 +66,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; @@ -131,9 +132,11 @@ namespace geode { void queueMods(std::vector& modQueue); void populateModList(std::vector& modQueue); void buildModGraph(); - void loadModGraph(Mod* node); + 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; diff --git a/loader/src/loader/Mod.cpp b/loader/src/loader/Mod.cpp index b6696fcc..33380a33 100644 --- a/loader/src/loader/Mod.cpp +++ b/loader/src/loader/Mod.cpp @@ -62,6 +62,10 @@ bool Mod::canEnable() const { return m_impl->canEnable(); } +bool Mod::needsEarlyLoad() const { + return m_impl->needsEarlyLoad(); +} + bool Mod::supportsUnloading() const { return false; } diff --git a/loader/src/loader/ModImpl.cpp b/loader/src/loader/ModImpl.cpp index 829cbcfd..79cb2a42 100644 --- a/loader/src/loader/ModImpl.cpp +++ b/loader/src/loader/ModImpl.cpp @@ -127,6 +127,14 @@ bool Mod::Impl::canEnable() const { })); } +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::wasSuccessfullyLoaded() const { return !this->isEnabled() || this->isLoaded(); } diff --git a/loader/src/loader/ModImpl.hpp b/loader/src/loader/ModImpl.hpp index a4b2560f..c6c7f762 100644 --- a/loader/src/loader/ModImpl.hpp +++ b/loader/src/loader/ModImpl.hpp @@ -85,6 +85,7 @@ namespace geode { bool supportsDisabling() const; bool canDisable() const; bool canEnable() const; + bool needsEarlyLoad() const; bool wasSuccessfullyLoaded() const; ModMetadata getMetadata() const; ghc::filesystem::path getTempDir() const; From 89082352341165229c54328a2335d32c555a87c8 Mon Sep 17 00:00:00 2001 From: ConfiG Date: Wed, 9 Aug 2023 19:05:14 +0300 Subject: [PATCH 11/36] add importance to incompat --- loader/include/Geode/loader/Loader.hpp | 1 + loader/include/Geode/loader/ModMetadata.hpp | 5 +++++ loader/src/loader/LoaderImpl.cpp | 24 +++++++++++++++----- loader/src/loader/ModImpl.cpp | 2 +- loader/src/loader/ModMetadataImpl.cpp | 25 +++++++++++++++++++-- 5 files changed, 48 insertions(+), 9 deletions(-) diff --git a/loader/include/Geode/loader/Loader.hpp b/loader/include/Geode/loader/Loader.hpp index 6384ef68..cb63fed1 100644 --- a/loader/include/Geode/loader/Loader.hpp +++ b/loader/include/Geode/loader/Loader.hpp @@ -25,6 +25,7 @@ namespace geode { Unknown, Suggestion, Recommendation, + Conflict, InvalidFile, Duplicate, SetupFailed, diff --git a/loader/include/Geode/loader/ModMetadata.hpp b/loader/include/Geode/loader/ModMetadata.hpp index 23d2f173..e0cf97f6 100644 --- a/loader/include/Geode/loader/ModMetadata.hpp +++ b/loader/include/Geode/loader/ModMetadata.hpp @@ -51,8 +51,13 @@ namespace geode { }; 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; }; diff --git a/loader/src/loader/LoaderImpl.cpp b/loader/src/loader/LoaderImpl.cpp index 5536a7f8..4eea6377 100644 --- a/loader/src/loader/LoaderImpl.cpp +++ b/loader/src/loader/LoaderImpl.cpp @@ -486,12 +486,24 @@ void Loader::Impl::findProblems() { for (auto const& dep : mod->getMetadata().getIncompatibilities()) { if (!dep.mod || !dep.version.compare(dep.mod->getVersion())) continue; - m_problems.push_back({ - LoadProblem::Type::PresentIncompatibility, - mod, - fmt::format("{} {}", dep.id, dep.version.toString()) - }); - log::error("{} is incompatible with {} {}", id, dep.id, dep.version); + 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 diff --git a/loader/src/loader/ModImpl.cpp b/loader/src/loader/ModImpl.cpp index 79cb2a42..be032a32 100644 --- a/loader/src/loader/ModImpl.cpp +++ b/loader/src/loader/ModImpl.cpp @@ -349,7 +349,7 @@ Result<> Mod::Impl::enable() { bool enabledDependencies = true; for (auto const& item : m_metadata.getDependencies()) { - if (!item.isResolved()) + if (!item.isResolved() || !item.mod) continue; auto res = item.mod->enable(); if (!res) { diff --git a/loader/src/loader/ModMetadataImpl.cpp b/loader/src/loader/ModMetadataImpl.cpp index a814d611..eab7d570 100644 --- a/loader/src/loader/ModMetadataImpl.cpp +++ b/loader/src/loader/ModMetadataImpl.cpp @@ -18,11 +18,12 @@ ModMetadata::Impl& ModMetadataImpl::getImpl(ModMetadata& info) { bool ModMetadata::Dependency::isResolved() const { return this->importance != Importance::Required || - (this->mod && this->mod->isLoaded() && this->version.compare(this->mod->getVersion())); + this->mod && this->mod->isLoaded() && this->version.compare(this->mod->getVersion()); } bool ModMetadata::Incompatibility::isResolved() const { - return !this->mod || !this->version.compare(this->mod->getVersion()); + return this->importance != Importance::Breaking || + (!this->mod || !this->version.compare(this->mod->getVersion())); } ModMetadata::Dependency::operator geode::Dependency() { @@ -138,6 +139,7 @@ Result ModMetadata::Impl::createFromSchemaV010(ModJson const& rawJs 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); @@ -512,3 +514,22 @@ struct json::Serialize { 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")"); + } +}; From 3d2e447333f92305fc711dbf1be253806ab68225 Mon Sep 17 00:00:00 2001 From: ConfiG Date: Wed, 9 Aug 2023 20:30:53 +0300 Subject: [PATCH 12/36] add epic super secret api that nobody should use --- loader/CMakeLists.txt | 5 +- loader/include/Geode/loader/Index.hpp | 9 +++ loader/include/Geode/loader/Mod.hpp | 5 ++ loader/include/Geode/loader/ModMetadata.hpp | 24 ++++++- loader/src/loader/Index.cpp | 26 ++++++++ loader/src/loader/Mod.cpp | 9 +++ loader/src/loader/ModImpl.cpp | 9 +++ loader/src/loader/ModImpl.hpp | 5 ++ loader/src/loader/ModMetadataImpl.cpp | 74 +++++++++++++++++++++ 9 files changed, 164 insertions(+), 2 deletions(-) diff --git a/loader/CMakeLists.txt b/loader/CMakeLists.txt index 39ce2a23..27cd7ed8 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 fb0830c1..79249af2 100644 --- a/loader/include/Geode/loader/Index.hpp +++ b/loader/include/Geode/loader/Index.hpp @@ -117,6 +117,15 @@ namespace geode { 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(); }; diff --git a/loader/include/Geode/loader/Mod.hpp b/loader/include/Geode/loader/Mod.hpp index aacddb7e..d80de2de 100644 --- a/loader/include/Geode/loader/Mod.hpp +++ b/loader/include/Geode/loader/Mod.hpp @@ -100,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(); diff --git a/loader/include/Geode/loader/ModMetadata.hpp b/loader/include/Geode/loader/ModMetadata.hpp index e0cf97f6..9a761118 100644 --- a/loader/include/Geode/loader/ModMetadata.hpp +++ b/loader/include/Geode/loader/ModMetadata.hpp @@ -163,11 +163,33 @@ namespace geode { /** * Whether this mod has to be loaded before the loading screen or not */ - [[ nodiscard]] bool needsEarlyLoad() const; + [[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 */ diff --git a/loader/src/loader/Index.cpp b/loader/src/loader/Index.cpp index 9d3da7c8..bf373fc6 100644 --- a/loader/src/loader/Index.cpp +++ b/loader/src/loader/Index.cpp @@ -107,6 +107,32 @@ 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") diff --git a/loader/src/loader/Mod.cpp b/loader/src/loader/Mod.cpp index 33380a33..cee3f4cf 100644 --- a/loader/src/loader/Mod.cpp +++ b/loader/src/loader/Mod.cpp @@ -97,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(); } diff --git a/loader/src/loader/ModImpl.cpp b/loader/src/loader/ModImpl.cpp index be032a32..be0a189b 100644 --- a/loader/src/loader/ModImpl.cpp +++ b/loader/src/loader/ModImpl.cpp @@ -79,6 +79,15 @@ 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; } diff --git a/loader/src/loader/ModImpl.hpp b/loader/src/loader/ModImpl.hpp index c6c7f762..a258272a 100644 --- a/loader/src/loader/ModImpl.hpp +++ b/loader/src/loader/ModImpl.hpp @@ -93,6 +93,11 @@ namespace geode { 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(); diff --git a/loader/src/loader/ModMetadataImpl.cpp b/loader/src/loader/ModMetadataImpl.cpp index eab7d570..6799b0d1 100644 --- a/loader/src/loader/ModMetadataImpl.cpp +++ b/loader/src/loader/ModMetadataImpl.cpp @@ -410,6 +410,80 @@ 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); } From a9cce769d5d507efeaee6b853c65bbc857ef6b5a Mon Sep 17 00:00:00 2001 From: ConfiG Date: Thu, 10 Aug 2023 23:35:55 +0300 Subject: [PATCH 13/36] fix index ui with show installed filter on --- loader/src/ui/internal/info/ModInfoPopup.cpp | 2 ++ loader/src/ui/internal/list/ModListCell.cpp | 6 ++++-- loader/src/ui/internal/list/ModListLayer.cpp | 8 +++++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/loader/src/ui/internal/info/ModInfoPopup.cpp b/loader/src/ui/internal/info/ModInfoPopup.cpp index 92068f44..ea77de18 100644 --- a/loader/src/ui/internal/info/ModInfoPopup.cpp +++ b/loader/src/ui/internal/info/ModInfoPopup.cpp @@ -686,6 +686,8 @@ bool IndexItemInfoPopup::init(IndexItemHandle item, ModListLayer* list) { if (!ModInfoPopup::init(item->getMetadata(), list)) return false; + if (item->isInstalled()) return true; + m_installBtnSpr = IconButtonSprite::create( "GE_button_01.png"_spr, CCSprite::createWithSpriteFrameName("install.png"_spr), diff --git a/loader/src/ui/internal/list/ModListCell.cpp b/loader/src/ui/internal/list/ModListCell.cpp index c9fcdf8f..25e026de 100644 --- a/loader/src/ui/internal/list/ModListCell.cpp +++ b/loader/src/ui/internal/list/ModListCell.cpp @@ -387,9 +387,11 @@ bool IndexItemCell::init( m_item = item; - this->setupInfo(item->getMetadata(), item->getTags().size(), display, item->isInstalled()); + bool justInstalled = item->isInstalled() && !Loader::get()->isModInstalled(item->getMetadata().getID()); - if (item->isInstalled()) { + 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); diff --git a/loader/src/ui/internal/list/ModListLayer.cpp b/loader/src/ui/internal/list/ModListLayer.cpp index 8a768c65..8bb873a3 100644 --- a/loader/src/ui/internal/list/ModListLayer.cpp +++ b/loader/src/ui/internal/list/ModListLayer.cpp @@ -136,7 +136,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; @@ -162,9 +162,11 @@ CCArray* ModListLayer::createModCells(ModListType type, ModListQuery const& quer // newly installed for (auto const& item : Index::get()->getItems()) { - if (!item->isInstalled()) + if (!item->isInstalled() || + Loader::get()->isModInstalled(item->getMetadata().getID())) continue; - if (auto match = queryMatch(query, item)) { + // 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 }); } From 0e1d639002cb9a3ca355fcb8a5a8c14aaae7f232 Mon Sep 17 00:00:00 2001 From: altalk23 <45172705+altalk23@users.noreply.github.com> Date: Fri, 11 Aug 2023 17:35:25 +0300 Subject: [PATCH 14/36] implement missing stuff to macos --- loader/src/loader/LoaderImpl.hpp | 2 ++ loader/src/platform/Objcpp.mm | 1 + .../mac/{LoaderImpl.cpp => LoaderImpl.mm} | 10 +++---- loader/src/platform/mac/util.mm | 29 +++++++++++++++++-- 4 files changed, 33 insertions(+), 9 deletions(-) rename loader/src/platform/mac/{LoaderImpl.cpp => LoaderImpl.mm} (92%) diff --git a/loader/src/loader/LoaderImpl.hpp b/loader/src/loader/LoaderImpl.hpp index 17d36117..36001a70 100644 --- a/loader/src/loader/LoaderImpl.hpp +++ b/loader/src/loader/LoaderImpl.hpp @@ -1,3 +1,5 @@ +#pragma once + #include "FileWatcher.hpp" #include diff --git a/loader/src/platform/Objcpp.mm b/loader/src/platform/Objcpp.mm index 8e5d54df..e8c49b91 100644 --- a/loader/src/platform/Objcpp.mm +++ b/loader/src/platform/Objcpp.mm @@ -5,6 +5,7 @@ using namespace geode::prelude; #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 1ed684ab..a1a8148e 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/util.mm b/loader/src/platform/mac/util.mm index 250e0047..cd2c83c0 100644 --- a/loader/src/platform/mac/util.mm +++ b/loader/src/platform/mac/util.mm @@ -181,7 +181,7 @@ ghc::filesystem::path dirs::getGameDir() { _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; }(); @@ -208,8 +208,31 @@ void geode::utils::game::restart() { return; } - // TODO: implement restarting on mac - log::warn("Restarting is not yet implemented on macOS!"); + 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 From 3707418355da8adb31182ad829031db1125de8e1 Mon Sep 17 00:00:00 2001 From: ConfiG Date: Sun, 13 Aug 2023 04:37:33 +0300 Subject: [PATCH 15/36] new install list ui --- loader/src/ui/internal/info/ModInfoPopup.cpp | 66 +--- loader/src/ui/internal/info/ModInfoPopup.hpp | 5 +- .../src/ui/internal/list/InstallListCell.cpp | 327 ++++++++++++++++++ .../src/ui/internal/list/InstallListCell.hpp | 114 ++++++ .../src/ui/internal/list/InstallListPopup.cpp | 229 ++++++++++++ .../src/ui/internal/list/InstallListPopup.hpp | 30 ++ 6 files changed, 716 insertions(+), 55 deletions(-) create mode 100644 loader/src/ui/internal/list/InstallListCell.cpp create mode 100644 loader/src/ui/internal/list/InstallListCell.hpp create mode 100644 loader/src/ui/internal/list/InstallListPopup.cpp create mode 100644 loader/src/ui/internal/list/InstallListPopup.hpp diff --git a/loader/src/ui/internal/info/ModInfoPopup.cpp b/loader/src/ui/internal/info/ModInfoPopup.cpp index ea77de18..f9a69f04 100644 --- a/loader/src/ui/internal/info/ModInfoPopup.cpp +++ b/loader/src/ui/internal/info/ModInfoPopup.cpp @@ -20,6 +20,7 @@ #include #include #include +#include static constexpr int const TAG_CONFIRM_UNINSTALL = 5; static constexpr int const TAG_CONFIRM_UPDATE = 6; @@ -749,63 +750,26 @@ 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, - "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->getMetadata().getName(), - handle->getMetadata().getID() - ); - } - ), - "\n " - ) - ), - "Cancel", "OK" - )->show(); + InstallListPopup::create(m_item, [&](IndexInstallList const& list) { + if (m_latestVersionLabel) { + m_latestVersionLabel->setVisible(false); + } + this->setInstallStatus(UpdateProgress(0, "Starting install")); + + m_installBtn->setTarget( + this, menu_selector(IndexItemInfoPopup::onCancel) + ); + m_installBtnSpr->setString("Cancel"); + m_installBtnSpr->setBG("GJ_button_06.png", false); + + Index::get()->install(list); + })->show(); } void IndexItemInfoPopup::onCancel(CCObject*) { Index::get()->cancelInstall(m_item); } -void IndexItemInfoPopup::doInstall() { - if (m_latestVersionLabel) { - m_latestVersionLabel->setVisible(false); - } - this->setInstallStatus(UpdateProgress(0, "Starting install")); - - m_installBtn->setTarget( - this, menu_selector(IndexItemInfoPopup::onCancel) - ); - 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(); - } -} - CCNode* IndexItemInfoPopup::createLogo(CCSize const& size) { return geode::createIndexItemLogo(m_item, size); } diff --git a/loader/src/ui/internal/info/ModInfoPopup.hpp b/loader/src/ui/internal/info/ModInfoPopup.hpp index 773c41b2..e5bd0aec 100644 --- a/loader/src/ui/internal/info/ModInfoPopup.hpp +++ b/loader/src/ui/internal/info/ModInfoPopup.hpp @@ -89,7 +89,7 @@ public: static LocalModInfoPopup* create(Mod* mod, ModListLayer* list); }; -class IndexItemInfoPopup : public ModInfoPopup, public FLAlertLayerProtocol { +class IndexItemInfoPopup : public ModInfoPopup { protected: IndexItemHandle m_item; EventListener m_installListener; @@ -99,9 +99,6 @@ protected: void onInstallProgress(ModInstallEvent* event); void onInstall(CCObject*); void onCancel(CCObject*); - void doInstall(); - - void FLAlert_Clicked(FLAlertLayer*, bool) override; CCNode* createLogo(CCSize const& size) override; ModMetadata getMetadata() const override; diff --git a/loader/src/ui/internal/list/InstallListCell.cpp b/loader/src/ui/internal/list/InstallListCell.cpp new file mode 100644 index 00000000..92803a61 --- /dev/null +++ b/loader/src/ui/internal/list/InstallListCell.cpp @@ -0,0 +1,327 @@ +#include "InstallListCell.hpp" +#include "InstallListPopup.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include "../info/TagNode.hpp" +#include "../info/DevProfilePopup.hpp" + +template +static bool tryOrAlert(Result const& res, char const* title) { + if (!res) { + FLAlertLayer::create(title, res.unwrapErr(), "OK")->show(); + } + return res.isOk(); +} + +// 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({ 120, 15, 15 }); + } + } + + 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({ 120, 15, 15 }); + } + 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 00000000..582ca24d --- /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 00000000..fa639355 --- /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 00000000..7802bccb --- /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); +}; From 73169fbf22f9bc2ba9be02d4a01d901f50ccac6a Mon Sep 17 00:00:00 2001 From: ConfiG Date: Sun, 13 Aug 2023 14:00:18 +0300 Subject: [PATCH 16/36] improve index related ui - hide install list behind a popup - remove android and ios filters - don't show not installable mods - allow quick popups to be completely cancelled by esc --- loader/include/Geode/loader/Index.hpp | 6 +++ loader/include/Geode/ui/Popup.hpp | 10 ++++ loader/src/loader/Index.cpp | 52 +++++++++++++++++-- loader/src/ui/internal/info/ModInfoPopup.cpp | 51 +++++++++++++----- loader/src/ui/internal/info/ModInfoPopup.hpp | 2 + .../src/ui/internal/list/InstallListCell.cpp | 4 +- loader/src/ui/internal/list/ModListLayer.cpp | 16 ++++-- loader/src/ui/internal/list/ModListLayer.hpp | 7 ++- .../ui/internal/list/SearchFilterPopup.cpp | 50 ++++++++++++------ .../ui/internal/list/SearchFilterPopup.hpp | 1 + loader/src/ui/nodes/Popup.cpp | 33 +++++++++++- 11 files changed, 191 insertions(+), 41 deletions(-) diff --git a/loader/include/Geode/loader/Index.hpp b/loader/include/Geode/loader/Index.hpp index 79249af2..d3db061a 100644 --- a/loader/include/Geode/loader/Index.hpp +++ b/loader/include/Geode/loader/Index.hpp @@ -243,6 +243,12 @@ namespace geode { * Check if any of the mods on the index have updates available */ bool areUpdatesAvailable() const; + /** + * Checks if the mod and its required dependencies can be installed + * @param item Item to get the list for + * @returns Success if the mod and its required dependencies can be installed, an error otherwise + */ + Result<> canInstall(IndexItemHandle item) const; /** * Get the list of items needed to install this item (dependencies, etc.) * @param item Item to get the list for diff --git a/loader/include/Geode/ui/Popup.hpp b/loader/include/Geode/ui/Popup.hpp index b0e4544a..6a0eb586 100644 --- a/loader/include/Geode/ui/Popup.hpp +++ b/loader/include/Geode/ui/Popup.hpp @@ -96,4 +96,14 @@ namespace geode { char const* title, std::string const& content, char const* btn1, char const* btn2, float width, utils::MiniFunction selected, bool doShow = true ); + + GEODE_DLL FLAlertLayer* createQuickPopup( + char const* title, std::string const& content, char const* btn1, char const* btn2, + utils::MiniFunction selected, bool doShow, bool cancelledByEscape + ); + + GEODE_DLL FLAlertLayer* createQuickPopup( + char const* title, std::string const& content, char const* btn1, char const* btn2, + float width, utils::MiniFunction selected, bool doShow, bool cancelledByEscape + ); } diff --git a/loader/src/loader/Index.cpp b/loader/src/loader/Index.cpp index bf373fc6..1f61124d 100644 --- a/loader/src/loader/Index.cpp +++ b/loader/src/loader/Index.cpp @@ -547,19 +547,18 @@ bool Index::areUpdatesAvailable() const { // Item installation -Result Index::getInstallList(IndexItemHandle item) const { +Result<> Index::canInstall(IndexItemHandle item) const { if (!item->getAvailablePlatforms().count(GEODE_PLATFORM_TARGET)) { return Err("Mod is not available on {}", GEODE_PLATFORM_NAME); } - IndexInstallList list; - list.target = item; - // TODO: ui for picking recommended and suggested mods for (auto& dep : item->getMetadata().getDependencies()) { // if the dep is resolved, then all its dependencies must be installed // already in order for that to have happened if (dep.isResolved()) continue; + if (dep.importance != ModMetadata::Dependency::Importance::Required) continue; + // check if this dep is available in the index if (auto depItem = this->getItem(dep.id, dep.version)) { if (!depItem->getAvailablePlatforms().count(GEODE_PLATFORM_TARGET)) { @@ -569,12 +568,55 @@ Result Index::getInstallList(IndexItemHandle item) const { ); } // recursively add dependencies + GEODE_UNWRAP_INTO(auto deps, this->canInstall(depItem)); + } + // otherwise user must get this dependency manually from somewhere + else { + return Err( + "Dependency {} version {} not found in the index! Likely " + "reason is that the version of the dependency this mod " + "depends on is not available. Please let the developer " + "of the mod ({}) know!", + dep.id, dep.version.toString(), item->getMetadata().getDeveloper() + ); + } + } + + return Ok(); +} + +Result Index::getInstallList(IndexItemHandle item) const { + if (!item->getAvailablePlatforms().count(GEODE_PLATFORM_TARGET)) { + return Err("Mod is not available on {}", GEODE_PLATFORM_NAME); + } + + IndexInstallList list; + list.target = item; + for (auto& dep : item->getMetadata().getDependencies()) { + // if the dep is resolved, then all its dependencies must be installed + // already in order for that to have happened + if (dep.isResolved()) continue; + + if (dep.importance == ModMetadata::Dependency::Importance::Suggested) continue; + + // check if this dep is available in the index + if (auto depItem = this->getItem(dep.id, dep.version)) { + if (!depItem->getAvailablePlatforms().count(GEODE_PLATFORM_TARGET)) { + // it's fine to not install optional dependencies + if (dep.importance != ModMetadata::Dependency::Importance::Required) continue; + return Err( + "Dependency {} is not available on {}", + dep.id, GEODE_PLATFORM_NAME + ); + } + // recursively add dependencies GEODE_UNWRAP_INTO(auto deps, this->getInstallList(depItem)); ranges::push(list.list, deps.list); } // otherwise user must get this dependency manually from somewhere - // else else { + // it's fine to not install optional dependencies + if (dep.importance != ModMetadata::Dependency::Importance::Required) continue; return Err( "Dependency {} version {} not found in the index! Likely " "reason is that the version of the dependency this mod " diff --git a/loader/src/ui/internal/info/ModInfoPopup.cpp b/loader/src/ui/internal/info/ModInfoPopup.cpp index f9a69f04..0e08853b 100644 --- a/loader/src/ui/internal/info/ModInfoPopup.cpp +++ b/loader/src/ui/internal/info/ModInfoPopup.cpp @@ -750,20 +750,47 @@ void IndexItemInfoPopup::onInstallProgress(ModInstallEvent* event) { } void IndexItemInfoPopup::onInstall(CCObject*) { - InstallListPopup::create(m_item, [&](IndexInstallList const& list) { - if (m_latestVersionLabel) { - m_latestVersionLabel->setVisible(false); - } - this->setInstallStatus(UpdateProgress(0, "Starting install")); + createQuickPopup( + "Confirm Install", + "Installing this mod requires a few other mods to be installed. " + "Would you like to continue with recommended settings or " + "customize which mods to install?", + "Recommended", "Customize", 320.f, + [&](FLAlertLayer*, bool btn2) { + if (!btn2) { + auto canInstall = Index::get()->canInstall(m_item); + if (!canInstall) { + FLAlertLayer::create( + "Unable to Install", + canInstall.unwrapErr(), + "OK" + )->show(); + return; + } + this->preInstall(); + Index::get()->install(m_item); + } + else { + InstallListPopup::create(m_item, [&](IndexInstallList const& list) { + this->preInstall(); + Index::get()->install(list); + })->show(); + } + }, true, true + ); +} - m_installBtn->setTarget( - this, menu_selector(IndexItemInfoPopup::onCancel) - ); - m_installBtnSpr->setString("Cancel"); - m_installBtnSpr->setBG("GJ_button_06.png", false); +void IndexItemInfoPopup::preInstall() { + if (m_latestVersionLabel) { + m_latestVersionLabel->setVisible(false); + } + this->setInstallStatus(UpdateProgress(0, "Starting install")); - Index::get()->install(list); - })->show(); + m_installBtn->setTarget( + this, menu_selector(IndexItemInfoPopup::onCancel) + ); + m_installBtnSpr->setString("Cancel"); + m_installBtnSpr->setBG("GJ_button_06.png", false); } void IndexItemInfoPopup::onCancel(CCObject*) { diff --git a/loader/src/ui/internal/info/ModInfoPopup.hpp b/loader/src/ui/internal/info/ModInfoPopup.hpp index e5bd0aec..062230a0 100644 --- a/loader/src/ui/internal/info/ModInfoPopup.hpp +++ b/loader/src/ui/internal/info/ModInfoPopup.hpp @@ -100,6 +100,8 @@ protected: void onInstall(CCObject*); void onCancel(CCObject*); + void preInstall(); + CCNode* createLogo(CCSize const& size) override; ModMetadata getMetadata() const override; diff --git a/loader/src/ui/internal/list/InstallListCell.cpp b/loader/src/ui/internal/list/InstallListCell.cpp index 92803a61..26f3bddc 100644 --- a/loader/src/ui/internal/list/InstallListCell.cpp +++ b/loader/src/ui/internal/list/InstallListCell.cpp @@ -229,7 +229,7 @@ bool IndexItemInstallListCell::init( if (importance != ModMetadata::Dependency::Importance::Required) { message->setCString("N/A (Optional)"); - message->setColor({ 120, 15, 15 }); + message->setColor({ 163, 24, 24 }); } } @@ -297,7 +297,7 @@ bool UnknownInstallListCell::init( message->setColor({ 240, 31, 31 }); if (optional) { message->setCString("Missing (Optional)"); - message->setColor({ 120, 15, 15 }); + message->setColor({ 163, 24, 24 }); } this->addChild(message); return true; diff --git a/loader/src/ui/internal/list/ModListLayer.cpp b/loader/src/ui/internal/list/ModListLayer.cpp index 8bb873a3..b12a5bed 100644 --- a/loader/src/ui/internal/list/ModListLayer.cpp +++ b/loader/src/ui/internal/list/ModListLayer.cpp @@ -97,8 +97,7 @@ static std::optional queryMatch(ModListQuery const& query, Mod* mod) { } static std::optional queryMatch(ModListQuery const& query, IndexItemHandle item) { - // if no force visibility was provided and item is already installed, don't - // show it + // if no force visibility was provided and item is already installed, don't show it if (!query.forceVisibility && Loader::get()->isModInstalled(item->getMetadata().getID())) { return std::nullopt; } @@ -114,6 +113,16 @@ static std::optional queryMatch(ModListQuery const& query, IndexItemHandle })) { return std::nullopt; } + // if no force visibility was provided and item is already installed, don't show it + auto canInstall = Index::get()->canInstall(item); + if (!query.forceInvalid && !canInstall) { + log::warn( + "Removing {} from the list because it cannot be installed: {}", + item->getMetadata().getID(), + canInstall.unwrapErr() + ); + return std::nullopt; + } // otherwise match keywords if (auto match = queryMatchKeywords(query, item->getMetadata())) { auto weighted = match.value(); @@ -190,7 +199,8 @@ CCArray* ModListLayer::createModCells(ModListType type, ModListQuery const& quer // sort the mods by match score std::multimap sorted; - for (auto const& item : Index::get()->getItems()) { + auto index = Index::get(); + for (auto const& item : index->getItems()) { if (auto match = queryMatch(query, item)) { sorted.insert({ match.value(), item }); } diff --git a/loader/src/ui/internal/list/ModListLayer.hpp b/loader/src/ui/internal/list/ModListLayer.hpp index 16ca3ec7..648e8309 100644 --- a/loader/src/ui/internal/list/ModListLayer.hpp +++ b/loader/src/ui/internal/list/ModListLayer.hpp @@ -25,10 +25,15 @@ struct ModListQuery { */ std::optional keywords; /** - * Force mods to be shown on the list unless they explicitly mismatch some + * Force already installed mods to be shown on the list unless they explicitly mismatch some * tags (used to show installed mods on index) */ bool forceVisibility; + /** + * Force not installable mods to be shown on the list unless they explicitly mismatch some + * tags (used to show installed mods on index) + */ + bool forceInvalid; /** * Empty means current platform */ diff --git a/loader/src/ui/internal/list/SearchFilterPopup.cpp b/loader/src/ui/internal/list/SearchFilterPopup.cpp index e0927a38..61353a7e 100644 --- a/loader/src/ui/internal/list/SearchFilterPopup.cpp +++ b/loader/src/ui/internal/list/SearchFilterPopup.cpp @@ -5,7 +5,9 @@ #include #include -#include + +// re-add when we actually add the platforms +const float iosAndAndroidSize = 45.f; bool SearchFilterPopup::setup(ModListLayer* layer, ModListType type) { m_noElasticity = true; @@ -14,66 +16,77 @@ bool SearchFilterPopup::setup(ModListLayer* layer, ModListType type) { this->setTitle("Search Filters"); auto winSize = CCDirector::sharedDirector()->getWinSize(); - auto pos = CCPoint { winSize.width / 2 - 140.f, winSize.height / 2 + 45.f }; + auto pos = CCPoint { winSize.width / 2 - 140.f, winSize.height / 2 + 45.f - iosAndAndroidSize * 0.25f }; // platforms auto platformTitle = CCLabelBMFont::create("Platforms", "goldFont.fnt"); - platformTitle->setPosition(winSize.width / 2 - 85.f, winSize.height / 2 + 75.f); + platformTitle->setAnchorPoint({ 0.5f, 1.f }); + platformTitle->setPosition(winSize.width / 2 - 85.f, winSize.height / 2 + 81.5f - iosAndAndroidSize * 0.25f); platformTitle->setScale(.5f); m_mainLayer->addChild(platformTitle); auto platformBG = CCScale9Sprite::create("square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f }); platformBG->setColor({ 0, 0, 0 }); platformBG->setOpacity(90); - platformBG->setContentSize({ 290.f, 205.f }); - platformBG->setPosition(winSize.width / 2 - 85.f, winSize.height / 2 + 11.f); + platformBG->setContentSize({ 290.f, 205.f - iosAndAndroidSize * 2.f }); + platformBG->setAnchorPoint({ 0.5f, 1.f }); + platformBG->setPosition(winSize.width / 2 - 85.f, winSize.height / 2 + 62.25f - iosAndAndroidSize * 0.25f); platformBG->setScale(.5f); m_mainLayer->addChild(platformBG); this->enable(this->addPlatformToggle("Windows", PlatformID::Windows, pos), type); this->enable(this->addPlatformToggle("macOS", PlatformID::MacOS, pos), type); - this->enable(this->addPlatformToggle("IOS", PlatformID::iOS, pos), type); - this->enable(this->addPlatformToggle("Android", PlatformID::Android, pos), type); + //this->enable(this->addPlatformToggle("IOS", PlatformID::iOS, pos), type); + //this->enable(this->addPlatformToggle("Android", PlatformID::Android, pos), type); // show installed auto installedTitle = CCLabelBMFont::create("Other", "goldFont.fnt"); - installedTitle->setPosition(winSize.width / 2 - 85.f, winSize.height / 2 - 57.f); + installedTitle->setAnchorPoint({ 0.5f, 1.f }); + installedTitle->setPosition(winSize.width / 2 - 85.f, winSize.height / 2 - 50.5f + iosAndAndroidSize - iosAndAndroidSize * 0.25f); installedTitle->setScale(.5f); m_mainLayer->addChild(installedTitle); auto installedBG = CCScale9Sprite::create("square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f }); installedBG->setColor({ 0, 0, 0 }); installedBG->setOpacity(90); - installedBG->setContentSize({ 290.f, 65.f }); - installedBG->setPosition(winSize.width / 2 - 85.f, winSize.height / 2 - 85.f); + installedBG->setContentSize({ 290.f, 110.f }); + installedBG->setAnchorPoint({ 0.5f, 1.f }); + installedBG->setPosition(winSize.width / 2 - 85.f, winSize.height / 2 - 68.75f + iosAndAndroidSize - iosAndAndroidSize * 0.25f); installedBG->setScale(.5f); m_mainLayer->addChild(installedBG); - pos = CCPoint { winSize.width / 2 - 140.f, winSize.height / 2 - 85.f }; + pos = CCPoint { winSize.width / 2 - 140.f, winSize.height / 2 - 85.f + iosAndAndroidSize - iosAndAndroidSize * 0.25f }; this->addToggle( "Show Installed", menu_selector(SearchFilterPopup::onShowInstalled), m_modLayer->getQuery().forceVisibility, 0, pos ); + this->addToggle( + "Show Invalid", menu_selector(SearchFilterPopup::onShowInvalid), + m_modLayer->getQuery().forceInvalid, 1, pos + ); + // tags auto tagsTitle = CCLabelBMFont::create("Tags", "goldFont.fnt"); - tagsTitle->setPosition(winSize.width / 2 + 85.f, winSize.height / 2 + 75.f); + tagsTitle->setAnchorPoint({ 0.5f, 1.f }); + tagsTitle->setPosition(winSize.width / 2 + 85.f, winSize.height / 2 + 81.5f - iosAndAndroidSize * 0.25f); tagsTitle->setScale(.5f); m_mainLayer->addChild(tagsTitle); auto tagsBG = CCScale9Sprite::create("square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f }); tagsBG->setColor({ 0, 0, 0 }); tagsBG->setOpacity(90); - tagsBG->setContentSize({ 290.f, 328.f }); - tagsBG->setPosition(winSize.width / 2 + 85.f, winSize.height / 2 - 19.5f); + tagsBG->setContentSize({ 290.f, 328.f - iosAndAndroidSize }); + tagsBG->setAnchorPoint({ 0.5f, 1.f }); + tagsBG->setPosition(winSize.width / 2 + 85.f, winSize.height / 2 + 62.5f - iosAndAndroidSize * 0.25f); tagsBG->setScale(.5f); m_mainLayer->addChild(tagsBG); - pos = CCPoint { winSize.width / 2 + 30.f, winSize.height / 2 + 45.f }; + pos = CCPoint { winSize.width / 2 + 30.f, winSize.height / 2 + 45.f - iosAndAndroidSize * 0.25f }; for (auto& tag : Index::get()->getTags()) { auto toggle = CCMenuItemToggler::createWithStandardSprites( @@ -116,6 +129,11 @@ void SearchFilterPopup::onShowInstalled(CCObject* sender) { m_modLayer->getQuery().forceVisibility = !toggle->isToggled(); } +void SearchFilterPopup::onShowInvalid(CCObject* sender) { + auto toggle = static_cast(sender); + m_modLayer->getQuery().forceInvalid = !toggle->isToggled(); +} + void SearchFilterPopup::enable(CCMenuItemToggler* toggle, ModListType type) { if (type == ModListType::Installed) { toggle->setEnabled(false); @@ -162,7 +180,7 @@ void SearchFilterPopup::onClose(CCObject* sender) { SearchFilterPopup* SearchFilterPopup::create(ModListLayer* layer, ModListType type) { auto ret = new SearchFilterPopup(); - if (ret && ret->init(350.f, 240.f, layer, type)) { + if (ret && ret->init(350.f, 240.f - iosAndAndroidSize * 0.5f, layer, type)) { ret->autorelease(); return ret; } diff --git a/loader/src/ui/internal/list/SearchFilterPopup.hpp b/loader/src/ui/internal/list/SearchFilterPopup.hpp index 5038bf89..a02fc22d 100644 --- a/loader/src/ui/internal/list/SearchFilterPopup.hpp +++ b/loader/src/ui/internal/list/SearchFilterPopup.hpp @@ -19,6 +19,7 @@ protected: void onPlatformToggle(CCObject*); void onShowInstalled(CCObject*); + void onShowInvalid(CCObject*); void onTag(CCObject*); void enable(CCMenuItemToggler* toggle, ModListType type); diff --git a/loader/src/ui/nodes/Popup.cpp b/loader/src/ui/nodes/Popup.cpp index 2703241f..3e728663 100644 --- a/loader/src/ui/nodes/Popup.cpp +++ b/loader/src/ui/nodes/Popup.cpp @@ -5,8 +5,18 @@ using namespace geode::prelude; class QuickPopup : public FLAlertLayer, public FLAlertLayerProtocol { protected: MiniFunction m_selected; + bool m_cancelledByEscape; + bool m_usedEscape = false; + + void keyBackClicked() override { + m_usedEscape = true; + FLAlertLayer::keyBackClicked(); + } void FLAlert_Clicked(FLAlertLayer* layer, bool btn2) override { + if (m_cancelledByEscape && m_usedEscape) { + return; + } if (m_selected) { m_selected(layer, btn2); } @@ -15,10 +25,11 @@ protected: public: static QuickPopup* create( char const* title, std::string const& content, char const* btn1, char const* btn2, - float width, MiniFunction selected + float width, MiniFunction selected, bool cancelledByEscape ) { auto inst = new QuickPopup; inst->m_selected = selected; + inst->m_cancelledByEscape = cancelledByEscape; if (inst && inst->init(inst, title, content, btn1, btn2, width, false, .0f)) { inst->autorelease(); return inst; @@ -32,7 +43,7 @@ FLAlertLayer* geode::createQuickPopup( char const* title, std::string const& content, char const* btn1, char const* btn2, float width, MiniFunction selected, bool doShow ) { - auto ret = QuickPopup::create(title, content, btn1, btn2, width, selected); + auto ret = QuickPopup::create(title, content, btn1, btn2, width, selected, false); if (doShow) { ret->show(); } @@ -45,3 +56,21 @@ FLAlertLayer* geode::createQuickPopup( ) { return createQuickPopup(title, content, btn1, btn2, 350.f, selected, doShow); } + +FLAlertLayer* geode::createQuickPopup( + char const* title, std::string const& content, char const* btn1, char const* btn2, float width, + MiniFunction selected, bool doShow, bool cancelledByEscape +) { + auto ret = QuickPopup::create(title, content, btn1, btn2, width, selected, cancelledByEscape); + if (doShow) { + ret->show(); + } + return ret; +} + +FLAlertLayer* geode::createQuickPopup( + char const* title, std::string const& content, char const* btn1, char const* btn2, + MiniFunction selected, bool doShow, bool cancelledByEscape +) { + return createQuickPopup(title, content, btn1, btn2, 350.f, selected, doShow, cancelledByEscape); +} From 6ab542d51a2d0f63b2ccf579a162b847d7221e9d Mon Sep 17 00:00:00 2001 From: ConfiG Date: Sun, 13 Aug 2023 14:27:56 +0300 Subject: [PATCH 17/36] dont enable mods on update --- loader/src/loader/Index.cpp | 12 ------------ loader/src/loader/LoaderImpl.cpp | 4 ++-- loader/src/ui/internal/list/ModListLayer.cpp | 3 ++- 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/loader/src/loader/Index.cpp b/loader/src/loader/Index.cpp index 1f61124d..0ccfc6c9 100644 --- a/loader/src/loader/Index.cpp +++ b/loader/src/loader/Index.cpp @@ -651,20 +651,8 @@ void Index::Impl::installNext(size_t index, IndexInstallList const& list) { item->getMetadata().getID(), res.unwrapErr() )); } - - // If the mod is already loaded, enable it - if (mod->isLoaded()) { - res = mod->enable(); - if (!res) { - return postError(fmt::format( - "Unable to enable {}: {}", - item->getMetadata().getID(), res.unwrapErr() - )); - } - } } - // Move the temp file try { ghc::filesystem::rename( diff --git a/loader/src/loader/LoaderImpl.cpp b/loader/src/loader/LoaderImpl.cpp index 4eea6377..d0471ab4 100644 --- a/loader/src/loader/LoaderImpl.cpp +++ b/loader/src/loader/LoaderImpl.cpp @@ -202,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; } } diff --git a/loader/src/ui/internal/list/ModListLayer.cpp b/loader/src/ui/internal/list/ModListLayer.cpp index b12a5bed..4c29c4cf 100644 --- a/loader/src/ui/internal/list/ModListLayer.cpp +++ b/loader/src/ui/internal/list/ModListLayer.cpp @@ -172,7 +172,8 @@ CCArray* ModListLayer::createModCells(ModListType type, ModListQuery const& quer // newly installed for (auto const& item : Index::get()->getItems()) { if (!item->isInstalled() || - Loader::get()->isModInstalled(item->getMetadata().getID())) + 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())) { From aee84c0ffcb84a3526e59012ebef06ae6fae7ab9 Mon Sep 17 00:00:00 2001 From: ConfiG Date: Sun, 13 Aug 2023 22:20:53 +0300 Subject: [PATCH 18/36] problems list ui --- loader/src/loader/LoaderImpl.cpp | 2 +- .../src/ui/internal/list/InstallListCell.cpp | 8 - loader/src/ui/internal/list/ModListCell.cpp | 139 ++++++++++++++--- loader/src/ui/internal/list/ModListCell.hpp | 29 ++++ loader/src/ui/internal/list/ModListLayer.cpp | 18 ++- .../src/ui/internal/list/ProblemsListCell.cpp | 140 ++++++++++++++++++ .../src/ui/internal/list/ProblemsListCell.hpp | 33 +++++ .../ui/internal/list/ProblemsListPopup.cpp | 106 +++++++++++++ .../ui/internal/list/ProblemsListPopup.hpp | 22 +++ 9 files changed, 465 insertions(+), 32 deletions(-) create mode 100644 loader/src/ui/internal/list/ProblemsListCell.cpp create mode 100644 loader/src/ui/internal/list/ProblemsListCell.hpp create mode 100644 loader/src/ui/internal/list/ProblemsListPopup.cpp create mode 100644 loader/src/ui/internal/list/ProblemsListPopup.hpp diff --git a/loader/src/loader/LoaderImpl.cpp b/loader/src/loader/LoaderImpl.cpp index d0471ab4..4927e1c4 100644 --- a/loader/src/loader/LoaderImpl.cpp +++ b/loader/src/loader/LoaderImpl.cpp @@ -301,7 +301,7 @@ void Loader::Impl::queueMods(std::vector& modQueue) { m_problems.push_back({ LoadProblem::Type::Duplicate, modMetadata, - "a mod with the same ID is already present" + "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(); diff --git a/loader/src/ui/internal/list/InstallListCell.cpp b/loader/src/ui/internal/list/InstallListCell.cpp index 26f3bddc..0ba4be3e 100644 --- a/loader/src/ui/internal/list/InstallListCell.cpp +++ b/loader/src/ui/internal/list/InstallListCell.cpp @@ -11,14 +11,6 @@ #include "../info/TagNode.hpp" #include "../info/DevProfilePopup.hpp" -template -static bool tryOrAlert(Result const& res, char const* title) { - if (!res) { - FLAlertLayer::create(title, res.unwrapErr(), "OK")->show(); - } - return res.isOk(); -} - // InstallListCell void InstallListCell::draw() { diff --git a/loader/src/ui/internal/list/ModListCell.cpp b/loader/src/ui/internal/list/ModListCell.cpp index 25e026de..ba48fb89 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) { @@ -234,20 +234,8 @@ void ModCell::onEnable(CCObject* sender) { }); } -// TODO: for fod maybe :3 show problems related to this mod 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*) { @@ -258,7 +246,6 @@ void ModCell::onRestart(CCObject*) { utils::game::restart(); } -// TODO: for fod maybe :3 check if there are any problems related to this mod void ModCell::updateState() { bool unresolved = m_mod->hasUnresolvedDependencies(); if (m_enableToggle) { @@ -269,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( @@ -513,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 = @@ -547,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 64892045..0944d54a 100644 --- a/loader/src/ui/internal/list/ModListCell.hpp +++ b/loader/src/ui/internal/list/ModListCell.hpp @@ -131,3 +131,32 @@ public: 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 4c29c4cf..34478314 100644 --- a/loader/src/ui/internal/list/ModListLayer.cpp +++ b/loader/src/ui/internal/list/ModListLayer.cpp @@ -156,12 +156,9 @@ 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 @@ -474,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()) { diff --git a/loader/src/ui/internal/list/ProblemsListCell.cpp b/loader/src/ui/internal/list/ProblemsListCell.cpp new file mode 100644 index 00000000..faa2086b --- /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 00000000..8e7fc19c --- /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 00000000..9140c115 --- /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 00000000..b74d7a2d --- /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); +}; From 86accf9a3331fc7163f26996ad72a4840f20efa5 Mon Sep 17 00:00:00 2001 From: altalk23 <45172705+altalk23@users.noreply.github.com> Date: Mon, 14 Aug 2023 17:03:40 +0300 Subject: [PATCH 19/36] remove friend ModInfo::Impl (created a warning) --- loader/include/Geode/loader/ModMetadata.hpp | 2 -- loader/src/loader/LoaderImpl.hpp | 2 +- loader/src/loader/ModImpl.hpp | 2 +- loader/src/loader/ModInfoImpl.cpp | 10 +++++----- loader/src/loader/ModInfoImpl.hpp | 4 ++-- loader/src/loader/ModMetadataImpl.cpp | 4 ++-- loader/src/loader/ModMetadataImpl.hpp | 2 +- 7 files changed, 12 insertions(+), 14 deletions(-) diff --git a/loader/include/Geode/loader/ModMetadata.hpp b/loader/include/Geode/loader/ModMetadata.hpp index 9a761118..0647d98a 100644 --- a/loader/include/Geode/loader/ModMetadata.hpp +++ b/loader/include/Geode/loader/ModMetadata.hpp @@ -240,8 +240,6 @@ namespace geode { friend class Loader; friend class ModMetadataImpl; - - friend class ModInfo; }; } diff --git a/loader/src/loader/LoaderImpl.hpp b/loader/src/loader/LoaderImpl.hpp index 36001a70..0fe1b87d 100644 --- a/loader/src/loader/LoaderImpl.hpp +++ b/loader/src/loader/LoaderImpl.hpp @@ -182,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/ModImpl.hpp b/loader/src/loader/ModImpl.hpp index a258272a..a33ae12a 100644 --- a/loader/src/loader/ModImpl.hpp +++ b/loader/src/loader/ModImpl.hpp @@ -134,7 +134,7 @@ namespace geode { ModJson getRuntimeInfo() const; }; - class ModImpl : public Mod { + class ModImpl : public Mod::Impl { public: static Mod::Impl* get(); diff --git a/loader/src/loader/ModInfoImpl.cpp b/loader/src/loader/ModInfoImpl.cpp index d968c386..c482b7b6 100644 --- a/loader/src/loader/ModInfoImpl.cpp +++ b/loader/src/loader/ModInfoImpl.cpp @@ -157,19 +157,19 @@ bool const& ModInfo::isAPI() const { } Result ModInfo::createFromGeodeZip(utils::file::Unzip& zip) { - return ModMetadata::Impl::createFromGeodeZip(zip); + return ModMetadataImpl::createFromGeodeZip(zip); } Result ModInfo::createFromGeodeFile(ghc::filesystem::path const& path) { - return ModMetadata::Impl::createFromGeodeFile(path); + return ModMetadataImpl::createFromGeodeFile(path); } Result ModInfo::createFromFile(ghc::filesystem::path const& path) { - return ModMetadata::Impl::createFromFile(path); + return ModMetadataImpl::createFromFile(path); } Result ModInfo::create(ModJson const& json) { - return ModMetadata::Impl::create(json); + return ModMetadataImpl::create(json); } ModJson ModInfo::toJSON() const { @@ -227,7 +227,7 @@ ModJson const& ModInfo::rawJSON() const { } Result ModInfo::createFromSchemaV010(geode::ModJson const& json) { - return ModMetadata::Impl::createFromSchemaV010(json); + return ModMetadataImpl::createFromSchemaV010(json); } Result<> ModInfo::addSpecialFiles(ghc::filesystem::path const& dir) { diff --git a/loader/src/loader/ModInfoImpl.hpp b/loader/src/loader/ModInfoImpl.hpp index fcd80232..e7757630 100644 --- a/loader/src/loader/ModInfoImpl.hpp +++ b/loader/src/loader/ModInfoImpl.hpp @@ -14,7 +14,7 @@ using namespace geode::prelude; namespace geode { class [[deprecated]] ModInfo::Impl { public: - ModMetadata::Impl m_metadata; + ModMetadataImpl m_metadata; std::optional m_issues; std::vector m_dependencies; bool m_supportsDisabling = true; @@ -23,7 +23,7 @@ namespace geode { bool operator==(ModInfo::Impl const& other) const; }; - class [[deprecated]] ModInfoImpl { + class [[deprecated]] ModInfoImpl : public ModInfo::Impl { public: static ModInfo::Impl& getImpl(ModInfo& info); }; diff --git a/loader/src/loader/ModMetadataImpl.cpp b/loader/src/loader/ModMetadataImpl.cpp index 6799b0d1..884ace85 100644 --- a/loader/src/loader/ModMetadataImpl.cpp +++ b/loader/src/loader/ModMetadataImpl.cpp @@ -549,7 +549,7 @@ ModMetadata& ModMetadata::operator=(ModMetadata&& other) noexcept { ModMetadata::operator ModInfo() { ModInfo info; auto infoImpl = ModInfoImpl::getImpl(info); - infoImpl.m_metadata = *m_impl; + 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); @@ -558,7 +558,7 @@ ModMetadata::operator ModInfo() { ModMetadata::operator ModInfo() const { ModInfo info; auto infoImpl = ModInfoImpl::getImpl(info); - infoImpl.m_metadata = *m_impl; + 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); diff --git a/loader/src/loader/ModMetadataImpl.hpp b/loader/src/loader/ModMetadataImpl.hpp index c1cedbfa..fefa6acf 100644 --- a/loader/src/loader/ModMetadataImpl.hpp +++ b/loader/src/loader/ModMetadataImpl.hpp @@ -51,7 +51,7 @@ namespace geode { std::vector*>> getSpecialFiles(); }; - class ModMetadataImpl { + class ModMetadataImpl : public ModMetadata::Impl { public: static ModMetadata::Impl& getImpl(ModMetadata& info); }; From cd772bd0563033177afe5c389fe28b6400125cd2 Mon Sep 17 00:00:00 2001 From: ConfiG Date: Wed, 16 Aug 2023 21:13:37 +0300 Subject: [PATCH 20/36] swap Recommended/Customize, Cancel/Delete > Keep/Delete --- loader/src/ui/internal/info/ModInfoPopup.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/loader/src/ui/internal/info/ModInfoPopup.cpp b/loader/src/ui/internal/info/ModInfoPopup.cpp index 0e08853b..d6918d65 100644 --- a/loader/src/ui/internal/info/ModInfoPopup.cpp +++ b/loader/src/ui/internal/info/ModInfoPopup.cpp @@ -653,7 +653,7 @@ void LocalModInfoPopup::doUninstall() { "for the mod to take full effect). " "Would you also like to delete the mod's " "save data?", - "Cancel", + "Keep", "Delete", 350.f ); @@ -755,9 +755,9 @@ void IndexItemInfoPopup::onInstall(CCObject*) { "Installing this mod requires a few other mods to be installed. " "Would you like to continue with recommended settings or " "customize which mods to install?", - "Recommended", "Customize", 320.f, + "Customize", "Recommended", 320.f, [&](FLAlertLayer*, bool btn2) { - if (!btn2) { + if (btn2) { auto canInstall = Index::get()->canInstall(m_item); if (!canInstall) { FLAlertLayer::create( From f316c867562e5e4fb7cf12d0422b35dba819a22b Mon Sep 17 00:00:00 2001 From: ConfiG Date: Wed, 16 Aug 2023 21:16:15 +0300 Subject: [PATCH 21/36] fix typos in "successfully" --- loader/include/Geode/loader/Setting.hpp | 2 +- loader/src/internal/crashlog.hpp | 2 +- loader/src/ui/internal/info/ModInfoPopup.cpp | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/loader/include/Geode/loader/Setting.hpp b/loader/include/Geode/loader/Setting.hpp index 3f2ff368..fca764c9 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/src/internal/crashlog.hpp b/loader/src/internal/crashlog.hpp index 4d0e5cc8..4ed85293 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/ui/internal/info/ModInfoPopup.cpp b/loader/src/ui/internal/info/ModInfoPopup.cpp index d6918d65..3b30236d 100644 --- a/loader/src/ui/internal/info/ModInfoPopup.cpp +++ b/loader/src/ui/internal/info/ModInfoPopup.cpp @@ -648,7 +648,7 @@ 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 " @@ -718,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" From 1d5fae8fbbf76604c3f9a3cc84ceb55c666a0f91 Mon Sep 17 00:00:00 2001 From: ConfiG Date: Wed, 16 Aug 2023 23:29:51 +0300 Subject: [PATCH 22/36] fix loading layer not using m_fields for m_loadedModsLabel --- loader/src/hooks/LoadingLayer.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/loader/src/hooks/LoadingLayer.cpp b/loader/src/hooks/LoadingLayer.cpp index d92f0879..333b7c18 100644 --- a/loader/src/hooks/LoadingLayer.cpp +++ b/loader/src/hooks/LoadingLayer.cpp @@ -13,13 +13,13 @@ struct CustomLoadingLayer : Modify { CustomLoadingLayer() : m_loadedModsLabel(nullptr), m_updatingResources(false) {} - void updateLoadedModsLabel() const { + 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_loadedModsLabel->setCString(str.c_str()); + m_fields->m_loadedModsLabel->setCString(str.c_str()); } bool init(bool fromReload) { @@ -31,11 +31,11 @@ struct CustomLoadingLayer : Modify { auto winSize = CCDirector::sharedDirector()->getWinSize(); - m_loadedModsLabel = CCLabelBMFont::create("Geode: Loaded 0/0 mods", "goldFont.fnt"); - m_loadedModsLabel->setPosition(winSize.width / 2, 30.f); - m_loadedModsLabel->setScale(.45f); - m_loadedModsLabel->setID("geode-loaded-info"); - this->addChild(m_loadedModsLabel); + 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 From 05cb03035a5ba42ea92fea9bf4f757fd4dedbc2f Mon Sep 17 00:00:00 2001 From: ConfiG Date: Thu, 17 Aug 2023 00:04:23 +0300 Subject: [PATCH 23/36] changelog :splat: --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71b01a4e..4acc75bc 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 (82e128bb) + * Add logger nesting: log::pushNest, log::pushNest (7d74f16c) + * Add "any" version to comparable versions (2b1dc178) + * Deprecate ModInfo, replaced with ModMetadata (53b52eae) + * Add utils::game::restart (7f449b99, 0e1d6390) + * Rework the way dependencies and mod loading works (52001285) + * Early load loads mods before the game starts, while non-early load loads on the loading screen now (e7180695) + * Add support for specifying incompatibilities (52001285, 89082352) + * Add support for specifying suggested and recommended optional dependencies (52001285) + * Add UI to select which mods to install (37074183, 73169fbf, cd772bd0) + * Dependencies/dependants automatically get toggled on toggle (52001285, 6ab542d5) + * Add problems list (52001285, aee84c0f) + * Show amount of currently loaded mods on the loading screen while they're loading (e7180695, 1d5fae8f) + * Improve index-related UI (73169fbf) + * 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 (cb8759be) + * Fix index not displaying tags (ed5b5c96) + * Change "Cancel" to say "Keep" in the remove mod data on mod uninstall dialogue (cd772bd0) + * Fix typos in the word "successfully" (52001285, f316c867) ## v1.0.3 * Fix mod changelogs only showing Geode's own changelog (2945422) From 2810ce7dccb401d2b963bec81ceae95cb4db4b88 Mon Sep 17 00:00:00 2001 From: altalk23 <45172705+altalk23@users.noreply.github.com> Date: Thu, 17 Aug 2023 01:00:07 +0300 Subject: [PATCH 24/36] remove the funny compilation flags from macchew --- cmake/Platform.cmake | 1 - 1 file changed, 1 deletion(-) diff --git a/cmake/Platform.cmake b/cmake/Platform.cmake index 712f9c18..2577fc7e 100644 --- a/cmake/Platform.cmake +++ b/cmake/Platform.cmake @@ -49,7 +49,6 @@ elseif (GEODE_TARGET_PLATFORM STREQUAL "MacOS") ${CURL_LIBRARIES} ${GEODE_LOADER_PATH}/include/link/libfmod.dylib ) - target_compile_options(${PROJECT_NAME} INTERFACE -fms-extensions #[[-Wno-deprecated]] -Wno-ignored-attributes -Os #[[-flto]] #[[-fvisibility=internal]]) set(GEODE_PLATFORM_BINARY "Geode.dylib") From 4ceac1e9325dbf3a44eabd1df7e9cc071e36b4e5 Mon Sep 17 00:00:00 2001 From: matcool <26722564+matcool@users.noreply.github.com> Date: Wed, 16 Aug 2023 20:23:27 -0300 Subject: [PATCH 25/36] bump version --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 9084fa2f..524cb552 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.0 +1.1.1 From 38cc38c2c325984f2b0bc1ae171e922123718c13 Mon Sep 17 00:00:00 2001 From: matcool <26722564+matcool@users.noreply.github.com> Date: Wed, 16 Aug 2023 21:02:13 -0300 Subject: [PATCH 26/36] remove unnecessary gd thread queues for mod events --- loader/src/loader/ModImpl.cpp | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/loader/src/loader/ModImpl.cpp b/loader/src/loader/ModImpl.cpp index be0a189b..dddce70e 100644 --- a/loader/src/loader/ModImpl.cpp +++ b/loader/src/loader/ModImpl.cpp @@ -345,9 +345,7 @@ Result<> Mod::Impl::loadBinary() { LoaderImpl::get()->releaseNextMod(); - Loader::get()->queueInGDThread([&]() { - ModStateEvent(m_self, ModEventType::Loaded).post(); - }); + ModStateEvent(m_self, ModEventType::Loaded).post(); return Ok(); } @@ -390,10 +388,8 @@ Result<> Mod::Impl::enable() { } } - Loader::get()->queueInGDThread([&]() { - ModStateEvent(m_self, ModEventType::Enabled).post(); - }); m_enabled = true; + ModStateEvent(m_self, ModEventType::Enabled).post(); return Ok(); } @@ -421,10 +417,6 @@ Result<> Mod::Impl::disable() { 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) { auto res = this->disableHook(hook); @@ -438,6 +430,7 @@ Result<> Mod::Impl::disable() { } m_enabled = false; + ModStateEvent(m_self, ModEventType::Disabled).post(); if (!errors.empty()) return Err(utils::string::join(errors, "\n")); From 9a3f1c9e7b9e3e85a4001ba37f9496e46ddc4166 Mon Sep 17 00:00:00 2001 From: ConfiG Date: Fri, 18 Aug 2023 01:38:09 +0300 Subject: [PATCH 27/36] change GEODE_UNWRAP to require ; at the end --- loader/include/Geode/utils/Result.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/loader/include/Geode/utils/Result.hpp b/loader/include/Geode/utils/Result.hpp index d762f7c3..4cc1bc25 100644 --- a/loader/include/Geode/utils/Result.hpp +++ b/loader/include/Geode/utils/Result.hpp @@ -281,10 +281,10 @@ namespace geode { into = std::move(GEODE_CONCAT(unwrap_res_, __LINE__).unwrap()) #define GEODE_UNWRAP(...) \ - { \ + do { \ auto GEODE_CONCAT(unwrap_res_, __LINE__) = (__VA_ARGS__); \ if (GEODE_CONCAT(unwrap_res_, __LINE__).isErr()) { \ return geode::Err(std::move(GEODE_CONCAT(unwrap_res_, __LINE__).unwrapErr())); \ } \ - } + } while(false) } From e54bd552ce1c6141d99348404edadf2a9680ee69 Mon Sep 17 00:00:00 2001 From: altalk23 <45172705+altalk23@users.noreply.github.com> Date: Fri, 18 Aug 2023 10:07:13 +0300 Subject: [PATCH 28/36] replace macos entry with objc hook --- loader/src/platform/mac/main.mm | 33 +++++++++++---------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/loader/src/platform/mac/main.mm b/loader/src/platform/mac/main.mm index ec92b1cd..770dbd21 100644 --- a/loader/src/platform/mac/main.mm +++ b/loader/src/platform/mac/main.mm @@ -14,6 +14,7 @@ #include "../../loader/LoaderImpl.hpp" #include #include +#include using namespace geode::prelude; @@ -111,38 +112,26 @@ void updateGeode() { extern "C" void fake() {} -void applicationDidFinishLaunchingHook(void* self, SEL sel, NSNotification* notification) { +static IMP s_applicationDidFinishLaunching; +void applicationDidFinishLaunching(id self, SEL sel, NSNotification* notification) { updateGeode(); - std::array patchBytes = { - 0x55, - 0x48, 0x89, 0xe5, - 0x41, 0x57 - }; - - auto res = tulip::hook::writeMemory((void*)(base::get() + 0x69a0), patchBytes.data(), 6); - if (!res) - return; - int exitCode = geodeEntry(nullptr); if (exitCode != 0) return; - - return reinterpret_cast(geode::base::get() + 0x69a0)(self, sel, notification); + + using Type = decltype(&applicationDidFinishLaunching); + return reinterpret_cast(s_applicationDidFinishLaunching)(self, sel, notification); } bool loadGeode() { - auto detourAddr = reinterpret_cast(&applicationDidFinishLaunchingHook) - geode::base::get() - 0x69a5; - auto detourAddrPtr = reinterpret_cast(&detourAddr); + Class class_ = objc_getClass("AppController"); + SEL selector = @selector(applicationDidFinishLaunching:); + IMP function = (IMP)applicationDidFinishLaunching; + using Type = decltype(&applicationDidFinishLaunching); - std::array patchBytes = { - 0xe9, detourAddrPtr[0], detourAddrPtr[1], detourAddrPtr[2], detourAddrPtr[3] - }; - - auto res = tulip::hook::writeMemory((void*)(base::get() + 0x69a0), patchBytes.data(), 5); - if (!res) - return false; + s_applicationDidFinishLaunching = class_replaceMethod(class_, selector, function, @encode(Type)); return true; } From 794dded30f95b242426e841d7a7204ea46954af1 Mon Sep 17 00:00:00 2001 From: altalk23 <45172705+altalk23@users.noreply.github.com> Date: Fri, 18 Aug 2023 11:04:53 +0300 Subject: [PATCH 29/36] Revert "replace macos entry with objc hook" --- loader/src/platform/mac/main.mm | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/loader/src/platform/mac/main.mm b/loader/src/platform/mac/main.mm index 770dbd21..ec92b1cd 100644 --- a/loader/src/platform/mac/main.mm +++ b/loader/src/platform/mac/main.mm @@ -14,7 +14,6 @@ #include "../../loader/LoaderImpl.hpp" #include #include -#include using namespace geode::prelude; @@ -112,26 +111,38 @@ void updateGeode() { extern "C" void fake() {} -static IMP s_applicationDidFinishLaunching; -void applicationDidFinishLaunching(id self, SEL sel, NSNotification* notification) { +void applicationDidFinishLaunchingHook(void* self, SEL sel, NSNotification* notification) { updateGeode(); + std::array patchBytes = { + 0x55, + 0x48, 0x89, 0xe5, + 0x41, 0x57 + }; + + auto res = tulip::hook::writeMemory((void*)(base::get() + 0x69a0), patchBytes.data(), 6); + if (!res) + return; + int exitCode = geodeEntry(nullptr); if (exitCode != 0) return; - - using Type = decltype(&applicationDidFinishLaunching); - return reinterpret_cast(s_applicationDidFinishLaunching)(self, sel, notification); + + return reinterpret_cast(geode::base::get() + 0x69a0)(self, sel, notification); } bool loadGeode() { - Class class_ = objc_getClass("AppController"); - SEL selector = @selector(applicationDidFinishLaunching:); - IMP function = (IMP)applicationDidFinishLaunching; - using Type = decltype(&applicationDidFinishLaunching); + auto detourAddr = reinterpret_cast(&applicationDidFinishLaunchingHook) - geode::base::get() - 0x69a5; + auto detourAddrPtr = reinterpret_cast(&detourAddr); - s_applicationDidFinishLaunching = class_replaceMethod(class_, selector, function, @encode(Type)); + std::array patchBytes = { + 0xe9, detourAddrPtr[0], detourAddrPtr[1], detourAddrPtr[2], detourAddrPtr[3] + }; + + auto res = tulip::hook::writeMemory((void*)(base::get() + 0x69a0), patchBytes.data(), 5); + if (!res) + return false; return true; } From 919276977c74197951e6ca56c06136879d4fbb61 Mon Sep 17 00:00:00 2001 From: ConfiG Date: Fri, 18 Aug 2023 02:31:16 +0300 Subject: [PATCH 30/36] improve install confirmation popup --- loader/src/ui/internal/info/ModInfoPopup.cpp | 95 ++++++++++++++------ 1 file changed, 68 insertions(+), 27 deletions(-) diff --git a/loader/src/ui/internal/info/ModInfoPopup.cpp b/loader/src/ui/internal/info/ModInfoPopup.cpp index 3b30236d..4173072d 100644 --- a/loader/src/ui/internal/info/ModInfoPopup.cpp +++ b/loader/src/ui/internal/info/ModInfoPopup.cpp @@ -750,34 +750,75 @@ void IndexItemInfoPopup::onInstallProgress(ModInstallEvent* event) { } void IndexItemInfoPopup::onInstall(CCObject*) { - createQuickPopup( - "Confirm Install", - "Installing this mod requires a few other mods to be installed. " - "Would you like to continue with recommended settings or " - "customize which mods to install?", - "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; - } + auto deps = m_item->getMetadata().getDependencies(); + enum class DepState { + None, + HasOnlyRequired, + HasOptional + } depState = DepState::None; + for (auto const& item : deps) { + // resolved means it's already installed, so + // no need to ask the user whether they want to install it + if (Loader::get()->isModLoaded(item.id)) + continue; + if (item.importance != ModMetadata::Dependency::Importance::Required) { + depState = DepState::HasOptional; + break; + } + depState = DepState::HasOnlyRequired; + } + + std::string content; + char const* btn1; + char const* btn2; + switch (depState) { + case DepState::None: + content = fmt::format( + "Are you sure you want to install {}?", + m_item->getMetadata().getName() + ); + btn1 = "Info"; + btn2 = "Install"; + break; + case DepState::HasOnlyRequired: + content = + "Installing this mod requires other mods to be installed. " + "Would you like to proceed with the installation or " + "view which mods are going to be installed?"; + btn1 = "View"; + btn2 = "Proceed"; + break; + case DepState::HasOptional: + content = + "This mod recommends installing other mods alongside it. " + "Would you like to continue with recommended settings or " + "customize which mods to install?"; + btn1 = "Customize"; + btn2 = "Recommended"; + break; + } + + createQuickPopup("Confirm Install", content, btn1, btn2, 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(m_item); - } - else { - InstallListPopup::create(m_item, [&](IndexInstallList const& list) { - this->preInstall(); - Index::get()->install(list); - })->show(); - } - }, true, true - ); + Index::get()->install(list); + })->show(); + } + }, true, true); } void IndexItemInfoPopup::preInstall() { From ef1f1d15c6ed9746e958db5102f5fd58d82b5a3b Mon Sep 17 00:00:00 2001 From: ConfiG Date: Fri, 18 Aug 2023 22:52:48 +0300 Subject: [PATCH 31/36] fix search buttons not clickable over view buttons --- loader/src/ui/internal/list/ModListLayer.cpp | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/loader/src/ui/internal/list/ModListLayer.cpp b/loader/src/ui/internal/list/ModListLayer.cpp index 34478314..3ff3b930 100644 --- a/loader/src/ui/internal/list/ModListLayer.cpp +++ b/loader/src/ui/internal/list/ModListLayer.cpp @@ -428,6 +428,9 @@ void ModListLayer::createSearchControl() { inputBG->setScale(.5f); m_searchBG->addChild(inputBG); + if (m_searchInput) + return; + m_searchInput = CCTextInputNode::create(310.f - buttonSpace, 20.f, "Search Mods...", "bigFont.fnt"); m_searchInput->setLabelPlaceholderColor({ 150, 150, 150 }); @@ -457,10 +460,7 @@ void ModListLayer::reloadList(std::optional const& query) { std::nullopt; // remove old list - if (m_list) { - if (m_searchBG) m_searchBG->retain(); - m_list->removeFromParent(); - } + if (m_list) m_list->removeFromParent(); auto items = this->createModCells(g_tab, m_query); @@ -522,13 +522,7 @@ void ModListLayer::reloadList(std::optional const& query) { m_tabsGradientSprite->setPosition(m_list->getPosition() + CCPoint{179.f, 235.f}); // add search input to list - if (!m_searchInput) { - this->createSearchControl(); - } - else { - m_list->addChild(m_searchBG); - m_searchBG->release(); - } + this->createSearchControl(); // enable filter button m_filterBtn->setEnabled(g_tab != ModListType::Installed); From 108f56aa6220f6c48b9b2555bb25b4a1e07f7e91 Mon Sep 17 00:00:00 2001 From: ConfiG Date: Sat, 19 Aug 2023 00:11:17 +0300 Subject: [PATCH 32/36] improve tab textures --- .../blanks/baseTab_Normal_Selected.png | Bin 8706 -> 8838 bytes .../blanks/baseTab_Normal_Unselected.png | Bin 25844 -> 9304 bytes .../blanks/baseTab_Normal_UnselectedDark.png | Bin 30623 -> 9409 bytes loader/resources/images/tab-gradient-mask.png | Bin 6449 -> 5884 bytes 4 files changed, 0 insertions(+), 0 deletions(-) diff --git a/loader/resources/blanks/baseTab_Normal_Selected.png b/loader/resources/blanks/baseTab_Normal_Selected.png index b5363c29cbbe95890a37c07b20ceef5da9e2fb25..49e452554ac1fa8ca67e1d39f8081ea5ecbd9443 100644 GIT binary patch delta 5413 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 EKj^M9GXMYp delta 5321 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 literal 25844 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 XGpZQ0m9M*Q7f0(F>6B`nzx2NVtRDDD literal 30623 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�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 delta 3056 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 adv8`s Date: Sat, 19 Aug 2023 00:12:56 +0300 Subject: [PATCH 33/36] update changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 553a0704..799109de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Geode Changelog +## v1.1.1 + * Improve installation confirmation popup (3ec44cf) + * Remove unnecessary main thread queues for mod events (38cc38c) + * Fix search and filter buttons being not clickable when over the view/restart button of a mod (e616336) + * Improve tab textures (00d30de) + * Properly align the borders + * Make the selected and unselected tabs the same height + ## v1.1.0 * Fix json library not actually being dynamically exported/imported (5f65d97) * Update TulipHook, gets rid of keystone dependency and adds stdcall support (efcbf58, 7b90903) From fa7f486c5cea91ac7494f4a767280416753c99cc Mon Sep 17 00:00:00 2001 From: ConfiG Date: Sat, 19 Aug 2023 00:14:36 +0300 Subject: [PATCH 34/36] fix updated changelog --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 799109de..4d27369f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,10 @@ # Geode Changelog ## v1.1.1 - * Improve installation confirmation popup (3ec44cf) + * Improve installation confirmation popup (9192769) * Remove unnecessary main thread queues for mod events (38cc38c) - * Fix search and filter buttons being not clickable when over the view/restart button of a mod (e616336) - * Improve tab textures (00d30de) + * Fix search and filter buttons being not clickable when over the view/restart button of a mod (ef1f1d1) + * Improve tab textures (108f56a) * Properly align the borders * Make the selected and unselected tabs the same height From dfc75c8940feae26e250cf204ee91ad27ad798ca Mon Sep 17 00:00:00 2001 From: Cvolton Date: Fri, 18 Aug 2023 23:18:36 +0200 Subject: [PATCH 35/36] fix normal percentage showing up as negative (#211) --- bindings/GeometryDash.bro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/GeometryDash.bro b/bindings/GeometryDash.bro index ce03576c..c8481280 100644 --- a/bindings/GeometryDash.bro +++ b/bindings/GeometryDash.bro @@ -2330,7 +2330,7 @@ class GJGameLevel : cocos2d::CCNode { int m_chk; bool m_isChkValid; bool m_isCompletionLegitimate; - geode::SeedValueVRS m_normalPercent; + geode::SeedValueVSR m_normalPercent; geode::SeedValueRSV m_orbCompletion; geode::SeedValueRSV m_newNormalPercent2; int m_practicePercent; From a54e23606c5530f1df11ff188d97c80125e7a03f Mon Sep 17 00:00:00 2001 From: Cvolton Date: Sat, 19 Aug 2023 00:06:47 +0200 Subject: [PATCH 36/36] add LevelCell and GJScoreCell constructors to bindings --- bindings/GeometryDash.bro | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bindings/GeometryDash.bro b/bindings/GeometryDash.bro index c8481280..b8b5d930 100644 --- a/bindings/GeometryDash.bro +++ b/bindings/GeometryDash.bro @@ -2597,6 +2597,7 @@ class GJScoreCell : TableViewCell { void loadFromScore(GJUserScore* score) = win 0x61440; void onViewProfile(cocos2d::CCObject* sender) = win 0x62380; void updateBGColor(int index) = win 0x5c6b0; + GJScoreCell(char const* key, float width, float height) = win 0x613C0; } class GJSearchObject : cocos2d::CCNode { @@ -3761,6 +3762,7 @@ class LevelCell : TableViewCell { void loadCustomLevelCell() = mac 0x1183b0, win 0x5a020; void updateBGColor(int index) = win 0x5c6b0; void loadFromLevel(GJGameLevel* level) = win 0x59FD0; + LevelCell(char const* key, float width, float height) = win 0x59F40; } class LevelCommentDelegate {