refactor: pet game messages as structs

Converts the pet taming minigame, naming, command and bouncer messages to
NetGameMsgs in PetMessages.{h,cpp}: NotifyPetTamingMinigame,
NotifyTamingModelLoadedOnServer, NotifyPetTamingPuzzleSelected,
PetTamingTryBuildResult, PetResponse, AddPetToPlayer, RegisterPetID,
RegisterPetDBID, ShowPetActionButton, BouncerActiveStatus, SetPetName,
SetPetNameModerated and PetNameChanged (sent), and PetTamingTryBuild,
NotifyTamingBuildSuccess, RequestSetPetName, StartServerPetMinigameTimer,
ClientExitTamingMinigame, CommandPet and DespawnPet (received, registered
in GameMessageHandler's map; each Handle hands the message to the
player's taming or active PetComponent exactly as the old handler did).
PetComponent, BouncerComponent and the hydrant/catapult scripts build
the structs; the old Send*/Handle* functions and switch cases are gone.
The never-called SendClientExitTamingMinigame is folded into the
ClientExitTamingMinigame struct. MarkInventoryItemAsActive stays with
the inventory messages.

No wire change and no change in recipients. Verified byte for byte
against a frozen verbatim copy of the old functions 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's Serialize/Deserialize in Ghidra.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-26 18:44:25 -05:00
parent f5ca541869
commit 795c937bbb
14 changed files with 1509 additions and 533 deletions

View File

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

View File

@@ -0,0 +1,333 @@
#ifndef PETMESSAGESLEGACY_H
#define PETMESSAGESLEGACY_H
// FROZEN ORACLE - DO NOT EDIT.
// Verbatim copies of the hand written GameMessages functions that PetMessages.h replaced
// (dGame/dGameMessages/GameMessages.cpp, branched from origin/main 129199e4). Only the namespace changed.
// The Read* functions are the read sequences of the replaced GameMessages::Handle* functions.
#include "BitStreamUtils.h"
#include "Brick.h"
#include "dCommonVars.h"
#include "dServer.h"
#include "ePetAbilityType.h"
#include "ePetTamingNotifyType.h"
#include "Game.h"
#include "MessageType/Client.h"
#include "MessageType/Game.h"
#include "NiPoint3.h"
#include "NiQuaternion.h"
#include "ServiceType.h"
#include <string>
#include <vector>
namespace LegacyGameMessages {
inline void SendNotifyPetTamingMinigame(LWOOBJID objectId, LWOOBJID petId, LWOOBJID playerTamingId, bool bForceTeleport, ePetTamingNotifyType notifyType, NiPoint3 petsDestPos, NiPoint3 telePos, NiQuaternion teleRot, const SystemAddress& sysAddr) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(objectId);
bitStream.Write(MessageType::Game::NOTIFY_PET_TAMING_MINIGAME);
bitStream.Write(petId);
bitStream.Write(playerTamingId);
bitStream.Write(bForceTeleport);
bitStream.Write(notifyType);
bitStream.Write(petsDestPos);
bitStream.Write(telePos);
const bool hasDefault = teleRot != QuatUtils::IDENTITY;
bitStream.Write(hasDefault);
if (hasDefault) bitStream.Write(teleRot);
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST;
SEND_PACKET;
}
inline void SendNotifyTamingModelLoadedOnServer(LWOOBJID objectId, const SystemAddress& sysAddr) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(objectId);
bitStream.Write(MessageType::Game::NOTIFY_TAMING_MODEL_LOADED_ON_SERVER);
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST;
SEND_PACKET;
}
inline void SendNotifyPetTamingPuzzleSelected(LWOOBJID objectId, const std::vector<Brick>& bricks, const SystemAddress& sysAddr) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(objectId);
bitStream.Write(MessageType::Game::NOTIFY_TAMING_PUZZLE_SELECTED);
bitStream.Write<uint32_t>(bricks.size());
for (const auto& brick : bricks) {
bitStream.Write(brick.designerID);
bitStream.Write(brick.materialID);
}
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST;
SEND_PACKET;
}
inline void SendPetTamingTryBuildResult(LWOOBJID objectId, bool bSuccess, int32_t iNumCorrect, const SystemAddress& sysAddr) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(objectId);
bitStream.Write(MessageType::Game::PET_TAMING_TRY_BUILD_RESULT);
bitStream.Write(bSuccess);
bitStream.Write(iNumCorrect != 0);
if (iNumCorrect != 0) bitStream.Write(iNumCorrect);
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST;
SEND_PACKET;
}
inline void SendPetResponse(LWOOBJID objectId, LWOOBJID objIDPet, int32_t iPetCommandType, int32_t iResponse, int32_t iTypeID, const SystemAddress& sysAddr) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(objectId);
bitStream.Write(MessageType::Game::PET_RESPONSE);
bitStream.Write(objIDPet);
bitStream.Write(iPetCommandType);
bitStream.Write(iResponse);
bitStream.Write(iTypeID);
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST;
SEND_PACKET;
}
inline void SendAddPetToPlayer(LWOOBJID objectId, int32_t iElementalType, std::u16string name, LWOOBJID petDBID, LOT petLOT, const SystemAddress& sysAddr) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(objectId);
bitStream.Write(MessageType::Game::ADD_PET_TO_PLAYER);
bitStream.Write(iElementalType);
bitStream.Write<uint32_t>(name.size());
for (const auto character : name) {
bitStream.Write(character);
}
bitStream.Write(petDBID);
bitStream.Write(petLOT);
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST;
SEND_PACKET;
}
inline void SendRegisterPetID(LWOOBJID objectId, LWOOBJID objID, const SystemAddress& sysAddr) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(objectId);
bitStream.Write(MessageType::Game::REGISTER_PET_ID);
bitStream.Write(objID);
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST;
SEND_PACKET;
}
inline void SendRegisterPetDBID(LWOOBJID objectId, LWOOBJID petDBID, const SystemAddress& sysAddr) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(objectId);
bitStream.Write(MessageType::Game::REGISTER_PET_DBID);
bitStream.Write(petDBID);
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST;
SEND_PACKET;
}
inline void SendClientExitTamingMinigame(LWOOBJID objectId, bool bVoluntaryExit, const SystemAddress& sysAddr) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(objectId);
bitStream.Write(MessageType::Game::CLIENT_EXIT_TAMING_MINIGAME);
bitStream.Write(bVoluntaryExit);
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST;
SEND_PACKET;
}
inline void SendShowPetActionButton(const LWOOBJID objectId, const ePetAbilityType petAbility, const bool bShow, const SystemAddress& sysAddr) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(objectId);
bitStream.Write(MessageType::Game::SHOW_PET_ACTION_BUTTON);
bitStream.Write(petAbility);
bitStream.Write(bShow);
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST;
SEND_PACKET;
}
inline void SendBouncerActiveStatus(LWOOBJID objectId, bool bActive, const SystemAddress& sysAddr) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(objectId);
bitStream.Write(MessageType::Game::BOUNCER_ACTIVE_STATUS);
bitStream.Write(bActive);
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST;
SEND_PACKET;
}
inline void SendSetPetName(LWOOBJID objectId, std::u16string name, LWOOBJID petDBID, const SystemAddress& sysAddr) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(objectId);
bitStream.Write(MessageType::Game::SET_PET_NAME);
bitStream.Write<uint32_t>(name.size());
for (const auto character : name) {
bitStream.Write(character);
}
bitStream.Write(petDBID != LWOOBJID_EMPTY);
if (petDBID != LWOOBJID_EMPTY) bitStream.Write(petDBID);
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST;
SEND_PACKET;
}
inline void SendSetPetNameModerated(LWOOBJID objectId, LWOOBJID petDBID, int32_t nModerationStatus, const SystemAddress& sysAddr) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(objectId);
bitStream.Write(MessageType::Game::SET_PET_NAME_MODERATED);
bitStream.Write(petDBID != LWOOBJID_EMPTY);
if (petDBID != LWOOBJID_EMPTY) bitStream.Write(petDBID);
bitStream.Write(nModerationStatus);
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST;
SEND_PACKET;
}
inline void SendPetNameChanged(LWOOBJID objectId, int32_t moderationStatus, std::u16string name, std::u16string ownerName, const SystemAddress& sysAddr) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(objectId);
bitStream.Write(MessageType::Game::PET_NAME_CHANGED);
bitStream.Write(moderationStatus);
bitStream.Write<uint32_t>(name.size());
for (const auto character : name) {
bitStream.Write(character);
}
bitStream.Write<uint32_t>(ownerName.size());
for (const auto character : ownerName) {
bitStream.Write(character);
}
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST;
SEND_PACKET;
}
// GameMessages::HandleClientExitTamingMinigame
inline bool ReadClientExitTamingMinigame(RakNet::BitStream& inStream) {
bool bVoluntaryExit = inStream.ReadBit();
return bVoluntaryExit;
}
// GameMessages::HandlePetTamingTryBuild
struct LegacyPetTamingTryBuild { std::vector<Brick> bricks; bool clientFailed{}; bool rejected{}; };
inline LegacyPetTamingTryBuild ReadPetTamingTryBuild(RakNet::BitStream& inStream) {
uint32_t brickCount;
std::vector<Brick> bricks;
bool clientFailed;
inStream.Read(brickCount);
if (brickCount > MAX_MESSAGE_LENGTH) return { {}, false, true }; // Prevent DoS via unbounded brick count
bricks.reserve(brickCount);
for (uint32_t i = 0; i < brickCount; i++) {
Brick brick;
inStream.Read(brick);
bricks.push_back(brick);
}
clientFailed = inStream.ReadBit();
return { bricks, clientFailed, false };
}
// GameMessages::HandleNotifyTamingBuildSuccess
inline NiPoint3 ReadNotifyTamingBuildSuccess(RakNet::BitStream& inStream) {
NiPoint3 position;
inStream.Read(position);
return position;
}
// GameMessages::HandleRequestSetPetName
struct LegacyRequestSetPetName { std::u16string name; bool rejected{}; };
inline LegacyRequestSetPetName ReadRequestSetPetName(RakNet::BitStream& inStream) {
uint32_t nameLength;
std::u16string name;
inStream.Read(nameLength);
if (nameLength > MAX_MESSAGE_LENGTH) return { {}, true };
for (size_t i = 0; i < nameLength; i++) {
char16_t character;
inStream.Read(character);
name.push_back(character);
}
return { name, false };
}
// GameMessages::HandleCommandPet
struct LegacyCommandPet { NiPoint3 genericPosInfo; LWOOBJID objIdSource{}; int32_t iPetCommandType{}; int32_t iTypeID{}; bool overrideObey{}; };
inline LegacyCommandPet ReadCommandPet(RakNet::BitStream& inStream) {
NiPoint3 genericPosInfo;
LWOOBJID objIdSource;
int32_t iPetCommandType;
int32_t iTypeID;
bool overrideObey;
inStream.Read(genericPosInfo);
inStream.Read(objIdSource);
inStream.Read(iPetCommandType);
inStream.Read(iTypeID);
overrideObey = inStream.ReadBit();
return { genericPosInfo, objIdSource, iPetCommandType, iTypeID, overrideObey };
}
// GameMessages::HandleDespawnPet
inline bool ReadDespawnPet(RakNet::BitStream& inStream) {
bool bDeletePet;
bDeletePet = inStream.ReadBit();
return bDeletePet;
}
}
#endif // PETMESSAGESLEGACY_H

View File

@@ -0,0 +1,351 @@
#include "PetMessages.h"
#include "GameDependencies.h"
#include "GameMessageTestUtils.h"
#include "Legacy/PetMessagesLegacy.h"
#include <functional>
#include <limits>
#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() };
const std::vector<std::u16string> g_Strings = { u"", u"Rex", u"Pét ☃ \U0001F436", std::u16string(300, u'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.0f, 0.0f, 0.0f, 0.0f), NiQuaternion(0.5f, 0.5f, -0.5f, 0.5f) };
const std::vector<std::vector<Brick>> g_BrickSets = { {}, { { 3001, 21 } }, { { 1, 2 }, { 0xFFFFFFFF, 0 }, { 3040, 194 } } };
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;
}
bool SameBricks(const std::vector<Brick>& a, const std::vector<Brick>& b) {
if (a.size() != b.size()) return false;
for (size_t i = 0; i < a.size(); i++) {
if (a[i].designerID != b[i].designerID || a[i].materialID != b[i].materialID) return false;
}
return true;
}
}
class PetMessagesTests : public GameDependenciesTest {
protected:
void SetUp() override { SetUpDependencies(); }
void TearDown() override { TearDownDependencies(); }
};
TEST_F(PetMessagesTests, NotifyPetTamingMinigameMatchesLegacy) {
for (const auto target : g_Targets) {
for (const auto id : g_Targets) {
for (const bool force : { false, true }) {
for (const auto type : { ePetTamingNotifyType::SUCCESS, ePetTamingNotifyType::BEGIN, ePetTamingNotifyType::NAMINGPET, static_cast<ePetTamingNotifyType>(0xFFFFFFFF) }) {
for (const auto& point : g_Points) {
for (const auto& rotation : g_Rotations) {
GameMessages::NotifyPetTamingMinigame msg;
msg.target = target;
msg.PetID = id;
msg.PlayerTamingID = target;
msg.bForceTeleport = force;
msg.notifyType = type;
msg.petsDestPos = point;
msg.telePos = NiPoint3(point.z, point.x, point.y);
msg.teleRot = rotation;
ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendNotifyPetTamingMinigame(target, id, target, force, type, point, msg.telePos, rotation, a); }, msg);
const auto copy = RoundTrip(msg);
EXPECT_EQ(copy.PetID, id);
EXPECT_EQ(copy.notifyType, type);
EXPECT_EQ(copy.teleRot, rotation);
ExpectTruncatedFails(msg);
}
}
}
}
}
}
}
TEST_F(PetMessagesTests, NoPayloadMessagesMatchLegacy) {
for (const auto target : g_Targets) {
GameMessages::NotifyTamingModelLoadedOnServer msg;
msg.target = target;
ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendNotifyTamingModelLoadedOnServer(target, a); }, msg);
EXPECT_EQ(Payload(msg).bits, 0);
}
}
TEST_F(PetMessagesTests, BrickMessagesMatchLegacy) {
for (const auto target : g_Targets) {
for (const auto& bricks : g_BrickSets) {
GameMessages::NotifyPetTamingPuzzleSelected msg;
msg.target = target;
msg.bricks = bricks;
ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendNotifyPetTamingPuzzleSelected(target, bricks, a); }, msg);
EXPECT_TRUE(SameBricks(RoundTrip(msg).bricks, bricks));
ExpectTruncatedFails(msg);
for (const bool clientFailed : { false, true }) {
GameMessages::PetTamingTryBuild tryBuild;
tryBuild.bricks = bricks;
tryBuild.clientFailed = clientFailed;
const auto legacy = ReadWithLegacy<LegacyGameMessages::LegacyPetTamingTryBuild>(tryBuild, LegacyGameMessages::ReadPetTamingTryBuild);
const auto copy = RoundTrip(tryBuild);
EXPECT_FALSE(legacy.rejected);
EXPECT_TRUE(SameBricks(copy.bricks, legacy.bricks));
EXPECT_EQ(copy.clientFailed, legacy.clientFailed);
ExpectTruncatedFails(tryBuild);
}
}
}
// A brick count the stream cannot hold is rejected before anything is allocated.
RakNet::BitStream huge;
huge.Write<uint32_t>(MAX_MESSAGE_LENGTH + 1);
GameMessages::PetTamingTryBuild tryBuild;
EXPECT_FALSE(tryBuild.Deserialize(huge));
RakNet::BitStream lying;
lying.Write<uint32_t>(1000);
lying.Write<uint64_t>(0);
EXPECT_FALSE(tryBuild.Deserialize(lying));
}
TEST_F(PetMessagesTests, PetTamingTryBuildResultMatchesLegacy) {
for (const auto target : g_Targets) {
for (const bool success : { false, true }) {
for (const auto numCorrect : g_Ints) {
GameMessages::PetTamingTryBuildResult msg;
msg.target = target;
msg.bSuccess = success;
msg.iNumCorrect = numCorrect;
ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendPetTamingTryBuildResult(target, success, numCorrect, a); }, msg);
const auto copy = RoundTrip(msg);
EXPECT_EQ(copy.bSuccess, success);
EXPECT_EQ(copy.iNumCorrect, numCorrect);
}
}
}
}
TEST_F(PetMessagesTests, PetResponseMatchesLegacy) {
for (const auto target : g_Targets) {
for (const auto value : g_Ints) {
GameMessages::PetResponse msg;
msg.target = target;
msg.ObjIDPet = target ^ 0x55;
msg.iPetCommandType = value;
msg.iResponse = -value;
msg.iTypeID = value / 2;
ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendPetResponse(target, msg.ObjIDPet, msg.iPetCommandType, msg.iResponse, msg.iTypeID, a); }, msg);
const auto copy = RoundTrip(msg);
EXPECT_EQ(copy.ObjIDPet, msg.ObjIDPet);
EXPECT_EQ(copy.iResponse, msg.iResponse);
ExpectTruncatedFails(msg);
}
}
}
TEST_F(PetMessagesTests, AddPetToPlayerMatchesLegacy) {
for (const auto target : g_Targets) {
for (const auto& name : g_Strings) {
for (const LOT lot : { LOT_NULL, 3050, 0 }) {
GameMessages::AddPetToPlayer msg;
msg.target = target;
msg.iElementalType = lot / 7;
msg.name = name;
msg.petDBID = target;
msg.petLOT = lot;
ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendAddPetToPlayer(target, msg.iElementalType, name, target, lot, a); }, msg);
const auto copy = RoundTrip(msg);
EXPECT_EQ(copy.name, name);
EXPECT_EQ(copy.petLOT, lot);
ExpectTruncatedFails(msg);
}
}
}
}
TEST_F(PetMessagesTests, RegisterPetMessagesMatchLegacy) {
for (const auto target : g_Targets) {
for (const auto id : g_Targets) {
GameMessages::RegisterPetID petId;
petId.target = target;
petId.objID = id;
ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendRegisterPetID(target, id, a); }, petId);
EXPECT_EQ(RoundTrip(petId).objID, id);
GameMessages::RegisterPetDBID dbId;
dbId.target = target;
dbId.petDBID = id;
ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendRegisterPetDBID(target, id, a); }, dbId);
EXPECT_EQ(RoundTrip(dbId).petDBID, id);
}
}
}
TEST_F(PetMessagesTests, BoolMessagesMatchLegacy) {
for (const auto target : g_Targets) {
for (const bool value : { false, true }) {
GameMessages::ClientExitTamingMinigame exit;
exit.target = target;
exit.bVoluntaryExit = value;
ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendClientExitTamingMinigame(target, value, a); }, exit);
EXPECT_EQ(ReadWithLegacy<bool>(exit, LegacyGameMessages::ReadClientExitTamingMinigame), value);
EXPECT_EQ(RoundTrip(exit).bVoluntaryExit, value);
ExpectTruncatedFails(exit);
GameMessages::BouncerActiveStatus bouncer;
bouncer.target = target;
bouncer.bActive = value;
ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendBouncerActiveStatus(target, value, a); }, bouncer);
EXPECT_EQ(RoundTrip(bouncer).bActive, value);
GameMessages::DespawnPet despawn;
despawn.bDeletePet = value;
EXPECT_EQ(ReadWithLegacy<bool>(despawn, LegacyGameMessages::ReadDespawnPet), value);
EXPECT_EQ(RoundTrip(despawn).bDeletePet, value);
ExpectTruncatedFails(despawn);
for (const auto ability : { ePetAbilityType::Invalid, ePetAbilityType::DigAtPosition, static_cast<ePetAbilityType>(0xFFFFFFFF) }) {
GameMessages::ShowPetActionButton button;
button.target = target;
button.ButtonLabel = ability;
button.bShow = value;
ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendShowPetActionButton(target, ability, value, a); }, button);
const auto copy = RoundTrip(button);
EXPECT_EQ(copy.ButtonLabel, ability);
EXPECT_EQ(copy.bShow, value);
}
}
}
}
TEST_F(PetMessagesTests, PetNameMessagesMatchLegacy) {
for (const auto target : g_Targets) {
for (const auto& name : g_Strings) {
for (const auto dbId : g_Targets) {
GameMessages::SetPetName setName;
setName.target = target;
setName.name = name;
setName.petDBID = dbId;
ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendSetPetName(target, name, dbId, a); }, setName);
const auto copy = RoundTrip(setName);
EXPECT_EQ(copy.name, name);
EXPECT_EQ(copy.petDBID, dbId);
ExpectTruncatedFails(setName);
}
for (const auto status : g_Ints) {
GameMessages::PetNameChanged changed;
changed.target = target;
changed.moderationStatus = status;
changed.name = name;
changed.ownerName = name + u"'s owner";
ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendPetNameChanged(target, status, name, changed.ownerName, a); }, changed);
const auto copy = RoundTrip(changed);
EXPECT_EQ(copy.name, name);
EXPECT_EQ(copy.ownerName, changed.ownerName);
ExpectTruncatedFails(changed);
}
GameMessages::RequestSetPetName request;
request.name = name;
const auto legacy = ReadWithLegacy<LegacyGameMessages::LegacyRequestSetPetName>(request, LegacyGameMessages::ReadRequestSetPetName);
EXPECT_FALSE(legacy.rejected);
EXPECT_EQ(RoundTrip(request).name, legacy.name);
ExpectTruncatedFails(request);
}
for (const auto dbId : g_Targets) {
for (const auto status : g_Ints) {
GameMessages::SetPetNameModerated moderated;
moderated.target = target;
moderated.PetDBID = dbId;
moderated.nModerationStatus = status;
ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendSetPetNameModerated(target, dbId, status, a); }, moderated);
const auto copy = RoundTrip(moderated);
EXPECT_EQ(copy.PetDBID, dbId);
EXPECT_EQ(copy.nModerationStatus, status);
}
}
}
}
TEST_F(PetMessagesTests, InboundReadsLikeLegacy) {
for (const auto& point : g_Points) {
GameMessages::NotifyTamingBuildSuccess success;
success.buildPosition = point;
EXPECT_EQ(ReadWithLegacy<NiPoint3>(success, LegacyGameMessages::ReadNotifyTamingBuildSuccess), point);
EXPECT_EQ(RoundTrip(success).buildPosition, point);
ExpectTruncatedFails(success);
for (const auto id : g_Targets) {
for (const auto value : g_Ints) {
for (const bool obey : { false, true }) {
GameMessages::CommandPet command;
command.GenericPosInfo = point;
command.ObjIDSource = id;
command.iPetCommandType = value;
command.iTypeID = -value;
command.overrideObey = obey;
const auto legacy = ReadWithLegacy<LegacyGameMessages::LegacyCommandPet>(command, LegacyGameMessages::ReadCommandPet);
const auto copy = RoundTrip(command);
EXPECT_EQ(copy.GenericPosInfo, legacy.genericPosInfo);
EXPECT_EQ(copy.ObjIDSource, legacy.objIdSource);
EXPECT_EQ(copy.iPetCommandType, legacy.iPetCommandType);
EXPECT_EQ(copy.iTypeID, legacy.iTypeID);
EXPECT_EQ(copy.overrideObey, legacy.overrideObey);
ExpectTruncatedFails(command);
}
}
}
}
}
// Independent of the legacy code: hand computed payloads (RakNet writes MSB first).
TEST_F(PetMessagesTests, GoldenBytes) {
GameMessages::PetTamingTryBuildResult result;
EXPECT_PACKET_EQ(FromHex("80", 2), Payload(result));
result.bSuccess = false;
result.iNumCorrect = 3;
// 0, 1, then 03 00 00 00 shifted right by two bits
EXPECT_PACKET_EQ(FromHex("40 c0 00 00 00", 34), Payload(result));
GameMessages::NotifyPetTamingPuzzleSelected puzzle;
puzzle.bricks = { { 3001, 21 } };
EXPECT_PACKET_EQ(FromHex("01 00 00 00 b9 0b 00 00 15 00 00 00"), Payload(puzzle));
GameMessages::SetPetName setName;
setName.name = u"Hi";
EXPECT_PACKET_EQ(FromHex("02 00 00 00 48 00 69 00 00", 65), Payload(setName));
setName.petDBID = 0x11;
EXPECT_PACKET_EQ(FromHex("02 00 00 00 48 00 69 00 88 80 00 00 00 00 00 00 00", 129), Payload(setName));
GameMessages::SetPetNameModerated moderated;
moderated.nModerationStatus = 2;
EXPECT_PACKET_EQ(FromHex("01 00 00 00 00", 33), Payload(moderated));
GameMessages::ShowPetActionButton button;
button.ButtonLabel = ePetAbilityType::JumpOnObject;
button.bShow = true;
EXPECT_PACKET_EQ(FromHex("02 00 00 00 80", 33), Payload(button));
GameMessages::DespawnPet despawn;
despawn.bDeletePet = true;
EXPECT_PACKET_EQ(FromHex("80", 1), Payload(despawn));
}