feat: UGC server that makes and serves player models' meshes and icons

A new server (dUgcServer, started by master with enable_ugc_server=1) that
takes unprocessed ugc and ugc_modular_build rows from the database and makes
what the client downloads with UGCUSE3DSERVICES: an optimized NIF (hidden
faces removed, ambient occlusion baked into vertex colors) and a 128px DDS
icon for player models, rendered by a software rasterizer from the client's
LDD brick primitives, and icons for cars and rockets assembled from their
modules per ModularBuildComponent/ModuleComponent. It serves them, with the
models' LXFML, over HTTP in the client's UGCC<dc>/3DOPTIMIZED and
IMAGE128DDS layout with .gz and .checksum files, and keeps its folder under
a size cap.

Processing state lives in ugc.is_optimized plus new processed_at,
process_attempts and process_error columns (and the same on
ugc_modular_build). ServiceType::UGC is appended. NifFile moves to dCommon
and records named node transforms for the modules' attach points.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-27 03:52:22 -05:00
parent 90ded73596
commit abf1cc1d80
52 changed files with 3869 additions and 14 deletions

View File

@@ -704,6 +704,50 @@ TEST_F(ParitySeeded, UgcModel) {
auto model = db.GetUgcModel(1152921510000500001LL);
return model ? json{ model->id, model->modelID, model->lxfmlData.str() } : json();
});
// The UGC server's processing state (processed_at is the time of the call, so only whether it's set is compared)
const auto infoJson = [](const std::optional<IUgc::ProcessInfo>& info) {
return info ? json{ info->id, info->characterId, info->characterName, static_cast<int32_t>(info->state), info->attempts,
info->processedAt > 0, info->error, info->bakeAo, info->details } : json();
};
const auto countsJson = [](const std::vector<std::pair<IUgc::eProcessState, uint64_t>>& counts) {
json out = json::array();
for (const auto& [state, count] : counts) out.push_back({ static_cast<int32_t>(state), count });
return out;
};
Both("GetUgcModelsToProcess", [](GameDatabase& db) {
json out = json::array();
for (const auto& model : db.GetUgcModelsToProcess(10)) out.push_back({ model.id, model.lxfml, model.attempts });
return out;
});
Both("SetUgcModelProcessed failed", [&](GameDatabase& db) {
db.SetUgcModelProcessed(1152921510000500001LL, IUgc::eProcessState::FAILED, 3, "no bricks", false);
return json{ infoJson(db.GetUgcProcessInfo(1152921510000500001LL)), countsJson(db.GetUgcProcessCounts()), db.GetUgcModelsToProcess(10).size() };
});
Both("GetUgcProcessList", [&](GameDatabase& db) {
json out = json::array();
for (const auto& info : db.GetUgcProcessList(IUgc::eProcessState::FAILED, 0, 10)) out.push_back(infoJson(info));
out.push_back(db.GetUgcProcessList(std::nullopt, 0, 10).size());
out.push_back(db.GetUgcProcessList(IUgc::eProcessState::DONE, 0, 10).size());
return out;
});
Both("ResetUgcModelProcessing", [&](GameDatabase& db) {
const auto changed = db.ResetUgcModelProcessing(std::nullopt, true);
db.SetUgcModelProcessed(1152921510000500001LL, IUgc::eProcessState::DONE, 1, "", true);
return json{ changed, infoJson(db.GetUgcProcessInfo(1152921510000500001LL)), infoJson(db.GetUgcProcessInfo(1)) };
});
Both("Modular build processing", [&](GameDatabase& db) {
db.InsertUgcBuild("1:4713+1:4714+1:4715", 1152921510000500002LL, CHAR_ALICE);
json out = json::array();
for (const auto& build : db.GetModularBuildsToProcess(10)) out.push_back({ build.id, build.modules, build.attempts });
db.SetModularBuildProcessed(1152921510000500002LL, IUgc::eProcessState::PENDING, 1, "no mesh");
out.push_back(infoJson(db.GetModularBuildProcessInfo(1152921510000500002LL)));
out.push_back(countsJson(db.GetModularBuildProcessCounts()));
out.push_back(db.GetModularBuildProcessList(std::nullopt, 0, 10).size());
out.push_back(db.ResetModularBuildProcessing(1152921510000500002LL, false));
out.push_back(infoJson(db.GetModularBuildProcessInfo(1152921510000500002LL)));
return out;
});
}
TEST_F(ParitySeeded, LogsAndAudit) {