mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 02:43:44 +00:00
Adds FromLiveCapture, which reads a whole captured game message packet into a struct and requires the struct to write the same bytes back, and checks DownloadPropertyData (716) with an unowned property packet from the 2011/2012 live captures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
654 lines
26 KiB
C++
654 lines
26 KiB
C++
#include "PropertyMessages.h"
|
|
#include "GameDependencies.h"
|
|
#include "GameMessageTestUtils.h"
|
|
#include "Legacy/PropertyMessagesLegacy.h"
|
|
|
|
#include "Amf3.h"
|
|
#include "GeneralUtils.h"
|
|
|
|
#include <functional>
|
|
#include <limits>
|
|
#include <map>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
using namespace GameMessageTestUtils;
|
|
|
|
namespace {
|
|
const std::vector<int32_t> g_Ints = { 0, 1, -1, 1727, std::numeric_limits<int32_t>::max(), std::numeric_limits<int32_t>::min() };
|
|
// UTF-8 as the database holds it; the structs take UTF-16 converted the way the old code converted.
|
|
const std::vector<std::string> g_Utf8 = { "", "Home", "Caf\xc3\xa9 \xe2\x98\x83 \xf0\x9f\x8f\xa0", std::string(300, 'x') };
|
|
const std::vector<NiPoint3> g_Points = { NiPoint3Constant::ZERO, NiPoint3(1.5f, -2.0f, 1e6f) };
|
|
const std::vector<NiQuaternion> g_Rotations = { QuatUtils::IDENTITY, NiQuaternion(0.5f, 0.5f, -0.5f, 0.5f) };
|
|
|
|
PacketBytes Payload(const GameMessages::NetGameMsg& msg) {
|
|
RakNet::BitStream bitStream;
|
|
msg.Serialize(bitStream);
|
|
return FromBitStream(bitStream);
|
|
}
|
|
|
|
// Serializes msg, then reads it with the legacy read sequence; both must consume exactly the same bits.
|
|
template<typename Result>
|
|
Result ReadWithLegacy(const GameMessages::NetGameMsg& msg, const std::function<Result(RakNet::BitStream&)>& read) {
|
|
RakNet::BitStream wire;
|
|
msg.Serialize(wire);
|
|
RakNet::BitStream legacyStream(wire.GetData(), wire.GetNumberOfBytesUsed(), false);
|
|
auto result = read(legacyStream);
|
|
EXPECT_EQ(legacyStream.GetReadOffset(), wire.GetNumberOfBitsUsed());
|
|
return result;
|
|
}
|
|
|
|
// The inputs PropertyManagementComponent::OnQueryPropertyData fills in.
|
|
struct PropertyDataInput {
|
|
uint32_t templateId{};
|
|
uint32_t mapId{};
|
|
uint32_t vendorMapId{};
|
|
std::string spawnName;
|
|
bool moderatorRequested{};
|
|
uint32_t reputation{};
|
|
uint32_t lastUpdatedTime{};
|
|
LWOOBJID ownerId{};
|
|
std::string ownerName;
|
|
std::string name;
|
|
std::string description;
|
|
uint64_t claimed{};
|
|
char privacy{};
|
|
LWOCLONEID cloneId{};
|
|
std::string rejectionReason;
|
|
std::vector<NiPoint3> paths;
|
|
};
|
|
|
|
// Builds the struct exactly as PropertyManagementComponent::OnQueryPropertyData does.
|
|
GameMessages::DownloadPropertyData Build(const PropertyDataInput& in, LWOOBJID author) {
|
|
GameMessages::DownloadPropertyData message;
|
|
message.target = author;
|
|
message.propertyId = 0;
|
|
message.templateId = static_cast<int32_t>(in.templateId);
|
|
message.mapId = static_cast<uint16_t>(in.mapId);
|
|
message.vendorMapId = static_cast<uint16_t>(in.vendorMapId);
|
|
message.cloneId = in.cloneId;
|
|
message.name = GeneralUtils::UTF8ToUTF16(in.name);
|
|
message.description = GeneralUtils::UTF8ToUTF16(in.description);
|
|
message.ownerName = GeneralUtils::UTF8ToUTF16(in.ownerName);
|
|
message.ownerId = in.ownerId;
|
|
message.propertyType = 0;
|
|
message.zoneCode = 0;
|
|
message.rent = 0;
|
|
message.rentalPeriod = 1;
|
|
message.expirationDate = in.lastUpdatedTime;
|
|
message.rentAmount = 1;
|
|
message.reputation = in.reputation;
|
|
message.spawnName = GeneralUtils::ASCIIToUTF16(in.spawnName);
|
|
message.rentDuration = 0;
|
|
message.votes = 1;
|
|
message.durationType = 1;
|
|
message.renew = static_cast<uint8_t>(in.privacy);
|
|
message.ownerAccountID = 0;
|
|
if (in.rejectionReason != "") message.moderationStatus = GameMessages::DownloadPropertyData::REJECTION_STATUS_REJECTED;
|
|
else if (in.moderatorRequested == true && in.rejectionReason == "") message.moderationStatus = GameMessages::DownloadPropertyData::REJECTION_STATUS_APPROVED;
|
|
else message.moderationStatus = GameMessages::DownloadPropertyData::REJECTION_STATUS_PENDING;
|
|
message.lastLogoutTime = 0;
|
|
message.dayOfMonthPlaqueWasBought = 1;
|
|
message.repAchievementReq = 1;
|
|
message.zonePosition = { 548.0f, 406.0f, 178.0f };
|
|
message.maxBuildHeight = 128.0f;
|
|
message.rentalDate = in.claimed;
|
|
message.accessType = static_cast<uint8_t>(in.privacy);
|
|
message.pathPositions = in.paths;
|
|
return message;
|
|
}
|
|
|
|
LegacyGameMessages::PropertyDataMessage BuildLegacy(const PropertyDataInput& in) {
|
|
LegacyGameMessages::PropertyDataMessage message(in.mapId);
|
|
message.TemplateID = in.templateId;
|
|
message.ZoneId = static_cast<uint16_t>(in.mapId);
|
|
message.VendorMapId = static_cast<uint16_t>(in.vendorMapId);
|
|
message.SpawnName = in.spawnName;
|
|
message.moderatorRequested = in.moderatorRequested;
|
|
message.reputation = in.reputation;
|
|
message.LastUpdatedTime = in.lastUpdatedTime;
|
|
message.OwnerId = in.ownerId;
|
|
message.OwnerName = in.ownerName;
|
|
message.Name = in.name;
|
|
message.Description = in.description;
|
|
message.ClaimedTime = in.claimed;
|
|
message.PrivacyOption = in.privacy;
|
|
message.cloneId = in.cloneId;
|
|
message.rejectionReason = in.rejectionReason;
|
|
message.Paths = in.paths;
|
|
return message;
|
|
}
|
|
|
|
std::vector<GameMessages::PropertySelectQuery::PropertyInfo> ToInfos(const std::vector<LegacyGameMessages::PropertySelectQueryProperty>& legacy) {
|
|
std::vector<GameMessages::PropertySelectQuery::PropertyInfo> out;
|
|
for (const auto& entry : legacy) {
|
|
auto& info = out.emplace_back();
|
|
info.cloneId = entry.CloneId;
|
|
info.ownerName = GeneralUtils::UTF8ToUTF16(entry.OwnerName);
|
|
info.name = GeneralUtils::UTF8ToUTF16(entry.Name);
|
|
info.description = GeneralUtils::UTF8ToUTF16(entry.Description);
|
|
info.reputation = entry.Reputation;
|
|
info.isBff = entry.IsBestFriend;
|
|
info.isFriend = entry.IsFriend;
|
|
info.isModApproved = entry.IsModeratorApproved;
|
|
info.isAlt = entry.IsAlt;
|
|
info.isOwned = entry.IsOwned;
|
|
info.accessType = entry.AccessType;
|
|
info.dateLastPublished = entry.DateLastPublished;
|
|
info.performanceCost = entry.PerformanceCost;
|
|
}
|
|
return out;
|
|
}
|
|
}
|
|
|
|
class PropertyMessagesTests : public GameDependenciesTest {
|
|
protected:
|
|
void SetUp() override { SetUpDependencies(); }
|
|
void TearDown() override { TearDownDependencies(); }
|
|
};
|
|
|
|
TEST_F(PropertyMessagesTests, NoPayloadMessagesMatchLegacy) {
|
|
for (const auto target : g_Targets) {
|
|
GameMessages::OpenPropertyVendor vendor;
|
|
vendor.target = target;
|
|
ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendOpenPropertyVendor(target, a); }, vendor);
|
|
EXPECT_EQ(Payload(vendor).bits, 0);
|
|
|
|
GameMessages::PropertyEntranceBegin begin;
|
|
begin.target = target;
|
|
ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendPropertyEntranceBegin(target, a); }, begin);
|
|
EXPECT_EQ(Payload(begin).bits, 0);
|
|
}
|
|
|
|
// The legacy SendOpenPropertyManagment took its target from PropertyManagementComponent::Instance(), which needs a
|
|
// property world, so it is checked against hand written bytes: header, target, message id and nothing else.
|
|
GameMessages::OpenPropertyManagement management;
|
|
management.target = 0x0102030405060708LL;
|
|
EXPECT_PACKET_EQ(FromHex("53 05 00 0c 00 00 00 00 08 07 06 05 04 03 02 01 5c 03"), StructPacket(management));
|
|
}
|
|
|
|
TEST_F(PropertyMessagesTests, DownloadPropertyDataMatchesLegacy) {
|
|
std::vector<PropertyDataInput> inputs;
|
|
for (const auto& text : g_Utf8) {
|
|
PropertyDataInput in;
|
|
in.templateId = 25166;
|
|
in.mapId = 1150;
|
|
in.vendorMapId = 1100;
|
|
in.spawnName = "AGSmallProperty";
|
|
in.name = text;
|
|
in.description = text + "!";
|
|
in.ownerName = text.substr(0, 10);
|
|
inputs.push_back(in);
|
|
}
|
|
PropertyDataInput full;
|
|
full.templateId = 0xFFFFFFFF;
|
|
full.mapId = 0x12345;
|
|
full.vendorMapId = 0xFFFF;
|
|
full.spawnName = "NSSmallProperty";
|
|
full.moderatorRequested = true;
|
|
full.reputation = 0xFFFFFFFF;
|
|
full.lastUpdatedTime = 1727000000;
|
|
full.ownerId = 0x1000000000000456LL;
|
|
full.ownerName = "Owner";
|
|
full.name = "Name";
|
|
full.description = "Desc";
|
|
full.claimed = 0x0102030405060708ULL;
|
|
full.privacy = 2;
|
|
full.cloneId = 1234567;
|
|
full.paths = { NiPoint3(1, 2, 3), NiPoint3(-4, 5.5f, 6), NiPoint3Constant::ZERO };
|
|
inputs.push_back(full);
|
|
full.rejectionReason = "no";
|
|
inputs.push_back(full);
|
|
full.rejectionReason = "";
|
|
full.moderatorRequested = false;
|
|
full.privacy = static_cast<char>(-1);
|
|
inputs.push_back(full);
|
|
|
|
for (const auto target : g_Targets) {
|
|
for (const auto& in : inputs) {
|
|
const auto msg = Build(in, target);
|
|
const auto legacy = BuildLegacy(in);
|
|
ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendDownloadPropertyData(target, legacy, a); }, msg);
|
|
const auto copy = RoundTrip(msg);
|
|
EXPECT_EQ(copy.name, msg.name);
|
|
EXPECT_EQ(copy.moderationStatus, msg.moderationStatus);
|
|
EXPECT_EQ(copy.pathPositions, msg.pathPositions);
|
|
EXPECT_EQ(copy.rentalDate, msg.rentalDate);
|
|
ExpectTruncatedFails(msg);
|
|
}
|
|
}
|
|
}
|
|
|
|
// A packet from a 2011/2012 live capture: DownloadPropertyData (716) for an unclaimed Nimbus Station medium
|
|
// property (no owner, so no player data). Every complete 716 packet in the captures decodes with this layout.
|
|
TEST_F(PropertyMessagesTests, DownloadPropertyDataMatchesLiveCapture) {
|
|
const auto msg = FromLiveCapture<GameMessages::DownloadPropertyData>(
|
|
"5305000c000000009847000000400004cc02000000000000000065620000e304b004000000000000000000000000000000000000"
|
|
"0000000000000000000000000000e8030000010000000000000000000000e803000000000000000000000d0000004e0053004d00"
|
|
"65006400500072006f007000650072007400790000000000000000000600000000000000010000000000000000000000000000"
|
|
"000000000000000000000000000000000000000000000080b4c3000090430000e0420000004300000000000000000004000000"
|
|
"cdcc8a42cd8ce343cd4c1f4300008ac2cd8ce343cd4c1f4300008ac2cd8ce343cdcc0242cdcc8a42cd8ce343cdcc0242");
|
|
EXPECT_EQ(msg.target, 0x0400400000004798LL);
|
|
EXPECT_EQ(msg.propertyId, LWOOBJID_EMPTY);
|
|
EXPECT_EQ(msg.templateId, 25189);
|
|
EXPECT_EQ(msg.mapId, 1251);
|
|
EXPECT_EQ(msg.vendorMapId, 1200);
|
|
EXPECT_EQ(msg.ownerId, LWOOBJID_EMPTY);
|
|
EXPECT_EQ(msg.rent, 1000u);
|
|
EXPECT_EQ(msg.rentalPeriod, 1u);
|
|
EXPECT_EQ(msg.rentAmount, 1000u);
|
|
EXPECT_EQ(msg.spawnName, u"NSMedProperty");
|
|
EXPECT_EQ(msg.rentDuration, 6u);
|
|
EXPECT_EQ(msg.durationType, 1u);
|
|
EXPECT_EQ(msg.zonePosition, NiPoint3(-361.0f, 288.0f, 112.0f));
|
|
EXPECT_EQ(msg.maxBuildHeight, 128.0f);
|
|
EXPECT_EQ(msg.pathPositions.size(), 4u);
|
|
}
|
|
|
|
TEST_F(PropertyMessagesTests, PropertyRentalResponseMatchesLegacy) {
|
|
for (const auto target : g_Targets) {
|
|
for (const auto value : g_Ints) {
|
|
GameMessages::PropertyRentalResponse msg;
|
|
msg.target = target;
|
|
msg.cloneid = static_cast<LWOCLONEID>(value);
|
|
msg.code = static_cast<uint32_t>(value / 3);
|
|
msg.propertyID = target;
|
|
msg.rentdue = static_cast<int64_t>(value) * 1000;
|
|
ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendPropertyRentalResponse(target, msg.cloneid, msg.code, msg.propertyID, msg.rentdue, a); }, msg);
|
|
const auto copy = RoundTrip(msg);
|
|
EXPECT_EQ(copy.cloneid, msg.cloneid);
|
|
EXPECT_EQ(copy.rentdue, msg.rentdue);
|
|
ExpectTruncatedFails(msg);
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_F(PropertyMessagesTests, PropertySelectQueryMatchesLegacy) {
|
|
std::vector<std::vector<LegacyGameMessages::PropertySelectQueryProperty>> pages = { {} };
|
|
std::vector<LegacyGameMessages::PropertySelectQueryProperty> page;
|
|
for (size_t i = 0; i < g_Utf8.size(); i++) {
|
|
LegacyGameMessages::PropertySelectQueryProperty entry;
|
|
entry.CloneId = static_cast<LWOCLONEID>(i * 1000);
|
|
entry.OwnerName = g_Utf8[i];
|
|
entry.Name = g_Utf8[(i + 1) % g_Utf8.size()];
|
|
entry.Description = g_Utf8[(i + 2) % g_Utf8.size()];
|
|
entry.Reputation = static_cast<float>(i) * 12.5f;
|
|
entry.IsBestFriend = i & 1;
|
|
entry.IsFriend = i & 2;
|
|
entry.IsModeratorApproved = i & 1;
|
|
entry.IsAlt = i & 2;
|
|
entry.IsOwned = i == 0;
|
|
entry.AccessType = static_cast<uint32_t>(i);
|
|
entry.DateLastPublished = 1727000000ULL + i;
|
|
entry.PerformanceCost = 0.25f * static_cast<float>(i);
|
|
page.push_back(entry);
|
|
}
|
|
page.emplace_back(); // defaults (cloneId LWOCLONEID_INVALID)
|
|
pages.push_back(page);
|
|
|
|
for (const auto target : g_Targets) {
|
|
for (const auto& entries : pages) {
|
|
for (const auto navOffset : { 0, 7, -1 }) {
|
|
for (const bool flag : { false, true }) {
|
|
GameMessages::PropertySelectQuery msg;
|
|
msg.target = target;
|
|
msg.navOffset = navOffset;
|
|
msg.thereAreMore = flag;
|
|
msg.cloneId = navOffset * 11;
|
|
msg.hasFeaturedProperty = !flag;
|
|
msg.wasFriends = flag;
|
|
msg.properties = ToInfos(entries);
|
|
ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendPropertySelectQuery(target, navOffset, flag, msg.cloneId, !flag, flag, entries, a); }, msg);
|
|
const auto copy = RoundTrip(msg);
|
|
ASSERT_EQ(copy.properties.size(), msg.properties.size());
|
|
for (size_t i = 0; i < copy.properties.size(); i++) {
|
|
EXPECT_EQ(copy.properties[i].name, msg.properties[i].name);
|
|
EXPECT_EQ(copy.properties[i].cloneId, msg.properties[i].cloneId);
|
|
EXPECT_EQ(copy.properties[i].isAlt, msg.properties[i].isAlt);
|
|
}
|
|
ExpectTruncatedFails(msg);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_F(PropertyMessagesTests, GetModelsOnPropertyMatchesLegacy) {
|
|
const std::vector<std::map<LWOOBJID, LWOOBJID>> modelSets = { {}, { { 5, 6 } }, { { 0x1000000000000001LL, 1 }, { 2, 0x0102030405060708LL }, { -1, 0 } } };
|
|
for (const auto target : g_Targets) {
|
|
for (const auto& models : modelSets) {
|
|
GameMessages::GetModelsOnProperty msg;
|
|
msg.target = target;
|
|
msg.models = { models.begin(), models.end() };
|
|
ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendGetModelsOnProperty(target, models, a); }, msg);
|
|
EXPECT_EQ(RoundTrip(msg).models, msg.models);
|
|
ExpectTruncatedFails(msg);
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_F(PropertyMessagesTests, PlaceModelResponseMatchesLegacy) {
|
|
for (const auto target : g_Targets) {
|
|
for (const auto& position : g_Points) {
|
|
for (const auto plaque : g_Targets) {
|
|
for (const auto response : { 0, 14, 16, -1 }) {
|
|
for (const auto& rotation : g_Rotations) {
|
|
GameMessages::PlaceModelResponse msg;
|
|
msg.target = target;
|
|
msg.position = position;
|
|
msg.propertyPlaqueID = plaque;
|
|
msg.response = response;
|
|
msg.rotation = rotation;
|
|
// WIRE FIX: the legacy bytes wrote response where the rotation belongs; they only still match when
|
|
// the rotation is the identity (its flag is 0 and nothing follows).
|
|
if (rotation == QuatUtils::IDENTITY) {
|
|
ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendPlaceModelResponse(target, a, position, plaque, response, rotation); }, msg);
|
|
}
|
|
const auto copy = RoundTrip(msg);
|
|
EXPECT_EQ(copy.position, position);
|
|
EXPECT_EQ(copy.propertyPlaqueID, plaque);
|
|
EXPECT_EQ(copy.response, response);
|
|
EXPECT_EQ(copy.rotation, rotation);
|
|
ExpectTruncatedFails(msg);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// WIRE FIX golden bytes: a placed model turned 90 degrees, as a live server answered it. The rotation goes on the wire
|
|
// as w, x, y, z after its flag (0x00dc0170), not the 4-byte response DLU used to repeat there.
|
|
TEST_F(PropertyMessagesTests, PlaceModelResponseWritesRotation) {
|
|
GameMessages::PlaceModelResponse msg;
|
|
msg.response = 14;
|
|
msg.rotation = NiQuaternion(0.5f, 0.5f, 0.5f, 0.5f); // glm: w, x, y, z
|
|
// flags 0 (position), 0 (plaque), 1 + 0e 00 00 00, 1 + four 0.5f (00 00 00 3f)
|
|
EXPECT_PACKET_EQ(FromHex("21 c0 00 00 10 00 00 03 f0 00 00 03 f0 00 00 03 f0 00 00 03 f0", 164), Payload(msg));
|
|
}
|
|
|
|
TEST_F(PropertyMessagesTests, UgcEquipMessagesMatchLegacy) {
|
|
for (const auto target : g_Targets) {
|
|
for (const auto id : g_Targets) {
|
|
for (const auto value : g_Ints) {
|
|
GameMessages::HandleUGCEquipPreCreateBasedOnEditMode pre;
|
|
pre.target = target;
|
|
pre.modelCount = value;
|
|
pre.modelID = id;
|
|
ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendUGCEquipPreCreateBasedOnEditMode(target, a, value, id); }, pre);
|
|
EXPECT_EQ(RoundTrip(pre).modelCount, value);
|
|
|
|
GameMessages::HandleUGCEquipPostDeleteBasedOnEditMode post;
|
|
post.target = target;
|
|
post.invItem = id;
|
|
post.itemsTotal = value;
|
|
ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendUGCEquipPostDeleteBasedOnEditMode(target, a, id, value); }, post);
|
|
const auto copy = RoundTrip(post);
|
|
EXPECT_EQ(copy.invItem, id);
|
|
EXPECT_EQ(copy.itemsTotal, value);
|
|
ExpectTruncatedFails(post);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_F(PropertyMessagesTests, HotPropertiesMatchLegacy) {
|
|
GameMessages::NewsSendHotPropertiesInfoToClient msg;
|
|
for (int i = 0; i < 3; i++) {
|
|
RakNet::BitStream legacy;
|
|
LegacyGameMessages::SerializeHotProperties(legacy, msg.properties);
|
|
EXPECT_PACKET_EQ(FromBitStream(legacy), Payload(msg));
|
|
const auto copy = RoundTrip(msg);
|
|
EXPECT_EQ(copy.properties.size(), msg.properties.size());
|
|
|
|
auto& info = msg.properties.emplace_back();
|
|
info.propertyId = 0x1000000000000123LL + i;
|
|
info.ownerId = 0x1000000000000456LL;
|
|
info.ownerName = GeneralUtils::UTF8ToUTF16(g_Utf8[i + 1]);
|
|
info.reputation = 12345ULL << (i * 10);
|
|
info.templateId = 25166 + i;
|
|
info.name = GeneralUtils::UTF8ToUTF16(g_Utf8[i]);
|
|
info.description = u"Desc";
|
|
info.performanceCost = 0.5f * static_cast<float>(i);
|
|
info.lastPublished = 1727000000ULL;
|
|
info.cloneId = static_cast<uint32_t>(i);
|
|
}
|
|
ExpectTruncatedFails(msg);
|
|
}
|
|
|
|
TEST_F(PropertyMessagesTests, InboundReadsLikeLegacy) {
|
|
for (const uint8_t access : { 0, 1, 2, 255 }) {
|
|
for (const auto renew : g_Ints) {
|
|
GameMessages::SetPropertyAccess msg;
|
|
msg.accessType = access;
|
|
msg.renew = renew;
|
|
const auto legacy = ReadWithLegacy<LegacyGameMessages::LegacySetPropertyAccess>(msg, LegacyGameMessages::ReadSetPropertyAccess);
|
|
const auto copy = RoundTrip(msg);
|
|
EXPECT_EQ(copy.accessType, legacy.accessType);
|
|
EXPECT_EQ(copy.renew, legacy.renew);
|
|
ExpectTruncatedFails(msg);
|
|
}
|
|
}
|
|
|
|
for (const auto id : g_Targets) {
|
|
for (const auto& text : g_Utf8) {
|
|
for (const bool isProperty : { false, true }) {
|
|
GameMessages::UpdatePropertyOrModelForFilterCheck msg;
|
|
msg.isProperty = isProperty;
|
|
msg.ugcId = id;
|
|
msg.playerId = ~id;
|
|
msg.worldId = id / 3;
|
|
msg.newDescription = GeneralUtils::UTF8ToUTF16(text);
|
|
msg.newName = GeneralUtils::UTF8ToUTF16(text.substr(0, 5));
|
|
const auto legacy = ReadWithLegacy<LegacyGameMessages::LegacyFilterCheck>(msg, LegacyGameMessages::ReadUpdatePropertyOrModelForFilterCheck);
|
|
const auto copy = RoundTrip(msg);
|
|
EXPECT_FALSE(legacy.rejected);
|
|
EXPECT_EQ(copy.isProperty, legacy.isProperty);
|
|
EXPECT_EQ(copy.ugcId, legacy.objectId);
|
|
EXPECT_EQ(copy.playerId, legacy.playerId);
|
|
EXPECT_EQ(copy.worldId, legacy.worldId);
|
|
EXPECT_EQ(copy.newName, legacy.name);
|
|
EXPECT_EQ(copy.newDescription, legacy.description);
|
|
ExpectTruncatedFails(msg);
|
|
}
|
|
|
|
for (const auto reason : { 0, 1, 2, -1 }) {
|
|
GameMessages::DeleteModelFromClient del;
|
|
del.modelID = id;
|
|
del.reason = reason;
|
|
const auto legacy = ReadWithLegacy<LegacyGameMessages::LegacyDeletePropertyModel>(del, LegacyGameMessages::ReadDeletePropertyModel);
|
|
const auto copy = RoundTrip(del);
|
|
EXPECT_EQ(copy.modelID, legacy.model);
|
|
EXPECT_EQ(copy.reason, legacy.deleteReason);
|
|
ExpectTruncatedFails(del);
|
|
}
|
|
|
|
GameMessages::ReportOffensiveModel model;
|
|
model.description = GeneralUtils::UTF8ToUTF16(text);
|
|
model.offensiveObjectID = id;
|
|
GameMessages::ReportOffensiveProperty property;
|
|
property.description = model.description;
|
|
property.propertyPlaqueObjectID = id;
|
|
for (const GameMessages::NetGameMsg* report : std::initializer_list<const GameMessages::NetGameMsg*>{ &model, &property }) {
|
|
std::string description;
|
|
LWOOBJID objectId{};
|
|
const bool read = ReadWithLegacy<bool>(*report, [&](RakNet::BitStream& bs) { return LegacyGameMessages::ReadDescriptionAndObject(bs, description, objectId); });
|
|
EXPECT_TRUE(read);
|
|
EXPECT_EQ(description, GeneralUtils::UTF16ToWTF8(model.description));
|
|
EXPECT_EQ(objectId, id);
|
|
}
|
|
EXPECT_EQ(RoundTrip(model).description, model.description);
|
|
EXPECT_EQ(RoundTrip(property).propertyPlaqueObjectID, id);
|
|
ExpectTruncatedFails(model);
|
|
ExpectTruncatedFails(property);
|
|
}
|
|
|
|
GameMessages::PlacePropertyModel place;
|
|
place.modelID = id;
|
|
EXPECT_EQ(ReadWithLegacy<LWOOBJID>(place, LegacyGameMessages::ReadPlacePropertyModel), id);
|
|
EXPECT_EQ(RoundTrip(place).modelID, id);
|
|
ExpectTruncatedFails(place);
|
|
|
|
for (const auto& position : g_Points) {
|
|
for (const auto& rotation : g_Rotations) {
|
|
GameMessages::UpdateModelFromClient update;
|
|
update.modelID = id;
|
|
update.position = position;
|
|
update.rotation = rotation;
|
|
const auto legacy = ReadWithLegacy<LegacyGameMessages::LegacyUpdatePropertyModel>(update, LegacyGameMessages::ReadUpdatePropertyModel);
|
|
const auto copy = RoundTrip(update);
|
|
EXPECT_EQ(copy.modelID, legacy.model);
|
|
EXPECT_EQ(copy.position, legacy.position);
|
|
EXPECT_EQ(copy.rotation, legacy.rotation);
|
|
ExpectTruncatedFails(update);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const auto value : g_Ints) {
|
|
for (const bool flag : { false, true }) {
|
|
GameMessages::PropertyEntranceSync sync;
|
|
sync.includeNullAddress = flag;
|
|
sync.includeNullDescription = !flag;
|
|
sync.playersOwn = flag;
|
|
sync.updateUI = !flag;
|
|
sync.numResults = value;
|
|
sync.reputationTime = -value;
|
|
sync.sortMethod = value / 2;
|
|
sync.startIndex = value / 3;
|
|
sync.filterText = flag ? "search" : "";
|
|
const auto legacy = ReadWithLegacy<LegacyGameMessages::LegacyPropertyEntranceSync>(sync, LegacyGameMessages::ReadPropertyEntranceSync);
|
|
const auto copy = RoundTrip(sync);
|
|
EXPECT_FALSE(legacy.rejected);
|
|
EXPECT_EQ(copy.includeNullAddress, legacy.includeNullAddress);
|
|
EXPECT_EQ(copy.includeNullDescription, legacy.includeNullDescription);
|
|
EXPECT_EQ(copy.playersOwn, legacy.playerOwn);
|
|
EXPECT_EQ(copy.updateUI, legacy.updateUi);
|
|
EXPECT_EQ(copy.numResults, legacy.numResults);
|
|
EXPECT_EQ(copy.reputationTime, legacy.reputation);
|
|
EXPECT_EQ(copy.sortMethod, legacy.sortMethod);
|
|
EXPECT_EQ(copy.startIndex, legacy.startIndex);
|
|
EXPECT_EQ(copy.filterText, legacy.filterText);
|
|
ExpectTruncatedFails(sync);
|
|
|
|
GameMessages::EnterProperty1 enter;
|
|
enter.index = value;
|
|
enter.returnToZone = flag;
|
|
const auto legacyEnter = ReadWithLegacy<LegacyGameMessages::LegacyEnterProperty>(enter, LegacyGameMessages::ReadEnterProperty);
|
|
const auto enterCopy = RoundTrip(enter);
|
|
EXPECT_EQ(static_cast<uint32_t>(enterCopy.index), legacyEnter.index);
|
|
EXPECT_EQ(enterCopy.returnToZone, legacyEnter.returnToZone);
|
|
ExpectTruncatedFails(enter);
|
|
}
|
|
}
|
|
|
|
for (const float cost : { 0.0f, 1.5f, -2.25f, 1e9f }) {
|
|
GameMessages::UpdatePropertyPerformanceCost msg;
|
|
msg.performanceCost = cost;
|
|
EXPECT_EQ(ReadWithLegacy<float>(msg, LegacyGameMessages::ReadUpdatePropertyPerformanceCost), cost);
|
|
EXPECT_EQ(RoundTrip(msg).performanceCost, cost);
|
|
ExpectTruncatedFails(msg);
|
|
}
|
|
}
|
|
|
|
// Messages whose payload DLU used to ignore: layouts from the client's Deserialize functions.
|
|
TEST_F(PropertyMessagesTests, ClientLayoutsRoundTrip) {
|
|
GameMessages::PropertyEditorBegin begin;
|
|
EXPECT_PACKET_EQ(FromHex("00", 4), Payload(begin));
|
|
begin.distanceType = 2;
|
|
begin.propertyObjectID = 0x11;
|
|
begin.startMode = 3;
|
|
begin.startPaused = true;
|
|
const auto beginCopy = RoundTrip(begin);
|
|
EXPECT_EQ(beginCopy.distanceType, 2);
|
|
EXPECT_EQ(beginCopy.propertyObjectID, 0x11);
|
|
EXPECT_EQ(beginCopy.startMode, 3);
|
|
EXPECT_TRUE(beginCopy.startPaused);
|
|
ExpectTruncatedFails(begin);
|
|
|
|
for (const bool queryDB : { false, true }) {
|
|
GameMessages::PropertyContentsFromClient contents;
|
|
contents.queryDB = queryDB;
|
|
EXPECT_EQ(RoundTrip(contents).queryDB, queryDB);
|
|
ExpectTruncatedFails(contents);
|
|
}
|
|
|
|
for (const auto id : g_Targets) {
|
|
GameMessages::ZonePropertyModelEquipped equipped;
|
|
equipped.playerID = id;
|
|
equipped.propertyID = ~id;
|
|
const auto equippedCopy = RoundTrip(equipped);
|
|
EXPECT_EQ(equippedCopy.playerID, id);
|
|
EXPECT_EQ(equippedCopy.propertyID, ~id);
|
|
ExpectTruncatedFails(equipped);
|
|
|
|
GameMessages::ZonePropertyModelRotated rotated;
|
|
rotated.playerID = ~id;
|
|
rotated.propertyID = id;
|
|
const auto rotatedCopy = RoundTrip(rotated);
|
|
EXPECT_EQ(rotatedCopy.playerID, ~id);
|
|
EXPECT_EQ(rotatedCopy.propertyID, id);
|
|
ExpectTruncatedFails(rotated);
|
|
}
|
|
}
|
|
|
|
TEST_F(PropertyMessagesTests, ControlBehaviorsRoundTrips) {
|
|
GameMessages::ControlBehaviors msg;
|
|
msg.args = std::make_unique<AMFArrayValue>();
|
|
// One key: the associative part is unordered, so more keys may come back in another order.
|
|
msg.args->Insert("BehaviorID", "10446");
|
|
msg.command = "rename";
|
|
RakNet::BitStream bitStream;
|
|
msg.Serialize(bitStream);
|
|
GameMessages::ControlBehaviors copy;
|
|
ASSERT_TRUE(copy.Deserialize(bitStream));
|
|
EXPECT_EQ(bitStream.GetNumberOfUnreadBits(), 0);
|
|
EXPECT_EQ(copy.command, "rename");
|
|
ASSERT_NE(copy.args, nullptr);
|
|
ASSERT_NE(copy.args->Get<std::string>("BehaviorID"), nullptr);
|
|
EXPECT_EQ(copy.args->Get<std::string>("BehaviorID")->GetValue(), "10446");
|
|
RakNet::BitStream again;
|
|
copy.Serialize(again);
|
|
EXPECT_PACKET_EQ(FromBitStream(bitStream), FromBitStream(again));
|
|
|
|
// Not an AMF array: dropped.
|
|
RakNet::BitStream notArray;
|
|
notArray.Write<uint8_t>(0x04); // integer marker
|
|
notArray.Write<uint8_t>(0x01);
|
|
GameMessages::ControlBehaviors bad;
|
|
EXPECT_FALSE(bad.Deserialize(notArray));
|
|
}
|
|
|
|
// Independent of the legacy code: hand computed payloads (RakNet writes MSB first).
|
|
TEST_F(PropertyMessagesTests, GoldenBytes) {
|
|
GameMessages::HandleUGCEquipPostDeleteBasedOnEditMode post;
|
|
post.invItem = 0x22;
|
|
EXPECT_PACKET_EQ(FromHex("22 00 00 00 00 00 00 00 00", 65), Payload(post));
|
|
|
|
GameMessages::PlaceModelResponse response;
|
|
response.response = 16;
|
|
// three flags 0, 0, 1, then 10 00 00 00, then the rotation flag 0
|
|
EXPECT_PACKET_EQ(FromHex("22 00 00 00 00", 36), Payload(response));
|
|
|
|
GameMessages::SetPropertyAccess access;
|
|
access.accessType = 2;
|
|
// 1, 02, 0
|
|
EXPECT_PACKET_EQ(FromHex("81 00", 10), Payload(access));
|
|
|
|
GameMessages::GetModelsOnProperty models;
|
|
models.models = { { 1, 2 } };
|
|
EXPECT_PACKET_EQ(FromHex("01 00 00 00 01 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00"), Payload(models));
|
|
|
|
GameMessages::EnterProperty1 enter;
|
|
enter.index = -1;
|
|
EXPECT_PACKET_EQ(FromHex("ff ff ff ff 80", 33), Payload(enter));
|
|
}
|
|
|
|
// A live server's RequeryPropertyModels (after a brick by brick save) had no payload
|
|
TEST_F(PropertyMessagesTests, RequeryPropertyModelsHasNoPayload) {
|
|
GameMessages::RequeryPropertyModels requery;
|
|
EXPECT_EQ(Payload(requery).bits, 0u);
|
|
}
|