diff --git a/CHANGELOG.md b/CHANGELOG.md index 4febf3d8..4c29d15d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,38 @@ # Geode Changelog -## v3.5.0 - * Major rework of the entire settings system with lots of new features; see the [docs page](todo) for more +## v3.6.0 + * Major rework of the entire settings system with lots of new features; see the [docs page](https://docs.geode-sdk.org/mods/settings) for more * Rework JSON validation; now uses the `JsonExpectedValue` class with the `checkJson` helper (89d1a51) * Add `Task::cancelled` for creating immediately cancelled Tasks (1a82d12) * Add function type utilities in `utils/function.hpp` (659c168) * Add `typeinfo_pointer_cast` for casting `std::shared_ptr`s (28cc6fd) * Add `GEODE_PLATFORM_SHORT_IDENTIFIER_NOARCH` (1032d9a) + * Add `PlatformID::getCovered` (d5718be) * Rename `toByteArray` to `toBytes` (6eb0797) * Improve `AxisLayout::getSizeHint` (85e7b5e) * Fix issues with file dialogs on Windows (62b6241, 971e3fb) + * Mod incompatibilities may now be platform-specific (9f1c70a) + +## v3.5.0 + * Move CCLighting to cocos headers (#1036) + * Add new `gd::string` constructor (bae22b4) + * Use `getChildren` instead of member in `getChildByID` (fe730ed) + * Fix sprite order in `CCMenuItemExt::createToggler` (d729a12, 59a0ade) + * Add restart button to windows's crashlog window (#1025) + * Update FMOD headers (63b82f9) + * Change SwelvyBG sprites to be 2048x512 (#1029) + * Fix missing `GEODE_DLL` (e4054d4) + * Add code of conduct (80693c1, ab8ace0, ca3a2a3) + * Add ID system to Geode's web requests (#1040, 1f2aa2c, 1b5ae86) + * Add `Notification::cancel` (cd5a66c) + * Update matjson (e5dd2c9) + * Update TulipHook (a31c68f) + * Fix a bug where only 1 word wrap variant can exist (#1058) + * Fix ScrollLayer when anchor point is not ignored (d95a43b) + * Move macOS builds to using apple clang, fixing issues on older macOS versions (#1030) + * Allow dashes when searching for developers (#1023) + * Split update checks into multiple batches (#1066) + * Show invalid mods on mod search (#1065) ## v3.4.0 * Add an API for modifying the Geode UI via events; see [the corresponding docs page](https://docs.geode-sdk.org/tutorials/modify-geode) (2a3c35f) diff --git a/VERSION b/VERSION index e5b82034..40c341bd 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.5.0 \ No newline at end of file +3.6.0 diff --git a/loader/include/Geode/loader/ModMetadata.hpp b/loader/include/Geode/loader/ModMetadata.hpp index 9628d5ee..588e9f96 100644 --- a/loader/include/Geode/loader/ModMetadata.hpp +++ b/loader/include/Geode/loader/ModMetadata.hpp @@ -243,6 +243,8 @@ namespace geode { void setTags(std::unordered_set const& value); void setNeedsEarlyLoad(bool const& value); void setIsAPI(bool const& value); + void setGameVersion(std::string const& value); + void setGeodeVersion(VersionInfo const& value); ModMetadataLinks& getLinksMut(); #endif diff --git a/loader/src/loader/ModMetadataImpl.cpp b/loader/src/loader/ModMetadataImpl.cpp index 51181915..460398ea 100644 --- a/loader/src/loader/ModMetadataImpl.cpp +++ b/loader/src/loader/ModMetadataImpl.cpp @@ -651,6 +651,12 @@ void ModMetadata::setNeedsEarlyLoad(bool const& value) { void ModMetadata::setIsAPI(bool const& value) { m_impl->m_isAPI = value; } +void ModMetadata::setGameVersion(std::string const& value) { + m_impl->m_gdVersion = value; +} +void ModMetadata::setGeodeVersion(VersionInfo const& value) { + m_impl->m_geodeVersion = value; +} ModMetadataLinks& ModMetadata::getLinksMut() { return m_impl->m_links; } diff --git a/loader/src/server/Server.cpp b/loader/src/server/Server.cpp index de0fca5d..cfdb195d 100644 --- a/loader/src/server/Server.cpp +++ b/loader/src/server/Server.cpp @@ -264,22 +264,17 @@ Result ServerModVersion::parse(matjson::Value const& raw) { auto res = ServerModVersion(); - // Verify target Geode version - auto version = root.needs("geode").template get(); - if (!semverCompare(Loader::get()->getVersion(), version)) { - return Err( - "Mod targets version {} but Geode is version {}", - version, Loader::get()->getVersion() - ); - } + res.metadata.setGeodeVersion(root.needs("geode").template get()); // Verify target GD version - auto gd = root.needs("gd").obj().needs(GEODE_PLATFORM_SHORT_IDENTIFIER).template get(); - if (gd != GEODE_GD_VERSION_STR && gd != "*") { - return Err( - "Mod targets GD version {} but current is version {}", - gd, GEODE_GD_VERSION_STR - ); + auto gd_obj = root.needs("gd").obj(); + std::string gd = "0.000"; + if (gd_obj.has(GEODE_PLATFORM_SHORT_IDENTIFIER)) { + gd = gd_obj.has(GEODE_PLATFORM_SHORT_IDENTIFIER).template get(); + } + + if (gd != "*") { + res.metadata.setGameVersion(gd); } // Get server info @@ -571,14 +566,15 @@ ServerRequest server::getMods(ModsQuery const& query, bool useCa auto req = web::WebRequest(); req.userAgent(getServerUserAgent()); - // Always target current GD version and Loader version - req.param("gd", GEODE_GD_VERSION_STR); - req.param("geode", Loader::get()->getVersion().toNonVString()); - // Add search params if (query.query) { req.param("query", *query.query); + } else { + // Target current GD version and Loader version when query is not set + req.param("gd", GEODE_GD_VERSION_STR); + req.param("geode", Loader::get()->getVersion().toNonVString()); } + if (query.platforms.size()) { std::string plats = ""; bool first = true; @@ -789,24 +785,14 @@ ServerRequest> server::checkUpdates(Mod const* mo ); } -ServerRequest> server::checkAllUpdates(bool useCache) { - if (useCache) { - return getCache().get(); - } - - auto modIDs = ranges::map>( - Loader::get()->getAllMods(), - [](auto mod) { return mod->getID(); } - ); - +ServerRequest> server::batchedCheckUpdates(std::vector const& batch) { auto req = web::WebRequest(); req.userAgent(getServerUserAgent()); req.param("platform", GEODE_PLATFORM_SHORT_IDENTIFIER); req.param("gd", GEODE_GD_VERSION_STR); req.param("geode", Loader::get()->getVersion().toNonVString()); - if (modIDs.size()) { - req.param("ids", ranges::join(modIDs, ";")); - } + + req.param("ids", ranges::join(batch, ";")); return req.get(formatServerURL("/mods/updates")).map( [](web::WebResponse* response) -> Result, ServerError> { if (response->ok()) { @@ -830,6 +816,79 @@ ServerRequest> server::checkAllUpdates(bool useCach ); } +void server::queueBatches( + ServerRequest>::PostResult const resolve, + std::shared_ptr>> const batches, + std::shared_ptr> accum +) { + // we have to do the copy here, or else our values die + batchedCheckUpdates(batches->back()).listen([resolve, batches, accum](auto result) { + if (result->ok()) { + auto serverValues = result->unwrap(); + + accum->reserve(accum->size() + serverValues.size()); + accum->insert(accum->end(), serverValues.begin(), serverValues.end()); + + if (batches->size() > 1) { + batches->pop_back(); + queueBatches(resolve, batches, accum); + } + else { + resolve(Ok(*accum)); + } + } + else { + resolve(*result); + } + }); +} + +ServerRequest> server::checkAllUpdates(bool useCache) { + if (useCache) { + return getCache().get(); + } + + auto modIDs = ranges::map>( + Loader::get()->getAllMods(), + [](auto mod) { return mod->getID(); } + ); + + // if there's no mods, the request would just be empty anyways + if (modIDs.empty()) { + // you would think it could infer like literally anything + return ServerRequest>::immediate( + Ok>({}) + ); + } + + auto modBatches = std::make_shared>>(); + auto modCount = modIDs.size(); + std::size_t maxMods = 200u; // this affects 0.03% of users + + if (modCount <= maxMods) { + // no tricks needed + return batchedCheckUpdates(modIDs); + } + + // even out the mod count, so a request with 230 mods sends two 115 mod requests + auto batchCount = modCount / maxMods + 1; + auto maxBatchSize = modCount / batchCount + 1; + + for (std::size_t i = 0u; i < modCount; i += maxBatchSize) { + auto end = std::min(modCount, i + maxBatchSize); + modBatches->emplace_back(modIDs.begin() + i, modIDs.begin() + end); + } + + // chain requests to avoid doing too many large requests at once + return ServerRequest>::runWithCallback( + [modBatches](auto finish, auto progress, auto hasBeenCancelled) { + auto accum = std::make_shared>(); + queueBatches(finish, modBatches, accum); + }, + "Mod Update Check" + ); +} + void server::clearServerCaches(bool clearGlobalCaches) { getCache<&getMods>().clear(); getCache<&getMod>().clear(); diff --git a/loader/src/server/Server.hpp b/loader/src/server/Server.hpp index 908d4eab..34b8f336 100644 --- a/loader/src/server/Server.hpp +++ b/loader/src/server/Server.hpp @@ -151,7 +151,15 @@ namespace server { ServerRequest> getTags(bool useCache = true); ServerRequest> checkUpdates(Mod const* mod); + + ServerRequest> batchedCheckUpdates(std::vector const& batch); + void queueBatches( + ServerRequest>::PostResult const finish, + std::shared_ptr>> const batches, + std::shared_ptr> const accum + ); + ServerRequest> checkAllUpdates(bool useCache = true); - + void clearServerCaches(bool clearGlobalCaches = false); } diff --git a/loader/src/ui/mods/list/ModItem.cpp b/loader/src/ui/mods/list/ModItem.cpp index 91e769ae..e35255c8 100644 --- a/loader/src/ui/mods/list/ModItem.cpp +++ b/loader/src/ui/mods/list/ModItem.cpp @@ -134,12 +134,27 @@ bool ModItem::init(ModSource&& source) { m_viewMenu->setAnchorPoint({ 1.f, .5f }); m_viewMenu->setScale(.55f); - ButtonSprite* spr; - if (Loader::get()->isModInstalled(m_source.getID())) { - spr = createGeodeButton("View", 50, false, true); - } else { - spr = createGeodeButton("Get", 50, false, true, GeodeButtonSprite::Install); + ButtonSprite* spr = nullptr; + if (auto serverMod = m_source.asServer(); serverMod != nullptr) { + auto version = serverMod->latestVersion(); + + auto geodeValid = Loader::get()->isModVersionSupported(version.getGeodeVersion()); + auto gameVersion = version.getGameVersion(); + auto gdValid = gameVersion == "*" || gameVersion == GEODE_STR(GEODE_GD_VERSION); + + if (!geodeValid || !gdValid) { + spr = createGeodeButton("NA", 50, false, true, GeodeButtonSprite::Default); + } } + + if (!spr) { + if (Loader::get()->isModInstalled(m_source.getID())) { + spr = createGeodeButton("View", 50, false, true); + } else { + spr = createGeodeButton("Get", 50, false, true, GeodeButtonSprite::Install); + } + } + auto viewBtn = CCMenuItemSpriteExtra::create( spr, this, menu_selector(ModItem::onView) @@ -478,6 +493,25 @@ void ModItem::onView(CCObject*) { )->show(); } + if (auto serverMod = m_source.asServer(); serverMod != nullptr) { + auto version = serverMod->latestVersion(); + auto geodeVersion = version.getGeodeVersion(); + auto geodeValid = Loader::get()->isModVersionSupported(geodeVersion); + + if (auto res = version.checkGameVersion(); !res) { + FLAlertLayer::create(nullptr, "Unavailable", res.unwrapErr(), "Close", nullptr)->show(); + return; + } else if (!geodeValid) { + auto msg = fmt::format( + "Geode {} is required to view this mod. You currently have {}.", + geodeVersion.toVString(), + Loader::get()->getVersion().toVString() + ); + FLAlertLayer::create(nullptr, "Unavailable", msg, "Close", nullptr)->show(); + return; + } + } + // Always open up the popup for the installed mod page if that is possible ModPopup::create(m_source.convertForPopup())->show(); } diff --git a/loader/src/ui/mods/sources/ServerModListSource.cpp b/loader/src/ui/mods/sources/ServerModListSource.cpp index d85e9f13..9ae8d828 100644 --- a/loader/src/ui/mods/sources/ServerModListSource.cpp +++ b/loader/src/ui/mods/sources/ServerModListSource.cpp @@ -80,7 +80,13 @@ ServerModListSource* ServerModListSource::get(ServerModListType type) { } void ServerModListSource::setSearchQuery(std::string const& query) { - m_query.query = query.size() ? std::optional(query) : std::nullopt; + if (query.empty()) { + m_query.query = std::nullopt; + m_query.platforms = { GEODE_PLATFORM_TARGET }; + } else { + m_query.query = std::optional(query); + m_query.platforms = {}; + } } std::unordered_set ServerModListSource::getModTags() const {