mirror of
https://github.com/geode-sdk/geode.git
synced 2025-02-17 00:30:26 -05:00
fetch mod info from server on installed mods
This commit is contained in:
parent
0a17fcc985
commit
abe41a79a3
6 changed files with 185 additions and 42 deletions
|
@ -6,19 +6,53 @@ using namespace server;
|
|||
|
||||
#define GEODE_GD_VERSION_STR GEODE_STR(GEODE_GD_VERSION)
|
||||
|
||||
static const char* jsonTypeToString(matjson::Type const& type) {
|
||||
switch (type) {
|
||||
case matjson::Type::Object: return "object";
|
||||
case matjson::Type::Array: return "array";
|
||||
case matjson::Type::Bool: return "boolean";
|
||||
case matjson::Type::Number: return "number";
|
||||
case matjson::Type::String: return "string";
|
||||
case matjson::Type::Null: return "null";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static Result<matjson::Value, ServerError> parseServerPayload(web::WebResponse&& response) {
|
||||
auto asJson = response.json();
|
||||
if (!asJson) {
|
||||
return Err(ServerError(response.code(), "Response was not valid JSON: {}", asJson.unwrapErr()));
|
||||
}
|
||||
auto json = std::move(asJson).unwrap();
|
||||
if (!json.is_object()) {
|
||||
return Err(ServerError(response.code(), "Expected object, got {}", jsonTypeToString(json.type())));
|
||||
}
|
||||
auto obj = json.as_object();
|
||||
if (!obj.contains("payload")) {
|
||||
return Err(ServerError(response.code(), "Object does not contain \"payload\" key - got {}", json.dump()));
|
||||
}
|
||||
return Ok(obj["payload"]);
|
||||
}
|
||||
|
||||
static void parseServerError(auto reject, auto error) {
|
||||
// The server should return errors as `{ "error": "...", "payload": "" }`
|
||||
if (auto json = error.json()) {
|
||||
reject(ServerError(
|
||||
"Error code: {}; details: {}",
|
||||
error.code(), json.unwrap().template get<std::string>("error")
|
||||
));
|
||||
if (auto asJson = error.json()) {
|
||||
auto json = asJson.unwrap();
|
||||
if (json.is_object() && json.contains("error")) {
|
||||
reject(ServerError(
|
||||
error.code(),
|
||||
"{}", json.template get<std::string>("error")
|
||||
));
|
||||
}
|
||||
else {
|
||||
reject(ServerError(error.code(), "Unknown (not valid JSON)"));
|
||||
}
|
||||
}
|
||||
// But if we get something else for some reason, return that
|
||||
else {
|
||||
reject(ServerError(
|
||||
"Error code: {}; details: {}",
|
||||
error.code(), error.string().unwrapOr("Unknown (not a valid string)")
|
||||
error.code(),
|
||||
"{}", error.string().unwrapOr("Unknown (not a valid string)")
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -178,7 +212,7 @@ Result<ServerModMetadata> ServerModMetadata::parse(matjson::Value const& raw) {
|
|||
Result<ServerModsList> ServerModsList::parse(matjson::Value const& raw) {
|
||||
auto json = raw;
|
||||
JsonChecker checker(json);
|
||||
auto payload = checker.root("ServerModsList").obj().needs("payload").obj();
|
||||
auto payload = checker.root("ServerModsList").obj();
|
||||
|
||||
auto list = ServerModsList();
|
||||
for (auto item : payload.needs("data").iterate()) {
|
||||
|
@ -251,16 +285,18 @@ ServerPromise<ServerModsList> server::getMods(ModsQuery const& query) {
|
|||
return ServerPromise<ServerModsList>([req = std::move(req)](auto resolve, auto reject, auto progress, auto cancel) mutable {
|
||||
req.get(getServerAPIBaseURL() + "/mods")
|
||||
.then([resolve, reject](auto value) {
|
||||
// Validate that the response was JSON
|
||||
auto asJson = value.json();
|
||||
if (!asJson) {
|
||||
return reject(ServerError("Response was not valid JSON: {}", asJson.unwrapErr()));
|
||||
}
|
||||
// Store the code, since the value is moved afterwards
|
||||
auto code = value.code();
|
||||
|
||||
// Parse payload
|
||||
auto payload = parseServerPayload(std::move(value));
|
||||
if (!payload) {
|
||||
return reject(payload.unwrapErr());
|
||||
}
|
||||
// Parse response
|
||||
auto list = ServerModsList::parse(asJson.unwrap());
|
||||
auto list = ServerModsList::parse(payload.unwrap());
|
||||
if (!list) {
|
||||
return reject(ServerError("Unable to parse response: {}", list.unwrapErr()));
|
||||
return reject(ServerError(code, "Unable to parse response: {}", list.unwrapErr()));
|
||||
}
|
||||
resolve(list.unwrap());
|
||||
})
|
||||
|
@ -278,11 +314,40 @@ ServerPromise<ServerModsList> server::getMods(ModsQuery const& query) {
|
|||
});
|
||||
}
|
||||
|
||||
ServerPromise<ServerModMetadata> server::getMod(std::string const& id) {
|
||||
auto req = web::WebRequest();
|
||||
req.userAgent(getServerUserAgent());
|
||||
return ServerPromise<ServerModMetadata>([req = std::move(req), id](auto resolve, auto reject, auto progress, auto cancel) mutable {
|
||||
req.get(getServerAPIBaseURL() + "/mods/" + id)
|
||||
.then([resolve, reject](auto value) {
|
||||
// Store the code, since the value is moved afterwards
|
||||
auto code = value.code();
|
||||
|
||||
// Parse payload
|
||||
auto payload = parseServerPayload(std::move(value));
|
||||
if (!payload) {
|
||||
return reject(payload.unwrapErr());
|
||||
}
|
||||
// Parse response
|
||||
auto list = ServerModMetadata::parse(payload.unwrap());
|
||||
if (!list) {
|
||||
return reject(ServerError(code, "Unable to parse response: {}", list.unwrapErr()));
|
||||
}
|
||||
resolve(list.unwrap());
|
||||
})
|
||||
.expect([reject](auto error) {
|
||||
parseServerError(reject, error);
|
||||
})
|
||||
.progress([progress, id](auto prog) {
|
||||
parseServerProgress(progress, prog, "Downloading logo for " + id);
|
||||
})
|
||||
.link(cancel);
|
||||
});
|
||||
}
|
||||
|
||||
ServerPromise<ByteVector> server::getModLogo(std::string const& id) {
|
||||
auto req = web::WebRequest();
|
||||
req.userAgent(getServerUserAgent());
|
||||
|
||||
req.param("id", id);
|
||||
return ServerPromise<ByteVector>([req = std::move(req), id](auto resolve, auto reject, auto progress, auto cancel) mutable {
|
||||
req.get(getServerAPIBaseURL() + "/mods/" + id + "/logo")
|
||||
.then([resolve](auto response) {
|
||||
|
|
|
@ -61,15 +61,17 @@ namespace server {
|
|||
};
|
||||
|
||||
struct ServerError {
|
||||
int code;
|
||||
std::string details;
|
||||
|
||||
ServerError() = default;
|
||||
|
||||
template <class... Args>
|
||||
ServerError(
|
||||
int code,
|
||||
fmt::string_view format,
|
||||
Args&&... args
|
||||
) : details(fmt::vformat(format, fmt::make_format_args(args...))) {}
|
||||
) : code(code), details(fmt::vformat(format, fmt::make_format_args(args...))) {}
|
||||
};
|
||||
template <class T>
|
||||
using ServerPromise = Promise<T, ServerError>;
|
||||
|
@ -77,5 +79,6 @@ namespace server {
|
|||
std::string getServerAPIBaseURL();
|
||||
std::string getServerUserAgent();
|
||||
ServerPromise<ServerModsList> getMods(ModsQuery const& query);
|
||||
ServerPromise<ServerModMetadata> getMod(std::string const& id);
|
||||
ServerPromise<ByteVector> getModLogo(std::string const& id);
|
||||
}
|
||||
|
|
|
@ -58,18 +58,18 @@ bool ModPopup::setup(ModSource&& src) {
|
|||
statsBG->setContentSize(statsContainer->getContentSize() / statsBG->getScale());
|
||||
statsContainer->addChildAtPosition(statsBG, Anchor::Center);
|
||||
|
||||
auto statsLayout = CCNode::create();
|
||||
statsLayout->setContentSize(statsContainer->getContentSize() - ccp(10, 10));
|
||||
statsLayout->setAnchorPoint({ .5f, .5f });
|
||||
m_stats = CCNode::create();
|
||||
m_stats->setContentSize(statsContainer->getContentSize() - ccp(10, 10));
|
||||
m_stats->setAnchorPoint({ .5f, .5f });
|
||||
|
||||
for (auto stat : std::initializer_list<std::tuple<const char*, const char*, std::string, ccColor3B>> {
|
||||
{ "GJ_downloadsIcon_001.png", "Downloads", "TODO", { 255, 255, 255 } },
|
||||
{ "GJ_timeIcon_001.png", "Released", "TODO", { 105, 155, 255 } },
|
||||
{ "GJ_timeIcon_001.png", "Updated", "TODO", { 105, 155, 255 } },
|
||||
{ "version.png"_spr, "Version", src.getMetadata().getVersion().toString(), { 105, 255, 155 } },
|
||||
for (auto stat : std::initializer_list<std::tuple<const char*, const char*, const char*, std::optional<std::string>>> {
|
||||
{ "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", src.getMetadata().getVersion().toString() },
|
||||
}) {
|
||||
auto container = CCNode::create();
|
||||
container->setContentSize({ statsLayout->getContentWidth(), 10 });
|
||||
container->setContentSize({ m_stats->getContentWidth(), 10 });
|
||||
|
||||
auto iconSize = container->getContentHeight();
|
||||
auto icon = CCSprite::createWithSpriteFrameName(std::get<0>(stat));
|
||||
|
@ -88,29 +88,30 @@ bool ModPopup::setup(ModSource&& src) {
|
|||
);
|
||||
container->addChildAtPosition(title, Anchor::Left, ccp(container->getContentHeight() + 5, 0));
|
||||
|
||||
auto value = CCLabelBMFont::create(std::get<2>(stat).c_str(), "bigFont.fnt");
|
||||
value->setAnchorPoint({ 1.f, .5f });
|
||||
value->setColor(std::get<3>(stat));
|
||||
limitNodeSize(
|
||||
value,
|
||||
{
|
||||
container->getContentWidth() / 2.2f,
|
||||
container->getContentHeight()
|
||||
},
|
||||
.3f, .1f
|
||||
);
|
||||
container->addChildAtPosition(value, Anchor::Right);
|
||||
if (auto value = std::get<3>(stat)) {
|
||||
this->setStatValue(container, value.value());
|
||||
}
|
||||
// Loading indicator for stats that need to be fetched from the server
|
||||
else {
|
||||
// todo: refactor these spinners into a reusable class that's not the ass LoadingCircle is
|
||||
auto spinner = CCSprite::create("loadingCircle.png");
|
||||
spinner->setBlendFunc({ GL_ONE, GL_ONE });
|
||||
spinner->runAction(CCRepeatForever::create(CCRotateBy::create(1.f, 360.f)));
|
||||
limitNodeSize(spinner, { iconSize, iconSize }, 1.f, .1f);
|
||||
spinner->setID("loading-spinner");
|
||||
container->addChildAtPosition(spinner, Anchor::Right, ccp(-iconSize / 2, 0));
|
||||
}
|
||||
|
||||
statsLayout->addChild(container);
|
||||
m_stats->addChild(container);
|
||||
}
|
||||
|
||||
statsLayout->setLayout(
|
||||
m_stats->setLayout(
|
||||
ColumnLayout::create()
|
||||
->setAxisReverse(true)
|
||||
->setDefaultScaleLimits(.1f, 1)
|
||||
->setAxisAlignment(AxisAlignment::Even)
|
||||
);
|
||||
statsContainer->addChildAtPosition(statsLayout, Anchor::Center);
|
||||
statsContainer->addChildAtPosition(m_stats, Anchor::Center);
|
||||
|
||||
leftColumn->addChild(statsContainer);
|
||||
|
||||
|
@ -161,9 +162,53 @@ bool ModPopup::setup(ModSource&& src) {
|
|||
// Select details tab
|
||||
this->loadTab(Tab::Details);
|
||||
|
||||
// Load stats from server (or just from the source if it already has them)
|
||||
m_statsListener.bind(this, &ModPopup::onLoadServerInfo);
|
||||
m_statsListener.setFilter(m_source.fetchServerInfo().listen());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ModPopup::setStatValue(CCNode* stat, std::string const& value) {
|
||||
// Remove old value if it exists
|
||||
stat->removeChildByID("value-label");
|
||||
|
||||
auto valueLabel = CCLabelBMFont::create(value.c_str(), "bigFont.fnt");
|
||||
valueLabel->setAnchorPoint({ 1.f, .5f });
|
||||
limitNodeSize(
|
||||
valueLabel,
|
||||
{
|
||||
stat->getContentWidth() / 2.2f,
|
||||
stat->getContentHeight()
|
||||
},
|
||||
.3f, .1f
|
||||
);
|
||||
valueLabel->setID("value-label");
|
||||
stat->addChildAtPosition(valueLabel, Anchor::Right);
|
||||
}
|
||||
|
||||
void ModPopup::onLoadServerInfo(PromiseEvent<server::ServerModMetadata, server::ServerError>* event) {
|
||||
if (auto data = event->getResolve()) {
|
||||
for (auto id : std::initializer_list<std::pair<const char*, std::string>> {
|
||||
{ "downloads", std::to_string(data->downloadCount) },
|
||||
{ "release-date", "todo" },
|
||||
{ "update-date", "todo" },
|
||||
}) {
|
||||
auto stat = m_stats->getChildByID(id.first);
|
||||
stat->removeChildByID("loading-spinner");
|
||||
this->setStatValue(stat, id.second);
|
||||
}
|
||||
}
|
||||
else if (auto err = event->getReject()) {
|
||||
for (auto child : CCArrayExt<CCNode*>(m_stats->getChildren())) {
|
||||
if (auto spinner = child->getChildByID("loading-spinner")) {
|
||||
spinner->removeFromParent();
|
||||
this->setStatValue(child, "N/A");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ModPopup::loadTab(ModPopup::Tab tab) {
|
||||
// Remove current page
|
||||
if (m_currentTabPage) {
|
||||
|
|
|
@ -16,12 +16,17 @@ protected:
|
|||
};
|
||||
|
||||
ModSource m_source;
|
||||
CCNode* m_stats;
|
||||
CCNode* m_rightColumn;
|
||||
CCNode* m_currentTabPage = nullptr;
|
||||
std::unordered_map<Tab, std::pair<GeodeTabSprite*, Ref<CCNode>>> m_tabs;
|
||||
EventListener<PromiseEventFilter<server::ServerModMetadata, server::ServerError>> m_statsListener;
|
||||
|
||||
bool setup(ModSource&& src) override;
|
||||
|
||||
void setStatValue(CCNode* stat, std::string const& value);
|
||||
|
||||
void onLoadServerInfo(PromiseEvent<server::ServerModMetadata, server::ServerError>* event);
|
||||
void loadTab(Tab tab);
|
||||
void onTab(CCObject* sender);
|
||||
|
||||
|
|
|
@ -44,3 +44,17 @@ Mod* ModSource::asMod() const {
|
|||
server::ServerModMetadata const* ModSource::asServer() const {
|
||||
return std::get_if<server::ServerModMetadata>(&m_value);
|
||||
}
|
||||
|
||||
server::ServerPromise<server::ServerModMetadata> ModSource::fetchServerInfo() const {
|
||||
return std::visit(makeVisitor {
|
||||
[](Mod* mod) {
|
||||
// todo: cache
|
||||
return server::getMod(mod->getID());
|
||||
},
|
||||
[](server::ServerModMetadata const& metadata) {
|
||||
return server::ServerPromise<server::ServerModMetadata>([&metadata](auto resolve, auto) {
|
||||
resolve(metadata);
|
||||
});
|
||||
}
|
||||
}, m_value);
|
||||
}
|
||||
|
|
|
@ -24,4 +24,15 @@ public:
|
|||
|
||||
Mod* asMod() const;
|
||||
server::ServerModMetadata const* asServer() const;
|
||||
|
||||
/**
|
||||
* Fetches the server info for this mod source. If the mod source already
|
||||
* is the `ServerModMetadata` variant, then it just immediately resolves to
|
||||
* that. Otherwise, it uses the mod ID to fetch the server info.
|
||||
*
|
||||
* In other words, this does NOT mean that the ModSource is converted to
|
||||
* `ServerModMetadata` or that it only works for that, or that it's required
|
||||
* for that
|
||||
*/
|
||||
server::ServerPromise<server::ServerModMetadata> fetchServerInfo() const;
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue