diff --git a/dDashboardServer/routes/SettingsCatalog.cpp b/dDashboardServer/routes/SettingsCatalog.cpp index 4dc91ace6..22c0134e8 100644 --- a/dDashboardServer/routes/SettingsCatalog.cpp +++ b/dDashboardServer/routes/SettingsCatalog.cpp @@ -424,7 +424,7 @@ namespace { c.Add(Port(UGC, "net_port", "Master connection port", "UDP; the next port is used too.", "2012")); c.Add(Text(UGC, "client_path", "Download path", "The game client's UGCSERVERDIR.", "/ugc", true)); c.Add(Bool(SHARED, "ugc_manifest", "Answer clients without 3D services", "Worlds tell game clients with UGCUSE3DSERVICES=7:0 (the default) the checksums of the files the UGC server made, so they download icons of cars, rockets and models from UGCSERVERIP, UGCSERVERPORT and UGCSERVERDIR in their boot.cfg (default: the patch server's). A client that can't connect there is logged out, so only turn on when every player's boot.cfg points at the UGC server.", false)); - c.Add(When(Bool(SHARED, "ugc_manifest_models", "Placed models from the UGC server", "With the answer to clients without 3D services on: placed player models the UGC server has made are downloaded from it (its mesh) instead of built by each client from the LXFML the world sends. It makes no physics: a client that never built a model itself has no collision for it. Models not made yet are still sent as LXFML.", false), SHARED, "ugc_manifest", { "1" })); + c.Add(When(Bool(SHARED, "ugc_manifest_models", "Placed models from the UGC server", "With the answer to clients without 3D services on: placed player models the UGC server has made are shown with its mesh. Each client still gets every model's LXFML and builds it first (for its collision; the UGC server makes no physics), then is switched to the served mesh a few seconds after it has loaded. Models not made yet stay as the client built them.", false), SHARED, "ugc_manifest", { "1" })); c.Add(Format(Text(DASHBOARD, "ugc_internal_url", "UGC server address (internal)", "Where the dashboard itself reaches the UGC server for its status and files. Empty: http://127.0.0.1:2008.", ""), eFormat::URL)); c.Add(Format(Text(DASHBOARD, "ugc_public_url", "UGC server address (public)", "Only for the \"open on the UGC server\" links, e.g. https://ugc.example.com. The dashboard's own pages don't need the browser to reach it.", ""), eFormat::URL)); c.AddSection("UGC processing", "When models are made and how much of the machine the workers may use.", eLayout::ROWS, Condition{ MASTER, "enable_ugc_server", { "1" } }); diff --git a/dGame/dUtilities/UgcManifest.cpp b/dGame/dUtilities/UgcManifest.cpp index 509180b4b..1109989db 100644 --- a/dGame/dUtilities/UgcManifest.cpp +++ b/dGame/dUtilities/UgcManifest.cpp @@ -29,6 +29,10 @@ 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; struct Waiting { SystemAddress sysAddr; @@ -42,6 +46,17 @@ 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; + 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) @@ -122,6 +137,7 @@ 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)); @@ -137,8 +153,8 @@ UgcManifest::eAction UgcManifest::Decide(const eUgcResourceType type, const bool case eUgcResourceType::LXFML: return modelsOn && meshMade ? eAction::ANSWER : eAction::SEND_LXFML; case eUgcResourceType::HKX: - // The UGC server makes no physics: the client uses an HKX it built before, else it asks and gets 404 - return modelsOn && meshMade ? eAction::ANSWER_UNKNOWN : eAction::SEND_LXFML; + // The UGC server makes no physics: the client builds it from the LXFML (and is then switched to a served mesh) + return eAction::SEND_LXFML; default: return eAction::NONE; } } @@ -167,8 +183,25 @@ bool UgcManifest::ServesModels() { return ManifestOn() && ModelsOn(); } -bool UgcManifest::ClientBuildsModel(const LWOOBJID blueprintId) { - return !ServesModels() || !MeshMade(blueprintId); +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) { @@ -206,10 +239,38 @@ void UgcManifest::OnRequest(const SystemAddress& sysAddr, LWOOBJID blueprintId, g_Waiting.push_back({ sysAddr, blueprintId, resourceType, std::chrono::steady_clock::now() }); } +namespace { + // 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; + GameMessages::NotifyClientUGCModelReady ready; + ready.target = model->GetObjectID(); + ready.blueprintID = pending.blueprintId; + ready.Send(pending.sysAddr); + shown++; + } + LOG_DEBUG("Switched a client to the served mesh of %llu (%zu placed)", static_cast(pending.blueprintId), shown); + return true; + } +} + void UgcManifest::Update() { - if (g_Waiting.empty()) return; const auto now = std::chrono::steady_clock::now(); - if (now < g_NextRetry) return; + std::erase_if(g_Switches, [now](Switch& pending) { return now - pending.since > MAX_WAIT || SwitchToServedMesh(pending, now); }); + + if (g_Waiting.empty() || now < g_NextRetry) return; g_NextRetry = now + RETRY_INTERVAL; std::erase_if(g_Waiting, [now](const Waiting& waiting) { return now - waiting.since > MAX_WAIT || TryAnswerIcon(waiting.sysAddr, waiting.blueprintId, waiting.resourceType); @@ -219,6 +280,8 @@ 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; }); } void UgcManifest::OnModelsMade(const std::vector& blueprintIds) { diff --git a/dGame/dUtilities/UgcManifest.h b/dGame/dUtilities/UgcManifest.h index 26fdb7c60..9c561f84c 100644 --- a/dGame/dUtilities/UgcManifest.h +++ b/dGame/dUtilities/UgcManifest.h @@ -20,10 +20,13 @@ * * Player models (NIF, HKX, LXFML): a placed model's client always asks for all three (its BlueprintComponent loads the * blueprint's NIF and HKX, its ModelBehaviorComponent the LXFML) and waits for the answers without a timeout. With * `ugc_manifest_models` 1 and the model's mesh made, the NIF is answered with the UGC server's checksum (the client - * downloads the served mesh), the LXFML with the stored LXFML's and the HKX as not known (valid 0: the client uses - * one it has, else asks the UGC server, which answers 404). Otherwise 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: a model is never left waiting. + * 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. * * Main thread only: each lookup is one indexed query and the answers are sent from here. See docs/UgcServer.md. */ @@ -47,13 +50,20 @@ namespace UgcManifest { // ugc_manifest=1 and ugc_manifest_models=1: placed models' meshes come from the UGC server when made bool ServesModels(); - // Whether the client should get this model's LXFML when a property loads (it builds the model itself): not when - // ServesModels() and the UGC server made its mesh - bool ClientBuildsModel(LWOOBJID blueprintId); + // 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) + 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, and forgets the ones waited on too long + // Answers the waiting requests whose files have been made since, forgets the ones waited on too long, and switches + // clients to served meshes that are due void Update(); // A client left: its waiting requests are dropped diff --git a/dWorldServer/WorldServer.cpp b/dWorldServer/WorldServer.cpp index 231bcbd8d..373a5fd90 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 left out: the client asks for their files and downloads - // the mesh (UgcManifest, docs/UgcServer.md). + // 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). auto bbbModels = Database::Get()->GetUgcModels(propertyId); if (bbbModels.empty()) { @@ -1047,10 +1047,7 @@ void LoadPlayer(const SystemAddress& sysAddr) { response.reasonCode = eBlueprintSaveResponseType::EverythingWorked; size_t served = 0; for (auto& bbbModel : bbbModels) { - if (!UgcManifest::ClientBuildsModel(bbbModel.id)) { - served++; - continue; - } + if (UgcManifest::ScheduleServedMesh(sysAddr, bbbModel.id)) served++; LOG("Getting lxfml ugcID: %llu", bbbModel.id); bbbModel.lxfmlData.seekg(0, std::ios::end); @@ -1062,7 +1059,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 come from the UGC server", served, bbbModels.size()); + if (served > 0) LOG("%zu of the property's %zu models get their mesh from the UGC server once built", served, bbbModels.size()); if (!response.models.empty()) response.Send(sysAddr); } diff --git a/docs/UgcServer.md b/docs/UgcServer.md index 09e821b9c..b5dd5f27a 100644 --- a/docs/UgcServer.md +++ b/docs/UgcServer.md @@ -401,14 +401,19 @@ 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 (one `BlueprintSaveResponse`) only for the models whose mesh the UGC server hasn't - made (no `model.nif` in `ugc_file_checksums`: pending, failed, empty or the UGC server not running). Made ones are - left out; the client asks for their files. +* **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 and `NotifyClientUGCModelReady` for each placed copy. The client drops what it + cached for the model and asks again: its cache now has the served NIF's checksum, which its own file doesn't match, + so it downloads the served mesh, and still has its own HKX's, so it keeps the collision it built. The model shows + as the client built it for those few seconds. * **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 a made model: valid 0. The UGC server makes no physics: a client that built the model itself before keeps - using its HKX; others ask the UGC server, get 404 (not a logout) and have no collision for that model. +* **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. * **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/resources/sharedconfig.ini b/resources/sharedconfig.ini index a9152cf25..1b3d5c762 100644 --- a/resources/sharedconfig.ini +++ b/resources/sharedconfig.ini @@ -100,8 +100,7 @@ ugc_debounce_seconds=120 # out, so only set 1 when every player's boot.cfg points at the UGC server. See docs/UgcServer.md. ugc_manifest=0 -# 0 or 1, with ugc_manifest=1: placed player models the UGC server has made are downloaded from it (their mesh) instead -# of being built by each client from the LXFML the world sends; models not made yet are still sent as LXFML. The UGC -# server makes no physics (.hkx): a client that never built a model itself has no collision for it. See -# docs/UgcServer.md. +# 0 or 1, with ugc_manifest=1: placed player models the UGC server has made are shown with its mesh. Each client still +# gets every model's LXFML and builds it first (for its collision: the UGC server makes no physics), then is switched to +# the served mesh a few seconds after it has loaded. See docs/UgcServer.md. ugc_manifest_models=0 diff --git a/tests/dGameTests/UgcManifestTests.cpp b/tests/dGameTests/UgcManifestTests.cpp index cf2e26505..83b567a3c 100644 --- a/tests/dGameTests/UgcManifestTests.cpp +++ b/tests/dGameTests/UgcManifestTests.cpp @@ -23,7 +23,8 @@ namespace { // What each request gets. A placed model's client asks for its NIF, HKX and LXFML and waits for every answer without a // timeout (LWOResMgr2Interface::RequestBlueprintManifestThenLoad, 0x0105a910), so model files are always answered: -// from the UGC server when it made the mesh and models are served, else with the LXFML for the client to build. +// from the UGC server when it made the mesh and models are served, else with the LXFML for the client to build. The HKX +// always gets the LXFML: the UGC server makes no physics, the client builds its own (then gets the served mesh). TEST(UgcManifestTests, DecideCoversEveryType) { using T = eUgcResourceType; for (const auto type : { T::LXFML, T::NIF, T::HKX, T::DDS }) { @@ -34,7 +35,7 @@ TEST(UgcManifestTests, DecideCoversEveryType) { EXPECT_EQ(UgcManifest::Decide(T::NIF, true, true, true), eAction::ANSWER); EXPECT_EQ(UgcManifest::Decide(T::LXFML, true, true, true), eAction::ANSWER); - EXPECT_EQ(UgcManifest::Decide(T::HKX, true, true, true), eAction::ANSWER_UNKNOWN); + EXPECT_EQ(UgcManifest::Decide(T::HKX, true, true, true), eAction::SEND_LXFML); for (const auto type : { T::LXFML, T::NIF, T::HKX }) { EXPECT_EQ(UgcManifest::Decide(type, true, true, false), eAction::SEND_LXFML); // not made yet @@ -70,14 +71,16 @@ TEST_F(UgcManifestRequestTests, NothingWhenOff) { const auto sent = Capture([] { UgcManifest::OnRequest(Client(), 1234, eUgcResourceType::NIF); }); EXPECT_TRUE(sent.empty()); EXPECT_FALSE(UgcManifest::ServesModels()); - EXPECT_TRUE(UgcManifest::ClientBuildsModel(1234)); + 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 TEST_F(UgcManifestRequestTests, UnknownModelFilesAreAnsweredNotKnown) { Settings("1", "1"); EXPECT_TRUE(UgcManifest::ServesModels()); - EXPECT_TRUE(UgcManifest::ClientBuildsModel(1234)); // its mesh isn't made + 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);