refactor: property game messages as structs

Converts the property messages to NetGameMsgs in PropertyMessages.{h,cpp}.
Sent: OpenPropertyVendor, OpenPropertyManagement, DownloadPropertyData
(replaces PropertyDataMessage, now with the client's PropertyData field
names), PropertyRentalResponse, PropertyEntranceBegin,
PropertySelectQuery (replaces PropertySelectQueryProperty with the
client's PropertyInfo), GetModelsOnProperty, PlaceModelResponse and
HandleUGCEquipPre/PostDeleteBasedOnEditMode. Received, registered in
GameMessageHandler's map: SetPropertyAccess,
UpdatePropertyOrModelForFilterCheck, QueryPropertyData,
PropertyEditorBegin/End, PropertyContentsFromClient,
ZonePropertyModelEquipped/Rotated, PlacePropertyModel,
UpdateModelFromClient, DeleteModelFromClient, ControlBehaviors,
PropertyEntranceSync, EnterProperty1, UpdatePropertyPerformanceCost,
ReportOffensiveModel/Property and GetHotPropertyData. The news screen's
NewsSendHotPropertiesInfoToClient moves here with the top properties
lookup; the dashboard's player reports now take the decoded text.

Handlers keep their logic and hand the work to PropertyManagementComponent,
PropertyVendorComponent, PropertyEntranceComponent and
MultiZoneEntranceComponent as before. Messages whose payload DLU ignored
(PropertyEditorBegin, PropertyContentsFromClient, ZonePropertyModel*)
now read it with the client's layout. The never-called
SendZonePropertyModelEquipped (which wrote no default flags) is gone.

No wire change and no change in recipients. Verified byte for byte
against a frozen verbatim copy of the old functions, PropertyDataMessage
and PropertySelectQueryProperty over an input grid (to one client and
broadcast), received messages compared with the old handlers' read
sequences with every truncated payload rejected, hand computed golden
bytes, round trips and a mutation check. Layouts confirmed against the
1.10.64 client in Ghidra. Known wire bug kept as is: PlaceModelResponse
writes response where the client reads a rotation quaternion
(PlaceModelResponse::Deserialize 0x00dc0170).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-26 19:12:17 -05:00
parent 795c937bbb
commit 1e4df609b0
21 changed files with 2580 additions and 1140 deletions

View File

@@ -4,6 +4,7 @@ SET(DGAMEMESSAGES_TESTS
"GameMsgSplitTests.cpp"
"MissionMessagesTests.cpp"
"PetMessagesTests.cpp"
"PropertyMessagesTests.cpp"
"RacingMessagesTests.cpp"
"UnSmashTests.cpp"
"LegacyGameMessageTests.cpp"

View File

@@ -1,4 +1,4 @@
#include "GameMessages.h"
#include "PropertyMessages.h"
#include "BitStream.h"
#include <gtest/gtest.h>

View File

@@ -0,0 +1,589 @@
#ifndef PROPERTYMESSAGESLEGACY_H
#define PROPERTYMESSAGESLEGACY_H
// FROZEN ORACLE - DO NOT EDIT.
// Verbatim copies of the hand written code that PropertyMessages.h replaced (dGame/dGameMessages/GameMessages.cpp,
// PropertyDataMessage.{h,cpp} and PropertySelectQueryProperty.{h,cpp}, branched from origin/main 129199e4 plus the
// news screen's hot properties). Only the namespace changed. The Read* functions are the read sequences of the
// replaced GameMessages::Handle* functions (and PlayerReports' report readers).
#include "BitStreamUtils.h"
#include "CDClientManager.h"
#include "CDPropertyTemplateTable.h"
#include "dCommonVars.h"
#include "dServer.h"
#include "Entity.h"
#include "Game.h"
#include "GeneralUtils.h"
#include "Logger.h"
#include "MessageType/Client.h"
#include "MessageType/Game.h"
#include "NiPoint3.h"
#include "NiQuaternion.h"
#include "PropertyManagementComponent.h"
#include "PropertyMessages.h"
#include "ServiceType.h"
#include <map>
#include <string>
#include <vector>
namespace LegacyGameMessages {
class PropertyDataMessage final
{
public:
explicit PropertyDataMessage(uint32_t mapID);
void Serialize(RakNet::BitStream& stream) const;
std::string OwnerName = "";
LWOOBJID OwnerId = LWOOBJID_EMPTY;
// Temporary values
uint32_t TemplateID = 25166;
uint16_t ZoneId = 1150;
uint16_t VendorMapId = 1100;
std::string SpawnName = "AGSmallProperty";
std::string Name = "";
std::string Description = "";
std::string rejectionReason = "";
bool moderatorRequested = 0;
LWOCLONEID cloneId = 0;
uint32_t reputation = 0;
uint64_t ClaimedTime = 0;
uint64_t LastUpdatedTime = 0;
NiPoint3 ZonePosition = { 548.0f, 406.0f, 178.0f };
char PrivacyOption = 0;
float MaxBuildHeight = 128.0f;
std::vector<NiPoint3> Paths = {};
private:
enum RejectionStatus : uint32_t {
REJECTION_STATUS_APPROVED = 0,
REJECTION_STATUS_PENDING = 1,
REJECTION_STATUS_REJECTED = 2
};
};
inline void PropertyDataMessage::Serialize(RakNet::BitStream& stream) const {
stream.Write<int64_t>(0); // - property id
stream.Write<int32_t>(TemplateID); // - template id
stream.Write<uint16_t>(ZoneId); // - map id
stream.Write<uint16_t>(VendorMapId); // - vendor map id
stream.Write<uint32_t>(cloneId); // clone id
const auto& name = GeneralUtils::UTF8ToUTF16(Name);
stream.Write<uint32_t>(name.size());
for (uint32_t i = 0; i < name.size(); ++i) {
stream.Write<uint16_t>(name[i]);
}
const auto& description = GeneralUtils::UTF8ToUTF16(Description);
stream.Write<uint32_t>(description.size());
for (uint32_t i = 0; i < description.size(); ++i) {
stream.Write<uint16_t>(description[i]);
}
const auto& owner = GeneralUtils::UTF8ToUTF16(OwnerName);
stream.Write<uint32_t>(owner.size());
for (uint32_t i = 0; i < owner.size(); ++i) {
stream.Write<uint16_t>(owner[i]);
}
stream.Write<LWOOBJID>(OwnerId);
stream.Write<uint32_t>(0); // - type
stream.Write<uint32_t>(0); // - zone code
stream.Write<uint32_t>(0); // - minimum price
stream.Write<uint32_t>(1); // - rent duration
stream.Write<uint64_t>(LastUpdatedTime); // - timestamp
stream.Write<uint32_t>(1);
stream.Write<uint32_t>(reputation); // Reputation
stream.Write<uint32_t>(0);
const auto& spawn = GeneralUtils::ASCIIToUTF16(SpawnName);
stream.Write<uint32_t>(spawn.size());
for (uint32_t i = 0; i < spawn.size(); ++i) {
stream.Write<uint16_t>(spawn[i]);
}
stream.Write<uint32_t>(0); // String length
stream.Write<uint32_t>(0); // String length
stream.Write<uint32_t>(0); // - duration type
stream.Write<uint32_t>(1);
stream.Write<uint32_t>(1);
stream.Write<char>(PrivacyOption);
stream.Write<uint64_t>(0);
if (rejectionReason != "") stream.Write<uint32_t>(REJECTION_STATUS_REJECTED);
else if (moderatorRequested == true && rejectionReason == "") stream.Write<uint32_t>(REJECTION_STATUS_APPROVED);
else stream.Write<uint32_t>(REJECTION_STATUS_PENDING);
// Does this go here???
// const auto& rejectionReasonConverted = GeneralUtils::UTF8ToUTF16(rejectionReason);
// stream.Write<uint32_t>(rejectionReasonConverted.size());
// for (uint32_t i = 0; i < rejectionReasonConverted.size(); ++i) {
// stream.Write<uint16_t>(rejectionReasonConverted[i]);
// }
stream.Write<uint32_t>(0);
stream.Write<uint64_t>(0);
stream.Write<uint32_t>(1);
stream.Write<uint32_t>(1);
stream.Write<float>(ZonePosition.x);
stream.Write<float>(ZonePosition.y);
stream.Write<float>(ZonePosition.z);
stream.Write<float>(MaxBuildHeight);
stream.Write(ClaimedTime); // - timestamp
stream.Write<char>(PrivacyOption);
stream.Write<uint32_t>(Paths.size());
for (const auto& path : Paths) {
stream.Write(path.x);
stream.Write(path.y);
stream.Write(path.z);
}
}
inline PropertyDataMessage::PropertyDataMessage(uint32_t mapID) {
const auto propertyTemplate = CDClientManager::GetTable<CDPropertyTemplateTable>()->GetByMapID(mapID);
TemplateID = propertyTemplate.id;
ZoneId = propertyTemplate.mapID;
VendorMapId = propertyTemplate.vendorMapID;
SpawnName = propertyTemplate.spawnName;
}
class PropertySelectQueryProperty final {
public:
void Serialize(RakNet::BitStream& stream) const;
void Deserialize(RakNet::BitStream& stream) const;
LWOCLONEID CloneId = LWOCLONEID_INVALID; // The cloneID of the property
std::string OwnerName = ""; // The property owners name
std::string Name = ""; // The property name
std::string Description = ""; // The property description
float Reputation = 0; // The reputation of the property
bool IsBestFriend = false; // Whether or not the property belongs to a best friend
bool IsFriend = false; // Whether or not the property belongs to a friend
bool IsModeratorApproved = false; // Whether or not a moderator has approved this property
bool IsAlt = false; // Whether or not the property is owned by an alt of the account owner
bool IsOwned = false; // Whether or not the property is owned
uint32_t AccessType = 0; // The privacy option of the property
uint64_t DateLastPublished = 0; // The last day the property was published
float PerformanceCost = 0; // The performance cost of the property
};
inline void PropertySelectQueryProperty::Serialize(RakNet::BitStream& stream) const {
stream.Write(CloneId);
const auto& owner = GeneralUtils::UTF8ToUTF16(OwnerName);
stream.Write<uint32_t>(owner.size());
for (uint32_t i = 0; i < owner.size(); ++i) {
stream.Write<uint16_t>(owner[i]);
}
const auto& name = GeneralUtils::UTF8ToUTF16(Name);
stream.Write<uint32_t>(name.size());
for (uint32_t i = 0; i < name.size(); ++i) {
stream.Write<uint16_t>(name[i]);
}
const auto& description = GeneralUtils::UTF8ToUTF16(Description);
stream.Write<uint32_t>(description.size());
for (uint32_t i = 0; i < description.size(); ++i) {
stream.Write<uint16_t>(description[i]);
}
stream.Write(Reputation);
stream.Write(IsBestFriend);
stream.Write(IsFriend);
stream.Write(IsModeratorApproved);
stream.Write(IsAlt);
stream.Write(IsOwned);
stream.Write(AccessType);
stream.Write(DateLastPublished);
stream.Write(PerformanceCost);
}
inline void PropertySelectQueryProperty::Deserialize(RakNet::BitStream& stream) const {
// Do we need this?
// no
}
// GameMessages::NewsSendHotPropertiesInfoToClient::Serialize (was a member function; now takes the entries)
inline void SerializeHotProperties(RakNet::BitStream& stream, const std::vector<GameMessages::NewsSendHotPropertiesInfoToClient::HotPropertyInfo>& properties) {
const auto writeWString = [&stream](const std::u16string& text) {
stream.Write<uint32_t>(text.size());
for (const auto character : text) stream.Write<uint16_t>(character);
};
stream.Write<uint32_t>(properties.size());
for (const auto& info : properties) {
stream.Write(info.propertyId);
stream.Write(info.ownerId);
writeWString(info.ownerName);
stream.Write(info.reputation);
stream.Write(info.templateId);
writeWString(info.name);
writeWString(info.description);
stream.Write(info.performanceCost);
stream.Write(info.lastPublished);
stream.Write(info.cloneId);
}
}
inline void SendOpenPropertyVendor(const LWOOBJID objectId, const SystemAddress& sysAddr) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(objectId);
bitStream.Write(MessageType::Game::OPEN_PROPERTY_VENDOR);
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST;
SEND_PACKET;
}
inline void SendOpenPropertyManagment(const LWOOBJID objectId, const SystemAddress& sysAddr) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(PropertyManagementComponent::Instance()->GetParent()->GetObjectID());
bitStream.Write(MessageType::Game::OPEN_PROPERTY_MANAGEMENT);
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST;
SEND_PACKET;
}
inline void SendDownloadPropertyData(const LWOOBJID objectId, const PropertyDataMessage& data, const SystemAddress& sysAddr) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(objectId);
bitStream.Write(MessageType::Game::DOWNLOAD_PROPERTY_DATA);
data.Serialize(bitStream);
LOG("(%llu) sending property data (%d)", objectId, sysAddr == UNASSIGNED_SYSTEM_ADDRESS);
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST;
SEND_PACKET;
}
inline void SendPropertyRentalResponse(const LWOOBJID objectId, const LWOCLONEID cloneId, const uint32_t code, const LWOOBJID propertyId, const int64_t rentDue, const SystemAddress& sysAddr) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(objectId);
bitStream.Write(MessageType::Game::PROPERTY_RENTAL_RESPONSE);
bitStream.Write(cloneId);
bitStream.Write(code);
bitStream.Write(propertyId);
bitStream.Write(rentDue);
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST;
SEND_PACKET;
}
inline void SendGetModelsOnProperty(LWOOBJID objectId, std::map<LWOOBJID, LWOOBJID> models, const SystemAddress& sysAddr) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(objectId);
bitStream.Write(MessageType::Game::GET_MODELS_ON_PROPERTY);
bitStream.Write<uint32_t>(models.size());
for (const auto& pair : models) {
bitStream.Write(pair.first);
bitStream.Write(pair.second);
}
LOG("Sending property models to (%llu) (%d)", objectId, sysAddr == UNASSIGNED_SYSTEM_ADDRESS);
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST;
SEND_PACKET;
}
inline void SendPlaceModelResponse(LWOOBJID objectId, const SystemAddress& sysAddr, NiPoint3 position,
LWOOBJID plaque, int32_t response, NiQuaternion rotation) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(objectId);
bitStream.Write(MessageType::Game::PLACE_MODEL_RESPONSE);
bitStream.Write(position != NiPoint3Constant::ZERO);
if (position != NiPoint3Constant::ZERO) {
bitStream.Write(position);
}
bitStream.Write(plaque != LWOOBJID_EMPTY);
if (plaque != LWOOBJID_EMPTY) {
bitStream.Write(plaque);
}
bitStream.Write(response != 0);
if (response != 0) {
bitStream.Write(response);
}
bitStream.Write(rotation != QuatUtils::IDENTITY);
if (rotation != QuatUtils::IDENTITY) {
bitStream.Write(response);
}
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST;
SEND_PACKET;
}
inline void SendUGCEquipPreCreateBasedOnEditMode(LWOOBJID objectId, const SystemAddress& sysAddr, int modelCount, LWOOBJID model) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(objectId);
bitStream.Write(MessageType::Game::HANDLE_UGC_POST_CREATE_BASED_ON_EDIT_MODE);
bitStream.Write(modelCount);
bitStream.Write(model);
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST;
SEND_PACKET;
}
inline void SendUGCEquipPostDeleteBasedOnEditMode(LWOOBJID objectId, const SystemAddress& sysAddr, LWOOBJID inventoryItem, int itemTotal) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(objectId);
bitStream.Write(MessageType::Game::HANDLE_UGC_POST_DELETE_BASED_ON_EDIT_MODE);
bitStream.Write(inventoryItem);
bitStream.Write(itemTotal != 0);
if (itemTotal != 0) {
bitStream.Write(itemTotal);
}
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST;
SEND_PACKET;
}
inline void SendPropertyEntranceBegin(LWOOBJID objectId, const SystemAddress& sysAddr) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(objectId);
bitStream.Write(MessageType::Game::PROPERTY_ENTRANCE_BEGIN);
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST;
SEND_PACKET;
}
inline void SendPropertySelectQuery(LWOOBJID objectId, int32_t navOffset, bool thereAreMore, int32_t cloneId, bool hasFeaturedProperty, bool wasFriends, const std::vector<PropertySelectQueryProperty>& entries, const SystemAddress& sysAddr) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(objectId);
bitStream.Write(MessageType::Game::PROPERTY_SELECT_QUERY);
bitStream.Write(navOffset);
bitStream.Write(thereAreMore);
bitStream.Write(cloneId);
bitStream.Write(hasFeaturedProperty);
bitStream.Write(wasFriends);
bitStream.Write<uint32_t>(entries.size());
for (auto& entry : entries) {
entry.Serialize(bitStream);
}
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST;
SEND_PACKET;
}
// GameMessages::HandleSetPropertyAccess
struct LegacySetPropertyAccess { uint8_t accessType{}; int32_t renew{}; };
inline LegacySetPropertyAccess ReadSetPropertyAccess(RakNet::BitStream& inStream) {
uint8_t accessType{};
int32_t renew{};
bool accessTypeIsDefault{};
inStream.Read(accessTypeIsDefault);
if (accessTypeIsDefault != 0) inStream.Read(accessType);
bool renewIsDefault{};
inStream.Read(renewIsDefault);
if (renewIsDefault != 0) inStream.Read(renew);
return { accessType, renew };
}
// GameMessages::HandleUpdatePropertyOrModelForFilterCheck
struct LegacyFilterCheck { bool isProperty{}; LWOOBJID objectId{}; LWOOBJID playerId{}; LWOOBJID worldId{}; std::u16string name; std::u16string description; bool rejected{}; };
inline LegacyFilterCheck ReadUpdatePropertyOrModelForFilterCheck(RakNet::BitStream& inStream) {
bool isProperty{};
LWOOBJID objectId{};
LWOOBJID playerId{};
LWOOBJID worldId{};
uint32_t nameLength{};
std::u16string name{};
uint32_t descriptionLength{};
std::u16string description{};
inStream.Read(isProperty);
inStream.Read(objectId);
inStream.Read(playerId);
inStream.Read(worldId);
inStream.Read(descriptionLength);
if (descriptionLength > MAX_MESSAGE_LENGTH) return { .rejected = true };
for (uint32_t i = 0; i < descriptionLength; ++i) {
uint16_t character;
inStream.Read(character);
description.push_back(character);
}
inStream.Read(nameLength);
if (nameLength > MAX_MESSAGE_LENGTH) return { .rejected = true };
for (uint32_t i = 0; i < nameLength; ++i) {
uint16_t character;
inStream.Read(character);
name.push_back(character);
}
return { isProperty, objectId, playerId, worldId, name, description, false };
}
// GameMessages::HandlePlacePropertyModel
inline LWOOBJID ReadPlacePropertyModel(RakNet::BitStream& inStream) {
LWOOBJID model;
inStream.Read(model);
return model;
}
// GameMessages::HandleUpdatePropertyModel
struct LegacyUpdatePropertyModel { LWOOBJID model{}; NiPoint3 position; NiQuaternion rotation; };
inline LegacyUpdatePropertyModel ReadUpdatePropertyModel(RakNet::BitStream& inStream) {
LWOOBJID model;
NiPoint3 position;
NiQuaternion rotation = QuatUtils::IDENTITY;
inStream.Read(model);
inStream.Read(position);
if (inStream.ReadBit()) {
inStream.Read(rotation);
}
return { model, position, rotation };
}
// GameMessages::HandleDeletePropertyModel
struct LegacyDeletePropertyModel { LWOOBJID model{}; int deleteReason{}; };
inline LegacyDeletePropertyModel ReadDeletePropertyModel(RakNet::BitStream& inStream) {
LWOOBJID model = LWOOBJID_EMPTY;
int deleteReason = 0;
if (inStream.ReadBit()) {
inStream.Read(model);
}
if (inStream.ReadBit()) {
inStream.Read(deleteReason);
}
return { model, deleteReason };
}
// GameMessages::HandlePropertyEntranceSync
struct LegacyPropertyEntranceSync {
bool includeNullAddress{}; bool includeNullDescription{}; bool playerOwn{}; bool updateUi{};
int32_t numResults{}; int32_t reputation{}; int32_t sortMethod{}; int32_t startIndex{}; std::string filterText{}; bool rejected{};
};
inline LegacyPropertyEntranceSync ReadPropertyEntranceSync(RakNet::BitStream& inStream) {
bool includeNullAddress{};
bool includeNullDescription{};
bool playerOwn{};
bool updateUi{};
int32_t numResults{};
int32_t reputation{};
int32_t sortMethod{};
int32_t startIndex{};
uint32_t filterTextLength{};
std::string filterText{};
inStream.Read(includeNullAddress);
inStream.Read(includeNullDescription);
inStream.Read(playerOwn);
inStream.Read(updateUi);
inStream.Read(numResults);
inStream.Read(reputation);
inStream.Read(sortMethod);
inStream.Read(startIndex);
inStream.Read(filterTextLength);
if (filterTextLength > MAX_MESSAGE_LENGTH) return { .rejected = true };
for (auto i = 0u; i < filterTextLength; i++) {
char c;
inStream.Read(c);
filterText.push_back(c);
}
return { includeNullAddress, includeNullDescription, playerOwn, updateUi, numResults, reputation, sortMethod, startIndex, filterText, false };
}
// GameMessages::HandleEnterProperty
struct LegacyEnterProperty { uint32_t index{}; bool returnToZone{}; };
inline LegacyEnterProperty ReadEnterProperty(RakNet::BitStream& inStream) {
uint32_t index{};
bool returnToZone{};
inStream.Read(index);
inStream.Read(returnToZone);
return { index, returnToZone };
}
// GameMessages::HandleUpdatePropertyPerformanceCost
inline float ReadUpdatePropertyPerformanceCost(RakNet::BitStream& inStream) {
float performanceCost = 0.0f;
if (inStream.ReadBit()) inStream.Read(performanceCost);
return performanceCost;
}
// PlayerReports' ReadDescriptionAndObject (ReportOffensiveModel / ReportOffensiveProperty), MAX_BODY = 2000
constexpr uint32_t MAX_BODY = 2000;
// Description (u32 length + UTF-16) followed by an object ID
inline bool ReadDescriptionAndObject(RakNet::BitStream& inStream, std::string& description, LWOOBJID& objectId) {
uint32_t length{};
if (!inStream.Read(length) || length > MAX_BODY) return false;
std::u16string text;
text.reserve(length);
for (uint32_t i = 0; i < length; i++) {
char16_t character{};
if (!inStream.Read(character)) return false;
text.push_back(character);
}
description = GeneralUtils::UTF16ToWTF8(text);
return inStream.Read(objectId);
}
}
#endif // PROPERTYMESSAGESLEGACY_H

View File

@@ -0,0 +1,609 @@
#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);
}
}
}
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;
ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendPlaceModelResponse(target, a, position, plaque, response, rotation); }, msg);
// The kept wire bug (response written where rotation belongs) only round trips with the default rotation.
if (rotation == QuatUtils::IDENTITY) {
const auto copy = RoundTrip(msg);
EXPECT_EQ(copy.position, position);
EXPECT_EQ(copy.propertyPlaqueID, plaque);
EXPECT_EQ(copy.response, response);
ExpectTruncatedFails(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));
}