From 266f8b81c5245ded9d18fc8e6ffd1ba08b460d30 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 00:05:14 -0500 Subject: [PATCH] fix(ugc): send the served mesh checksums when a player loads a property A client asks for a served model's HKX and is answered with the model's LXFML, so it builds its own collision. That build also writes the client's own .nif over the served one it downloaded and caches that file's MD5 as the NIF's manifest info (client 1.10.64: LWOBBBInterface::GenerateModelFromLxfml 0x00b6c220, MainThread_Process- ModelResponse 0x00b5a1e0; valid entries never expire, 0x010186d0). On the next load of the property the client used its cached checksum and file without asking, and drew its own build instead of the served mesh. Now, before the property's objects are constructed for a player, the world sends them the served NIF checksum of every made model placed there (UgcManifest::OnPropertyLoading). A client whose cached checksum is its own build's downloads the served mesh again; one that has it already downloads nothing. Nothing is sent unless ugc_manifest and ugc_manifest_models are 1. Check in game (ugc_manifest=1, ugc_manifest_models=1): visit a property with made models, leave, come back in the same session (and after a client restart): the models show the served mesh every time, and still have collision. Co-Authored-By: Claude Opus 5.5 --- dGame/dUtilities/UgcManifest.cpp | 16 +++++++++++++ dGame/dUtilities/UgcManifest.h | 6 +++++ dWorldServer/WorldServer.cpp | 9 +++++++ docs/UgcServer.md | 10 ++++++-- tests/dGameTests/UgcManifestTests.cpp | 34 +++++++++++++++++++++++++++ 5 files changed, 73 insertions(+), 2 deletions(-) diff --git a/dGame/dUtilities/UgcManifest.cpp b/dGame/dUtilities/UgcManifest.cpp index d8f75b415..261f91f37 100644 --- a/dGame/dUtilities/UgcManifest.cpp +++ b/dGame/dUtilities/UgcManifest.cpp @@ -418,6 +418,22 @@ namespace { } } +size_t UgcManifest::OnPropertyLoading(const SystemAddress& sysAddr, const LWOOBJID propertyId) { + if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS || !ServesModels()) return 0; + std::set blueprintIds; + for (const auto& model : Database::Get()->GetPropertyModels(propertyId)) { + if (model.ugcId != 0) blueprintIds.insert(model.ugcId); + } + size_t sent = 0; + for (const auto id : blueprintIds) { + const auto checksum = Database::Get()->GetUgcFileChecksum(id, "model.nif"); + if (!checksum || checksum->md5.size() != 32) continue; // not made: its LXFML is sent + Send(sysAddr, id, eUgcResourceType::NIF, checksum); + sent++; + } + return sent; +} + 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()); diff --git a/dGame/dUtilities/UgcManifest.h b/dGame/dUtilities/UgcManifest.h index 2dd5f2fa6..831eb7817 100644 --- a/dGame/dUtilities/UgcManifest.h +++ b/dGame/dUtilities/UgcManifest.h @@ -110,6 +110,12 @@ namespace UgcManifest { // 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 player is loading a property (before its models are constructed for them): the client is sent the served NIF + // checksum of every placed model whose mesh the UGC server made. Its cached one can be its own build's (the LXFML it + // was sent for the HKX, or before the model was made, replaced the served .nif and its checksum), which the client + // would use as it is; with the served checksum it downloads the served mesh again. Returns how many were sent. + size_t OnPropertyLoading(const SystemAddress& sysAddr, LWOOBJID propertyId); + // 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); diff --git a/dWorldServer/WorldServer.cpp b/dWorldServer/WorldServer.cpp index b793de1b1..ba264dc6d 100644 --- a/dWorldServer/WorldServer.cpp +++ b/dWorldServer/WorldServer.cpp @@ -1062,6 +1062,15 @@ void LoadPlayer(const SystemAddress& sysAddr) { respawnCheckpoint.SendToClient(player->GetSystemAddress()); } + // Before the models are constructed: the served meshes' checksums, so a client whose cached checksum is its own + // build's downloads the served mesh again (UgcManifest, docs/UgcServer.md) + if (g_CloneID != 0 && UgcManifest::ServesModels()) { + const auto mapId = Game::zoneManager->GetZone()->GetZoneID().GetMapID(); + if (const auto property = Database::Get()->GetPropertyInfo(mapId, g_CloneID)) { + UgcManifest::OnPropertyLoading(sysAddr, property->id); + } + } + Game::entityManager->ConstructAllEntities(sysAddr); characterComponent->RocketUnEquip(player); diff --git a/docs/UgcServer.md b/docs/UgcServer.md index 45c07d95e..f44b9a350 100644 --- a/docs/UgcServer.md +++ b/docs/UgcServer.md @@ -569,7 +569,10 @@ 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) + the served mesh and draws it. Before the models are constructed for the player, the world sends them the served NIF + checksum of every made model placed there (`UgcManifest::OnPropertyLoading`): after an earlier visit the client's + cached NIF checksum is its own build's (from the LXFML it got for the HKX, or before the model was made), which it + would use without asking; with the served one it downloads the served mesh again. Tried and dropped: sending every model's LXFML (the client builds its own NIF and HKX) 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"). @@ -577,7 +580,10 @@ serving), the worlds: * **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). 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. + loads its own HKX, so served models have collision; the mesh already drawn stays the served one. The build also + replaces the served `.nif` on disk and its cached checksum (see above), so the next load needs the served checksum + again (Property load). The HKX's checksum stays cached (valid entries never expire), so the client asks for the + HKX, and rebuilds, only while it has no entry for it. * **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 48d03b832..2d92b5c2c 100644 --- a/tests/dGameTests/UgcManifestTests.cpp +++ b/tests/dGameTests/UgcManifestTests.cpp @@ -221,3 +221,37 @@ TEST_F(UgcManifestRequestTests, SwitchSendsTheChecksumThenModelReady) { 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])); } + +namespace { + // A property with four placed models: 1234 (made) twice, 5678 (not made) and a prefab without a blueprint + class PropertyDatabase : public TestSQLDatabase { + public: + std::vector GetPropertyModels(const LWOOBJID&) override { + std::vector models(4); + models[0].ugcId = 1234; + models[1].ugcId = 1234; + models[2].ugcId = 5678; + return models; + } + std::optional GetUgcFileChecksum(const LWOOBJID blueprintId, const std::string_view file) override { + if (blueprintId == 1234 && file == "model.nif") return IUgc::FileChecksum{ "00112233445566778899aabbccddeeff", 0x01020304 }; + return std::nullopt; + } + }; +} + +// A property load sends the served NIF checksum of each made model once, before the models are constructed: the +// client's cached one can be its own build's, which it would use as it is +TEST_F(UgcManifestRequestTests, PropertyLoadSendsTheServedChecksums) { + Database::_setDatabase(new PropertyDatabase()); + Settings("1", "1"); + const auto sent = Capture([] { EXPECT_EQ(UgcManifest::OnPropertyLoading(Client(), 42), 1); }); + ASSERT_EQ(sent.size(), 1); + EXPECT_EQ(sent[0].sysAddr, Client()); + EXPECT_PACKET_EQ(FromHex("53 05 00 3c 00 00 00 00 d2 04 00 00 00 00 00 00 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])); + + // Models not served: nothing + Settings("1", "0"); + EXPECT_TRUE(Capture([] { EXPECT_EQ(UgcManifest::OnPropertyLoading(Client(), 42), 0); }).empty()); +}