diff --git a/dGame/dUtilities/BrickByBrick.cpp b/dGame/dUtilities/BrickByBrick.cpp index 01be999f2..821ee670d 100644 --- a/dGame/dUtilities/BrickByBrick.cpp +++ b/dGame/dUtilities/BrickByBrick.cpp @@ -26,6 +26,7 @@ #include "PropertyMessages.h" #include "Sd0.h" #include "UgcKeys.h" +#include "UgcManifest.h" #include "User.h" #include @@ -276,6 +277,8 @@ void BrickByBrick::Save(Entity& player, const LWOOBJID localId, const std::strin responseModel.data = model.sd0; } response.Send(player.GetSystemAddress()); + // The client builds the models from it: the switch to their served meshes waits for that (UgcManifest) + for (const auto& model : models) UgcManifest::OnLxfmlSent(player.GetSystemAddress(), model.blueprintId); for (auto* item : ItemsIn(*inventory, eInventoryType::MODELS_IN_BBB)) { item->SetCount(0, false, false, false, eLootSourceType::INVENTORY); diff --git a/dGame/dUtilities/UgcManifest.cpp b/dGame/dUtilities/UgcManifest.cpp index a1fc28f85..d8f75b415 100644 --- a/dGame/dUtilities/UgcManifest.cpp +++ b/dGame/dUtilities/UgcManifest.cpp @@ -1,5 +1,6 @@ #include "UgcManifest.h" +#include #include #include #include @@ -51,7 +52,8 @@ namespace { std::vector g_Waiting; std::chrono::steady_clock::time_point g_NextRetry{}; - std::map, std::chrono::steady_clock::time_point> g_LxfmlSent; + // LXFMLs sent to clients (building those models) and the switches to served meshes waiting for them + UgcManifest::ServedMeshSwitches g_Switches; std::map g_LxfmlChecksums; // Placed models to construct again for a client once its flush is done @@ -71,6 +73,8 @@ namespace { }; std::optional g_PropertyReload; + void RunDueSwitches(std::chrono::steady_clock::time_point now); + bool ManifestOn() { // 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) @@ -140,8 +144,7 @@ namespace { // no such model bool SendLxfml(const SystemAddress& sysAddr, LWOOBJID blueprintId) { const auto now = std::chrono::steady_clock::now(); - std::erase_if(g_LxfmlSent, [now](const auto& sent) { return now - sent.second > LXFML_RESEND_AFTER; }); - if (g_LxfmlSent.contains({ sysAddr, blueprintId })) return true; + if (g_Switches.SentWithin(sysAddr, blueprintId, LXFML_RESEND_AFTER, now)) return true; auto model = Database::Get()->GetUgcModel(blueprintId); if (!model) return false; @@ -150,7 +153,7 @@ namespace { response.reasonCode = eBlueprintSaveResponseType::EverythingWorked; response.models.push_back({ blueprintId, model->lxfmlData.str() }); response.Send(sysAddr); - g_LxfmlSent[{ sysAddr, blueprintId }] = now; + g_Switches.LxfmlSent(sysAddr, blueprintId, now); // Its mesh is wanted: a model still in its quiet period after a save is made now Database::Get()->ExpediteUgcModel(blueprintId); LOG_DEBUG("Sent the LXFML of %llu for the client to build", static_cast(blueprintId)); @@ -158,6 +161,42 @@ namespace { } } +void UgcManifest::ServedMeshSwitches::LxfmlSent(const SystemAddress& sysAddr, const LWOOBJID blueprintId, const Clock::time_point now) { + std::erase_if(m_LxfmlSent, [now](const auto& sent) { return now - sent.second >= BUILD_SETTLE; }); + const Key key{ sysAddr, blueprintId }; + m_LxfmlSent[key] = now; + // A switch now would be undone by the build this starts + if (const auto due = m_Due.find(key); due != m_Due.end()) due->second = std::max(due->second, now + BUILD_SETTLE); +} + +bool UgcManifest::ServedMeshSwitches::SentWithin(const SystemAddress& sysAddr, const LWOOBJID blueprintId, const Clock::duration within, const Clock::time_point now) const { + const auto sent = m_LxfmlSent.find({ sysAddr, blueprintId }); + return sent != m_LxfmlSent.end() && now - sent->second < within; +} + +void UgcManifest::ServedMeshSwitches::Schedule(const SystemAddress& sysAddr, const LWOOBJID blueprintId, const Clock::time_point now) { + const Key key{ sysAddr, blueprintId }; + auto due = now; + if (const auto sent = m_LxfmlSent.find(key); sent != m_LxfmlSent.end()) due = std::max(due, sent->second + BUILD_SETTLE); + if (const auto pending = m_Due.find(key); pending != m_Due.end()) due = std::max(due, pending->second); + m_Due[key] = due; +} + +std::vector UgcManifest::ServedMeshSwitches::TakeDue(const Clock::time_point now) { + std::vector due; + std::erase_if(m_Due, [&](const auto& pending) { + if (pending.second > now) return false; + due.push_back({ pending.first.first, pending.first.second }); + return true; + }); + return due; +} + +void UgcManifest::ServedMeshSwitches::Forget(const SystemAddress& sysAddr) { + std::erase_if(m_LxfmlSent, [&sysAddr](const auto& sent) { return sent.first.first == sysAddr; }); + std::erase_if(m_Due, [&sysAddr](const auto& pending) { return pending.first.first == sysAddr; }); +} + UgcManifest::eAction UgcManifest::Decide(const eUgcResourceType type, const bool manifestOn, const bool modelsOn, const bool meshMade) { if (!manifestOn) return eAction::NONE; switch (type) { @@ -235,28 +274,26 @@ void UgcManifest::OnRequest(const SystemAddress& sysAddr, LWOOBJID blueprintId, g_Waiting.push_back({ sysAddr, blueprintId, resourceType, std::chrono::steady_clock::now() }); } -namespace { - // Shows a placed model to one client again with the served mesh. NotifyClientUGCModelReady only flushes the - // client's cached NIF, HKX and LXFML and loads them again as preloads (LWOBlueprintComponent:: - // OnNotifyClientUGCModelReady, 0x00ca6430): an object already drawn keeps its mesh. So the object is also taken - // down and constructed again for that client, once the flush is done (RECONSTRUCT_DELAY; constructed at once, the - // flush can land after the new object loaded its mesh, which then disappears). Its new render component loads the - // NIF, whose cached checksum is now the served one (sent first); the HKX's is still the one the client built, so - // it keeps its collision. - void ShowServedMesh(Entity& model, Entity& player, const LWOOBJID blueprintId) { - const auto sysAddr = player.GetSystemAddress(); - if (model.GetIsGhostingCandidate()) { - auto* const ghost = player.GetComponent(); - if (!ghost || !ghost->IsObserved(model.GetObjectID())) return; // not shown to this client (yet) - } +void UgcManifest::SwitchClient(const SystemAddress& sysAddr, const LWOOBJID blueprintId, const IUgc::FileChecksum& checksum, const std::vector& modelIds) { + // The new checksum first: the client keeps its cached one (its own build's) otherwise, and loads the file it has + Send(sysAddr, blueprintId, eUgcResourceType::NIF, checksum); + const auto due = std::chrono::steady_clock::now() + RECONSTRUCT_DELAY; + for (const auto objectId : modelIds) { + // Flushes the client's cached NIF, HKX and LXFML of the blueprint and preloads the NIF and HKX again + // (LWOBlueprintComponent::OnNotifyClientUGCModelReady, 0x00ca6430); an object already drawn keeps its mesh GameMessages::NotifyClientUGCModelReady ready; - ready.target = model.GetObjectID(); + ready.target = objectId; ready.blueprintID = blueprintId; ready.Send(sysAddr); - std::erase_if(g_Reconstructs, [&](const Reconstruct& r) { return r.sysAddr == sysAddr && r.objectId == model.GetObjectID(); }); - g_Reconstructs.push_back({ sysAddr, model.GetObjectID(), std::chrono::steady_clock::now() + RECONSTRUCT_DELAY }); + // So the model is also taken down and constructed again once the flush is done (constructed at once, the flush + // can land after the new object loaded its mesh, which then disappears). Its HKX's cached checksum is still the + // client's own build's, so it keeps its collision. + std::erase_if(g_Reconstructs, [&](const Reconstruct& r) { return r.sysAddr == sysAddr && r.objectId == objectId; }); + g_Reconstructs.push_back({ sysAddr, objectId, due }); } +} +namespace { // Takes a model down for a client whose flush is done, then constructs it again a moment later (not in the same // batch: the client would drop the construction of an object it hasn't deleted yet); false until it's done bool ConstructAgain(Reconstruct& pending, const std::chrono::steady_clock::time_point now) { @@ -336,6 +373,7 @@ void UgcManifest::Update() { g_PropertyReload.reset(); } } + RunDueSwitches(now); std::erase_if(g_Reconstructs, [now](Reconstruct& pending) { return ConstructAgain(pending, now); }); if (g_Waiting.empty() || now < g_NextRetry) return; @@ -347,36 +385,63 @@ void UgcManifest::Update() { void UgcManifest::OnDisconnect(const SystemAddress& 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; }); + g_Switches.Forget(sysAddr); std::erase_if(g_Reconstructs, [&sysAddr](const Reconstruct& pending) { return pending.sysAddr == sysAddr; }); } +namespace { + // The switches that are due: each client is sent the served checksum and its models shown to it are switched + void RunDueSwitches(const std::chrono::steady_clock::time_point now) { + if (g_Switches.Pending() == 0) return; + const auto due = g_Switches.TakeDue(now); + if (due.empty() || !Game::entityManager) return; + const auto models = Game::entityManager->GetEntitiesByLOT(BrickByBrick::MODEL_OBJECT_LOT); + for (const auto& pending : due) { + // Models of a /reprocessproperty: the players are reloaded once they're all made + if (g_PropertyReload && g_PropertyReload->blueprintIds.contains(pending.blueprintId)) continue; + auto* const player = PlayerManager::GetPlayer(pending.sysAddr); + if (!player) continue; + auto* const ghost = player->GetComponent(); + std::vector shown; + for (auto* const model : models) { + if (!model || model->GetVar(u"blueprintid") != pending.blueprintId) continue; + // Not shown to this client (yet): it loads the served mesh when it's constructed + if (model->GetIsGhostingCandidate() && (!ghost || !ghost->IsObserved(model->GetObjectID()))) continue; + shown.push_back(model->GetObjectID()); + } + if (shown.empty()) continue; + const auto checksum = Database::Get()->GetUgcFileChecksum(pending.blueprintId, "model.nif"); + if (!checksum || checksum->md5.size() != 32) continue; + UgcManifest::SwitchClient(pending.sysAddr, pending.blueprintId, *checksum, shown); + LOG("Switching %s to the served mesh of model %llu (%zu placed)", pending.sysAddr.ToString(), static_cast(pending.blueprintId), shown.size()); + } + } +} + +void UgcManifest::OnLxfmlSent(const SystemAddress& sysAddr, const LWOOBJID blueprintId) { + if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) return; + g_Switches.LxfmlSent(sysAddr, blueprintId, std::chrono::steady_clock::now()); +} + void UgcManifest::OnModelsMade(const std::vector& blueprintIds) { for (const auto id : blueprintIds) g_LxfmlChecksums.erase(id); // made again after its LXFML changed, maybe if (!ServesModels() || !Game::entityManager) return; - // Models of a /reprocessproperty: the players are reloaded once they're all made const auto& players = PlayerManager::GetAllPlayers(); if (players.empty()) return; const auto models = Game::entityManager->GetEntitiesByLOT(BrickByBrick::MODEL_OBJECT_LOT); + const auto now = std::chrono::steady_clock::now(); for (const auto id : blueprintIds) { + // Models of a /reprocessproperty: the players are reloaded once they're all made if (g_PropertyReload && g_PropertyReload->blueprintIds.contains(id)) continue; - std::vector shown; - for (auto* const model : models) { - if (model && model->GetVar(u"blueprintid") == id) shown.push_back(model); - } - if (shown.empty()) continue; - const auto checksum = Database::Get()->GetUgcFileChecksum(id, "model.nif"); - if (!checksum || checksum->md5.size() != 32) continue; - - // The new checksum first: the client keeps its cached one otherwise, and would load the file it has + const auto placed = std::count_if(models.begin(), models.end(), [id](Entity* model) { return model && model->GetVar(u"blueprintid") == id; }); + if (placed == 0) continue; + // A client sent the model's LXFML lately may still be building it, and its build would undo the switch: it's + // switched once it can't be building it any more, the others now for (auto* const player : players) { - if (player) Send(player->GetSystemAddress(), id, eUgcResourceType::NIF, checksum); + if (player) g_Switches.Schedule(player->GetSystemAddress(), id, now); } - for (auto* const player : players) { - if (!player) continue; - for (auto* const model : shown) ShowServedMesh(*model, *player, id); - } - LOG("The UGC server made model %llu again: told %zu client(s) about %zu placed model(s)", static_cast(id), players.size(), shown.size()); + LOG("The UGC server made model %llu (%zu placed here): switching %zu client(s) to it", static_cast(id), static_cast(placed), players.size()); } + RunDueSwitches(now); } diff --git a/dGame/dUtilities/UgcManifest.h b/dGame/dUtilities/UgcManifest.h index cca6d3ddb..2dd5f2fa6 100644 --- a/dGame/dUtilities/UgcManifest.h +++ b/dGame/dUtilities/UgcManifest.h @@ -1,8 +1,11 @@ #ifndef UGCMANIFEST_H #define UGCMANIFEST_H +#include +#include #include #include +#include #include #include "dCommonVars.h" @@ -25,12 +28,47 @@ * and HKX itself and caches their checksums, which answers its own requests, so a model is never left waiting. * * Served meshes with the client's own physics: the client shows the served NIF it downloaded, and asks for the HKX, * which is answered with the model's LXFML: it builds the model from it and loads its own HKX. The mesh it has drawn - * stays the served one. (Built from the LXFML first, the client kept showing its own build even after - * NotifyClientUGCModelReady and the model constructed again.) + * stays the served one. + * * A client building a model from its LXFML writes its own .nif over the served one and caches that file's MD5 as the + * NIF's manifest info when it's done (LWOBBBInterface::GenerateModelFromLxfml, 0x00b6c220; + * MainThread_ProcessModelResponse, 0x00b5a1e0). So a client is switched to a served mesh only once it can't still be + * building that model (ServedMeshSwitches). * * Main thread only: each lookup is one indexed query and the answers are sent from here. See docs/UgcServer.md. */ namespace UgcManifest { + using Clock = std::chrono::steady_clock; + + // How long after a client was sent a model's LXFML it may still be building the model from it + constexpr auto BUILD_SETTLE = std::chrono::seconds(30); + + // Which clients get switched to which served meshes, and when: at once, or BUILD_SETTLE after the model's LXFML was + // last sent to that client (a switch made while it builds is undone by its build). Main thread only. + class ServedMeshSwitches { + public: + struct Switch { + SystemAddress sysAddr; + LWOOBJID blueprintId{}; + }; + + // The model's LXFML was sent to the client: a switch waiting for it waits BUILD_SETTLE from now + void LxfmlSent(const SystemAddress& sysAddr, LWOOBJID blueprintId, Clock::time_point now); + // The model's LXFML was sent to the client less than `within` ago + bool SentWithin(const SystemAddress& sysAddr, LWOOBJID blueprintId, Clock::duration within, Clock::time_point now) const; + // The client is to be switched to the model's served mesh once it can't be building it any more + void Schedule(const SystemAddress& sysAddr, LWOOBJID blueprintId, Clock::time_point now); + // The switches that are due, removed from the pending ones + std::vector TakeDue(Clock::time_point now); + // The client left + void Forget(const SystemAddress& sysAddr); + size_t Pending() const { return m_Due.size(); } + + private: + using Key = std::pair; + std::map m_LxfmlSent; + std::map m_Due; + }; + // What a request gets enum class eAction { NONE, // not answered (ugc_manifest off, or not a UGC type) @@ -68,9 +106,18 @@ namespace UgcManifest { // A client left (or went back to character select): its waiting requests and pending switches are dropped void OnDisconnect(const SystemAddress& sysAddr); - // The UGC server made these models' meshes (again, with a new checksum): each one placed in this world is sent the - // new checksum and NotifyClientUGCModelReady, so clients load the served mesh + // The UGC server made these models' meshes (again, with a new checksum): every client this world shows one of them + // to is switched to the served mesh (SwitchClient), at once or once it can't be building that model any more void OnModelsMade(const std::vector& blueprintIds); + + // A model's LXFML was sent to a client another way (a brick by brick save, a property load): it builds the model + void OnLxfmlSent(const SystemAddress& sysAddr, LWOOBJID blueprintId); + + // Switches one client to a model's served mesh: the served NIF's checksum (the client's cached one is its own build's), + // then NotifyClientUGCModelReady to each of `modelIds` (flushes the cached NIF, HKX and LXFML and preloads the NIF, + // which is downloaded, and the HKX, still the client's own), then each model taken down and constructed again for + // that client (Update) so its render loads the served NIF + void SwitchClient(const SystemAddress& sysAddr, LWOOBJID blueprintId, const IUgc::FileChecksum& checksum, const std::vector& modelIds); } #endif //!UGCMANIFEST_H diff --git a/dWorldServer/WorldServer.cpp b/dWorldServer/WorldServer.cpp index adff4f93d..b793de1b1 100644 --- a/dWorldServer/WorldServer.cpp +++ b/dWorldServer/WorldServer.cpp @@ -1118,6 +1118,8 @@ void LoadPlayer(const SystemAddress& sysAddr) { } if (served > 0) LOG("%zu of the property's %zu models come from the UGC server", served, bbbModels.size()); if (!response.models.empty()) response.Send(sysAddr); + // The client builds these models: a switch to their served meshes waits for that (UgcManifest) + for (const auto& model : response.models) UgcManifest::OnLxfmlSent(sysAddr, model.blueprintId); } noBBB: diff --git a/docs/UgcServer.md b/docs/UgcServer.md index e2f038a02..45c07d95e 100644 --- a/docs/UgcServer.md +++ b/docs/UgcServer.md @@ -537,7 +537,8 @@ What the 1.10.64 client does with a placed player model (LOT 14, spawned with `b 0x00c75220). There's no choice between LXFML and NIF on the render side, and the world needn't send `renderUserGen`. The ModelBehaviorComponent loads the blueprint's LXFML (type 0) as well (`RequestBlueprintData`, 0x00c24640). * Every such request first needs the blueprint's manifest info (`UGCManifest_Base::GetOrRequestManifestInfo`, - 0x0101e0e0): a cached entry (kept 300 seconds; stored in `res/BrickModels/UserMade/manifest.cache`) is used at once, + 0x0101e0e0): a cached entry is used at once (an entry with valid 1 never expires; one with valid 0 expires after + 300 seconds, `UGCManifest_Client::IsEntryExpired`, 0x010186d0), else the request waits and the client sends `REQUEST_UGC_MANIFEST_INFO`. **There is no timeout**: a manifest request the world never answers leaves that file, and the model, waiting for good (`LWOResMgr2Interface::RequestBlueprintManifestThenLoad`, 0x0105a910). @@ -548,7 +549,14 @@ What the 1.10.64 client does with a placed player model (LOT 14, spawned with `b * The LXFML the world sends when a property loads (`BlueprintSaveResponse` with local id 0) makes the client build the NIF and HKX itself and cache the manifest info of the LXFML, NIF and HKX with its own files' MD5s (`LWOBBBInterface::MainThread_ProcessModelResponse`, 0x00b5a1e0), which answers its own requests, so nothing is - downloaded. That's how DLU always showed models. + downloaded. That's how DLU always showed models. The build runs on the client's BBB thread and writes + `BrickModels/UserMade//.lxfml`, `.nif` and `.hkx`, the paths a download uses + (`LWOBBBInterface::GenerateModelFromLxfml`, 0x00b6c220). So every LXFML sent to a client replaces a served `.nif` it + downloaded, on disk and in its manifest cache, once that build is done; a mesh already drawn stays. +* A single-asset flush (ResMgr2 0x2b06, what `NotifyClientUGCModelReady` sends) erases the path from every resource + cache at once (`ResourceCache::FlushCachedAsset`, 0x010372a0); objects already drawn keep their mesh. +* A download is tried 3 times; after that the client uses the file it has, if any (its own build), else the model has + no file (`LWOResMgr2Interface::GetResource`, 0x0105ca50). * `NotifyClientUGCModelReady` (game message 909, the blueprint id only) to a model: its BlueprintComponent flushes the cached NIF, HKX and LXFML of that blueprint and requests the NIF and HKX again (`LWOBlueprintComponent::OnNotifyClientUGCModelReady`, 0x00ca6430). It doesn't clear the manifest cache, so the new @@ -562,8 +570,9 @@ serving), the worlds: * **Property load**: send the LXFML (one `BlueprintSaveResponse`) only for the models whose mesh the UGC server hasn't made (no `model.nif` in `ugc_file_checksums`). Made ones are left out: the client asks for their files, downloads the served mesh and draws it. Tried and dropped: sending every model's LXFML (the client builds its own NIF and HKX) - and then switching to the served mesh with the served NIF's checksum, `NotifyClientUGCModelReady` and the model - constructed again: the client kept drawing its own build. + and switching to the served mesh 3 s later with the served NIF's checksum, `NotifyClientUGCModelReady` and the model + constructed again: the client kept drawing its own build (most likely because its builds of those LXFMLs finished + after the switch and cached their own NIFs again; see "Waiting for the client's own build"). * **NIF** of a made model: the UGC server's checksum; the client downloads `.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.sd0`), worked out once per model and kept. @@ -574,9 +583,17 @@ serving), the worlds: never left waiting. A blueprint that isn't a player model gets valid 0. * **Made again**: when the UGC server writes a model's mesh with a different checksum than before (made for the first time, or remade after a change), it sends `UGC_MODELS_MADE` (master message 37, the blueprint ids) to the master, - which passes it to every world. A world with that model placed sends every player the new NIF checksum, then - `NotifyClientUGCModelReady` and the model constructed again to each player it's shown to, so the served mesh - replaces what the client showed. + which passes it to every world. A world with that model placed switches each player the model is shown to + (`UgcManifest::SwitchClient`): the served NIF's checksum, `NotifyClientUGCModelReady` to each placed copy (the + client flushes its cached NIF, HKX and LXFML and downloads the served NIF), and 1.5 s later each copy taken down for + that player and constructed again 1 s after that; the new object draws the served NIF and loads the client's own HKX + (still cached), so it keeps collision. +* **Waiting for the client's own build**: a player sent the model's LXFML less than 30 s before (`BUILD_SETTLE`: the + property load, a request for a model not made yet, the owner's brick by brick save) may still be building it, and + that build would put its own NIF and checksum back after the switch. Such a player is switched 30 s after the last + LXFML sent to them (`UgcManifest::ServedMeshSwitches`); another LXFML sent meanwhile starts the 30 s again. The + others are switched at once. This is the common case: another player's client asks for a model the owner just + placed, gets its LXFML, and the UGC server is told to make that model now (`ExpediteUgcModel`). A model made again unchanged (after eviction) isn't sent. Nothing polls: one message per batch of made models. * **`/reprocessproperty`** (GM 8): every model placed on the property the player is on goes back to the UGC server's diff --git a/tests/dGameTests/UgcManifestTests.cpp b/tests/dGameTests/UgcManifestTests.cpp index c644bbf3e..48d03b832 100644 --- a/tests/dGameTests/UgcManifestTests.cpp +++ b/tests/dGameTests/UgcManifestTests.cpp @@ -120,3 +120,104 @@ TEST_F(UgcManifestRequestTests, LxfmlChecksumIsOfTheInflatedLxfml) { EXPECT_FALSE(UgcManifest::LxfmlChecksum("").has_value()); EXPECT_FALSE(UgcManifest::LxfmlChecksum("not an sd0 stream").has_value()); } + +namespace { + SystemAddress Address(uint16_t port) { + SystemAddress address; + address.binaryAddress = 0x0100007f; + address.port = port; + return address; + } +} + +// A client never sent the model's LXFML (it built the model long ago, or never): switched at once +TEST(UgcServedMeshSwitchesTests, SwitchedAtOnceWithoutARecentLxfml) { + UgcManifest::ServedMeshSwitches switches; + const auto t0 = UgcManifest::Clock::time_point{} + std::chrono::hours(1); + switches.Schedule(Address(1), 1234, t0); + const auto due = switches.TakeDue(t0); + ASSERT_EQ(due.size(), 1); + EXPECT_EQ(due[0].sysAddr, Address(1)); + EXPECT_EQ(due[0].blueprintId, 1234); + EXPECT_EQ(switches.Pending(), 0); + EXPECT_TRUE(switches.TakeDue(t0 + std::chrono::minutes(5)).empty()); // once +} + +// A client sent the model's LXFML lately is building the model, and its build writes its own NIF and caches its checksum +// over the served one (LWOBBBInterface::GenerateModelFromLxfml, 0x00b6c220; MainThread_ProcessModelResponse, +// 0x00b5a1e0): it's switched BUILD_SETTLE after that send, not before +TEST(UgcServedMeshSwitchesTests, WaitsForTheClientsOwnBuild) { + UgcManifest::ServedMeshSwitches switches; + const auto t0 = UgcManifest::Clock::time_point{} + std::chrono::hours(1); + switches.LxfmlSent(Address(1), 1234, t0); + switches.Schedule(Address(1), 1234, t0 + std::chrono::seconds(5)); + switches.Schedule(Address(2), 1234, t0 + std::chrono::seconds(5)); // another client: never sent it + + auto due = switches.TakeDue(t0 + std::chrono::seconds(5)); + ASSERT_EQ(due.size(), 1); + EXPECT_EQ(due[0].sysAddr, Address(2)); + EXPECT_TRUE(switches.TakeDue(t0 + UgcManifest::BUILD_SETTLE - std::chrono::seconds(1)).empty()); + due = switches.TakeDue(t0 + UgcManifest::BUILD_SETTLE); + ASSERT_EQ(due.size(), 1); + EXPECT_EQ(due[0].sysAddr, Address(1)); + EXPECT_EQ(due[0].blueprintId, 1234); +} + +// The LXFML sent again while a switch waits (the client asked again): it starts another build, so the switch waits again +TEST(UgcServedMeshSwitchesTests, AnotherLxfmlPushesTheSwitchBack) { + UgcManifest::ServedMeshSwitches switches; + const auto t0 = UgcManifest::Clock::time_point{} + std::chrono::hours(1); + switches.LxfmlSent(Address(1), 1234, t0); + switches.Schedule(Address(1), 1234, t0); + switches.LxfmlSent(Address(1), 1234, t0 + std::chrono::seconds(20)); + EXPECT_TRUE(switches.TakeDue(t0 + UgcManifest::BUILD_SETTLE).empty()); + EXPECT_EQ(switches.TakeDue(t0 + std::chrono::seconds(20) + UgcManifest::BUILD_SETTLE).size(), 1); + + // Another model's LXFML doesn't hold this one + switches.LxfmlSent(Address(1), 5678, t0 + std::chrono::minutes(2)); + switches.Schedule(Address(1), 1234, t0 + std::chrono::minutes(2)); + EXPECT_EQ(switches.TakeDue(t0 + std::chrono::minutes(2)).size(), 1); +} + +// SentWithin is the 10 second guard against sending the LXFML for each of a model's three requests +TEST(UgcServedMeshSwitchesTests, SentWithin) { + UgcManifest::ServedMeshSwitches switches; + const auto t0 = UgcManifest::Clock::time_point{} + std::chrono::hours(1); + EXPECT_FALSE(switches.SentWithin(Address(1), 1234, std::chrono::seconds(10), t0)); + switches.LxfmlSent(Address(1), 1234, t0); + EXPECT_TRUE(switches.SentWithin(Address(1), 1234, std::chrono::seconds(10), t0 + std::chrono::seconds(9))); + EXPECT_FALSE(switches.SentWithin(Address(1), 1234, std::chrono::seconds(10), t0 + std::chrono::seconds(10))); + EXPECT_FALSE(switches.SentWithin(Address(2), 1234, std::chrono::seconds(10), t0)); +} + +// A client that leaves loses its pending switches and sends +TEST(UgcServedMeshSwitchesTests, ForgetDropsTheClient) { + UgcManifest::ServedMeshSwitches switches; + const auto t0 = UgcManifest::Clock::time_point{} + std::chrono::hours(1); + switches.LxfmlSent(Address(1), 1234, t0); + switches.Schedule(Address(1), 1234, t0); + switches.Schedule(Address(2), 1234, t0 + std::chrono::seconds(1)); + switches.Forget(Address(1)); + EXPECT_FALSE(switches.SentWithin(Address(1), 1234, UgcManifest::BUILD_SETTLE, t0)); + const auto due = switches.TakeDue(t0 + std::chrono::hours(1)); + ASSERT_EQ(due.size(), 1); + EXPECT_EQ(due[0].sysAddr, Address(2)); +} + +// The switch of one client: the served NIF's checksum first (the client's cached one is its own build's), then +// NotifyClientUGCModelReady to each placed model, all to that client only +TEST_F(UgcManifestRequestTests, SwitchSendsTheChecksumThenModelReady) { + Settings("1", "1"); + const IUgc::FileChecksum checksum{ "00112233445566778899aabbccddeeff", 0x01020304 }; + const auto sent = Capture([&] { UgcManifest::SwitchClient(Client(), 0x0102030405060708, checksum, { 0x1122334455667788, 0x1122334455667799 }); }); + ASSERT_EQ(sent.size(), 3); + for (const auto& packet : sent) { + EXPECT_EQ(packet.sysAddr, Client()); + EXPECT_FALSE(packet.broadcast); + } + // UGC_MANIFEST_RESPONSE: blueprint, type 1 (NIF), valid, size, MD5 + EXPECT_PACKET_EQ(FromHex("53 05 00 3c 00 00 00 00 08 07 06 05 04 03 02 01 01 01 04 03 02 01 " + "00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff"), FromCapture(sent[0])); + EXPECT_PACKET_EQ(FromHex("53 05 00 0c 00 00 00 00 88 77 66 55 44 33 22 11 8d 03 08 07 06 05 04 03 02 01"), FromCapture(sent[1])); + EXPECT_PACKET_EQ(FromHex("53 05 00 0c 00 00 00 00 99 77 66 55 44 33 22 11 8d 03 08 07 06 05 04 03 02 01"), FromCapture(sent[2])); +}