mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-04 11:53:44 +00:00
A placed model's client (1.10.64, UGCUSE3DSERVICES=7:0) always loads its blueprint's NIF and HKX (its BlueprintComponent sets renderUserGen and physicsUserGen itself) and its LXFML, asks the world for each file's manifest first and waits for the answer with no timeout. With ugc_manifest=1 and the new ugc_manifest_models=1 (default 0) the world: - leaves the models whose mesh the UGC server made out of the LXFML it sends when a property loads, - answers their NIF with the UGC server's checksum, their LXFML with the stored LXFML's (worked out once and kept) and their HKX as not known, - sends a model that isn't made its LXFML (once for the three requests), so the client builds it itself and no model is left waiting. When the UGC server writes a model's mesh with a new checksum it sends UGC_MODELS_MADE (a new master message, appended) to the master, which passes it to every world; a world with that model placed sends its players the new NIF checksum and NotifyClientUGCModelReady (game message 909, the blueprint id), so clients switch to the served mesh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
122 lines
4.9 KiB
C++
122 lines
4.9 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.
|
|
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::ANSWER_UNKNOWN);
|
|
|
|
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_TRUE(UgcManifest::ClientBuildsModel(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_TRUE(UgcManifest::ClientBuildsModel(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());
|
|
}
|