mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-25 06:02:49 +00:00
The UGC page's List view (models and car/rocket assemblies) and an assembly's References panel are server-side DataTables like the rest of the dashboard: sort by clicking a column heading, in either direction, with the page length and sort remembered per user. They still read /api/ugc and /api/ugc/assembly/builds, so the prefix search and filters above the list keep working; the gallery keeps its own pager and sort. /api/ugc takes reverse=1 for a sort's other direction (the model list's SQL order and the assembly sort both flip), and the references endpoint sorts by id, owner, account or state. Tests cover the reversed orders. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
158 lines
6.4 KiB
C++
158 lines
6.4 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;
|
|
}
|
|
|
|
// reverse: the sort's other direction (the order is flipped as a whole)
|
|
inline void Sort(std::vector<Assembly>& list, eSort sort, const std::map<uint32_t, ModuleInfo>& modules, bool reverse = false) {
|
|
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();
|
|
}
|
|
});
|
|
if (reverse) std::reverse(list.begin(), list.end());
|
|
}
|
|
}
|