feat(ugc): processing totals on the UGC page; no shared icon preset for player models

- IUgc::GetUgcProcessTotals sums what the UGC server made, for models and
  for car and rocket builds: count, time spent (and how many are timed),
  CPU time, average and slowest, average and most memory (estimate),
  bricks, triangles and triangles saved (models whose count before hidden
  face removal is known). /api/ugc returns them as totals and the UGC page
  shows a card for each kind. Parity tested.
- Player models no longer have a shared icon preset: every one is a
  different size and shape, so its icon is fitted to it from the settings.
  The UGC server ignores a kind:model preset, the dashboard refuses to save
  one, and the icon editor hides the type buttons for models. One model's
  own icon values still work. Car and rocket build types keep theirs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-28 19:27:37 -05:00
parent bdb4c39db3
commit 4679edd2b2
13 changed files with 121 additions and 8 deletions

View File

@@ -123,6 +123,24 @@ public:
// How many models are in each state
virtual std::vector<std::pair<eProcessState, uint64_t>> GetUgcProcessCounts() = 0;
// Sums over the made items (is_optimized 1) of the model table, or of the car and rocket builds (`modular`; they
// have no bricks or triangles). Items made before a stat was recorded have 0 for it and are left out of its averages
// and of the triangles saved.
struct ProcessTotals {
uint64_t made{};
uint64_t timed{}; // made ones with a recorded time
uint64_t milliseconds{}; // wall time, summed
uint64_t cpuMilliseconds{};
uint64_t maxMilliseconds{};
uint64_t memoryKbAverage{}; // estimate
uint64_t memoryKbMax{};
uint64_t bricks{};
uint64_t triangles{};
uint64_t trianglesBefore{}; // of the models whose count before hidden-face removal is known
uint64_t trianglesAfter{}; // the same models' after
};
virtual ProcessTotals GetUgcProcessTotals(const bool modular) = 0;
// ---- Checksums of the files the UGC server made (ugc_file_checksums), for the client's manifest requests ----
// What the files are stored under: a player model's ugc id, or a combination of car or rocket modules

View File

@@ -438,6 +438,7 @@ public:
std::optional<IUgc::ProcessInfo> GetUgcProcessInfo(const LWOOBJID id) override;
uint64_t ResetUgcModelProcessing(const std::optional<LWOOBJID> id, const bool failedOnly) override;
uint64_t ResetPropertyUgcModelProcessing(const LWOOBJID propertyId) override;
IUgc::ProcessTotals GetUgcProcessTotals(const bool modular) 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<std::pair<IUgc::eProcessState, uint64_t>> GetUgcProcessCounts() override;
std::vector<IUgcModularBuild::PendingBuild> GetModularBuildsToProcess(const uint32_t limit) override;

View File

@@ -1,6 +1,7 @@
#include "MySQLDatabase.h"
#include <chrono>
#include <cmath>
namespace {
IUgc::ProcessInfo ReadUgcProcessInfo(PreparedStmtResultSet& result, bool modular) {
@@ -146,6 +147,24 @@ uint64_t MySQLDatabase::ResetUgcModelProcessing(const std::optional<LWOOBJID> id
return ExecuteUpdate("UPDATE ugc SET is_optimized = 0, process_attempts = 0, process_error = '', process_after = 0;");
}
IUgc::ProcessTotals MySQLDatabase::GetUgcProcessTotals(const bool modular) {
const std::string sql = modular
? "SELECT COUNT(*) AS made, SUM(CASE WHEN process_ms > 0 THEN 1 ELSE 0 END) AS timed, COALESCE(SUM(process_ms), 0) AS ms, COALESCE(SUM(process_cpu_ms), 0) AS cpu, "
"COALESCE(MAX(process_ms), 0) AS maxms, COALESCE(AVG(NULLIF(process_memory_kb, 0)), 0) AS memavg, COALESCE(MAX(process_memory_kb), 0) AS memmax, "
"0 AS bricks, 0 AS tris, 0 AS before_tris, 0 AS after_tris FROM ugc_modular_build WHERE is_optimized = 1;"
: "SELECT COUNT(*) AS made, SUM(CASE WHEN process_ms > 0 THEN 1 ELSE 0 END) AS timed, COALESCE(SUM(process_ms), 0) AS ms, COALESCE(SUM(process_cpu_ms), 0) AS cpu, "
"COALESCE(MAX(process_ms), 0) AS maxms, COALESCE(AVG(NULLIF(process_memory_kb, 0)), 0) AS memavg, COALESCE(MAX(process_memory_kb), 0) AS memmax, "
"COALESCE(SUM(brick_count), 0) AS bricks, COALESCE(SUM(triangle_count), 0) AS tris, "
"COALESCE(SUM(CASE WHEN triangle_count_before > 0 THEN triangle_count_before ELSE 0 END), 0) AS before_tris, "
"COALESCE(SUM(CASE WHEN triangle_count_before > 0 THEN triangle_count ELSE 0 END), 0) AS after_tris FROM ugc WHERE is_optimized = 1;";
auto result = ExecuteSelect(sql);
IUgc::ProcessTotals totals;
if (!result->next()) return totals;
const auto field = [&](const char* name) { return static_cast<uint64_t>(std::llround(result->getDouble(name))); };
totals = { field("made"), field("timed"), field("ms"), field("cpu"), field("maxms"), field("memavg"), field("memmax"), field("bricks"), field("tris"), field("before_tris"), field("after_tris") };
return totals;
}
uint64_t MySQLDatabase::ResetPropertyUgcModelProcessing(const LWOOBJID propertyId) {
return ExecuteUpdate("UPDATE ugc SET is_optimized = 0, process_attempts = 0, process_error = '', process_after = 0, priority = 1 "
"WHERE id IN (SELECT ugc_id FROM properties_contents WHERE property_id = ? AND ugc_id IS NOT NULL);", propertyId);

View File

@@ -422,6 +422,7 @@ public:
std::optional<IUgc::ProcessInfo> GetUgcProcessInfo(const LWOOBJID id) override;
uint64_t ResetUgcModelProcessing(const std::optional<LWOOBJID> id, const bool failedOnly) override;
uint64_t ResetPropertyUgcModelProcessing(const LWOOBJID propertyId) override;
IUgc::ProcessTotals GetUgcProcessTotals(const bool modular) 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<std::pair<IUgc::eProcessState, uint64_t>> GetUgcProcessCounts() override;
std::vector<IUgcModularBuild::PendingBuild> GetModularBuildsToProcess(const uint32_t limit) override;

View File

@@ -1,6 +1,7 @@
#include "SQLiteDatabase.h"
#include <chrono>
#include <cmath>
namespace {
IUgc::ProcessInfo ReadUgcProcessInfo(CppSQLite3Query& result, bool modular) {
@@ -147,6 +148,24 @@ uint64_t SQLiteDatabase::ResetUgcModelProcessing(const std::optional<LWOOBJID> i
return ExecuteUpdate("UPDATE ugc SET is_optimized = 0, process_attempts = 0, process_error = '', process_after = 0;");
}
IUgc::ProcessTotals SQLiteDatabase::GetUgcProcessTotals(const bool modular) {
const std::string sql = modular
? "SELECT COUNT(*) AS made, SUM(CASE WHEN process_ms > 0 THEN 1 ELSE 0 END) AS timed, COALESCE(SUM(process_ms), 0) AS ms, COALESCE(SUM(process_cpu_ms), 0) AS cpu, "
"COALESCE(MAX(process_ms), 0) AS maxms, COALESCE(AVG(NULLIF(process_memory_kb, 0)), 0) AS memavg, COALESCE(MAX(process_memory_kb), 0) AS memmax, "
"0 AS bricks, 0 AS tris, 0 AS before_tris, 0 AS after_tris FROM ugc_modular_build WHERE is_optimized = 1;"
: "SELECT COUNT(*) AS made, SUM(CASE WHEN process_ms > 0 THEN 1 ELSE 0 END) AS timed, COALESCE(SUM(process_ms), 0) AS ms, COALESCE(SUM(process_cpu_ms), 0) AS cpu, "
"COALESCE(MAX(process_ms), 0) AS maxms, COALESCE(AVG(NULLIF(process_memory_kb, 0)), 0) AS memavg, COALESCE(MAX(process_memory_kb), 0) AS memmax, "
"COALESCE(SUM(brick_count), 0) AS bricks, COALESCE(SUM(triangle_count), 0) AS tris, "
"COALESCE(SUM(CASE WHEN triangle_count_before > 0 THEN triangle_count_before ELSE 0 END), 0) AS before_tris, "
"COALESCE(SUM(CASE WHEN triangle_count_before > 0 THEN triangle_count ELSE 0 END), 0) AS after_tris FROM ugc WHERE is_optimized = 1;";
auto [_, result] = ExecuteSelect(sql);
IUgc::ProcessTotals totals;
if (result.eof()) return totals;
const auto field = [&](const char* name) { return static_cast<uint64_t>(std::llround(result.getFloatField(name, 0.0))); };
totals = { field("made"), field("timed"), field("ms"), field("cpu"), field("maxms"), field("memavg"), field("memmax"), field("bricks"), field("tris"), field("before_tris"), field("after_tris") };
return totals;
}
uint64_t SQLiteDatabase::ResetPropertyUgcModelProcessing(const LWOOBJID propertyId) {
return ExecuteUpdate("UPDATE ugc SET is_optimized = 0, process_attempts = 0, process_error = '', process_after = 0, priority = 1 "
"WHERE id IN (SELECT ugc_id FROM properties_contents WHERE property_id = ? AND ugc_id IS NOT NULL);", propertyId);

View File

@@ -420,6 +420,7 @@ class TestSQLDatabase : public GameDatabase {
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 ResetPropertyUgcModelProcessing(const LWOOBJID propertyId) override { return 0; }
IUgc::ProcessTotals GetUgcProcessTotals(const bool modular) 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 {}; }
std::vector<std::pair<IUgc::eProcessState, uint64_t>> GetUgcProcessCounts() override { return {}; }
std::vector<IUgcModularBuild::PendingBuild> GetModularBuildsToProcess(const uint32_t limit) override { return {}; }