diff --git a/CHANGELOG.md b/CHANGELOG.md index f76676d5..27b007a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Geode Changelog +## v1.3.0 + * Completely remove runtime enabling & disabling of mods (d817749) + * Patches auto enabling can be disabled (69821f3) + * Move ModEventType::Loaded to after hooks & patches are enabled (23c3095) + * Update index to be able to store multiple versions of the same mod (5572f9c) + * Implement UI for selection of downloading specific mod versions (5d15eb0) + * Change install & uninstall popups to reflect the new changes (d40f467) + * Keep the scroll when enabling, installing etc. a mod (b3d444a) + * Update MacOS crashlog to include base and offset (7816c43) + * Add user agent to AsyncWebRequest (c256207) + * Add post and custom requests to AsyncWebRequest (c256207) + ## v1.2.1 * Mods now target macOS 10.13 instead of 10.14 (7cc1cd4) * Fix CustomizeObjectLayer ids moving around when multiple objects are selected (9ee0994, 87749d4) diff --git a/CMakeLists.txt b/CMakeLists.txt index e1db6660..62e9b72c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -96,7 +96,7 @@ if (PROJECT_IS_TOP_LEVEL AND NOT GEODE_BUILDING_DOCS) set(TULIP_LINK_SOURCE ON) endif() set(CMAKE_WARN_DEPRECATED OFF CACHE BOOL "" FORCE) -CPMAddPackage("gh:geode-sdk/TulipHook#f77ccbe") +CPMAddPackage("gh:geode-sdk/TulipHook#2e4cb5a") set(CMAKE_WARN_DEPRECATED ON CACHE BOOL "" FORCE) # Silence warnings from dependencies diff --git a/VERSION b/VERSION index cb174d58..f0bb29e7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.2.1 \ No newline at end of file +1.3.0 diff --git a/loader/include/Geode/loader/Hook.hpp b/loader/include/Geode/loader/Hook.hpp index 51ffdf00..6d931365 100644 --- a/loader/include/Geode/loader/Hook.hpp +++ b/loader/include/Geode/loader/Hook.hpp @@ -12,7 +12,7 @@ namespace geode { class Mod; class Loader; - class GEODE_DLL Hook { + class GEODE_DLL Hook final { private: class Impl; std::shared_ptr m_impl; @@ -143,20 +143,23 @@ namespace geode { void setAutoEnable(bool autoEnable); }; - class GEODE_DLL Patch { + class GEODE_DLL Patch final { + // Change to private in 2.0.0 protected: Mod* m_owner; void* m_address; ByteVector m_original; ByteVector m_patch; bool m_applied; + bool m_autoEnable; // Only allow friend classes to create // patches. Whatever method created the // patches should take care of populating // m_owner, m_address, m_original and // m_patch. - Patch() : m_applied(false) {} + Patch(); + ~Patch(); // no copying Patch(Patch const&) = delete; @@ -170,17 +173,13 @@ namespace geode { * Get the address of the patch. * @returns Address */ - uintptr_t getAddress() const { - return reinterpret_cast(m_address); - } + uintptr_t getAddress() const; /** * Get whether the patch is applied or not. * @returns True if applied, false if not. */ - bool isApplied() const { - return m_applied; - } + bool isApplied() const; bool apply(); bool restore(); @@ -189,14 +188,24 @@ namespace geode { * Get the owner of this patch. * @returns Pointer to the owner's Mod handle. */ - Mod* getOwner() const { - return m_owner; - } + Mod* getOwner() const; /** * Get info about the patch as JSON * @note For IPC */ json::Value getRuntimeInfo() const; + + /** + * Get whether the patch should be auto enabled or not. + * @returns Auto enable + */ + bool getAutoEnable() const; + + /** + * Set whether the patch should be auto enabled or not. + * @param autoEnable Auto enable + */ + void setAutoEnable(bool autoEnable); }; } diff --git a/loader/include/Geode/loader/Index.hpp b/loader/include/Geode/loader/Index.hpp index d3db061a..4e75565b 100644 --- a/loader/include/Geode/loader/Index.hpp +++ b/loader/include/Geode/loader/Index.hpp @@ -107,6 +107,14 @@ namespace geode { std::unique_ptr m_impl; public: + /** + * Returns the path that contains all the versions + */ + ghc::filesystem::path getRootPath() const; + + /** + * Returns the path to the specific version + */ ghc::filesystem::path getPath() const; [[deprecated("use getMetadata instead")]] ModInfo getModInfo() const; ModMetadata getMetadata() const; @@ -124,10 +132,14 @@ namespace geode { void setAvailablePlatforms(std::unordered_set const& value); void setIsFeatured(bool const& value); void setTags(std::unordered_set const& value); + void setIsInstalled(bool const& value); #endif IndexItem(); ~IndexItem(); + + friend class ModMetadata; + friend class Index; }; using IndexItemHandle = std::shared_ptr; @@ -168,12 +180,22 @@ namespace geode { * Get all featured index items */ std::vector getFeaturedItems() const; + /** + * Get all latest index items + */ + std::vector getLatestItems() const; /** * Get all index items by a developer */ std::vector getItemsByDeveloper( std::string const& name ) const; + /** + * Get all index items for a specific mod + */ + std::vector getItemsByModID( + std::string const& modID + ) const; /** * Check if an item with this ID is found on the index, and optionally * provide the version sought after diff --git a/loader/include/Geode/loader/Mod.hpp b/loader/include/Geode/loader/Mod.hpp index d80de2de..27f8eb44 100644 --- a/loader/include/Geode/loader/Mod.hpp +++ b/loader/include/Geode/loader/Mod.hpp @@ -33,6 +33,13 @@ namespace geode { ~HandleToSaved(); }; + enum class ModRequestedAction { + None, + Enable, + Disable, + Uninstall, + }; + GEODE_HIDDEN Mod* takeNextLoaderMod(); class ModImpl; @@ -337,6 +344,8 @@ namespace geode { Result<> uninstall(); bool isUninstalled() const; + ModRequestedAction getRequestedAction() const; + /** * Check whether or not this Mod * depends on another mod diff --git a/loader/include/Geode/loader/ModMetadata.hpp b/loader/include/Geode/loader/ModMetadata.hpp index 0647d98a..f13f2788 100644 --- a/loader/include/Geode/loader/ModMetadata.hpp +++ b/loader/include/Geode/loader/ModMetadata.hpp @@ -240,6 +240,7 @@ namespace geode { friend class Loader; friend class ModMetadataImpl; + friend class IndexItem; }; } diff --git a/loader/include/Geode/utils/web.hpp b/loader/include/Geode/utils/web.hpp index 4bf2d140..f87313d4 100644 --- a/loader/include/Geode/utils/web.hpp +++ b/loader/include/Geode/utils/web.hpp @@ -104,6 +104,16 @@ namespace geode::utils::web { template using DataConverter = Result (*)(ByteVector const&); + // Hack until 2.0.0 to store extra members in AsyncWebRequest + struct AsyncWebRequestData { + std::string m_userAgent; + std::string m_customRequest; + bool m_isPostRequest = false; + std::string m_postFields; + bool m_isJsonRequest = false; + bool m_sent = false; + }; + /** * An asynchronous, thread-safe web request. Downloads data from the * internet without slowing the main thread. All callbacks are run in the @@ -111,16 +121,22 @@ namespace geode::utils::web { */ class GEODE_DLL AsyncWebRequest { private: + // i want to cry whose idea was to not make this pimpl + // For 2.0.0: make this pimpl + std::optional m_joinID; std::string m_url; AsyncThen m_then = nullptr; AsyncExpectCode m_expect = nullptr; AsyncProgress m_progress = nullptr; AsyncCancelled m_cancelled = nullptr; - bool m_sent = false; + mutable AsyncWebRequestData* m_extra = nullptr; std::variant m_target; std::vector m_httpHeaders; + AsyncWebRequestData& extra(); + AsyncWebRequestData const& extra() const; + template friend class AsyncWebResult; friend class SentAsyncWebRequest; @@ -151,6 +167,29 @@ namespace geode::utils::web { * Can be called more than once. */ AsyncWebRequest& header(std::string const& header); + /** + * In order to specify an user agent to the request, give it here. + */ + AsyncWebRequest& userAgent(std::string const& userAgent); + /** + * Specify that the request is a POST request. + */ + AsyncWebRequest& postRequest(); + /** + * Specify that the request is a custom request like PUT and DELETE. + */ + AsyncWebRequest& customRequest(std::string const& request); + /** + * Specify the post fields to send with the request. Only valid if + * `postRequest` or `customRequest` was called before. + */ + AsyncWebRequest& postFields(std::string const& fields); + /** + * Specify the post fields to send with the request. Only valid if + * `postRequest` or `customRequest` was called before. Additionally + * sets the content type to application/json. + */ + AsyncWebRequest& postFields(json::Value const& fields); /** * URL to fetch from the internet asynchronously * @param url URL of the data to download. Redirects will be diff --git a/loader/src/loader/Hook.cpp b/loader/src/loader/Hook.cpp index 7ae74b37..c1b42c57 100644 --- a/loader/src/loader/Hook.cpp +++ b/loader/src/loader/Hook.cpp @@ -12,6 +12,7 @@ using namespace geode::prelude; Hook::Hook(std::shared_ptr&& impl) : m_impl(std::move(impl)) {} Hook::~Hook() {} +// These classes (Hook and Patch) are nasty using new and delete, change them in 2.0.0 Hook* Hook::create( Mod* owner, void* address, diff --git a/loader/src/loader/Index.cpp b/loader/src/loader/Index.cpp index 1a0da86d..d223ae0c 100644 --- a/loader/src/loader/Index.cpp +++ b/loader/src/loader/Index.cpp @@ -47,12 +47,14 @@ IndexUpdateFilter::IndexUpdateFilter() {} class IndexItem::Impl final { private: + ghc::filesystem::path m_rootPath; ghc::filesystem::path m_path; ModMetadata m_metadata; std::string m_downloadURL; std::string m_downloadHash; std::unordered_set m_platforms; - bool m_isFeatured; + bool m_isFeatured = false; + bool m_isInstalled = false; std::unordered_set m_tags; friend class IndexItem; @@ -62,6 +64,7 @@ public: * Create IndexItem from a directory */ static Result> create( + ghc::filesystem::path const& rootDir, ghc::filesystem::path const& dir ); @@ -71,6 +74,10 @@ public: IndexItem::IndexItem() : m_impl(std::make_unique()) {} IndexItem::~IndexItem() = default; +ghc::filesystem::path IndexItem::getRootPath() const { + return m_impl->m_rootPath; +} + ghc::filesystem::path IndexItem::getPath() const { return m_impl->m_path; } @@ -131,9 +138,13 @@ void IndexItem::setIsFeatured(bool const& value) { void IndexItem::setTags(std::unordered_set const& value) { m_impl->m_tags = value; } + +void IndexItem::setIsInstalled(bool const& value) { + m_impl->m_isInstalled = value; +} #endif -Result IndexItem::Impl::create(ghc::filesystem::path const& dir) { +Result IndexItem::Impl::create(ghc::filesystem::path const& rootDir, ghc::filesystem::path const& dir) { GEODE_UNWRAP_INTO( auto entry, file::readJson(dir / "entry.json") .expect("Unable to read entry.json") @@ -142,6 +153,10 @@ Result IndexItem::Impl::create(ghc::filesystem::path const& dir auto metadata, ModMetadata::createFromFile(dir / "mod.json") .expect("Unable to read mod.json: {error}") ); + auto metadataRes = metadata.addSpecialFiles(rootDir); + if (!metadataRes) { + log::warn("Unable to add special files from {}: {}", rootDir, metadataRes.unwrapErr()); + } JsonChecker checker(entry); auto root = checker.root("[entry.json]").obj(); @@ -157,6 +172,7 @@ Result IndexItem::Impl::create(ghc::filesystem::path const& dir } auto item = std::make_shared(); + item->m_impl->m_rootPath = rootDir; item->m_impl->m_path = dir; item->m_impl->m_metadata = metadata; item->m_impl->m_platforms = platforms; @@ -172,7 +188,17 @@ Result IndexItem::Impl::create(ghc::filesystem::path const& dir } bool IndexItem::Impl::isInstalled() const { - return ghc::filesystem::exists(dirs::getModsDir() / (m_metadata.getID() + ".geode")); + if (m_isInstalled) { + return true; + } + if (!Loader::get()->isModInstalled(m_metadata.getID())) { + return false; + } + auto installed = Loader::get()->getInstalledMod(m_metadata.getID()); + if (installed->getVersion() != m_metadata.getVersion()) { + return false; + } + return true; } // Helpers @@ -209,7 +235,7 @@ class Index::Impl final { public: // for once, the fact that std::map is ordered is useful (this makes // getting the latest version of a mod as easy as items.rbegin()) - using ItemVersions = std::map; + using ItemVersions = std::map; private: std::unordered_map< @@ -335,6 +361,7 @@ void Index::Impl::checkForUpdates() { auto oldSHA = file::readString(checksum).unwrapOr(""); web::AsyncWebRequest() .join("index-update") + .userAgent("github_api/1.0") .header(fmt::format("If-None-Match: \"{}\"", oldSHA)) .header("Accept: application/vnd.github.sha") .fetch("https://api.github.com/repos/geode-sdk/mods/commits/main") @@ -369,34 +396,36 @@ void Index::Impl::updateFromLocalTree() { // delete old items m_items.clear(); - // read directory and add new items - try { - for (auto& dir : ghc::filesystem::directory_iterator(dirs::getIndexDir() / "v0" / "mods")) { - auto addRes = IndexItem::Impl::create(dir); + auto indexRoot = dirs::getIndexDir() / "v0"; + auto entriesRoot = indexRoot / "mods-v2"; + + auto configRes = file::readJson(indexRoot / "config.json"); + if (!configRes) { + IndexUpdateEvent("Unable to read index config").post(); + return; + } + auto config = configRes.unwrap(); + + JsonChecker checker(config); + auto root = checker.root("[config.json]").obj(); + + for (auto& [modID, entry] : root.has("entries").items()) { + for (auto& version : entry.obj().has("versions").iterate()) { + auto rootDir = entriesRoot / modID; + auto dir = rootDir / version.get(); + + auto addRes = IndexItem::Impl::create(rootDir, dir); if (!addRes) { log::warn("Unable to add index item from {}: {}", dir, addRes.unwrapErr()); continue; } auto add = addRes.unwrap(); auto metadata = add->getMetadata(); - // check if this major version of this item has already been added - if (m_items[metadata.getID()].count(metadata.getVersion().getMajor())) { - log::warn( - "Item {}@{} has already been added, skipping", - metadata.getID(), - metadata.getVersion() - ); - continue; - } - // add new major version of this item - m_items[metadata.getID()].insert({metadata.getVersion().getMajor(), + + m_items[modID].insert({metadata.getVersion(), add }); } - } catch(std::exception& e) { - log::error("Unable to read local index tree: {}", e.what()); - IndexUpdateEvent("Unable to read local index tree").post(); - return; } // mark source as finished @@ -436,6 +465,14 @@ std::vector Index::getItems() const { return res; } +std::vector Index::getLatestItems() const { + std::vector res; + for (auto& [modID, versions] : m_impl->m_items) { + res.push_back(this->getMajorItem(modID)); + } + return res; +} + std::vector Index::getFeaturedItems() const { std::vector res; for (auto& items : map::values(m_impl->m_items)) { @@ -462,6 +499,18 @@ std::vector Index::getItemsByDeveloper( return res; } +std::vector Index::getItemsByModID( + std::string const& modID +) const { + std::vector res; + if (m_impl->m_items.count(modID)) { + for (auto& [versionStr, item] : m_impl->m_items[modID]) { + res.push_back(item); + } + } + return res; +} + bool Index::isKnownItem( std::string const& id, std::optional version @@ -485,19 +534,14 @@ IndexItemHandle Index::getItem( if (m_impl->m_items.count(id)) { auto versions = m_impl->m_items.at(id); if (version) { - // prefer most major version for (auto& [_, item] : ranges::reverse(m_impl->m_items.at(id))) { if (version.value() == item->getMetadata().getVersion()) { return item; } } - } else { - if (!versions.empty()) { - return m_impl->m_items.at(id).rbegin()->second; - } } } - return nullptr; + return this->getMajorItem(id); } IndexItemHandle Index::getItem( @@ -718,6 +762,8 @@ void Index::Impl::installNext(size_t index, IndexInstallList const& list) { )); } + item->setIsInstalled(true); + // Install next item in queue this->installNext(index + 1, list); }) @@ -752,6 +798,10 @@ void Index::cancelInstall(IndexItemHandle item) { } void Index::install(IndexInstallList const& list) { + if (list.list.empty()) { + ModInstallEvent(list.target->getMetadata().getID(), UpdateFinished()).post(); + return; + } Loader::get()->queueInMainThread([this, list]() { m_impl->installNext(0, list); }); diff --git a/loader/src/loader/LoaderImpl.cpp b/loader/src/loader/LoaderImpl.cpp index 63edac8e..61afcb13 100644 --- a/loader/src/loader/LoaderImpl.cpp +++ b/loader/src/loader/LoaderImpl.cpp @@ -166,7 +166,6 @@ 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->isUninstalled() || mod->isEnabled()); auto r = mod->saveData(); if (!r) { log::warn("Unable to save data for mod \"{}\": {}", mod->getID(), r.unwrapErr()); @@ -411,31 +410,16 @@ void Loader::Impl::loadModGraph(Mod* node, bool early) { 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(); + log::debug("Load"); + auto res = node->m_impl->loadBinary(); if (!res) { - node->m_impl->m_enabled = true; - (void)node->m_impl->disable(); m_problems.push_back({ - LoadProblem::Type::EnableFailed, + LoadProblem::Type::LoadFailed, node, res.unwrapErr() }); - log::error("Failed to enable: {}", res.unwrapErr()); + log::error("Failed to load binary: {}", res.unwrapErr()); log::popNest(); return; } @@ -509,19 +493,19 @@ void Loader::Impl::findProblems() { 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); - } + // 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(); } @@ -533,7 +517,7 @@ void Loader::Impl::refreshModGraph() { auto begin = std::chrono::high_resolution_clock::now(); - if (m_mods.size() > 1) { + if (m_isSetup) { log::error("Cannot refresh mod graph after startup"); log::popNest(); return; @@ -564,7 +548,7 @@ void Loader::Impl::refreshModGraph() { m_loadingState = LoadingState::EarlyMods; log::debug("Loading early mods"); log::pushNest(); - for (auto const& dep : Mod::get()->m_impl->m_dependants) { + for (auto const& dep : ModImpl::get()->m_dependants) { this->loadModGraph(dep, true); } log::popNest(); @@ -716,6 +700,7 @@ void Loader::Impl::fetchLatestGithubRelease( // TODO: add header to not get rate limited web::AsyncWebRequest() .join("loader-auto-update-check") + .userAgent("github_api/1.0") .fetch("https://api.github.com/repos/geode-sdk/geode/releases/latest") .json() .then([this, then](json::Value const& json) { @@ -784,6 +769,7 @@ void Loader::Impl::downloadLoaderResources(bool useLatestRelease) { if (!useLatestRelease) { web::AsyncWebRequest() .join("loader-tag-exists-check") + .userAgent("github_api/1.0") .fetch(fmt::format( "https://api.github.com/repos/geode-sdk/geode/git/ref/tags/{}", this->getVersion().toString() diff --git a/loader/src/loader/Mod.cpp b/loader/src/loader/Mod.cpp index cee3f4cf..392d91c4 100644 --- a/loader/src/loader/Mod.cpp +++ b/loader/src/loader/Mod.cpp @@ -198,6 +198,10 @@ bool Mod::isUninstalled() const { return m_impl->isUninstalled(); } +ModRequestedAction Mod::getRequestedAction() const { + return m_impl->getRequestedAction(); +} + bool Mod::depends(std::string const& id) const { return m_impl->depends(id); } diff --git a/loader/src/loader/ModImpl.cpp b/loader/src/loader/ModImpl.cpp index 7f05c554..dc1c838a 100644 --- a/loader/src/loader/ModImpl.cpp +++ b/loader/src/loader/ModImpl.cpp @@ -117,23 +117,15 @@ bool Mod::Impl::isLoaded() const { } bool Mod::Impl::supportsDisabling() const { - return m_metadata.getID() != "geode.loader" && !m_metadata.isAPI(); + return m_metadata.getID() != "geode.loader"; } 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(); - })); + return true; } 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(); - })); + return true; } bool Mod::Impl::needsEarlyLoad() const { @@ -345,119 +337,74 @@ Result<> Mod::Impl::loadBinary() { LoaderImpl::get()->releaseNextMod(); - ModStateEvent(m_self, ModEventType::Loaded).post(); - - return Ok(); -} - -Result<> Mod::Impl::enable() { - 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() || !item.mod) - 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()); continue; } if (hook->getAutoEnable()) { - GEODE_UNWRAP(this->enableHook(hook)); + auto res = this->enableHook(hook); + if (!res) { + log::error("Can't enable hook {} for mod {}: {}", hook->getDisplayName(), m_metadata.getID(), res.unwrapErr()); + } } } for (auto const& patch : m_patches) { - if (!patch->apply()) { - log::warn("Unable to apply patch at {}", patch->getAddress()); + if (!patch) { + log::warn("Patch is null in mod \"{}\"", m_metadata.getName()); continue; } + if (patch->getAutoEnable()) { + if (!patch->apply()) { + log::warn("Unable to apply patch at {}", patch->getAddress()); + continue; + } + } } m_enabled = true; + + ModStateEvent(m_self, ModEventType::Loaded).post(); ModStateEvent(m_self, ModEventType::Enabled).post(); return Ok(); } +Result<> Mod::Impl::enable() { + if (m_requestedAction != ModRequestedAction::None) { + return Err("Mod already has a requested action"); + } + + m_requestedAction = ModRequestedAction::Enable; + Mod::get()->setSavedValue("should-load-" + m_metadata.getID(), true); + + return Ok(); +} + Result<> Mod::Impl::disable() { - 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_requestedAction != ModRequestedAction::None) { + return Err("Mod already has a requested action"); } - if (!disabledDependants) - return Err("Mod cannot be disabled because one or more of its dependants cannot be disabled."); - - std::vector errors; - for (auto const& hook : m_hooks) { - auto res = this->disableHook(hook); - if (!res) - errors.push_back(res.unwrapErr()); - } - for (auto const& patch : m_patches) { - auto res = this->unpatch(patch); - if (!res) - errors.push_back(res.unwrapErr()); - } - - m_enabled = false; - ModStateEvent(m_self, ModEventType::Disabled).post(); - - if (!errors.empty()) - return Err(utils::string::join(errors, "\n")); + m_requestedAction = ModRequestedAction::Disable; + Mod::get()->setSavedValue("should-load-" + m_metadata.getID(), false); return Ok(); } Result<> Mod::Impl::uninstall() { - if (supportsDisabling()) { - GEODE_UNWRAP(this->disable()); - } - else { - for (auto& item : m_dependants) { - if (!item->canDisable()) - continue; - GEODE_UNWRAP(item->disable()); - } + if (m_requestedAction != ModRequestedAction::None) { + return Err("Mod already has a requested action"); } - try { - ghc::filesystem::remove(m_metadata.getPath()); - } - catch (std::exception& e) { + m_requestedAction = ModRequestedAction::Uninstall; + + std::error_code ec; + ghc::filesystem::remove(m_metadata.getPath(), ec); + if (ec) { return Err( - "Unable to delete mod's .geode file! " - "This might be due to insufficient permissions - " - "try running GD as administrator." + "Unable to delete mod's .geode file: " + ec.message() ); } @@ -465,7 +412,11 @@ Result<> Mod::Impl::uninstall() { } bool Mod::Impl::isUninstalled() const { - return m_self != Mod::get() && !ghc::filesystem::exists(m_metadata.getPath()); + return m_requestedAction == ModRequestedAction::Uninstall; +} + +ModRequestedAction Mod::Impl::getRequestedAction() const { + return m_requestedAction; } // Dependencies @@ -661,7 +612,7 @@ ModJson Mod::Impl::getRuntimeInfo() const { for (auto patch : m_patches) { obj["patches"].as_array().push_back(ModJson(patch->getRuntimeInfo())); } - obj["enabled"] = m_enabled; + // obj["enabled"] = m_enabled; obj["loaded"] = m_binaryLoaded; obj["temp-dir"] = this->getTempDir(); obj["save-dir"] = this->getSaveDir(); diff --git a/loader/src/loader/ModImpl.hpp b/loader/src/loader/ModImpl.hpp index a33ae12a..9048036d 100644 --- a/loader/src/loader/ModImpl.hpp +++ b/loader/src/loader/ModImpl.hpp @@ -62,6 +62,9 @@ namespace geode { */ bool m_resourcesLoaded = false; + + ModRequestedAction m_requestedAction = ModRequestedAction::None; + Impl(Mod* self, ModMetadata const& metadata); ~Impl(); @@ -122,6 +125,10 @@ namespace geode { Result<> disable(); Result<> uninstall(); bool isUninstalled() const; + + // 1.3.0 additions + ModRequestedAction getRequestedAction() const; + bool depends(std::string const& id) const; Result<> updateDependencies(); bool hasUnresolvedDependencies() const; diff --git a/loader/src/loader/Patch.cpp b/loader/src/loader/Patch.cpp index c7a71c1f..a056396d 100644 --- a/loader/src/loader/Patch.cpp +++ b/loader/src/loader/Patch.cpp @@ -4,13 +4,43 @@ using namespace geode::prelude; bool Patch::apply() { - return bool(tulip::hook::writeMemory(m_address, m_patch.data(), m_patch.size())); + bool res = bool(tulip::hook::writeMemory(m_address, m_patch.data(), m_patch.size())); + if (res) + m_applied = true; + return res; } bool Patch::restore() { - return bool(tulip::hook::writeMemory(m_address, m_original.data(), m_original.size())); + bool res = bool(tulip::hook::writeMemory(m_address, m_original.data(), m_original.size())); + if (res) + m_applied = false; + return res; } +Patch::Patch() : m_owner(nullptr), m_address(nullptr), m_applied(false), m_autoEnable(true) {} + +void Patch::setAutoEnable(bool autoEnable) { + m_autoEnable = autoEnable; +} + +bool Patch::getAutoEnable() const { + return m_autoEnable; +} + +uintptr_t Patch::getAddress() const { + return reinterpret_cast(m_address); +} + +bool Patch::isApplied() const { + return m_applied; +} + +Mod* Patch::getOwner() const { + return m_owner; +} + +Patch::~Patch() {} + template <> struct json::Serialize { static json::Value to_json(ByteVector const& bytes) { diff --git a/loader/src/platform/mac/crashlog.mm b/loader/src/platform/mac/crashlog.mm index dc5434dc..1c7704f8 100644 --- a/loader/src/platform/mac/crashlog.mm +++ b/loader/src/platform/mac/crashlog.mm @@ -251,7 +251,20 @@ static std::string getStacktrace() { stream >> std::hex >> address >> std::dec; if (!line.empty()) { - stacktrace << " - " << std::showbase << std::hex << address << std::dec; + // log::debug("address: {}", address); + auto image = imageFromAddress(reinterpret_cast(address)); + // log::debug("image: {}", image); + stacktrace << " - " << std::showbase << std::hex; + + if (image) { + auto baseAddress = image->imageLoadAddress; + auto imageName = getImageName(image); + stacktrace << imageName << " + " << (address - (uintptr_t)baseAddress); + } + else { + stacktrace << address; + } + stacktrace << std::dec; stacktrace << ": " << line << "\n"; } else { @@ -319,14 +332,15 @@ static void handlerThread() { s_cv.wait(lock, [] { return s_signal != 0; }); auto signalAddress = reinterpret_cast(s_context->uc_mcontext->__ss.__rip); - Mod* faultyMod = nullptr; - for (int i = 1; i < s_backtraceSize; ++i) { - auto mod = modFromAddress(s_backtrace[i]); - if (mod != nullptr) { - faultyMod = mod; - break; - } - } + // Mod* faultyMod = nullptr; + // for (int i = 1; i < s_backtraceSize; ++i) { + // auto mod = modFromAddress(s_backtrace[i]); + // if (mod != nullptr) { + // faultyMod = mod; + // break; + // } + // } + Mod* faultyMod = modFromAddress(signalAddress); auto text = crashlog::writeCrashlog(faultyMod, getInfo(signalAddress, faultyMod), getStacktrace(), getRegisters()); @@ -334,6 +348,8 @@ static void handlerThread() { s_signal = 0; s_cv.notify_all(); + + log::debug("Notified"); } static bool s_lastLaunchCrashed; diff --git a/loader/src/platform/mac/gdstdlib.cpp b/loader/src/platform/mac/gdstdlib.cpp index 0c508a74..ce6ff4fa 100644 --- a/loader/src/platform/mac/gdstdlib.cpp +++ b/loader/src/platform/mac/gdstdlib.cpp @@ -57,7 +57,7 @@ namespace gd { return std::string(*this) == std::string(other); } - // TODO: these need to stay for old mods linking against geode <1.2.0 + // TODO: these need to stay for old mods linking against geode <1.2.0, remove in 2.0.0 template class map; template class map; template class map; diff --git a/loader/src/ui/internal/GeodeUI.cpp b/loader/src/ui/internal/GeodeUI.cpp index b7e1d3e9..a575a737 100644 --- a/loader/src/ui/internal/GeodeUI.cpp +++ b/loader/src/ui/internal/GeodeUI.cpp @@ -83,7 +83,7 @@ CCNode* geode::createModLogo(Mod* mod, CCSize const& size) { } CCNode* geode::createIndexItemLogo(IndexItemHandle item, CCSize const& size) { - auto logoPath = ghc::filesystem::absolute(item->getPath() / "logo.png"); + auto logoPath = ghc::filesystem::absolute(item->getRootPath() / "logo.png"); 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/ModInfoPopup.cpp b/loader/src/ui/internal/info/ModInfoPopup.cpp index 4173072d..1ec147d1 100644 --- a/loader/src/ui/internal/info/ModInfoPopup.cpp +++ b/loader/src/ui/internal/info/ModInfoPopup.cpp @@ -277,6 +277,95 @@ void ModInfoPopup::setInstallStatus(std::optional const& progres } } +void ModInfoPopup::popupInstallItem(IndexItemHandle item) { + auto deps = item->getMetadata().getDependencies(); + enum class DepState { + None, + HasOnlyRequired, + HasOptional + } depState = DepState::None; + for (auto const& dep : deps) { + // resolved means it's already installed, so + // no need to ask the user whether they want to install it + if (Loader::get()->isModLoaded(dep.id)) + continue; + if (dep.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 {}?", + 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(list); + })->show(); + } + }, true, true); +} + +void ModInfoPopup::preInstall() { + if (m_latestVersionLabel) { + m_latestVersionLabel->setVisible(false); + } + this->setInstallStatus(UpdateProgress(0, "Starting install")); + + m_installBtn->setTarget( + this, menu_selector(ModInfoPopup::onCancelInstall) + ); + m_installBtnSpr->setString("Cancel"); + m_installBtnSpr->setBG("GJ_button_06.png", false); +} + +void ModInfoPopup::onCancelInstall(CCObject*) { + Index::get()->cancelInstall(m_item); +} + // LocalModInfoPopup LocalModInfoPopup::LocalModInfoPopup() : m_installListener( @@ -462,8 +551,8 @@ void LocalModInfoPopup::onUpdateProgress(ModInstallEvent* event) { FLAlertLayer::create( "Update complete", "Mod successfully updated! :) " - "(You may need to restart the game " - "for the mod to take full effect)", + "(You have to restart the game " + "for the mod to take effect)", "OK" )->show(); @@ -493,61 +582,7 @@ void LocalModInfoPopup::onUpdateProgress(ModInstallEvent* event) { } void LocalModInfoPopup::onUpdate(CCObject*) { - auto list = Index::get()->getInstallList(m_item); - if (!list) { - return FLAlertLayer::create( - "Unable to Update", - list.unwrapErr(), - "OK" - )->show(); - } - auto layer = FLAlertLayer::create( - this, - "Confirm Update", - fmt::format( - "The following mods will be updated:\n {}", - // le nest - ranges::join( - ranges::map>( - list.unwrap().list, - [](IndexItemHandle handle) { - return fmt::format( - " - {} ({})", - handle->getMetadata().getName(), - handle->getMetadata().getID() - ); - } - ), - "\n " - ) - ), - "Cancel", "OK" - ); - layer->setTag(TAG_CONFIRM_UPDATE); - layer->show(); -} - -void LocalModInfoPopup::onCancel(CCObject*) { - Index::get()->cancelInstall(m_item); -} - -void LocalModInfoPopup::doUpdate() { - if (m_latestVersionLabel) { - m_latestVersionLabel->setVisible(false); - } - - if (m_minorVersionLabel) { - m_minorVersionLabel->setVisible(false); - } - this->setInstallStatus(UpdateProgress(0, "Starting update")); - - m_installBtn->setTarget( - this, menu_selector(LocalModInfoPopup::onCancel) - ); - m_installBtnSpr->setString("Cancel"); - m_installBtnSpr->setBG("GJ_button_06.png", false); - - Index::get()->install(m_item); + this->popupInstallItem(m_item); } void LocalModInfoPopup::onUninstall(CCObject*) { @@ -631,12 +666,6 @@ void LocalModInfoPopup::FLAlert_Clicked(FLAlertLayer* layer, bool btn2) { } this->onClose(nullptr); } break; - - case TAG_CONFIRM_UPDATE: { - if (btn2) { - this->doUpdate(); - } - } break; } } @@ -649,8 +678,8 @@ void LocalModInfoPopup::doUninstall() { this, "Uninstall complete", "Mod was successfully uninstalled! :) " - "(You may need to restart the game " - "for the mod to take full effect). " + "(You have to restart the game " + "for the mod to take effect). " "Would you also like to delete the mod's " "save data?", "Keep", @@ -687,7 +716,8 @@ bool IndexItemInfoPopup::init(IndexItemHandle item, ModListLayer* list) { if (!ModInfoPopup::init(item->getMetadata(), list)) return false; - if (item->isInstalled()) return true; + // bruh why is this here if we are allowing for browsing already installed mods + // if (item->isInstalled()) return true; m_installBtnSpr = IconButtonSprite::create( "GE_button_01.png"_spr, @@ -719,8 +749,8 @@ void IndexItemInfoPopup::onInstallProgress(ModInstallEvent* event) { FLAlertLayer::create( "Install complete", "Mod successfully installed! :) " - "(You may need to restart the game " - "for the mod to take full effect)", + "(You have to restart the game " + "for the mod to take effect)", "OK" )->show(); @@ -750,92 +780,7 @@ void IndexItemInfoPopup::onInstallProgress(ModInstallEvent* event) { } void IndexItemInfoPopup::onInstall(CCObject*) { - 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(list); - })->show(); - } - }, true, true); -} - -void IndexItemInfoPopup::preInstall() { - 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); -} - -void IndexItemInfoPopup::onCancel(CCObject*) { - Index::get()->cancelInstall(m_item); + this->popupInstallItem(m_item); } CCNode* IndexItemInfoPopup::createLogo(CCSize const& size) { diff --git a/loader/src/ui/internal/info/ModInfoPopup.hpp b/loader/src/ui/internal/info/ModInfoPopup.hpp index 062230a0..7870dc9c 100644 --- a/loader/src/ui/internal/info/ModInfoPopup.hpp +++ b/loader/src/ui/internal/info/ModInfoPopup.hpp @@ -38,6 +38,7 @@ protected: MDTextArea* m_detailsArea; MDTextArea* m_changelogArea = nullptr; Scrollbar* m_scrollbar; + IndexItemHandle m_item; void onChangelog(CCObject*); void onRepository(CCObject*); @@ -51,13 +52,16 @@ protected: void setInstallStatus(std::optional const& progress); + void popupInstallItem(IndexItemHandle item); + void preInstall(); + void onCancelInstall(CCObject*); + virtual CCNode* createLogo(CCSize const& size) = 0; virtual ModMetadata getMetadata() const = 0; }; class LocalModInfoPopup : public ModInfoPopup, public FLAlertLayerProtocol { protected: - IndexItemHandle m_item; EventListener m_installListener; Mod* m_mod; @@ -74,8 +78,6 @@ protected: void onUpdateProgress(ModInstallEvent* event); void onUpdate(CCObject*); - void onCancel(CCObject*); - void doUpdate(); void FLAlert_Clicked(FLAlertLayer*, bool) override; @@ -91,16 +93,12 @@ public: class IndexItemInfoPopup : public ModInfoPopup { protected: - IndexItemHandle m_item; EventListener m_installListener; bool init(IndexItemHandle item, ModListLayer* list); void onInstallProgress(ModInstallEvent* event); void onInstall(CCObject*); - void onCancel(CCObject*); - - void 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 0ba4be3e..07daca20 100644 --- a/loader/src/ui/internal/list/InstallListCell.cpp +++ b/loader/src/ui/internal/list/InstallListCell.cpp @@ -8,7 +8,6 @@ #include #include #include -#include "../info/TagNode.hpp" #include "../info/DevProfilePopup.hpp" // InstallListCell @@ -27,8 +26,11 @@ void InstallListCell::setupInfo( std::variant version, bool inactive ) { + m_inactive = inactive; m_menu = CCMenu::create(); - m_menu->setPosition(m_width - 10.f, m_height / 2); + m_menu->setPosition(0, 0); + m_menu->setAnchorPoint({ .0f, .0f }); + m_menu->setContentSize({m_width, m_height}); this->addChild(m_menu); auto logoSize = this->getLogoSize(); @@ -41,15 +43,15 @@ void InstallListCell::setupInfo( } 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); + m_titleLabel = CCLabelBMFont::create(name.c_str(), "bigFont.fnt"); + m_titleLabel->setAnchorPoint({ .0f, .5f }); + m_titleLabel->setPositionX(m_height / 2 + logoSize / 2 + 13.f); + m_titleLabel->setPositionY(m_height / 2); + m_titleLabel->limitLabelWidth(m_width / 2 - 70.f, .4f, .1f); if (inactive) { - titleLabel->setColor({ 163, 163, 163 }); + m_titleLabel->setColor({ 163, 163, 163 }); } - this->addChild(titleLabel); + this->addChild(m_titleLabel); m_developerBtn = nullptr; if (developer) { @@ -64,43 +66,55 @@ void InstallListCell::setupInfo( 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_titleLabel->getPositionX() + m_titleLabel->getScaledContentSize().width + 3.f + + creatorLabel->getScaledContentSize().width / 2, + m_height / 2 ); m_menu->addChild(m_developerBtn); } - auto versionLabel = CCLabelBMFont::create( + this->setupVersion(version); +} + +void InstallListCell::setupVersion(std::variant version) { + if (m_versionLabel) { + m_versionLabel->removeFromParent(); + m_versionLabel = nullptr; + } + if (m_tagLabel) { + m_tagLabel->removeFromParent(); + m_tagLabel = nullptr; + } + + m_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_versionLabel->setAnchorPoint({ .0f, .5f }); + m_versionLabel->setScale(.2f); + m_versionLabel->setPosition( + m_titleLabel->getPositionX() + m_titleLabel->getScaledContentSize().width + 3.f + (m_developerBtn ? m_developerBtn->getScaledContentSize().width + 3.f : 0.f), - titleLabel->getPositionY() - 1.f + m_titleLabel->getPositionY() - 1.f ); - versionLabel->setColor({ 0, 255, 0 }); - if (inactive) { - versionLabel->setColor({ 0, 163, 0 }); + m_versionLabel->setColor({ 0, 255, 0 }); + if (m_inactive) { + m_versionLabel->setColor({ 0, 163, 0 }); } - this->addChild(versionLabel); + this->addChild(m_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() + m_tagLabel = TagNode::create(tag->toString()); + m_tagLabel->setAnchorPoint({.0f, .5f}); + m_tagLabel->setScale(.2f); + m_tagLabel->setPosition( + m_versionLabel->getPositionX() + m_versionLabel->getScaledContentSize().width + 3.f, + m_versionLabel->getPositionY() ); - this->addChild(tagLabel); + this->addChild(m_tagLabel); } } @@ -134,7 +148,7 @@ bool ModInstallListCell::init(Mod* mod, InstallListPopup* list, CCSize const& si this->setupInfo(mod->getMetadata(), true); auto message = CCLabelBMFont::create("Installed", "bigFont.fnt"); message->setAnchorPoint({ 1.f, .5f }); - message->setPositionX(m_menu->getPositionX()); + message->setPositionX(m_width - 10.0f); message->setPositionY(16.f); message->setScale(0.4f); message->setColor({ 163, 163, 163 }); @@ -174,23 +188,38 @@ bool IndexItemInstallListCell::init( 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; - } + + // TODO: show installed label properly + // if (item->isInstalled()) { + // auto message = CCLabelBMFont::create("Installed", "bigFont.fnt"); + // message->setAnchorPoint({ 1.f, .5f }); + // message->setPositionX(m_width - 10.0f); + // 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); + m_toggle->setAnchorPoint({1.f, .5f}); + m_toggle->setPosition(m_width - 5, m_height / 2); + + // recycling sprites in my Geode?? noo never + auto versionSelectSpr = EditorButtonSprite::createWithSpriteFrameName( + "filters.png"_spr, 1.0f, EditorBaseColor::Gray + ); + versionSelectSpr->setScale(.7f); + + auto versionSelectBtn = + CCMenuItemSpriteExtra::create(versionSelectSpr, this, menu_selector(IndexItemInstallListCell::onSelectVersion)); + versionSelectBtn->setAnchorPoint({1.f, .5f}); + versionSelectBtn->setPosition({m_toggle->getPositionX() - m_toggle->getContentSize().width - 5, m_height / 2}); + m_menu->addChild(versionSelectBtn); switch (importance) { case ModMetadata::Dependency::Importance::Required: @@ -207,13 +236,18 @@ bool IndexItemInstallListCell::init( break; } + if (item->isInstalled()) { + m_toggle->setClickable(false); + m_toggle->toggle(true); + } + 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->setPositionX(m_width - 5.f); message->setPositionY(16.f); message->setScale(0.4f); message->setColor({ 240, 31, 31 }); @@ -269,6 +303,15 @@ IndexItemHandle IndexItemInstallListCell::getItem() { return m_item; } +void IndexItemInstallListCell::setVersionFromItem(IndexItemHandle item) { + this->setupVersion(item->getMetadata().getVersion()); + m_item = item; +} + +void IndexItemInstallListCell::onSelectVersion(CCObject*) { + SelectVersionPopup::create(m_item->getMetadata().getID(), this)->show(); +} + // UnknownInstallListCell bool UnknownInstallListCell::init( @@ -317,3 +360,50 @@ std::string UnknownInstallListCell::getID() const { std::string UnknownInstallListCell::getDeveloper() const { return ""; } + +// SelectVersionCell + +bool SelectVersionCell::init(IndexItemHandle item, SelectVersionPopup* versionPopup, CCSize const& size) { + if (!InstallListCell::init(nullptr, size)) + return false; + m_item = item; + m_versionPopup = versionPopup; + this->setupInfo(item->getMetadata(), item->isInstalled()); + + auto selectSpr = ButtonSprite::create( + "Select", 0, 0, "bigFont.fnt", "GJ_button_01.png", 0, .6f + ); + selectSpr->setScale(.6f); + + auto selectBtn = CCMenuItemSpriteExtra::create( + selectSpr, this, menu_selector(SelectVersionCell::onSelected) + ); + selectBtn->setAnchorPoint({1.f, .5f}); + selectBtn->setPosition({m_width - 5, m_height / 2}); + m_menu->addChild(selectBtn); + + return true; +} + +void SelectVersionCell::onSelected(CCObject*) { + m_versionPopup->selectItem(m_item); +} + +SelectVersionCell* SelectVersionCell::create(IndexItemHandle item, SelectVersionPopup* versionPopup, CCSize const& size) { + auto ret = new SelectVersionCell(); + if (ret->init(item, versionPopup, size)) { + return ret; + } + CC_SAFE_DELETE(ret); + return nullptr; +} + +CCNode* SelectVersionCell::createLogo(CCSize const& size) { + return geode::createIndexItemLogo(m_item, size); +} +std::string SelectVersionCell::getID() const { + return m_item->getMetadata().getID(); +} +std::string SelectVersionCell::getDeveloper() const { + return m_item->getMetadata().getDeveloper(); +} \ No newline at end of file diff --git a/loader/src/ui/internal/list/InstallListCell.hpp b/loader/src/ui/internal/list/InstallListCell.hpp index 582ca24d..f2fd677c 100644 --- a/loader/src/ui/internal/list/InstallListCell.hpp +++ b/loader/src/ui/internal/list/InstallListCell.hpp @@ -6,6 +6,8 @@ #include #include +#include "../info/TagNode.hpp" + using namespace geode::prelude; class InstallListPopup; @@ -17,10 +19,14 @@ class InstallListCell : public CCLayer { protected: float m_width; float m_height; - InstallListPopup* m_layer; - CCMenu* m_menu; - CCMenuItemSpriteExtra* m_developerBtn; + InstallListPopup* m_layer = nullptr; + CCMenu* m_menu = nullptr; + CCMenuItemSpriteExtra* m_developerBtn = nullptr; + CCLabelBMFont* m_titleLabel = nullptr; + CCLabelBMFont* m_versionLabel = nullptr; + TagNode* m_tagLabel = nullptr; CCMenuItemToggler* m_toggle = nullptr; + bool m_inactive = false; void setupInfo( std::string name, @@ -29,6 +35,8 @@ protected: bool inactive ); + void setupVersion(std::variant version); + bool init(InstallListPopup* list, CCSize const& size); void setupInfo(ModMetadata const& metadata, bool inactive); void draw() override; @@ -90,6 +98,9 @@ public: [[nodiscard]] std::string getDeveloper() const override; IndexItemHandle getItem(); + void setVersionFromItem(IndexItemHandle item); + + void onSelectVersion(CCObject*); }; /** @@ -112,3 +123,23 @@ public: [[nodiscard]] std::string getID() const override; [[nodiscard]] std::string getDeveloper() const override; }; + +class SelectVersionPopup; +/** + * Select version list item + */ +class SelectVersionCell : public InstallListCell { +protected: + IndexItemHandle m_item; + SelectVersionPopup* m_versionPopup; + + bool init(IndexItemHandle item, SelectVersionPopup* versionPopup, CCSize const& size); + + void onSelected(CCObject*); +public: + static SelectVersionCell* create(IndexItemHandle item, SelectVersionPopup* versionPopup, CCSize const& size); + + CCNode* createLogo(CCSize const& size) override; + [[nodiscard]] std::string getID() const override; + [[nodiscard]] std::string getDeveloper() const override; +}; \ No newline at end of file diff --git a/loader/src/ui/internal/list/InstallListPopup.cpp b/loader/src/ui/internal/list/InstallListPopup.cpp index fa639355..e561ca9b 100644 --- a/loader/src/ui/internal/list/InstallListPopup.cpp +++ b/loader/src/ui/internal/list/InstallListPopup.cpp @@ -112,7 +112,8 @@ CCArray* InstallListPopup::createCells(std::unordered_mapisUninstalled()) { + // TODO: we should be able to select a different version even if its installed + if (/*item.mod && !item.mod->isUninstalled()*/item.mod->getMetadata().getID() == "geode.loader") { bottom.push_back(ModInstallListCell::create(item.mod, this, this->getCellSize())); for (auto const& dep : item.mod->getMetadata().getDependencies()) { queue.push(dep); @@ -204,7 +205,7 @@ void InstallListPopup::onInstall(cocos2d::CCObject* obj) { 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()) + if (!itemCell || !itemCell->isIncluded() || itemCell->getItem()->isInstalled()) continue; IndexItemHandle item = itemCell->getItem(); list.list.push_back(item); @@ -227,3 +228,70 @@ InstallListPopup* InstallListPopup::create( ret->autorelease(); return ret; } + +// SelectVersionPopup + +bool SelectVersionPopup::setup(std::string const& modID, IndexItemInstallListCell* installCell) { + m_modID = modID; + m_installCell = installCell; + + this->setTitle("Select Version"); + + this->createList(); + + return true; +} + +void SelectVersionPopup::createList() { + auto winSize = CCDirector::sharedDirector()->getWinSize(); + + if (m_listParent) { + m_listParent->removeFromParent(); + } + + m_listParent = CCNode::create(); + m_mainLayer->addChild(m_listParent); + + auto items = this->createCells(); + 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); + + addListBorders(m_listParent, winSize / 2, m_list->getScaledContentSize()); +} + +CCArray* SelectVersionPopup::createCells() { + auto cells = CCArray::create(); + for (auto& item : ranges::reverse(Index::get()->getItemsByModID(m_modID))) { + cells->addObject(SelectVersionCell::create(item, this, this->getCellSize())); + } + return cells; +} + +CCSize SelectVersionPopup::getCellSize() const { + return { getListSize().width, 30.f }; +} +CCSize SelectVersionPopup::getListSize() const { + return { 340.f, 170.f }; +} + +void SelectVersionPopup::selectItem(IndexItemHandle item) { + this->onBtn2(nullptr); + + m_installCell->setVersionFromItem(item); +} + +SelectVersionPopup* SelectVersionPopup::create(std::string const& modID, IndexItemInstallListCell* installCell) { + auto ret = new SelectVersionPopup(); + if (!ret->init(380.f, 250.f, modID, installCell)) { + CC_SAFE_DELETE(ret); + return nullptr; + } + ret->autorelease(); + return ret; +} \ No newline at end of file diff --git a/loader/src/ui/internal/list/InstallListPopup.hpp b/loader/src/ui/internal/list/InstallListPopup.hpp index 7802bccb..fbab3f5c 100644 --- a/loader/src/ui/internal/list/InstallListPopup.hpp +++ b/loader/src/ui/internal/list/InstallListPopup.hpp @@ -28,3 +28,24 @@ public: static InstallListPopup* create(IndexItemHandle item, MiniFunction onInstall); }; + +class SelectVersionPopup : public Popup { +protected: + std::string m_modID; + CCNode* m_listParent; + ListView* m_list; + IndexItemInstallListCell* m_installCell; + + bool setup(std::string const& modID, IndexItemInstallListCell* installCell) override; + + void createList(); + CCArray* createCells(); + CCSize getCellSize() const; + CCSize getListSize() const; + + +public: + void selectItem(IndexItemHandle item); + + static SelectVersionPopup* create(std::string const& modID, IndexItemInstallListCell* installCell); +}; diff --git a/loader/src/ui/internal/list/ModListCell.cpp b/loader/src/ui/internal/list/ModListCell.cpp index a1951860..3c9c199d 100644 --- a/loader/src/ui/internal/list/ModListCell.cpp +++ b/loader/src/ui/internal/list/ModListCell.cpp @@ -227,11 +227,7 @@ void ModCell::onEnable(CCObject* sender) { else { tryOrAlert(m_mod->disable(), "Error disabling mod"); } - Loader::get()->queueInMainThread([this]() { - if (m_layer) { - m_layer->updateAllStates(); - } - }); + m_layer->reloadList(); } void ModCell::onUnresolvedInfo(CCObject*) { @@ -278,9 +274,9 @@ bool ModCell::init( return false; m_mod = mod; - this->setupInfo(mod->getMetadata(), false, display, m_mod->isUninstalled()); + this->setupInfo(mod->getMetadata(), false, display, mod->getRequestedAction() != ModRequestedAction::None); - if (mod->isUninstalled()) { + if (mod->getRequestedAction() != ModRequestedAction::None) { auto restartSpr = ButtonSprite::create("Restart", "bigFont.fnt", "GJ_button_03.png", .8f); restartSpr->setScale(.65f); @@ -317,14 +313,16 @@ bool ModCell::init( } } } + + if (m_mod->wasSuccessfullyLoaded() && m_mod->getMetadata().getID() != "geode.loader") { + m_enableToggle = + CCMenuItemToggler::createWithStandardSprites(this, menu_selector(ModCell::onEnable), .7f); + m_enableToggle->setPosition(-45.f, 0.f); + m_menu->addChild(m_enableToggle); + } } - 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); - m_menu->addChild(m_enableToggle); - } + auto exMark = CCSprite::createWithSpriteFrameName("exMark_001.png"); exMark->setScale(.5f); diff --git a/loader/src/ui/internal/list/ModListLayer.cpp b/loader/src/ui/internal/list/ModListLayer.cpp index 3ff3b930..df4fbecc 100644 --- a/loader/src/ui/internal/list/ModListLayer.cpp +++ b/loader/src/ui/internal/list/ModListLayer.cpp @@ -198,7 +198,7 @@ CCArray* ModListLayer::createModCells(ModListType type, ModListQuery const& quer std::multimap sorted; auto index = Index::get(); - for (auto const& item : index->getItems()) { + for (auto const& item : index->getLatestItems()) { if (auto match = queryMatch(query, item)) { sorted.insert({ match.value(), item }); } @@ -444,13 +444,18 @@ void ModListLayer::createSearchControl() { this->addChild(m_searchInput); } -void ModListLayer::reloadList(std::optional const& query) { +void ModListLayer::reloadList(bool keepScroll, std::optional const& query) { auto winSize = CCDirector::sharedDirector()->getWinSize(); if (query) { m_query = query.value(); } + float scroll = 0.0f; + if (keepScroll && m_list) { + scroll = m_list->m_listView->m_tableView->m_contentLayer->getPositionY(); + } + // set search query m_query.keywords = m_searchInput && @@ -489,6 +494,12 @@ void ModListLayer::reloadList(std::optional const& query) { m_listLabel->setVisible(false); } + if (keepScroll) { + list->m_tableView->m_contentLayer->setPosition( + { 0.0f, scroll } + ); + } + // update index if needed if (g_tab == ModListType::Download && !Index::get()->hasTriedToUpdate()) { m_listLabel->setVisible(true); @@ -658,7 +669,7 @@ void ModListLayer::onTab(CCObject* pSender) { if (pSender) { g_tab = static_cast(pSender->getTag()); } - this->reloadList(); + this->reloadList(false); auto toggleTab = [this](CCMenuItemToggler* member) -> void { auto isSelected = member->getTag() == static_cast(g_tab); @@ -670,7 +681,7 @@ void ModListLayer::onTab(CCObject* pSender) { targetMenu->addChild(member); member->release(); } - if (isSelected) + if (isSelected && m_tabsGradientStencil) m_tabsGradientStencil->setPosition(member->m_onButton->convertToWorldSpace({0.f, -1.f})); }; @@ -686,7 +697,7 @@ void ModListLayer::keyDown(enumKeyCodes key) { } void ModListLayer::textChanged(CCTextInputNode* input) { - this->reloadList(); + this->reloadList(false); } // Constructors etc. diff --git a/loader/src/ui/internal/list/ModListLayer.hpp b/loader/src/ui/internal/list/ModListLayer.hpp index 648e8309..0afa2839 100644 --- a/loader/src/ui/internal/list/ModListLayer.hpp +++ b/loader/src/ui/internal/list/ModListLayer.hpp @@ -94,5 +94,5 @@ public: ModListDisplay getDisplay() const; ModListQuery& getQuery(); - void reloadList(std::optional const& query = std::nullopt); + void reloadList(bool keepScroll = true, std::optional const& query = std::nullopt); }; diff --git a/loader/src/ui/internal/list/SearchFilterPopup.cpp b/loader/src/ui/internal/list/SearchFilterPopup.cpp index 61353a7e..e64ed651 100644 --- a/loader/src/ui/internal/list/SearchFilterPopup.cpp +++ b/loader/src/ui/internal/list/SearchFilterPopup.cpp @@ -175,7 +175,7 @@ void SearchFilterPopup::onPlatformToggle(CCObject* sender) { void SearchFilterPopup::onClose(CCObject* sender) { Popup::onClose(sender); - m_modLayer->reloadList(); + m_modLayer->reloadList(false); } SearchFilterPopup* SearchFilterPopup::create(ModListLayer* layer, ModListType type) { diff --git a/loader/src/utils/web.cpp b/loader/src/utils/web.cpp index 72ca2eea..9bc7cd99 100644 --- a/loader/src/utils/web.cpp +++ b/loader/src/utils/web.cpp @@ -48,7 +48,6 @@ Result<> web::fetchFile( curl_easy_setopt(curl, CURLOPT_WRITEDATA, &file); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, utils::fetch::writeBinaryData); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1); - curl_easy_setopt(curl, CURLOPT_USERAGENT, "github_api/1.0"); if (prog) { curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0); curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, utils::fetch::progress); @@ -81,7 +80,6 @@ Result web::fetchBytes(std::string const& url) { curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ret); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, utils::fetch::writeBytes); - curl_easy_setopt(curl, CURLOPT_USERAGENT, "github_api/1.0"); auto res = curl_easy_perform(curl); if (res != CURLE_OK) { curl_easy_cleanup(curl); @@ -120,7 +118,6 @@ Result web::fetch(std::string const& url) { curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ret); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, utils::fetch::writeString); - curl_easy_setopt(curl, CURLOPT_USERAGENT, "github_api/1.0"); auto res = curl_easy_perform(curl); if (res != CURLE_OK) { curl_easy_cleanup(curl); @@ -162,9 +159,10 @@ private: SentAsyncWebRequest* m_self; mutable std::mutex m_mutex; - std::variant m_target = - std::monostate(); + AsyncWebRequestData m_extra; + std::variant m_target; std::vector m_httpHeaders; + template friend class AsyncWebResult; @@ -187,7 +185,7 @@ static std::unordered_map RUNNING_REQUES static std::mutex RUNNING_REQUESTS_MUTEX; SentAsyncWebRequest::Impl::Impl(SentAsyncWebRequest* self, AsyncWebRequest const& req, std::string const& id) : - m_id(id), m_url(req.m_url), m_target(req.m_target), m_httpHeaders(req.m_httpHeaders) { + m_id(id), m_url(req.m_url), m_target(req.m_target), m_extra(req.extra()), m_httpHeaders(req.m_httpHeaders) { #define AWAIT_RESUME() \ {\ @@ -242,8 +240,30 @@ SentAsyncWebRequest::Impl::Impl(SentAsyncWebRequest* self, AsyncWebRequest const // No need to verify SSL, we trust our domains :-) curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0); - // Github User Agent - curl_easy_setopt(curl, CURLOPT_USERAGENT, "github_api/1.0"); + // User Agent + curl_easy_setopt(curl, CURLOPT_USERAGENT, m_extra.m_userAgent.c_str()); + + // Headers + curl_slist* headers = nullptr; + for (auto& header : m_httpHeaders) { + headers = curl_slist_append(headers, header.c_str()); + } + + // Post request + if (m_extra.m_isPostRequest || m_extra.m_customRequest.size()) { + if (m_extra.m_isPostRequest) { + curl_easy_setopt(curl, CURLOPT_POST, 1L); + } + else { + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, m_extra.m_customRequest.c_str()); + } + if (m_extra.m_isJsonRequest) { + headers = curl_slist_append(headers, "Content-Type: application/json"); + } + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, m_extra.m_postFields.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, m_extra.m_postFields.size()); + } + // Track progress curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0); // Follow redirects @@ -251,10 +271,7 @@ SentAsyncWebRequest::Impl::Impl(SentAsyncWebRequest* self, AsyncWebRequest const // Fail if response code is 4XX or 5XX curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L); - curl_slist* headers = nullptr; - for (auto& header : m_httpHeaders) { - headers = curl_slist_append(headers, header.c_str()); - } + // Headers end curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); struct ProgressData { @@ -410,11 +427,50 @@ void SentAsyncWebRequest::error(std::string const& error, int code) { return m_impl->error(error, code); } +AsyncWebRequestData& AsyncWebRequest::extra() { + if (!m_extra) { + m_extra = new AsyncWebRequestData(); + } + return *m_extra; +} + +AsyncWebRequestData const& AsyncWebRequest::extra() const { + if (!m_extra) { + m_extra = new AsyncWebRequestData(); + } + return *m_extra; +} + AsyncWebRequest& AsyncWebRequest::join(std::string const& requestID) { m_joinID = requestID; return *this; } +AsyncWebRequest& AsyncWebRequest::userAgent(std::string const& userAgent) { + this->extra().m_userAgent = userAgent; + return *this; +} + +AsyncWebRequest& AsyncWebRequest::postRequest() { + this->extra().m_isPostRequest = true; + return *this; +} + +AsyncWebRequest& AsyncWebRequest::customRequest(std::string const& request) { + this->extra().m_customRequest = request; + return *this; +} + +AsyncWebRequest& AsyncWebRequest::postFields(std::string const& fields) { + this->extra().m_postFields = fields; + return *this; +} + +AsyncWebRequest& AsyncWebRequest::postFields(json::Value const& fields) { + this->extra().m_isJsonRequest = true; + return this->postFields(fields.dump()); +} + AsyncWebRequest& AsyncWebRequest::header(std::string const& header) { m_httpHeaders.push_back(header); return *this; @@ -448,8 +504,8 @@ AsyncWebRequest& AsyncWebRequest::cancelled(AsyncCancelled cancelledFunc) { } SentAsyncWebRequestHandle AsyncWebRequest::send() { - if (m_sent) return nullptr; - m_sent = true; + if (this->extra().m_sent) return nullptr; + this->extra().m_sent = true; std::lock_guard __(RUNNING_REQUESTS_MUTEX); @@ -486,6 +542,7 @@ SentAsyncWebRequestHandle AsyncWebRequest::send() { AsyncWebRequest::~AsyncWebRequest() { this->send(); + delete m_extra; } AsyncWebResult AsyncWebResponse::into(std::ostream& stream) {