mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 02:43:44 +00:00
fix(ugc): served models keep the served mesh; physics from the LXFML sent for the HKX
Sending every model's LXFML at property load and then switching to the served mesh (served checksum, NotifyClientUGCModelReady, the model constructed again) left the client drawing its own build. Now made models' LXFML is left out of the property load again: the client downloads and draws the served mesh, and when it asks for the HKX it gets the LXFML, builds the model and loads its own HKX, so the model has collision while the mesh drawn stays the served one. The load-time switch is removed; the switch for a model made again while players are there stays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,10 +31,6 @@ namespace {
|
|||||||
constexpr auto LXFML_RESEND_AFTER = std::chrono::seconds(10);
|
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)
|
// 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;
|
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;
|
|
||||||
// How long after NotifyClientUGCModelReady the model is constructed again: the client flushes the model's files on
|
// 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
|
// 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);
|
constexpr auto RECONSTRUCT_DELAY = std::chrono::milliseconds(1500);
|
||||||
@@ -54,17 +50,6 @@ namespace {
|
|||||||
std::map<std::pair<SystemAddress, LWOOBJID>, std::chrono::steady_clock::time_point> g_LxfmlSent;
|
std::map<std::pair<SystemAddress, LWOOBJID>, std::chrono::steady_clock::time_point> g_LxfmlSent;
|
||||||
std::map<LWOOBJID, IUgc::FileChecksum> g_LxfmlChecksums;
|
std::map<LWOOBJID, IUgc::FileChecksum> 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<std::chrono::steady_clock::time_point> loadedAt;
|
|
||||||
};
|
|
||||||
std::vector<Switch> g_Switches;
|
|
||||||
std::map<std::pair<SystemAddress, LWOOBJID>, int> g_SwitchCount;
|
|
||||||
|
|
||||||
// Placed models to construct again for a client once its flush is done
|
// Placed models to construct again for a client once its flush is done
|
||||||
struct Reconstruct {
|
struct Reconstruct {
|
||||||
SystemAddress sysAddr;
|
SystemAddress sysAddr;
|
||||||
@@ -154,7 +139,6 @@ namespace {
|
|||||||
response.models.push_back({ blueprintId, model->lxfmlData.str() });
|
response.models.push_back({ blueprintId, model->lxfmlData.str() });
|
||||||
response.Send(sysAddr);
|
response.Send(sysAddr);
|
||||||
g_LxfmlSent[{ sysAddr, blueprintId }] = now;
|
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
|
// Its mesh is wanted: a model still in its quiet period after a save is made now
|
||||||
Database::Get()->ExpediteUgcModel(blueprintId);
|
Database::Get()->ExpediteUgcModel(blueprintId);
|
||||||
LOG_DEBUG("Sent the LXFML of %llu for the client to build", static_cast<unsigned long long>(blueprintId));
|
LOG_DEBUG("Sent the LXFML of %llu for the client to build", static_cast<unsigned long long>(blueprintId));
|
||||||
@@ -204,23 +188,6 @@ bool UgcManifest::ServesMesh(const LWOOBJID blueprintId) {
|
|||||||
return ServesModels() && MeshMade(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) {
|
void UgcManifest::OnRequest(const SystemAddress& sysAddr, LWOOBJID blueprintId, eUgcResourceType resourceType) {
|
||||||
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) return;
|
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) return;
|
||||||
const auto manifestOn = ManifestOn();
|
const auto manifestOn = ManifestOn();
|
||||||
@@ -295,33 +262,10 @@ namespace {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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<LWOOBJID>(u"blueprintid") != pending.blueprintId) continue;
|
|
||||||
ShowServedMesh(*model, *player, pending.blueprintId);
|
|
||||||
shown++;
|
|
||||||
}
|
|
||||||
LOG("Showed %s the served mesh of model %llu (%zu placed)", player->GetCharacter() ? player->GetCharacter()->GetName().c_str() : "a player",
|
|
||||||
static_cast<unsigned long long>(pending.blueprintId), shown);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void UgcManifest::Update() {
|
void UgcManifest::Update() {
|
||||||
const auto now = std::chrono::steady_clock::now();
|
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](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;
|
if (g_Waiting.empty() || now < g_NextRetry) return;
|
||||||
@@ -334,8 +278,6 @@ void UgcManifest::Update() {
|
|||||||
void UgcManifest::OnDisconnect(const SystemAddress& sysAddr) {
|
void UgcManifest::OnDisconnect(const SystemAddress& sysAddr) {
|
||||||
std::erase_if(g_Waiting, [&sysAddr](const Waiting& waiting) { return waiting.sysAddr == 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_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; });
|
|
||||||
std::erase_if(g_Reconstructs, [&sysAddr](const Reconstruct& pending) { return pending.sysAddr == sysAddr; });
|
std::erase_if(g_Reconstructs, [&sysAddr](const Reconstruct& pending) { return pending.sysAddr == sysAddr; });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,10 +23,10 @@
|
|||||||
* downloads the served mesh) and the LXFML with the stored LXFML's. Otherwise, and always for the HKX (the UGC server
|
* 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
|
* 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.
|
* 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
|
* * Served meshes with the client's own physics: the client shows the served NIF it downloaded, and asks for the HKX,
|
||||||
* own NIF and HKX, then (once it has loaded and a moment after) gets the served NIF's checksum and
|
* which is answered with the model's LXFML: it builds the model from it and loads its own HKX. The mesh it has drawn
|
||||||
* NotifyClientUGCModelReady for that model. It drops what it cached for the model and asks again: the NIF's checksum
|
* stays the served one. (Built from the LXFML first, the client kept showing its own build even after
|
||||||
* is now the served one, so it downloads the served mesh; the HKX's is still its own, so it keeps its collision.
|
* NotifyClientUGCModelReady and the model constructed again.)
|
||||||
*
|
*
|
||||||
* Main thread only: each lookup is one indexed query and the answers are sent from here. See docs/UgcServer.md.
|
* Main thread only: each lookup is one indexed query and the answers are sent from here. See docs/UgcServer.md.
|
||||||
*/
|
*/
|
||||||
@@ -50,16 +50,10 @@ namespace UgcManifest {
|
|||||||
// ugc_manifest=1 and ugc_manifest_models=1: placed models' meshes come from the UGC server when made
|
// ugc_manifest=1 and ugc_manifest_models=1: placed models' meshes come from the UGC server when made
|
||||||
bool ServesModels();
|
bool ServesModels();
|
||||||
|
|
||||||
// ServesModels() and the UGC server made this model's mesh: a client that built the model from its LXFML (for its
|
// ServesModels() and the UGC server made this model's mesh: the model's LXFML isn't sent when a property loads, the
|
||||||
// physics) is then switched to the served mesh (ScheduleServedMesh)
|
// client downloads the served mesh, and builds the model's physics from the LXFML sent when it asks for the HKX
|
||||||
bool ServesMesh(LWOOBJID blueprintId);
|
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);
|
void OnRequest(const SystemAddress& sysAddr, LWOOBJID blueprintId, eUgcResourceType resourceType);
|
||||||
|
|
||||||
// Answers the waiting requests whose files have been made since, forgets the ones waited on too long, and switches
|
// Answers the waiting requests whose files have been made since, forgets the ones waited on too long, and switches
|
||||||
|
|||||||
@@ -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' 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 then switched to the served mesh, keeping the client's
|
// the models whose mesh the UGC server made are left out: the client downloads their mesh, and gets the
|
||||||
// own physics (UgcManifest, docs/UgcServer.md).
|
// LXFML (for its physics) when it asks for the HKX (UgcManifest, docs/UgcServer.md).
|
||||||
|
|
||||||
auto bbbModels = Database::Get()->GetUgcModels(propertyId);
|
auto bbbModels = Database::Get()->GetUgcModels(propertyId);
|
||||||
if (bbbModels.empty()) {
|
if (bbbModels.empty()) {
|
||||||
@@ -1047,7 +1047,11 @@ void LoadPlayer(const SystemAddress& sysAddr) {
|
|||||||
response.reasonCode = eBlueprintSaveResponseType::EverythingWorked;
|
response.reasonCode = eBlueprintSaveResponseType::EverythingWorked;
|
||||||
size_t served = 0;
|
size_t served = 0;
|
||||||
for (auto& bbbModel : bbbModels) {
|
for (auto& bbbModel : bbbModels) {
|
||||||
if (UgcManifest::ScheduleServedMesh(sysAddr, bbbModel.id)) served++;
|
// Its served mesh is downloaded; its LXFML is sent when the client asks for the HKX (UgcManifest)
|
||||||
|
if (UgcManifest::ServesMesh(bbbModel.id)) {
|
||||||
|
served++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
LOG("Getting lxfml ugcID: %llu", bbbModel.id);
|
LOG("Getting lxfml ugcID: %llu", bbbModel.id);
|
||||||
|
|
||||||
bbbModel.lxfmlData.seekg(0, std::ios::end);
|
bbbModel.lxfmlData.seekg(0, std::ios::end);
|
||||||
@@ -1059,7 +1063,7 @@ void LoadPlayer(const SystemAddress& sysAddr) {
|
|||||||
model.blueprintId = bbbModel.id;
|
model.blueprintId = bbbModel.id;
|
||||||
model.data = bbbModel.lxfmlData.str().substr(0, lxfmlSize);
|
model.data = bbbModel.lxfmlData.str().substr(0, lxfmlSize);
|
||||||
}
|
}
|
||||||
if (served > 0) LOG("%zu of the property's %zu models get their mesh from the UGC server once built", served, bbbModels.size());
|
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);
|
if (!response.models.empty()) response.Send(sysAddr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -405,24 +405,16 @@ 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
|
With `ugc_manifest=1` and `ugc_manifest_models=1` (`sharedconfig.ini`, default 0; the dashboard shows it under UGC
|
||||||
serving), the worlds:
|
serving), the worlds:
|
||||||
|
|
||||||
* **Property load**: send the LXFML of every model (one `BlueprintSaveResponse`): the client builds each model's NIF
|
* **Property load**: send the LXFML (one `BlueprintSaveResponse`) only for the models whose mesh the UGC server hasn't
|
||||||
and HKX (its collision; the UGC server makes no physics). For the models whose mesh the UGC server made (a
|
made (no `model.nif` in `ugc_file_checksums`). Made ones are left out: the client asks for their files, downloads
|
||||||
`model.nif` in `ugc_file_checksums`), once the client has loaded (`PlayerLoaded`) and 3 seconds after, the world
|
the served mesh and draws it. Tried and dropped: sending every model's LXFML (the client builds its own NIF and HKX)
|
||||||
sends it the served NIF's checksum, `NotifyClientUGCModelReady`, and takes the model down and constructs it again for
|
and then switching to the served mesh with the served NIF's checksum, `NotifyClientUGCModelReady` and the model
|
||||||
that client. `NotifyClientUGCModelReady` alone only flushes the client's cached NIF, HKX and LXFML and loads them
|
constructed again: the client kept drawing its own build.
|
||||||
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.
|
|
||||||
* **NIF** of a made model: the UGC server's checksum; the client downloads `<id>.nif.sd0` from the UGC server.
|
* **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
|
* **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.
|
`<id>.lxfml.sd0`), worked out once per model and kept.
|
||||||
* **HKX** of any model: the model's LXFML (the UGC server makes no physics), and for a made model the same switch to
|
* **HKX** of any model: the model's LXFML (the UGC server makes no physics). The client builds the model from it and
|
||||||
the served mesh afterwards. At most 3 switches per client and model, so a client that keeps asking isn't switched
|
loads its own HKX, so served models have collision; the mesh already drawn stays the served one.
|
||||||
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`):
|
* **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
|
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.
|
never left waiting. A blueprint that isn't a player model gets valid 0.
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ TEST_F(UgcManifestRequestTests, NothingWhenOff) {
|
|||||||
EXPECT_TRUE(sent.empty());
|
EXPECT_TRUE(sent.empty());
|
||||||
EXPECT_FALSE(UgcManifest::ServesModels());
|
EXPECT_FALSE(UgcManifest::ServesModels());
|
||||||
EXPECT_FALSE(UgcManifest::ServesMesh(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
|
// A model file of a blueprint that isn't a player model: answered as not known (37 bytes, valid 0), never left waiting
|
||||||
@@ -80,7 +79,6 @@ TEST_F(UgcManifestRequestTests, UnknownModelFilesAreAnsweredNotKnown) {
|
|||||||
Settings("1", "1");
|
Settings("1", "1");
|
||||||
EXPECT_TRUE(UgcManifest::ServesModels());
|
EXPECT_TRUE(UgcManifest::ServesModels());
|
||||||
EXPECT_FALSE(UgcManifest::ServesMesh(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 }) {
|
for (const auto type : { eUgcResourceType::NIF, eUgcResourceType::HKX, eUgcResourceType::LXFML }) {
|
||||||
const auto sent = Capture([type] { UgcManifest::OnRequest(Client(), 0x0102030405060708, type); });
|
const auto sent = Capture([type] { UgcManifest::OnRequest(Client(), 0x0102030405060708, type); });
|
||||||
ASSERT_EQ(sent.size(), 1);
|
ASSERT_EQ(sent.size(), 1);
|
||||||
|
|||||||
Reference in New Issue
Block a user