Files
DarkflameServer/dDashboardServer/routes/UgcAssemblies.h
Aaron Kimbrell 6498582b50 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>
2026-09-27 15:55:01 -05:00

156 lines
6.2 KiB
C++

#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();
}
});
}
}