fix(ugc): send the served mesh checksums when a player loads a property

A client asks for a served model's HKX and is answered with the model's
LXFML, so it builds its own collision. That build also writes the
client's own .nif over the served one it downloaded and caches that
file's MD5 as the NIF's manifest info (client 1.10.64:
LWOBBBInterface::GenerateModelFromLxfml 0x00b6c220, MainThread_Process-
ModelResponse 0x00b5a1e0; valid entries never expire, 0x010186d0). On the
next load of the property the client used its cached checksum and file
without asking, and drew its own build instead of the served mesh.

Now, before the property's objects are constructed for a player, the
world sends them the served NIF checksum of every made model placed there
(UgcManifest::OnPropertyLoading). A client whose cached checksum is its
own build's downloads the served mesh again; one that has it already
downloads nothing. Nothing is sent unless ugc_manifest and
ugc_manifest_models are 1.

Check in game (ugc_manifest=1, ugc_manifest_models=1): visit a property
with made models, leave, come back in the same session (and after a
client restart): the models show the served mesh every time, and still
have collision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 00:05:14 -05:00
parent bd1d76193f
commit 266f8b81c5
5 changed files with 73 additions and 2 deletions

View File

@@ -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<LWOOBJID> 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());

View File

@@ -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<LWOOBJID>& 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);