refactor: skill and projectile game messages as structs

Converts the skill messages to NetGameMsgs in SkillMessages.{h,cpp}:
AddSkill and RemoveSkill (SendToClient, as before), EchoStartSkill,
EchoSyncSkill and DoClientProjectileImpact, and the received
SelectSkill, StartSkill, SyncSkill and RequestServerProjectileImpact.
The one-off StartSkill, EchoStartSkill, SyncSkill, EchoSyncSkill,
RequestServerProjectileImpact and DoClientProjectileImpact classes are
deleted; SkillComponent, BehaviorContext and InventoryComponent build
the structs, and the dashboard's message decoder reads them.

The received messages are registered in GameMessageHandler's map and
their switch cases are deleted; the handlers call SkillComponent as
before. The echoes still go to every client except the caster, through
the new NetGameMsg::BroadcastExcept. SelectSkill still accepts any
payload, since the old case read nothing. Handler logic is unchanged
(the SyncSkill case's unused hex dump of the payload is dropped).

No wire change. Verified byte for byte against frozen verbatim copies
of the old classes and functions over an input grid (every optional
field set and unset, empty, short and long behavior streams), received
messages compared with the old classes' read sequences and truncated
payloads rejected, the echo's broadcast-except destination compared
with the old send, hand computed golden bytes, round trips, and a
deliberate mutation made the tests fail. Messages that fail to
deserialize are now dropped instead of being handled half read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-26 19:11:26 -05:00
parent 2711c69e3e
commit 27de0cf268
20 changed files with 1385 additions and 801 deletions

View File

@@ -9,6 +9,7 @@ SET(DGAMEMESSAGES_TESTS
"PetMessagesTests.cpp"
"PropertyMessagesTests.cpp"
"RacingMessagesTests.cpp"
"SkillMessagesTests.cpp"
"UnSmashTests.cpp"
"VendorMessagesTests.cpp"
"LegacyGameMessageTests.cpp"

View File

@@ -3,8 +3,7 @@
#include "GameMessageDecoder.h"
#include "dCommonVars.h"
#include "MessageType/Game.h"
#include "EchoSyncSkill.h"
#include "StartSkill.h"
#include "SkillMessages.h"
TEST(GameMessageDecoderTest, DecodesATypedClientMessage) {
RakNet::BitStream payload;
@@ -26,14 +25,14 @@ TEST(GameMessageDecoderTest, DecodesATypedClientMessage) {
}
TEST(GameMessageDecoderTest, ReadsWhatTheServerWrites) {
// The server's own Serialize writes the message ID first; the decoder gets what follows it
StartSkill sent(1234, std::string("\x01\x02", 2), 42);
// The decoder gets the payload, what follows the message ID
GameMessages::StartSkill sent;
sent.optionalOriginatorID = 1234;
sent.sBitStream = std::string("\x01\x02", 2);
sent.skillID = 42;
sent.optionalTargetID = 77;
RakNet::BitStream stream;
sent.Serialize(stream);
MessageType::Game id{};
ASSERT_TRUE(stream.Read(id));
ASSERT_EQ(id, MessageType::Game::START_SKILL);
const auto fields = GameMessageDecoder::Decode(MessageType::Game::START_SKILL, true, stream);
ASSERT_TRUE(fields);
@@ -44,13 +43,11 @@ TEST(GameMessageDecoderTest, ReadsWhatTheServerWrites) {
}
TEST(GameMessageDecoderTest, DirectionMatters) {
EchoSyncSkill sent;
GameMessages::EchoSyncSkill sent;
sent.bDone = true;
sent.uiSkillHandle = 5;
RakNet::BitStream stream;
sent.Serialize(stream);
MessageType::Game id{};
ASSERT_TRUE(stream.Read(id));
EXPECT_FALSE(GameMessageDecoder::CanDecode(MessageType::Game::ECHO_SYNC_SKILL, true));
const auto fields = GameMessageDecoder::Decode(MessageType::Game::ECHO_SYNC_SKILL, false, stream);

View File

@@ -0,0 +1,600 @@
#ifndef SKILLMESSAGESLEGACY_H
#define SKILLMESSAGESLEGACY_H
// FROZEN ORACLE - DO NOT EDIT.
// Verbatim copies of the one-off skill and projectile message classes that SkillMessages.h replaced
// (dGame/dGameMessages/{StartSkill,EchoStartSkill,SyncSkill,EchoSyncSkill,RequestServerProjectileImpact,
// DoClientProjectileImpact}.h) and of GameMessages::SendAddSkill / SendRemoveSkill
// (dGame/dGameMessages/GameMessages.cpp), branched from origin/main 129199e4. Only the namespace changed.
// Each class's Serialize writes the message ID and then the payload; the old senders wrote the CLIENT/GAME_MSG
// header and the target object ID before it. The Read* function is the read sequence of the replaced switch case.
#include "BehaviorSlot.h"
#include "BitStream.h"
#include "BitStreamUtils.h"
#include "dCommonVars.h"
#include "dServer.h"
#include "Entity.h"
#include "Game.h"
#include "MessageType/Client.h"
#include "MessageType/Game.h"
#include "NiPoint3.h"
#include "NiQuaternion.h"
#include "ServiceType.h"
#include <cstdint>
#include <string>
namespace LegacyGameMessages {
/**
* Same as sync skill but with different network options. An echo down to other clients that need to play the skill.
*/
class StartSkill {
public:
StartSkill() {
bUsedMouse = false;
consumableItemID = LWOOBJID_EMPTY;
fCasterLatency = 0.0f;
iCastType = 0;
lastClickedPosit = NiPoint3Constant::ZERO;
optionalTargetID = LWOOBJID_EMPTY;
originatorRot = QuatUtils::IDENTITY;
uiSkillHandle = 0;
}
StartSkill(LWOOBJID _optionalOriginatorID, std::string _sBitStream, TSkillID _skillID, bool _bUsedMouse = false, LWOOBJID _consumableItemID = LWOOBJID_EMPTY, float _fCasterLatency = 0.0f, int32_t _iCastType = 0, NiPoint3 _lastClickedPosit = NiPoint3Constant::ZERO, LWOOBJID _optionalTargetID = LWOOBJID_EMPTY, NiQuaternion _originatorRot = QuatUtils::IDENTITY, uint32_t _uiSkillHandle = 0) {
bUsedMouse = _bUsedMouse;
consumableItemID = _consumableItemID;
fCasterLatency = _fCasterLatency;
iCastType = _iCastType;
lastClickedPosit = _lastClickedPosit;
optionalOriginatorID = _optionalOriginatorID;
optionalTargetID = _optionalTargetID;
originatorRot = _originatorRot;
sBitStream = _sBitStream;
skillID = _skillID;
uiSkillHandle = _uiSkillHandle;
}
StartSkill(RakNet::BitStream& stream) : StartSkill() {
Deserialize(stream);
}
~StartSkill() {
}
void Serialize(RakNet::BitStream& stream) {
stream.Write(MessageType::Game::START_SKILL);
stream.Write(bUsedMouse);
stream.Write(consumableItemID != LWOOBJID_EMPTY);
if (consumableItemID != LWOOBJID_EMPTY) stream.Write(consumableItemID);
stream.Write(fCasterLatency != 0.0f);
if (fCasterLatency != 0.0f) stream.Write(fCasterLatency);
stream.Write(iCastType != 0);
if (iCastType != 0) stream.Write(iCastType);
stream.Write(lastClickedPosit != NiPoint3Constant::ZERO);
if (lastClickedPosit != NiPoint3Constant::ZERO) stream.Write(lastClickedPosit);
stream.Write(optionalOriginatorID);
stream.Write(optionalTargetID != LWOOBJID_EMPTY);
if (optionalTargetID != LWOOBJID_EMPTY) stream.Write(optionalTargetID);
stream.Write(originatorRot != QuatUtils::IDENTITY);
if (originatorRot != QuatUtils::IDENTITY) stream.Write(originatorRot);
uint32_t sBitStreamLength = sBitStream.length();
stream.Write(sBitStreamLength);
for (uint32_t k = 0; k < sBitStreamLength; k++) {
stream.Write(sBitStream[k]);
}
stream.Write(skillID);
stream.Write(uiSkillHandle != 0);
if (uiSkillHandle != 0) stream.Write(uiSkillHandle);
}
bool Deserialize(RakNet::BitStream& stream) {
stream.Read(bUsedMouse);
bool consumableItemIDIsDefault{};
stream.Read(consumableItemIDIsDefault);
if (consumableItemIDIsDefault != 0) stream.Read(consumableItemID);
bool fCasterLatencyIsDefault{};
stream.Read(fCasterLatencyIsDefault);
if (fCasterLatencyIsDefault != 0) stream.Read(fCasterLatency);
bool iCastTypeIsDefault{};
stream.Read(iCastTypeIsDefault);
if (iCastTypeIsDefault != 0) stream.Read(iCastType);
bool lastClickedPositIsDefault{};
stream.Read(lastClickedPositIsDefault);
if (lastClickedPositIsDefault != 0) stream.Read(lastClickedPosit);
stream.Read(optionalOriginatorID);
bool optionalTargetIDIsDefault{};
stream.Read(optionalTargetIDIsDefault);
if (optionalTargetIDIsDefault != 0) stream.Read(optionalTargetID);
bool originatorRotIsDefault{};
stream.Read(originatorRotIsDefault);
if (originatorRotIsDefault != 0) stream.Read(originatorRot);
uint32_t sBitStreamLength{};
stream.Read(sBitStreamLength);
if (sBitStreamLength > MAX_MESSAGE_LENGTH) return false;
for (uint32_t k = 0; k < sBitStreamLength; k++) {
unsigned char character;
stream.Read(character);
sBitStream.push_back(character);
}
stream.Read(skillID);
bool uiSkillHandleIsDefault{};
stream.Read(uiSkillHandleIsDefault);
if (uiSkillHandleIsDefault != 0) stream.Read(uiSkillHandle);
return true;
}
bool bUsedMouse = false;
LWOOBJID consumableItemID{};
float fCasterLatency{};
int32_t iCastType{};
NiPoint3 lastClickedPosit{};
LWOOBJID optionalOriginatorID{};
LWOOBJID optionalTargetID{};
NiQuaternion originatorRot = QuatUtils::IDENTITY;
std::string sBitStream = "";
TSkillID skillID = 0;
uint32_t uiSkillHandle = 0;
};
/* Same as start skill but with different network options. An echo down to other clients that need to play the skill. */
class EchoStartSkill {
public:
EchoStartSkill() {
bUsedMouse = false;
fCasterLatency = 0.0f;
iCastType = 0;
lastClickedPosit = NiPoint3Constant::ZERO;
optionalTargetID = LWOOBJID_EMPTY;
originatorRot = QuatUtils::IDENTITY;
uiSkillHandle = 0;
}
EchoStartSkill(LWOOBJID _optionalOriginatorID, std::string _sBitStream, TSkillID _skillID, bool _bUsedMouse = false, float _fCasterLatency = 0.0f, int32_t _iCastType = 0, NiPoint3 _lastClickedPosit = NiPoint3Constant::ZERO, LWOOBJID _optionalTargetID = LWOOBJID_EMPTY, NiQuaternion _originatorRot = QuatUtils::IDENTITY, uint32_t _uiSkillHandle = 0) {
bUsedMouse = _bUsedMouse;
fCasterLatency = _fCasterLatency;
iCastType = _iCastType;
lastClickedPosit = _lastClickedPosit;
optionalOriginatorID = _optionalOriginatorID;
optionalTargetID = _optionalTargetID;
originatorRot = _originatorRot;
sBitStream = _sBitStream;
skillID = _skillID;
uiSkillHandle = _uiSkillHandle;
}
EchoStartSkill(RakNet::BitStream& stream) : EchoStartSkill() {
Deserialize(stream);
}
~EchoStartSkill() {
}
void Serialize(RakNet::BitStream& stream) {
stream.Write(MessageType::Game::ECHO_START_SKILL);
stream.Write(bUsedMouse);
stream.Write(fCasterLatency != 0.0f);
if (fCasterLatency != 0.0f) stream.Write(fCasterLatency);
stream.Write(iCastType != 0);
if (iCastType != 0) stream.Write(iCastType);
stream.Write(lastClickedPosit != NiPoint3Constant::ZERO);
if (lastClickedPosit != NiPoint3Constant::ZERO) stream.Write(lastClickedPosit);
stream.Write(optionalOriginatorID);
stream.Write(optionalTargetID != LWOOBJID_EMPTY);
if (optionalTargetID != LWOOBJID_EMPTY) stream.Write(optionalTargetID);
stream.Write(originatorRot != QuatUtils::IDENTITY);
if (originatorRot != QuatUtils::IDENTITY) stream.Write(originatorRot);
uint32_t sBitStreamLength = sBitStream.length();
stream.Write(sBitStreamLength);
for (uint32_t k = 0; k < sBitStreamLength; k++) {
stream.Write(sBitStream[k]);
}
stream.Write(skillID);
stream.Write(uiSkillHandle != 0);
if (uiSkillHandle != 0) stream.Write(uiSkillHandle);
}
bool Deserialize(RakNet::BitStream& stream) {
stream.Read(bUsedMouse);
bool fCasterLatencyIsDefault{};
stream.Read(fCasterLatencyIsDefault);
if (fCasterLatencyIsDefault != 0) stream.Read(fCasterLatency);
bool iCastTypeIsDefault{};
stream.Read(iCastTypeIsDefault);
if (iCastTypeIsDefault != 0) stream.Read(iCastType);
bool lastClickedPositIsDefault{};
stream.Read(lastClickedPositIsDefault);
if (lastClickedPositIsDefault != 0) stream.Read(lastClickedPosit);
stream.Read(optionalOriginatorID);
bool optionalTargetIDIsDefault{};
stream.Read(optionalTargetIDIsDefault);
if (optionalTargetIDIsDefault != 0) stream.Read(optionalTargetID);
bool originatorRotIsDefault{};
stream.Read(originatorRotIsDefault);
if (originatorRotIsDefault != 0) stream.Read(originatorRot);
uint32_t sBitStreamLength{};
stream.Read(sBitStreamLength);
if (sBitStreamLength > MAX_MESSAGE_LENGTH) return false;
for (uint32_t k = 0; k < sBitStreamLength; k++) {
unsigned char character;
stream.Read(character);
sBitStream.push_back(character);
}
stream.Read(skillID);
bool uiSkillHandleIsDefault{};
stream.Read(uiSkillHandleIsDefault);
if (uiSkillHandleIsDefault != 0) stream.Read(uiSkillHandle);
return true;
}
bool bUsedMouse;
float fCasterLatency;
int32_t iCastType;
NiPoint3 lastClickedPosit;
LWOOBJID optionalOriginatorID;
LWOOBJID optionalTargetID;
NiQuaternion originatorRot = QuatUtils::IDENTITY;
std::string sBitStream;
TSkillID skillID;
uint32_t uiSkillHandle;
};
/* Message to synchronize a skill cast */
class SyncSkill {
public:
SyncSkill() {
bDone = false;
}
SyncSkill(std::string _sBitStream, uint32_t _uiBehaviorHandle, uint32_t _uiSkillHandle, bool _bDone = false) {
bDone = _bDone;
sBitStream = _sBitStream;
uiBehaviorHandle = _uiBehaviorHandle;
uiSkillHandle = _uiSkillHandle;
}
SyncSkill(RakNet::BitStream& stream) : SyncSkill() {
Deserialize(stream);
}
~SyncSkill() {
}
void Serialize(RakNet::BitStream& stream) {
stream.Write(MessageType::Game::SYNC_SKILL);
stream.Write(bDone);
uint32_t sBitStreamLength = sBitStream.length();
stream.Write(sBitStreamLength);
for (unsigned int k = 0; k < sBitStreamLength; k++) {
stream.Write(sBitStream[k]);
}
stream.Write(uiBehaviorHandle);
stream.Write(uiSkillHandle);
}
bool Deserialize(RakNet::BitStream& stream) {
stream.Read(bDone);
uint32_t sBitStreamLength{};
stream.Read(sBitStreamLength);
if (sBitStreamLength > MAX_MESSAGE_LENGTH) return false;
for (uint32_t k = 0; k < sBitStreamLength; k++) {
unsigned char character;
stream.Read(character);
sBitStream.push_back(character);
}
stream.Read(uiBehaviorHandle);
stream.Read(uiSkillHandle);
return true;
}
bool bDone{};
std::string sBitStream{};
uint32_t uiBehaviorHandle{};
uint32_t uiSkillHandle{};
};
/* Message to synchronize a skill cast */
class EchoSyncSkill {
public:
EchoSyncSkill() {
bDone = false;
}
EchoSyncSkill(std::string _sBitStream, uint32_t _uiBehaviorHandle, uint32_t _uiSkillHandle, bool _bDone = false) {
bDone = _bDone;
sBitStream = _sBitStream;
uiBehaviorHandle = _uiBehaviorHandle;
uiSkillHandle = _uiSkillHandle;
}
EchoSyncSkill(RakNet::BitStream& stream) : EchoSyncSkill() {
Deserialize(stream);
}
~EchoSyncSkill() {
}
void Serialize(RakNet::BitStream& stream) {
stream.Write(MessageType::Game::ECHO_SYNC_SKILL);
stream.Write(bDone);
uint32_t sBitStreamLength = sBitStream.length();
stream.Write(sBitStreamLength);
for (uint32_t k = 0; k < sBitStreamLength; k++) {
stream.Write(sBitStream[k]);
}
stream.Write(uiBehaviorHandle);
stream.Write(uiSkillHandle);
}
bool Deserialize(RakNet::BitStream& stream) {
stream.Read(bDone);
uint32_t sBitStreamLength{};
stream.Read(sBitStreamLength);
if (sBitStreamLength > MAX_MESSAGE_LENGTH) return false;
for (unsigned int k = 0; k < sBitStreamLength; k++) {
unsigned char character;
stream.Read(character);
sBitStream.push_back(character);
}
stream.Read(uiBehaviorHandle);
stream.Read(uiSkillHandle);
return true;
}
bool bDone{};
std::string sBitStream{};
uint32_t uiBehaviorHandle{};
uint32_t uiSkillHandle{};
};
/* Notifying the server that a locally owned projectile impacted. Sent to the caster of the projectile
should always be the local char. */
class RequestServerProjectileImpact {
public:
RequestServerProjectileImpact() {
i64LocalID = LWOOBJID_EMPTY;
i64TargetID = LWOOBJID_EMPTY;
}
RequestServerProjectileImpact(std::string _sBitStream, LWOOBJID _i64LocalID = LWOOBJID_EMPTY, LWOOBJID _i64TargetID = LWOOBJID_EMPTY) {
i64LocalID = _i64LocalID;
i64TargetID = _i64TargetID;
sBitStream = _sBitStream;
}
RequestServerProjectileImpact(RakNet::BitStream& stream) : RequestServerProjectileImpact() {
Deserialize(stream);
}
~RequestServerProjectileImpact() {
}
void Serialize(RakNet::BitStream& stream) {
stream.Write(MessageType::Game::REQUEST_SERVER_PROJECTILE_IMPACT);
stream.Write(i64LocalID != LWOOBJID_EMPTY);
if (i64LocalID != LWOOBJID_EMPTY) stream.Write(i64LocalID);
stream.Write(i64TargetID != LWOOBJID_EMPTY);
if (i64TargetID != LWOOBJID_EMPTY) stream.Write(i64TargetID);
uint32_t sBitStreamLength = sBitStream.length();
stream.Write(sBitStreamLength);
for (uint32_t k = 0; k < sBitStreamLength; k++) {
stream.Write(sBitStream[k]);
}
}
bool Deserialize(RakNet::BitStream& stream) {
bool i64LocalIDIsDefault{};
stream.Read(i64LocalIDIsDefault);
if (i64LocalIDIsDefault != 0) stream.Read(i64LocalID);
bool i64TargetIDIsDefault{};
stream.Read(i64TargetIDIsDefault);
if (i64TargetIDIsDefault != 0) stream.Read(i64TargetID);
uint32_t sBitStreamLength{};
stream.Read(sBitStreamLength);
if (sBitStreamLength > MAX_MESSAGE_LENGTH) return false;
for (uint32_t k = 0; k < sBitStreamLength; k++) {
unsigned char character;
stream.Read(character);
sBitStream.push_back(character);
}
return true;
}
LWOOBJID i64LocalID;
LWOOBJID i64TargetID;
std::string sBitStream;
};
/* Tell a client local projectile to impact */
class DoClientProjectileImpact {
public:
DoClientProjectileImpact() {
i64OrgID = LWOOBJID_EMPTY;
i64OwnerID = LWOOBJID_EMPTY;
i64TargetID = LWOOBJID_EMPTY;
}
DoClientProjectileImpact(std::string _sBitStream, LWOOBJID _i64OrgID = LWOOBJID_EMPTY, LWOOBJID _i64OwnerID = LWOOBJID_EMPTY, LWOOBJID _i64TargetID = LWOOBJID_EMPTY) {
i64OrgID = _i64OrgID;
i64OwnerID = _i64OwnerID;
i64TargetID = _i64TargetID;
sBitStream = _sBitStream;
}
DoClientProjectileImpact(RakNet::BitStream& stream) : DoClientProjectileImpact() {
Deserialize(stream);
}
~DoClientProjectileImpact() {
}
void Serialize(RakNet::BitStream& stream) {
stream.Write(MessageType::Game::DO_CLIENT_PROJECTILE_IMPACT);
stream.Write(i64OrgID != LWOOBJID_EMPTY);
if (i64OrgID != LWOOBJID_EMPTY) stream.Write(i64OrgID);
stream.Write(i64OwnerID != LWOOBJID_EMPTY);
if (i64OwnerID != LWOOBJID_EMPTY) stream.Write(i64OwnerID);
stream.Write(i64TargetID != LWOOBJID_EMPTY);
if (i64TargetID != LWOOBJID_EMPTY) stream.Write(i64TargetID);
uint32_t sBitStreamLength = sBitStream.length();
stream.Write(sBitStreamLength);
for (uint32_t k = 0; k < sBitStreamLength; k++) {
stream.Write(sBitStream[k]);
}
}
bool Deserialize(RakNet::BitStream& stream) {
bool i64OrgIDIsDefault{};
stream.Read(i64OrgIDIsDefault);
if (i64OrgIDIsDefault != 0) stream.Read(i64OrgID);
bool i64OwnerIDIsDefault{};
stream.Read(i64OwnerIDIsDefault);
if (i64OwnerIDIsDefault != 0) stream.Read(i64OwnerID);
bool i64TargetIDIsDefault{};
stream.Read(i64TargetIDIsDefault);
if (i64TargetIDIsDefault != 0) stream.Read(i64TargetID);
uint32_t sBitStreamLength{};
stream.Read(sBitStreamLength);
if (sBitStreamLength > MAX_MESSAGE_LENGTH) return false;
for (uint32_t k = 0; k < sBitStreamLength; k++) {
unsigned char character;
stream.Read(character);
sBitStream.push_back(character);
}
return true;
}
LWOOBJID i64OrgID;
LWOOBJID i64OwnerID;
LWOOBJID i64TargetID;
std::string sBitStream;
};
inline void SendAddSkill(Entity* entity, TSkillID skillID, BehaviorSlot slotID) {
int AICombatWeight = 0;
bool bFromSkillSet = false;
int castType = 0;
float fTimeSecs = -1.0f;
int iTimesCanCast = -1;
bool temporary = true;
CBITSTREAM;
CMSGHEADER;
bitStream.Write(entity->GetObjectID());
bitStream.Write(MessageType::Game::ADD_SKILL);
bitStream.Write(AICombatWeight != 0);
if (AICombatWeight != 0) bitStream.Write(AICombatWeight);
bitStream.Write(bFromSkillSet);
bitStream.Write(castType != 0);
if (castType != 0) bitStream.Write(castType);
bitStream.Write(fTimeSecs != -1.0f);
if (fTimeSecs != -1.0f) bitStream.Write(fTimeSecs);
bitStream.Write(iTimesCanCast != -1);
if (iTimesCanCast != -1) bitStream.Write(iTimesCanCast);
bitStream.Write(skillID);
bitStream.Write(slotID != BehaviorSlot::Invalid);
if (slotID != BehaviorSlot::Invalid) bitStream.Write(slotID);
bitStream.Write(temporary);
SystemAddress sysAddr = entity->GetSystemAddress();
SEND_PACKET;
}
inline void SendRemoveSkill(Entity* entity, TSkillID skillID) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(entity->GetObjectID());
bitStream.Write(MessageType::Game::REMOVE_SKILL);
bitStream.Write(false);
bitStream.Write(skillID);
SystemAddress sysAddr = entity->GetSystemAddress();
SEND_PACKET;
}
// The START_SKILL switch case in GameMessageHandler::HandleMessage read the message with StartSkill::Deserialize,
// SYNC_SKILL with the SyncSkill(RakNet::BitStream&) constructor and REQUEST_SERVER_PROJECTILE_IMPACT with
// RequestServerProjectileImpact::Deserialize; the classes above are those read sequences.
}
#endif // SKILLMESSAGESLEGACY_H

View File

@@ -0,0 +1,313 @@
#include "SkillMessages.h"
#include "GameDependencies.h"
#include "GameMessageTestUtils.h"
#include "Legacy/SkillMessagesLegacy.h"
#include "BehaviorSlot.h"
#include <limits>
#include <string>
#include <vector>
#include <gtest/gtest.h>
using namespace GameMessageTestUtils;
namespace {
const std::vector<std::string> g_Streams = { "", std::string("\x00", 1), std::string("\x01\x02\xff", 3), std::string(1000, '\x5a') };
const std::vector<LWOOBJID> g_Ids = { LWOOBJID_EMPTY, 0x1000000000000001LL, 0x0102030405060708LL };
const std::vector<NiPoint3> g_Points = { NiPoint3Constant::ZERO, NiPoint3(1.0f, -2.5f, 3.0f) };
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);
}
// What the old senders put on the wire: the CLIENT/GAME_MSG header, the target, then the class's Serialize
// (message ID and payload).
template<typename Legacy>
PacketBytes LegacyPacket(LWOOBJID target, Legacy& legacy) {
RakNet::BitStream bitStream;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::GAME_MSG);
bitStream.Write(target);
legacy.Serialize(bitStream);
return FromBitStream(bitStream);
}
// Serializes msg and reads it back with the legacy class's Deserialize; both must consume the same bits.
template<typename Legacy>
Legacy ReadWithLegacy(const GameMessages::NetGameMsg& msg) {
RakNet::BitStream wire;
msg.Serialize(wire);
RakNet::BitStream legacyStream(wire.GetData(), wire.GetNumberOfBytesUsed(), false);
Legacy legacy;
EXPECT_TRUE(legacy.Deserialize(legacyStream));
EXPECT_EQ(legacyStream.GetReadOffset(), wire.GetNumberOfBitsUsed());
return legacy;
}
}
class SkillMessagesTests : public GameDependenciesTest {
protected:
void SetUp() override { SetUpDependencies(); }
void TearDown() override { TearDownDependencies(); }
};
TEST_F(SkillMessagesTests, AddAndRemoveSkillMatchLegacy) {
for (const auto target : g_Targets) {
Entity entity(target, info);
for (const TSkillID skillID : { 0, 1, -1, 1727, std::numeric_limits<int32_t>::max() }) {
for (const auto slot : { BehaviorSlot::Invalid, BehaviorSlot::Primary, BehaviorSlot::Offhand, BehaviorSlot::Neck, BehaviorSlot::Head, BehaviorSlot::Consumable }) {
GameMessages::AddSkill msg;
msg.target = target;
msg.skillID = skillID;
msg.slotID = slot;
const auto legacy = Capture([&] { LegacyGameMessages::SendAddSkill(&entity, skillID, slot); });
const auto ours = Capture([&] { msg.SendToClient(entity.GetSystemAddress()); });
ASSERT_EQ(legacy.size(), 1);
ASSERT_EQ(ours.size(), 1);
EXPECT_PACKET_EQ(FromCapture(legacy[0]), FromCapture(ours[0]));
EXPECT_EQ(legacy[0].broadcast, ours[0].broadcast);
EXPECT_EQ(legacy[0].sysAddr, ours[0].sysAddr);
const auto copy = RoundTrip(msg);
EXPECT_EQ(copy.skillID, skillID);
EXPECT_EQ(copy.slotID, slot);
EXPECT_TRUE(copy.temporary);
}
GameMessages::RemoveSkill remove;
remove.target = target;
remove.skillID = skillID;
const auto legacy = Capture([&] { LegacyGameMessages::SendRemoveSkill(&entity, skillID); });
const auto ours = Capture([&] { remove.SendToClient(entity.GetSystemAddress()); });
ASSERT_EQ(legacy.size(), 1);
ASSERT_EQ(ours.size(), 1);
EXPECT_PACKET_EQ(FromCapture(legacy[0]), FromCapture(ours[0]));
EXPECT_EQ(legacy[0].broadcast, ours[0].broadcast);
EXPECT_EQ(RoundTrip(remove).skillID, skillID);
}
}
// Every optional field of AddSkill set, which DLU never sends, still round trips.
GameMessages::AddSkill full;
full.AICombatWeight = 3;
full.bFromSkillSet = true;
full.castType = 2;
full.fTimeSecs = 1.5f;
full.iTimesCanCast = 4;
full.skillID = 9;
full.slotID = BehaviorSlot::Head;
full.temporary = false;
const auto copy = RoundTrip(full);
EXPECT_EQ(copy.AICombatWeight, 3);
EXPECT_EQ(copy.castType, 2);
EXPECT_EQ(copy.fTimeSecs, 1.5f);
EXPECT_EQ(copy.iTimesCanCast, 4);
ExpectTruncatedFails(full);
}
TEST_F(SkillMessagesTests, StartSkillAndEchoMatchLegacy) {
for (const auto target : g_Targets) {
for (const auto& sBitStream : g_Streams) {
for (const auto optionalID : g_Ids) {
for (const auto& point : g_Points) {
for (const auto& rotation : g_Rotations) {
for (const bool flag : { false, true }) {
const float latency = flag ? 0.25f : 0.0f;
const int32_t castType = flag ? 3 : 0;
const uint32_t handle = flag ? 77u : 0u;
const TSkillID skillID = flag ? 1727 : -1;
GameMessages::EchoStartSkill echo;
echo.target = target;
echo.bUsedMouse = flag;
echo.fCasterLatency = latency;
echo.iCastType = castType;
echo.lastClickedPosit = point;
echo.optionalOriginatorID = target;
echo.optionalTargetID = optionalID;
echo.originatorRot = rotation;
echo.sBitStream = sBitStream;
echo.skillID = skillID;
echo.uiSkillHandle = handle;
LegacyGameMessages::EchoStartSkill legacyEcho(target, sBitStream, skillID, flag, latency, castType, point, optionalID, rotation, handle);
EXPECT_PACKET_EQ(LegacyPacket(target, legacyEcho), StructPacket(echo));
const auto echoRead = ReadWithLegacy<LegacyGameMessages::EchoStartSkill>(echo);
EXPECT_EQ(echoRead.sBitStream, sBitStream);
EXPECT_EQ(echoRead.optionalTargetID, optionalID);
const auto echoCopy = RoundTrip(echo);
EXPECT_EQ(echoCopy.sBitStream, sBitStream);
EXPECT_EQ(echoCopy.originatorRot, rotation);
EXPECT_EQ(echoCopy.lastClickedPosit, point);
GameMessages::StartSkill start;
start.bUsedMouse = flag;
start.consumableItemID = optionalID;
start.fCasterLatency = latency;
start.iCastType = castType;
start.lastClickedPosit = point;
start.optionalOriginatorID = target;
start.optionalTargetID = optionalID;
start.originatorRot = rotation;
start.sBitStream = sBitStream;
start.skillID = skillID;
start.uiSkillHandle = handle;
LegacyGameMessages::StartSkill legacyStart(target, sBitStream, skillID, flag, optionalID, latency, castType, point, optionalID, rotation, handle);
EXPECT_PACKET_EQ(LegacyPacket(target, legacyStart), [&] { auto copy = start; copy.target = target; return StructPacket(copy); }());
const auto legacy = ReadWithLegacy<LegacyGameMessages::StartSkill>(start);
EXPECT_EQ(legacy.bUsedMouse, start.bUsedMouse);
EXPECT_EQ(legacy.consumableItemID, start.consumableItemID);
EXPECT_EQ(legacy.fCasterLatency, start.fCasterLatency);
EXPECT_EQ(legacy.iCastType, start.iCastType);
EXPECT_EQ(legacy.lastClickedPosit, start.lastClickedPosit);
EXPECT_EQ(legacy.optionalOriginatorID, start.optionalOriginatorID);
EXPECT_EQ(legacy.optionalTargetID, start.optionalTargetID);
EXPECT_EQ(legacy.originatorRot, start.originatorRot);
EXPECT_EQ(legacy.sBitStream, start.sBitStream);
EXPECT_EQ(legacy.skillID, start.skillID);
EXPECT_EQ(legacy.uiSkillHandle, start.uiSkillHandle);
const auto startCopy = RoundTrip(start);
EXPECT_EQ(startCopy.sBitStream, sBitStream);
EXPECT_EQ(startCopy.consumableItemID, optionalID);
if (sBitStream.size() < 8) {
ExpectTruncatedFails(start);
ExpectTruncatedFails(echo);
}
}
}
}
}
}
}
}
TEST_F(SkillMessagesTests, SyncSkillAndEchoMatchLegacy) {
for (const auto target : g_Targets) {
for (const auto& sBitStream : g_Streams) {
for (const bool bDone : { false, true }) {
for (const uint32_t handle : { 0u, 1u, 0xFFFFFFFFu }) {
GameMessages::EchoSyncSkill echo;
echo.target = target;
echo.bDone = bDone;
echo.sBitStream = sBitStream;
echo.uiBehaviorHandle = handle;
echo.uiSkillHandle = handle ^ 0x5u;
LegacyGameMessages::EchoSyncSkill legacyEcho(sBitStream, handle, handle ^ 0x5u, bDone);
EXPECT_PACKET_EQ(LegacyPacket(target, legacyEcho), StructPacket(echo));
EXPECT_EQ(RoundTrip(echo).sBitStream, sBitStream);
GameMessages::SyncSkill sync;
sync.bDone = bDone;
sync.sBitStream = sBitStream;
sync.uiBehaviorHandle = handle;
sync.uiSkillHandle = handle ^ 0x5u;
const auto legacy = ReadWithLegacy<LegacyGameMessages::SyncSkill>(sync);
EXPECT_EQ(legacy.bDone, bDone);
EXPECT_EQ(legacy.sBitStream, sBitStream);
EXPECT_EQ(legacy.uiBehaviorHandle, handle);
EXPECT_EQ(legacy.uiSkillHandle, handle ^ 0x5u);
LegacyGameMessages::SyncSkill legacySync(sBitStream, handle, handle ^ 0x5u, bDone);
EXPECT_PACKET_EQ(LegacyPacket(target, legacySync), [&] { auto copy = sync; copy.target = target; return StructPacket(copy); }());
RoundTrip(sync);
if (sBitStream.size() < 8) ExpectTruncatedFails(sync);
}
}
}
}
}
TEST_F(SkillMessagesTests, ProjectileImpactsMatchLegacy) {
for (const auto target : g_Targets) {
for (const auto& sBitStream : g_Streams) {
for (const auto a : g_Ids) {
for (const auto b : g_Ids) {
GameMessages::DoClientProjectileImpact impact;
impact.target = target;
impact.i64OrgID = a;
impact.i64OwnerID = b;
impact.i64TargetID = target;
impact.sBitStream = sBitStream;
LegacyGameMessages::DoClientProjectileImpact legacyImpact(sBitStream, a, b, target);
EXPECT_PACKET_EQ(LegacyPacket(target, legacyImpact), StructPacket(impact));
const auto impactCopy = RoundTrip(impact);
EXPECT_EQ(impactCopy.i64OrgID, a);
EXPECT_EQ(impactCopy.i64OwnerID, b);
GameMessages::RequestServerProjectileImpact request;
request.i64LocalID = a;
request.i64TargetID = b;
request.sBitStream = sBitStream;
const auto legacy = ReadWithLegacy<LegacyGameMessages::RequestServerProjectileImpact>(request);
EXPECT_EQ(legacy.i64LocalID, a);
EXPECT_EQ(legacy.i64TargetID, b);
EXPECT_EQ(legacy.sBitStream, sBitStream);
LegacyGameMessages::RequestServerProjectileImpact legacyRequest(sBitStream, a, b);
EXPECT_PACKET_EQ(LegacyPacket(target, legacyRequest), [&] { auto copy = request; copy.target = target; return StructPacket(copy); }());
RoundTrip(request);
if (sBitStream.size() < 8) ExpectTruncatedFails(request);
}
}
}
}
}
// The echoes went to everyone except the caster: a RakNet broadcast with the caster's address excluded.
TEST_F(SkillMessagesTests, EchoesBroadcastExceptTheCaster) {
GameMessages::EchoSyncSkill echo;
echo.target = 0x0102030405060708LL;
echo.bDone = true;
echo.sBitStream = "ab";
const auto caster = ClientAddress();
const auto legacy = Capture([&] {
RakNet::BitStream bitStreamLocal;
BitStreamUtils::WriteHeader(bitStreamLocal, ServiceType::CLIENT, MessageType::Client::GAME_MSG);
bitStreamLocal.Write(echo.target);
LegacyGameMessages::EchoSyncSkill legacyEcho("ab", 0, 0, true);
legacyEcho.Serialize(bitStreamLocal);
Game::server->Send(bitStreamLocal, caster, true);
});
const auto ours = Capture([&] { echo.BroadcastExcept(caster); });
ASSERT_EQ(legacy.size(), 1);
ASSERT_EQ(ours.size(), 1);
EXPECT_PACKET_EQ(FromCapture(legacy[0]), FromCapture(ours[0]));
EXPECT_TRUE(ours[0].broadcast);
EXPECT_EQ(ours[0].sysAddr, caster);
}
TEST_F(SkillMessagesTests, SelectSkillAcceptsAnyPayload) {
GameMessages::SelectSkill select;
select.bFromSkillSet = true;
select.skillID = 1727;
const auto copy = RoundTrip(select);
EXPECT_TRUE(copy.bFromSkillSet);
EXPECT_EQ(copy.skillID, 1727);
RakNet::BitStream empty;
GameMessages::SelectSkill fromEmpty;
EXPECT_TRUE(fromEmpty.Deserialize(empty));
}
// Independent of the legacy code: hand computed payloads (RakNet writes MSB first).
TEST_F(SkillMessagesTests, GoldenBytes) {
GameMessages::AddSkill add;
add.skillID = 1;
add.slotID = BehaviorSlot::Primary;
// 5 zero bits (weight, fromSkillSet, castType, time, timesCanCast), skillID 1, bit + slot 0, temporary.
EXPECT_PACKET_EQ(FromHex("00 08 00 00 04 00 00 00 02", 71), Payload(add));
GameMessages::RemoveSkill remove;
remove.skillID = 1727;
EXPECT_PACKET_EQ(FromHex("5f 83 00 00 00", 33), Payload(remove));
GameMessages::SyncSkill sync;
sync.bDone = true;
sync.sBitStream = std::string("\xab", 1);
sync.uiBehaviorHandle = 2;
sync.uiSkillHandle = 3;
EXPECT_PACKET_EQ(FromHex("80 80 00 00 55 81 00 00 00 01 80 00 00 00", 105), Payload(sync));
GameMessages::RequestServerProjectileImpact request;
request.i64TargetID = 0x11;
EXPECT_PACKET_EQ(FromHex("44 40 00 00 00 00 00 00 00 00 00 00 00", 98), Payload(request));
}