Files
DarkflameServer/tests/dGameTests/UgcManifestTests.cpp
Aaron Kimbrell bd1d76193f 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>
2026-09-29 00:05:42 -05:00

224 lines
10 KiB
C++

#include "UgcManifest.h"
#include "ClientPackets.h"
#include "dConfig.h"
#include "GameDependencies.h"
#include "MD5.h"
#include "PacketTestUtils.h"
#include "Sd0.h"
#include <gtest/gtest.h>
using namespace PacketTestUtils;
using UgcManifest::eAction;
namespace {
std::string Md5Hex(const std::string& data) {
MD5 md5;
md5.update(reinterpret_cast<const unsigned char*>(data.data()), static_cast<MD5::size_type>(data.size()));
md5.finalize();
return md5.hexdigest();
}
}
// 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. 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 }) {
EXPECT_EQ(UgcManifest::Decide(type, false, true, true), eAction::NONE);
}
EXPECT_EQ(UgcManifest::Decide(T::DDS, true, false, false), eAction::WAIT);
EXPECT_EQ(UgcManifest::Decide(T::DDS, true, true, true), eAction::WAIT);
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::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
EXPECT_EQ(UgcManifest::Decide(type, true, false, true), eAction::SEND_LXFML); // models not served
EXPECT_EQ(UgcManifest::Decide(type, true, false, false), eAction::SEND_LXFML);
}
EXPECT_EQ(UgcManifest::Decide(static_cast<T>(7), true, true, true), eAction::NONE);
}
class UgcManifestRequestTests : public GameDependenciesTest {
protected:
void SetUp() override { SetUpDependencies(); }
void TearDown() override {
UgcManifest::OnDisconnect(Client());
TearDownDependencies();
}
static SystemAddress Client() {
SystemAddress address;
address.binaryAddress = 0x0100007f;
address.port = 15001;
return address;
}
void Settings(const std::string& manifest, const std::string& models) {
Game::config->SetDatabaseValues({ { "ugc_manifest", manifest }, { "ugc_manifest_models", models } }, {});
}
};
// Off (the default): nothing is answered, as before
TEST_F(UgcManifestRequestTests, NothingWhenOff) {
Settings("0", "1");
const auto sent = Capture([] { UgcManifest::OnRequest(Client(), 1234, eUgcResourceType::NIF); });
EXPECT_TRUE(sent.empty());
EXPECT_FALSE(UgcManifest::ServesModels());
EXPECT_FALSE(UgcManifest::ServesMesh(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_FALSE(UgcManifest::ServesMesh(1234)); // its mesh isn't made
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);
EXPECT_EQ(sent[0].sysAddr, Client());
EXPECT_FALSE(sent[0].broadcast);
EXPECT_PACKET_EQ(FromHex("53 05 00 3c 00 00 00 00 08 07 06 05 04 03 02 01 0" + std::to_string(static_cast<int>(type)) +
" 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"), FromCapture(sent[0]));
}
}
// An icon that isn't made yet waits (answered by Update once the UGC server stored its checksum)
TEST_F(UgcManifestRequestTests, IconsWait) {
Settings("1", "0");
const auto sent = Capture([] { UgcManifest::OnRequest(Client(), 1234, eUgcResourceType::DDS); });
EXPECT_TRUE(sent.empty());
}
// Models made while nobody is on a property with them: nothing to tell
TEST_F(UgcManifestRequestTests, ModelsMadeWithNobodyToTell) {
Settings("1", "1");
const auto sent = Capture([] { UgcManifest::OnModelsMade({ 1234, 5678 }); });
EXPECT_TRUE(sent.empty());
}
// The client checks the MD5 and size of the LXFML as it has it after inflating the UGC server's .lxfml.sd0, which is
// the stored LXFML (sd0, or plain XML in old rows) inflated
TEST_F(UgcManifestRequestTests, LxfmlChecksumIsOfTheInflatedLxfml) {
const std::string lxfml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\" ?>\n<LXFML versionMajor=\"5\"></LXFML>\n";
const auto plain = UgcManifest::LxfmlChecksum(lxfml);
ASSERT_TRUE(plain.has_value());
EXPECT_EQ(plain->md5, Md5Hex(lxfml));
EXPECT_EQ(plain->size, lxfml.size());
const auto compressed = UgcManifest::LxfmlChecksum(Sd0::Compress(lxfml));
ASSERT_TRUE(compressed.has_value());
EXPECT_EQ(compressed->md5, plain->md5);
EXPECT_EQ(compressed->size, plain->size);
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]));
}