implement proper mod update checking

This commit is contained in:
HJfod 2024-03-26 22:18:34 +02:00
parent 269ba9002a
commit fc7d3343d9
16 changed files with 526 additions and 99 deletions

View file

@ -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);

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View file

@ -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

View file

@ -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");
});
}

View file

@ -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();
}
}
}

View file

@ -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) {

View file

@ -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;
});
}

View file

@ -43,5 +43,7 @@ public:
static ModsLayer* create();
static ModsLayer* scene();
static server::ServerPromise<std::vector<std::string>> checkInstalledModsForUpdates();
void gotoTab(ModListSourceType type);
};

View file

@ -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;
}

View file

@ -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);

View file

@ -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));
}

View file

@ -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*);

View file

@ -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;
});
}

View file

@ -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();
};

View file

@ -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);
}