diff --git a/dGame/dUtilities/UgcManifest.cpp b/dGame/dUtilities/UgcManifest.cpp index 3b1179c37..39a571f61 100644 --- a/dGame/dUtilities/UgcManifest.cpp +++ b/dGame/dUtilities/UgcManifest.cpp @@ -31,10 +31,6 @@ namespace { 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) 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; // How long after NotifyClientUGCModelReady the model is constructed again: the client flushes the model's files on // its load thread, and a model constructed before that is done loses the mesh it just loaded constexpr auto RECONSTRUCT_DELAY = std::chrono::milliseconds(1500); @@ -54,17 +50,6 @@ namespace { std::map, std::chrono::steady_clock::time_point> g_LxfmlSent; std::map 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 loadedAt; - }; - std::vector g_Switches; - std::map, int> g_SwitchCount; - // Placed models to construct again for a client once its flush is done struct Reconstruct { SystemAddress sysAddr; @@ -154,7 +139,6 @@ namespace { response.models.push_back({ blueprintId, model->lxfmlData.str() }); response.Send(sysAddr); 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 Database::Get()->ExpediteUgcModel(blueprintId); LOG_DEBUG("Sent the LXFML of %llu for the client to build", static_cast(blueprintId)); @@ -204,23 +188,6 @@ bool UgcManifest::ServesMesh(const LWOOBJID 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) { if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) return; const auto manifestOn = ManifestOn(); @@ -295,33 +262,10 @@ namespace { return true; } - // 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(u"blueprintid") != pending.blueprintId) continue; - ShowServedMesh(*model, *player, pending.blueprintId); - shown++; - } - LOG("Showed %s the served mesh of model %llu (%zu placed)", player->GetCharacter() ? player->GetCharacter()->GetName().c_str() : "a player", - static_cast(pending.blueprintId), shown); - return true; - } } void UgcManifest::Update() { const auto now = std::chrono::steady_clock::now(); - std::erase_if(g_Switches, [now](Switch& pending) { return now - pending.since > MAX_WAIT || SwitchToServedMesh(pending, now); }); std::erase_if(g_Reconstructs, [now](Reconstruct& pending) { return ConstructAgain(pending, now); }); if (g_Waiting.empty() || now < g_NextRetry) return; @@ -334,8 +278,6 @@ 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; }); - 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; }); std::erase_if(g_Reconstructs, [&sysAddr](const Reconstruct& pending) { return pending.sysAddr == sysAddr; }); } diff --git a/dGame/dUtilities/UgcManifest.h b/dGame/dUtilities/UgcManifest.h index 68fc721cc..b69968b51 100644 --- a/dGame/dUtilities/UgcManifest.h +++ b/dGame/dUtilities/UgcManifest.h @@ -23,10 +23,10 @@ * downloads the served mesh) and the LXFML with the stored LXFML's. Otherwise, and always for the HKX (the UGC server * makes no physics), the world sends that client the model's LXFML (BlueprintSaveResponse): the client builds the NIF * 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: 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. + * * 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.) * * Main thread only: each lookup is one indexed query and the answers are sent from here. See docs/UgcServer.md. */ @@ -50,16 +50,10 @@ namespace UgcManifest { // ugc_manifest=1 and ugc_manifest_models=1: placed models' meshes come from the UGC server when made bool ServesModels(); - // ServesModels() and the UGC server made this model's mesh: a client that built the model from its LXFML (for its - // physics) is then switched to the served mesh (ScheduleServedMesh) + // ServesModels() and the UGC server made this model's mesh: the model's LXFML isn't sent when a property loads, the + // client downloads the served mesh, and builds the model's physics from the LXFML sent when it asks for the HKX 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); // Answers the waiting requests whose files have been made since, forgets the ones waited on too long, and switches diff --git a/dWorldServer/WorldServer.cpp b/dWorldServer/WorldServer.cpp index 62d180c6e..47ce9a847 100644 --- a/dWorldServer/WorldServer.cpp +++ b/dWorldServer/WorldServer.cpp @@ -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 whose mesh the UGC server made are then switched to the served mesh, keeping the client's - // own physics (UgcManifest, docs/UgcServer.md). + // the models whose mesh the UGC server made are left out: the client downloads their mesh, and gets the + // LXFML (for its physics) when it asks for the HKX (UgcManifest, docs/UgcServer.md). auto bbbModels = Database::Get()->GetUgcModels(propertyId); if (bbbModels.empty()) { @@ -1047,7 +1047,11 @@ void LoadPlayer(const SystemAddress& sysAddr) { response.reasonCode = eBlueprintSaveResponseType::EverythingWorked; size_t served = 0; for (auto& bbbModel : bbbModels) { - if (UgcManifest::ScheduleServedMesh(sysAddr, bbbModel.id)) served++; + // Its served mesh is downloaded; its LXFML is sent when the client asks for the HKX (UgcManifest) + if (UgcManifest::ServesMesh(bbbModel.id)) { + served++; + continue; + } LOG("Getting lxfml ugcID: %llu", bbbModel.id); bbbModel.lxfmlData.seekg(0, std::ios::end); @@ -1059,7 +1063,7 @@ void LoadPlayer(const SystemAddress& sysAddr) { model.blueprintId = bbbModel.id; model.data = bbbModel.lxfmlData.str().substr(0, lxfmlSize); } - if (served > 0) LOG("%zu of the property's %zu models get their mesh from the UGC server once built", served, bbbModels.size()); + 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); } diff --git a/docs/UgcServer.md b/docs/UgcServer.md index cde0a2055..57abc0c6d 100644 --- a/docs/UgcServer.md +++ b/docs/UgcServer.md @@ -405,24 +405,16 @@ 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 serving), the worlds: -* **Property load**: send the LXFML of every model (one `BlueprintSaveResponse`): the client builds each model's NIF - and HKX (its collision; the UGC server makes no physics). For the models whose mesh the UGC server made (a - `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, `NotifyClientUGCModelReady`, and takes the model down and constructs it again for - that client. `NotifyClientUGCModelReady` alone only flushes the client's cached NIF, HKX and LXFML and loads them - again as preloads (0x00ca6430): an object already drawn keeps its mesh. The object is taken down 1.5 seconds - later, after the client's load thread has done the flush, and constructed again 1 second after that (the client - deletes objects later and drops the construction of an object it still has; in the same batch, most models - disappeared). The object constructed again loads the NIF, - whose cached checksum is now the served one, which the client's own file doesn't match, so it downloads the served - mesh; the HKX's is still its own, so it keeps the collision it built. The model shows as the client built it for - those few seconds. +* **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. * **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. -* **HKX** of any model: the model's LXFML (the UGC server makes no physics), and for a made model the same switch to - 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. +* **HKX** of any model: the model's LXFML (the UGC server makes no physics). The client builds the model from it and + loads its own HKX, so served models have collision; the mesh already drawn stays the served one. * **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 never left waiting. A blueprint that isn't a player model gets valid 0. diff --git a/tests/dGameTests/UgcManifestTests.cpp b/tests/dGameTests/UgcManifestTests.cpp index 83b567a3c..c644bbf3e 100644 --- a/tests/dGameTests/UgcManifestTests.cpp +++ b/tests/dGameTests/UgcManifestTests.cpp @@ -72,7 +72,6 @@ TEST_F(UgcManifestRequestTests, NothingWhenOff) { EXPECT_TRUE(sent.empty()); EXPECT_FALSE(UgcManifest::ServesModels()); 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 @@ -80,7 +79,6 @@ TEST_F(UgcManifestRequestTests, UnknownModelFilesAreAnsweredNotKnown) { Settings("1", "1"); EXPECT_TRUE(UgcManifest::ServesModels()); 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 }) { const auto sent = Capture([type] { UgcManifest::OnRequest(Client(), 0x0102030405060708, type); }); ASSERT_EQ(sent.size(), 1);