#include "SkillMessages.h" #include "GameDependencies.h" #include "GameMessageTestUtils.h" #include "Legacy/SkillMessagesLegacy.h" #include "BehaviorSlot.h" #include #include #include #include using namespace GameMessageTestUtils; namespace { const std::vector g_Streams = { "", std::string("\x00", 1), std::string("\x01\x02\xff", 3), std::string(1000, '\x5a') }; const std::vector g_Ids = { LWOOBJID_EMPTY, 0x1000000000000001LL, 0x0102030405060708LL }; const std::vector g_Points = { NiPoint3Constant::ZERO, NiPoint3(1.0f, -2.5f, 3.0f) }; const std::vector 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 PacketBytes LegacyPacket(LWOOBJID target, Legacy& legacy) { RakNet::BitStream bitStream; LUBitStream(ServiceType::CLIENT, MessageType::Client::GAME_MSG).WriteHeader(bitStream); 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 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::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(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(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(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(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; LUBitStream(ServiceType::CLIENT, MessageType::Client::GAME_MSG).WriteHeader(bitStreamLocal); 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)); } // Live echoes keep the cast and drop the caster's input (all 78,841 EchoStartSkill in the live captures). TEST_F(SkillMessagesTests, EchoOfClientCastMatchesLive) { GameMessages::StartSkill cast; cast.bUsedMouse = true; cast.consumableItemID = 0x0102030405060700LL; cast.fCasterLatency = 0.25f; cast.iCastType = 1; cast.lastClickedPosit = NiPoint3(1.0f, 2.0f, 3.0f); cast.optionalOriginatorID = 0x1000000000000001LL; cast.optionalTargetID = 0x0102030405060708LL; cast.originatorRot = NiQuaternion(0.5f, 0.5f, -0.5f, 0.5f); cast.skillID = 381; cast.uiSkillHandle = 1; const auto echo = cast.MakeEcho(0x1000000000000001LL); EXPECT_EQ(echo.target, 0x1000000000000001LL); EXPECT_FALSE(echo.bUsedMouse); EXPECT_EQ(echo.fCasterLatency, 0.0f); EXPECT_EQ(echo.lastClickedPosit, NiPoint3Constant::ZERO); EXPECT_EQ(echo.originatorRot, GameMessages::EchoStartSkill::LIVE_ORIGINATOR_ROT); EXPECT_EQ(echo.iCastType, 1); EXPECT_EQ(echo.optionalOriginatorID, cast.optionalOriginatorID); EXPECT_EQ(echo.optionalTargetID, cast.optionalTargetID); EXPECT_EQ(echo.skillID, 381); EXPECT_EQ(echo.uiSkillHandle, 1u); // A live echo of skill 381 (cast type 1, handle 1, no behavior data) with the caster and target IDs replaced. EXPECT_PACKET_EQ(FromHex( "53 05 00 0c 00 00 00 00 01 00 00 00 00 00 00 10 76 00 20 20 00 00 00 10 00 00 00 00 00 01 08 40 38 30 28 20 18 10 0c " "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 f4 04 00 02 02 00 00 00", 535), StructPacket(echo)); } // Server casts echo a zero rotation too, never the caster's facing. TEST_F(SkillMessagesTests, LiveOriginatorRotationIsWrittenAsZero) { GameMessages::EchoStartSkill echo; echo.originatorRot = GameMessages::EchoStartSkill::LIVE_ORIGINATOR_ROT; const auto copy = RoundTrip(echo); EXPECT_EQ(copy.originatorRot, GameMessages::EchoStartSkill::LIVE_ORIGINATOR_ROT); EXPECT_NE(copy.originatorRot, QuatUtils::IDENTITY); }