fix(ugc): served player models keep the client's own collision

With ugc_manifest_models the world left made models out of the LXFML it
sends when a property loads, so the client never built them and had no HKX:
the UGC server makes no physics, and the HKX request got a 404, so served
models had no collision.

Now every model's LXFML is sent and the client builds each one (NIF and
HKX). For the models whose mesh the UGC server made, once the client has
loaded and 3 seconds after, the world sends it the served NIF's checksum and
NotifyClientUGCModelReady: the client drops what it cached for the model and
asks again, downloads the served mesh (its own NIF no longer matches) and
keeps its own HKX (still matching). An HKX request is answered with the LXFML
too, followed by the same switch, at most 3 times per client and model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-28 11:02:29 -05:00
parent 87e47bcaa9
commit 2384f46a57
7 changed files with 112 additions and 35 deletions

View File

@@ -424,7 +424,7 @@ namespace {
c.Add(Port(UGC, "net_port", "Master connection port", "UDP; the next port is used too.", "2012")); c.Add(Port(UGC, "net_port", "Master connection port", "UDP; the next port is used too.", "2012"));
c.Add(Text(UGC, "client_path", "Download path", "The game client's UGCSERVERDIR.", "/ugc", true)); c.Add(Text(UGC, "client_path", "Download path", "The game client's UGCSERVERDIR.", "/ugc", true));
c.Add(Bool(SHARED, "ugc_manifest", "Answer clients without 3D services", "Worlds tell game clients with UGCUSE3DSERVICES=7:0 (the default) the checksums of the files the UGC server made, so they download icons of cars, rockets and models from UGCSERVERIP, UGCSERVERPORT and UGCSERVERDIR in their boot.cfg (default: the patch server's). A client that can't connect there is logged out, so only turn on when every player's boot.cfg points at the UGC server.", false)); c.Add(Bool(SHARED, "ugc_manifest", "Answer clients without 3D services", "Worlds tell game clients with UGCUSE3DSERVICES=7:0 (the default) the checksums of the files the UGC server made, so they download icons of cars, rockets and models from UGCSERVERIP, UGCSERVERPORT and UGCSERVERDIR in their boot.cfg (default: the patch server's). A client that can't connect there is logged out, so only turn on when every player's boot.cfg points at the UGC server.", false));
c.Add(When(Bool(SHARED, "ugc_manifest_models", "Placed models from the UGC server", "With the answer to clients without 3D services on: placed player models the UGC server has made are downloaded from it (its mesh) instead of built by each client from the LXFML the world sends. It makes no physics: a client that never built a model itself has no collision for it. Models not made yet are still sent as LXFML.", false), SHARED, "ugc_manifest", { "1" })); c.Add(When(Bool(SHARED, "ugc_manifest_models", "Placed models from the UGC server", "With the answer to clients without 3D services on: placed player models the UGC server has made are shown with its mesh. Each client still gets every model's LXFML and builds it first (for its collision; the UGC server makes no physics), then is switched to the served mesh a few seconds after it has loaded. Models not made yet stay as the client built them.", false), SHARED, "ugc_manifest", { "1" }));
c.Add(Format(Text(DASHBOARD, "ugc_internal_url", "UGC server address (internal)", "Where the dashboard itself reaches the UGC server for its status and files. Empty: http://127.0.0.1:2008.", ""), eFormat::URL)); c.Add(Format(Text(DASHBOARD, "ugc_internal_url", "UGC server address (internal)", "Where the dashboard itself reaches the UGC server for its status and files. Empty: http://127.0.0.1:2008.", ""), eFormat::URL));
c.Add(Format(Text(DASHBOARD, "ugc_public_url", "UGC server address (public)", "Only for the \"open on the UGC server\" links, e.g. https://ugc.example.com. The dashboard's own pages don't need the browser to reach it.", ""), eFormat::URL)); c.Add(Format(Text(DASHBOARD, "ugc_public_url", "UGC server address (public)", "Only for the \"open on the UGC server\" links, e.g. https://ugc.example.com. The dashboard's own pages don't need the browser to reach it.", ""), eFormat::URL));
c.AddSection("UGC processing", "When models are made and how much of the machine the workers may use.", eLayout::ROWS, Condition{ MASTER, "enable_ugc_server", { "1" } }); c.AddSection("UGC processing", "When models are made and how much of the machine the workers may use.", eLayout::ROWS, Condition{ MASTER, "enable_ugc_server", { "1" } });

View File

@@ -29,6 +29,10 @@ namespace {
constexpr auto LXFML_RESEND_AFTER = std::chrono::seconds(10); constexpr auto LXFML_RESEND_AFTER = std::chrono::seconds(10);
// LXFML checksums worked out, kept for the next clients (a model's LXFML only changes with a new save) // LXFML checksums worked out, kept for the next clients (a model's LXFML only changes with a new save)
constexpr size_t MAX_LXFML_CHECKSUMS = 4096; constexpr size_t MAX_LXFML_CHECKSUMS = 4096;
// How long after a client has loaded it is switched to a served mesh (it builds the model from the LXFML first)
constexpr auto SERVED_MESH_DELAY = std::chrono::seconds(3);
constexpr size_t MAX_SWITCHES = 4096;
constexpr int MAX_SWITCHES_PER_MODEL = 3;
struct Waiting { struct Waiting {
SystemAddress sysAddr; SystemAddress sysAddr;
@@ -42,6 +46,17 @@ namespace {
std::map<std::pair<SystemAddress, LWOOBJID>, std::chrono::steady_clock::time_point> g_LxfmlSent; std::map<std::pair<SystemAddress, LWOOBJID>, std::chrono::steady_clock::time_point> g_LxfmlSent;
std::map<LWOOBJID, IUgc::FileChecksum> g_LxfmlChecksums; std::map<LWOOBJID, IUgc::FileChecksum> g_LxfmlChecksums;
// Clients given a served model's LXFML, to be switched to its served mesh: since when the client is loaded (unset
// while it isn't)
struct Switch {
SystemAddress sysAddr;
LWOOBJID blueprintId{};
std::chrono::steady_clock::time_point since;
std::optional<std::chrono::steady_clock::time_point> loadedAt;
};
std::vector<Switch> g_Switches;
std::map<std::pair<SystemAddress, LWOOBJID>, int> g_SwitchCount;
bool ManifestOn() { bool ManifestOn() {
// Off unless set: a client whose boot.cfg doesn't point it at the UGC server downloads from its built-in // Off unless set: a client whose boot.cfg doesn't point it at the UGC server downloads from its built-in
// http://127.0.0.1:80/lwoclient and is logged out when it can't connect there (docs/UgcServer.md) // http://127.0.0.1:80/lwoclient and is logged out when it can't connect there (docs/UgcServer.md)
@@ -122,6 +137,7 @@ namespace {
response.models.push_back({ blueprintId, model->lxfmlData.str() }); response.models.push_back({ blueprintId, model->lxfmlData.str() });
response.Send(sysAddr); response.Send(sysAddr);
g_LxfmlSent[{ sysAddr, blueprintId }] = now; g_LxfmlSent[{ sysAddr, blueprintId }] = now;
UgcManifest::ScheduleServedMesh(sysAddr, blueprintId);
// Its mesh is wanted: a model still in its quiet period after a save is made now // Its mesh is wanted: a model still in its quiet period after a save is made now
Database::Get()->ExpediteUgcModel(blueprintId); Database::Get()->ExpediteUgcModel(blueprintId);
LOG_DEBUG("Sent the LXFML of %llu for the client to build", static_cast<unsigned long long>(blueprintId)); LOG_DEBUG("Sent the LXFML of %llu for the client to build", static_cast<unsigned long long>(blueprintId));
@@ -137,8 +153,8 @@ UgcManifest::eAction UgcManifest::Decide(const eUgcResourceType type, const bool
case eUgcResourceType::LXFML: case eUgcResourceType::LXFML:
return modelsOn && meshMade ? eAction::ANSWER : eAction::SEND_LXFML; return modelsOn && meshMade ? eAction::ANSWER : eAction::SEND_LXFML;
case eUgcResourceType::HKX: case eUgcResourceType::HKX:
// The UGC server makes no physics: the client uses an HKX it built before, else it asks and gets 404 // The UGC server makes no physics: the client builds it from the LXFML (and is then switched to a served mesh)
return modelsOn && meshMade ? eAction::ANSWER_UNKNOWN : eAction::SEND_LXFML; return eAction::SEND_LXFML;
default: return eAction::NONE; default: return eAction::NONE;
} }
} }
@@ -167,8 +183,25 @@ bool UgcManifest::ServesModels() {
return ManifestOn() && ModelsOn(); return ManifestOn() && ModelsOn();
} }
bool UgcManifest::ClientBuildsModel(const LWOOBJID blueprintId) { bool UgcManifest::ServesMesh(const LWOOBJID blueprintId) {
return !ServesModels() || !MeshMade(blueprintId); return ServesModels() && MeshMade(blueprintId);
}
bool UgcManifest::ScheduleServedMesh(const SystemAddress& sysAddr, const LWOOBJID blueprintId) {
if (!ServesMesh(blueprintId)) return false;
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) return true;
for (auto& pending : g_Switches) {
// Sent again (built again): the switch waits for the new build
if (pending.sysAddr == sysAddr && pending.blueprintId == blueprintId) {
pending.since = std::chrono::steady_clock::now();
pending.loadedAt.reset();
return true;
}
}
if (++g_SwitchCount[{ sysAddr, blueprintId }] > MAX_SWITCHES_PER_MODEL) return true;
if (g_Switches.size() >= MAX_SWITCHES) g_Switches.erase(g_Switches.begin());
g_Switches.push_back({ sysAddr, blueprintId, std::chrono::steady_clock::now(), std::nullopt });
return true;
} }
void UgcManifest::OnRequest(const SystemAddress& sysAddr, LWOOBJID blueprintId, eUgcResourceType resourceType) { void UgcManifest::OnRequest(const SystemAddress& sysAddr, LWOOBJID blueprintId, eUgcResourceType resourceType) {
@@ -206,10 +239,38 @@ void UgcManifest::OnRequest(const SystemAddress& sysAddr, LWOOBJID blueprintId,
g_Waiting.push_back({ sysAddr, blueprintId, resourceType, std::chrono::steady_clock::now() }); g_Waiting.push_back({ sysAddr, blueprintId, resourceType, std::chrono::steady_clock::now() });
} }
namespace {
// Switches one client to a model's served mesh; false while the client isn't ready for it yet
bool SwitchToServedMesh(Switch& pending, const std::chrono::steady_clock::time_point now) {
auto* const player = PlayerManager::GetPlayer(pending.sysAddr);
if (!player) return true; // gone
if (!player->GetPlayerReadyForUpdates()) return false;
if (!pending.loadedAt) pending.loadedAt = now;
if (now - *pending.loadedAt < SERVED_MESH_DELAY) return false;
const auto checksum = Database::Get()->GetUgcFileChecksum(pending.blueprintId, "model.nif");
if (!checksum || checksum->md5.size() != 32 || !Game::entityManager) return true;
// The served checksum first: NotifyClientUGCModelReady has the client ask again, and its cache answers
Send(pending.sysAddr, pending.blueprintId, eUgcResourceType::NIF, checksum);
size_t shown = 0;
for (auto* const model : Game::entityManager->GetEntitiesByLOT(BrickByBrick::MODEL_OBJECT_LOT)) {
if (!model || model->GetVar<LWOOBJID>(u"blueprintid") != pending.blueprintId) continue;
GameMessages::NotifyClientUGCModelReady ready;
ready.target = model->GetObjectID();
ready.blueprintID = pending.blueprintId;
ready.Send(pending.sysAddr);
shown++;
}
LOG_DEBUG("Switched a client to the served mesh of %llu (%zu placed)", static_cast<unsigned long long>(pending.blueprintId), shown);
return true;
}
}
void UgcManifest::Update() { void UgcManifest::Update() {
if (g_Waiting.empty()) return;
const auto now = std::chrono::steady_clock::now(); const auto now = std::chrono::steady_clock::now();
if (now < g_NextRetry) return; std::erase_if(g_Switches, [now](Switch& pending) { return now - pending.since > MAX_WAIT || SwitchToServedMesh(pending, now); });
if (g_Waiting.empty() || now < g_NextRetry) return;
g_NextRetry = now + RETRY_INTERVAL; g_NextRetry = now + RETRY_INTERVAL;
std::erase_if(g_Waiting, [now](const Waiting& waiting) { std::erase_if(g_Waiting, [now](const Waiting& waiting) {
return now - waiting.since > MAX_WAIT || TryAnswerIcon(waiting.sysAddr, waiting.blueprintId, waiting.resourceType); return now - waiting.since > MAX_WAIT || TryAnswerIcon(waiting.sysAddr, waiting.blueprintId, waiting.resourceType);
@@ -219,6 +280,8 @@ void UgcManifest::Update() {
void UgcManifest::OnDisconnect(const SystemAddress& sysAddr) { void UgcManifest::OnDisconnect(const SystemAddress& sysAddr) {
std::erase_if(g_Waiting, [&sysAddr](const Waiting& waiting) { return waiting.sysAddr == sysAddr; }); std::erase_if(g_Waiting, [&sysAddr](const Waiting& waiting) { return waiting.sysAddr == sysAddr; });
std::erase_if(g_LxfmlSent, [&sysAddr](const auto& sent) { return sent.first.first == sysAddr; }); std::erase_if(g_LxfmlSent, [&sysAddr](const auto& sent) { return sent.first.first == sysAddr; });
std::erase_if(g_Switches, [&sysAddr](const Switch& pending) { return pending.sysAddr == sysAddr; });
std::erase_if(g_SwitchCount, [&sysAddr](const auto& count) { return count.first.first == sysAddr; });
} }
void UgcManifest::OnModelsMade(const std::vector<LWOOBJID>& blueprintIds) { void UgcManifest::OnModelsMade(const std::vector<LWOOBJID>& blueprintIds) {

View File

@@ -20,10 +20,13 @@
* * Player models (NIF, HKX, LXFML): a placed model's client always asks for all three (its BlueprintComponent loads the * * Player models (NIF, HKX, LXFML): a placed model's client always asks for all three (its BlueprintComponent loads the
* blueprint's NIF and HKX, its ModelBehaviorComponent the LXFML) and waits for the answers without a timeout. With * blueprint's NIF and HKX, its ModelBehaviorComponent the LXFML) and waits for the answers without a timeout. With
* `ugc_manifest_models` 1 and the model's mesh made, the NIF is answered with the UGC server's checksum (the client * `ugc_manifest_models` 1 and the model's mesh made, the NIF is answered with the UGC server's checksum (the client
* downloads the served mesh), the LXFML with the stored LXFML's and the HKX as not known (valid 0: the client uses * downloads the served mesh) and the LXFML with the stored LXFML's. Otherwise, and always for the HKX (the UGC server
* one it has, else asks the UGC server, which answers 404). Otherwise the world sends that client the model's LXFML * makes no physics), the world sends that client the model's LXFML (BlueprintSaveResponse): the client builds the NIF
* (BlueprintSaveResponse), the client builds the NIF and HKX itself and caches their checksums, which answers its * and HKX itself and caches their checksums, which answers its own requests, so a model is never left waiting.
* own requests: a model is never left waiting. * * Served meshes with the client's own physics: a client given the LXFML of a model whose mesh is served builds its
* own NIF and HKX, then (once it has loaded and a moment after) gets the served NIF's checksum and
* NotifyClientUGCModelReady for that model. It drops what it cached for the model and asks again: the NIF's checksum
* is now the served one, so it downloads the served mesh; the HKX's is still its own, so it keeps its collision.
* *
* Main thread only: each lookup is one indexed query and the answers are sent from here. See docs/UgcServer.md. * Main thread only: each lookup is one indexed query and the answers are sent from here. See docs/UgcServer.md.
*/ */
@@ -47,13 +50,20 @@ namespace UgcManifest {
// ugc_manifest=1 and ugc_manifest_models=1: placed models' meshes come from the UGC server when made // ugc_manifest=1 and ugc_manifest_models=1: placed models' meshes come from the UGC server when made
bool ServesModels(); bool ServesModels();
// Whether the client should get this model's LXFML when a property loads (it builds the model itself): not when // ServesModels() and the UGC server made this model's mesh: a client that built the model from its LXFML (for its
// ServesModels() and the UGC server made its mesh // physics) is then switched to the served mesh (ScheduleServedMesh)
bool ClientBuildsModel(LWOOBJID blueprintId); bool ServesMesh(LWOOBJID blueprintId);
// The client was sent this model's LXFML: when the mesh is served, once the client has loaded (and a moment after,
// for it to build the model) it gets the served NIF's checksum and NotifyClientUGCModelReady. At most a few times
// per client and model (a client that keeps asking for the HKX isn't switched back and forth for ever). Returns
// whether the mesh is served.
bool ScheduleServedMesh(const SystemAddress& sysAddr, LWOOBJID blueprintId);
void OnRequest(const SystemAddress& sysAddr, LWOOBJID blueprintId, eUgcResourceType resourceType); void OnRequest(const SystemAddress& sysAddr, LWOOBJID blueprintId, eUgcResourceType resourceType);
// Answers the waiting requests whose files have been made since, and forgets the ones waited on too long // Answers the waiting requests whose files have been made since, forgets the ones waited on too long, and switches
// clients to served meshes that are due
void Update(); void Update();
// A client left: its waiting requests are dropped // A client left: its waiting requests are dropped

View File

@@ -1033,8 +1033,8 @@ void LoadPlayer(const SystemAddress& sysAddr) {
} }
// The models' LXFML, for the client to build each model's NIF and HKX itself. With ugc_manifest_models // The models' LXFML, for the client to build each model's NIF and HKX itself. With ugc_manifest_models
// the models whose mesh the UGC server made are left out: the client asks for their files and downloads // the models whose mesh the UGC server made are then switched to the served mesh, keeping the client's
// the mesh (UgcManifest, docs/UgcServer.md). // own physics (UgcManifest, docs/UgcServer.md).
auto bbbModels = Database::Get()->GetUgcModels(propertyId); auto bbbModels = Database::Get()->GetUgcModels(propertyId);
if (bbbModels.empty()) { if (bbbModels.empty()) {
@@ -1047,10 +1047,7 @@ void LoadPlayer(const SystemAddress& sysAddr) {
response.reasonCode = eBlueprintSaveResponseType::EverythingWorked; response.reasonCode = eBlueprintSaveResponseType::EverythingWorked;
size_t served = 0; size_t served = 0;
for (auto& bbbModel : bbbModels) { for (auto& bbbModel : bbbModels) {
if (!UgcManifest::ClientBuildsModel(bbbModel.id)) { if (UgcManifest::ScheduleServedMesh(sysAddr, bbbModel.id)) served++;
served++;
continue;
}
LOG("Getting lxfml ugcID: %llu", bbbModel.id); LOG("Getting lxfml ugcID: %llu", bbbModel.id);
bbbModel.lxfmlData.seekg(0, std::ios::end); bbbModel.lxfmlData.seekg(0, std::ios::end);
@@ -1062,7 +1059,7 @@ void LoadPlayer(const SystemAddress& sysAddr) {
model.blueprintId = bbbModel.id; model.blueprintId = bbbModel.id;
model.data = bbbModel.lxfmlData.str().substr(0, lxfmlSize); model.data = bbbModel.lxfmlData.str().substr(0, lxfmlSize);
} }
if (served > 0) LOG("%zu of the property's %zu models come from the UGC server", served, bbbModels.size()); if (served > 0) LOG("%zu of the property's %zu models get their mesh from the UGC server once built", served, bbbModels.size());
if (!response.models.empty()) response.Send(sysAddr); if (!response.models.empty()) response.Send(sysAddr);
} }

View File

@@ -401,14 +401,19 @@ What the 1.10.64 client does with a placed player model (LOT 14, spawned with `b
With `ugc_manifest=1` and `ugc_manifest_models=1` (`sharedconfig.ini`, default 0; the dashboard shows it under UGC With `ugc_manifest=1` and `ugc_manifest_models=1` (`sharedconfig.ini`, default 0; the dashboard shows it under UGC
serving), the worlds: serving), the worlds:
* **Property load**: send the LXFML (one `BlueprintSaveResponse`) only for the models whose mesh the UGC server hasn't * **Property load**: send the LXFML of every model (one `BlueprintSaveResponse`): the client builds each model's NIF
made (no `model.nif` in `ugc_file_checksums`: pending, failed, empty or the UGC server not running). Made ones are and HKX (its collision; the UGC server makes no physics). For the models whose mesh the UGC server made (a
left out; the client asks for their files. `model.nif` in `ugc_file_checksums`), once the client has loaded (`PlayerLoaded`) and 3 seconds after, the world
sends it the served NIF's checksum and `NotifyClientUGCModelReady` for each placed copy. The client drops what it
cached for the model and asks again: its cache now has the served NIF's checksum, which its own file doesn't match,
so it downloads the served mesh, and still has its own HKX's, so it keeps the collision it built. The model shows
as the client built it for those few seconds.
* **NIF** of a made model: the UGC server's checksum; the client downloads `<id>.nif.sd0` from the UGC server. * **NIF** of a made model: the UGC server's checksum; the client downloads `<id>.nif.sd0` from the UGC server.
* **LXFML** of a made model: the MD5 and size of the stored LXFML inflated (what the UGC server serves as * **LXFML** of a made model: the MD5 and size of the stored LXFML inflated (what the UGC server serves as
`<id>.lxfml.sd0`), worked out once per model and kept. `<id>.lxfml.sd0`), worked out once per model and kept.
* **HKX** of a made model: valid 0. The UGC server makes no physics: a client that built the model itself before keeps * **HKX** of any model: the model's LXFML (the UGC server makes no physics), and for a made model the same switch to
using its HKX; others ask the UGC server, get 404 (not a logout) and have no collision for that model. the served mesh afterwards. At most 3 switches per client and model, so a client that keeps asking isn't switched
back and forth for ever.
* **Any model file of a model that isn't made** (e.g. a model someone else just placed, or `ugc_manifest_models=0`): * **Any model file of a model that isn't made** (e.g. a model someone else just placed, or `ugc_manifest_models=0`):
the model's LXFML to that client (once per 10 seconds for the three requests), which builds it itself, so a model is the model's LXFML to that client (once per 10 seconds for the three requests), which builds it itself, so a model is
never left waiting. A blueprint that isn't a player model gets valid 0. never left waiting. A blueprint that isn't a player model gets valid 0.

View File

@@ -100,8 +100,7 @@ ugc_debounce_seconds=120
# out, so only set 1 when every player's boot.cfg points at the UGC server. See docs/UgcServer.md. # out, so only set 1 when every player's boot.cfg points at the UGC server. See docs/UgcServer.md.
ugc_manifest=0 ugc_manifest=0
# 0 or 1, with ugc_manifest=1: placed player models the UGC server has made are downloaded from it (their mesh) instead # 0 or 1, with ugc_manifest=1: placed player models the UGC server has made are shown with its mesh. Each client still
# of being built by each client from the LXFML the world sends; models not made yet are still sent as LXFML. The UGC # gets every model's LXFML and builds it first (for its collision: the UGC server makes no physics), then is switched to
# server makes no physics (.hkx): a client that never built a model itself has no collision for it. See # the served mesh a few seconds after it has loaded. See docs/UgcServer.md.
# docs/UgcServer.md.
ugc_manifest_models=0 ugc_manifest_models=0

View File

@@ -23,7 +23,8 @@ namespace {
// What each request gets. A placed model's client asks for its NIF, HKX and LXFML and waits for every answer without a // What each request gets. A placed model's client asks for its NIF, HKX and LXFML and waits for every answer without a
// timeout (LWOResMgr2Interface::RequestBlueprintManifestThenLoad, 0x0105a910), so model files are always answered: // timeout (LWOResMgr2Interface::RequestBlueprintManifestThenLoad, 0x0105a910), so model files are always answered:
// from the UGC server when it made the mesh and models are served, else with the LXFML for the client to build. // from the UGC server when it made the mesh and models are served, else with the LXFML for the client to build. The HKX
// always gets the LXFML: the UGC server makes no physics, the client builds its own (then gets the served mesh).
TEST(UgcManifestTests, DecideCoversEveryType) { TEST(UgcManifestTests, DecideCoversEveryType) {
using T = eUgcResourceType; using T = eUgcResourceType;
for (const auto type : { T::LXFML, T::NIF, T::HKX, T::DDS }) { for (const auto type : { T::LXFML, T::NIF, T::HKX, T::DDS }) {
@@ -34,7 +35,7 @@ TEST(UgcManifestTests, DecideCoversEveryType) {
EXPECT_EQ(UgcManifest::Decide(T::NIF, true, true, true), eAction::ANSWER); EXPECT_EQ(UgcManifest::Decide(T::NIF, true, true, true), eAction::ANSWER);
EXPECT_EQ(UgcManifest::Decide(T::LXFML, true, true, true), eAction::ANSWER); EXPECT_EQ(UgcManifest::Decide(T::LXFML, true, true, true), eAction::ANSWER);
EXPECT_EQ(UgcManifest::Decide(T::HKX, true, true, true), eAction::ANSWER_UNKNOWN); EXPECT_EQ(UgcManifest::Decide(T::HKX, true, true, true), eAction::SEND_LXFML);
for (const auto type : { T::LXFML, T::NIF, T::HKX }) { for (const auto type : { T::LXFML, T::NIF, T::HKX }) {
EXPECT_EQ(UgcManifest::Decide(type, true, true, false), eAction::SEND_LXFML); // not made yet EXPECT_EQ(UgcManifest::Decide(type, true, true, false), eAction::SEND_LXFML); // not made yet
@@ -70,14 +71,16 @@ TEST_F(UgcManifestRequestTests, NothingWhenOff) {
const auto sent = Capture([] { UgcManifest::OnRequest(Client(), 1234, eUgcResourceType::NIF); }); const auto sent = Capture([] { UgcManifest::OnRequest(Client(), 1234, eUgcResourceType::NIF); });
EXPECT_TRUE(sent.empty()); EXPECT_TRUE(sent.empty());
EXPECT_FALSE(UgcManifest::ServesModels()); EXPECT_FALSE(UgcManifest::ServesModels());
EXPECT_TRUE(UgcManifest::ClientBuildsModel(1234)); EXPECT_FALSE(UgcManifest::ServesMesh(1234));
EXPECT_FALSE(UgcManifest::ScheduleServedMesh(Client(), 1234));
} }
// A model file of a blueprint that isn't a player model: answered as not known (37 bytes, valid 0), never left waiting // A model file of a blueprint that isn't a player model: answered as not known (37 bytes, valid 0), never left waiting
TEST_F(UgcManifestRequestTests, UnknownModelFilesAreAnsweredNotKnown) { TEST_F(UgcManifestRequestTests, UnknownModelFilesAreAnsweredNotKnown) {
Settings("1", "1"); Settings("1", "1");
EXPECT_TRUE(UgcManifest::ServesModels()); EXPECT_TRUE(UgcManifest::ServesModels());
EXPECT_TRUE(UgcManifest::ClientBuildsModel(1234)); // its mesh isn't made EXPECT_FALSE(UgcManifest::ServesMesh(1234)); // its mesh isn't made
EXPECT_FALSE(UgcManifest::ScheduleServedMesh(Client(), 1234));
for (const auto type : { eUgcResourceType::NIF, eUgcResourceType::HKX, eUgcResourceType::LXFML }) { for (const auto type : { eUgcResourceType::NIF, eUgcResourceType::HKX, eUgcResourceType::LXFML }) {
const auto sent = Capture([type] { UgcManifest::OnRequest(Client(), 0x0102030405060708, type); }); const auto sent = Capture([type] { UgcManifest::OnRequest(Client(), 0x0102030405060708, type); });
ASSERT_EQ(sent.size(), 1); ASSERT_EQ(sent.size(), 1);