mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-12 15:53:44 +00:00
fix(ugc): served player models keep the client's own collision
With ugc_manifest_models the world left made models out of the LXFML it sends when a property loads, so the client never built them and had no HKX: the UGC server makes no physics, and the HKX request got a 404, so served models had no collision. Now every model's LXFML is sent and the client builds each one (NIF and HKX). For the models whose mesh the UGC server made, once the client has loaded and 3 seconds after, the world sends it the served NIF's checksum and NotifyClientUGCModelReady: the client drops what it cached for the model and asks again, downloads the served mesh (its own NIF no longer matches) and keeps its own HKX (still matching). An HKX request is answered with the LXFML too, followed by the same switch, at most 3 times per client and model. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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::pair<SystemAddress, LWOOBJID>, std::chrono::steady_clock::time_point> g_LxfmlSent;
|
||||
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;
|
||||
|
||||
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<unsigned long long>(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<LWOOBJID>(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<unsigned long long>(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<LWOOBJID>& blueprintIds) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user