mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 02:43:44 +00:00
fix(ugc): switch a client to a served mesh only once its own build is done
When a model finishes on the UGC server while players are on the property, each client was sent the served NIF checksum, NotifyClientUGCModelReady and the model constructed again at once. A client that was sent the model's LXFML shortly before (the property load, a request for a model not made yet, the owner's brick by brick save) is still building it then: the build writes its own .nif over BrickModels/UserMade/<id>.nif and caches that file's MD5 as the NIF's manifest info when it finishes (client 1.10.64: LWOBBBInterface::GenerateModelFromLxfml 0x00b6c220, MainThread_Process- ModelResponse 0x00b5a1e0), undoing the switch, so it kept its own build. That is the usual case: another player's client asks for a model the owner just placed, gets its LXFML, and that request makes the UGC server make the model at once. Now such a client is switched 30 seconds after the last LXFML sent to it (UgcManifest::ServedMeshSwitches, BUILD_SETTLE); another LXFML sent meanwhile starts the wait again. Other clients are switched at once, as before. The served checksum is looked up when the switch happens. HKX requests are still answered with the LXFML, so collision stays the client's own. Check in game (ugc_manifest=1, ugc_manifest_models=1, two accounts on one property): the owner saves a new model; within about 30 s of the UGC server making it, the other player's copy blinks out and back with the served mesh (vertex AO, look of the dashboard's viewer), and still has collision (walk into it). The owner's copy switches the same way. The world log shows "Switching ... to the served mesh of model ...". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,7 @@
|
||||
#include "PropertyMessages.h"
|
||||
#include "Sd0.h"
|
||||
#include "UgcKeys.h"
|
||||
#include "UgcManifest.h"
|
||||
#include "User.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -276,6 +277,8 @@ void BrickByBrick::Save(Entity& player, const LWOOBJID localId, const std::strin
|
||||
responseModel.data = model.sd0;
|
||||
}
|
||||
response.Send(player.GetSystemAddress());
|
||||
// The client builds the models from it: the switch to their served meshes waits for that (UgcManifest)
|
||||
for (const auto& model : models) UgcManifest::OnLxfmlSent(player.GetSystemAddress(), model.blueprintId);
|
||||
|
||||
for (auto* item : ItemsIn(*inventory, eInventoryType::MODELS_IN_BBB)) {
|
||||
item->SetCount(0, false, false, false, eLootSourceType::INVENTORY);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "UgcManifest.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <map>
|
||||
#include <set>
|
||||
@@ -51,7 +52,8 @@ namespace {
|
||||
|
||||
std::vector<Waiting> g_Waiting;
|
||||
std::chrono::steady_clock::time_point g_NextRetry{};
|
||||
std::map<std::pair<SystemAddress, LWOOBJID>, std::chrono::steady_clock::time_point> g_LxfmlSent;
|
||||
// LXFMLs sent to clients (building those models) and the switches to served meshes waiting for them
|
||||
UgcManifest::ServedMeshSwitches g_Switches;
|
||||
std::map<LWOOBJID, IUgc::FileChecksum> g_LxfmlChecksums;
|
||||
|
||||
// Placed models to construct again for a client once its flush is done
|
||||
@@ -71,6 +73,8 @@ namespace {
|
||||
};
|
||||
std::optional<PropertyReload> g_PropertyReload;
|
||||
|
||||
void RunDueSwitches(std::chrono::steady_clock::time_point now);
|
||||
|
||||
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)
|
||||
@@ -140,8 +144,7 @@ namespace {
|
||||
// no such model
|
||||
bool SendLxfml(const SystemAddress& sysAddr, LWOOBJID blueprintId) {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
std::erase_if(g_LxfmlSent, [now](const auto& sent) { return now - sent.second > LXFML_RESEND_AFTER; });
|
||||
if (g_LxfmlSent.contains({ sysAddr, blueprintId })) return true;
|
||||
if (g_Switches.SentWithin(sysAddr, blueprintId, LXFML_RESEND_AFTER, now)) return true;
|
||||
|
||||
auto model = Database::Get()->GetUgcModel(blueprintId);
|
||||
if (!model) return false;
|
||||
@@ -150,7 +153,7 @@ namespace {
|
||||
response.reasonCode = eBlueprintSaveResponseType::EverythingWorked;
|
||||
response.models.push_back({ blueprintId, model->lxfmlData.str() });
|
||||
response.Send(sysAddr);
|
||||
g_LxfmlSent[{ sysAddr, blueprintId }] = now;
|
||||
g_Switches.LxfmlSent(sysAddr, blueprintId, now);
|
||||
// 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<unsigned long long>(blueprintId));
|
||||
@@ -158,6 +161,42 @@ namespace {
|
||||
}
|
||||
}
|
||||
|
||||
void UgcManifest::ServedMeshSwitches::LxfmlSent(const SystemAddress& sysAddr, const LWOOBJID blueprintId, const Clock::time_point now) {
|
||||
std::erase_if(m_LxfmlSent, [now](const auto& sent) { return now - sent.second >= BUILD_SETTLE; });
|
||||
const Key key{ sysAddr, blueprintId };
|
||||
m_LxfmlSent[key] = now;
|
||||
// A switch now would be undone by the build this starts
|
||||
if (const auto due = m_Due.find(key); due != m_Due.end()) due->second = std::max(due->second, now + BUILD_SETTLE);
|
||||
}
|
||||
|
||||
bool UgcManifest::ServedMeshSwitches::SentWithin(const SystemAddress& sysAddr, const LWOOBJID blueprintId, const Clock::duration within, const Clock::time_point now) const {
|
||||
const auto sent = m_LxfmlSent.find({ sysAddr, blueprintId });
|
||||
return sent != m_LxfmlSent.end() && now - sent->second < within;
|
||||
}
|
||||
|
||||
void UgcManifest::ServedMeshSwitches::Schedule(const SystemAddress& sysAddr, const LWOOBJID blueprintId, const Clock::time_point now) {
|
||||
const Key key{ sysAddr, blueprintId };
|
||||
auto due = now;
|
||||
if (const auto sent = m_LxfmlSent.find(key); sent != m_LxfmlSent.end()) due = std::max(due, sent->second + BUILD_SETTLE);
|
||||
if (const auto pending = m_Due.find(key); pending != m_Due.end()) due = std::max(due, pending->second);
|
||||
m_Due[key] = due;
|
||||
}
|
||||
|
||||
std::vector<UgcManifest::ServedMeshSwitches::Switch> UgcManifest::ServedMeshSwitches::TakeDue(const Clock::time_point now) {
|
||||
std::vector<Switch> due;
|
||||
std::erase_if(m_Due, [&](const auto& pending) {
|
||||
if (pending.second > now) return false;
|
||||
due.push_back({ pending.first.first, pending.first.second });
|
||||
return true;
|
||||
});
|
||||
return due;
|
||||
}
|
||||
|
||||
void UgcManifest::ServedMeshSwitches::Forget(const SystemAddress& sysAddr) {
|
||||
std::erase_if(m_LxfmlSent, [&sysAddr](const auto& sent) { return sent.first.first == sysAddr; });
|
||||
std::erase_if(m_Due, [&sysAddr](const auto& pending) { return pending.first.first == sysAddr; });
|
||||
}
|
||||
|
||||
UgcManifest::eAction UgcManifest::Decide(const eUgcResourceType type, const bool manifestOn, const bool modelsOn, const bool meshMade) {
|
||||
if (!manifestOn) return eAction::NONE;
|
||||
switch (type) {
|
||||
@@ -235,28 +274,26 @@ void UgcManifest::OnRequest(const SystemAddress& sysAddr, LWOOBJID blueprintId,
|
||||
g_Waiting.push_back({ sysAddr, blueprintId, resourceType, std::chrono::steady_clock::now() });
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Shows a placed model to one client again with the served mesh. NotifyClientUGCModelReady only flushes the
|
||||
// client's cached NIF, HKX and LXFML and loads them again as preloads (LWOBlueprintComponent::
|
||||
// OnNotifyClientUGCModelReady, 0x00ca6430): an object already drawn keeps its mesh. So the object is also taken
|
||||
// down and constructed again for that client, once the flush is done (RECONSTRUCT_DELAY; constructed at once, the
|
||||
// flush can land after the new object loaded its mesh, which then disappears). Its new render component loads the
|
||||
// NIF, whose cached checksum is now the served one (sent first); the HKX's is still the one the client built, so
|
||||
// it keeps its collision.
|
||||
void ShowServedMesh(Entity& model, Entity& player, const LWOOBJID blueprintId) {
|
||||
const auto sysAddr = player.GetSystemAddress();
|
||||
if (model.GetIsGhostingCandidate()) {
|
||||
auto* const ghost = player.GetComponent<GhostComponent>();
|
||||
if (!ghost || !ghost->IsObserved(model.GetObjectID())) return; // not shown to this client (yet)
|
||||
}
|
||||
void UgcManifest::SwitchClient(const SystemAddress& sysAddr, const LWOOBJID blueprintId, const IUgc::FileChecksum& checksum, const std::vector<LWOOBJID>& modelIds) {
|
||||
// The new checksum first: the client keeps its cached one (its own build's) otherwise, and loads the file it has
|
||||
Send(sysAddr, blueprintId, eUgcResourceType::NIF, checksum);
|
||||
const auto due = std::chrono::steady_clock::now() + RECONSTRUCT_DELAY;
|
||||
for (const auto objectId : modelIds) {
|
||||
// Flushes the client's cached NIF, HKX and LXFML of the blueprint and preloads the NIF and HKX again
|
||||
// (LWOBlueprintComponent::OnNotifyClientUGCModelReady, 0x00ca6430); an object already drawn keeps its mesh
|
||||
GameMessages::NotifyClientUGCModelReady ready;
|
||||
ready.target = model.GetObjectID();
|
||||
ready.target = objectId;
|
||||
ready.blueprintID = blueprintId;
|
||||
ready.Send(sysAddr);
|
||||
std::erase_if(g_Reconstructs, [&](const Reconstruct& r) { return r.sysAddr == sysAddr && r.objectId == model.GetObjectID(); });
|
||||
g_Reconstructs.push_back({ sysAddr, model.GetObjectID(), std::chrono::steady_clock::now() + RECONSTRUCT_DELAY });
|
||||
// So the model is also taken down and constructed again once the flush is done (constructed at once, the flush
|
||||
// can land after the new object loaded its mesh, which then disappears). Its HKX's cached checksum is still the
|
||||
// client's own build's, so it keeps its collision.
|
||||
std::erase_if(g_Reconstructs, [&](const Reconstruct& r) { return r.sysAddr == sysAddr && r.objectId == objectId; });
|
||||
g_Reconstructs.push_back({ sysAddr, objectId, due });
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
// 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) {
|
||||
@@ -336,6 +373,7 @@ void UgcManifest::Update() {
|
||||
g_PropertyReload.reset();
|
||||
}
|
||||
}
|
||||
RunDueSwitches(now);
|
||||
std::erase_if(g_Reconstructs, [now](Reconstruct& pending) { return ConstructAgain(pending, now); });
|
||||
|
||||
if (g_Waiting.empty() || now < g_NextRetry) return;
|
||||
@@ -347,36 +385,63 @@ 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; });
|
||||
g_Switches.Forget(sysAddr);
|
||||
std::erase_if(g_Reconstructs, [&sysAddr](const Reconstruct& pending) { return pending.sysAddr == sysAddr; });
|
||||
}
|
||||
|
||||
namespace {
|
||||
// The switches that are due: each client is sent the served checksum and its models shown to it are switched
|
||||
void RunDueSwitches(const std::chrono::steady_clock::time_point now) {
|
||||
if (g_Switches.Pending() == 0) return;
|
||||
const auto due = g_Switches.TakeDue(now);
|
||||
if (due.empty() || !Game::entityManager) return;
|
||||
const auto models = Game::entityManager->GetEntitiesByLOT(BrickByBrick::MODEL_OBJECT_LOT);
|
||||
for (const auto& pending : due) {
|
||||
// Models of a /reprocessproperty: the players are reloaded once they're all made
|
||||
if (g_PropertyReload && g_PropertyReload->blueprintIds.contains(pending.blueprintId)) continue;
|
||||
auto* const player = PlayerManager::GetPlayer(pending.sysAddr);
|
||||
if (!player) continue;
|
||||
auto* const ghost = player->GetComponent<GhostComponent>();
|
||||
std::vector<LWOOBJID> shown;
|
||||
for (auto* const model : models) {
|
||||
if (!model || model->GetVar<LWOOBJID>(u"blueprintid") != pending.blueprintId) continue;
|
||||
// Not shown to this client (yet): it loads the served mesh when it's constructed
|
||||
if (model->GetIsGhostingCandidate() && (!ghost || !ghost->IsObserved(model->GetObjectID()))) continue;
|
||||
shown.push_back(model->GetObjectID());
|
||||
}
|
||||
if (shown.empty()) continue;
|
||||
const auto checksum = Database::Get()->GetUgcFileChecksum(pending.blueprintId, "model.nif");
|
||||
if (!checksum || checksum->md5.size() != 32) continue;
|
||||
UgcManifest::SwitchClient(pending.sysAddr, pending.blueprintId, *checksum, shown);
|
||||
LOG("Switching %s to the served mesh of model %llu (%zu placed)", pending.sysAddr.ToString(), static_cast<unsigned long long>(pending.blueprintId), shown.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
void UgcManifest::OnModelsMade(const std::vector<LWOOBJID>& blueprintIds) {
|
||||
for (const auto id : blueprintIds) g_LxfmlChecksums.erase(id); // made again after its LXFML changed, maybe
|
||||
if (!ServesModels() || !Game::entityManager) return;
|
||||
// Models of a /reprocessproperty: the players are reloaded once they're all made
|
||||
|
||||
const auto& players = PlayerManager::GetAllPlayers();
|
||||
if (players.empty()) return;
|
||||
const auto models = Game::entityManager->GetEntitiesByLOT(BrickByBrick::MODEL_OBJECT_LOT);
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
for (const auto id : blueprintIds) {
|
||||
// Models of a /reprocessproperty: the players are reloaded once they're all made
|
||||
if (g_PropertyReload && g_PropertyReload->blueprintIds.contains(id)) continue;
|
||||
std::vector<Entity*> shown;
|
||||
for (auto* const model : models) {
|
||||
if (model && model->GetVar<LWOOBJID>(u"blueprintid") == id) shown.push_back(model);
|
||||
}
|
||||
if (shown.empty()) continue;
|
||||
const auto checksum = Database::Get()->GetUgcFileChecksum(id, "model.nif");
|
||||
if (!checksum || checksum->md5.size() != 32) continue;
|
||||
|
||||
// The new checksum first: the client keeps its cached one otherwise, and would load the file it has
|
||||
const auto placed = std::count_if(models.begin(), models.end(), [id](Entity* model) { return model && model->GetVar<LWOOBJID>(u"blueprintid") == id; });
|
||||
if (placed == 0) continue;
|
||||
// A client sent the model's LXFML lately may still be building it, and its build would undo the switch: it's
|
||||
// switched once it can't be building it any more, the others now
|
||||
for (auto* const player : players) {
|
||||
if (player) Send(player->GetSystemAddress(), id, eUgcResourceType::NIF, checksum);
|
||||
if (player) g_Switches.Schedule(player->GetSystemAddress(), id, now);
|
||||
}
|
||||
for (auto* const player : players) {
|
||||
if (!player) continue;
|
||||
for (auto* const model : shown) ShowServedMesh(*model, *player, id);
|
||||
}
|
||||
LOG("The UGC server made model %llu again: told %zu client(s) about %zu placed model(s)", static_cast<unsigned long long>(id), players.size(), shown.size());
|
||||
LOG("The UGC server made model %llu (%zu placed here): switching %zu client(s) to it", static_cast<unsigned long long>(id), static_cast<size_t>(placed), players.size());
|
||||
}
|
||||
RunDueSwitches(now);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
#ifndef UGCMANIFEST_H
|
||||
#define UGCMANIFEST_H
|
||||
|
||||
#include <chrono>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "dCommonVars.h"
|
||||
@@ -25,12 +28,47 @@
|
||||
* 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: 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.)
|
||||
* stays the served one.
|
||||
* * A client building a model from its LXFML writes its own .nif over the served one and caches that file's MD5 as the
|
||||
* NIF's manifest info when it's done (LWOBBBInterface::GenerateModelFromLxfml, 0x00b6c220;
|
||||
* MainThread_ProcessModelResponse, 0x00b5a1e0). So a client is switched to a served mesh only once it can't still be
|
||||
* building that model (ServedMeshSwitches).
|
||||
*
|
||||
* Main thread only: each lookup is one indexed query and the answers are sent from here. See docs/UgcServer.md.
|
||||
*/
|
||||
namespace UgcManifest {
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
// How long after a client was sent a model's LXFML it may still be building the model from it
|
||||
constexpr auto BUILD_SETTLE = std::chrono::seconds(30);
|
||||
|
||||
// Which clients get switched to which served meshes, and when: at once, or BUILD_SETTLE after the model's LXFML was
|
||||
// last sent to that client (a switch made while it builds is undone by its build). Main thread only.
|
||||
class ServedMeshSwitches {
|
||||
public:
|
||||
struct Switch {
|
||||
SystemAddress sysAddr;
|
||||
LWOOBJID blueprintId{};
|
||||
};
|
||||
|
||||
// The model's LXFML was sent to the client: a switch waiting for it waits BUILD_SETTLE from now
|
||||
void LxfmlSent(const SystemAddress& sysAddr, LWOOBJID blueprintId, Clock::time_point now);
|
||||
// The model's LXFML was sent to the client less than `within` ago
|
||||
bool SentWithin(const SystemAddress& sysAddr, LWOOBJID blueprintId, Clock::duration within, Clock::time_point now) const;
|
||||
// The client is to be switched to the model's served mesh once it can't be building it any more
|
||||
void Schedule(const SystemAddress& sysAddr, LWOOBJID blueprintId, Clock::time_point now);
|
||||
// The switches that are due, removed from the pending ones
|
||||
std::vector<Switch> TakeDue(Clock::time_point now);
|
||||
// The client left
|
||||
void Forget(const SystemAddress& sysAddr);
|
||||
size_t Pending() const { return m_Due.size(); }
|
||||
|
||||
private:
|
||||
using Key = std::pair<SystemAddress, LWOOBJID>;
|
||||
std::map<Key, Clock::time_point> m_LxfmlSent;
|
||||
std::map<Key, Clock::time_point> m_Due;
|
||||
};
|
||||
|
||||
// What a request gets
|
||||
enum class eAction {
|
||||
NONE, // not answered (ugc_manifest off, or not a UGC type)
|
||||
@@ -68,9 +106,18 @@ namespace UgcManifest {
|
||||
// A client left (or went back to character select): its waiting requests and pending switches are dropped
|
||||
void OnDisconnect(const SystemAddress& sysAddr);
|
||||
|
||||
// The UGC server made these models' meshes (again, with a new checksum): each one placed in this world is sent the
|
||||
// new checksum and NotifyClientUGCModelReady, so clients load the served mesh
|
||||
// The UGC server made these models' meshes (again, with a new checksum): every client this world shows one of them
|
||||
// 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<LWOOBJID>& blueprintIds);
|
||||
|
||||
// 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);
|
||||
|
||||
// Switches one client to a model's served mesh: the served NIF's checksum (the client's cached one is its own build's),
|
||||
// then NotifyClientUGCModelReady to each of `modelIds` (flushes the cached NIF, HKX and LXFML and preloads the NIF,
|
||||
// which is downloaded, and the HKX, still the client's own), then each model taken down and constructed again for
|
||||
// that client (Update) so its render loads the served NIF
|
||||
void SwitchClient(const SystemAddress& sysAddr, LWOOBJID blueprintId, const IUgc::FileChecksum& checksum, const std::vector<LWOOBJID>& modelIds);
|
||||
}
|
||||
|
||||
#endif //!UGCMANIFEST_H
|
||||
|
||||
@@ -1118,6 +1118,8 @@ void LoadPlayer(const SystemAddress& sysAddr) {
|
||||
}
|
||||
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);
|
||||
// The client builds these models: a switch to their served meshes waits for that (UgcManifest)
|
||||
for (const auto& model : response.models) UgcManifest::OnLxfmlSent(sysAddr, model.blueprintId);
|
||||
}
|
||||
|
||||
noBBB:
|
||||
|
||||
@@ -537,7 +537,8 @@ What the 1.10.64 client does with a placed player model (LOT 14, spawned with `b
|
||||
0x00c75220). There's no choice between LXFML and NIF on the render side, and the world needn't send `renderUserGen`.
|
||||
The ModelBehaviorComponent loads the blueprint's LXFML (type 0) as well (`RequestBlueprintData`, 0x00c24640).
|
||||
* Every such request first needs the blueprint's manifest info (`UGCManifest_Base::GetOrRequestManifestInfo`,
|
||||
0x0101e0e0): a cached entry (kept 300 seconds; stored in `res/BrickModels/UserMade/manifest.cache`) is used at once,
|
||||
0x0101e0e0): a cached entry is used at once (an entry with valid 1 never expires; one with valid 0 expires after
|
||||
300 seconds, `UGCManifest_Client::IsEntryExpired`, 0x010186d0),
|
||||
else the request waits and the client sends `REQUEST_UGC_MANIFEST_INFO`. **There is no timeout**: a manifest request
|
||||
the world never answers leaves that file, and the model, waiting for good
|
||||
(`LWOResMgr2Interface::RequestBlueprintManifestThenLoad`, 0x0105a910).
|
||||
@@ -548,7 +549,14 @@ What the 1.10.64 client does with a placed player model (LOT 14, spawned with `b
|
||||
* The LXFML the world sends when a property loads (`BlueprintSaveResponse` with local id 0) makes the client build the
|
||||
NIF and HKX itself and cache the manifest info of the LXFML, NIF and HKX with its own files' MD5s
|
||||
(`LWOBBBInterface::MainThread_ProcessModelResponse`, 0x00b5a1e0), which answers its own requests, so nothing is
|
||||
downloaded. That's how DLU always showed models.
|
||||
downloaded. That's how DLU always showed models. The build runs on the client's BBB thread and writes
|
||||
`BrickModels/UserMade/<bucket>/<id>.lxfml`, `.nif` and `.hkx`, the paths a download uses
|
||||
(`LWOBBBInterface::GenerateModelFromLxfml`, 0x00b6c220). So every LXFML sent to a client replaces a served `.nif` it
|
||||
downloaded, on disk and in its manifest cache, once that build is done; a mesh already drawn stays.
|
||||
* A single-asset flush (ResMgr2 0x2b06, what `NotifyClientUGCModelReady` sends) erases the path from every resource
|
||||
cache at once (`ResourceCache::FlushCachedAsset`, 0x010372a0); objects already drawn keep their mesh.
|
||||
* A download is tried 3 times; after that the client uses the file it has, if any (its own build), else the model has
|
||||
no file (`LWOResMgr2Interface::GetResource`, 0x0105ca50).
|
||||
* `NotifyClientUGCModelReady` (game message 909, the blueprint id only) to a model: its BlueprintComponent flushes the
|
||||
cached NIF, HKX and LXFML of that blueprint and requests the NIF and HKX again
|
||||
(`LWOBlueprintComponent::OnNotifyClientUGCModelReady`, 0x00ca6430). It doesn't clear the manifest cache, so the new
|
||||
@@ -562,8 +570,9 @@ 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)
|
||||
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.
|
||||
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").
|
||||
* **NIF** of a made model: the UGC server's checksum; the client downloads `<id>.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
|
||||
`<id>.lxfml.sd0`), worked out once per model and kept.
|
||||
@@ -574,9 +583,17 @@ serving), the worlds:
|
||||
never left waiting. A blueprint that isn't a player model gets valid 0.
|
||||
* **Made again**: when the UGC server writes a model's mesh with a different checksum than before (made for the first
|
||||
time, or remade after a change), it sends `UGC_MODELS_MADE` (master message 37, the blueprint ids) to the master,
|
||||
which passes it to every world. A world with that model placed sends every player the new NIF checksum, then
|
||||
`NotifyClientUGCModelReady` and the model constructed again to each player it's shown to, so the served mesh
|
||||
replaces what the client showed.
|
||||
which passes it to every world. A world with that model placed switches each player the model is shown to
|
||||
(`UgcManifest::SwitchClient`): the served NIF's checksum, `NotifyClientUGCModelReady` to each placed copy (the
|
||||
client flushes its cached NIF, HKX and LXFML and downloads the served NIF), and 1.5 s later each copy taken down for
|
||||
that player and constructed again 1 s after that; the new object draws the served NIF and loads the client's own HKX
|
||||
(still cached), so it keeps collision.
|
||||
* **Waiting for the client's own build**: a player sent the model's LXFML less than 30 s before (`BUILD_SETTLE`: the
|
||||
property load, a request for a model not made yet, the owner's brick by brick save) may still be building it, and
|
||||
that build would put its own NIF and checksum back after the switch. Such a player is switched 30 s after the last
|
||||
LXFML sent to them (`UgcManifest::ServedMeshSwitches`); another LXFML sent meanwhile starts the 30 s again. The
|
||||
others are switched at once. This is the common case: another player's client asks for a model the owner just
|
||||
placed, gets its LXFML, and the UGC server is told to make that model now (`ExpediteUgcModel`).
|
||||
A model made again unchanged (after eviction) isn't sent. Nothing polls: one message per batch of made models.
|
||||
|
||||
* **`/reprocessproperty`** (GM 8): every model placed on the property the player is on goes back to the UGC server's
|
||||
|
||||
@@ -120,3 +120,104 @@ TEST_F(UgcManifestRequestTests, LxfmlChecksumIsOfTheInflatedLxfml) {
|
||||
EXPECT_FALSE(UgcManifest::LxfmlChecksum("").has_value());
|
||||
EXPECT_FALSE(UgcManifest::LxfmlChecksum("not an sd0 stream").has_value());
|
||||
}
|
||||
|
||||
namespace {
|
||||
SystemAddress Address(uint16_t port) {
|
||||
SystemAddress address;
|
||||
address.binaryAddress = 0x0100007f;
|
||||
address.port = port;
|
||||
return address;
|
||||
}
|
||||
}
|
||||
|
||||
// A client never sent the model's LXFML (it built the model long ago, or never): switched at once
|
||||
TEST(UgcServedMeshSwitchesTests, SwitchedAtOnceWithoutARecentLxfml) {
|
||||
UgcManifest::ServedMeshSwitches switches;
|
||||
const auto t0 = UgcManifest::Clock::time_point{} + std::chrono::hours(1);
|
||||
switches.Schedule(Address(1), 1234, t0);
|
||||
const auto due = switches.TakeDue(t0);
|
||||
ASSERT_EQ(due.size(), 1);
|
||||
EXPECT_EQ(due[0].sysAddr, Address(1));
|
||||
EXPECT_EQ(due[0].blueprintId, 1234);
|
||||
EXPECT_EQ(switches.Pending(), 0);
|
||||
EXPECT_TRUE(switches.TakeDue(t0 + std::chrono::minutes(5)).empty()); // once
|
||||
}
|
||||
|
||||
// A client sent the model's LXFML lately is building the model, and its build writes its own NIF and caches its checksum
|
||||
// over the served one (LWOBBBInterface::GenerateModelFromLxfml, 0x00b6c220; MainThread_ProcessModelResponse,
|
||||
// 0x00b5a1e0): it's switched BUILD_SETTLE after that send, not before
|
||||
TEST(UgcServedMeshSwitchesTests, WaitsForTheClientsOwnBuild) {
|
||||
UgcManifest::ServedMeshSwitches switches;
|
||||
const auto t0 = UgcManifest::Clock::time_point{} + std::chrono::hours(1);
|
||||
switches.LxfmlSent(Address(1), 1234, t0);
|
||||
switches.Schedule(Address(1), 1234, t0 + std::chrono::seconds(5));
|
||||
switches.Schedule(Address(2), 1234, t0 + std::chrono::seconds(5)); // another client: never sent it
|
||||
|
||||
auto due = switches.TakeDue(t0 + std::chrono::seconds(5));
|
||||
ASSERT_EQ(due.size(), 1);
|
||||
EXPECT_EQ(due[0].sysAddr, Address(2));
|
||||
EXPECT_TRUE(switches.TakeDue(t0 + UgcManifest::BUILD_SETTLE - std::chrono::seconds(1)).empty());
|
||||
due = switches.TakeDue(t0 + UgcManifest::BUILD_SETTLE);
|
||||
ASSERT_EQ(due.size(), 1);
|
||||
EXPECT_EQ(due[0].sysAddr, Address(1));
|
||||
EXPECT_EQ(due[0].blueprintId, 1234);
|
||||
}
|
||||
|
||||
// The LXFML sent again while a switch waits (the client asked again): it starts another build, so the switch waits again
|
||||
TEST(UgcServedMeshSwitchesTests, AnotherLxfmlPushesTheSwitchBack) {
|
||||
UgcManifest::ServedMeshSwitches switches;
|
||||
const auto t0 = UgcManifest::Clock::time_point{} + std::chrono::hours(1);
|
||||
switches.LxfmlSent(Address(1), 1234, t0);
|
||||
switches.Schedule(Address(1), 1234, t0);
|
||||
switches.LxfmlSent(Address(1), 1234, t0 + std::chrono::seconds(20));
|
||||
EXPECT_TRUE(switches.TakeDue(t0 + UgcManifest::BUILD_SETTLE).empty());
|
||||
EXPECT_EQ(switches.TakeDue(t0 + std::chrono::seconds(20) + UgcManifest::BUILD_SETTLE).size(), 1);
|
||||
|
||||
// Another model's LXFML doesn't hold this one
|
||||
switches.LxfmlSent(Address(1), 5678, t0 + std::chrono::minutes(2));
|
||||
switches.Schedule(Address(1), 1234, t0 + std::chrono::minutes(2));
|
||||
EXPECT_EQ(switches.TakeDue(t0 + std::chrono::minutes(2)).size(), 1);
|
||||
}
|
||||
|
||||
// SentWithin is the 10 second guard against sending the LXFML for each of a model's three requests
|
||||
TEST(UgcServedMeshSwitchesTests, SentWithin) {
|
||||
UgcManifest::ServedMeshSwitches switches;
|
||||
const auto t0 = UgcManifest::Clock::time_point{} + std::chrono::hours(1);
|
||||
EXPECT_FALSE(switches.SentWithin(Address(1), 1234, std::chrono::seconds(10), t0));
|
||||
switches.LxfmlSent(Address(1), 1234, t0);
|
||||
EXPECT_TRUE(switches.SentWithin(Address(1), 1234, std::chrono::seconds(10), t0 + std::chrono::seconds(9)));
|
||||
EXPECT_FALSE(switches.SentWithin(Address(1), 1234, std::chrono::seconds(10), t0 + std::chrono::seconds(10)));
|
||||
EXPECT_FALSE(switches.SentWithin(Address(2), 1234, std::chrono::seconds(10), t0));
|
||||
}
|
||||
|
||||
// A client that leaves loses its pending switches and sends
|
||||
TEST(UgcServedMeshSwitchesTests, ForgetDropsTheClient) {
|
||||
UgcManifest::ServedMeshSwitches switches;
|
||||
const auto t0 = UgcManifest::Clock::time_point{} + std::chrono::hours(1);
|
||||
switches.LxfmlSent(Address(1), 1234, t0);
|
||||
switches.Schedule(Address(1), 1234, t0);
|
||||
switches.Schedule(Address(2), 1234, t0 + std::chrono::seconds(1));
|
||||
switches.Forget(Address(1));
|
||||
EXPECT_FALSE(switches.SentWithin(Address(1), 1234, UgcManifest::BUILD_SETTLE, t0));
|
||||
const auto due = switches.TakeDue(t0 + std::chrono::hours(1));
|
||||
ASSERT_EQ(due.size(), 1);
|
||||
EXPECT_EQ(due[0].sysAddr, Address(2));
|
||||
}
|
||||
|
||||
// The switch of one client: the served NIF's checksum first (the client's cached one is its own build's), then
|
||||
// NotifyClientUGCModelReady to each placed model, all to that client only
|
||||
TEST_F(UgcManifestRequestTests, SwitchSendsTheChecksumThenModelReady) {
|
||||
Settings("1", "1");
|
||||
const IUgc::FileChecksum checksum{ "00112233445566778899aabbccddeeff", 0x01020304 };
|
||||
const auto sent = Capture([&] { UgcManifest::SwitchClient(Client(), 0x0102030405060708, checksum, { 0x1122334455667788, 0x1122334455667799 }); });
|
||||
ASSERT_EQ(sent.size(), 3);
|
||||
for (const auto& packet : sent) {
|
||||
EXPECT_EQ(packet.sysAddr, Client());
|
||||
EXPECT_FALSE(packet.broadcast);
|
||||
}
|
||||
// UGC_MANIFEST_RESPONSE: blueprint, type 1 (NIF), valid, size, MD5
|
||||
EXPECT_PACKET_EQ(FromHex("53 05 00 3c 00 00 00 00 08 07 06 05 04 03 02 01 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]));
|
||||
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]));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user