work on new index

- add PlatformID::from string
This commit is contained in:
HJfod 2022-12-01 22:42:49 +02:00
parent f9b6595073
commit 66d12395e1
21 changed files with 614 additions and 1028 deletions

View file

@ -22,7 +22,6 @@ file(GLOB SOURCES CONFIGURE_DEPENDS
src/loader/*.cpp src/loader/*.cpp
src/main.cpp src/main.cpp
src/utils/*.cpp src/utils/*.cpp
src/index/*.cpp
src/ui/nodes/*.cpp src/ui/nodes/*.cpp
src/ui/internal/*.cpp src/ui/internal/*.cpp
src/ui/internal/credits/*.cpp src/ui/internal/credits/*.cpp
@ -110,7 +109,6 @@ target_include_directories(${PROJECT_NAME} PRIVATE
src/internal/ src/internal/
src/platform/ src/platform/
src/gui/ src/gui/
src/index/
md4c/src/ md4c/src/
hash/ hash/
./ # lilac ./ # lilac

View file

@ -99,7 +99,8 @@ namespace geode {
public: public:
void postFrom(Mod* sender); void postFrom(Mod* sender);
inline void post() { template<class = void>
void post() {
postFrom(Mod::get()); postFrom(Mod::get());
} }

View file

@ -0,0 +1,86 @@
#pragma once
#include "Types.hpp"
#include "ModInfo.hpp"
#include "Event.hpp"
#include <unordered_set>
namespace geode {
using UpdateFinished = std::monostate;
using UpdateProgress = std::pair<uint8_t, std::string>;
using UpdateError = std::string;
using UpdateStatus = std::variant<UpdateFinished, UpdateProgress, UpdateError>;
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<IndexUpdateEvent> {
public:
using Callback = void(IndexUpdateEvent*);
ListenerResult handle(std::function<Callback> 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<PlatformID> 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<size_t, IndexItem>;
std::vector<IndexSource> m_sources;
std::unordered_map<std::string, ItemVersions> 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<IndexSource> getSources() const;
std::vector<IndexItem> getItems() const;
bool isKnownItem(std::string const& id, std::optional<size_t> version) const;
std::optional<IndexItem> getItem(std::string const& id, std::optional<size_t> version) const;
bool isUpToDate() const;
void update(bool force = false);
};
}

View file

@ -1,88 +1,9 @@
#pragma once #pragma once
#include "cplatform.h" #include "cplatform.h"
#include <string>
#include <functional> #include <functional>
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 <class T>
static PlatformID from(T t) {
return static_cast<Type>(t);
}
template <class T>
T to() const {
return static_cast<T>(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<geode::PlatformID> {
inline std::size_t operator()(geode::PlatformID const& id) const {
return std::hash<geode::PlatformID::Type>()(id.m_value);
}
};
}
#if !defined(__PRETTY_FUNCTION__) && !defined(__GNUC__) #if !defined(__PRETTY_FUNCTION__) && !defined(__GNUC__)
#define GEODE_PRETTY_FUNCTION std::string(__FUNCSIG__) #define GEODE_PRETTY_FUNCTION std::string(__FUNCSIG__)
#else #else
@ -92,7 +13,6 @@ namespace std {
// Windows // Windows
#ifdef GEODE_IS_WINDOWS #ifdef GEODE_IS_WINDOWS
#define GEODE_PLATFORM_TARGET PlatformID::Windows
#define GEODE_HIDDEN #define GEODE_HIDDEN
#define GEODE_INLINE __forceinline #define GEODE_INLINE __forceinline
#define GEODE_VIRTUAL_CONSTEXPR #define GEODE_VIRTUAL_CONSTEXPR
@ -111,7 +31,6 @@ namespace std {
#elif defined(GEODE_IS_MACOS) #elif defined(GEODE_IS_MACOS)
#define GEODE_PLATFORM_TARGET PlatformID::MacOS
#define GEODE_HIDDEN __attribute__((visibility("hidden"))) #define GEODE_HIDDEN __attribute__((visibility("hidden")))
#define GEODE_INLINE inline __attribute__((always_inline)) #define GEODE_INLINE inline __attribute__((always_inline))
#define GEODE_VIRTUAL_CONSTEXPR constexpr #define GEODE_VIRTUAL_CONSTEXPR constexpr
@ -130,7 +49,6 @@ namespace std {
#elif defined(GEODE_IS_IOS) #elif defined(GEODE_IS_IOS)
#define GEODE_PLATFORM_TARGET PlatformID::iOS
#define GEODE_HIDDEN __attribute__((visibility("hidden"))) #define GEODE_HIDDEN __attribute__((visibility("hidden")))
#define GEODE_INLINE inline __attribute__((always_inline)) #define GEODE_INLINE inline __attribute__((always_inline))
#define GEODE_VIRTUAL_CONSTEXPR constexpr #define GEODE_VIRTUAL_CONSTEXPR constexpr
@ -149,7 +67,6 @@ namespace std {
#elif defined(GEODE_IS_ANDROID) #elif defined(GEODE_IS_ANDROID)
#define GEODE_PLATFORM_TARGET PlatformID::Android
#define GEODE_HIDDEN __attribute__((visibility("hidden"))) #define GEODE_HIDDEN __attribute__((visibility("hidden")))
#define GEODE_INLINE inline __attribute__((always_inline)) #define GEODE_INLINE inline __attribute__((always_inline))
#define GEODE_VIRTUAL_CONSTEXPR constexpr #define GEODE_VIRTUAL_CONSTEXPR constexpr
@ -171,3 +88,105 @@ namespace std {
#error "Unsupported Platform!" #error "Unsupported Platform!"
#endif #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 <class T>
requires requires(T t) {
static_cast<Type>(t);
}
constexpr static PlatformID from(T t) {
return static_cast<Type>(t);
}
template <class T>
requires requires(Type t) {
static_cast<T>(t);
}
constexpr T to() const {
return static_cast<T>(m_value);
}
};
}
namespace std {
template <>
struct hash<geode::PlatformID> {
inline std::size_t operator()(geode::PlatformID const& id) const {
return std::hash<geode::PlatformID::Type>()(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

View file

@ -3,10 +3,11 @@
#include "casts.hpp" #include "casts.hpp"
#include "../external/json/json.hpp" #include "../external/json/json.hpp"
#include "general.hpp" #include "general.hpp"
#include <Geode/DefaultInclude.hpp> #include "../DefaultInclude.hpp"
#include <cocos2d.h> #include <cocos2d.h>
#include <functional> #include <functional>
#include <type_traits> #include <type_traits>
#include "../loader/Event.hpp"
// support converting ccColor3B / ccColor4B to / from json // support converting ccColor3B / ccColor4B to / from json
namespace cocos2d { namespace cocos2d {
@ -183,7 +184,7 @@ namespace geode {
} }
} }
// Ref // Ref & Bug
namespace geode { namespace geode {
/** /**
* A smart pointer to a managed CCObject-deriving class. Retains shared * A smart pointer to a managed CCObject-deriving class. Retains shared

View file

@ -71,15 +71,20 @@ namespace geode::utils::file {
* @param dir Directory to unzip the contents to * @param dir Directory to unzip the contents to
*/ */
Result<> extractAllTo(Path const& dir); Result<> extractAllTo(Path const& dir);
};
/** /**
* Unzip file to directory * Helper method for quickly unzipping a file
* @param from File to unzip * @param from ZIP file to unzip
* @param to Directory to unzip to * @param to Directory to unzip to
* @returns Ok on success, Error on error * @param deleteZipAfter Whether to delete the zip after unzipping
* @returns Succesful result on success, errorful result on error
*/ */
GEODE_DLL Result<> unzipTo(ghc::filesystem::path const& from, ghc::filesystem::path const& to); static Result<> intoDir(
Path const& from,
Path const& to,
bool deleteZipAfter = false
);
};
GEODE_DLL bool openFolder(ghc::filesystem::path const& path); GEODE_DLL bool openFolder(ghc::filesystem::path const& path);

View file

@ -2,7 +2,7 @@
#include "Result.hpp" #include "Result.hpp"
#include <Geode/DefaultInclude.hpp> #include "../DefaultInclude.hpp"
#include <chrono> #include <chrono>
#include <iomanip> #include <iomanip>
#include <sstream> #include <sstream>

View file

@ -18,7 +18,6 @@ namespace geode::utils::map {
* beeing looked for, and false if not. * beeing looked for, and false if not.
* @returns True if value matching `containFunc` was found, * @returns True if value matching `containFunc` was found,
* false if not. * false if not.
* @author HJfod
*/ */
template <typename T, typename R> template <typename T, typename R>
bool contains(std::unordered_map<T, R> const& map, std::function<bool(R)> containFunc) { bool contains(std::unordered_map<T, R> const& map, std::function<bool(R)> containFunc) {
@ -38,7 +37,6 @@ namespace geode::utils::map {
* @returns The value matching `selectFunc` if one was found, * @returns The value matching `selectFunc` if one was found,
* otherwise the default value for type R or `nullptr` if R is * otherwise the default value for type R or `nullptr` if R is
* a pointer. * a pointer.
* @author HJfod
*/ */
template <class T, class R> template <class T, class R>
R select(std::unordered_map<T, R> const& map, std::function<bool(R)> selectFunc) { R select(std::unordered_map<T, R> const& map, std::function<bool(R)> selectFunc) {
@ -58,7 +56,6 @@ namespace geode::utils::map {
* return true if the item matches what is * return true if the item matches what is
* beeing looked for, and false if not. * beeing looked for, and false if not.
* @returns Vector of all values that matched. * @returns Vector of all values that matched.
* @author HJfod
*/ */
template <class T, class R> template <class T, class R>
std::vector<R> selectAll( std::vector<R> selectAll(
@ -77,10 +74,9 @@ namespace geode::utils::map {
* Get all values in a map. * Get all values in a map.
* @param map Map to get values from * @param map Map to get values from
* @returns Vector of all values. * @returns Vector of all values.
* @author HJfod
*/ */
template <class T, class R> template <class T, class R>
std::vector<R> getValues(std::unordered_map<T, R> const& map) { std::vector<R> values(std::unordered_map<T, R> const& map) {
std::vector<R> res; std::vector<R> res;
for (auto const& [_, r] : map) { for (auto const& [_, r] : map) {
res.push_back(r); res.push_back(r);
@ -92,10 +88,9 @@ namespace geode::utils::map {
* Get all keys in a map. * Get all keys in a map.
* @param map Map to get keys from * @param map Map to get keys from
* @returns Vector of all keys. * @returns Vector of all keys.
* @author HJfod
*/ */
template <class T, class R> template <class T, class R>
std::vector<T> getKeys(std::unordered_map<T, R> const& map) { std::vector<T> keys(std::unordered_map<T, R> const& map) {
std::vector<T> res; std::vector<T> res;
for (auto const& [t, _] : map) { for (auto const& [t, _] : map) {
res.push_back(t); res.push_back(t);

View file

@ -1,13 +1,14 @@
#include <InternalLoader.hpp> #include <InternalLoader.hpp>
#include <array> #include <array>
USE_GEODE_NAMESPACE();
#include <Geode/modify/LoadingLayer.hpp> #include <Geode/modify/LoadingLayer.hpp>
#include <fmt/format.h> #include <fmt/format.h>
USE_GEODE_NAMESPACE();
struct CustomLoadingLayer : Modify<CustomLoadingLayer, LoadingLayer> { struct CustomLoadingLayer : Modify<CustomLoadingLayer, LoadingLayer> {
bool m_updatingResources; bool m_updatingResources;
EventListener<ResourceDownloadFilter> m_resourceListener;
CustomLoadingLayer() : m_updatingResources(false) {} CustomLoadingLayer() : m_updatingResources(false) {}
@ -27,28 +28,14 @@ struct CustomLoadingLayer : Modify<CustomLoadingLayer, LoadingLayer> {
label->setID("geode-loaded-info"); label->setID("geode-loaded-info");
this->addChild(label); this->addChild(label);
m_fields->m_resourceListener.bind(std::bind(
&CustomLoadingLayer::updateResourcesProgress,
this, std::placeholders::_1
));
// verify loader resources // verify loader resources
if (!InternalLoader::get()->verifyLoaderResources(std::bind( if (!InternalLoader::get()->verifyLoaderResources()) {
&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);
m_fields->m_updatingResources = true; m_fields->m_updatingResources = true;
this->setUpdateText("Downloading Resources"); this->setUpdateText("Downloading Resources");
} }
@ -67,30 +54,28 @@ struct CustomLoadingLayer : Modify<CustomLoadingLayer, LoadingLayer> {
// ); // );
} }
void updateResourcesProgress(UpdateStatus status, std::string const& info, uint8_t progress) { void updateResourcesProgress(ResourceDownloadEvent* event) {
switch (status) { auto status = event->getStatus();
case UpdateStatus::Progress: { if (std::holds_alternative<UpdateProgress>(status)) {
this->setUpdateText("Downloading Resources: " + std::to_string(progress) + "%"); auto prog = std::get<UpdateProgress>(status);
} break; this->setUpdateText("Downloading Resources: " + std::to_string(prog.first) + "%");
}
case UpdateStatus::Finished: { else if (std::holds_alternative<UpdateFinished>(status)) {
this->setUpdateText("Resources Downloaded"); this->setUpdateText("Resources Downloaded");
m_fields->m_updatingResources = false; m_fields->m_updatingResources = false;
this->loadAssets(); this->loadAssets();
} break; }
else {
case UpdateStatus::Failed: {
InternalLoader::platformMessageBox( InternalLoader::platformMessageBox(
"Error updating resources", "Error updating resources",
"Unable to update Geode resources: " + info + "Unable to update Geode resources: " +
".\n" std::get<UpdateError>(status) + ".\n"
"The game will be loaded as normal, but please be aware " "The game will be loaded as normal, but please be aware "
"that it may very likely crash." "that it may very likely crash."
); );
this->setUpdateText("Resource Download Failed"); this->setUpdateText("Resource Download Failed");
m_fields->m_updatingResources = false; m_fields->m_updatingResources = false;
this->loadAssets(); this->loadAssets();
} break;
} }
} }

View file

@ -1,703 +0,0 @@
// #include "Index.hpp"
// #include <Geode/binding/FLAlertLayer.hpp>
// #include <Geode/loader/Loader.hpp>
// #include <Geode/loader/Mod.hpp>
// #include <Geode/loader/Index.hpp>
// #include <Geode/utils/JsonValidation.hpp>
// #include <Geode/utils/web.hpp>
// #include <Geode/utils/file.hpp>
// #include <Geode/utils/general.hpp>
// #include <Geode/external/json/json.hpp>
// #include <Geode/utils/map.hpp>
// #include <Geode/utils/ranges.hpp>
// #include <Geode/utils/string.hpp>
// #include <fmt/format.h>
// #include <hash.hpp>
// #include <thread>
// #define GITHUB_DONT_RATE_LIMIT_ME_PLS 0
// template <class Json = nlohmann::json>
// static Result<Json> 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<IndexItem> Index::getFeaturedItems() const {
// std::vector<IndexItem> 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<int>(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<int>(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<std::unordered_set<std::string>>();
// 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<IndexItem> Index::getItems() const {
// return m_items;
// }
// std::unordered_set<std::string> 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<UninstalledDependency>& 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<std::vector<std::string>> Index::checkDependenciesForItem(IndexItem const& item) {
// // todo: check versions
// std::vector<UninstalledDependency> deps;
// getUninstalledDependenciesRecursive(item.m_info, deps);
// if (deps.size()) {
// std::vector<std::string> 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 += "<cp>" + ud + "</c>, ";
// }
// list.pop_back();
// list.pop_back();
// return Err(
// "This mod or its dependencies <cb>depends</c> on the "
// "following unknown mods: " +
// list +
// ". You will have "
// "to manually install these mods before you can install "
// "this one."
// );
// }
// std::vector<std::string> 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<std::vector<std::string>>({ item.m_info.m_id });
// }
// }
// Result<InstallHandle> Index::installItems(std::vector<IndexItem> const& items) {
// std::vector<std::string> 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<InstallItems>(std::unordered_set(ids.begin(), ids.end()));
// m_installations.insert(ret);
// return Ok(ret);
// }
// Result<InstallHandle> 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<InstallHandle> Index::installAllUpdates() {
// // find items that need updating
// std::vector<IndexItem> itemsToUpdate {};
// for (auto& item : m_items) {
// if (this->isUpdateAvailableForItem(item)) {
// itemsToUpdate.push_back(item);
// }
// }
// return this->installItems(itemsToUpdate);
// }
// std::vector<InstallHandle> Index::getRunningInstallations() const {
// return std::vector<InstallHandle>(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<std::string> 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 <cy>mods</c> have been installed: " +
// ranges::join(m_toInstall, std::string(",")) +
// "\n"
// "Please <cr>restart the game</c> 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<uint8_t>(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();
// }
// }

View file

@ -1,117 +0,0 @@
#pragma once
#include <Geode/utils/web.hpp>
#include <mutex>
#include <optional>
#include <unordered_set>
USE_GEODE_NAMESPACE();
class Index;
struct ModInstallUpdate;
struct InstallItems;
using InstallHandle = std::shared_ptr<InstallItems>;
// todo: make index use events
enum class UpdateStatus {
Progress,
Failed,
Finished,
};
using ItemInstallCallback =
std::function<void(InstallHandle, UpdateStatus, std::string const&, uint8_t)>;
using IndexUpdateCallback = std::function<void(UpdateStatus, std::string const&, uint8_t)>;
struct IndexItem {
struct Download {
std::string m_url;
std::string m_filename;
std::string m_hash;
std::unordered_set<PlatformID> m_platforms;
};
ghc::filesystem::path m_path;
ModInfo m_info;
Download m_download;
std::unordered_set<std::string> m_categories;
};
struct InstallItems final : public std::enable_shared_from_this<InstallItems> {
public:
using CallbackID = size_t;
private:
bool m_started = false;
std::unordered_set<std::string> m_toInstall;
std::vector<web::SentAsyncWebRequestHandle> m_handles;
std::unordered_map<CallbackID, ItemInstallCallback> m_callbacks;
std::vector<ghc::filesystem::path> 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<std::string> toInstall() const;
inline InstallItems(std::unordered_set<std::string> 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<IndexItem> m_items;
std::unordered_set<InstallHandle> m_installations;
mutable std::mutex m_ticketsMutex;
std::unordered_set<std::string> m_featured;
std::unordered_set<std::string> m_categories;
std::unordered_set<std::string> m_updated;
void addIndexItemFromFolder(ghc::filesystem::path const& dir);
Result<> updateIndexFromLocalCache();
Result<std::vector<std::string>> checkDependenciesForItem(IndexItem const& item);
friend struct InstallItems;
public:
static Index* get();
std::vector<IndexItem> getItems() const;
bool isKnownItem(std::string const& id) const;
IndexItem getKnownItem(std::string const& id) const;
std::unordered_set<std::string> getCategories() const;
std::vector<IndexItem> getFeaturedItems() const;
bool isFeaturedItem(std::string const& item) const;
Result<InstallHandle> installItems(std::vector<IndexItem> const& item);
Result<InstallHandle> installItem(IndexItem const& item);
std::vector<InstallHandle> 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<InstallHandle> installAllUpdates();
bool isIndexUpdated() const;
void updateIndex(IndexUpdateCallback callback, bool force = false);
};

View file

@ -16,6 +16,16 @@
#include <thread> #include <thread>
#include <vector> #include <vector>
ListenerResult ResourceDownloadFilter::handle(
std::function<Callback> fn,
ResourceDownloadEvent* event
) {
fn(event);
return ListenerResult::Propagate;
}
ResourceDownloadFilter::ResourceDownloadFilter() {}
InternalLoader::InternalLoader() : Loader() {} InternalLoader::InternalLoader() : Loader() {}
InternalLoader::~InternalLoader() { InternalLoader::~InternalLoader() {
@ -93,7 +103,7 @@ void InternalLoader::loadInfoAlerts(nlohmann::json& json) {
m_shownInfoAlerts = json["alerts"].get<std::unordered_set<std::string>>(); m_shownInfoAlerts = json["alerts"].get<std::unordered_set<std::string>>();
} }
void InternalLoader::downloadLoaderResources(IndexUpdateCallback callback) { void InternalLoader::downloadLoaderResources() {
auto version = this->getVersion().toString(); auto version = this->getVersion().toString();
auto tempResourcesZip = dirs::getTempDir() / "new.zip"; auto tempResourcesZip = dirs::getTempDir() / "new.zip";
auto resourcesDir = dirs::getGeodeResourcesDir() / InternalMod::get()->getID(); 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 "https://github.com/geode-sdk/geode/releases/download/{}/resources.zip", version
)) ))
.into(tempResourcesZip) .into(tempResourcesZip)
.then([tempResourcesZip, resourcesDir, callback](auto) { .then([tempResourcesZip, resourcesDir](auto) {
// unzip resources zip // unzip resources zip
auto unzip = file::unzipTo(tempResourcesZip, resourcesDir); auto unzip = file::Unzip::intoDir(tempResourcesZip, resourcesDir, true);
if (!unzip) { if (!unzip) {
if (callback) return ResourceDownloadEvent(
callback( UpdateError("Unable to unzip new resources: " + unzip.unwrapErr())
UpdateStatus::Failed, "Unable to unzip new resources: " + unzip.unwrapErr(), 0 ).post();
);
return;
} }
// delete resources zip ResourceDownloadEvent(UpdateFinished()).post();
try {
ghc::filesystem::remove(tempResourcesZip);
}
catch (...) {
}
if (callback) callback(UpdateStatus::Finished, "Resources updated", 100);
}) })
.expect([callback](std::string const& info) { .expect([](std::string const& info) {
if (callback) callback(UpdateStatus::Failed, info, 0); ResourceDownloadEvent(
UpdateError("Unable to download resources: " + info)
).post();
}) })
.progress([callback](auto&, double now, double total) { .progress([](auto&, double now, double total) {
if (callback) ResourceDownloadEvent(
callback( UpdateProgress(
UpdateStatus::Progress, "Downloading resources", static_cast<uint8_t>(now / total * 100.0),
static_cast<uint8_t>(now / total * 100.0) "Downloading resources"
); )
).post();
}); });
} }
bool InternalLoader::verifyLoaderResources(IndexUpdateCallback callback) { bool InternalLoader::verifyLoaderResources() {
static std::optional<bool> CACHED = std::nullopt; static std::optional<bool> CACHED = std::nullopt;
if (CACHED.has_value()) { if (CACHED.has_value()) {
return CACHED.value(); return CACHED.value();
@ -145,8 +149,11 @@ bool InternalLoader::verifyLoaderResources(IndexUpdateCallback callback) {
auto resourcesDir = dirs::getGeodeResourcesDir() / InternalMod::get()->getID(); auto resourcesDir = dirs::getGeodeResourcesDir() / InternalMod::get()->getID();
// if the resources dir doesn't exist, then it's probably incorrect // if the resources dir doesn't exist, then it's probably incorrect
if (!(ghc::filesystem::exists(resourcesDir) && ghc::filesystem::is_directory(resourcesDir))) { if (!(
this->downloadLoaderResources(callback); ghc::filesystem::exists(resourcesDir) &&
ghc::filesystem::is_directory(resourcesDir)
)) {
this->downloadLoaderResources();
return false; return false;
} }
@ -166,7 +173,7 @@ bool InternalLoader::verifyLoaderResources(IndexUpdateCallback callback) {
log::debug( log::debug(
"compare {} {} {}", file.path().string(), hash, LOADER_RESOURCE_HASHES.at(name) "compare {} {} {}", file.path().string(), hash, LOADER_RESOURCE_HASHES.at(name)
); );
this->downloadLoaderResources(callback); this->downloadLoaderResources();
return false; return false;
} }
coverage += 1; coverage += 1;
@ -174,7 +181,7 @@ bool InternalLoader::verifyLoaderResources(IndexUpdateCallback callback) {
// make sure every file was found // make sure every file was found
if (coverage != LOADER_RESOURCE_HASHES.size()) { if (coverage != LOADER_RESOURCE_HASHES.size()) {
this->downloadLoaderResources(callback); this->downloadLoaderResources();
return false; return false;
} }

View file

@ -1,8 +1,8 @@
#pragma once #pragma once
#include "../index/Index.hpp"
#include "FileWatcher.hpp" #include "FileWatcher.hpp"
#include <Geode/loader/Index.hpp>
#include <Geode/loader/Loader.hpp> #include <Geode/loader/Loader.hpp>
#include <Geode/loader/Log.hpp> #include <Geode/loader/Log.hpp>
#include <Geode/utils/Result.hpp> #include <Geode/utils/Result.hpp>
@ -15,6 +15,23 @@
USE_GEODE_NAMESPACE(); USE_GEODE_NAMESPACE();
class ResourceDownloadEvent : public Event {
protected:
UpdateStatus m_status;
public:
ResourceDownloadEvent(UpdateStatus status);
UpdateStatus getStatus() const;
};
class GEODE_DLL ResourceDownloadFilter : public EventFilter<ResourceDownloadEvent> {
public:
using Callback = void(ResourceDownloadEvent*);
ListenerResult handle(std::function<Callback> fn, ResourceDownloadEvent* event);
ResourceDownloadFilter();
};
/** /**
* Internal extension of Loader for private information * Internal extension of Loader for private information
* @class InternalLoader * @class InternalLoader
@ -29,7 +46,7 @@ protected:
void saveInfoAlerts(nlohmann::json& json); void saveInfoAlerts(nlohmann::json& json);
void loadInfoAlerts(nlohmann::json& json); void loadInfoAlerts(nlohmann::json& json);
void downloadLoaderResources(IndexUpdateCallback callback); void downloadLoaderResources();
bool loadHooks(); bool loadHooks();
void setupIPC(); void setupIPC();
@ -66,7 +83,7 @@ public:
void closePlatformConsole(); void closePlatformConsole();
static void platformMessageBox(char const* title, std::string const& info); static void platformMessageBox(char const* title, std::string const& info);
bool verifyLoaderResources(IndexUpdateCallback callback); bool verifyLoaderResources();
friend int geodeEntry(void* platformData); friend int geodeEntry(void* platformData);
}; };

256
loader/src/loader/Index.cpp Normal file
View file

@ -0,0 +1,256 @@
#include <Geode/loader/Index.hpp>
#include <Geode/loader/Loader.hpp>
#include <Geode/loader/Dirs.hpp>
#include <Geode/utils/ranges.hpp>
#include <Geode/utils/web.hpp>
#include <Geode/utils/string.hpp>
#include <Geode/utils/map.hpp>
USE_GEODE_NAMESPACE();
struct IndexSourceSaveData {
std::string downloadedCommitSHA;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IndexSourceSaveData, downloadedCommitSHA);
struct IndexSaveData {
std::unordered_map<std::string, IndexSourceSaveData> 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<Callback> 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<IndexSource> 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<IndexSaveData>("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<IndexSaveData>("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<uint8_t>(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<IndexItem> Index::getItems() const {
std::vector<IndexItem> 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<size_t> 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<IndexItem> Index::getItem(
std::string const& id,
std::optional<size_t> 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;
}

View file

@ -275,7 +275,7 @@ Mod* Loader::getLoadedMod(std::string const& id) const {
} }
std::vector<Mod*> Loader::getAllMods() { std::vector<Mod*> Loader::getAllMods() {
return map::getValues(m_mods); return map::values(m_mods);
} }
Mod* Loader::getInternalMod() { Mod* Loader::getInternalMod() {

View file

@ -1,4 +1,5 @@
#include "../index/Index.hpp"
#include <Geode/loader/Index.hpp>
#include "info/ModInfoLayer.hpp" #include "info/ModInfoLayer.hpp"
#include "list/ModListLayer.hpp" #include "list/ModListLayer.hpp"
#include "settings/ModSettingsPopup.hpp" #include "settings/ModSettingsPopup.hpp"
@ -15,8 +16,9 @@ void geode::openInfoPopup(Mod* mod) {
void geode::openIndexPopup(Mod* mod) { void geode::openIndexPopup(Mod* mod) {
if (Index::get()->isKnownItem(mod->getID())) { if (Index::get()->isKnownItem(mod->getID())) {
ModInfoLayer::create(new ModObject(Index::get()->getKnownItem(mod->getID())), nullptr) ModInfoLayer::create(
->show(); new ModObject(Index::get()->getKnownItem(mod->getID()))
)->show();
} }
} }

View file

@ -4,7 +4,7 @@
#include <Geode/binding/FLAlertLayerProtocol.hpp> #include <Geode/binding/FLAlertLayerProtocol.hpp>
#include <Geode/ui/MDTextArea.hpp> #include <Geode/ui/MDTextArea.hpp>
#include <Geode/ui/Scrollbar.hpp> #include <Geode/ui/Scrollbar.hpp>
#include <Index.hpp> #include <Geode/loader/Index.hpp>
USE_GEODE_NAMESPACE(); USE_GEODE_NAMESPACE();

View file

@ -3,7 +3,7 @@
#include "ModListView.hpp" #include "ModListView.hpp"
#include <Geode/binding/TextInputDelegate.hpp> #include <Geode/binding/TextInputDelegate.hpp>
#include <Index.hpp> #include <Geode/loader/Index.hpp>
USE_GEODE_NAMESPACE(); USE_GEODE_NAMESPACE();

View file

@ -3,7 +3,8 @@
#include <Geode/binding/CustomListView.hpp> #include <Geode/binding/CustomListView.hpp>
#include <Geode/binding/FLAlertLayerProtocol.hpp> #include <Geode/binding/FLAlertLayerProtocol.hpp>
#include <Geode/binding/TableViewCell.hpp> #include <Geode/binding/TableViewCell.hpp>
#include <Index.hpp> #include <Geode/loader/Index.hpp>
#include <Geode/loader/Loader.hpp>
#include <optional> #include <optional>
USE_GEODE_NAMESPACE(); USE_GEODE_NAMESPACE();
@ -18,7 +19,7 @@ enum class ModListType {
enum class ModObjectType { enum class ModObjectType {
Mod, Mod,
Unloaded, Invalid,
Index, Index,
}; };
@ -27,22 +28,25 @@ class ModListLayer;
// Wrapper so you can pass Mods in a CCArray // Wrapper so you can pass Mods in a CCArray
struct ModObject : public CCObject { struct ModObject : public CCObject {
ModObjectType m_type; ModObjectType m_type;
Mod* m_mod; std::variant<Mod*, InvalidGeodeFile, IndexItem> m_data;
InvalidGeodeFile m_info;
IndexItem m_index;
inline ModObject(Mod* mod) : m_mod(mod), m_type(ModObjectType::Mod) { inline ModObject(Mod* mod)
: m_data(mod), m_type(ModObjectType::Mod)
{
this->autorelease(); this->autorelease();
}; }
inline ModObject(InvalidGeodeFile const& info) : inline ModObject(InvalidGeodeFile const& info)
m_info(info), m_type(ModObjectType::Unloaded) { : m_data(info), m_type(ModObjectType::Invalid)
{
this->autorelease(); 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(); this->autorelease();
}; }
}; };
class ModListView; class ModListView;

View file

@ -0,0 +1,22 @@
#include <Geode/platform/platform.hpp>
#include <Geode/utils/general.hpp>
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());
}

View file

@ -116,11 +116,6 @@ Result<std::vector<std::string>> utils::file::listFilesRecursively(std::string c
return Ok(res); 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; static constexpr auto MAX_ENTRY_PATH_LEN = 256;
struct ZipEntry { struct ZipEntry {
@ -244,7 +239,7 @@ ghc::filesystem::path Unzip::getPath() const {
} }
std::vector<ghc::filesystem::path> Unzip::getEntries() const { std::vector<ghc::filesystem::path> Unzip::getEntries() const {
return map::getKeys(m_impl->entries()); return map::keys(m_impl->entries());
} }
bool Unzip::hasEntry(Path const& name) { bool Unzip::hasEntry(Path const& name) {
@ -268,3 +263,16 @@ Result<> Unzip::extractAllTo(Path const& dir) {
} }
return Ok(); 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();
}