mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 02:43:44 +00:00
feat(dashboard): UGC lists paged with totals, prefix search and sorting; car and rocket assemblies
IUgcLookup::ListUgc pages either kind with the UGC search's matching, a state and a sort, and counts the matches (MySQL and SQLite share the SQL). The UGC server records each model's bricks and triangles (new ugc columns) so models can be sorted by size. Cars and rockets are listed as assemblies, one per combination of modules, with their build type, module names and how many builds use them; filters for type, state, module and owner. Each assembly's builds (with where they are) are paged, and a build's assembly can be looked up for links. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
155
dDashboardServer/routes/UgcAssemblies.h
Normal file
155
dDashboardServer/routes/UgcAssemblies.h
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "IUgcLookup.h"
|
||||||
|
#include "UgcKeys.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cars and rockets as the /ugc page lists them: one assembly per combination of modules (UgcModularKey::Normalize),
|
||||||
|
* however many builds (ugc_modular_build rows) use it, since the UGC server makes one icon per combination. Pure (the
|
||||||
|
* rows and the modules' CDClient data come in), so it can be unit tested; routes/UgcRoutes.cpp runs it.
|
||||||
|
*/
|
||||||
|
namespace UgcAssemblies {
|
||||||
|
struct ModuleInfo {
|
||||||
|
int32_t buildType{ -1 }; // ModuleComponent.buildType, -1 when the LOT isn't a module
|
||||||
|
std::string name; // Objects.displayName, else name
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Assembly {
|
||||||
|
std::string key;
|
||||||
|
std::vector<uint32_t> lots;
|
||||||
|
int32_t buildType{ -1 }; // of the first module that is one
|
||||||
|
std::vector<LWOOBJID> builds; // newest first
|
||||||
|
std::set<LWOOBJID> owners; // creator characters
|
||||||
|
IUgc::eProcessState state{ IUgc::eProcessState::PENDING };
|
||||||
|
LWOOBJID iconBuild{}; // a build whose icon is made (the newest made one, else the newest)
|
||||||
|
std::string error; // a failed build's error, when none is made
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class eSort : uint8_t { NEWEST, OLDEST, REFERENCES, NAME };
|
||||||
|
|
||||||
|
inline std::optional<eSort> ParseSort(std::string_view text) {
|
||||||
|
if (text.empty() || text == "newest") return eSort::NEWEST;
|
||||||
|
if (text == "oldest") return eSort::OLDEST;
|
||||||
|
if (text == "references" || text == "uses") return eSort::REFERENCES;
|
||||||
|
if (text == "name") return eSort::NAME;
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string Lower(std::string_view text) {
|
||||||
|
std::string out(text);
|
||||||
|
std::transform(out.begin(), out.end(), out.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The builds grouped by their combination; builds without modules are left out
|
||||||
|
inline std::vector<Assembly> Group(const std::vector<IUgcLookup::UgcEntry>& builds, const std::map<uint32_t, ModuleInfo>& modules) {
|
||||||
|
std::map<std::string, Assembly> byKey;
|
||||||
|
for (const auto& build : builds) {
|
||||||
|
auto key = UgcModularKey::Normalize(build.detail);
|
||||||
|
if (key.empty()) continue;
|
||||||
|
auto& assembly = byKey[key];
|
||||||
|
if (assembly.key.empty()) {
|
||||||
|
assembly.key = key;
|
||||||
|
assembly.lots = UgcModularKey::Lots(build.detail);
|
||||||
|
std::sort(assembly.lots.begin(), assembly.lots.end());
|
||||||
|
assembly.lots.erase(std::unique(assembly.lots.begin(), assembly.lots.end()), assembly.lots.end());
|
||||||
|
for (const auto lot : assembly.lots) {
|
||||||
|
const auto it = modules.find(lot);
|
||||||
|
if (it != modules.end() && it->second.buildType >= 0) {
|
||||||
|
assembly.buildType = it->second.buildType;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assembly.builds.push_back(build.id);
|
||||||
|
if (build.characterId != LWOOBJID_EMPTY) assembly.owners.insert(build.characterId);
|
||||||
|
}
|
||||||
|
// The combination's state is its icon's: made when any build of it is, else waiting, failed or empty
|
||||||
|
std::map<LWOOBJID, const IUgcLookup::UgcEntry*> byId;
|
||||||
|
for (const auto& build : builds) byId[build.id] = &build;
|
||||||
|
std::vector<Assembly> out;
|
||||||
|
for (auto& [_, assembly] : byKey) {
|
||||||
|
std::sort(assembly.builds.rbegin(), assembly.builds.rend());
|
||||||
|
bool made = false, waiting = false, failed = false;
|
||||||
|
for (const auto id : assembly.builds) {
|
||||||
|
const auto& build = *byId[id];
|
||||||
|
if (build.state == IUgc::eProcessState::DONE && !made) {
|
||||||
|
made = true;
|
||||||
|
assembly.iconBuild = id;
|
||||||
|
}
|
||||||
|
waiting = waiting || build.state == IUgc::eProcessState::PENDING;
|
||||||
|
if (build.state == IUgc::eProcessState::FAILED && !failed) {
|
||||||
|
failed = true;
|
||||||
|
assembly.error = build.error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!made) assembly.iconBuild = assembly.builds.front();
|
||||||
|
assembly.state = made ? IUgc::eProcessState::DONE : waiting ? IUgc::eProcessState::PENDING : failed ? IUgc::eProcessState::FAILED : IUgc::eProcessState::EMPTY;
|
||||||
|
if (made) assembly.error.clear();
|
||||||
|
out.push_back(std::move(assembly));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Filter {
|
||||||
|
std::optional<int32_t> buildType;
|
||||||
|
std::optional<IUgc::eProcessState> state;
|
||||||
|
std::optional<std::set<LWOOBJID>> builds; // only assemblies using one of these builds (a database search's matches)
|
||||||
|
std::string moduleText; // or: a module's name contains this (any case)
|
||||||
|
std::optional<uint32_t> moduleLot; // or: this module is in it
|
||||||
|
};
|
||||||
|
|
||||||
|
inline bool Matches(const Assembly& assembly, const Filter& filter, const std::map<uint32_t, ModuleInfo>& modules) {
|
||||||
|
if (filter.buildType && assembly.buildType != *filter.buildType) return false;
|
||||||
|
if (filter.state && assembly.state != *filter.state) return false;
|
||||||
|
const bool searching = filter.builds || !filter.moduleText.empty() || filter.moduleLot;
|
||||||
|
if (!searching) return true;
|
||||||
|
if (filter.builds) {
|
||||||
|
for (const auto id : assembly.builds) {
|
||||||
|
if (filter.builds->contains(id)) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (filter.moduleLot && std::find(assembly.lots.begin(), assembly.lots.end(), *filter.moduleLot) != assembly.lots.end()) return true;
|
||||||
|
if (!filter.moduleText.empty()) {
|
||||||
|
const auto wanted = Lower(filter.moduleText);
|
||||||
|
for (const auto lot : assembly.lots) {
|
||||||
|
const auto it = modules.find(lot);
|
||||||
|
if (it != modules.end() && Lower(it->second.name).find(wanted) != std::string::npos) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The first module's name (what "name" sorts by)
|
||||||
|
inline std::string Name(const Assembly& assembly, const std::map<uint32_t, ModuleInfo>& modules) {
|
||||||
|
for (const auto lot : assembly.lots) {
|
||||||
|
const auto it = modules.find(lot);
|
||||||
|
if (it != modules.end() && !it->second.name.empty()) return it->second.name;
|
||||||
|
}
|
||||||
|
return assembly.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void Sort(std::vector<Assembly>& list, eSort sort, const std::map<uint32_t, ModuleInfo>& modules) {
|
||||||
|
std::stable_sort(list.begin(), list.end(), [&](const Assembly& a, const Assembly& b) {
|
||||||
|
switch (sort) {
|
||||||
|
case eSort::OLDEST: return a.builds.back() < b.builds.back();
|
||||||
|
case eSort::REFERENCES: return a.builds.size() != b.builds.size() ? a.builds.size() > b.builds.size() : a.builds.front() > b.builds.front();
|
||||||
|
case eSort::NAME: {
|
||||||
|
const auto an = Lower(Name(a, modules)), bn = Lower(Name(b, modules));
|
||||||
|
return an != bn ? an < bn : a.key < b.key;
|
||||||
|
}
|
||||||
|
default: return a.builds.front() > b.builds.front();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -87,6 +87,46 @@ namespace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
namespace UgcLinks {
|
namespace UgcLinks {
|
||||||
|
std::map<LWOOBJID, nlohmann::json> Whereabouts(const std::vector<IUgcLookup::UgcEntry>& entries) {
|
||||||
|
std::vector<LWOOBJID> ids, modularIds;
|
||||||
|
std::set<LWOOBJID> models, modular;
|
||||||
|
for (const auto& entry : entries) {
|
||||||
|
ids.push_back(entry.id);
|
||||||
|
if (entry.kind == eUgcKind::MODEL) {
|
||||||
|
models.insert(entry.id);
|
||||||
|
} else {
|
||||||
|
modular.insert(entry.id);
|
||||||
|
modularIds.push_back(entry.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::map<LWOOBJID, nlohmann::json> where;
|
||||||
|
for (const auto& p : Database::Get()->GetUgcPlacements(ids)) {
|
||||||
|
where[p.ugcId].push_back({ { "type", "property" }, { "propertyId", std::to_string(p.propertyId) }, { "propertyName", p.propertyName },
|
||||||
|
{ "ownerId", std::to_string(p.ownerId) }, { "ownerName", p.ownerName }, { "zoneId", p.zoneId }, { "modelId", std::to_string(p.modelId) },
|
||||||
|
{ "lot", p.lot }, { "modelName", p.modelName }, { "modelDescription", p.modelDescription } });
|
||||||
|
}
|
||||||
|
for (const auto& mail : Database::Get()->GetUgcMail(modularIds, MODEL_ITEM_LOT)) {
|
||||||
|
if (const auto creation = MailCreation(mail, models, modular)) {
|
||||||
|
where[creation->second].push_back({ { "type", "mail" }, { "mailId", std::to_string(mail.id) }, { "characterId", std::to_string(mail.receiverId) },
|
||||||
|
{ "characterName", mail.receiverName } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Not placed or mailed: most are still with whoever made them
|
||||||
|
std::set<LWOOBJID> creators;
|
||||||
|
for (const auto& entry : entries) {
|
||||||
|
if (!where.contains(entry.id) && entry.characterId != LWOOBJID_EMPTY && creators.size() < MAX_INVENTORIES) creators.insert(entry.characterId);
|
||||||
|
}
|
||||||
|
for (const auto creator : creators) {
|
||||||
|
for (const auto& link : CharacterCreations(creator)) {
|
||||||
|
const bool wanted = link.kind == eUgcKind::MODEL ? models.contains(link.ugcId) : modular.contains(link.ugcId);
|
||||||
|
if (!wanted) continue;
|
||||||
|
where[link.ugcId].push_back({ { "type", "inventory" }, { "characterId", std::to_string(creator) }, { "inventory", link.item.inventory },
|
||||||
|
{ "itemId", std::to_string(link.item.itemId) }, { "lot", link.item.lot } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return where;
|
||||||
|
}
|
||||||
|
|
||||||
void RegisterRoutes() {
|
void RegisterRoutes() {
|
||||||
Route(eHTTPMethod::GET, "/ugc_search", Perm("properties_view"), "Find players' models, cars and rockets and where they are",
|
Route(eHTTPMethod::GET, "/ugc_search", Perm("properties_view"), "Find players' models, cars and rockets and where they are",
|
||||||
[](HTTPReply& reply, const HTTPContext& context) {
|
[](HTTPReply& reply, const HTTPContext& context) {
|
||||||
@@ -105,44 +145,7 @@ namespace UgcLinks {
|
|||||||
const auto limit = std::clamp(GeneralUtils::TryParse<uint32_t>(QueryValue(context.queryString, "limit")).value_or(SEARCH_LIMIT), 1u, SEARCH_LIMIT);
|
const auto limit = std::clamp(GeneralUtils::TryParse<uint32_t>(QueryValue(context.queryString, "limit")).value_or(SEARCH_LIMIT), 1u, SEARCH_LIMIT);
|
||||||
const auto entries = Database::Get()->SearchUgc(search, limit);
|
const auto entries = Database::Get()->SearchUgc(search, limit);
|
||||||
|
|
||||||
std::vector<LWOOBJID> ids, modularIds;
|
auto where = UgcLinks::Whereabouts(entries);
|
||||||
std::set<LWOOBJID> models, modular;
|
|
||||||
for (const auto& entry : entries) {
|
|
||||||
ids.push_back(entry.id);
|
|
||||||
if (entry.kind == eUgcKind::MODEL) {
|
|
||||||
models.insert(entry.id);
|
|
||||||
} else {
|
|
||||||
modular.insert(entry.id);
|
|
||||||
modularIds.push_back(entry.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
std::map<std::pair<eUgcKind, LWOOBJID>, nlohmann::json> where;
|
|
||||||
for (const auto& p : Database::Get()->GetUgcPlacements(ids)) {
|
|
||||||
const auto kind = models.contains(p.ugcId) ? eUgcKind::MODEL : eUgcKind::MODULAR;
|
|
||||||
where[{ kind, p.ugcId }].push_back({ { "type", "property" }, { "propertyId", std::to_string(p.propertyId) }, { "propertyName", p.propertyName },
|
|
||||||
{ "ownerId", std::to_string(p.ownerId) }, { "ownerName", p.ownerName }, { "zoneId", p.zoneId }, { "modelId", std::to_string(p.modelId) },
|
|
||||||
{ "lot", p.lot }, { "modelName", p.modelName }, { "modelDescription", p.modelDescription } });
|
|
||||||
}
|
|
||||||
for (const auto& mail : Database::Get()->GetUgcMail(modularIds, MODEL_ITEM_LOT)) {
|
|
||||||
if (const auto creation = MailCreation(mail, models, modular)) {
|
|
||||||
where[*creation].push_back({ { "type", "mail" }, { "mailId", std::to_string(mail.id) }, { "characterId", std::to_string(mail.receiverId) },
|
|
||||||
{ "characterName", mail.receiverName } });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Not placed or mailed: most are still with whoever made them
|
|
||||||
std::set<LWOOBJID> creators;
|
|
||||||
for (const auto& entry : entries) {
|
|
||||||
if (!where.contains({ entry.kind, entry.id }) && entry.characterId != LWOOBJID_EMPTY && creators.size() < MAX_INVENTORIES) creators.insert(entry.characterId);
|
|
||||||
}
|
|
||||||
for (const auto creator : creators) {
|
|
||||||
for (const auto& link : CharacterCreations(creator)) {
|
|
||||||
const std::pair key{ link.kind, link.ugcId };
|
|
||||||
const bool wanted = link.kind == eUgcKind::MODEL ? models.contains(link.ugcId) : modular.contains(link.ugcId);
|
|
||||||
if (!wanted) continue;
|
|
||||||
where[key].push_back({ { "type", "inventory" }, { "characterId", std::to_string(creator) }, { "inventory", link.item.inventory },
|
|
||||||
{ "itemId", std::to_string(link.item.itemId) }, { "lot", link.item.lot } });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nlohmann::json items = nlohmann::json::array();
|
nlohmann::json items = nlohmann::json::array();
|
||||||
for (const auto& entry : entries) {
|
for (const auto& entry : entries) {
|
||||||
@@ -152,7 +155,7 @@ namespace UgcLinks {
|
|||||||
item["characterName"] = entry.characterName;
|
item["characterName"] = entry.characterName;
|
||||||
item["accountId"] = entry.accountId;
|
item["accountId"] = entry.accountId;
|
||||||
item["accountName"] = entry.accountName;
|
item["accountName"] = entry.accountName;
|
||||||
const auto found = where.find({ entry.kind, entry.id });
|
const auto found = where.find(entry.id);
|
||||||
item["where"] = found == where.end() ? nlohmann::json::array() : found->second;
|
item["where"] = found == where.end() ? nlohmann::json::array() : found->second;
|
||||||
items.push_back(std::move(item));
|
items.push_back(std::move(item));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "IUgcLookup.h"
|
||||||
|
#include "json.hpp"
|
||||||
|
|
||||||
// Finding players' creations and showing them where they are: the UGC search, and the UGC server's icons and models on
|
// Finding players' creations and showing them where they are: the UGC search, and the UGC server's icons and models on
|
||||||
// the property and character pages (for whoever may view those pages)
|
// the property and character pages (for whoever may view those pages)
|
||||||
namespace UgcLinks {
|
namespace UgcLinks {
|
||||||
void RegisterRoutes();
|
void RegisterRoutes();
|
||||||
|
|
||||||
|
// Web thread: where each of these creations is (by id): placed on properties, attached to mails, or in its creator's
|
||||||
|
// inventories (looked in only for those not placed or mailed, and for at most a few creators). [{type: property|mail|inventory, ...}]
|
||||||
|
std::map<LWOOBJID, nlohmann::json> Whereabouts(const std::vector<IUgcLookup::UgcEntry>& entries);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ namespace UgcLookup {
|
|||||||
static const std::map<std::string, UgcSearch::eField> FIELDS = {
|
static const std::map<std::string, UgcSearch::eField> FIELDS = {
|
||||||
{ "id", UgcSearch::eField::ID }, { "owner", UgcSearch::eField::OWNER }, { "character", UgcSearch::eField::OWNER },
|
{ "id", UgcSearch::eField::ID }, { "owner", UgcSearch::eField::OWNER }, { "character", UgcSearch::eField::OWNER },
|
||||||
{ "account", UgcSearch::eField::OWNER }, { "property", UgcSearch::eField::PROPERTY }, { "model", UgcSearch::eField::MODEL },
|
{ "account", UgcSearch::eField::OWNER }, { "property", UgcSearch::eField::PROPERTY }, { "model", UgcSearch::eField::MODEL },
|
||||||
{ "name", UgcSearch::eField::MODEL }, { "lot", UgcSearch::eField::LOT },
|
{ "name", UgcSearch::eField::MODEL }, { "lot", UgcSearch::eField::LOT }, { "module", UgcSearch::eField::LOT },
|
||||||
};
|
};
|
||||||
const auto field = FIELDS.find(key);
|
const auto field = FIELDS.find(key);
|
||||||
if (field != FIELDS.end()) {
|
if (field != FIELDS.end()) {
|
||||||
|
|||||||
@@ -10,7 +10,10 @@
|
|||||||
#include "NifFile.h"
|
#include "NifFile.h"
|
||||||
#include "SettingsCatalog.h"
|
#include "SettingsCatalog.h"
|
||||||
#include "SettingsHistory.h"
|
#include "SettingsHistory.h"
|
||||||
|
#include "UgcAssemblies.h"
|
||||||
#include "UgcFetch.h"
|
#include "UgcFetch.h"
|
||||||
|
#include "UgcLinks.h"
|
||||||
|
#include "UgcLookup.h"
|
||||||
#include "UgcKeys.h"
|
#include "UgcKeys.h"
|
||||||
#include "Workers.h"
|
#include "Workers.h"
|
||||||
#include "Game.h"
|
#include "Game.h"
|
||||||
@@ -129,6 +132,76 @@ namespace {
|
|||||||
return kinds;
|
return kinds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Web thread: every module in the client's data (ModuleComponent) with its build type and name, read once
|
||||||
|
const std::map<uint32_t, UgcAssemblies::ModuleInfo>& Modules() {
|
||||||
|
static std::map<uint32_t, UgcAssemblies::ModuleInfo> modules;
|
||||||
|
static bool loaded = false;
|
||||||
|
if (loaded) return modules;
|
||||||
|
loaded = true;
|
||||||
|
auto result = CDClientDatabase::ExecuteQuery("SELECT cr.id AS lot, m.buildType AS buildType, o.name AS name, o.displayName AS displayName FROM ComponentsRegistry cr "
|
||||||
|
"JOIN ModuleComponent m ON m.id = cr.component_id LEFT JOIN Objects o ON o.id = cr.id WHERE cr.component_type = 28;");
|
||||||
|
while (!result.eof()) {
|
||||||
|
std::string name = result.getStringField("displayName", "");
|
||||||
|
if (name.empty()) name = result.getStringField("name", "");
|
||||||
|
modules[static_cast<uint32_t>(result.getIntField("lot", 0))] = { result.getIntField("buildType", -1), name };
|
||||||
|
result.nextRow();
|
||||||
|
}
|
||||||
|
return modules;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The /ugc search box: "state:" and "kind:" (or "type:") words are filters of their own, the rest is a UGC search
|
||||||
|
// (UgcLookup::ParseQuery: "owner:", "account:", "property:", "name:", "lot:"/"module:", "id:", or plain text)
|
||||||
|
struct ListSearch {
|
||||||
|
IUgcLookup::UgcSearch search;
|
||||||
|
std::optional<IUgc::eProcessState> state;
|
||||||
|
std::string kind;
|
||||||
|
std::string text; // what went to ParseQuery
|
||||||
|
};
|
||||||
|
ListSearch ParseListSearch(const std::string& input) {
|
||||||
|
ListSearch out;
|
||||||
|
std::string rest;
|
||||||
|
size_t start = 0;
|
||||||
|
while (start < input.size()) {
|
||||||
|
auto end = input.find(' ', start);
|
||||||
|
if (end == std::string::npos) end = input.size();
|
||||||
|
const auto word = input.substr(start, end - start);
|
||||||
|
const auto lower = UgcAssemblies::Lower(word);
|
||||||
|
if (lower.starts_with("state:") && IUgc::ParseProcessState(lower.substr(6))) out.state = IUgc::ParseProcessState(lower.substr(6));
|
||||||
|
else if (lower.starts_with("kind:") && lower.size() > 5) out.kind = lower.substr(5);
|
||||||
|
else if (lower.starts_with("type:") && lower.size() > 5) out.kind = lower.substr(5);
|
||||||
|
else if (!word.empty()) rest += (rest.empty() ? "" : " ") + word;
|
||||||
|
start = end + 1;
|
||||||
|
}
|
||||||
|
out.text = rest;
|
||||||
|
out.search = UgcLookup::ParseQuery(rest);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
nlohmann::json ModulesJson(const std::vector<uint32_t>& lots) {
|
||||||
|
nlohmann::json out = nlohmann::json::array();
|
||||||
|
const auto& modules = Modules();
|
||||||
|
for (const auto lot : lots) {
|
||||||
|
const auto it = modules.find(lot);
|
||||||
|
out.push_back({ { "lot", lot }, { "name", it != modules.end() ? it->second.name : std::string() }, { "icon", "/api/icon/" + std::to_string(lot) } });
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
nlohmann::json EntryJson(const IUgcLookup::UgcEntry& entry) {
|
||||||
|
return { { "id", std::to_string(entry.id) }, { "characterId", std::to_string(entry.characterId) }, { "characterName", entry.characterName },
|
||||||
|
{ "accountId", entry.accountId }, { "accountName", entry.accountName }, { "state", IUgc::ProcessStateName(entry.state) }, { "attempts", entry.attempts },
|
||||||
|
{ "processedAt", entry.processedAt }, { "error", entry.error }, { "bakeAo", entry.bakeAo }, { "processAfter", entry.processAfter },
|
||||||
|
{ "detail", entry.detail }, { "bricks", entry.bricks }, { "triangles", entry.triangles } };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Web thread: every car and rocket build (the assemblies are made from them)
|
||||||
|
std::vector<IUgcLookup::UgcEntry> AllBuilds(const IUgcLookup::UgcSearch& search = {}) {
|
||||||
|
IUgcLookup::UgcListQuery query;
|
||||||
|
query.search = search;
|
||||||
|
query.limit = 1000000;
|
||||||
|
return Database::Get()->ListUgc(IUgcLookup::eUgcKind::MODULAR, query).first;
|
||||||
|
}
|
||||||
|
|
||||||
// Web thread: the values stored for a target, or null
|
// Web thread: the values stored for a target, or null
|
||||||
nlohmann::json StoredValues(const std::string& target) {
|
nlohmann::json StoredValues(const std::string& target) {
|
||||||
const auto stored = Database::Get()->GetUgcIconSettings(target);
|
const auto stored = Database::Get()->GetUgcIconSettings(target);
|
||||||
@@ -165,35 +238,129 @@ namespace UgcRoutes {
|
|||||||
[](HTTPReply& reply, const HTTPContext& context) { RenderPage(reply, context, "ugc.jinja2", "ugc"); });
|
[](HTTPReply& reply, const HTTPContext& context) { RenderPage(reply, context, "ugc.jinja2", "ugc"); });
|
||||||
|
|
||||||
Route(eHTTPMethod::GET, "/api/ugc", Perm("properties_view"),
|
Route(eHTTPMethod::GET, "/api/ugc", Perm("properties_view"),
|
||||||
"Processing state: {counts: {model, modular: {pending, done, failed}}, items: [{id, characterId, characterName, state, attempts, processedAt, "
|
"A page of player models (kind=model) or of car and rocket assemblies (kind=modular: one per combination of modules, however many builds use "
|
||||||
"error, bakeAo, modules}], ugcPublicUrl, canManage}. Query: ?kind=model|modular&state=pending|done|failed&search=(id or owner name)&page=&size=",
|
"it). Query: q= (\"state:\", \"kind:\"/\"type:\" (a build type, e.g. build6), \"owner:\", \"account:\", \"property:\", \"name:\", "
|
||||||
|
"\"lot:\"/\"module:\" (a LOT or a module's name), \"id:\", or plain text across names, owners and ids), state=, type=, sort=newest|oldest|owner|name|"
|
||||||
|
"bricks|triangles (models) or newest|oldest|references|name (assemblies), page= (from 0), size= (1-200). {items, total, page, size, counts, "
|
||||||
|
"kinds, ugcPublicUrl, canManage}",
|
||||||
[](HTTPReply& reply, const HTTPContext& context) {
|
[](HTTPReply& reply, const HTTPContext& context) {
|
||||||
const bool modular = QueryValue(context.queryString, "kind") == "modular";
|
const bool modular = QueryValue(context.queryString, "kind") == "modular";
|
||||||
const auto state = ParseState(QueryValue(context.queryString, "state"));
|
auto parsed = ParseListSearch(QueryValue(context.queryString, "q").substr(0, 100));
|
||||||
|
if (const auto state = IUgc::ParseProcessState(QueryValue(context.queryString, "state"))) parsed.state = state;
|
||||||
|
if (const auto type = QueryValue(context.queryString, "type"); !type.empty()) parsed.kind = type;
|
||||||
const auto page = GeneralUtils::TryParse<uint32_t>(QueryValue(context.queryString, "page")).value_or(0);
|
const auto page = GeneralUtils::TryParse<uint32_t>(QueryValue(context.queryString, "page")).value_or(0);
|
||||||
const auto search = QueryValue(context.queryString, "search").substr(0, 64);
|
|
||||||
const auto size = std::clamp(GeneralUtils::TryParse<uint32_t>(QueryValue(context.queryString, "size")).value_or(PAGE_SIZE), 1u, 200u);
|
const auto size = std::clamp(GeneralUtils::TryParse<uint32_t>(QueryValue(context.queryString, "size")).value_or(PAGE_SIZE), 1u, 200u);
|
||||||
const auto list = modular ? Database::Get()->GetModularBuildProcessList(state, search, page * size, size + 1)
|
const auto sortText = QueryValue(context.queryString, "sort");
|
||||||
: Database::Get()->GetUgcProcessList(state, search, page * size, size + 1);
|
|
||||||
// How many builds share each combination of modules (they share one icon)
|
|
||||||
std::map<std::string, uint64_t> reuse;
|
|
||||||
if (modular) {
|
|
||||||
for (const auto& [ldf, count] : Database::Get()->GetModularBuildConfigCounts()) reuse[UgcModularKey::Normalize(ldf)] += count;
|
|
||||||
}
|
|
||||||
nlohmann::json items = nlohmann::json::array();
|
nlohmann::json items = nlohmann::json::array();
|
||||||
for (size_t i = 0; i < list.size() && i < size; i++) {
|
uint64_t total = 0;
|
||||||
const auto& info = list[i];
|
const auto kinds = IconKinds();
|
||||||
items.push_back({ { "id", std::to_string(info.id) }, { "characterId", std::to_string(info.characterId) }, { "characterName", info.characterName },
|
if (!modular) {
|
||||||
{ "state", StateName(info.state) }, { "attempts", info.attempts }, { "processedAt", info.processedAt }, { "error", info.error },
|
IUgcLookup::UgcListQuery query;
|
||||||
{ "bakeAo", info.bakeAo }, { "modules", info.details }, { "processAfter", info.processAfter } });
|
query.search = parsed.search;
|
||||||
if (modular) {
|
query.state = parsed.state;
|
||||||
const auto key = UgcModularKey::Normalize(info.details);
|
static const std::map<std::string, IUgcLookup::eSort> SORTS = { { "newest", IUgcLookup::eSort::NEWEST }, { "oldest", IUgcLookup::eSort::OLDEST },
|
||||||
items.back()["combination"] = key;
|
{ "owner", IUgcLookup::eSort::OWNER }, { "name", IUgcLookup::eSort::NAME }, { "bricks", IUgcLookup::eSort::BRICKS }, { "triangles", IUgcLookup::eSort::TRIANGLES } };
|
||||||
items.back()["sharedBy"] = reuse[key];
|
if (const auto it = SORTS.find(sortText); it != SORTS.end()) query.sort = it->second;
|
||||||
|
query.offset = page * size;
|
||||||
|
query.limit = size;
|
||||||
|
const auto [entries, count] = Database::Get()->ListUgc(IUgcLookup::eUgcKind::MODEL, query);
|
||||||
|
total = count;
|
||||||
|
for (const auto& entry : entries) items.push_back(EntryJson(entry));
|
||||||
|
} else {
|
||||||
|
UgcAssemblies::Filter filter;
|
||||||
|
filter.state = parsed.state;
|
||||||
|
if (!parsed.kind.empty()) {
|
||||||
|
for (const auto& k : kinds) {
|
||||||
|
if (k.contains("buildType") && (k["kind"] == parsed.kind || UgcAssemblies::Lower(k["label"].get<std::string>()).find(parsed.kind) != std::string::npos)) {
|
||||||
|
filter.buildType = k["buildType"].get<int32_t>();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!filter.buildType) filter.buildType = -2; // no such type: nothing
|
||||||
|
}
|
||||||
|
const auto& modules = Modules();
|
||||||
|
if (!parsed.search.text.empty() || parsed.search.number) {
|
||||||
|
using eField = IUgcLookup::UgcSearch::eField;
|
||||||
|
const auto field = parsed.search.field;
|
||||||
|
if (field != eField::LOT) {
|
||||||
|
std::set<LWOOBJID> matched;
|
||||||
|
for (const auto& build : AllBuilds(parsed.search)) matched.insert(build.id);
|
||||||
|
filter.builds = matched;
|
||||||
|
}
|
||||||
|
if (field == eField::ANY || field == eField::LOT) {
|
||||||
|
filter.moduleText = parsed.search.text;
|
||||||
|
if (parsed.search.number) filter.moduleLot = static_cast<uint32_t>(*parsed.search.number);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto assemblies = UgcAssemblies::Group(AllBuilds(), modules);
|
||||||
|
std::erase_if(assemblies, [&](const auto& a) { return !UgcAssemblies::Matches(a, filter, modules); });
|
||||||
|
UgcAssemblies::Sort(assemblies, UgcAssemblies::ParseSort(sortText).value_or(UgcAssemblies::eSort::NEWEST), modules);
|
||||||
|
total = assemblies.size();
|
||||||
|
for (size_t i = static_cast<size_t>(page) * size; i < assemblies.size() && i < static_cast<size_t>(page + 1) * size; i++) {
|
||||||
|
const auto& a = assemblies[i];
|
||||||
|
std::string label, kind;
|
||||||
|
for (const auto& k : kinds) {
|
||||||
|
if (k.contains("buildType") && k["buildType"] == a.buildType) {
|
||||||
|
label = k["label"];
|
||||||
|
kind = k["kind"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::string ldf = a.key;
|
||||||
|
std::replace(ldf.begin(), ldf.end(), '-', '+');
|
||||||
|
items.push_back({ { "id", a.key }, { "key", a.key }, { "modules", ldf }, { "moduleList", ModulesJson(a.lots) }, { "buildType", a.buildType },
|
||||||
|
{ "kind", kind }, { "kindLabel", label }, { "state", IUgc::ProcessStateName(a.state) }, { "error", a.error }, { "uses", a.builds.size() },
|
||||||
|
{ "owners", a.owners.size() }, { "iconBuild", std::to_string(a.iconBuild) }, { "newestBuild", std::to_string(a.builds.front()) },
|
||||||
|
{ "storageId", std::to_string(UgcModularKey::StorageId(a.key)) } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
JsonSuccess(reply, { { "counts", { { "model", Counts(Database::Get()->GetUgcProcessCounts()) }, { "modular", Counts(Database::Get()->GetModularBuildProcessCounts()) } } },
|
JsonSuccess(reply, { { "counts", { { "model", Counts(Database::Get()->GetUgcProcessCounts()) }, { "modular", Counts(Database::Get()->GetModularBuildProcessCounts()) } } },
|
||||||
{ "items", items }, { "more", list.size() > size }, { "ugcPublicUrl", Game::config->GetValue("ugc_public_url") }, { "canManage", Can(context, "ugc_manage") } });
|
{ "items", items }, { "total", total }, { "page", page }, { "size", size }, { "more", static_cast<uint64_t>(page + 1) * size < total }, { "kinds", kinds },
|
||||||
|
{ "ugcPublicUrl", Game::config->GetValue("ugc_public_url") }, { "canManage", Can(context, "ugc_manage") } });
|
||||||
|
});
|
||||||
|
|
||||||
|
Route(eHTTPMethod::GET, "/api/ugc/assembly/builds", Perm("properties_view"),
|
||||||
|
"The builds (ugc_modular_build rows) that use a combination of modules: {items: [{id, characterId, characterName, accountId, accountName, state, "
|
||||||
|
"attempts, processedAt, error, where: [{type: property|mail|inventory, ...}]}], total, page, size}. Query: modules= (the combination), q= (owner, "
|
||||||
|
"account, property or id, as the list's search), page=, size=, build= (a build to show: the page holding it is given)",
|
||||||
|
[](HTTPReply& reply, const HTTPContext& context) {
|
||||||
|
auto asked = QueryValue(context.queryString, "modules");
|
||||||
|
std::replace(asked.begin(), asked.end(), '-', '+');
|
||||||
|
const auto key = UgcModularKey::Normalize(asked);
|
||||||
|
if (key.empty()) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "No modules");
|
||||||
|
const auto parsed = ParseListSearch(QueryValue(context.queryString, "q").substr(0, 100));
|
||||||
|
auto builds = AllBuilds(parsed.search);
|
||||||
|
std::erase_if(builds, [&](const auto& b) { return UgcModularKey::Normalize(b.detail) != key || (parsed.state && b.state != *parsed.state); });
|
||||||
|
std::sort(builds.begin(), builds.end(), [](const auto& a, const auto& b) { return a.id > b.id; });
|
||||||
|
const auto size = std::clamp(GeneralUtils::TryParse<uint32_t>(QueryValue(context.queryString, "size")).value_or(25), 1u, 200u);
|
||||||
|
auto page = GeneralUtils::TryParse<uint32_t>(QueryValue(context.queryString, "page")).value_or(0);
|
||||||
|
if (const auto wanted = GeneralUtils::TryParse<LWOOBJID>(QueryValue(context.queryString, "build"))) {
|
||||||
|
const auto it = std::find_if(builds.begin(), builds.end(), [&](const auto& b) { return b.id == *wanted; });
|
||||||
|
if (it != builds.end()) page = static_cast<uint32_t>((it - builds.begin()) / size);
|
||||||
|
}
|
||||||
|
std::vector<IUgcLookup::UgcEntry> shown;
|
||||||
|
for (size_t i = static_cast<size_t>(page) * size; i < builds.size() && i < static_cast<size_t>(page + 1) * size; i++) shown.push_back(builds[i]);
|
||||||
|
const auto where = UgcLinks::Whereabouts(shown);
|
||||||
|
nlohmann::json items = nlohmann::json::array();
|
||||||
|
for (const auto& build : shown) {
|
||||||
|
auto item = EntryJson(build);
|
||||||
|
const auto found = where.find(build.id);
|
||||||
|
item["where"] = found == where.end() ? nlohmann::json::array() : found->second;
|
||||||
|
items.push_back(std::move(item));
|
||||||
|
}
|
||||||
|
JsonSuccess(reply, { { "items", items }, { "total", builds.size() }, { "page", page }, { "size", size }, { "key", key } });
|
||||||
|
});
|
||||||
|
|
||||||
|
Route(eHTTPMethod::GET, "/api/ugc/assembly/of/:id", Perm("properties_view"),
|
||||||
|
"The assembly (combination of modules) a car or rocket build uses: {key, modules}",
|
||||||
|
[](HTTPReply& reply, const HTTPContext& context) {
|
||||||
|
const auto id = PathId<LWOOBJID>(context.path, 4);
|
||||||
|
if (!id) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "Invalid id");
|
||||||
|
const auto info = Database::Get()->GetModularBuildProcessInfo(*id);
|
||||||
|
if (!info) return JsonError(reply, eHTTPStatusCode::NOT_FOUND, "No such build");
|
||||||
|
const auto key = UgcModularKey::Normalize(info->details);
|
||||||
|
std::string ldf = key;
|
||||||
|
std::replace(ldf.begin(), ldf.end(), '-', '+');
|
||||||
|
JsonSuccess(reply, { { "key", key }, { "modules", ldf }, { "moduleList", ModulesJson(UgcModularKey::Lots(ldf)) } });
|
||||||
});
|
});
|
||||||
|
|
||||||
Route(eHTTPMethod::GET, "/api/ugc/:id/lxfml", Perm("properties_view"), "A player model's LXFML",
|
Route(eHTTPMethod::GET, "/api/ugc/:id/lxfml", Perm("properties_view"), "A player model's LXFML",
|
||||||
|
|||||||
@@ -92,6 +92,9 @@ public:
|
|||||||
// whether lighting was baked in; processed_at becomes now
|
// whether lighting was baked in; processed_at becomes now
|
||||||
virtual void SetUgcModelProcessed(const LWOOBJID id, const eProcessState state, const uint32_t attempts, const std::string_view error, const bool bakeAo) = 0;
|
virtual void SetUgcModelProcessed(const LWOOBJID id, const eProcessState state, const uint32_t attempts, const std::string_view error, const bool bakeAo) = 0;
|
||||||
|
|
||||||
|
// What the UGC server counted when it made a model: its bricks and the most detailed mesh's triangles
|
||||||
|
virtual void SetUgcModelStats(const LWOOBJID id, const uint32_t bricks, const uint32_t triangles) = 0;
|
||||||
|
|
||||||
virtual std::optional<ProcessInfo> GetUgcProcessInfo(const LWOOBJID id) = 0;
|
virtual std::optional<ProcessInfo> GetUgcProcessInfo(const LWOOBJID id) = 0;
|
||||||
|
|
||||||
// Sets models back to pending with no attempts: one (`id`), or all of them (`id` nullopt; only the failed ones
|
// Sets models back to pending with no attempts: one (`id`), or all of them (`id` nullopt; only the failed ones
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ public:
|
|||||||
IUgc::eProcessState state{};
|
IUgc::eProcessState state{};
|
||||||
std::string error; // why the UGC server failed, if it did
|
std::string error; // why the UGC server failed, if it did
|
||||||
std::string detail; // models: the file name it was uploaded as; modular builds: the modules
|
std::string detail; // models: the file name it was uploaded as; modular builds: the modules
|
||||||
|
uint32_t attempts{};
|
||||||
|
int64_t processedAt{}; // Unix seconds of the UGC server's last attempt, 0 for none
|
||||||
|
int64_t processAfter{}; // models: the end of the quiet period after a save
|
||||||
|
bool bakeAo{};
|
||||||
|
uint32_t bricks{}; // models: counted by the UGC server when it made them (0: not yet)
|
||||||
|
uint32_t triangles{}; // models: of the made mesh's most detailed level (0: not yet)
|
||||||
};
|
};
|
||||||
|
|
||||||
// What SearchUgc matches. A number (when set) is matched against ids; text against names
|
// What SearchUgc matches. A number (when set) is matched against ids; text against names
|
||||||
@@ -47,6 +53,27 @@ public:
|
|||||||
std::optional<int64_t> number; // matched exactly against ids
|
std::optional<int64_t> number; // matched exactly against ids
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class eSort : uint8_t {
|
||||||
|
NEWEST, // the highest id first (ids are handed out in order)
|
||||||
|
OLDEST,
|
||||||
|
OWNER, // the creator's character name
|
||||||
|
NAME, // models: the upload's file name; modular builds: the modules
|
||||||
|
BRICKS, // models: the most bricks first
|
||||||
|
TRIANGLES, // models: the most triangles first
|
||||||
|
};
|
||||||
|
|
||||||
|
// A page of one kind: all of them or those matching the search (the same matching as SearchUgc), in a state or any
|
||||||
|
struct UgcListQuery {
|
||||||
|
UgcSearch search;
|
||||||
|
std::optional<IUgc::eProcessState> state;
|
||||||
|
eSort sort{ eSort::NEWEST };
|
||||||
|
uint32_t offset{};
|
||||||
|
uint32_t limit{ 50 };
|
||||||
|
};
|
||||||
|
|
||||||
|
// The page and how many match in all
|
||||||
|
virtual std::pair<std::vector<UgcEntry>, uint64_t> ListUgc(const eUgcKind kind, const UgcListQuery& query) = 0;
|
||||||
|
|
||||||
// Up to `limit` of each kind, the newest first
|
// Up to `limit` of each kind, the newest first
|
||||||
virtual std::vector<UgcEntry> SearchUgc(const UgcSearch& search, const uint32_t limit) = 0;
|
virtual std::vector<UgcEntry> SearchUgc(const UgcSearch& search, const uint32_t limit) = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -19,15 +19,45 @@ namespace UgcLookupSql {
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The columns every entry query selects, for models (ugc AS u) or modular builds (ugc_modular_build AS b)
|
// The tables every entry query reads, for models (ugc AS u) or modular builds (ugc_modular_build AS b)
|
||||||
inline std::string Select(bool modular) {
|
inline std::string From(bool modular) {
|
||||||
return modular
|
return modular
|
||||||
|
? "FROM ugc_modular_build AS b LEFT JOIN charinfo AS c ON c.id = b.character_id LEFT JOIN accounts AS a ON a.id = c.account_id "
|
||||||
|
: "FROM ugc AS u LEFT JOIN charinfo AS c ON c.id = u.character_id LEFT JOIN accounts AS a ON a.id = u.account_id ";
|
||||||
|
}
|
||||||
|
|
||||||
|
// The columns every entry query selects
|
||||||
|
inline std::string Select(bool modular) {
|
||||||
|
return (modular
|
||||||
? "SELECT b.ugc_id AS id, b.character_id, c.name AS character_name, COALESCE(c.account_id, 0) AS account_id, a.name AS account_name, "
|
? "SELECT b.ugc_id AS id, b.character_id, c.name AS character_name, COALESCE(c.account_id, 0) AS account_id, a.name AS account_name, "
|
||||||
"b.is_optimized, b.process_error, b.ldf_config AS detail FROM ugc_modular_build AS b "
|
"b.is_optimized, b.process_error, b.ldf_config AS detail, b.process_attempts, b.processed_at, 0 AS process_after, 0 AS bake_ao, "
|
||||||
"LEFT JOIN charinfo AS c ON c.id = b.character_id LEFT JOIN accounts AS a ON a.id = c.account_id "
|
"0 AS brick_count, 0 AS triangle_count "
|
||||||
: "SELECT u.id, u.character_id, c.name AS character_name, u.account_id, a.name AS account_name, "
|
: "SELECT u.id, u.character_id, c.name AS character_name, u.account_id, a.name AS account_name, "
|
||||||
"u.is_optimized, u.process_error, u.filename AS detail FROM ugc AS u "
|
"u.is_optimized, u.process_error, u.filename AS detail, u.process_attempts, u.processed_at, u.process_after, u.bake_ao, "
|
||||||
"LEFT JOIN charinfo AS c ON c.id = u.character_id LEFT JOIN accounts AS a ON a.id = u.account_id ";
|
"u.brick_count, u.triangle_count ") + From(modular);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whether a search has anything to match (else a list is of everything)
|
||||||
|
inline bool Searching(const IUgcLookup::UgcSearch& search) { return !search.text.empty() || search.number.has_value(); }
|
||||||
|
|
||||||
|
// A list's state filter and order (after Where, or "WHERE 1=1 " when not searching)
|
||||||
|
inline std::string ListFilter(const IUgcLookup::UgcListQuery& query, bool modular) {
|
||||||
|
const std::string table = modular ? "b." : "u.";
|
||||||
|
std::string sql = query.state ? "AND " + table + "is_optimized = " + std::to_string(static_cast<int32_t>(*query.state)) + " " : "";
|
||||||
|
return sql;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string ListOrder(const IUgcLookup::UgcListQuery& query, bool modular) {
|
||||||
|
using eSort = IUgcLookup::eSort;
|
||||||
|
const std::string id = modular ? "b.ugc_id" : "u.id";
|
||||||
|
switch (query.sort) {
|
||||||
|
case eSort::OLDEST: return "ORDER BY " + id + " ASC ";
|
||||||
|
case eSort::OWNER: return "ORDER BY c.name ASC, " + id + " DESC ";
|
||||||
|
case eSort::NAME: return std::string("ORDER BY ") + (modular ? "b.ldf_config" : "u.filename") + " ASC, " + id + " DESC ";
|
||||||
|
case eSort::BRICKS: return modular ? "ORDER BY " + id + " DESC " : "ORDER BY u.brick_count DESC, u.id DESC ";
|
||||||
|
case eSort::TRIANGLES: return modular ? "ORDER BY " + id + " DESC " : "ORDER BY u.triangle_count DESC, u.id DESC ";
|
||||||
|
default: return "ORDER BY " + id + " DESC ";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WHERE for a search; binds the search's %text% TEXT_BINDS times
|
// WHERE for a search; binds the search's %text% TEXT_BINDS times
|
||||||
|
|||||||
@@ -222,6 +222,7 @@ public:
|
|||||||
|
|
||||||
// IUgcLookup
|
// IUgcLookup
|
||||||
std::vector<UgcEntry> SearchUgc(const UgcSearch& search, const uint32_t limit) override;
|
std::vector<UgcEntry> SearchUgc(const UgcSearch& search, const uint32_t limit) override;
|
||||||
|
std::pair<std::vector<UgcEntry>, uint64_t> ListUgc(const eUgcKind kind, const UgcListQuery& query) override;
|
||||||
std::vector<UgcEntry> GetUgcEntries(const std::vector<LWOOBJID>& ids) override;
|
std::vector<UgcEntry> GetUgcEntries(const std::vector<LWOOBJID>& ids) override;
|
||||||
std::vector<UgcPlacement> GetUgcPlacements(const std::vector<LWOOBJID>& ugcIds) override;
|
std::vector<UgcPlacement> GetUgcPlacements(const std::vector<LWOOBJID>& ugcIds) override;
|
||||||
std::vector<UgcMail> GetUgcMail(const std::vector<LWOOBJID>& subkeys, const LOT modelItemLot) override;
|
std::vector<UgcMail> GetUgcMail(const std::vector<LWOOBJID>& subkeys, const LOT modelItemLot) override;
|
||||||
@@ -427,6 +428,7 @@ public:
|
|||||||
void SetUgcIconSettings(const std::string_view target, const std::string_view params) override;
|
void SetUgcIconSettings(const std::string_view target, const std::string_view params) override;
|
||||||
void DeleteUgcIconSettings(const std::string_view target) override;
|
void DeleteUgcIconSettings(const std::string_view target) override;
|
||||||
void SetUgcModelProcessed(const LWOOBJID id, const eProcessState state, const uint32_t attempts, const std::string_view error, const bool bakeAo) override;
|
void SetUgcModelProcessed(const LWOOBJID id, const eProcessState state, const uint32_t attempts, const std::string_view error, const bool bakeAo) override;
|
||||||
|
void SetUgcModelStats(const LWOOBJID id, const uint32_t bricks, const uint32_t triangles) override;
|
||||||
std::optional<IUgc::ProcessInfo> GetUgcProcessInfo(const LWOOBJID id) override;
|
std::optional<IUgc::ProcessInfo> GetUgcProcessInfo(const LWOOBJID id) override;
|
||||||
uint64_t ResetUgcModelProcessing(const std::optional<LWOOBJID> id, const bool failedOnly) override;
|
uint64_t ResetUgcModelProcessing(const std::optional<LWOOBJID> id, const bool failedOnly) override;
|
||||||
std::vector<IUgc::ProcessInfo> GetUgcProcessList(const std::optional<eProcessState> state, const std::string_view search, const uint32_t offset, const uint32_t limit) override;
|
std::vector<IUgc::ProcessInfo> GetUgcProcessList(const std::optional<eProcessState> state, const std::string_view search, const uint32_t offset, const uint32_t limit) override;
|
||||||
|
|||||||
@@ -177,3 +177,7 @@ void MySQLDatabase::ExpediteUgcModel(const LWOOBJID id) {
|
|||||||
void MySQLDatabase::ExpediteUgcModels(const LWOOBJID characterId) {
|
void MySQLDatabase::ExpediteUgcModels(const LWOOBJID characterId) {
|
||||||
ExecuteUpdate("UPDATE ugc SET process_after = 0 WHERE character_id = ? AND is_optimized = 0 AND process_after > 0;", characterId);
|
ExecuteUpdate("UPDATE ugc SET process_after = 0 WHERE character_id = ? AND is_optimized = 0 AND process_after > 0;", characterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MySQLDatabase::SetUgcModelStats(const LWOOBJID id, const uint32_t bricks, const uint32_t triangles) {
|
||||||
|
ExecuteUpdate("UPDATE ugc SET brick_count = ?, triangle_count = ? WHERE id = ?;", bricks, triangles, id);
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ namespace {
|
|||||||
entry.state = static_cast<IUgc::eProcessState>(result->getInt("is_optimized"));
|
entry.state = static_cast<IUgc::eProcessState>(result->getInt("is_optimized"));
|
||||||
entry.error = std::string(result->getString("process_error").c_str());
|
entry.error = std::string(result->getString("process_error").c_str());
|
||||||
entry.detail = std::string(result->getString("detail").c_str());
|
entry.detail = std::string(result->getString("detail").c_str());
|
||||||
|
entry.attempts = static_cast<uint32_t>(result->getInt("process_attempts"));
|
||||||
|
entry.processedAt = result->getInt64("processed_at");
|
||||||
|
entry.processAfter = result->getInt64("process_after");
|
||||||
|
entry.bakeAo = result->getInt("bake_ao") != 0;
|
||||||
|
entry.bricks = static_cast<uint32_t>(result->getInt64("brick_count"));
|
||||||
|
entry.triangles = static_cast<uint32_t>(result->getInt64("triangle_count"));
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,3 +81,26 @@ std::vector<IUgcLookup::UgcMail> MySQLDatabase::GetUgcMail(const std::vector<LWO
|
|||||||
}
|
}
|
||||||
return mail;
|
return mail;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::pair<std::vector<IUgcLookup::UgcEntry>, uint64_t> MySQLDatabase::ListUgc(const eUgcKind kind, const UgcListQuery& query) {
|
||||||
|
const bool modular = kind == eUgcKind::MODULAR;
|
||||||
|
const std::string pattern = "%" + query.search.text + "%";
|
||||||
|
const bool searching = UgcLookupSql::Searching(query.search);
|
||||||
|
const auto where = (searching ? UgcLookupSql::Where(query.search, modular) : std::string("WHERE 1=1 ")) + UgcLookupSql::ListFilter(query, modular);
|
||||||
|
std::vector<UgcEntry> entries;
|
||||||
|
uint64_t total = 0;
|
||||||
|
const auto page = UgcLookupSql::Select(modular) + where + UgcLookupSql::ListOrder(query, modular) + "LIMIT ? OFFSET ?;";
|
||||||
|
const auto count = "SELECT COUNT(*) AS n " + UgcLookupSql::From(modular) + where + ";";
|
||||||
|
if (searching) {
|
||||||
|
auto result = ExecuteSelect(page, pattern, pattern, pattern, pattern, pattern, pattern, query.limit, query.offset);
|
||||||
|
while (result->next()) entries.push_back(ReadEntry(result, kind));
|
||||||
|
auto counted = ExecuteSelect(count, pattern, pattern, pattern, pattern, pattern, pattern);
|
||||||
|
if (counted->next()) total = static_cast<uint64_t>(counted->getInt64("n"));
|
||||||
|
} else {
|
||||||
|
auto result = ExecuteSelect(page, query.limit, query.offset);
|
||||||
|
while (result->next()) entries.push_back(ReadEntry(result, kind));
|
||||||
|
auto counted = ExecuteSelect(count);
|
||||||
|
if (counted->next()) total = static_cast<uint64_t>(counted->getInt64("n"));
|
||||||
|
}
|
||||||
|
return { entries, total };
|
||||||
|
}
|
||||||
|
|||||||
@@ -206,6 +206,7 @@ public:
|
|||||||
|
|
||||||
// IUgcLookup
|
// IUgcLookup
|
||||||
std::vector<UgcEntry> SearchUgc(const UgcSearch& search, const uint32_t limit) override;
|
std::vector<UgcEntry> SearchUgc(const UgcSearch& search, const uint32_t limit) override;
|
||||||
|
std::pair<std::vector<UgcEntry>, uint64_t> ListUgc(const eUgcKind kind, const UgcListQuery& query) override;
|
||||||
std::vector<UgcEntry> GetUgcEntries(const std::vector<LWOOBJID>& ids) override;
|
std::vector<UgcEntry> GetUgcEntries(const std::vector<LWOOBJID>& ids) override;
|
||||||
std::vector<UgcPlacement> GetUgcPlacements(const std::vector<LWOOBJID>& ugcIds) override;
|
std::vector<UgcPlacement> GetUgcPlacements(const std::vector<LWOOBJID>& ugcIds) override;
|
||||||
std::vector<UgcMail> GetUgcMail(const std::vector<LWOOBJID>& subkeys, const LOT modelItemLot) override;
|
std::vector<UgcMail> GetUgcMail(const std::vector<LWOOBJID>& subkeys, const LOT modelItemLot) override;
|
||||||
@@ -411,6 +412,7 @@ public:
|
|||||||
void SetUgcIconSettings(const std::string_view target, const std::string_view params) override;
|
void SetUgcIconSettings(const std::string_view target, const std::string_view params) override;
|
||||||
void DeleteUgcIconSettings(const std::string_view target) override;
|
void DeleteUgcIconSettings(const std::string_view target) override;
|
||||||
void SetUgcModelProcessed(const LWOOBJID id, const eProcessState state, const uint32_t attempts, const std::string_view error, const bool bakeAo) override;
|
void SetUgcModelProcessed(const LWOOBJID id, const eProcessState state, const uint32_t attempts, const std::string_view error, const bool bakeAo) override;
|
||||||
|
void SetUgcModelStats(const LWOOBJID id, const uint32_t bricks, const uint32_t triangles) override;
|
||||||
std::optional<IUgc::ProcessInfo> GetUgcProcessInfo(const LWOOBJID id) override;
|
std::optional<IUgc::ProcessInfo> GetUgcProcessInfo(const LWOOBJID id) override;
|
||||||
uint64_t ResetUgcModelProcessing(const std::optional<LWOOBJID> id, const bool failedOnly) override;
|
uint64_t ResetUgcModelProcessing(const std::optional<LWOOBJID> id, const bool failedOnly) override;
|
||||||
std::vector<IUgc::ProcessInfo> GetUgcProcessList(const std::optional<eProcessState> state, const std::string_view search, const uint32_t offset, const uint32_t limit) override;
|
std::vector<IUgc::ProcessInfo> GetUgcProcessList(const std::optional<eProcessState> state, const std::string_view search, const uint32_t offset, const uint32_t limit) override;
|
||||||
|
|||||||
@@ -180,3 +180,7 @@ void SQLiteDatabase::ExpediteUgcModel(const LWOOBJID id) {
|
|||||||
void SQLiteDatabase::ExpediteUgcModels(const LWOOBJID characterId) {
|
void SQLiteDatabase::ExpediteUgcModels(const LWOOBJID characterId) {
|
||||||
ExecuteUpdate("UPDATE ugc SET process_after = 0 WHERE character_id = ? AND is_optimized = 0 AND process_after > 0;", characterId);
|
ExecuteUpdate("UPDATE ugc SET process_after = 0 WHERE character_id = ? AND is_optimized = 0 AND process_after > 0;", characterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SQLiteDatabase::SetUgcModelStats(const LWOOBJID id, const uint32_t bricks, const uint32_t triangles) {
|
||||||
|
ExecuteUpdate("UPDATE ugc SET brick_count = ?, triangle_count = ? WHERE id = ?;", bricks, triangles, id);
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ namespace {
|
|||||||
entry.state = static_cast<IUgc::eProcessState>(result.getIntField("is_optimized"));
|
entry.state = static_cast<IUgc::eProcessState>(result.getIntField("is_optimized"));
|
||||||
entry.error = result.getStringField("process_error", "");
|
entry.error = result.getStringField("process_error", "");
|
||||||
entry.detail = result.getStringField("detail", "");
|
entry.detail = result.getStringField("detail", "");
|
||||||
|
entry.attempts = static_cast<uint32_t>(result.getIntField("process_attempts"));
|
||||||
|
entry.processedAt = result.getInt64Field("processed_at");
|
||||||
|
entry.processAfter = result.getInt64Field("process_after");
|
||||||
|
entry.bakeAo = result.getIntField("bake_ao") != 0;
|
||||||
|
entry.bricks = static_cast<uint32_t>(result.getInt64Field("brick_count"));
|
||||||
|
entry.triangles = static_cast<uint32_t>(result.getInt64Field("triangle_count"));
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,3 +81,26 @@ std::vector<IUgcLookup::UgcMail> SQLiteDatabase::GetUgcMail(const std::vector<LW
|
|||||||
}
|
}
|
||||||
return mail;
|
return mail;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::pair<std::vector<IUgcLookup::UgcEntry>, uint64_t> SQLiteDatabase::ListUgc(const eUgcKind kind, const UgcListQuery& query) {
|
||||||
|
const bool modular = kind == eUgcKind::MODULAR;
|
||||||
|
const std::string pattern = "%" + query.search.text + "%";
|
||||||
|
const bool searching = UgcLookupSql::Searching(query.search);
|
||||||
|
const auto where = (searching ? UgcLookupSql::Where(query.search, modular) : std::string("WHERE 1=1 ")) + UgcLookupSql::ListFilter(query, modular);
|
||||||
|
std::vector<UgcEntry> entries;
|
||||||
|
uint64_t total = 0;
|
||||||
|
const auto page = UgcLookupSql::Select(modular) + where + UgcLookupSql::ListOrder(query, modular) + "LIMIT ? OFFSET ?;";
|
||||||
|
const auto count = "SELECT COUNT(*) AS n " + UgcLookupSql::From(modular) + where + ";";
|
||||||
|
if (searching) {
|
||||||
|
auto [_, result] = ExecuteSelect(page, pattern, pattern, pattern, pattern, pattern, pattern, query.limit, query.offset);
|
||||||
|
for (; !result.eof(); result.nextRow()) entries.push_back(ReadEntry(result, kind));
|
||||||
|
auto [__, counted] = ExecuteSelect(count, pattern, pattern, pattern, pattern, pattern, pattern);
|
||||||
|
if (!counted.eof()) total = static_cast<uint64_t>(counted.getInt64Field("n"));
|
||||||
|
} else {
|
||||||
|
auto [_, result] = ExecuteSelect(page, query.limit, query.offset);
|
||||||
|
for (; !result.eof(); result.nextRow()) entries.push_back(ReadEntry(result, kind));
|
||||||
|
auto [__, counted] = ExecuteSelect(count);
|
||||||
|
if (!counted.eof()) total = static_cast<uint64_t>(counted.getInt64Field("n"));
|
||||||
|
}
|
||||||
|
return { entries, total };
|
||||||
|
}
|
||||||
|
|||||||
@@ -270,6 +270,7 @@ class TestSQLDatabase : public GameDatabase {
|
|||||||
|
|
||||||
// IUgcLookup
|
// IUgcLookup
|
||||||
std::vector<UgcEntry> SearchUgc(const UgcSearch& search, const uint32_t limit) override { return {}; }
|
std::vector<UgcEntry> SearchUgc(const UgcSearch& search, const uint32_t limit) override { return {}; }
|
||||||
|
std::pair<std::vector<UgcEntry>, uint64_t> ListUgc(const eUgcKind kind, const UgcListQuery& query) override { return {}; }
|
||||||
std::vector<UgcEntry> GetUgcEntries(const std::vector<LWOOBJID>& ids) override { return {}; }
|
std::vector<UgcEntry> GetUgcEntries(const std::vector<LWOOBJID>& ids) override { return {}; }
|
||||||
std::vector<UgcPlacement> GetUgcPlacements(const std::vector<LWOOBJID>& ugcIds) override { return {}; }
|
std::vector<UgcPlacement> GetUgcPlacements(const std::vector<LWOOBJID>& ugcIds) override { return {}; }
|
||||||
std::vector<UgcMail> GetUgcMail(const std::vector<LWOOBJID>& subkeys, const LOT modelItemLot) override { return {}; }
|
std::vector<UgcMail> GetUgcMail(const std::vector<LWOOBJID>& subkeys, const LOT modelItemLot) override { return {}; }
|
||||||
@@ -409,6 +410,7 @@ class TestSQLDatabase : public GameDatabase {
|
|||||||
void SetUgcIconSettings(const std::string_view target, const std::string_view params) override {}
|
void SetUgcIconSettings(const std::string_view target, const std::string_view params) override {}
|
||||||
void DeleteUgcIconSettings(const std::string_view target) override {}
|
void DeleteUgcIconSettings(const std::string_view target) override {}
|
||||||
void SetUgcModelProcessed(const LWOOBJID id, const eProcessState state, const uint32_t attempts, const std::string_view error, const bool bakeAo) override {}
|
void SetUgcModelProcessed(const LWOOBJID id, const eProcessState state, const uint32_t attempts, const std::string_view error, const bool bakeAo) override {}
|
||||||
|
void SetUgcModelStats(const LWOOBJID id, const uint32_t bricks, const uint32_t triangles) override {}
|
||||||
std::optional<IUgc::ProcessInfo> GetUgcProcessInfo(const LWOOBJID id) override { return {}; }
|
std::optional<IUgc::ProcessInfo> GetUgcProcessInfo(const LWOOBJID id) override { return {}; }
|
||||||
uint64_t ResetUgcModelProcessing(const std::optional<LWOOBJID> id, const bool failedOnly) override { return 0; }
|
uint64_t ResetUgcModelProcessing(const std::optional<LWOOBJID> id, const bool failedOnly) override { return 0; }
|
||||||
std::vector<IUgc::ProcessInfo> GetUgcProcessList(const std::optional<eProcessState> state, const std::string_view search, const uint32_t offset, const uint32_t limit) override { return {}; }
|
std::vector<IUgc::ProcessInfo> GetUgcProcessList(const std::optional<eProcessState> state, const std::string_view search, const uint32_t offset, const uint32_t limit) override { return {}; }
|
||||||
|
|||||||
@@ -346,6 +346,13 @@ void UgcProcessor::Record(const Done& done) {
|
|||||||
: attempts >= m_Config.maxAttempts ? IUgc::eProcessState::FAILED : IUgc::eProcessState::PENDING;
|
: attempts >= m_Config.maxAttempts ? IUgc::eProcessState::FAILED : IUgc::eProcessState::PENDING;
|
||||||
if (done.kind == Kind::MODEL) {
|
if (done.kind == Kind::MODEL) {
|
||||||
Database::Get()->SetUgcModelProcessed(done.id, state, attempts, error, done.outcome.ok && done.outcome.aoBaked);
|
Database::Get()->SetUgcModelProcessed(done.id, state, attempts, error, done.outcome.ok && done.outcome.aoBaked);
|
||||||
|
// What it counted (stats.json), for sorting on the dashboard
|
||||||
|
const auto stats = done.outcome.ok && !done.outcome.stats.empty() ? nlohmann::json::parse(done.outcome.stats, nullptr, false) : nlohmann::json();
|
||||||
|
if (stats.is_object()) {
|
||||||
|
const auto& lods = stats.value("lods", nlohmann::json::array());
|
||||||
|
const auto& first = lods.is_array() && !lods.empty() ? lods.front() : nlohmann::json::object();
|
||||||
|
Database::Get()->SetUgcModelStats(done.id, stats.value("bricks", 0u), first.value("opaqueAfter", 0u) + first.value("transparent", 0u));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Database::Get()->SetModularBuildProcessed(done.id, state, attempts, error);
|
Database::Get()->SetModularBuildProcessed(done.id, state, attempts, error);
|
||||||
}
|
}
|
||||||
|
|||||||
13
migrations/dlu/mysql/88_ugc_model_stats.sql
Normal file
13
migrations/dlu/mysql/88_ugc_model_stats.sql
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/* UGC server: ugc.brick_count and ugc.triangle_count are what it counted when it made a model (the bricks, and the
|
||||||
|
triangles of the made mesh's most detailed level; 0 until it has), so the dashboard can sort models by size. */
|
||||||
|
SET @dlu_column = (SELECT IF(COUNT(*) = 0, 'ALTER TABLE ugc ADD COLUMN brick_count INT UNSIGNED NOT NULL DEFAULT 0', 'DO 0') FROM information_schema.columns
|
||||||
|
WHERE table_schema = DATABASE() AND table_name = 'ugc' AND column_name = 'brick_count');
|
||||||
|
PREPARE dlu_column_stmt FROM @dlu_column;
|
||||||
|
EXECUTE dlu_column_stmt;
|
||||||
|
DEALLOCATE PREPARE dlu_column_stmt;
|
||||||
|
|
||||||
|
SET @dlu_column = (SELECT IF(COUNT(*) = 0, 'ALTER TABLE ugc ADD COLUMN triangle_count INT UNSIGNED NOT NULL DEFAULT 0', 'DO 0') FROM information_schema.columns
|
||||||
|
WHERE table_schema = DATABASE() AND table_name = 'ugc' AND column_name = 'triangle_count');
|
||||||
|
PREPARE dlu_column_stmt FROM @dlu_column;
|
||||||
|
EXECUTE dlu_column_stmt;
|
||||||
|
DEALLOCATE PREPARE dlu_column_stmt;
|
||||||
3
migrations/dlu/sqlite/71_ugc_model_stats.sql
Normal file
3
migrations/dlu/sqlite/71_ugc_model_stats.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/* UGC server: the bricks and triangles it counted when it made a model. See the MySQL migration. */
|
||||||
|
ALTER TABLE ugc ADD COLUMN brick_count INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE ugc ADD COLUMN triangle_count INTEGER NOT NULL DEFAULT 0;
|
||||||
Reference in New Issue
Block a user