mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-18 02:32:50 +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:
@@ -92,6 +92,9 @@ public:
|
||||
// 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;
|
||||
|
||||
// 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;
|
||||
|
||||
// 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{};
|
||||
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
|
||||
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
|
||||
@@ -47,6 +53,27 @@ public:
|
||||
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
|
||||
virtual std::vector<UgcEntry> SearchUgc(const UgcSearch& search, const uint32_t limit) = 0;
|
||||
|
||||
|
||||
@@ -19,15 +19,45 @@ namespace UgcLookupSql {
|
||||
return list;
|
||||
}
|
||||
|
||||
// The columns every entry query selects, for models (ugc AS u) or modular builds (ugc_modular_build AS b)
|
||||
inline std::string Select(bool modular) {
|
||||
// The tables every entry query reads, for models (ugc AS u) or modular builds (ugc_modular_build AS b)
|
||||
inline std::string From(bool 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, "
|
||||
"b.is_optimized, b.process_error, b.ldf_config AS detail 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 "
|
||||
"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, "
|
||||
"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, "
|
||||
"u.is_optimized, u.process_error, u.filename AS detail 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 ";
|
||||
"u.is_optimized, u.process_error, u.filename AS detail, u.process_attempts, u.processed_at, u.process_after, u.bake_ao, "
|
||||
"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
|
||||
|
||||
@@ -222,6 +222,7 @@ public:
|
||||
|
||||
// IUgcLookup
|
||||
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<UgcPlacement> GetUgcPlacements(const std::vector<LWOOBJID>& ugcIds) 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 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 SetUgcModelStats(const LWOOBJID id, const uint32_t bricks, const uint32_t triangles) override;
|
||||
std::optional<IUgc::ProcessInfo> GetUgcProcessInfo(const LWOOBJID id) 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;
|
||||
|
||||
@@ -177,3 +177,7 @@ void MySQLDatabase::ExpediteUgcModel(const LWOOBJID id) {
|
||||
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);
|
||||
}
|
||||
|
||||
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.error = std::string(result->getString("process_error").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;
|
||||
}
|
||||
}
|
||||
@@ -75,3 +81,26 @@ std::vector<IUgcLookup::UgcMail> MySQLDatabase::GetUgcMail(const std::vector<LWO
|
||||
}
|
||||
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
|
||||
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<UgcPlacement> GetUgcPlacements(const std::vector<LWOOBJID>& ugcIds) 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 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 SetUgcModelStats(const LWOOBJID id, const uint32_t bricks, const uint32_t triangles) override;
|
||||
std::optional<IUgc::ProcessInfo> GetUgcProcessInfo(const LWOOBJID id) 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;
|
||||
|
||||
@@ -180,3 +180,7 @@ void SQLiteDatabase::ExpediteUgcModel(const LWOOBJID id) {
|
||||
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);
|
||||
}
|
||||
|
||||
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.error = result.getStringField("process_error", "");
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -75,3 +81,26 @@ std::vector<IUgcLookup::UgcMail> SQLiteDatabase::GetUgcMail(const std::vector<LW
|
||||
}
|
||||
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
|
||||
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<UgcPlacement> GetUgcPlacements(const std::vector<LWOOBJID>& ugcIds) 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 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 SetUgcModelStats(const LWOOBJID id, const uint32_t bricks, const uint32_t triangles) override {}
|
||||
std::optional<IUgc::ProcessInfo> GetUgcProcessInfo(const LWOOBJID id) override { return {}; }
|
||||
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 {}; }
|
||||
|
||||
Reference in New Issue
Block a user