#pragma once #include #include #include #include #include #include #include #include #include #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 lots; int32_t buildType{ -1 }; // of the first module that is one std::vector builds; // newest first std::set 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 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(std::tolower(c)); }); return out; } // The builds grouped by their combination; builds without modules are left out inline std::vector Group(const std::vector& builds, const std::map& modules) { std::map 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 byId; for (const auto& build : builds) byId[build.id] = &build; std::vector 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 buildType; std::optional state; std::optional> 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 moduleLot; // or: this module is in it }; inline bool Matches(const Assembly& assembly, const Filter& filter, const std::map& 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& 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& list, eSort sort, const std::map& 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()); } }