mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-11-03 02:17:14 +00:00
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>
156 lines
6.2 KiB
C++
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();
|
|
}
|
|
});
|
|
}
|
|
}
|