From e27f2085dc90d742efeb3f627512ca0b2dbe79ec Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Mon, 28 Sep 2026 11:40:31 -0500 Subject: [PATCH] fix(ugc): construct the model again a second after taking it down Taken down and constructed again in the same batch, the client dropped the construction of the object it hadn't deleted yet, and most models disappeared. The construction now follows a second later. Co-Authored-By: Claude Opus 5.5 --- dGame/dUtilities/UgcManifest.cpp | 18 ++++++++++++++---- docs/UgcServer.md | 7 ++++--- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/dGame/dUtilities/UgcManifest.cpp b/dGame/dUtilities/UgcManifest.cpp index 43a1d33e1..3b1179c37 100644 --- a/dGame/dUtilities/UgcManifest.cpp +++ b/dGame/dUtilities/UgcManifest.cpp @@ -38,6 +38,9 @@ namespace { // 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); + // How long after taking the model down it is constructed again: the client deletes objects later, and a + // construction of an object it still has is dropped (the model then disappears) + constexpr auto CONSTRUCT_AFTER_DESTRUCT = std::chrono::milliseconds(1000); struct Waiting { SystemAddress sysAddr; @@ -67,6 +70,7 @@ namespace { SystemAddress sysAddr; LWOOBJID objectId{}; std::chrono::steady_clock::time_point due; + bool destructed{}; // taken down; constructed at `due` }; std::vector g_Reconstructs; @@ -274,13 +278,19 @@ namespace { g_Reconstructs.push_back({ sysAddr, model.GetObjectID(), std::chrono::steady_clock::now() + RECONSTRUCT_DELAY }); } - // Constructs a model again for a client whose flush is done; false while it isn't due - bool ConstructAgain(const Reconstruct& pending, const std::chrono::steady_clock::time_point now) { + // 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) { if (now < pending.due) return false; if (!Game::entityManager || !PlayerManager::GetPlayer(pending.sysAddr)) return true; auto* const model = Game::entityManager->GetEntity(pending.objectId); if (!model) return true; - Game::entityManager->DestructEntity(model, pending.sysAddr); + if (!pending.destructed) { + Game::entityManager->DestructEntity(model, pending.sysAddr); + pending.destructed = true; + pending.due = now + CONSTRUCT_AFTER_DESTRUCT; + return false; + } Game::entityManager->ConstructEntity(model, pending.sysAddr); return true; } @@ -312,7 +322,7 @@ namespace { 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](const Reconstruct& pending) { return ConstructAgain(pending, now); }); + std::erase_if(g_Reconstructs, [now](Reconstruct& pending) { return ConstructAgain(pending, now); }); if (g_Waiting.empty() || now < g_NextRetry) return; g_NextRetry = now + RETRY_INTERVAL; diff --git a/docs/UgcServer.md b/docs/UgcServer.md index 454ccf87a..cde0a2055 100644 --- a/docs/UgcServer.md +++ b/docs/UgcServer.md @@ -410,9 +410,10 @@ serving), the worlds: `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 constructed again 1.5 seconds - later, after the client's load thread has done the flush (constructed at once, the flush can land after the new - object loaded its mesh, which then disappears). The object constructed again loads the NIF, + 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.