Files
DarkflameServer/tests/dGameTests/UgcManifestTests.cpp
Aaron Kimbrell 5396594690 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>
2026-09-28 22:31:20 -05:00

123 lines
5.0 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());
}