mirror of
https://github.com/geode-sdk/geode.git
synced 2024-11-14 19:15:05 -05:00
implement proper mod update checking
This commit is contained in:
parent
269ba9002a
commit
fc7d3343d9
16 changed files with 526 additions and 99 deletions
|
@ -149,6 +149,7 @@ namespace geode {
|
|||
int m_size;
|
||||
int m_color;
|
||||
cocos2d::CCNode* m_onTop = nullptr;
|
||||
float m_onTopRelativeScale = 1.f;
|
||||
cocos2d::CCPoint m_topOffset = cocos2d::CCPointZero;
|
||||
|
||||
bool init(cocos2d::CCNode* ontop, BaseType type, int size, int color);
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#include "Result.hpp"
|
||||
#include "MiniFunction.hpp"
|
||||
#include "../loader/Event.hpp"
|
||||
#include "ranges.hpp"
|
||||
|
||||
namespace geode {
|
||||
namespace impl {
|
||||
|
@ -254,6 +255,10 @@ namespace geode {
|
|||
);
|
||||
}
|
||||
|
||||
Promise forward() {
|
||||
return make_fwd<T, E, P>([](auto state) { return std::move(state); }, m_data);
|
||||
}
|
||||
|
||||
void resolve(Value&& value) {
|
||||
invoke_callback(State::make_value(std::move(value)), m_data);
|
||||
}
|
||||
|
@ -274,6 +279,73 @@ namespace geode {
|
|||
*/
|
||||
PromiseEventFilter<T, E, P> listen();
|
||||
|
||||
// UNFINISHED!!
|
||||
// I'm pretty sure this has a memory leak somewhere in it too
|
||||
// static Promise<std::vector<T>, E, P> all(std::vector<Promise>&& promises, bool own = true, bool threaded = true) {
|
||||
// return Promise<std::vector<T>, E, P>([own, promises = std::move(promises)](auto resolve, auto reject, auto progress, auto const& cancelled) {
|
||||
// struct All final {
|
||||
// std::vector<T> results;
|
||||
// std::vector<Promise> promises;
|
||||
// };
|
||||
// auto all = std::make_shared<All>(All {
|
||||
// .results = {},
|
||||
// .promises = std::move(promises),
|
||||
// });
|
||||
// for (auto& promise : all->promises) {
|
||||
// // SAFETY: all of the accesses to `all` are safe since the Promise
|
||||
// // callbacks are guaranteed to run in the same thread
|
||||
// promise
|
||||
// // Wait for all of them to finish
|
||||
// .then([all, resolve](auto result) {
|
||||
// all->results.push_back(result);
|
||||
// if (all->results.size() >= all->promises.size()) {
|
||||
// resolve(all->results);
|
||||
// all->promises.clear();
|
||||
// all->results.clear();
|
||||
// }
|
||||
// })
|
||||
// // If some Promise fails, the whole `all` fails
|
||||
// .expect([own, all, reject](auto error) {
|
||||
// // Only cancel contained Promises if the `all` is considered to be
|
||||
// // owning them, since cancelling shared Promises could have bad
|
||||
// // consequences
|
||||
// if (own) {
|
||||
// for (auto& promise : all->promises) {
|
||||
// promise.cancel();
|
||||
// }
|
||||
// }
|
||||
// all->promises.clear();
|
||||
// all->results.clear();
|
||||
// reject(error);
|
||||
// })
|
||||
// // Check if the `Promise::all` has been cancelled
|
||||
// .progress([&cancelled, own, all, progress](auto prog) {
|
||||
// if (cancelled) {
|
||||
// // Only cancel contained Promises if the `all` is considered to be
|
||||
// // owning them, since cancelling shared Promises could have bad
|
||||
// // consequences
|
||||
// if (own) {
|
||||
// for (auto& promise : all->promises) {
|
||||
// promise.cancel();
|
||||
// }
|
||||
// }
|
||||
// all->promises.clear();
|
||||
// all->results.clear();
|
||||
// }
|
||||
// else {
|
||||
// progress(prog);
|
||||
// }
|
||||
// })
|
||||
// // Remove cancelled promises from the list
|
||||
// .cancelled([promise, all] {
|
||||
// utils::ranges::remove(all->promises, [promise](auto other) {
|
||||
// return other.m_data == promise.m_data;
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
// }, threaded);
|
||||
// }
|
||||
|
||||
private:
|
||||
// I'm not sure just how un-performant this is, although then again you
|
||||
// should not be using Promises in performance-sensitive code since the
|
||||
|
|
BIN
loader/resources/update.png
Normal file
BIN
loader/resources/update.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
|
@ -151,7 +151,27 @@ struct CustomMenuLayer : Modify<CustomMenuLayer, MenuLayer> {
|
|||
popup->show();
|
||||
}
|
||||
|
||||
this->addUpdateIndicator();
|
||||
// Check for mod updates
|
||||
ModsLayer::checkInstalledModsForUpdates()
|
||||
.then([this](std::vector<std::string> updatesFound) {
|
||||
if (updatesFound.size() && !m_fields->m_geodeButton->getChildByID("updates-available")) {
|
||||
log::info("Found updates for mods: {}!", updatesFound);
|
||||
auto icon = CCSprite::createWithSpriteFrameName("updates-available.png"_spr);
|
||||
icon->setPosition(
|
||||
m_fields->m_geodeButton->getContentSize() - CCSize { 10.f, 10.f }
|
||||
);
|
||||
icon->setID("updates-available");
|
||||
icon->setZOrder(99);
|
||||
icon->setScale(.5f);
|
||||
m_fields->m_geodeButton->addChild(icon);
|
||||
}
|
||||
else {
|
||||
log::error("No updates found :(");
|
||||
}
|
||||
})
|
||||
.expect([](server::ServerError error) {
|
||||
log::error("Unable to check for mod updates ({}): {}", error.code, error.details);
|
||||
});
|
||||
|
||||
for (auto mod : Loader::get()->getAllMods()) {
|
||||
if (mod->getMetadata().usesDeprecatedIDForm()) {
|
||||
|
@ -224,20 +244,6 @@ struct CustomMenuLayer : Modify<CustomMenuLayer, MenuLayer> {
|
|||
}
|
||||
}
|
||||
|
||||
void addUpdateIndicator() {
|
||||
#pragma message("todo")
|
||||
// todo: bring back
|
||||
// if (!m_fields->m_menuDisabled && Index::get()->areUpdatesAvailable()) {
|
||||
// auto icon = CCSprite::createWithSpriteFrameName("updates-available.png"_spr);
|
||||
// icon->setPosition(
|
||||
// m_fields->m_geodeButton->getContentSize() - CCSize { 10.f, 10.f }
|
||||
// );
|
||||
// icon->setZOrder(99);
|
||||
// icon->setScale(.5f);
|
||||
// m_fields->m_geodeButton->addChild(icon);
|
||||
// }
|
||||
}
|
||||
|
||||
void onMissingTextures(CCObject*) {
|
||||
|
||||
#ifdef GEODE_IS_DESKTOP
|
||||
|
|
|
@ -184,6 +184,7 @@ Result<ServerModVersion> ServerModVersion::parse(matjson::Value const& raw) {
|
|||
|
||||
incompatibilities.push_back(incompatibility);
|
||||
}
|
||||
res.metadata.setIncompatibilities(incompatibilities);
|
||||
|
||||
// Check for errors and return result
|
||||
if (root.isError()) {
|
||||
|
@ -192,6 +193,53 @@ Result<ServerModVersion> ServerModVersion::parse(matjson::Value const& raw) {
|
|||
return Ok(res);
|
||||
}
|
||||
|
||||
Result<ServerModUpdate> ServerModUpdate::parse(matjson::Value const& raw) {
|
||||
auto json = raw;
|
||||
JsonChecker checker(json);
|
||||
auto root = checker.root("ServerModUpdate").obj();
|
||||
|
||||
auto res = ServerModUpdate();
|
||||
|
||||
root.needs("id").into(res.id);
|
||||
root.needs("version").into(res.version);
|
||||
|
||||
// Check for errors and return result
|
||||
if (root.isError()) {
|
||||
return Err(root.getError());
|
||||
}
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
Result<std::vector<ServerModUpdate>> ServerModUpdate::parseList(matjson::Value const& raw) {
|
||||
auto json = raw;
|
||||
JsonChecker checker(json);
|
||||
auto payload = checker.root("ServerModUpdatesList").array();
|
||||
|
||||
std::vector<ServerModUpdate> list {};
|
||||
for (auto item : payload.iterate()) {
|
||||
auto mod = ServerModUpdate::parse(item.json());
|
||||
if (mod) {
|
||||
list.push_back(mod.unwrap());
|
||||
}
|
||||
else {
|
||||
log::error("Unable to parse mod update from the server: {}", mod.unwrapErr());
|
||||
}
|
||||
}
|
||||
|
||||
// Check for errors and return result
|
||||
if (payload.isError()) {
|
||||
return Err(payload.getError());
|
||||
}
|
||||
return Ok(list);
|
||||
}
|
||||
|
||||
bool ServerModUpdate::hasUpdateForInstalledMod() const {
|
||||
if (auto mod = Loader::get()->getLoadedMod(this->id)) {
|
||||
return mod->getVersion() < this->version;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Result<ServerModMetadata> ServerModMetadata::parse(matjson::Value const& raw) {
|
||||
auto json = raw;
|
||||
JsonChecker checker(json);
|
||||
|
@ -456,3 +504,39 @@ ServerPromise<std::unordered_set<std::string>> server::getTags(std::monostate) {
|
|||
return parseServerProgress(prog, "Downloading valid tags");
|
||||
});
|
||||
}
|
||||
|
||||
ServerPromise<std::vector<ServerModUpdate>> server::checkUpdates(std::vector<std::string> const& modIDs) {
|
||||
auto req = web::WebRequest();
|
||||
req.userAgent(getServerUserAgent());
|
||||
req.param("platform", GEODE_PLATFORM_SHORT_IDENTIFIER);
|
||||
if (modIDs.size()) {
|
||||
req.param("ids", ranges::join(modIDs, ";"));
|
||||
}
|
||||
return req.get(getServerAPIBaseURL() + "/mods/updates")
|
||||
.then<std::vector<ServerModUpdate>, ServerError>([](auto result) -> Result<std::vector<ServerModUpdate>, ServerError> {
|
||||
if (result) {
|
||||
auto value = result.unwrap();
|
||||
|
||||
// Store the code, since the value is moved afterwards
|
||||
auto code = value.code();
|
||||
|
||||
// Parse payload
|
||||
auto payload = parseServerPayload(std::move(value));
|
||||
if (!payload) {
|
||||
return Err(payload.unwrapErr());
|
||||
}
|
||||
// Parse response
|
||||
auto list = ServerModUpdate::parseList(payload.unwrap());
|
||||
if (!list) {
|
||||
return Err(ServerError(code, "Unable to parse response: {}", list.unwrapErr()));
|
||||
}
|
||||
return Ok(list.unwrap());
|
||||
}
|
||||
else {
|
||||
return Err(parseServerError(result.unwrapErr()));
|
||||
}
|
||||
})
|
||||
.progress<ServerProgress>([](auto prog) {
|
||||
return parseServerProgress(prog, "Checking updates for mods");
|
||||
});
|
||||
}
|
||||
|
|
|
@ -19,12 +19,12 @@ namespace server {
|
|||
static Result<ServerDateTime> parse(std::string const& str);
|
||||
};
|
||||
|
||||
struct ServerDeveloper {
|
||||
struct ServerDeveloper final {
|
||||
std::string username;
|
||||
std::string displayName;
|
||||
};
|
||||
|
||||
struct ServerModVersion {
|
||||
struct ServerModVersion final {
|
||||
ModMetadata metadata;
|
||||
std::string downloadURL;
|
||||
std::string hash;
|
||||
|
@ -33,7 +33,17 @@ namespace server {
|
|||
static Result<ServerModVersion> parse(matjson::Value const& json);
|
||||
};
|
||||
|
||||
struct ServerModMetadata {
|
||||
struct ServerModUpdate final {
|
||||
std::string id;
|
||||
VersionInfo version;
|
||||
|
||||
static Result<ServerModUpdate> parse(matjson::Value const& json);
|
||||
static Result<std::vector<ServerModUpdate>> parseList(matjson::Value const& json);
|
||||
|
||||
bool hasUpdateForInstalledMod() const;
|
||||
};
|
||||
|
||||
struct ServerModMetadata final {
|
||||
std::string id;
|
||||
bool featured;
|
||||
size_t downloadCount;
|
||||
|
@ -52,7 +62,7 @@ namespace server {
|
|||
bool hasUpdateForInstalledMod() const;
|
||||
};
|
||||
|
||||
struct ServerModsList {
|
||||
struct ServerModsList final {
|
||||
std::vector<ServerModMetadata> mods;
|
||||
size_t totalModCount = 0;
|
||||
|
||||
|
@ -67,7 +77,7 @@ namespace server {
|
|||
|
||||
static const char* sortToString(ModsSort sorting);
|
||||
|
||||
struct ModsQuery {
|
||||
struct ModsQuery final {
|
||||
std::optional<std::string> query;
|
||||
std::unordered_set<PlatformID> platforms = { GEODE_PLATFORM_TARGET };
|
||||
std::unordered_set<std::string> tags;
|
||||
|
@ -80,7 +90,7 @@ namespace server {
|
|||
bool operator==(ModsQuery const& other) const = default;
|
||||
};
|
||||
|
||||
struct ServerError {
|
||||
struct ServerError final {
|
||||
int code;
|
||||
std::string details;
|
||||
|
||||
|
@ -103,30 +113,30 @@ namespace server {
|
|||
ServerPromise<ServerModMetadata> getMod(std::string const& id);
|
||||
ServerPromise<ByteVector> getModLogo(std::string const& id);
|
||||
ServerPromise<std::unordered_set<std::string>> getTags(std::monostate = std::monostate());
|
||||
ServerPromise<std::vector<ServerModUpdate>> checkUpdates(std::vector<std::string> const& modIDs);
|
||||
|
||||
// ^^ Note: Any funcs with `std::monostate` parameter is because ServerResultCache expects a single query arg
|
||||
|
||||
// Caching for server endpoints
|
||||
namespace detail {
|
||||
template <class R, class Q>
|
||||
struct ExtractServerReqParams {
|
||||
struct ExtractServerReqParams final {
|
||||
using Result = R;
|
||||
using Query = Q;
|
||||
ExtractServerReqParams(ServerPromise<R>(*)(Q)) {}
|
||||
ExtractServerReqParams(ServerPromise<R>(*)(Q const&)) {}
|
||||
};
|
||||
}
|
||||
|
||||
template <auto F>
|
||||
class ServerResultCache final {
|
||||
public:
|
||||
using Extract = decltype(detail::ExtractServerReqParams(F));
|
||||
using Result = typename Extract::Result;
|
||||
using Query = typename Extract::Query;
|
||||
static std::optional<ServerModUpdate> mapModUpdateList(
|
||||
std::string const& modID,
|
||||
std::vector<ServerModUpdate>&& result
|
||||
) {
|
||||
return ranges::find(result, [modID](auto v) { return v.id == modID; });
|
||||
}
|
||||
|
||||
private:
|
||||
class Cache final {
|
||||
std::mutex m_mutex;
|
||||
template <class K, class V>
|
||||
class CacheMap final {
|
||||
private:
|
||||
// I know this looks like a goofy choice over just
|
||||
// `std::unordered_map`, but hear me out:
|
||||
//
|
||||
|
@ -149,39 +159,68 @@ namespace server {
|
|||
// lightning-fast (🚀), and besides the main performance benefit
|
||||
// comes from the lack of a web request - not how many extra
|
||||
// milliseconds we can squeeze out of a map access
|
||||
std::vector<std::pair<Query, ServerPromise<Result>>> m_values;
|
||||
std::vector<std::pair<K, V>> m_values;
|
||||
size_t m_sizeLimit = 20;
|
||||
|
||||
public:
|
||||
ServerPromise<Result> get(Query const& query) {
|
||||
std::unique_lock _(m_mutex);
|
||||
|
||||
auto it = std::find_if(m_values.begin(), m_values.end(), [query](auto const& q) {
|
||||
return q.first == query;
|
||||
std::optional<V> get(K const& key) {
|
||||
auto it = std::find_if(m_values.begin(), m_values.end(), [key](auto const& q) {
|
||||
return q.first == key;
|
||||
});
|
||||
|
||||
// If there is already a Promise with this queue (pending or
|
||||
// resolved) just return a copy of that to the caller so they
|
||||
// can listen to it
|
||||
if (it != m_values.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
// Create the Promise
|
||||
auto promise = F(Query(query));
|
||||
|
||||
// Store the created Promise in the cache
|
||||
auto value = std::make_pair(std::move(query), ServerPromise<Result>(promise));
|
||||
return std::nullopt;
|
||||
}
|
||||
void add(K&& key, V&& value) {
|
||||
auto pair = std::make_pair(std::move(key), std::move(value));
|
||||
|
||||
// Shift and replace last element if we're at cache size limit
|
||||
if (m_values.size() >= m_sizeLimit) {
|
||||
std::shift_left(m_values.begin(), m_values.end(), 1);
|
||||
m_values.back() = std::move(value);
|
||||
m_values.back() = std::move(pair);
|
||||
}
|
||||
// Otherwise append at end
|
||||
else {
|
||||
m_values.emplace_back(std::move(value));
|
||||
m_values.emplace_back(std::move(pair));
|
||||
}
|
||||
}
|
||||
void remove(K const& key) {
|
||||
ranges::remove(m_values, [&key](auto const& q) { return q.first == key; });
|
||||
}
|
||||
void clear() {
|
||||
m_values.clear();
|
||||
}
|
||||
void limit(size_t size) {
|
||||
m_sizeLimit = size;
|
||||
m_values.clear();
|
||||
}
|
||||
size_t limit() const {
|
||||
return m_sizeLimit;
|
||||
}
|
||||
};
|
||||
|
||||
template <class Query, class Result>
|
||||
class DefaultCache final {
|
||||
std::mutex m_mutex;
|
||||
CacheMap<Query, ServerPromise<Result>> m_cache;
|
||||
|
||||
public:
|
||||
ServerPromise<Result> get(auto func, Query const& query) {
|
||||
std::unique_lock _(m_mutex);
|
||||
|
||||
// If there is already a Promise with this queue (pending or
|
||||
// resolved) just return a copy of that to the caller so they
|
||||
// can listen to it
|
||||
if (auto old = m_cache.get(query)) {
|
||||
return *old;
|
||||
}
|
||||
|
||||
// Create the Promise
|
||||
auto promise = func(Query(query));
|
||||
|
||||
// Store the created Promise in the cache
|
||||
m_cache.add(Query(query), ServerPromise<Result>(promise));
|
||||
|
||||
// Give a copy of the created Promise to the caller so they
|
||||
// can listen to it
|
||||
|
@ -189,20 +228,134 @@ namespace server {
|
|||
}
|
||||
void clear(Query const& query) {
|
||||
std::unique_lock _(m_mutex);
|
||||
ranges::remove(m_values, [&query](auto const& q) { return q.first == query; });
|
||||
m_cache.remove(query);
|
||||
}
|
||||
void clear() {
|
||||
std::unique_lock _(m_mutex);
|
||||
m_values.clear();
|
||||
m_cache.clear();
|
||||
}
|
||||
// @warning also clears the cache
|
||||
void limit(size_t size) {
|
||||
std::unique_lock _(m_mutex);
|
||||
m_sizeLimit = size;
|
||||
m_values.clear();
|
||||
m_cache.limit(size);
|
||||
}
|
||||
};
|
||||
|
||||
class ModUpdatesCache final {
|
||||
public:
|
||||
using UpdatePromise = ServerPromise<std::optional<ServerModUpdate>>;
|
||||
|
||||
private:
|
||||
std::mutex m_mutex;
|
||||
CacheMap<std::string, UpdatePromise> m_cache;
|
||||
|
||||
public:
|
||||
ServerPromise<std::vector<ServerModUpdate>> get(auto, std::vector<std::string> const& modIDs) {
|
||||
std::unique_lock _(m_mutex);
|
||||
|
||||
// Limit cache size to installed mods count
|
||||
if (m_cache.limit() != Loader::get()->getAllMods().size()) {
|
||||
m_cache.limit(Loader::get()->getAllMods().size());
|
||||
}
|
||||
|
||||
std::vector<UpdatePromise> promises;
|
||||
std::vector<std::string> idsNotFoundInCache {};
|
||||
for (auto const& modID : modIDs) {
|
||||
if (auto old = m_cache.get(modID)) {
|
||||
promises.push_back(*old);
|
||||
}
|
||||
else {
|
||||
idsNotFoundInCache.push_back(modID);
|
||||
}
|
||||
}
|
||||
|
||||
if (idsNotFoundInCache.size()) {
|
||||
// Check updates for all IDs not hit in cache
|
||||
auto promise = checkUpdates(idsNotFoundInCache);
|
||||
for (auto modID : std::move(idsNotFoundInCache)) {
|
||||
auto individual = promise.then<std::optional<ServerModUpdate>>([modID](auto result) {
|
||||
return detail::mapModUpdateList(modID, std::move(result));
|
||||
});
|
||||
m_cache.add(std::move(modID), UpdatePromise(individual));
|
||||
promises.push_back(individual);
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for all the promises to resolve
|
||||
return ServerPromise<std::vector<ServerModUpdate>>([promises = std::move(promises)](auto resolve, auto) {
|
||||
struct Data final {
|
||||
std::vector<ServerModUpdate> results;
|
||||
size_t waitingFor;
|
||||
};
|
||||
std::shared_ptr<Data> data = std::make_shared<Data>();
|
||||
data->waitingFor = promises.size();
|
||||
for (auto promise : promises) {
|
||||
promise.then([data, resolve](auto update) {
|
||||
if (data->waitingFor > 0) {
|
||||
if (update) {
|
||||
data->results.emplace_back(std::move(std::move(update).value()));
|
||||
}
|
||||
data->waitingFor -= 1;
|
||||
if (data->waitingFor == 0) {
|
||||
resolve(std::move(data->results));
|
||||
}
|
||||
}
|
||||
}).expect([data, resolve](auto) {
|
||||
if (data->waitingFor > 0) {
|
||||
data->waitingFor -= 1;
|
||||
if (data->waitingFor == 0) {
|
||||
resolve(std::move(data->results));
|
||||
}
|
||||
}
|
||||
}).cancelled([data, resolve] {
|
||||
if (data->waitingFor > 0) {
|
||||
data->waitingFor -= 1;
|
||||
if (data->waitingFor == 0) {
|
||||
resolve(std::move(data->results));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
void clear(std::vector<std::string> const& modIDs) {
|
||||
std::unique_lock _(m_mutex);
|
||||
for (auto const& modID : modIDs) {
|
||||
m_cache.remove(modID);
|
||||
}
|
||||
}
|
||||
void clear() {
|
||||
std::unique_lock _(m_mutex);
|
||||
m_cache.clear();
|
||||
}
|
||||
void limit(size_t) {
|
||||
// cache limit is automatically and always set to installed mods count
|
||||
}
|
||||
};
|
||||
|
||||
template <auto F>
|
||||
struct CacheFor final {
|
||||
using Extract = decltype(detail::ExtractServerReqParams(F));
|
||||
using Result = typename Extract::Result;
|
||||
using Query = typename Extract::Query;
|
||||
using Cache = DefaultCache<Query, Result>;
|
||||
};
|
||||
|
||||
template <>
|
||||
struct CacheFor<&checkUpdates> {
|
||||
using Cache = ModUpdatesCache;
|
||||
};
|
||||
}
|
||||
|
||||
template <auto F>
|
||||
class ServerResultCache final {
|
||||
public:
|
||||
using Extract = decltype(detail::ExtractServerReqParams(F));
|
||||
using Result = typename Extract::Result;
|
||||
using Query = typename Extract::Query;
|
||||
using Cache = typename detail::CacheFor<F>::Cache;
|
||||
|
||||
private:
|
||||
Cache m_cache;
|
||||
|
||||
public:
|
||||
|
@ -215,16 +368,16 @@ namespace server {
|
|||
}
|
||||
|
||||
ServerPromise<Result> get() requires std::is_same_v<Query, std::monostate> {
|
||||
return m_cache.get(Query());
|
||||
return m_cache.get(F, Query());
|
||||
}
|
||||
ServerPromise<Result> get(Query const& query) requires (!std::is_same_v<Query, std::monostate>) {
|
||||
return m_cache.get(query);
|
||||
return m_cache.get(F, query);
|
||||
}
|
||||
ServerPromise<Result> refetch(Query const& query) {
|
||||
// Clear cache for this query only
|
||||
m_cache.clear(query);
|
||||
// Fetch new value
|
||||
return m_cache.get(std::move(query));
|
||||
return m_cache.get(F, std::move(query));
|
||||
}
|
||||
// Invalidate the whole cache
|
||||
void invalidateAll() {
|
||||
|
@ -237,9 +390,15 @@ namespace server {
|
|||
};
|
||||
|
||||
// Clear all shared server endpoint caches
|
||||
static void clearServerCaches() {
|
||||
static void clearServerCaches(bool clearGlobalCaches = false) {
|
||||
ServerResultCache<&getMods>::shared().invalidateAll();
|
||||
ServerResultCache<&getMod>::shared().invalidateAll();
|
||||
ServerResultCache<&getModLogo>::shared().invalidateAll();
|
||||
|
||||
// Only clear global caches if explicitly requested
|
||||
if (clearGlobalCaches) {
|
||||
ServerResultCache<&getTags>::shared().invalidateAll();
|
||||
ServerResultCache<&checkUpdates>::shared().invalidateAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,13 +3,16 @@
|
|||
#include <Geode/utils/ColorProvider.hpp>
|
||||
|
||||
$execute {
|
||||
// todo: these names should probably be shorter so they fit in SSO...
|
||||
ColorProvider::get()->define("mod-list-bg"_spr, { 25, 17, 37, 255 });
|
||||
ColorProvider::get()->define("mod-list-version-label"_spr, ccc3(86, 235, 41));
|
||||
ColorProvider::get()->define("mod-list-version-label-updates-available"_spr, ccc3(41, 235, 225));
|
||||
ColorProvider::get()->define("mod-list-restart-required-label"_spr, ccc3(153, 245, 245));
|
||||
ColorProvider::get()->define("mod-list-restart-required-label-bg"_spr, ccc3(123, 156, 163));
|
||||
ColorProvider::get()->define("mod-list-search-bg"_spr, { 83, 65, 109, 255 });
|
||||
ColorProvider::get()->define("mod-list-tab-selected-bg"_spr, { 168, 147, 185, 255 });
|
||||
ColorProvider::get()->define("mod-list-tab-selected-bg-alt"_spr, { 147, 163, 185, 255 });
|
||||
ColorProvider::get()->define("mod-list-featured-color"_spr, { 255, 255, 120, 255 });
|
||||
}
|
||||
|
||||
bool GeodeSquareSprite::init(CCSprite* top, bool* state) {
|
||||
|
|
|
@ -320,3 +320,19 @@ ModsLayer* ModsLayer::scene() {
|
|||
CCDirector::sharedDirector()->replaceScene(CCTransitionFade::create(.5f, scene));
|
||||
return layer;
|
||||
}
|
||||
|
||||
server::ServerPromise<std::vector<std::string>> ModsLayer::checkInstalledModsForUpdates() {
|
||||
return server::ServerResultCache<&server::checkUpdates>::shared().get(ranges::map<std::vector<std::string>>(
|
||||
Loader::get()->getAllMods(),
|
||||
[](auto mod) { return mod->getID(); })
|
||||
)
|
||||
.then<std::vector<std::string>>([](std::vector<server::ServerModUpdate> list) {
|
||||
std::vector<std::string> updatesFound;
|
||||
for (auto& update : list) {
|
||||
if (update.hasUpdateForInstalledMod()) {
|
||||
updatesFound.push_back(update.id);
|
||||
}
|
||||
}
|
||||
return updatesFound;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -43,5 +43,7 @@ public:
|
|||
static ModsLayer* create();
|
||||
static ModsLayer* scene();
|
||||
|
||||
static server::ServerPromise<std::vector<std::string>> checkInstalledModsForUpdates();
|
||||
|
||||
void gotoTab(ModListSourceType type);
|
||||
};
|
||||
|
|
|
@ -43,8 +43,7 @@ bool ModItem::init(ModSource&& source) {
|
|||
m_titleLabel = CCLabelBMFont::create(m_source.getMetadata().getName().c_str(), "bigFont.fnt");
|
||||
m_titleContainer->addChild(m_titleLabel);
|
||||
|
||||
m_versionLabel = CCLabelBMFont::create(m_source.getMetadata().getVersion().toString().c_str(), "bigFont.fnt");
|
||||
m_versionLabel->setColor(to3B(ColorProvider::get()->color("mod-list-version-label"_spr)));
|
||||
m_versionLabel = CCLabelBMFont::create("", "bigFont.fnt");
|
||||
m_versionLabel->setLayoutOptions(AxisLayoutOptions::create()->setMaxScale(.7f));
|
||||
m_titleContainer->addChild(m_versionLabel);
|
||||
|
||||
|
@ -111,19 +110,33 @@ bool ModItem::init(ModSource&& source) {
|
|||
},
|
||||
[this](server::ServerModMetadata const& metadata) {
|
||||
if (metadata.featured) {
|
||||
m_checkmark = CCScale9Sprite::createWithSpriteFrameName("GJ_colorBtn_001.png");
|
||||
m_checkmark->setContentSize({ 50, 38 });
|
||||
m_checkmark->setColor({ 255, 255, 120 });
|
||||
m_checkmark->setOpacity(45);
|
||||
auto starBG = CCScale9Sprite::createWithSpriteFrameName("GJ_colorBtn_001.png");
|
||||
starBG->setContentSize({ 50, 38 });
|
||||
starBG->setColor(to3B(ColorProvider::get()->color("mod-list-featured-color"_spr)));
|
||||
starBG->setOpacity(45);
|
||||
|
||||
auto tick = CCSprite::createWithSpriteFrameName("GJ_starsIcon_001.png");
|
||||
m_checkmark->addChildAtPosition(tick, Anchor::Center);
|
||||
auto star = CCSprite::createWithSpriteFrameName("GJ_starsIcon_001.png");
|
||||
starBG->addChildAtPosition(star, Anchor::Center);
|
||||
|
||||
m_titleContainer->addChild(m_checkmark);
|
||||
m_titleContainer->addChild(starBG);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
auto updateSpr = CircleButtonSprite::createWithSpriteFrameName(
|
||||
"update.png"_spr, 1.15f, CircleBaseColor::DarkAqua
|
||||
);
|
||||
m_updateBtn = CCMenuItemSpriteExtra::create(
|
||||
updateSpr, this, menu_selector(ModItem::onInstall)
|
||||
);
|
||||
m_viewMenu->addChild(m_updateBtn);
|
||||
m_updateBtn->setVisible(false);
|
||||
|
||||
if (m_source.asMod()) {
|
||||
m_checkUpdateListener.bind(this, &ModItem::onCheckUpdates);
|
||||
m_checkUpdateListener.setFilter(m_source.checkUpdates().listen());
|
||||
}
|
||||
|
||||
this->updateState();
|
||||
|
||||
// Only listen for updates on this mod specifically
|
||||
|
@ -152,13 +165,30 @@ void ModItem::updateState() {
|
|||
[this](server::ServerModMetadata const& metadata) {
|
||||
m_bg->setColor({ 255, 255, 255 });
|
||||
m_bg->setOpacity(25);
|
||||
if (metadata.featured && m_checkmark) {
|
||||
m_bg->setColor(m_checkmark->getColor());
|
||||
if (metadata.featured) {
|
||||
m_bg->setColor(to3B(ColorProvider::get()->color("mod-list-featured-color"_spr)));
|
||||
m_bg->setOpacity(40);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (auto update = m_source.hasUpdates()) {
|
||||
m_updateBtn->setVisible(true);
|
||||
auto updateString = m_source.getMetadata().getVersion().toString() + " -> " + update->version.toString();
|
||||
m_versionLabel->setString(updateString.c_str());
|
||||
m_versionLabel->setColor(to3B(ColorProvider::get()->color("mod-list-version-label-updates-available"_spr)));
|
||||
|
||||
m_bg->setColor(to3B(ColorProvider::get()->color("mod-list-version-label-updates-available"_spr)));
|
||||
m_bg->setOpacity(40);
|
||||
}
|
||||
else {
|
||||
m_updateBtn->setVisible(false);
|
||||
m_versionLabel->setString(m_source.getMetadata().getVersion().toString().c_str());
|
||||
m_versionLabel->setColor(to3B(ColorProvider::get()->color("mod-list-version-label"_spr)));
|
||||
}
|
||||
m_viewMenu->updateLayout();
|
||||
m_titleContainer->updateLayout();
|
||||
|
||||
// Highlight item via BG if it wants to restart for extra UI attention
|
||||
if (wantsRestart) {
|
||||
m_bg->setColor(to3B(ColorProvider::get()->color("mod-list-restart-required-label"_spr)));
|
||||
|
@ -215,14 +245,10 @@ void ModItem::updateSize(float width, bool big) {
|
|||
this->updateLayout();
|
||||
}
|
||||
|
||||
ModItem* ModItem::create(ModSource&& source) {
|
||||
auto ret = new ModItem();
|
||||
if (ret && ret->init(std::move(source))) {
|
||||
ret->autorelease();
|
||||
return ret;
|
||||
void ModItem::onCheckUpdates(PromiseEvent<std::optional<server::ServerModUpdate>, server::ServerError>* event) {
|
||||
if (auto resolved = event->getResolve()) {
|
||||
this->updateState();
|
||||
}
|
||||
CC_SAFE_DELETE(ret);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void ModItem::onView(CCObject*) {
|
||||
|
@ -246,3 +272,22 @@ void ModItem::onEnable(CCObject*) {
|
|||
// Update state of the mod item
|
||||
UpdateModListStateEvent(UpdateModState(m_source.getID())).post();
|
||||
}
|
||||
|
||||
void ModItem::onInstall(CCObject*) {
|
||||
if (auto data = m_source.asServer()) {
|
||||
|
||||
}
|
||||
|
||||
// Update state of the mod item
|
||||
UpdateModListStateEvent(UpdateModState(m_source.getID())).post();
|
||||
}
|
||||
|
||||
ModItem* ModItem::create(ModSource&& source) {
|
||||
auto ret = new ModItem();
|
||||
if (ret && ret->init(std::move(source))) {
|
||||
ret->autorelease();
|
||||
return ret;
|
||||
}
|
||||
CC_SAFE_DELETE(ret);
|
||||
return nullptr;
|
||||
}
|
||||
|
|
|
@ -18,11 +18,13 @@ protected:
|
|||
CCLabelBMFont* m_versionLabel;
|
||||
CCNode* m_developers;
|
||||
CCLabelBMFont* m_developerLabel;
|
||||
ButtonSprite* m_restartRequiredLabel = nullptr;
|
||||
ButtonSprite* m_restartRequiredLabel;
|
||||
CCMenu* m_viewMenu;
|
||||
CCMenuItemToggler* m_enableToggle = nullptr;
|
||||
CCScale9Sprite* m_checkmark = nullptr;
|
||||
CCMenuItemSpriteExtra* m_updateBtn = nullptr;
|
||||
EventListener<UpdateModListStateFilter> m_updateStateListener;
|
||||
EventListener<PromiseEventFilter<std::optional<server::ServerModUpdate>, server::ServerError>> m_checkUpdateListener;
|
||||
std::optional<server::ServerModUpdate> m_availableUpdate;
|
||||
|
||||
/**
|
||||
* @warning Make sure `getMetadata` and `createModLogo` are callable
|
||||
|
@ -32,8 +34,11 @@ protected:
|
|||
|
||||
void updateState();
|
||||
|
||||
void onCheckUpdates(PromiseEvent<std::optional<server::ServerModUpdate>, server::ServerError>* event);
|
||||
|
||||
void onEnable(CCObject*);
|
||||
void onView(CCObject*);
|
||||
void onInstall(CCObject*);
|
||||
|
||||
public:
|
||||
static ModItem* create(ModSource&& source);
|
||||
|
|
|
@ -74,20 +74,18 @@ bool ModPopup::setup(ModSource&& src) {
|
|||
m_stats->setAnchorPoint({ .5f, .5f });
|
||||
|
||||
for (auto stat : std::initializer_list<std::tuple<
|
||||
const char*, const char*, const char*, std::optional<std::string>
|
||||
const char*, const char*, const char*, std::optional<std::string>, const char*
|
||||
>> {
|
||||
{ "GJ_downloadsIcon_001.png", "Downloads", "downloads", std::nullopt },
|
||||
{ "GJ_timeIcon_001.png", "Released", "release-date", std::nullopt },
|
||||
{ "GJ_timeIcon_001.png", "Updated", "update-date", std::nullopt },
|
||||
{ "version.png"_spr, "Version", "version", m_source.getMetadata().getVersion().toString() },
|
||||
{ nullptr, "Checking for updates", "update-check", std::nullopt },
|
||||
{ "GJ_downloadsIcon_001.png", "Downloads", "downloads", std::nullopt, "stats" },
|
||||
{ "GJ_timeIcon_001.png", "Released", "release-date", std::nullopt, "stats" },
|
||||
{ "GJ_timeIcon_001.png", "Updated", "update-date", std::nullopt, "stats" },
|
||||
{ "version.png"_spr, "Version", "version", m_source.getMetadata().getVersion().toString(), "client" },
|
||||
{ nullptr, "Checking for updates", "update-check", std::nullopt, "updates" },
|
||||
}) {
|
||||
auto container = CCNode::create();
|
||||
container->setContentSize({ m_stats->getContentWidth(), 10 });
|
||||
container->setID(std::get<2>(stat));
|
||||
if (std::get<3>(stat).has_value()) {
|
||||
container->setUserObject("client-side", CCBool::create(true));
|
||||
}
|
||||
container->setUserObject(std::get<4>(stat), CCBool::create(true));
|
||||
|
||||
auto labelContainer = CCNode::create();
|
||||
labelContainer->setID("labels");
|
||||
|
@ -434,6 +432,11 @@ bool ModPopup::setup(ModSource&& src) {
|
|||
m_tagsListener.bind(this, &ModPopup::onLoadTags);
|
||||
m_tagsListener.setFilter(m_source.fetchValidTags().listen());
|
||||
|
||||
if (m_source.asMod()) {
|
||||
m_checkUpdateListener.bind(this, &ModPopup::onCheckUpdates);
|
||||
m_checkUpdateListener.setFilter(m_source.checkUpdates().listen());
|
||||
}
|
||||
|
||||
// Only listen for updates on this mod specifically
|
||||
m_updateStateListener.setFilter(UpdateModListStateFilter(UpdateModState(m_source.getID())));
|
||||
m_updateStateListener.bind([this](auto) { this->updateState(); });
|
||||
|
@ -537,13 +540,24 @@ void ModPopup::onLoadServerInfo(PromiseEvent<server::ServerModMetadata, server::
|
|||
this->setStatValue(stat, id.second);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (auto err = event->getReject()) {
|
||||
for (auto child : CCArrayExt<CCNode*>(m_stats->getChildren())) {
|
||||
if (child->getUserObject("stats")) {
|
||||
this->setStatValue(child, "N/A");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ModPopup::onCheckUpdates(PromiseEvent<std::optional<server::ServerModUpdate>, server::ServerError>* event) {
|
||||
if (auto resolved = event->getResolve()) {
|
||||
// Check if this has updates for an installed mod
|
||||
auto updatesStat = m_stats->getChildByID("update-check");
|
||||
if (data->hasUpdateForInstalledMod()) {
|
||||
if (resolved->has_value()) {
|
||||
this->setStatIcon(updatesStat, "updates-available.png"_spr);
|
||||
this->setStatLabel(updatesStat, "Update Found", false, ccc3(99, 250, 255));
|
||||
this->setStatValue(updatesStat, data->latestVersion().getVersion().toString());
|
||||
this->setStatValue(updatesStat, resolved->value().version.toString());
|
||||
}
|
||||
else {
|
||||
this->setStatIcon(updatesStat, "GJ_completesIcon_001.png");
|
||||
|
@ -551,11 +565,6 @@ void ModPopup::onLoadServerInfo(PromiseEvent<server::ServerModMetadata, server::
|
|||
}
|
||||
}
|
||||
else if (auto err = event->getReject()) {
|
||||
for (auto child : CCArrayExt<CCNode*>(m_stats->getChildren())) {
|
||||
if (!child->getUserObject("client-side")) {
|
||||
this->setStatValue(child, "N/A");
|
||||
}
|
||||
}
|
||||
auto updatesStat = m_stats->getChildByID("update-check");
|
||||
this->setStatLabel(updatesStat, "No Updates Found", true, ccc3(125, 125, 125));
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ protected:
|
|||
std::unordered_map<Tab, std::pair<GeodeTabSprite*, Ref<CCNode>>> m_tabs;
|
||||
EventListener<PromiseEventFilter<server::ServerModMetadata, server::ServerError>> m_statsListener;
|
||||
EventListener<PromiseEventFilter<std::unordered_set<std::string>, server::ServerError>> m_tagsListener;
|
||||
EventListener<PromiseEventFilter<std::optional<server::ServerModUpdate>, server::ServerError>> m_checkUpdateListener;
|
||||
EventListener<UpdateModListStateFilter> m_updateStateListener;
|
||||
|
||||
bool setup(ModSource&& src) override;
|
||||
|
@ -41,8 +42,10 @@ protected:
|
|||
void setStatLabel(CCNode* stat, std::string const& value, bool noValue = false, ccColor3B color = ccWHITE);
|
||||
void setStatValue(CCNode* stat, std::optional<std::string> const& value);
|
||||
|
||||
void onCheckUpdates(PromiseEvent<std::optional<server::ServerModUpdate>, server::ServerError>* event);
|
||||
void onLoadServerInfo(PromiseEvent<server::ServerModMetadata, server::ServerError>* event);
|
||||
void onLoadTags(PromiseEvent<std::unordered_set<std::string>, server::ServerError>* event);
|
||||
|
||||
void loadTab(Tab tab);
|
||||
void onTab(CCObject* sender);
|
||||
void onEnable(CCObject*);
|
||||
|
|
|
@ -69,6 +69,9 @@ bool ModSource::wantsRestart() const {
|
|||
}
|
||||
}, m_value);
|
||||
}
|
||||
std::optional<server::ServerModUpdate> ModSource::hasUpdates() const {
|
||||
return m_availableUpdate;
|
||||
}
|
||||
|
||||
ModSource ModSource::tryConvertToMod() const {
|
||||
return std::visit(makeVisitor {
|
||||
|
@ -104,7 +107,6 @@ server::ServerPromise<server::ServerModMetadata> ModSource::fetchServerInfo() co
|
|||
}
|
||||
}, m_value);
|
||||
}
|
||||
|
||||
server::ServerPromise<std::unordered_set<std::string>> ModSource::fetchValidTags() const {
|
||||
return std::visit(makeVisitor {
|
||||
[](Mod* mod) {
|
||||
|
@ -129,3 +131,18 @@ server::ServerPromise<std::unordered_set<std::string>> ModSource::fetchValidTags
|
|||
}
|
||||
}, m_value);
|
||||
}
|
||||
server::ServerPromise<std::optional<server::ServerModUpdate>> ModSource::checkUpdates() {
|
||||
m_availableUpdate = std::nullopt;
|
||||
return server::ServerResultCache<&server::checkUpdates>::shared()
|
||||
.get({ this->getID() })
|
||||
.then<std::optional<server::ServerModUpdate>>([this](auto updates) -> std::optional<server::ServerModUpdate> {
|
||||
if (!updates.empty()) {
|
||||
auto update = std::move(std::move(updates).at(0));
|
||||
if (update.version > this->getMetadata().getVersion()) {
|
||||
m_availableUpdate = update;
|
||||
return m_availableUpdate;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ using namespace geode::prelude;
|
|||
class ModSource final {
|
||||
private:
|
||||
std::variant<Mod*, server::ServerModMetadata> m_value;
|
||||
std::optional<server::ServerModUpdate> m_availableUpdate;
|
||||
|
||||
public:
|
||||
ModSource() = default;
|
||||
|
@ -20,6 +21,8 @@ public:
|
|||
std::optional<std::string> getChangelog() const;
|
||||
CCNode* createModLogo() const;
|
||||
bool wantsRestart() const;
|
||||
// note: be sure to call checkUpdates first...
|
||||
std::optional<server::ServerModUpdate> hasUpdates() const;
|
||||
|
||||
auto visit(auto&& func) {
|
||||
return std::visit(func, m_value);
|
||||
|
@ -34,4 +37,5 @@ public:
|
|||
|
||||
server::ServerPromise<server::ServerModMetadata> fetchServerInfo() const;
|
||||
server::ServerPromise<std::unordered_set<std::string>> fetchValidTags() const;
|
||||
server::ServerPromise<std::optional<server::ServerModUpdate>> checkUpdates();
|
||||
};
|
||||
|
|
|
@ -192,7 +192,8 @@ bool BasedButtonSprite::init(CCNode* ontop, BaseType type, int size, int color)
|
|||
if (ontop) {
|
||||
m_onTop = ontop;
|
||||
m_onTop->setPosition(this->getContentSize() / 2 + m_topOffset);
|
||||
limitNodeSize(m_onTop, this->getMaxTopSize(), m_onTop->getScale(), .1f);
|
||||
limitNodeSize(m_onTop, this->getMaxTopSize(), 999.f, .1f);
|
||||
m_onTop->setScale(m_onTop->getScale() * m_onTopRelativeScale);
|
||||
this->addChild(m_onTop);
|
||||
}
|
||||
|
||||
|
@ -215,7 +216,7 @@ bool BasedButtonSprite::initWithSprite(
|
|||
) {
|
||||
auto spr = CCSprite::create(sprName);
|
||||
if (!spr) return false;
|
||||
spr->setScale(sprScale);
|
||||
m_onTopRelativeScale = sprScale;
|
||||
return this->init(spr, type, size, color);
|
||||
}
|
||||
|
||||
|
@ -224,7 +225,7 @@ bool BasedButtonSprite::initWithSpriteFrameName(
|
|||
) {
|
||||
auto spr = CCSprite::createWithSpriteFrameName(sprName);
|
||||
if (!spr) return false;
|
||||
spr->setScale(sprScale);
|
||||
m_onTopRelativeScale = sprScale;
|
||||
return this->init(spr, type, size, color);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue