2024-02-26 15:14:53 -05:00
|
|
|
#include "Server.hpp"
|
|
|
|
#include <Geode/utils/JsonValidation.hpp>
|
2024-06-03 10:39:15 -04:00
|
|
|
#include <Geode/utils/ranges.hpp>
|
2024-06-08 05:13:31 -04:00
|
|
|
#include <chrono>
|
2024-06-11 10:59:49 -04:00
|
|
|
#include <date/date.h>
|
2024-06-03 10:39:15 -04:00
|
|
|
#include <fmt/core.h>
|
2024-02-26 15:14:53 -05:00
|
|
|
#include <loader/ModMetadataImpl.hpp>
|
2024-03-02 17:28:06 -05:00
|
|
|
#include <fmt/chrono.h>
|
2024-06-13 23:54:31 -04:00
|
|
|
#include <loader/LoaderImpl.hpp>
|
|
|
|
#include "../internal/about.hpp"
|
2024-02-26 15:14:53 -05:00
|
|
|
|
|
|
|
using namespace server;
|
|
|
|
|
2024-02-26 20:59:06 -05:00
|
|
|
#define GEODE_GD_VERSION_STR GEODE_STR(GEODE_GD_VERSION)
|
2024-02-26 15:14:53 -05:00
|
|
|
|
2024-05-30 12:11:18 -04:00
|
|
|
template <class K, class V>
|
2024-04-26 06:52:15 -04:00
|
|
|
requires std::equality_comparable<K> && std::copy_constructible<K>
|
2024-04-21 17:08:10 -04:00
|
|
|
class CacheMap final {
|
|
|
|
private:
|
2024-05-30 12:11:18 -04:00
|
|
|
// I know this looks like a goofy choice over just
|
2024-04-21 17:08:10 -04:00
|
|
|
// `std::unordered_map`, but hear me out:
|
2024-05-30 12:11:18 -04:00
|
|
|
//
|
|
|
|
// This needs preserved insertion order (so shrinking the cache
|
|
|
|
// to match size limits doesn't just have to erase random
|
|
|
|
// elements)
|
|
|
|
//
|
|
|
|
// If this used a map for values and another vector for storing
|
|
|
|
// insertion order, it would have a pretty big memory footprint
|
|
|
|
// (two copies of Query, one for order, one for map + two heap
|
2024-04-21 17:08:10 -04:00
|
|
|
// allocations on top of that)
|
2024-05-30 12:11:18 -04:00
|
|
|
//
|
|
|
|
// In addition, it would be a bad idea to have a cache of 1000s
|
|
|
|
// of items in any case (since that would likely take up a ton
|
|
|
|
// of memory, which we want to avoid since it's likely many
|
|
|
|
// crashes with the old index were due to too much memory
|
2024-04-21 17:08:10 -04:00
|
|
|
// usage)
|
2024-05-30 12:11:18 -04:00
|
|
|
//
|
|
|
|
// Linear searching a vector of at most a couple dozen items is
|
|
|
|
// lightning-fast (🚀), and besides the main performance benefit
|
|
|
|
// comes from the lack of a web request - not how many extra
|
2024-04-21 17:08:10 -04:00
|
|
|
// milliseconds we can squeeze out of a map access
|
|
|
|
std::vector<std::pair<K, V>> m_values;
|
|
|
|
size_t m_sizeLimit = 20;
|
|
|
|
|
|
|
|
public:
|
|
|
|
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 (it != m_values.end()) {
|
|
|
|
return it->second;
|
|
|
|
}
|
|
|
|
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(pair);
|
|
|
|
}
|
|
|
|
// Otherwise append at end
|
|
|
|
else {
|
|
|
|
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 size() const {
|
|
|
|
return m_values.size();
|
|
|
|
}
|
|
|
|
size_t limit() const {
|
|
|
|
return m_sizeLimit;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
template <class F>
|
|
|
|
struct ExtractFun;
|
|
|
|
|
2024-04-23 15:24:08 -04:00
|
|
|
template <class V, class... Args>
|
|
|
|
struct ExtractFun<ServerRequest<V>(*)(Args...)> {
|
|
|
|
using CacheKey = std::tuple<std::remove_cvref_t<Args>...>;
|
2024-04-21 17:08:10 -04:00
|
|
|
using Value = V;
|
|
|
|
|
2024-04-23 15:24:08 -04:00
|
|
|
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);
|
2024-04-21 17:08:10 -04:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
template <auto F>
|
|
|
|
class FunCache final {
|
|
|
|
public:
|
2024-04-23 15:24:08 -04:00
|
|
|
using Extract = ExtractFun<decltype(F)>;
|
|
|
|
using CacheKey = typename Extract::CacheKey;
|
|
|
|
using Value = typename Extract::Value;
|
2024-04-21 17:08:10 -04:00
|
|
|
|
|
|
|
private:
|
2024-04-22 11:16:26 -04:00
|
|
|
std::mutex m_mutex;
|
2024-04-23 15:24:08 -04:00
|
|
|
CacheMap<CacheKey, ServerRequest<Value>> m_cache;
|
2024-04-21 17:08:10 -04:00
|
|
|
|
|
|
|
public:
|
|
|
|
FunCache() = default;
|
|
|
|
FunCache(FunCache const&) = delete;
|
|
|
|
FunCache(FunCache&&) = delete;
|
|
|
|
|
2024-04-23 15:24:08 -04:00
|
|
|
template <class... Args>
|
|
|
|
ServerRequest<Value> get(Args const&... args) {
|
2024-04-22 11:16:26 -04:00
|
|
|
std::unique_lock lock(m_mutex);
|
2024-04-23 15:24:08 -04:00
|
|
|
if (auto v = m_cache.get(Extract::key(args...))) {
|
2024-04-21 17:08:10 -04:00
|
|
|
return *v;
|
|
|
|
}
|
2024-04-23 15:24:08 -04:00
|
|
|
auto f = Extract::invoke(F, args...);
|
|
|
|
m_cache.add(Extract::key(args...), ServerRequest<Value>(f));
|
2024-04-21 17:08:10 -04:00
|
|
|
return f;
|
|
|
|
}
|
2024-05-30 12:11:18 -04:00
|
|
|
|
|
|
|
template <class... Args>
|
|
|
|
void remove(Args const&... args) {
|
|
|
|
std::unique_lock lock(m_mutex);
|
|
|
|
m_cache.remove(Extract::key(args...));
|
|
|
|
}
|
|
|
|
|
2024-04-22 08:21:44 -04:00
|
|
|
size_t size() {
|
2024-04-22 11:16:26 -04:00
|
|
|
std::unique_lock lock(m_mutex);
|
2024-04-22 08:21:44 -04:00
|
|
|
return m_cache.size();
|
|
|
|
}
|
2024-04-22 06:35:12 -04:00
|
|
|
void limit(size_t size) {
|
2024-04-22 11:16:26 -04:00
|
|
|
std::unique_lock lock(m_mutex);
|
2024-04-22 06:35:12 -04:00
|
|
|
m_cache.limit(size);
|
|
|
|
}
|
2024-04-21 17:08:10 -04:00
|
|
|
void clear() {
|
2024-04-22 11:16:26 -04:00
|
|
|
std::unique_lock lock(m_mutex);
|
2024-04-21 17:08:10 -04:00
|
|
|
m_cache.clear();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
template <auto F>
|
|
|
|
FunCache<F>& getCache() {
|
|
|
|
static auto inst = FunCache<F>();
|
|
|
|
return inst;
|
|
|
|
}
|
|
|
|
|
2024-03-02 14:57:40 -05:00
|
|
|
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";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-21 17:08:10 -04:00
|
|
|
static Result<matjson::Value, ServerError> parseServerPayload(web::WebResponse const& response) {
|
2024-03-02 14:57:40 -05:00
|
|
|
auto asJson = response.json();
|
|
|
|
if (!asJson) {
|
|
|
|
return Err(ServerError(response.code(), "Response was not valid JSON: {}", asJson.unwrapErr()));
|
|
|
|
}
|
|
|
|
auto json = std::move(asJson).unwrap();
|
2024-11-09 13:16:24 -05:00
|
|
|
if (!json.isObject()) {
|
2024-03-02 14:57:40 -05:00
|
|
|
return Err(ServerError(response.code(), "Expected object, got {}", jsonTypeToString(json.type())));
|
|
|
|
}
|
2024-11-09 13:16:24 -05:00
|
|
|
if (!json.contains("payload")) {
|
|
|
|
return Err(ServerError(response.code(), "Object does not contain \"payload\" key - got {}", json.dump().unwrapOr("?")));
|
2024-03-02 14:57:40 -05:00
|
|
|
}
|
2024-11-09 13:16:24 -05:00
|
|
|
return Ok(json["payload"]);
|
2024-03-02 14:57:40 -05:00
|
|
|
}
|
|
|
|
|
2024-04-21 17:08:10 -04:00
|
|
|
static ServerError parseServerError(web::WebResponse const& error) {
|
2024-02-26 17:55:33 -05:00
|
|
|
// The server should return errors as `{ "error": "...", "payload": "" }`
|
2024-03-02 14:57:40 -05:00
|
|
|
if (auto asJson = error.json()) {
|
|
|
|
auto json = asJson.unwrap();
|
2024-11-09 13:16:24 -05:00
|
|
|
if (json.isObject() && json.contains("error") && json["error"].isString()) {
|
2024-03-12 16:41:17 -04:00
|
|
|
return ServerError(
|
2024-03-02 14:57:40 -05:00
|
|
|
error.code(),
|
2024-11-09 13:16:24 -05:00
|
|
|
"{}", json["error"].asString().unwrapOr("Unknown (no error message)")
|
2024-03-12 16:41:17 -04:00
|
|
|
);
|
2024-03-02 14:57:40 -05:00
|
|
|
}
|
|
|
|
else {
|
2024-03-12 16:41:17 -04:00
|
|
|
return ServerError(error.code(), "Unknown (not valid JSON)");
|
2024-03-02 14:57:40 -05:00
|
|
|
}
|
2024-02-26 17:55:33 -05:00
|
|
|
}
|
|
|
|
// But if we get something else for some reason, return that
|
|
|
|
else {
|
2024-03-12 16:41:17 -04:00
|
|
|
return ServerError(
|
2024-03-02 14:57:40 -05:00
|
|
|
error.code(),
|
|
|
|
"{}", error.string().unwrapOr("Unknown (not a valid string)")
|
2024-03-12 16:41:17 -04:00
|
|
|
);
|
2024-02-26 17:55:33 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-21 17:08:10 -04:00
|
|
|
static ServerProgress parseServerProgress(web::WebProgress const& prog, auto msg) {
|
2024-02-26 17:55:33 -05:00
|
|
|
if (auto per = prog.downloadProgress()) {
|
2024-03-12 16:41:17 -04:00
|
|
|
return ServerProgress(msg, static_cast<uint8_t>(*per));
|
2024-02-26 17:55:33 -05:00
|
|
|
}
|
|
|
|
else {
|
2024-03-12 16:41:17 -04:00
|
|
|
return ServerProgress(msg);
|
2024-02-26 17:55:33 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-26 15:14:53 -05:00
|
|
|
const char* server::sortToString(ModsSort sorting) {
|
|
|
|
switch (sorting) {
|
|
|
|
default:
|
|
|
|
case ModsSort::Downloads: return "downloads";
|
|
|
|
case ModsSort::RecentlyUpdated: return "recently_updated";
|
|
|
|
case ModsSort::RecentlyPublished: return "recently_published";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-02 17:28:06 -05:00
|
|
|
std::string ServerDateTime::toAgoString() const {
|
|
|
|
auto const fmtPlural = [](auto count, auto unit) {
|
|
|
|
if (count == 1) {
|
|
|
|
return fmt::format("{} {} ago", count, unit);
|
|
|
|
}
|
|
|
|
return fmt::format("{} {}s ago", count, unit);
|
|
|
|
};
|
|
|
|
auto now = Clock::now();
|
|
|
|
auto len = std::chrono::duration_cast<std::chrono::minutes>(now - value).count();
|
|
|
|
if (len < 60) {
|
|
|
|
return fmtPlural(len, "minute");
|
|
|
|
}
|
|
|
|
len = std::chrono::duration_cast<std::chrono::hours>(now - value).count();
|
|
|
|
if (len < 24) {
|
|
|
|
return fmtPlural(len, "hour");
|
|
|
|
}
|
|
|
|
len = std::chrono::duration_cast<std::chrono::days>(now - value).count();
|
|
|
|
if (len < 31) {
|
|
|
|
return fmtPlural(len, "day");
|
|
|
|
}
|
2024-03-02 17:45:26 -05:00
|
|
|
return fmt::format("{:%b %d %Y}", value);
|
2024-03-02 17:28:06 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
Result<ServerDateTime> ServerDateTime::parse(std::string const& str) {
|
|
|
|
std::stringstream ss(str);
|
2024-06-11 10:59:49 -04:00
|
|
|
date::sys_seconds seconds;
|
|
|
|
if (ss >> date::parse("%Y-%m-%dT%H:%M:%S%Z", seconds)) {
|
2024-03-02 17:28:06 -05:00
|
|
|
return Ok(ServerDateTime {
|
2024-06-08 05:13:31 -04:00
|
|
|
.value = seconds
|
2024-03-02 17:28:06 -05:00
|
|
|
});
|
|
|
|
}
|
|
|
|
return Err("Invalid date time format '{}'", str);
|
|
|
|
}
|
|
|
|
|
2024-02-26 15:14:53 -05:00
|
|
|
Result<ServerModVersion> ServerModVersion::parse(matjson::Value const& raw) {
|
2024-11-04 12:14:23 -05:00
|
|
|
auto root = checkJson(raw, "ServerModVersion");
|
2024-02-26 15:14:53 -05:00
|
|
|
|
|
|
|
auto res = ServerModVersion();
|
|
|
|
|
2024-10-11 16:52:50 -04:00
|
|
|
res.metadata.setGeodeVersion(root.needs("geode").get<VersionInfo>());
|
2024-02-26 15:14:53 -05:00
|
|
|
|
|
|
|
// Verify target GD version
|
2024-11-04 12:28:38 -05:00
|
|
|
auto gd_obj = root.needs("gd");
|
2024-09-09 04:55:20 -04:00
|
|
|
std::string gd = "0.000";
|
2024-11-09 13:16:24 -05:00
|
|
|
if (gd_obj.hasNullable(GEODE_PLATFORM_SHORT_IDENTIFIER)) {
|
|
|
|
gd = gd_obj.hasNullable(GEODE_PLATFORM_SHORT_IDENTIFIER). get<std::string>();
|
2024-09-09 04:55:20 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
if (gd != "*") {
|
|
|
|
res.metadata.setGameVersion(gd);
|
2024-02-26 15:14:53 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// Get server info
|
|
|
|
root.needs("download_link").into(res.downloadURL);
|
|
|
|
root.needs("download_count").into(res.downloadCount);
|
|
|
|
root.needs("hash").into(res.hash);
|
|
|
|
|
|
|
|
// Get mod metadata info
|
2024-10-11 16:52:50 -04:00
|
|
|
res.metadata.setID(root.needs("mod_id").get<std::string>());
|
|
|
|
res.metadata.setName(root.needs("name").get<std::string>());
|
|
|
|
res.metadata.setDescription(root.needs("description").get<std::string>());
|
|
|
|
res.metadata.setVersion(root.needs("version").get<VersionInfo>());
|
|
|
|
res.metadata.setIsAPI(root.needs("api").get<bool>());
|
2024-02-26 15:14:53 -05:00
|
|
|
|
|
|
|
std::vector<ModMetadata::Dependency> dependencies {};
|
2024-11-09 13:16:24 -05:00
|
|
|
for (auto& obj : root.hasNullable("dependencies").items()) {
|
2024-02-26 15:14:53 -05:00
|
|
|
// todo: this should probably be generalized to use the same function as mod.json
|
|
|
|
|
2024-11-09 13:16:24 -05:00
|
|
|
bool onThisPlatform = !obj.hasNullable("platforms");
|
|
|
|
for (auto& plat : obj.hasNullable("platforms").items()) {
|
2024-06-08 02:42:54 -04:00
|
|
|
if (PlatformID::coveredBy(plat.get<std::string>(), GEODE_PLATFORM_TARGET)) {
|
2024-02-26 15:14:53 -05:00
|
|
|
onThisPlatform = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!onThisPlatform) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
ModMetadata::Dependency dependency;
|
2024-11-04 12:28:38 -05:00
|
|
|
obj.needs("mod_id").mustBe<std::string>("a valid id", &ModMetadata::validateID).into(dependency.id);
|
2024-02-26 15:14:53 -05:00
|
|
|
obj.needs("version").into(dependency.version);
|
2024-11-09 13:16:24 -05:00
|
|
|
obj.hasNullable("importance").into(dependency.importance);
|
2024-02-26 15:14:53 -05:00
|
|
|
|
2024-04-23 15:24:08 -04:00
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
2024-02-26 15:14:53 -05:00
|
|
|
dependencies.push_back(dependency);
|
|
|
|
}
|
|
|
|
res.metadata.setDependencies(dependencies);
|
|
|
|
|
|
|
|
std::vector<ModMetadata::Incompatibility> incompatibilities {};
|
2024-11-09 13:16:24 -05:00
|
|
|
for (auto& obj : root.hasNullable("incompatibilities").items()) {
|
2024-02-26 15:14:53 -05:00
|
|
|
ModMetadata::Incompatibility incompatibility;
|
2024-11-09 13:16:24 -05:00
|
|
|
obj.hasNullable("importance").into(incompatibility.importance);
|
2024-02-26 15:14:53 -05:00
|
|
|
|
2024-06-07 19:40:25 -04:00
|
|
|
auto modIdValue = obj.needs("mod_id");
|
|
|
|
|
|
|
|
// Do not validate if we have a supersede, maybe the old ID is invalid
|
|
|
|
if (incompatibility.importance == ModMetadata::Incompatibility::Importance::Superseded) {
|
|
|
|
modIdValue.into(incompatibility.id);
|
|
|
|
} else {
|
2024-11-04 12:28:38 -05:00
|
|
|
modIdValue.mustBe<std::string>("a valid id", &ModMetadata::validateID).into(incompatibility.id);
|
2024-06-07 19:40:25 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
obj.needs("version").into(incompatibility.version);
|
|
|
|
|
2024-04-23 15:24:08 -04:00
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
2024-02-26 15:14:53 -05:00
|
|
|
incompatibilities.push_back(incompatibility);
|
|
|
|
}
|
2024-03-26 16:18:34 -04:00
|
|
|
res.metadata.setIncompatibilities(incompatibilities);
|
2024-02-26 15:14:53 -05:00
|
|
|
|
2024-11-04 12:28:38 -05:00
|
|
|
return root.ok(res);
|
2024-02-26 15:14:53 -05:00
|
|
|
}
|
|
|
|
|
2024-06-03 11:12:48 -04:00
|
|
|
Result<ServerModReplacement> ServerModReplacement::parse(matjson::Value const& raw) {
|
2024-11-04 12:14:23 -05:00
|
|
|
auto root = checkJson(raw, "ServerModReplacement");
|
2024-06-03 11:12:48 -04:00
|
|
|
auto res = ServerModReplacement();
|
|
|
|
|
|
|
|
root.needs("id").into(res.id);
|
|
|
|
root.needs("version").into(res.version);
|
|
|
|
|
2024-11-04 12:28:38 -05:00
|
|
|
return root.ok(res);
|
2024-06-03 11:12:48 -04:00
|
|
|
}
|
|
|
|
|
2024-03-26 16:18:34 -04:00
|
|
|
Result<ServerModUpdate> ServerModUpdate::parse(matjson::Value const& raw) {
|
2024-11-04 12:14:23 -05:00
|
|
|
auto root = checkJson(raw, "ServerModUpdate");
|
2024-03-26 16:18:34 -04:00
|
|
|
|
|
|
|
auto res = ServerModUpdate();
|
|
|
|
|
|
|
|
root.needs("id").into(res.id);
|
|
|
|
root.needs("version").into(res.version);
|
2024-11-09 13:16:24 -05:00
|
|
|
if (root.hasNullable("replacement")) {
|
|
|
|
GEODE_UNWRAP_INTO(res.replacement, ServerModReplacement::parse(root.hasNullable("replacement").json()));
|
2024-06-03 11:12:48 -04:00
|
|
|
}
|
2024-03-26 16:18:34 -04:00
|
|
|
|
2024-11-04 12:28:38 -05:00
|
|
|
return root.ok(res);
|
2024-03-26 16:18:34 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
Result<std::vector<ServerModUpdate>> ServerModUpdate::parseList(matjson::Value const& raw) {
|
2024-11-04 12:14:23 -05:00
|
|
|
auto payload = checkJson(raw, "ServerModUpdatesList");
|
2024-03-26 16:18:34 -04:00
|
|
|
|
|
|
|
std::vector<ServerModUpdate> list {};
|
2024-11-04 12:28:38 -05:00
|
|
|
for (auto& item : payload.items()) {
|
2024-03-26 16:18:34 -04:00
|
|
|
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());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-04 12:28:38 -05:00
|
|
|
return payload.ok(list);
|
2024-03-26 16:18:34 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
bool ServerModUpdate::hasUpdateForInstalledMod() const {
|
2024-03-27 06:09:13 -04:00
|
|
|
if (auto mod = Loader::get()->getInstalledMod(this->id)) {
|
2024-06-03 11:12:48 -04:00
|
|
|
return mod->getVersion() < this->version || this->replacement.has_value();
|
2024-03-26 16:18:34 -04:00
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2024-02-26 15:14:53 -05:00
|
|
|
Result<ServerModMetadata> ServerModMetadata::parse(matjson::Value const& raw) {
|
2024-11-04 12:14:23 -05:00
|
|
|
auto root = checkJson(raw, "ServerModMetadata");
|
2024-02-26 15:14:53 -05:00
|
|
|
|
|
|
|
auto res = ServerModMetadata();
|
|
|
|
root.needs("id").into(res.id);
|
|
|
|
root.needs("featured").into(res.featured);
|
|
|
|
root.needs("download_count").into(res.downloadCount);
|
2024-11-09 13:16:24 -05:00
|
|
|
root.hasNullable("about").into(res.about);
|
|
|
|
root.hasNullable("changelog").into(res.changelog);
|
|
|
|
root.hasNullable("repository").into(res.repository);
|
2024-03-02 17:28:06 -05:00
|
|
|
if (root.has("created_at")) {
|
2024-10-11 16:52:50 -04:00
|
|
|
GEODE_UNWRAP_INTO(res.createdAt, ServerDateTime::parse(root.has("created_at").get<std::string>()));
|
2024-03-02 17:28:06 -05:00
|
|
|
}
|
|
|
|
if (root.has("updated_at")) {
|
2024-10-11 16:52:50 -04:00
|
|
|
GEODE_UNWRAP_INTO(res.updatedAt, ServerDateTime::parse(root.has("updated_at").get<std::string>()));
|
2024-03-02 17:28:06 -05:00
|
|
|
}
|
2024-02-26 15:14:53 -05:00
|
|
|
|
2024-02-26 17:55:33 -05:00
|
|
|
std::vector<std::string> developerNames;
|
2024-11-04 12:28:38 -05:00
|
|
|
for (auto& obj : root.needs("developers").items()) {
|
2024-02-26 15:14:53 -05:00
|
|
|
auto dev = ServerDeveloper();
|
|
|
|
obj.needs("username").into(dev.username);
|
|
|
|
obj.needs("display_name").into(dev.displayName);
|
2024-06-03 10:39:15 -04:00
|
|
|
obj.needs("is_owner").into(dev.isOwner);
|
2024-02-26 15:14:53 -05:00
|
|
|
res.developers.push_back(dev);
|
2024-02-26 17:55:33 -05:00
|
|
|
developerNames.push_back(dev.displayName);
|
2024-02-26 15:14:53 -05:00
|
|
|
}
|
2024-11-04 12:28:38 -05:00
|
|
|
for (auto& item : root.needs("versions").items()) {
|
2024-02-26 15:14:53 -05:00
|
|
|
auto versionRes = ServerModVersion::parse(item.json());
|
|
|
|
if (versionRes) {
|
|
|
|
auto version = versionRes.unwrap();
|
|
|
|
version.metadata.setDetails(res.about);
|
|
|
|
version.metadata.setChangelog(res.changelog);
|
2024-02-26 17:55:33 -05:00
|
|
|
version.metadata.setDevelopers(developerNames);
|
2024-03-25 06:36:33 -04:00
|
|
|
version.metadata.setRepository(res.repository);
|
2024-02-26 15:14:53 -05:00
|
|
|
res.versions.push_back(version);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
log::error("Unable to parse mod '{}' version from the server: {}", res.id, versionRes.unwrapErr());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure there's at least one valid version
|
|
|
|
if (res.versions.empty()) {
|
|
|
|
return Err("Mod '{}' has no (valid) versions", res.id);
|
|
|
|
}
|
2024-05-30 12:11:18 -04:00
|
|
|
|
2024-11-09 13:16:24 -05:00
|
|
|
for (auto& item : root.hasNullable("tags").items()) {
|
2024-10-11 16:52:50 -04:00
|
|
|
res.tags.insert(item.get<std::string>());
|
2024-02-26 15:14:53 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
root.needs("download_count").into(res.downloadCount);
|
|
|
|
|
2024-11-04 12:28:38 -05:00
|
|
|
return root.ok(res);
|
2024-02-26 15:14:53 -05:00
|
|
|
}
|
|
|
|
|
2024-06-03 10:39:15 -04:00
|
|
|
std::string ServerModMetadata::formatDevelopersToString() const {
|
|
|
|
std::optional<ServerDeveloper> owner = ranges::find(developers, [] (auto item) {
|
|
|
|
return item.isOwner;
|
|
|
|
});
|
|
|
|
switch (developers.size()) {
|
|
|
|
case 0: return "Unknown"; break;
|
|
|
|
case 1: return developers.front().displayName; break;
|
|
|
|
case 2: return developers.front().displayName + " & " + developers.back().displayName; break;
|
|
|
|
default: {
|
|
|
|
if (owner) {
|
|
|
|
return fmt::format("{} + {} More", owner->displayName, developers.size() - 1);
|
|
|
|
} else {
|
|
|
|
return fmt::format("{} + {} More", developers.front().displayName, developers.size() - 1);
|
|
|
|
}
|
|
|
|
} break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-26 15:14:53 -05:00
|
|
|
Result<ServerModsList> ServerModsList::parse(matjson::Value const& raw) {
|
2024-11-04 12:14:23 -05:00
|
|
|
auto payload = checkJson(raw, "ServerModsList");
|
2024-02-26 15:14:53 -05:00
|
|
|
|
|
|
|
auto list = ServerModsList();
|
2024-11-04 12:28:38 -05:00
|
|
|
for (auto& item : payload.needs("data").items()) {
|
2024-02-26 18:16:24 -05:00
|
|
|
auto mod = ServerModMetadata::parse(item.json());
|
|
|
|
if (mod) {
|
|
|
|
list.mods.push_back(mod.unwrap());
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
log::error("Unable to parse mod from the server: {}", mod.unwrapErr());
|
|
|
|
}
|
2024-02-26 15:14:53 -05:00
|
|
|
}
|
|
|
|
payload.needs("count").into(list.totalModCount);
|
|
|
|
|
2024-11-04 12:28:38 -05:00
|
|
|
return payload.ok(list);
|
2024-02-26 15:14:53 -05:00
|
|
|
}
|
|
|
|
|
2024-03-05 11:25:35 -05:00
|
|
|
ModMetadata ServerModMetadata::latestVersion() const {
|
|
|
|
return this->versions.front().metadata;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ServerModMetadata::hasUpdateForInstalledMod() const {
|
2024-03-27 06:09:13 -04:00
|
|
|
if (auto mod = Loader::get()->getInstalledMod(this->id)) {
|
2024-03-05 11:25:35 -05:00
|
|
|
return mod->getVersion() < this->latestVersion().getVersion();
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2024-02-26 15:14:53 -05:00
|
|
|
std::string server::getServerAPIBaseURL() {
|
|
|
|
return "https://api.geode-sdk.org/v1";
|
|
|
|
}
|
|
|
|
|
2024-04-28 17:31:14 -04:00
|
|
|
template <class... Args>
|
|
|
|
std::string formatServerURL(fmt::format_string<Args...> fmt, Args&&... args) {
|
|
|
|
return getServerAPIBaseURL() + fmt::format(fmt, std::forward<Args>(args)...);
|
|
|
|
}
|
|
|
|
|
2024-02-26 20:36:38 -05:00
|
|
|
std::string server::getServerUserAgent() {
|
2024-06-13 23:54:31 -04:00
|
|
|
// no need to compute this more than once
|
|
|
|
static const auto value = [] {
|
|
|
|
// TODO: is this enough info? is it too much?
|
2024-06-20 18:18:58 -04:00
|
|
|
return fmt::format("Geode Loader (ver={};commit={};platform={};gd={})",
|
2024-06-13 23:54:31 -04:00
|
|
|
Loader::get()->getVersion().toNonVString(),
|
|
|
|
about::getLoaderCommitHash(),
|
|
|
|
GEODE_PLATFORM_SHORT_IDENTIFIER,
|
2024-06-20 18:18:58 -04:00
|
|
|
LoaderImpl::get()->getGameVersion()
|
2024-06-13 23:54:31 -04:00
|
|
|
);
|
|
|
|
}();
|
|
|
|
return value;
|
2024-02-26 20:36:38 -05:00
|
|
|
}
|
|
|
|
|
2024-04-22 06:35:12 -04:00
|
|
|
ServerRequest<ServerModsList> server::getMods(ModsQuery const& query, bool useCache) {
|
2024-04-21 17:08:10 -04:00
|
|
|
if (useCache) {
|
2024-04-22 06:35:12 -04:00
|
|
|
return getCache<getMods>().get(query);
|
2024-04-21 17:08:10 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
auto req = web::WebRequest();
|
|
|
|
req.userAgent(getServerUserAgent());
|
|
|
|
|
|
|
|
// Add search params
|
|
|
|
if (query.query) {
|
|
|
|
req.param("query", *query.query);
|
2024-09-09 04:55:20 -04:00
|
|
|
} else {
|
|
|
|
// Target current GD version and Loader version when query is not set
|
|
|
|
req.param("gd", GEODE_GD_VERSION_STR);
|
|
|
|
req.param("geode", Loader::get()->getVersion().toNonVString());
|
2024-04-21 17:08:10 -04:00
|
|
|
}
|
2024-09-09 04:55:20 -04:00
|
|
|
|
2024-04-21 17:08:10 -04:00
|
|
|
if (query.platforms.size()) {
|
|
|
|
std::string plats = "";
|
|
|
|
bool first = true;
|
|
|
|
for (auto plat : query.platforms) {
|
|
|
|
if (!first) plats += ",";
|
|
|
|
plats += PlatformID::toShortString(plat.m_value);
|
|
|
|
first = false;
|
|
|
|
}
|
|
|
|
req.param("platforms", plats);
|
|
|
|
}
|
|
|
|
if (query.tags.size()) {
|
|
|
|
req.param("tags", ranges::join(query.tags, ","));
|
|
|
|
}
|
|
|
|
if (query.featured) {
|
|
|
|
req.param("featured", query.featured.value() ? "true" : "false");
|
|
|
|
}
|
|
|
|
req.param("sort", sortToString(query.sorting));
|
|
|
|
if (query.developer) {
|
|
|
|
req.param("developer", *query.developer);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Paging (1-based on server, 0-based locally)
|
|
|
|
req.param("page", std::to_string(query.page + 1));
|
|
|
|
req.param("per_page", std::to_string(query.pageSize));
|
|
|
|
|
2024-04-28 17:31:14 -04:00
|
|
|
return req.get(formatServerURL("/mods")).map(
|
2024-04-21 17:08:10 -04:00
|
|
|
[](web::WebResponse* response) -> Result<ServerModsList, ServerError> {
|
|
|
|
if (response->ok()) {
|
|
|
|
// Parse payload
|
|
|
|
auto payload = parseServerPayload(*response);
|
|
|
|
if (!payload) {
|
|
|
|
return Err(payload.unwrapErr());
|
|
|
|
}
|
|
|
|
// Parse response
|
|
|
|
auto list = ServerModsList::parse(payload.unwrap());
|
|
|
|
if (!list) {
|
|
|
|
return Err(ServerError(response->code(), "Unable to parse response: {}", list.unwrapErr()));
|
|
|
|
}
|
|
|
|
return Ok(list.unwrap());
|
|
|
|
}
|
2024-04-22 06:35:12 -04:00
|
|
|
// Treat a 404 as empty mods list
|
|
|
|
if (response->code() == 404) {
|
|
|
|
return Ok(ServerModsList());
|
2024-04-21 17:08:10 -04:00
|
|
|
}
|
2024-04-22 06:35:12 -04:00
|
|
|
return Err(parseServerError(*response));
|
2024-04-21 17:08:10 -04:00
|
|
|
},
|
|
|
|
[](web::WebProgress* progress) {
|
|
|
|
return parseServerProgress(*progress, "Downloading mods");
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-04-22 06:35:12 -04:00
|
|
|
ServerRequest<ServerModMetadata> server::getMod(std::string const& id, bool useCache) {
|
|
|
|
if (useCache) {
|
|
|
|
return getCache<getMod>().get(id);
|
2024-02-26 15:14:53 -05:00
|
|
|
}
|
2024-02-26 17:55:33 -05:00
|
|
|
auto req = web::WebRequest();
|
2024-02-26 20:36:38 -05:00
|
|
|
req.userAgent(getServerUserAgent());
|
2024-04-28 17:31:14 -04:00
|
|
|
return req.get(formatServerURL("/mods/{}", id)).map(
|
2024-04-22 06:35:12 -04:00
|
|
|
[](web::WebResponse* response) -> Result<ServerModMetadata, ServerError> {
|
|
|
|
if (response->ok()) {
|
2024-03-02 14:57:40 -05:00
|
|
|
// Parse payload
|
2024-04-22 06:35:12 -04:00
|
|
|
auto payload = parseServerPayload(*response);
|
2024-03-02 14:57:40 -05:00
|
|
|
if (!payload) {
|
2024-03-12 16:41:17 -04:00
|
|
|
return Err(payload.unwrapErr());
|
2024-03-02 14:57:40 -05:00
|
|
|
}
|
|
|
|
// Parse response
|
|
|
|
auto list = ServerModMetadata::parse(payload.unwrap());
|
|
|
|
if (!list) {
|
2024-04-22 06:35:12 -04:00
|
|
|
return Err(ServerError(response->code(), "Unable to parse response: {}", list.unwrapErr()));
|
2024-03-02 14:57:40 -05:00
|
|
|
}
|
2024-03-12 16:41:17 -04:00
|
|
|
return Ok(list.unwrap());
|
|
|
|
}
|
2024-04-22 06:35:12 -04:00
|
|
|
return Err(parseServerError(*response));
|
|
|
|
},
|
|
|
|
[id](web::WebProgress* progress) {
|
2024-04-23 15:24:08 -04:00
|
|
|
return parseServerProgress(*progress, "Downloading metadata for " + id);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-04-26 06:52:15 -04:00
|
|
|
ServerRequest<ServerModVersion> server::getModVersion(std::string const& id, ModVersion const& version, bool useCache) {
|
2024-04-23 15:24:08 -04:00
|
|
|
if (useCache) {
|
2024-05-30 12:11:18 -04:00
|
|
|
auto& cache = getCache<getModVersion>();
|
|
|
|
|
|
|
|
auto cachedRequest = cache.get(id, version);
|
|
|
|
|
|
|
|
// if mod installation was cancelled, remove it from cache and fetch again
|
|
|
|
if (cachedRequest.isCancelled()) {
|
|
|
|
cache.remove(id, version);
|
|
|
|
return cache.get(id, version);
|
|
|
|
} else {
|
|
|
|
return cachedRequest;
|
|
|
|
}
|
2024-04-23 15:24:08 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
auto req = web::WebRequest();
|
|
|
|
req.userAgent(getServerUserAgent());
|
|
|
|
|
2024-04-26 06:52:15 -04:00
|
|
|
std::string versionURL;
|
|
|
|
std::visit(makeVisitor {
|
|
|
|
[&](ModVersionLatest const&) {
|
|
|
|
versionURL = "latest";
|
|
|
|
},
|
|
|
|
[&](ModVersionMajor const& ver) {
|
|
|
|
versionURL = "latest";
|
|
|
|
req.param("major", std::to_string(ver.major));
|
|
|
|
},
|
|
|
|
[&](ModVersionSpecific const& ver) {
|
2024-06-12 05:18:58 -04:00
|
|
|
versionURL = ver.toNonVString();
|
2024-04-26 06:52:15 -04:00
|
|
|
},
|
|
|
|
}, version);
|
|
|
|
|
2024-04-28 17:31:14 -04:00
|
|
|
return req.get(formatServerURL("/mods/{}/versions/{}", id, versionURL)).map(
|
2024-04-23 15:24:08 -04:00
|
|
|
[](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) {
|
2024-04-22 06:35:12 -04:00
|
|
|
return parseServerProgress(*progress, "Downloading metadata for " + id);
|
|
|
|
}
|
|
|
|
);
|
2024-03-02 14:57:40 -05:00
|
|
|
}
|
2024-02-26 20:36:38 -05:00
|
|
|
|
2024-04-22 06:35:12 -04:00
|
|
|
ServerRequest<ByteVector> server::getModLogo(std::string const& id, bool useCache) {
|
|
|
|
if (useCache) {
|
|
|
|
return getCache<getModLogo>().get(id);
|
|
|
|
}
|
2024-03-02 14:57:40 -05:00
|
|
|
auto req = web::WebRequest();
|
|
|
|
req.userAgent(getServerUserAgent());
|
2024-04-28 17:31:14 -04:00
|
|
|
return req.get(formatServerURL("/mods/{}/logo", id)).map(
|
2024-04-22 06:35:12 -04:00
|
|
|
[](web::WebResponse* response) -> Result<ByteVector, ServerError> {
|
|
|
|
if (response->ok()) {
|
|
|
|
return Ok(response->data());
|
|
|
|
}
|
|
|
|
return Err(parseServerError(*response));
|
|
|
|
},
|
|
|
|
[id](web::WebProgress* progress) {
|
|
|
|
return parseServerProgress(*progress, "Downloading logo for " + id);
|
|
|
|
}
|
|
|
|
);
|
2024-02-26 15:14:53 -05:00
|
|
|
}
|
2024-03-24 05:27:28 -04:00
|
|
|
|
2024-04-22 06:35:12 -04:00
|
|
|
ServerRequest<std::unordered_set<std::string>> server::getTags(bool useCache) {
|
|
|
|
if (useCache) {
|
|
|
|
return getCache<getTags>().get();
|
|
|
|
}
|
2024-03-24 05:27:28 -04:00
|
|
|
auto req = web::WebRequest();
|
|
|
|
req.userAgent(getServerUserAgent());
|
2024-04-28 17:31:14 -04:00
|
|
|
return req.get(formatServerURL("/tags")).map(
|
2024-04-22 06:35:12 -04:00
|
|
|
[](web::WebResponse* response) -> Result<std::unordered_set<std::string>, ServerError> {
|
|
|
|
if (response->ok()) {
|
2024-03-24 05:27:28 -04:00
|
|
|
// Parse payload
|
2024-04-22 06:35:12 -04:00
|
|
|
auto payload = parseServerPayload(*response);
|
2024-03-24 05:27:28 -04:00
|
|
|
if (!payload) {
|
|
|
|
return Err(payload.unwrapErr());
|
|
|
|
}
|
|
|
|
matjson::Value json = payload.unwrap();
|
2024-11-09 13:16:24 -05:00
|
|
|
if (!json.isArray()) {
|
2024-04-22 06:35:12 -04:00
|
|
|
return Err(ServerError(response->code(), "Expected a string array"));
|
2024-03-24 05:27:28 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
std::unordered_set<std::string> tags;
|
2024-11-09 13:16:24 -05:00
|
|
|
for (auto item : json) {
|
|
|
|
if (!item.isString()) {
|
2024-04-22 06:35:12 -04:00
|
|
|
return Err(ServerError(response->code(), "Expected a string array"));
|
2024-03-24 05:27:28 -04:00
|
|
|
}
|
2024-11-09 13:16:24 -05:00
|
|
|
tags.insert(item.asString().unwrap());
|
2024-03-24 05:27:28 -04:00
|
|
|
}
|
|
|
|
return Ok(tags);
|
|
|
|
}
|
2024-04-22 06:35:12 -04:00
|
|
|
return Err(parseServerError(*response));
|
|
|
|
},
|
|
|
|
[](web::WebProgress* progress) {
|
|
|
|
return parseServerProgress(*progress, "Downloading valid tags");
|
|
|
|
}
|
|
|
|
);
|
2024-03-24 05:27:28 -04:00
|
|
|
}
|
2024-03-26 16:18:34 -04:00
|
|
|
|
2024-07-06 10:06:50 -04:00
|
|
|
ServerRequest<std::optional<ServerModUpdate>> server::checkUpdates(Mod const* mod) {
|
2024-04-22 11:41:49 -04:00
|
|
|
return checkAllUpdates().map(
|
|
|
|
[mod](Result<std::vector<ServerModUpdate>, ServerError>* result) -> Result<std::optional<ServerModUpdate>, ServerError> {
|
|
|
|
if (result->isOk()) {
|
|
|
|
for (auto& update : result->unwrap()) {
|
2024-06-03 11:12:48 -04:00
|
|
|
if (
|
|
|
|
update.id == mod->getID() &&
|
|
|
|
(update.version > mod->getVersion() || update.replacement.has_value())
|
|
|
|
) {
|
2024-04-22 11:41:49 -04:00
|
|
|
return Ok(update);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return Ok(std::nullopt);
|
|
|
|
}
|
|
|
|
return Err(result->unwrapErr());
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-09-09 04:55:09 -04:00
|
|
|
ServerRequest<std::vector<ServerModUpdate>> server::batchedCheckUpdates(std::vector<std::string> const& batch) {
|
2024-03-26 16:18:34 -04:00
|
|
|
auto req = web::WebRequest();
|
|
|
|
req.userAgent(getServerUserAgent());
|
|
|
|
req.param("platform", GEODE_PLATFORM_SHORT_IDENTIFIER);
|
2024-06-06 17:46:37 -04:00
|
|
|
req.param("gd", GEODE_GD_VERSION_STR);
|
2024-06-06 17:52:23 -04:00
|
|
|
req.param("geode", Loader::get()->getVersion().toNonVString());
|
2024-09-09 04:55:09 -04:00
|
|
|
|
|
|
|
req.param("ids", ranges::join(batch, ";"));
|
2024-04-28 17:31:14 -04:00
|
|
|
return req.get(formatServerURL("/mods/updates")).map(
|
2024-04-22 06:35:12 -04:00
|
|
|
[](web::WebResponse* response) -> Result<std::vector<ServerModUpdate>, ServerError> {
|
|
|
|
if (response->ok()) {
|
2024-03-26 16:18:34 -04:00
|
|
|
// Parse payload
|
2024-04-22 06:35:12 -04:00
|
|
|
auto payload = parseServerPayload(*response);
|
2024-03-26 16:18:34 -04:00
|
|
|
if (!payload) {
|
|
|
|
return Err(payload.unwrapErr());
|
|
|
|
}
|
|
|
|
// Parse response
|
|
|
|
auto list = ServerModUpdate::parseList(payload.unwrap());
|
|
|
|
if (!list) {
|
2024-04-22 06:35:12 -04:00
|
|
|
return Err(ServerError(response->code(), "Unable to parse response: {}", list.unwrapErr()));
|
2024-03-26 16:18:34 -04:00
|
|
|
}
|
|
|
|
return Ok(list.unwrap());
|
|
|
|
}
|
2024-04-22 06:35:12 -04:00
|
|
|
return Err(parseServerError(*response));
|
|
|
|
},
|
|
|
|
[](web::WebProgress* progress) {
|
|
|
|
return parseServerProgress(*progress, "Checking updates for mods");
|
|
|
|
}
|
|
|
|
);
|
2024-03-26 16:18:34 -04:00
|
|
|
}
|
2024-04-21 17:08:10 -04:00
|
|
|
|
2024-09-09 04:55:09 -04:00
|
|
|
void server::queueBatches(
|
|
|
|
ServerRequest<std::vector<ServerModUpdate>>::PostResult const resolve,
|
|
|
|
std::shared_ptr<std::vector<std::vector<std::string>>> const batches,
|
|
|
|
std::shared_ptr<std::vector<ServerModUpdate>> accum
|
|
|
|
) {
|
|
|
|
// we have to do the copy here, or else our values die
|
|
|
|
batchedCheckUpdates(batches->back()).listen([resolve, batches, accum](auto result) {
|
2024-11-04 15:24:20 -05:00
|
|
|
if (result->isOk()) {
|
2024-09-09 04:55:09 -04:00
|
|
|
auto serverValues = result->unwrap();
|
|
|
|
|
|
|
|
accum->reserve(accum->size() + serverValues.size());
|
|
|
|
accum->insert(accum->end(), serverValues.begin(), serverValues.end());
|
|
|
|
|
|
|
|
if (batches->size() > 1) {
|
|
|
|
batches->pop_back();
|
|
|
|
queueBatches(resolve, batches, accum);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
resolve(Ok(*accum));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
2024-11-04 15:24:20 -05:00
|
|
|
if (result->isOk()) {
|
|
|
|
resolve(Ok(result->unwrap()));
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
resolve(Err(result->unwrapErr()));
|
|
|
|
}
|
2024-09-09 04:55:09 -04:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
ServerRequest<std::vector<ServerModUpdate>> server::checkAllUpdates(bool useCache) {
|
|
|
|
if (useCache) {
|
|
|
|
return getCache<checkAllUpdates>().get();
|
|
|
|
}
|
|
|
|
|
|
|
|
auto modIDs = ranges::map<std::vector<std::string>>(
|
|
|
|
Loader::get()->getAllMods(),
|
|
|
|
[](auto mod) { return mod->getID(); }
|
|
|
|
);
|
|
|
|
|
|
|
|
// if there's no mods, the request would just be empty anyways
|
|
|
|
if (modIDs.empty()) {
|
|
|
|
// you would think it could infer like literally anything
|
|
|
|
return ServerRequest<std::vector<ServerModUpdate>>::immediate(
|
|
|
|
Ok<std::vector<ServerModUpdate>>({})
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
auto modBatches = std::make_shared<std::vector<std::vector<std::string>>>();
|
|
|
|
auto modCount = modIDs.size();
|
|
|
|
std::size_t maxMods = 200u; // this affects 0.03% of users
|
|
|
|
|
|
|
|
if (modCount <= maxMods) {
|
|
|
|
// no tricks needed
|
|
|
|
return batchedCheckUpdates(modIDs);
|
|
|
|
}
|
|
|
|
|
|
|
|
// even out the mod count, so a request with 230 mods sends two 115 mod requests
|
|
|
|
auto batchCount = modCount / maxMods + 1;
|
|
|
|
auto maxBatchSize = modCount / batchCount + 1;
|
|
|
|
|
|
|
|
for (std::size_t i = 0u; i < modCount; i += maxBatchSize) {
|
|
|
|
auto end = std::min(modCount, i + maxBatchSize);
|
|
|
|
modBatches->emplace_back(modIDs.begin() + i, modIDs.begin() + end);
|
|
|
|
}
|
|
|
|
|
|
|
|
// chain requests to avoid doing too many large requests at once
|
|
|
|
return ServerRequest<std::vector<ServerModUpdate>>::runWithCallback(
|
|
|
|
[modBatches](auto finish, auto progress, auto hasBeenCancelled) {
|
|
|
|
auto accum = std::make_shared<std::vector<ServerModUpdate>>();
|
|
|
|
queueBatches(finish, modBatches, accum);
|
|
|
|
},
|
|
|
|
"Mod Update Check"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-04-22 06:35:12 -04:00
|
|
|
void server::clearServerCaches(bool clearGlobalCaches) {
|
|
|
|
getCache<&getMods>().clear();
|
|
|
|
getCache<&getMod>().clear();
|
|
|
|
getCache<&getModLogo>().clear();
|
2024-04-21 17:08:10 -04:00
|
|
|
|
|
|
|
// Only clear global caches if explicitly requested
|
|
|
|
if (clearGlobalCaches) {
|
2024-04-22 06:35:12 -04:00
|
|
|
getCache<&getTags>().clear();
|
2024-04-22 11:41:49 -04:00
|
|
|
getCache<&checkAllUpdates>().clear();
|
2024-04-21 17:08:10 -04:00
|
|
|
}
|
|
|
|
}
|
2024-04-22 06:35:12 -04:00
|
|
|
|
2024-06-23 17:45:02 -04:00
|
|
|
$on_mod(Loaded) {
|
2024-04-22 06:35:12 -04:00
|
|
|
listenForSettingChanges<int64_t>("server-cache-size-limit", +[](int64_t size) {
|
|
|
|
getCache<&server::getMods>().limit(size);
|
|
|
|
getCache<&server::getMod>().limit(size);
|
|
|
|
getCache<&server::getModLogo>().limit(size);
|
|
|
|
getCache<&server::getTags>().limit(size);
|
2024-04-22 11:41:49 -04:00
|
|
|
getCache<&server::checkAllUpdates>().limit(size);
|
2024-04-22 06:35:12 -04:00
|
|
|
});
|
|
|
|
}
|