diff --git a/loader/CMakeLists.txt b/loader/CMakeLists.txt index f2f716e2..4078d47f 100644 --- a/loader/CMakeLists.txt +++ b/loader/CMakeLists.txt @@ -22,7 +22,6 @@ file(GLOB SOURCES CONFIGURE_DEPENDS src/loader/*.cpp src/main.cpp src/utils/*.cpp - src/index/*.cpp src/ui/nodes/*.cpp src/ui/internal/*.cpp src/ui/internal/credits/*.cpp @@ -110,7 +109,6 @@ target_include_directories(${PROJECT_NAME} PRIVATE src/internal/ src/platform/ src/gui/ - src/index/ md4c/src/ hash/ ./ # lilac diff --git a/loader/include/Geode/loader/Event.hpp b/loader/include/Geode/loader/Event.hpp index 1ccc3348..75231573 100644 --- a/loader/include/Geode/loader/Event.hpp +++ b/loader/include/Geode/loader/Event.hpp @@ -99,7 +99,8 @@ namespace geode { public: void postFrom(Mod* sender); - inline void post() { + template + void post() { postFrom(Mod::get()); } diff --git a/loader/include/Geode/loader/Index.hpp b/loader/include/Geode/loader/Index.hpp new file mode 100644 index 00000000..8478435f --- /dev/null +++ b/loader/include/Geode/loader/Index.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include "Types.hpp" +#include "ModInfo.hpp" +#include "Event.hpp" +#include + +namespace geode { + using UpdateFinished = std::monostate; + using UpdateProgress = std::pair; + using UpdateError = std::string; + using UpdateStatus = std::variant; + + class GEODE_DLL IndexUpdateEvent : public Event { + protected: + std::string m_sourceRepository; + UpdateStatus m_status; + + public: + IndexUpdateEvent( + std::string const& src, + UpdateStatus status + ); + std::string getSource() const; + UpdateStatus getStatus() const; + }; + + class GEODE_DLL IndexUpdateFilter : public EventFilter { + public: + using Callback = void(IndexUpdateEvent*); + + ListenerResult handle(std::function fn, IndexUpdateEvent* event); + IndexUpdateFilter(); + }; + + struct GEODE_DLL IndexItem { + std::string sourceRepository; + ghc::filesystem::path path; + ModInfo info; + struct { + std::string url; + std::string hash; + std::unordered_set platforms; + } download; + bool isFeatured; + }; + + struct GEODE_DLL IndexSource final { + std::string repository; + bool isUpToDate = false; + + std::string dirname() const; + }; + + class GEODE_DLL Index final { + protected: + // 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; + + std::vector m_sources; + std::unordered_map m_items; + + Index(); + + void checkSourceUpdates(IndexSource& src); + void downloadSource(IndexSource& src); + void updateSourceFromLocal(IndexSource& src); + void cleanupItems(); + + public: + static Index* get(); + + void addSource(std::string const& repository); + void removeSource(std::string const& repository); + std::vector getSources() const; + + std::vector getItems() const; + bool isKnownItem(std::string const& id, std::optional version) const; + std::optional getItem(std::string const& id, std::optional version) const; + + bool isUpToDate() const; + void update(bool force = false); + }; + +} diff --git a/loader/include/Geode/platform/platform.hpp b/loader/include/Geode/platform/platform.hpp index 1da9f6e3..88164d28 100644 --- a/loader/include/Geode/platform/platform.hpp +++ b/loader/include/Geode/platform/platform.hpp @@ -1,88 +1,9 @@ #pragma once #include "cplatform.h" - +#include #include -namespace geode { - class PlatformID { - public: - enum { - Unknown = -1, - Windows, - MacOS, - iOS, - Android, - Linux, - }; - - using Type = decltype(Unknown); - - Type m_value; - - PlatformID(Type t) { - m_value = t; - } - - PlatformID& operator=(Type t) { - m_value = t; - return *this; - } - - bool operator==(int other) const { - return m_value == other; - } - - bool operator==(PlatformID const& other) const { - return m_value == other.m_value; - } - - bool operator<(PlatformID const& other) const { - return m_value < other.m_value; - } - - bool operator>(PlatformID const& other) const { - return m_value > other.m_value; - } - - operator int() const { - return m_value; - } - - template - static PlatformID from(T t) { - return static_cast(t); - } - - template - T to() const { - return static_cast(m_value); - } - - static constexpr char const* toString(Type lp) { - switch (lp) { - case Unknown: return "Unknown"; - case Windows: return "Windows"; - case MacOS: return "MacOS"; - case iOS: return "iOS"; - case Android: return "Android"; - case Linux: return "Linux"; - default: break; - } - return "Undefined"; - } - }; -} - -namespace std { - template <> - struct hash { - inline std::size_t operator()(geode::PlatformID const& id) const { - return std::hash()(id.m_value); - } - }; -} - #if !defined(__PRETTY_FUNCTION__) && !defined(__GNUC__) #define GEODE_PRETTY_FUNCTION std::string(__FUNCSIG__) #else @@ -92,7 +13,6 @@ namespace std { // Windows #ifdef GEODE_IS_WINDOWS - #define GEODE_PLATFORM_TARGET PlatformID::Windows #define GEODE_HIDDEN #define GEODE_INLINE __forceinline #define GEODE_VIRTUAL_CONSTEXPR @@ -111,7 +31,6 @@ namespace std { #elif defined(GEODE_IS_MACOS) - #define GEODE_PLATFORM_TARGET PlatformID::MacOS #define GEODE_HIDDEN __attribute__((visibility("hidden"))) #define GEODE_INLINE inline __attribute__((always_inline)) #define GEODE_VIRTUAL_CONSTEXPR constexpr @@ -130,7 +49,6 @@ namespace std { #elif defined(GEODE_IS_IOS) - #define GEODE_PLATFORM_TARGET PlatformID::iOS #define GEODE_HIDDEN __attribute__((visibility("hidden"))) #define GEODE_INLINE inline __attribute__((always_inline)) #define GEODE_VIRTUAL_CONSTEXPR constexpr @@ -149,7 +67,6 @@ namespace std { #elif defined(GEODE_IS_ANDROID) - #define GEODE_PLATFORM_TARGET PlatformID::Android #define GEODE_HIDDEN __attribute__((visibility("hidden"))) #define GEODE_INLINE inline __attribute__((always_inline)) #define GEODE_VIRTUAL_CONSTEXPR constexpr @@ -171,3 +88,105 @@ namespace std { #error "Unsupported Platform!" #endif + +namespace geode { + class PlatformID { + public: + enum { + Unknown = -1, + Windows, + MacOS, + iOS, + Android, + Linux, + }; + + using Type = decltype(Unknown); + + Type m_value; + + constexpr PlatformID(Type t) { + m_value = t; + } + + constexpr PlatformID& operator=(Type t) { + m_value = t; + return *this; + } + + constexpr bool operator==(int other) const { + return m_value == other; + } + + constexpr bool operator==(PlatformID const& other) const { + return m_value == other.m_value; + } + + constexpr bool operator<(PlatformID const& other) const { + return m_value < other.m_value; + } + + constexpr bool operator>(PlatformID const& other) const { + return m_value > other.m_value; + } + + constexpr operator int() const { + return m_value; + } + + /** + * Parse string into PlatformID. String should be all-lowercase, for + * example "windows" or "linux" + */ + static GEODE_DLL PlatformID from(const char* str); + static GEODE_DLL PlatformID from(std::string const& str); + + static constexpr char const* toString(Type lp) { + switch (lp) { + case Unknown: return "Unknown"; + case Windows: return "Windows"; + case MacOS: return "MacOS"; + case iOS: return "iOS"; + case Android: return "Android"; + case Linux: return "Linux"; + default: break; + } + return "Undefined"; + } + + template + requires requires(T t) { + static_cast(t); + } + constexpr static PlatformID from(T t) { + return static_cast(t); + } + + template + requires requires(Type t) { + static_cast(t); + } + constexpr T to() const { + return static_cast(m_value); + } + }; +} + +namespace std { + template <> + struct hash { + inline std::size_t operator()(geode::PlatformID const& id) const { + return std::hash()(id.m_value); + } + }; +} + +#ifdef GEODE_IS_WINDOWS + #define GEODE_PLATFORM_TARGET PlatformID::Windows +#elif defined(GEODE_IS_MACOS) + #define GEODE_PLATFORM_TARGET PlatformID::MacOS +#elif defined(GEODE_IS_IOS) + #define GEODE_PLATFORM_TARGET PlatformID::iOS +#elif defined(GEODE_IS_ANDROID) + #define GEODE_PLATFORM_TARGET PlatformID::Android +#endif diff --git a/loader/include/Geode/utils/cocos.hpp b/loader/include/Geode/utils/cocos.hpp index acbc9603..5650c2b3 100644 --- a/loader/include/Geode/utils/cocos.hpp +++ b/loader/include/Geode/utils/cocos.hpp @@ -3,10 +3,11 @@ #include "casts.hpp" #include "../external/json/json.hpp" #include "general.hpp" -#include +#include "../DefaultInclude.hpp" #include #include #include +#include "../loader/Event.hpp" // support converting ccColor3B / ccColor4B to / from json namespace cocos2d { @@ -183,7 +184,7 @@ namespace geode { } } -// Ref +// Ref & Bug namespace geode { /** * A smart pointer to a managed CCObject-deriving class. Retains shared diff --git a/loader/include/Geode/utils/file.hpp b/loader/include/Geode/utils/file.hpp index 2d75fd7e..4ed20b8d 100644 --- a/loader/include/Geode/utils/file.hpp +++ b/loader/include/Geode/utils/file.hpp @@ -71,15 +71,20 @@ namespace geode::utils::file { * @param dir Directory to unzip the contents to */ Result<> extractAllTo(Path const& dir); - }; - /** - * Unzip file to directory - * @param from File to unzip - * @param to Directory to unzip to - * @returns Ok on success, Error on error - */ - GEODE_DLL Result<> unzipTo(ghc::filesystem::path const& from, ghc::filesystem::path const& to); + /** + * Helper method for quickly unzipping a file + * @param from ZIP file to unzip + * @param to Directory to unzip to + * @param deleteZipAfter Whether to delete the zip after unzipping + * @returns Succesful result on success, errorful result on error + */ + static Result<> intoDir( + Path const& from, + Path const& to, + bool deleteZipAfter = false + ); + }; GEODE_DLL bool openFolder(ghc::filesystem::path const& path); diff --git a/loader/include/Geode/utils/general.hpp b/loader/include/Geode/utils/general.hpp index 51330c63..707a0f78 100644 --- a/loader/include/Geode/utils/general.hpp +++ b/loader/include/Geode/utils/general.hpp @@ -2,7 +2,7 @@ #include "Result.hpp" -#include +#include "../DefaultInclude.hpp" #include #include #include diff --git a/loader/include/Geode/utils/map.hpp b/loader/include/Geode/utils/map.hpp index 5460aab0..e5110a27 100644 --- a/loader/include/Geode/utils/map.hpp +++ b/loader/include/Geode/utils/map.hpp @@ -18,7 +18,6 @@ namespace geode::utils::map { * beeing looked for, and false if not. * @returns True if value matching `containFunc` was found, * false if not. - * @author HJfod */ template bool contains(std::unordered_map const& map, std::function containFunc) { @@ -38,7 +37,6 @@ namespace geode::utils::map { * @returns The value matching `selectFunc` if one was found, * otherwise the default value for type R or `nullptr` if R is * a pointer. - * @author HJfod */ template R select(std::unordered_map const& map, std::function selectFunc) { @@ -58,7 +56,6 @@ namespace geode::utils::map { * return true if the item matches what is * beeing looked for, and false if not. * @returns Vector of all values that matched. - * @author HJfod */ template std::vector selectAll( @@ -77,10 +74,9 @@ namespace geode::utils::map { * Get all values in a map. * @param map Map to get values from * @returns Vector of all values. - * @author HJfod */ template - std::vector getValues(std::unordered_map const& map) { + std::vector values(std::unordered_map const& map) { std::vector res; for (auto const& [_, r] : map) { res.push_back(r); @@ -92,10 +88,9 @@ namespace geode::utils::map { * Get all keys in a map. * @param map Map to get keys from * @returns Vector of all keys. - * @author HJfod */ template - std::vector getKeys(std::unordered_map const& map) { + std::vector keys(std::unordered_map const& map) { std::vector res; for (auto const& [t, _] : map) { res.push_back(t); diff --git a/loader/src/hooks/LoadingLayer.cpp b/loader/src/hooks/LoadingLayer.cpp index b40300a2..20257afa 100644 --- a/loader/src/hooks/LoadingLayer.cpp +++ b/loader/src/hooks/LoadingLayer.cpp @@ -1,13 +1,14 @@ + #include #include - -USE_GEODE_NAMESPACE(); - #include #include +USE_GEODE_NAMESPACE(); + struct CustomLoadingLayer : Modify { bool m_updatingResources; + EventListener m_resourceListener; CustomLoadingLayer() : m_updatingResources(false) {} @@ -27,28 +28,14 @@ struct CustomLoadingLayer : Modify { label->setID("geode-loaded-info"); this->addChild(label); + m_fields->m_resourceListener.bind(std::bind( + &CustomLoadingLayer::updateResourcesProgress, + this, std::placeholders::_1 + )); + // verify loader resources - if (!InternalLoader::get()->verifyLoaderResources(std::bind( - &CustomLoadingLayer::updateResourcesProgress, this, std::placeholders::_1, - std::placeholders::_2, std::placeholders::_3 - ))) { - // auto bg = CCScale9Sprite::create( - // "square02b_001.png", { 0.0f, 0.0f, 80.0f, 80.0f } - // ); - // bg->setScale(.6f); - // bg->setColor({ 0, 0, 0 }); - // bg->setOpacity(150); - // bg->setPosition(winSize / 2); - // this->addChild(bg); - - // m_fields->m_updatingResourcesBG = bg; - - // auto label = CCLabelBMFont::create("", "goldFont.fnt"); - // label->setScale(1.1f); - // bg->addChild(label); - + if (!InternalLoader::get()->verifyLoaderResources()) { m_fields->m_updatingResources = true; - this->setUpdateText("Downloading Resources"); } @@ -67,30 +54,28 @@ struct CustomLoadingLayer : Modify { // ); } - void updateResourcesProgress(UpdateStatus status, std::string const& info, uint8_t progress) { - switch (status) { - case UpdateStatus::Progress: { - this->setUpdateText("Downloading Resources: " + std::to_string(progress) + "%"); - } break; - - case UpdateStatus::Finished: { - this->setUpdateText("Resources Downloaded"); - m_fields->m_updatingResources = false; - this->loadAssets(); - } break; - - case UpdateStatus::Failed: { - InternalLoader::platformMessageBox( - "Error updating resources", - "Unable to update Geode resources: " + info + - ".\n" - "The game will be loaded as normal, but please be aware " - "that it may very likely crash." - ); - this->setUpdateText("Resource Download Failed"); - m_fields->m_updatingResources = false; - this->loadAssets(); - } break; + void updateResourcesProgress(ResourceDownloadEvent* event) { + auto status = event->getStatus(); + if (std::holds_alternative(status)) { + auto prog = std::get(status); + this->setUpdateText("Downloading Resources: " + std::to_string(prog.first) + "%"); + } + else if (std::holds_alternative(status)) { + this->setUpdateText("Resources Downloaded"); + m_fields->m_updatingResources = false; + this->loadAssets(); + } + else { + InternalLoader::platformMessageBox( + "Error updating resources", + "Unable to update Geode resources: " + + std::get(status) + ".\n" + "The game will be loaded as normal, but please be aware " + "that it may very likely crash." + ); + this->setUpdateText("Resource Download Failed"); + m_fields->m_updatingResources = false; + this->loadAssets(); } } diff --git a/loader/src/index/Index.cpp b/loader/src/index/Index.cpp deleted file mode 100644 index 690259db..00000000 --- a/loader/src/index/Index.cpp +++ /dev/null @@ -1,703 +0,0 @@ -// #include "Index.hpp" - -// #include -// #include -// #include -// #include -// #include -// #include -// #include -// #include -// #include -// #include -// #include -// #include -// #include -// #include -// #include - -// #define GITHUB_DONT_RATE_LIMIT_ME_PLS 0 - -// template -// static Result readJSON(ghc::filesystem::path const& path) { -// GEODE_UNWRAP_INTO( -// std::string indexJsonData, -// utils::file::readString(path).expect("Unable to read {}", path.string()) -// ); -// try { -// return Ok(Json::parse(indexJsonData)); -// } -// catch (std::exception& e) { -// return Err("Error parsing JSON: " + std::string(e.what())); -// } -// } - -// static PlatformID platformFromString(std::string const& str) { -// switch (hash(utils::string::trim(utils::string::toLower(str)).c_str())) { -// default: -// case hash("unknown"): return PlatformID::Unknown; -// case hash("windows"): return PlatformID::Windows; -// case hash("macos"): return PlatformID::MacOS; -// case hash("ios"): return PlatformID::iOS; -// case hash("android"): return PlatformID::Android; -// case hash("linux"): return PlatformID::Linux; -// } -// } - -// Index* Index::get() { -// static auto ret = new Index(); -// return ret; -// } - -// bool Index::isIndexUpdated() const { -// return m_upToDate; -// } - -// std::vector Index::getFeaturedItems() const { -// std::vector items; -// items.reserve(m_featured.size()); -// std::transform( -// m_featured.begin(), m_featured.end(), std::back_inserter(items), -// [this](auto const& item) { -// return this->getKnownItem(item); -// } -// ); -// return items; -// } - -// bool Index::isFeaturedItem(std::string const& item) const { -// return m_featured.count(item); -// } - -// void Index::updateIndex(IndexUpdateCallback callback, bool force) { -// #define RETURN_ERROR(str) \ -// std::string err__ = (str); \ -// if (callback) callback(UpdateStatus::Failed, err__, 0); \ -// log::info("Index update failed: {}", err__); \ -// return; - -// // if already updated and no force, let -// // delegate know -// if (!force && m_upToDate) { -// if (callback) { -// callback(UpdateStatus::Finished, "Index already updated", 100); -// } -// return; -// } - -// // create directory for the local clone of -// // the index -// auto indexDir = dirs::getIndexDir(); -// ghc::filesystem::create_directories(indexDir); - -// #if GITHUB_DONT_RATE_LIMIT_ME_PLS == 1 - -// auto err = this->updateIndexFromLocalCache(); -// if (!err) { -// RETURN_ERROR(err); -// } - -// m_upToDate = true; -// m_updating = false; - -// if (callback) callback(UpdateStatus::Finished, "", 100); -// return; - -// #endif - -// // read sha of currently installed commit -// std::string currentCommitSHA = ""; -// if (ghc::filesystem::exists(indexDir / "current")) { -// auto data = utils::file::readString(indexDir / "current"); -// if (data) { -// currentCommitSHA = data.unwrap(); -// } -// } - -// web::AsyncWebRequest() -// .join("index-update") -// .header(fmt::format("If-None-Match: \"{}\"", currentCommitSHA)) -// .header("Accept: application/vnd.github.sha") -// .fetch("https://api.github.com/repos/geode-sdk/mods/commits/main") -// .text() -// .then([this, force, callback, currentCommitSHA](std::string const& upcomingCommitSHA) { -// auto indexDir = Loader::get()->getGeodeSaveDirectory() / GEODE_INDEX_DIRECTORY; - -// // gee i sure hope no one does 60 commits to the mod index an hour and download every -// // single one of them -// if (upcomingCommitSHA == "") { -// auto err = this->updateIndexFromLocalCache(); -// if (!err) { -// RETURN_ERROR(err.unwrapErr()); -// } - -// m_upToDate = true; -// m_updating = false; - -// if (callback) callback(UpdateStatus::Finished, "", 100); -// return; -// } - -// // update if forced or latest commit has -// // different sha -// if (force || currentCommitSHA != upcomingCommitSHA) { -// // save new sha in file -// (void)utils::file::writeString(indexDir / "current", upcomingCommitSHA); - -// web::AsyncWebRequest() -// .join("index-download") -// .fetch("https://github.com/geode-sdk/mods/zipball/main") -// .into(indexDir / "index.zip") -// .then([this, indexDir, callback](auto) { -// // delete old index -// try { -// if (ghc::filesystem::exists(indexDir / "index")) { -// ghc::filesystem::remove_all(indexDir / "index"); -// } -// } -// catch (std::exception& e) { -// RETURN_ERROR("Unable to delete old index " + std::string(e.what())); -// } - -// // unzip new index -// auto unzip = file::unzipTo(indexDir / "index.zip", indexDir); -// if (!unzip) { -// RETURN_ERROR(unzip.unwrapErr()); -// } - -// // update index -// auto err = this->updateIndexFromLocalCache(); -// if (!err) { -// RETURN_ERROR(err.unwrapErr()); -// } - -// m_upToDate = true; -// m_updating = false; - -// if (callback) callback(UpdateStatus::Finished, "", 100); -// }) -// .expect([callback](std::string const& err) { -// RETURN_ERROR(err); -// }) -// .progress([callback](web::SentAsyncWebRequest& req, double now, double total) { -// if (callback) -// callback( -// UpdateStatus::Progress, "Downloading", -// static_cast(now / total * 100.0) -// ); -// }); -// } -// else { -// auto err = this->updateIndexFromLocalCache(); -// if (!err) { -// RETURN_ERROR(err.unwrapErr()); -// } - -// m_upToDate = true; -// m_updating = false; - -// if (callback) callback(UpdateStatus::Finished, "", 100); -// } -// }) -// .expect([callback](std::string const& err) { -// RETURN_ERROR(err); -// }) -// .progress([callback](web::SentAsyncWebRequest& req, double now, double total) { -// if (callback) -// callback( -// UpdateStatus::Progress, "Downloading", static_cast(now / total * 100.0) -// ); -// }); -// } - -// void Index::addIndexItemFromFolder(ghc::filesystem::path const& dir) { -// if (ghc::filesystem::exists(dir / "index.json")) { -// auto readJson = readJSON(dir / "index.json"); -// if (!readJson) { -// log::warn("Error reading index.json: {}, skipping", readJson.unwrapErr()); -// return; -// } -// auto json = readJson.unwrap(); -// if (!json.is_object()) { -// log::warn("[index.json] is not an object, skipping"); -// return; -// } - -// auto infoRes = ModInfo::createFromFile(dir / "mod.json"); -// if (!infoRes) { -// log::warn("{}: {}, skipping", dir, infoRes.unwrapErr()); -// return; -// } -// auto info = infoRes.unwrap(); - -// // make sure only latest version is present in index -// auto old = std::find_if(m_items.begin(), m_items.end(), [info](IndexItem const& item) { -// return item.m_info.m_id == info.m_id; -// }); -// if (old != m_items.end()) { -// // this one is newer -// if (old->m_info.m_version < info.m_version) { -// m_items.erase(old); -// } else { -// log::warn( -// "Found older version of ({} < {}) of {}, skipping", -// info.m_version, old->m_info.m_version, info.m_id -// ); -// return; -// } -// } - -// IndexItem item; - -// item.m_path = dir; -// item.m_info = info; - -// if (!json.contains("download") || !json["download"].is_object()) { -// log::warn("[index.json].download is not an object, skipping"); -// return; -// } - -// #define REQUIRE_DOWNLOAD_KEY(key, type) \ -// if (!download.contains(key) || !download[key].is_##type()) { \ -// log::warn("[index.json].download." key " is not a " #type ", skipping"); \ -// return; \ -// } - -// try { -// auto download = json["download"]; - -// REQUIRE_DOWNLOAD_KEY("url", string); -// REQUIRE_DOWNLOAD_KEY("name", string); -// REQUIRE_DOWNLOAD_KEY("hash", string); -// REQUIRE_DOWNLOAD_KEY("platforms", array); - -// item.m_download.m_url = download["url"]; -// item.m_download.m_filename = download["name"]; -// item.m_download.m_hash = download["hash"]; -// for (auto& platform : download["platforms"]) { -// item.m_download.m_platforms.insert(platformFromString(platform)); -// } - -// if (json.contains("categories")) { -// if (!json["categories"].is_array()) { -// log::warn("[index.json].categories is not an array, skipping"); -// return; -// } -// item.m_categories = json["categories"].template get>(); -// m_categories.insert(item.m_categories.begin(), item.m_categories.end()); -// } -// } -// catch (std::exception& e) { -// log::warn("[index.json] parsing error: {}, skipping", e.what()); -// return; -// } - -// m_items.push_back(item); -// } -// else { -// log::warn("Index directory {} is missing index.json, skipping", dir); -// } -// } - -// Result<> Index::updateIndexFromLocalCache() { -// m_items.clear(); -// auto baseIndexDir = Loader::get()->getGeodeSaveDirectory() / GEODE_INDEX_DIRECTORY; - -// // load geode.json (index settings) -// if (auto baseIndexJson = readJSON(baseIndexDir / "geode.json")) { -// auto json = baseIndexJson.unwrap(); -// auto checker = JsonChecker(json); -// checker.root("[index/geode.json]").obj().has("featured").into(m_featured); -// } - -// // load index mods -// auto modsDir = baseIndexDir / "index"; -// if (ghc::filesystem::exists(modsDir)) { -// for (auto const& dir : ghc::filesystem::directory_iterator(modsDir)) { -// if (ghc::filesystem::is_directory(dir)) { -// this->addIndexItemFromFolder(dir); -// } -// } -// log::info("Index updated"); -// return Ok(); -// } -// else { -// return Err( -// "Index appears not to have been " -// "downloaded, or is fully empty" -// ); -// } -// } - -// std::vector Index::getItems() const { -// return m_items; -// } - -// std::unordered_set Index::getCategories() const { -// return m_categories; -// } - -// bool Index::isKnownItem(std::string const& id) const { -// for (auto& item : m_items) { -// if (item.m_info.m_id == id) return true; -// } -// return false; -// } - -// IndexItem Index::getKnownItem(std::string const& id) const { -// for (auto& item : m_items) { -// if (item.m_info.m_id == id) { -// return item; -// } -// } -// return IndexItem(); -// } - -// struct UninstalledDependency { -// std::string m_id; -// bool m_isInIndex; -// }; - -// static void getUninstalledDependenciesRecursive( -// ModInfo const& info, std::vector& deps -// ) { -// for (auto& dep : info.m_dependencies) { -// UninstalledDependency d; -// d.m_isInIndex = Index::get()->isKnownItem(dep.m_id); -// if (!Loader::get()->isModInstalled(dep.m_id)) { -// d.m_id = dep.m_id; -// deps.push_back(d); -// } -// if (d.m_isInIndex) { -// getUninstalledDependenciesRecursive(Index::get()->getKnownItem(dep.m_id).m_info, deps); -// } -// } -// } - -// Result> Index::checkDependenciesForItem(IndexItem const& item) { -// // todo: check versions -// std::vector deps; -// getUninstalledDependenciesRecursive(item.m_info, deps); -// if (deps.size()) { -// std::vector unknownDeps; -// for (auto& dep : deps) { -// if (!dep.m_isInIndex) { -// unknownDeps.push_back(dep.m_id); -// } -// } -// if (unknownDeps.size()) { -// std::string list = ""; -// for (auto& ud : unknownDeps) { -// list += "" + ud + ", "; -// } -// list.pop_back(); -// list.pop_back(); -// return Err( -// "This mod or its dependencies depends on the " -// "following unknown mods: " + -// list + -// ". You will have " -// "to manually install these mods before you can install " -// "this one." -// ); -// } -// std::vector list = {}; -// for (auto& d : deps) { -// list.push_back(d.m_id); -// } -// list.push_back(item.m_info.m_id); -// return Ok(list); -// } -// else { -// return Ok>({ item.m_info.m_id }); -// } -// } - -// Result Index::installItems(std::vector const& items) { -// std::vector ids {}; -// for (auto& item : items) { -// if (!item.m_download.m_platforms.count(GEODE_PLATFORM_TARGET)) { -// return Err( -// "This mod is not available on your " -// "current platform \"" GEODE_PLATFORM_NAME "\" - Sorry! :(" -// ); -// } -// if (!item.m_download.m_url.size()) { -// return Err( -// "Download URL not set! Report this bug to " -// "the Geode developers - this should not happen, ever." -// ); -// } -// if (!item.m_download.m_filename.size()) { -// return Err( -// "Download filename not set! Report this bug to " -// "the Geode developers - this should not happen, ever." -// ); -// } -// if (!item.m_download.m_hash.size()) { -// return Err( -// "Checksum not set! Report this bug to " -// "the Geode developers - this should not happen, ever." -// ); -// } -// GEODE_UNWRAP_INTO(auto list, checkDependenciesForItem(item)); -// ranges::push(ids, list); -// } -// auto ret = std::make_shared(std::unordered_set(ids.begin(), ids.end())); -// m_installations.insert(ret); -// return Ok(ret); -// } - -// Result Index::installItem(IndexItem const& item) { -// return this->installItems({ item }); -// } - -// bool Index::isUpdateAvailableForItem(std::string const& id) const { -// if (!this->isKnownItem(id)) { -// return false; -// } -// return this->isUpdateAvailableForItem(this->getKnownItem(id)); -// } - -// bool Index::isUpdateAvailableForItem(IndexItem const& item) const { -// if (!Loader::get()->isModInstalled(item.m_info.m_id)) { -// return false; -// } -// // has the mod been updated (but update not yet been applied by restarting game) -// if (m_updated.count(item.m_info.m_id)) { -// return false; -// } -// return item.m_info.m_version > Loader::get()->getInstalledMod(item.m_info.m_id)->getVersion(); -// } - -// bool Index::areUpdatesAvailable() const { -// for (auto& item : m_items) { -// if (this->isUpdateAvailableForItem(item.m_info.m_id)) { -// return true; -// } -// } -// return false; -// } - -// Result Index::installAllUpdates() { -// // find items that need updating -// std::vector itemsToUpdate {}; -// for (auto& item : m_items) { -// if (this->isUpdateAvailableForItem(item)) { -// itemsToUpdate.push_back(item); -// } -// } -// return this->installItems(itemsToUpdate); -// } - -// std::vector Index::getRunningInstallations() const { -// return std::vector(m_installations.begin(), m_installations.end()); -// } - -// InstallHandle Index::isInstallingItem(std::string const& id) { -// for (auto& inst : m_installations) { -// if (inst->m_toInstall.count(id)) { -// return inst; -// } -// } -// return nullptr; -// } - -// std::unordered_set InstallItems::toInstall() const { -// return m_toInstall; -// } - -// InstallItems::CallbackID InstallItems::join(ItemInstallCallback callback) { -// // already finished? -// if (m_started && this->finished()) { -// callback(shared_from_this(), UpdateStatus::Finished, "", 100); -// return 0; -// } -// // start at one because 0 means invalid callback -// static CallbackID COUNTER = 1; -// if (callback) { -// auto id = COUNTER++; -// m_callbacks.insert({ id, callback }); -// return id; -// } -// return 0; -// } - -// void InstallItems::leave(InstallItems::CallbackID id) { -// m_callbacks.erase(id); -// } - -// void InstallItems::post(UpdateStatus status, std::string const& info, uint8_t progress) { -// for (auto& [_, cb] : m_callbacks) { -// cb(shared_from_this(), status, info, progress); -// } -// } - -// void InstallItems::progress(std::string const& info, uint8_t progress) { -// this->post(UpdateStatus::Progress, info, progress); -// } - -// void InstallItems::error(std::string const& info) { -// this->post(UpdateStatus::Failed, info, 0); -// } - -// void InstallItems::finish(bool replaceFiles) { -// // move files from temp dir to geode directory -// auto tempDir = Loader::get()->getGeodeSaveDirectory() / GEODE_INDEX_DIRECTORY / "temp"; -// for (auto& file : ghc::filesystem::directory_iterator(tempDir)) { -// try { -// auto modDir = Loader::get()->getGeodeDirectory() / "mods"; -// auto targetFile = modDir / file.path().filename(); -// auto targetName = file.path().stem(); - -// if (!replaceFiles) { -// // find valid filename that doesn't exist yet -// auto filename = ghc::filesystem::path(targetName).replace_extension("").string(); - -// size_t number = 0; -// while (ghc::filesystem::exists(targetFile)) { -// targetFile = modDir / (filename + std::to_string(number) + ".geode"); -// number++; -// } -// } - -// // move file -// ghc::filesystem::rename(file, targetFile); -// } -// catch (std::exception& e) { -// try { -// ghc::filesystem::remove_all(tempDir); -// } -// catch (...) { -// } -// return this->error( -// "Unable to move downloaded file to mods directory: \"" + std::string(e.what()) + -// " \" " -// "(This might be due to insufficient permissions to " -// "write files under SteamLibrary, try running GD as " -// "administrator)" -// ); -// } -// } - -// // load mods -// (void)Loader::get()->refreshModsList(); - -// // finished -// this->post(UpdateStatus::Finished, "", 100); - -// // let index know these mods have been updated -// for (auto& inst : m_toInstall) { -// Index::get()->m_updated.insert(inst); -// } - -// // if no one is listening, show a popup anyway -// if (!m_callbacks.size()) { -// FLAlertLayer::create( -// "Mods installed", -// "The following mods have been installed: " + -// ranges::join(m_toInstall, std::string(",")) + -// "\n" -// "Please restart the game to apply", -// "OK" -// ) -// ->show(); -// } - -// // no longer need to ensure aliveness -// Index::get()->m_installations.erase(shared_from_this()); -// } - -// InstallItems::CallbackID InstallItems::start(ItemInstallCallback callback, bool replaceFiles) { -// auto id = this->join(callback); - -// // check if started already, if so, behave like join -// if (m_started) return id; -// m_started = true; - -// for (auto& inst : m_toInstall) { -// // by virtue of running this function we know item must be valid -// auto item = Index::get()->getKnownItem(inst); - -// auto indexDir = Loader::get()->getGeodeSaveDirectory() / GEODE_INDEX_DIRECTORY; -// (void)file::createDirectoryAll(indexDir / "temp"); -// auto tempFile = indexDir / "temp" / item.m_download.m_filename; - -// m_downloaded.push_back(tempFile); - -// auto handle = -// web::AsyncWebRequest() -// .join("install_mod_" + inst) -// .fetch(item.m_download.m_url) -// .into(tempFile) -// .then([this, replaceFiles, item, inst, indexDir, tempFile](auto) { -// // check for 404 -// auto notFound = utils::file::readString(tempFile); -// if (notFound && notFound.unwrap() == "Not Found") { -// try { -// ghc::filesystem::remove(tempFile); -// } -// catch (...) { -// } -// return this->error( -// "Binary file download returned \"Not found\". Report " -// "this to the Geode development team." -// ); -// } - -// // verify checksum -// this->progress("Verifying", 100); -// if (::calculateHash(tempFile) != item.m_download.m_hash) { -// try { -// ghc::filesystem::remove(tempFile); -// } -// catch (...) { -// } -// return this->error( -// "Checksum mismatch! (Downloaded file did not match what " -// "was expected. Try again, and if the download fails another time, " -// "report this to the Geode development team." -// ); -// } - -// // finished() just checks if the web requests are done -// if (this->finished()) { -// this->finish(replaceFiles); -// } -// }) -// .expect([this, inst](std::string const& error) { -// this->error(error); -// this->cancel(); -// }) -// .cancelled([this, item](auto&) { -// this->cancel(); -// }) -// .progress([this, inst](web::SentAsyncWebRequest&, double now, double total) { -// this->progress("Downloading binary", static_cast(now / total * 100.0)); -// }) -// .send(); - -// m_handles.push_back(handle); -// } -// // manage installation in the index until it's finished so -// // even if no one listens to it it doesn't get freed from -// // memory -// Index::get()->m_installations.insert(shared_from_this()); - -// return id; -// } - -// bool InstallItems::finished() const { -// for (auto& inst : m_handles) { -// if (!inst->finished()) { -// return false; -// } -// } -// return true; -// } - -// void InstallItems::cancel() { -// for (auto& inst : m_handles) { -// inst->cancel(); -// } -// } diff --git a/loader/src/index/Index.hpp b/loader/src/index/Index.hpp deleted file mode 100644 index 1b548628..00000000 --- a/loader/src/index/Index.hpp +++ /dev/null @@ -1,117 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -USE_GEODE_NAMESPACE(); - -class Index; -struct ModInstallUpdate; -struct InstallItems; - -using InstallHandle = std::shared_ptr; - -// todo: make index use events - -enum class UpdateStatus { - Progress, - Failed, - Finished, -}; - -using ItemInstallCallback = - std::function; -using IndexUpdateCallback = std::function; - -struct IndexItem { - struct Download { - std::string m_url; - std::string m_filename; - std::string m_hash; - std::unordered_set m_platforms; - }; - - ghc::filesystem::path m_path; - ModInfo m_info; - Download m_download; - std::unordered_set m_categories; -}; - -struct InstallItems final : public std::enable_shared_from_this { -public: - using CallbackID = size_t; - -private: - bool m_started = false; - std::unordered_set m_toInstall; - std::vector m_handles; - std::unordered_map m_callbacks; - std::vector m_downloaded; - - void post(UpdateStatus status, std::string const& info, uint8_t progress); - void progress(std::string const& info, uint8_t progress); - void error(std::string const& info); - void finish(bool replaceFiles); - - friend class Index; - -public: - std::unordered_set toInstall() const; - - inline InstallItems(std::unordered_set const& toInstall) : - m_toInstall(toInstall) {} - - void cancel(); - bool finished() const; - - CallbackID join(ItemInstallCallback callback); - void leave(CallbackID id); - - CallbackID start(ItemInstallCallback callback, bool replaceFiles = true); -}; - -class Index { -protected: - bool m_upToDate = false; - bool m_updating = false; - mutable std::mutex m_callbacksMutex; - std::vector m_items; - std::unordered_set m_installations; - mutable std::mutex m_ticketsMutex; - std::unordered_set m_featured; - std::unordered_set m_categories; - std::unordered_set m_updated; - - void addIndexItemFromFolder(ghc::filesystem::path const& dir); - Result<> updateIndexFromLocalCache(); - - Result> checkDependenciesForItem(IndexItem const& item); - - friend struct InstallItems; - -public: - static Index* get(); - - std::vector getItems() const; - bool isKnownItem(std::string const& id) const; - IndexItem getKnownItem(std::string const& id) const; - - std::unordered_set getCategories() const; - std::vector getFeaturedItems() const; - bool isFeaturedItem(std::string const& item) const; - - Result installItems(std::vector const& item); - Result installItem(IndexItem const& item); - std::vector getRunningInstallations() const; - InstallHandle isInstallingItem(std::string const& id); - - bool isUpdateAvailableForItem(std::string const& id) const; - bool isUpdateAvailableForItem(IndexItem const& item) const; - bool areUpdatesAvailable() const; - Result installAllUpdates(); - - bool isIndexUpdated() const; - void updateIndex(IndexUpdateCallback callback, bool force = false); -}; diff --git a/loader/src/internal/InternalLoader.cpp b/loader/src/internal/InternalLoader.cpp index 4a718741..f96dd4c2 100644 --- a/loader/src/internal/InternalLoader.cpp +++ b/loader/src/internal/InternalLoader.cpp @@ -16,6 +16,16 @@ #include #include +ListenerResult ResourceDownloadFilter::handle( + std::function fn, + ResourceDownloadEvent* event +) { + fn(event); + return ListenerResult::Propagate; +} + +ResourceDownloadFilter::ResourceDownloadFilter() {} + InternalLoader::InternalLoader() : Loader() {} InternalLoader::~InternalLoader() { @@ -93,7 +103,7 @@ void InternalLoader::loadInfoAlerts(nlohmann::json& json) { m_shownInfoAlerts = json["alerts"].get>(); } -void InternalLoader::downloadLoaderResources(IndexUpdateCallback callback) { +void InternalLoader::downloadLoaderResources() { auto version = this->getVersion().toString(); auto tempResourcesZip = dirs::getTempDir() / "new.zip"; auto resourcesDir = dirs::getGeodeResourcesDir() / InternalMod::get()->getID(); @@ -104,38 +114,32 @@ void InternalLoader::downloadLoaderResources(IndexUpdateCallback callback) { "https://github.com/geode-sdk/geode/releases/download/{}/resources.zip", version )) .into(tempResourcesZip) - .then([tempResourcesZip, resourcesDir, callback](auto) { + .then([tempResourcesZip, resourcesDir](auto) { // unzip resources zip - auto unzip = file::unzipTo(tempResourcesZip, resourcesDir); + auto unzip = file::Unzip::intoDir(tempResourcesZip, resourcesDir, true); if (!unzip) { - if (callback) - callback( - UpdateStatus::Failed, "Unable to unzip new resources: " + unzip.unwrapErr(), 0 - ); - return; + return ResourceDownloadEvent( + UpdateError("Unable to unzip new resources: " + unzip.unwrapErr()) + ).post(); } - // delete resources zip - try { - ghc::filesystem::remove(tempResourcesZip); - } - catch (...) { - } - - if (callback) callback(UpdateStatus::Finished, "Resources updated", 100); + ResourceDownloadEvent(UpdateFinished()).post(); }) - .expect([callback](std::string const& info) { - if (callback) callback(UpdateStatus::Failed, info, 0); + .expect([](std::string const& info) { + ResourceDownloadEvent( + UpdateError("Unable to download resources: " + info) + ).post(); }) - .progress([callback](auto&, double now, double total) { - if (callback) - callback( - UpdateStatus::Progress, "Downloading resources", - static_cast(now / total * 100.0) - ); + .progress([](auto&, double now, double total) { + ResourceDownloadEvent( + UpdateProgress( + static_cast(now / total * 100.0), + "Downloading resources" + ) + ).post(); }); } -bool InternalLoader::verifyLoaderResources(IndexUpdateCallback callback) { +bool InternalLoader::verifyLoaderResources() { static std::optional CACHED = std::nullopt; if (CACHED.has_value()) { return CACHED.value(); @@ -145,8 +149,11 @@ bool InternalLoader::verifyLoaderResources(IndexUpdateCallback callback) { auto resourcesDir = dirs::getGeodeResourcesDir() / InternalMod::get()->getID(); // if the resources dir doesn't exist, then it's probably incorrect - if (!(ghc::filesystem::exists(resourcesDir) && ghc::filesystem::is_directory(resourcesDir))) { - this->downloadLoaderResources(callback); + if (!( + ghc::filesystem::exists(resourcesDir) && + ghc::filesystem::is_directory(resourcesDir) + )) { + this->downloadLoaderResources(); return false; } @@ -166,7 +173,7 @@ bool InternalLoader::verifyLoaderResources(IndexUpdateCallback callback) { log::debug( "compare {} {} {}", file.path().string(), hash, LOADER_RESOURCE_HASHES.at(name) ); - this->downloadLoaderResources(callback); + this->downloadLoaderResources(); return false; } coverage += 1; @@ -174,7 +181,7 @@ bool InternalLoader::verifyLoaderResources(IndexUpdateCallback callback) { // make sure every file was found if (coverage != LOADER_RESOURCE_HASHES.size()) { - this->downloadLoaderResources(callback); + this->downloadLoaderResources(); return false; } diff --git a/loader/src/internal/InternalLoader.hpp b/loader/src/internal/InternalLoader.hpp index 8ac2170b..716278b6 100644 --- a/loader/src/internal/InternalLoader.hpp +++ b/loader/src/internal/InternalLoader.hpp @@ -1,8 +1,8 @@ #pragma once -#include "../index/Index.hpp" #include "FileWatcher.hpp" +#include #include #include #include @@ -15,6 +15,23 @@ USE_GEODE_NAMESPACE(); +class ResourceDownloadEvent : public Event { +protected: + UpdateStatus m_status; + +public: + ResourceDownloadEvent(UpdateStatus status); + UpdateStatus getStatus() const; +}; + +class GEODE_DLL ResourceDownloadFilter : public EventFilter { +public: + using Callback = void(ResourceDownloadEvent*); + + ListenerResult handle(std::function fn, ResourceDownloadEvent* event); + ResourceDownloadFilter(); +}; + /** * Internal extension of Loader for private information * @class InternalLoader @@ -29,7 +46,7 @@ protected: void saveInfoAlerts(nlohmann::json& json); void loadInfoAlerts(nlohmann::json& json); - void downloadLoaderResources(IndexUpdateCallback callback); + void downloadLoaderResources(); bool loadHooks(); void setupIPC(); @@ -66,7 +83,7 @@ public: void closePlatformConsole(); static void platformMessageBox(char const* title, std::string const& info); - bool verifyLoaderResources(IndexUpdateCallback callback); + bool verifyLoaderResources(); friend int geodeEntry(void* platformData); }; diff --git a/loader/src/loader/Index.cpp b/loader/src/loader/Index.cpp new file mode 100644 index 00000000..bef2e78f --- /dev/null +++ b/loader/src/loader/Index.cpp @@ -0,0 +1,256 @@ +#include +#include +#include +#include +#include +#include +#include + +USE_GEODE_NAMESPACE(); + +struct IndexSourceSaveData { + std::string downloadedCommitSHA; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IndexSourceSaveData, downloadedCommitSHA); + +struct IndexSaveData { + std::unordered_map sources; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IndexSaveData, sources); + +std::string IndexSource::dirname() const { + return string::replace(this->repository, "/", "_"); +} + +IndexUpdateEvent::IndexUpdateEvent( + std::string const& src, + UpdateStatus status +) : m_sourceRepository(src), m_status(status) {} + +std::string IndexUpdateEvent::getSource() const { + return m_sourceRepository; +} + +UpdateStatus IndexUpdateEvent::getStatus() const { + return m_status; +} + +ListenerResult IndexUpdateFilter::handle( + std::function fn, + IndexUpdateEvent* event +) { + fn(event); + return ListenerResult::Propagate; +} + +IndexUpdateFilter::IndexUpdateFilter() {} + +Index::Index() { + this->addSource("https://github.com/geode-sdk/index-test"); +} + +Index* Index::get() { + auto inst = new Index(); + return inst; +} + +void Index::addSource(std::string const& repository) { + m_sources.push_back(IndexSource { + .repository = repository + }); +} + +void Index::removeSource(std::string const& repository) { + ranges::remove(m_sources, [repository](IndexSource const& src) { + return src.repository == repository; + }); +} + +std::vector Index::getSources() const { + return m_sources; +} + +bool Index::isUpToDate() const { + for (auto& source : m_sources) { + if (!source.isUpToDate) { + return false; + } + } + return true; +} + +void Index::checkSourceUpdates(IndexSource& src) { + if (src.isUpToDate) { + IndexUpdateEvent(src.repository, UpdateFinished()).post(); + return; + } + IndexUpdateEvent(src.repository, UpdateProgress(0, "Checking status")).post(); + auto data = Mod::get()->getSavedMutable("index"); + auto oldSHA = data.sources[src.repository].downloadedCommitSHA; + web::AsyncWebRequest() + .join(fmt::format("index-update-{}", src.repository)) + .header(fmt::format("If-None-Match: \"{}\"", oldSHA)) + .header("Accept: application/vnd.github.sha") + .fetch(fmt::format("https://api.github.com/repos/{}/commits/main", src.repository)) + .text() + .then([this, &src, oldSHA](std::string const& newSHA) { + // if no new hash was given (rate limited) or the new hash is the + // same as old, then just update from local cache + if (newSHA.empty() || oldSHA == newSHA) { + this->updateSourceFromLocal(src); + } + // otherwise save hash and download source + else { + auto data = Mod::get()->getSavedMutable("index"); + data.sources[src.repository].downloadedCommitSHA = newSHA; + this->downloadSource(src); + } + }) + .expect([&src](std::string const& err) { + IndexUpdateEvent( + src.repository, + UpdateError(fmt::format("Error checking for updates: {}", err)) + ).post(); + }); +} + +void Index::downloadSource(IndexSource& src) { + IndexUpdateEvent(src.repository, UpdateProgress(0, "Beginning download")).post(); + + auto targetFile = dirs::getIndexDir() / fmt::format("{}.zip", src.dirname()); + + web::AsyncWebRequest() + .join(fmt::format("index-download-{}", src.repository)) + .fetch(fmt::format("https://github.com/{}/zipball/main", src.repository)) + .into(targetFile) + .then([this, &src, targetFile](auto) { + auto targetDir = dirs::getIndexDir() / src.dirname(); + // delete old unzipped index + try { + if (ghc::filesystem::exists(targetDir)) { + ghc::filesystem::remove_all(targetDir); + } + } + catch(...) { + return IndexUpdateEvent( + src.repository, + UpdateError("Unable to clear cached index") + ).post(); + } + + // unzip new index + auto unzip = file::Unzip::intoDir(targetFile, targetDir, true); + if (!unzip) { + return IndexUpdateEvent( + src.repository, + UpdateError("Unable to unzip new index") + ).post(); + } + + // update index + this->updateSourceFromLocal(src); + }) + .expect([&src](std::string const& err) { + IndexUpdateEvent( + src.repository, + UpdateError(fmt::format("Error downloading: {}", err)) + ).post(); + }) + .progress([&src](auto&, double now, double total) { + IndexUpdateEvent( + src.repository, + UpdateProgress(static_cast(now / total * 100.0), "Downloading") + ).post(); + }); +} + +void Index::updateSourceFromLocal(IndexSource& src) { + IndexUpdateEvent(src.repository, UpdateProgress(100, "Updating local cache")).post(); + // delete old items from this url if such exist + for (auto& [_, versions] : m_items) { + for (auto it = versions.begin(); it != versions.end(); ) { + if (it->second.sourceRepository == src.repository) { + it = versions.erase(it); + } else { + ++it; + } + } + } + + // todo: add shit + + this->cleanupItems(); + src.isUpToDate = true; + IndexUpdateEvent(src.repository, UpdateFinished()).post(); +} + +void Index::cleanupItems() { + // delete mods with no versions + for (auto it = m_items.begin(); it != m_items.end(); ) { + if (!it->second.size()) { + it = m_items.erase(it); + } else { + ++it; + } + } +} + +void Index::update(bool force) { + // create index dir if it doesn't exist + (void)file::createDirectoryAll(dirs::getIndexDir()); + + // update all sources in GD thread + Loader::get()->queueInGDThread([force, this]() { + for (auto& src : m_sources) { + if (force) { + this->downloadSource(src); + } else { + this->checkSourceUpdates(src); + } + } + }); +} + +std::vector Index::getItems() const { + std::vector res; + for (auto& items : map::values(m_items)) { + if (items.size()) { + res.push_back(items.rbegin()->second); + } + } + return res; +} + +bool Index::isKnownItem( + std::string const& id, + std::optional version +) const { + if (m_items.count(id)) { + if (version) { + return m_items.at(id).count(version.value()); + } else { + return true; + } + } else { + return false; + } +} + +std::optional Index::getItem( + std::string const& id, + std::optional version +) const { + if (m_items.count(id)) { + auto versions = m_items.at(id); + if (version) { + if (versions.count(version.value())) { + return versions.at(version.value()); + } + } else { + if (versions.size()) { + return m_items.at(id).rbegin()->second; + } + } + } + return std::nullopt; +} diff --git a/loader/src/loader/Loader.cpp b/loader/src/loader/Loader.cpp index 90da5472..3eed862a 100644 --- a/loader/src/loader/Loader.cpp +++ b/loader/src/loader/Loader.cpp @@ -275,7 +275,7 @@ Mod* Loader::getLoadedMod(std::string const& id) const { } std::vector Loader::getAllMods() { - return map::getValues(m_mods); + return map::values(m_mods); } Mod* Loader::getInternalMod() { diff --git a/loader/src/ui/internal/GeodeUI.cpp b/loader/src/ui/internal/GeodeUI.cpp index 78dcacb9..8a61232c 100644 --- a/loader/src/ui/internal/GeodeUI.cpp +++ b/loader/src/ui/internal/GeodeUI.cpp @@ -1,4 +1,5 @@ -#include "../index/Index.hpp" + +#include #include "info/ModInfoLayer.hpp" #include "list/ModListLayer.hpp" #include "settings/ModSettingsPopup.hpp" @@ -15,8 +16,9 @@ void geode::openInfoPopup(Mod* mod) { void geode::openIndexPopup(Mod* mod) { if (Index::get()->isKnownItem(mod->getID())) { - ModInfoLayer::create(new ModObject(Index::get()->getKnownItem(mod->getID())), nullptr) - ->show(); + ModInfoLayer::create( + new ModObject(Index::get()->getKnownItem(mod->getID())) + )->show(); } } diff --git a/loader/src/ui/internal/info/ModInfoLayer.hpp b/loader/src/ui/internal/info/ModInfoLayer.hpp index 10056612..996b0a28 100644 --- a/loader/src/ui/internal/info/ModInfoLayer.hpp +++ b/loader/src/ui/internal/info/ModInfoLayer.hpp @@ -4,7 +4,7 @@ #include #include #include -#include +#include USE_GEODE_NAMESPACE(); diff --git a/loader/src/ui/internal/list/ModListLayer.hpp b/loader/src/ui/internal/list/ModListLayer.hpp index e31243c2..233806a1 100644 --- a/loader/src/ui/internal/list/ModListLayer.hpp +++ b/loader/src/ui/internal/list/ModListLayer.hpp @@ -3,7 +3,7 @@ #include "ModListView.hpp" #include -#include +#include USE_GEODE_NAMESPACE(); diff --git a/loader/src/ui/internal/list/ModListView.hpp b/loader/src/ui/internal/list/ModListView.hpp index 129d6ef2..0813078f 100644 --- a/loader/src/ui/internal/list/ModListView.hpp +++ b/loader/src/ui/internal/list/ModListView.hpp @@ -3,7 +3,8 @@ #include #include #include -#include +#include +#include #include USE_GEODE_NAMESPACE(); @@ -18,7 +19,7 @@ enum class ModListType { enum class ModObjectType { Mod, - Unloaded, + Invalid, Index, }; @@ -27,22 +28,25 @@ class ModListLayer; // Wrapper so you can pass Mods in a CCArray struct ModObject : public CCObject { ModObjectType m_type; - Mod* m_mod; - InvalidGeodeFile m_info; - IndexItem m_index; + std::variant m_data; - inline ModObject(Mod* mod) : m_mod(mod), m_type(ModObjectType::Mod) { + inline ModObject(Mod* mod) + : m_data(mod), m_type(ModObjectType::Mod) + { this->autorelease(); - }; + } - inline ModObject(InvalidGeodeFile const& info) : - m_info(info), m_type(ModObjectType::Unloaded) { + inline ModObject(InvalidGeodeFile const& info) + : m_data(info), m_type(ModObjectType::Invalid) + { this->autorelease(); - }; + } - inline ModObject(IndexItem const& index) : m_index(index), m_type(ModObjectType::Index) { + inline ModObject(IndexItem const& index) + : m_data(index), m_type(ModObjectType::Index) + { this->autorelease(); - }; + } }; class ModListView; diff --git a/loader/src/utils/PlatformID.cpp b/loader/src/utils/PlatformID.cpp new file mode 100644 index 00000000..45714ed7 --- /dev/null +++ b/loader/src/utils/PlatformID.cpp @@ -0,0 +1,22 @@ + +#include +#include + +USE_GEODE_NAMESPACE(); + +PlatformID PlatformID::from(const char* str) { + switch (hash(str)) { + default: + case hash("unknown"): return PlatformID::Unknown; + case hash("windows"): return PlatformID::Windows; + case hash("macos"): return PlatformID::MacOS; + case hash("ios"): return PlatformID::iOS; + case hash("android"): return PlatformID::Android; + case hash("linux"): return PlatformID::Linux; + } +} + +PlatformID PlatformID::from(std::string const& str) { + return PlatformID::from(str.c_str()); +} + diff --git a/loader/src/utils/file.cpp b/loader/src/utils/file.cpp index 03c0a237..f8306afa 100644 --- a/loader/src/utils/file.cpp +++ b/loader/src/utils/file.cpp @@ -116,11 +116,6 @@ Result> utils::file::listFilesRecursively(std::string c return Ok(res); } -Result<> utils::file::unzipTo(ghc::filesystem::path const& from, ghc::filesystem::path const& to) { - GEODE_UNWRAP_INTO(auto unzip, Unzip::create(from)); - return unzip.extractAllTo(to); -} - static constexpr auto MAX_ENTRY_PATH_LEN = 256; struct ZipEntry { @@ -244,7 +239,7 @@ ghc::filesystem::path Unzip::getPath() const { } std::vector Unzip::getEntries() const { - return map::getKeys(m_impl->entries()); + return map::keys(m_impl->entries()); } bool Unzip::hasEntry(Path const& name) { @@ -268,3 +263,16 @@ Result<> Unzip::extractAllTo(Path const& dir) { } return Ok(); } + +Result<> Unzip::intoDir( + Path const& from, + Path const& to, + bool deleteZipAfter +) { + GEODE_UNWRAP_INTO(auto unzip, Unzip::create(from)); + GEODE_UNWRAP(unzip.extractAllTo(to)); + if (deleteZipAfter) { + try { ghc::filesystem::remove(from); } catch(...) {} + } + return Ok(); +}