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()); +}