From 27de0cf268a890fbe495c7088972c0f19beffb31 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Sat, 26 Sep 2026 19:11:26 -0500 Subject: [PATCH] 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 --- dGame/dBehaviors/BehaviorContext.cpp | 24 +- dGame/dComponents/InventoryComponent.cpp | 29 +- dGame/dComponents/SkillComponent.cpp | 26 +- dGame/dGameMessages/CMakeLists.txt | 1 + .../dGameMessages/DoClientProjectileImpact.h | 81 --- dGame/dGameMessages/EchoStartSkill.h | 131 ---- dGame/dGameMessages/EchoSyncSkill.h | 69 -- dGame/dGameMessages/GameMessageDecoder.cpp | 7 +- dGame/dGameMessages/GameMessageHandler.cpp | 131 +--- dGame/dGameMessages/GameMessages.cpp | 58 +- dGame/dGameMessages/GameMessages.h | 7 +- .../RequestServerProjectileImpact.h | 73 --- dGame/dGameMessages/SkillMessages.cpp | 265 ++++++++ dGame/dGameMessages/SkillMessages.h | 142 +++++ dGame/dGameMessages/StartSkill.h | 143 ----- dGame/dGameMessages/SyncSkill.h | 68 -- .../dGameMessagesTests/CMakeLists.txt | 1 + .../GameMessageDecoderTests.cpp | 17 +- .../Legacy/SkillMessagesLegacy.h | 600 ++++++++++++++++++ .../dGameMessagesTests/SkillMessagesTests.cpp | 313 +++++++++ 20 files changed, 1385 insertions(+), 801 deletions(-) delete mode 100644 dGame/dGameMessages/DoClientProjectileImpact.h delete mode 100644 dGame/dGameMessages/EchoStartSkill.h delete mode 100644 dGame/dGameMessages/EchoSyncSkill.h delete mode 100644 dGame/dGameMessages/RequestServerProjectileImpact.h create mode 100644 dGame/dGameMessages/SkillMessages.cpp create mode 100644 dGame/dGameMessages/SkillMessages.h delete mode 100644 dGame/dGameMessages/StartSkill.h delete mode 100644 dGame/dGameMessages/SyncSkill.h create mode 100644 tests/dGameTests/dGameMessagesTests/Legacy/SkillMessagesLegacy.h create mode 100644 tests/dGameTests/dGameMessagesTests/SkillMessagesTests.cpp diff --git a/dGame/dBehaviors/BehaviorContext.cpp b/dGame/dBehaviors/BehaviorContext.cpp index c8c126b6b..6c7ec0eeb 100644 --- a/dGame/dBehaviors/BehaviorContext.cpp +++ b/dGame/dBehaviors/BehaviorContext.cpp @@ -11,7 +11,7 @@ #include #include "DestroyableComponent.h" -#include "EchoSyncSkill.h" +#include "SkillMessages.h" #include "PhantomPhysicsComponent.h" #include "QuickBuildComponent.h" #include "eReplicaComponentType.h" @@ -201,7 +201,8 @@ void BehaviorContext::UpdatePlayerSyncs(float deltaTime) { } if (this->skillUId != 0 && !clientInitalized) { - EchoSyncSkill echo; + GameMessages::EchoSyncSkill echo; + echo.target = this->originator; echo.bDone = true; echo.uiSkillHandle = this->skillUId; echo.uiBehaviorHandle = entry.handle; @@ -211,12 +212,7 @@ void BehaviorContext::UpdatePlayerSyncs(float deltaTime) { echo.sBitStream.assign(reinterpret_cast(bitStream.GetData()), bitStream.GetNumberOfBytesUsed()); - RakNet::BitStream message; - BitStreamUtils::WriteHeader(message, ServiceType::CLIENT, MessageType::Client::GAME_MSG); - message.Write(this->originator); - echo.Serialize(message); - - Game::server->Send(message, UNASSIGNED_SYSTEM_ADDRESS, true); + echo.Send(UNASSIGNED_SYSTEM_ADDRESS); } this->syncEntries.erase(this->syncEntries.begin() + i); @@ -268,7 +264,8 @@ bool BehaviorContext::CalculateUpdate(const float deltaTime) { } // Echo sync - EchoSyncSkill echo; + GameMessages::EchoSyncSkill echo; + echo.target = this->originator; echo.bDone = true; echo.uiBehaviorHandle = entry.handle; @@ -282,14 +279,7 @@ bool BehaviorContext::CalculateUpdate(const float deltaTime) { if (!clientInitalized) { echo.sBitStream.assign(reinterpret_cast(bitStream.GetData()), bitStream.GetNumberOfBytesUsed()); - // Write message - RakNet::BitStream message; - - BitStreamUtils::WriteHeader(message, ServiceType::CLIENT, MessageType::Client::GAME_MSG); - message.Write(this->originator); - echo.Serialize(message); - - Game::server->Send(message, UNASSIGNED_SYSTEM_ADDRESS, true); + echo.Send(UNASSIGNED_SYSTEM_ADDRESS); } ExecuteUpdates(); diff --git a/dGame/dComponents/InventoryComponent.cpp b/dGame/dComponents/InventoryComponent.cpp index a6fc452e2..56f54a437 100644 --- a/dGame/dComponents/InventoryComponent.cpp +++ b/dGame/dComponents/InventoryComponent.cpp @@ -15,6 +15,7 @@ #include "GameMessages.h" #include "InventoryMessages.h" #include "Database.h" +#include "SkillMessages.h" #include "SkillComponent.h" #include "Character.h" #include "EntityManager.h" @@ -1324,14 +1325,25 @@ void InventoryComponent::RemoveItemSkills(const LOT lot) { // The client stores one acquiredSkillsInfo entry per skillID, tagged with the slotID // it was originally added with. Always send RemoveSkill to clear that entry, then // re-add with the surviving slot so the client shows it in the correct place. - GameMessages::SendRemoveSkill(m_Parent, skillId); + GameMessages::RemoveSkill removeSkill; + removeSkill.target = m_Parent->GetObjectID(); + removeSkill.skillID = skillId; + removeSkill.SendToClient(m_Parent->GetSystemAddress()); if (surviving != m_Skills.end()) { - GameMessages::SendAddSkill(m_Parent, skillId, surviving->first); + GameMessages::AddSkill addSkill; + addSkill.target = m_Parent->GetObjectID(); + addSkill.skillID = skillId; + addSkill.slotID = surviving->first; + addSkill.SendToClient(m_Parent->GetSystemAddress()); } if (slot == BehaviorSlot::Primary) { m_Skills.insert_or_assign(BehaviorSlot::Primary, 1); - GameMessages::SendAddSkill(m_Parent, 1, BehaviorSlot::Primary); + GameMessages::AddSkill addSkill; + addSkill.target = m_Parent->GetObjectID(); + addSkill.skillID = 1; + addSkill.slotID = BehaviorSlot::Primary; + addSkill.SendToClient(m_Parent->GetSystemAddress()); } } @@ -1784,7 +1796,10 @@ bool InventoryComponent::SetSkill(BehaviorSlot slot, uint32_t skillId) { return pair.first != slot && pair.second == old; }); if (!usedElsewhere) { - GameMessages::SendRemoveSkill(m_Parent, old); + GameMessages::RemoveSkill removeSkill; + removeSkill.target = m_Parent->GetObjectID(); + removeSkill.skillID = old; + removeSkill.SendToClient(m_Parent->GetSystemAddress()); } } @@ -1795,7 +1810,11 @@ bool InventoryComponent::SetSkill(BehaviorSlot slot, uint32_t skillId) { return pair.first != slot && pair.second == skillId; }); if (!alreadyKnown) { - GameMessages::SendAddSkill(m_Parent, skillId, slot); + GameMessages::AddSkill addSkill; + addSkill.target = m_Parent->GetObjectID(); + addSkill.skillID = skillId; + addSkill.slotID = slot; + addSkill.SendToClient(m_Parent->GetSystemAddress()); } m_Skills.insert_or_assign(slot, skillId); diff --git a/dGame/dComponents/SkillComponent.cpp b/dGame/dComponents/SkillComponent.cpp index 1d4eb29d6..c2771816f 100644 --- a/dGame/dComponents/SkillComponent.cpp +++ b/dGame/dComponents/SkillComponent.cpp @@ -19,8 +19,7 @@ #include "BaseCombatAIComponent.h" #include "ScriptComponent.h" #include "BuffComponent.h" -#include "EchoStartSkill.h" -#include "DoClientProjectileImpact.h" +#include "SkillMessages.h" #include "CDClientManager.h" #include "CDSkillBehaviorTable.h" #include "ServiceType.h" @@ -301,7 +300,8 @@ SkillExecutionResult SkillComponent::CalculateBehavior( if (!clientInitalized) { // Echo start skill - EchoStartSkill start; + GameMessages::EchoStartSkill start; + start.target = this->m_Parent->GetObjectID(); start.iCastType = castType; start.skillID = skillId; @@ -322,14 +322,7 @@ SkillExecutionResult SkillComponent::CalculateBehavior( start.sBitStream.assign(reinterpret_cast(bitStream.GetData()), bitStream.GetNumberOfBytesUsed()); - // Write message - RakNet::BitStream message; - - BitStreamUtils::WriteHeader(message, ServiceType::CLIENT, MessageType::Client::GAME_MSG); - message.Write(this->m_Parent->GetObjectID()); - start.Serialize(message); - - Game::server->Send(message, UNASSIGNED_SYSTEM_ADDRESS, true); + start.Send(UNASSIGNED_SYSTEM_ADDRESS); } context->ExecuteUpdates(); @@ -447,20 +440,15 @@ void SkillComponent::SyncProjectileCalculation(const ProjectileSyncEntry& entry) behavior->Calculate(entry.context, bitStream, entry.branchContext); - DoClientProjectileImpact projectileImpact; + GameMessages::DoClientProjectileImpact projectileImpact; + projectileImpact.target = this->m_Parent->GetObjectID(); projectileImpact.sBitStream.assign(reinterpret_cast(bitStream.GetData()), bitStream.GetNumberOfBytesUsed()); projectileImpact.i64OwnerID = this->m_Parent->GetObjectID(); projectileImpact.i64OrgID = entry.id; projectileImpact.i64TargetID = entry.branchContext.target; - RakNet::BitStream message; - - BitStreamUtils::WriteHeader(message, ServiceType::CLIENT, MessageType::Client::GAME_MSG); - message.Write(this->m_Parent->GetObjectID()); - projectileImpact.Serialize(message); - - Game::server->Send(message, UNASSIGNED_SYSTEM_ADDRESS, true); + projectileImpact.Send(UNASSIGNED_SYSTEM_ADDRESS); entry.context->ExecuteUpdates(); } diff --git a/dGame/dGameMessages/CMakeLists.txt b/dGame/dGameMessages/CMakeLists.txt index 23a4c08e0..7c189ec03 100644 --- a/dGame/dGameMessages/CMakeLists.txt +++ b/dGame/dGameMessages/CMakeLists.txt @@ -10,6 +10,7 @@ set(DGAME_DGAMEMESSAGES_SOURCES "PetMessages.cpp" "PropertyMessages.cpp" "RacingMessages.cpp" + "SkillMessages.cpp" "TradeMessages.cpp" "VendorMessages.cpp") diff --git a/dGame/dGameMessages/DoClientProjectileImpact.h b/dGame/dGameMessages/DoClientProjectileImpact.h deleted file mode 100644 index 16d326fad..000000000 --- a/dGame/dGameMessages/DoClientProjectileImpact.h +++ /dev/null @@ -1,81 +0,0 @@ -#ifndef __DOCLIENTPROJECTILEIMPACT__H__ -#define __DOCLIENTPROJECTILEIMPACT__H__ - -#include "dCommonVars.h" - -/* 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; -}; - -#endif //!__DOCLIENTPROJECTILEIMPACT__H__ diff --git a/dGame/dGameMessages/EchoStartSkill.h b/dGame/dGameMessages/EchoStartSkill.h deleted file mode 100644 index f2229f889..000000000 --- a/dGame/dGameMessages/EchoStartSkill.h +++ /dev/null @@ -1,131 +0,0 @@ -#ifndef __ECHOSTARTSKILL__H__ -#define __ECHOSTARTSKILL__H__ - -#include "dCommonVars.h" -#include "NiPoint3.h" -#include "NiQuaternion.h" -#include "MessageType/Game.h" - -/* 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; -}; - -#endif //!__ECHOSTARTSKILL__H__ diff --git a/dGame/dGameMessages/EchoSyncSkill.h b/dGame/dGameMessages/EchoSyncSkill.h deleted file mode 100644 index 63c23b3e0..000000000 --- a/dGame/dGameMessages/EchoSyncSkill.h +++ /dev/null @@ -1,69 +0,0 @@ -#ifndef __ECHOSYNCSKILL__H__ -#define __ECHOSYNCSKILL__H__ - -#include - -#include "BitStream.h" -#include "MessageType/Game.h" - - -/* 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{}; -}; - -#endif //!__ECHOSYNCSKILL__H__ diff --git a/dGame/dGameMessages/GameMessageDecoder.cpp b/dGame/dGameMessages/GameMessageDecoder.cpp index 95fbc1437..6681c0d53 100644 --- a/dGame/dGameMessages/GameMessageDecoder.cpp +++ b/dGame/dGameMessages/GameMessageDecoder.cpp @@ -6,12 +6,7 @@ #include #include "GameMessages.h" -#include "StartSkill.h" -#include "SyncSkill.h" -#include "EchoStartSkill.h" -#include "EchoSyncSkill.h" -#include "RequestServerProjectileImpact.h" -#include "DoClientProjectileImpact.h" +#include "SkillMessages.h" #include "master/MessageCapture.h" namespace { diff --git a/dGame/dGameMessages/GameMessageHandler.cpp b/dGame/dGameMessages/GameMessageHandler.cpp index 77bf73da1..9523f353f 100644 --- a/dGame/dGameMessages/GameMessageHandler.cpp +++ b/dGame/dGameMessages/GameMessageHandler.cpp @@ -26,15 +26,11 @@ #include "CDSkillBehaviorTable.h" #include "SkillComponent.h" #include "RacingControlComponent.h" -#include "RequestServerProjectileImpact.h" -#include "SyncSkill.h" -#include "StartSkill.h" -#include "EchoStartSkill.h" -#include "EchoSyncSkill.h" #include "ActivityMessages.h" #include "BuildingMessages.h" #include "RacingMessages.h" #include "MissionMessages.h" +#include "SkillMessages.h" #include "TradeMessages.h" #include "VendorMessages.h" #include "EffectsMessages.h" @@ -157,6 +153,12 @@ namespace { { CLIENT_TRADE_CANCEL, []() { return std::make_unique(); } }, { CLIENT_TRADE_ACCEPT, []() { return std::make_unique(); } }, { CLIENT_TRADE_UPDATE, []() { return std::make_unique(); } }, + + // Skills + { SELECT_SKILL, []() { return std::make_unique(); } }, + { START_SKILL, []() { return std::make_unique(); } }, + { SYNC_SKILL, []() { return std::make_unique(); } }, + { REQUEST_SERVER_PROJECTILE_IMPACT, []() { return std::make_unique(); } }, }; }; @@ -207,18 +209,6 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System switch (messageID) { - // Currently not actually used for our implementation, however its used right now to get around invisible inventory items in the client. - case MessageType::Game::SELECT_SKILL: { - auto var = entity->GetVar(u"dlu_first_time_load"); - if (var) { - entity->SetVar(u"dlu_first_time_load", false); - InventoryComponent* inventoryComponent = entity->GetComponent(); - - if (inventoryComponent) inventoryComponent->FixInvisibleItems(); - } - break; - } - case MessageType::Game::PLAYER_LOADED: { GameMessages::SendPlayerReady(entity, sysAddr); entity->SetPlayerReadyForUpdates(); @@ -371,113 +361,6 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System GameMessages::SendResurrect(entity); break; } - case MessageType::Game::REQUEST_SERVER_PROJECTILE_IMPACT: - { - auto message = RequestServerProjectileImpact(); - - message.Deserialize(inStream); - - auto* skill_component = entity->GetComponent(); - - if (skill_component != nullptr) { - auto bs = RakNet::BitStream(reinterpret_cast(&message.sBitStream[0]), message.sBitStream.size(), false); - - skill_component->SyncPlayerProjectile(message.i64LocalID, bs, message.i64TargetID); - } - - break; - } - - case MessageType::Game::START_SKILL: { - StartSkill startSkill = StartSkill(); - startSkill.Deserialize(inStream); // inStream replaces &bitStream - - if (startSkill.skillID == 1561 || startSkill.skillID == 1562 || startSkill.skillID == 1541) return; - - MissionComponent* comp = entity->GetComponent(); - if (comp) { - comp->Progress(eMissionTaskType::USE_SKILL, startSkill.skillID); - } - - CDSkillBehaviorTable* skillTable = CDClientManager::GetTable(); - unsigned int behaviorId = skillTable->GetSkillByID(startSkill.skillID).behaviorID; - - bool success = false; - - if (behaviorId > 0) { - auto bs = RakNet::BitStream(reinterpret_cast(&startSkill.sBitStream[0]), startSkill.sBitStream.size(), false); - - auto* const skillComponent = entity->GetComponent(); - - if (skillComponent) success = skillComponent->CastPlayerSkill(behaviorId, startSkill.uiSkillHandle, bs, startSkill.optionalTargetID, startSkill.skillID); - - if (success && entity->GetCharacter()) { - DestroyableComponent* destComp = entity->GetComponent(); - destComp->SetImagination(destComp->GetImagination() - skillTable->GetSkillByID(startSkill.skillID).imaginationcost); - } - } - - if (Game::server->GetZoneID() == 1302) { - break; - } - - if (success) { - //Broadcast our startSkill: - RakNet::BitStream bitStreamLocal; - BitStreamUtils::WriteHeader(bitStreamLocal, ServiceType::CLIENT, MessageType::Client::GAME_MSG); - bitStreamLocal.Write(entity->GetObjectID()); - - EchoStartSkill echoStartSkill; - echoStartSkill.bUsedMouse = startSkill.bUsedMouse; - echoStartSkill.fCasterLatency = startSkill.fCasterLatency; - echoStartSkill.iCastType = startSkill.iCastType; - echoStartSkill.lastClickedPosit = startSkill.lastClickedPosit; - echoStartSkill.optionalOriginatorID = startSkill.optionalOriginatorID; - echoStartSkill.optionalTargetID = startSkill.optionalTargetID; - echoStartSkill.originatorRot = startSkill.originatorRot; - echoStartSkill.sBitStream = startSkill.sBitStream; - echoStartSkill.skillID = startSkill.skillID; - echoStartSkill.uiSkillHandle = startSkill.uiSkillHandle; - echoStartSkill.Serialize(bitStreamLocal); - - Game::server->Send(bitStreamLocal, entity->GetSystemAddress(), true); - } - } break; - - case MessageType::Game::SYNC_SKILL: { - RakNet::BitStream bitStreamLocal; - BitStreamUtils::WriteHeader(bitStreamLocal, ServiceType::CLIENT, MessageType::Client::GAME_MSG); - bitStreamLocal.Write(entity->GetObjectID()); - - SyncSkill sync = SyncSkill(inStream); // inStream replaced &bitStream - - std::ostringstream buffer; - - for (unsigned int k = 0; k < sync.sBitStream.size(); k++) { - char s; - s = sync.sBitStream.at(k); - buffer << std::setw(2) << std::hex << std::setfill('0') << static_cast(s) << " "; - } - - if (usr != nullptr) { - auto bs = RakNet::BitStream(reinterpret_cast(&sync.sBitStream[0]), sync.sBitStream.size(), false); - - auto* const skillComponent = entity->GetComponent(); - - if (skillComponent) skillComponent->SyncPlayerSkill(sync.uiSkillHandle, sync.uiBehaviorHandle, bs); - } - - EchoSyncSkill echo = EchoSyncSkill(); - echo.bDone = sync.bDone; - echo.sBitStream = sync.sBitStream; - echo.uiBehaviorHandle = sync.uiBehaviorHandle; - echo.uiSkillHandle = sync.uiSkillHandle; - - echo.Serialize(bitStreamLocal); - - Game::server->Send(bitStreamLocal, sysAddr, true); - } break; - case MessageType::Game::REQUEST_SMASH_PLAYER: entity->Smash(entity->GetObjectID()); break; diff --git a/dGame/dGameMessages/GameMessages.cpp b/dGame/dGameMessages/GameMessages.cpp index 9d06d0c1f..7b7a83363 100644 --- a/dGame/dGameMessages/GameMessages.cpp +++ b/dGame/dGameMessages/GameMessages.cpp @@ -658,58 +658,6 @@ void GameMessages::SendPlayerReachedRespawnCheckpoint(Entity* entity, const NiPo SEND_PACKET; } -void GameMessages::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; -} - -void GameMessages::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; -} - void GameMessages::SendMatchResponse(Entity* entity, const SystemAddress& sysAddr, int response) { CBITSTREAM; CMSGHEADER; @@ -2196,6 +2144,12 @@ namespace GameMessages { SEND_PACKET; } + void NetGameMsg::BroadcastExcept(const SystemAddress& excluded) const { + CBITSTREAM; + WritePacket(bitStream); + Game::server->Send(bitStream, excluded, true); + } + void NetGameMsg::Send(const SystemAddress& sysAddr) const { CBITSTREAM; WritePacket(bitStream); diff --git a/dGame/dGameMessages/GameMessages.h b/dGame/dGameMessages/GameMessages.h index 8d0653c5d..1478cac01 100644 --- a/dGame/dGameMessages/GameMessages.h +++ b/dGame/dGameMessages/GameMessages.h @@ -87,6 +87,10 @@ namespace GameMessages { // nothing is delivered (RakNet rejects a non-broadcast send without an address). void SendToClient(const SystemAddress& sysAddr) const; + // Sends the message to every client except excluded (a RakNet broadcast with that address left out). + // Used to echo a client's message to everyone else. + void BroadcastExcept(const SystemAddress& excluded) const; + // Writes the complete client packet (CLIENT/GAME_MSG header, target, msgId, then Serialize()) into bitStream. // This is exactly what Send(sysAddr) puts on the wire; tests use it to compare bytes without a server. void WritePacket(RakNet::BitStream& bitStream) const; @@ -169,9 +173,6 @@ namespace GameMessages { void SendSetPlayerControlScheme(Entity* entity, eControlScheme controlScheme); void SendPlayerReachedRespawnCheckpoint(Entity* entity, const NiPoint3& position, const NiQuaternion& rotation); - void SendAddSkill(Entity* entity, TSkillID skillID, BehaviorSlot slotID); - void SendRemoveSkill(Entity* entity, TSkillID skillID); - void SendMatchResponse(Entity* entity, const SystemAddress& sysAddr, int response); void SendMatchUpdate(Entity* entity, const SystemAddress& sysAddr, std::string data, eMatchUpdate type); diff --git a/dGame/dGameMessages/RequestServerProjectileImpact.h b/dGame/dGameMessages/RequestServerProjectileImpact.h deleted file mode 100644 index 181583995..000000000 --- a/dGame/dGameMessages/RequestServerProjectileImpact.h +++ /dev/null @@ -1,73 +0,0 @@ -#ifndef __REQUESTSERVERPROJECTILEIMPACT__H__ -#define __REQUESTSERVERPROJECTILEIMPACT__H__ - -#include "dCommonVars.h" -#include "MessageType/Game.h" - -/* 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; -}; - -#endif //!__REQUESTSERVERPROJECTILEIMPACT__H__ diff --git a/dGame/dGameMessages/SkillMessages.cpp b/dGame/dGameMessages/SkillMessages.cpp new file mode 100644 index 000000000..b1929c358 --- /dev/null +++ b/dGame/dGameMessages/SkillMessages.cpp @@ -0,0 +1,265 @@ +#include "SkillMessages.h" + +#include "BehaviorSlot.h" +#include "BitStreamUtils.h" +#include "CDClientManager.h" +#include "CDSkillBehaviorTable.h" +#include "DestroyableComponent.h" +#include "dServer.h" +#include "eMissionTaskType.h" +#include "Entity.h" +#include "Game.h" +#include "InventoryComponent.h" +#include "MissionComponent.h" +#include "SkillComponent.h" + +namespace GameMessages { + void AddSkill::Serialize(RakNet::BitStream& bitStream) const { + BitStreamUtils::WriteOptional(bitStream, AICombatWeight, 0); + bitStream.Write(bFromSkillSet); + BitStreamUtils::WriteOptional(bitStream, castType, 0); + BitStreamUtils::WriteOptional(bitStream, fTimeSecs, -1.0f); + BitStreamUtils::WriteOptional(bitStream, iTimesCanCast, -1); + bitStream.Write(skillID); + BitStreamUtils::WriteOptional(bitStream, slotID, BehaviorSlot::Invalid); + bitStream.Write(temporary); + } + + bool AddSkill::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, AICombatWeight, 0)); + VALIDATE_READ(bitStream.Read(bFromSkillSet)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, castType, 0)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, fTimeSecs, -1.0f)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, iTimesCanCast, -1)); + VALIDATE_READ(bitStream.Read(skillID)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, slotID, BehaviorSlot::Invalid)); + VALIDATE_READ(bitStream.Read(temporary)); + return true; + } + + void RemoveSkill::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(bFromSkillSet); + bitStream.Write(skillID); + } + + bool RemoveSkill::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(bFromSkillSet)); + VALIDATE_READ(bitStream.Read(skillID)); + return true; + } + + void SelectSkill::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(bFromSkillSet); + bitStream.Write(skillID); + } + + bool SelectSkill::Deserialize(RakNet::BitStream& bitStream) { + // The old handler read nothing; keep accepting the message whatever its payload. + if (!bitStream.Read(bFromSkillSet) || !bitStream.Read(skillID)) { + bFromSkillSet = false; + skillID = 0; + } + return true; + } + + // Currently not actually used for our implementation, however its used right now to get around invisible inventory items in the client. + void SelectSkill::Handle(Entity& entity, const SystemAddress& sysAddr) { + auto var = entity.GetVar(u"dlu_first_time_load"); + if (var) { + entity.SetVar(u"dlu_first_time_load", false); + InventoryComponent* inventoryComponent = entity.GetComponent(); + + if (inventoryComponent) inventoryComponent->FixInvisibleItems(); + } + } + + void StartSkill::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(bUsedMouse); + BitStreamUtils::WriteOptional(bitStream, consumableItemID, LWOOBJID_EMPTY); + BitStreamUtils::WriteOptional(bitStream, fCasterLatency, 0.0f); + BitStreamUtils::WriteOptional(bitStream, iCastType, 0); + BitStreamUtils::WriteOptional(bitStream, lastClickedPosit, NiPoint3Constant::ZERO); + bitStream.Write(optionalOriginatorID); + BitStreamUtils::WriteOptional(bitStream, optionalTargetID, LWOOBJID_EMPTY); + BitStreamUtils::WriteOptional(bitStream, originatorRot, QuatUtils::IDENTITY); + BitStreamUtils::WriteLengthPrefixed(bitStream, sBitStream); + bitStream.Write(skillID); + BitStreamUtils::WriteOptional(bitStream, uiSkillHandle, 0u); + } + + bool StartSkill::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(bUsedMouse)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, consumableItemID, LWOOBJID_EMPTY)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, fCasterLatency, 0.0f)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, iCastType, 0)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, lastClickedPosit, NiPoint3Constant::ZERO)); + VALIDATE_READ(bitStream.Read(optionalOriginatorID)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, optionalTargetID, LWOOBJID_EMPTY)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, originatorRot, QuatUtils::IDENTITY)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, sBitStream)); + VALIDATE_READ(bitStream.Read(skillID)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, uiSkillHandle, 0u)); + return true; + } + + void StartSkill::Handle(Entity& entityRef, const SystemAddress& sysAddr) { + Entity* entity = &entityRef; + if (skillID == 1561 || skillID == 1562 || skillID == 1541) return; + + MissionComponent* comp = entity->GetComponent(); + if (comp) { + comp->Progress(eMissionTaskType::USE_SKILL, skillID); + } + + CDSkillBehaviorTable* skillTable = CDClientManager::GetTable(); + unsigned int behaviorId = skillTable->GetSkillByID(skillID).behaviorID; + + bool success = false; + + if (behaviorId > 0) { + auto bs = RakNet::BitStream(reinterpret_cast(&sBitStream[0]), sBitStream.size(), false); + + auto* const skillComponent = entity->GetComponent(); + + if (skillComponent) success = skillComponent->CastPlayerSkill(behaviorId, uiSkillHandle, bs, optionalTargetID, skillID); + + if (success && entity->GetCharacter()) { + DestroyableComponent* destComp = entity->GetComponent(); + destComp->SetImagination(destComp->GetImagination() - skillTable->GetSkillByID(skillID).imaginationcost); + } + } + + if (Game::server->GetZoneID() == 1302) { + return; + } + + if (success) { + // Echo the cast to every other client + EchoStartSkill echoStartSkill; + echoStartSkill.target = entity->GetObjectID(); + echoStartSkill.bUsedMouse = bUsedMouse; + echoStartSkill.fCasterLatency = fCasterLatency; + echoStartSkill.iCastType = iCastType; + echoStartSkill.lastClickedPosit = lastClickedPosit; + echoStartSkill.optionalOriginatorID = optionalOriginatorID; + echoStartSkill.optionalTargetID = optionalTargetID; + echoStartSkill.originatorRot = originatorRot; + echoStartSkill.sBitStream = sBitStream; + echoStartSkill.skillID = skillID; + echoStartSkill.uiSkillHandle = uiSkillHandle; + echoStartSkill.BroadcastExcept(entity->GetSystemAddress()); + } + } + + void EchoStartSkill::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(bUsedMouse); + BitStreamUtils::WriteOptional(bitStream, fCasterLatency, 0.0f); + BitStreamUtils::WriteOptional(bitStream, iCastType, 0); + BitStreamUtils::WriteOptional(bitStream, lastClickedPosit, NiPoint3Constant::ZERO); + bitStream.Write(optionalOriginatorID); + BitStreamUtils::WriteOptional(bitStream, optionalTargetID, LWOOBJID_EMPTY); + BitStreamUtils::WriteOptional(bitStream, originatorRot, QuatUtils::IDENTITY); + BitStreamUtils::WriteLengthPrefixed(bitStream, sBitStream); + bitStream.Write(skillID); + BitStreamUtils::WriteOptional(bitStream, uiSkillHandle, 0u); + } + + bool EchoStartSkill::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(bUsedMouse)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, fCasterLatency, 0.0f)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, iCastType, 0)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, lastClickedPosit, NiPoint3Constant::ZERO)); + VALIDATE_READ(bitStream.Read(optionalOriginatorID)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, optionalTargetID, LWOOBJID_EMPTY)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, originatorRot, QuatUtils::IDENTITY)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, sBitStream)); + VALIDATE_READ(bitStream.Read(skillID)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, uiSkillHandle, 0u)); + return true; + } + + void SyncSkill::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(bDone); + BitStreamUtils::WriteLengthPrefixed(bitStream, sBitStream); + bitStream.Write(uiBehaviorHandle); + bitStream.Write(uiSkillHandle); + } + + bool SyncSkill::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(bDone)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, sBitStream)); + VALIDATE_READ(bitStream.Read(uiBehaviorHandle)); + VALIDATE_READ(bitStream.Read(uiSkillHandle)); + return true; + } + + void SyncSkill::Handle(Entity& entity, const SystemAddress& sysAddr) { + auto bs = RakNet::BitStream(reinterpret_cast(&sBitStream[0]), sBitStream.size(), false); + + auto* const skillComponent = entity.GetComponent(); + + if (skillComponent) skillComponent->SyncPlayerSkill(uiSkillHandle, uiBehaviorHandle, bs); + + // Echo the sync to every other client + EchoSyncSkill echo; + echo.target = entity.GetObjectID(); + echo.bDone = bDone; + echo.sBitStream = sBitStream; + echo.uiBehaviorHandle = uiBehaviorHandle; + echo.uiSkillHandle = uiSkillHandle; + echo.BroadcastExcept(sysAddr); + } + + void EchoSyncSkill::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(bDone); + BitStreamUtils::WriteLengthPrefixed(bitStream, sBitStream); + bitStream.Write(uiBehaviorHandle); + bitStream.Write(uiSkillHandle); + } + + bool EchoSyncSkill::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(bDone)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, sBitStream)); + VALIDATE_READ(bitStream.Read(uiBehaviorHandle)); + VALIDATE_READ(bitStream.Read(uiSkillHandle)); + return true; + } + + void RequestServerProjectileImpact::Serialize(RakNet::BitStream& bitStream) const { + BitStreamUtils::WriteOptional(bitStream, i64LocalID, LWOOBJID_EMPTY); + BitStreamUtils::WriteOptional(bitStream, i64TargetID, LWOOBJID_EMPTY); + BitStreamUtils::WriteLengthPrefixed(bitStream, sBitStream); + } + + bool RequestServerProjectileImpact::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, i64LocalID, LWOOBJID_EMPTY)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, i64TargetID, LWOOBJID_EMPTY)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, sBitStream)); + return true; + } + + void RequestServerProjectileImpact::Handle(Entity& entity, const SystemAddress& sysAddr) { + auto* skill_component = entity.GetComponent(); + + if (skill_component != nullptr) { + auto bs = RakNet::BitStream(reinterpret_cast(&sBitStream[0]), sBitStream.size(), false); + + skill_component->SyncPlayerProjectile(i64LocalID, bs, i64TargetID); + } + } + + void DoClientProjectileImpact::Serialize(RakNet::BitStream& bitStream) const { + BitStreamUtils::WriteOptional(bitStream, i64OrgID, LWOOBJID_EMPTY); + BitStreamUtils::WriteOptional(bitStream, i64OwnerID, LWOOBJID_EMPTY); + BitStreamUtils::WriteOptional(bitStream, i64TargetID, LWOOBJID_EMPTY); + BitStreamUtils::WriteLengthPrefixed(bitStream, sBitStream); + } + + bool DoClientProjectileImpact::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, i64OrgID, LWOOBJID_EMPTY)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, i64OwnerID, LWOOBJID_EMPTY)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, i64TargetID, LWOOBJID_EMPTY)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, sBitStream)); + return true; + } +} diff --git a/dGame/dGameMessages/SkillMessages.h b/dGame/dGameMessages/SkillMessages.h new file mode 100644 index 000000000..26d1195b7 --- /dev/null +++ b/dGame/dGameMessages/SkillMessages.h @@ -0,0 +1,142 @@ +#ifndef SKILLMESSAGES_H +#define SKILLMESSAGES_H + +#include "GameMessages.h" +#include "NiPoint3.h" +#include "NiQuaternion.h" + +#include + +enum class BehaviorSlot : int32_t; + +// Game messages for skills: casting (StartSkill / SyncSkill and their echoes), projectile impacts and the +// player's skill bar. Field names follow the client (legouniverse.exe 1.10.64); fields are listed in wire order. +// sBitStream is a skill's behavior data (its own bit stream), sent as a u32 byte count and the bytes. +namespace GameMessages { + // Server -> client, to the player only. + struct AddSkill : public NetGameMsg { + AddSkill() : NetGameMsg(MessageType::Game::ADD_SKILL) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + int32_t AICombatWeight{ 0 }; // optional + bool bFromSkillSet{ false }; + int32_t castType{ 0 }; // optional + float fTimeSecs{ -1.0f }; // optional + int32_t iTimesCanCast{ -1 }; // optional + TSkillID skillID{}; + BehaviorSlot slotID{ static_cast(-1) }; // optional, BehaviorSlot::Invalid when unset + bool temporary{ true }; + }; + + // Server -> client, to the player only. + struct RemoveSkill : public NetGameMsg { + RemoveSkill() : NetGameMsg(MessageType::Game::REMOVE_SKILL) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + bool bFromSkillSet{ false }; + TSkillID skillID{}; + }; + + // Client -> server. The player picked a skill; DLU only uses it to fix invisible items on first load. + struct SelectSkill : public NetGameMsg { + SelectSkill() : NetGameMsg(MessageType::Game::SELECT_SKILL) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + // Not read by DLU: the old handler ignored the payload, so a message without it is still accepted. + bool bFromSkillSet{ false }; + int32_t skillID{}; + }; + + // Client -> server. The player cast a skill. + struct StartSkill : public NetGameMsg { + StartSkill() : NetGameMsg(MessageType::Game::START_SKILL) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + bool bUsedMouse{ false }; + LWOOBJID consumableItemID{ LWOOBJID_EMPTY }; // optional + float fCasterLatency{ 0.0f }; // optional + int32_t iCastType{ 0 }; // optional + NiPoint3 lastClickedPosit{ NiPoint3Constant::ZERO }; // optional + LWOOBJID optionalOriginatorID{}; + LWOOBJID optionalTargetID{ LWOOBJID_EMPTY }; // optional + NiQuaternion originatorRot{ QuatUtils::IDENTITY }; // optional + std::string sBitStream{}; + TSkillID skillID{ 0 }; + uint32_t uiSkillHandle{ 0 }; // optional + }; + + // Server -> clients. A skill cast echoed to the other clients so they play it too. + struct EchoStartSkill : public NetGameMsg { + EchoStartSkill() : NetGameMsg(MessageType::Game::ECHO_START_SKILL) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + bool bUsedMouse{ false }; + float fCasterLatency{ 0.0f }; // optional + int32_t iCastType{ 0 }; // optional + NiPoint3 lastClickedPosit{ NiPoint3Constant::ZERO }; // optional + LWOOBJID optionalOriginatorID{}; + LWOOBJID optionalTargetID{ LWOOBJID_EMPTY }; // optional + NiQuaternion originatorRot{ QuatUtils::IDENTITY }; // optional + std::string sBitStream{}; + TSkillID skillID{ 0 }; + uint32_t uiSkillHandle{ 0 }; // optional + }; + + // Client -> server. More behavior data for a skill that is being cast. + struct SyncSkill : public NetGameMsg { + SyncSkill() : NetGameMsg(MessageType::Game::SYNC_SKILL) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + bool bDone{}; + std::string sBitStream{}; + uint32_t uiBehaviorHandle{}; + uint32_t uiSkillHandle{}; + }; + + // Server -> clients. + struct EchoSyncSkill : public NetGameMsg { + EchoSyncSkill() : NetGameMsg(MessageType::Game::ECHO_SYNC_SKILL) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + bool bDone{}; + std::string sBitStream{}; + uint32_t uiBehaviorHandle{}; + uint32_t uiSkillHandle{}; + }; + + // Client -> server. A player's projectile hit something. + struct RequestServerProjectileImpact : public NetGameMsg { + RequestServerProjectileImpact() : NetGameMsg(MessageType::Game::REQUEST_SERVER_PROJECTILE_IMPACT) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + LWOOBJID i64LocalID{ LWOOBJID_EMPTY }; // optional + LWOOBJID i64TargetID{ LWOOBJID_EMPTY }; // optional + std::string sBitStream{}; + }; + + // Server -> clients. A server side projectile hit something. + struct DoClientProjectileImpact : public NetGameMsg { + DoClientProjectileImpact() : NetGameMsg(MessageType::Game::DO_CLIENT_PROJECTILE_IMPACT) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + LWOOBJID i64OrgID{ LWOOBJID_EMPTY }; // optional + LWOOBJID i64OwnerID{ LWOOBJID_EMPTY }; // optional + LWOOBJID i64TargetID{ LWOOBJID_EMPTY }; // optional + std::string sBitStream{}; + }; +}; + +#endif // SKILLMESSAGES_H diff --git a/dGame/dGameMessages/StartSkill.h b/dGame/dGameMessages/StartSkill.h deleted file mode 100644 index 6ca510086..000000000 --- a/dGame/dGameMessages/StartSkill.h +++ /dev/null @@ -1,143 +0,0 @@ -#ifndef __STARTSKILL__H__ -#define __STARTSKILL__H__ - -#include "dCommonVars.h" -#include "NiPoint3.h" -#include "NiQuaternion.h" -#include "MessageType/Game.h" - -/** - * 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; -}; - -#endif //!__STARTSKILL__H__ diff --git a/dGame/dGameMessages/SyncSkill.h b/dGame/dGameMessages/SyncSkill.h deleted file mode 100644 index 3128ce91f..000000000 --- a/dGame/dGameMessages/SyncSkill.h +++ /dev/null @@ -1,68 +0,0 @@ -#ifndef __SYNCSKILL__H__ -#define __SYNCSKILL__H__ - -#include -#include - -#include "BitStream.h" -#include "MessageType/Game.h" - -/* 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{}; -}; - -#endif //!__SYNCSKILL__H__ diff --git a/tests/dGameTests/dGameMessagesTests/CMakeLists.txt b/tests/dGameTests/dGameMessagesTests/CMakeLists.txt index 098fa353c..d9e855536 100644 --- a/tests/dGameTests/dGameMessagesTests/CMakeLists.txt +++ b/tests/dGameTests/dGameMessagesTests/CMakeLists.txt @@ -9,6 +9,7 @@ SET(DGAMEMESSAGES_TESTS "PetMessagesTests.cpp" "PropertyMessagesTests.cpp" "RacingMessagesTests.cpp" + "SkillMessagesTests.cpp" "UnSmashTests.cpp" "VendorMessagesTests.cpp" "LegacyGameMessageTests.cpp" diff --git a/tests/dGameTests/dGameMessagesTests/GameMessageDecoderTests.cpp b/tests/dGameTests/dGameMessagesTests/GameMessageDecoderTests.cpp index 7dc967c64..5529b305d 100644 --- a/tests/dGameTests/dGameMessagesTests/GameMessageDecoderTests.cpp +++ b/tests/dGameTests/dGameMessagesTests/GameMessageDecoderTests.cpp @@ -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); diff --git a/tests/dGameTests/dGameMessagesTests/Legacy/SkillMessagesLegacy.h b/tests/dGameTests/dGameMessagesTests/Legacy/SkillMessagesLegacy.h new file mode 100644 index 000000000..5ab64d25e --- /dev/null +++ b/tests/dGameTests/dGameMessagesTests/Legacy/SkillMessagesLegacy.h @@ -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 +#include + +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 diff --git a/tests/dGameTests/dGameMessagesTests/SkillMessagesTests.cpp b/tests/dGameTests/dGameMessagesTests/SkillMessagesTests.cpp new file mode 100644 index 000000000..64a353781 --- /dev/null +++ b/tests/dGameTests/dGameMessagesTests/SkillMessagesTests.cpp @@ -0,0 +1,313 @@ +#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; + 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 + 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; + 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)); +}