installing & updating mods (needs work still)

This commit is contained in:
HJfod 2024-04-23 22:24:08 +03:00
parent 5c3111e564
commit 684a109dee
21 changed files with 1324 additions and 214 deletions

View file

@ -112,3 +112,16 @@ namespace geode {
cocos2d::ccColor3B color3b(std::string const& id) const;
};
}
GEODE_HIDDEN inline cocos2d::ccColor4B operator"" _cc4b_gd(const char* str, size_t) {
return geode::ColorProvider::get()->color(str);
}
GEODE_HIDDEN inline cocos2d::ccColor3B operator"" _cc3b_gd(const char* str, size_t) {
return geode::ColorProvider::get()->color3b(str);
}
GEODE_HIDDEN inline cocos2d::ccColor4B operator"" _cc4b(const char* str, size_t) {
return geode::ColorProvider::get()->color(geode::Mod::get()->expandSpriteName(str));
}
GEODE_HIDDEN inline cocos2d::ccColor3B operator"" _cc3b(const char* str, size_t) {
return geode::ColorProvider::get()->color3b(geode::Mod::get()->expandSpriteName(str));
}

View file

@ -238,6 +238,10 @@ namespace geode {
}
}
constexpr VersionInfo getUnderlyingVersion() const {
return m_version;
}
std::string toString() const;
friend GEODE_DLL std::string format_as(ComparableVersionInfo const& version);
};

View file

@ -33,26 +33,22 @@ namespace cocos2d {
pos.y *= mul;
return pos;
}
static cocos2d::CCSize& operator*=(cocos2d::CCSize& size, float mul) {
size.width *= mul;
size.height *= mul;
return size;
}
static cocos2d::CCSize operator*(cocos2d::CCSize const& size, cocos2d::CCPoint const& point) {
return {
size.width * point.x,
size.height * point.y,
};
}
static cocos2d::CCRect operator*=(cocos2d::CCRect& rect, float mul) {
rect.origin *= mul;
rect.size *= mul;
return rect;
}
static cocos2d::CCRect operator*(cocos2d::CCRect const& rect, float mul) {
return {
rect.origin.x * mul,
@ -61,147 +57,130 @@ namespace cocos2d {
rect.size.height * mul,
};
}
static cocos2d::CCPoint operator/=(cocos2d::CCPoint& pos, float div) {
pos.x /= div;
pos.y /= div;
return pos;
}
static cocos2d::CCSize operator/=(cocos2d::CCSize& size, float div) {
size.width /= div;
size.height /= div;
return size;
}
static cocos2d::CCRect operator/=(cocos2d::CCRect& rect, float div) {
rect.origin /= div;
rect.size /= div;
return rect;
}
static cocos2d::CCPoint operator+=(cocos2d::CCPoint& pos, cocos2d::CCPoint const& add) {
pos.x += add.x;
pos.y += add.y;
return pos;
}
static cocos2d::CCSize operator+=(cocos2d::CCSize& size, cocos2d::CCPoint const& add) {
size.width += add.x;
size.height += add.y;
return size;
}
static cocos2d::CCSize operator+=(cocos2d::CCSize& size, cocos2d::CCSize const& add) {
size.width += add.width;
size.height += add.height;
return size;
}
static cocos2d::CCRect operator+=(cocos2d::CCRect& rect, cocos2d::CCPoint const& add) {
rect.origin += add;
return rect;
}
static cocos2d::CCRect operator+=(cocos2d::CCRect& rect, cocos2d::CCSize const& add) {
rect.size += add;
return rect;
}
static cocos2d::CCRect operator+=(cocos2d::CCRect& rect, cocos2d::CCRect const& add) {
rect.origin += add.origin;
rect.size += add.size;
return rect;
}
static cocos2d::CCPoint operator-=(cocos2d::CCPoint& pos, cocos2d::CCPoint const& add) {
pos.x -= add.x;
pos.y -= add.y;
return pos;
}
static cocos2d::CCSize operator-=(cocos2d::CCSize& size, cocos2d::CCPoint const& add) {
size.width -= add.x;
size.height -= add.y;
return size;
}
static cocos2d::CCSize operator-=(cocos2d::CCSize& size, cocos2d::CCSize const& add) {
size.width -= add.width;
size.height -= add.height;
return size;
}
static cocos2d::CCRect operator-=(cocos2d::CCRect& rect, cocos2d::CCPoint const& add) {
rect.origin -= add;
return rect;
}
static cocos2d::CCRect operator-=(cocos2d::CCRect& rect, cocos2d::CCSize const& add) {
rect.size -= add;
return rect;
}
static cocos2d::CCRect operator-=(cocos2d::CCRect& rect, cocos2d::CCRect const& add) {
rect.origin -= add.origin;
rect.size -= add.size;
return rect;
}
static cocos2d::CCSize operator-(cocos2d::CCSize const& size, float f) {
return {size.width - f, size.height - f};
}
static cocos2d::CCSize operator-(cocos2d::CCSize const& size) {
return {-size.width, -size.height};
}
static bool operator==(cocos2d::CCPoint const& p1, cocos2d::CCPoint const& p2) {
return p1.x == p2.x && p1.y == p2.y;
}
static bool operator!=(cocos2d::CCPoint const& p1, cocos2d::CCPoint const& p2) {
return p1.x != p2.x || p1.y != p2.y;
}
static bool operator==(cocos2d::CCSize const& s1, cocos2d::CCSize const& s2) {
return s1.width == s2.width && s1.height == s2.height;
}
static bool operator!=(cocos2d::CCSize const& s1, cocos2d::CCSize const& s2) {
return s1.width != s2.width || s1.height != s2.height;
}
static bool operator<(cocos2d::CCSize const& s1, cocos2d::CCSize const& s2) {
return s1.width < s2.width || s1.height < s2.height;
}
static bool operator<=(cocos2d::CCSize const& s1, cocos2d::CCSize const& s2) {
return s1.width <= s2.width || s1.height <= s2.height;
}
static bool operator>(cocos2d::CCSize const& s1, cocos2d::CCSize const& s2) {
return s1.width > s2.width || s1.height > s2.height;
}
static bool operator>=(cocos2d::CCSize const& s1, cocos2d::CCSize const& s2) {
return s1.width >= s2.width || s1.height >= s2.height;
}
static bool operator==(cocos2d::CCRect const& r1, cocos2d::CCRect const& r2) {
return r1.origin == r2.origin && r1.size == r2.size;
}
static bool operator!=(cocos2d::CCRect const& r1, cocos2d::CCRect const& r2) {
return r1.origin != r2.origin || r1.size != r2.size;
}
static bool operator==(cocos2d::ccColor4B const& c1, cocos2d::ccColor4B const& c2) {
return c1.r == c2.r && c1.g == c2.g && c1.b == c2.b && c1.a == c2.a;
}
static bool operator!=(cocos2d::ccColor4B const& c1, cocos2d::ccColor4B const& c2) {
return c1.r != c2.r || c1.g != c2.g || c1.b != c2.b || c1.a != c2.a;
}
static bool operator==(cocos2d::ccColor3B const& c1, cocos2d::ccColor3B const& c2) {
return c1.r == c2.r && c1.g == c2.g && c1.b == c2.b;
}
static bool operator!=(cocos2d::ccColor3B const& c1, cocos2d::ccColor3B const& c2) {
return c1.r != c2.r || c1.g != c2.g || c1.b != c2.b;
}
static bool operator==(cocos2d::ccHSVValue const& c1, cocos2d::ccHSVValue const& c2) {
return c1.h == c2.h && c1.s == c2.s && c1.v == c2.v &&
c1.absoluteSaturation == c2.absoluteSaturation &&
c1.absoluteBrightness == c2.absoluteBrightness;
}
static bool operator!=(cocos2d::ccHSVValue const& c1, cocos2d::ccHSVValue const& c2) {
return !(c1 == c2);
}

View file

@ -0,0 +1,346 @@
#include "DownloadManager.hpp"
using namespace server;
ModDownloadEvent::ModDownloadEvent(std::string const& id) : id(id) {}
ListenerResult ModDownloadFilter::handle(MiniFunction<Callback> fn, ModDownloadEvent* event) {
if (m_id.empty() || m_id == event->id) {
fn(event);
}
return ListenerResult::Propagate;
}
ModDownloadFilter::ModDownloadFilter() {}
ModDownloadFilter::ModDownloadFilter(std::string const& id) : m_id(id) {}
class ModDownload::Impl final {
public:
std::string m_id;
std::optional<VersionInfo> m_version;
std::optional<std::string> m_dependencyFor;
DownloadStatus m_status;
EventListener<ServerRequest<ServerModVersion>> m_infoListener;
EventListener<web::WebTask> m_downloadListener;
Impl(
std::string const& id,
std::optional<VersionInfo> const& version,
std::optional<std::string> const& dependencyFor
)
: m_id(id),
m_version(version),
m_dependencyFor(dependencyFor),
m_status(DownloadStatusFetching {
.percentage = 0,
})
{
m_infoListener.bind([this](ServerRequest<ServerModVersion>::Event* event) {
if (auto result = event->getValue()) {
if (result->isOk()) {
auto data = result->unwrap();
m_version = data.metadata.getVersion();
// Start downloads for any missing dependencies
for (auto dep : data.metadata.getDependencies()) {
if (!dep.mod) {
ModDownloadManager::get()->startDownload(
dep.id, dep.version.getUnderlyingVersion(), m_id
);
}
}
m_status = DownloadStatusConfirm {
.version = data,
};
}
else {
m_status = DownloadStatusError {
.details = result->unwrapErr().details,
};
}
// Clear the listener to free up the memory
m_infoListener.setFilter(ServerRequest<ServerModVersion>());
}
else if (auto progress = event->getProgress()) {
m_status = DownloadStatusFetching {
.percentage = progress->percentage.value_or(0),
};
}
else if (event->isCancelled()) {
m_status = DownloadStatusCancelled();
m_infoListener.setFilter(ServerRequest<ServerModVersion>());
}
if (!ModDownloadManager::get()->checkAutoConfirm()) {
ModDownloadEvent(m_id).post();
}
});
m_infoListener.setFilter(getModVersion(id, version));
Loader::get()->queueInMainThread([id = m_id] {
ModDownloadEvent(id).post();
});
}
void confirm() {
auto confirm = std::get_if<DownloadStatusConfirm>(&m_status);
if (!confirm) return;
auto version = confirm->version;
m_status = DownloadStatusDownloading {
.percentage = 0,
};
m_downloadListener.bind([this, hash = version.hash](web::WebTask::Event* event) {
if (auto value = event->getValue()) {
if (value->ok()) {
bool removingInstalledWasError = false;
// If this was an update, delete the old file first
if (auto mod = Loader::get()->getInstalledMod(m_id)) {
std::error_code ec;
ghc::filesystem::remove(mod->getPackagePath(), ec);
if (ec) {
removingInstalledWasError = true;
m_status = DownloadStatusError {
.details = fmt::format("Unable to delete existing .geode package (code {})", ec),
};
}
}
if (!removingInstalledWasError) {
auto ok = file::writeBinary(dirs::getModsDir() / (m_id + ".geode"), value->data());
if (!ok) {
m_status = DownloadStatusError {
.details = ok.unwrapErr(),
};
}
else {
m_status = DownloadStatusDone();
}
}
}
else {
m_status = DownloadStatusError {
.details = value->string().unwrapOr("Unknown error"),
};
}
}
else if (auto progress = event->getProgress()) {
m_status = DownloadStatusDownloading {
.percentage = static_cast<uint8_t>(progress->downloadProgress().value_or(0)),
};
}
else if (event->isCancelled()) {
m_status = DownloadStatusCancelled();
}
ModDownloadEvent(m_id).post();
});
auto req = web::WebRequest();
m_downloadListener.setFilter(req.get(version.downloadURL));
ModDownloadEvent(m_id).post();
}
};
ModDownload::ModDownload(
std::string const& id,
std::optional<VersionInfo> const& version,
std::optional<std::string> const& dependencyFor
) : m_impl(std::make_shared<Impl>(id, version, dependencyFor)) {}
void ModDownload::confirm() {
m_impl->confirm();
}
std::optional<std::string> ModDownload::getDependencyFor() const {
return m_impl->m_dependencyFor;
}
bool ModDownload::isDone() const {
return std::holds_alternative<DownloadStatusDone>(m_impl->m_status);
}
bool ModDownload::isActive() const {
return !(
std::holds_alternative<DownloadStatusDone>(m_impl->m_status) ||
std::holds_alternative<DownloadStatusError>(m_impl->m_status) ||
std::holds_alternative<DownloadStatusCancelled>(m_impl->m_status)
);
}
bool ModDownload::canRetry() const {
return
std::holds_alternative<DownloadStatusError>(m_impl->m_status) ||
std::holds_alternative<DownloadStatusCancelled>(m_impl->m_status);
}
std::string ModDownload::getID() const {
return m_impl->m_id;
}
DownloadStatus ModDownload::getStatus() const {
return m_impl->m_status;
}
std::optional<VersionInfo> ModDownload::getVersion() const {
return m_impl->m_version;
}
class ModDownloadManager::Impl {
public:
std::unordered_map<std::string, ModDownload> m_downloads;
Task<std::monostate> m_updateAllTask;
void cancelOrphanedDependencies() {
// "This doesn't handle circular dependencies!!!!"
// Well OK and the human skull doesn't handle the 5000 newtons
// of force from this anvil I'm about to drop on your head
for (auto& [_, d] : m_downloads) {
if (auto depFor = d.m_impl->m_dependencyFor) {
if (
!m_downloads.contains(*depFor) ||
!(m_downloads.at(*depFor).isActive() || m_downloads.at(*depFor).isDone())
) {
// d.cancel() will cause cancelOrphanedDependencies() to be called again
// We want that anyway because cancelling one dependency might cause
// dependencies down the chain to become orphaned
return d.cancel();
}
}
}
}
};
void ModDownload::cancel() {
if (!std::holds_alternative<DownloadStatusDone>(m_impl->m_status)) {
m_impl->m_status = DownloadStatusCancelled();
m_impl->m_infoListener.getFilter().cancel();
m_impl->m_infoListener.setFilter(ServerRequest<ServerModVersion>());
// Cancel any dependencies of this mod left over (unless some other
// installation depends on them still)
ModDownloadManager::get()->m_impl->cancelOrphanedDependencies();
ModDownloadEvent(m_impl->m_id).post();
}
}
std::optional<ModDownload> ModDownloadManager::startDownload(
std::string const& id,
std::optional<VersionInfo> const& version,
std::optional<std::string> const& dependencyFor
) {
// If this mod has already been succesfully downloaded or is currently
// being downloaded, return as you can't download multiple versions of the
// same mod simultaniously, since that wouldn't make sense. I mean the new
// version would just immediately override to the other one
if (m_impl->m_downloads.contains(id)) {
// If the download errored last time, then we can try again
if (m_impl->m_downloads.at(id).canRetry()) {
m_impl->m_downloads.erase(id);
}
// Otherwise return
else return std::nullopt;
}
// Start a new download by constructing a ModDownload (which starts the
// download)
m_impl->m_downloads.emplace(id, ModDownload(id, version, dependencyFor));
return m_impl->m_downloads.at(id);
}
void ModDownloadManager::cancelAll() {
for (auto& [_, d] : m_impl->m_downloads) {
d.cancel();
}
}
void ModDownloadManager::confirmAll() {
for (auto& [_, d] : m_impl->m_downloads) {
d.confirm();
}
}
void ModDownloadManager::startUpdateAll() {
m_impl->m_updateAllTask = checkAllUpdates().map(
[this](Result<std::vector<ServerModUpdate>, ServerError>* result) {
if (result->isOk()) {
for (auto& mod : result->unwrap()) {
if (mod.hasUpdateForInstalledMod()) {
this->startDownload(mod.id, std::nullopt);
}
}
}
return std::monostate();
},
[](auto) { return std::monostate(); }
);
}
void ModDownloadManager::dismissAll() {
std::erase_if(m_impl->m_downloads, [](auto const& d) {
return d.second.canRetry();
});
ModDownloadEvent("").post();
}
bool ModDownloadManager::checkAutoConfirm() {
for (auto& [_, download] : m_impl->m_downloads) {
auto status = download.getStatus();
if (auto confirm = std::get_if<server::DownloadStatusConfirm>(&status)) {
for (auto dep : confirm->version.metadata.getDependencies()) {
// If some mod has an optional dependency that isn't installed,
// we need to ask for confirmation
if (!dep.mod && dep.importance != ModMetadata::Dependency::Importance::Required) {
return false;
}
}
for (auto inc : confirm->version.metadata.getIncompatibilities()) {
// If some mod has an incompatability that is installed,
// we need to ask for confirmation
if (inc.mod) {
return false;
}
}
// If some installed mod is incompatible with this one,
// we need to ask for confirmation
for (auto mod : Loader::get()->getAllMods()) {
for (auto inc : mod->getMetadata().getIncompatibilities()) {
if (inc.id == download.getID()) {
return false;
}
}
}
}
// If there are mods we aren't sure about yet, we can't auto-confirm
else if (std::holds_alternative<DownloadStatusFetching>(status)) {
return false;
}
}
// If we have reached this point, we can auto-confirm
this->confirmAll();
return true;
}
std::vector<ModDownload> ModDownloadManager::getDownloads() const {
return map::values(m_impl->m_downloads);
}
std::optional<ModDownload> ModDownloadManager::getDownload(std::string const& id) const {
if (m_impl->m_downloads.contains(id)) {
return m_impl->m_downloads.at(id);
}
return std::nullopt;
}
bool ModDownloadManager::hasActiveDownloads() const {
for (auto& [_, download] : m_impl->m_downloads) {
if (download.isActive()) {
return true;
}
}
return false;
}
bool ModDownloadManager::wantsRestart() const {
for (auto& [key, v] : m_impl->m_downloads) {
if (v.isDone()) {
return true;
}
}
return false;
}
ModDownloadManager* ModDownloadManager::get() {
static auto inst = new ModDownloadManager();
return inst;
}
ModDownloadManager::ModDownloadManager() : m_impl(std::make_unique<Impl>()) {}
ModDownloadManager::~ModDownloadManager() = default;

View file

@ -0,0 +1,115 @@
#pragma once
#include "Server.hpp"
namespace server {
struct DownloadStatusFetching {
uint8_t percentage;
bool operator==(DownloadStatusFetching const&) const = default;
};
struct DownloadStatusConfirm {
ServerModVersion version;
bool operator==(DownloadStatusConfirm const&) const = default;
};
struct DownloadStatusDownloading {
uint8_t percentage;
bool operator==(DownloadStatusDownloading const&) const = default;
};
struct DownloadStatusDone {
bool operator==(DownloadStatusDone const&) const = default;
};
struct DownloadStatusError {
std::string details;
bool operator==(DownloadStatusError const&) const = default;
};
struct DownloadStatusCancelled {
bool operator==(DownloadStatusCancelled const&) const = default;
};
using DownloadStatus = std::variant<
DownloadStatusFetching,
DownloadStatusConfirm,
DownloadStatusDownloading,
DownloadStatusDone,
DownloadStatusError,
DownloadStatusCancelled
>;
struct ModDownloadEvent : public Event {
std::string id;
ModDownloadEvent(std::string const& id);
};
class ModDownloadFilter : public EventFilter<ModDownloadEvent> {
public:
using Callback = void(ModDownloadEvent*);
protected:
std::string m_id;
public:
ListenerResult handle(MiniFunction<Callback> fn, ModDownloadEvent* event);
ModDownloadFilter();
ModDownloadFilter(std::string const& id);
};
class ModDownload final {
private:
class Impl;
std::shared_ptr<Impl> m_impl;
ModDownload(
std::string const& id,
std::optional<VersionInfo> const& version,
std::optional<std::string> const& dependencyFor
);
friend class ModDownloadManager;
public:
void confirm();
void cancel();
bool isDone() const;
bool isActive() const;
bool canRetry() const;
std::optional<std::string> getDependencyFor() const;
std::string getID() const;
DownloadStatus getStatus() const;
std::optional<VersionInfo> getVersion() const;
};
class ModDownloadManager final {
private:
class Impl;
std::unique_ptr<Impl> m_impl;
ModDownloadManager();
friend class ModDownload;
public:
static ModDownloadManager* get();
~ModDownloadManager();
std::optional<ModDownload> startDownload(
std::string const& id,
std::optional<VersionInfo> const& version,
std::optional<std::string> const& dependencyFor = std::nullopt
);
void startUpdateAll();
void confirmAll();
void cancelAll();
void dismissAll();
bool checkAutoConfirm();
std::optional<ModDownload> getDownload(std::string const& id) const;
std::vector<ModDownload> getDownloads() const;
bool hasActiveDownloads() const;
bool wantsRestart() const;
};
}

View file

@ -76,55 +76,48 @@ public:
}
};
template <class Q, class V>
using ServerFuncQ = V(*)(Q const&, bool);
template <class V>
using ServerFuncNQ = V(*)(bool);
template <class F>
struct ExtractFun;
template <class Q, class V>
struct ExtractFun<ServerRequest<V>(*)(Q const&, bool)> {
using Query = Q;
template <class V, class... Args>
struct ExtractFun<ServerRequest<V>(*)(Args...)> {
using CacheKey = std::tuple<std::remove_cvref_t<Args>...>;
using Value = V;
static ServerRequest<V> invoke(auto&& func, Query const& query) {
return func(query, false);
}
};
template <class V>
struct ExtractFun<ServerRequest<V>(*)(bool)> {
using Query = std::monostate;
using Value = V;
static ServerRequest<V> invoke(auto&& func, Query const&) {
return func(false);
template <class... CArgs>
static CacheKey key(CArgs const&... args) {
return std::make_tuple(args..., false);
}
template <class... CArgs>
static ServerRequest<V> invoke(auto&& func, CArgs const&... args) {
return func(args..., false);
}
};
template <auto F>
class FunCache final {
public:
using Extract = ExtractFun<decltype(F)>;
using Query = typename Extract::Query;
using Value = typename Extract::Value;
using Extract = ExtractFun<decltype(F)>;
using CacheKey = typename Extract::CacheKey;
using Value = typename Extract::Value;
private:
std::mutex m_mutex;
CacheMap<Query, ServerRequest<Value>> m_cache;
CacheMap<CacheKey, ServerRequest<Value>> m_cache;
public:
FunCache() = default;
FunCache(FunCache const&) = delete;
FunCache(FunCache&&) = delete;
ServerRequest<Value> get(Query const& query = Query()) {
template <class... Args>
ServerRequest<Value> get(Args const&... args) {
std::unique_lock lock(m_mutex);
if (auto v = m_cache.get(query)) {
if (auto v = m_cache.get(Extract::key(args...))) {
return *v;
}
auto f = Extract::invoke(F, query);
m_cache.add(Query(query), ServerRequest<Value>(f));
auto f = Extract::invoke(F, args...);
m_cache.add(Extract::key(args...), ServerRequest<Value>(f));
return f;
}
size_t size() {
@ -305,10 +298,16 @@ Result<ServerModVersion> ServerModVersion::parse(matjson::Value const& raw) {
}
ModMetadata::Dependency dependency;
obj.needs("id").validate(MiniFunction<bool(std::string const&)>(&ModMetadata::validateID)).into(dependency.id);
obj.needs("mod_id").validate(MiniFunction<bool(std::string const&)>(&ModMetadata::validateID)).into(dependency.id);
obj.needs("version").into(dependency.version);
obj.has("importance").into(dependency.importance);
// Check if this dependency is installed, and if so assign the `mod` member to mark that
auto mod = Loader::get()->getInstalledMod(dependency.id);
if (mod && dependency.version.compare(mod->getVersion())) {
dependency.mod = mod;
}
dependencies.push_back(dependency);
}
res.metadata.setDependencies(dependencies);
@ -318,10 +317,16 @@ Result<ServerModVersion> ServerModVersion::parse(matjson::Value const& raw) {
auto obj = incompat.obj();
ModMetadata::Incompatibility incompatibility;
obj.needs("id").validate(MiniFunction<bool(std::string const&)>(&ModMetadata::validateID)).into(incompatibility.id);
obj.needs("mod_id").validate(MiniFunction<bool(std::string const&)>(&ModMetadata::validateID)).into(incompatibility.id);
obj.needs("version").into(incompatibility.version);
obj.has("importance").into(incompatibility.importance);
// Check if this incompatability is installed, and if so assign the `mod` member to mark that
auto mod = Loader::get()->getInstalledMod(incompatibility.id);
if (mod && incompatibility.version.compare(mod->getVersion())) {
incompatibility.mod = mod;
}
incompatibilities.push_back(incompatibility);
}
res.metadata.setIncompatibilities(incompatibilities);
@ -585,6 +590,38 @@ ServerRequest<ServerModMetadata> server::getMod(std::string const& id, bool useC
);
}
ServerRequest<ServerModVersion> server::getModVersion(std::string const& id, std::optional<VersionInfo> const& version, bool useCache) {
if (useCache) {
return getCache<getModVersion>().get(id, version);
}
auto req = web::WebRequest();
req.userAgent(getServerUserAgent());
auto versionURL = version ? version->toString(false) : "latest";
return req.get(getServerAPIBaseURL() + "/mods/" + id + "/versions/" + versionURL).map(
[](web::WebResponse* response) -> Result<ServerModVersion, ServerError> {
if (response->ok()) {
// Parse payload
auto payload = parseServerPayload(*response);
if (!payload) {
return Err(payload.unwrapErr());
}
// Parse response
auto list = ServerModVersion::parse(payload.unwrap());
if (!list) {
return Err(ServerError(response->code(), "Unable to parse response: {}", list.unwrapErr()));
}
return Ok(list.unwrap());
}
return Err(parseServerError(*response));
},
[id](web::WebProgress* progress) {
return parseServerProgress(*progress, "Downloading metadata for " + id);
}
);
}
ServerRequest<ByteVector> server::getModLogo(std::string const& id, bool useCache) {
if (useCache) {
return getCache<getModLogo>().get(id);

View file

@ -29,6 +29,8 @@ namespace server {
std::string hash;
size_t downloadCount;
bool operator==(ServerModVersion const&) const = default;
static Result<ServerModVersion> parse(matjson::Value const& json);
};
@ -116,12 +118,15 @@ namespace server {
std::string getServerAPIBaseURL();
std::string getServerUserAgent();
ServerRequest<ServerModsList> getMods(ModsQuery const& query, bool useCache = true);
ServerRequest<ServerModMetadata> getMod(std::string const& id, bool useCache = true);
ServerRequest<ServerModVersion> getModVersion(std::string const& id, std::optional<VersionInfo> const& version = std::nullopt, bool useCache = true);
ServerRequest<ByteVector> getModLogo(std::string const& id, bool useCache = true);
ServerRequest<std::unordered_set<std::string>> getTags(bool useCache = true);
ServerRequest<std::optional<ServerModUpdate>> checkUpdates(Mod* mod);
ServerRequest<std::vector<ServerModUpdate>> checkAllUpdates(bool useCache = true);
void clearServerCaches(bool clearGlobalCaches = false);
}

View file

@ -109,7 +109,9 @@ public:
void setVisible(bool visible) override {
CCNode::setVisible(visible);
this->spin();
if (visible) {
this->spin();
}
}
};

View file

@ -2,8 +2,263 @@
#include "SwelvyBG.hpp"
#include <Geode/ui/TextInput.hpp>
#include <Geode/utils/ColorProvider.hpp>
#include "popups/ConfirmInstallPopup.hpp"
#include "GeodeStyle.hpp"
bool ModsStatusNode::init() {
if (!CCNode::init())
return false;
this->ignoreAnchorPointForPosition(false);
this->setAnchorPoint({ .5f, 1.f });
this->setContentSize({ 300, 35 });
m_statusBG = CCScale9Sprite::create("black-square.png"_spr);
m_statusBG->setContentSize({ 570, 40 });
m_statusBG->setScale(.5f);
m_status = CCLabelBMFont::create("", "bigFont.fnt");
m_status->setScale(.8f);
m_statusBG->addChildAtPosition(m_status, Anchor::Center);
m_statusPercentage = CCLabelBMFont::create("", "bigFont.fnt");
m_statusPercentage->setScale(.8f);
m_statusBG->addChildAtPosition(m_statusPercentage, Anchor::Right, ccp(-25, 0));
m_loadingCircle = createLoadingCircle(32);
m_statusBG->addChildAtPosition(m_loadingCircle, Anchor::Left, ccp(25, 0));
m_progressBar = Slider::create(nullptr, nullptr);
m_progressBar->m_touchLogic->m_thumb->setVisible(false);
m_progressBar->setScale(2.f);
m_progressBar->setAnchorPoint({ 0, 0 }),
m_statusBG->addChildAtPosition(m_progressBar, Anchor::Center);
this->addChildAtPosition(m_statusBG, Anchor::Bottom);
m_btnMenu = CCMenu::create();
m_btnMenu->setContentWidth(m_obContentSize.width);
auto restartSpr = createGeodeButton("Restart Now");
restartSpr->setScale(.65f);
m_restartBtn = CCMenuItemSpriteExtra::create(
restartSpr, this, menu_selector(ModsStatusNode::onRestart)
);
m_btnMenu->addChild(m_restartBtn);
auto viewSpr = createGeodeButton("View");
viewSpr->setScale(.65f);
m_viewBtn = CCMenuItemSpriteExtra::create(viewSpr, this, nullptr);
m_btnMenu->addChild(m_viewBtn);
auto cancelSpr = createGeodeButton("Cancel");
cancelSpr->setScale(.65f);
m_cancelBtn = CCMenuItemSpriteExtra::create(
cancelSpr, this, menu_selector(ModsStatusNode::onCancel)
);
m_btnMenu->addChild(m_cancelBtn);
m_btnMenu->setLayout(RowLayout::create());
m_btnMenu->getLayout()->ignoreInvisibleChildren(true);
this->addChildAtPosition(m_btnMenu, Anchor::Center, ccp(0, 5));
m_updateStateListener.bind([this](auto) { this->updateState(); });
m_updateStateListener.setFilter(UpdateModListStateFilter());
m_downloadListener.bind([this](auto) { this->updateState(); });
this->updateState();
return true;
}
void ModsStatusNode::updateState() {
enum class DownloadState {
None,
SomeCancelled,
AllDone,
SomeErrored,
SomeToBeConfirmed,
SomeFetching,
SomeDownloading,
};
DownloadState state = DownloadState::None;
auto upgradeState = [&](DownloadState into) {
if (static_cast<int>(state) < static_cast<int>(into)) {
state = into;
}
};
auto downloads = server::ModDownloadManager::get()->getDownloads();
for (auto& download : downloads) {
std::visit(makeVisitor {
[&](server::DownloadStatusFetching const&) {
upgradeState(DownloadState::SomeFetching);
},
[&](server::DownloadStatusConfirm const&) {
upgradeState(DownloadState::SomeToBeConfirmed);
},
[&](server::DownloadStatusDownloading const&) {
upgradeState(DownloadState::SomeDownloading);
},
[&](server::DownloadStatusDone const&) {
upgradeState(DownloadState::AllDone);
},
[&](server::DownloadStatusError const&) {
upgradeState(DownloadState::SomeErrored);
},
[&](server::DownloadStatusCancelled const&) {
upgradeState(DownloadState::SomeCancelled);
},
}, download.getStatus());
}
// Reset the state to default
m_statusBG->setVisible(false);
m_status->setVisible(false);
m_statusPercentage->setVisible(false);
m_loadingCircle->setVisible(false);
m_progressBar->setVisible(false);
m_restartBtn->setVisible(false);
m_cancelBtn->setVisible(false);
m_viewBtn->setVisible(false);
switch (state) {
// If there are no downloads happening, just show the restart button if needed
case DownloadState::None: {
m_restartBtn->setVisible(isRestartRequired());
} break;
// If some downloads were cancelled, show the restart button normally
case DownloadState::SomeCancelled: {
m_status->setString("Download(s) Cancelled");
m_status->setColor(ccWHITE);
m_status->setVisible(true);
m_restartBtn->setVisible(isRestartRequired());
} break;
// If all downloads were finished, show the restart button normally
// but also a "all done" status
case DownloadState::AllDone: {
m_status->setString(fmt::format("{} Mod(s) Installed/Updated", downloads.size()).c_str());
m_status->setColor("mod-list-enabled"_cc3b);
m_status->setVisible(true);
m_statusBG->setVisible(true);
m_restartBtn->setVisible(isRestartRequired());
} break;
case DownloadState::SomeErrored: {
m_status->setString("Some Downloads Failed");
m_status->setColor("mod-list-disabled"_cc3b);
m_status->setVisible(true);
m_statusBG->setVisible(true);
m_viewBtn->setVisible(true);
m_viewBtn->setTarget(this, menu_selector(ModsStatusNode::onViewErrors));
} break;
case DownloadState::SomeToBeConfirmed: {
size_t totalToConfirm = 0;
for (auto& download : downloads) {
auto status = download.getStatus();
if (auto loading = std::get_if<server::DownloadStatusConfirm>(&status)) {
totalToConfirm += 1;
}
}
m_status->setString(fmt::format("Click to Confirm {} Download(s)", totalToConfirm).c_str());
m_status->setColor(ccWHITE);
m_status->setVisible(true);
m_statusBG->setVisible(true);
m_viewBtn->setVisible(true);
m_viewBtn->setTarget(this, menu_selector(ModsStatusNode::onConfirm));
} break;
case DownloadState::SomeFetching: {
m_status->setString("Preparing Download(s)");
m_status->setColor(ccWHITE);
m_status->setVisible(true);
m_loadingCircle->setVisible(true);
m_statusBG->setVisible(true);
m_cancelBtn->setVisible(true);
} break;
case DownloadState::SomeDownloading: {
size_t totalProgress = 0;
size_t totalDownloading = 0;
for (auto& download : downloads) {
auto status = download.getStatus();
if (auto loading = std::get_if<server::DownloadStatusDownloading>(&status)) {
totalProgress += loading->percentage;
totalDownloading += 1;
}
}
auto percentage = totalProgress / static_cast<float>(totalDownloading);
m_statusPercentage->setString(fmt::format("{}%", static_cast<size_t>(percentage)).c_str());
m_statusPercentage->setVisible(true);
m_loadingCircle->setVisible(true);
m_statusBG->setVisible(true);
m_cancelBtn->setVisible(true);
m_progressBar->setVisible(true);
m_progressBar->setValue(percentage / 100.f);
m_progressBar->updateBar();
} break;
}
m_btnMenu->updateLayout();
}
void ModsStatusNode::onViewErrors(CCObject*) {
auto downloads = server::ModDownloadManager::get()->getDownloads();
std::vector<std::string> errors;
for (auto& download : downloads) {
auto status = download.getStatus();
if (auto error = std::get_if<server::DownloadStatusError>(&status)) {
errors.push_back(fmt::format("<cr>{}</c>: {}", download.getID(), error->details));
}
}
createQuickPopup(
"Download Errors", ranges::join(errors, "\n"),
"OK", "Dismiss",
[](auto, bool btn2) {
if (btn2) {
server::ModDownloadManager::get()->dismissAll();
}
}
);
}
void ModsStatusNode::onConfirm(CCObject*) {
ConfirmInstallPopup::askForCustomize();
}
void ModsStatusNode::onCancel(CCObject*) {
server::ModDownloadManager::get()->cancelAll();
}
void ModsStatusNode::onRestart(CCObject*) {
// Update button state to let user know it's restarting but it might take a bit
m_restartBtn->setEnabled(false);
static_cast<ButtonSprite*>(m_restartBtn->getNormalImage())->setString("Restarting...");
m_restartBtn->updateSprite();
// Actually restart
game::restart();
}
ModsStatusNode* ModsStatusNode::create() {
auto ret = new ModsStatusNode();
if (ret && ret->init()) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
bool ModsLayer::init() {
if (!CCLayer::init())
return false;
@ -124,19 +379,9 @@ bool ModsLayer::init() {
listActionsMenu->setLayout(ColumnLayout::create());
m_frame->addChildAtPosition(listActionsMenu, Anchor::Left, ccp(-5, 25));
auto restartMenu = CCMenu::create();
restartMenu->setContentWidth(200.f);
restartMenu->setAnchorPoint({ .5f, 1.f });
restartMenu->setScale(.7f);
m_restartBtn = CCMenuItemSpriteExtra::create(
createGeodeButton("Restart Now"),
this, menu_selector(ModsLayer::onRestart)
);
restartMenu->addChild(m_restartBtn);
restartMenu->setLayout(RowLayout::create());
m_frame->addChildAtPosition(restartMenu, Anchor::Bottom, ccp(0, 0));
m_statusNode = ModsStatusNode::create();
m_statusNode->setZOrder(4);
m_frame->addChildAtPosition(m_statusNode, Anchor::Bottom);
m_pageMenu = CCMenu::create();
m_pageMenu->setContentWidth(200.f);
@ -191,13 +436,13 @@ void ModsLayer::gotoTab(ModListSource* src) {
// Lazily create new list and add it to UI
if (!m_lists.contains(src)) {
auto list = ModList::create(src, m_frame->getContentSize() - ccp(30, 0));
list->setPosition(m_frame->getPosition());
this->addChild(list);
list->setPosition(m_frame->getContentSize() / 2);
m_frame->addChild(list);
m_lists.emplace(src, list);
}
// Add list to UI
else {
this->addChild(m_lists.at(src));
m_frame->addChild(m_lists.at(src));
}
// Update current source
@ -245,14 +490,6 @@ void ModsLayer::updateState() {
else {
m_pageMenu->setVisible(false);
}
// Update visibility of the restart button
m_restartBtn->setVisible(false);
for (auto& [src, _] : m_lists) {
if (src->wantsRestart()) {
m_restartBtn->setVisible(true);
}
}
}
void ModsLayer::onTab(CCObject* sender) {
@ -297,15 +534,6 @@ void ModsLayer::onSearch(CCObject*) {
}
}
void ModsLayer::onRestart(CCObject*) {
// Update button state to let user know it's restarting but it might take a bit
m_restartBtn->setEnabled(false);
static_cast<ButtonSprite*>(m_restartBtn->getNormalImage())->setString("Restarting...");
// Actually restart
game::restart();
}
ModsLayer* ModsLayer::create() {
auto ret = new ModsLayer();
if (ret && ret->init()) {

View file

@ -7,9 +7,36 @@
#include "list/ModList.hpp"
#include "sources/ModListSource.hpp"
#include "UpdateModListState.hpp"
#include <server/DownloadManager.hpp>
using namespace geode::prelude;
class ModsStatusNode : public CCNode {
protected:
CCScale9Sprite* m_statusBG;
CCLabelBMFont* m_status;
CCLabelBMFont* m_statusPercentage;
Slider* m_progressBar;
CCNode* m_loadingCircle;
CCMenu* m_btnMenu;
CCMenuItemSpriteExtra* m_viewBtn;
CCMenuItemSpriteExtra* m_cancelBtn;
CCMenuItemSpriteExtra* m_restartBtn;
EventListener<UpdateModListStateFilter> m_updateStateListener;
EventListener<server::ModDownloadFilter> m_downloadListener;
bool init();
void updateState();
void onRestart(CCObject*);
void onViewErrors(CCObject*);
void onConfirm(CCObject*);
void onCancel(CCObject*);
public:
static ModsStatusNode* create();
};
class ModsLayer : public CCLayer, public SetTextPopupDelegate {
protected:
CCNode* m_frame;
@ -19,7 +46,7 @@ protected:
CCMenu* m_pageMenu;
CCLabelBMFont* m_pageLabel;
CCMenuItemSpriteExtra* m_goToPageBtn;
CCMenuItemSpriteExtra* m_restartBtn;
ModsStatusNode* m_statusNode;
EventListener<UpdateModListStateFilter> m_updateStateListener;
bool m_showSearch = false;
bool m_bigView = false;
@ -33,9 +60,8 @@ protected:
void onBigView(CCObject*);
void onSearch(CCObject*);
void onGoToPage(CCObject*);
void onBack(CCObject*);
void onRefreshList(CCObject*);
void onRestart(CCObject*);
void onBack(CCObject*);
void updateState();

View file

@ -76,6 +76,34 @@ bool ModItem::init(ModSource&& source) {
m_restartRequiredLabel->setLayoutOptions(AxisLayoutOptions::create()->setMaxScale(.75f));
m_infoContainer->addChild(m_restartRequiredLabel);
m_downloadBarContainer = CCNode::create();
m_downloadBarContainer->setContentSize({ 225, 30 });
m_downloadBar = Slider::create(nullptr, nullptr);
m_downloadBar->m_touchLogic->m_thumb->setVisible(false);
m_downloadBar->setLayoutOptions(AxisLayoutOptions::create()->setMaxScale(.35f));
m_downloadBarContainer->addChildAtPosition(m_downloadBar, Anchor::Center, ccp(0, 0), ccp(0, 0));
m_infoContainer->addChild(m_downloadBarContainer);
m_downloadWaiting = CCNode::create();
m_downloadWaiting->setContentSize({ 225, 30 });
auto downloadWaitingLabel = CCLabelBMFont::create("Preparing Download...", "bigFont.fnt");
downloadWaitingLabel->setScale(.75f);
m_downloadWaiting->addChildAtPosition(
downloadWaitingLabel, Anchor::Left,
ccp(m_downloadWaiting->getContentHeight(), 0), ccp(0, .5f)
);
auto downloadWaitingSpinner = createLoadingCircle(20);
m_downloadWaiting->addChildAtPosition(
downloadWaitingSpinner, Anchor::Left,
ccp(m_downloadWaiting->getContentHeight() / 2, 0)
);
m_infoContainer->addChild(m_downloadWaiting);
this->addChild(m_infoContainer);
m_viewMenu = CCMenu::create();
@ -142,16 +170,47 @@ bool ModItem::init(ModSource&& source) {
this->updateState();
// Only listen for updates on this mod specifically
m_updateStateListener.setFilter(UpdateModListStateFilter(UpdateModState(m_source.getID())));
m_updateStateListener.bind([this](auto) { this->updateState(); });
m_updateStateListener.setFilter(UpdateModListStateFilter(UpdateModState(m_source.getID())));
m_downloadListener.bind([this](auto) { this->updateState(); });
m_downloadListener.setFilter(server::ModDownloadFilter(m_source.getID()));
return true;
}
void ModItem::updateState() {
auto wantsRestart = m_source.wantsRestart();
m_restartRequiredLabel->setVisible(wantsRestart);
m_developers->setVisible(!wantsRestart);
auto download = server::ModDownloadManager::get()->getDownload(m_source.getID());
// If there is an active download ongoing, show that in place of developer name
if (download && download->isActive()) {
m_updateBtn->setVisible(false);
m_restartRequiredLabel->setVisible(false);
m_developers->setVisible(false);
auto status = download->getStatus();
if (auto prog = std::get_if<server::DownloadStatusDownloading>(&status)) {
m_downloadWaiting->setVisible(false);
m_downloadBarContainer->setVisible(true);
m_downloadBar->setValue(prog->percentage / 100.f);
m_downloadBar->updateBar();
}
else {
m_downloadBarContainer->setVisible(false);
m_downloadWaiting->setVisible(true);
// Make sure the spinner is spinning by ticking its setVisible
m_downloadWaiting->getChildByID("loading-spinner")->setVisible(true);
}
}
// Otherwise show "Restart Required" button if needed in place of dev name
else {
m_restartRequiredLabel->setVisible(wantsRestart);
m_developers->setVisible(!wantsRestart);
m_downloadBarContainer->setVisible(false);
m_downloadWaiting->setVisible(false);
}
m_infoContainer->updateLayout();
// Set default colors based on source to start off with
@ -174,7 +233,10 @@ void ModItem::updateState() {
},
});
if (auto update = m_source.hasUpdates()) {
if (
auto update = m_source.hasUpdates();
update && !(download && (download->isActive() || download->isDone()))
) {
m_updateBtn->setVisible(true);
auto updateString = m_source.getMetadata().getVersion().toString() + " -> " + update->version.toString();
m_versionLabel->setString(updateString.c_str());
@ -276,12 +338,7 @@ void ModItem::onEnable(CCObject*) {
}
void ModItem::onInstall(CCObject*) {
if (auto data = m_source.asServer()) {
}
// Update state of the mod item
UpdateModListStateEvent(UpdateModState(m_source.getID())).post();
server::ModDownloadManager::get()->startDownload(m_source.getID(), std::nullopt);
}
ModItem* ModItem::create(ModSource&& source) {

View file

@ -2,6 +2,7 @@
#include <Geode/ui/General.hpp>
#include <server/Server.hpp>
#include <server/DownloadManager.hpp>
#include "../sources/ModSource.hpp"
#include "../UpdateModListState.hpp"
@ -19,11 +20,15 @@ protected:
CCNode* m_developers;
CCLabelBMFont* m_developerLabel;
ButtonSprite* m_restartRequiredLabel;
CCNode* m_downloadWaiting;
CCNode* m_downloadBarContainer;
Slider* m_downloadBar;
CCMenu* m_viewMenu;
CCMenuItemToggler* m_enableToggle = nullptr;
CCMenuItemSpriteExtra* m_updateBtn = nullptr;
EventListener<UpdateModListStateFilter> m_updateStateListener;
EventListener<server::ServerRequest<std::optional<server::ServerModUpdate>>> m_checkUpdateListener;
EventListener<server::ModDownloadFilter> m_downloadListener;
std::optional<server::ServerModUpdate> m_availableUpdate;
/**

View file

@ -30,6 +30,79 @@ bool ModList::init(ModListSource* src, CCSize const& size) {
m_topContainer->setContentWidth(size.width);
m_topContainer->setAnchorPoint({ .5f, 1.f });
// Check for updates on installed mods, and show an update all button if there are some
if (typeinfo_cast<InstalledModListSource*>(m_source)) {
m_checkUpdatesListener.bind(this, &ModList::onCheckUpdates);
m_checkUpdatesListener.setFilter(ModsLayer::checkInstalledModsForUpdates());
m_updateAllContainer = CCNode::create();
m_updateAllContainer->ignoreAnchorPointForPosition(false);
m_updateAllContainer->setContentSize({ size.width, 30 });
m_updateAllContainer->setVisible(false);
auto updateAllBG = CCLayerGradient::create(
"mod-list-updates-available-bg"_cc4b,
"mod-list-updates-available-bg-2"_cc4b,
ccp(1, -.5f)
);
updateAllBG->setContentSize(m_updateAllContainer->getContentSize());
updateAllBG->ignoreAnchorPointForPosition(false);
updateAllBG->addChildAtPosition(
CCLayerColor::create("mod-list-bg"_cc4b, m_updateAllContainer->getContentWidth(), 1),
Anchor::TopLeft
);
updateAllBG->addChildAtPosition(
CCLayerColor::create("mod-list-bg"_cc4b, m_updateAllContainer->getContentWidth(), 1),
Anchor::BottomLeft, ccp(0, -1)
);
m_updateAllContainer->addChildAtPosition(updateAllBG, Anchor::Center);
m_updateCountLabel = TextArea::create("", "bigFont.fnt", .35f, size.width / 2 - 30, ccp(0, 1), 12.f, false);
m_updateAllContainer->addChildAtPosition(m_updateCountLabel, Anchor::Left, ccp(10, 0), ccp(0, 0));
m_updateAllMenu = CCMenu::create();
m_updateAllMenu->setContentWidth(size.width / 2);
m_updateAllMenu->setAnchorPoint({ 1, .5f });
auto showUpdatesSpr = createGeodeButton(
CCSprite::createWithSpriteFrameName("GJ_filterIcon_001.png"),
"Show Updates", "GE_button_01.png"_spr
);
auto hideUpdatesSpr = createGeodeButton(
CCSprite::createWithSpriteFrameName("GJ_filterIcon_001.png"),
"Hide Updates", "GE_button_05.png"_spr
);
m_toggleUpdatesOnlyBtn = CCMenuItemToggler::create(
showUpdatesSpr, hideUpdatesSpr, this, menu_selector(ModList::onToggleUpdates)
);
m_toggleUpdatesOnlyBtn->m_notClickable = true;
m_updateAllMenu->addChild(m_toggleUpdatesOnlyBtn);
auto updateAllSpr = createGeodeButton(
CCSprite::createWithSpriteFrameName("update.png"_spr),
"Update All", "GE_button_01.png"_spr
);
m_updateAllBtn = CCMenuItemSpriteExtra::create(
updateAllSpr, this, menu_selector(ModList::onUpdateAll)
);
m_updateAllMenu->addChild(m_updateAllBtn);
m_updateAllLoadingCircle = createLoadingCircle(32);
m_updateAllMenu->addChild(m_updateAllLoadingCircle);
m_updateAllMenu->setLayout(
RowLayout::create()
->setAxisAlignment(AxisAlignment::End)
->setDefaultScaleLimits(.1f, .75f)
);
m_updateAllMenu->getLayout()->ignoreInvisibleChildren(true);
m_updateAllContainer->addChildAtPosition(m_updateAllMenu, Anchor::Right, ccp(-10, 0));
m_topContainer->addChild(m_updateAllContainer);
}
m_searchMenu = CCNode::create();
m_searchMenu->ignoreAnchorPointForPosition(false);
m_searchMenu->setContentSize({ size.width, 30 });
@ -104,65 +177,6 @@ bool ModList::init(ModListSource* src, CCSize const& size) {
m_topContainer->addChild(m_searchMenu);
// Check for updates on installed mods, and show an update all button if there are some
if (typeinfo_cast<InstalledModListSource*>(m_source)) {
m_checkUpdatesListener.bind(this, &ModList::onCheckUpdates);
m_checkUpdatesListener.setFilter(ModsLayer::checkInstalledModsForUpdates());
m_updateAllMenu = CCNode::create();
m_updateAllMenu->ignoreAnchorPointForPosition(false);
m_updateAllMenu->setContentSize({ size.width, 30 });
m_updateAllMenu->setVisible(false);
auto updateAllBG = CCLayerGradient::create(
ColorProvider::get()->color("mod-list-updates-available-bg"_spr),
ColorProvider::get()->color("mod-list-updates-available-bg-2"_spr),
ccp(1, -.5f)
);
updateAllBG->setContentSize(m_updateAllMenu->getContentSize());
updateAllBG->ignoreAnchorPointForPosition(false);
m_updateAllMenu->addChildAtPosition(updateAllBG, Anchor::Center);
m_updateCountLabel = TextArea::create("", "bigFont.fnt", .35f, size.width / 2 - 30, ccp(0, 1), 12.f, false);
m_updateAllMenu->addChildAtPosition(m_updateCountLabel, Anchor::Left, ccp(10, 0), ccp(0, 0));
auto updateAllMenu = CCMenu::create();
updateAllMenu->setContentWidth(size.width / 2);
updateAllMenu->setAnchorPoint({ 1, .5f });
auto showUpdatesSpr = createGeodeButton(
CCSprite::createWithSpriteFrameName("GJ_filterIcon_001.png"),
"Show Updates", "GE_button_01.png"_spr
);
auto hideUpdatesSpr = createGeodeButton(
CCSprite::createWithSpriteFrameName("GJ_filterIcon_001.png"),
"Hide Updates", "GE_button_05.png"_spr
);
m_toggleUpdatesOnlyBtn = CCMenuItemToggler::create(
showUpdatesSpr, hideUpdatesSpr, this, menu_selector(ModList::onToggleUpdates)
);
m_toggleUpdatesOnlyBtn->m_notClickable = true;
updateAllMenu->addChild(m_toggleUpdatesOnlyBtn);
auto updateAllSpr = createGeodeButton(
CCSprite::createWithSpriteFrameName("update.png"_spr),
"Update All", "GE_button_01.png"_spr
);
auto updateAllBtn = CCMenuItemSpriteExtra::create(
updateAllSpr, this, menu_selector(ModList::onUpdateAll)
);
updateAllMenu->addChild(updateAllBtn);
updateAllMenu->setLayout(
RowLayout::create()
->setAxisAlignment(AxisAlignment::End)
->setDefaultScaleLimits(.1f, 1.f)
);
m_updateAllMenu->addChildAtPosition(updateAllMenu, Anchor::Right, ccp(-10, 0));
m_topContainer->addChild(m_updateAllMenu);
}
m_topContainer->setLayout(
ColumnLayout::create()
->setGap(0)
@ -255,6 +269,8 @@ bool ModList::init(ModListSource* src, CCSize const& size) {
m_invalidateCacheListener.bind(this, &ModList::onInvalidateCache);
m_invalidateCacheListener.setFilter(InvalidateCacheFilter(m_source));
m_downloadListener.bind([this](auto) { this->updateTopContainer(); });
this->gotoPage(0);
this->updateTopContainer();
@ -355,7 +371,7 @@ void ModList::onCheckUpdates(typename server::ServerRequest<std::vector<std::str
fmt = fmt::format("There are <cg>{}</c> updates available!", mods.size());
}
m_updateCountLabel->setString(fmt.c_str());
m_updateAllMenu->setVisible(true);
m_updateAllContainer->setVisible(true);
this->updateTopContainer();
}
}
@ -393,6 +409,15 @@ void ModList::updateTopContainer() {
m_list->m_contentLayer->getContentHeight() - m_list->getContentHeight()
) * oldPosition);
// If there are active downloads, hide the Update All button
if (m_updateAllContainer) {
auto shouldShowLoading = server::ModDownloadManager::get()->hasActiveDownloads();
m_updateAllBtn->setEnabled(!shouldShowLoading);
static_cast<IconButtonSprite*>(m_updateAllBtn->getNormalImage())->setOpacity(shouldShowLoading ? 90 : 255);
m_updateAllLoadingCircle->setVisible(shouldShowLoading);
m_updateAllMenu->updateLayout();
}
// ModList uses an anchor layout, so this puts the list in the right place
this->updateLayout();
}
@ -504,7 +529,9 @@ void ModList::onToggleUpdates(CCObject*) {
}
}
void ModList::onUpdateAll(CCObject*) {}
void ModList::onUpdateAll(CCObject*) {
server::ModDownloadManager::get()->startUpdateAll();
}
size_t ModList::getPage() const {
return m_page;

View file

@ -6,6 +6,7 @@
#include <Geode/ui/TextInput.hpp>
#include "ModItem.hpp"
#include "../sources/ModListSource.hpp"
#include <server/DownloadManager.hpp>
using namespace geode::prelude;
@ -32,12 +33,16 @@ protected:
CCMenuItemSpriteExtra* m_pageNextBtn;
CCNode* m_topContainer;
CCNode* m_searchMenu;
CCNode* m_updateAllMenu = nullptr;
CCNode* m_updateAllContainer = nullptr;
CCMenu* m_updateAllMenu = nullptr;
CCMenuItemSpriteExtra* m_updateAllBtn = nullptr;
CCNode* m_updateAllLoadingCircle = nullptr;
CCMenuItemToggler* m_toggleUpdatesOnlyBtn = nullptr;
TextArea* m_updateCountLabel = nullptr;
TextInput* m_searchInput;
EventListener<InvalidateCacheFilter> m_invalidateCacheListener;
EventListener<server::ServerRequest<std::vector<std::string>>> m_checkUpdatesListener;
EventListener<server::ModDownloadFilter> m_downloadListener;
bool m_bigSize = false;
std::atomic<size_t> m_searchInputThreads = 0;

View file

@ -0,0 +1,162 @@
#include "ConfirmInstallPopup.hpp"
#include <Geode/ui/GeodeUI.hpp>
class SmallModItem : public CCNode {
protected:
bool init(ModMetadata const& metadata) {
if (!CCNode::init())
return false;
this->setContentSize({ 60, 80 });
auto logo = createServerModLogo(metadata.getID());
limitNodeSize(logo, { 45, 45 }, 5.f, .1f);
this->addChildAtPosition(logo, Anchor::Center, ccp(0, 10));
auto title = CCLabelBMFont::create(metadata.getName().c_str(), "bigFont.fnt");
title->limitLabelWidth(m_obContentSize.width, .3f, .1f);
this->addChildAtPosition(title, Anchor::Center, ccp(0, -20));
auto developer = CCLabelBMFont::create(fmt::format("by {}", metadata.getDeveloper()).c_str(), "goldFont.fnt");
developer->limitLabelWidth(m_obContentSize.width, .3f, .1f);
this->addChildAtPosition(developer, Anchor::Center, ccp(0, -30));
return true;
}
public:
static SmallModItem* create(ModMetadata const& metadata) {
auto ret = new SmallModItem();
if (ret && ret->init(metadata)) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
};
bool ConfirmInstallPopup::setup() {
m_noElasticity = true;
this->setTitle("Customize Installation");
auto installationsTitle = CCLabelBMFont::create("You are Installing:", "bigFont.fnt");
installationsTitle->setScale(.5f);
m_mainLayer->addChildAtPosition(installationsTitle, Anchor::Center, ccp(0, 100));
m_installations = CCNode::create();
m_installations->setAnchorPoint({ .5f, .5f });
m_installations->setContentSize({ m_size.width, 60 });
m_installations->setLayout(
RowLayout::create()
->setDefaultScaleLimits(.1f, .65f)
->setGrowCrossAxis(true)
->setCrossAxisOverflow(false)
);
m_mainLayer->addChildAtPosition(m_installations, Anchor::Center, ccp(0, 60));
auto dependenciesTitle = CCLabelBMFont::create("Which Depend(s) on:", "bigFont.fnt");
dependenciesTitle->setScale(.5f);
m_mainLayer->addChildAtPosition(dependenciesTitle, Anchor::Center, ccp(0, 20));
m_dependencies = CCNode::create();
m_dependencies->setAnchorPoint({ .5f, .5f });
m_dependencies->setContentSize({ m_size.width, 60 });
m_dependencies->setLayout(
RowLayout::create()
->setDefaultScaleLimits(.1f, .65f)
->setGrowCrossAxis(true)
->setCrossAxisOverflow(false)
);
m_mainLayer->addChildAtPosition(m_dependencies, Anchor::Center, ccp(0, -20));
auto incompatablitiesTitle = CCLabelBMFont::create("But is/are Incompatible with:", "bigFont.fnt");
incompatablitiesTitle->setScale(.5f);
m_mainLayer->addChildAtPosition(incompatablitiesTitle, Anchor::Center, ccp(0, -60));
m_incompatabilities = CCNode::create();
m_incompatabilities->setAnchorPoint({ .5f, .5f });
m_incompatabilities->setContentSize({ m_size.width, 60 });
m_incompatabilities->setLayout(
RowLayout::create()
->setDefaultScaleLimits(.1f, .65f)
->setGrowCrossAxis(true)
->setCrossAxisOverflow(false)
);
m_mainLayer->addChildAtPosition(m_incompatabilities, Anchor::Center, ccp(0, -100));
m_downloadListener.bind([this](auto) { this->updateState(); });
this->updateState();
return true;
}
void ConfirmInstallPopup::updateState() {
m_installations->removeAllChildren();
m_dependencies->removeAllChildren();
m_incompatabilities->removeAllChildren();
for (auto& download : server::ModDownloadManager::get()->getDownloads()) {
auto status = download.getStatus();
if (auto confirm = std::get_if<server::DownloadStatusConfirm>(&status)) {
if (download.getDependencyFor()) {
m_dependencies->addChild(SmallModItem::create(confirm->version.metadata));
}
else {
m_installations->addChild(SmallModItem::create(confirm->version.metadata));
}
for (auto& inc : confirm->version.metadata.getIncompatibilities()) {
if (inc.mod) {
m_installations->addChild(SmallModItem::create(inc.mod->getMetadata()));
}
}
}
}
m_installations->updateLayout();
m_dependencies->updateLayout();
m_incompatabilities->updateLayout();
}
ConfirmInstallPopup* ConfirmInstallPopup::create() {
auto ret = new ConfirmInstallPopup();
if (ret && ret->init(350, 280)) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
void ConfirmInstallPopup::askForCustomize() {
size_t confirmCount = 0;
size_t dependencyCount = 0;
for (auto& download : server::ModDownloadManager::get()->getDownloads()) {
auto status = download.getStatus();
if (auto confirm = std::get_if<server::DownloadStatusConfirm>(&status)) {
confirmCount += 1;
if (download.getDependencyFor()) {
dependencyCount += 1;
}
}
}
createQuickPopup(
"Confirm Install",
fmt::format(
"<cy>{} mods</c> will be installed, out of which <cr>{}</c> are <cr>dependencies</c>. "
"Do you want to <cb>customize</c> the installation, or continue with default options?",
confirmCount, dependencyCount
),
"Customize", "Continue",
[](auto*, bool btn2) {
if (btn2) {
server::ModDownloadManager::get()->confirmAll();
}
else {
ConfirmInstallPopup::create()->show();
}
}
);
}

View file

@ -0,0 +1,23 @@
#pragma once
#include <Geode/ui/Popup.hpp>
#include <server/DownloadManager.hpp>
#include "../GeodeStyle.hpp"
using namespace geode::prelude;
class ConfirmInstallPopup : public GeodePopup<> {
protected:
CCNode* m_installations;
CCNode* m_dependencies;
CCNode* m_incompatabilities;
EventListener<server::ModDownloadFilter> m_downloadListener;
bool setup() override;
void updateState();
public:
static ConfirmInstallPopup* create();
static void askForCustomize();
};

View file

@ -267,7 +267,7 @@ bool ModPopup::setup(ModSource&& src) {
);
updateModSpr->setScale(.5f);
m_updateBtn = CCMenuItemSpriteExtra::create(
updateModSpr, this, nullptr
updateModSpr, this, menu_selector(ModPopup::onInstall)
);
m_installMenu->addChild(m_updateBtn);
@ -316,7 +316,7 @@ bool ModPopup::setup(ModSource&& src) {
);
installModSpr->setScale(.5f);
m_installBtn = CCMenuItemSpriteExtra::create(
installModSpr, this, nullptr
installModSpr, this, menu_selector(ModPopup::onInstall)
);
m_installMenu->addChild(m_installBtn);
@ -331,6 +331,17 @@ bool ModPopup::setup(ModSource&& src) {
);
m_installMenu->addChild(m_uninstallBtn);
auto cancelDownloadSpr = createGeodeButton(
CCSprite::createWithSpriteFrameName("GJ_deleteIcon_001.png"),
"Cancel",
"GE_button_05.png"_spr
);
cancelDownloadSpr->setScale(.5f);
m_cancelBtn = CCMenuItemSpriteExtra::create(
cancelDownloadSpr, this, menu_selector(ModPopup::onCancelDownload)
);
m_installMenu->addChild(m_cancelBtn);
m_installStatusLabel = CCLabelBMFont::create("", "bigFont.fnt");
m_installStatusLabel->setOpacity(120);
m_installStatusLabel->setVisible(false);
@ -489,8 +500,11 @@ bool ModPopup::setup(ModSource&& src) {
}
// Only listen for updates on this mod specifically
m_updateStateListener.setFilter(UpdateModListStateFilter(UpdateModState(m_source.getID())));
m_updateStateListener.bind([this](auto) { this->updateState(); });
m_updateStateListener.setFilter(UpdateModListStateFilter(UpdateModState(m_source.getID())));
m_downloadListener.bind([this](auto) { this->updateState(); });
m_downloadListener.setFilter(m_source.getID());
return true;
}
@ -500,16 +514,6 @@ void ModPopup::updateState() {
auto asServer = m_source.asServer();
auto wantsRestart = m_source.wantsRestart();
m_enableBtn->toggle(asMod && asMod->isOrWillBeEnabled());
m_enableBtn->setVisible(asMod && asMod->getRequestedAction() == ModRequestedAction::None);
m_reenableBtn->toggle(m_enableBtn->isToggled());
m_reenableBtn->setVisible(asMod && modRequestedActionIsToggle(asMod->getRequestedAction()));
m_updateBtn->setVisible(m_source.hasUpdates().has_value() && asMod->getRequestedAction() == ModRequestedAction::None);
m_installBtn->setVisible(asServer);
m_uninstallBtn->setVisible(asMod && asMod->getRequestedAction() == ModRequestedAction::None);
m_installBG->setColor(wantsRestart ? to3B(ColorProvider::get()->color("mod-list-restart-required-label"_spr)) : ccc3(0, 0, 0));
m_installBG->setOpacity(wantsRestart ? 40 : 75);
m_restartRequiredLabel->setVisible(wantsRestart);
@ -529,6 +533,18 @@ void ModPopup::updateState() {
m_enabledStatusLabel->setVisible(false);
}
m_cancelBtn->setVisible(false);
m_enableBtn->toggle(asMod && asMod->isOrWillBeEnabled());
m_enableBtn->setVisible(asMod && asMod->getRequestedAction() == ModRequestedAction::None);
m_reenableBtn->toggle(m_enableBtn->isToggled());
m_reenableBtn->setVisible(asMod && modRequestedActionIsToggle(asMod->getRequestedAction()));
m_updateBtn->setVisible(m_source.hasUpdates().has_value() && asMod->getRequestedAction() == ModRequestedAction::None);
m_installBtn->setVisible(asServer);
m_uninstallBtn->setVisible(asMod && asMod->getRequestedAction() == ModRequestedAction::None);
if (asMod && modRequestedActionIsUninstall(asMod->getRequestedAction())) {
m_installStatusLabel->setString("Mod has been uninstalled");
m_installStatusLabel->setVisible(true);
@ -558,6 +574,60 @@ void ModPopup::updateState() {
m_installStatusLabel->setVisible(true);
}
auto download = server::ModDownloadManager::get()->getDownload(m_source.getID());
if (download) {
if (download->isActive()) {
m_enableBtn->setVisible(false);
m_reenableBtn->setVisible(false);
m_updateBtn->setVisible(false);
m_installBtn->setVisible(false);
m_uninstallBtn->setVisible(false);
m_cancelBtn->setVisible(true);
auto status = download->getStatus();
if (auto d = std::get_if<server::DownloadStatusDownloading>(&status)) {
m_enabledStatusLabel->setString(fmt::format("Downloading {}%", d->percentage).c_str());
m_enabledStatusLabel->setColor(ccWHITE);
m_enabledStatusLabel->setVisible(true);
// todo: progress bar
}
else {
m_enabledStatusLabel->setString("Preparing");
m_enabledStatusLabel->setColor(ccWHITE);
m_enabledStatusLabel->setVisible(true);
// todo: spinner
}
}
else {
std::visit(makeVisitor {
[this](server::DownloadStatusError const& e) {
m_enabledStatusLabel->setString("Error");
m_enabledStatusLabel->setColor(to3B(ColorProvider::get()->color("mod-list-disabled"_spr)));
m_enabledStatusLabel->setVisible(true);
// todo: show error details somewhere (like an info button)
},
[this](server::DownloadStatusCancelled const&) {
m_enabledStatusLabel->setString("Cancelled");
m_enabledStatusLabel->setColor(to3B(ColorProvider::get()->color("mod-list-disabled"_spr)));
m_enabledStatusLabel->setVisible(true);
},
[this](server::DownloadStatusDone const&) {
m_enableBtn->setVisible(false);
m_reenableBtn->setVisible(false);
m_updateBtn->setVisible(false);
m_installBtn->setVisible(false);
m_uninstallBtn->setVisible(false);
m_cancelBtn->setVisible(false);
m_installStatusLabel->setString("Mod has been installed");
m_installStatusLabel->setVisible(true);
},
// rest are unreachable due to the isActive() check
[this](auto const&) {},
}, download->getStatus());
}
}
m_installMenu->updateLayout();
}
@ -765,6 +835,11 @@ void ModPopup::onEnable(CCObject*) {
UpdateModListStateEvent(UpdateModState(m_source.getID())).post();
}
void ModPopup::onInstall(CCObject*) {
server::ModDownloadManager::get()->startDownload(m_source.getID(), std::nullopt);
this->onClose(nullptr);
}
void ModPopup::onUninstall(CCObject*) {
if (auto mod = m_source.asMod()) {
ConfirmUninstallPopup::create(mod)->show();
@ -778,6 +853,13 @@ void ModPopup::onUninstall(CCObject*) {
}
}
void ModPopup::onCancelDownload(CCObject*) {
auto download = server::ModDownloadManager::get()->getDownload(m_source.getID());
if (download) {
download->cancel();
}
}
void ModPopup::onLink(CCObject* sender) {
auto url = static_cast<CCString*>(static_cast<CCNode*>(sender)->getUserObject("url"));
web::openLinkInBrowser(url->getCString());

View file

@ -5,6 +5,7 @@
#include "../sources/ModSource.hpp"
#include "../GeodeStyle.hpp"
#include "../UpdateModListState.hpp"
#include <server/DownloadManager.hpp>
using namespace geode::prelude;
@ -25,6 +26,7 @@ protected:
CCMenuItemSpriteExtra* m_uninstallBtn;
CCMenuItemSpriteExtra* m_installBtn;
CCMenuItemSpriteExtra* m_updateBtn;
CCMenuItemSpriteExtra* m_cancelBtn;
CCLabelBMFont* m_installStatusLabel;
CCScale9Sprite* m_installBG;
CCLabelBMFont* m_enabledStatusLabel;
@ -36,6 +38,7 @@ protected:
EventListener<server::ServerRequest<std::unordered_set<std::string>>> m_tagsListener;
EventListener<server::ServerRequest<std::optional<server::ServerModUpdate>>> m_checkUpdateListener;
EventListener<UpdateModListStateFilter> m_updateStateListener;
EventListener<server::ModDownloadFilter> m_downloadListener;
bool setup(ModSource&& src) override;
void updateState();
@ -51,7 +54,9 @@ protected:
void loadTab(Tab tab);
void onTab(CCObject* sender);
void onEnable(CCObject*);
void onInstall(CCObject*);
void onUninstall(CCObject*);
void onCancelDownload(CCObject*);
void onLink(CCObject*);
void onSupport(CCObject*);

View file

@ -1,4 +1,5 @@
#include "ModListSource.hpp"
#include <server/DownloadManager.hpp>
#define FTS_FUZZY_MATCH_IMPLEMENTATION
#include <Geode/external/fts/fts_fuzzy_match.h>
@ -250,15 +251,6 @@ InvalidateQueryAfter<InstalledModsQuery> InstalledModListSource::getQueryMut() {
return InvalidateQueryAfter(m_query, this);
}
bool InstalledModListSource::wantsRestart() const {
for (auto mod : Loader::get()->getAllMods()) {
if (mod->getRequestedAction() != ModRequestedAction::None) {
return true;
}
}
return false;
}
void ServerModListSource::resetQuery() {
switch (m_type) {
case ServerModListType::Download: {
@ -357,11 +349,6 @@ InvalidateQueryAfter<server::ModsQuery> ServerModListSource::getQueryMut() {
return InvalidateQueryAfter(m_query, this);
}
bool ServerModListSource::wantsRestart() const {
// todo
return false;
}
void ModPackListSource::resetQuery() {}
ModPackListSource::ProviderTask ModPackListSource::fetchPage(size_t page, size_t pageSize, bool forceUpdate) {
return ProviderTask::immediate(Err(LoadPageError("Coming soon ;)")));
@ -381,10 +368,6 @@ std::unordered_set<std::string> ModPackListSource::getModTags() const {
}
void ModPackListSource::setModTags(std::unordered_set<std::string> const& set) {}
bool ModPackListSource::wantsRestart() const {
return false;
}
void clearAllModListSourceCaches() {
InstalledModListSource::get(false)->clearCache();
InstalledModListSource::get(true)->clearCache();
@ -396,3 +379,14 @@ void clearAllModListSourceCaches() {
ModPackListSource::get()->clearCache();
}
bool isRestartRequired() {
for (auto mod : Loader::get()->getAllMods()) {
if (mod->getRequestedAction() != ModRequestedAction::None) {
return true;
}
}
if (server::ModDownloadManager::get()->wantsRestart()) {
return true;
}
return false;
}

View file

@ -81,11 +81,6 @@ public:
PageLoadTask loadPage(size_t page, bool forceUpdate = false);
std::optional<size_t> getPageCount() const;
std::optional<size_t> getItemCount() const;
/**
* True if the source consists only of installed mods
*/
virtual bool wantsRestart() const = 0;
};
template <class T>
@ -123,8 +118,6 @@ public:
InstalledModsQuery const& getQuery() const;
InvalidateQueryAfter<InstalledModsQuery> getQueryMut();
bool wantsRestart() const override;
};
enum class ServerModListType {
@ -153,8 +146,6 @@ public:
server::ModsQuery const& getQuery() const;
InvalidateQueryAfter<server::ModsQuery> getQueryMut();
bool wantsRestart() const override;
};
class ModPackListSource : public ModListSource {
@ -170,8 +161,7 @@ public:
std::unordered_set<std::string> getModTags() const override;
void setModTags(std::unordered_set<std::string> const& tags) override;
bool wantsRestart() const override;
};
void clearAllModListSourceCaches();
bool isRestartRequired();

View file

@ -1,5 +1,6 @@
#include "ModSource.hpp"
#include <Geode/ui/GeodeUI.hpp>
#include <server/DownloadManager.hpp>
ModSource::ModSource(Mod* mod) : m_value(mod) {}
ModSource::ModSource(server::ServerModMetadata&& metadata) : m_value(metadata) {}
@ -37,12 +38,16 @@ CCNode* ModSource::createModLogo() const {
}, m_value);
}
bool ModSource::wantsRestart() const {
// If some download has been done for this mod, always want a restart
auto download = server::ModDownloadManager::get()->getDownload(this->getID());
if (download && download->isDone()) {
return true;
}
return std::visit(makeVisitor {
[](Mod* mod) {
return mod->getRequestedAction() != ModRequestedAction::None;
},
[](server::ServerModMetadata const& metdata) {
// todo: check if the mod has been installed
return false;
}
}, m_value);